Skip to main content

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

adc_appkit-0.2.0.tar.gz (47.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

adc_appkit-0.2.0-py3-none-any.whl (18.0 kB view details)

Uploaded Python 3

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

Hashes for adc_appkit-0.2.0.tar.gz
Algorithm Hash digest
SHA256 abc6db47c08e6ab0535c6aa056e41167ffd845fb48ee8ff7adbfe74c5512ab04
MD5 9ff7ebf23469f04db9a02a1eaefd9133
BLAKE2b-256 66571ac6435ba2f66265c7c8fadbaeb081a5a2f7ea208301ab133f591e2af9fd

See more details on using hashes here.

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

Hashes for adc_appkit-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7f93b4c9c39d34fe58281a84717a1d58f6a842dc66993350349f3fc3a32657e5
MD5 c9bde7608e2c0241173f52bc86dfda2f
BLAKE2b-256 58396dae5a15e303d6a3daa10ba407f8a5846493d0587bd8f4dfba6b35b74e8e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page