From b6a5c09b54db7bc4c600fdc5a68d4e3fc7d0275f Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 14 Oct 2024 10:59:33 +0200 Subject: [PATCH] Bakery DI docs (#54) * add __Cake__ field specifier for required keyword-only arguments * piece of cake should not work test * test needs to be fixed * tests for missing arguments+session only arguments lifetime have to be fixed; * remove copy micro-optimization (very doubtful) * cake_replace method * tmp commit; fixes needed * fix test about unbaked cakes error * enhance test * fix piece of cake test * fix default logging * add is_undefined function * BAKE_NO_BAKE method for undefined recipe * fix docstring * replacement works now, but refactoring needed + obj representation for replacement * test recipe format * recipe format function * move recipe format function to cake stuff * replace/unreplace cakes util functions * add logs concerning replacement * replace/unreplace cakes util functions * mutliple openings with keyword arguments are prohibited and discouraged * fix strings * fix with cake replacement * add bakery mock examples * add default logger test * quick start example test * fastapi examples from docs * add bakery examples section * add tests for readme docs * remove main section * readme fix * fix readme; fix bakery examples section * fix * fix docs * fix docs * fix * add docs and examples * fix docs * fix bakery_mock docs and tests * fix * fix * bump patch version * fix readme --- README.md | 327 ++++++-------- docs/bakery_di.md | 180 ++++++++ docs/bakery_examples.md | 226 ++++++++++ docs/test_bakery.md | 92 ++-- examples/docs_bakery_di_example/poetry.lock | 229 ++++++++++ .../docs_bakery_di_example/pyproject.toml | 19 + .../test_app/__init__.py | 0 .../test_app/__main__.py | 74 ++++ examples/docs_fastapi_example/poetry.lock | 409 ++++++++++++++++++ examples/docs_fastapi_example/pyproject.toml | 22 + .../docs_fastapi_example/test_app/__init__.py | 0 .../docs_fastapi_example/test_app/__main__.py | 151 +++++++ mkdocs.yml | 2 + pyproject.toml | 2 +- tests/bakery_mock/test_docs_examples.py | 12 + tests/docs_examples/__init__.py | 0 .../test_almost_file_examples.py | 82 ++++ .../docs_examples/test_quick_start_example.py | 24 + tests/docs_examples/test_raw_example.py | 47 ++ 19 files changed, 1680 insertions(+), 218 deletions(-) create mode 100644 docs/bakery_di.md create mode 100644 docs/bakery_examples.md create mode 100644 examples/docs_bakery_di_example/poetry.lock create mode 100644 examples/docs_bakery_di_example/pyproject.toml create mode 100644 examples/docs_bakery_di_example/test_app/__init__.py create mode 100644 examples/docs_bakery_di_example/test_app/__main__.py create mode 100644 examples/docs_fastapi_example/poetry.lock create mode 100644 examples/docs_fastapi_example/pyproject.toml create mode 100644 examples/docs_fastapi_example/test_app/__init__.py create mode 100644 examples/docs_fastapi_example/test_app/__main__.py create mode 100644 tests/docs_examples/__init__.py create mode 100644 tests/docs_examples/test_almost_file_examples.py create mode 100644 tests/docs_examples/test_quick_start_example.py create mode 100644 tests/docs_examples/test_raw_example.py diff --git a/README.md b/README.md index 387d7f2..7163a33 100644 --- a/README.md +++ b/README.md @@ -42,228 +42,175 @@ $ 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 +### Quickstart +This example is intended to show the nature of Dependency Injection and the ease of use the library. Many of us work 8 hours per day on average, 5 days a week, i.e. ~ 40 hours per week. Let's describe it using DI and bakery: ```python -import asyncio - -from dataclasses import dataclass from bakery import Bakery, Cake -# your dependecies -@dataclass -class Settings: - database_dsn: str - info_id_list: list[int] +def full_days_in(hours: int) -> float: + return hours / 24 -class Database: - def __init__(self, dsn: str): - self.dsn: str = dsn +def average(total: int, num: int) -> float: + return total / num - async def fetch_info(self, info_id: int) -> dict: - return {"dsn": self.dsn, "info_id": info_id} +class WorkingBakery(Bakery): + average_hours: int = Cake(8) + week_hours: int = Cake(sum, [average_hours, average_hours, 7, 9, average_hours]) + full_days: float = Cake(full_days_in, week_hours) -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 +async def main() -> None: + async with WorkingBakery() as bakery: + assert bakery.week_hours == 40 + assert bakery.full_days - 0.00001 < full_days_in(40) + assert int(bakery.average_hours) == 8 +``` +You can see it's as simple as it can be. +### One more example +Let's suppose we have a thin wrapper around file object. +```python +from typing import ClassVar, Final -# 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) +from typing_extensions import Self -# 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"] +class FileWrapper: + file_opened: bool = False + write_lines: ClassVar[list[str]] = [] + def __init__(self, filename: str) -> None: + self.filename: Final = filename -# just a piece of service code -if __name__ == "__main__": - asyncio.run(main()) -``` + def write(self, line: str) -> int: + type(self).write_lines.append(line) + return len(line) + + def __enter__(self) -> Self: + type(self).file_opened = True + return self -### FastAPI example -This is a full-fledged complex example of how you can use IoC with your FastAPI application: + def __exit__(self, *_args: object) -> None: + type(self).file_opened = False + type(self).write_lines.clear() +``` +This wrapper acts exactly like a file object: it can be opened, closed, and can write line to file. +Let's open file `hello.txt`, write 2 lines into it and close it. Let's do all this with the bakery syntax: ```python -import asyncio -import random -import typing +from bakery import Bakery, Cake + -import bakery -import fastapi -import pydantic -from loguru import logger +class FileBakery(Bakery): + _file_obj: FileWrapper = Cake(FileWrapper, "hello.txt") + file_obj: FileWrapper = Cake(_file_obj) + write_1_bytes: int = Cake(file_obj.write, "hello, ") + write_2_bytes: int = Cake(file_obj.write, "world") -# The following is a long and boring list of dependencies -class PersonOut(pydantic.BaseModel): - """Person out.""" +async def main() -> None: + assert FileWrapper.file_opened is False + assert FileWrapper.write_lines == [] + async with FileBakery() as bakery: + assert bakery.file_obj.filename == "hello.txt" + assert FileWrapper.file_opened is True + assert FileWrapper.write_lines == ["hello, ", "world"] + + assert FileWrapper.file_opened is False + assert FileWrapper.write_lines == [] +``` +Maybe you noticed some strange things concerning `FileBakery` bakery: +1. `_file_obj` and `file_obj` objects. Do we need them both? +2. Unused `write_1_bytes` and `write_2_bytes` objects. Do we need them? + +Let's try to fix both cases. First, let's figure out why do we need `_file_obj` and `file_obj` objects? +- The first `Cake` for `_file_obj` initiates `FileWrapper` object, i.e. calls `__init__` method; +- the second `Cake` for `file_obj` calls context-manager, i.e. calls `__enter__` method on enter and `__exit__` method on exit. - first_name: str - second_name: str - age: int - person_id: int +Actually, we can merge these two statements into single one: +```python +# class FileBakery(Bakery): + file_obj: FileWrapper = Cake(Cake(FileWrapper, "hello.txt")) +``` +So, what about unused arguments? OK, let's re-write this gist a little bit. First, let's declare the list of strings we want to write: +```python +# class FileBakery(Bakery): + strs_to_write: list[str] = Cake(["hello, ", "world"]) +``` +How to apply function to every string in this list? There are several ways to do it. One of them is built-in [`map`](https://docs.python.org/3/library/functions.html#map) function. +```python +map_cake = Cake(map, file_obj.write, strs_to_write) +``` +But `map` function returns iterator and we need to get elements from it. Built-in [`list`](https://docs.python.org/3/library/functions.html#func-list) function will do the job. +```python +list_cake = Cake(list, map_cake) +``` +In the same manner as we did for `file_obj` let's merge these two statements into one. The final `FileBakery` will look like this: +```python +class FileBakeryMap(Bakery): + file_obj: FileWrapper = Cake(Cake(FileWrapper, "hello.txt")) + strs_to_write: list[str] = Cake(["hello, ", "world"]) + _: list[int] = Cake(list, Cake(map, file_obj.write, strs_to_write)) +``` +The last thing nobody likes is hard-coded strings! In this case such strings are: +- the name of the file `hello.txt` +- list of strings to write: `hello, ` and `world` +What if we've got another filename or other strings to write? Let's define filename and list of strings as `FileBakery` parameters: +```python +from bakery import Bakery, Cake, __Cake__ -class FakeDbConnection: - """Fake db connection.""" - def __init__(self, *_: typing.Any, **__: typing.Any): +class FileBakery(Bakery): + filename: str = __Cake__() + strs_to_write: list[str] = __Cake__() + file_obj: FileWrapper = Cake(Cake(FileWrapper, filename)) + _: list[int] = Cake(list, Cake(map, file_obj.write, strs_to_write)) +``` +To define parameters you can use dunder-cake construction: `__Cake__()`. +To pass arguments into `FileBakery` you can use native python syntax: +```python +# async def main() -> None: + async with FileBakeryMapWithParams( + filename="hello.txt", strs_to_write=["hello, ", "world"] + ) as bakery: ... +``` +And the whole example will look like this: +```python +from typing import ClassVar, Final +from typing_extensions import Self -class DatabaseFakeService: - """Fake database layer.""" +from bakery import Bakery, Cake, __Cake__ - 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 +# class FileWrapper: ... - 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) + +class FileBakery(Bakery): + filename: str = __Cake__() + strs_to_write: list[str] = __Cake__() + file_obj: FileWrapper = Cake(Cake(FileWrapper, filename)) + _: list[int] = Cake(list, Cake(map, file_obj.write, strs_to_write)) + + +async def main() -> None: + assert FileWrapper.file_opened is False + assert FileWrapper.write_lines == [] + async with FileBakeryMapWithParams( + filename="hello.txt", strs_to_write=["hello, ", "world"] + ) as bakery: + assert bakery.file_obj.filename == "hello.txt" + assert FileWrapper.file_opened is True + assert FileWrapper.write_lines == ["hello, ", "world"] + + assert FileWrapper.file_opened is False + assert FileWrapper.write_lines == [] ``` -To run this example, you will need to do the following: -1. Install dependencies: - ``` - pip install uvicorn fastapi loguru fresh-bakery - ``` -1. Save the example text to the file test.py -1. Run uvicorn - ``` - uvicorn test:MY_APP - ``` -1. Open this address in the browser: http://127.0.0.1:8000/docs#/default/create_person_person_random__get -1. And don't forget to read the logs in the console - -For a more complete examples, see [bakery examples](https://github.com/Mityuha/fresh-bakery/tree/main/examples). +More examples are presented in section [bakery examples](https://fresh-bakery.readthedocs.io/en/latest/bakery_examples/). ## Dependencies diff --git a/docs/bakery_di.md b/docs/bakery_di.md new file mode 100644 index 0000000..0ecd399 --- /dev/null +++ b/docs/bakery_di.md @@ -0,0 +1,180 @@ +# Bakery DI + +!!! note + New in version 0.4.2 + + +Fresh bakery provides a Dependency Injection (DI) mechanism. And sometimes it's very convenient to structure your application so that different modules have their own bakeries. +Even more: sometimes it's really convenient to initiate modules over their bakeries. +If so, why don't pass arguments to such bakeries? +Let's suppose we have Resource Owner Password Authentication Flow: we have to receive token by (username, password) credentials. + +```python +from typing import Final, Iterator + +import httpx +from httpx import Auth, Request + + +class IntranetAuth(Auth): + def __init__(self, url: str, *, login: str, password: str) -> None: + self._url: Final = url + self._auth: Final = (login, password) + + def sync_auth_flow(self, request: Request) -> Iterator[Request]: + resp = httpx.get(self._url, auth=self._auth) + token: str = resp.json()["token"] + request.headers["Authorization"] = f"Bearer {token}" + yield request + +``` +And we have `AuthBakery` bakery for such an authentication: +```python +from bakery import Bakery, Cake, __Cake__ + + +class AuthBakery(Bakery): + auth_url: str = __Cake__() + username: str = __Cake__() + password: str = __Cake__() + + auth: IntranetAuth = Cake(IntranetAuth, auth_url, login=username, password=password) +``` +Also we have a client to receive user info: +```python +from typing import Final + +from httpx import Client + + +class UserClient: + def __init__(self, client: Client) -> None: + self.client: Final = client + + def user_info(self, user_id: int) -> dict: + resp = self.client.get(f"/api/v1/users/{user_id}") + return resp.json() +``` +and bakery for this client: +```python +from httpx import Auth, Client + +from bakery import Bakery, Cake, __Cake__ + + +class UserClientBakery(Bakery): + base_url: str = __Cake__() + auth: Auth = __Cake__() + http_client: Client = Cake(Cake(Client, base_url=base_url, auth=auth)) + client: UserClient = Cake(UserClient, http_client) +``` +We can see to initiate `AuthBakery` we need: +- Authentication url `auth_url` +- `username` +- `password` + +To initiate `UserClientBakery` we need: +- Url to go: `base_url` +- authentication `auth` + +All right, let's glue those two bakeries over 3rd `ApplicationBakery` bakery: +```python +from bakery import Bakery, Cake + + +class ApplicationBakery(Bakery): + auth_url: str = Cake("https://your.auth.url") + intranet_url: str = Cake("https://your.intranet.url") + username: str = Cake("[masked]") + password: str = Cake("[masked]") + + auth_bakery: AuthBakery = Cake( + Cake(AuthBakery, auth_url=auth_url, username=username, password=password) + ) + + client_bakery: UserClientBakery = Cake( + Cake(UserClientBakery, base_url=intranet_url, auth=auth_bakery.auth) + ) +``` +We defined all urls and credentials just right in `ApplicationBakery`, but of course, we can redefine it over arguments passing: +```python +async def main() -> None: + async with ApplicationBakery(auth_url="https://google.com") as bakery: + ... +``` +The complete example can look like this: +```python +import asyncio +from typing import Final, Iterator + +import httpx +from httpx import Auth, Client, Request +from loguru import logger + +from bakery import Bakery, Cake, __Cake__ + + +class IntranetAuth(Auth): + def __init__(self, url: str, *, login: str, password: str) -> None: + self._url: Final = url + self._auth: Final = (login, password) + + def sync_auth_flow(self, request: Request) -> Iterator[Request]: + resp = httpx.get(self._url, auth=self._auth) + token: str = resp.json()["token"] + request.headers["Authorization"] = f"Bearer {token}" + yield request + + +class UserClient: + def __init__(self, client: Client) -> None: + self.client: Final = client + + def user_info(self, user_id: int) -> dict: + resp = self.client.get(f"/api/v1/users/{user_id}") + return resp.json() + + +class AuthBakery(Bakery): + auth_url: str = __Cake__() + username: str = __Cake__() + password: str = __Cake__() + + auth: IntranetAuth = Cake(IntranetAuth, auth_url, login=username, password=password) + + +class UserClientBakery(Bakery): + base_url: str = __Cake__() + auth: Auth = __Cake__() + http_client: Client = Cake(Cake(Client, base_url=base_url, auth=auth)) + client: UserClient = Cake(UserClient, http_client) + + +class ApplicationBakery(Bakery): + auth_url: str = Cake("https://your.auth.url") + intranet_url: str = Cake("https://your.intranet.url") + username: str = Cake("[masked]") + password: str = Cake("[masked]") + + auth_bakery: AuthBakery = Cake( + Cake(AuthBakery, auth_url=auth_url, username=username, password=password) + ) + + client_bakery: UserClientBakery = Cake( + Cake(UserClientBakery, base_url=intranet_url, auth=auth_bakery.auth) + ) + + +async def main() -> None: + user_id: int = 123 + async with ApplicationBakery(auth_url="https://google.com") as bakery: + client = bakery.client_bakery.client + user_info = client.user_info(user_id) + + logger.debug(f"User '{user_id}' info: {user_info}") + + +if __name__ == "__main__": + asyncio.run(main()) +``` +You can inspect this example for more details in [examples folder](https://github.com/Mityuha/fresh-bakery/tree/main/examples/docs_bakery_di_example). But I hope the idea is clear: you can use the power of Dependency Injection for bakeries as simple as for plain classes and functions. diff --git a/docs/bakery_examples.md b/docs/bakery_examples.md new file mode 100644 index 0000000..edaf83a --- /dev/null +++ b/docs/bakery_examples.md @@ -0,0 +1,226 @@ +# Bakery examples +Let's consider several more complex 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 +```python +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) -> None: + 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) -> None: + 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 MyBakeryIOC() 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: +```python +import asyncio +import random +from contextlib import asynccontextmanager +from typing import Any, AsyncIterator, Final, Literal, Mapping + +from fastapi import Depends, FastAPI +from loguru import logger +from pydantic import BaseModel, Field, PostgresDsn +from pydantic_settings import BaseSettings +from typing_extensions import Annotated, Self + +import bakery +from bakery import Bakery, Cake + +# ruff: noqa: S311 + + +# The following is a long and boring list of dependencies +class PersonOut(BaseModel): + """Person out.""" + + first_name: str + second_name: str + age: int + person_id: int + + +class FakeDbConnection: + """Fake db connection.""" + + def __init__(self, *_: Any, **__: Any) -> None: ... + + +class DatabaseFakeService: + """Fake database layer.""" + + def __init__(self, connection: FakeDbConnection) -> None: + # wannabe connection only for test purposes + self._connection: Final = connection + + async def __aenter__(self) -> Self: + return self + + async def __aexit__(self, *_args: object) -> None: + await asyncio.sleep(0) + + async def fetch_person( + self, person_id: int + ) -> dict[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(BaseSettings): + """Service settings.""" + + postgres_dsn: PostgresDsn = 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, + ) -> None: + 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: 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): + """Main bakery.""" + + config: Settings = Cake(get_settings) + _connection: FakeDbConnection = Cake( + FakeDbConnection, + config.postgres_dsn, + min_size=config.postgres_pool_min_size, + max_size=config.postgres_pool_max_size, + ) + database: DatabaseFakeService = Cake( + Cake( + DatabaseFakeService, + connection=_connection, + ) + ) + controller: ServiceController = Cake( + ServiceController, + database=database, + logger_name=config.controller_logger_name, + ) + + +@asynccontextmanager +async def lifespan(_app: FastAPI) -> AsyncIterator[None]: + logger.info("Init resources...") + bakery.logger = logger + async with MainBakeryIOC(): + yield + logger.info("Shutdown resources...") + + +MY_APP: Final = FastAPI(lifespan=lifespan) + + +# Finally, an example of how you can use your dependencies +@MY_APP.get("/person/random/") +async def create_person( + inversed_controller: Annotated[ServiceController, Depends(MainBakeryIOC.controller)], +) -> PersonOut | None: + """Fetch random person from the «database».""" + person_id: 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 pydantic pydantic-settings + ``` +2. Save the example text to the file `test.py` +3. Run uvicorn + ``` + uvicorn test:MY_APP + ``` +4. [Touch the link](http://127.0.0.1:8000/docs#/default/create_person_person_random__get) in the browser +5. And don't forget to read the logs in the console + +For a more complete examples, see examples' [source code](https://github.com/Mityuha/fresh-bakery/tree/main/examples). Feel free to install dependencies and run examples locally! diff --git a/docs/test_bakery.md b/docs/test_bakery.md index 26aa7bb..3673ee0 100644 --- a/docs/test_bakery.md +++ b/docs/test_bakery.md @@ -1,22 +1,5 @@ # Test Bakery -It is recommended to use pytest framework to test your bakeries. Fresh Bakery appreciate the usage of pytest framework: you can use `bakery_mock` fixture out of the box. - -!!! note - To use `bakery_mock` fixture you have to also install pytest-mock library (besides pytest itself): - ```bash - $ pip install pytest-mock - ``` - -!!! info - `bakery_mock` is a function-scoped fixture. There are also other fixtures with different fixture scopes: - - - `class_bakery_mock` is a class-scoped fixture - - `module_bakery_mock` is a module-scoped fixture - - `package_bakery_mock` is a package-scoped fixture - - `session_bakery_mock` is a session-scoped fixture - -## Patch before bakery opened -Let's see hot to use `bakery_mock` fixture to test your bakery. +Let's suppose we have class `Settings` and bakery `MyBakery`: ```python # file example.py from dataclasses import dataclass @@ -31,12 +14,64 @@ class Settings: class MyBakery(Bakery): dsn: str = Cake("real dsn") settings: Settings = Cake(Settings, dsn=dsn) +``` +Let's consider two approaches for bakery testing: framework agnostic and pytest related approaches. + +## Framework agnostic approach +You can override any bakery member by passing argument for it to bakery: +```python +from .example import MyBakery + + +async def test_example_1_no_mocks() -> None: + async with MyBakery(dsn="fake dsn"): # <<< pass new dsn value + assert MyBakery().dsn == "fake dsn" + assert MyBakery().settings.dsn == "fake dsn" +``` +No particular framework required for it. It just works as expected and out-of-the-box. +The only downside of this approach is that you can't pass new arguments if bakery's been already opened: +```python +import pytest + +from .example import MyBakery + +async def test_example_1_cant_pass_after_open() -> None: + await MyBakery.aopen() + with pytest.raises(TypeError): + async with MyBakery(dsn="fake dsn"): # <<< passing new arguments after open is prohibited + ... + + await MyBakery.aclose() +``` +If for some reason you need to override bakery's member **after** opening, please use pytest related approach. + +## Pytest related approach +Fresh Bakery appreciate the usage of pytest framework: you can use `bakery_mock` fixture out-of-the-box. + +!!! note + To use `bakery_mock` fixture you have to also install pytest-mock library (besides pytest itself): + ```bash + $ pip install pytest-mock + ``` + +!!! info + `bakery_mock` is a function-scoped fixture. There are also other fixtures with different fixture scopes: + + - `class_bakery_mock` is a class-scoped fixture + - `module_bakery_mock` is a module-scoped fixture + - `package_bakery_mock` is a package-scoped fixture + - `session_bakery_mock` is a session-scoped fixture + +### Patch before bakery opened +Let's see how to use `bakery_mock` fixture to test your bakery. +```python # file test_example.py from bakery import Cake from bakery.testbakery import BakeryMock -from example import MyBakery + +from .example import MyBakery async def test_example_1(bakery_mock: BakeryMock) -> None: @@ -50,11 +85,15 @@ In this example we patch `dsn` attribute (of any bakery) and then bind `bakery_m !!! note Note that bacause of we patch `dsn` attribute **BEFORE** bakery opened, all depending cakes are also becoming patched. In the example above `settings` cake are also patched with fake dsn. -## Patch after bakery opened +### Patch after bakery opened You could also patch attributes **after** bakery was opened. But you should realize that the cake patched is the only thing that will be patched. ```python # file test_example.py +from bakery import Cake +from bakery.testbakery import BakeryMock + +from .example import MyBakery async def test_example_2(bakery_mock: BakeryMock) -> None: @@ -67,13 +106,16 @@ async def test_example_2(bakery_mock: BakeryMock) -> None: await MyBakery.aclose() # close anywhere ``` -Note that unlike the `test_example_1` example, in the `test_example_2` we patch `MyBakery` bakery after the bakery is opened. It means, that cake `settings` is already baked and its `.dsn` value is `"real dsn"`. +Note that unlike the `test_example_1` example, in the `test_example_2` we patch `MyBakery` bakery **after** the bakery is opened. It means the cake `settings` is already baked and its `.dsn` value is `"real dsn"`. -## Patch hand made cakes +### Patch hand made cakes Hand made cakes are also supported: ```python # file test_example.py -from bakery import hand_made, BakingMethod +from bakery import Bakery, BakingMethod, Cake, hand_made +from bakery.testbakery import BakeryMock + +from .example import MyBakery async def test_example_3(bakery_mock: BakeryMock) -> None: @@ -84,7 +126,3 @@ async def test_example_3(bakery_mock: BakeryMock) -> None: async with bakery_mock(MyBakery): assert MyBakery().settings is list ``` - -## Unittest -You surely could patch the whole cakes with the unittest. All cake's dependencies patching may be a little bit tricky. But don't give up and continue experimenting. -Or just move to pytest ;) diff --git a/examples/docs_bakery_di_example/poetry.lock b/examples/docs_bakery_di_example/poetry.lock new file mode 100644 index 0000000..3cfb0c0 --- /dev/null +++ b/examples/docs_bakery_di_example/poetry.lock @@ -0,0 +1,229 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "anyio" +version = "4.6.1" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.9" +files = [ + {file = "anyio-4.6.1-py3-none-any.whl", hash = "sha256:0632863c9044798a494a05cab0b159dfad6a3f064094863a45878320eb4e8ed2"}, + {file = "anyio-4.6.1.tar.gz", hash = "sha256:936e6613a08e8f71a300cfffca1c1c0806335607247696ac45f9b32c63bfb9aa"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "certifi" +version = "2024.8.30" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fresh-bakery" +version = "0.4.1" +description = "Bake your dependencies stupidly simple!" +optional = false +python-versions = ">=3.8,<3.13" +files = [] +develop = false + +[package.source] +type = "directory" +url = "../.." + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.6" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f"}, + {file = "httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.27.2" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "loguru" +version = "0.7.2" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = ">=3.5" +files = [ + {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, + {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] + +[[package]] +name = "ruff" +version = "0.6.9" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"}, + {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"}, + {file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"}, + {file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"}, + {file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"}, + {file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"}, + {file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +files = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.9,<3.13" +content-hash = "f20ba3db86b112617d743ce6b37e6fbe2d4aa4484b07db6560db0e308e046ddc" diff --git a/examples/docs_bakery_di_example/pyproject.toml b/examples/docs_bakery_di_example/pyproject.toml new file mode 100644 index 0000000..e7ff85e --- /dev/null +++ b/examples/docs_bakery_di_example/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "docs_bakery_di" +version = "0.0.1" +description = "Real bakery tests" +authors = ["Dmitry Makarov"] +classifiers = ["Programming Language :: Python :: 3.12"] + + +[tool.poetry.dependencies] +python = ">=3.9,<3.13" +httpx = "*" +loguru = "*" +fresh-bakery = {path = "../.."} + +[tool.poetry.dev-dependencies] +ruff = "*" + +[tool.mypy] +plugins = "bakery.mypy" diff --git a/examples/docs_bakery_di_example/test_app/__init__.py b/examples/docs_bakery_di_example/test_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/docs_bakery_di_example/test_app/__main__.py b/examples/docs_bakery_di_example/test_app/__main__.py new file mode 100644 index 0000000..10bb5bc --- /dev/null +++ b/examples/docs_bakery_di_example/test_app/__main__.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import asyncio +from typing import Final, Iterator + +import httpx +from httpx import Auth, Client, Request +from loguru import logger + +from bakery import Bakery, Cake, __Cake__ + + +class IntranetAuth(Auth): + def __init__(self, url: str, *, login: str, password: str) -> None: + self._url: Final = url + self._auth: Final = (login, password) + + def sync_auth_flow(self, request: Request) -> Iterator[Request]: + resp = httpx.get(self._url, auth=self._auth) + token: str = resp.json()["token"] + request.headers["Authorization"] = f"Bearer {token}" + yield request + + +class UserClient: + def __init__(self, client: Client) -> None: + self.client: Final = client + + def user_info(self, user_id: int) -> dict: + resp = self.client.get(f"/api/v1/users/{user_id}") + return resp.json() + + +class AuthBakery(Bakery): + auth_url: str = __Cake__() + username: str = __Cake__() + password: str = __Cake__() + + auth: IntranetAuth = Cake(IntranetAuth, auth_url, login=username, password=password) + + +class UserClientBakery(Bakery): + base_url: str = __Cake__() + auth: Auth = __Cake__() + http_client: Client = Cake(Cake(Client, base_url=base_url, auth=auth)) + client: UserClient = Cake(UserClient, http_client) + + +class ApplicationBakery(Bakery): + auth_url: str = Cake("https://your.auth.url") + intranet_url: str = Cake("https://your.intranet.url") + username: str = Cake("[masked]") + password: str = Cake("[masked]") + + auth_bakery: AuthBakery = Cake( + Cake(AuthBakery, auth_url=auth_url, username=username, password=password) + ) + + client_bakery: UserClientBakery = Cake( + Cake(UserClientBakery, base_url=intranet_url, auth=auth_bakery.auth) + ) + + +async def main() -> None: + user_id: int = 123 + async with ApplicationBakery(auth_url="https://google.com") as bakery: + client = bakery.client_bakery.client + user_info = client.user_info(user_id) + + logger.debug(f"User '{user_id}' info: {user_info}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/docs_fastapi_example/poetry.lock b/examples/docs_fastapi_example/poetry.lock new file mode 100644 index 0000000..ce4db6b --- /dev/null +++ b/examples/docs_fastapi_example/poetry.lock @@ -0,0 +1,409 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.6.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.9" +files = [ + {file = "anyio-4.6.0-py3-none-any.whl", hash = "sha256:c7d2e9d63e31599eeb636c8c5c03a7e108d73b345f064f1c19fdc87b79036a9a"}, + {file = "anyio-4.6.0.tar.gz", hash = "sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.21.0b1)"] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fastapi" +version = "0.115.2" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.115.2-py3-none-any.whl", hash = "sha256:61704c71286579cc5a598763905928f24ee98bfcc07aabe84cfefb98812bbc86"}, + {file = "fastapi-0.115.2.tar.gz", hash = "sha256:3995739e0b09fa12f984bce8fa9ae197b35d433750d3d312422d846e283697ee"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.37.2,<0.41.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "fresh-bakery" +version = "0.4.1" +description = "Bake your dependencies stupidly simple!" +optional = false +python-versions = ">=3.8,<3.13" +files = [] +develop = false + +[package.source] +type = "directory" +url = "../.." + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "loguru" +version = "0.7.2" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = ">=3.5" +files = [ + {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, + {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] + +[[package]] +name = "pydantic" +version = "2.9.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.23.4" +typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.23.4" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, + {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, + {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, + {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, + {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, + {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, + {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, + {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, + {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, + {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, + {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, + {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, + {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, + {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydantic-settings" +version = "2.5.2" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_settings-2.5.2-py3-none-any.whl", hash = "sha256:2c912e55fd5794a59bf8c832b9de832dcfdf4778d79ff79b708744eed499a907"}, + {file = "pydantic_settings-2.5.2.tar.gz", hash = "sha256:f90b139682bee4d2065273d5185d71d37ea46cfe57e1b5ae184fc6a0b2484ca0"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" + +[package.extras] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "ruff" +version = "0.6.9" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"}, + {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"}, + {file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"}, + {file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"}, + {file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"}, + {file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"}, + {file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "starlette" +version = "0.39.2" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.8" +files = [ + {file = "starlette-0.39.2-py3-none-any.whl", hash = "sha256:134dd6deb655a9775991d352312d53f1879775e5cc8a481f966e83416a2c3f71"}, + {file = "starlette-0.39.2.tar.gz", hash = "sha256:caaa3b87ef8518ef913dac4f073dea44e85f73343ad2bdc17941931835b2a26a"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "uvicorn" +version = "0.31.1" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.31.1-py3-none-any.whl", hash = "sha256:adc42d9cac80cf3e51af97c1851648066841e7cfb6993a4ca8de29ac1548ed41"}, + {file = "uvicorn-0.31.1.tar.gz", hash = "sha256:f5167919867b161b7bcaf32646c6a94cdbd4c3aa2eb5c17d36bb9aa5cfd8c493"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +files = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.9,<3.13" +content-hash = "4246272be11c04819a6e2ca614d93f672cf9eb58479907533b80009efd7c95d8" diff --git a/examples/docs_fastapi_example/pyproject.toml b/examples/docs_fastapi_example/pyproject.toml new file mode 100644 index 0000000..5c35e57 --- /dev/null +++ b/examples/docs_fastapi_example/pyproject.toml @@ -0,0 +1,22 @@ +[tool.poetry] +name = "docs_fastapi" +version = "0.0.1" +description = "Real bakery tests" +authors = ["Dmitry Makarov"] +classifiers = ["Programming Language :: Python :: 3.12"] + + +[tool.poetry.dependencies] +python = ">=3.9,<3.13" +fastapi = "*" +uvicorn = "*" +loguru = "*" +pydantic = ">2.0" +fresh-bakery = {path = "../.."} +pydantic-settings = "^2.5.2" + +[tool.poetry.dev-dependencies] +ruff = "*" + +[tool.mypy] +plugins = "bakery.mypy" diff --git a/examples/docs_fastapi_example/test_app/__init__.py b/examples/docs_fastapi_example/test_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/docs_fastapi_example/test_app/__main__.py b/examples/docs_fastapi_example/test_app/__main__.py new file mode 100644 index 0000000..72c98ed --- /dev/null +++ b/examples/docs_fastapi_example/test_app/__main__.py @@ -0,0 +1,151 @@ +from __future__ import annotations + +import asyncio +import random +from contextlib import asynccontextmanager +from typing import Any, AsyncIterator, Final, Literal, Mapping + +from fastapi import Depends, FastAPI +from loguru import logger +from pydantic import BaseModel, Field, PostgresDsn +from pydantic_settings import BaseSettings +from typing_extensions import Annotated, Self + +import bakery +from bakery import Bakery, Cake + +# ruff: noqa: S311 + + +# The following is a long and boring list of dependencies +class PersonOut(BaseModel): + """Person out.""" + + first_name: str + second_name: str + age: int + person_id: int + + +class FakeDbConnection: + """Fake db connection.""" + + def __init__(self, *_: Any, **__: Any) -> None: ... + + +class DatabaseFakeService: + """Fake database layer.""" + + def __init__(self, connection: FakeDbConnection) -> None: + # wannabe connection only for test purposes + self._connection: Final = connection + + async def __aenter__(self) -> Self: + return self + + async def __aexit__(self, *_args: object) -> None: + await asyncio.sleep(0) + + async def fetch_person( + self, person_id: int + ) -> dict[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(BaseSettings): + """Service settings.""" + + postgres_dsn: PostgresDsn = 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, + ) -> None: + 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: 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): + """Main bakery.""" + + config: Settings = Cake(get_settings) + _connection: FakeDbConnection = Cake( + FakeDbConnection, + config.postgres_dsn, + min_size=config.postgres_pool_min_size, + max_size=config.postgres_pool_max_size, + ) + database: DatabaseFakeService = Cake( + Cake( + DatabaseFakeService, + connection=_connection, + ) + ) + controller: ServiceController = Cake( + ServiceController, + database=database, + logger_name=config.controller_logger_name, + ) + + +@asynccontextmanager +async def lifespan(_app: FastAPI) -> AsyncIterator[None]: + logger.info("Init resources...") + bakery.logger = logger + async with MainBakeryIOC(): + yield + logger.info("Shutdown resources...") + + +MY_APP: Final = FastAPI(lifespan=lifespan) + + +# Finally, an example of how you can use your dependencies +@MY_APP.get("/person/random/") +async def create_person( + inversed_controller: Annotated[ServiceController, Depends(MainBakeryIOC.controller)], +) -> PersonOut | None: + """Fetch random person from the «database».""" + person_id: int = random.randint(10**1, 10**6) + return await inversed_controller.fetch_person(person_id) diff --git a/mkdocs.yml b/mkdocs.yml index ab8aa1a..a519dc5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,6 +9,8 @@ repo_url: https://github.com/mityuha/fresh-bakery nav: - Introduction: 'index.md' + - Bakery examples: 'bakery_examples.md' + - Bakery DI: 'bakery_di.md' - Bakery and Cakes: 'bakery_and_cakes.md' - Baking Methods: 'baking_methods.md' - Test Bakery: 'test_bakery.md' diff --git a/pyproject.toml b/pyproject.toml index a371ef4..07544a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "fresh-bakery" -version = "0.4.1" +version = "0.4.2" description = "Bake your dependencies stupidly simple!" readme = "README.md" license = "MIT" diff --git a/tests/bakery_mock/test_docs_examples.py b/tests/bakery_mock/test_docs_examples.py index 7a8aba5..c94b8f6 100644 --- a/tests/bakery_mock/test_docs_examples.py +++ b/tests/bakery_mock/test_docs_examples.py @@ -2,6 +2,8 @@ from dataclasses import dataclass +import pytest + from bakery import Bakery, BakingMethod, Cake, hand_made from bakery.testbakery import BakeryMock @@ -29,6 +31,16 @@ async def test_example_1_no_mock() -> None: assert MyBakery().settings.dsn == "fake dsn" +async def test_example_1_cant_pass_after_open() -> None: + await MyBakery.aopen() + + with pytest.raises(TypeError): + async with MyBakery(dsn="fake dsn"): # <<< passing new arguments after open is prohibited + ... + + await MyBakery.aclose() + + async def test_example_2(bakery_mock: BakeryMock) -> None: await MyBakery.aopen() # open bakery anywhere diff --git a/tests/docs_examples/__init__.py b/tests/docs_examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/docs_examples/test_almost_file_examples.py b/tests/docs_examples/test_almost_file_examples.py new file mode 100644 index 0000000..ec68963 --- /dev/null +++ b/tests/docs_examples/test_almost_file_examples.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from typing import Any, ClassVar, Final + +import pytest +from typing_extensions import Self + +from bakery import Bakery, Cake, __Cake__ + + +class FileWrapper: + file_opened: bool = False + write_lines: ClassVar[list[str]] = [] + + def __init__(self, filename: str) -> None: + self.filename: Final = filename + + def write(self, line: str) -> int: + type(self).write_lines.append(line) + return len(line) + + def __enter__(self) -> Self: + type(self).file_opened = True + return self + + def __exit__(self, *_args: object) -> None: + type(self).file_opened = False + type(self).write_lines.clear() + + +class FileBakerySeq(Bakery): + _file_obj: FileWrapper = Cake(FileWrapper, "hello.txt") + file_obj: FileWrapper = Cake(_file_obj) + write_1_bytes: int = Cake(file_obj.write, "hello, ") + write_2_bytes: int = Cake(file_obj.write, "world") + + +class FileBakery(Bakery): + file_obj: FileWrapper = Cake(Cake(FileWrapper, "hello.txt")) + write_1_bytes: int = Cake(file_obj.write, "hello, ") + write_2_bytes: int = Cake(file_obj.write, "world") + + +class FileBakeryMap(Bakery): + file_obj: FileWrapper = Cake(Cake(FileWrapper, "hello.txt")) + strs_to_write: list[str] = Cake(["hello, ", "world"]) + _: list[int] = Cake(list, Cake(map, file_obj.write, strs_to_write)) + + +class FileBakeryMapWithParams(Bakery): + filename: str = __Cake__() + strs_to_write: list[str] = __Cake__() + file_obj: FileWrapper = Cake(Cake(FileWrapper, filename)) + _: list[int] = Cake(list, Cake(map, file_obj.write, strs_to_write)) + + +@pytest.mark.parametrize( + ("bakery_cls", "kwargs"), + [ + (FileBakerySeq, {}), + (FileBakery, {}), + (FileBakeryMap, {}), + ( + FileBakeryMapWithParams, + {"filename": "hello.txt", "strs_to_write": ["hello, ", "world"]}, + ), + ], +) +async def test_file_example_1(bakery_cls: Any, kwargs: dict) -> None: + assert FileWrapper.file_opened is False + assert FileWrapper.write_lines == [] + async with FileBakeryMapWithParams( + filename="hello.txt", strs_to_write=["hello, ", "world"] + ) as bakery: + ... + async with bakery_cls(**kwargs) as bakery: + assert bakery.file_obj.filename == "hello.txt" + assert FileWrapper.file_opened is True + assert FileWrapper.write_lines == ["hello, ", "world"] + + assert FileWrapper.file_opened is False + assert FileWrapper.write_lines == [] diff --git a/tests/docs_examples/test_quick_start_example.py b/tests/docs_examples/test_quick_start_example.py new file mode 100644 index 0000000..0ebb61c --- /dev/null +++ b/tests/docs_examples/test_quick_start_example.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from bakery import Bakery, Cake + + +def full_days_in(hours: int) -> float: + return hours / 24 + + +def average(total: int, num: int) -> float: + return total / num + + +class WorkingBakery(Bakery): + average_hours: int = Cake(8) + week_hours: int = Cake(sum, [average_hours, average_hours, 7, 9, average_hours]) + full_days: float = Cake(full_days_in, week_hours) + + +async def test_hours_example() -> None: + async with WorkingBakery() as bakery: + assert bakery.week_hours == 40 + assert bakery.full_days - 0.00001 < full_days_in(40) + assert int(bakery.average_hours) == 8 diff --git a/tests/docs_examples/test_raw_example.py b/tests/docs_examples/test_raw_example.py new file mode 100644 index 0000000..f18c8fb --- /dev/null +++ b/tests/docs_examples/test_raw_example.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +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) -> None: + 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) -> None: + 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 test_raw_example() -> None: + async with MyBakeryIOC() 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"]