done refactoring
Gitea Actions Demo / build_and_push (push) Failing after 1m14s Details

This commit is contained in:
artem 2025-12-16 21:34:07 +03:00
parent 9f5071a7b8
commit efba462335
20 changed files with 196 additions and 155 deletions

View File

@ -27,6 +27,8 @@ dependencies = [
"botocore>=1.42.9",
"types-aiofiles>=25.1.0.20251011",
"betterconf>=4.5.0",
"dataclasses-ujson>=0.0.34",
"asyncpg>=0.31.0",
]
[project.optional-dependencies]

View File

@ -5,6 +5,7 @@ from functools import lru_cache
from betterconf import betterconf, field
from betterconf.caster import to_bool, to_int, to_list
@betterconf
class AppConfig:
# pylint: disable=R0903
@ -18,12 +19,12 @@ class AppConfig:
app_port: int = field("APP_PORT", default=8000, caster=to_int)
app_public_url: str = field("APP_PUBLIC_URL", default="http://127.0.0.1:8000")
sentry_dns: str = field("SENTRY_DNS", default="")
log_level: str = field("LOG_LEVEL", "INFO")
db_uri: str = field("DB_URI", "postgresql+asyncpg://svcuser:svcpass@localhost:5432/svc")
db_uri: str = field(
"DB_URI", "postgresql+asyncpg://svcuser:svcpass@localhost:5432/svc"
)
db_pass_salt: str = field("DB_PASS_SALT", "")
db_search_path: str = field("DB_SEARCH_PATH", "public")
@ -34,7 +35,6 @@ class AppConfig:
fs_s3_endpoint: str = field("FS_S3_ENDPOINT", "")
@lru_cache
def get_app_config() -> AppConfig:
# pylint: disable=C0116

View File

@ -2,4 +2,11 @@ from server.infra.db.abc import AbstractDB, AbstractSession, ExecuteFun
from server.infra.db.mock import MockDB, MockSession
from server.infra.db.pg import AsyncDB
__all__ = ["AsyncDB", "AbstractDB", "ExecuteFun", "AbstractSession", "MockDB", "MockSession"]
__all__ = [
"AsyncDB",
"AbstractDB",
"ExecuteFun",
"AbstractSession",
"MockDB",
"MockSession",
]

View File

@ -15,7 +15,10 @@ class AsyncDB(AbstractDB):
if "postgresql+asyncpg" in str(cnf.db_uri):
con_arg = {"server_settings": {"search_path": cnf.db_search_path}}
self.engine = asyncio.create_async_engine(
str(cnf.db_uri), echo=bool(cnf.app_debug), connect_args=con_arg, pool_recycle=1800
str(cnf.db_uri),
echo=bool(cnf.app_debug),
connect_args=con_arg,
pool_recycle=1800,
)
# self.engine.execution_options(stream_results=True)

View File

@ -52,9 +52,21 @@ LOGGING_CONFIG = {
},
"loggers": {
"": {"handlers": ["default"], "level": cnf.log_level, "propagate": False},
"uvicorn.access": {"handlers": ["uvicorn_access"], "level": "INFO", "propagate": False},
"uvicorn.error": {"handlers": ["uvicorn_default"], "level": "INFO", "propagate": False},
"uvicorn.asgi": {"handlers": ["uvicorn_default"], "level": "INFO", "propagate": False},
"uvicorn.access": {
"handlers": ["uvicorn_access"],
"level": "INFO",
"propagate": False,
},
"uvicorn.error": {
"handlers": ["uvicorn_default"],
"level": "INFO",
"propagate": False,
},
"uvicorn.asgi": {
"handlers": ["uvicorn_default"],
"level": "INFO",
"propagate": False,
},
},
}

View File

