Skip to main content

Bake your dependencies stupidly simple!

Project description

fresh-bakery

🍰 The little DI framework that tastes like a cake. 🍰


Documentation: https://fresh-bakery.readthedocs.io/en/latest/


Fresh Bakery

Fresh bakery is a lightweight [Dependency Injection][DI] framework/toolkit, which is ideal for building object dependencies in Python.

It is [nearly] production-ready, and gives you the following:

  • A lightweight, stupidly simple DI framework.
  • Fully asynchronous, no synchronous mode.
  • Any async backends compatible (asyncio, trio).
  • Zero dependencies.
  • Mypy compatible (no probably need for # type: ignore).
  • FastAPI fully compatible.
  • Pytest fully compatible (Fresh Bakery encourages the use of pytest).
  • Ease of testing.
  • Easily extended (contribution is welcome).

Requirements

Python 3.6+

Installation

$ pip3 install fresh-bakery

Examples

Raw example

In this example, you can see how to create a specific IoC container using the fresh bakery library in plain python code

import asyncio

from dataclasses import dataclass
from bakery import Bakery, Cake


# your dependecies
@dataclass
class Settings:
    database_dsn: str
    info_id_list: list[int]


class Database:
    def __init__(self, dsn: str):
        self.dsn: str = dsn

    async def fetch_info(self, info_id: int) -> dict:
        return {"dsn": self.dsn, "info_id": info_id}


class InfoManager:
    def __init__(self, database: Database):
        self.database: Database = database

    async def fetch_full_info(self, info_id: int) -> dict:
        info: dict = await self.database.fetch_info(info_id)
        info["full"] = True
        return info


# specific ioc container, all magic happens here
class MyBakeryIOC(Bakery):
    settings: Settings = Cake(Settings, database_dsn="my_dsn", info_id_list=[1,2,3])
    database: Database = Cake(Database, dsn=settings.database_dsn)
    manager: InfoManager = Cake(InfoManager, database=database)


# code in your application that needs those dependencies ↑
async def main() -> None:
    async with MyBakery() as bakery:
        for info_id in bakery.settings.info_id_list:
            info: dict = await bakery.manager.fetch_full_info(info_id)
            assert info["dsn"] == bakery.settings.database_dsn
            assert info["info_id"] == info_id
            assert info["full"]


# just a piece of service code
if __name__ == "__main__":
    asyncio.run(main())

FastAPI example

This is a full-fledged complex example of how you can use IoC with your FastAPI application:

import asyncio
import random
import typing

import bakery
import fastapi
import pydantic
from loguru import logger


# The following is a long and boring list of dependencies
class PersonOut(pydantic.BaseModel):
    """Person out."""

    first_name: str
    second_name: str
    age: int
    person_id: int


class FakeDbConnection:
    """Fake db connection."""

    def __init__(self, *_: typing.Any, **__: typing.Any):
        ...


class DatabaseFakeService:
    """Fake database layer."""

    def __init__(self, connection: FakeDbConnection) -> None:
        # wannabe connection only for test purposes
        self._connection: FakeDbConnection = connection

    async def __aenter__(self) -> "DatabaseFakeService":
        """On startup."""
        return self

    async def __aexit__(self, *_args: typing.Any) -> None:
        """Wannabe shutdown."""
        await asyncio.sleep(0)

    async def fetch_person(
        self, person_id: int
    ) -> dict[typing.Literal['first_name', 'second_name', 'age', 'id'], str | int]:
        """Fetch (fictitious) person."""
        return {
            'first_name': random.choice(('John', 'Danku', 'Ichigo', 'Sakura', 'Jugem', 'Ittō')),
            'second_name': random.choice(( 'Dow', 'Kurosaki', 'Amaterasu', 'Kasō', 'HiryuGekizokuShintenRaiho')),
            'age': random.randint(18, 120),
            'id': person_id,
        }


class Settings(pydantic.BaseSettings):
    """Service settings."""

    postgres_dsn: pydantic.PostgresDsn = pydantic.Field(
        default="postgresql://bakery_tester:bakery_tester@0.0.0.0:5432/bakery_tester"
    )
    postgres_pool_min_size: int = 5
    postgres_pool_max_size: int = 20
    controller_logger_name: str = "[Controller]"


class ServiceController:
    """Service controller."""

    def __init__(
        self,
        *,
        database: DatabaseFakeService,
        logger_name: str,
    ):
        self._database = database
        self._logger_name = logger_name

    def __repr__(self) -> str:
        return self._logger_name

    async def fetch_person(self, person_id: int, /) -> PersonOut | None:
        """Fetch person by id."""
        person: typing.Mapping | None = await self._database.fetch_person(person_id)
        if not person:
            return None
        res: PersonOut = PersonOut(
            first_name=person["first_name"],
            second_name=person["second_name"],
            age=person["age"],
            person_id=person_id,
        )
        return res


def get_settings() -> Settings:
    """Get settings."""
    return Settings()


# Here is your specific IoC container
class MainBakeryIOC(bakery.Bakery):
    """Main bakery."""

    config: Settings = bakery.Cake(get_settings)
    _connection: FakeDbConnection = bakery.Cake(
        FakeDbConnection,
        config.postgres_dsn,
        min_size=config.postgres_pool_min_size,
        max_size=config.postgres_pool_max_size,
    )
    database: DatabaseFakeService = bakery.Cake(
        bakery.Cake(
            DatabaseFakeService,
            connection=_connection,
        )
    )
    controller: ServiceController = bakery.Cake(
        ServiceController,
        database=database,
        logger_name=config.controller_logger_name,
    )


async def startup() -> None:
    logger.info("Init resources...")
    bakery.logger = logger
    await MainBakeryIOC.aopen()


async def shutdown() -> None:
    logger.info("Shutdown resources...")
    await MainBakeryIOC.aclose()


MY_APP: fastapi.FastAPI = fastapi.FastAPI(
    on_startup=[startup],
    on_shutdown=[shutdown],
)


# Finally, an example of how you can use your dependencies
@MY_APP.get('/person/random/')
async def create_person(
    inversed_controller: ServiceController = fastapi.Depends(MainBakeryIOC.controller),
) -> PersonOut | None:
    """Fetch random person from the «database»."""
    person_id: typing.Final[int] = random.randint(10**1, 10**6)
    return await inversed_controller.fetch_person(person_id)

To run this example, you will need to do the following:

  1. Install dependencies:
    pip install uvicorn fastapi loguru fresh-bakery
    
  2. Save the example text to the file test.py
  3. Run uvicorn
    uvicorn test:MY_APP
    
  4. Open this address in the browser: http://127.0.0.1:8000/docs#/default/create_person_person_random__get
  5. And don't forget to read the logs in the console

For a more complete examples, see bakery examples.

Dependencies

No dependencies ;)

Changelog

You can see the release history here: https://github.com/Mityuha/fresh-bakery/releases/


Fresh Bakery is MIT licensed code.

Project details


Download files

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

Source Distribution

fresh_bakery-0.2.1.tar.gz (18.5 kB view hashes)

Uploaded Source

Built Distribution

fresh_bakery-0.2.1-py3-none-any.whl (17.3 kB view hashes)

Uploaded Python 3

Supported by

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