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", "botocore>=1.42.9",
"types-aiofiles>=25.1.0.20251011", "types-aiofiles>=25.1.0.20251011",
"betterconf>=4.5.0", "betterconf>=4.5.0",
"dataclasses-ujson>=0.0.34",
"asyncpg>=0.31.0",
] ]
[project.optional-dependencies] [project.optional-dependencies]

View File

@ -5,6 +5,7 @@ from functools import lru_cache
from betterconf import betterconf, field from betterconf import betterconf, field
from betterconf.caster import to_bool, to_int, to_list from betterconf.caster import to_bool, to_int, to_list
@betterconf @betterconf
class AppConfig: class AppConfig:
# pylint: disable=R0903 # pylint: disable=R0903
@ -18,12 +19,12 @@ class AppConfig:
app_port: int = field("APP_PORT", default=8000, caster=to_int) 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") app_public_url: str = field("APP_PUBLIC_URL", default="http://127.0.0.1:8000")
sentry_dns: str = field("SENTRY_DNS", default="") sentry_dns: str = field("SENTRY_DNS", default="")
log_level: str = field("LOG_LEVEL", "INFO") log_level: str = field("LOG_LEVEL", "INFO")
db_uri: str = field(
db_uri: str = field("DB_URI", "postgresql+asyncpg://svcuser:svcpass@localhost:5432/svc") "DB_URI", "postgresql+asyncpg://svcuser:svcpass@localhost:5432/svc"
)
db_pass_salt: str = field("DB_PASS_SALT", "") db_pass_salt: str = field("DB_PASS_SALT", "")
db_search_path: str = field("DB_SEARCH_PATH", "public") db_search_path: str = field("DB_SEARCH_PATH", "public")
@ -34,7 +35,6 @@ class AppConfig:
fs_s3_endpoint: str = field("FS_S3_ENDPOINT", "") fs_s3_endpoint: str = field("FS_S3_ENDPOINT", "")
@lru_cache @lru_cache
def get_app_config() -> AppConfig: def get_app_config() -> AppConfig:
# pylint: disable=C0116 # 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.mock import MockDB, MockSession
from server.infra.db.pg import AsyncDB 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): if "postgresql+asyncpg" in str(cnf.db_uri):
con_arg = {"server_settings": {"search_path": cnf.db_search_path}} con_arg = {"server_settings": {"search_path": cnf.db_search_path}}
self.engine = asyncio.create_async_engine( 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) # self.engine.execution_options(stream_results=True)

View File

