upgrade schema, resolvers, panel added

This commit is contained in:
2025-05-16 09:23:48 +03:00
parent 8a60bec73a
commit 2d382be794
80 changed files with 8641 additions and 1100 deletions

View File

@@ -1,120 +1,90 @@
from functools import wraps
from typing import Tuple
from cache.cache import get_cached_author_by_user_id
from resolvers.stat import get_with_stat
from services.schema import request_graphql_data
from settings import ADMIN_SECRET, AUTH_URL
from utils.logger import root_logger as logger
from auth.internal import verify_internal_auth
from sqlalchemy import exc
from services.db import local_session
from auth.orm import Author, Role
# Список разрешенных заголовков
ALLOWED_HEADERS = ["Authorization", "Content-Type"]
async def check_auth(req):
async def check_auth(req) -> Tuple[str, list[str]]:
"""
Проверка авторизации пользователя.
Эта функция проверяет токен авторизации, переданный в заголовках запроса,
и возвращает идентификатор пользователя и его роли.
Проверяет токен и получает данные из локальной БД.
Параметры:
- req: Входящий GraphQL запрос, содержащий заголовок авторизации.
Возвращает:
- user_id: str - Идентификатор пользователя.
- user_roles: list[str] - Список ролей пользователя.
- user_id: str - Идентификатор пользователя
- user_roles: list[str] - Список ролей пользователя
"""
# Проверяем наличие токена
token = req.headers.get("Authorization")
if not token:
return "", []
host = req.headers.get("host", "")
logger.debug(f"check_auth: host={host}")
auth_url = AUTH_URL
if ".dscrs.site" in host or "localhost" in host:
auth_url = "https://auth.dscrs.site/graphql"
user_id = ""
user_roles = []
if token:
# Проверяем и очищаем токен от префикса Bearer если он есть
if token.startswith("Bearer "):
token = token.split("Bearer ")[-1].strip()
# Logging the authentication token
logger.debug(f"TOKEN: {token}")
query_name = "validate_jwt_token"
operation = "ValidateToken"
variables = {"params": {"token_type": "access_token", "token": token}}
# Очищаем токен от префикса Bearer если он есть
if token.startswith("Bearer "):
token = token.split("Bearer ")[-1].strip()
# Только необходимые заголовки для GraphQL запроса
headers = {"Content-Type": "application/json"}
logger.debug(f"Checking auth token: {token[:10]}...")
gql = {
"query": f"query {operation}($params: ValidateJWTTokenInput!)"
+ "{"
+ f"{query_name}(params: $params) {{ is_valid claims }} "
+ "}",
"variables": variables,
"operationName": operation,
}
data = await request_graphql_data(gql, url=auth_url, headers=headers)
if data:
logger.debug(f"Auth response: {data}")
validation_result = data.get("data", {}).get(query_name, {})
logger.debug(f"Validation result: {validation_result}")
is_valid = validation_result.get("is_valid", False)
if not is_valid:
logger.error(f"Token validation failed: {validation_result}")
return "", []
user_data = validation_result.get("claims", {})
logger.debug(f"User claims: {user_data}")
user_id = user_data.get("sub", "")
user_roles = user_data.get("allowed_roles", [])
return user_id, user_roles
# Проверяем авторизацию внутренним механизмом
logger.debug("Using internal authentication")
return await verify_internal_auth(token)
async def add_user_role(user_id):
async def add_user_role(user_id: str, roles: list[str] = None):
"""
Добавление роли пользователя.
Добавление ролей пользователю в локальной БД.
Эта функция добавляет роли "author" и "reader" для указанного пользователя
в системе авторизации.
Параметры:
- user_id: str - Идентификатор пользователя, которому нужно добавить роли.
Возвращает:
- user_id: str - Идентификатор пользователя, если операция прошла успешно.
Args:
user_id: ID пользователя
roles: Список ролей для добавления. По умолчанию ["author", "reader"]
"""
logger.info(f"add author role for user_id: {user_id}")
query_name = "_update_user"
operation = "UpdateUserRoles"
headers = {
"Content-Type": "application/json",
"x-authorizer-admin-secret": ADMIN_SECRET,
}
variables = {"params": {"roles": "author, reader", "id": user_id}}
gql = {
"query": f"mutation {operation}($params: UpdateUserInput!) {{ {query_name}(params: $params) {{ id roles }} }}",
"variables": variables,
"operationName": operation,
}
data = await request_graphql_data(gql, headers=headers)
if data:
user_id = data.get("data", {}).get(query_name, {}).get("id")
return user_id
if not roles:
roles = ["author", "reader"]
logger.info(f"Adding roles {roles} to user {user_id}")
logger.debug("Using local authentication")
with local_session() as session:
try:
author = session.query(Author).filter(Author.id == user_id).one()
# Получаем существующие роли
existing_roles = set(role.name for role in author.roles)
# Добавляем новые роли
for role_name in roles:
if role_name not in existing_roles:
# Получаем или создаем роль
role = session.query(Role).filter(Role.name == role_name).first()
if not role:
role = Role(id=role_name, name=role_name)
session.add(role)
# Добавляем роль автору
author.roles.append(role)
session.commit()
return user_id
except exc.NoResultFound:
logger.error(f"Author {user_id} not found")
return None
def login_required(f):
"""
Декоратор для проверки авторизации пользователя.
Этот декоратор проверяет, авторизован ли пользователь, <20><> добавляет
информацию о пользователе в контекст функции.
Параметры:
- f: Функция, которую нужно декорировать.
Возвращает:
- Обернутую функцию с добавленной проверкой авторизации.
"""
"""Декоратор для проверки авторизации пользователя."""
@wraps(f)
async def decorated_function(*args, **kwargs):
@@ -135,18 +105,7 @@ def login_required(f):
def login_accepted(f):
"""
Декоратор для добавления данных авторизации в контекст.
Этот декоратор добавляет данные авторизации в контекст, если они доступны,
но не блокирует доступ для неавторизованных пользователей.
Параметры:
- f: Функция, которую нужно декорировать.
Возвращает:
- Обернутую функцию с добавленной проверкой авторизации.
"""
"""Декоратор для добавления данных авторизации в контекст."""
@wraps(f)
async def decorated_function(*args, **kwargs):
@@ -166,12 +125,11 @@ def login_accepted(f):
author = await get_cached_author_by_user_id(user_id, get_with_stat)
if author:
logger.debug(f"login_accepted: Найден профиль автора: {author}")
# Предполагается, что `author` является объектом с атрибутом `id`
info.context["author"] = author.dict()
else:
logger.error(
f"login_accepted: Профиль автора не найден для пользователя {user_id}. Используем базовые данные."
) # Используем базовую информацию об автор
)
else:
logger.debug("login_accepted: Пользователь не авторизован. Очищаем контекст.")
info.context["user_id"] = None

