ADC AppKit - библиотека для управления компонентами и состоянием приложения
Project description
ADC AppKit
Компонентная архитектура для async Python приложений: декларативные компоненты, dependency injection, request scope.
Install
pip install adc-appkit
Quick start
import asyncio
from adc_appkit import BaseApp, component
from adc_appkit.components.component import Component
class Database:
def __init__(self, host: str, port: int):
self.host = host
self.port = port
async def close(self):
pass
class DatabaseComponent(Component[Database]):
async def _start(self, host: str, port: int, **kw) -> Database:
return Database(host, port)
async def _stop(self) -> None:
await self.obj.close()
class App(BaseApp):
db = component(DatabaseComponent, config_key="db")
async def main():
app = App(components_config={"db": {"host": "localhost", "port": 5432}})
await app.start()
# дескриптор возвращает объект напрямую
print(app.db.host) # "localhost"
health = await app.healthcheck() # {"db": True}
await app.stop()
asyncio.run(main())
Core concepts
Component[T]
Базовый класс для компонентов с управляемым жизненным циклом:
class PGComponent(Component[Pool]):
async def _start(self, dsn: str, **kw) -> Pool:
return await asyncpg.create_pool(dsn)
async def _stop(self) -> None:
await self.obj.close()
async def is_alive(self) -> bool:
return not self.obj._closed
_start(**config)— создает и возвращает управляемый объект_stop()— освобождает ресурсыis_alive()— healthcheck (по умолчаниюTrue)
create_component(cls)
Оборачивает обычный класс в Component без написания boilerplate. Конструктор класса вызывается с параметрами из конфига:
from adc_appkit.components.component import create_component
class DAO:
def __init__(self, pool: Pool):
self.pool = pool
async def get_user(self, user_id):
return await self.pool.fetchrow("SELECT * FROM users WHERE id=$1", user_id)
class App(BaseApp):
pg = component(PGComponent, config_key="pg")
dao = component(create_component(DAO), config_key="dao", dependencies={"pool": "pg"})
При остановке create_component автоматически вызывает close() (sync или async) на объекте, если метод существует.
Strategies: SINGLETON vs REQUEST
from adc_appkit import ComponentStrategy
class App(BaseApp):
# создается один раз при app.start(), живет до app.stop()
pg = component(PGComponent, config_key="pg")
# создается на каждый request_scope, уничтожается при выходе
current_user = component(
CurrentUserComponent,
config_key="current_user",
strategy=ComponentStrategy.REQUEST,
)
SINGLETON (по умолчанию) — один экземпляр на всё приложение.
REQUEST — новый экземпляр на каждый request scope; изолирован через contextvars.ContextVar.
Dependency injection
Зависимости объявляются как маппинг {имя_параметра: имя_компонента}. При старте компонента в _start() автоматически передается .obj зависимости:
class App(BaseApp):
pg = component(PGComponent, config_key="pg")
dao = component(create_component(DAO), config_key="dao", dependencies={"pool": "pg"})
# ^^^^^ ^^^^
# параметр в __init__ имя компонента
Правила:
- SINGLETON может зависеть от SINGLETON
- REQUEST может зависеть от SINGLETON и REQUEST
- SINGLETON не может зависеть от REQUEST (защита от утечки request-scoped состояния)
Компоненты запускаются в топологическом порядке: зависимости стартуют раньше зависимых.
Request scope
Request scope управляет жизненным циклом REQUEST компонентов. Контекст (ctx) передает параметры, специфичные для запроса:
async with app.request_scope({"current_user": {"user_id": uid}}) as scope:
# все REQUEST компоненты созданы и запущены
user = app.current_user # obj напрямую через дескриптор
# или
user = scope.use("current_user")
Конфиг REQUEST компонента собирается в порядке: base config -> ctx overrides -> DI injection. Ключ в ctx должен совпадать с config_key компонента.
Real-world patterns
App с PG + DAO + request-scoped identity
Паттерн из реального auth-сервиса: PG pool (SINGLETON) -> DAO (SINGLETON, зависит от PG) -> CurrentIdentity (REQUEST, получает sub из JWT и dao как зависимость):
from adc_appkit import BaseApp, ComponentStrategy, component
from adc_appkit.components.component import Component, create_component
from adc_appkit.components.pg import PG
class DAO:
def __init__(self, pool):
self.pool = pool
async def get_user_by_id(self, user_id):
return await self.pool.fetchrow("SELECT * FROM users WHERE id=$1", user_id)
class CurrentIdentity(Component):
"""REQUEST-scoped: загружает текущего пользователя по ID из JWT."""
async def _start(self, sub, dao, **kw):
user = await dao.get_user_by_id(sub)
if not user:
raise ValueError(f"User {sub} not found")
return user
async def _stop(self):
pass
class App(BaseApp):
pg = component(PG, config_key="pg")
dao = component(create_component(DAO), config_key="dao", dependencies={"pool": "pg"})
current_identity = component(
CurrentIdentity,
config_key="current_identity",
strategy=ComponentStrategy.REQUEST,
dependencies={"dao": "dao"},
)
Endpoint: request_scope с контекстом из JWT
async def logout_handler(request):
app: App = request.app.state.app
user_id = decode_jwt(request.headers["Authorization"]).sub
async with app.request_scope({"current_identity": {"sub": user_id}}):
# app.current_identity — загруженный из БД пользователь
await app.revoke_session(session_id)
Значение "sub" из ctx объединяется с base config и DI-зависимостью dao, а затем передается в CurrentIdentity._start(sub=..., dao=...).
Healthcheck
health = await app.healthcheck()
# {"pg": True, "dao": True} — только SINGLETON компоненты
Architecture
- Топологическая сортировка — компоненты запускаются и останавливаются в порядке, учитывающем зависимости
- ContextVar изоляция — каждый request scope хранит свой кэш компонентов в
contextvars.ContextVar, безопасно для concurrent asyncio tasks - Сборка через MRO — дескрипторы компонентов собираются с учетом наследования (
reversed(cls.mro())) - Graceful cleanup — при ошибке старта REQUEST компонента уже запущенные компоненты корректно останавливаются
Built-in components
| Компонент | Модуль | Описание |
|---|---|---|
| PG | adc_appkit.components.pg |
PostgreSQL connection pool (asyncpg) |
| HTTP | adc_appkit.components.http |
HTTP client (aiohttp) |
| S3 | adc_appkit.components.s3 |
S3 client (boto3/aioboto3) |
Development
uv sync --dev
uv run pytest -v
uv run black adc_appkit tests
uv run isort adc_appkit tests
uv run mypy adc_appkit
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file adc_appkit-0.2.0.tar.gz.
File metadata
- Download URL: adc_appkit-0.2.0.tar.gz
- Upload date:
- Size: 47.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.16
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
abc6db47c08e6ab0535c6aa056e41167ffd845fb48ee8ff7adbfe74c5512ab04
|
|
| MD5 |
9ff7ebf23469f04db9a02a1eaefd9133
|
|
| BLAKE2b-256 |
66571ac6435ba2f66265c7c8fadbaeb081a5a2f7ea208301ab133f591e2af9fd
|
File details
Details for the file adc_appkit-0.2.0-py3-none-any.whl.
File metadata
- Download URL: adc_appkit-0.2.0-py3-none-any.whl
- Upload date:
- Size: 18.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.16
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7f93b4c9c39d34fe58281a84717a1d58f6a842dc66993350349f3fc3a32657e5
|
|
| MD5 |
c9bde7608e2c0241173f52bc86dfda2f
|
|
| BLAKE2b-256 |
58396dae5a15e303d6a3daa10ba407f8a5846493d0587bd8f4dfba6b35b74e8e
|