style: ruff format и правки линтера на каркасе шаблона

Без смены поведения: форматирование, сортировка импортов, удаление неиспользуемых
импортов, __all__ в src/db, datetime.UTC, HTTPStatus.NOT_FOUND вместо 404,
аннотация lvl в logger, noqa F401 на проверке доступности raisa_fastapi_protected_api,
mypy: miniopy_async не подтягивается (follow_imports=skip).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-03 18:02:19 +04:00
parent bf63d3a9dc
commit 72415ab891
13 changed files with 69 additions and 37 deletions

View File

@ -1,12 +1,13 @@
import asyncio import asyncio
from logging.config import fileConfig from logging.config import fileConfig
from sqlalchemy import engine_from_config from sqlalchemy import engine_from_config, pool
from sqlalchemy import pool
from sqlalchemy.ext.asyncio import AsyncEngine from sqlalchemy.ext.asyncio import AsyncEngine
from alembic import context from alembic import context
from src.core.config import settings from src.core.config import settings
from src.models import Base # noqa: F401 — модели регистрируются в src/models/__init__.py from src.models import Base # noqa: F401 — модели регистрируются в src/models/__init__.py
# this is the Alembic Config object, which provides # this is the Alembic Config object, which provides
# access to the values within the .ini file in use. # access to the values within the .ini file in use.
config = context.config config = context.config
@ -28,9 +29,10 @@ target_metadata = Base.metadata
# my_important_option = config.get_main_option("my_important_option") # my_important_option = config.get_main_option("my_important_option")
# ... etc. # ... etc.
def include_name(name, type_, parent_names): def include_name(name, type_, parent_names):
if type_ == 'schema': if type_ == "schema":
return name == 'dkb' return name == "dkb"
return True return True
@ -41,12 +43,13 @@ def do_run_migrations(connection) -> None:
compare_type=True, compare_type=True,
version_table_schema="dkb", version_table_schema="dkb",
include_schemas=True, include_schemas=True,
include_name=include_name include_name=include_name,
) )
with context.begin_transaction(): with context.begin_transaction():
context.run_migrations() context.run_migrations()
def run_migrations_offline() -> None: def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode. """Run migrations in 'offline' mode.
@ -67,7 +70,7 @@ def run_migrations_offline() -> None:
dialect_opts={"paramstyle": "named"}, dialect_opts={"paramstyle": "named"},
version_table_schema="dkb", version_table_schema="dkb",
include_schemas=True, include_schemas=True,
include_name=include_name include_name=include_name,
) )
with context.begin_transaction(): with context.begin_transaction():
@ -83,10 +86,7 @@ async def run_migrations_online() -> None:
""" """
connectable = AsyncEngine( connectable = AsyncEngine(
engine_from_config( engine_from_config(
config.get_section(config.config_ini_section), config.get_section(config.config_ini_section), prefix="sqlalchemy.", poolclass=pool.NullPool, future=True
prefix="sqlalchemy.",
poolclass=pool.NullPool,
future=True
) )
) )

View File

@ -36,5 +36,11 @@ extra_checks = true
# Сторонние пакеты без типов (или недоступные вне банка) # Сторонние пакеты без типов (или недоступные вне банка)
[[tool.mypy.overrides]] [[tool.mypy.overrides]]
module = ["smbclient.*", "nkeys.*", "miniopy_async.*", "nats.*", "raisa_fastapi_protected_api", "raisa_fastapi_protected_api.*", "pydbml.*"] module = ["smbclient.*", "nkeys.*", "nats.*", "raisa_fastapi_protected_api", "raisa_fastapi_protected_api.*", "pydbml.*"]
ignore_missing_imports = true
# miniopy_async: пакет типизирован с ошибкой (Minio не виден mypy при реэкспорте) — типы не подтягиваем, Minio = Any
[[tool.mypy.overrides]]
module = ["miniopy_async", "miniopy_async.*"]
follow_imports = "skip"
ignore_missing_imports = true ignore_missing_imports = true

View File

@ -1,4 +1,5 @@
import uvicorn import uvicorn
from src import app, get_settings from src import app, get_settings
if __name__ == "__main__": if __name__ == "__main__":
@ -9,4 +10,3 @@ if __name__ == "__main__":
port=settings.server.PORT, port=settings.server.PORT,
lifespan="on", lifespan="on",
) )

View File

@ -2,6 +2,6 @@ from .app import app
from .core.config import get_settings from .core.config import get_settings
__all__ = [ __all__ = [
'app', "app",
'get_settings', "get_settings",
] ]

View File

@ -5,10 +5,12 @@ middleware (gzip, proxy headers, CORS, РАИСА при DEBUG=False), healthche
Что добавляет Срез 0: роутеры в src/api/router.py, outbox-паблишер и consumer recognition.result Что добавляет Срез 0: роутеры в src/api/router.py, outbox-паблишер и consumer recognition.result
(docs/context/03-architecture.md), зависимость require(permission). (docs/context/03-architecture.md), зависимость require(permission).
""" """
import base64 import base64
import os import os
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from datetime import datetime, timezone from datetime import UTC, datetime
from http import HTTPStatus
import nkeys import nkeys
import smbclient import smbclient
@ -30,7 +32,7 @@ from src.core.exception_handlers import register_exception_handlers
settings = get_settings() settings = get_settings()
VERSION = settings.VERSION VERSION = settings.VERSION
UP_TIME = datetime.now(timezone.utc) UP_TIME = datetime.now(UTC)
# --- NATS: аутентификация по nkeys/JWT --------------------------------------------------------- # --- NATS: аутентификация по nkeys/JWT ---------------------------------------------------------
@ -51,7 +53,7 @@ def sig_cb(nonce):
protected_middleware_available = False protected_middleware_available = False
if not settings.DEBUG: if not settings.DEBUG:
try: try:
from raisa_fastapi_protected_api import ( # pyright: ignore[reportMissingImports] from raisa_fastapi_protected_api import ( # noqa: F401 # pyright: ignore[reportMissingImports]
AuthorizationMiddleware, AuthorizationMiddleware,
OpenEndpoint, OpenEndpoint,
ProtectedOAuthSettings, ProtectedOAuthSettings,
@ -177,7 +179,7 @@ class SPAStaticFiles(StaticFiles):
try: try:
return await super().get_response(path, scope) return await super().get_response(path, scope)
except (HTTPException, StarletteHTTPException) as ex: except (HTTPException, StarletteHTTPException) as ex:
if ex.status_code == 404: if ex.status_code == HTTPStatus.NOT_FOUND:
if "images" in path: if "images" in path:
return await super().get_response(sep_static(path, "images"), scope) return await super().get_response(sep_static(path, "images"), scope)
if "static" in path: if "static" in path:

View File

@ -1,5 +1,6 @@
class NotFoundError(Exception): class NotFoundError(Exception):
pass pass
class CouldntCreateError(Exception): class CouldntCreateError(Exception):
pass pass

View File

@ -1,6 +1,7 @@
"""Клиенты инфраструктуры. Каркас: оставлен S3Storage; outbox-паблишер (PollWorker) реализуется в Срезе 0 """Клиенты инфраструктуры. Каркас: оставлен S3Storage; outbox-паблишер (PollWorker) реализуется в Срезе 0
по docs/context/03-architecture.md (референс — reference-old-arm/api/src/core/clients.py). по docs/context/03-architecture.md (референс — reference-old-arm/api/src/core/clients.py).
""" """
from io import BytesIO from io import BytesIO
from miniopy_async import Minio from miniopy_async import Minio
@ -10,11 +11,11 @@ class S3Storage:
def __init__(self, client: Minio, bucket_name: str): def __init__(self, client: Minio, bucket_name: str):
self.client = client self.client = client
self.bucket = bucket_name self.bucket = bucket_name
async def put_object(self, obj_name, file_data): async def put_object(self, obj_name, file_data):
try: try:
stream = BytesIO(file_data) stream = BytesIO(file_data)
await self.client.put_object(self.bucket, obj_name, stream, length=len(file_data)) await self.client.put_object(self.bucket, obj_name, stream, length=len(file_data))
except: except:
raise raise
@ -23,5 +24,5 @@ class S3Storage:
response = await self.client.get_object(self.bucket, obj_name) response = await self.client.get_object(self.bucket, obj_name)
# file_bytes = await response.content.read() # file_bytes = await response.content.read()
return response return response
except: except:
raise raise

View File

@ -1,42 +1,48 @@
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings
class DBSettings(BaseSettings): class DBSettings(BaseSettings):
DATABASE_URL: str DATABASE_URL: str
class MinioSettings(BaseSettings): class MinioSettings(BaseSettings):
MINIO_URL: str MINIO_URL: str
MINIO_ADMIN_LOGIN: str MINIO_ADMIN_LOGIN: str
MINIO_ADMIN_PASSWORD: str MINIO_ADMIN_PASSWORD: str
MINIO_BUCKET: str MINIO_BUCKET: str
class NATSSettings(BaseSettings): class NATSSettings(BaseSettings):
NATS_URL: str NATS_URL: str
NATS_SEED: str NATS_SEED: str
NATS_JWT: str NATS_JWT: str
class SMBSettings(BaseSettings): class SMBSettings(BaseSettings):
SMB_HOST: str SMB_HOST: str
SMB_PATH: str SMB_PATH: str
SMB_LOGIN: str SMB_LOGIN: str
SMB_PASSWORD: str SMB_PASSWORD: str
class OpenBAOSettings(BaseSettings): class OpenBAOSettings(BaseSettings):
DATABASE: DBSettings DATABASE: DBSettings
MINIO: MinioSettings MINIO: MinioSettings
NATS: NATSSettings NATS: NATSSettings
SMB: SMBSettings SMB: SMBSettings
class ServerSettings(BaseSettings): class ServerSettings(BaseSettings):
HOST: str = "0.0.0.0" HOST: str = "0.0.0.0"
PORT: int = 8000 PORT: int = 8000
class Settings(BaseSettings): class Settings(BaseSettings):
OPENBAO: OpenBAOSettings OPENBAO: OpenBAOSettings
DEBUG: bool = False DEBUG: bool = False
server: ServerSettings = ServerSettings() server: ServerSettings = ServerSettings()
ROOT_PATH: str = '/' ROOT_PATH: str = "/"
VERSION: str = '1.0.0' VERSION: str = "1.0.0"
class Config: class Config:
extra = "ignore" extra = "ignore"
@ -44,7 +50,9 @@ class Settings(BaseSettings):
env_nested_delimiter = "__" env_nested_delimiter = "__"
nested_model_default_partial_update = False nested_model_default_partial_update = False
def get_settings() -> Settings: def get_settings() -> Settings:
return Settings() return Settings()
settings = get_settings()
settings = get_settings()

View File

@ -5,7 +5,6 @@ import sys
from loguru import logger from loguru import logger
# Периодические пробы мониторинга/оркестратора и служебные doc-эндпоинты. # Периодические пробы мониторинга/оркестратора и служебные doc-эндпоинты.
NOISE_PATHS: tuple[str, ...] = ( NOISE_PATHS: tuple[str, ...] = (
"/healthcheck", "/healthcheck",
@ -22,8 +21,20 @@ _NOISE_MARKERS: tuple[str, ...] = ("/static/", "/static-swagger/")
# Расширения статических ассетов (картинки, стили, скрипты, шрифты). # Расширения статических ассетов (картинки, стили, скрипты, шрифты).
_STATIC_SUFFIXES: tuple[str, ...] = ( _STATIC_SUFFIXES: tuple[str, ...] = (
".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".webp", ".png",
".css", ".js", ".map", ".woff", ".woff2", ".ttf", ".eot", ".jpg",
".jpeg",
".gif",
".svg",
".ico",
".webp",
".css",
".js",
".map",
".woff",
".woff2",
".ttf",
".eot",
) )
@ -74,7 +85,7 @@ def setup_logger(
class InterceptHandler(logging.Handler): class InterceptHandler(logging.Handler):
def emit(self, record: logging.LogRecord) -> None: def emit(self, record: logging.LogRecord) -> None:
try: try:
lvl = logger.level(record.levelname).name lvl: str | int = logger.level(record.levelname).name
except ValueError: except ValueError:
lvl = record.levelno lvl = record.levelno

View File

@ -4,6 +4,7 @@
маппинг JSON из S3 в document/osv_entry/financial_statement_*) реализуется в Срезе 0–2 по маппинг JSON из S3 в document/osv_entry/financial_statement_*) реализуется в Срезе 0–2 по
docs/context/03-architecture.md. Для референса — reference-old-arm/api/src/core/nats_callback.py. docs/context/03-architecture.md. Для референса — reference-old-arm/api/src/core/nats_callback.py.
""" """
from loguru import logger from loguru import logger
from nats.aio.client import Client as NATS from nats.aio.client import Client as NATS

View File

@ -1 +1,3 @@
from .db import get_async_session from .db import get_async_session
__all__ = ["get_async_session"]

View File

@ -1,11 +1,11 @@
from contextlib import asynccontextmanager from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine, AsyncSession
from fastapi import Depends
# from fastapi_users.db import SQLAlchemyUserDatabase, SQLAlchemyBaseUserTableUUID # from fastapi_users.db import SQLAlchemyUserDatabase, SQLAlchemyBaseUserTableUUID
from src.core.config import settings from src.core.config import settings
engine = create_async_engine(settings.OPENBAO.DATABASE.DATABASE_URL, connect_args={"server_settings": {"search_path": 'dkb'}}) engine = create_async_engine(
settings.OPENBAO.DATABASE.DATABASE_URL, connect_args={"server_settings": {"search_path": "dkb"}}
)
async_session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) async_session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
@ -13,4 +13,3 @@ async_session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_
async def get_async_session(): async def get_async_session():
async with async_session_maker() as session: async with async_session_maker() as session:
yield session yield session

View File

@ -1,4 +1,5 @@
"""ORM-модели. Источник истины — docs/model/data-model-v5.13.dbml; заполняется в Срезе 0.""" """ORM-модели. Источник истины — docs/model/data-model-v5.13.dbml; заполняется в Срезе 0."""
from .base import Base, UUIDPrimaryMixin from .base import Base, UUIDPrimaryMixin
__all__ = ["Base", "UUIDPrimaryMixin"] __all__ = ["Base", "UUIDPrimaryMixin"]