Compare commits

...

5 Commits

Author SHA1 Message Date
artem cf23b04d3e s3
Gitea Actions Demo / build_and_push (push) Successful in 18m48s Details
2026-01-14 22:28:32 +03:00
artem e1ecf09470 rating
Gitea Actions Demo / build_and_push (push) Has been cancelled Details
2026-01-14 19:05:24 +03:00
artem e121c6af34 породы в БД
Gitea Actions Demo / build_and_push (push) Has been cancelled Details
2026-01-13 15:55:27 +03:00
artem ce5c715611 породы в БД 2026-01-13 15:54:58 +03:00
artem 6eb8cdff6e DB migration 2026-01-12 18:29:42 +03:00
65 changed files with 9739 additions and 293 deletions

View File

@ -1,5 +1,5 @@
api:
uv run granian --interface asgi server.main:app --host 0.0.0.0
alembic upgrade head && uv run granian --interface asgi server.main:app --host 0.0.0.0
dog-train:
uv run ml/dogs.py
@ -16,3 +16,9 @@ lint:
pipinstall:
uv pip sync requirements.txt
migrate-up:
alembic upgrade head
migration-generate:
git rev-parse --short HEAD | xargs -I {} alembic revision --autogenerate -m "{}"

116
alembic.ini Normal file
View File

@ -0,0 +1,116 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = server/migration
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
file_template = %%(year)d-%%(month).2d-%%(day).2d-%%(hour).2d%%(minute).2d-%%(rev)s-parent-commit-%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to migration/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:migration/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@ -1,11 +1,10 @@
import json
from PIL import ImageFile
import torch.nn as nn
from torchvision.datasets import ImageFolder # type: ignore
from PIL import ImageFile
from torch.utils.data import DataLoader
from train import get_labels, load_model, get_loaders, train, show, DEVICE # type: ignore
from torchvision.datasets import ImageFolder # type: ignore
from train import DEVICE, get_labels, get_loaders, load_model, show, train # type: ignore
ImageFile.LOAD_TRUNCATED_IMAGES = True

View File

@ -1,11 +1,10 @@
import json
from PIL import ImageFile
import torch.nn as nn
from torchvision.datasets import ImageFolder # type: ignore
from PIL import ImageFile
from torch.utils.data import DataLoader
from train import get_labels, load_model, get_loaders, train, show, DEVICE # type: ignore
from torchvision.datasets import ImageFolder # type: ignore
from train import DEVICE, get_labels, get_loaders, load_model, show, train # type: ignore
ImageFile.LOAD_TRUNCATED_IMAGES = True

View File

@ -1,12 +1,12 @@
import torch
from torchvision import transforms # type: ignore
import torch.nn.functional as F
from PIL import Image
import json
import torch
import torch.nn.functional as F
from PIL import Image
from torchvision import transforms # type: ignore
# Создание labels_dict для соответствия классов и индексов
with open("labels.json", "r") as f:
with open("labels.json") as f:
data_labels = f.read()
labels_dict = json.loads(data_labels)

View File

@ -1,13 +1,13 @@
import os
import matplotlib.pyplot as plt # type: ignore
import torch
import torch.nn as nn
from torchvision.datasets import ImageFolder # type: ignore
from torch.utils.data import Dataset, DataLoader, random_split
from torchvision import transforms # type: ignore
import torchvision
from torch.utils.data import DataLoader, Dataset, random_split
from torchvision import transforms # type: ignore
from torchvision.datasets import ImageFolder # type: ignore
from torchvision.models import ResNet50_Weights # type: ignore
from typing import Tuple
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
@ -29,7 +29,7 @@ def get_labels(input_dir, img_size):
return labels_dict, dataset
def get_loaders(dataset: Dataset) -> Tuple[DataLoader, DataLoader]:
def get_loaders(dataset: Dataset) -> tuple[DataLoader, DataLoader]:
# Разделение данных на тренировочные и валидационные
train_size = int(0.8 * float(len(dataset))) # type: ignore[arg-type]
val_size = len(dataset) - train_size # type: ignore[arg-type]
@ -61,7 +61,7 @@ def train(
model: nn.Module,
train_loader: DataLoader,
val_loader: DataLoader,
) -> Tuple[list[float], list[float], list[float], list[float]]:
) -> tuple[list[float], list[float], list[float], list[float]]:
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.fc.parameters(), lr=1e-4) # type: ignore[union-attr]
# История метрик
@ -96,9 +96,7 @@ def train(
train_acc = 100.0 * correct / total
train_loss_history.append(train_loss)
train_acc_history.append(train_acc)
print(
f"Epoch {epoch + 1}/{num_epochs}, Train Loss: {train_loss:.4f}, Train Accuracy: {train_acc:.2f}%"
)
print(f"Epoch {epoch + 1}/{num_epochs}, Train Loss: {train_loss:.4f}, Train Accuracy: {train_acc:.2f}%")
# Оценка на валидационных данных
model.eval()

View File

@ -24,11 +24,14 @@ dependencies = [
"sqlalchemy>=2.0.44",
"inject>=5.3.0",
"aiofiles>=25.1.0",
"botocore>=1.42.9",
"types-aiofiles>=25.1.0.20251011",
"betterconf>=4.5.0",
"dataclasses-ujson>=0.0.34",
"asyncpg>=0.31.0",
"alembic>=1.18.0",
"aioboto3>=15.0.0",
"python-magic>=0.4.27",
"psycopg2-binary>=2.9.11",
]
[project.optional-dependencies]
@ -43,3 +46,117 @@ default = [
"matplotlib>=3.10.1",
]
# MYPY
[tool.mypy]
exclude = [
".venv",
"venv",
"tmp",
"scripts",
"tests"
]
plugins = ["sqlalchemy.ext.mypy.plugin"]
mypy_path = "./stubs"
ignore_missing_imports = true
# RUFF
[tool.ruff]
target-version = "py312"
show-fixes = true
src = ["app"]
# Same as Black.
line-length = 120
indent-width = 4
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
"venv",
"stubs",
"scripts",
]
[tool.ruff.lint.isort]
known-first-party = ["app"]
[tool.ruff.format]
# Like Black, use double quotes for strings.
quote-style = "double"
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
[tool.ruff.lint.per-file-ignores]
"stubs/*" = ["F403"]
"server/migration/*" = ["E501", "F403"]
"server/config/__init__.py" = ["E501"]
"scripts/*" = ["T201", "E501"]
"server/admin/*" = ["E501", "E711"]
"vk_api/*"= ["T201", "C416", "A001", "E501"]
"ml/*"= ["T201", "C416", "A001", "E501", "C416", "N812"]
"tests/**/*.py" = [
"E501", "ASYNC230",
# at least this three should be fine in tests:
"S101", # asserts allowed in tests...
"S106", # Possible hardcoded password assigned to argument: "password"
"S110", # consider logging the exception
"ARG", # Unused function args -> fixtures nevertheless are functionally relevant...
"FBT", # Don't care about booleans as positional arguments in tests, e.g. via @pytest.mark.parametrize()
# The below are debateable
"PLR2004", # Magic value used in comparison, ...
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
"INP001", # File `...` is part of an implicit namespace package. Add an `__init__.py`.
"SLF001", # Private member accessed: `_...`
]
"tests/__init__.py" = ["I001"]
[tool.ruff.lint]
# https://docs.astral.sh/ruff/rules/
select = ["DTZ", "F", "C4", "B", "A", "E", "T", "I", "N", "UP", "ASYNC", "Q"]
ignore = ["E712", "B904", "B019", "C417"]
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.lint.mccabe]
# https://docs.astral.sh/ruff/settings/#mccabe
# Flag errors (`C901`) whenever the complexity level exceeds 5.
max-complexity = 12
[tool.ruff.lint.flake8-pytest-style]
# https://docs.astral.sh/ruff/settings/#flake8-pytest-style
fixture-parentheses = false
mark-parentheses = false

84
scripts/assets_fiil.py Normal file
View File

@ -0,0 +1,84 @@
import os
import random
import shutil
from PIL import Image
def copy_convert_to_webp(
source_dir,
dest_dir,
samples_per_folder=5,
size_threshold=1024 * 1024,
quality_high=90,
quality_low=70,
):
"""
Копирует структуру папок и конвертирует изображения в WebP
:param source_dir: Исходная директория с изображениями
:param dest_dir: Целевая директория для копирования
:param samples_per_folder: Количество изображений для выбора из каждой папки
:param size_threshold: Пороговый размер файла (в байтах) для сильного сжатия
:param quality_high: Качество для маленьких изображений (0-100)
:param quality_low: Качество для больших изображений (0-100)
"""
image_extensions = (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".webp")
def is_image_file(filename):
return filename.lower().endswith(image_extensions)
def convert_to_webp(src_path, dest_path, quality):
try:
with Image.open(src_path) as img:
# Сохраняем прозрачность для RGBA
if img.mode in ("RGBA", "LA"):
img = img.convert("RGBA")
img.save(dest_path, "WEBP", quality=quality, lossless=False, method=6)
else:
# Конвертируем в RGB для JPEG-подобных изображений
img = img.convert("RGB")
img.save(dest_path, "WEBP", quality=quality, method=6)
except Exception as e:
print(f"Error converting {src_path}: {e}")
return False
return True
# Создаем корневую целевую папку
os.makedirs(dest_dir, exist_ok=True)
for root, dirs, files in os.walk(source_dir):
rel_path = os.path.relpath(root, source_dir)
target_dir = os.path.join(dest_dir, rel_path)
os.makedirs(target_dir, exist_ok=True)
images = [f for f in files if is_image_file(f)]
if not images:
continue
selected = random.sample(images, min(samples_per_folder, len(images)))
for file in selected:
src_path = os.path.join(root, file)
base_name = os.path.splitext(file)[0]
dest_path = os.path.join(target_dir, f"{base_name}.webp")
# Определяем качество на основе размера
file_size = os.path.getsize(src_path)
quality = quality_low if file_size > size_threshold else quality_high
# Конвертируем в WebP
if not convert_to_webp(src_path, dest_path, quality):
# Если ошибка конвертации - копируем оригинал
shutil.copy2(src_path, dest_path)
print(f"Copied original file instead: {file}")
if __name__ == "__main__":
copy_convert_to_webp(
source_dir="assets",
dest_dir="webp_output",
samples_per_folder=5,
size_threshold=1 * 1024 * 20, # 1 MB
quality_high=70,
quality_low=70,
)