@ -94,11 +94,19 @@ class FilterQuery:
@staticmethod
def mass_and(fields: list[object], values: list[Any]) -> "FilterQuery":
return FilterQuery(filters=[Filter.eq(field, val) for field, val in zip(fields, values)])
return FilterQuery(
filters=[Filter.eq(field, val) for field, val in zip(fields, values)]
)
@staticmethod
def mass_or(fields: list[object], values: list[Any]) -> "FilterQuery":
return FilterQuery(filters=[Filter.or_([Filter.eq(field, val) for field, val in zip(fields, values)])])
return FilterQuery(
filters=[
Filter.or_(
[Filter.eq(field, val) for field, val in zip(fields, values)]
)
]
)
@staticmethod
def eq(field: object, value: Any) -> "FilterQuery":
@ -129,7 +137,9 @@ class DataclassInstance(Protocol):
__dataclass_fields__: ClassVar[dict[str, Field[Any]]]
async def indexes_by_id(input_data: list, values: list[str], id_name="id") -> Optional[list[int]]:
async def indexes_by_id(
input_data: list, values: list[str], id_name="id"
) -> Optional[list[int]]:
r_data: list[int] = []
for i, _ in enumerate(input_data):
if getattr(input_data[i], id_name) in values:
@ -139,7 +149,9 @@ async def indexes_by_id(input_data: list, values: list[str], id_name="id") -> Op
return r_data
def data_by_filter[T: DataclassInstance](input_data: list[T], q: FilterQuery) -> list[T]:
def data_by_filter[T: DataclassInstance](
input_data: list[T], q: FilterQuery
) -> list[T]:
# can't do query AND(OR() + AND())
data: list[T] = []
data_or: list[T] = []
@ -233,10 +245,14 @@ def sqlalchemy_conditions(q: FilterQuery):
conditions = []
for f in q.filters:
if f.sign == FilterSign.OR:
conditions.append(or_(*sqlalchemy_conditions(q=FilterQuery(filters=f.right))))
conditions.append(
or_(*sqlalchemy_conditions(q=FilterQuery(filters=f.right)))
)
continue
if f.sign == FilterSign.AND:
conditions.append(and_(*sqlalchemy_conditions(q=FilterQuery(filters=f.right))))
conditions.append(
and_(*sqlalchemy_conditions(q=FilterQuery(filters=f.right)))
)
continue
if f.left is None:
continue
@ -266,7 +282,9 @@ def sqlalchemy_conditions(q: FilterQuery):
return conditions
def sqlalchemy_restrictions(f: FilterQuery, q: Select, dict_to_sort: Optional[dict] = None) -> Select:
def sqlalchemy_restrictions(
f: FilterQuery, q: Select, dict_to_sort: Optional[dict] = None
) -> Select:
if f.limit:
q = q.limit(f.limit)
if f.offset:

View File

@ -1,31 +1,15 @@
import asyncio
from pathlib import Path
import os
import inject
import markdown
from litestar import (
Controller,
get,
post,
MediaType,
Litestar,
)
from litestar.enums import RequestEncodingType
from litestar.datastructures import UploadFile
from litestar.params import Body
from litestar.exceptions import HTTPException
from litestar.contrib.jinja import JinjaTemplateEngine
from litestar.template.config import TemplateConfig
from litestar.response import Template
from litestar.static_files import create_static_files_router
from server.infra import logger
from server.config import get_app_config
from server.infra.cache import LocalCacheRepository
from server.infra.db import AsyncDB
from server.modules.descriptions import CharactersService, Breed, CharactersRepository
from server.modules.recognizer import RecognizerService, RecognizerRepository
from server.modules.descriptions import CharactersService
from server.modules.recognizer import RecognizerService
class DescriptionController(Controller):
path = "/"
@ -48,6 +32,7 @@ class DescriptionController(Controller):
@get("/dogs-characteristics")
async def dogs_characteristics(self) -> Template:
characters_service: CharactersService = inject.instance(CharactersService)
breeds = await characters_service.get_characters()
return Template(
template_name="dogs-characteristics.html", context={"breeds": breeds}
@ -55,9 +40,11 @@ class DescriptionController(Controller):
@get("/dogs-characteristics/{name:str}")
async def beer_description(self, name: str) -> Template:
characters_service: CharactersService = inject.instance(CharactersService)
breed = await characters_service.get_character(name)
if breed is None:
raise HTTPException(status_code=404, detail="Порода не найдена")
recognizer_service: RecognizerService = inject.instance(RecognizerService)
images = await recognizer_service.images_dogs()
return Template(
template_name="beers-description.html",
@ -67,4 +54,3 @@ class DescriptionController(Controller):
"images": [f"/static/assets/dog/{name}/{i}" for i in images[name]],
},
)

View File

