-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
- Loading branch information
Showing
19 changed files
with
1,680 additions
and
218 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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:[email protected]: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 | ||
|
||
|
Oops, something went wrong.