167
scripts/filldb.py Normal file
View File

@ -0,0 +1,167 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
import_beerds_sql.py
Скрипт читает файлы из каталогов:
- server/modules/descriptions/repository/breed_descriptions
- server/modules/descriptions/repository/breed_signs
и вместо непосредственной вставки генерирует PostgreSQLSQLфайл, который
можно применить через `psql -f <имя_файла.sql>`:
CREATE TABLE beerds.beerds (
id varchar NOT NULL,
name text NOT NULL,
descriptions text NOT NULL,
signs json NOT NULL,
alias text NOT NULL,
CONSTRAINT beerds_pkey PRIMARY KEY (id)
);
"""
import sys
import json
import hashlib
import pathlib
# ------------------------------------------------------------------
# 2⃣ Путь к каталогам (можно изменить под свой проект)
# ------------------------------------------------------------------
BASE_DIR = pathlib.Path(__file__).resolve().parent.parent
DESC_DIR = BASE_DIR / "server/modules/descriptions/repository/breed_descriptions"
SIGN_DIR = BASE_DIR / "server/modules/descriptions/repository/breed_signs"
print(f"DESC_DIR = {DESC_DIR}")
# ------------------------------------------------------------------
# 3⃣ Функции чтения файлов
# ------------------------------------------------------------------
def read_text_file(path: pathlib.Path) -> str:
"""Возвращает содержимое текстового файла (unicode)."""
with path.open("r", encoding="utf-8") as f:
return f.read().strip()
def read_json_file(path: pathlib.Path) -> dict:
"""Возвращает JSONобъект из файла."""
with path.open("r", encoding="utf-8") as f:
return json.load(f)
# ------------------------------------------------------------------
# 4⃣ Формируем словарь: имя → (описание, сигналы)
# ------------------------------------------------------------------
def build_breed_map(desc_dir: pathlib.Path, sign_dir: pathlib.Path) -> dict:
"""Возвращает dict: stem → (description, signs)"""
desc_files = {p.stem: p for p in desc_dir.glob("*.txt")}
sign_files = {p.stem: p for p in sign_dir.glob("*.txt")}
common_keys = desc_files.keys() & sign_files.keys()
if not common_keys:
print("⚠️ Нет общих файлов между каталогами описаний и сигналов.")
return {}
breeds = {}
for key in sorted(common_keys):
desc_path = desc_files[key]
sign_path = sign_files[key]
try:
description = read_text_file(desc_path)
except Exception as e:
print(f"Не удалось прочитать описание {desc_path}: {e}")
continue
try:
signs = read_json_file(sign_path)
except Exception as e:
print(f"Не удалось прочитать сигналы {sign_path}: {e}")
continue
breeds[key] = (description, signs)
return breeds
# ------------------------------------------------------------------
# 5⃣ Генерация id (если нужно уникальное)
# ------------------------------------------------------------------
def generate_id(name: str, alias: str) -> str:
"""Генерирует MD5hash из `name:alias`."""
key = f"{name}:{alias}"
return hashlib.md5(key.encode("utf-8")).hexdigest()
# ------------------------------------------------------------------
# 6⃣ Генерация SQLфайла
# ------------------------------------------------------------------
def write_sql_file(breeds: dict, out_path: pathlib.Path):
"""Создаёт один .sqlфайл, содержащий INSERTоперации для всех пород."""
if not breeds:
print("⚠️ Нет данных для генерации SQL.")
return
lines = []
header = (
"-- Автоматически сгенерированный скрипт вставки пород\n"
"-- Запустить: psql -f beerds_insert.sql\n\n"
"BEGIN;\n"
)
lines.append(header)
# Формируем один блок INSERT с несколькими строками VALUES
# 1. Столбцы
cols = "(id, name, descriptions, signs, alias)"
# 2. Генерируем строки VALUES
value_lines = []
for name, (description, signs) in breeds.items():
alias = name # можно изменить при необходимости
breed_id = generate_id(name, alias)
# Подготовка значений: экранирование апострофов и JSON
escaped_name = name.replace("_", " ").replace("'", "''")
escaped_alias = alias.replace("'", "''")
escaped_desc = description.replace("'", "''")
# JSON как строка (в PostgreSQL json можно передавать как текст)
json_str = json.dumps(signs, ensure_ascii=False).replace("'", "''")
value_line = f"('{breed_id}', '{escaped_name}', '{escaped_desc}', '{json_str}', '{escaped_alias}')"
value_lines.append(value_line)
# Объединяем VALUES через запятую
values_section = ",\n".join(value_lines)
insert_stmt = (
f"INSERT INTO beerds.beerds {cols}\n"
f"VALUES\n{values_section}\n"
f"ON CONFLICT (id) DO UPDATE\n"
f" SET name = EXCLUDED.name,\n"
f" descriptions = EXCLUDED.descriptions,\n"
f" signs = EXCLUDED.signs,\n"
f" alias = EXCLUDED.alias;\n"
)
lines.append(insert_stmt)
lines.append("\nCOMMIT;")
# Записываем в файл
out_path.write_text("\n".join(lines), encoding="utf-8")
print(f"✅ SQLфайл успешно сгенерирован: {out_path}")
# ------------------------------------------------------------------
# 7⃣ Основная точка входа
# ------------------------------------------------------------------
def main():
print("🔍 Читаем файлы…")
breeds_map = build_breed_map(DESC_DIR, SIGN_DIR)
if not breeds_map:
print("❌ Ничего не найдено. Завершение.")
sys.exit(1)
# Путь для вывода SQLфайла
sql_file_path = BASE_DIR / "beerds_insert.sql"
print(f"📝 Записываем INSERTоперации в файл: {sql_file_path}")
write_sql_file(breeds_map, sql_file_path)
print("🎉 Готово!")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,46 @@
import json
from pathlib import Path
def generate_folder_structure(root_path):
result = {}
# Поддерживаемые форматы изображений
image_ext = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff"}
# Основные категории (cat и dog)
for category in ["cat", "dog"]:
category_path = Path(root_path) / category
if not category_path.is_dir():
continue
category_dict = {}
# Обрабатываем подпапки внутри категории
for subfolder in category_path.iterdir():
if subfolder.is_dir():
# Собираем изображения
images = [
file.name for file in subfolder.iterdir() if file.is_file() and file.suffix.lower() in image_ext
]
category_dict[subfolder.name] = sorted(images)
result[category] = category_dict
return result
def save_to_json(data, output_file):
with open(output_file, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
if __name__ == "__main__":
# Настройки
ASSETS_DIR = "server/static/assets"
OUTPUT_JSON = "structure.json"
# Генерация структуры
structure = generate_folder_structure(ASSETS_DIR)
# Сохранение в файл
save_to_json(structure, OUTPUT_JSON)
print(f"JSON структура сохранена в файл: {OUTPUT_JSON}")

View File

@ -22,13 +22,11 @@ class AppConfig:
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")
db_search_path: str = field("DB_SEARCH_PATH", "beerds")
fs_local_mount_dir: str = field("FS_LOCAL_MOUNT_DIR", default="./tmp/files")
fs_local_mount_dir: str = field("FS_LOCAL_MOUNT_DIR", default="./files")
fs_s3_bucket: str = field("FS_S3_BUCKET", "")
fs_s3_access_key_id: str = field("FS_ACCESS_KEY_ID", "")
fs_s3_access_key: str = field("FS_SECRET_ACCESS_KEY", "")

View File

@ -1,14 +1,13 @@
from abc import ABCMeta, abstractmethod
from typing import Optional
class CacheRepository(metaclass=ABCMeta):
@abstractmethod
async def get(self, key: str) -> Optional[str]:
async def get(self, key: str) -> str | None:
pass
@abstractmethod
async def set(self, key: str, data: str, _exp_min: Optional[int] = None):
async def set(self, key: str, data: str, _exp_min: int | None = None):
pass
@abstractmethod
@ -23,10 +22,10 @@ class LocalCacheRepository(CacheRepository):
def __init__(self) -> None:
self._data = {}
async def get(self, key: str) -> Optional[str]:
async def get(self, key: str) -> str | None:
return self._data.get(key)
async def set(self, key: str, data: str, _exp_min: Optional[int] = None):
async def set(self, key: str, data: str, _exp_min: int | None = None):
self._data[key] = data
async def delete(self, key: str):

View File

@ -1,10 +1,12 @@
"""Abstract realiztion for DB"""
from typing import Any, AsyncContextManager, Awaitable, Callable, TypeAlias
from collections.abc import Awaitable, Callable
from contextlib import AbstractAsyncContextManager as AsyncContextManager
from typing import Any
from server.config import AppConfig
ExecuteFun: TypeAlias = Callable[[Any], Awaitable[None]]
type ExecuteFun = Callable[[Any], Awaitable[None]]
class ConnectError(Exception):

View File

@ -1,11 +1,7 @@
from typing import Type, TypeVar
from sqlalchemy.orm import registry
mapper_registry = registry()
DC = TypeVar("DC")
def dict_to_dataclass(data: dict, class_type: Type[DC]) -> DC:
def dict_to_dataclass[T](data: dict, class_type: type[T]) -> T:
return class_type(**data)

View File

@ -1,4 +1,5 @@
from typing import Any, AsyncContextManager
from contextlib import AbstractAsyncContextManager as AsyncContextManager
from typing import Any
from server.config import AppConfig
from server.infra.db.abc import AbstractDB, AbstractSession

View File

@ -39,3 +39,9 @@ class AsyncDB(AbstractDB):
def new_session(self):
return asyncio.async_sessionmaker(self.engine, expire_on_commit=False)()
def session_master(self):
return self.new_session()
def session_slave(self):
return self.new_session()

View File

@ -33,10 +33,10 @@ LOGGING_CONFIG = {
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
},
"sentry": {
"level": "ERROR",
"class": "sentry_sdk.integrations.logging.EventHandler",
},
# "sentry": {
# "level": "ERROR",
# "class": "sentry_sdk.integrations.logging.EventHandler",
# },
"uvicorn_default": {
"level": "INFO",
"formatter": "uvicorn_default",

View File

@ -1,7 +1,7 @@
from copy import copy
from dataclasses import Field, asdict, dataclass
from enum import Enum
from typing import Any, ClassVar, Optional, Protocol, assert_never
from typing import Any, ClassVar, Protocol, assert_never
from sqlalchemy import Select, and_, or_
@ -28,7 +28,7 @@ class FilterLeftField(Protocol):
class Filter:
right: Any
sign: FilterSign
left: Optional[FilterLeftField] = None
left: FilterLeftField | None = None
@staticmethod
def not_eq(f1: Any, f2: Any):
@ -79,33 +79,27 @@ class RestrictionField:
@dataclass
class QueryRestriction:
filters: Optional[list[Filter]] = None
limit: Optional[int] = None
offset: Optional[int] = None
sort: Optional[list[RestrictionField]] = None
filters: list[Filter] | None = None
limit: int | None = None
offset: int | None = None
sort: list[RestrictionField] | None = None
@dataclass(frozen=False)
class FilterQuery:
filters: list[Filter]
limit: Optional[int] = None
offset: Optional[int] = None
sort: Optional[list[RestrictionField]] = None
limit: int | None = None
offset: int | None = None
sort: list[RestrictionField] | None = None
@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, strict=True)])
@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)]
)
]
filters=[Filter.or_([Filter.eq(field, val) for field, val in zip(fields, values, strict=True)])]
)
@staticmethod
@ -119,7 +113,7 @@ class FilterQuery:
def add_and(self, field: object, value: Any):
self.filters.append(Filter.eq(field, value))
def add_query_restistions(self, q_restriction: Optional[QueryRestriction] = None):
def add_query_restistions(self, q_restriction: QueryRestriction | None = None):
if not q_restriction:
return None
if q_restriction.limit:
@ -137,9 +131,7 @@ 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") -> list[int] | None:
r_data: list[int] = []
for i, _ in enumerate(input_data):
if getattr(input_data[i], id_name) in values:
@ -149,9 +141,7 @@ async def indexes_by_id(
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] = []
@ -245,14 +235,10 @@ 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
@ -282,9 +268,7 @@ 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: dict | None = None) -> Select:
if f.limit:
q = q.limit(f.limit)
if f.offset:

View File

@ -1,5 +1,6 @@
from server.infra.web.description import DescriptionController
from server.infra.web.seo import SeoController
from server.infra.web.recognizer import BreedsController
from server.infra.web.seo import SeoController
from server.infra.web.vote import VoteController
__all__ = ("DescriptionController", "SeoController", "BreedsController")
__all__ = ("DescriptionController", "SeoController", "BreedsController", "VoteController")

View File

@ -34,9 +34,7 @@ class DescriptionController(Controller):
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}
)
return Template(template_name="dogs-characteristics.html", context={"breeds": breeds})
@get("/dogs-characteristics/{name:str}")
async def beer_description(self, name: str) -> Template:

View File

@ -1,10 +1,12 @@
from typing import Annotated
import inject
from litestar import (
Controller,
post,
)
from litestar.enums import RequestEncodingType
from litestar.datastructures import UploadFile
from litestar.enums import RequestEncodingType
from litestar.params import Body
from server.modules.recognizer import RecognizerService
@ -14,17 +16,15 @@ class BreedsController(Controller):
path = "/beerds"
@post("/dogs")
async def beerds_dogs(
self, data: UploadFile = Body(media_type=RequestEncodingType.MULTI_PART)
) -> dict:
async def beerds_dogs(self, data: Annotated[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)
result = await recognizer_service.predict_dog_image(body)
return result.to_serializable()
@post("/cats")
async def beerds_cats(
self, data: UploadFile = Body(media_type=RequestEncodingType.MULTI_PART)
) -> dict:
async def beerds_cats(self, data: Annotated[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)
result = await recognizer_service.predict_cat_image(body)
return result.to_serializable()

View File

@ -1,11 +1,11 @@
import inject
from litestar import (
Controller,
get,
MediaType,
get,
)
from server.modules.descriptions import CharactersService, Breed
from server.modules.descriptions import Breed, CharactersService
class SeoController(Controller):

39
server/infra/web/vote.py Normal file
View File

@ -0,0 +1,39 @@
from datetime import UTC, datetime
from uuid import uuid4
import inject
from litestar import (
Controller,
post,
)
from pydantic import BaseModel
from server.modules.descriptions import CharactersService
from server.modules.rate import Vote, VotesService
class VoteReq(BaseModel):
attachment_id: str
beerd_name: str
rate: int
def to_domain(self, name_id_convert: dict) -> Vote:
return Vote(
id=str(uuid4()),
attachment_id=self.attachment_id,
beerd_id=name_id_convert[self.beerd_name],
rate=self.rate,
created_at=datetime.now(UTC),
)
class VoteController(Controller):
path = "/votes"
@post("/do")
async def beerds_dogs(self, data: VoteReq) -> dict:
rate_service: VotesService = inject.instance(VotesService)
characters_service: CharactersService = inject.instance(CharactersService)
breeds = await characters_service.get_characters()
await rate_service.add_vote(data.to_domain({b.name: b.id for b in breeds}))
return {"success": True}

View File

@ -1,20 +1,22 @@
import asyncio
from pathlib import Path
import os
from pathlib import Path
import inject
from litestar import (
Litestar,
)
from litestar.contrib.jinja import JinjaTemplateEngine
from litestar.template.config import TemplateConfig
from litestar.static_files import create_static_files_router
from litestar.template.config import TemplateConfig
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, CharactersRepository
from server.modules.recognizer import RecognizerService, RecognizerRepository
from server.infra.web import BreedsController, DescriptionController, SeoController, VoteController
from server.modules.attachments import AtachmentService, DBAttachmentRepository, S3StorageDriver
from server.modules.descriptions import CharactersService, PGCharactersRepository
from server.modules.rate import PGVoteRepository, VotesService
from server.modules.recognizer import RecognizerRepository, RecognizerService
os.environ["CUDA_VISIBLE_DEVICES"] = "-1"
@ -28,8 +30,11 @@ def inject_config(binder: inject.Binder):
cnf = get_app_config()
db = AsyncDB(cnf)
loop.run_until_complete(db.connect())
binder.bind(RecognizerService, RecognizerService(RecognizerRepository()))
binder.bind(CharactersService, CharactersService(CharactersRepository()))
attach_service = AtachmentService(S3StorageDriver(cnf=cnf), DBAttachmentRepository(db))
binder.bind(RecognizerService, RecognizerService(RecognizerRepository(), attach_service))
binder.bind(CharactersService, CharactersService(PGCharactersRepository(db)))
binder.bind(VotesService, VotesService(PGVoteRepository(db)))
binder.bind(AtachmentService, attach_service)
inject.configure(inject_config)
@ -39,6 +44,7 @@ app = Litestar(
BreedsController,
DescriptionController,
SeoController,
VoteController,
create_static_files_router(path="/static", directories=["server/static"]),
],
template_config=TemplateConfig(

1
server/migration/README Normal file
View File

@ -0,0 +1 @@
Generic single-database configuration.

86
server/migration/env.py Normal file
View File

@ -0,0 +1,86 @@
from logging.config import fileConfig
from alembic import context
from server.config import get_app_config
from server.infra.db.db_mapper import mapper_registry
from server.modules.attachments.repository.models import *
from server.modules.descriptions.repository.models import *
from server.modules.rate.repository.models import *
from sqlalchemy import engine_from_config, pool
from sqlalchemy.ext.declarative import declarative_base
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
Base = declarative_base()
target_metadata = Base.metadata
for table_name, table in mapper_registry.metadata.tables.items():
target_metadata._add_table(table_name, table.schema, table)
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = "{}?options=-c search_path={}".format(
str(get_app_config().db_uri).replace("+asyncpg", ""),
get_app_config().db_search_path,
)
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
alemb_cnf = config.get_section(config.config_ini_section, {})
if not alemb_cnf["sqlalchemy.url"] or alemb_cnf["sqlalchemy.url"] == "driver://user:pass@localhost/dbname":
alemb_cnf["sqlalchemy.url"] = "{}?options=-c search_path={}".format(
str(get_app_config().db_uri).replace("+asyncpg", ""),
get_app_config().db_search_path,
)
connectable = engine_from_config(
alemb_cnf,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@ -0,0 +1,26 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,71 @@
"""a284498
Revision ID: 474b572b7fe2
Revises:
Create Date: 2026-01-12 18:28:37.783462
"""
import pathlib
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "474b572b7fe2"
down_revision: str | None = None
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"attachments",
sa.Column("id", sa.String(), nullable=False),
sa.Column("size", sa.BigInteger(), nullable=False),
sa.Column("storage_driver_name", sa.String(), nullable=False),
sa.Column("path", sa.String(), nullable=False),
sa.Column("media_type", sa.String(), nullable=False),
sa.Column("content_type", sa.String(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("is_deleted", sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"beerds",
sa.Column("id", sa.String(), nullable=False),
sa.Column("name", sa.Text(), nullable=False),
sa.Column("alias", sa.Text(), nullable=False),
sa.Column("descriptions", sa.Text(), nullable=False),
sa.Column("signs", sa.JSON(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"votes",
sa.Column("id", sa.String(), nullable=False),
sa.Column("attachment_id", sa.String(), nullable=False),
sa.Column("beerd_id", sa.String(), nullable=False),
sa.Column("rate", sa.BigInteger(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["attachment_id"], ["attachments.id"], name="votes_attachment_id_fk"),
sa.ForeignKeyConstraint(["beerd_id"], ["beerds.id"], name="votes_beerd_id_fk"),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###
with open(
pathlib.Path(__file__).resolve().parent / "dumps/beerds_insert.sql",
encoding="utf-8",
) as upgrade_file:
sql = upgrade_file.read()
op.execute(sql)
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("votes")
op.drop_table("beerds")
op.drop_table("attachments")
# ### end Alembic commands ###

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@ Working with media files - uploading, storing, receiving
"""
from server.modules.attachments.domains.attachments import Attachment
from server.modules.attachments.repositories.attachments import (
from server.modules.attachments.repository.attachments import (
AttachmentRepository,
DBAttachmentRepository,
MockAttachmentRepository,

View File

@ -13,6 +13,5 @@ class Attachment(UJsonMixin):
storage_driver_name: str
path: str
media_type: str
created_by: str
content_type: str
is_deleted: bool = False

View File

@ -1,19 +1,16 @@
from abc import ABCMeta, abstractmethod
from datetime import UTC, datetime
from typing import Tuple
from sqlalchemy import CursorResult, delete, insert, select, update
from server.config import get_app_config
from server.infra.db import AbstractDB, AbstractSession, AsyncDB, MockDB
from server.modules.attachments.domains.attachments import Attachment
from server.modules.attachments.repository.models 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
@ -89,9 +86,7 @@ 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)
@ -119,14 +114,12 @@ 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
)
attachment: list[Attachment] = []
result: CursorResult[Tuple[Attachment]] = await session.execute(q) # type: ignore
result: CursorResult[tuple[Attachment]] = await session.execute(q) # type: ignore
for d in result.all():
attachment.append(d[0])
return attachment