@ -1,31 +1,14 @@
import asyncio
from pathlib import Path
import os
import inject
import markdown
from litestar import (
Controller,
get,
post,
MediaType,
Litestar,
)
from litestar.enums import RequestEncodingType
from litestar.datastructures import UploadFile
from litestar.params import Body
from litestar.exceptions import HTTPException
from litestar.contrib.jinja import JinjaTemplateEngine
from litestar.template.config import TemplateConfig
from litestar.response import Template
from litestar.static_files import create_static_files_router
from server.infra import logger
from server.config import get_app_config
from server.infra.cache import LocalCacheRepository
from server.infra.db import AsyncDB
from server.modules.descriptions import CharactersService, Breed, CharactersRepository
from server.modules.recognizer import RecognizerService, RecognizerRepository
from server.modules.recognizer import RecognizerService
class BreedsController(Controller):
path = "/beerds"
@ -34,6 +17,7 @@ class BreedsController(Controller):
async def beerds_dogs(
self, data: UploadFile = Body(media_type=RequestEncodingType.MULTI_PART)
) -> dict:
recognizer_service: RecognizerService = inject.instance(RecognizerService)
body = await data.read()
return await recognizer_service.predict_dog_image(body)
@ -41,5 +25,6 @@ class BreedsController(Controller):
async def beerds_cats(
self, data: UploadFile = Body(media_type=RequestEncodingType.MULTI_PART)
) -> dict:
recognizer_service: RecognizerService = inject.instance(RecognizerService)
body = await data.read()
return await recognizer_service.predict_cat_image(body)

View File

@ -1,35 +1,17 @@
import asyncio
from pathlib import Path
import os
import inject
import markdown
from litestar import (
Controller,
get,
post,
MediaType,
Litestar,
)
from litestar.enums import RequestEncodingType
from litestar.datastructures import UploadFile
from litestar.params import Body
from litestar.exceptions import HTTPException
from litestar.contrib.jinja import JinjaTemplateEngine
from litestar.template.config import TemplateConfig
from litestar.response import Template
from litestar.static_files import create_static_files_router
from server.infra import logger
from server.config import get_app_config
from server.infra.cache import LocalCacheRepository
from server.infra.db import AsyncDB
from server.modules.descriptions import CharactersService, Breed, CharactersRepository
from server.modules.recognizer import RecognizerService, RecognizerRepository
from server.modules.descriptions import CharactersService, Breed
class SeoController(Controller):
@get("/sitemap.xml", media_type=MediaType.XML)
async def sitemaps(self) -> bytes:
characters_service: CharactersService = inject.instance(CharactersService)
breeds: list[Breed] = await characters_service.get_characters()
lastmod = "2025-10-04T19:01:03+00:00"
beers_url = ""

View File

@ -3,53 +3,37 @@ from pathlib import Path
import os
import inject
import markdown
from litestar import (
Controller,
get,
post,
MediaType,
Litestar,
)
from litestar.enums import RequestEncodingType
from litestar.datastructures import UploadFile
from litestar.params import Body
from litestar.exceptions import HTTPException
from litestar.contrib.jinja import JinjaTemplateEngine
from litestar.template.config import TemplateConfig
from litestar.response import Template
from litestar.static_files import create_static_files_router
from server.infra import logger
from server.config import get_app_config
from server.infra.web import BreedsController, DescriptionController, SeoController
from server.infra.db import AsyncDB
from server.modules.descriptions import CharactersService, Breed, CharactersRepository
from server.modules.descriptions import CharactersService, CharactersRepository
from server.modules.recognizer import RecognizerService, RecognizerRepository
os.environ["CUDA_VISIBLE_DEVICES"] = "-1"
loop = asyncio.new_event_loop()
def inject_config(binder: inject.Binder):
"""initialization inject_config for server FastApi"""
loop.run_until_complete(db.connect())
cnf = get_app_config()
db = AsyncDB(cnf)
loop.run_until_complete(db.connect())
binder.bind(RecognizerService, RecognizerService(RecognizerRepository()))
binder.bind(CharactersService, CharactersService(CharactersRepository()))
if __name__ == "__main__":
inject.configure(inject_config)
app = Litestar(
inject.configure(inject_config)
app = Litestar(
debug=True,
route_handlers=[
BreedsController,
@ -61,4 +45,4 @@ if __name__ == "__main__":
directory=Path("server/templates"),
engine=JinjaTemplateEngine,
),
)
)

View File