@ -52,9 +52,21 @@ LOGGING_CONFIG = {
}, },
"loggers": { "loggers": {
"": {"handlers": ["default"], "level": cnf.log_level, "propagate": False}, "": {"handlers": ["default"], "level": cnf.log_level, "propagate": False},
"uvicorn.access": {"handlers": ["uvicorn_access"], "level": "INFO", "propagate": False}, "uvicorn.access": {
"uvicorn.error": {"handlers": ["uvicorn_default"], "level": "INFO", "propagate": False}, "handlers": ["uvicorn_access"],
"uvicorn.asgi": {"handlers": ["uvicorn_default"], "level": "INFO", "propagate": False}, "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 @staticmethod
def mass_and(fields: list[object], values: list[Any]) -> "FilterQuery": 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 @staticmethod
def mass_or(fields: list[object], values: list[Any]) -> "FilterQuery": 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 @staticmethod
def eq(field: object, value: Any) -> "FilterQuery": def eq(field: object, value: Any) -> "FilterQuery":
@ -129,7 +137,9 @@ class DataclassInstance(Protocol):
__dataclass_fields__: ClassVar[dict[str, Field[Any]]] __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] = [] r_data: list[int] = []
for i, _ in enumerate(input_data): for i, _ in enumerate(input_data):
if getattr(input_data[i], id_name) in values: 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 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()) # can't do query AND(OR() + AND())
data: list[T] = [] data: list[T] = []
data_or: list[T] = [] data_or: list[T] = []
@ -233,10 +245,14 @@ def sqlalchemy_conditions(q: FilterQuery):
conditions = [] conditions = []
for f in q.filters: for f in q.filters:
if f.sign == FilterSign.OR: 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 continue
if f.sign == FilterSign.AND: 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 continue
if f.left is None: if f.left is None:
continue continue
@ -266,7 +282,9 @@ def sqlalchemy_conditions(q: FilterQuery):
return conditions 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: if f.limit:
q = q.limit(f.limit) q = q.limit(f.limit)
if f.offset: if f.offset:

View File

@ -1,31 +1,15 @@
import asyncio
from pathlib import Path
import os
import inject import inject
import markdown import markdown
from litestar import ( from litestar import (
Controller, Controller,
get, 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.exceptions import HTTPException
from litestar.contrib.jinja import JinjaTemplateEngine
from litestar.template.config import TemplateConfig
from litestar.response import Template from litestar.response import Template
from litestar.static_files import create_static_files_router
from server.infra import logger from server.modules.descriptions import CharactersService
from server.config import get_app_config from server.modules.recognizer import RecognizerService
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
class DescriptionController(Controller): class DescriptionController(Controller):
path = "/" path = "/"
@ -48,6 +32,7 @@ class DescriptionController(Controller):
@get("/dogs-characteristics") @get("/dogs-characteristics")
async def dogs_characteristics(self) -> Template: async def dogs_characteristics(self) -> Template:
characters_service: CharactersService = inject.instance(CharactersService)
breeds = await characters_service.get_characters() breeds = await characters_service.get_characters()
return Template( return Template(
template_name="dogs-characteristics.html", context={"breeds": breeds} template_name="dogs-characteristics.html", context={"breeds": breeds}
@ -55,9 +40,11 @@ class DescriptionController(Controller):
@get("/dogs-characteristics/{name:str}") @get("/dogs-characteristics/{name:str}")
async def beer_description(self, name: str) -> Template: async def beer_description(self, name: str) -> Template:
characters_service: CharactersService = inject.instance(CharactersService)
breed = await characters_service.get_character(name) breed = await characters_service.get_character(name)
if breed is None: if breed is None:
raise HTTPException(status_code=404, detail="Порода не найдена") raise HTTPException(status_code=404, detail="Порода не найдена")
recognizer_service: RecognizerService = inject.instance(RecognizerService)
images = await recognizer_service.images_dogs() images = await recognizer_service.images_dogs()
return Template( return Template(
template_name="beers-description.html", template_name="beers-description.html",
@ -67,4 +54,3 @@ class DescriptionController(Controller):
"images": [f"/static/assets/dog/{name}/{i}" for i in images[name]], "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 inject
import markdown
from litestar import ( from litestar import (
Controller, Controller,
get,
post, post,
MediaType,
Litestar,
) )
from litestar.enums import RequestEncodingType from litestar.enums import RequestEncodingType
from litestar.datastructures import UploadFile from litestar.datastructures import UploadFile
from litestar.params import Body 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.modules.recognizer import RecognizerService
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
class BreedsController(Controller): class BreedsController(Controller):
path = "/beerds" path = "/beerds"
@ -34,6 +17,7 @@ class BreedsController(Controller):
async def beerds_dogs( async def beerds_dogs(
self, data: UploadFile = Body(media_type=RequestEncodingType.MULTI_PART) self, data: UploadFile = Body(media_type=RequestEncodingType.MULTI_PART)
) -> dict: ) -> dict:
recognizer_service: RecognizerService = inject.instance(RecognizerService)
body = await data.read() body = await data.read()
return await recognizer_service.predict_dog_image(body) return await recognizer_service.predict_dog_image(body)
@ -41,5 +25,6 @@ class BreedsController(Controller):
async def beerds_cats( async def beerds_cats(
self, data: UploadFile = Body(media_type=RequestEncodingType.MULTI_PART) self, data: UploadFile = Body(media_type=RequestEncodingType.MULTI_PART)
) -> dict: ) -> dict:
recognizer_service: RecognizerService = inject.instance(RecognizerService)
body = await data.read() body = await data.read()
return await recognizer_service.predict_cat_image(body) 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 inject
import markdown
from litestar import ( from litestar import (
Controller, Controller,
get, get,
post,
MediaType, 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.modules.descriptions import CharactersService, Breed
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
class SeoController(Controller): class SeoController(Controller):
@get("/sitemap.xml", media_type=MediaType.XML) @get("/sitemap.xml", media_type=MediaType.XML)
async def sitemaps(self) -> bytes: async def sitemaps(self) -> bytes:
characters_service: CharactersService = inject.instance(CharactersService)
breeds: list[Breed] = await characters_service.get_characters() breeds: list[Breed] = await characters_service.get_characters()
lastmod = "2025-10-04T19:01:03+00:00" lastmod = "2025-10-04T19:01:03+00:00"
beers_url = "" beers_url = ""

View File

@ -3,62 +3,46 @@ from pathlib import Path
import os import os
import inject import inject
import markdown
from litestar import ( from litestar import (
Controller,
get,
post,
MediaType,
Litestar, 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.contrib.jinja import JinjaTemplateEngine
from litestar.template.config import TemplateConfig from litestar.template.config import TemplateConfig
from litestar.response import Template
from litestar.static_files import create_static_files_router from litestar.static_files import create_static_files_router
from server.infra import logger
from server.config import get_app_config from server.config import get_app_config
from server.infra.web import BreedsController, DescriptionController, SeoController from server.infra.web import BreedsController, DescriptionController, SeoController
from server.infra.db import AsyncDB 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 from server.modules.recognizer import RecognizerService, RecognizerRepository
os.environ["CUDA_VISIBLE_DEVICES"] = "-1" os.environ["CUDA_VISIBLE_DEVICES"] = "-1"
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
def inject_config(binder: inject.Binder): def inject_config(binder: inject.Binder):
"""initialization inject_config for server FastApi""" """initialization inject_config for server FastApi"""
loop.run_until_complete(db.connect())
cnf = get_app_config() cnf = get_app_config()
db = AsyncDB(cnf) db = AsyncDB(cnf)
loop.run_until_complete(db.connect())
binder.bind(RecognizerService, RecognizerService(RecognizerRepository())) binder.bind(RecognizerService, RecognizerService(RecognizerRepository()))
binder.bind(CharactersService, CharactersService(CharactersRepository())) binder.bind(CharactersService, CharactersService(CharactersRepository()))
inject.configure(inject_config)
app = Litestar(
debug=True,
route_handlers=[
if __name__ == "__main__": BreedsController,
inject.configure(inject_config) DescriptionController,
app = Litestar( SeoController,
debug=True, create_static_files_router(path="/static", directories=["server/static"]),
route_handlers=[ ],
BreedsController, template_config=TemplateConfig(
DescriptionController, directory=Path("server/templates"),
SeoController, engine=JinjaTemplateEngine,
create_static_files_router(path="/static", directories=["server/static"]), ),
], )
template_config=TemplateConfig(
directory=Path("server/templates"),
engine=JinjaTemplateEngine,
),
)

View File

@ -1,10 +1,11 @@
from dataclasses import dataclass 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) @dataclass(frozen=True)
class Attachment: class Attachment(UJsonMixin):
id: str id: str
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime

View File

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

View File

@ -1,14 +1,11 @@
import hashlib import hashlib
import hmac
import os.path import os.path
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from datetime import UTC, datetime from datetime import UTC, datetime
from enum import Enum from enum import Enum
from http import HTTPStatus
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Any, AsyncIterable, AsyncIterator, Optional from typing import Any, AsyncIterable, AsyncIterator, Optional
from urllib.parse import urlparse
import uuid import uuid
import aioboto3 # type: ignore import aioboto3 # type: ignore
@ -141,7 +138,9 @@ class S3StorageDriver(StorageDriver):
return self._session.client("s3", endpoint_url=self._cnf.fs_s3_endpoint) return self._session.client("s3", endpoint_url=self._cnf.fs_s3_endpoint)
def _normalize_path(self, path: str) -> str: 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: async def put(self, data: bytes) -> str:
sign = hashlib.file_digest(BytesIO(data), "sha256").hexdigest() 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}") self._logger.error(f"stream client error: {str(e)}, path: {path}")
raise FileNotFoundError raise FileNotFoundError
except Exception as e: 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 raise FileNotFoundError
async def take(self, path: str) -> Optional[bytes]: async def take(self, path: str) -> Optional[bytes]:
@ -190,7 +191,10 @@ class S3StorageDriver(StorageDriver):
async def delete(self, path: str) -> None: async def delete(self, path: str) -> None:
async with await self._client() as s3: 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_MAX_SIZE = 100_000
RESIZE_PARAMS = (500, 500) RESIZE_PARAMS = (500, 500)
@ -240,22 +244,8 @@ class AtachmentService:
def url(self, attachment_id: str, content_type: str | None = None) -> str: 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.{ return f"{self._cnf.app_public_url}/api/v0/attachment/{attachment_id}.original.{
self.extension(content_type)}" 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]
async def create(self, file: bytes, user_id: str) -> Attachment: async def create(self, file: bytes, user_id: str) -> Attachment:
path = await self._driver.put(file) path = await self._driver.put(file)
@ -269,12 +259,14 @@ class AtachmentService:
created_by=user_id, created_by=user_id,
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
created_at=datetime.now(UTC), created_at=datetime.now(UTC),
updated_at=datetime.now(UTC) updated_at=datetime.now(UTC),
) )
await self._repository.create(attach) await self._repository.create(attach)
return 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: if not attach_id:
return [] return []
if session is not None: if session is not None:
@ -285,13 +277,17 @@ class AtachmentService:
def get_name(self, attachment: Attachment) -> str: def get_name(self, attachment: Attachment) -> str:
return f"{attachment.id}.{self.extension(attachment.content_type)}" 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]) file = await self._repository.get_by_id(session, [attach_id])
if not file: if not file:
return None return None
return await self._driver.take(file[0].path) 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): async def _stream_iterator(is_empty: bool):
if is_empty: if is_empty:
return return
@ -347,5 +343,7 @@ class AtachmentService:
f"delete:{item.path}", f"delete:{item.path}",
) )
path = await self._driver.put(d) 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) await self._driver.delete(item.path)

View File

@ -22,7 +22,7 @@ class CharactersRepository(ACharactersRepository):
@cached(ttl=60, cache=Cache.MEMORY) @cached(ttl=60, cache=Cache.MEMORY)
async def get_characters(self) -> list[Breed]: 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] = [] breeds: list[Breed] = []
# Идем по каждому текстовому файлу с описанием породы # Идем по каждому текстовому файлу с описанием породы
@ -57,7 +57,7 @@ class PGCharactersRepository(ACharactersRepository):
@cached(ttl=60, cache=Cache.MEMORY) @cached(ttl=60, cache=Cache.MEMORY)
async def get_characters(self) -> list[Breed]: 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] = [] breeds: list[Breed] = []
# Идем по каждому текстовому файлу с описанием породы # Идем по каждому текстовому файлу с описанием породы