View File

@ -0,0 +1,48 @@
from dataclasses import dataclass, field
from datetime import UTC, datetime
from dataclasses_ujson.dataclasses_ujson import UJsonMixin # type: ignore
from sqlalchemy import BigInteger, Boolean, Column, DateTime, String
from server.config import get_app_config
from server.infra.db.db_mapper import mapper_registry
from server.modules.attachments.domains.attachments import Attachment as AttachmentModel
@mapper_registry.mapped
@dataclass
class Attachment(UJsonMixin):
__sa_dataclass_metadata_key__ = "sa"
__tablename__ = "attachments"
id: str = field(metadata={"sa": Column(String(), primary_key=True, nullable=False)})
size: int = field(metadata={"sa": Column(BigInteger(), nullable=False)})
storage_driver_name: str = field(metadata={"sa": Column(String(), nullable=False)})
path: str = field(metadata={"sa": Column(String(), nullable=False)})
media_type: str = field(metadata={"sa": Column(String(), nullable=False)})
content_type: str = field(metadata={"sa": Column(String(), nullable=False)})
created_at: datetime = field(
default=datetime.now(UTC),
metadata={"sa": Column(DateTime(timezone=True), nullable=False)},
)
updated_at: datetime = field(
default=datetime.now(UTC),
metadata={"sa": Column(DateTime(timezone=True), nullable=False)},
)
is_deleted: bool = field(default=False, metadata={"sa": Column(Boolean(), nullable=False, default=False)})
def __str__(self):
return f"{get_app_config().app_public_url}/api/v0/attachment/{self.id}.original.ext"
def to_domain(self) -> AttachmentModel:
return AttachmentModel(
id=self.id,
size=self.size,
media_type=self.media_type,
content_type=self.content_type,
created_at=self.created_at,
updated_at=self.updated_at,
storage_driver_name=self.storage_driver_name,
path=self.path,
)