@ -1,10 +1,11 @@
from dataclasses import dataclass
from datetime import UTC, datetime
from datetime import datetime
from dataclasses_ujson.dataclasses_ujson import UJsonMixin # type: ignore
@dataclass(frozen=True)
class Attachment:
class Attachment(UJsonMixin):
id: str
created_at: datetime
updated_at: datetime

View File

@ -11,7 +11,9 @@ from server.modules.attachments.domains.attachments import Attachment
class AttachmentRepository(metaclass=ABCMeta):
@abstractmethod
async def get_by_id(self, session: AbstractSession, attach_id: list[str]) -> list[Attachment]:
async def get_by_id(
self, session: AbstractSession, attach_id: list[str]
) -> list[Attachment]:
"""Get Attachment by ID"""
pass
@ -87,7 +89,9 @@ class MockAttachmentRepository(AttachmentRepository):
}
self._db = MockDB(get_app_config())
async def get_by_id(self, session: AbstractSession, attach_id: list[str]) -> list[Attachment]:
async def get_by_id(
self, session: AbstractSession, attach_id: list[str]
) -> list[Attachment]:
f: list[Attachment] = []
for f_id in attach_id:
f_item = self._data.get(f_id)
@ -115,7 +119,9 @@ class DBAttachmentRepository(AttachmentRepository):
def __init__(self, db: AsyncDB):
self._db = db
async def get_by_id(self, session: AbstractSession, attach_id: list[str]) -> list[Attachment]:
async def get_by_id(
self, session: AbstractSession, attach_id: list[str]
) -> list[Attachment]:
q = select(Attachment).where(
Attachment.id.in_(attach_id) # type: ignore
)

View File

@ -1,14 +1,11 @@
import hashlib
import hmac
import os.path
from abc import ABCMeta, abstractmethod
from datetime import UTC, datetime
from enum import Enum
from http import HTTPStatus
from io import BytesIO
from pathlib import Path
from typing import Any, AsyncIterable, AsyncIterator, Optional
from urllib.parse import urlparse
import uuid
import aioboto3 # type: ignore
@ -141,7 +138,9 @@ class S3StorageDriver(StorageDriver):
return self._session.client("s3", endpoint_url=self._cnf.fs_s3_endpoint)
def _normalize_path(self, path: str) -> str:
return f"{S3StorageDriver._prefix}{path}".replace(self._cnf.fs_local_mount_dir, "")
return f"{S3StorageDriver._prefix}{path}".replace(
self._cnf.fs_local_mount_dir, ""
)
async def put(self, data: bytes) -> str:
sign = hashlib.file_digest(BytesIO(data), "sha256").hexdigest()
@ -177,7 +176,9 @@ class S3StorageDriver(StorageDriver):
self._logger.error(f"stream client error: {str(e)}, path: {path}")
raise FileNotFoundError
except Exception as e:
self._logger.error(f"stream error: {type(e).__name__} {str(e)}, path: {path}")
self._logger.error(
f"stream error: {type(e).__name__} {str(e)}, path: {path}"
)
raise FileNotFoundError
async def take(self, path: str) -> Optional[bytes]:
@ -190,7 +191,10 @@ class S3StorageDriver(StorageDriver):
async def delete(self, path: str) -> None:
async with await self._client() as s3:
await s3.delete_object(Bucket=self._cnf.fs_s3_bucket, Key=self._normalize_path(path))
await s3.delete_object(
Bucket=self._cnf.fs_s3_bucket, Key=self._normalize_path(path)
)
RESIZE_MAX_SIZE = 100_000
RESIZE_PARAMS = (500, 500)
@ -240,22 +244,8 @@ class AtachmentService:
def url(self, attachment_id: str, content_type: str | None = None) -> str:
return f"{self._cnf.app_public_url}/api/v0/attachment/{attachment_id}.original.{
self.extension(content_type)}"
def audio(self, attachment: list[Attachment] | None) -> list[Attachment]:
if not attachment:
return []
return [a for a in attachment if a.media_type == MediaType.AUDIO.value and a.is_deleted == False]
def svg_images(self, attachment: list[Attachment] | None) -> list[Attachment]:
if not attachment:
return []
return [a for a in attachment if a.media_type == MediaType.SVG_IMAGE.value and a.is_deleted == False]
def images(self, attachment: list[Attachment] | None) -> list[Attachment]:
if not attachment:
return []
return [a for a in attachment if a.media_type == MediaType.IMAGE.value and a.is_deleted == False]
self.extension(content_type)
}"
async def create(self, file: bytes, user_id: str) -> Attachment:
path = await self._driver.put(file)
@ -269,12 +259,14 @@ class AtachmentService:
created_by=user_id,
id=str(uuid.uuid4()),
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC)
updated_at=datetime.now(UTC),
)
await self._repository.create(attach)
return attach
async def get_info(self, session: AbstractSession | None, attach_id: list[str]) -> list[Attachment]:
async def get_info(
self, session: AbstractSession | None, attach_id: list[str]
) -> list[Attachment]:
if not attach_id:
return []
if session is not None:
@ -285,13 +277,17 @@ class AtachmentService:
def get_name(self, attachment: Attachment) -> str:
return f"{attachment.id}.{self.extension(attachment.content_type)}"
async def get_data(self, session: AbstractSession, attach_id: str) -> Optional[bytes]:
async def get_data(
self, session: AbstractSession, attach_id: str
) -> Optional[bytes]:
file = await self._repository.get_by_id(session, [attach_id])
if not file:
return None
return await self._driver.take(file[0].path)
async def get_stream(self, session: AbstractSession | None, attach_id: str) -> AsyncIterator[bytes]:
async def get_stream(
self, session: AbstractSession | None, attach_id: str
) -> AsyncIterator[bytes]:
async def _stream_iterator(is_empty: bool):
if is_empty:
return
@ -347,5 +343,7 @@ class AtachmentService:
f"delete:{item.path}",
)
path = await self._driver.put(d)
await self._repository.update(item.id, path=path, content_type="image/jpeg", size=len(d))
await self._repository.update(
item.id, path=path, content_type="image/jpeg", size=len(d)
)
await self._driver.delete(item.path)

