Observed since 20 Oct, 19:14 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
ADC AppKit - библиотека для управления компонентами и состоянием приложения
Компонентная архитектура для async Python приложений: декларативные компоненты, dependency injection, request scope.
pip install adc-appkit
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())
Базовый класс для компонентов с управляемым жизненным циклом:
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)Оборачивает обычный класс в 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) на объекте, если метод существует.
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.
Зависимости объявляются как маппинг {имя_параметра: имя_компонента}. При старте компонента в _start() автоматически передается .obj зависимости:
class App(BaseApp):
pg = component(PGComponent, config_key="pg")
dao = component(create_component(DAO), config_key="dao", dependencies={"pool": "pg"})
# ^^^^^ ^^^^
# параметр в __init__ имя компонента
Правила:
Компоненты запускаются в топологическом порядке: зависимости стартуют раньше зависимых.
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 компонента.
Паттерн из реального 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"},
)
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=...).
health = await app.healthcheck()
# {"pg": True, "dao": True} — только SINGLETON компоненты
contextvars.ContextVar, безопасно для concurrent asyncio tasksreversed(cls.mro()))| Компонент | Модуль | Описание |
|---|---|---|
| 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) |
uv sync --dev
uv run pytest -v
uv run black adc_appkit tests
uv run isort adc_appkit tests
uv run mypy adc_appkit
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.2.0 | 12 Feb 2026 | 5 |
This describes our collected data; it is not a security assessment.
Observed since 20 Oct, 19:14 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:1158c66b33c391f9aa7350e258c69cc759314ac6c93bb455c360717e11791238