View File

@ -1,12 +1,13 @@
import hashlib
import os.path
import uuid
from abc import ABCMeta, abstractmethod
from collections.abc import AsyncIterable, AsyncIterator
from datetime import UTC, datetime
from enum import Enum
from io import BytesIO
from pathlib import Path
from typing import Any, AsyncIterable, AsyncIterator, Optional
import uuid
from typing import Any
import aioboto3 # type: ignore
import aiofiles
@ -19,7 +20,8 @@ from server.config import AppConfig, get_app_config
from server.infra.db import AbstractSession
from server.infra.logger import get_logger
from server.modules.attachments.domains.attachments import Attachment
from server.modules.attachments.repositories.attachments import AttachmentRepository
from server.modules.attachments.repository.attachments import AttachmentRepository
from server.modules.attachments.repository.models import Attachment as AttachmentModel
class StorageDriversType(str, Enum):
@ -46,7 +48,7 @@ class StorageDriver(metaclass=ABCMeta):
pass
@abstractmethod
async def take(self, path: str) -> Optional[bytes]:
async def take(self, path: str) -> bytes | None:
pass
@abstractmethod
@ -78,7 +80,7 @@ class LocalStorageDriver(StorageDriver):
await f.write(data)
return str(path)
async def take(self, path: str) -> Optional[bytes]:
async def take(self, path: str) -> bytes | None:
if not os.path.isfile(path):
return None
async with aiofiles.open(path, "rb") as f:
@ -112,7 +114,7 @@ class MockStorageDriver(StorageDriver):
self._store[path] = data
return path
async def take(self, path: str) -> Optional[bytes]:
async def take(self, path: str) -> bytes | None:
return self._store.get(path)
async def delete(self, path: str):
@ -120,7 +122,7 @@ class MockStorageDriver(StorageDriver):
class S3StorageDriver(StorageDriver):
_prefix: str = "pvc-435e5137-052f-43b1-ace2-e350c9d50c76"
_prefix: str = "beerds"
def __init__(self, cnf: AppConfig) -> None:
self._chunk_size: int = 69 * 1024
@ -138,9 +140,7 @@ 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()
@ -176,12 +176,10 @@ 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]:
async def take(self, path: str) -> bytes | None:
buffer = BytesIO()
async for chunk in self.stream(path):
if chunk:
@ -191,9 +189,7 @@ 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
@ -243,30 +239,25 @@ class AtachmentService:
return parts.replace("/", "")
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)
}"
return f"{self._cnf.app_public_url}/api/v0/attachment/{attachment_id}.original.{self.extension(content_type)}"
async def create(self, file: bytes, user_id: str) -> Attachment:
async def create(self, file: bytes) -> Attachment:
path = await self._driver.put(file)
content_type = self.content_type(file)
attach = Attachment(
attach = AttachmentModel(
size=len(file),
storage_driver_name=str(self._driver.get_name()),
path=path,
media_type=self.media_type(content_type),
content_type=content_type,
created_by=user_id,
id=str(uuid.uuid4()),
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC),
)
await self._repository.create(attach)
return attach
return attach.to_domain()
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:
@ -277,17 +268,13 @@ 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) -> bytes | None:
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
@ -343,7 +330,5 @@ 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

@ -1,13 +1,11 @@
from server.modules.descriptions.repository import (
CharactersRepository,
ACharactersRepository,
)
from server.modules.descriptions.service import CharactersService
from server.modules.descriptions.domain import Breed
from server.modules.descriptions.repository import ACharactersRepository, CharactersRepository, PGCharactersRepository
from server.modules.descriptions.service import CharactersService
__all__ = (
"CharactersRepository",
"ACharactersRepository",
"CharactersService",
"PGCharactersRepository",
"Breed",
)

View File

@ -3,6 +3,7 @@ from dataclasses import dataclass
@dataclass(frozen=True)
class Breed:
id: str
name: str
alias: str
description: str

View File

@ -1,6 +1,7 @@
from server.modules.descriptions.repository.repository import (
CharactersRepository,
ACharactersRepository,
CharactersRepository,
PGCharactersRepository,
)
__all__ = ("CharactersRepository", "ACharactersRepository")
__all__ = ("CharactersRepository", "ACharactersRepository", "PGCharactersRepository")

View File

@ -1,6 +1,3 @@
```json
{
"people_friendly": true,
"child_friendly": true,
@ -13,5 +10,4 @@
"good_health": false,
"tolerates_loneliness": false,
"hypoallergenic": false
}
```
}

View File

@ -1,6 +1,3 @@
```json
{
"active": true,
"colors": ["blue", "gray", "black"],
@ -9,5 +6,4 @@
"smart": true,
"good_health": false,
"tolerates_loneliness": false
}
```
}

View File

@ -1,6 +1,3 @@
```json
{
"people_friendly": true,
"child_friendly": true,
@ -12,5 +9,4 @@
"good_health": false,
"tolerates_loneliness": false,
"hypoallergenic": false
}
```
}

View File

@ -1,6 +1,3 @@
```json
{
"people_friendly": true,
"child_friendly": true,
@ -16,5 +13,4 @@
"good_health": false,
"tolerates_loneliness": false,
"hypoallergenic": false
}
```
}

View File

@ -1,3 +0,0 @@
It seems like the text you've shared is repeated multiple times, possibly due to an error. Could you clarify your question or specify what you need help with? For example, are you looking for tips on dog care, training, nutrition, or something else? Let me know, and I'll provide a clear, concise response! 🐾

View File

@ -1,6 +1,3 @@
```json
{
"people_friendly": true,
"child_friendly": true,
@ -12,5 +9,4 @@
"good_health": false,
"tolerates_loneliness": false,
"hypoallergenic": false
}
```
}

View File

@ -1,16 +1,7 @@
The Entlebucher Mountain Dog exhibits the following traits based on the provided description:
- **Child-friendly**: Yes. The text explicitly states, "Прекрасно ладят с детьми" (Great with children).
- **High energy**: Yes. The breed requires daily walks, games, and activities, and lacks of stimulation may lead to destructive behavior.
- **Friendly**: Yes. The dog is described as "обычно дружелюбны" (usually friendly) with people and other animals, and it is noted that they are good with children.
Other attributes:
- **People-friendly**: Ambiguous. While the breed is friendly with people, the term "people-friendly" is not explicitly mentioned.
- **Dog-friendly**: Unclear. The text notes "обычно дружелюбны" (usually friendly) with other animals but mentions a hunting instinct toward small animals, which may not directly apply to dogs.
- **Low maintenance**: No. The breed requires regular grooming (brushing) and active socialization/training.
- **Hypoallergenic**: No. There is no mention of hypoallergenic traits.
**Final Answer**:
Child-friendly, High energy, Friendly.
{
"people_friendly": true,
"child_friendly": true,
"active": true,
"need_attentions": true,
"good_health": false
}

