From 43296b5581aaef0dc95a6a64c6ab502b68256ccc Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Wed, 23 Apr 2025 20:06:48 +0300 Subject: [PATCH 1/7] add docs initial structure --- docs/css/code.css | 10 ++ docs/dev/contributing.md | 19 +++ docs/dev/decisions.md | 13 ++ docs/index.md | 22 ++++ docs/integrations/fastapi.md | 100 +++++++++++++++ docs/integrations/faststream.md | 62 +++++++++ docs/integrations/litestar.md | 88 +++++++++++++ docs/introduction/application-settings.md | 24 ++++ docs/introduction/context-data.md | 7 ++ docs/introduction/inject-factories.md | 60 +++++++++ docs/introduction/key-concepts.md | 74 +++++++++++ docs/migration/from-that-depends.md | 146 ++++++++++++++++++++++ docs/providers/collections.md | 43 +++++++ docs/providers/context-providers.md | 60 +++++++++ docs/providers/factories.md | 48 +++++++ docs/providers/object.md | 15 +++ docs/providers/resources.md | 51 ++++++++ docs/providers/singletons.md | 55 ++++++++ docs/requirements.txt | 2 + docs/testing/fixtures.md | 64 ++++++++++ mkdocs.yml | 67 ++++++++++ 21 files changed, 1030 insertions(+) create mode 100644 docs/css/code.css create mode 100644 docs/dev/contributing.md create mode 100644 docs/dev/decisions.md create mode 100644 docs/index.md create mode 100644 docs/integrations/fastapi.md create mode 100644 docs/integrations/faststream.md create mode 100644 docs/integrations/litestar.md create mode 100644 docs/introduction/application-settings.md create mode 100644 docs/introduction/context-data.md create mode 100644 docs/introduction/inject-factories.md create mode 100644 docs/introduction/key-concepts.md create mode 100644 docs/migration/from-that-depends.md create mode 100644 docs/providers/collections.md create mode 100644 docs/providers/context-providers.md create mode 100644 docs/providers/factories.md create mode 100644 docs/providers/object.md create mode 100644 docs/providers/resources.md create mode 100644 docs/providers/singletons.md create mode 100644 docs/requirements.txt create mode 100644 docs/testing/fixtures.md create mode 100644 mkdocs.yml diff --git a/docs/css/code.css b/docs/css/code.css new file mode 100644 index 0000000..c066a04 --- /dev/null +++ b/docs/css/code.css @@ -0,0 +1,10 @@ +/* Round the corners of code blocks */ +.md-typeset .highlight code { + border-radius: 10px !important; +} + +@media (max-width: 600px) { + .md-typeset code { + border-radius: 4px; + } +} diff --git a/docs/dev/contributing.md b/docs/dev/contributing.md new file mode 100644 index 0000000..1dfdacf --- /dev/null +++ b/docs/dev/contributing.md @@ -0,0 +1,19 @@ +# Contributing +This is an opensource project, and we are opened to new contributors. + +## Getting started +1. Make sure that you have [uv](https://docs.astral.sh/uv/) and [just](https://just.systems/) installed. +2. Clone project: +``` +git@github.com:modern-python/modern-di.git +cd modern-di +``` +3. Install dependencies running `just install` + +## Running linters +`Ruff` and `mypy` are used for static analysis. + +Run all checks by command `just lint` + +## Running tests +Run all tests by command `just test` diff --git a/docs/dev/decisions.md b/docs/dev/decisions.md new file mode 100644 index 0000000..59f45ff --- /dev/null +++ b/docs/dev/decisions.md @@ -0,0 +1,13 @@ +# Decisions +1. Dependency resolving is async and sync: + - if resolving requires event loop in sync mode `RuntimeError` is raised; + - framework was developed mostly for usage with async python applications; + - sync resolving is also possible, but it will fail in runtime in case of unresolved async dependencies; +2. Resources and singletons are safe for concurrent resolving: + - in async resolving `asyncio.Lock` is used; + - in sync resolving `threading.Lock` is used; +3. No global state -> all state lives in containers: + - it's needed for scopes to work; +4. Focus on maximum compatibility with mypy: + - no need for `# type: ignore` + - no need for `typing.cast` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..3286c21 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,22 @@ +# Modern DI + +Welcome to the `lite-bootstrap` documentation! + +`lite-bootstrap` assists you in creating applications with all the necessary instruments already set up. + +With `lite-bootstrap`, you receive an application with lightweight built-in support for: +- `sentry` +- `prometheus` +- `opentelemetry` +- `structlog` +- `cors` +- `swagger` - with additional offline version support +- `health-checks` + +Those instruments can be bootstrapped for: +- `fastapi`, +- `litestar`, +- `faststream`, +- services without these frameworks. + +--- diff --git a/docs/integrations/fastapi.md b/docs/integrations/fastapi.md new file mode 100644 index 0000000..998120c --- /dev/null +++ b/docs/integrations/fastapi.md @@ -0,0 +1,100 @@ +# Usage with `Fastapi` + +*More advanced example of usage with FastAPI - [fastapi-sqlalchemy-template](https://github.com/modern-python/fastapi-sqlalchemy-template)* + +## How to use + +1. Install `modern-di-fastapi`: + +=== "uv" + + ```bash + uv add modern-di-fastapi + ``` + +=== "pip" + + ```bash + pip install modern-di-fastapi + ``` + +=== "poetry" + + ```bash + poetry add modern-di-fastapi + ``` + +2. Apply this code example to your application: +```python +import datetime +import contextlib +import typing + +import fastapi +import modern_di_fastapi +from modern_di import Scope, providers + + +app = fastapi.FastAPI() +modern_di_fastapi.setup_di(app) + + +async def create_async_resource() -> typing.AsyncIterator[datetime.datetime]: + # async resource initiated + try: + yield datetime.datetime.now(tz=datetime.timezone.utc) + finally: + pass # async resource destructed + + +async_resource = providers.Resource(Scope.APP, create_async_resource) + + +@app.get("/") +async def read_root( + instance: typing.Annotated[ + datetime.datetime, + modern_di_fastapi.FromDI(async_resource), + ], +) -> datetime.datetime: + return instance + +``` + +## Websockets + +Usually our application uses only two scopes: `APP` and `REQUEST`. + +But when websockets are used, `SESSION` scope is used as well: +- for the lifetime of websocket-connection we have `SESSION` scope +- for each message we have `REQUEST` scope + +`APP` → `SESSION` → `REQUEST` + +`SESSION` scope is entered automatically. +`REQUEST` scope must be entered manually: + +```python +import typing + +import fastapi +import modern_di +import modern_di_fastapi + + +app = fastapi.FastAPI() + + +@app.websocket("/ws") +async def websocket_endpoint( + websocket: fastapi.WebSocket, + session_container: typing.Annotated[modern_di.Container, fastapi.Depends(modern_di_fastapi.build_di_container)], +) -> None: + with session_container.build_child_container() as request_container: + # REQUEST scope is entered here + pass + + await websocket.accept() + await websocket.send_text("test") + await websocket.close() +``` diff --git a/docs/integrations/faststream.md b/docs/integrations/faststream.md new file mode 100644 index 0000000..20998ca --- /dev/null +++ b/docs/integrations/faststream.md @@ -0,0 +1,62 @@ +# Usage with `FastStream` + +## How to use + +1. Install `modern-di-faststream`: + +=== "uv" + + ```bash + uv add modern-di-faststream + ``` + +=== "pip" + + ```bash + pip install modern-di-faststream + ``` + +=== "poetry" + + ```bash + poetry add modern-di-faststream + ``` + +2. Apply this code example to your application: + +```python +import datetime +import typing + +import faststream +from faststream.nats import NatsBroker +import modern_di_faststream +from modern_di import Scope, providers + + +broker = NatsBroker() +app = faststream.FastStream(broker=broker) +modern_di_faststream.setup_di(app) + + +async def create_async_resource() -> typing.AsyncIterator[datetime.datetime]: + # async resource initiated + try: + yield datetime.datetime.now(tz=datetime.timezone.utc) + finally: + pass # async resource destructed + + +async_resource = providers.Resource(Scope.APP, create_async_resource) + + +@broker.subscriber("in") +async def read_root( + instance: typing.Annotated[ + datetime.datetime, + modern_di_faststream.FromDI(async_resource), + ], +) -> datetime.datetime: + return instance + +``` diff --git a/docs/integrations/litestar.md b/docs/integrations/litestar.md new file mode 100644 index 0000000..c172025 --- /dev/null +++ b/docs/integrations/litestar.md @@ -0,0 +1,88 @@ +# Usage with `Litestar` + +*More advanced example of usage with LiteStar - [litestar-sqlalchemy-template](https://github.com/modern-python/litestar-sqlalchemy-template)* + +## How to use + +1. Install `modern-di-litestar`: + +=== "uv" + + ```bash + uv add modern-di-litestar + ``` + +=== "pip" + + ```bash + pip install modern-di-litestar + ``` + +=== "poetry" + + ```bash + poetry add modern-di-litestar + ``` + +2. Apply this code example to your application: +```python +import datetime +import typing + +from litestar import Litestar, get +import modern_di_litestar +from modern_di import Scope, providers + + +async def create_async_resource() -> typing.AsyncIterator[datetime.datetime]: + # async resource initiated + try: + yield datetime.datetime.now(tz=datetime.timezone.utc) + finally: + pass # async resource destructed + + +async_resource = providers.Resource(Scope.APP, create_async_resource) + + +@get("/", dependencies={"injected": modern_di_litestar.FromDI(async_resource)}) +async def index(injected: datetime.datetime) -> str: + return injected.isoformat() + + +app = Litestar( + route_handlers=[index], + plugins=[modern_di_litestar.ModernDIPlugin()], +) +``` + +## Websockets + +Usually our application uses only two scopes: `APP` and `REQUEST`. + +But when websockets are used, `SESSION` scope is used as well: +- for the lifetime of websocket-connection we have `SESSION` scope +- for each message we have `REQUEST` scope + +`APP` → `SESSION` → `REQUEST` + +`SESSION` scope is entered automatically. +`REQUEST` scope must be entered manually: + +```python +import litestar +import modern_di +import modern_di_litestar + + +app = litestar.Litestar(plugins=[modern_di_litestar.ModernDIPlugin()]) + + +@litestar.websocket_listener("/ws") +async def websocket_handler(data: str, di_container: modern_di.Container) -> None: + with di_container.build_child_container() as request_container: + # REQUEST scope is entered here + pass + +app.register(websocket_handler) +``` diff --git a/docs/introduction/application-settings.md b/docs/introduction/application-settings.md new file mode 100644 index 0000000..2a008ef --- /dev/null +++ b/docs/introduction/application-settings.md @@ -0,0 +1,24 @@ +# Application settings +For example, you have application settings in `pydantic_settings` +```python +import pydantic_settings + + +class Settings(pydantic_settings.BaseSettings): + service_name: str = "FastAPI template" + debug: bool = False + ... +``` + +You can register settings as `Singleton` in DI container + +```python +from modern_di import BaseGraph, Scope, providers + + +class DIContainer(BaseGraph): + settings = providers.Singleton(Scope.APP, Settings) + some_factory = providers.Factory(Scope.APP, SomeFactory, service_name=settings.cast.service_name) +``` + +And when `some_factory` is resolved it will receive `service_name` attribute from `Settings` diff --git a/docs/introduction/context-data.md b/docs/introduction/context-data.md new file mode 100644 index 0000000..0684136 --- /dev/null +++ b/docs/introduction/context-data.md @@ -0,0 +1,7 @@ +# Context data + +Often, scopes are connected with external events: HTTP-requests, message from queue, callbacks from framework. + +These events can be represented by some objects which can be used for dependencies creation. + +There are several providers with access to such context data. You can read more [here](../../providers/context-providers): diff --git a/docs/introduction/inject-factories.md b/docs/introduction/inject-factories.md new file mode 100644 index 0000000..55d6dbf --- /dev/null +++ b/docs/introduction/inject-factories.md @@ -0,0 +1,60 @@ +# Injecting factories + +When you need to inject the factory itself, but not the result of its call, use: + +1. `.async_provider` attribute for async resolver +2. `.sync_provider` attribute for sync resolver + +Let's first define providers with container: +```python +import dataclasses +import datetime +import typing + +from modern_di import BaseGraph, Container, Scope, providers + + +async def create_async_resource() -> typing.AsyncIterator[datetime.datetime]: + yield datetime.datetime.now(tz=datetime.timezone.utc) + + +@dataclasses.dataclass(kw_only=True, slots=True) +class SomeFactory: + start_at: datetime.datetime + + +@dataclasses.dataclass(kw_only=True, slots=True) +class FactoryWithFactories: + sync_factory: typing.Callable[..., SomeFactory] + async_factory: typing.Callable[..., typing.Coroutine[typing.Any, typing.Any, SomeFactory]] + + +class DIContainer(BaseGraph): + async_resource = providers.Resource(Scope.APP, create_async_resource) + dependent_factory = providers.Factory(Scope.APP, SomeFactory, start_at=async_resource.cast) + factory_with_factories = providers.Factory( + Scope.APP, + FactoryWithFactories, + sync_factory=dependent_factory.sync_provider.cast, + async_factory=dependent_factory.async_provider.cast, + ) +``` + +Async factory from `.async_provider` attribute can be used like this: +```python +async with Container(scope=Scope.APP) as app_container: + factory_with_factories = await DIContainer.factory_with_factories.async_resolve(app_container) + instance1 = await factory_with_factories.async_factory() + instance2 = await factory_with_factories.async_factory() + assert instance1 is not instance2 +``` + +Sync factory from `.sync_provider` attribute can be used like this: +```python +async with Container(scope=Scope.APP) as app_container: + await DIContainer.async_resolve_creators(app_container) + factory_with_factories = await DIContainer.factory_with_factories.sync_resolve(app_container) + instance1 = factory_with_factories.sync_factory() + instance2 = factory_with_factories.sync_factory() + assert instance1 is not instance2 +``` diff --git a/docs/introduction/key-concepts.md b/docs/introduction/key-concepts.md new file mode 100644 index 0000000..3bf8bfe --- /dev/null +++ b/docs/introduction/key-concepts.md @@ -0,0 +1,74 @@ +# Key concepts + +## Scope + +- is a lifespan of a dependency; +- is required almost for each provider; +- in frameworks' integrations some scopes are entered automatically; +- dependencies of **Resource** and **Singleton** provider cached for the lifespan of its scope; + +### Default scopes + +**APP**: + + - tied to the entire application lifetime; + - can be used for singletons of **Resource** and **Singleton** providers; + - must be managed manually in lifecycle methods of the application: see **integrations** section; + +**SESSION**: + + - for websocket session lifetime; + - dependencies of this scope cannot be used for http-requests; + - managed automatically; + +**REQUEST**: + + - for dependencies which are created for each user request, for example database session; + - managed automatically for http-request; + - must be managed manually for websockets; + +**ACTION**: + + - for lifetime less than request; + - must be managed manually; + +**STEP**: + + - for lifetime less than **ACTION**; + - must be managed manually. + +## Provider + +Providers needed to assemble the objects. +They retrieve the underlying dependencies and inject them into the created object. +It causes the cascade effect that helps to assemble object graphs. + +More about providers: + +- do not contain assembled objects: + - **Singleton** and **Resource** objects are stored in container; + - **Factory** objects are built on each call. +- can have dependencies only of the same or more long-lived scopes: + - **APP**-scoped provider can have only **APP**-scoped dependencies; + - **SESSION**-scoped provider can have APP and **SESSION**-scoped dependencies, etc.; + +## Container + +Each container is assigned to a certain scope. +To enter a nested scope, a context manager should be used. +Nested scope contains link to parent container. + +All states live in containers: + +- assembled objects; +- context stacks for resources to finalize them at the end of lifecycle; +- overrides for tests; + +## Graph + +Graph is a collection of providers. They annot be instantiated. + +Graph can initialize its resources and singletons to container: + +- coroutine **async_resolve_creators** should be used to resolve asynchronously; +- function **sync_resolve_creators** should be used to resolve synchronously. diff --git a/docs/migration/from-that-depends.md b/docs/migration/from-that-depends.md new file mode 100644 index 0000000..1de74e8 --- /dev/null +++ b/docs/migration/from-that-depends.md @@ -0,0 +1,146 @@ +# Migration from `that-depends` + +## 1. Install `modern-di` using your favorite tool: + +If you need only `modern-di` without integrations: + +=== "uv" + + ```bash + uv add modern-di + ``` + +=== "pip" + + ```bash + pip install modern-di + ``` + +=== "poetry" + + ```bash + poetry add modern-di + ``` + +If you need to integrate with some framework, then install `modern-di-*`. + +## 2. Migrate dependencies graph +1. Use `modern-di.BaseGraph` instead of `that-depends.BaseContainer`. +2. Add scopes to all providers. + - Most of the providers will be `APP` scope. + - ContextResource usually becomes `Resource` of `REQUEST`-scope. + - Dependents of ContextResource usually has `REQUEST`-scope as well. + +=== "that-depends" + + ```python + from that_depends import BaseContainer, providers + + from app import repositories + from app.resources.db import create_sa_engine, create_session + + + class Dependencies(BaseContainer): + database_engine = providers.Resource(create_sa_engine, settings=settings.cast) + session = providers.ContextResource(create_session, engine=database_engine.cast) + decks_service = providers.Factory(repositories.DecksService, session=session) + cards_service = providers.Factory(repositories.CardsService, session=session) + ``` + +=== "modern-di" + + ```python + from modern_di import BaseGraph, Scope, providers + + from app import repositories + from app.resources.db import create_sa_engine, create_session + + + class Dependencies(BaseGraph): + database_engine = providers.Resource(Scope.APP, create_sa_engine) + session = providers.Resource(Scope.REQUEST, create_session, engine=database_engine.cast) + + decks_service = providers.Factory(Scope.REQUEST, repositories.DecksService, session=session.cast) + cards_service = providers.Factory(Scope.REQUEST, repositories.CardsService, session=session.cast) + ``` + +## 3. Migrate integration with framework + +=== "fastapi" + + ```python + import contextlib + import typing + + import fastapi + import modern_di_fastapi + + from app.ioc import Dependencies + + + app = fastapi.FastAPI(lifespan=lifespan_manager) + modern_di_fastapi.setup_di(app) + ``` + +=== "litestar" + + ```python + import contextlib + import typing + + from litestar import Litestar + import modern_di_litestar + + + app = Litestar( + route_handlers=[...], + plugins=[modern_di_litestar.ModernDIPlugin()], + ) + ``` + +## 4. Migrate routes + +=== "fastapi" + + For `fastapi` replace `fastapi.Depends` with `modern_di_fastapi.FromDI`: + + ```python + import typing + + import fastapi + from modern_di_fastapi import FromDI + + from app import ioc, schemas + from app.repositories import DecksService + + + ROUTER: typing.Final = fastapi.APIRouter() + + + @ROUTER.get("/decks/") + async def list_decks( + decks_service: DecksService = FromDI(ioc.Dependencies.decks_service), + ) -> schemas.Decks: + objects = await decks_service.list() + return typing.cast(schemas.Decks, {"items": objects}) + ``` + +=== "litestar" + + For `litestar` replace `litestar.di.Provide` with `modern_di_litestar.FromDI` + + ```python + import litestar + import modern_di_litestar + + from app import ioc, schemas + from app.repositories import DecksService + + + @litestar.get("/decks/", dependencies={ + "decks_service": modern_di_litestar.FromDI(ioc.Dependencies.decks_service), + }) + async def list_decks(decks_service: DecksService) -> schemas.Decks: + objects = await decks_service.list() + return schemas.Decks(items=objects) + ``` diff --git a/docs/providers/collections.md b/docs/providers/collections.md new file mode 100644 index 0000000..7b00a28 --- /dev/null +++ b/docs/providers/collections.md @@ -0,0 +1,43 @@ +# Collections + +There are several collection providers: `List` and `Dict` + +## List + +- List provider contains other providers. +- Resolves into list of dependencies. + +```python +import random +from modern_di import BaseGraph, Container, Scope, providers + + +class Dependencies(BaseGraph): + random_number = providers.Factory(Scope.APP, random.random) + numbers_sequence = providers.List(Scope.APP, random_number, random_number) + + +with Container(scope=Scope.APP) as container: + print(Dependencies.numbers_sequence.sync_resolve(container)) + # [0.3035656170071561, 0.8280498192037787] +``` + +## Dict + +- Dict provider is a collection of named providers. +- Resolves into dict of dependencies. + +```python +import random +from modern_di import BaseGraph, Container, Scope, providers + + +class Dependencies(BaseGraph): + random_number = providers.Factory(Scope.APP, random.random) + numbers_map = providers.Dict(Scope.APP, key1=random_number, key2=random_number) + + +with Container(scope=Scope.APP) as container: + print(Dependencies.numbers_map.sync_resolve(container)) + # {'key1': 0.6851384528299208, 'key2': 0.41044920948045294} +``` diff --git a/docs/providers/context-providers.md b/docs/providers/context-providers.md new file mode 100644 index 0000000..aff6e14 --- /dev/null +++ b/docs/providers/context-providers.md @@ -0,0 +1,60 @@ +# Context Providers + +There are several providers with access to container's context. + +## Selector + +- Receives context unpacked to callable object. +- Selector provider chooses between a provider based on a key. +- Resolves into a single dependency. + +```python +import typing + +from modern_di import BaseGraph, Container, Scope, providers + + +class StorageService(typing.Protocol): + ... + +class StorageServiceLocal(StorageService): + ... + +class StorageServiceRemote(StorageService): + ... + +def selector_function(*, storage_backend: str | None = None, **_: object) -> str: + return storage_backend or "local" + +class Dependencies(BaseGraph): + storage_service: providers.Selector[StorageService] = providers.Selector( + Scope.APP, + selector_function, + local=providers.Factory(Scope.APP, StorageServiceLocal), + remote=providers.Factory(Scope.APP, StorageServiceRemote), + ) + +with Container(scope=Scope.APP, context={"storage_backend": "remote"}) as container: + print(type(Dependencies.storage_service.sync_resolve(container))) + # StorageServiceRemote +``` + +## ContextAdapter + +- Receives context unpacked to callable object. +- Can be used in another providers to access data from context. + +```python +from modern_di import BaseGraph, Container, Scope, providers + + +def context_adapter_function(*, storage_backend: str | None = None, **_: object) -> str: + return storage_backend or "local" + +class Dependencies(BaseGraph): + context_adapter = providers.ContextAdapter(Scope.APP, context_adapter_function) + +with Container(scope=Scope.APP, context={"storage_backend": "remote"}) as container: + print(Dependencies.context_adapter.sync_resolve(container)) + # "remote" +``` diff --git a/docs/providers/factories.md b/docs/providers/factories.md new file mode 100644 index 0000000..d786ca0 --- /dev/null +++ b/docs/providers/factories.md @@ -0,0 +1,48 @@ +# Factories +Factories are initialized on every call. + +## Factory +- Class or simple function is allowed. + +```python +import dataclasses + +from modern_di import BaseGraph, Container, Scope, providers + + +@dataclasses.dataclass(kw_only=True, slots=True) +class IndependentFactory: + dep1: str + dep2: int + + +class Dependencies(BaseGraph): + independent_factory = providers.Factory(Scope.APP, IndependentFactory, dep1="text", dep2=123) + + +with Container(scope=Scope.APP) as container: + instance = Dependencies.independent_factory.sync_resolve(container) + assert isinstance(instance, IndependentFactory) +``` + +## AsyncFactory +- Async function is required. + +```python +import datetime + +from modern_di import BaseGraph, Container, Scope, providers + + +async def async_factory() -> datetime.datetime: + return datetime.datetime.now(tz=datetime.timezone.utc) + + +class Dependencies(BaseGraph): + async_factory = providers.AsyncFactory(Scope.APP, async_factory) + + +async with Container(scope=Scope.APP) as container: + instance = await Dependencies.async_factory.async_resolve(container) + assert isinstance(instance, datetime.datetime) +``` diff --git a/docs/providers/object.md b/docs/providers/object.md new file mode 100644 index 0000000..26bbd43 --- /dev/null +++ b/docs/providers/object.md @@ -0,0 +1,15 @@ +# Object + +Object provider returns an object “as is”. + +```python +from modern_di import BaseGraph, Container, Scope, providers + + +class Dependencies(BaseGraph): + object_provider = providers.Object(Scope.APP, 1) + + +with Container(scope=Scope.APP) as container: + assert Dependencies.object_provider.sync_resolve(container) == 1 +``` diff --git a/docs/providers/resources.md b/docs/providers/resources.md new file mode 100644 index 0000000..5c60081 --- /dev/null +++ b/docs/providers/resources.md @@ -0,0 +1,51 @@ +# Resource + +- Resources are initialized only once per scope and have teardown logic. +- Generator or async generator is required. +```python +import typing + +from modern_di import BaseGraph, Container, Scope, providers + + +def create_sync_resource() -> typing.Iterator[str]: + # resource initialization + try: + yield "sync resource" + finally: + pass # resource teardown + + +async def create_async_resource() -> typing.AsyncIterator[str]: + # resource initialization + try: + yield "async resource" + finally: + pass # resource teardown + + +class Dependencies(BaseGraph): + sync_resource = providers.Resource(Scope.APP, create_sync_resource) + async_resource = providers.Resource(Scope.REQUEST, create_async_resource) + + +with Container(scope=Scope.APP) as container: + # sync resource of app scope + sync_resource_instance = Dependencies.sync_resource.sync_resolve(container) + async with container.build_child_container(scope=Scope.REQUEST) as request_container: + # async resource of request scope + async_resource_instance = await Dependencies.async_resource.async_resolve(request_container) +``` + +## Concurrency safety + +`Resource` is safe to use in threading and asyncio concurrency: + +```python +with Container(scope=Scope.APP) as container: + # calling async_resolve concurrently in different coroutines will create only one instance + await Dependencies.sync_resource.async_resolve(container) + + # calling sync_resolve concurrently in different threads will create only one instance + Dependencies.sync_resource.sync_resolve(container) +``` diff --git a/docs/providers/singletons.md b/docs/providers/singletons.md new file mode 100644 index 0000000..7014bd1 --- /dev/null +++ b/docs/providers/singletons.md @@ -0,0 +1,55 @@ +# Singleton and AsyncSingleton + +- resolve the dependency only once and cache the resolved instance for future injections; +- class or simple function is allowed. + +## How it works + +```python +import asyncio +import datetime +import pytest +import random + +from modern_di import BaseGraph, Container, Scope, providers + + +def generate_random_number() -> float: + return random.random() + +async def async_creator() -> datetime.datetime: + await asyncio.sleep(0) + return datetime.datetime.now(tz=datetime.timezone.utc) + + +class Dependencies(BaseGraph): + singleton = providers.Singleton(Scope.APP, generate_random_number) + async_singleton = providers.AsyncSingleton(Scope.APP, async_creator) + + +with Container(scope=Scope.APP) as container: + # sync resolving + singleton_instance1 = Dependencies.singleton.sync_resolve(container) + with pytest.raises(RuntimeError, match="AsyncSingleton cannot be resolved synchronously"): + Dependencies.async_singleton.sync_resolve(container) + + # async resolving + singleton_instance2 = await Dependencies.singleton.async_resolve(container) + async_singleton_instance = await Dependencies.async_singleton.async_resolve(container) + + # if resolved in the same container, the instance will be the same + assert singleton_instance1 is singleton_instance2 +``` + +## Concurrency safety + +`Singleton` is safe to use in threading and asyncio concurrency: + +```python +with Container(scope=Scope.APP) as container: + # calling async_resolve concurrently in different coroutines will create only one instance + await Dependencies.singleton.async_resolve(container) + + # calling sync_resolve concurrently in different threads will create only one instance + Dependencies.singleton.sync_resolve(container) +``` diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..9a8a4ca --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +mkdocs +mkdocs-material diff --git a/docs/testing/fixtures.md b/docs/testing/fixtures.md new file mode 100644 index 0000000..31d4c9a --- /dev/null +++ b/docs/testing/fixtures.md @@ -0,0 +1,64 @@ +# Fixtures for testing + +## 1. Add required fixtures: + +```python +import typing + +import fastapi +import modern_di +import modern_di_fastapi +import pytest + +from app import ioc + + +# application object can be imported from somewhere +application = fastapi.FastAPI() +modern_di_fastapi.setup_di(application) + + +@pytest.fixture +async def di_container() -> typing.AsyncIterator[modern_di.Container]: + """Fixture with APP-scope container.""" + di_container_: typing.Final = modern_di_fastapi.fetch_di_container(application) + async with di_container_: + yield di_container_ + + +@pytest.fixture +async def request_di_container(di_container: modern_di.Container) -> typing.AsyncIterator[modern_di.Container]: + """Fixture with REQUEST-scope container.""" + async with di_container.build_child_container(scope=modern_di.Scope.REQUEST) as request_container: + yield request_container + + +@pytest.fixture +def mock_dependencies(di_container: modern_di.Container) -> None: + """Mock dependencies for tests.""" + ioc.Dependencies.simple_factory.override(ioc.SimpleFactory(dep1="mock", dep2=777), container=di_container) +``` + +## 2. Use fixtures in tests: + +```python +import pytest +from modern_di import Container + +from app.ioc import Dependencies + + +async def test_with_app_scope(di_container: Container) -> None: + sync_resource_instance = await Dependencies.sync_resource.async_resolve(di_container) + # do sth with dependency + + +async def test_with_request_scope(request_di_container: Container) -> None: + simple_factory_instance = await Dependencies.simple_factory.async_resolve(request_di_container) + # do sth with dependency + +@pytest.mark.usefixtures("mock_dependencies") +async def test_with_request_scope_mocked(request_di_container: Container) -> None: + simple_factory_instance = await Dependencies.simple_factory.async_resolve(request_di_container) + # dependency is mocked here +``` diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..f0a196a --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,67 @@ +site_name: lite-bootstrap +repo_url: https://github.com/modern-python/lite-bootstrap +docs_dir: docs +edit_uri: edit/main/docs/ +nav: + - Quick-Start: index.md + +theme: + name: material + features: + - content.code.copy + - content.code.annotate + - content.action.edit + - content.action.view + - navigation.footer + - navigation.sections + - navigation.expand + - navigation.top + - navigation.instant + - header.autohide + - announce.dismiss + icon: + edit: material/pencil + view: material/eye + repo: fontawesome/brands/git-alt + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: black + accent: pink + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + accent: pink + toggle: + icon: material/brightness-4 + name: Switch to system preference + +markdown_extensions: + - toc: + permalink: true + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.tabbed: + alternate_style: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - def_list + - codehilite: + use_pygments: true + - attr_list + - md_in_html + +extra_css: + - css/code.css + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/modern-python/lite-bootstrap + name: GitHub From 9bd5cd7f9b7eb513e7377cdd448729e87f06e6ed Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Wed, 23 Apr 2025 20:09:01 +0300 Subject: [PATCH 2/7] add .readthedocs.yaml --- .readthedocs.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..571df87 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,13 @@ +version: 2 + +build: + os: "ubuntu-22.04" + tools: + python: "3.10" + +python: + install: + - requirements: docs/requirements.txt + +mkdocs: + configuration: mkdocs.yml From 5f1a5db9f4b407a8f4b6f9f6f29b163ca05407de Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Wed, 23 Apr 2025 20:17:19 +0300 Subject: [PATCH 3/7] remove unused --- docs/dev/contributing.md | 19 --- docs/dev/decisions.md | 13 -- docs/index.md | 2 +- docs/integrations/fastapi.md | 100 --------------- docs/integrations/faststream.md | 62 --------- docs/integrations/litestar.md | 88 ------------- docs/introduction/application-settings.md | 24 ---- docs/introduction/context-data.md | 7 -- docs/introduction/inject-factories.md | 60 --------- docs/introduction/key-concepts.md | 74 ----------- docs/migration/from-that-depends.md | 146 ---------------------- docs/providers/collections.md | 43 ------- docs/providers/context-providers.md | 60 --------- docs/providers/factories.md | 48 ------- docs/providers/object.md | 15 --- docs/providers/resources.md | 51 -------- docs/providers/singletons.md | 55 -------- docs/testing/fixtures.md | 64 ---------- 18 files changed, 1 insertion(+), 930 deletions(-) delete mode 100644 docs/dev/contributing.md delete mode 100644 docs/dev/decisions.md delete mode 100644 docs/integrations/fastapi.md delete mode 100644 docs/integrations/faststream.md delete mode 100644 docs/integrations/litestar.md delete mode 100644 docs/introduction/application-settings.md delete mode 100644 docs/introduction/context-data.md delete mode 100644 docs/introduction/inject-factories.md delete mode 100644 docs/introduction/key-concepts.md delete mode 100644 docs/migration/from-that-depends.md delete mode 100644 docs/providers/collections.md delete mode 100644 docs/providers/context-providers.md delete mode 100644 docs/providers/factories.md delete mode 100644 docs/providers/object.md delete mode 100644 docs/providers/resources.md delete mode 100644 docs/providers/singletons.md delete mode 100644 docs/testing/fixtures.md diff --git a/docs/dev/contributing.md b/docs/dev/contributing.md deleted file mode 100644 index 1dfdacf..0000000 --- a/docs/dev/contributing.md +++ /dev/null @@ -1,19 +0,0 @@ -# Contributing -This is an opensource project, and we are opened to new contributors. - -## Getting started -1. Make sure that you have [uv](https://docs.astral.sh/uv/) and [just](https://just.systems/) installed. -2. Clone project: -``` -git@github.com:modern-python/modern-di.git -cd modern-di -``` -3. Install dependencies running `just install` - -## Running linters -`Ruff` and `mypy` are used for static analysis. - -Run all checks by command `just lint` - -## Running tests -Run all tests by command `just test` diff --git a/docs/dev/decisions.md b/docs/dev/decisions.md deleted file mode 100644 index 59f45ff..0000000 --- a/docs/dev/decisions.md +++ /dev/null @@ -1,13 +0,0 @@ -# Decisions -1. Dependency resolving is async and sync: - - if resolving requires event loop in sync mode `RuntimeError` is raised; - - framework was developed mostly for usage with async python applications; - - sync resolving is also possible, but it will fail in runtime in case of unresolved async dependencies; -2. Resources and singletons are safe for concurrent resolving: - - in async resolving `asyncio.Lock` is used; - - in sync resolving `threading.Lock` is used; -3. No global state -> all state lives in containers: - - it's needed for scopes to work; -4. Focus on maximum compatibility with mypy: - - no need for `# type: ignore` - - no need for `typing.cast` diff --git a/docs/index.md b/docs/index.md index 3286c21..788f220 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,4 @@ -# Modern DI +# Lite Bootstrap Welcome to the `lite-bootstrap` documentation! diff --git a/docs/integrations/fastapi.md b/docs/integrations/fastapi.md deleted file mode 100644 index 998120c..0000000 --- a/docs/integrations/fastapi.md +++ /dev/null @@ -1,100 +0,0 @@ -# Usage with `Fastapi` - -*More advanced example of usage with FastAPI - [fastapi-sqlalchemy-template](https://github.com/modern-python/fastapi-sqlalchemy-template)* - -## How to use - -1. Install `modern-di-fastapi`: - -=== "uv" - - ```bash - uv add modern-di-fastapi - ``` - -=== "pip" - - ```bash - pip install modern-di-fastapi - ``` - -=== "poetry" - - ```bash - poetry add modern-di-fastapi - ``` - -2. Apply this code example to your application: -```python -import datetime -import contextlib -import typing - -import fastapi -import modern_di_fastapi -from modern_di import Scope, providers - - -app = fastapi.FastAPI() -modern_di_fastapi.setup_di(app) - - -async def create_async_resource() -> typing.AsyncIterator[datetime.datetime]: - # async resource initiated - try: - yield datetime.datetime.now(tz=datetime.timezone.utc) - finally: - pass # async resource destructed - - -async_resource = providers.Resource(Scope.APP, create_async_resource) - - -@app.get("/") -async def read_root( - instance: typing.Annotated[ - datetime.datetime, - modern_di_fastapi.FromDI(async_resource), - ], -) -> datetime.datetime: - return instance - -``` - -## Websockets - -Usually our application uses only two scopes: `APP` and `REQUEST`. - -But when websockets are used, `SESSION` scope is used as well: -- for the lifetime of websocket-connection we have `SESSION` scope -- for each message we have `REQUEST` scope - -`APP` → `SESSION` → `REQUEST` - -`SESSION` scope is entered automatically. -`REQUEST` scope must be entered manually: - -```python -import typing - -import fastapi -import modern_di -import modern_di_fastapi - - -app = fastapi.FastAPI() - - -@app.websocket("/ws") -async def websocket_endpoint( - websocket: fastapi.WebSocket, - session_container: typing.Annotated[modern_di.Container, fastapi.Depends(modern_di_fastapi.build_di_container)], -) -> None: - with session_container.build_child_container() as request_container: - # REQUEST scope is entered here - pass - - await websocket.accept() - await websocket.send_text("test") - await websocket.close() -``` diff --git a/docs/integrations/faststream.md b/docs/integrations/faststream.md deleted file mode 100644 index 20998ca..0000000 --- a/docs/integrations/faststream.md +++ /dev/null @@ -1,62 +0,0 @@ -# Usage with `FastStream` - -## How to use - -1. Install `modern-di-faststream`: - -=== "uv" - - ```bash - uv add modern-di-faststream - ``` - -=== "pip" - - ```bash - pip install modern-di-faststream - ``` - -=== "poetry" - - ```bash - poetry add modern-di-faststream - ``` - -2. Apply this code example to your application: - -```python -import datetime -import typing - -import faststream -from faststream.nats import NatsBroker -import modern_di_faststream -from modern_di import Scope, providers - - -broker = NatsBroker() -app = faststream.FastStream(broker=broker) -modern_di_faststream.setup_di(app) - - -async def create_async_resource() -> typing.AsyncIterator[datetime.datetime]: - # async resource initiated - try: - yield datetime.datetime.now(tz=datetime.timezone.utc) - finally: - pass # async resource destructed - - -async_resource = providers.Resource(Scope.APP, create_async_resource) - - -@broker.subscriber("in") -async def read_root( - instance: typing.Annotated[ - datetime.datetime, - modern_di_faststream.FromDI(async_resource), - ], -) -> datetime.datetime: - return instance - -``` diff --git a/docs/integrations/litestar.md b/docs/integrations/litestar.md deleted file mode 100644 index c172025..0000000 --- a/docs/integrations/litestar.md +++ /dev/null @@ -1,88 +0,0 @@ -# Usage with `Litestar` - -*More advanced example of usage with LiteStar - [litestar-sqlalchemy-template](https://github.com/modern-python/litestar-sqlalchemy-template)* - -## How to use - -1. Install `modern-di-litestar`: - -=== "uv" - - ```bash - uv add modern-di-litestar - ``` - -=== "pip" - - ```bash - pip install modern-di-litestar - ``` - -=== "poetry" - - ```bash - poetry add modern-di-litestar - ``` - -2. Apply this code example to your application: -```python -import datetime -import typing - -from litestar import Litestar, get -import modern_di_litestar -from modern_di import Scope, providers - - -async def create_async_resource() -> typing.AsyncIterator[datetime.datetime]: - # async resource initiated - try: - yield datetime.datetime.now(tz=datetime.timezone.utc) - finally: - pass # async resource destructed - - -async_resource = providers.Resource(Scope.APP, create_async_resource) - - -@get("/", dependencies={"injected": modern_di_litestar.FromDI(async_resource)}) -async def index(injected: datetime.datetime) -> str: - return injected.isoformat() - - -app = Litestar( - route_handlers=[index], - plugins=[modern_di_litestar.ModernDIPlugin()], -) -``` - -## Websockets - -Usually our application uses only two scopes: `APP` and `REQUEST`. - -But when websockets are used, `SESSION` scope is used as well: -- for the lifetime of websocket-connection we have `SESSION` scope -- for each message we have `REQUEST` scope - -`APP` → `SESSION` → `REQUEST` - -`SESSION` scope is entered automatically. -`REQUEST` scope must be entered manually: - -```python -import litestar -import modern_di -import modern_di_litestar - - -app = litestar.Litestar(plugins=[modern_di_litestar.ModernDIPlugin()]) - - -@litestar.websocket_listener("/ws") -async def websocket_handler(data: str, di_container: modern_di.Container) -> None: - with di_container.build_child_container() as request_container: - # REQUEST scope is entered here - pass - -app.register(websocket_handler) -``` diff --git a/docs/introduction/application-settings.md b/docs/introduction/application-settings.md deleted file mode 100644 index 2a008ef..0000000 --- a/docs/introduction/application-settings.md +++ /dev/null @@ -1,24 +0,0 @@ -# Application settings -For example, you have application settings in `pydantic_settings` -```python -import pydantic_settings - - -class Settings(pydantic_settings.BaseSettings): - service_name: str = "FastAPI template" - debug: bool = False - ... -``` - -You can register settings as `Singleton` in DI container - -```python -from modern_di import BaseGraph, Scope, providers - - -class DIContainer(BaseGraph): - settings = providers.Singleton(Scope.APP, Settings) - some_factory = providers.Factory(Scope.APP, SomeFactory, service_name=settings.cast.service_name) -``` - -And when `some_factory` is resolved it will receive `service_name` attribute from `Settings` diff --git a/docs/introduction/context-data.md b/docs/introduction/context-data.md deleted file mode 100644 index 0684136..0000000 --- a/docs/introduction/context-data.md +++ /dev/null @@ -1,7 +0,0 @@ -# Context data - -Often, scopes are connected with external events: HTTP-requests, message from queue, callbacks from framework. - -These events can be represented by some objects which can be used for dependencies creation. - -There are several providers with access to such context data. You can read more [here](../../providers/context-providers): diff --git a/docs/introduction/inject-factories.md b/docs/introduction/inject-factories.md deleted file mode 100644 index 55d6dbf..0000000 --- a/docs/introduction/inject-factories.md +++ /dev/null @@ -1,60 +0,0 @@ -# Injecting factories - -When you need to inject the factory itself, but not the result of its call, use: - -1. `.async_provider` attribute for async resolver -2. `.sync_provider` attribute for sync resolver - -Let's first define providers with container: -```python -import dataclasses -import datetime -import typing - -from modern_di import BaseGraph, Container, Scope, providers - - -async def create_async_resource() -> typing.AsyncIterator[datetime.datetime]: - yield datetime.datetime.now(tz=datetime.timezone.utc) - - -@dataclasses.dataclass(kw_only=True, slots=True) -class SomeFactory: - start_at: datetime.datetime - - -@dataclasses.dataclass(kw_only=True, slots=True) -class FactoryWithFactories: - sync_factory: typing.Callable[..., SomeFactory] - async_factory: typing.Callable[..., typing.Coroutine[typing.Any, typing.Any, SomeFactory]] - - -class DIContainer(BaseGraph): - async_resource = providers.Resource(Scope.APP, create_async_resource) - dependent_factory = providers.Factory(Scope.APP, SomeFactory, start_at=async_resource.cast) - factory_with_factories = providers.Factory( - Scope.APP, - FactoryWithFactories, - sync_factory=dependent_factory.sync_provider.cast, - async_factory=dependent_factory.async_provider.cast, - ) -``` - -Async factory from `.async_provider` attribute can be used like this: -```python -async with Container(scope=Scope.APP) as app_container: - factory_with_factories = await DIContainer.factory_with_factories.async_resolve(app_container) - instance1 = await factory_with_factories.async_factory() - instance2 = await factory_with_factories.async_factory() - assert instance1 is not instance2 -``` - -Sync factory from `.sync_provider` attribute can be used like this: -```python -async with Container(scope=Scope.APP) as app_container: - await DIContainer.async_resolve_creators(app_container) - factory_with_factories = await DIContainer.factory_with_factories.sync_resolve(app_container) - instance1 = factory_with_factories.sync_factory() - instance2 = factory_with_factories.sync_factory() - assert instance1 is not instance2 -``` diff --git a/docs/introduction/key-concepts.md b/docs/introduction/key-concepts.md deleted file mode 100644 index 3bf8bfe..0000000 --- a/docs/introduction/key-concepts.md +++ /dev/null @@ -1,74 +0,0 @@ -# Key concepts - -## Scope - -- is a lifespan of a dependency; -- is required almost for each provider; -- in frameworks' integrations some scopes are entered automatically; -- dependencies of **Resource** and **Singleton** provider cached for the lifespan of its scope; - -### Default scopes - -**APP**: - - - tied to the entire application lifetime; - - can be used for singletons of **Resource** and **Singleton** providers; - - must be managed manually in lifecycle methods of the application: see **integrations** section; - -**SESSION**: - - - for websocket session lifetime; - - dependencies of this scope cannot be used for http-requests; - - managed automatically; - -**REQUEST**: - - - for dependencies which are created for each user request, for example database session; - - managed automatically for http-request; - - must be managed manually for websockets; - -**ACTION**: - - - for lifetime less than request; - - must be managed manually; - -**STEP**: - - - for lifetime less than **ACTION**; - - must be managed manually. - -## Provider - -Providers needed to assemble the objects. -They retrieve the underlying dependencies and inject them into the created object. -It causes the cascade effect that helps to assemble object graphs. - -More about providers: - -- do not contain assembled objects: - - **Singleton** and **Resource** objects are stored in container; - - **Factory** objects are built on each call. -- can have dependencies only of the same or more long-lived scopes: - - **APP**-scoped provider can have only **APP**-scoped dependencies; - - **SESSION**-scoped provider can have APP and **SESSION**-scoped dependencies, etc.; - -## Container - -Each container is assigned to a certain scope. -To enter a nested scope, a context manager should be used. -Nested scope contains link to parent container. - -All states live in containers: - -- assembled objects; -- context stacks for resources to finalize them at the end of lifecycle; -- overrides for tests; - -## Graph - -Graph is a collection of providers. They annot be instantiated. - -Graph can initialize its resources and singletons to container: - -- coroutine **async_resolve_creators** should be used to resolve asynchronously; -- function **sync_resolve_creators** should be used to resolve synchronously. diff --git a/docs/migration/from-that-depends.md b/docs/migration/from-that-depends.md deleted file mode 100644 index 1de74e8..0000000 --- a/docs/migration/from-that-depends.md +++ /dev/null @@ -1,146 +0,0 @@ -# Migration from `that-depends` - -## 1. Install `modern-di` using your favorite tool: - -If you need only `modern-di` without integrations: - -=== "uv" - - ```bash - uv add modern-di - ``` - -=== "pip" - - ```bash - pip install modern-di - ``` - -=== "poetry" - - ```bash - poetry add modern-di - ``` - -If you need to integrate with some framework, then install `modern-di-*`. - -## 2. Migrate dependencies graph -1. Use `modern-di.BaseGraph` instead of `that-depends.BaseContainer`. -2. Add scopes to all providers. - - Most of the providers will be `APP` scope. - - ContextResource usually becomes `Resource` of `REQUEST`-scope. - - Dependents of ContextResource usually has `REQUEST`-scope as well. - -=== "that-depends" - - ```python - from that_depends import BaseContainer, providers - - from app import repositories - from app.resources.db import create_sa_engine, create_session - - - class Dependencies(BaseContainer): - database_engine = providers.Resource(create_sa_engine, settings=settings.cast) - session = providers.ContextResource(create_session, engine=database_engine.cast) - decks_service = providers.Factory(repositories.DecksService, session=session) - cards_service = providers.Factory(repositories.CardsService, session=session) - ``` - -=== "modern-di" - - ```python - from modern_di import BaseGraph, Scope, providers - - from app import repositories - from app.resources.db import create_sa_engine, create_session - - - class Dependencies(BaseGraph): - database_engine = providers.Resource(Scope.APP, create_sa_engine) - session = providers.Resource(Scope.REQUEST, create_session, engine=database_engine.cast) - - decks_service = providers.Factory(Scope.REQUEST, repositories.DecksService, session=session.cast) - cards_service = providers.Factory(Scope.REQUEST, repositories.CardsService, session=session.cast) - ``` - -## 3. Migrate integration with framework - -=== "fastapi" - - ```python - import contextlib - import typing - - import fastapi - import modern_di_fastapi - - from app.ioc import Dependencies - - - app = fastapi.FastAPI(lifespan=lifespan_manager) - modern_di_fastapi.setup_di(app) - ``` - -=== "litestar" - - ```python - import contextlib - import typing - - from litestar import Litestar - import modern_di_litestar - - - app = Litestar( - route_handlers=[...], - plugins=[modern_di_litestar.ModernDIPlugin()], - ) - ``` - -## 4. Migrate routes - -=== "fastapi" - - For `fastapi` replace `fastapi.Depends` with `modern_di_fastapi.FromDI`: - - ```python - import typing - - import fastapi - from modern_di_fastapi import FromDI - - from app import ioc, schemas - from app.repositories import DecksService - - - ROUTER: typing.Final = fastapi.APIRouter() - - - @ROUTER.get("/decks/") - async def list_decks( - decks_service: DecksService = FromDI(ioc.Dependencies.decks_service), - ) -> schemas.Decks: - objects = await decks_service.list() - return typing.cast(schemas.Decks, {"items": objects}) - ``` - -=== "litestar" - - For `litestar` replace `litestar.di.Provide` with `modern_di_litestar.FromDI` - - ```python - import litestar - import modern_di_litestar - - from app import ioc, schemas - from app.repositories import DecksService - - - @litestar.get("/decks/", dependencies={ - "decks_service": modern_di_litestar.FromDI(ioc.Dependencies.decks_service), - }) - async def list_decks(decks_service: DecksService) -> schemas.Decks: - objects = await decks_service.list() - return schemas.Decks(items=objects) - ``` diff --git a/docs/providers/collections.md b/docs/providers/collections.md deleted file mode 100644 index 7b00a28..0000000 --- a/docs/providers/collections.md +++ /dev/null @@ -1,43 +0,0 @@ -# Collections - -There are several collection providers: `List` and `Dict` - -## List - -- List provider contains other providers. -- Resolves into list of dependencies. - -```python -import random -from modern_di import BaseGraph, Container, Scope, providers - - -class Dependencies(BaseGraph): - random_number = providers.Factory(Scope.APP, random.random) - numbers_sequence = providers.List(Scope.APP, random_number, random_number) - - -with Container(scope=Scope.APP) as container: - print(Dependencies.numbers_sequence.sync_resolve(container)) - # [0.3035656170071561, 0.8280498192037787] -``` - -## Dict - -- Dict provider is a collection of named providers. -- Resolves into dict of dependencies. - -```python -import random -from modern_di import BaseGraph, Container, Scope, providers - - -class Dependencies(BaseGraph): - random_number = providers.Factory(Scope.APP, random.random) - numbers_map = providers.Dict(Scope.APP, key1=random_number, key2=random_number) - - -with Container(scope=Scope.APP) as container: - print(Dependencies.numbers_map.sync_resolve(container)) - # {'key1': 0.6851384528299208, 'key2': 0.41044920948045294} -``` diff --git a/docs/providers/context-providers.md b/docs/providers/context-providers.md deleted file mode 100644 index aff6e14..0000000 --- a/docs/providers/context-providers.md +++ /dev/null @@ -1,60 +0,0 @@ -# Context Providers - -There are several providers with access to container's context. - -## Selector - -- Receives context unpacked to callable object. -- Selector provider chooses between a provider based on a key. -- Resolves into a single dependency. - -```python -import typing - -from modern_di import BaseGraph, Container, Scope, providers - - -class StorageService(typing.Protocol): - ... - -class StorageServiceLocal(StorageService): - ... - -class StorageServiceRemote(StorageService): - ... - -def selector_function(*, storage_backend: str | None = None, **_: object) -> str: - return storage_backend or "local" - -class Dependencies(BaseGraph): - storage_service: providers.Selector[StorageService] = providers.Selector( - Scope.APP, - selector_function, - local=providers.Factory(Scope.APP, StorageServiceLocal), - remote=providers.Factory(Scope.APP, StorageServiceRemote), - ) - -with Container(scope=Scope.APP, context={"storage_backend": "remote"}) as container: - print(type(Dependencies.storage_service.sync_resolve(container))) - # StorageServiceRemote -``` - -## ContextAdapter - -- Receives context unpacked to callable object. -- Can be used in another providers to access data from context. - -```python -from modern_di import BaseGraph, Container, Scope, providers - - -def context_adapter_function(*, storage_backend: str | None = None, **_: object) -> str: - return storage_backend or "local" - -class Dependencies(BaseGraph): - context_adapter = providers.ContextAdapter(Scope.APP, context_adapter_function) - -with Container(scope=Scope.APP, context={"storage_backend": "remote"}) as container: - print(Dependencies.context_adapter.sync_resolve(container)) - # "remote" -``` diff --git a/docs/providers/factories.md b/docs/providers/factories.md deleted file mode 100644 index d786ca0..0000000 --- a/docs/providers/factories.md +++ /dev/null @@ -1,48 +0,0 @@ -# Factories -Factories are initialized on every call. - -## Factory -- Class or simple function is allowed. - -```python -import dataclasses - -from modern_di import BaseGraph, Container, Scope, providers - - -@dataclasses.dataclass(kw_only=True, slots=True) -class IndependentFactory: - dep1: str - dep2: int - - -class Dependencies(BaseGraph): - independent_factory = providers.Factory(Scope.APP, IndependentFactory, dep1="text", dep2=123) - - -with Container(scope=Scope.APP) as container: - instance = Dependencies.independent_factory.sync_resolve(container) - assert isinstance(instance, IndependentFactory) -``` - -## AsyncFactory -- Async function is required. - -```python -import datetime - -from modern_di import BaseGraph, Container, Scope, providers - - -async def async_factory() -> datetime.datetime: - return datetime.datetime.now(tz=datetime.timezone.utc) - - -class Dependencies(BaseGraph): - async_factory = providers.AsyncFactory(Scope.APP, async_factory) - - -async with Container(scope=Scope.APP) as container: - instance = await Dependencies.async_factory.async_resolve(container) - assert isinstance(instance, datetime.datetime) -``` diff --git a/docs/providers/object.md b/docs/providers/object.md deleted file mode 100644 index 26bbd43..0000000 --- a/docs/providers/object.md +++ /dev/null @@ -1,15 +0,0 @@ -# Object - -Object provider returns an object “as is”. - -```python -from modern_di import BaseGraph, Container, Scope, providers - - -class Dependencies(BaseGraph): - object_provider = providers.Object(Scope.APP, 1) - - -with Container(scope=Scope.APP) as container: - assert Dependencies.object_provider.sync_resolve(container) == 1 -``` diff --git a/docs/providers/resources.md b/docs/providers/resources.md deleted file mode 100644 index 5c60081..0000000 --- a/docs/providers/resources.md +++ /dev/null @@ -1,51 +0,0 @@ -# Resource - -- Resources are initialized only once per scope and have teardown logic. -- Generator or async generator is required. -```python -import typing - -from modern_di import BaseGraph, Container, Scope, providers - - -def create_sync_resource() -> typing.Iterator[str]: - # resource initialization - try: - yield "sync resource" - finally: - pass # resource teardown - - -async def create_async_resource() -> typing.AsyncIterator[str]: - # resource initialization - try: - yield "async resource" - finally: - pass # resource teardown - - -class Dependencies(BaseGraph): - sync_resource = providers.Resource(Scope.APP, create_sync_resource) - async_resource = providers.Resource(Scope.REQUEST, create_async_resource) - - -with Container(scope=Scope.APP) as container: - # sync resource of app scope - sync_resource_instance = Dependencies.sync_resource.sync_resolve(container) - async with container.build_child_container(scope=Scope.REQUEST) as request_container: - # async resource of request scope - async_resource_instance = await Dependencies.async_resource.async_resolve(request_container) -``` - -## Concurrency safety - -`Resource` is safe to use in threading and asyncio concurrency: - -```python -with Container(scope=Scope.APP) as container: - # calling async_resolve concurrently in different coroutines will create only one instance - await Dependencies.sync_resource.async_resolve(container) - - # calling sync_resolve concurrently in different threads will create only one instance - Dependencies.sync_resource.sync_resolve(container) -``` diff --git a/docs/providers/singletons.md b/docs/providers/singletons.md deleted file mode 100644 index 7014bd1..0000000 --- a/docs/providers/singletons.md +++ /dev/null @@ -1,55 +0,0 @@ -# Singleton and AsyncSingleton - -- resolve the dependency only once and cache the resolved instance for future injections; -- class or simple function is allowed. - -## How it works - -```python -import asyncio -import datetime -import pytest -import random - -from modern_di import BaseGraph, Container, Scope, providers - - -def generate_random_number() -> float: - return random.random() - -async def async_creator() -> datetime.datetime: - await asyncio.sleep(0) - return datetime.datetime.now(tz=datetime.timezone.utc) - - -class Dependencies(BaseGraph): - singleton = providers.Singleton(Scope.APP, generate_random_number) - async_singleton = providers.AsyncSingleton(Scope.APP, async_creator) - - -with Container(scope=Scope.APP) as container: - # sync resolving - singleton_instance1 = Dependencies.singleton.sync_resolve(container) - with pytest.raises(RuntimeError, match="AsyncSingleton cannot be resolved synchronously"): - Dependencies.async_singleton.sync_resolve(container) - - # async resolving - singleton_instance2 = await Dependencies.singleton.async_resolve(container) - async_singleton_instance = await Dependencies.async_singleton.async_resolve(container) - - # if resolved in the same container, the instance will be the same - assert singleton_instance1 is singleton_instance2 -``` - -## Concurrency safety - -`Singleton` is safe to use in threading and asyncio concurrency: - -```python -with Container(scope=Scope.APP) as container: - # calling async_resolve concurrently in different coroutines will create only one instance - await Dependencies.singleton.async_resolve(container) - - # calling sync_resolve concurrently in different threads will create only one instance - Dependencies.singleton.sync_resolve(container) -``` diff --git a/docs/testing/fixtures.md b/docs/testing/fixtures.md deleted file mode 100644 index 31d4c9a..0000000 --- a/docs/testing/fixtures.md +++ /dev/null @@ -1,64 +0,0 @@ -# Fixtures for testing - -## 1. Add required fixtures: - -```python -import typing - -import fastapi -import modern_di -import modern_di_fastapi -import pytest - -from app import ioc - - -# application object can be imported from somewhere -application = fastapi.FastAPI() -modern_di_fastapi.setup_di(application) - - -@pytest.fixture -async def di_container() -> typing.AsyncIterator[modern_di.Container]: - """Fixture with APP-scope container.""" - di_container_: typing.Final = modern_di_fastapi.fetch_di_container(application) - async with di_container_: - yield di_container_ - - -@pytest.fixture -async def request_di_container(di_container: modern_di.Container) -> typing.AsyncIterator[modern_di.Container]: - """Fixture with REQUEST-scope container.""" - async with di_container.build_child_container(scope=modern_di.Scope.REQUEST) as request_container: - yield request_container - - -@pytest.fixture -def mock_dependencies(di_container: modern_di.Container) -> None: - """Mock dependencies for tests.""" - ioc.Dependencies.simple_factory.override(ioc.SimpleFactory(dep1="mock", dep2=777), container=di_container) -``` - -## 2. Use fixtures in tests: - -```python -import pytest -from modern_di import Container - -from app.ioc import Dependencies - - -async def test_with_app_scope(di_container: Container) -> None: - sync_resource_instance = await Dependencies.sync_resource.async_resolve(di_container) - # do sth with dependency - - -async def test_with_request_scope(request_di_container: Container) -> None: - simple_factory_instance = await Dependencies.simple_factory.async_resolve(request_di_container) - # do sth with dependency - -@pytest.mark.usefixtures("mock_dependencies") -async def test_with_request_scope_mocked(request_di_container: Container) -> None: - simple_factory_instance = await Dependencies.simple_factory.async_resolve(request_di_container) - # dependency is mocked here -``` From 805677d2336117d5c787d841a9f1c93c0745b2ad Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 27 Apr 2025 12:22:01 +0300 Subject: [PATCH 4/7] update docs --- README.md | 28 +++++++++++++---- docs/index.md | 8 ++--- docs/integrations/fastapi.md | 47 ++++++++++++++++++++++++++++ docs/integrations/faststream.md | 51 +++++++++++++++++++++++++++++++ docs/integrations/free.md | 38 +++++++++++++++++++++++ docs/integrations/litestar.md | 47 ++++++++++++++++++++++++++++ docs/introduction/installation.md | 41 +++++++++++++++++++++++++ mkdocs.yml | 8 +++-- 8 files changed, 256 insertions(+), 12 deletions(-) create mode 100644 docs/integrations/fastapi.md create mode 100644 docs/integrations/faststream.md create mode 100644 docs/integrations/free.md create mode 100644 docs/integrations/litestar.md create mode 100644 docs/introduction/installation.md diff --git a/README.md b/README.md index 640dadb..0d05a82 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,27 @@ Lite-Bootstrap [![downloads](https://img.shields.io/pypi/dm/lite-bootstrap.svg)](https://pypistats.org/packages/lite-bootstrap) [![GitHub stars](https://img.shields.io/github/stars/modern-python/lite-bootstrap)](https://github.com/modern-python/lite-bootstrap/stargazers) -This package helps to build new microservices +`lite-bootstrap` assists you in creating applications with all the necessary instruments already set up. -## Quickstart: -### Installation +With `lite-bootstrap`, you receive an application with lightweight built-in support for: +- `sentry` +- `prometheus` +- `opentelemetry` +- `structlog` +- `cors` +- `swagger` - with additional offline version support +- `health-checks` -```shell -$ pip install lite-bootstrap -``` +Those instruments can be bootstrapped for: + +1. LiteStar +2. FastStream +3. FastAPI +4. services and scripts without frameworks +--- + +## 📚 [Documentation](https://lite-bootstrap.readthedocs.io) + +## 📦 [PyPi](https://pypi.org/project/lite-bootstrap) + +## 📝 [License](LICENSE) diff --git a/docs/index.md b/docs/index.md index 788f220..878ea77 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,9 +14,9 @@ With `lite-bootstrap`, you receive an application with lightweight built-in supp - `health-checks` Those instruments can be bootstrapped for: -- `fastapi`, -- `litestar`, -- `faststream`, -- services without these frameworks. +1. [LiteStar](integrations/litestar) +2. [FastStream](integrations/faststream) +3. [FastAPI](integrations/fastapi) +4. [services and scripts without frameworks](integrations/free) --- diff --git a/docs/integrations/fastapi.md b/docs/integrations/fastapi.md new file mode 100644 index 0000000..743432f --- /dev/null +++ b/docs/integrations/fastapi.md @@ -0,0 +1,47 @@ +# Usage with `Fastapi` + +*Another example of usage with FastAPI - [fastapi-sqlalchemy-template](https://github.com/modern-python/fastapi-sqlalchemy-template)* + +## 1. Install `lite-bootstrapp[fastapi-all]`: + +=== "uv" + + ```bash + uv add lite-bootstrapp[fastapi-all] + ``` + +=== "pip" + + ```bash + pip install lite-bootstrapp[fastapi-all] + ``` + +=== "poetry" + + ```bash + poetry add lite-bootstrapp[fastapi-all] + ``` + +Read more about available extras [here](../../introduction/installation): + +## 2. Define bootstrapper config and build you application: + +```python +from lite_bootstrap import FastAPIConfig, FastAPIBootstrapper + + +bootstrapper_config = FastAPIConfig( + service_name="microservice", + service_version="2.0.0", + service_environment="test", + service_debug=False, + cors_allowed_origins=["http://test"], + health_checks_path="/custom-health/", + opentelemetry_endpoint="otl", + prometheus_metrics_path="/custom-metrics/", + sentry_dsn="https://testdsn@localhost/1", + swagger_offline_docs=True, +) +bootstrapper = FastAPIBootstrapper(bootstrapper_config) +application = bootstrapper.bootstrap() +``` diff --git a/docs/integrations/faststream.md b/docs/integrations/faststream.md new file mode 100644 index 0000000..f9454ad --- /dev/null +++ b/docs/integrations/faststream.md @@ -0,0 +1,51 @@ +# Usage with `FastStream` + +## 1. Install `lite-bootstrapp[faststream-all]`: + +=== "uv" + + ```bash + uv add lite-bootstrapp[faststream-all] + ``` + +=== "pip" + + ```bash + pip install lite-bootstrapp[faststream-all] + ``` + +=== "poetry" + + ```bash + poetry add lite-bootstrapp[faststream-all] + ``` + +Read more about available extras [here](../../introduction/installation): + +## 2. Define bootstrapper config and build you application: + +```python +from lite_bootstrap import FastStreamConfig, FastStreamBootstrapper +from faststream.redis.opentelemetry import RedisTelemetryMiddleware +from faststream.redis.prometheus import RedisPrometheusMiddleware +from faststream.redis import RedisBroker + + +broker = RedisBroker() +bootstrapper_config = FastStreamConfig( + service_name="microservice", + service_version="2.0.0", + service_environment="test", + service_debug=False, + opentelemetry_endpoint="otl", + opentelemetry_middleware_cls=RedisTelemetryMiddleware, + prometheus_metrics_path="/custom-metrics/", + prometheus_middleware_cls=RedisPrometheusMiddleware, + sentry_dsn="https://testdsn@localhost/1", + health_checks_path="/custom-health/", + logging_buffer_capacity=0, + broker=broker, +) +bootstrapper = FastStreamBootstrapper(bootstrapper_config) +application = bootstrapper.bootstrap() +``` diff --git a/docs/integrations/free.md b/docs/integrations/free.md new file mode 100644 index 0000000..978b262 --- /dev/null +++ b/docs/integrations/free.md @@ -0,0 +1,38 @@ +# Usage without frameworks + +## 1. Install `lite-bootstrapp[free-all]`: + +=== "uv" + + ```bash + uv add lite-bootstrapp[free-all] + ``` + +=== "pip" + + ```bash + pip install lite-bootstrapp[free-all] + ``` + +=== "poetry" + + ```bash + poetry add lite-bootstrapp[free-all] + ``` + +Read more about available extras [here](../../introduction/installation): + +## 2. Define bootstrapper config and build you application: + +```python +from lite_bootstrap import FreeBootstrapperConfig, FreeBootstrapper + + +bootstrapper_config = FreeBootstrapperConfig( + service_debug=False, + opentelemetry_endpoint="otl", + sentry_dsn="https://testdsn@localhost/1", +) +bootstrapper = FreeBootstrapper(bootstrapper_config) +bootstrapper.bootstrap() +``` diff --git a/docs/integrations/litestar.md b/docs/integrations/litestar.md new file mode 100644 index 0000000..95bc62e --- /dev/null +++ b/docs/integrations/litestar.md @@ -0,0 +1,47 @@ +# Usage with `Litestar` + +*Another example of usage with LiteStar - [litestar-sqlalchemy-template](https://github.com/modern-python/litestar-sqlalchemy-template)* + +## 1. Install `lite-bootstrap[litestar-all]`: + +=== "uv" + + ```bash + uv add lite-bootstrapp[litestar-all] + ``` + +=== "pip" + + ```bash + pip install lite-bootstrapp[litestar-all] + ``` + +=== "poetry" + + ```bash + poetry add lite-bootstrapp[litestar-all] + ``` + +Read more about available extras [here](../introduction/installation): + +## 2. Define bootstrapper config and build you application: + +```python +from lite_bootstrap import LitestarConfig, LitestarBootstrapper + + +bootstrapper_config = LitestarConfig( + service_name="microservice", + service_version="2.0.0", + service_environment="test", + service_debug=False, + cors_allowed_origins=["http://test"], + health_checks_path="/custom-health/", + opentelemetry_endpoint="otl", + prometheus_metrics_path="/custom-metrics/", + sentry_dsn="https://testdsn@localhost/1", + swagger_offline_docs=True, +) +bootstrapper = LitestarBootstrapper(bootstrapper_config) +application = bootstrapper.bootstrap() +``` diff --git a/docs/introduction/installation.md b/docs/introduction/installation.md new file mode 100644 index 0000000..ba2b2bc --- /dev/null +++ b/docs/introduction/installation.md @@ -0,0 +1,41 @@ +# Installation + +## Choose suitable extras + +You can choose required framework and instruments using this table: + +| Instrument | Litestar | Faststream | FastAPI | Free Bootstrapper, without framework | +|---------------|--------------------|----------------------|-------------------|--------------------------------------| +| sentry | `litestar-sentry` | `faststream-sentry` | `fastapi-sentry` | `sentry` | +| prometheus | `litestar-metrics` | `faststream-metrics` | `fastapi-metrics` | not used | +| opentelemetry | `litestar-otl` | `faststream-otl` | `fastapi-otl` | `otl` | +| structlog | `litestar-logging` | `faststream-logging` | `fastapi-logging` | `logging` | +| cors | no extra | not used | no extra | not used | +| swagger | no extra | not used | no extra | not used | +| health-checks | no extra | no extra | no extra | not used | +| all | `litestar-all` | `faststream-all` | `fastapi-all` | `free-all` | + +* not used - means that the instrument is not implemented in the integration. +* no extra - means that the instrument requires no additional dependencies. + +## Install `lite-bootstrap` using your favorite tool with choosen extras + +For example, if you want to bootstrap litestar with structlog and opentelemetry instruments: + +=== "uv" + + ```bash + uv add lite-bootstrapp[litestar-logging,litestar-otl] + ``` + +=== "pip" + + ```bash + pip install lite-bootstrapp[litestar-logging,litestar-otl] + ``` + +=== "poetry" + + ```bash + poetry add lite-bootstrapp[litestar-logging,litestar-otl] + ``` diff --git a/mkdocs.yml b/mkdocs.yml index f0a196a..af694cd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,8 +3,12 @@ repo_url: https://github.com/modern-python/lite-bootstrap docs_dir: docs edit_uri: edit/main/docs/ nav: - - Quick-Start: index.md - + - Introduction: + - Installation: introduction/installation.md + - Integrations: + - Litestar: integrations/litestar.md + - FastStream: integrations/faststream.md + - FastAPI: integrations/fastapi.md theme: name: material features: From 5add2a091234a30abf5ea38f2ac7b6b5e3ae6cff Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 27 Apr 2025 13:50:01 +0300 Subject: [PATCH 5/7] add configuration docs --- docs/index.md | 8 +- docs/integrations/fastapi.md | 2 + docs/integrations/faststream.md | 2 + docs/integrations/free.md | 2 + docs/integrations/litestar.md | 2 + docs/introduction/configuration.md | 128 ++++++++++++++++++ .../bootstrappers/fastapi_bootstrapper.py | 2 +- mkdocs.yml | 1 + 8 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 docs/introduction/configuration.md diff --git a/docs/index.md b/docs/index.md index 878ea77..68ab3ba 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,8 +15,8 @@ With `lite-bootstrap`, you receive an application with lightweight built-in supp Those instruments can be bootstrapped for: -1. [LiteStar](integrations/litestar) -2. [FastStream](integrations/faststream) -3. [FastAPI](integrations/fastapi) -4. [services and scripts without frameworks](integrations/free) +- [LiteStar](integrations/litestar) +- [FastStream](integrations/faststream) +- [FastAPI](integrations/fastapi) +- [services and scripts without frameworks](integrations/free) --- diff --git a/docs/integrations/fastapi.md b/docs/integrations/fastapi.md index 743432f..94526d9 100644 --- a/docs/integrations/fastapi.md +++ b/docs/integrations/fastapi.md @@ -45,3 +45,5 @@ bootstrapper_config = FastAPIConfig( bootstrapper = FastAPIBootstrapper(bootstrapper_config) application = bootstrapper.bootstrap() ``` + +Read more about available configuration options [here](../../introduction/configuration): diff --git a/docs/integrations/faststream.md b/docs/integrations/faststream.md index f9454ad..28c68e1 100644 --- a/docs/integrations/faststream.md +++ b/docs/integrations/faststream.md @@ -49,3 +49,5 @@ bootstrapper_config = FastStreamConfig( bootstrapper = FastStreamBootstrapper(bootstrapper_config) application = bootstrapper.bootstrap() ``` + +Read more about available configuration options [here](../../introduction/configuration): diff --git a/docs/integrations/free.md b/docs/integrations/free.md index 978b262..2268d75 100644 --- a/docs/integrations/free.md +++ b/docs/integrations/free.md @@ -36,3 +36,5 @@ bootstrapper_config = FreeBootstrapperConfig( bootstrapper = FreeBootstrapper(bootstrapper_config) bootstrapper.bootstrap() ``` + +Read more about available configuration options [here](../../introduction/configuration): diff --git a/docs/integrations/litestar.md b/docs/integrations/litestar.md index 95bc62e..56b4e62 100644 --- a/docs/integrations/litestar.md +++ b/docs/integrations/litestar.md @@ -45,3 +45,5 @@ bootstrapper_config = LitestarConfig( bootstrapper = LitestarBootstrapper(bootstrapper_config) application = bootstrapper.bootstrap() ``` + +Read more about available configuration options [here](../../introduction/configuration): diff --git a/docs/introduction/configuration.md b/docs/introduction/configuration.md new file mode 100644 index 0000000..5b6f0a2 --- /dev/null +++ b/docs/introduction/configuration.md @@ -0,0 +1,128 @@ +# Configuration + +## Sentry + +Sentry integration uses `sentry_sdk` package under the hood. + +To bootstrap Sentry, you must provide at least: + +- `sentry_dsn` - tells sentry-sdk where to send the events. + +Additional parameters can also be supplied through the settings object: + +- `sentry_traces_sample_rate` - in the range of 0.0 to 1.0, the percentage chance a given transaction will be sent +- `sentry_sample_rate` - in the range of 0.0 to 1.0, the sample rate for error events +- `sentry_max_breadcrumbs` - the total amount of breadcrumbs +- `sentry_max_value_length` - the max event payload length +- `sentry_attach_stacktrace` - if True, stack traces are automatically attached to all messages logged +- `sentry_integrations` - list of integrations to enable +- `sentry_tags` - key/value string pairs that are both indexed and searchable +- `sentry_additional_params** - additional params, which will be passed to `sentry_sdk.init` + +Read more about sentry_sdk params [here](https://docs.sentry.io/platforms/python/configuration/options/). + + +## Prometheus + +To bootstrap Prometheus, you must provide at least: + +- `prometheus_metrics_path`. + +Additional parameters: + +- `prometheus_metrics_include_in_schema`. + +### Prometheus Litestar + +Prometheus's integration for Litestar requires `prometheus_client` package. + +Additional parameters for Litestar integration: + +- `prometheus_additional_params` - passed to `litestar.plugins.prometheus.PrometheusConfig`. + +### Prometheus FastStream + +Prometheus's integration for FastStream requires `prometheus_client` package. + +To bootstrap Prometheus for FastStream, you must provide additionally: + +- `prometheus_middleware_cls`. + +### Prometheus FastAPI + +Prometheus's integration for FastAPI uses `prometheus_fastapi_instrumentator` package. + +Additional parameters for FastAPI integration: + +- `prometheus_instrumentator_params` - passed to `prometheus_fastapi_instrumentator.Instrumentator` +- `prometheus_instrument_params` - passed to `method Instrumentator(...).instrument` +- `prometheus_expose_params` - passed to `method Instrumentator(...).expose`. + + +## Opentelemetry + +To bootstrap Opentelemetry, you must provide at least: + +- `opentelemetry_endpoint`. + +Additional parameters: + +- `opentelemetry_service_name` +- `opentelemetry_container_name` +- `opentelemetry_endpoint` +- `opentelemetry_namespace` +- `opentelemetry_insecure` +- `opentelemetry_instrumentors` +- `opentelemetry_span_exporter` + +Additional parameters for Litestar and FastAPI: + +- `opentelemetry_excluded_urls` - by default, heath checks and metrics paths will be excluded. + +For FastStream you must provide additionally: + +- `opentelemetry_middleware_cls` + + +## Structlog + +To bootstrap Structlog, you must set `service_debug` to False + +Additional parameters: + +- `logging_log_level` +- `logging_flush_level` +- `logging_buffer_capacity` +- `logging_extra_processors` +- `logging_unset_handlers`. + +## CORS + +To bootstrap CORS headers, you must provide `cors_allowed_origins` or `cors_allowed_origin_regex`. + +Additional params: + +- `cors_allowed_methods` +- `cors_allowed_headers` +- `cors_exposed_headers` +- `cors_allowed_credentials` +- `cors_max_age` + +## Swagger + +To bootstrap swagger, you have the following parameters: + +- `swagger_static_path` - path for offline docs static +- `swagger_path` +- `swagger_offline_docs` - option to turn on offline docs. + +For Litestar `swagger_path` is required to bootstrap swagger instrument. + +## Health checks + +To bootstrap Health checks, you must provide set `health_checks_enabled` to True. + +Additional params: + +- `health_checks_path` +- `health_checks_include_in_schema` diff --git a/lite_bootstrap/bootstrappers/fastapi_bootstrapper.py b/lite_bootstrap/bootstrappers/fastapi_bootstrapper.py index 7cd8960..e613193 100644 --- a/lite_bootstrap/bootstrappers/fastapi_bootstrapper.py +++ b/lite_bootstrap/bootstrappers/fastapi_bootstrapper.py @@ -130,7 +130,7 @@ def check_dependencies() -> bool: return import_checker.is_prometheus_fastapi_instrumentator_installed def bootstrap(self) -> None: - Instrumentator(**self.bootstrap_config.prometheus_instrument_params).instrument( + Instrumentator(**self.bootstrap_config.prometheus_instrumentator_params).instrument( self.bootstrap_config.application, **self.bootstrap_config.prometheus_instrument_params, ).expose( diff --git a/mkdocs.yml b/mkdocs.yml index af694cd..0f6be7d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,6 +5,7 @@ edit_uri: edit/main/docs/ nav: - Introduction: - Installation: introduction/installation.md + - Configuration: introduction/configuration.md - Integrations: - Litestar: integrations/litestar.md - FastStream: integrations/faststream.md From 3d555ac5cffa4ca590058ec4b7c7e0c1246f1c8c Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 27 Apr 2025 13:52:17 +0300 Subject: [PATCH 6/7] fix --- docs/integrations/fastapi.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/integrations/fastapi.md b/docs/integrations/fastapi.md index 94526d9..fba0bae 100644 --- a/docs/integrations/fastapi.md +++ b/docs/integrations/fastapi.md @@ -22,7 +22,7 @@ poetry add lite-bootstrapp[fastapi-all] ``` -Read more about available extras [here](../../introduction/installation): +Read more about available extras [here](../../../introduction/installation): ## 2. Define bootstrapper config and build you application: @@ -46,4 +46,4 @@ bootstrapper = FastAPIBootstrapper(bootstrapper_config) application = bootstrapper.bootstrap() ``` -Read more about available configuration options [here](../../introduction/configuration): +Read more about available configuration options [here](../../../introduction/configuration): From 7ff36a6477f8d90c82fb0567a8912acb9ae3c060 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 27 Apr 2025 13:53:56 +0300 Subject: [PATCH 7/7] fix --- docs/integrations/faststream.md | 4 ++-- docs/integrations/free.md | 4 ++-- docs/integrations/litestar.md | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/integrations/faststream.md b/docs/integrations/faststream.md index 28c68e1..19d831a 100644 --- a/docs/integrations/faststream.md +++ b/docs/integrations/faststream.md @@ -20,7 +20,7 @@ poetry add lite-bootstrapp[faststream-all] ``` -Read more about available extras [here](../../introduction/installation): +Read more about available extras [here](../../../introduction/installation): ## 2. Define bootstrapper config and build you application: @@ -50,4 +50,4 @@ bootstrapper = FastStreamBootstrapper(bootstrapper_config) application = bootstrapper.bootstrap() ``` -Read more about available configuration options [here](../../introduction/configuration): +Read more about available configuration options [here](../../../introduction/configuration): diff --git a/docs/integrations/free.md b/docs/integrations/free.md index 2268d75..5f74c05 100644 --- a/docs/integrations/free.md +++ b/docs/integrations/free.md @@ -20,7 +20,7 @@ poetry add lite-bootstrapp[free-all] ``` -Read more about available extras [here](../../introduction/installation): +Read more about available extras [here](../../../introduction/installation): ## 2. Define bootstrapper config and build you application: @@ -37,4 +37,4 @@ bootstrapper = FreeBootstrapper(bootstrapper_config) bootstrapper.bootstrap() ``` -Read more about available configuration options [here](../../introduction/configuration): +Read more about available configuration options [here](../../../introduction/configuration): diff --git a/docs/integrations/litestar.md b/docs/integrations/litestar.md index 56b4e62..ca98070 100644 --- a/docs/integrations/litestar.md +++ b/docs/integrations/litestar.md @@ -22,7 +22,7 @@ poetry add lite-bootstrapp[litestar-all] ``` -Read more about available extras [here](../introduction/installation): +Read more about available extras [here](../../../introduction/installation): ## 2. Define bootstrapper config and build you application: @@ -46,4 +46,4 @@ bootstrapper = LitestarBootstrapper(bootstrapper_config) application = bootstrapper.bootstrap() ``` -Read more about available configuration options [here](../../introduction/configuration): +Read more about available configuration options [here](../../../introduction/configuration):