View File

@@ -50,10 +50,25 @@ FILTERED_FIELDS = ["_sa_instance_state", "search_vector"]
def create_table_if_not_exists(engine, table):
"""
Создает таблицу, если она не существует в базе данных.
Args:
engine: SQLAlchemy движок базы данных
table: Класс модели SQLAlchemy
"""
inspector = inspect(engine)
if table and not inspector.has_table(table.__tablename__):
table.__table__.create(engine)
logger.info(f"Table '{table.__tablename__}' created.")
try:
table.__table__.create(engine)
logger.info(f"Table '{table.__tablename__}' created.")
except exc.OperationalError as e:
# Проверяем, содержит ли ошибка упоминание о том, что индекс уже существует
if "already exists" in str(e):
logger.warning(f"Skipping index creation for table '{table.__tablename__}': {e}")
else:
# Перевыбрасываем ошибку, если она не связана с дублированием
raise
else:
logger.info(f"Table '{table.__tablename__}' ok.")
@@ -154,21 +169,43 @@ class Base(declarative_base()):
REGISTRY[cls.__name__] = cls
def dict(self) -> Dict[str, Any]:
"""
Конвертирует ORM объект в словарь.
Пропускает атрибуты, которые отсутствуют в объекте, но присутствуют в колонках таблицы.
Преобразует JSON поля в словари.
Добавляет синтетическое поле .stat, если оно существует.
Returns:
Dict[str, Any]: Словарь с атрибутами объекта
"""
column_names = filter(lambda x: x not in FILTERED_FIELDS, self.__table__.columns.keys())
data = {}
try:
for column_name in column_names:
value = getattr(self, column_name)
# Check if the value is JSON and decode it if necessary
if isinstance(value, (str, bytes)) and isinstance(self.__table__.columns[column_name].type, JSON):
try:
data[column_name] = orjson.loads(value)
except (TypeError, orjson.JSONDecodeError) as e:
logger.error(f"Error decoding JSON for column '{column_name}': {e}")
data[column_name] = value
else:
data[column_name] = value
# Add synthetic field .stat if it exists
try:
# Проверяем, существует ли атрибут в объекте
if hasattr(self, column_name):
value = getattr(self, column_name)
# Проверяем, является ли значение JSON и декодируем его при необходимости
if isinstance(value, (str, bytes)) and isinstance(
self.__table__.columns[column_name].type, JSON
):
try:
data[column_name] = orjson.loads(value)
except (TypeError, orjson.JSONDecodeError) as e:
logger.error(f"Error decoding JSON for column '{column_name}': {e}")
data[column_name] = value
else:
data[column_name] = value
else:
# Пропускаем атрибут, если его нет в объекте (может быть добавлен после миграции)
logger.debug(
f"Skipping missing attribute '{column_name}' for {self.__class__.__name__}"
)
except AttributeError as e:
logger.warning(f"Attribute error for column '{column_name}': {e}")
# Добавляем синтетическое поле .stat если оно существует
if hasattr(self, "stat"):
data["stat"] = self.stat
except Exception as e:
@@ -186,7 +223,9 @@ class Base(declarative_base()):
# Функция для вывода полного трейсбека при предупреждениях
def warning_with_traceback(message: Warning | str, category, filename: str, lineno: int, file=None, line=None):
def warning_with_traceback(
message: Warning | str, category, filename: str, lineno: int, file=None, line=None
):
tb = traceback.format_stack()
tb_str = "".join(tb)
return f"{message} ({filename}, {lineno}): {category.__name__}\n{tb_str}"