View File

@ -22,7 +22,7 @@ class CharactersRepository(ACharactersRepository):
@cached(ttl=60, cache=Cache.MEMORY)
async def get_characters(self) -> list[Breed]:
breed_dir = Path("server/services/descriptions/repository/breed_descriptions")
breed_dir = Path("server/modules/descriptions/repository/breed_descriptions")
breeds: list[Breed] = []
# Идем по каждому текстовому файлу с описанием породы
@ -57,7 +57,7 @@ class PGCharactersRepository(ACharactersRepository):
@cached(ttl=60, cache=Cache.MEMORY)
async def get_characters(self) -> list[Breed]:
breed_dir = Path("server/services/descriptions/repository/breed_descriptions")
breed_dir = Path("server/modules/descriptions/repository/breed_descriptions")
breeds: list[Breed] = []
# Идем по каждому текстовому файлу с описанием породы

View File

@ -29,18 +29,18 @@ class RecognizerRepository(ARecognizerRepository):
@cached(ttl=60, cache=Cache.MEMORY)
async def images_dogs(self) -> dict:
with open("server/services/recognizer/repository/meta/images.json", "r") as f:
with open("server/modules/recognizer/repository/meta/images.json", "r") as f:
return ujson.loads(f.read())["dog"]
@cached(ttl=60, cache=Cache.MEMORY)
async def images_cats(self) -> dict:
with open("server/services/recognizer/repository/meta/images.json", "r") as f:
with open("server/modules/recognizer/repository/meta/images.json", "r") as f:
return ujson.loads(f.read())["cat"]
@lru_cache
def labels_cats(self) -> dict:
with open(
"server/services/recognizer/repository/meta/labels_cats.json", "r"
"server/modules/recognizer/repository/meta/labels_cats.json", "r"
) as f:
data_labels = f.read()
return ujson.loads(data_labels)
@ -48,7 +48,7 @@ class RecognizerRepository(ARecognizerRepository):
@lru_cache
def labels_dogs(self) -> dict:
with open(
"server/services/recognizer/repository/meta/labels_dogs.json", "r"
"server/modules/recognizer/repository/meta/labels_dogs.json", "r"
) as f:
data_labels = f.read()
return ujson.loads(data_labels)

View File