View File

@ -0,0 +1,19 @@
from dataclasses import dataclass, field
from dataclasses_ujson.dataclasses_ujson import UJsonMixin # type: ignore
from sqlalchemy import JSON, Column, String, Text
from server.infra.db.db_mapper import mapper_registry
@mapper_registry.mapped
@dataclass
class Beerds(UJsonMixin):
__sa_dataclass_metadata_key__ = "sa"
__tablename__ = "beerds"
id: str = field(metadata={"sa": Column(String(), primary_key=True, nullable=False)})
name: str = field(metadata={"sa": Column(Text(), nullable=False)})
alias: str = field(metadata={"sa": Column(Text(), nullable=False)})
descriptions: str = field(metadata={"sa": Column(Text(), nullable=False)})
signs: dict | None = field(default=None, metadata={"sa": Column(JSON(), nullable=False)})

View File

@ -1,10 +1,12 @@
from abc import ABCMeta, abstractmethod
from pathlib import Path
from aiocache import cached, Cache # type: ignore
from aiocache import Cache, cached # type: ignore
from sqlalchemy import select
from server.infra.db import AsyncDB
from server.modules.descriptions.domain import Breed
from server.modules.descriptions.repository import models
class ACharactersRepository(metaclass=ABCMeta):
@ -12,6 +14,7 @@ class ACharactersRepository(metaclass=ABCMeta):
async def get_characters(self) -> list[Breed]:
pass
@abstractmethod
async def get_character(self, alias: str) -> Breed | None:
pass
@ -28,11 +31,10 @@ class CharactersRepository(ACharactersRepository):
# Идем по каждому текстовому файлу с описанием породы
for breed_file in breed_dir.glob("*.txt"):
breed_name = breed_file.stem # имя файла без расширения - название породы
description = breed_file.read_text(
encoding="utf-8"
) # читаем описание из файла
description = breed_file.read_text(encoding="utf-8") # читаем описание из файла
breeds.append(
Breed(
id=breed_name,
name=breed_name.replace("_", " "),
alias=breed_file.stem,
description=description.strip(),
@ -55,30 +57,71 @@ class PGCharactersRepository(ACharactersRepository):
def __init__(self, db: AsyncDB):
self._db = db
@cached(ttl=60, cache=Cache.MEMORY)
# ───────────────────────────────────────────────────────────────────── #
# 8⃣ Кешируемый метод, который возвращает **все** породы
# ───────────────────────────────────────────────────────────────────── #
@cached(ttl=60, cache=Cache.MEMORY) # 1мин. кеш
async def get_characters(self) -> list[Breed]:
breed_dir = Path("server/modules/descriptions/repository/breed_descriptions")
breeds: list[Breed] = []
"""
Читает данные из таблицы `beerds.beerds` и преобразует каждую строку
в экземпляр `Breed`. Поле `signs` игнорируется в `Breed` его нет.
"""
# Идем по каждому текстовому файлу с описанием породы
for breed_file in breed_dir.glob("*.txt"):
breed_name = breed_file.stem # имя файла без расширения - название породы
description = breed_file.read_text(
encoding="utf-8"
) # читаем описание из файла
breeds.append(
Breed(
name=breed_name.replace("_", " "),
alias=breed_file.stem,
description=description.strip(),
)
async with self._db.async_session() as session:
# Писем SELECTзапрос (получаем все строки)
stmt = select( # type: ignore
models.Beerds.id,
models.Beerds.name,
models.Beerds.alias,
models.Beerds.descriptions,
)
breeds.sort(key=lambda b: b.name)
result = await session.execute(stmt)
rows = result.fetchall()
# Конвертируем в Breed
breeds: list[Breed] = [
Breed(
id=str(row.id),
name=row.name.strip(),
alias=row.alias.strip(),
description=row.descriptions.strip(),
)
for row in rows
]
# Сортируем по имени, как было в файле‑реализации
breeds.sort(key=lambda b: b.name.lower())
return breeds
# ───────────────────────────────────────────────────────────────────── #
# 9⃣ Получить конкретную породу по псевдониму
# ───────────────────────────────────────────────────────────────────── #
async def get_character(self, alias: str) -> Breed | None:
breeds = await self.get_characters()
data = [b for b in breeds if b.alias == alias]
if len(data) == 0:
"""
Быстрый запрос без получения всех пород. Если результат
пустой возвращаем `None`.
"""
async with self._db.async_session() as session:
stmt = (
select( # type: ignore
models.Beerds.id,
models.Beerds.name,
models.Beerds.alias,
models.Beerds.descriptions,
)
.where(models.Beerds.alias == alias)
.limit(1)
)
result = await session.execute(stmt)
row = result.fetchone()
if row is None: # pragma: no cover
return None
return data[0]
return Breed(
id=str(row.id),
name=row.name.strip(),
alias=row.alias.strip(),
description=row.descriptions.strip(),
)

View File

@ -0,0 +1,10 @@
from server.modules.rate.domain import Vote
from server.modules.rate.repository import AVoteRepository, PGVoteRepository
from server.modules.rate.service import VotesService
__all__ = (
"VotesService",
"AVoteRepository",
"PGVoteRepository",
"Vote",
)

View File

@ -0,0 +1,11 @@
from dataclasses import dataclass
from datetime import datetime
@dataclass(frozen=True)
class Vote:
id: str
attachment_id: str
beerd_id: str
rate: int
created_at: datetime

View File

@ -0,0 +1,13 @@
from server.modules.rate.domain import Vote
from server.modules.rate.repository.models import Vote as VoteModel
from server.modules.rate.repository.repository import AVoteRepository, PGVoteRepository
from server.modules.rate.service import VotesService
__all__ = (
"Vote",
"PGVoteRepository",
"ACharactersRepository",
"AVoteRepository",
"VotesService",
"VoteModel",
)

View File

@ -0,0 +1,48 @@
from dataclasses import dataclass, field
from datetime import UTC, datetime
from dataclasses_ujson.dataclasses_ujson import UJsonMixin # type: ignore
from sqlalchemy import (
BigInteger,
Column,
DateTime,
ForeignKeyConstraint,
String,
)
from server.config import get_app_config
from server.infra.db.db_mapper import mapper_registry
from server.modules.rate import domain
@mapper_registry.mapped
@dataclass
class Vote(UJsonMixin):
__sa_dataclass_metadata_key__ = "sa"
__tablename__ = "votes"
__table_args__ = (
ForeignKeyConstraint(["attachment_id"], ["attachments.id"], "votes_attachment_id_fk"),
ForeignKeyConstraint(["beerd_id"], ["beerds.id"], "votes_beerd_id_fk"),
)
id: str = field(metadata={"sa": Column(String(), primary_key=True, nullable=False)})
attachment_id: str = field(metadata={"sa": Column(String(), nullable=False)})
beerd_id: str = field(metadata={"sa": Column(String(), nullable=False)})
rate: int = field(metadata={"sa": Column(BigInteger(), nullable=False)})
created_at: datetime = field(
default=datetime.now(UTC),
metadata={"sa": Column(DateTime(timezone=True), nullable=False)},
)
def __str__(self):
return f"{get_app_config().app_public_url}/api/v0/attachment/{self.id}.original.ext"
@staticmethod
def from_domain(d: domain.Vote) -> "Vote":
return Vote(
id=d.id,
attachment_id=d.attachment_id,
beerd_id=d.beerd_id,
rate=d.rate,
created_at=d.created_at,
)

View File

@ -0,0 +1,81 @@
"""
Repository for working with the :class:`~server.modules.rate.repository.models.Vote` model.
Only the writeonly operation adding a vote is required in the current
application logic. The repository keeps the database interaction
encapsulated and provides an asynchronous API that can be used by the
service layer (or any other consumer).
The implementation uses the same pattern that is already present in the
``CharactersRepository`` an async session manager from
``server.infra.db.AsyncDB`` and an interface that makes unittesting
straightforward.
"""
from abc import ABCMeta, abstractmethod
from server.infra.db import AsyncDB
from server.modules.rate.repository import models
# --------------------------------------------------------------------------- #
# 1⃣ Base interface
# --------------------------------------------------------------------------- #
class AVoteRepository(metaclass=ABCMeta):
"""
Abstract repository that declares the contract for working with votes.
At the moment only one operation is required inserting a new vote.
"""
@abstractmethod
async def add_vote(self, vote: models.Vote) -> None:
"""Persist ``vote`` into the database."""
raise NotImplementedError
# --------------------------------------------------------------------------- #
# 2⃣ Concrete PostgreSQL implementation
# --------------------------------------------------------------------------- #
class PGVoteRepository(AVoteRepository):
"""
PostgreSQL implementation of :class:`AVoteRepository`.
The repository is intentionally *minimal* only an ``add_vote`` method
is exposed. The rest of the CRUD operations are expected to be added
later if/when the business logic grows.
"""
_db: AsyncDB
def __init__(self, db: AsyncDB):
self._db = db
# --------------------------------------------------------------------- #
# 2.1 Add a vote
# --------------------------------------------------------------------- #
async def add_vote(self, vote: models.Vote) -> None:
"""
Insert ``vote`` into the ``votes`` table.
Parameters
----------
vote:
Instance of :class:`~server.modules.rate.repository.models.Vote`
containing all necessary fields (``id``, ``attachment_id``,
``beerd_id``, ``rate`` and ``created_at``). The instance
is a *mapped* dataclass, therefore SQLAlchemy can persist it
directly.
Notes
-----
* No explicit caching the operation mutates the database and
should never be served from a cache.
* The method is deliberately ``async`` because it uses
``AsyncDB.async_session``.
"""
async with self._db.async_session() as session:
# We use the *instance* directly SQLAlchemy will handle the
# mapping for us. ``insert`` could also be used, but the
# instancebased approach keeps typechecking and IDE
# autocompletion in sync with the dataclass.
session.add(vote)
await session.commit()

View File

@ -0,0 +1,12 @@
from server.modules.rate.domain import Vote
from server.modules.rate.repository import AVoteRepository, VoteModel
class VotesService:
__slots__ = ("_repository",)
def __init__(self, repository: AVoteRepository):
self._repository = repository
async def add_vote(self, vote: Vote):
return await self._repository.add_vote(VoteModel.from_domain(vote))

View File

@ -1,6 +1,6 @@
from server.modules.recognizer.repository import (
RecognizerRepository,
ARecognizerRepository,
RecognizerRepository,
)
from server.modules.recognizer.service import RecognizerService

View File

@ -1,6 +1,6 @@
from server.modules.recognizer.repository.repository import (
RecognizerRepository,
ARecognizerRepository,
RecognizerRepository,
)
__all__ = ("RecognizerRepository", "ARecognizerRepository")

View File

@ -1,8 +1,8 @@
from abc import ABCMeta, abstractmethod
from functools import lru_cache
from aiocache import cached, Cache # type: ignore
import ujson
from aiocache import Cache, cached # type: ignore
class ARecognizerRepository(metaclass=ABCMeta):
@ -29,26 +29,22 @@ class RecognizerRepository(ARecognizerRepository):
@cached(ttl=60, cache=Cache.MEMORY)
async def images_dogs(self) -> dict:
with open("server/modules/recognizer/repository/meta/images.json", "r") as f:
with open("server/modules/recognizer/repository/meta/images.json") as f: # noqa: ASYNC230
return ujson.loads(f.read())["dog"]
@cached(ttl=60, cache=Cache.MEMORY)
async def images_cats(self) -> dict:
with open("server/modules/recognizer/repository/meta/images.json", "r") as f:
with open("server/modules/recognizer/repository/meta/images.json") as f: # noqa: ASYNC230
return ujson.loads(f.read())["cat"]
@lru_cache
def labels_cats(self) -> dict:
with open(
"server/modules/recognizer/repository/meta/labels_cats.json", "r"
) as f:
with open("server/modules/recognizer/repository/meta/labels_cats.json") as f: # noqa: ASYNC230
data_labels = f.read()
return ujson.loads(data_labels)
@lru_cache
def labels_dogs(self) -> dict:
with open(
"server/modules/recognizer/repository/meta/labels_dogs.json", "r"
) as f:
with open("server/modules/recognizer/repository/meta/labels_dogs.json") as f: # noqa: ASYNC230
data_labels = f.read()
return ujson.loads(data_labels)

View File

@ -1,16 +1,18 @@
from typing import NewType, Any
import os
import io
import os
from dataclasses import dataclass
from typing import Any, NewType, Protocol
from dataclasses_ujson.dataclasses_ujson import UJsonMixin # type: ignore
from PIL import Image
os.environ["CUDA_VISIBLE_DEVICES"] = "-1"
import torch
from torchvision import transforms # type: ignore
from server.modules.attachments.domains.attachments import Attachment
from server.modules.recognizer.repository import ARecognizerRepository
TorchModel = NewType("TorchModel", torch.nn.Module)
@ -24,11 +26,31 @@ DOG_MODEL = load_model("server/models/dogs_model.pth")
CAT_MODEL = load_model("server/models/cats_model.pth")
class RecognizerService:
__slots__ = "_repository"
class AttachmentService(Protocol):
async def create(self, file: bytes) -> Attachment:
pass
def __init__(self, repository: ARecognizerRepository):
@dataclass
class ResultImages(UJsonMixin):
name: str
url: list[str]
@dataclass
class RecognizerResult(UJsonMixin):
results: dict
images: list
description: dict[str, list] | None
uploaded_attach_id: str | None
class RecognizerService:
__slots__ = ("_repository", "_attachment_service")
def __init__(self, repository: ARecognizerRepository, attachment_service: AttachmentService):
self._repository = repository
self._attachment_service = attachment_service
async def images_cats(self) -> dict:
return await self._repository.images_cats()
@ -36,7 +58,8 @@ class RecognizerService:
async def images_dogs(self) -> dict:
return await self._repository.images_dogs()
async def predict_dog_image(self, image: bytes) -> dict:
async def predict_dog_image(self, image: bytes) -> RecognizerResult:
attachment = await self._attachment_service.create(image)
predicted_data = self._predict(image, DOG_MODEL)
results = {}
images = []
@ -44,50 +67,36 @@ class RecognizerService:
images_dogs = await self._repository.images_dogs()
for d in predicted_data:
predicted_idx, probabilities = d
predicted_label = self._repository.labels_dogs()[str(predicted_idx)]
predicted_label: str = self._repository.labels_dogs()[str(predicted_idx)]
name = predicted_label.replace("_", " ")
images.append(
{
"name": name,
"url": [
f"/static/assets/dog/{predicted_label}/{i}"
for i in images_dogs[predicted_label]
],
}
)
description.setdefault(name, []).append(
f"/dogs-characteristics/{name.replace(' ', '_')}"
ResultImages(
name=name, url=[f"/static/assets/dog/{predicted_label}/{i}" for i in images_dogs[predicted_label]]
)
)
description.setdefault(name, []).append(f"/dogs-characteristics/{name.replace(' ', '_')}")
results[probabilities] = name
return {
"results": results,
"images": images,
"description": description,
}
return RecognizerResult(
results=results, images=images, description=description, uploaded_attach_id=attachment.id
)
async def predict_cat_image(self, image: bytes) -> dict:
async def predict_cat_image(self, image: bytes) -> RecognizerResult:
attachment = await self._attachment_service.create(image)
predicted_data = self._predict(image, CAT_MODEL)
results = {}
images = []
images_cats = await self._repository.images_cats()
for d in predicted_data:
predicted_idx, probabilities = d
predicted_label = self._repository.labels_cats()[str(predicted_idx)]
predicted_label: str = self._repository.labels_cats()[str(predicted_idx)]
name = predicted_label.replace("_", " ")
images.append(
{
"name": name,
"url": [
f"/static/assets/cat/{predicted_label}/{i}"
for i in images_cats[predicted_label]
],
}
ResultImages(
name=name, url=[f"/static/assets/cat/{predicted_label}/{i}" for i in images_cats[predicted_label]]
)
)
results[probabilities] = name
return {
"results": results,
"images": images,
}
return RecognizerResult(results=results, images=images, description=None, uploaded_attach_id=attachment.id)
def _predict(self, image: bytes, model, device="cpu") -> list[Any]:
img_size = (224, 224)
@ -99,9 +108,7 @@ class RecognizerService:
]
)
input_tensor = preprocess(Image.open(io.BytesIO(image)))
input_batch = input_tensor.unsqueeze(0).to(
device
) # Добавляем dimension для батча
input_batch = input_tensor.unsqueeze(0).to(device) # Добавляем dimension для батча
with torch.no_grad():
output = model(input_batch)
@ -112,7 +119,5 @@ class RecognizerService:
predicted_data = []
for i in range(k):
predicted_data.append(
(predicted_idx[i].item(), float(topk_probs[i].item()))
)
predicted_data.append((predicted_idx[i].item(), float(topk_probs[i].item())))
return predicted_data

View File

@ -1,6 +1,9 @@
let urlCreator = window.URL || window.webkitURL;
let current_attachment_id = 0;
let current_beerd_name = [];
async function SavePhoto(self) {
document.getElementById("result").innerHTML = "";
let photo = document.getElementById("file-input").files[0];
@ -10,6 +13,8 @@ async function SavePhoto(self) {
if (response.ok) {
let json = await response.json();
current_attachment_id = json.uploaded_attach_id;
let text = "<h3 class='image-results'>Результаты</h3>";
let uniqChecker = {};
@ -32,6 +37,7 @@ async function SavePhoto(self) {
// Обработка основных результатов
for (let key in json.results) {
current_beerd_name.push(json.results[key]);
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>`;
} else {
@ -51,7 +57,6 @@ async function SavePhoto(self) {
text += "</div>";
uniqChecker[json.results[key]] = key;
}
// Обработка дополнительных результатов
for (let key in json.results_net) {
if (uniqChecker[json.results_net[key]] !== undefined) continue;
@ -70,6 +75,9 @@ async function SavePhoto(self) {
}
document.getElementById("result").innerHTML = text;
document.querySelector(".star-rating").style.display = "block";
document.getElementById("rate-stars").style.display = "block";
document.getElementById("feedback").textContent = "";
setTimeout(function(){
window.scrollBy({
top: 300,
@ -181,4 +189,90 @@ function openModal(imgElement) {
}
function closeModal() {
document.getElementById('modal').style.display = "none";
}
}
/* ────────────────────────────────────────────────────── */
/* 3⃣ Скрипт: выбор звезды, отправка POSTзапроса */
/* ────────────────────────────────────────────────────── */
(() => {
const ratingContainer = document.querySelector('.star-rating');
const stars = Array.from(ratingContainer.querySelectorAll('.star'));
const feedback = document.getElementById('feedback');
let currentRating = 0; // 0 = пока не выбрано
// Установить визуальное состояние (цвет, ariachecked)
function setRating(value) {
currentRating = value;
stars.forEach(star => {
const starValue = Number(star.dataset.value);
const isSelected = starValue <= value;
star.classList.toggle('selected', isSelected);
star.setAttribute('aria-checked', isSelected);
});
}
// Отправка POST /vote
async function sendVote(value) {
// Подготовьте объектpayload. Добавьте idтокены, если нужны.
const payload = {
rate: value,
attachment_id: current_attachment_id,
beerd_name: current_beerd_name[0],
};
try {
const resp = await fetch('/votes/do', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
credentials: 'include' // если ваш API использует cookies
});
if (!resp.ok) {
throw new Error(`Ошибка ${resp.status}`);
}
feedback.textContent = 'Спасибо за оценку!';
feedback.style.color = '#080';
document.getElementById("rate-stars").style.display = "none";
} catch (err) {
console.error(err);
feedback.textContent = 'Произошла ошибка, попробуйте позже.';
feedback.style.color = '#a00';
}
}
// Обработчики клика и клавиатуры
stars.forEach(star => {
// Клик мышкой
star.addEventListener('click', () => {
const value = Number(star.dataset.value);
setRating(value);
sendVote(value);
});
// Навигация клавиатурой: пробел/Enter выбирает звезду
star.addEventListener('keydown', e => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
const value = Number(star.dataset.value);
setRating(value);
sendVote(value);
}
});
// При наведении меняем состояние всех звёзд до текущего
star.addEventListener('mouseover', () => {
setRating(Number(star.dataset.value));
});
star.addEventListener('mouseout', () => {
setRating(currentRating); // вернём к сохранённому
});
});
// По умолчанию не показываем выбранную оценку
setRating(0);
})();

View File

@ -339,4 +339,34 @@ input[type="text"]:hover {
color: white;
margin-top: 10px;
font-size: 18px;
}
}
/* ────────────────────────────────────────────────────── */
/* 1⃣ Стили для звездочек */
/* ────────────────────────────────────────────────────── */
.star-rating {
display: none;
}
.star-rating span {
display: inline-flex;
gap: .2rem; /* отступы между звёздами */
font-size: 2rem; /* размер звёзд */
user-select: none; /* отключаем выделение текста */
}
.star {
cursor: pointer;
color: #f0f0f0; /* белый (светло‑серый) фон */
transition: color .15s ease;/* плавное изменение цвета */
}
.star.selected,
.star:hover,
.star:focus {
color: #ffd700; /* золотой при наведении/выборе */
}
.star:focus-visible { /* обводка для клавиатурного фокуса */
outline: 2px solid #333;
outline-offset: 2px;
}

View File

@ -8,7 +8,7 @@
{% block meta %}{% endblock %}
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="/static/styles.css?v=4">
<link rel="stylesheet" href="/static/styles.css?v=5">
<!-- Yandex.Metrika counter -->
<script type="text/javascript" >
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
@ -45,6 +45,6 @@
{% block form %}{% endblock %}
</body>
</section>
<script src="/static/scripts.js?v=8"></script>
<script src="/static/scripts.js?v=6"></script>
</html>

View File

@ -19,6 +19,20 @@
<div id="upload-image">
<div id="upload-image-text"></div>
<img id="image" style="max-width: 200px;"/>
<!-- 2⃣ Контейнер со звёздочками -->
<div class="star-rating" role="radiogroup" aria-label="Оценка от 1 до 5">
<h2>Оцените результат</h2>
<div id="rate-stars">
<!-- 5 звёзд каждая имеет data-value и tabindex="0" для клавиатуры -->
<span class="star" role="radio" aria-checked="false" data-value="1" tabindex="0" aria-label="1 звезда">&#9733;</span>
<span class="star" role="radio" aria-checked="false" data-value="2" tabindex="0" aria-label="2 звезды">&#9733;</span>
<span class="star" role="radio" aria-checked="false" data-value="3" tabindex="0" aria-label="3 звезды">&#9733;</span>
<span class="star" role="radio" aria-checked="false" data-value="4" tabindex="0" aria-label="4 звезды">&#9733;</span>
<span class="star" role="radio" aria-checked="false" data-value="5" tabindex="0" aria-label="5 звёзд">&#9733;</span>
</div>
</div>
<p id="feedback" style="margin-top:.5rem;"></p>
</div>
<div id="result"></div>

509
uv.lock
View File

@ -12,11 +12,12 @@ name = "ai"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "aioboto3" },
{ name = "aiocache" },
{ name = "aiofiles" },
{ name = "alembic" },
{ name = "asyncpg" },
{ name = "betterconf" },
{ name = "botocore" },
{ name = "dataclasses-ujson" },
{ name = "granian" },
{ name = "inject" },
@ -26,7 +27,9 @@ dependencies = [
{ name = "mypy" },
{ name = "numpy" },
{ name = "pillow" },
{ name = "psycopg2-binary" },
{ name = "pydantic" },
{ name = "python-magic" },
{ name = "ruff" },
{ name = "sqlalchemy" },
{ name = "torch" },
@ -52,11 +55,12 @@ default = [
[package.metadata]
requires-dist = [
{ name = "aioboto3", specifier = ">=15.0.0" },
{ name = "aiocache" },
{ name = "aiofiles", specifier = ">=25.1.0" },
{ name = "alembic", specifier = ">=1.18.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" },
@ -68,8 +72,10 @@ requires-dist = [
{ name = "mypy", marker = "extra == 'default'", specifier = ">=1.18" },
{ name = "numpy", specifier = "==2.3.4" },
{ name = "pillow", specifier = ">=11.1.0" },
{ name = "psycopg2-binary", specifier = ">=2.9.11" },
{ name = "pydantic", specifier = ">=2.12.4" },
{ name = "pyqt5", marker = "extra == 'default'", specifier = ">=5.15.11" },
{ name = "python-magic", specifier = ">=0.4.27" },
{ name = "requests", marker = "extra == 'default'", specifier = ">=2.32.3" },
{ name = "ruff", specifier = ">=0.14.5" },
{ name = "ruff", marker = "extra == 'default'", specifier = ">=0.11.5" },
@ -87,6 +93,42 @@ requires-dist = [
]
provides-extras = ["default"]
[[package]]
name = "aioboto3"
version = "15.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiobotocore", extra = ["boto3"] },
{ name = "aiofiles" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a2/01/92e9ab00f36e2899315f49eefcd5b4685fbb19016c7f19a9edf06da80bb0/aioboto3-15.5.0.tar.gz", hash = "sha256:ea8d8787d315594842fbfcf2c4dce3bac2ad61be275bc8584b2ce9a3402a6979", size = 255069 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/3e/e8f5b665bca646d43b916763c901e00a07e40f7746c9128bdc912a089424/aioboto3-15.5.0-py3-none-any.whl", hash = "sha256:cc880c4d6a8481dd7e05da89f41c384dbd841454fc1998ae25ca9c39201437a6", size = 35913 },
]
[[package]]
name = "aiobotocore"
version = "2.25.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
{ name = "aioitertools" },
{ name = "botocore" },
{ name = "jmespath" },
{ name = "multidict" },
{ name = "python-dateutil" },
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/62/94/2e4ec48cf1abb89971cb2612d86f979a6240520f0a659b53a43116d344dc/aiobotocore-2.25.1.tar.gz", hash = "sha256:ea9be739bfd7ece8864f072ec99bb9ed5c7e78ebb2b0b15f29781fbe02daedbc", size = 120560 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/2a/d275ec4ce5cd0096665043995a7d76f5d0524853c76a3d04656de49f8808/aiobotocore-2.25.1-py3-none-any.whl", hash = "sha256:eb6daebe3cbef5b39a0bb2a97cffbe9c7cb46b2fcc399ad141f369f3c2134b1f", size = 86039 },
]
[package.optional-dependencies]
boto3 = [
{ name = "boto3" },
]
[[package]]
name = "aiocache"
version = "0.12.3"
@ -105,6 +147,118 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668 },
]
[[package]]
name = "aiohappyeyeballs"
version = "2.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 },
]
[[package]]
name = "aiohttp"
version = "3.13.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
{ name = "aiosignal" },
{ name = "attrs" },
{ name = "frozenlist" },
{ name = "multidict" },
{ name = "propcache" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190 },
{ url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783 },
{ url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704 },
{ url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652 },
{ url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014 },
{ url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777 },
{ url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276 },
{ url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131 },
{ url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863 },
{ url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793 },
{ url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676 },
{ url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217 },
{ url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303 },
{ url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673 },
{ url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120 },
{ url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383 },
{ url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899 },
{ url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238 },
{ url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292 },
{ url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021 },
{ url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263 },
{ url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107 },
{ url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196 },
{ url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591 },
{ url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277 },
{ url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575 },
{ url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455 },
{ url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417 },
{ url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968 },
{ url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690 },
{ url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390 },
{ url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188 },
{ url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126 },
{ url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128 },
{ url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512 },
{ url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444 },
{ url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798 },
{ url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835 },
{ url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486 },
{ url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951 },
{ url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001 },
{ url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246 },
{ url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131 },
{ url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196 },
{ url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841 },
{ url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193 },
{ url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979 },
{ url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193 },
{ url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801 },
{ url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523 },
{ url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694 },
]
[[package]]
name = "aioitertools"
version = "0.13.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c", size = 19322 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182 },
]
[[package]]
name = "aiosignal"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "frozenlist" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490 },
]
[[package]]
name = "alembic"
version = "1.18.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mako" },
{ name = "sqlalchemy" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/70/a5/57f989c26c078567a08f1d88c337acfcb69c8c9cac6876a34054f35b8112/alembic-1.18.0.tar.gz", hash = "sha256:0c4c03c927dc54d4c56821bdcc988652f4f63bf7b9017fd9d78d63f09fd22b48", size = 2043788 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/fd/68773667babd452fb48f974c4c1f6e6852c6e41bcf622c745faca1b06605/alembic-1.18.0-py3-none-any.whl", hash = "sha256:3993fcfbc371aa80cdcf13f928b7da21b1c9f783c914f03c3c6375f58efd9250", size = 260967 },
]
[[package]]
name = "annotated-types"
version = "0.7.0"
@ -159,6 +313,15 @@ wheels = [
{ 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 = "attrs"
version = "25.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615 },
]
[[package]]
name = "betterconf"
version = "4.5.0"
@ -168,18 +331,32 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/18/3c/86921e4e407f25413819e04471606c32377f47887af7873e0613f5036946/betterconf-4.5.0-py3-none-any.whl", hash = "sha256:ee6ad8ae4c49a7f977555dcb435eb258eb044e10a2f43fdd77bb67b8ff682118", size = 11769 },
]
[[package]]
name = "boto3"
version = "1.40.61"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
{ name = "jmespath" },
{ name = "s3transfer" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ed/f9/6ef8feb52c3cce5ec3967a535a6114b57ac7949fd166b0f3090c2b06e4e5/boto3-1.40.61.tar.gz", hash = "sha256:d6c56277251adf6c2bdd25249feae625abe4966831676689ff23b4694dea5b12", size = 111535 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/24/3bf865b07d15fea85b63504856e137029b6acbc73762496064219cdb265d/boto3-1.40.61-py3-none-any.whl", hash = "sha256:6b9c57b2a922b5d8c17766e29ed792586a818098efe84def27c8f582b33f898c", size = 139321 },
]
[[package]]
name = "botocore"
version = "1.42.9"
version = "1.40.61"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jmespath" },
{ name = "python-dateutil" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fd/f3/2d2cfb500e2dc00b0e33e3c8743306e6330f3cf219d19e9260dab2f3d6c2/botocore-1.42.9.tar.gz", hash = "sha256:74f69bfd116cc7c8215481284957eecdb48580e071dd50cb8c64356a866abd8c", size = 14861916 }
sdist = { url = "https://files.pythonhosted.org/packages/28/a3/81d3a47c2dbfd76f185d3b894f2ad01a75096c006a2dd91f237dca182188/botocore-1.40.61.tar.gz", hash = "sha256:a2487ad69b090f9cccd64cf07c7021cd80ee9c0655ad974f87045b02f3ef52cd", size = 14393956 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/2a/e9275f40042f7a09915c4be86b092cb02dc4bd74e77ab8864f485d998af1/botocore-1.42.9-py3-none-any.whl", hash = "sha256:f99ba2ca34e24c4ebec150376c815646970753c032eb84f230874b2975a185a8", size = 14537810 },
{ url = "https://files.pythonhosted.org/packages/38/c5/f6ce561004db45f0b847c2cd9b19c67c6bf348a82018a48cb718be6b58b0/botocore-1.40.61-py3-none-any.whl", hash = "sha256:17ebae412692fd4824f99cde0f08d50126dc97954008e5ba2b522eb049238aa7", size = 14055973 },
]
[[package]]
@ -381,6 +558,79 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175 },
]
[[package]]
name = "frozenlist"
version = "1.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717 },
{ url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651 },
{ url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417 },
{ url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391 },
{ url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048 },
{ url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549 },
{ url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833 },
{ url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363 },
{ url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314 },
{ url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365 },
{ url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763 },
{ url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110 },
{ url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717 },
{ url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628 },
{ url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882 },
{ url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676 },
{ url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235 },
{ url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742 },
{ url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725 },
{ url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533 },
{ url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506 },
{ url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161 },
{ url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676 },
{ url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638 },
{ url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067 },
{ url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101 },
{ url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901 },
{ url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395 },
{ url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659 },
{ url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492 },
{ url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034 },
{ url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749 },
{ url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127 },
{ url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698 },
{ url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749 },
{ url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298 },
{ url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015 },
{ url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038 },
{ url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130 },
{ url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845 },
{ url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131 },
{ url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542 },
{ url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308 },
{ url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210 },
{ url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972 },
{ url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536 },
{ url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330 },
{ url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627 },
{ url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238 },
{ url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738 },
{ url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739 },
{ url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186 },
{ url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196 },
{ url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830 },
{ url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289 },
{ url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318 },
{ url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814 },
{ url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762 },
{ url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470 },
{ url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042 },
{ url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148 },
{ url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676 },
{ url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451 },
{ url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507 },
{ url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409 },
]
[[package]]
name = "fsspec"
version = "2025.10.0"
@ -635,6 +885,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f2/24/8d99982f0aa9c1cd82073c6232b54a0dbe6797c7d63c0583a6c68ee3ddf2/litestar_htmx-0.5.0-py3-none-any.whl", hash = "sha256:92833aa47e0d0e868d2a7dbfab75261f124f4b83d4f9ad12b57b9a68f86c50e6", size = 9970 },
]
[[package]]
name = "mako"
version = "1.3.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509 },
]
[[package]]
name = "markdown"
version = "3.10"
@ -1197,6 +1459,105 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/11/87/6cd69c43e86bc5863f93dba3c9fb42ee4fda36a0fe5aa3cbd25001fb26f8/polyfactory-2.22.5-py3-none-any.whl", hash = "sha256:822d1af463520153200b4b62b06b0dc73a1d5edc8911e2e63ed0757ae21cc2b3", size = 63934 },
]
[[package]]
name = "propcache"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750 },
{ url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780 },
{ url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308 },
{ url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182 },
{ url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215 },
{ url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112 },
{ url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442 },
{ url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398 },
{ url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920 },
{ url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748 },
{ url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877 },
{ url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437 },
{ url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586 },
{ url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790 },
{ url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158 },
{ url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451 },
{ url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374 },
{ url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396 },
{ url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950 },
{ url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856 },
{ url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420 },
{ url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254 },
{ url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205 },
{ url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873 },
{ url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739 },
{ url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514 },
{ url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781 },
{ url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396 },
{ url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897 },
{ url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789 },
{ url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152 },
{ url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869 },
{ url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596 },
{ url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981 },
{ url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490 },
{ url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371 },
{ url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424 },
{ url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566 },
{ url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130 },
{ url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625 },
{ url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209 },
{ url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797 },
{ url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140 },
{ url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257 },
{ url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097 },
{ url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455 },
{ url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372 },
{ url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411 },
{ url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712 },
{ url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557 },
{ url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015 },
{ url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880 },
{ url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938 },
{ url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641 },
{ url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510 },
{ url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161 },
{ url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393 },
{ url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546 },
{ url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259 },
{ url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428 },
{ url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305 },
]
[[package]]
name = "psycopg2-binary"
version = "2.9.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572 },
{ url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529 },
{ url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242 },
{ url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258 },
{ url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295 },
{ url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133 },
{ url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383 },
{ url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168 },
{ url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712 },
{ url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549 },
{ url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215 },
{ url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567 },
{ url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755 },
{ url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646 },
{ url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701 },
{ url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293 },
{ url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184 },
{ url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650 },
{ url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663 },
{ url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737 },
{ url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643 },
{ url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913 },
]
[[package]]
name = "pydantic"
version = "2.12.4"
@ -1338,6 +1699,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
]
[[package]]
name = "python-magic"
version = "0.4.27"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/da/db/0b3e28ac047452d079d375ec6798bf76a036a08182dbb39ed38116a49130/python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b", size = 14677 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6c/73/9f872cb81fc5c3bb48f7227872c28975f998f3e7c2b1c16e95e6432bbb90/python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3", size = 13840 },
]
[[package]]
name = "pyyaml"
version = "6.0.3"
@ -1442,6 +1812,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/80/69756670caedcf3b9be597a6e12276a6cf6197076eb62aad0c608f8efce0/ruff-0.14.5-py3-none-win_arm64.whl", hash = "sha256:4b700459d4649e2594b31f20a9de33bc7c19976d4746d8d0798ad959621d64a4", size = 13433331 },
]
[[package]]
name = "s3transfer"
version = "0.14.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
]
sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712 },
]
[[package]]
name = "setuptools"
version = "80.9.0"
@ -1720,3 +2102,120 @@ sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef468
wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109 },
]
[[package]]
name = "wrapt"
version = "1.17.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003 },
{ url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025 },
{ url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108 },
{ url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072 },
{ url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214 },
{ url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105 },
{ url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766 },
{ url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711 },
{ url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885 },
{ url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896 },
{ url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132 },
{ url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091 },
{ url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172 },
{ url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163 },
{ url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963 },
{ url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945 },
{ url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857 },
{ url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178 },
{ url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310 },
{ url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266 },
{ url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544 },
{ url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283 },
{ url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366 },
{ url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571 },
{ url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094 },
{ url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659 },
{ url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946 },
{ url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717 },
{ url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334 },
{ url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471 },
{ url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591 },
]
[[package]]
name = "yarl"
version = "1.22.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "multidict" },
{ name = "propcache" },
]
sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980 },
{ url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424 },
{ url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821 },
{ url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243 },
{ url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361 },
{ url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036 },
{ url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671 },
{ url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059 },
{ url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356 },
{ url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331 },
{ url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590 },
{ url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316 },
{ url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431 },
{ url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555 },
{ url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965 },
{ url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205 },
{ url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209 },
{ url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966 },
{ url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312 },
{ url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967 },
{ url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949 },
{ url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818 },
{ url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626 },
{ url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129 },
{ url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776 },
{ url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879 },
{ url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996 },
{ url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047 },
{ url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947 },
{ url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943 },
{ url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715 },
{ url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857 },
{ url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520 },
{ url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504 },
{ url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282 },
{ url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080 },
{ url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696 },
{ url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121 },
{ url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080 },
{ url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661 },
{ url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645 },
{ url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361 },
{ url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451 },
{ url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814 },
{ url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799 },
{ url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990 },
{ url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292 },
{ url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888 },
{ url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223 },
{ url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981 },
{ url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303 },
{ url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820 },
{ url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203 },
{ url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173 },
{ url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562 },
{ url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828 },
{ url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551 },
{ url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512 },
{ url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400 },
{ url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140 },
{ url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473 },
{ url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056 },
{ url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292 },
{ url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171 },
{ url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814 },
]

View File

@ -1,5 +1,6 @@
import os
import time
import requests # type: ignore
# Получить токен чтобы:
@ -12,9 +13,7 @@ group_id = 220240483
dir = "../assets/dog"
list_labels = [fname for fname in os.listdir(dir)]
r = requests.get(
f"{VK_URL}photos.getAll{postfix}&access_token={TOKEN}&owner_id=-{group_id}&count=200"
)
r = requests.get(f"{VK_URL}photos.getAll{postfix}&access_token={TOKEN}&owner_id=-{group_id}&count=200")
if "error" in r.json():
print("error", r.json())
exit()