Update bundles example (#292)

* Fix dependency provider stub issue with required init arg

* Refactor bundles example app

* Rename bundles package

* Rename bundles example container

* Rename bundles mini app to decoupled packages

* Move decoupled packages example to main examples

* Fix a typo

* Add meta
This commit is contained in:
Roman Mogylatov 2020-09-06 21:51:38 -04:00 committed by GitHub
parent 06bc0f1bac
commit 29f209d382
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 337 additions and 205 deletions

View File

@ -1,74 +0,0 @@
Bundles mini application example
--------------------------------
.. currentmodule:: dependency_injector.containers
"Bundles" is an example mini application that is intended to demonstrate the
power of dependency injection for creation of re-usable application components
("bundles") with 100% transparency of their dependencies.
Example application
~~~~~~~~~~~~~~~~~~~
"Bundles" mini application has next structure:
.. code-block:: bash
bundles/
bundles/ <-- Bundles package
photos/ <-- Photos bundle
__init__.py <-- Photos bundle dependency injection container
entities.py
repositories.py
users/ <-- Users bundle
__init__.py <-- Users bundle dependency injection container
entities.py
repositories.py
run.py <-- Entrypoint
IoC containers
~~~~~~~~~~~~~~
Next two listings show :py:class:`DeclarativeContainer`'s for "users" and
"photos" bundles.
Listing of ``bundles/users/__init__.py``:
.. literalinclude:: ../../examples/miniapps/bundles/bundles/users/__init__.py
:language: python
.. note::
- ``Users`` container has dependency on database.
Listing of ``bundles/photos/__init__.py``:
.. literalinclude:: ../../examples/miniapps/bundles/bundles/photos/__init__.py
:language: python
.. note::
- ``Photos`` container has dependencies on database and file storage.
Run application
~~~~~~~~~~~~~~~
Finally, both "bundles" are initialized by providing needed dependencies.
Initialization of dependencies happens right in the runtime, not earlier.
Generally, it means, that any part of any bundle could be overridden on the
fly.
Listing of ``run.py``:
.. literalinclude:: ../../examples/miniapps/bundles/run.py
:language: python
Links
~~~~~
+ `Dependency Injector <https://github.com/ets-labs/python-dependency-injector/>`_
+ `Full example sources <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/bundles>`_
.. disqus::

View File

@ -8,14 +8,11 @@ Other examples
of inversion of control principle and powered by of inversion of control principle and powered by
"Dependency Injector" framework. "Dependency Injector" framework.
Current section of documentation is designed to provide several example mini This sections contains assorted ``Dependency Injector`` examples.
applications that are built according to the inversion of control principle
and powered by *Dependency Injector* framework.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
bundles_miniapp
use_cases_miniapp use_cases_miniapp
password_hashing_miniapp password_hashing_miniapp
chained_factories chained_factories

View File

@ -0,0 +1,130 @@
Decoupled packages example (multiple containers)
================================================
.. meta::
:keywords: Python,Dependency Injection,Inversion of Control,Container,Example,Application,
Framework,AWS,boto3,client
:description: This example shows how to use Dependency Injector to create Python decoupled packages.
To achieve a decoupling each package has a container with the components. When
a component needs a dependency from the outside of the package scope we use the
Dependency provider. The package container has no knowledge on where the
dependencies come from. It states a need that the dependencies must be provided.
This helps to decouple a package from the 3rd party dependencies and other
packages.
This example shows how to use ``Dependency Injector`` to create decoupled packages.
To achieve a decoupling each package has a container with the components. When a component needs a
dependency from the outside of the package scope we use the ``Dependency`` provider. The package
container has no knowledge on where the dependencies come from. It states a need that the
dependencies must be provided. This helps to decouple a package from the 3rd party dependencies
and other packages.
To wire the packages we use an application container. Application container has all 3rd party
dependencies and package containers. It wires the packages and dependencies to create a
complete application.
We build an example micro application that consists of 3 packages:
- ``user`` - a package with user domain logic, depends on a database
- ``photo`` - a package with photo domain logic, depends on a database and AWS S3
- ``analytics`` - a package with analytics domain logic, depends on the ``user`` and ``photo``
package components
.. image:: images/decoupled-packages.png
:width: 100%
:align: center
Start from the scratch or jump to the section:
.. contents::
:local:
:backlinks: none
You can find the source code and instructions for running on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/decoupled-packages>`_.
Application structure
---------------------
Application consists of an ``example`` package, a configuration file and a ``requirements.txt``
file.
.. code-block:: bash
./
├── example/
│ ├── analytics/
│ │ ├── __init__.py
│ │ ├── containers.py
│ │ └── services.py
│ ├── photo/
│ │ ├── __init__.py
│ │ ├── containers.py
│ │ ├── entities.py
│ │ └── repositories.py
│ ├── user/
│ │ ├── __init__.py
│ │ ├── containers.py
│ │ ├── entities.py
│ │ └── repositories.py
│ ├── __init__.py
│ ├── __main__.py
│ └── containers.py
├── config.ini
└── requirements.txt
Package containers
------------------
Listing of the ``example/user/containers.py``:
.. literalinclude:: ../../examples/miniapps/decoupled-packages/example/user/containers.py
:language: python
Listing of the ``example/photo/containers.py``:
.. literalinclude:: ../../examples/miniapps/decoupled-packages/example/photo/containers.py
:language: python
Listing of the ``example/analytics/containers.py``:
.. literalinclude:: ../../examples/miniapps/decoupled-packages/example/analytics/containers.py
:language: python
Application container
---------------------
Application container consists of all packages and 3rd party dependencies. Its role is to wire
everything together in a complete application.
Listing of the ``example/containers.py``:
.. literalinclude:: ../../examples/miniapps/decoupled-packages/example/containers.py
:language: python
.. note::
Package ``analytics`` has dependencies on the repositories from the ``user`` and
``photo`` packages. This is an example of how you can pass the dependencies from one package
to another.
Main module
-----------
Listing of the ``example/__main__.py``:
.. literalinclude:: ../../examples/miniapps/decoupled-packages/example/__main__.py
:language: python
Configuration
-------------
Listing of the ``config.ini``:
.. literalinclude:: ../../examples/miniapps/decoupled-packages/config.ini
:language: ini
Run the application
-------------------
You can find the source code and instructions for running on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/decoupled-packages>`_.
.. disqus::

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

View File

@ -12,5 +12,6 @@ Explore the examples to see the ``Dependency Injector`` in action.
application-single-container application-single-container
application-multiple-containers application-multiple-containers
decoupled-packages
.. disqus:: .. disqus::

View File

@ -1,8 +0,0 @@
Dependency Injector Bundles example
===================================
Instructions for running
.. code-block:: bash
python run.py

View File

@ -1 +0,0 @@
"""Bundles package."""

View File

@ -1,20 +0,0 @@
"""Photos bundle."""
from dependency_injector import containers
from dependency_injector import providers
from . import entities
from . import repositories
class Photos(containers.DeclarativeContainer):
"""Photos bundle container."""
database = providers.Dependency()
file_storage = providers.Dependency()
photo = providers.Factory(entities.Photo)
photo_repository = providers.Singleton(repositories.PhotoRepository,
object_factory=photo.provider,
fs=file_storage,
db=database)

View File

@ -1,5 +0,0 @@
"""Photos bundle entities module."""
class Photo:
"""Photo entity."""

View File

@ -1,11 +0,0 @@
"""Photos bundle entity repositories module."""
class PhotoRepository:
"""Photo entity repository."""
def __init__(self, object_factory, fs, db):
"""Initialize instance."""
self.object_factory = object_factory
self.fs = fs
self.db = db

View File

@ -1,18 +0,0 @@
"""Users bundle."""
from dependency_injector import containers
from dependency_injector import providers
from . import entities
from . import repositories
class Users(containers.DeclarativeContainer):
"""Users bundle container."""
database = providers.Dependency()
user = providers.Factory(entities.User)
user_repository = providers.Singleton(repositories.UserRepository,
object_factory=user.provider,
db=database)

View File

@ -1,9 +0,0 @@
"""Users bundle entities module."""
class User:
"""User entity."""
def __init__(self, id):
"""Initialize instance."""
self.id = id

View File

@ -1,14 +0,0 @@
"""Users bundle entity repositories module."""
class UserRepository:
"""User entity repository."""
def __init__(self, object_factory, db):
"""Initialize instance."""
self.object_factory = object_factory
self.db = db
def get(self, id):
"""Return user entity with given identifier."""
return self.object_factory(id=id)

View File

@ -1,40 +0,0 @@
"""Run 'Bundles' example application."""
import sqlite3
import boto3
from dependency_injector import containers
from dependency_injector import providers
from bundles.users import Users
from bundles.photos import Photos
class Core(containers.DeclarativeContainer):
"""Core container."""
config = providers.Configuration('config')
sqlite = providers.Singleton(sqlite3.connect, config.database.dsn)
s3 = providers.Singleton(
boto3.client, 's3',
aws_access_key_id=config.aws.access_key_id,
aws_secret_access_key=config.aws.secret_access_key)
if __name__ == '__main__':
# Initializing containers
core = Core(config={'database': {'dsn': ':memory:'},
'aws': {'access_key_id': 'KEY',
'secret_access_key': 'SECRET'}})
users = Users(database=core.sqlite)
photos = Photos(database=core.sqlite, file_storage=core.s3)
# Fetching few users
user_repository = users.user_repository()
user1 = user_repository.get(id=1)
user2 = user_repository.get(id=2)
# Making some checks
assert user1.id == 1
assert user2.id == 2
assert user_repository.db is core.sqlite()

View File

@ -0,0 +1,29 @@
Decoupled packages example
==========================
Create virtual env:
.. code-block:: bash
python3 -m venv venv
. venv/bin/activate
Install requirements:
.. code-block:: bash
pip install -r requirements.txt
Run:
.. code-block:: bash
python -m example
You should see:
.. code-block:: bash
Retrieve user id=1, photos count=5
Retrieve user id=2, photos count=10
Aggregate analytics from user and photo bundles

View File

@ -0,0 +1,6 @@
[database]
dsn=:memory:
[aws]
access_key_id=KEY
secret_access_key=SECRET

View File

@ -0,0 +1 @@
"""Top-level package."""

View File

@ -0,0 +1,24 @@
"""Main module."""
from .containers import ApplicationContainer
if __name__ == '__main__':
application = ApplicationContainer()
application.config.from_ini('config.ini')
user_repository = application.user_package.user_repository()
photo_repository = application.photo_package.photo_repository()
user1 = user_repository.get(id=1)
user1_photos = photo_repository.get_photos(user1.id)
print(f'Retrieve user id={user1.id}, photos count={len(user1_photos)}')
user2 = user_repository.get(id=2)
user2_photos = photo_repository.get_photos(user2.id)
print(f'Retrieve user id={user2.id}, photos count={len(user2_photos)}')
aggregation_service = application.analytics_package.aggregation_service()
assert aggregation_service.user_repository is user_repository
assert aggregation_service.photo_repository is photo_repository
print('Aggregate analytics from user and photo packages')

View File

@ -0,0 +1 @@
"""Analytics package."""

View File

@ -0,0 +1,17 @@
"""Analytics containers module."""
from dependency_injector import containers, providers
from . import services
class AnalyticsContainer(containers.DeclarativeContainer):
user_repository = providers.Dependency()
photo_repository = providers.Dependency()
aggregation_service = providers.Singleton(
services.AggregationService,
user_repository=user_repository,
photo_repository=photo_repository,
)

View File

@ -0,0 +1,8 @@
"""Analytics services module."""
class AggregationService:
def __init__(self, user_repository, photo_repository):
self.user_repository = user_repository
self.photo_repository = photo_repository

View File

@ -0,0 +1,41 @@
"""Containers module."""
import sqlite3
import boto3
from dependency_injector import containers, providers
from .user.containers import UserContainer
from .photo.containers import PhotoContainer
from .analytics.containers import AnalyticsContainer
class ApplicationContainer(containers.DeclarativeContainer):
config = providers.Configuration()
sqlite = providers.Singleton(sqlite3.connect, config.database.dsn)
s3 = providers.Singleton(
boto3.client,
service_name='s3',
aws_access_key_id=config.aws.access_key_id,
aws_secret_access_key=config.aws.secret_access_key,
)
user_package = providers.Container(
UserContainer,
database=sqlite,
)
photo_package = providers.Container(
PhotoContainer,
database=sqlite,
file_storage=s3,
)
analytics_package = providers.Container(
AnalyticsContainer,
user_repository=user_package.user_repository,
photo_repository=photo_package.photo_repository,
)

View File

@ -0,0 +1 @@
"""Photo package."""

View File

@ -0,0 +1,20 @@
"""Photo containers module."""
from dependency_injector import containers, providers
from . import entities, repositories
class PhotoContainer(containers.DeclarativeContainer):
database = providers.Dependency()
file_storage = providers.Dependency()
photo = providers.Factory(entities.Photo)
photo_repository = providers.Singleton(
repositories.PhotoRepository,
entity_factory=photo.provider,
fs=file_storage,
db=database,
)

View File

@ -0,0 +1,5 @@
"""Photo entities module."""
class Photo:
...

View File

@ -0,0 +1,12 @@
"""Photo repositories module."""
class PhotoRepository:
def __init__(self, entity_factory, fs, db):
self.entity_factory = entity_factory
self.fs = fs
self.db = db
def get_photos(self, user_id):
return [self.entity_factory() for _ in range(user_id*5)]

View File

@ -0,0 +1 @@
"""User package."""

View File

@ -0,0 +1,18 @@
"""User containers module."""
from dependency_injector import containers, providers
from . import entities, repositories
class UserContainer(containers.DeclarativeContainer):
database = providers.Dependency()
user = providers.Factory(entities.User)
user_repository = providers.Singleton(
repositories.UserRepository,
entity_factory=user.provider,
db=database,
)

View File

@ -0,0 +1,7 @@
"""User entities module."""
class User:
def __init__(self, id):
self.id = id

View File

@ -0,0 +1,11 @@
"""User repositories module."""
class UserRepository:
def __init__(self, entity_factory, db):
self.entity_factory = entity_factory
self.db = db
def get(self, id):
return self.entity_factory(id=id)

View File

@ -0,0 +1,2 @@
dependency-injector
boto3

View File

@ -59,7 +59,7 @@ class Delegate(Provider):
class Dependency(Provider, Generic[T]): class Dependency(Provider, Generic[T]):
def __init__(self, instance_of: Type[T]) -> None: ... def __init__(self, instance_of: Type[T] = object) -> None: ...
def __call__(self, *args: Injection, **kwargs: Injection) -> T: ... def __call__(self, *args: Injection, **kwargs: Injection) -> T: ...
@property @property
def provided(self) -> ProvidedInstance: ... def provided(self) -> ProvidedInstance: ...