111
services/env.py Normal file
View File

@@ -0,0 +1,111 @@
from typing import Dict, List, Optional
from dataclasses import dataclass
from redis import Redis
from settings import REDIS_URL
from utils.logger import root_logger as logger
@dataclass
class EnvVariable:
key: str
value: str
description: Optional[str] = None
type: str = "string"
is_secret: bool = False
@dataclass
class EnvSection:
name: str
variables: List[EnvVariable]
description: Optional[str] = None
class EnvManager:
"""
Менеджер переменных окружения с хранением в Redis
"""
def __init__(self):
self.redis = Redis.from_url(REDIS_URL)
self.prefix = "env:"
def get_all_variables(self) -> List[EnvSection]:
"""
Получение всех переменных окружения, сгруппированных по секциям
"""
try:
# Получаем все ключи с префиксом env:
keys = self.redis.keys(f"{self.prefix}*")
variables: Dict[str, str] = {}
for key in keys:
var_key = key.decode("utf-8").replace(self.prefix, "")
value = self.redis.get(key)
if value:
variables[var_key] = value.decode("utf-8")
# Группируем переменные по секциям
sections = [
EnvSection(
name="Авторизация",
description="Настройки системы авторизации",
variables=[
EnvVariable(
key="JWT_SECRET",
value=variables.get("JWT_SECRET", ""),
description="Секретный ключ для JWT токенов",
type="string",
is_secret=True,
),
],
),
EnvSection(
name="Redis",
description="Настройки подключения к Redis",
variables=[
EnvVariable(
key="REDIS_URL",
value=variables.get("REDIS_URL", ""),
description="URL подключения к Redis",
type="string",
)
],
),
# Добавьте другие секции по необходимости
]
return sections
except Exception as e:
logger.error(f"Ошибка получения переменных: {e}")
return []
def update_variable(self, key: str, value: str) -> bool:
"""
Обновление значения переменной
"""
try:
full_key = f"{self.prefix}{key}"
self.redis.set(full_key, value)
return True
except Exception as e:
logger.error(f"Ошибка обновления переменной {key}: {e}")
return False
def update_variables(self, variables: List[EnvVariable]) -> bool:
"""
Массовое обновление переменных
"""
try:
pipe = self.redis.pipeline()
for var in variables:
full_key = f"{self.prefix}{var.key}"
pipe.set(full_key, var.value)
pipe.execute()
return True
except Exception as e:
logger.error(f"Ошибка массового обновления переменных: {e}")
return False
env_manager = EnvManager()

View File

@@ -14,4 +14,7 @@ class ExceptionHandlerMiddleware(BaseHTTPMiddleware):
return response
except Exception as exc:
logger.exception(exc)
return JSONResponse({"detail": "An error occurred. Please try again later."}, status_code=500)
return JSONResponse(
{"detail": "An error occurred. Please try again later."},
status_code=500,
)

