{"id":28409654,"url":"https://github.com/woltapp/magic-di","last_synced_at":"2025-06-25T03:32:12.898Z","repository":{"id":230252628,"uuid":"772068701","full_name":"woltapp/magic-di","owner":"woltapp","description":"Dependency Injector with minimal boilerplate code, built-in support for FastAPI and Celery, and seamless integration to basically anything.","archived":false,"fork":false,"pushed_at":"2025-03-01T00:12:14.000Z","size":773,"stargazers_count":59,"open_issues_count":11,"forks_count":1,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-06-09T07:56:02.142Z","etag":null,"topics":["asyncio","celery","dependecy-container","dependency-injection","fastapi","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/woltapp.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-03-14T13:30:03.000Z","updated_at":"2025-05-25T15:43:48.000Z","dependencies_parsed_at":"2024-05-01T01:28:01.220Z","dependency_job_id":"a0979e88-78bf-4b98-ad11-0ee8cd5c1da7","html_url":"https://github.com/woltapp/magic-di","commit_stats":null,"previous_names":["woltapp/magic-di"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/woltapp/magic-di","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/woltapp%2Fmagic-di","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/woltapp%2Fmagic-di/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/woltapp%2Fmagic-di/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/woltapp%2Fmagic-di/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/woltapp","download_url":"https://codeload.github.com/woltapp/magic-di/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/woltapp%2Fmagic-di/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261798230,"owners_count":23211137,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asyncio","celery","dependecy-container","dependency-injection","fastapi","python"],"created_at":"2025-06-02T09:42:13.162Z","updated_at":"2025-06-25T03:32:12.889Z","avatar_url":"https://github.com/woltapp.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# magic-di\n\n[![PyPI](https://img.shields.io/pypi/v/magic-di?style=flat-square)](https://pypi.python.org/pypi/magic-di/)\n[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/magic-di?style=flat-square)](https://pypi.python.org/pypi/magic-di/)\n[![PyPI - License](https://img.shields.io/pypi/l/magic-di?style=flat-square)](https://pypi.python.org/pypi/magic-di/)\n[![Coookiecutter - Wolt](https://img.shields.io/badge/cookiecutter-Wolt-00c2e8?style=flat-square\u0026logo=cookiecutter\u0026logoColor=D4AA00\u0026link=https://github.com/woltapp/wolt-python-package-cookiecutter)](https://github.com/woltapp/wolt-python-package-cookiecutter)\n\n\n---\n\n**Documentation**: [https://woltapp.github.io/magic-di](https://woltapp.github.io/magic-di)\n\n**Source Code**: [https://github.com/woltapp/magic-di](https://github.com/woltapp/magic-di)\n\n**PyPI**: [https://pypi.org/project/magic-di/](https://pypi.org/project/magic-di/)\n\n---\n\nDependency Injector with minimal boilerplate code, built-in support for FastAPI and Celery, and seamless integration to basically anything.\n\n## Contents\n* [Install](#install)\n* [Getting Started](#getting-started)\n* [Clients Configuration](#clients-configuration)\n  * [Zero config clients](#zero-config-clients)\n  * [Clients with Config](#clients-with-config)\n* [Using interfaces instead of implementations](#using-interfaces-instead-of-implementations)\n* [Integration with Celery](#integration-with-celery)\n  * [Function based tasks](#function-based-celery-tasks)\n  * [Class based tasks](#class-based-celery-tasks)\n* [Custom integrations](#custom-integrations)\n  * [Manual injection](#manual-injection)\n* [Forced injections](#forced-injections)\n* [Healthcheck](#healthcheck)\n* [Testing](#testing)\n  * [Default simple mock](#default-simple-mock)\n  * [Custom mocks](#custom-mocks)\n* [Alternatives](#alternatives)\n* [Development](#development)\n\n\n## Install\n```bash\npip install magic-di\n```\n\nWith FastAPI integration:\n```bash\npip install 'magic-di[fastapi]'\n```\n\nWith Celery integration:\n```bash\npip install 'magic-di[celery]'\n```\n\n## Getting Started\n\n```python\nfrom fastapi import FastAPI\n\nfrom magic_di import Connectable\nfrom magic_di.fastapi import inject_app, Provide\n\napp = inject_app(FastAPI())\n\n\nclass Database:\n    connected: bool = False\n\n    def __connect__(self):\n        self.connected = True\n\n    def __disconnect__(self):\n        self.connected = False\n\n\nclass Service(Connectable):\n    def __init__(self, db: Database):\n        self.db = db\n\n    def is_connected(self):\n        return self.db.connected\n\n\n@app.get(path=\"/hello-world\")\ndef hello_world(service: Provide[Service]) -\u003e dict:\n    return {\n        \"is_connected\": service.is_connected()\n    }\n```\n\nThat's all!\n\nThis simple code will recursively inject all dependencies and connect them using the `__connect__` and `__disconnect__` magic methods.\n\nBut what happened there?\n1) We created a new FastAPI app and injected it. The `inject_app` function makes the injector connect all clients on app startup and disconnect them on shutdown. That’s how you can open and close all connections (e.g., session to DB).\n2) We defined new classes with `__connect__` and `__disconnect__` magic methods. __That’s how the injector finds classes that need to be injected__. The injector uses duck typing to check if some class has these methods. It means you don’t need to inherit from `ClientProtocol` (but you can to reduce the number of code lines).\n3) Wrapped the `Service` type hint into `Provide` so that FastAPI can use our DI. __Please note__: you need to use `Provide` only in FastAPI endpoints, which makes your codebase independent from FastAPI and this Dependency Injector.\n4) PROFIT!\n\nAs you can see, in this example, you don’t need to write special constructors to store your dependencies in global variables. All you need to do to complete the startup logic is to write it in the `__connect__` method.\n\n\n## Clients Configuration\nThis dependency injector promotes the idea of ‘zero-config clients’, but you can still use configurations if you prefer\n\n### Zero config clients\nSimply fetch everything needed from the environment. There is no need for an additional configuration file\n\n```python\nfrom dataclasses import dataclass, field\n\nfrom pydantic import Field\nfrom pydantic_settings import BaseSettings\nfrom redis.asyncio import Redis as RedisClient, from_url\n\n\nclass RedisConfig(BaseSettings):\n    url: str = Field(validation_alias='REDIS_URL')\n    decode_responses: bool = Field(validation_alias='REDIS_DECODE_RESPONSES')\n\n\n@dataclass\nclass Redis:\n    config: RedisConfig = field(default_factory=RedisConfig)\n    client: RedisClient = field(init=False)\n\n    async def __connect__(self):\n        self.client = await from_url(self.config.url, decode_responses=self.config.decode_responses)\n        await self.client.ping()\n\n    async def __disconnect__(self):\n        await self.client.close()\n\n    @property\n    def db(self) -\u003e RedisClient:\n        return self.client\n\n\nRedis()  # works even without passing arguments in the constructor.\n```\n\nAs an alternative, you can inject configs instead of using default factories.\n\n```python\nfrom dataclasses import dataclass, field\n\nfrom pydantic import Field\nfrom pydantic_settings import BaseSettings\nfrom redis.asyncio import Redis as RedisClient, from_url\n\nfrom magic_di import Connectable, DependencyInjector\n\n\nclass RedisConfig(Connectable, BaseSettings):\n    url: str = Field(validation_alias='REDIS_URL')\n    decode_responses: bool = Field(validation_alias='REDIS_DECODE_RESPONSES')\n\n\n@dataclass\nclass Redis:\n    config: RedisConfig\n    client: RedisClient = field(init=False)\n\n    async def __connect__(self):\n        self.client = await from_url(self.config.url, decode_responses=self.config.decode_responses)\n        await self.client.ping()\n\n    async def __disconnect__(self):\n        await self.client.close()\n\n    @property\n    def db(self) -\u003e RedisClient:\n        return self.client\n\n\ninjector = DependencyInjector()\nredis = injector.inject(Redis)()  # works even without passing arguments in the constructor.\n\nasync with injector:\n    await redis.db.ping()\n```\n\n## Using interfaces instead of implementations\nSometimes, you may not want to stick to a certain interface implementation everywhere. Therefore, you can use interfaces (protocols, abstract classes) with Dependency Injection (DI). With DI, you can effortlessly bind an implementation to an interface and subsequently update it if necessary.\n\n```python\nfrom typing import Protocol\n\nfrom fastapi import FastAPI\n\nfrom magic_di import Connectable, DependencyInjector\nfrom magic_di.fastapi import inject_app, Provide\n\n\nclass MyInterface(Protocol):\n    def do_something(self) -\u003e bool:\n        ...\n\n\nclass MyInterfaceImplementation(Connectable):\n    def do_something(self) -\u003e bool:\n        return True\n\n\napp = inject_app(FastAPI())\n\ninjector = DependencyInjector()\ninjector.bind({MyInterface: MyInterfaceImplementation})\n\n\n@app.get(path=\"/hello-world\")\ndef hello_world(service: Provide[MyInterface]) -\u003e dict:\n    return {\n        \"result\": service.do_something(),\n    }\n```\n\nUsing `injector.bind`, you can bind implementations that will be injected everywhere the bound interface is used.\n\n## Integration with Celery\n\n### Function based celery tasks\n\n```python\nfrom celery import Celery\n\nfrom magic_di.celery import get_celery_loader, InjectableCeleryTask, PROVIDE\n\napp = Celery(\n    loader=get_celery_loader(),\n    task_cls=InjectableCeleryTask,\n)\n\n\n@app.task\nasync def calculate(x: int, y: int, calculator: Calculator = PROVIDE):\n    await calculator.calculate(x, y)\n```\n\n\n### Class based celery tasks\n\n```python\nfrom dataclasses import dataclass\n\nfrom celery import Celery\n\nfrom magic_di.celery import get_celery_loader, InjectableCeleryTask, BaseCeleryConnectableDeps, PROVIDE\n\napp = Celery(\n    loader=get_celery_loader(),\n    task_cls=InjectableCeleryTask,\n)\n\n\n@dataclass\nclass CalculatorTaskDeps(BaseCeleryConnectableDeps):\n    calculator: Calculator\n\n\nclass CalculatorTask(InjectableCeleryTask):\n    deps: CalculatorTaskDeps\n\n    async def run(self, x: int, y: int, smart_processor: SmartProcessor = PROVIDE):\n        return smart_processor.process(\n            await self.deps.calculator.calculate(x, y)\n        )\n\n\napp.register_task(CalculatorTask)\n```\n\n### Limitations\nYou could notice that in these examples tasks are using Python async/await.\n`InjectableCeleryTask` provides support for writing async code. However, it still executes code synchronously.\n**Due to this, getting results from async tasks is not possible in the following cases:**\n* When the `task_always_eager` config flag is enabled and task creation occurs inside the running event loop (e.g., inside an async FastAPI endpoint)\n* When calling the `.apply()` method inside running event loop (e.g., inside an async FastAPI endpoint)\n\n\n\n## Custom integrations\nFor custom integration you can either use helper function `inject_and_run` or by using DependencyInjector manually\n```python\nfrom magic_di.utils import inject_and_run\n\n\nasync def main(worker: Worker):\n    await worker.run()\n\nif __name__ == '__main__':\n    inject_and_run(main)\n```\n\n### Manual injection\n\n```python\nimport asyncio\n\nfrom magic_di import DependencyInjector\n\n\nasync def run_worker(worker: Worker):\n    await worker.run()\n\n\nasync def main():\n    injector = DependencyInjector()\n\n    injected_fn = injector.inject(run_worker)\n\n    async with injector:\n        await injected_fn()\n\nif __name__ == '__main__':\n    asyncio.run(main())\n```\n\n## Forced injections\nYou can force injector to inject non-connectable dependencies with type hint annotation `Injectable`\n```python\nfrom typing import Annotated\n\nfrom magic_di import Injectable, Connectable\n\n\nclass Service(Connectable):\n    dependency: Annotated[NonConnectableDependency, Injectable]\n```\n\n## Healthcheck\nYou can implement `Pingable` protocol to define healthchecks for your clients. The `DependenciesHealthcheck` will call the `__ping__` method on all injected clients that implement this protocol.\n\n```python\nfrom magic_di.healthcheck import DependenciesHealthcheck\n\n\nclass Service(Connectable):\n    def __init__(self, db: Database):\n        self.db = db\n\n    def is_connected(self):\n        return self.db.connected\n\n    async def __ping__(self) -\u003e None:\n        if not self.is_connected():\n            raise Exception(\"Service is not connected\")\n\n\n@app.get(path=\"/hello-world\")\ndef hello_world(service: Provide[Service]) -\u003e dict:\n    return {\n        \"is_connected\": service.is_connected()\n    }\n\n\n@app.get(path=\"/healthcheck\")\nasync def healthcheck_handler(healthcheck: Provide[DependenciesHealthcheck]) -\u003e dict:\n    await healthcheck.ping_dependencies()\n    return {\"alive\": True}\n```\n\n## Testing\nIf you need to mock a dependency in tests, you can easily do so by using the `injector.override` context manager and still use this dependency injector.\n\nTo mock clients, you can use `InjectableMock` from the `testing` module.\n\n### Default simple mock\n\n```python\nimport pytest\nfrom fastapi.testclient import TestClient\nfrom my_app import app\n\nfrom magic_di import DependencyInjector\nfrom magic_di.testing import InjectableMock\n\n\n@pytest.fixture()\ndef injector():\n    return DependencyInjector()\n\n\n@pytest.fixture()\ndef service_mock() -\u003e Service:\n    return InjectableMock()\n\n\n@pytest.fixture()\ndef client(injector: DependencyInjector, service_mock: InjectableMock):\n    with injector.override({Service: service_mock.mock_cls}):\n        with TestClient(app) as client:\n            yield client\n\n\ndef test_http_handler(client):\n    resp = client.post('/hello-world')\n\n    assert resp.status_code == 200\n```\n\n### Custom mocks\nAs an alternative, you can your use custom mocks.\n\n```python\nfrom magic_di.testing import get_injectable_mock_cls\n\n\n@pytest.fixture()\ndef service_mock() -\u003e Service:\n    return SomeSmartServiceMock()\n\n\n@pytest.fixture()\ndef client(injector: DependencyInjector, service_mock: Service):\n    with injector.override({Service: get_injectable_mock_cls(service_mock)}):\n        with TestClient(app) as client:\n            yield client\n```\n\n## Alternatives\n\n### [FastAPI's built-in dependency injection](https://fastapi.tiangolo.com/tutorial/dependencies/)\n\nFastAPI's built-in DI is great, but it makes the project (and its business logic) dependent on FastAPI, `fastapi.Depends` specifically.\n\n`magic-di` decouples DI from other dependencies while still offering seamless integration to FastAPI, for example.\n\n### [python-dependency-injector](https://github.com/ets-labs/python-dependency-injector)\n\n[python-dependency-injector](https://github.com/ets-labs/python-dependency-injector) is great, but it requires a notable amount of boilerplate code.\n\nThe goal of `magic-di` is to __reduce the amount of code as much as possible__ and get rid of enterprise code with countless configs, containers, and fabrics.\nThe philosophy of `magic-di` is that clients know how to configure themselves and perform all startup routines.\n\n\n## Development\n\n* Clone this repository\n* Requirements:\n  * [Poetry](https://python-poetry.org/)\n  * Python 3.10+\n* Create a virtual environment and install the dependencies\n\n```sh\npoetry install --all-extras\n```\n\n* Activate the virtual environment\n\n```sh\npoetry shell\n```\n\n### Testing\n\n```sh\npytest\n```\n\n### Documentation\n\nThe documentation is automatically generated from the content of the [docs directory](https://github.com/woltapp/magic-di/tree/master/docs) and from the docstrings\n of the public signatures of the source code. The documentation is updated and published as a [Github Pages page](https://pages.github.com/) automatically as part each release.\n\n### Releasing\n\nTrigger the [Draft release workflow](https://github.com/woltapp/magic-di/actions/workflows/draft_release.yml)\n(press _Run workflow_). This will update the changelog \u0026 version and create a GitHub release which is in _Draft_ state.\n\nFind the draft release from the\n[GitHub releases](https://github.com/woltapp/magic-di/releases) and publish it. When\n a release is published, it'll trigger [release](https://github.com/woltapp/magic-di/blob/master/.github/workflows/release.yml) workflow which creates PyPI\n release and deploys updated documentation.\n\n### Pre-commit\n\nPre-commit hooks run all the auto-formatting (`ruff format`), linters (e.g. `ruff` and `mypy`), and other quality\n checks to make sure the changeset is in good shape before a commit/push happens.\n\nYou can install the hooks with (runs for each commit):\n\n```sh\npre-commit install\n```\n\nOr if you want them to run only for each push:\n\n```sh\npre-commit install -t pre-push\n```\n\nOr if you want e.g. want to run all checks manually for all files:\n\n```sh\npre-commit run --all-files\n```\n\n---\n\nThis project was generated using the [wolt-python-package-cookiecutter](https://github.com/woltapp/wolt-python-package-cookiecutter) template.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwoltapp%2Fmagic-di","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwoltapp%2Fmagic-di","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwoltapp%2Fmagic-di/lists"}