@ -40,7 +40,7 @@ class RecognizerService:
predicted_data = self._predict(image, DOG_MODEL)
results = {}
images = []
description = {}
description: dict[str, list] = {}
images_dogs = await self._repository.images_dogs()
for d in predicted_data:
predicted_idx, probabilities = d
@ -55,7 +55,9 @@ class RecognizerService:
],
}
)
description.setdefault(name, []).append(f"/dogs-characteristics/{name.replace(" ", "_")}")
description.setdefault(name, []).append(
f"/dogs-characteristics/{name.replace(' ', '_')}"
)
results[probabilities] = name
return {
"results": results,

View File

@ -33,7 +33,7 @@ async function SavePhoto(self) {
// Обработка основных результатов
for (let key in json.results) {
if (json.description != undefined) {
text += `<div class='image-block'><div class='image-text'>${json.results[key]} (вероятность: ${Math.round(parseFloat(key) * 100)}%) <br/><a href="${json.description[json.results[key]]}" target='_blank'>Описание </a></div>`;
text += `<div class='image-block'><div class='image-text'>${json.results[key]} (вероятность: ${Math.round(parseFloat(key) * 100)}%) <br/><a href="/dogs-characteristics/${json.results[key]}" target='_blank'>Описание </a></div>`;
} else {
text += `<div class='image-block'><div class='image-text'>${json.results[key]} (вероятность: ${Math.round(parseFloat(key) * 100)}%)</div>`;
}

View File

@ -45,6 +45,6 @@
{% block form %}{% endblock %}
</body>
</section>
<script src="/static/scripts.js?v=6"></script>
<script src="/static/scripts.js?v=7"></script>
</html>

55
uv.lock
View File

@ -14,8 +14,10 @@ source = { virtual = "." }
dependencies = [
{ name = "aiocache" },
{ name = "aiofiles" },
{ name = "asyncpg" },
{ name = "betterconf" },
{ name = "botocore" },
{ name = "dataclasses-ujson" },
{ name = "granian" },
{ name = "inject" },
{ name = "jinja2" },
@ -56,8 +58,10 @@ default = [
requires-dist = [
{ name = "aiocache" },
{ name = "aiofiles", specifier = ">=25.1.0" },
{ name = "asyncpg", specifier = ">=0.31.0" },
{ name = "betterconf", specifier = ">=4.5.0" },
{ name = "botocore", specifier = ">=1.42.9" },
{ name = "dataclasses-ujson", specifier = ">=0.0.34" },
{ name = "granian", specifier = "==2.5" },
{ name = "inject", specifier = ">=5.3.0" },
{ name = "jinja2", specifier = ">=3.1.6" },
@ -127,6 +131,38 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/19/24/44299477fe7dcc9cb58d0a57d5a7588d6af2ff403fdd2d47a246c91a3246/anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5", size = 80896 },
]
[[package]]
name = "asyncpg"
version = "0.31.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111 },
{ url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928 },
{ url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067 },
{ url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156 },
{ url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636 },
{ url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079 },
{ url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606 },
{ url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569 },
{ url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867 },
{ url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349 },
{ url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428 },
{ url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678 },
{ url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505 },
{ url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744 },
{ url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251 },
{ url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901 },
{ url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280 },
{ url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931 },
{ url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608 },
{ url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738 },
{ url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026 },
{ url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426 },
{ url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495 },
{ url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062 },
]
[[package]]
name = "betterconf"
version = "4.5.0"
@ -285,6 +321,16 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 },
]
[[package]]
name = "dataclasses-ujson"
version = "0.0.34"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "types-ujson" },
{ name = "ujson" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5f/73/c8aa3fa926f3368f65ec9bb7a74aa42349ed9aa43c54391102a5f0e7ab5c/dataclasses_ujson-0.0.34.tar.gz", hash = "sha256:14b70f85ef57f55e46e0a5233b5d70dd2d40d7e3aa202cfe105c03dd23ed109c", size = 4763 }
[[package]]
name = "faker"
version = "38.0.0"
@ -1460,6 +1506,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658 },
]
[[package]]
name = "types-ujson"
version = "5.10.0.20250822"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5c/bd/d372d44534f84864a96c19a7059d9b4d29db8541828b8b9dc3040f7a46d0/types_ujson-5.10.0.20250822.tar.gz", hash = "sha256:0a795558e1f78532373cf3f03f35b1f08bc60d52d924187b97995ee3597ba006", size = 8437 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/f2/d812543c350674d8b3f6e17c8922248ee3bb752c2a76f64beb8c538b40cf/types_ujson-5.10.0.20250822-py3-none-any.whl", hash = "sha256:3e9e73a6dc62ccc03449d9ac2c580cd1b7a8e4873220db498f7dd056754be080", size = 7657 },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"