From c5d5e243c15271074be1ec0418bf0e14c9dd2300 Mon Sep 17 00:00:00 2001 From: Azamat Date: Thu, 26 Mar 2026 21:56:10 +0300 Subject: [PATCH] [feat] add guides --- .gitignore | 4 + README.md | 85 +++++++++++++ docs/CLEAN_ARCHITECTURE_RU.md | 227 ++++++++++++++++++++++++++++++++++ docs/CODESTYLE.md | 83 +++++++++++++ docs/OBSERVABILITY_RU.md | 178 ++++++++++++++++++++++++++ docs/PROJECT_GUIDE_RU.md | 216 ++++++++++++++++++++++++++++++++ uv.lock | 86 ++++++------- 7 files changed, 836 insertions(+), 43 deletions(-) create mode 100644 README.md create mode 100644 docs/CLEAN_ARCHITECTURE_RU.md create mode 100644 docs/CODESTYLE.md create mode 100644 docs/OBSERVABILITY_RU.md create mode 100644 docs/PROJECT_GUIDE_RU.md diff --git a/.gitignore b/.gitignore index ec0991a..b304a23 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ wheels/ # Virtual environments .venv + +!docs +!AGENTS.md +!tasks.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..2339337 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +Это шаблон Python-сервиса на чистой архитектуре с заменяемым web-слоем, типизированным конфигом, явным dependency wiring и observability через порты. + +## Что это за проект + +- Небольшой референсный сервис со слоями `domain/`, `usecase/`, `repository/` и `adapter/` +- Шаблон для сервисов на FastAPI, где FastAPI остается только во внешнем HTTP adapter +- Проект, где конфиг собирается из `config/app.yaml`, `.env` и env vars в одно дерево dataclass-конфигов +- Проект, где repository и usecase создаются один раз на старте приложения в composition root +- Проект, где логи, метрики и трейсы скрыты за интерфейсами и могут работать через `stdout`, файл или OpenTelemetry runtime + +## Основные идеи + +- Clean Architecture и границы SOLID +- Направление зависимостей только внутрь +- Тонкие adapter-слои и явная сборка зависимостей +- Заменяемый HTTP-слой +- Observability без протекания OpenTelemetry во внутренние слои + +## Быстрый старт + +```bash +make install +APP_API_TOKEN=local-api-token APP_SIGNING_KEY=local-signing-key make run +``` + +Приложение стартует на `http://0.0.0.0:8123` и публикует versioned API под `/api/v1`. + +## Документация + +### Гайды + +- [Правила проекта и ограничения для агента](AGENTS.md) +- [Кодстайл проекта для AI-агента](docs/CODESTYLE.md) +- [Чистая архитектура, SOLID, DIP, Protocol и repository](docs/CLEAN_ARCHITECTURE_RU.md) +- [Логи, метрики и трейсы в этом проекте](docs/OBSERVABILITY_RU.md) +- [Как чистая архитектура реализована здесь](docs/PROJECT_GUIDE_RU.md) +- [План задач и история работ](tasks.md) + +### ADR + +- [001 Composition Root and Lifetimes](docs/001-composition-root-and-lifetimes.md) +- [002 Config From YAML and Env](docs/002-config-yaml-plus-env.md) +- [003 Observability Via Interfaces](docs/003-observability-via-interfaces.md) +- [004 Versioned HTTP API](docs/004-versioned-http-api.md) +- [005 Early FastAPI OTel Instrumentation](docs/005-fastapi-otel-early-instrumentation.md) + +## Структура проекта + +- `domain/` - core-сущности и доменные ошибки +- `usecase/` - прикладные сценарии и порты +- `repository/` - реализации repository +- `adapter/config/` - загрузка и модели типизированного конфига +- `adapter/observability/` - выбор runtime для logger, metrics и tracer +- `adapter/otel/` - OpenTelemetry adapters +- `adapter/di/` - composition root и singleton wiring +- `adapter/http/fastapi/` - HTTP-схемы, dependencies, middleware и routers +- `config/` - YAML-конфиг приложения и локального OTel collector + +## Для ИИ + +Если ты AI-агент и собираешься что-то менять в проекте, сначала прочитай документы в таком порядке: + +1. [Правила проекта и ограничения агента](AGENTS.md) - обязательные правила работы в этом репозитории +2. [Кодстайл проекта для AI-агента](docs/CODESTYLE.md) - границы слоев, стиль кода и правила зависимостей +3. [Как чистая архитектура реализована здесь](docs/PROJECT_GUIDE_RU.md) - практическая карта проекта и типовые сценарии изменений +4. [Чистая архитектура, SOLID, DIP, Protocol и repository](docs/CLEAN_ARCHITECTURE_RU.md) - базовые архитектурные принципы и примеры +5. [Логи, метрики и трейсы в этом проекте](docs/OBSERVABILITY_RU.md) - читать перед любыми изменениями в observability, middleware и runtime wiring +6. [ADR в `docs/`](docs/001-composition-root-and-lifetimes.md) - читать релевантные решения перед изменением архитектуры или startup wiring +7. [План задач и история работ](tasks.md) - понять, что уже сделано, что отложено и какие ограничения были зафиксированы + +Перед началом работы: + +- Определи, в каком слое будет изменение: `domain/`, `usecase/`, `repository/` или `adapter/` +- Убедись, что зависимости идут только внутрь +- Не тащи FastAPI и OpenTelemetry во внутренние слои +- Сначала изучи существующий код в нужной директории, потом вноси изменения +- Если задача затрагивает архитектурное решение, сначала сверяйся с ADR и проектными правилами + +## Запуск и команды + +- Для локального запуска нужны `APP_API_TOKEN` и `APP_SIGNING_KEY` +- `make run` запускает приложение локально +- `make run-otel` запускает приложение с локальными OTel endpoints из env vars +- `make pre-commit` запускает `ruff`, `mypy` и `pytest` +- `make compose-up` поднимает приложение и локальный LGTM stack через Docker Compose diff --git a/docs/CLEAN_ARCHITECTURE_RU.md b/docs/CLEAN_ARCHITECTURE_RU.md new file mode 100644 index 0000000..27a354b --- /dev/null +++ b/docs/CLEAN_ARCHITECTURE_RU.md @@ -0,0 +1,227 @@ +# Чистая архитектура, SOLID и контракты + +## Что такое чистая архитектура + +Чистая архитектура - это способ строить проект слоями так, чтобы бизнес-правила жили отдельно от фреймворков, транспорта, базы данных и внешних SDK. + +Главная идея: + +- самые важные правила лежат ближе к центру +- внешние детали зависят от внутренних правил +- смена FastAPI, базы или OTel не должна ломать `domain/` и `usecase/` + +В этом проекте центр выглядит так: + +- `domain/` - сущности и доменные ошибки +- `usecase/` - сценарии приложения и интерфейсы +- `repository/` - реализации хранилищ +- `adapter/` - HTTP, конфиг, DI, observability + +## SOLID коротко + +### S - Single Responsibility Principle + +Один класс делает одну работу. + +Хорошо: + +- `UserService` только исполняет сценарий CRUD пользователя +- `InMemoryUserRepository` только хранит и читает пользователей +- `StdoutLogger` только пишет логи в stdout + +Плохо: + +- use case читает env +- router сам работает с базой +- repository знает про HTTP-статусы + +### O - Open/Closed Principle + +Код должен расширяться без переписывания ядра. + +Пример: можно добавить `PostgresUserRepository`, не меняя `GetUser`, если обе реализации соблюдают один и тот же контракт `UserRepository`. + +### L - Liskov Substitution Principle + +Любая реализация интерфейса должна корректно подменять другую. + +Если use case ожидает `UserRepository`, ему должно быть все равно, это in-memory репозиторий, PostgreSQL или HTTP client repository. + +### I - Interface Segregation Principle + +Интерфейсы должны быть небольшими и точными. + +Лучше так: + +```py +class UserRepository(Protocol): + def get(self, user_id: str) -> User | None: ... + def get_by_email(self, email: str) -> User | None: ... + def save(self, user: User) -> None: ... +``` + +Чем так: + +```py +class MegaRepository(Protocol): + def get_user(self, user_id: str) -> User | None: ... + def save_user(self, user: User) -> None: ... + def send_email(self, message: str) -> None: ... + def collect_metric(self, name: str) -> None: ... +``` + +### D - Dependency Inversion Principle + +Высокоуровневый код не зависит от низкоуровневых деталей. + +Use case не должен зависеть от конкретного `InMemoryUserRepository`, `FastAPI`, `OTLPMetricExporter` или `logging.Logger`. +Он зависит только от абстракций. + +## Как работает инверсия зависимостей + +Смысл DIP здесь такой: + +1. use case объявляет, что ему нужно +2. это описывается через `Protocol` +3. внешняя реализация удовлетворяет протоколу +4. composition root связывает use case с реализацией + +Пример: + +```py +class UserRepository(Protocol): + def get(self, user_id: str) -> User | None: ... + + +class GetUser: + def __init__(self, repository: UserRepository, logger: Logger, tracer: Tracer) -> None: + self._repository = repository + self._logger = logger + self._tracer = tracer +``` + +`GetUser` знает только о контракте `UserRepository`. +Реализация уже подключается снаружи, в `adapter/di/container.py`. + +## Что такое Protocol в Python + +`Protocol` - это способ описать интерфейс через набор методов без жесткого наследования. + +Если класс имеет нужные методы, он уже подходит под протокол. + +Это удобно для чистой архитектуры, потому что: + +- use case задает контракт рядом с местом использования +- внешние слои могут реализовать контракт без сильной связки +- тестировать проще + +Пример логгера: + +```py +class Logger(Protocol): + def info(self, message: str, attrs: Attrs | None = None) -> None: ... + def warning(self, message: str, attrs: Attrs | None = None) -> None: ... + def error(self, message: str, attrs: Attrs | None = None) -> None: ... +``` + +Пример SRP через протоколы: + +- `Logger` отвечает только за логирование +- `Metrics` только за счетчики и гистограммы +- `Tracer` только за спаны + +## Архитектура слоев в проекте + +### `domain/` + +Здесь лежат самые базовые вещи: + +- сущности +- value objects, если появятся +- доменные ошибки + +Пример: + +- `domain/user.py` - сущность `User` +- `domain/error.py` - `DomainError`, `UserNotFoundError`, `UserConflictError` + +`domain/` не должен знать ни про FastAPI, ни про OTel, ни про YAML, ни про env. + +### `usecase/` + +Здесь лежат прикладные сценарии. + +Use case: + +- принимает простые входные данные +- работает через интерфейсы +- возвращает доменные сущности или прикладные результаты +- не знает про HTTP и инфраструктуру + +Пример: + +- `usecase/user.py` -> `GetUser` +- `usecase/interface.py` -> `UserRepository`, `Logger`, `Metrics`, `Tracer` + +### `repository/` + +Здесь живут реализации контрактов доступа к данным. + +Пример из проекта: + +```py +class InMemoryUserRepository(UserRepository): + def __init__(self, tracer: Tracer, users: Iterable[User] | None = None) -> None: + self._users = {user.id: user for user in users or ()} + self._tracer = tracer +``` + +Что важно: + +- repository может зависеть от `domain/` и `usecase/` +- repository не должен тянуть HTTP детали в use case +- repository может быть заменен на PostgreSQL, Redis, gRPC client, REST client + +Еще примеры возможных repository: + +- `PostgresUserRepository` +- `RedisSessionRepository` +- `ExternalBillingRepository` + +### `adapter/` + +Это внешний слой. + +Здесь находятся: + +- FastAPI router и схемы +- загрузка конфига +- DI контейнер +- логирование, метрики, трейсы +- интеграции со сторонними библиотеками + +Adapter переводит внешний мир в язык use case и обратно. + +## Пример разделения ответственности + +Хорошая цепочка: + +1. HTTP handler получает `user_id` +2. handler создает `GetUserQuery` +3. use case вызывает `UserRepository` +4. repository возвращает `User` +5. handler маппит `User` в HTTP response model + +Плохая цепочка: + +1. router сам идет в базу +2. router сам открывает span +3. router сам собирает config +4. router сам решает бизнес-правила + +## Почему это удобно + +- проще тестировать use case отдельно от HTTP и OTel +- проще менять инфраструктуру +- проще читать проект по слоям +- проще давать задачу AI-агенту без риска смешать ответственности diff --git a/docs/CODESTYLE.md b/docs/CODESTYLE.md new file mode 100644 index 0000000..add1ed8 --- /dev/null +++ b/docs/CODESTYLE.md @@ -0,0 +1,83 @@ +# Code Style Guide + +## Purpose + +This repository is a Python clean architecture template. +Write code that keeps the core portable, adapters thin, and wiring explicit. + +## Layer rules + +- `domain/` contains entities and domain errors only +- `usecase/` contains application logic and ports +- `repository/` contains repository implementations +- `adapter/` contains framework, config, DI, HTTP, and observability integrations +- Dependencies must point inward +- `domain/` must not import anything from project internals +- `usecase/` may import `domain/`, but must not import `adapter/`, `repository/`, FastAPI, or OpenTelemetry +- `repository/` and `adapter/` may depend on `usecase/` ports and `domain/` + +## Dependency inversion + +- Define interfaces as `Protocol` types at the point of use +- Keep implementations in outer layers +- Inject dependencies through constructors +- Prefer explicit wiring in `adapter/di/container.py` +- Do not hide object creation behind magic globals + +## Python conventions + +- Use simple names +- Prefer dataclasses for immutable values, config sections, and request models inside inner layers +- Keep `__init__.py` files empty +- Do not use `from __future__ import annotations` +- Prefer one clear responsibility per class +- Keep functions small and direct + +## Comments and errors + +- Add comments only when the code is not obvious +- Keep comments short +- Do not end comments with a period +- Keep error messages short +- Do not end error messages with a period + +## HTTP rules + +- Keep FastAPI code inside `adapter/http/fastapi/` +- Keep HTTP request and response schemas inside the HTTP adapter +- Handlers should translate HTTP input to usecase calls and map domain errors to HTTP responses +- Do not move business logic into routers, dependencies, or middleware + +## Observability rules + +- Inner layers know only `Logger`, `Metrics`, and `Tracer` ports +- OpenTelemetry code stays in `adapter/otel/` +- Use `Noop` implementations when a signal is disabled +- Request logging belongs in the HTTP adapter + +## Configuration rules + +- Keep non-secret defaults in `config/app.yaml` +- Read secrets from env vars +- Merge YAML, `.env`, and process env into one typed config tree +- Do not read env vars directly inside `domain/` or `usecase/` + +## Example + +```py +from typing import Protocol + + +class UserRepository(Protocol): + def get(self, user_id: str) -> User | None: ... + + +class GetUser: + def __init__(self, repository: UserRepository, logger: Logger, tracer: Tracer) -> None: + self._repository = repository + self._logger = logger + self._tracer = tracer +``` + +The use case owns the contract. +The repository implementation lives outside the use case layer. diff --git a/docs/OBSERVABILITY_RU.md b/docs/OBSERVABILITY_RU.md new file mode 100644 index 0000000..85d1fec --- /dev/null +++ b/docs/OBSERVABILITY_RU.md @@ -0,0 +1,178 @@ +# Логи, метрики и трейсы + +## Зачем нужны три сигнала + +### Логи + +Логи отвечают на вопрос: что произошло. + +Примеры: + +- пришел HTTP-запрос +- пользователь не найден +- сервис стартовал + +### Метрики + +Метрики отвечают на вопрос: как часто и насколько долго что-то происходит. + +Примеры: + +- сколько было запросов +- сколько было ошибок +- сколько миллисекунд заняла операция + +### Трейсы + +Трейсы отвечают на вопрос: как прошел путь одного конкретного запроса через систему. + +Примеры: + +- HTTP request -> use case -> repository +- где именно замедление +- на каком шаге произошла ошибка + +## Как observability устроена в этом проекте + +Внутренние слои не знают про OpenTelemetry напрямую. +Они работают только через порты из `usecase/interface.py`: + +- `Logger` +- `Metrics` +- `Tracer` +- `Span` + +Такой подход сохраняет чистую архитектуру: use case зависит от абстракций, а не от SDK. + +## Где лежат реализации + +### Логирование + +- `adapter/observability/logging.py` - логирование в `stdout` или файл +- `adapter/otel/logging.py` - логирование через OpenTelemetry exporter + +Поддерживаются режимы: + +- `stdout` +- `file` +- `otel` + +Поддерживаются форматы: + +- `text` +- `json` + +### Метрики + +- `adapter/otel/metrics.py` - адаптер над OTel `Meter` + +Метрики создаются лениво: счетчик или гистограмма заводятся при первом обращении по имени. + +### Трейсинг + +- `adapter/otel/tracing.py` - адаптер над OTel `Tracer` + +Use case или repository может открыть span через `with tracer.start_span(...):`. + +### Runtime factory + +- `adapter/observability/factory.py` - выбирает, какой runtime собрать +- `adapter/observability/noop.py` - noop-реализации, когда сигнал отключен +- `adapter/otel/bootstrap.py` - создает OTel providers и exporters + +## Как это работает на старте приложения + +1. Загружается `AppConfig` +2. `build_observability(config)` выбирает нужные реализации +3. Если хотя бы один сигнал должен идти через OTel, создается OTel runtime +4. В контейнер кладутся готовые `logger`, `metrics`, `tracer` +5. Use case и repository получают их через конструктор + +## Как observability работает в HTTP + +### Request logging + +В `adapter/http/fastapi/middleware.py` есть HTTP middleware, которое: + +- замеряет длительность запроса +- фиксирует метод, путь и статус +- пишет событие `http_request` + +### HTTP traces и HTTP metrics + +В `adapter/http/fastapi/app.py` вызывается `FastAPIInstrumentor.instrument_app(...)`. + +Это дает стандартную OTel instrumentation для FastAPI/ASGI без кастомного trace middleware. + +## Как пользоваться логгером в use case + +Пример: + +```py +class CreateUser: + def __init__(self, logger: Logger) -> None: + self._logger = logger + + def execute(self, email: str) -> None: + self._logger.info('user_create', attrs={'user.email': email}) +``` + +Рекомендации: + +- message короткий и стабильный +- важные поля передавать в `attrs` +- не шить JSON руками в строку сообщения + +## Как пользоваться метриками + +Пример: + +```py +class CreateUser: + def __init__(self, metrics: Metrics) -> None: + self._metrics = metrics + + def execute(self, email: str) -> None: + self._metrics.increment('user.create.total', attrs={'source': 'api'}) + self._metrics.record('user.create.duration_ms', 12.5) +``` + +Практика именования: + +- счетчики: `something.total`, `something.error` +- длительности: `something.duration_ms` +- атрибуты делать короткими и стабильными + +## Как пользоваться трейсами + +Пример: + +```py +class CreateUser: + def __init__(self, tracer: Tracer) -> None: + self._tracer = tracer + + def execute(self, user_id: str) -> None: + with self._tracer.start_span('usecase.create_user', attrs={'user.id': user_id}) as span: + span.set_attribute('user.flow', 'signup') +``` + +Если внутри возникнет ошибка, адаптер трейсинга сможет записать ее в span. + +## Как включать и выключать сигналы + +В `config/app.yaml` и env есть основные флаги: + +- `logging.output=stdout|file|otel` +- `logging.format=text|json` +- `logging.file_path=...` +- `metrics.enabled=true|false` +- `tracing.enabled=true|false` + +Когда метрики или трейсы выключены, внутренние слои получают `Noop`-реализации. +Это значит, что код use case не приходится усложнять проверками `if enabled`. + +## Главное правило + +`domain/` и `usecase/` не импортируют OpenTelemetry. +Весь OTel код остается во внешних слоях. diff --git a/docs/PROJECT_GUIDE_RU.md b/docs/PROJECT_GUIDE_RU.md new file mode 100644 index 0000000..25a79ee --- /dev/null +++ b/docs/PROJECT_GUIDE_RU.md @@ -0,0 +1,216 @@ +# Как эта архитектура реализована в проекте + +## Общая схема + +Проект собирается так: + +1. `adapter/config/loader.py` читает YAML, `.env` и env vars +2. `adapter/di/container.py` строит единый контейнер +3. `adapter/observability/factory.py` создает logger, metrics и tracer runtime +4. repository и usecase создаются один раз при старте +5. `adapter/http/fastapi/app.py` создает FastAPI app и подключает router + middleware + +Это и есть composition root: все внешние зависимости собираются в одном месте и потом передаются внутрь. + +## Как здесь реализована чистая архитектура + +### `domain/` + +- хранит сущности и доменные ошибки +- не зависит от framework и SDK + +### `usecase/` + +- описывает сценарии приложения +- задает интерфейсы через `Protocol` +- работает только с абстракциями + +### `repository/` + +- реализует интерфейсы доступа к данным +- может использовать tracer, clients, DB drivers + +### `adapter/` + +- оборачивает внешние инструменты +- переводит HTTP/config/OTel в контракты приложения + +## Как создать новый use case + +Рабочая последовательность: + +1. Если нужна новая бизнес-сущность или ошибка, добавить ее в `domain/` +2. Если use case зависит от внешнего ресурса, описать контракт в `usecase/` +4. Создать класс use case +5. Внедрить зависимости через конструктор +6. Подключить use case в `adapter/di/container.py` + +Минимальный шаблон: + +```py +from usecase.interface import Logger, Metrics, Tracer, UserRepository +from domain.user import User + + +class CreateUser: + def __init__(self, repository: UserRepository, logger: Logger, metrics: Metrics, tracer: Tracer) -> None: + self._repository = repository + self._logger = logger + self._metrics = metrics + self._tracer = tracer + + def execute(self, email: str) -> User: + with self._tracer.start_span('usecase.create_user', attrs={'user.email': email}): + user = self._repository.save(email) + self._metrics.increment('user.create.total') + self._logger.info('user_create', attrs={'user.email': email}) + return user +``` + +Правила: + +- не импортировать FastAPI в use case +- не читать env vars в use case +- не работать с `Request`, `Response`, `HTTPException` + +## Как работать с repository + +### 1. Сначала описать интерфейс + +Контракт должен жить рядом с use case, который его использует. + +```py +class UserRepository(Protocol): + def get(self, user_id: str) -> User | None: ... + def save(self, user: User) -> None: ... +``` + +### 2. Потом сделать реализацию + +Реализация уходит во внешний слой, например в `repository/`. + +```py +class InMemoryUserRepository(UserRepository): + def __init__(self, tracer: Tracer) -> None: + self._tracer = tracer + self._users: dict[str, User] = {} +``` + +### 3. Подключить в контейнере + +Именно `adapter/di/container.py` решает, какую реализацию дать use case. + +Это важно: use case не должен сам выбирать repository. + +## Как добавить новую HTTP ручку + +Последовательность: + +1. Создать или переиспользовать use case +2. При необходимости добавить схемы в `adapter/http/fastapi/schemas.py` +3. Добавить dependency, если нужен новый use case из контейнера +4. Добавить маршрут в `adapter/http/fastapi/routers/v1/router.py` +5. Преобразовать HTTP input в команду или query use case +6. Преобразовать результат use case в response model +7. Смаппить доменные ошибки в `HTTPException` + +Шаблон: + +```py +@router.post('/users', response_model=UserResponse) +def create_user(payload: CreateUserRequest, usecase: CreateUser = Depends(get_create_user)) -> UserResponse: + try: + user = usecase.execute(payload.email) + except UserConflictError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + + return UserResponse(id=user.id, email=user.email) +``` + +Правило: router остается тонким. + +## Как пользоваться логгером, метриками и трейсами + +### Логгер + +- использовать для бизнес-событий и предупреждений +- передавать данные через `attrs` +- сообщения делать короткими и стабильными + +### Метрики + +- использовать для счетчиков и длительностей +- не создавать условные ветки `if metrics_enabled` +- runtime сам подставит рабочую или noop-реализацию + +### Трейсы + +- открывать span на границе важной операции +- класть ключевые атрибуты в `attrs` +- ошибки писать через `record_error`, если это делает смысл явным + +## Как конфигурировать проект + +### Базовая конфигурация + +Основной файл - `config/app.yaml`. + +Там лежат: + +- имя приложения +- окружение +- HTTP host/port +- режим логирования +- флаги метрик и трейсов +- OTel endpoints +- имя заголовка токена + +### Секреты + +Обязательные секреты берутся из env: + +- `APP_API_TOKEN` +- `APP_SIGNING_KEY` + +### Переопределение через env + +Можно переопределять YAML через переменные окружения, например: + +- `APP_HTTP_HOST` +- `APP_HTTP_PORT` +- `APP_LOGGING_LEVEL` +- `APP_LOGGING_OUTPUT` +- `APP_LOGGING_FORMAT` +- `APP_LOGGING_FILE_PATH` +- `APP_METRICS_ENABLED` +- `APP_TRACING_ENABLED` +- `APP_OTEL_LOGS_ENDPOINT` +- `APP_OTEL_METRICS_ENDPOINT` +- `APP_OTEL_TRACES_ENDPOINT` + +### Локальный запуск + +```bash +APP_API_TOKEN=local-api-token APP_SIGNING_KEY=local-signing-key make run +``` + +### Запуск с OTel + +```bash +APP_API_TOKEN=local-api-token APP_SIGNING_KEY=local-signing-key make run-otel +``` + +### Docker Compose + +```bash +APP_API_TOKEN=local-api-token APP_SIGNING_KEY=local-signing-key make compose-up +``` + +## Короткая памятка + +- новая бизнес-логика -> `usecase/` +- новый контракт -> рядом с use case +- новая реализация контракта -> `repository/` или внешний `adapter/` +- новый HTTP маршрут -> `adapter/http/fastapi/` +- новый infra/runtime code -> `adapter/` +- зависимости связывать только в `adapter/di/container.py` diff --git a/uv.lock b/uv.lock index dcd0a7e..9ce78f6 100644 --- a/uv.lock +++ b/uv.lock @@ -242,6 +242,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, ] +[[package]] +name = "master" +version = "0.0.1" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation-fastapi" }, + { name = "opentelemetry-sdk" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvicorn" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "ruff" }, + { name = "types-pyyaml" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.116.1" }, + { name = "opentelemetry-api", specifier = ">=1.31.1" }, + { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.31.1" }, + { name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.52b1" }, + { name = "opentelemetry-sdk", specifier = ">=1.31.1" }, + { name = "python-dotenv", specifier = ">=1.2.2" }, + { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "uvicorn", specifier = ">=0.35.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.18.2" }, + { name = "pytest", specifier = ">=8.4.2" }, + { name = "ruff", specifier = ">=0.13.1" }, + { name = "types-pyyaml", specifier = ">=6.0.12.20250915" }, +] + [[package]] name = "mypy" version = "1.19.1" @@ -700,49 +743,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" }, ] -[[package]] -name = "web-python-skelet" -version = "0.0.1" -source = { virtual = "." } -dependencies = [ - { name = "fastapi" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-instrumentation-fastapi" }, - { name = "opentelemetry-sdk" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, - { name = "uvicorn" }, -] - -[package.dev-dependencies] -dev = [ - { name = "mypy" }, - { name = "pytest" }, - { name = "ruff" }, - { name = "types-pyyaml" }, -] - -[package.metadata] -requires-dist = [ - { name = "fastapi", specifier = ">=0.116.1" }, - { name = "opentelemetry-api", specifier = ">=1.31.1" }, - { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.31.1" }, - { name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.52b1" }, - { name = "opentelemetry-sdk", specifier = ">=1.31.1" }, - { name = "python-dotenv", specifier = ">=1.2.2" }, - { name = "pyyaml", specifier = ">=6.0.2" }, - { name = "uvicorn", specifier = ">=0.35.0" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "mypy", specifier = ">=1.18.2" }, - { name = "pytest", specifier = ">=8.4.2" }, - { name = "ruff", specifier = ">=0.13.1" }, - { name = "types-pyyaml", specifier = ">=6.0.12.20250915" }, -] - [[package]] name = "wrapt" version = "1.17.3"