From efba4623358f1f95582b8d357c042eca7339cb15 Mon Sep 17 00:00:00 2001 From: artem Date: Tue, 16 Dec 2025 21:34:07 +0300 Subject: [PATCH] done refactoring --- pyproject.toml | 2 + server/config/__init__.py | 8 +-- server/infra/db/__init__.py | 9 ++- server/infra/db/pg.py | 5 +- server/infra/logger/__init__.py | 18 +++++- server/infra/tools/db/filter.py | 32 ++++++++--- server/infra/web/__init__.py | 2 +- server/infra/web/description.py | 26 ++------- server/infra/web/recognizer.py | 23 ++------ server/infra/web/seo.py | 26 ++------- server/main.py | 50 ++++++----------- .../attachments/domains/attachments.py | 7 ++- .../attachments/repositories/attachments.py | 12 +++- .../attachments/services/attachment.py | 52 +++++++++--------- .../descriptions/repository/repository.py | 6 +- .../recognizer/repository/repository.py | 8 +-- server/modules/recognizer/service.py | 6 +- server/static/scripts.js | 2 +- server/templates/base.html | 2 +- uv.lock | 55 +++++++++++++++++++ 20 files changed, 196 insertions(+), 155 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6322c66..c29afc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] diff --git a/server/config/__init__.py b/server/config/__init__.py index edf2b1c..2ed1a49 100644 --- a/server/config/__init__.py +++ b/server/config/__init__.py @@ -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 diff --git a/server/infra/db/__init__.py b/server/infra/db/__init__.py index 6f9544f..5771c10 100644 --- a/server/infra/db/__init__.py +++ b/server/infra/db/__init__.py @@ -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", +] diff --git a/server/infra/db/pg.py b/server/infra/db/pg.py index 97b8512..3128a36 100644 --- a/server/infra/db/pg.py +++ b/server/infra/db/pg.py @@ -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) diff --git a/server/infra/logger/__init__.py b/server/infra/logger/__init__.py index 7b42bc2..233bba6 100644 --- a/server/infra/logger/__init__.py +++ b/server/infra/logger/__init__.py @@ -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, + }, }, } diff --git a/server/infra/tools/db/filter.py b/server/infra/tools/db/filter.py index 1f3d6d0..3ac0498 100644 --- a/server/infra/tools/db/filter.py +++ b/server/infra/tools/db/filter.py @@ -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: diff --git a/server/infra/web/__init__.py b/server/infra/web/__init__.py index 85b2a61..6e1fbe6 100644 --- a/server/infra/web/__init__.py +++ b/server/infra/web/__init__.py @@ -2,4 +2,4 @@ from server.infra.web.description import DescriptionController from server.infra.web.seo import SeoController from server.infra.web.recognizer import BreedsController -__all__ = ("DescriptionController", "SeoController", "BreedsController") \ No newline at end of file +__all__ = ("DescriptionController", "SeoController", "BreedsController") diff --git a/server/infra/web/description.py b/server/infra/web/description.py index 2f8094d..37d79bf 100644 --- a/server/infra/web/description.py +++ b/server/infra/web/description.py @@ -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]], }, ) - \ No newline at end of file diff --git a/server/infra/web/recognizer.py b/server/infra/web/recognizer.py index 1ca6ea7..2481bde 100644 --- a/server/infra/web/recognizer.py +++ b/server/infra/web/recognizer.py @@ -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) diff --git a/server/infra/web/seo.py b/server/infra/web/seo.py index 5d9ea1b..caf6566 100644 --- a/server/infra/web/seo.py +++ b/server/infra/web/seo.py @@ -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 = "" @@ -77,4 +59,4 @@ User-agent: * Allow: / Sitemap: https://xn-----6kcp3cadbabfh8a0a.xn--p1ai/sitemap.xml -""" \ No newline at end of file +""" diff --git a/server/main.py b/server/main.py index 97165a9..99cd273 100644 --- a/server/main.py +++ b/server/main.py @@ -3,62 +3,46 @@ 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( - debug=True, - route_handlers=[ - BreedsController, - DescriptionController, - SeoController, - create_static_files_router(path="/static", directories=["server/static"]), - ], - template_config=TemplateConfig( - directory=Path("server/templates"), - engine=JinjaTemplateEngine, - ), - ) +inject.configure(inject_config) +app = Litestar( + debug=True, + route_handlers=[ + BreedsController, + DescriptionController, + SeoController, + create_static_files_router(path="/static", directories=["server/static"]), + ], + template_config=TemplateConfig( + directory=Path("server/templates"), + engine=JinjaTemplateEngine, + ), +) diff --git a/server/modules/attachments/domains/attachments.py b/server/modules/attachments/domains/attachments.py index 165d281..0d79639 100644 --- a/server/modules/attachments/domains/attachments.py +++ b/server/modules/attachments/domains/attachments.py @@ -1,13 +1,14 @@ 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 + updated_at: datetime size: int storage_driver_name: str path: str diff --git a/server/modules/attachments/repositories/attachments.py b/server/modules/attachments/repositories/attachments.py index f78125e..69b235e 100644 --- a/server/modules/attachments/repositories/attachments.py +++ b/server/modules/attachments/repositories/attachments.py @@ -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 ) diff --git a/server/modules/attachments/services/attachment.py b/server/modules/attachments/services/attachment.py index 1c39625..2dccc61 100644 --- a/server/modules/attachments/services/attachment.py +++ b/server/modules/attachments/services/attachment.py @@ -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) diff --git a/server/modules/descriptions/repository/repository.py b/server/modules/descriptions/repository/repository.py index 7477529..aa7d51c 100644 --- a/server/modules/descriptions/repository/repository.py +++ b/server/modules/descriptions/repository/repository.py @@ -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] = [] # Идем по каждому текстовому файлу с описанием породы @@ -81,4 +81,4 @@ class PGCharactersRepository(ACharactersRepository): data = [b for b in breeds if b.alias == alias] if len(data) == 0: return None - return data[0] \ No newline at end of file + return data[0] diff --git a/server/modules/recognizer/repository/repository.py b/server/modules/recognizer/repository/repository.py index 45bf6f6..0be0291 100644 --- a/server/modules/recognizer/repository/repository.py +++ b/server/modules/recognizer/repository/repository.py @@ -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) diff --git a/server/modules/recognizer/service.py b/server/modules/recognizer/service.py index 54398c2..6d22305 100644 --- a/server/modules/recognizer/service.py +++ b/server/modules/recognizer/service.py @@ -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, diff --git a/server/static/scripts.js b/server/static/scripts.js index c923fcc..5d96922 100644 --- a/server/static/scripts.js +++ b/server/static/scripts.js @@ -33,7 +33,7 @@ async function SavePhoto(self) { // Обработка основных результатов for (let key in json.results) { if (json.description != undefined) { - text += `
${json.results[key]} (вероятность: ${Math.round(parseFloat(key) * 100)}%)
Описание
`; + text += `
${json.results[key]} (вероятность: ${Math.round(parseFloat(key) * 100)}%)
Описание
`; } else { text += `
${json.results[key]} (вероятность: ${Math.round(parseFloat(key) * 100)}%)
`; } diff --git a/server/templates/base.html b/server/templates/base.html index 7ecd5af..4bbb1af 100644 --- a/server/templates/base.html +++ b/server/templates/base.html @@ -45,6 +45,6 @@ {% block form %}{% endblock %} - + diff --git a/uv.lock b/uv.lock index 16a10aa..75bfcce 100644 --- a/uv.lock +++ b/uv.lock @@ -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"