View File

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

View File

@ -40,7 +40,7 @@ class RecognizerService:
predicted_data = self._predict(image, DOG_MODEL) predicted_data = self._predict(image, DOG_MODEL)
results = {} results = {}
images = [] images = []
description = {} description: dict[str, list] = {}
images_dogs = await self._repository.images_dogs() images_dogs = await self._repository.images_dogs()
for d in predicted_data: for d in predicted_data:
predicted_idx, probabilities = d 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 results[probabilities] = name
return { return {
"results": results, "results": results,

View File

@ -33,7 +33,7 @@ async function SavePhoto(self) {
// Обработка основных результатов // Обработка основных результатов
for (let key in json.results) { for (let key in json.results) {
if (json.description != undefined) { 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 { } else {
text += `<div class='image-block'><div class='image-text'>${json.results[key]} (вероятность: ${Math.round(parseFloat(key) * 100)}%)</div>`; 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 %} {% block form %}{% endblock %}
</body> </body>
</section> </section>
<script src="/static/scripts.js?v=6"></script> <script src="/static/scripts.js?v=7"></script>
</html> </html>

55
uv.lock
View File

@ -14,8 +14,10 @@ source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "aiocache" }, { name = "aiocache" },
{ name = "aiofiles" }, { name = "aiofiles" },
{ name = "asyncpg" },
{ name = "betterconf" }, { name = "betterconf" },
{ name = "botocore" }, { name = "botocore" },
{ name = "dataclasses-ujson" },
{ name = "granian" }, { name = "granian" },
{ name = "inject" }, { name = "inject" },
{ name = "jinja2" }, { name = "jinja2" },
@ -56,8 +58,10 @@ default = [
requires-dist = [ requires-dist = [
{ name = "aiocache" }, { name = "aiocache" },
{ name = "aiofiles", specifier = ">=25.1.0" }, { name = "aiofiles", specifier = ">=25.1.0" },
{ name = "asyncpg", specifier = ">=0.31.0" },
{ name = "betterconf", specifier = ">=4.5.0" }, { name = "betterconf", specifier = ">=4.5.0" },
{ name = "botocore", specifier = ">=1.42.9" }, { name = "botocore", specifier = ">=1.42.9" },
{ name = "dataclasses-ujson", specifier = ">=0.0.34" },
{ name = "granian", specifier = "==2.5" }, { name = "granian", specifier = "==2.5" },
{ name = "inject", specifier = ">=5.3.0" }, { name = "inject", specifier = ">=5.3.0" },
{ name = "jinja2", specifier = ">=3.1.6" }, { 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 }, { 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]] [[package]]
name = "betterconf" name = "betterconf"
version = "4.5.0" 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 }, { 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]] [[package]]
name = "faker" name = "faker"
version = "38.0.0" 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 }, { 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]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.15.0" version = "4.15.0"