├── .DS_Store ├── .github ├── .DS_Store └── workflows │ └── main.yaml ├── .gitignore ├── LICENSE ├── README.md ├── adapters ├── __init__.py ├── base_connection.py ├── base_repository.py ├── in_memory_connection.py ├── in_memory_order_repository.py ├── in_memory_person_repository.py ├── sql_alchemy_connection.py ├── sql_alchemy_mappers.py ├── sql_alchemy_order_repository.py ├── sql_alchemy_person_repository.py ├── sqlite_connection.py ├── sqlite_order_repository.py └── sqlite_person_repository.py ├── custom_di ├── .DS_Store ├── __init__.py └── container.py ├── db └── data.db ├── di_pyramid.jpg ├── domain ├── __init__.py ├── order.py └── person.py ├── main_in_memory_custom.py ├── main_in_memory_dependency_injector.py ├── main_in_memory_injector.py ├── requirements.txt ├── tests ├── .DS_Store ├── __init__.py ├── test_container.py ├── test_in_memory_order_repository.py ├── test_in_memory_person_repository.py ├── test_sql_alchemy_person_repository.py ├── test_sqlite_order_repository.py └── test_sqlite_person_repository.py └── use_cases ├── __init__.py ├── create_person_and_order_use_case.py └── unit_of_work.py /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PatrickKalkman/python-di/2230ea4ce0e33e74b5f4f8d9a31fa5dd44f8b486/.DS_Store -------------------------------------------------------------------------------- /.github/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PatrickKalkman/python-di/2230ea4ce0e33e74b5f4f8d9a31fa5dd44f8b486/.github/.DS_Store -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: Python CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Check out code 17 | uses: actions/checkout@v3 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v3 21 | with: 22 | python-version: 3.9 23 | 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install flake8 mypy pytest 28 | pip install -r requirements.txt 29 | 30 | - name: Run flake8 31 | run: | 32 | flake8 . 33 | 34 | - name: Run mypy 35 | run: | 36 | mypy . 37 | 38 | - name: Run pytest 39 | run: | 40 | pytest 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # PyCharm 132 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 133 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 134 | # and can be added to the global gitignore or merged into this file. For a more nuclear 135 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 136 | #.idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Patrick Kalkman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-di 2 | Python’s increasing popularity has led to the development of larger and more complex projects. This growth has sparked developers’ interest in high-level software design patterns, like those prescribed by domain-driven design (DDD). 3 | 4 | Yet, implementing these patterns in Python can be challenging. 5 | 6 | This hands-on series aims to provide Python developers with practical examples. The focus is on proven architectural design patterns to manage application complexity. 7 | 8 | This repository contains examples that go with this [Medium article](https://medium.com/itnext/dependency-injection-in-python-a1e56ab8bdd0) that describes using dependency injection (DI) and DI frameworks with Python. 9 | 10 | ![Dependency in Python](/di_pyramid.jpg "Dependency in Python") -------------------------------------------------------------------------------- /adapters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PatrickKalkman/python-di/2230ea4ce0e33e74b5f4f8d9a31fa5dd44f8b486/adapters/__init__.py -------------------------------------------------------------------------------- /adapters/base_connection.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class BaseConnection(ABC): 5 | @abstractmethod 6 | def commit(self): 7 | raise NotImplementedError() 8 | 9 | @abstractmethod 10 | def rollback(self): 11 | raise NotImplementedError() 12 | -------------------------------------------------------------------------------- /adapters/base_repository.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, Generic, Optional 2 | from abc import ABC, abstractmethod 3 | 4 | T = TypeVar('T') 5 | 6 | 7 | class BaseRepository(ABC, Generic[T]): 8 | """A base class for repositories""" 9 | 10 | @abstractmethod 11 | def add(self, item: T): 12 | """Add a new item to a repository""" 13 | raise NotImplementedError() 14 | 15 | @abstractmethod 16 | def update(self, item: T): 17 | """Update an existing item in the repository""" 18 | raise NotImplementedError() 19 | 20 | @abstractmethod 21 | def delete(self, item_id: int): 22 | """Delete an existing item from a repository""" 23 | raise NotImplementedError() 24 | 25 | @abstractmethod 26 | def get_by_id(self, item_id: int) -> Optional[T]: 27 | """Retrieve an item by its id""" 28 | raise NotImplementedError() 29 | -------------------------------------------------------------------------------- /adapters/in_memory_connection.py: -------------------------------------------------------------------------------- 1 | 2 | from adapters.base_connection import BaseConnection 3 | 4 | 5 | class InMemoryConnection(BaseConnection): 6 | def commit(self): 7 | pass 8 | 9 | def rollback(self): 10 | pass 11 | -------------------------------------------------------------------------------- /adapters/in_memory_order_repository.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from adapters.base_repository import BaseRepository 3 | from domain.order import Order 4 | 5 | 6 | class InMemoryOrderRepository(BaseRepository[Order]): 7 | def __init__(self): 8 | self.orders = {} 9 | 10 | def add(self, order: Order): 11 | self.orders[order.id] = order 12 | 13 | def get_by_id(self, order_id: int) -> Optional[Order]: 14 | return self.orders.get(order_id) 15 | 16 | def update(self, order: Order): 17 | self.orders[order.id] = order 18 | 19 | def delete(self, order_id: int): 20 | self.orders.pop(order_id, None) 21 | -------------------------------------------------------------------------------- /adapters/in_memory_person_repository.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from adapters.base_repository import BaseRepository 3 | from domain.person import Person 4 | 5 | 6 | class InMemoryPersonRepository(BaseRepository[Person]): 7 | def __init__(self): 8 | self.persons = {} 9 | 10 | def add(self, person: Person): 11 | self.persons[person.id] = person 12 | 13 | def get_by_id(self, person_id: int) -> Optional[Person]: 14 | return self.persons.get(person_id) 15 | 16 | def update(self, person: Person): 17 | self.persons[person.id] = person 18 | 19 | def delete(self, person_id: int): 20 | self.persons.pop(person_id, None) 21 | -------------------------------------------------------------------------------- /adapters/sql_alchemy_connection.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | from adapters.base_connection import BaseConnection 3 | 4 | 5 | class SQLAlchemyConnection(BaseConnection): 6 | def __init__(self, session: Session): 7 | self.session = session 8 | 9 | def commit(self): 10 | self.session.commit() 11 | 12 | def rollback(self): 13 | self.session.rollback() 14 | -------------------------------------------------------------------------------- /adapters/sql_alchemy_mappers.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Table, Column, Integer, String, Float, ForeignKey 2 | from sqlalchemy.orm import registry 3 | 4 | from domain.order import Order 5 | from domain.person import Person 6 | 7 | 8 | def create_tables_and_mappers(metadata): 9 | person_table = Table( 10 | 'person', metadata, 11 | Column('id', Integer, primary_key=True), 12 | Column('name', String), 13 | Column('age', Integer) 14 | ) 15 | 16 | order_table = Table( 17 | 'order', metadata, 18 | Column('id', Integer, primary_key=True), 19 | Column('person_id', Integer, ForeignKey('person.id')), 20 | Column('order_date', String), 21 | Column('total_amount', Float) 22 | ) 23 | 24 | mapper_registry = registry() 25 | mapper_registry.map_imperatively(Person, person_table) 26 | mapper_registry.map_imperatively(Order, order_table) 27 | -------------------------------------------------------------------------------- /adapters/sql_alchemy_order_repository.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from sqlalchemy.orm import Session 3 | 4 | from adapters.base_repository import BaseRepository 5 | from domain.order import Order 6 | 7 | 8 | class SQLAlchemyOrderRepository(BaseRepository[Order]): 9 | def __init__(self, session: Session): 10 | self.session = session 11 | 12 | def add(self, order: Order): 13 | self.session.add(order) 14 | 15 | def update(self, order: Order): 16 | self.session.merge(order) 17 | 18 | def delete(self, order_id: int): 19 | order = self.session.get(Order, order_id) 20 | if order: 21 | self.session.delete(order) 22 | 23 | def get_by_id(self, order_id: int) -> Optional[Order]: 24 | return self.session.get(Order, order_id) 25 | -------------------------------------------------------------------------------- /adapters/sql_alchemy_person_repository.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from sqlalchemy.orm import Session 3 | 4 | from adapters.base_repository import BaseRepository 5 | from domain.person import Person 6 | 7 | 8 | class SQLAlchemyPersonRepository(BaseRepository[Person]): 9 | def __init__(self, session: Session): 10 | self.session = session 11 | 12 | def add(self, person: Person): 13 | self.session.add(person) 14 | # flush() is needed to get the id of the person 15 | self.session.flush() 16 | 17 | def update(self, person: Person): 18 | self.session.merge(person) 19 | 20 | def delete(self, person_id: int): 21 | person = self.session.get(Person, person_id) 22 | if person: 23 | self.session.delete(person) 24 | 25 | def get_by_id(self, person_id: int) -> Optional[Person]: 26 | return self.session.get(Person, person_id) 27 | -------------------------------------------------------------------------------- /adapters/sqlite_connection.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from adapters.base_connection import BaseConnection 3 | 4 | 5 | class SQLiteConnection(BaseConnection): 6 | def __init__(self, connection: sqlite3.Connection): 7 | self.connection = connection 8 | 9 | def commit(self): 10 | self.connection.commit() 11 | 12 | def rollback(self): 13 | self.connection.rollback() 14 | -------------------------------------------------------------------------------- /adapters/sqlite_order_repository.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from domain.order import Order 4 | from adapters.base_repository import BaseRepository 5 | 6 | 7 | class SQLiteOrderRepository(BaseRepository[Order]): 8 | def __init__(self, connection): 9 | self.connection = connection 10 | self._create_table() 11 | 12 | def _create_table(self): 13 | cursor = self.connection.cursor() 14 | cursor.execute(''' 15 | CREATE TABLE IF NOT EXISTS orders ( 16 | id INTEGER PRIMARY KEY AUTOINCREMENT, 17 | person_id INTEGER NOT NULL, 18 | order_date TEXT NOT NULL, 19 | total_amount REAL NOT NULL, 20 | FOREIGN KEY (person_id) REFERENCES persons (id) 21 | ) 22 | ''') 23 | self.connection.commit() 24 | 25 | def add(self, order: Order): 26 | query = """ 27 | INSERT INTO orders (person_id, order_date, total_amount) 28 | VALUES (?, ?, ?) 29 | """ 30 | cursor = self.connection.cursor() 31 | cursor.execute(query, (order.person_id, order.order_date, 32 | order.total_amount)) 33 | order.id = cursor.lastrowid 34 | 35 | def update(self, order: Order): 36 | query = """ 37 | UPDATE orders 38 | SET person_id = ?, order_date = ?, total_amount = ? 39 | WHERE id = ? 40 | """ 41 | self.connection.execute(query, (order.person_id, order.order_date, 42 | order.total_amount, order.id)) 43 | 44 | def delete(self, order_id: int): 45 | query = "DELETE FROM orders WHERE id = ?" 46 | self.connection.execute(query, (order_id,)) 47 | 48 | def get_by_id(self, order_id: int) -> Optional[Order]: 49 | query = """ 50 | SELECT id, person_id, order_date, total_amount 51 | FROM orders WHERE id = ? 52 | """ 53 | cursor = self.connection.execute(query, (order_id,)) 54 | row = cursor.fetchone() 55 | if row: 56 | return Order(id=row[0], person_id=row[1], order_date=row[2], 57 | total_amount=row[3]) 58 | return None 59 | -------------------------------------------------------------------------------- /adapters/sqlite_person_repository.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from adapters.base_repository import BaseRepository 4 | from domain.person import Person 5 | 6 | 7 | class SQLitePersonRepository(BaseRepository[Person]): 8 | def __init__(self, connection): 9 | self.connection = connection 10 | self._create_table() 11 | 12 | def _create_table(self): 13 | cursor = self.connection.cursor() 14 | cursor.execute(""" 15 | CREATE TABLE IF NOT EXISTS persons ( 16 | id INTEGER PRIMARY KEY AUTOINCREMENT, 17 | name TEXT NOT NULL, 18 | age INTEGER NOT NULL 19 | ) 20 | """) 21 | self.connection.commit() 22 | 23 | def add(self, person: Person): 24 | cursor = self.connection.cursor() 25 | cursor.execute( 26 | "INSERT INTO persons (name, age) VALUES (?, ?)", 27 | (person.name, person.age) 28 | ) 29 | person.id = cursor.lastrowid 30 | 31 | def get_by_id(self, person_id: int) -> Optional[Person]: 32 | cursor = self.connection.cursor() 33 | cursor.execute("SELECT id, name, age FROM persons WHERE id=?", 34 | (person_id,)) 35 | row = cursor.fetchone() 36 | if row: 37 | return Person(row[1], row[2], row[0]) 38 | return None 39 | 40 | def update(self, person: Person): 41 | cursor = self.connection.cursor() 42 | cursor.execute( 43 | "UPDATE persons SET name=?, age=? WHERE id=?", 44 | (person.name, person.age, person.id) 45 | ) 46 | 47 | def delete(self, person_id: int): 48 | cursor = self.connection.cursor() 49 | cursor.execute("DELETE FROM persons WHERE id=?", (person_id,)) 50 | -------------------------------------------------------------------------------- /custom_di/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PatrickKalkman/python-di/2230ea4ce0e33e74b5f4f8d9a31fa5dd44f8b486/custom_di/.DS_Store -------------------------------------------------------------------------------- /custom_di/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PatrickKalkman/python-di/2230ea4ce0e33e74b5f4f8d9a31fa5dd44f8b486/custom_di/__init__.py -------------------------------------------------------------------------------- /custom_di/container.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | 4 | class Container: 5 | def __init__(self): 6 | self._registry = {} 7 | 8 | def register(self, dependency_type, implementation=None): 9 | if not implementation: 10 | implementation = dependency_type 11 | 12 | for base in inspect.getmro(implementation): 13 | if base not in (object, dependency_type): 14 | self._registry[base] = implementation 15 | 16 | self._registry[dependency_type] = implementation 17 | 18 | def resolve(self, dependency_type): 19 | if dependency_type not in self._registry: 20 | raise ValueError(f"Dependency {dependency_type} not registered") 21 | implementation = self._registry[dependency_type] 22 | constructor_signature = inspect.signature(implementation.__init__) 23 | constructor_params = constructor_signature.parameters.values() 24 | 25 | dependencies = [ 26 | self.resolve(param.annotation) 27 | for param in constructor_params 28 | if param.annotation is not inspect.Parameter.empty 29 | ] 30 | 31 | return implementation(*dependencies) 32 | -------------------------------------------------------------------------------- /db/data.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PatrickKalkman/python-di/2230ea4ce0e33e74b5f4f8d9a31fa5dd44f8b486/db/data.db -------------------------------------------------------------------------------- /di_pyramid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PatrickKalkman/python-di/2230ea4ce0e33e74b5f4f8d9a31fa5dd44f8b486/di_pyramid.jpg -------------------------------------------------------------------------------- /domain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PatrickKalkman/python-di/2230ea4ce0e33e74b5f4f8d9a31fa5dd44f8b486/domain/__init__.py -------------------------------------------------------------------------------- /domain/order.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | 5 | @dataclass 6 | class Order: 7 | order_date: str 8 | total_amount: float 9 | person_id: Optional[int] = None 10 | id: Optional[int] = None 11 | -------------------------------------------------------------------------------- /domain/person.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | 5 | @dataclass 6 | class Person: 7 | name: str 8 | age: int 9 | id: Optional[int] = None 10 | -------------------------------------------------------------------------------- /main_in_memory_custom.py: -------------------------------------------------------------------------------- 1 | from adapters.in_memory_person_repository import InMemoryPersonRepository 2 | from adapters.base_repository import BaseRepository 3 | from adapters.in_memory_order_repository import InMemoryOrderRepository 4 | from adapters.in_memory_connection import InMemoryConnection 5 | from adapters.base_connection import BaseConnection 6 | from use_cases.unit_of_work import UnitOfWork 7 | from domain.person import Person 8 | from domain.order import Order 9 | from use_cases.create_person_and_order_use_case import ( 10 | CreatePersonAndOrderUseCase) 11 | 12 | from custom_di.container import Container 13 | 14 | container = Container() 15 | container.register(BaseConnection, InMemoryConnection) 16 | container.register(BaseRepository[Person], InMemoryPersonRepository) 17 | container.register(BaseRepository[Order], InMemoryOrderRepository) 18 | container.register(UnitOfWork) 19 | container.register(CreatePersonAndOrderUseCase) 20 | 21 | create_use_case = container.resolve(CreatePersonAndOrderUseCase) 22 | 23 | new_person = Person(id=1, name="John Doe", age=30) 24 | new_order = Order(id=1, order_date="2023-04-03", total_amount=100.0) 25 | 26 | person, order = create_use_case.execute(new_person, new_order) 27 | print(person, order) 28 | -------------------------------------------------------------------------------- /main_in_memory_dependency_injector.py: -------------------------------------------------------------------------------- 1 | from dependency_injector import providers, containers 2 | from adapters.in_memory_person_repository import InMemoryPersonRepository 3 | from adapters.in_memory_order_repository import InMemoryOrderRepository 4 | from adapters.in_memory_connection import InMemoryConnection 5 | from use_cases.unit_of_work import UnitOfWork 6 | from domain.person import Person 7 | from domain.order import Order 8 | from use_cases.create_person_and_order_use_case import ( 9 | CreatePersonAndOrderUseCase) 10 | 11 | 12 | class Container(containers.DeclarativeContainer): 13 | connection = providers.Singleton( 14 | InMemoryConnection 15 | ) 16 | 17 | person_repository = providers.Singleton( 18 | InMemoryPersonRepository 19 | ) 20 | 21 | order_repository = providers.Singleton( 22 | InMemoryOrderRepository 23 | ) 24 | 25 | unit_of_work = providers.Singleton( 26 | UnitOfWork, 27 | connection=connection, 28 | person_repository=person_repository, 29 | order_repository=order_repository 30 | ) 31 | 32 | create_use_case = providers.Factory( 33 | CreatePersonAndOrderUseCase, 34 | unit_of_work=unit_of_work 35 | ) 36 | 37 | 38 | if __name__ == '__main__': 39 | container = Container() 40 | create_use_case = container.create_use_case() 41 | 42 | new_person = Person(id=1, name="John Doe", age=30) 43 | new_order = Order(id=1, order_date="2023-04-03", total_amount=100.0) 44 | 45 | person, order = create_use_case.execute(new_person, new_order) 46 | print(person, order) 47 | -------------------------------------------------------------------------------- /main_in_memory_injector.py: -------------------------------------------------------------------------------- 1 | from injector import Injector, inject, Module, provider, singleton 2 | from adapters.in_memory_person_repository import InMemoryPersonRepository 3 | from adapters.in_memory_order_repository import InMemoryOrderRepository 4 | from adapters.in_memory_connection import InMemoryConnection 5 | from use_cases.unit_of_work import UnitOfWork 6 | from domain.person import Person 7 | from domain.order import Order 8 | from use_cases.create_person_and_order_use_case import ( 9 | CreatePersonAndOrderUseCase) 10 | 11 | 12 | class AppModule(Module): 13 | @singleton 14 | @provider 15 | def provide_connection(self) -> InMemoryConnection: 16 | return InMemoryConnection() 17 | 18 | @singleton 19 | @provider 20 | def provide_person_repository(self) -> InMemoryPersonRepository: 21 | return InMemoryPersonRepository() 22 | 23 | @singleton 24 | @provider 25 | def provide_order_repository(self) -> InMemoryOrderRepository: 26 | return InMemoryOrderRepository() 27 | 28 | @inject 29 | @singleton 30 | @provider 31 | def provide_unit_of_work( 32 | self, 33 | connection: InMemoryConnection, 34 | person_repository: InMemoryPersonRepository, 35 | order_repository: InMemoryOrderRepository 36 | ) -> UnitOfWork: 37 | return UnitOfWork(connection, person_repository, order_repository) 38 | 39 | @inject 40 | @singleton 41 | @provider 42 | def provide_create_use_case( 43 | self, 44 | unit_of_work: UnitOfWork 45 | ) -> CreatePersonAndOrderUseCase: 46 | return CreatePersonAndOrderUseCase(unit_of_work) 47 | 48 | 49 | injector = Injector(AppModule()) 50 | create_use_case = injector.get(CreatePersonAndOrderUseCase) 51 | 52 | new_person = Person(id=1, name="John Doe", age=30) 53 | new_order = Order(id=1, order_date="2023-04-03", total_amount=100.0) 54 | 55 | person, order = create_use_case.execute(new_person, new_order) 56 | print(person, order) 57 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dependency_injector==4.41.0 2 | injector==0.20.1 3 | pytest==7.2.2 4 | SQLAlchemy==2.0.9 5 | -------------------------------------------------------------------------------- /tests/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PatrickKalkman/python-di/2230ea4ce0e33e74b5f4f8d9a31fa5dd44f8b486/tests/.DS_Store -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PatrickKalkman/python-di/2230ea4ce0e33e74b5f4f8d9a31fa5dd44f8b486/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_container.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from custom_di.container import Container 3 | 4 | 5 | # Sample classes for testing 6 | class Engine: 7 | pass 8 | 9 | 10 | class DieselEngine(Engine): 11 | pass 12 | 13 | 14 | class Car: 15 | def __init__(self, engine: Engine): 16 | self.engine = engine 17 | 18 | 19 | # Test functions 20 | def test_resolve_simple_dependency(): 21 | container = Container() 22 | container.register(Engine, DieselEngine) 23 | container.register(Car) 24 | 25 | car_instance = container.resolve(Car) 26 | assert isinstance(car_instance, Car) 27 | assert isinstance(car_instance.engine, DieselEngine) 28 | 29 | 30 | def test_resolve_unregistered_dependency(): 31 | container = Container() 32 | 33 | with pytest.raises(ValueError): 34 | container.resolve(Engine) 35 | 36 | 37 | def test_resolve_base_class_dependency(): 38 | container = Container() 39 | container.register(Engine, DieselEngine) 40 | container.register(Car) 41 | 42 | engine_instance = container.resolve(Engine) 43 | assert isinstance(engine_instance, DieselEngine) 44 | -------------------------------------------------------------------------------- /tests/test_in_memory_order_repository.py: -------------------------------------------------------------------------------- 1 | from domain.order import Order 2 | from adapters.in_memory_order_repository import InMemoryOrderRepository 3 | 4 | 5 | def test_in_memory_order_repository(): 6 | repo = InMemoryOrderRepository() 7 | order1 = Order(id=1, person_id=1, order_date="2022-01-01", 8 | total_amount=10.0) 9 | order2 = Order(id=2, person_id=2, order_date="2022-01-02", 10 | total_amount=20.0) 11 | 12 | # Add orders 13 | repo.add(order1) 14 | repo.add(order2) 15 | 16 | # Get order by id 17 | assert repo.get_by_id(order1.id) == order1 18 | assert repo.get_by_id(order2.id) == order2 19 | 20 | # Update order 21 | order1.total_amount = 15.0 22 | repo.update(order1) 23 | assert repo.get_by_id(order1.id).total_amount == 15.0 24 | 25 | # Delete order 26 | repo.delete(order2.id) 27 | assert repo.get_by_id(order2.id) is None 28 | -------------------------------------------------------------------------------- /tests/test_in_memory_person_repository.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from domain.person import Person 3 | from adapters.in_memory_person_repository import InMemoryPersonRepository 4 | 5 | 6 | @pytest.fixture 7 | def repository(): 8 | return InMemoryPersonRepository() 9 | 10 | 11 | def test_add_person(repository): 12 | person = Person(name="John Doe", age=30, id=1) 13 | repository.add(person) 14 | 15 | retrieved_person = repository.get_by_id(1) 16 | assert retrieved_person is not None 17 | assert retrieved_person.name == person.name 18 | assert retrieved_person.age == person.age 19 | 20 | 21 | def test_update_person(repository): 22 | person = Person(name="John Doe", age=30, id=1) 23 | repository.add(person) 24 | 25 | person.name = "Jane Doe" 26 | person.age = 28 27 | repository.update(person) 28 | 29 | updated_person = repository.get_by_id(1) 30 | assert updated_person.name == "Jane Doe" 31 | assert updated_person.age == 28 32 | 33 | 34 | def test_delete_person(repository): 35 | person = Person(name="John Doe", age=30, id=1) 36 | repository.add(person) 37 | repository.delete(1) 38 | 39 | deleted_person = repository.get_by_id(1) 40 | assert deleted_person is None 41 | 42 | 43 | def test_get_by_id_person_not_found(repository): 44 | non_existent_person = repository.get_by_id(999) 45 | assert non_existent_person is None 46 | -------------------------------------------------------------------------------- /tests/test_sql_alchemy_person_repository.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy import create_engine 3 | from sqlalchemy.orm import sessionmaker, declarative_base 4 | 5 | from domain.person import Person 6 | from adapters.sql_alchemy_person_repository import SQLAlchemyPersonRepository 7 | from adapters.sql_alchemy_mappers import create_tables_and_mappers 8 | 9 | 10 | Base = declarative_base() 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | def engine(): 15 | return create_engine("sqlite:///:memory:") 16 | 17 | 18 | @pytest.fixture(scope="module") 19 | def connection(engine): 20 | with engine.connect() as connection: 21 | yield connection 22 | 23 | 24 | @pytest.fixture(scope="module") 25 | def session(engine, connection): 26 | Session = sessionmaker(bind=engine) 27 | create_tables_and_mappers(Base.metadata) 28 | Base.metadata.create_all(bind=engine) 29 | session = Session(bind=connection) 30 | yield session 31 | session.close() 32 | 33 | 34 | @pytest.fixture 35 | def repository(session): 36 | return SQLAlchemyPersonRepository(session) 37 | 38 | 39 | def test_add_person(repository): 40 | person = Person(name="John Doe", age=30) 41 | repository.add(person) 42 | 43 | retrieved_person = repository.get_by_id(person.id) 44 | assert retrieved_person is not None 45 | assert retrieved_person.name == person.name 46 | assert retrieved_person.age == person.age 47 | 48 | 49 | def test_update_person(repository): 50 | person = Person(name="John Doe", age=30) 51 | repository.add(person) 52 | 53 | person.name = "Jane Doe" 54 | person.age = 28 55 | repository.update(person) 56 | 57 | updated_person = repository.get_by_id(person.id) 58 | assert updated_person.name == "Jane Doe" 59 | assert updated_person.age == 28 60 | 61 | 62 | def test_delete_person(repository, session): 63 | person = Person(name="John Doe", age=30) 64 | repository.add(person) 65 | repository.delete(person.id) 66 | session.flush() 67 | 68 | deleted_person = repository.get_by_id(person.id) 69 | assert deleted_person is None 70 | 71 | 72 | def test_get_by_id_person_not_found(repository): 73 | non_existent_person = repository.get_by_id(999) 74 | assert non_existent_person is None 75 | -------------------------------------------------------------------------------- /tests/test_sqlite_order_repository.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from domain.order import Order 3 | from adapters.sqlite_order_repository import SQLiteOrderRepository 4 | import pytest 5 | 6 | 7 | @pytest.fixture 8 | def connection(): 9 | conn = sqlite3.connect(":memory:") 10 | yield conn 11 | conn.close() 12 | 13 | 14 | @pytest.fixture 15 | def repository(connection): 16 | return SQLiteOrderRepository(connection) 17 | 18 | 19 | def test_add_order(repository): 20 | order = Order(person_id=1, order_date="2022-01-01", total_amount=100.0) 21 | repository.add(order) 22 | 23 | retrieved_order = repository.get_by_id(order.id) 24 | assert retrieved_order is not None 25 | assert retrieved_order.person_id == order.person_id 26 | assert retrieved_order.order_date == order.order_date 27 | assert retrieved_order.total_amount == order.total_amount 28 | 29 | 30 | def test_update_order(repository): 31 | order = Order(person_id=1, order_date="2022-01-01", total_amount=100.0) 32 | repository.add(order) 33 | 34 | order.person_id = 2 35 | order.order_date = "2022-01-02" 36 | order.total_amount = 200.0 37 | repository.update(order) 38 | 39 | updated_order = repository.get_by_id(order.id) 40 | assert updated_order.person_id == 2 41 | assert updated_order.order_date == "2022-01-02" 42 | assert updated_order.total_amount == 200.0 43 | 44 | 45 | def test_delete_order(repository): 46 | order = Order(person_id=1, order_date="2022-01-01", total_amount=100.0) 47 | repository.add(order) 48 | repository.delete(order.id) 49 | 50 | deleted_order = repository.get_by_id(order.id) 51 | assert deleted_order is None 52 | 53 | 54 | def test_get_by_id_order_not_found(repository): 55 | non_existent_order = repository.get_by_id(999) 56 | assert non_existent_order is None 57 | -------------------------------------------------------------------------------- /tests/test_sqlite_person_repository.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from domain.person import Person 3 | from adapters.sqlite_person_repository import SQLitePersonRepository 4 | import pytest 5 | 6 | 7 | @pytest.fixture 8 | def connection(): 9 | conn = sqlite3.connect(":memory:") 10 | yield conn 11 | conn.close() 12 | 13 | 14 | @pytest.fixture 15 | def repository(connection): 16 | return SQLitePersonRepository(connection) 17 | 18 | 19 | def test_add_person(repository): 20 | person = Person(name="John Doe", age=30) 21 | repository.add(person) 22 | 23 | retrieved_person = repository.get_by_id(person.id) 24 | assert retrieved_person is not None 25 | assert retrieved_person.name == person.name 26 | assert retrieved_person.age == person.age 27 | 28 | 29 | def test_update_person(repository): 30 | person = Person(name="John Doe", age=30) 31 | repository.add(person) 32 | 33 | person.name = "Jane Doe" 34 | person.age = 28 35 | repository.update(person) 36 | 37 | updated_person = repository.get_by_id(person.id) 38 | assert updated_person.name == "Jane Doe" 39 | assert updated_person.age == 28 40 | 41 | 42 | def test_delete_person(repository): 43 | person = Person(name="John Doe", age=30) 44 | repository.add(person) 45 | repository.delete(person.id) 46 | 47 | deleted_person = repository.get_by_id(person.id) 48 | assert deleted_person is None 49 | 50 | 51 | def test_get_by_id_person_not_found(repository): 52 | non_existent_person = repository.get_by_id(999) 53 | assert non_existent_person is None 54 | -------------------------------------------------------------------------------- /use_cases/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PatrickKalkman/python-di/2230ea4ce0e33e74b5f4f8d9a31fa5dd44f8b486/use_cases/__init__.py -------------------------------------------------------------------------------- /use_cases/create_person_and_order_use_case.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | from domain.person import Person 4 | from domain.order import Order 5 | from use_cases.unit_of_work import UnitOfWork 6 | 7 | 8 | class CreatePersonAndOrderUseCase: 9 | def __init__(self, unit_of_work: UnitOfWork): 10 | self.unit_of_work = unit_of_work 11 | 12 | def execute(self, person: Person, order: Order) -> Tuple[Person, Order]: 13 | with self.unit_of_work as uow: 14 | uow.persons.add(person) 15 | 16 | if person.id is not None: 17 | order.person_id = int(person.id) 18 | else: 19 | raise ValueError("Person id cannot be None") 20 | 21 | uow.orders.add(order) 22 | 23 | return person, order 24 | -------------------------------------------------------------------------------- /use_cases/unit_of_work.py: -------------------------------------------------------------------------------- 1 | from adapters.base_connection import BaseConnection 2 | from adapters.base_repository import BaseRepository 3 | from domain.order import Order 4 | from domain.person import Person 5 | 6 | 7 | class UnitOfWork: 8 | def __init__(self, connection: BaseConnection, 9 | person_repository: BaseRepository[Person], 10 | order_repository: BaseRepository[Order]): 11 | self.persons = person_repository 12 | self.orders = order_repository 13 | self.connection = connection 14 | 15 | def __enter__(self): 16 | return self 17 | 18 | def __exit__(self, exc_type, exc_val, exc_tb): 19 | if exc_type: 20 | self.rollback() 21 | else: 22 | self.commit() 23 | 24 | def commit(self): 25 | self.connection.commit() 26 | 27 | def rollback(self): 28 | self.connection.rollback() 29 | --------------------------------------------------------------------------------