View File

@@ -94,8 +94,7 @@ async def notify_draft(draft_data, action: str = "publish"):
# Если переданы связанные атрибуты, добавим их
if hasattr(draft_data, "topics") and draft_data.topics is not None:
draft_payload["topics"] = [
{"id": t.id, "name": t.name, "slug": t.slug}
for t in draft_data.topics
{"id": t.id, "name": t.name, "slug": t.slug} for t in draft_data.topics
]
if hasattr(draft_data, "authors") and draft_data.authors is not None:

View File

@@ -40,6 +40,17 @@ class RedisService:
except Exception as e:
logger.error(e)
def pipeline(self):
"""
Возвращает пайплайн Redis для выполнения нескольких команд в одной транзакции.
Returns:
Pipeline: объект pipeline Redis
"""
if self._client:
return self._client.pipeline()
raise Exception("Redis client is not initialized")
async def subscribe(self, *channels):
if self._client:
async with self._client.pubsub() as pubsub:
@@ -75,6 +86,82 @@ class RedisService:
async def get(self, key):
return await self.execute("get", key)
async def delete(self, *keys):
"""
Удаляет ключи из Redis.
Args:
*keys: Ключи для удаления
Returns:
int: Количество удаленных ключей
"""
if not self._client or not keys:
return 0
return await self._client.delete(*keys)
async def hmset(self, key, mapping):
"""
Устанавливает несколько полей хеша.
Args:
key: Ключ хеша
mapping: Словарь с полями и значениями
"""
if not self._client:
return
await self._client.hset(key, mapping=mapping)
async def expire(self, key, seconds):
"""
Устанавливает время жизни ключа.
Args:
key: Ключ
seconds: Время жизни в секундах
"""
if not self._client:
return
await self._client.expire(key, seconds)
async def sadd(self, key, *values):
"""
Добавляет значения в множество.
Args:
key: Ключ множества
*values: Значения для добавления
"""
if not self._client:
return
await self._client.sadd(key, *values)
async def srem(self, key, *values):
"""
Удаляет значения из множества.
Args:
key: Ключ множества
*values: Значения для удаления
"""
if not self._client:
return
await self._client.srem(key, *values)
async def smembers(self, key):
"""
Получает все элементы множества.
Args:
key: Ключ множества
Returns:
set: Множество элементов
"""
if not self._client:
return set()
return await self._client.smembers(key)
redis = RedisService()

View File

@@ -1,10 +1,8 @@
from asyncio.log import logger
import httpx
from ariadne import MutationType, ObjectType, QueryType
from services.db import create_table_if_not_exists, local_session
from settings import AUTH_URL
query = QueryType()
mutation = MutationType()
@@ -12,50 +10,19 @@ type_draft = ObjectType("Draft")
resolvers = [query, mutation, type_draft]
async def request_graphql_data(gql, url=AUTH_URL, headers=None):
"""
Выполняет GraphQL запрос к указанному URL
:param gql: GraphQL запрос
:param url: URL для запроса, по умолчанию AUTH_URL
:param headers: Заголовки запроса
:return: Результат запроса или None в случае ошибки
"""
if not url:
return None
if headers is None:
headers = {"Content-Type": "application/json"}
try:
async with httpx.AsyncClient() as client:
response = await client.post(url, json=gql, headers=headers)
if response.status_code == 200:
data = response.json()
errors = data.get("errors")
if errors:
logger.error(f"{url} response: {data}")
else:
return data
else:
logger.error(f"{url}: {response.status_code} {response.text}")
except Exception as _e:
import traceback
logger.error(f"request_graphql_data error: {traceback.format_exc()}")
return None
def create_all_tables():
"""Create all database tables in the correct order."""
from orm import author, community, draft, notification, reaction, shout, topic
from auth.orm import Author, AuthorFollower, AuthorBookmark, AuthorRating
from orm import community, draft, notification, reaction, shout, topic
# Порядок важен - сначала таблицы без внешних ключей, затем зависимые таблицы
models_in_order = [
# user.User, # Базовая таблица auth
author.Author, # Базовая таблица
Author, # Базовая таблица
community.Community, # Базовая таблица
topic.Topic, # Базовая таблица
# Связи для базовых таблиц
author.AuthorFollower, # Зависит от Author
AuthorFollower, # Зависит от Author
community.CommunityFollower, # Зависит от Community
topic.TopicFollower, # Зависит от Topic
# Черновики (теперь без зависимости от Shout)
@@ -70,7 +37,8 @@ def create_all_tables():
reaction.Reaction, # Зависит от Author и Shout
shout.ShoutReactionsFollower, # Зависит от Shout и Reaction
# Дополнительные таблицы
author.AuthorRating, # Зависит от Author
AuthorRating, # Зависит от Author
AuthorBookmark, # Зависит от Author
notification.Notification, # Зависит от Author
notification.NotificationSeen, # Зависит от Notification
# collection.Collection,

View File

@@ -171,11 +171,16 @@ class SearchService:
}
asyncio.create_task(self.perform_index(shout, index_body))
def close(self):
if self.client:
self.client.close()
async def perform_index(self, shout, index_body):
if self.client:
try:
await asyncio.wait_for(
self.client.index(index=self.index_name, id=str(shout.id), body=index_body), timeout=40.0
self.client.index(index=self.index_name, id=str(shout.id), body=index_body),
timeout=40.0,
)
except asyncio.TimeoutError:
logger.error(f"Indexing timeout for shout {shout.id}")
@@ -188,7 +193,9 @@ class SearchService:
logger.info(f"Ищем: {text} {offset}+{limit}")
search_body = {
"query": {"multi_match": {"query": text, "fields": ["title", "lead", "subtitle", "body", "media"]}}
"query": {
"multi_match": {"query": text, "fields": ["title", "lead", "subtitle", "body", "media"]}
}
}
if self.client:

View File

@@ -14,7 +14,7 @@ from google.analytics.data_v1beta.types import (
)
from google.analytics.data_v1beta.types import Filter as GAFilter
from orm.author import Author
from auth.orm import Author
from orm.shout import Shout, ShoutAuthor, ShoutTopic
from orm.topic import Topic
from services.db import local_session
@@ -228,12 +228,20 @@ class ViewedStorage:
# Обновление тем и авторов с использованием вспомогательной функции
for [_st, topic] in (
session.query(ShoutTopic, Topic).join(Topic).join(Shout).where(Shout.slug == shout_slug).all()
session.query(ShoutTopic, Topic)
.join(Topic)
.join(Shout)
.where(Shout.slug == shout_slug)
.all()
):
update_groups(self.shouts_by_topic, topic.slug, shout_slug)
for [_st, author] in (
session.query(ShoutAuthor, Author).join(Author).join(Shout).where(Shout.slug == shout_slug).all()
session.query(ShoutAuthor, Author)
.join(Author)
.join(Shout)
.where(Shout.slug == shout_slug)
.all()
):
update_groups(self.shouts_by_author, author.slug, shout_slug)
@@ -266,7 +274,9 @@ class ViewedStorage:
if failed == 0:
when = datetime.now(timezone.utc) + timedelta(seconds=self.period)
t = format(when.astimezone().isoformat())
logger.info(" ⎩ next update: %s" % (t.split("T")[0] + " " + t.split("T")[1].split(".")[0]))
logger.info(
" ⎩ next update: %s" % (t.split("T")[0] + " " + t.split("T")[1].split(".")[0])
)
await asyncio.sleep(self.period)
else:
await asyncio.sleep(10)

View File

@@ -1,175 +0,0 @@
import asyncio
import os
import re
from asyncio.log import logger
from sqlalchemy import select
from starlette.endpoints import HTTPEndpoint
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import JSONResponse
from cache.cache import cache_author
from orm.author import Author
from resolvers.stat import get_with_stat
from services.db import local_session
from services.schema import request_graphql_data
from settings import ADMIN_SECRET, WEBHOOK_SECRET
async def check_webhook_existence():
"""
Проверяет существование вебхука для user.login события
Returns:
tuple: (bool, str, str) - существует ли вебхук, его id и endpoint если существует
"""
logger.info("check_webhook_existence called")
if not ADMIN_SECRET:
logger.error("ADMIN_SECRET is not set")
return False, None, None
headers = {"Content-Type": "application/json", "X-Authorizer-Admin-Secret": ADMIN_SECRET}
operation = "GetWebhooks"
query_name = "_webhooks"
variables = {"params": {}}
# https://docs.authorizer.dev/core/graphql-api#_webhooks
gql = {
"query": f"query {operation}($params: PaginatedInput!)"
+ "{"
+ f"{query_name}(params: $params) {{ webhooks {{ id event_name endpoint }} }} "
+ "}",
"variables": variables,
"operationName": operation,
}
result = await request_graphql_data(gql, headers=headers)
if result:
webhooks = result.get("data", {}).get(query_name, {}).get("webhooks", [])
logger.info(webhooks)
for webhook in webhooks:
if webhook["event_name"].startswith("user.login"):
return True, webhook["id"], webhook["endpoint"]
return False, None, None
async def create_webhook_endpoint():
"""
Создает вебхук для user.login события.
Если существует старый вебхук - удаляет его и создает новый.
"""
logger.info("create_webhook_endpoint called")
headers = {"Content-Type": "application/json", "X-Authorizer-Admin-Secret": ADMIN_SECRET}
exists, webhook_id, current_endpoint = await check_webhook_existence()
# Определяем endpoint в зависимости от окружения
host = os.environ.get("HOST", "core.dscrs.site")
endpoint = f"https://{host}/new-author"
if exists:
# Если вебхук существует, но с другим endpoint или с модифицированным именем
if current_endpoint != endpoint or webhook_id:
# https://docs.authorizer.dev/core/graphql-api#_delete_webhook
operation = "DeleteWebhook"
query_name = "_delete_webhook"
variables = {"params": {"id": webhook_id}} # Изменено с id на webhook_id
gql = {
"query": f"mutation {operation}($params: WebhookRequest!)"
+ "{"
+ f"{query_name}(params: $params) {{ message }} "
+ "}",
"variables": variables,
"operationName": operation,
}
try:
await request_graphql_data(gql, headers=headers)
exists = False
except Exception as e:
logger.error(f"Failed to delete webhook: {e}")
# Продолжаем выполнение даже при ошибке удаления
exists = False
else:
logger.info(f"Webhook already exists and configured correctly: {webhook_id}")
return
if not exists:
# https://docs.authorizer.dev/core/graphql-api#_add_webhook
operation = "AddWebhook"
query_name = "_add_webhook"
variables = {
"params": {
"event_name": "user.login",
"endpoint": endpoint,
"enabled": True,
"headers": {"Authorization": WEBHOOK_SECRET},
}
}
gql = {
"query": f"mutation {operation}($params: AddWebhookRequest!)"
+ "{"
+ f"{query_name}(params: $params) {{ message }} "
+ "}",
"variables": variables,
"operationName": operation,
}
try:
result = await request_graphql_data(gql, headers=headers)
logger.info(result)
except Exception as e:
logger.error(f"Failed to create webhook: {e}")
class WebhookEndpoint(HTTPEndpoint):
async def post(self, request: Request) -> JSONResponse:
try:
data = await request.json()
if not data:
raise HTTPException(status_code=400, detail="Request body is empty")
auth = request.headers.get("Authorization")
if not auth or auth != os.environ.get("WEBHOOK_SECRET"):
raise HTTPException(status_code=401, detail="Invalid Authorization header")
# logger.debug(data)
user = data.get("user")
if not isinstance(user, dict):
raise HTTPException(status_code=400, detail="User data is not a dictionary")
#
name: str = (
f"{user.get('given_name', user.get('slug'))} {user.get('middle_name', '')}"
+ f"{user.get('family_name', '')}".strip()
) or "Аноним"
user_id: str = user.get("id", "")
email: str = user.get("email", "")
pic: str = user.get("picture", "")
if user_id:
with local_session() as session:
author = session.query(Author).filter(Author.user == user_id).first()
if not author:
# If the author does not exist, create a new one
slug: str = email.split("@")[0].replace(".", "-").lower()
slug: str = re.sub("[^0-9a-z]+", "-", slug)
while True:
author = session.query(Author).filter(Author.slug == slug).first()
if not author:
break
slug = f"{slug}-{len(session.query(Author).filter(Author.email == email).all()) + 1}"
author = Author(user=user_id, slug=slug, name=name, pic=pic)
session.add(author)
session.commit()
author_query = select(Author).filter(Author.user == user_id)
result = get_with_stat(author_query)
if result:
author_with_stat = result[0]
author_dict = author_with_stat.dict()
# await cache_author(author_with_stat)
asyncio.create_task(cache_author(author_dict))
return JSONResponse({"status": "success"})
except HTTPException as e:
return JSONResponse({"status": "error", "message": str(e.detail)}, status_code=e.status_code)
except Exception as e:
import traceback
traceback.print_exc()
return JSONResponse({"status": "error", "message": str(e)}, status_code=500)