Merge branch 'release/4.0.0' into master
85
README.rst
|
@ -67,14 +67,18 @@ Key features of the ``Dependency Injector``:
|
|||
See `Configuration provider <http://python-dependency-injector.ets-labs.org/providers/configuration.html>`_.
|
||||
- **Containers**. Provides declarative and dynamic containers.
|
||||
See `Containers <http://python-dependency-injector.ets-labs.org/containers/index.html>`_.
|
||||
- **Performance**. Fast. Written in ``Cython``.
|
||||
- **Wiring**. Injects dependencies into functions and methods. Helps integrating with
|
||||
other frameworks: Django, Flask, Aiohttp, etc.
|
||||
See `Wiring <http://python-dependency-injector.ets-labs.org/wiring.html>`_.
|
||||
- **Typing**. Provides typing stubs, ``mypy``-friendly.
|
||||
See `Typing and mypy <http://python-dependency-injector.ets-labs.org/providers/typing_mypy.html>`_.
|
||||
- **Performance**. Fast. Written in ``Cython``.
|
||||
- **Maturity**. Mature and production-ready. Well-tested, documented and supported.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.wiring import Provide
|
||||
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
@ -93,23 +97,38 @@ Key features of the ``Dependency Injector``:
|
|||
)
|
||||
|
||||
|
||||
def main(service: Service = Provide[Container.service]):
|
||||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
container = Container()
|
||||
container.config.api_key.from_env('API_KEY')
|
||||
container.config.timeout.from_env('TIMEOUT')
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
|
||||
service = container.service()
|
||||
main() # <-- dependency is injected automatically
|
||||
|
||||
With the ``Dependency Injector`` you keep **application structure in one place**.
|
||||
This place is called **the container**. You use the container to manage all the components of the
|
||||
application. All the component dependencies are defined explicitly. This provides the control on
|
||||
the application structure. It is **easy to understand and change** it.
|
||||
with container.api_client.override(mock.Mock()):
|
||||
main() # <-- overridden dependency is injected automatically
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/wiki/ets-labs/python-dependency-injector/img/di-map.svg
|
||||
When you call ``main()`` function the ``Service`` dependency is assembled and injected automatically.
|
||||
|
||||
When doing a testing you call the ``container.api_client.override()`` to replace the real API
|
||||
client with a mock. When you call ``main()`` the mock is injected.
|
||||
|
||||
You can override any provider with another provider.
|
||||
|
||||
It also helps you in configuring project for the different environments: replace an API client
|
||||
with a stub on the dev or stage.
|
||||
|
||||
With the ``Dependency Injector`` objects assembling is consolidated in the container.
|
||||
Dependency injections are defined explicitly.
|
||||
This makes easier to understand and change how application works.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/wiki/ets-labs/python-dependency-injector/img/di-readme.svg
|
||||
:target: https://github.com/ets-labs/python-dependency-injector
|
||||
|
||||
*The container is like a map of your application. You always know what depends on what.*
|
||||
|
||||
Visit the docs to know more about the
|
||||
`Dependency injection and inversion of control in Python <http://python-dependency-injector.ets-labs.org/introduction/di_in_python.html>`_.
|
||||
|
||||
|
@ -133,6 +152,10 @@ Choose one of the following:
|
|||
- `Application example (single container) <http://python-dependency-injector.ets-labs.org/examples/application-single-container.html>`_
|
||||
- `Application example (multiple containers) <http://python-dependency-injector.ets-labs.org/examples/application-multiple-containers.html>`_
|
||||
- `Decoupled packages example (multiple containers) <http://python-dependency-injector.ets-labs.org/examples/decoupled-packages.html>`_
|
||||
- `Django example <http://python-dependency-injector.ets-labs.org/examples/django.html>`_
|
||||
- `Flask example <http://python-dependency-injector.ets-labs.org/examples/flask.html>`_
|
||||
- `Aiohttp example <http://python-dependency-injector.ets-labs.org/examples/aiohttp.html>`_
|
||||
- `Sanic example <http://python-dependency-injector.ets-labs.org/examples/sanic.html>`_
|
||||
|
||||
Tutorials
|
||||
---------
|
||||
|
@ -147,22 +170,16 @@ Choose one of the following:
|
|||
Concept
|
||||
-------
|
||||
|
||||
``Dependency Injector`` stands on two principles:
|
||||
The framework stands on the `PEP20 (The Zen of Python) <https://www.python.org/dev/peps/pep-0020/>`_ principle:
|
||||
|
||||
- Explicit is better than implicit (PEP20).
|
||||
- Do no magic to your code.
|
||||
.. code-block:: plain
|
||||
|
||||
How is it different from the other frameworks?
|
||||
Explicit is better than implicit
|
||||
|
||||
- **No autowiring.** The framework does NOT do any autowiring / autoresolving of the dependencies. You need to specify everything explicitly. Because *"Explicit is better than implicit" (PEP20)*.
|
||||
- **Does not pollute your code.** Your application does NOT know and does NOT depend on the framework. No ``@inject`` decorators, annotations, patching or any other magic tricks.
|
||||
You need to specify how to assemble and where to inject the dependencies explicitly.
|
||||
|
||||
``Dependency Injector`` makes a simple contract with you:
|
||||
|
||||
- You tell the framework how to assemble your objects
|
||||
- The framework does it for you
|
||||
|
||||
The power of the ``Dependency Injector`` is in its simplicity and straightforwardness. It is a simple tool for the powerful concept.
|
||||
The power of the framework is in a simplicity.
|
||||
``Dependency Injector`` is a simple tool for the powerful concept.
|
||||
|
||||
Frequently asked questions
|
||||
--------------------------
|
||||
|
@ -171,35 +188,17 @@ What is the dependency injection?
|
|||
- dependency injection is a principle that decreases coupling and increases cohesion
|
||||
|
||||
Why should I do the dependency injection?
|
||||
- your code becomes more flexible, testable and clear
|
||||
- you have no problems when you need to understand how it works or change it 😎
|
||||
- your code becomes more flexible, testable and clear 😎
|
||||
|
||||
How do I start doing the dependency injection?
|
||||
- you start writing the code following the dependency injection principle
|
||||
- you register all of your application components and their dependencies in the container
|
||||
- when you need a component, you get it from the container
|
||||
|
||||
Why do I need a framework for this?
|
||||
- you need the framework for this to not create it by your own
|
||||
- this framework gives you the container and the providers
|
||||
- the container is like a dictionary with the batteries 🔋
|
||||
- the providers manage the lifetime of your components, you will need factories, singletons, smart config object etc
|
||||
- when you need a component, you specify where to inject it or get it from the container
|
||||
|
||||
What price do I pay and what do I get?
|
||||
- you need to explicitly specify the dependencies in the container
|
||||
- you need to explicitly specify the dependencies
|
||||
- it will be extra work in the beginning
|
||||
- it will payoff when project grows or in two weeks 😊 (when you forget what project was about)
|
||||
|
||||
What features does the framework have?
|
||||
- building objects graph
|
||||
- smart configuration object
|
||||
- providers: factory, singleton, thread locals registers, etc
|
||||
- positional and keyword context injections
|
||||
- overriding of the objects in any part of the graph
|
||||
|
||||
What features the framework does NOT have?
|
||||
- autowiring / autoresolving of the dependencies
|
||||
- the annotations and ``@inject``-like decorators
|
||||
- it will payoff as the project grows
|
||||
|
||||
Have a question?
|
||||
- Open a `Github Issue <https://github.com/ets-labs/python-dependency-injector/issues>`_
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
dependency_injector.ext.aiohttp
|
||||
===============================
|
||||
|
||||
.. automodule:: dependency_injector.ext.aiohttp
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
.. disqus::
|
|
@ -1,9 +0,0 @@
|
|||
dependency_injector.ext.flask
|
||||
=============================
|
||||
|
||||
.. automodule:: dependency_injector.ext.flask
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
.. disqus::
|
|
@ -4,9 +4,8 @@ API Documentation
|
|||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
top_level
|
||||
providers
|
||||
containers
|
||||
errors
|
||||
aiohttpext
|
||||
flaskext
|
||||
top-level
|
||||
providers
|
||||
containers
|
||||
wiring
|
||||
errors
|
||||
|
|
7
docs/api/wiring.rst
Normal file
|
@ -0,0 +1,7 @@
|
|||
dependency_injector.wiring
|
||||
=============================
|
||||
|
||||
.. automodule:: dependency_injector.wiring
|
||||
:members:
|
||||
|
||||
.. disqus::
|
81
docs/examples/aiohttp.rst
Normal file
|
@ -0,0 +1,81 @@
|
|||
.. _aiohttp-example:
|
||||
|
||||
Aiohttp example
|
||||
===============
|
||||
|
||||
.. meta::
|
||||
:keywords: Python,Dependency Injection,Aiohttp,Example
|
||||
:description: This example demonstrates a usage of the Aiohttp and Dependency Injector.
|
||||
|
||||
|
||||
This example shows how to use ``Dependency Injector`` with `Aiohttp <https://docs.aiohttp.org/>`_.
|
||||
|
||||
The example application is a REST API that searches for funny GIFs on the `Giphy <https://giphy.com/>`_.
|
||||
|
||||
The source code is available on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/aiohttp>`_.
|
||||
|
||||
:ref:`aiohttp-tutorial` demonstrates how to build this application step-by-step.
|
||||
|
||||
Application structure
|
||||
---------------------
|
||||
|
||||
Application has next structure:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./
|
||||
├── giphynavigator/
|
||||
│ ├── __init__.py
|
||||
│ ├── application.py
|
||||
│ ├── containers.py
|
||||
│ ├── giphy.py
|
||||
│ ├── handlers.py
|
||||
│ ├── services.py
|
||||
│ └── tests.py
|
||||
├── config.yml
|
||||
└── requirements.txt
|
||||
|
||||
Container
|
||||
---------
|
||||
|
||||
Declarative container is defined in ``giphynavigator/containers.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/aiohttp/giphynavigator/containers.py
|
||||
:language: python
|
||||
|
||||
Handlers
|
||||
--------
|
||||
|
||||
Handler has dependencies on search service and some config options. The dependencies are injected
|
||||
using :ref:`wiring` feature.
|
||||
|
||||
Listing of ``giphynavigator/handlers.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/aiohttp/giphynavigator/handlers.py
|
||||
:language: python
|
||||
|
||||
Application factory
|
||||
-------------------
|
||||
Application factory creates container, wires it with the ``handlers`` module, creates
|
||||
``Aiohttp`` app and setup routes.
|
||||
|
||||
Listing of ``giphynavigator/application.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/aiohttp/giphynavigator/application.py
|
||||
:language: python
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
Tests use :ref:`provider-overriding` feature to replace giphy client with a mock ``giphynavigator/tests.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/aiohttp/giphynavigator/tests.py
|
||||
:language: python
|
||||
:emphasize-lines: 32,59,73
|
||||
|
||||
Sources
|
||||
-------
|
||||
|
||||
Explore the sources on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/aiohttp>`_.
|
||||
|
||||
.. disqus::
|
97
docs/examples/django.rst
Normal file
|
@ -0,0 +1,97 @@
|
|||
.. _django-example:
|
||||
|
||||
Django example
|
||||
==============
|
||||
|
||||
.. meta::
|
||||
:keywords: Python,Dependency Injection,Django,Example
|
||||
:description: This example demonstrates a usage of the Django and Dependency Injector.
|
||||
|
||||
|
||||
This example shows how to use ``Dependency Injector`` with `Django <https://www.djangoproject.com/>`_.
|
||||
|
||||
The example application helps to search for repositories on the Github.
|
||||
|
||||
.. image:: images/django.png
|
||||
:width: 100%
|
||||
:align: center
|
||||
|
||||
The source code is available on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/django>`_.
|
||||
|
||||
Application structure
|
||||
---------------------
|
||||
|
||||
Application has standard Django project structure. It consists of ``githubnavigator`` project package and
|
||||
``web`` application package:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./
|
||||
├── githubnavigator/
|
||||
│ ├── __init__.py
|
||||
│ ├── asgi.py
|
||||
│ ├── containers.py
|
||||
│ ├── services.py
|
||||
│ ├── settings.py
|
||||
│ ├── urls.py
|
||||
│ └── wsgi.py
|
||||
├── web/
|
||||
│ ├── templates/
|
||||
│ │ ├── base.html
|
||||
│ │ └── index.html
|
||||
│ ├── __init__.py
|
||||
│ ├── apps.py
|
||||
│ ├── tests.py
|
||||
│ ├── urls.py
|
||||
│ └── views.py
|
||||
├── manage.py
|
||||
└── requirements.txt
|
||||
|
||||
Container
|
||||
---------
|
||||
|
||||
Declarative container is defined in ``githubnavigator/containers.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/django/githubnavigator/containers.py
|
||||
:language: python
|
||||
|
||||
Container instance is created in ``githubnavigator/__init__.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/django/githubnavigator/__init__.py
|
||||
:language: python
|
||||
|
||||
Views
|
||||
-----
|
||||
|
||||
View has dependencies on search service and some config options. The dependencies are injected
|
||||
using :ref:`wiring` feature.
|
||||
|
||||
Listing of ``web/views.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/django/web/views.py
|
||||
:language: python
|
||||
|
||||
App config
|
||||
----------
|
||||
|
||||
Container is wired to the ``views`` module in the app config ``web/apps.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/django/web/apps.py
|
||||
:language: python
|
||||
:emphasize-lines: 13
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
Tests use :ref:`provider-overriding` feature to replace github client with a mock ``web/tests.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/django/web/tests.py
|
||||
:language: python
|
||||
:emphasize-lines: 39,60
|
||||
|
||||
Sources
|
||||
-------
|
||||
|
||||
Explore the sources on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/django>`_.
|
||||
|
||||
.. disqus::
|
87
docs/examples/flask.rst
Normal file
|
@ -0,0 +1,87 @@
|
|||
.. _flask-example:
|
||||
|
||||
Flask example
|
||||
=============
|
||||
|
||||
.. meta::
|
||||
:keywords: Python,Dependency Injection,Flask,Example
|
||||
:description: This example demonstrates a usage of the Flask and Dependency Injector.
|
||||
|
||||
|
||||
This example shows how to use ``Dependency Injector`` with `Flask <https://flask.palletsprojects.com/en/1.1.x/>`_.
|
||||
|
||||
The example application helps to search for repositories on the Github.
|
||||
|
||||
.. image:: images/flask.png
|
||||
:width: 100%
|
||||
:align: center
|
||||
|
||||
The source code is available on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/flask>`_.
|
||||
|
||||
:ref:`flask-tutorial` demonstrates how to build this application step-by-step.
|
||||
|
||||
Application structure
|
||||
---------------------
|
||||
|
||||
Application has next structure:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./
|
||||
├── githubnavigator/
|
||||
│ ├── templates
|
||||
│ │ ├── base.html
|
||||
│ │ └── index.py
|
||||
│ ├── __init__.py
|
||||
│ ├── application.py
|
||||
│ ├── containers.py
|
||||
│ ├── services.py
|
||||
│ ├── tests.py
|
||||
│ └── views.py
|
||||
├── config.yml
|
||||
└── requirements.txt
|
||||
|
||||
Container
|
||||
---------
|
||||
|
||||
Declarative container is defined in ``githubnavigator/containers.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/flask/githubnavigator/containers.py
|
||||
:language: python
|
||||
|
||||
Views
|
||||
-----
|
||||
|
||||
View has dependencies on search service and some config options. The dependencies are injected
|
||||
using :ref:`wiring` feature.
|
||||
|
||||
Listing of ``githubnavigator/views.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/flask/githubnavigator/views.py
|
||||
:language: python
|
||||
|
||||
Application factory
|
||||
-------------------
|
||||
Application factory creates container, wires it with the ``views`` module, creates
|
||||
``Flask`` app and setup routes.
|
||||
|
||||
Listing of ``githubnavigator/application.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/flask/githubnavigator/application.py
|
||||
:language: python
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
Tests use :ref:`provider-overriding` feature to replace github client with a mock ``githubnavigator/tests.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/flask/githubnavigator/tests.py
|
||||
:language: python
|
||||
:emphasize-lines: 44,67
|
||||
|
||||
Sources
|
||||
-------
|
||||
|
||||
Explore the sources on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/flask>`_.
|
||||
|
||||
.. disqus::
|
BIN
docs/examples/images/django.png
Normal file
After Width: | Height: | Size: 382 KiB |
Before Width: | Height: | Size: 647 KiB After Width: | Height: | Size: 647 KiB |
|
@ -13,5 +13,9 @@ Explore the examples to see the ``Dependency Injector`` in action.
|
|||
application-single-container
|
||||
application-multiple-containers
|
||||
decoupled-packages
|
||||
django
|
||||
flask
|
||||
aiohttp
|
||||
sanic
|
||||
|
||||
.. disqus::
|
||||
|
|
80
docs/examples/sanic.rst
Normal file
|
@ -0,0 +1,80 @@
|
|||
.. _sanic-example:
|
||||
|
||||
Sanic example
|
||||
==============
|
||||
|
||||
.. meta::
|
||||
:keywords: Python,Dependency Injection,Sanic,Example
|
||||
:description: This example demonstrates a usage of the Sanic and Dependency Injector.
|
||||
|
||||
|
||||
This example shows how to use ``Dependency Injector`` with `Sanic <https://sanic.readthedocs.io/en/latest/>`_.
|
||||
|
||||
The example application is a REST API that searches for funny GIFs on the `Giphy <https://giphy.com/>`_.
|
||||
|
||||
The source code is available on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/sanic>`_.
|
||||
|
||||
Application structure
|
||||
---------------------
|
||||
|
||||
Application has next structure:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./
|
||||
├── giphynavigator/
|
||||
│ ├── __init__.py
|
||||
│ ├── __main__.py
|
||||
│ ├── application.py
|
||||
│ ├── containers.py
|
||||
│ ├── giphy.py
|
||||
│ ├── handlers.py
|
||||
│ ├── services.py
|
||||
│ └── tests.py
|
||||
├── config.yml
|
||||
└── requirements.txt
|
||||
|
||||
Container
|
||||
---------
|
||||
|
||||
Declarative container is defined in ``giphynavigator/containers.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/sanic/giphynavigator/containers.py
|
||||
:language: python
|
||||
|
||||
Handlers
|
||||
--------
|
||||
|
||||
Handler has dependencies on search service and some config options. The dependencies are injected
|
||||
using :ref:`wiring` feature.
|
||||
|
||||
Listing of ``giphynavigator/handlers.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/sanic/giphynavigator/handlers.py
|
||||
:language: python
|
||||
|
||||
Application factory
|
||||
-------------------
|
||||
Application factory creates container, wires it with the ``handlers`` module, creates
|
||||
``Sanic`` app and setup routes.
|
||||
|
||||
Listing of ``giphynavigator/application.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/sanic/giphynavigator/application.py
|
||||
:language: python
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
Tests use :ref:`provider-overriding` feature to replace giphy client with a mock ``giphynavigator/tests.py``:
|
||||
|
||||
.. literalinclude:: ../../examples/miniapps/sanic/giphynavigator/tests.py
|
||||
:language: python
|
||||
:emphasize-lines: 27,54,68
|
||||
|
||||
Sources
|
||||
-------
|
||||
|
||||
Explore the sources on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/sanic>`_.
|
||||
|
||||
.. disqus::
|
Before Width: | Height: | Size: 13 KiB |
|
@ -77,13 +77,16 @@ Key features of the ``Dependency Injector``:
|
|||
- **Configuration**. Read configuration from ``yaml`` & ``ini`` files, environment variables
|
||||
and dictionaries. See :ref:`configuration-provider`.
|
||||
- **Containers**. Provides declarative and dynamic containers. See :ref:`containers`.
|
||||
- **Performance**. Fast. Written in ``Cython``.
|
||||
- **Wiring**. Injects dependencies into functions and methods. Helps integrating with
|
||||
other frameworks: Django, Flask, Aiohttp, etc. See :ref:`wiring`.
|
||||
- **Typing**. Provides typing stubs, ``mypy``-friendly. See :ref:`provider-typing`.
|
||||
- **Performance**. Fast. Written in ``Cython``.
|
||||
- **Maturity**. Mature and production-ready. Well-tested, documented and supported.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.wiring import Provide
|
||||
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
@ -102,23 +105,28 @@ Key features of the ``Dependency Injector``:
|
|||
)
|
||||
|
||||
|
||||
def main(service: Service = Provide[Container.service]):
|
||||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
container = Container()
|
||||
container.config.api_key.from_env('API_KEY')
|
||||
container.config.timeout.from_env('TIMEOUT')
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
|
||||
service = container.service()
|
||||
main() # <-- dependency is injected automatically
|
||||
|
||||
With the ``Dependency Injector`` you keep **application structure in one place**.
|
||||
This place is called **the container**. You use the container to manage all the components of the
|
||||
application. All the component dependencies are defined explicitly. This provides the control on
|
||||
the application structure. It is **easy to understand and change** it.
|
||||
with container.api_client.override(mock.Mock()):
|
||||
main() # <-- overridden dependency is injected automatically
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/wiki/ets-labs/python-dependency-injector/img/di-map.svg
|
||||
With the ``Dependency Injector`` objects assembling is consolidated in the container.
|
||||
Dependency injections are defined explicitly.
|
||||
This makes easier to understand and change how application works.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/wiki/ets-labs/python-dependency-injector/img/di-readme.svg
|
||||
:target: https://github.com/ets-labs/python-dependency-injector
|
||||
|
||||
*The container is like a map of your application. You always know what depends on what.*
|
||||
|
||||
Explore the documentation to know more about the ``Dependency Injector``.
|
||||
|
||||
.. _contents:
|
||||
|
@ -126,15 +134,16 @@ Explore the documentation to know more about the ``Dependency Injector``.
|
|||
Contents
|
||||
--------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
introduction/index
|
||||
examples/index
|
||||
tutorials/index
|
||||
providers/index
|
||||
containers/index
|
||||
examples-other/index
|
||||
api/index
|
||||
main/feedback
|
||||
main/changelog
|
||||
introduction/index
|
||||
examples/index
|
||||
tutorials/index
|
||||
providers/index
|
||||
containers/index
|
||||
wiring
|
||||
examples-other/index
|
||||
api/index
|
||||
main/feedback
|
||||
main/changelog
|
||||
|
|
|
@ -67,19 +67,23 @@ Before:
|
|||
class ApiClient:
|
||||
|
||||
def __init__(self):
|
||||
self.api_key = os.getenv('API_KEY') # <-- the dependency
|
||||
self.timeout = os.getenv('TIMEOUT') # <-- the dependency
|
||||
self.api_key = os.getenv('API_KEY') # <-- dependency
|
||||
self.timeout = os.getenv('TIMEOUT') # <-- dependency
|
||||
|
||||
|
||||
class Service:
|
||||
|
||||
def __init__(self):
|
||||
self.api_client = ApiClient() # <-- the dependency
|
||||
self.api_client = ApiClient() # <-- dependency
|
||||
|
||||
|
||||
def main() -> None:
|
||||
service = Service() # <-- dependency
|
||||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
service = Service()
|
||||
|
||||
main()
|
||||
|
||||
After:
|
||||
|
||||
|
@ -91,18 +95,29 @@ After:
|
|||
class ApiClient:
|
||||
|
||||
def __init__(self, api_key: str, timeout: int):
|
||||
self.api_key = api_key # <-- the dependency is injected
|
||||
self.timeout = timeout # <-- the dependency is injected
|
||||
self.api_key = api_key # <-- dependency is injected
|
||||
self.timeout = timeout # <-- dependency is injected
|
||||
|
||||
|
||||
class Service:
|
||||
|
||||
def __init__(self, api_client: ApiClient):
|
||||
self.api_client = api_client # <-- the dependency is injected
|
||||
self.api_client = api_client # <-- dependency is injected
|
||||
|
||||
|
||||
def main(service: Service): # <-- dependency is injected
|
||||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT')))
|
||||
main(
|
||||
service=Service(
|
||||
api_client=ApiClient(
|
||||
api_key=os.getenv('API_KEY'),
|
||||
timeout=os.getenv('TIMEOUT'),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
``ApiClient`` is decoupled from knowing where the options come from. You can read a key and a
|
||||
timeout from a configuration file or even get them from a database.
|
||||
|
@ -110,11 +125,22 @@ timeout from a configuration file or even get them from a database.
|
|||
``Service`` is decoupled from the ``ApiClient``. It does not create it anymore. You can provide a
|
||||
stub or other compatible object.
|
||||
|
||||
Function ``main()`` is decoupled from ``Service``. It receives it as an argument.
|
||||
|
||||
Flexibility comes with a price.
|
||||
|
||||
Now you need to assemble the objects like this::
|
||||
Now you need to assemble and inject the objects like this:
|
||||
|
||||
service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT')))
|
||||
.. code-block:: python
|
||||
|
||||
main(
|
||||
service=Service(
|
||||
api_client=ApiClient(
|
||||
api_key=os.getenv('API_KEY'),
|
||||
timeout=os.getenv('TIMEOUT'),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
The assembly code might get duplicated and it'll become harder to change the application structure.
|
||||
|
||||
|
@ -123,18 +149,20 @@ Here comes the ``Dependency Injector``.
|
|||
What does the Dependency Injector do?
|
||||
-------------------------------------
|
||||
|
||||
With the dependency injection pattern objects loose the responsibility of assembling the
|
||||
dependencies. The ``Dependency Injector`` absorbs that responsibility.
|
||||
With the dependency injection pattern objects loose the responsibility of assembling
|
||||
the dependencies. The ``Dependency Injector`` absorbs that responsibilities.
|
||||
|
||||
``Dependency Injector`` helps to assemble the objects.
|
||||
``Dependency Injector`` helps to assemble and inject the dependencies.
|
||||
|
||||
It provides a container and providers that help you with the objects assembly. When you
|
||||
need an object you get it from the container. The rest of the assembly work is done by the
|
||||
framework:
|
||||
It provides a container and providers that help you with the objects assembly.
|
||||
When you need an object you place a ``Provide`` marker as a default value of a
|
||||
function argument. When you call this function framework assembles and injects
|
||||
the dependency.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.wiring import Provide
|
||||
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
@ -153,36 +181,34 @@ framework:
|
|||
)
|
||||
|
||||
|
||||
def main(service: Service = Provide[Container.service]):
|
||||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
container = Container()
|
||||
container.config.api_key.from_env('API_KEY')
|
||||
container.config.timeout.from_env('TIMEOUT')
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
|
||||
service = container.service()
|
||||
main() # <-- dependency is injected automatically
|
||||
|
||||
Retrieving of the ``Service`` instance now is done like this::
|
||||
with container.api_client.override(mock.Mock()):
|
||||
main() # <-- overridden dependency is injected automatically
|
||||
|
||||
service = container.service()
|
||||
|
||||
Objects assembling is consolidated in the container. When you need to make a change you do it in
|
||||
one place.
|
||||
When you call ``main()`` function the ``Service`` dependency is assembled and injected automatically.
|
||||
|
||||
When doing a testing you call the ``container.api_client.override()`` to replace the real API
|
||||
client with a mock:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from unittest import mock
|
||||
|
||||
|
||||
with container.api_client.override(mock.Mock()):
|
||||
service = container.service()
|
||||
client with a mock. When you call ``main()`` the mock is injected.
|
||||
|
||||
You can override any provider with another provider.
|
||||
|
||||
It also helps you in configuring project for the different environments: replace an API client
|
||||
with a stub on the dev or stage.
|
||||
|
||||
Objects assembling is consolidated in the container. Dependency injections are defined explicitly.
|
||||
This makes easier to understand and change how application works.
|
||||
|
||||
Testing, Monkey-patching and dependency injection
|
||||
-------------------------------------------------
|
||||
|
||||
|
@ -254,6 +280,10 @@ Choose one of the following as a next step:
|
|||
- :ref:`application-single-container`
|
||||
- :ref:`application-multiple-containers`
|
||||
- :ref:`decoupled-packages`
|
||||
- :ref:`django-example`
|
||||
- :ref:`flask-example`
|
||||
- :ref:`aiohttp-example`
|
||||
- :ref:`sanic-example`
|
||||
- Pass the tutorials:
|
||||
- :ref:`flask-tutorial`
|
||||
- :ref:`aiohttp-tutorial`
|
||||
|
@ -261,6 +291,7 @@ Choose one of the following as a next step:
|
|||
- :ref:`cli-tutorial`
|
||||
- Know more about the ``Dependency Injector`` :ref:`key-features`
|
||||
- Know more about the :ref:`providers`
|
||||
- Know more about the :ref:`wiring`
|
||||
- Go to the :ref:`contents`
|
||||
|
||||
Useful links
|
||||
|
|
|
@ -1,41 +1,42 @@
|
|||
Installation
|
||||
============
|
||||
|
||||
*Dependency Injector* framework is distributed by PyPi_.
|
||||
|
||||
Latest stable version (and all previous versions) of *Dependency Injector*
|
||||
framework can be installed from PyPi_:
|
||||
``Dependency Injector`` is available on `PyPI <https://pypi.org/project/dependency-injector/>`_.
|
||||
To install latest version you can use ``pip``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install dependency-injector
|
||||
|
||||
.. note::
|
||||
Some components of *Dependency Injector* are implemented as C extension types.
|
||||
*Dependency Injector* is distributed as an archive with a source code, so
|
||||
C compiler and Python header files are required for the installation.
|
||||
Some modules of the ``Dependency Injector`` are implemented as C extensions.
|
||||
``Dependency Injector`` is distributed as a pre-compiled wheels. Wheels are
|
||||
available for all supported Python versions on Linux, Windows and MacOS.
|
||||
Linux distribution uses `manylinux <https://github.com/pypa/manylinux>`_.
|
||||
|
||||
Sources can be cloned from GitHub_:
|
||||
If there is no appropriate wheel for your environment (Python version and OS)
|
||||
installer will compile the package from sources on your machine. You'll need
|
||||
a C compiler and Python header files.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git clone https://github.com/ets-labs/python-dependency-injector.git
|
||||
|
||||
Also all *Dependency Injector* releases can be downloaded from
|
||||
`GitHub releases page`_.
|
||||
|
||||
Verification of currently installed version could be done using
|
||||
:py:obj:`dependency_injector.VERSION` constant:
|
||||
To verify the installed version:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
>>> import dependency_injector
|
||||
>>> dependency_injector.__version__
|
||||
'3.43.0'
|
||||
'4.0.0'
|
||||
|
||||
.. _PyPi: https://pypi.org/project/dependency-injector/
|
||||
.. _GitHub: https://github.com/ets-labs/python-dependency-injector
|
||||
.. _GitHub releases page: https://github.com/ets-labs/python-dependency-injector/releases
|
||||
.. note::
|
||||
When add ``Dependency Injector`` to the ``requirements.txt`` don't forget to pin version
|
||||
to the current major:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
dependency-injector>=4.0,<5.0
|
||||
|
||||
*Next major version can be incompatible.*
|
||||
|
||||
All releases are available on `PyPI release history page <https://pypi.org/project/dependency-injector/#history>`_.
|
||||
Each release has appropriate tag. The tags are available on
|
||||
`GitHub releases page <https://github.com/ets-labs/python-dependency-injector/releases>`_.
|
||||
|
||||
.. disqus::
|
||||
|
|
|
@ -19,20 +19,21 @@ Key features of the ``Dependency Injector``:
|
|||
- **Configuration**. Read configuration from ``yaml`` & ``ini`` files, environment variables
|
||||
and dictionaries. See :ref:`configuration-provider`.
|
||||
- **Containers**. Provides declarative and dynamic containers. See :ref:`containers`.
|
||||
- **Performance**. Fast. Written in ``Cython``.
|
||||
- **Wiring**. Injects dependencies into functions and methods. Helps integrating with
|
||||
other frameworks: Django, Flask, Aiohttp, etc. See :ref:`wiring`.
|
||||
- **Typing**. Provides typing stubs, ``mypy``-friendly. See :ref:`provider-typing`.
|
||||
- **Performance**. Fast. Written in ``Cython``.
|
||||
- **Maturity**. Mature and production-ready. Well-tested, documented and supported.
|
||||
|
||||
The framework stands on two principles:
|
||||
The framework stands on the `PEP20 (The Zen of Python) <https://www.python.org/dev/peps/pep-0020/>`_ principle:
|
||||
|
||||
- **Explicit is better than implicit (PEP20)**.
|
||||
- **Do not do any magic to your code**.
|
||||
.. code-block:: plain
|
||||
|
||||
How is that different from the other frameworks?
|
||||
Explicit is better than implicit
|
||||
|
||||
- **No autowiring.** The framework does NOT do any autowiring / autoresolving of the dependencies. You need to specify everything explicitly. Because *"Explicit is better than implicit" (PEP20)*.
|
||||
- **Does not pollute your code.** Your application does NOT know and does NOT depend on the framework. No ``@inject`` decorators, annotations, patching or any other magic tricks.
|
||||
You need to specify how to assemble and where to inject the dependencies explicitly.
|
||||
|
||||
The power of the framework is in a simplicity. ``Dependency Injector`` is a simple tool for the powerful concept.
|
||||
The power of the framework is in a simplicity.
|
||||
``Dependency Injector`` is a simple tool for the powerful concept.
|
||||
|
||||
.. disqus::
|
||||
|
|
|
@ -7,6 +7,44 @@ that were made in every particular version.
|
|||
From version 0.7.6 *Dependency Injector* framework strictly
|
||||
follows `Semantic versioning`_
|
||||
|
||||
4.0.0
|
||||
-----
|
||||
New features:
|
||||
|
||||
- Add ``wiring`` feature.
|
||||
|
||||
Deprecations:
|
||||
|
||||
- Deprecate ``ext.aiohttp`` module in favor of ``wiring`` feature.
|
||||
- Deprecate ``ext.flask`` module in favor of ``wiring`` feature.
|
||||
- Deprecate ``.delegate()`` provider method in favor of ``.provider`` attribute.
|
||||
|
||||
Removals:
|
||||
|
||||
- Remove deprecated ``types`` module.
|
||||
|
||||
Tutorials:
|
||||
|
||||
- Update ``flask`` tutorial.
|
||||
- Update ``aiohttp`` tutorial.
|
||||
- Update ``asyncio`` daemon tutorial.
|
||||
- Update CLI application tutorial.
|
||||
|
||||
Examples:
|
||||
|
||||
- Add ``django`` example.
|
||||
- Add ``sanic`` example.
|
||||
- Update ``aiohttp`` example.
|
||||
- Update ``flask`` example.
|
||||
- Update ``asyncio`` daemon example.
|
||||
- Update ``movie-lister`` example.
|
||||
- Update CLI application example.
|
||||
|
||||
Misc:
|
||||
|
||||
- Regenerate C sources using Cython 0.29.21.
|
||||
- Improve documentation and README (typos removal, rewording, etc).
|
||||
|
||||
3.44.0
|
||||
------
|
||||
- Add native support of the generics to the providers: ``some_provider = providers.Provider[SomeClass]``.
|
||||
|
|
|
@ -21,7 +21,7 @@ Start from the scratch or jump to the section:
|
|||
:backlinks: none
|
||||
|
||||
You can find complete project on the
|
||||
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/giphynav-aiohttp>`_.
|
||||
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/aiohttp>`_.
|
||||
|
||||
What are we going to build?
|
||||
---------------------------
|
||||
|
@ -88,18 +88,18 @@ Prepare the environment
|
|||
|
||||
Let's create the environment for the project.
|
||||
|
||||
First we need to create a project folder and the virtual environment:
|
||||
First we need to create a project folder:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
mkdir giphynav-aiohttp-tutorial
|
||||
cd giphynav-aiohttp-tutorial
|
||||
python3 -m venv venv
|
||||
|
||||
Now let's activate the virtual environment:
|
||||
Now let's create and activate virtual environment:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python3 -m venv venv
|
||||
. venv/bin/activate
|
||||
|
||||
Environment is ready and now we're going to create the layout of the project.
|
||||
|
@ -116,7 +116,7 @@ Initial project layout::
|
|||
│ ├── __init__.py
|
||||
│ ├── application.py
|
||||
│ ├── containers.py
|
||||
│ └── views.py
|
||||
│ └── handlers.py
|
||||
├── venv/
|
||||
└── requirements.txt
|
||||
|
||||
|
@ -164,14 +164,14 @@ The requirements are setup. Now we will build a minimal application.
|
|||
Minimal application
|
||||
-------------------
|
||||
|
||||
In this section we will build a minimal application. It will have an endpoint that we can call.
|
||||
The endpoint will answer in the right format and will have no data.
|
||||
In this section we will build a minimal application. It will have an endpoint that
|
||||
will answer our requests in json format. There will be no payload for now.
|
||||
|
||||
Edit ``views.py``:
|
||||
Edit ``handlers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"""Views module."""
|
||||
"""Handlers module."""
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
|
@ -190,34 +190,25 @@ Edit ``views.py``:
|
|||
},
|
||||
)
|
||||
|
||||
Now let's create the main part of our application - the container. Container will keep all of the
|
||||
application components and their dependencies. First two providers we need to add are
|
||||
the ``aiohttp`` application provider and the view provider.
|
||||
Now let's create a container. Container will keep all of the application components and their dependencies.
|
||||
|
||||
Put next into the ``containers.py``:
|
||||
Edit ``containers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"""Application containers module."""
|
||||
"""Containers module."""
|
||||
|
||||
from dependency_injector import containers
|
||||
from dependency_injector.ext import aiohttp
|
||||
from aiohttp import web
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
class Container(containers.DeclarativeContainer):
|
||||
...
|
||||
|
||||
app = aiohttp.Application(web.Application)
|
||||
Container is empty for now. We will add the providers in the following sections.
|
||||
|
||||
index_view = aiohttp.View(views.index)
|
||||
|
||||
At the last we need to create the ``aiohttp`` application factory. It is traditionally called
|
||||
``create_app()``. It will create the container. Then it will use the container to create
|
||||
the ``aiohttp`` application. Last step is to configure the routing - we will assign
|
||||
``index_view`` from the container to handle the requests to the root ``/`` of our REST API server.
|
||||
Finally we need to create ``aiohttp`` application factory. It will create and configure container
|
||||
and ``web.Application``. It is traditionally called ``create_app()``.
|
||||
We will assign ``index`` handler to handle user requests to the root ``/`` of our web application.
|
||||
|
||||
Put next into the ``application.py``:
|
||||
|
||||
|
@ -227,28 +218,20 @@ Put next into the ``application.py``:
|
|||
|
||||
from aiohttp import web
|
||||
|
||||
from .containers import ApplicationContainer
|
||||
from .containers import Container
|
||||
from . import handlers
|
||||
|
||||
|
||||
def create_app():
|
||||
"""Create and return aiohttp application."""
|
||||
container = ApplicationContainer()
|
||||
def create_app() -> web.Application:
|
||||
container = Container()
|
||||
|
||||
app: web.Application = container.app()
|
||||
app = web.Application()
|
||||
app.container = container
|
||||
|
||||
app.add_routes([
|
||||
web.get('/', container.index_view.as_view()),
|
||||
web.get('/', handlers.index),
|
||||
])
|
||||
|
||||
return app
|
||||
|
||||
.. note::
|
||||
|
||||
Container is the first object in the application.
|
||||
|
||||
The container is used to create all other objects.
|
||||
|
||||
Now we're ready to run our application
|
||||
|
||||
Do next in the terminal:
|
||||
|
@ -264,7 +247,7 @@ The output should be something like:
|
|||
[18:52:59] Starting aux server at http://localhost:8001 ◆
|
||||
[18:52:59] Starting dev server at http://localhost:8000 ●
|
||||
|
||||
Let's use ``httpie`` to check that it works:
|
||||
Let's check that it works. Open another terminal session and use ``httpie``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
|
@ -306,7 +289,7 @@ Create ``giphy.py`` module in the ``giphynavigator`` package:
|
|||
│ ├── application.py
|
||||
│ ├── containers.py
|
||||
│ ├── giphy.py
|
||||
│ └── views.py
|
||||
│ └── handlers.py
|
||||
├── venv/
|
||||
└── requirements.txt
|
||||
|
||||
|
@ -351,21 +334,16 @@ providers from the ``dependency_injector.providers`` module:
|
|||
Edit ``containers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 3,7,15,17-21
|
||||
:emphasize-lines: 3-5,10-16
|
||||
|
||||
"""Application containers module."""
|
||||
"""Containers module."""
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.ext import aiohttp
|
||||
from aiohttp import web
|
||||
|
||||
from . import giphy, views
|
||||
from . import giphy
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
|
||||
app = aiohttp.Application(web.Application)
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
|
@ -375,8 +353,6 @@ Edit ``containers.py``:
|
|||
timeout=config.giphy.request_timeout,
|
||||
)
|
||||
|
||||
index_view = aiohttp.View(views.index)
|
||||
|
||||
.. note::
|
||||
|
||||
We have used the configuration value before it was defined. That's the principle how the
|
||||
|
@ -399,7 +375,7 @@ Create an empty file ``config.yml`` in the root root of the project:
|
|||
│ ├── application.py
|
||||
│ ├── containers.py
|
||||
│ ├── giphy.py
|
||||
│ └── views.py
|
||||
│ └── handlers.py
|
||||
├── venv/
|
||||
├── config.yml
|
||||
└── requirements.txt
|
||||
|
@ -427,24 +403,23 @@ Edit ``application.py``:
|
|||
|
||||
from aiohttp import web
|
||||
|
||||
from .containers import ApplicationContainer
|
||||
from .containers import Container
|
||||
from . import handlers
|
||||
|
||||
|
||||
def create_app():
|
||||
"""Create and return aiohttp application."""
|
||||
container = ApplicationContainer()
|
||||
def create_app() -> web.Application:
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
container.config.giphy.api_key.from_env('GIPHY_API_KEY')
|
||||
|
||||
app: web.Application = container.app()
|
||||
app = web.Application()
|
||||
app.container = container
|
||||
|
||||
app.add_routes([
|
||||
web.get('/', container.index_view.as_view()),
|
||||
web.get('/', handlers.index),
|
||||
])
|
||||
|
||||
return app
|
||||
|
||||
|
||||
Now we need to create an API key and set it to the environment variable.
|
||||
|
||||
As for now, don’t worry, just take this one:
|
||||
|
@ -473,7 +448,7 @@ Now it's time to add the ``SearchService``. It will:
|
|||
Create ``services.py`` module in the ``giphynavigator`` package:
|
||||
|
||||
.. code-block:: bash
|
||||
:emphasize-lines: 7
|
||||
:emphasize-lines: 8
|
||||
|
||||
./
|
||||
├── giphynavigator/
|
||||
|
@ -481,9 +456,10 @@ Create ``services.py`` module in the ``giphynavigator`` package:
|
|||
│ ├── application.py
|
||||
│ ├── containers.py
|
||||
│ ├── giphy.py
|
||||
│ ├── services.py
|
||||
│ └── views.py
|
||||
│ ├── handlers.py
|
||||
│ └── services.py
|
||||
├── venv/
|
||||
├── config.yml
|
||||
└── requirements.txt
|
||||
|
||||
and put next into it:
|
||||
|
@ -509,27 +485,22 @@ and put next into it:
|
|||
|
||||
return [{'url': gif['url']} for gif in result['data']]
|
||||
|
||||
The ``SearchService`` has a dependency on the ``GiphyClient``. This dependency will be injected.
|
||||
Let's add ``SearchService`` to the container.
|
||||
The ``SearchService`` has a dependency on the ``GiphyClient``. This dependency will be
|
||||
injected when we add ``SearchService`` to the container.
|
||||
|
||||
Edit ``containers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 7,23-26
|
||||
:emphasize-lines: 5,18-21
|
||||
|
||||
"""Application containers module."""
|
||||
"""Containers module."""
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.ext import aiohttp
|
||||
from aiohttp import web
|
||||
|
||||
from . import giphy, services, views
|
||||
from . import giphy, services
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
|
||||
app = aiohttp.Application(web.Application)
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
|
@ -544,31 +515,31 @@ Edit ``containers.py``:
|
|||
giphy_client=giphy_client,
|
||||
)
|
||||
|
||||
index_view = aiohttp.View(views.index)
|
||||
|
||||
|
||||
The search service is ready. In the next section we're going to make it work.
|
||||
The search service is ready. In next section we're going to put it to work.
|
||||
|
||||
Make the search work
|
||||
--------------------
|
||||
|
||||
Now we are ready to make the search work. Let's use the ``SearchService`` in the ``index`` view.
|
||||
Now we are ready to put the search into work. Let's inject ``SearchService`` into
|
||||
the ``index`` handler. We will use :ref:`wiring` feature.
|
||||
|
||||
Edit ``views.py``:
|
||||
Edit ``handlers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 5,8-11,15
|
||||
:emphasize-lines: 4-7,10-13,17
|
||||
|
||||
"""Views module."""
|
||||
"""Handlers module."""
|
||||
|
||||
from aiohttp import web
|
||||
from dependency_injector.wiring import Provide
|
||||
|
||||
from .services import SearchService
|
||||
from .containers import Container
|
||||
|
||||
|
||||
async def index(
|
||||
request: web.Request,
|
||||
search_service: SearchService,
|
||||
search_service: SearchService = Provide[Container.search_service],
|
||||
) -> web.Response:
|
||||
query = request.query.get('query', 'Dependency Injector')
|
||||
limit = int(request.query.get('limit', 10))
|
||||
|
@ -583,44 +554,35 @@ Edit ``views.py``:
|
|||
},
|
||||
)
|
||||
|
||||
Now let's inject the ``SearchService`` dependency into the ``index`` view.
|
||||
To make the injection work we need to wire the container instance with the ``handlers`` module.
|
||||
This needs to be done once. After it's done we can use ``Provide`` markers to specify as many
|
||||
injections as needed for any handler.
|
||||
|
||||
Edit ``containers.py``:
|
||||
Edit ``application.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 28-31
|
||||
:emphasize-lines: 13
|
||||
|
||||
"""Application containers module."""
|
||||
"""Application module."""
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.ext import aiohttp
|
||||
from aiohttp import web
|
||||
|
||||
from . import giphy, services, views
|
||||
from .containers import Container
|
||||
from . import handlers
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
def create_app() -> web.Application:
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
container.config.giphy.api_key.from_env('GIPHY_API_KEY')
|
||||
container.wire(modules=[handlers])
|
||||
|
||||
app = aiohttp.Application(web.Application)
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
giphy_client = providers.Factory(
|
||||
giphy.GiphyClient,
|
||||
api_key=config.giphy.api_key,
|
||||
timeout=config.giphy.request_timeout,
|
||||
)
|
||||
|
||||
search_service = providers.Factory(
|
||||
services.SearchService,
|
||||
giphy_client=giphy_client,
|
||||
)
|
||||
|
||||
index_view = aiohttp.View(
|
||||
views.index,
|
||||
search_service=search_service,
|
||||
)
|
||||
app = web.Application()
|
||||
app.container = container
|
||||
app.add_routes([
|
||||
web.get('/', handlers.index),
|
||||
])
|
||||
return app
|
||||
|
||||
Make sure the app is running or use:
|
||||
|
||||
|
@ -639,30 +601,30 @@ You should see:
|
|||
.. code-block:: json
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Length: 850
|
||||
Content-Length: 492
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Date: Wed, 29 Jul 2020 22:22:55 GMT
|
||||
Date: Fri, 09 Oct 2020 01:35:48 GMT
|
||||
Server: Python/3.8 aiohttp/3.6.2
|
||||
|
||||
{
|
||||
"gifs": [
|
||||
{
|
||||
"url": "https://giphy.com/gifs/dollyparton-3xIVVMnZfG3KQ9v4Ye"
|
||||
},
|
||||
{
|
||||
"url": "https://giphy.com/gifs/tennistv-unbelievable-disbelief-cant-believe-UWWJnhHHbpGvZOapEh"
|
||||
},
|
||||
{
|
||||
"url": "https://giphy.com/gifs/discoverychannel-nugget-gold-rush-rick-ness-KGGPIlnC4hr4u2s3pY"
|
||||
},
|
||||
{
|
||||
"url": "https://giphy.com/gifs/primevideoin-ll1hyBS2IrUPLE0E71"
|
||||
"url": "https://giphy.com/gifs/soulpancake-wow-work-xUe4HVXTPi0wQ2OAJC"
|
||||
},
|
||||
{
|
||||
"url": "https://giphy.com/gifs/jackman-works-jackmanworks-l4pTgQoCrmXq8Txlu"
|
||||
},
|
||||
{
|
||||
"url": "https://giphy.com/gifs/cat-massage-at-work-l46CzMaOlJXAFuO3u"
|
||||
},
|
||||
{
|
||||
"url": "https://giphy.com/gifs/everwhatproductions-fun-christmas-3oxHQCI8tKXoeW4IBq"
|
||||
},
|
||||
"url": "https://giphy.com/gifs/readingrainbow-teamwork-levar-burton-reading-rainbow-3o7qE1EaTWLQGDSabK"
|
||||
}
|
||||
],
|
||||
"limit": 10,
|
||||
"limit": 5,
|
||||
"query": "wow,it works"
|
||||
}
|
||||
|
||||
|
@ -673,30 +635,32 @@ The search works!
|
|||
Make some refactoring
|
||||
---------------------
|
||||
|
||||
Our ``index`` view has two hardcoded config values:
|
||||
Our ``index`` handler has two hardcoded config values:
|
||||
|
||||
- Default search query
|
||||
- Default results limit
|
||||
|
||||
Let's make some refactoring. We will move these values to the config.
|
||||
|
||||
Edit ``views.py``:
|
||||
Edit ``handlers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 11-12,14-15
|
||||
:emphasize-lines: 13-14,16-17
|
||||
|
||||
"""Views module."""
|
||||
"""Handlers module."""
|
||||
|
||||
from aiohttp import web
|
||||
from dependency_injector.wiring import Provide
|
||||
|
||||
from .services import SearchService
|
||||
from .containers import Container
|
||||
|
||||
|
||||
async def index(
|
||||
request: web.Request,
|
||||
search_service: SearchService,
|
||||
default_query: str,
|
||||
default_limit: int,
|
||||
search_service: SearchService = Provide[Container.search_service],
|
||||
default_query: str = Provide[Container.config.default.query],
|
||||
default_limit: int = Provide[Container.config.default.limit.as_int()],
|
||||
) -> web.Response:
|
||||
query = request.query.get('query', default_query)
|
||||
limit = int(request.query.get('limit', default_limit))
|
||||
|
@ -711,48 +675,7 @@ Edit ``views.py``:
|
|||
},
|
||||
)
|
||||
|
||||
Now we need to inject these values. Let's update the container.
|
||||
|
||||
Edit ``containers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 31-32
|
||||
|
||||
"""Application containers module."""
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.ext import aiohttp
|
||||
from aiohttp import web
|
||||
|
||||
from . import giphy, services, views
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
|
||||
app = aiohttp.Application(web.Application)
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
giphy_client = providers.Factory(
|
||||
giphy.GiphyClient,
|
||||
api_key=config.giphy.api_key,
|
||||
timeout=config.giphy.request_timeout,
|
||||
)
|
||||
|
||||
search_service = providers.Factory(
|
||||
services.SearchService,
|
||||
giphy_client=giphy_client,
|
||||
)
|
||||
|
||||
index_view = aiohttp.View(
|
||||
views.index,
|
||||
search_service=search_service,
|
||||
default_query=config.search.default_query,
|
||||
default_limit=config.search.default_limit,
|
||||
)
|
||||
|
||||
Finally let's update the config.
|
||||
Let's update the config.
|
||||
|
||||
Edit ``config.yml``:
|
||||
|
||||
|
@ -761,26 +684,21 @@ Edit ``config.yml``:
|
|||
|
||||
giphy:
|
||||
request_timeout: 10
|
||||
search:
|
||||
default_query: "Dependency Injector"
|
||||
default_limit: 10
|
||||
default:
|
||||
query: "Dependency Injector"
|
||||
limit: 10
|
||||
|
||||
The refactoring is done. We've made it cleaner - hardcoded values are now moved to the config.
|
||||
|
||||
In the next section we will add some tests.
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
It would be nice to add some tests. Let's do it.
|
||||
|
||||
We will use `pytest <https://docs.pytest.org/en/stable/>`_ and
|
||||
`coverage <https://coverage.readthedocs.io/>`_.
|
||||
In this section we will add some tests.
|
||||
|
||||
Create ``tests.py`` module in the ``giphynavigator`` package:
|
||||
|
||||
.. code-block:: bash
|
||||
:emphasize-lines: 8
|
||||
:emphasize-lines: 9
|
||||
|
||||
./
|
||||
├── giphynavigator/
|
||||
|
@ -788,16 +706,17 @@ Create ``tests.py`` module in the ``giphynavigator`` package:
|
|||
│ ├── application.py
|
||||
│ ├── containers.py
|
||||
│ ├── giphy.py
|
||||
│ ├── handlers.py
|
||||
│ ├── services.py
|
||||
│ ├── tests.py
|
||||
│ └── views.py
|
||||
│ └── tests.py
|
||||
├── venv/
|
||||
├── config.yml
|
||||
└── requirements.txt
|
||||
|
||||
and put next into it:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 30,57,71
|
||||
:emphasize-lines: 32,59,73
|
||||
|
||||
"""Tests module."""
|
||||
|
||||
|
@ -811,7 +730,9 @@ and put next into it:
|
|||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
return create_app()
|
||||
app = create_app()
|
||||
yield app
|
||||
app.container.unwire()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -874,8 +795,8 @@ and put next into it:
|
|||
|
||||
assert response.status == 200
|
||||
data = await response.json()
|
||||
assert data['query'] == app.container.config.search.default_query()
|
||||
assert data['limit'] == app.container.config.search.default_limit()
|
||||
assert data['query'] == app.container.config.default.query()
|
||||
assert data['limit'] == app.container.config.default.limit()
|
||||
|
||||
Now let's run it and check the coverage:
|
||||
|
||||
|
@ -885,7 +806,7 @@ Now let's run it and check the coverage:
|
|||
|
||||
You should see:
|
||||
|
||||
.. code-block:: bash
|
||||
.. code-block::
|
||||
|
||||
platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
|
||||
plugins: cov-2.10.0, aiohttp-0.3.0, asyncio-0.14.0
|
||||
|
@ -897,15 +818,14 @@ You should see:
|
|||
Name Stmts Miss Cover
|
||||
---------------------------------------------------
|
||||
giphynavigator/__init__.py 0 0 100%
|
||||
giphynavigator/__main__.py 5 5 0%
|
||||
giphynavigator/application.py 10 0 100%
|
||||
giphynavigator/containers.py 10 0 100%
|
||||
giphynavigator/application.py 12 0 100%
|
||||
giphynavigator/containers.py 6 0 100%
|
||||
giphynavigator/giphy.py 14 9 36%
|
||||
giphynavigator/handlers.py 9 0 100%
|
||||
giphynavigator/services.py 9 1 89%
|
||||
giphynavigator/tests.py 35 0 100%
|
||||
giphynavigator/views.py 7 0 100%
|
||||
giphynavigator/tests.py 37 0 100%
|
||||
---------------------------------------------------
|
||||
TOTAL 90 15 83%
|
||||
TOTAL 87 10 89%
|
||||
|
||||
.. note::
|
||||
|
||||
|
@ -920,45 +840,19 @@ In this tutorial we've built an ``aiohttp`` REST API application following the d
|
|||
injection principle.
|
||||
We've used the ``Dependency Injector`` as a dependency injection framework.
|
||||
|
||||
The benefit you get with the ``Dependency Injector`` is the container. It starts to payoff
|
||||
when you need to understand or change your application structure. It's easy with the container,
|
||||
cause you have everything defined explicitly in one place:
|
||||
:ref:`containers` and :ref:`providers` helped to specify how to assemble search service and
|
||||
giphy client.
|
||||
|
||||
.. code-block:: python
|
||||
:ref:`configuration-provider` helped to deal with reading YAML file and environment variable.
|
||||
|
||||
"""Application containers module."""
|
||||
We used :ref:`wiring` feature to inject the dependencies into the ``index()`` handler.
|
||||
:ref:`provider-overriding` feature helped in testing.
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.ext import aiohttp
|
||||
from aiohttp import web
|
||||
We kept all the dependencies injected explicitly. This will help when you need to add or
|
||||
change something in future.
|
||||
|
||||
from . import giphy, services, views
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
|
||||
app = aiohttp.Application(web.Application)
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
giphy_client = providers.Factory(
|
||||
giphy.GiphyClient,
|
||||
api_key=config.giphy.api_key,
|
||||
timeout=config.giphy.request_timeout,
|
||||
)
|
||||
|
||||
search_service = providers.Factory(
|
||||
services.SearchService,
|
||||
giphy_client=giphy_client,
|
||||
)
|
||||
|
||||
index_view = aiohttp.View(
|
||||
views.index,
|
||||
search_service=search_service,
|
||||
default_query=config.search.default_query,
|
||||
default_limit=config.search.default_limit,
|
||||
)
|
||||
You can find complete project on the
|
||||
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/aiohttp>`_.
|
||||
|
||||
What's next?
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ Start from the scratch or jump to the section:
|
|||
:backlinks: none
|
||||
|
||||
You can find complete project on the
|
||||
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/monitoring-daemon-asyncio>`_.
|
||||
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/asyncio-daemon>`_.
|
||||
|
||||
What are we going to build?
|
||||
---------------------------
|
||||
|
@ -42,7 +42,7 @@ response it will log:
|
|||
- The amount of bytes in the response
|
||||
- The time took to complete the response
|
||||
|
||||
.. image:: asyncio_images/diagram.png
|
||||
.. image:: asyncio-images/diagram.png
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
|
@ -79,8 +79,8 @@ Create the project root folder and set it as a working directory:
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
mkdir monitoring-daemon-tutorial
|
||||
cd monitoring-daemon-tutorial
|
||||
mkdir asyncio-daemon-tutorial
|
||||
cd asyncio-daemon-tutorial
|
||||
|
||||
Now we need to create the initial project structure. Create the files and folders following next
|
||||
layout. All files should be empty for now. We will fill them later.
|
||||
|
@ -190,10 +190,10 @@ The output should look like:
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
Creating network "monitoring-daemon-tutorial_default" with the default driver
|
||||
Creating monitoring-daemon-tutorial_monitor_1 ... done
|
||||
Attaching to monitoring-daemon-tutorial_monitor_1
|
||||
monitoring-daemon-tutorial_monitor_1 exited with code 0
|
||||
Creating network "asyncio-daemon-tutorial_default" with the default driver
|
||||
Creating asyncio-daemon-tutorial_monitor_1 ... done
|
||||
Attaching to asyncio-daemon-tutorial_monitor_1
|
||||
asyncio-daemon-tutorial_monitor_1 exited with code 0
|
||||
|
||||
The environment is ready. The application does not do any work and just exits with a code ``0``.
|
||||
|
||||
|
@ -214,7 +214,7 @@ Put next lines into the ``containers.py`` file:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
"""Application containers module."""
|
||||
"""Containers module."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
@ -222,8 +222,7 @@ Put next lines into the ``containers.py`` file:
|
|||
from dependency_injector import containers, providers
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
|
@ -259,29 +258,27 @@ Put next lines into the ``__main__.py`` file:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
"""Main module."""
|
||||
"""Main module."""
|
||||
|
||||
from .containers import ApplicationContainer
|
||||
from .containers import Container
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the application."""
|
||||
container = ApplicationContainer()
|
||||
|
||||
container.config.from_yaml('config.yml')
|
||||
container.configure_logging()
|
||||
def main() -> None:
|
||||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
if __name__ == '__main__':
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
container.configure_logging()
|
||||
|
||||
main()
|
||||
|
||||
.. note::
|
||||
|
||||
Container is the first object in the application.
|
||||
|
||||
The container is used to create all other objects.
|
||||
|
||||
Logging and configuration parsing part is done. In the next section we will create the monitoring
|
||||
Logging and configuration parsing part is done. In next section we will create the monitoring
|
||||
checks dispatcher.
|
||||
|
||||
Dispatcher
|
||||
|
@ -293,7 +290,7 @@ The dispatcher will control a list of the monitoring tasks. It will execute each
|
|||
to the configured schedule. The ``Monitor`` class is the base class for all the monitors. You can
|
||||
create different monitors by subclassing it and implementing the ``check()`` method.
|
||||
|
||||
.. image:: asyncio_images/class_1.png
|
||||
.. image:: asyncio-images/classes-01.png
|
||||
|
||||
Let's create dispatcher and the monitor base classes.
|
||||
|
||||
|
@ -336,7 +333,7 @@ and next into the ``dispatcher.py``:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
""""Dispatcher module."""
|
||||
"""Dispatcher module."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
@ -382,6 +379,7 @@ and next into the ``dispatcher.py``:
|
|||
self._logger.info('Shutting down')
|
||||
for task, monitor in zip(self._monitor_tasks, self._monitors):
|
||||
task.cancel()
|
||||
self._monitor_tasks.clear()
|
||||
self._logger.info('Shutdown finished successfully')
|
||||
|
||||
@staticmethod
|
||||
|
@ -407,9 +405,9 @@ Now we need to add the dispatcher to the container.
|
|||
Edit ``containers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 8,23-28
|
||||
:emphasize-lines: 8,22-27
|
||||
|
||||
"""Application containers module."""
|
||||
"""Containers module."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
@ -419,8 +417,7 @@ Edit ``containers.py``:
|
|||
from . import dispatcher
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
|
@ -438,35 +435,35 @@ Edit ``containers.py``:
|
|||
),
|
||||
)
|
||||
|
||||
.. note::
|
||||
At the last we will inject dispatcher into the ``main()`` function
|
||||
and call the ``run()`` method. We will use :ref:`wiring` feature.
|
||||
|
||||
Every component should be added to the container.
|
||||
|
||||
At the last we will add the dispatcher in the ``main()`` function. We will retrieve the
|
||||
dispatcher instance from the container and call the ``run()`` method.
|
||||
|
||||
Edit ``__main__.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 13-14
|
||||
:emphasize-lines: 3-7,11-12,19
|
||||
|
||||
"""Main module."""
|
||||
|
||||
from .containers import ApplicationContainer
|
||||
import sys
|
||||
|
||||
from dependency_injector.wiring import Provide
|
||||
|
||||
from .dispatcher import Dispatcher
|
||||
from .containers import Container
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the application."""
|
||||
container = ApplicationContainer()
|
||||
|
||||
container.config.from_yaml('config.yml')
|
||||
container.configure_logging()
|
||||
|
||||
dispatcher = container.dispatcher()
|
||||
def main(dispatcher: Dispatcher = Provide[Container.dispatcher]) -> None:
|
||||
dispatcher.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
container.configure_logging()
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
|
||||
main()
|
||||
|
||||
Finally let's start the daemon to check that all works.
|
||||
|
@ -481,22 +478,22 @@ The output should look like:
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
Starting monitoring-daemon-tutorial_monitor_1 ... done
|
||||
Attaching to monitoring-daemon-tutorial_monitor_1
|
||||
Starting asyncio-daemon-tutorial_monitor_1 ... done
|
||||
Attaching to asyncio-daemon-tutorial_monitor_1
|
||||
monitor_1 | [2020-08-08 16:12:35,772] [INFO] [Dispatcher]: Starting up
|
||||
monitor_1 | [2020-08-08 16:12:35,774] [INFO] [Dispatcher]: Shutting down
|
||||
monitor_1 | [2020-08-08 16:12:35,774] [INFO] [Dispatcher]: Shutdown finished successfully
|
||||
monitoring-daemon-tutorial_monitor_1 exited with code 0
|
||||
asyncio-daemon-tutorial_monitor_1 exited with code 0
|
||||
|
||||
Everything works properly. Dispatcher starts up and exits because there are no monitoring tasks.
|
||||
|
||||
By the end of this section we have the application skeleton ready. In the next section will will
|
||||
By the end of this section we have the application skeleton ready. In next section will will
|
||||
add first monitoring task.
|
||||
|
||||
Example.com monitor
|
||||
-------------------
|
||||
|
||||
In this section we will add the monitoring task that will check the availability of the
|
||||
In this section we will add a monitoring task that will check the availability of the
|
||||
`http://example.com <http://example.com>`_.
|
||||
|
||||
We will start from the extending of our class model with a new type of the monitoring check, the
|
||||
|
@ -506,9 +503,9 @@ The ``HttpMonitor`` is a subclass of the ``Monitor``. We will implement the ``ch
|
|||
will send the HTTP request to the specified URL. The http request sending will be delegated to
|
||||
the ``HttpClient``.
|
||||
|
||||
.. image:: asyncio_images/class_2.png
|
||||
.. image:: asyncio-images/classes-02.png
|
||||
|
||||
First, we need to create the ``HttpClient``.
|
||||
First we need to create the ``HttpClient``.
|
||||
|
||||
Create ``http.py`` in the ``monitoringdaemon`` package:
|
||||
|
||||
|
@ -549,9 +546,9 @@ Now we need to add the ``HttpClient`` to the container.
|
|||
Edit ``containers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 8, 23
|
||||
:emphasize-lines: 8,22
|
||||
|
||||
"""Application containers module."""
|
||||
"""Containers module."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
@ -561,8 +558,7 @@ Edit ``containers.py``:
|
|||
from . import http, dispatcher
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
|
@ -587,7 +583,7 @@ Now we're ready to add the ``HttpMonitor``. We will add it to the ``monitors`` m
|
|||
Edit ``monitors.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 4-5,7,20-56
|
||||
:emphasize-lines: 4-7,20-56
|
||||
|
||||
"""Monitors module."""
|
||||
|
||||
|
@ -638,7 +634,7 @@ Edit ``monitors.py``:
|
|||
' %s %s\n'
|
||||
' response code: %s\n'
|
||||
' content length: %s\n'
|
||||
' request took: %s seconds\n',
|
||||
' request took: %s seconds',
|
||||
self._method,
|
||||
self._url,
|
||||
response.status,
|
||||
|
@ -655,9 +651,9 @@ We make two changes in the container:
|
|||
Edit ``containers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 8,25-29,34
|
||||
:emphasize-lines: 8,24-28,33
|
||||
|
||||
"""Application containers module."""
|
||||
"""Containers module."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
@ -667,8 +663,7 @@ Edit ``containers.py``:
|
|||
from . import http, monitors, dispatcher
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
|
@ -727,15 +722,14 @@ You should see:
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
Starting monitoring-daemon-tutorial_monitor_1 ... done
|
||||
Attaching to monitoring-daemon-tutorial_monitor_1
|
||||
Starting asyncio-daemon-tutorial_monitor_1 ... done
|
||||
Attaching to asyncio-daemon-tutorial_monitor_1
|
||||
monitor_1 | [2020-08-08 17:06:41,965] [INFO] [Dispatcher]: Starting up
|
||||
monitor_1 | [2020-08-08 17:06:42,033] [INFO] [HttpMonitor]: Check
|
||||
monitor_1 | GET http://example.com
|
||||
monitor_1 | response code: 200
|
||||
monitor_1 | content length: 648
|
||||
monitor_1 | request took: 0.067 seconds
|
||||
monitor_1 |
|
||||
monitor_1 | [2020-08-08 17:06:47,040] [INFO] [HttpMonitor]: Check
|
||||
monitor_1 | GET http://example.com
|
||||
monitor_1 | response code: 200
|
||||
|
@ -744,21 +738,21 @@ You should see:
|
|||
|
||||
Our daemon can monitor `http://example.com <http://example.com>`_ availability.
|
||||
|
||||
Let's add the monitor for the `http://httpbin.org <http://httpbin.org>`_.
|
||||
Let's add a monitor for the `http://httpbin.org <http://httpbin.org>`_.
|
||||
|
||||
Httpbin.org monitor
|
||||
-------------------
|
||||
|
||||
Adding of the monitor for the `httpbin.org`_ will be much easier because we have all the
|
||||
Adding of a monitor for the `httpbin.org`_ will be much easier because we have all the
|
||||
components ready. We just need to create a new provider in the container and update the
|
||||
configuration.
|
||||
|
||||
Edit ``containers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 31-35,41
|
||||
:emphasize-lines: 30-34,40
|
||||
|
||||
"""Application containers module."""
|
||||
"""Containers module."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
@ -768,8 +762,7 @@ Edit ``containers.py``:
|
|||
from . import http, monitors, dispatcher
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
|
@ -837,27 +830,24 @@ You should see:
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
Starting monitoring-daemon-tutorial_monitor_1 ... done
|
||||
Attaching to monitoring-daemon-tutorial_monitor_1
|
||||
Starting asyncio-daemon-tutorial_monitor_1 ... done
|
||||
Attaching to asyncio-daemon-tutorial_monitor_1
|
||||
monitor_1 | [2020-08-08 18:09:08,540] [INFO] [Dispatcher]: Starting up
|
||||
monitor_1 | [2020-08-08 18:09:08,618] [INFO] [HttpMonitor]: Check
|
||||
monitor_1 | GET http://example.com
|
||||
monitor_1 | response code: 200
|
||||
monitor_1 | content length: 648
|
||||
monitor_1 | request took: 0.077 seconds
|
||||
monitor_1 |
|
||||
monitor_1 | [2020-08-08 18:09:08,722] [INFO] [HttpMonitor]: Check
|
||||
monitor_1 | GET https://httpbin.org/get
|
||||
monitor_1 | response code: 200
|
||||
monitor_1 | content length: 310
|
||||
monitor_1 | request took: 0.18 seconds
|
||||
monitor_1 |
|
||||
monitor_1 | [2020-08-08 18:09:13,619] [INFO] [HttpMonitor]: Check
|
||||
monitor_1 | GET http://example.com
|
||||
monitor_1 | response code: 200
|
||||
monitor_1 | content length: 648
|
||||
monitor_1 | request took: 0.066 seconds
|
||||
monitor_1 |
|
||||
monitor_1 | [2020-08-08 18:09:13,681] [INFO] [HttpMonitor]: Check
|
||||
monitor_1 | GET https://httpbin.org/get
|
||||
monitor_1 | response code: 200
|
||||
|
@ -867,12 +857,12 @@ You should see:
|
|||
The functional part is done. Daemon monitors `http://example.com <http://example.com>`_ and
|
||||
`https://httpbin.org <https://httpbin.org>`_.
|
||||
|
||||
In the next section we will add some tests.
|
||||
In next section we will add some tests.
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
It would be nice to add some tests. Let's do it.
|
||||
In this section we will add some tests.
|
||||
|
||||
We will use `pytest <https://docs.pytest.org/en/stable/>`_ and
|
||||
`coverage <https://coverage.readthedocs.io/>`_.
|
||||
|
@ -909,7 +899,7 @@ and put next into it:
|
|||
|
||||
import pytest
|
||||
|
||||
from .containers import ApplicationContainer
|
||||
from .containers import Container
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
|
@ -920,7 +910,7 @@ and put next into it:
|
|||
|
||||
@pytest.fixture
|
||||
def container():
|
||||
container = ApplicationContainer()
|
||||
container = Container()
|
||||
container.config.from_dict({
|
||||
'log': {
|
||||
'level': 'INFO',
|
||||
|
@ -1002,14 +992,14 @@ You should see:
|
|||
Name Stmts Miss Cover
|
||||
----------------------------------------------------
|
||||
monitoringdaemon/__init__.py 0 0 100%
|
||||
monitoringdaemon/__main__.py 9 9 0%
|
||||
monitoringdaemon/__main__.py 12 12 0%
|
||||
monitoringdaemon/containers.py 11 0 100%
|
||||
monitoringdaemon/dispatcher.py 43 5 88%
|
||||
monitoringdaemon/dispatcher.py 44 5 89%
|
||||
monitoringdaemon/http.py 6 3 50%
|
||||
monitoringdaemon/monitors.py 23 1 96%
|
||||
monitoringdaemon/tests.py 37 0 100%
|
||||
----------------------------------------------------
|
||||
TOTAL 129 18 86%
|
||||
TOTAL 133 21 84%
|
||||
|
||||
.. note::
|
||||
|
||||
|
@ -1028,55 +1018,19 @@ In this tutorial we've built an ``asyncio`` monitoring daemon following the dep
|
|||
injection principle.
|
||||
We've used the ``Dependency Injector`` as a dependency injection framework.
|
||||
|
||||
The benefit you get with the ``Dependency Injector`` is the container. It starts to payoff
|
||||
when you need to understand or change your application structure. It's easy with the container,
|
||||
cause you have everything defined explicitly in one place:
|
||||
With a help of :ref:`containers` and :ref:`providers` we have defined how to assemble application components.
|
||||
|
||||
.. code-block:: python
|
||||
``List`` provider helped to inject a list of monitors into dispatcher.
|
||||
:ref:`configuration-provider` helped to deal with reading YAML file.
|
||||
|
||||
"""Application containers module."""
|
||||
We used :ref:`wiring` feature to inject dispatcher into the ``main()`` function.
|
||||
:ref:`provider-overriding` feature helped in testing.
|
||||
|
||||
import logging
|
||||
import sys
|
||||
We kept all the dependencies injected explicitly. This will help when you need to add or
|
||||
change something in future.
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
|
||||
from . import http, monitors, dispatcher
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
configure_logging = providers.Callable(
|
||||
logging.basicConfig,
|
||||
stream=sys.stdout,
|
||||
level=config.log.level,
|
||||
format=config.log.format,
|
||||
)
|
||||
|
||||
http_client = providers.Factory(http.HttpClient)
|
||||
|
||||
example_monitor = providers.Factory(
|
||||
monitors.HttpMonitor,
|
||||
http_client=http_client,
|
||||
options=config.monitors.example,
|
||||
)
|
||||
|
||||
httpbin_monitor = providers.Factory(
|
||||
monitors.HttpMonitor,
|
||||
http_client=http_client,
|
||||
options=config.monitors.httpbin,
|
||||
)
|
||||
|
||||
dispatcher = providers.Factory(
|
||||
dispatcher.Dispatcher,
|
||||
monitors=providers.List(
|
||||
example_monitor,
|
||||
httpbin_monitor,
|
||||
),
|
||||
)
|
||||
You can find complete project on the
|
||||
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/asyncio-daemon>`_.
|
||||
|
||||
What's next?
|
||||
|
||||
|
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
@ -50,7 +50,7 @@ inversion of control:
|
|||
|
||||
Here is a class diagram of the Movie Lister application:
|
||||
|
||||
.. image:: cli-images/classes_01.png
|
||||
.. image:: cli-images/classes-01.png
|
||||
|
||||
The responsibilities are split next way:
|
||||
|
||||
|
@ -63,18 +63,18 @@ Prepare the environment
|
|||
|
||||
Let's create the environment for the project.
|
||||
|
||||
First we need to create a project folder and the virtual environment:
|
||||
First we need to create a project folder:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
mkdir movie-lister-tutorial
|
||||
cd movie-lister-tutorial
|
||||
python3 -m venv venv
|
||||
|
||||
Now let's activate the virtual environment:
|
||||
Now let's create and activate virtual environment:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python3 -m venv venv
|
||||
. venv/bin/activate
|
||||
|
||||
Project layout
|
||||
|
@ -245,13 +245,13 @@ Edit ``containers.py``:
|
|||
from dependency_injector import containers
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
class Container(containers.DeclarativeContainer):
|
||||
...
|
||||
|
||||
Container is empty for now. We will add the providers in the following sections.
|
||||
|
||||
Let's also create the ``main()`` function. Its responsibility is to run our application. For now
|
||||
it will just create the container.
|
||||
it will just do nothing.
|
||||
|
||||
Edit ``__main__.py``:
|
||||
|
||||
|
@ -259,22 +259,18 @@ Edit ``__main__.py``:
|
|||
|
||||
"""Main module."""
|
||||
|
||||
from .containers import ApplicationContainer
|
||||
from .containers import Container
|
||||
|
||||
|
||||
def main():
|
||||
container = ApplicationContainer()
|
||||
def main() -> None:
|
||||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
container = Container()
|
||||
|
||||
main()
|
||||
|
||||
.. note::
|
||||
|
||||
Container is the first object in the application.
|
||||
|
||||
The container is used to create all other objects.
|
||||
|
||||
Csv finder
|
||||
----------
|
||||
|
||||
|
@ -289,7 +285,7 @@ We will add:
|
|||
|
||||
After each step we will add the provider to the container.
|
||||
|
||||
.. image:: cli-images/classes_02.png
|
||||
.. image:: cli-images/classes-02.png
|
||||
|
||||
Create the ``entities.py`` in the ``movies`` package:
|
||||
|
||||
|
@ -338,7 +334,7 @@ Now we need to add the ``Movie`` factory to the container. We need to add import
|
|||
Edit ``containers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 3,5,9
|
||||
:emphasize-lines: 3,5,10
|
||||
|
||||
"""Containers module."""
|
||||
|
||||
|
@ -346,7 +342,8 @@ Edit ``containers.py``:
|
|||
|
||||
from . import entities
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
movie = providers.Factory(entities.Movie)
|
||||
|
||||
|
@ -420,7 +417,7 @@ Now let's add the csv finder into the container.
|
|||
Edit ``containers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 5,9,13-18
|
||||
:emphasize-lines: 5,10,14-19
|
||||
|
||||
"""Containers module."""
|
||||
|
||||
|
@ -428,7 +425,8 @@ Edit ``containers.py``:
|
|||
|
||||
from . import finders, entities
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
|
@ -474,20 +472,21 @@ The configuration file is ready. Now let's update the ``main()`` function to sp
|
|||
Edit ``__main__.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 9
|
||||
:emphasize-lines: 12
|
||||
|
||||
"""Main module."""
|
||||
|
||||
from .containers import ApplicationContainer
|
||||
from .containers import Container
|
||||
|
||||
|
||||
def main():
|
||||
container = ApplicationContainer()
|
||||
|
||||
container.config.from_yaml('config.yml')
|
||||
def main() -> None:
|
||||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
|
||||
main()
|
||||
|
||||
Move on to the lister.
|
||||
|
@ -542,7 +541,7 @@ and put next into it:
|
|||
and edit ``containers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 5,20-23
|
||||
:emphasize-lines: 5,21-24
|
||||
|
||||
"""Containers module."""
|
||||
|
||||
|
@ -550,7 +549,8 @@ and edit ``containers.py``:
|
|||
|
||||
from . import finders, listers, entities
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
|
@ -570,36 +570,69 @@ and edit ``containers.py``:
|
|||
|
||||
All the components are created and added to the container.
|
||||
|
||||
Finally let's update the ``main()`` function.
|
||||
Let's inject the ``lister`` into the ``main()`` function.
|
||||
|
||||
Edit ``__main__.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 11-20
|
||||
:emphasize-lines: 3-7,11,18
|
||||
|
||||
"""Main module."""
|
||||
|
||||
from .containers import ApplicationContainer
|
||||
import sys
|
||||
|
||||
from dependency_injector.wiring import Provide
|
||||
|
||||
from .listers import MovieLister
|
||||
from .containers import Container
|
||||
|
||||
|
||||
def main():
|
||||
container = ApplicationContainer()
|
||||
|
||||
container.config.from_yaml('config.yml')
|
||||
|
||||
lister = container.lister()
|
||||
|
||||
print(
|
||||
'Francis Lawrence movies:',
|
||||
lister.movies_directed_by('Francis Lawrence'),
|
||||
)
|
||||
print(
|
||||
'2016 movies:',
|
||||
lister.movies_released_in(2016),
|
||||
)
|
||||
def main(lister: MovieLister = Provide[Container.lister]) -> None:
|
||||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
|
||||
main()
|
||||
|
||||
Now when we call ``main()`` the container will assemble and inject the movie lister.
|
||||
|
||||
Let's add some payload to ``main()`` function. It will list movies directed by
|
||||
Francis Lawrence and movies released in 2016.
|
||||
|
||||
Edit ``__main__.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 12-18
|
||||
|
||||
"""Main module."""
|
||||
|
||||
import sys
|
||||
|
||||
from dependency_injector.wiring import Provide
|
||||
|
||||
from .listers import MovieLister
|
||||
from .containers import Container
|
||||
|
||||
|
||||
def main(lister: MovieLister = Provide[Container.lister]) -> None:
|
||||
print('Francis Lawrence movies:')
|
||||
for movie in lister.movies_directed_by('Francis Lawrence'):
|
||||
print('\t-', movie)
|
||||
|
||||
print('2016 movies:')
|
||||
for movie in lister.movies_released_in(2016):
|
||||
print('\t-', movie)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
|
||||
main()
|
||||
|
||||
All set. Now we run the application.
|
||||
|
@ -612,12 +645,15 @@ Run in the terminal:
|
|||
|
||||
You should see:
|
||||
|
||||
.. code-block:: bash
|
||||
.. code-block:: plain
|
||||
|
||||
Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')]
|
||||
2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')]
|
||||
Francis Lawrence movies:
|
||||
- Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')
|
||||
2016 movies:
|
||||
- Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards')
|
||||
- Movie(title='The Jungle Book', year=2016, director='Jon Favreau')
|
||||
|
||||
Our application can work with the movies database in the csv format. We also need to support
|
||||
Our application can work with the movies database in the csv format. We also want to support
|
||||
the sqlite format. We will deal with it in the next section.
|
||||
|
||||
Sqlite finder
|
||||
|
@ -688,7 +724,7 @@ Now we need to add the sqlite finder to the container and update lister's depend
|
|||
Edit ``containers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 20-24,28
|
||||
:emphasize-lines: 21-25,29
|
||||
|
||||
"""Containers module."""
|
||||
|
||||
|
@ -696,7 +732,8 @@ Edit ``containers.py``:
|
|||
|
||||
from . import finders, listers, entities
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
|
@ -747,10 +784,13 @@ Run in the terminal:
|
|||
|
||||
You should see:
|
||||
|
||||
.. code-block:: bash
|
||||
.. code-block:: plain
|
||||
|
||||
Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')]
|
||||
2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')]
|
||||
Francis Lawrence movies:
|
||||
- Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')
|
||||
2016 movies:
|
||||
- Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards')
|
||||
- Movie(title='The Jungle Book', year=2016, director='Jon Favreau')
|
||||
|
||||
Our application now supports both formats: csv files and sqlite databases. Every time when we
|
||||
need to work with the different format we need to make a code change in the container. We will
|
||||
|
@ -782,7 +822,7 @@ Edit ``containers.py``:
|
|||
from . import finders, listers, entities
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
|
@ -812,7 +852,7 @@ Edit ``containers.py``:
|
|||
movie_finder=finder,
|
||||
)
|
||||
|
||||
The switch is the ``config.finder.type`` option. When its value is ``csv``, the provider under
|
||||
The switch is the ``config.finder.type`` option. When its value is ``csv``, the provider with the
|
||||
``csv`` key is used. The same is for ``sqlite``.
|
||||
|
||||
Now we need to read the value of the ``config.finder.type`` option from the environment variable
|
||||
|
@ -821,32 +861,34 @@ Now we need to read the value of the ``config.finder.type`` option from the envi
|
|||
Edit ``__main__.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 10
|
||||
:emphasize-lines: 24
|
||||
|
||||
"""Main module."""
|
||||
|
||||
from .containers import ApplicationContainer
|
||||
import sys
|
||||
|
||||
from dependency_injector.wiring import Provide
|
||||
|
||||
from .listers import MovieLister
|
||||
from .containers import Container
|
||||
|
||||
|
||||
def main():
|
||||
container = ApplicationContainer()
|
||||
def main(lister: MovieLister = Provide[Container.lister]) -> None:
|
||||
print('Francis Lawrence movies:')
|
||||
for movie in lister.movies_directed_by('Francis Lawrence'):
|
||||
print('\t-', movie)
|
||||
|
||||
container.config.from_yaml('config.yml')
|
||||
container.config.finder.type.from_env('MOVIE_FINDER_TYPE')
|
||||
|
||||
lister = container.lister()
|
||||
|
||||
print(
|
||||
'Francis Lawrence movies:',
|
||||
lister.movies_directed_by('Francis Lawrence'),
|
||||
)
|
||||
print(
|
||||
'2016 movies:',
|
||||
lister.movies_released_in(2016),
|
||||
)
|
||||
print('2016 movies:')
|
||||
for movie in lister.movies_released_in(2016):
|
||||
print('\t-', movie)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
container.config.finder.type.from_env('MOVIE_FINDER_TYPE')
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
|
||||
main()
|
||||
|
||||
Done.
|
||||
|
@ -858,12 +900,15 @@ Run in the terminal line by line:
|
|||
MOVIE_FINDER_TYPE=csv python -m movies
|
||||
MOVIE_FINDER_TYPE=sqlite python -m movies
|
||||
|
||||
The output should be something like this for each command:
|
||||
The output should be similar for each command:
|
||||
|
||||
.. code-block:: bash
|
||||
.. code-block:: plain
|
||||
|
||||
Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')]
|
||||
2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')]
|
||||
Francis Lawrence movies:
|
||||
- Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')
|
||||
2016 movies:
|
||||
- Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards')
|
||||
- Movie(title='The Jungle Book', year=2016, director='Jon Favreau')
|
||||
|
||||
In the next section we will add some tests.
|
||||
|
||||
|
@ -908,12 +953,12 @@ and put next into it:
|
|||
|
||||
import pytest
|
||||
|
||||
from .containers import ApplicationContainer
|
||||
from .containers import Container
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def container():
|
||||
container = ApplicationContainer()
|
||||
container = Container()
|
||||
container.config.from_dict({
|
||||
'finder': {
|
||||
'type': 'csv',
|
||||
|
@ -966,7 +1011,7 @@ Run in the terminal:
|
|||
|
||||
You should see:
|
||||
|
||||
.. code-block:: bash
|
||||
.. code-block::
|
||||
|
||||
platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
|
||||
plugins: cov-2.10.0
|
||||
|
@ -974,18 +1019,18 @@ You should see:
|
|||
|
||||
movies/tests.py .. [100%]
|
||||
|
||||
---------- coverage: platform darwin, python 3.8.3-final-0 -----------
|
||||
---------- coverage: platform darwin, python 3.8.5-final-0 -----------
|
||||
Name Stmts Miss Cover
|
||||
------------------------------------------
|
||||
movies/__init__.py 0 0 100%
|
||||
movies/__main__.py 10 10 0%
|
||||
movies/__main__.py 17 17 0%
|
||||
movies/containers.py 9 0 100%
|
||||
movies/entities.py 7 1 86%
|
||||
movies/finders.py 26 13 50%
|
||||
movies/listers.py 8 0 100%
|
||||
movies/tests.py 24 0 100%
|
||||
------------------------------------------
|
||||
TOTAL 84 24 71%
|
||||
TOTAL 91 31 66%
|
||||
|
||||
.. note::
|
||||
|
||||
|
@ -1002,48 +1047,19 @@ Conclusion
|
|||
In this tutorial we've built a CLI application following the dependency injection principle.
|
||||
We've used the ``Dependency Injector`` as a dependency injection framework.
|
||||
|
||||
The benefit you get with the ``Dependency Injector`` is the container. It starts to payoff
|
||||
when you need to understand or change your application structure. It's easy with the container,
|
||||
cause you have everything defined explicitly in one place:
|
||||
With a help of :ref:`containers` and :ref:`providers` we have defined how to assemble application components.
|
||||
|
||||
.. code-block:: python
|
||||
``Selector`` provider served as a switch for selecting the database format based on a configuration.
|
||||
:ref:`configuration-provider` helped to deal with reading YAML file and environment variable.
|
||||
|
||||
"""Containers module."""
|
||||
We used :ref:`wiring` feature to inject the dependencies into the ``main()`` function.
|
||||
:ref:`provider-overriding` feature helped in testing.
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
We kept all the dependencies injected explicitly. This will help when you need to add or
|
||||
change something in future.
|
||||
|
||||
from . import finders, listers, entities
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
movie = providers.Factory(entities.Movie)
|
||||
|
||||
csv_finder = providers.Singleton(
|
||||
finders.CsvMovieFinder,
|
||||
movie_factory=movie.provider,
|
||||
path=config.finder.csv.path,
|
||||
delimiter=config.finder.csv.delimiter,
|
||||
)
|
||||
|
||||
sqlite_finder = providers.Singleton(
|
||||
finders.SqliteMovieFinder,
|
||||
movie_factory=movie.provider,
|
||||
path=config.finder.sqlite.path,
|
||||
)
|
||||
|
||||
finder = providers.Selector(
|
||||
config.finder.type,
|
||||
csv=csv_finder,
|
||||
sqlite=sqlite_finder,
|
||||
)
|
||||
|
||||
lister = providers.Factory(
|
||||
listers.MovieLister,
|
||||
movie_finder=finder,
|
||||
)
|
||||
You can find complete project on the
|
||||
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/movie-lister>`_.
|
||||
|
||||
What's next?
|
||||
|
||||
|
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
Before Width: | Height: | Size: 647 KiB After Width: | Height: | Size: 647 KiB |
|
@ -21,7 +21,7 @@ Start from the scratch or jump to the section:
|
|||
:backlinks: none
|
||||
|
||||
You can find complete project on the
|
||||
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/ghnav-flask>`_.
|
||||
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/flask>`_.
|
||||
|
||||
What are we going to build?
|
||||
---------------------------
|
||||
|
@ -43,25 +43,25 @@ How does Github Navigator work?
|
|||
- User can click on the repository, the repository owner or the last commit to open its web page
|
||||
on the Github.
|
||||
|
||||
.. image:: flask_images/screen_02.png
|
||||
.. image:: flask-images/screen-02.png
|
||||
|
||||
Prepare the environment
|
||||
-----------------------
|
||||
|
||||
Let's create the environment for the project.
|
||||
|
||||
First we need to create a project folder and the virtual environment:
|
||||
First we need to create a project folder:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
mkdir ghnav-flask-tutorial
|
||||
cd ghnav-flask-tutorial
|
||||
python3 -m venv venv
|
||||
|
||||
Now let's activate the virtual environment:
|
||||
Now let's create and activate virtual environment:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python3 -m venv venv
|
||||
. venv/bin/activate
|
||||
|
||||
Project layout
|
||||
|
@ -110,13 +110,13 @@ You should see something like:
|
|||
.. code-block:: bash
|
||||
|
||||
(venv) $ python -c "import dependency_injector; print(dependency_injector.__version__)"
|
||||
3.22.0
|
||||
4.0.0
|
||||
(venv) $ python -c "import flask; print(flask.__version__)"
|
||||
1.1.2
|
||||
|
||||
*Versions can be different. That's fine.*
|
||||
|
||||
Hello world!
|
||||
Hello World!
|
||||
------------
|
||||
|
||||
Let's create minimal application.
|
||||
|
@ -133,34 +133,25 @@ Put next into the ``views.py``:
|
|||
|
||||
Ok, we have the view.
|
||||
|
||||
Now let's create the main part of our application - the container. Container will keep all of the
|
||||
application components and their dependencies. First two providers we need to add are
|
||||
the ``Flask`` application provider and the view provider.
|
||||
Now let's create a container. Container will keep all of the application components and their dependencies.
|
||||
|
||||
Put next into the ``containers.py``:
|
||||
Edit ``containers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"""Application containers module."""
|
||||
"""Containers module."""
|
||||
|
||||
from dependency_injector import containers
|
||||
from dependency_injector.ext import flask
|
||||
from flask import Flask
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
class Container(containers.DeclarativeContainer):
|
||||
...
|
||||
|
||||
app = flask.Application(Flask, __name__)
|
||||
Container is empty for now. We will add the providers in the following sections.
|
||||
|
||||
index_view = flask.View(views.index)
|
||||
|
||||
Finally we need to create the Flask application factory. It is traditionally called
|
||||
``create_app()``. It will create the container. Then it will use the container to create
|
||||
the Flask application. Last step is to configure the routing - we will assign ``index_view`` from
|
||||
the container to handle user requests to the root ``/`` of our web application.
|
||||
Finally we need to create Flask application factory. It will create and configure container
|
||||
and Flask application. It is traditionally called ``create_app()``.
|
||||
We will assign ``index`` view to handle user requests to the root ``/`` of our web application.
|
||||
|
||||
Put next into the ``application.py``:
|
||||
|
||||
|
@ -168,26 +159,21 @@ Put next into the ``application.py``:
|
|||
|
||||
"""Application module."""
|
||||
|
||||
from .containers import ApplicationContainer
|
||||
from flask import Flask
|
||||
|
||||
from .containers import Container
|
||||
from . import views
|
||||
|
||||
|
||||
def create_app():
|
||||
"""Create and return Flask application."""
|
||||
container = ApplicationContainer()
|
||||
def create_app() -> Flask:
|
||||
container = Container()
|
||||
|
||||
app = container.app()
|
||||
app = Flask(__name__)
|
||||
app.container = container
|
||||
|
||||
app.add_url_rule('/', view_func=container.index_view.as_view())
|
||||
app.add_url_rule('/', 'index', views.index)
|
||||
|
||||
return app
|
||||
|
||||
.. note::
|
||||
|
||||
Container is the first object in the application.
|
||||
|
||||
The container is used to create all other objects.
|
||||
|
||||
Ok. Now we're ready to say "Hello, World!".
|
||||
|
||||
Do next in the terminal:
|
||||
|
@ -237,58 +223,34 @@ and run in the terminal:
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install --upgrade -r requirements.txt
|
||||
|
||||
Now we need to add ``bootstrap-flask`` extension to the container.
|
||||
|
||||
Edit ``containers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 6,16
|
||||
|
||||
"""Application containers module."""
|
||||
|
||||
from dependency_injector import containers
|
||||
from dependency_injector.ext import flask
|
||||
from flask import Flask
|
||||
from flask_bootstrap import Bootstrap
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
|
||||
app = flask.Application(Flask, __name__)
|
||||
|
||||
bootstrap = flask.Extension(Bootstrap)
|
||||
|
||||
index_view = flask.View(views.index)
|
||||
pip install -r requirements.txt
|
||||
|
||||
Let's initialize ``bootstrap-flask`` extension. We will need to modify ``create_app()``.
|
||||
|
||||
Edit ``application.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 13-14
|
||||
:emphasize-lines: 4,17-18
|
||||
|
||||
"""Application module."""
|
||||
|
||||
from .containers import ApplicationContainer
|
||||
from flask import Flask
|
||||
from flask_bootstrap import Bootstrap
|
||||
|
||||
from .containers import Container
|
||||
from . import views
|
||||
|
||||
|
||||
def create_app():
|
||||
"""Create and return Flask application."""
|
||||
container = ApplicationContainer()
|
||||
def create_app() -> Flask:
|
||||
container = Container()
|
||||
|
||||
app = container.app()
|
||||
app = Flask(__name__)
|
||||
app.container = container
|
||||
app.add_url_rule('/', 'index', views.index)
|
||||
|
||||
bootstrap = container.bootstrap()
|
||||
bootstrap = Bootstrap()
|
||||
bootstrap.init_app(app)
|
||||
|
||||
app.add_url_rule('/', view_func=container.index_view.as_view())
|
||||
|
||||
return app
|
||||
|
||||
Now we need to add the templates. For doing this we will need to add the folder ``templates/`` to
|
||||
|
@ -454,7 +416,7 @@ Make sure the app is running or use ``flask run`` and open ``http://127.0.0.1:50
|
|||
|
||||
You should see:
|
||||
|
||||
.. image:: flask_images/screen_01.png
|
||||
.. image:: flask-images/screen-01.png
|
||||
|
||||
Connect to the GitHub
|
||||
---------------------
|
||||
|
@ -477,7 +439,7 @@ and run in the terminal:
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install --upgrade -r requirements.txt
|
||||
pip install -r requirements.txt
|
||||
|
||||
Now we need to add Github API client the container. We will need to add two more providers from
|
||||
the ``dependency_injector.providers`` module:
|
||||
|
@ -486,30 +448,18 @@ the ``dependency_injector.providers`` module:
|
|||
- ``Configuration`` provider that will be used for providing the API token and the request timeout
|
||||
for the ``Github`` client.
|
||||
|
||||
Let's do it.
|
||||
|
||||
Edit ``containers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 3,7,19,21-25
|
||||
:emphasize-lines: 3-4,9,11-15
|
||||
|
||||
"""Application containers module."""
|
||||
"""Containers module."""
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.ext import flask
|
||||
from flask import Flask
|
||||
from flask_bootstrap import Bootstrap
|
||||
from github import Github
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
|
||||
app = flask.Application(Flask, __name__)
|
||||
|
||||
bootstrap = flask.Extension(Bootstrap)
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
|
@ -519,8 +469,6 @@ Edit ``containers.py``:
|
|||
timeout=config.github.request_timeout,
|
||||
)
|
||||
|
||||
index_view = flask.View(views.index)
|
||||
|
||||
.. note::
|
||||
|
||||
We have used the configuration value before it was defined. That's the principle how
|
||||
|
@ -528,11 +476,16 @@ Edit ``containers.py``:
|
|||
|
||||
Use first, define later.
|
||||
|
||||
.. note::
|
||||
|
||||
Don't forget to remove the Ellipsis ``...`` from the container. We don't need it anymore
|
||||
since we container is not empty.
|
||||
|
||||
Now let's add the configuration file.
|
||||
|
||||
We will use YAML.
|
||||
|
||||
Create an empty file ``config.yml`` in the root root of the project:
|
||||
Create an empty file ``config.yml`` in the root of the project:
|
||||
|
||||
.. code-block:: bash
|
||||
:emphasize-lines: 11
|
||||
|
@ -575,7 +528,7 @@ and install it:
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install --upgrade -r requirements.txt
|
||||
pip install -r requirements.txt
|
||||
|
||||
We will use environment variable ``GITHUB_TOKEN`` to provide the API token.
|
||||
|
||||
|
@ -587,27 +540,29 @@ Now we need to edit ``create_app()`` to make two things when application starts:
|
|||
Edit ``application.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 9-10
|
||||
:emphasize-lines: 12-13
|
||||
|
||||
"""Application module."""
|
||||
|
||||
from .containers import ApplicationContainer
|
||||
from flask import Flask
|
||||
from flask_bootstrap import Bootstrap
|
||||
|
||||
from .containers import Container
|
||||
from . import views
|
||||
|
||||
|
||||
def create_app():
|
||||
"""Create and return Flask application."""
|
||||
container = ApplicationContainer()
|
||||
def create_app() -> Flask:
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
container.config.github.auth_token.from_env('GITHUB_TOKEN')
|
||||
|
||||
app = container.app()
|
||||
app = Flask(__name__)
|
||||
app.container = container
|
||||
app.add_url_rule('/', 'index', views.index)
|
||||
|
||||
bootstrap = container.bootstrap()
|
||||
bootstrap = Bootstrap()
|
||||
bootstrap.init_app(app)
|
||||
|
||||
app.add_url_rule('/', view_func=container.index_view.as_view())
|
||||
|
||||
return app
|
||||
|
||||
Now we need create an API token.
|
||||
|
@ -636,7 +591,7 @@ Github API client setup is done.
|
|||
Search service
|
||||
--------------
|
||||
|
||||
Now it's time to add the ``SearchService``. It will:
|
||||
Now it's time to add ``SearchService``. It will:
|
||||
|
||||
- Perform the search.
|
||||
- Fetch commit extra data for each result.
|
||||
|
@ -717,25 +672,17 @@ Now let's add ``SearchService`` to the container.
|
|||
Edit ``containers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 9,27-30
|
||||
:emphasize-lines: 6,19-22
|
||||
|
||||
"""Application containers module."""
|
||||
"""Containers module."""
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.ext import flask
|
||||
from flask import Flask
|
||||
from flask_bootstrap import Bootstrap
|
||||
from github import Github
|
||||
|
||||
from . import services, views
|
||||
from . import services
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
|
||||
app = flask.Application(Flask, __name__)
|
||||
|
||||
bootstrap = flask.Extension(Bootstrap)
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
|
@ -750,26 +697,28 @@ Edit ``containers.py``:
|
|||
github_client=github_client,
|
||||
)
|
||||
|
||||
index_view = flask.View(views.index)
|
||||
Inject search service into view
|
||||
-------------------------------
|
||||
|
||||
Make the search work
|
||||
--------------------
|
||||
Now we are ready to make the search work.
|
||||
|
||||
Now we are ready to make the search work. Let's use the ``SearchService`` in the ``index`` view.
|
||||
Let's inject ``SearchService`` into the ``index`` view. We will use :ref:`Wiring` feature.
|
||||
|
||||
Edit ``views.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 5,8,12
|
||||
:emphasize-lines: 4,6-7,10,14
|
||||
|
||||
"""Views module."""
|
||||
|
||||
from flask import request, render_template
|
||||
from dependency_injector.wiring import Provide
|
||||
|
||||
from .services import SearchService
|
||||
from .containers import Container
|
||||
|
||||
|
||||
def index(search_service: SearchService):
|
||||
def index(search_service: SearchService = Provide[Container.search_service]):
|
||||
query = request.args.get('query', 'Dependency Injector')
|
||||
limit = request.args.get('limit', 10, int)
|
||||
|
||||
|
@ -782,54 +731,44 @@ Edit ``views.py``:
|
|||
repositories=repositories,
|
||||
)
|
||||
|
||||
Now let's inject the ``SearchService`` dependency into the ``index`` view.
|
||||
To make the injection work we need to wire the container instance with the ``views`` module.
|
||||
This needs to be done once. After it's done we can use ``Provide`` markers to specify as many
|
||||
injections as needed for any view.
|
||||
|
||||
Edit ``containers.py``:
|
||||
Edit ``application.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 32-35
|
||||
:emphasize-lines: 14
|
||||
|
||||
"""Application containers module."""
|
||||
"""Application module."""
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.ext import flask
|
||||
from flask import Flask
|
||||
from flask_bootstrap import Bootstrap
|
||||
from github import Github
|
||||
|
||||
from . import services, views
|
||||
from .containers import Container
|
||||
from . import views
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
def create_app() -> Flask:
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
container.config.github.auth_token.from_env('GITHUB_TOKEN')
|
||||
container.wire(modules=[views])
|
||||
|
||||
app = flask.Application(Flask, __name__)
|
||||
app = Flask(__name__)
|
||||
app.container = container
|
||||
app.add_url_rule('/', 'index', views.index)
|
||||
|
||||
bootstrap = flask.Extension(Bootstrap)
|
||||
bootstrap = Bootstrap()
|
||||
bootstrap.init_app(app)
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
github_client = providers.Factory(
|
||||
Github,
|
||||
login_or_token=config.github.auth_token,
|
||||
timeout=config.github.request_timeout,
|
||||
)
|
||||
|
||||
search_service = providers.Factory(
|
||||
services.SearchService,
|
||||
github_client=github_client,
|
||||
)
|
||||
|
||||
index_view = flask.View(
|
||||
views.index,
|
||||
search_service=search_service,
|
||||
)
|
||||
return app
|
||||
|
||||
Make sure the app is running or use ``flask run`` and open ``http://127.0.0.1:5000/``.
|
||||
|
||||
You should see:
|
||||
|
||||
.. image:: flask_images/screen_02.png
|
||||
.. image:: flask-images/screen-02.png
|
||||
|
||||
Make some refactoring
|
||||
---------------------
|
||||
|
@ -844,19 +783,21 @@ Let's make some refactoring. We will move these values to the config.
|
|||
Edit ``views.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 8-14
|
||||
:emphasize-lines: 10-16
|
||||
|
||||
"""Views module."""
|
||||
|
||||
from flask import request, render_template
|
||||
from dependency_injector.wiring import Provide
|
||||
|
||||
from .services import SearchService
|
||||
from .containers import Container
|
||||
|
||||
|
||||
def index(
|
||||
search_service: SearchService,
|
||||
default_query: str,
|
||||
default_limit: int,
|
||||
search_service: SearchService = Provide[Container.search_service],
|
||||
default_query: str = Provide[Container.config.default.query],
|
||||
default_limit: int = Provide[Container.config.default.limit.as_int()],
|
||||
):
|
||||
query = request.args.get('query', default_query)
|
||||
limit = request.args.get('limit', default_limit, int)
|
||||
|
@ -870,53 +811,6 @@ Edit ``views.py``:
|
|||
repositories=repositories,
|
||||
)
|
||||
|
||||
Now we need to inject these values. Let's update the container.
|
||||
|
||||
Edit ``containers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 35-36
|
||||
|
||||
"""Application containers module."""
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.ext import flask
|
||||
from flask import Flask
|
||||
from flask_bootstrap import Bootstrap
|
||||
from github import Github
|
||||
|
||||
from . import services, views
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
|
||||
app = flask.Application(Flask, __name__)
|
||||
|
||||
bootstrap = flask.Extension(Bootstrap)
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
github_client = providers.Factory(
|
||||
Github,
|
||||
login_or_token=config.github.auth_token,
|
||||
timeout=config.github.request_timeout,
|
||||
)
|
||||
|
||||
search_service = providers.Factory(
|
||||
services.SearchService,
|
||||
github_client=github_client,
|
||||
)
|
||||
|
||||
index_view = flask.View(
|
||||
views.index,
|
||||
search_service=search_service,
|
||||
default_query=config.search.default_query,
|
||||
default_limit=config.search.default_limit,
|
||||
)
|
||||
|
||||
Finally let's update the config.
|
||||
|
||||
Edit ``config.yml``:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
@ -924,20 +818,18 @@ Edit ``config.yml``:
|
|||
|
||||
github:
|
||||
request_timeout: 10
|
||||
search:
|
||||
default_query: "Dependency Injector"
|
||||
default_limit: 10
|
||||
default:
|
||||
query: "Dependency Injector"
|
||||
limit: 10
|
||||
|
||||
That's it.
|
||||
|
||||
The refactoring is done. We've made it cleaner.
|
||||
That's it. The refactoring is done. We've made it cleaner.
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
It would be nice to add some tests. Let's do this.
|
||||
In this section we will add some tests.
|
||||
|
||||
We will use `pytest <https://docs.pytest.org/en/stable/>`_ and
|
||||
We will use `pytest <https://docs.pytest.org/en/stable/>`_ with its Flask extension and
|
||||
`coverage <https://coverage.readthedocs.io/>`_.
|
||||
|
||||
Edit ``requirements.txt``:
|
||||
|
@ -953,7 +845,7 @@ Edit ``requirements.txt``:
|
|||
pytest-flask
|
||||
pytest-cov
|
||||
|
||||
And let's install it:
|
||||
And install added packages:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
|
@ -982,7 +874,7 @@ Create empty file ``tests.py`` in the ``githubnavigator`` package:
|
|||
and put next into it:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 42,65
|
||||
:emphasize-lines: 44,67
|
||||
|
||||
"""Tests module."""
|
||||
|
||||
|
@ -997,7 +889,9 @@ and put next into it:
|
|||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
return create_app()
|
||||
app = create_app()
|
||||
yield app
|
||||
app.container.unwire()
|
||||
|
||||
|
||||
def test_index(client, app):
|
||||
|
@ -1074,13 +968,13 @@ You should see:
|
|||
Name Stmts Miss Cover
|
||||
----------------------------------------------------
|
||||
githubnavigator/__init__.py 0 0 100%
|
||||
githubnavigator/application.py 11 0 100%
|
||||
githubnavigator/containers.py 13 0 100%
|
||||
githubnavigator/application.py 15 0 100%
|
||||
githubnavigator/containers.py 7 0 100%
|
||||
githubnavigator/services.py 14 0 100%
|
||||
githubnavigator/tests.py 32 0 100%
|
||||
githubnavigator/views.py 7 0 100%
|
||||
githubnavigator/tests.py 34 0 100%
|
||||
githubnavigator/views.py 9 0 100%
|
||||
----------------------------------------------------
|
||||
TOTAL 77 0 100%
|
||||
TOTAL 79 0 100%
|
||||
|
||||
.. note::
|
||||
|
||||
|
@ -1091,53 +985,22 @@ You should see:
|
|||
Conclusion
|
||||
----------
|
||||
|
||||
We are done.
|
||||
|
||||
In this tutorial we've built a ``Flask`` application following the dependency injection principle.
|
||||
We've used the ``Dependency Injector`` as a dependency injection framework.
|
||||
|
||||
The main part of this application is the container. It keeps all the application components and
|
||||
their dependencies defined explicitly in one place:
|
||||
:ref:`containers` and :ref:`providers` helped to specify how to assemble search service and
|
||||
integrate it with a 3rd-party library.
|
||||
|
||||
.. code-block:: python
|
||||
:ref:`configuration-provider` helped to deal with reading YAML file and environment variable.
|
||||
|
||||
"""Application containers module."""
|
||||
We used :ref:`wiring` feature to inject the dependencies into the ``index()`` view.
|
||||
:ref:`provider-overriding` feature helped in testing.
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.ext import flask
|
||||
from flask import Flask
|
||||
from flask_bootstrap import Bootstrap
|
||||
from github import Github
|
||||
We kept all the dependencies injected explicitly. This will help when you need to add or
|
||||
change something in future.
|
||||
|
||||
from . import services, views
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
|
||||
app = flask.Application(Flask, __name__)
|
||||
|
||||
bootstrap = flask.Extension(Bootstrap)
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
github_client = providers.Factory(
|
||||
Github,
|
||||
login_or_token=config.github.auth_token,
|
||||
timeout=config.github.request_timeout,
|
||||
)
|
||||
|
||||
search_service = providers.Factory(
|
||||
services.SearchService,
|
||||
github_client=github_client,
|
||||
)
|
||||
|
||||
index_view = flask.View(
|
||||
views.index,
|
||||
search_service=search_service,
|
||||
default_query=config.search.default_query,
|
||||
default_limit=config.search.default_limit,
|
||||
)
|
||||
You can find complete project on the
|
||||
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/flask>`_.
|
||||
|
||||
What's next?
|
||||
|
||||
|
|
201
docs/wiring.rst
Normal file
|
@ -0,0 +1,201 @@
|
|||
.. _wiring:
|
||||
|
||||
Wiring
|
||||
======
|
||||
|
||||
Wiring feature provides a way to inject container providers into the functions and methods.
|
||||
|
||||
To use wiring you need:
|
||||
|
||||
- **Place markers in the code**. Wiring marker specifies what provider to inject,
|
||||
e.g. ``Provide[Container.bar]``. This helps container to find the injections.
|
||||
- **Wire the container with the markers in the code**. Call ``container.wire()``
|
||||
specifying modules and packages you would like to wire it with.
|
||||
- **Use functions and classes as you normally do**. Framework will provide specified injections.
|
||||
|
||||
.. literalinclude:: ../examples/wiring/example.py
|
||||
:language: python
|
||||
:lines: 3-
|
||||
|
||||
Markers
|
||||
-------
|
||||
|
||||
Wiring feature uses markers to make injections. Injection marker is specified as a default value of
|
||||
a function or method argument:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from dependency_injector.wiring import Provide
|
||||
|
||||
|
||||
def foo(bar: Bar = Provide[Container.bar]):
|
||||
...
|
||||
|
||||
Specifying an annotation is optional.
|
||||
|
||||
There are two types of markers:
|
||||
|
||||
- ``Provide[foo]`` - call the provider ``foo`` and injects the result
|
||||
- ``Provider[foo]`` - injects the provider ``foo`` itself
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from dependency_injector.wiring import Provider
|
||||
|
||||
|
||||
def foo(bar_provider: Callable[..., Bar] = Provider[Container.bar]):
|
||||
bar = bar_provider()
|
||||
...
|
||||
|
||||
You can use configuration, provided instance and sub-container providers as you normally do.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def foo(token: str = Provide[Container.config.api_token]):
|
||||
...
|
||||
|
||||
|
||||
def foo(timeout: int = Provide[Container.config.timeout.as_(int)]):
|
||||
...
|
||||
|
||||
|
||||
def foo(baz: Baz = Provide[Container.bar.provided.baz]):
|
||||
...
|
||||
|
||||
|
||||
def foo(bar: Bar = Provide[Container.subcontainer.bar]):
|
||||
...
|
||||
|
||||
Wiring with modules and packages
|
||||
--------------------------------
|
||||
|
||||
To wire a container with a module you need to call ``container.wire(modules=[...])`` method. Argument
|
||||
``modules`` is an iterable of the module objects.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from yourapp import module1, module2
|
||||
|
||||
|
||||
container = Container()
|
||||
container.wire(modules=[module1, module2])
|
||||
|
||||
You can wire container with a package. Container walks recursively over package modules.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from yourapp import package1, package2
|
||||
|
||||
|
||||
container = Container()
|
||||
container.wire(packages=[package1, package2])
|
||||
|
||||
Arguments ``modules`` and ``packages`` can be used together.
|
||||
|
||||
When wiring is done functions and methods with the markers are patched to provide injections when called.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def foo(bar: Bar = Provide[Container.bar]):
|
||||
...
|
||||
|
||||
|
||||
container = Container()
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
|
||||
foo() # <--- Argument "bar" is injected
|
||||
|
||||
Injections are done as keyword arguments.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
foo() # Equivalent to:
|
||||
foo(bar=container.bar())
|
||||
|
||||
Context keyword arguments have a priority over injections.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
foo(bar=Bar()) # Bar() is injected
|
||||
|
||||
To unpatch previously patched functions and methods call ``container.unwire()`` method.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
container.unwire()
|
||||
|
||||
You can use that in testing to re-create and re-wire a container before each test.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
class SomeTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.container = Container()
|
||||
self.container.wire(modules=[module1, module2])
|
||||
self.addCleanup(self.container.unwire)
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def container():
|
||||
container = Container()
|
||||
container.wire(modules=[module1, module2])
|
||||
yield container
|
||||
container.unwire()
|
||||
|
||||
.. note::
|
||||
Wiring can take time if you have a large codebase. Consider to persist a container instance and
|
||||
avoid re-wiring between tests.
|
||||
|
||||
.. note::
|
||||
Python has a limitation on patching already imported individual members. To protect from errors
|
||||
prefer an import of modules instead of individual members or make sure that imports happen
|
||||
after the wiring:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from . import module
|
||||
|
||||
module.fn()
|
||||
|
||||
# instead of
|
||||
|
||||
from .module import fn
|
||||
|
||||
fn()
|
||||
|
||||
Integration with other frameworks
|
||||
---------------------------------
|
||||
|
||||
Wiring feature helps to integrate with other frameworks like Django, Flask, etc.
|
||||
|
||||
With wiring you do not need to change the traditional application structure of your framework.
|
||||
|
||||
1. Create a container and put framework-independent components as providers.
|
||||
2. Place wiring markers in the functions and methods where you want the providers
|
||||
to be injected (Flask or Django views, Aiohttp or Sanic handlers, etc).
|
||||
3. Wire the container with the application modules.
|
||||
4. Run the application.
|
||||
|
||||
.. literalinclude:: ../examples/wiring/flask_example.py
|
||||
:language: python
|
||||
:lines: 3-
|
||||
|
||||
Take a look at other application examples:
|
||||
|
||||
- :ref:`application-single-container`
|
||||
- :ref:`application-multiple-containers`
|
||||
- :ref:`decoupled-packages`
|
||||
- :ref:`django-example`
|
||||
- :ref:`flask-example`
|
||||
- :ref:`aiohttp-example`
|
||||
- :ref:`sanic-example`
|
||||
|
||||
.. disqus::
|
29
examples/demo/after.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
import os
|
||||
|
||||
|
||||
class ApiClient:
|
||||
|
||||
def __init__(self, api_key: str, timeout: int):
|
||||
self.api_key = api_key # <-- dependency is injected
|
||||
self.timeout = timeout # <-- dependency is injected
|
||||
|
||||
|
||||
class Service:
|
||||
|
||||
def __init__(self, api_client: ApiClient):
|
||||
self.api_client = api_client # <-- dependency is injected
|
||||
|
||||
|
||||
def main(service: Service): # <-- dependency is injected
|
||||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(
|
||||
service=Service(
|
||||
api_client=ApiClient(
|
||||
api_key=os.getenv('API_KEY'),
|
||||
timeout=os.getenv('TIMEOUT'),
|
||||
),
|
||||
),
|
||||
)
|
23
examples/demo/before.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
import os
|
||||
|
||||
|
||||
class ApiClient:
|
||||
|
||||
def __init__(self):
|
||||
self.api_key = os.getenv('API_KEY') # <-- dependency
|
||||
self.timeout = os.getenv('TIMEOUT') # <-- dependency
|
||||
|
||||
|
||||
class Service:
|
||||
|
||||
def __init__(self):
|
||||
self.api_client = ApiClient() # <-- dependency
|
||||
|
||||
|
||||
def main() -> None:
|
||||
service = Service() # <-- dependency
|
||||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,17 +1,10 @@
|
|||
import sys
|
||||
from unittest import mock
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.wiring import Provide
|
||||
|
||||
|
||||
class ApiClient:
|
||||
|
||||
def __init__(self, api_key: str, timeout: int):
|
||||
self.api_key = api_key
|
||||
self.timeout = timeout
|
||||
|
||||
|
||||
class Service:
|
||||
|
||||
def __init__(self, api_client: ApiClient):
|
||||
self.api_client = api_client
|
||||
from after import ApiClient, Service
|
||||
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
@ -30,9 +23,17 @@ class Container(containers.DeclarativeContainer):
|
|||
)
|
||||
|
||||
|
||||
def main(service: Service = Provide[Container.service]):
|
||||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
container = Container()
|
||||
container.config.api_key.from_env('API_KEY')
|
||||
container.config.timeout.from_env('TIMEOUT')
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
|
||||
service = container.service()
|
||||
main() # <-- dependency is injected automatically
|
||||
|
||||
with container.api_client.override(mock.Mock()):
|
||||
main() # <-- overridden dependency is injected automatically
|
|
@ -1,18 +0,0 @@
|
|||
import os
|
||||
|
||||
|
||||
class ApiClient:
|
||||
|
||||
def __init__(self, api_key: str, timeout: int):
|
||||
self.api_key = api_key
|
||||
self.timeout = timeout
|
||||
|
||||
|
||||
class Service:
|
||||
|
||||
def __init__(self, api_client: ApiClient):
|
||||
self.api_client = api_client
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT')))
|
|
@ -1,18 +0,0 @@
|
|||
import os
|
||||
|
||||
|
||||
class ApiClient:
|
||||
|
||||
def __init__(self):
|
||||
self.api_key = os.getenv('API_KEY')
|
||||
self.timeout = os.getenv('TIMEOUT')
|
||||
|
||||
|
||||
class Service:
|
||||
|
||||
def __init__(self):
|
||||
self.api_client = ApiClient()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
service = Service()
|
|
@ -1,11 +0,0 @@
|
|||
from unittest import mock
|
||||
|
||||
from demo import Container
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
container = Container()
|
||||
|
||||
with container.api_client.override(mock.Mock()):
|
||||
service = container.service()
|
||||
assert isinstance(service.api_client, mock.Mock)
|
|
@ -1,8 +1,10 @@
|
|||
Aiohttp Dependency Injection Example
|
||||
====================================
|
||||
Aiohttp + Dependency Injector Example
|
||||
=====================================
|
||||
|
||||
Application ``giphynavigator`` is an `Aiohttp <https://docs.aiohttp.org/>`_ +
|
||||
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ application.
|
||||
This is an `Aiohttp <https://docs.aiohttp.org/>`_ +
|
||||
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ example application.
|
||||
|
||||
The example application is a REST API that searches for funny GIFs on the `Giphy <https://giphy.com/>`_.
|
||||
|
||||
Run
|
||||
---
|
||||
|
@ -106,12 +108,11 @@ The output should be something like:
|
|||
Name Stmts Miss Cover
|
||||
---------------------------------------------------
|
||||
giphynavigator/__init__.py 0 0 100%
|
||||
giphynavigator/__main__.py 5 5 0%
|
||||
giphynavigator/application.py 10 0 100%
|
||||
giphynavigator/containers.py 10 0 100%
|
||||
giphynavigator/application.py 12 0 100%
|
||||
giphynavigator/containers.py 6 0 100%
|
||||
giphynavigator/giphy.py 14 9 36%
|
||||
giphynavigator/handlers.py 9 0 100%
|
||||
giphynavigator/services.py 9 1 89%
|
||||
giphynavigator/tests.py 35 0 100%
|
||||
giphynavigator/views.py 7 0 100%
|
||||
giphynavigator/tests.py 37 0 100%
|
||||
---------------------------------------------------
|
||||
TOTAL 90 15 83%
|
||||
TOTAL 87 10 89%
|
5
examples/miniapps/aiohttp/config.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
giphy:
|
||||
request_timeout: 10
|
||||
default:
|
||||
query: "Dependency Injector"
|
||||
limit: 10
|
20
examples/miniapps/aiohttp/giphynavigator/application.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
"""Application module."""
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from .containers import Container
|
||||
from . import handlers
|
||||
|
||||
|
||||
def create_app() -> web.Application:
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
container.config.giphy.api_key.from_env('GIPHY_API_KEY')
|
||||
container.wire(modules=[handlers])
|
||||
|
||||
app = web.Application()
|
||||
app.container = container
|
||||
app.add_routes([
|
||||
web.get('/', handlers.index),
|
||||
])
|
||||
return app
|
21
examples/miniapps/aiohttp/giphynavigator/containers.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
"""Containers module."""
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
|
||||
from . import giphy, services
|
||||
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
giphy_client = providers.Factory(
|
||||
giphy.GiphyClient,
|
||||
api_key=config.giphy.api_key,
|
||||
timeout=config.giphy.request_timeout,
|
||||
)
|
||||
|
||||
search_service = providers.Factory(
|
||||
services.SearchService,
|
||||
giphy_client=giphy_client,
|
||||
)
|
|
@ -1,15 +1,17 @@
|
|||
"""Views module."""
|
||||
"""Handlers module."""
|
||||
|
||||
from aiohttp import web
|
||||
from dependency_injector.wiring import Provide
|
||||
|
||||
from .services import SearchService
|
||||
from .containers import Container
|
||||
|
||||
|
||||
async def index(
|
||||
request: web.Request,
|
||||
search_service: SearchService,
|
||||
default_query: str,
|
||||
default_limit: int,
|
||||
search_service: SearchService = Provide[Container.search_service],
|
||||
default_query: str = Provide[Container.config.default.query],
|
||||
default_limit: int = Provide[Container.config.default.limit.as_int()],
|
||||
) -> web.Response:
|
||||
query = request.query.get('query', default_query)
|
||||
limit = int(request.query.get('limit', default_limit))
|
|
@ -10,7 +10,9 @@ from giphynavigator.giphy import GiphyClient
|
|||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
return create_app()
|
||||
app = create_app()
|
||||
yield app
|
||||
app.container.unwire()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -73,5 +75,5 @@ async def test_index_default_params(client, app):
|
|||
|
||||
assert response.status == 200
|
||||
data = await response.json()
|
||||
assert data['query'] == app.container.config.search.default_query()
|
||||
assert data['limit'] == app.container.config.search.default_limit()
|
||||
assert data['query'] == app.container.config.default.query()
|
||||
assert data['limit'] == app.container.config.default.limit()
|
|
@ -24,6 +24,6 @@ You should see:
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
[2020-09-04 16:06:00,750] [DEBUG] [example.services.UserService]: User user@example.com has been found in database
|
||||
[2020-09-04 16:06:00,750] [DEBUG] [example.services.AuthService]: User user@example.com has been successfully authenticated
|
||||
[2020-09-04 16:06:00,750] [DEBUG] [example.services.PhotoService]: Photo photo.jpg has been successfully uploaded by user user@example.com
|
||||
[2020-10-06 15:36:55,961] [DEBUG] [example.services.UserService]: User user@example.com has been found in database
|
||||
[2020-10-06 15:36:55,961] [DEBUG] [example.services.AuthService]: User user@example.com has been successfully authenticated
|
||||
[2020-10-06 15:36:55,961] [DEBUG] [example.services.PhotoService]: Photo photo.jpg has been successfully uploaded by user user@example.com
|
||||
|
|
|
@ -2,23 +2,29 @@
|
|||
|
||||
import sys
|
||||
|
||||
from dependency_injector.wiring import Provide
|
||||
|
||||
from .services import UserService, AuthService, PhotoService
|
||||
from .containers import Application
|
||||
|
||||
|
||||
def main(email: str, password: str, photo: str) -> None:
|
||||
application = Application()
|
||||
|
||||
application.config.from_yaml('config.yml')
|
||||
application.core.configure_logging()
|
||||
|
||||
user_service = application.services.user()
|
||||
auth_service = application.services.auth()
|
||||
photo_service = application.services.photo()
|
||||
|
||||
def main(
|
||||
email: str,
|
||||
password: str,
|
||||
photo: str,
|
||||
user_service: UserService = Provide[Application.services.user],
|
||||
auth_service: AuthService = Provide[Application.services.auth],
|
||||
photo_service: PhotoService = Provide[Application.services.photo],
|
||||
) -> None:
|
||||
user = user_service.get_user(email)
|
||||
auth_service.authenticate(user, password)
|
||||
photo_service.upload_photo(user, photo)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
application = Application()
|
||||
application.config.from_yaml('config.yml')
|
||||
application.core.configure_logging()
|
||||
application.wire(modules=[sys.modules[__name__]])
|
||||
|
||||
main(*sys.argv[1:])
|
||||
|
|
|
@ -24,6 +24,6 @@ You should see:
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
[2020-09-04 15:27:27,727] [DEBUG] [example.services.UserService]: User user@example.com has been found in database
|
||||
[2020-09-04 15:27:27,727] [DEBUG] [example.services.AuthService]: User user@example.com has been successfully authenticated
|
||||
[2020-09-04 15:27:27,727] [DEBUG] [example.services.PhotoService]: Photo photo.jpg has been successfully uploaded by user user@example.com
|
||||
[2020-10-06 15:32:33,195] [DEBUG] [example.services.UserService]: User user@example.com has been found in database
|
||||
[2020-10-06 15:32:33,195] [DEBUG] [example.services.AuthService]: User user@example.com has been successfully authenticated
|
||||
[2020-10-06 15:32:33,195] [DEBUG] [example.services.PhotoService]: Photo photo.jpg has been successfully uploaded by user user@example.com
|
||||
|
|
|
@ -2,23 +2,29 @@
|
|||
|
||||
import sys
|
||||
|
||||
from dependency_injector.wiring import Provide
|
||||
|
||||
from .services import UserService, AuthService, PhotoService
|
||||
from .containers import Container
|
||||
|
||||
|
||||
def main(email: str, password: str, photo: str) -> None:
|
||||
container = Container()
|
||||
|
||||
container.configure_logging()
|
||||
container.config.from_ini('config.ini')
|
||||
|
||||
user_service = container.user_service()
|
||||
auth_service = container.auth_service()
|
||||
photo_service = container.photo_service()
|
||||
|
||||
def main(
|
||||
email: str,
|
||||
password: str,
|
||||
photo: str,
|
||||
user_service: UserService = Provide[Container.user_service],
|
||||
auth_service: AuthService = Provide[Container.auth_service],
|
||||
photo_service: PhotoService = Provide[Container.photo_service],
|
||||
) -> None:
|
||||
user = user_service.get_user(email)
|
||||
auth_service.authenticate(user, password)
|
||||
photo_service.upload_photo(user, photo)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
container = Container()
|
||||
container.configure_logging()
|
||||
container.config.from_ini('config.ini')
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
|
||||
main(*sys.argv[1:])
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
Asyncio Daemon Dependency Injection Example
|
||||
===========================================
|
||||
Asyncio Daemon + Dependency Injector Example
|
||||
============================================
|
||||
|
||||
Application ``monitoringdaemon`` is an `asyncio <https://docs.python.org/3/library/asyncio.html>`_
|
||||
+ `Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ application.
|
||||
This is an `asyncio <https://docs.python.org/3/library/asyncio.html>`_ +
|
||||
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ example application.
|
||||
|
||||
The example application is a daemon that monitors availability of web services.
|
||||
|
||||
Run
|
||||
---
|
||||
|
@ -31,19 +33,16 @@ The output should be something like:
|
|||
monitor_1 | response code: 200
|
||||
monitor_1 | content length: 648
|
||||
monitor_1 | request took: 0.074 seconds
|
||||
monitor_1 |
|
||||
monitor_1 | [2020-08-08 17:04:36,811] [INFO] [HttpMonitor]: Check
|
||||
monitor_1 | GET https://httpbin.org/get
|
||||
monitor_1 | response code: 200
|
||||
monitor_1 | content length: 310
|
||||
monitor_1 | request took: 0.153 seconds
|
||||
monitor_1 |
|
||||
monitor_1 | [2020-08-08 17:04:41,731] [INFO] [HttpMonitor]: Check
|
||||
monitor_1 | GET http://example.com
|
||||
monitor_1 | response code: 200
|
||||
monitor_1 | content length: 648
|
||||
monitor_1 | request took: 0.067 seconds
|
||||
monitor_1 |
|
||||
monitor_1 | [2020-08-08 17:04:41,787] [INFO] [HttpMonitor]: Check
|
||||
monitor_1 | GET https://httpbin.org/get
|
||||
monitor_1 | response code: 200
|
||||
|
@ -71,17 +70,17 @@ The output should be something like:
|
|||
plugins: asyncio-0.14.0, cov-2.10.0
|
||||
collected 2 items
|
||||
|
||||
monitoringdaemon/tests.py .. [100%]
|
||||
monitoringdaemon/tests.py .. [100%]
|
||||
|
||||
----------- coverage: platform linux, python 3.8.3-final-0 -----------
|
||||
Name Stmts Miss Cover
|
||||
----------------------------------------------------
|
||||
monitoringdaemon/__init__.py 0 0 100%
|
||||
monitoringdaemon/__main__.py 9 9 0%
|
||||
monitoringdaemon/__main__.py 12 12 0%
|
||||
monitoringdaemon/containers.py 11 0 100%
|
||||
monitoringdaemon/dispatcher.py 43 5 88%
|
||||
monitoringdaemon/dispatcher.py 44 5 89%
|
||||
monitoringdaemon/http.py 6 3 50%
|
||||
monitoringdaemon/monitors.py 23 1 96%
|
||||
monitoringdaemon/tests.py 37 0 100%
|
||||
----------------------------------------------------
|
||||
TOTAL 129 18 86%
|
||||
TOTAL 133 21 84%
|
|
@ -0,0 +1,21 @@
|
|||
"""Main module."""
|
||||
|
||||
import sys
|
||||
|
||||
from dependency_injector.wiring import Provide
|
||||
|
||||
from .dispatcher import Dispatcher
|
||||
from .containers import Container
|
||||
|
||||
|
||||
def main(dispatcher: Dispatcher = Provide[Container.dispatcher]) -> None:
|
||||
dispatcher.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
container.configure_logging()
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
|
||||
main()
|
|
@ -1,4 +1,4 @@
|
|||
"""Application containers module."""
|
||||
"""Containers module."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
@ -8,8 +8,7 @@ from dependency_injector import containers, providers
|
|||
from . import http, monitors, dispatcher
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
|
|
@ -44,6 +44,7 @@ class Dispatcher:
|
|||
self._logger.info('Shutting down')
|
||||
for task, monitor in zip(self._monitor_tasks, self._monitors):
|
||||
task.cancel()
|
||||
self._monitor_tasks.clear()
|
||||
self._logger.info('Shutdown finished successfully')
|
||||
|
||||
@staticmethod
|
|
@ -47,7 +47,7 @@ class HttpMonitor(Monitor):
|
|||
' %s %s\n'
|
||||
' response code: %s\n'
|
||||
' content length: %s\n'
|
||||
' request took: %s seconds\n',
|
||||
' request took: %s seconds',
|
||||
self._method,
|
||||
self._url,
|
||||
response.status,
|
|
@ -6,7 +6,7 @@ from unittest import mock
|
|||
|
||||
import pytest
|
||||
|
||||
from .containers import ApplicationContainer
|
||||
from .containers import Container
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
|
@ -17,7 +17,7 @@ class RequestStub:
|
|||
|
||||
@pytest.fixture
|
||||
def container():
|
||||
container = ApplicationContainer()
|
||||
container = Container()
|
||||
container.config.from_dict({
|
||||
'log': {
|
||||
'level': 'INFO',
|
|
@ -1,15 +1,26 @@
|
|||
"""Main module."""
|
||||
|
||||
import sys
|
||||
|
||||
from dependency_injector.wiring import Provide
|
||||
|
||||
from .user.repositories import UserRepository
|
||||
from .photo.repositories import PhotoRepository
|
||||
from .analytics.services import AggregationService
|
||||
from .containers import ApplicationContainer
|
||||
|
||||
|
||||
def main() -> None:
|
||||
application = ApplicationContainer()
|
||||
application.config.from_ini('config.ini')
|
||||
|
||||
user_repository = application.user_package.user_repository()
|
||||
photo_repository = application.photo_package.photo_repository()
|
||||
|
||||
def main(
|
||||
user_repository: UserRepository = Provide[
|
||||
ApplicationContainer.user_package.user_repository
|
||||
],
|
||||
photo_repository: PhotoRepository = Provide[
|
||||
ApplicationContainer.photo_package.photo_repository
|
||||
],
|
||||
aggregation_service: AggregationService = Provide[
|
||||
ApplicationContainer.analytics_package.aggregation_service
|
||||
],
|
||||
) -> None:
|
||||
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)}')
|
||||
|
@ -18,11 +29,14 @@ def main() -> None:
|
|||
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')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
application = ApplicationContainer()
|
||||
application.config.from_ini('config.ini')
|
||||
application.wire(modules=[sys.modules[__name__]])
|
||||
|
||||
main()
|
||||
|
|
1
examples/miniapps/django/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.sqlite3
|
2
examples/miniapps/django/.pydocstylerc
Normal file
|
@ -0,0 +1,2 @@
|
|||
[pydocstyle]
|
||||
ignore = D100,D101,D102,D103,D105,D107,D203,D212,D213,D400,D406,D407,D411,D413,D415
|
113
examples/miniapps/django/README.rst
Normal file
|
@ -0,0 +1,113 @@
|
|||
Django + Dependency Injector Example
|
||||
====================================
|
||||
|
||||
This is a `Django <https://www.djangoproject.com/>`_ +
|
||||
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ example application.
|
||||
|
||||
The example application helps to search for repositories on the Github.
|
||||
|
||||
.. image:: screenshot.png
|
||||
|
||||
Run
|
||||
---
|
||||
|
||||
Create virtual environment:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
virtualenv venv
|
||||
. venv/bin/activate
|
||||
|
||||
Install requirements:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
Run migrations:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python manage.py migrate
|
||||
|
||||
To run the application do:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python manage.py runserver
|
||||
|
||||
The output should be something like:
|
||||
|
||||
.. code-block::
|
||||
|
||||
Watching for file changes with StatReloader
|
||||
Performing system checks...
|
||||
|
||||
System check identified no issues (0 silenced).
|
||||
October 05, 2020 - 03:17:05
|
||||
Django version 3.1.2, using settings 'githubnavigator.settings'
|
||||
Starting development server at http://127.0.0.1:8000/
|
||||
Quit the server with CONTROL-C.
|
||||
|
||||
After that visit http://127.0.0.1:8000/ in your browser.
|
||||
|
||||
.. note::
|
||||
|
||||
Github has a rate limit. When the rate limit is exceed you will see an exception
|
||||
``github.GithubException.RateLimitExceededException``. For unauthenticated requests, the rate
|
||||
limit allows for up to 60 requests per hour. To extend the limit to 5000 requests per hour you
|
||||
need to set personal access token.
|
||||
|
||||
It's easy:
|
||||
|
||||
- Follow this `guide <https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token>`_ to create a token.
|
||||
- Set a token to the environment variable:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
export GITHUB_TOKEN=<your token>
|
||||
|
||||
- Restart the app with ``python manage.py runserver``
|
||||
|
||||
`Read more on Github rate limit <https://developer.github.com/v3/#rate-limiting>`_
|
||||
|
||||
Test
|
||||
----
|
||||
|
||||
This application comes with the unit tests.
|
||||
|
||||
To run the tests do:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
coverage run --source='.' manage.py test && coverage report
|
||||
|
||||
The output should be something like:
|
||||
|
||||
.. code-block::
|
||||
|
||||
Creating test database for alias 'default'...
|
||||
System check identified no issues (0 silenced).
|
||||
..
|
||||
----------------------------------------------------------------------
|
||||
Ran 2 tests in 0.037s
|
||||
|
||||
OK
|
||||
Destroying test database for alias 'default'...
|
||||
Name Stmts Miss Cover
|
||||
---------------------------------------------------
|
||||
githubnavigator/__init__.py 4 0 100%
|
||||
githubnavigator/asgi.py 4 4 0%
|
||||
githubnavigator/containers.py 7 0 100%
|
||||
githubnavigator/services.py 14 0 100%
|
||||
githubnavigator/settings.py 23 0 100%
|
||||
githubnavigator/urls.py 3 0 100%
|
||||
githubnavigator/wsgi.py 4 4 0%
|
||||
manage.py 12 2 83%
|
||||
web/__init__.py 0 0 100%
|
||||
web/apps.py 7 0 100%
|
||||
web/tests.py 28 0 100%
|
||||
web/urls.py 3 0 100%
|
||||
web/views.py 11 0 100%
|
||||
---------------------------------------------------
|
||||
TOTAL 120 10 92%
|
8
examples/miniapps/django/githubnavigator/__init__.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
"""Project package."""
|
||||
|
||||
from .containers import Container
|
||||
from . import settings
|
||||
|
||||
|
||||
container = Container()
|
||||
container.config.from_dict(settings.__dict__)
|
15
examples/miniapps/django/githubnavigator/asgi.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
"""ASGI config for githubnavigator project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'githubnavigator.settings')
|
||||
|
||||
application = get_asgi_application()
|
22
examples/miniapps/django/githubnavigator/containers.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
"""Containers module."""
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from github import Github
|
||||
|
||||
from . import services
|
||||
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
github_client = providers.Factory(
|
||||
Github,
|
||||
login_or_token=config.GITHUB_TOKEN,
|
||||
timeout=config.GITHUB_REQUEST_TIMEOUT,
|
||||
)
|
||||
|
||||
search_service = providers.Factory(
|
||||
services.SearchService,
|
||||
github_client=github_client,
|
||||
)
|
131
examples/miniapps/django/githubnavigator/settings.py
Normal file
|
@ -0,0 +1,131 @@
|
|||
"""
|
||||
Django settings for githubnavigator project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 3.0.8.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.0/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = ')6*iyg26c9l!fvyvwd&3+vyf-dcw)e=5x2t(j)(*c29z@ykhi0'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'web.apps.WebConfig',
|
||||
'bootstrap4',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'githubnavigator.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'githubnavigator.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/3.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/3.0/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
# Github client settings
|
||||
GITHUB_TOKEN = os.getenv('GITHUB_TOKEN')
|
||||
GITHUB_REQUEST_TIMEOUT = 10
|
||||
|
||||
# Search settings
|
||||
DEFAULT_LIMIT = 5
|
||||
DEFAULT_QUERY = 'Dependency Injector'
|
||||
LIMIT_OPTIONS = [5, 10, 20]
|
22
examples/miniapps/django/githubnavigator/urls.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
"""githubnavigator URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/3.0/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('', include('web.urls')),
|
||||
path('admin/', admin.site.urls),
|
||||
]
|
16
examples/miniapps/django/githubnavigator/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for githubnavigator project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'githubnavigator.settings')
|
||||
|
||||
application = get_wsgi_application()
|
21
examples/miniapps/django/manage.py
Executable file
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'githubnavigator.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
5
examples/miniapps/django/requirements.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
dependency-injector
|
||||
django
|
||||
django-bootstrap4
|
||||
pygithub
|
||||
coverage
|
BIN
examples/miniapps/django/screenshot.png
Normal file
After Width: | Height: | Size: 382 KiB |
1
examples/miniapps/django/web/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Web application package."""
|
13
examples/miniapps/django/web/apps.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
"""Application config module."""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
from githubnavigator import container
|
||||
from . import views
|
||||
|
||||
|
||||
class WebConfig(AppConfig):
|
||||
name = 'web'
|
||||
|
||||
def ready(self):
|
||||
container.wire(modules=[views])
|
10
examples/miniapps/django/web/templates/base.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
{% extends 'bootstrap4/bootstrap4.html' %}
|
||||
|
||||
{% load bootstrap4 %}
|
||||
|
||||
{% block bootstrap4_title %}{% block title %}{% endblock %}{% endblock %}
|
||||
|
||||
{% block bootstrap4_content %}
|
||||
{% autoescape off %}{% bootstrap_messages %}{% endautoescape %}
|
||||
{% block content %}(no content){% endblock %}
|
||||
{% endblock %}
|
69
examples/miniapps/django/web/templates/index.html
Normal file
|
@ -0,0 +1,69 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Github Navigator{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1 class="mb-4">Github Navigator</h1>
|
||||
|
||||
<form>
|
||||
<div class="form-group form-row">
|
||||
<div class="col-10">
|
||||
<label for="search_query" class="col-form-label">
|
||||
Search for:
|
||||
</label>
|
||||
<input class="form-control" type="text" id="search_query"
|
||||
placeholder="Type something to search on the GitHub"
|
||||
name="query"
|
||||
value="{{ query }}">
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="search_limit" class="col-form-label">
|
||||
Limit:
|
||||
</label>
|
||||
<select class="form-control" id="search_limit" name="limit">
|
||||
{% for value in limit_options %}
|
||||
<option {% if value == limit %}selected{% endif %}>
|
||||
{{ value }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p><small>Results found: {{ repositories|length }}</small></p>
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Repository</th>
|
||||
<th class="text-nowrap">Repository owner</th>
|
||||
<th class="text-nowrap">Last commit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for repository in repositories %} {{n}}
|
||||
<tr>
|
||||
<th>{{ loop.index }}</th>
|
||||
<td><a href="{{ repository.url }}">
|
||||
{{ repository.name }}</a>
|
||||
</td>
|
||||
<td><a href="{{ repository.owner.url }}">
|
||||
<img src="{{ repository.owner.avatar_url }}"
|
||||
alt="avatar" height="24" width="24"/></a>
|
||||
<a href="{{ repository.owner.url }}">
|
||||
{{ repository.owner.login }}</a>
|
||||
</td>
|
||||
<td><a href="{{ repository.latest_commit.url }}">
|
||||
{{ repository.latest_commit.sha }}</a>
|
||||
{{ repository.latest_commit.message }}
|
||||
{{ repository.latest_commit.author_name }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
63
examples/miniapps/django/web/tests.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
"""Tests module."""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from django.urls import reverse
|
||||
from django.test import TestCase
|
||||
from github import Github
|
||||
|
||||
from githubnavigator import container
|
||||
|
||||
|
||||
class IndexTests(TestCase):
|
||||
|
||||
def test_index(self):
|
||||
github_client_mock = mock.Mock(spec=Github)
|
||||
github_client_mock.search_repositories.return_value = [
|
||||
mock.Mock(
|
||||
html_url='repo1-url',
|
||||
name='repo1-name',
|
||||
owner=mock.Mock(
|
||||
login='owner1-login',
|
||||
html_url='owner1-url',
|
||||
avatar_url='owner1-avatar-url',
|
||||
),
|
||||
get_commits=mock.Mock(return_value=[mock.Mock()]),
|
||||
),
|
||||
mock.Mock(
|
||||
html_url='repo2-url',
|
||||
name='repo2-name',
|
||||
owner=mock.Mock(
|
||||
login='owner2-login',
|
||||
html_url='owner2-url',
|
||||
avatar_url='owner2-avatar-url',
|
||||
),
|
||||
get_commits=mock.Mock(return_value=[mock.Mock()]),
|
||||
),
|
||||
]
|
||||
|
||||
with container.github_client.override(github_client_mock):
|
||||
response = self.client.get(reverse('index'))
|
||||
|
||||
self.assertContains(response, 'Results found: 2')
|
||||
|
||||
self.assertContains(response, 'repo1-url')
|
||||
self.assertContains(response, 'repo1-name')
|
||||
self.assertContains(response, 'owner1-login')
|
||||
self.assertContains(response, 'owner1-url')
|
||||
self.assertContains(response, 'owner1-avatar-url')
|
||||
|
||||
self.assertContains(response, 'repo2-url')
|
||||
self.assertContains(response, 'repo2-name')
|
||||
self.assertContains(response, 'owner2-login')
|
||||
self.assertContains(response, 'owner2-url')
|
||||
self.assertContains(response, 'owner2-avatar-url')
|
||||
|
||||
def test_index_no_results(self):
|
||||
github_client_mock = mock.Mock(spec=Github)
|
||||
github_client_mock.search_repositories.return_value = []
|
||||
|
||||
with container.github_client.override(github_client_mock):
|
||||
response = self.client.get(reverse('index'))
|
||||
|
||||
self.assertContains(response, 'Results found: 0')
|
9
examples/miniapps/django/web/urls.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
"""URLs module."""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
]
|
34
examples/miniapps/django/web/views.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
"""Views module."""
|
||||
|
||||
from typing import List
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render
|
||||
from dependency_injector.wiring import Provide
|
||||
|
||||
from githubnavigator.containers import Container
|
||||
from githubnavigator.services import SearchService
|
||||
|
||||
|
||||
def index(
|
||||
request: HttpRequest,
|
||||
search_service: SearchService = Provide[Container.search_service],
|
||||
default_query: str = Provide[Container.config.DEFAULT_QUERY],
|
||||
default_limit: int = Provide[Container.config.DEFAULT_LIMIT.as_int()],
|
||||
limit_options: List[int] = Provide[Container.config.LIMIT_OPTIONS],
|
||||
) -> HttpResponse:
|
||||
query = request.GET.get('query', default_query)
|
||||
limit = int(request.GET.get('limit', default_limit))
|
||||
|
||||
repositories = search_service.search_repositories(query, limit)
|
||||
|
||||
return render(
|
||||
request,
|
||||
template_name='index.html',
|
||||
context={
|
||||
'query': query,
|
||||
'limit': limit,
|
||||
'limit_options': limit_options,
|
||||
'repositories': repositories,
|
||||
}
|
||||
)
|
|
@ -1,8 +1,10 @@
|
|||
Flask Dependency Injection Example
|
||||
==================================
|
||||
Flask + Dependency Injector Example
|
||||
===================================
|
||||
|
||||
Application ``githubnavigator`` is a `Flask <https://flask.palletsprojects.com/>`_ +
|
||||
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ application.
|
||||
This is a `Flask <https://flask.palletsprojects.com/>`_ +
|
||||
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ example application.
|
||||
|
||||
The example application helps to search for repositories on the Github.
|
||||
|
||||
.. image:: screenshot.png
|
||||
|
||||
|
@ -46,8 +48,7 @@ After that visit http://127.0.0.1:5000/ in your browser.
|
|||
|
||||
.. note::
|
||||
|
||||
|
||||
Github has a rate limit. When thre rate limit is exceed you will see an exception
|
||||
Github has a rate limit. When the rate limit is exceed you will see an exception
|
||||
``github.GithubException.RateLimitExceededException``. For unauthenticated requests, the rate
|
||||
limit allows for up to 60 requests per hour. To extend the limit to 5000 requests per hour you
|
||||
need to set personal access token.
|
||||
|
@ -90,10 +91,10 @@ The output should be something like:
|
|||
Name Stmts Miss Cover
|
||||
----------------------------------------------------
|
||||
githubnavigator/__init__.py 0 0 100%
|
||||
githubnavigator/application.py 11 0 100%
|
||||
githubnavigator/containers.py 13 0 100%
|
||||
githubnavigator/application.py 15 0 100%
|
||||
githubnavigator/containers.py 7 0 100%
|
||||
githubnavigator/services.py 14 0 100%
|
||||
githubnavigator/tests.py 32 0 100%
|
||||
githubnavigator/views.py 7 0 100%
|
||||
githubnavigator/tests.py 34 0 100%
|
||||
githubnavigator/views.py 9 0 100%
|
||||
----------------------------------------------------
|
||||
TOTAL 77 0 100%
|
||||
TOTAL 79 0 100%
|
5
examples/miniapps/flask/config.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
github:
|
||||
request_timeout: 10
|
||||
default:
|
||||
query: "Dependency Injector"
|
||||
limit: 10
|
23
examples/miniapps/flask/githubnavigator/application.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
"""Application module."""
|
||||
|
||||
from flask import Flask
|
||||
from flask_bootstrap import Bootstrap
|
||||
|
||||
from .containers import Container
|
||||
from . import views
|
||||
|
||||
|
||||
def create_app() -> Flask:
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
container.config.github.auth_token.from_env('GITHUB_TOKEN')
|
||||
container.wire(modules=[views])
|
||||
|
||||
app = Flask(__name__)
|
||||
app.container = container
|
||||
app.add_url_rule('/', 'index', views.index)
|
||||
|
||||
bootstrap = Bootstrap()
|
||||
bootstrap.init_app(app)
|
||||
|
||||
return app
|
22
examples/miniapps/flask/githubnavigator/containers.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
"""Containers module."""
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from github import Github
|
||||
|
||||
from . import services
|
||||
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
github_client = providers.Factory(
|
||||
Github,
|
||||
login_or_token=config.github.auth_token,
|
||||
timeout=config.github.request_timeout,
|
||||
)
|
||||
|
||||
search_service = providers.Factory(
|
||||
services.SearchService,
|
||||
github_client=github_client,
|
||||
)
|
44
examples/miniapps/flask/githubnavigator/services.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
"""Services module."""
|
||||
|
||||
from github import Github
|
||||
from github.Repository import Repository
|
||||
from github.Commit import Commit
|
||||
|
||||
|
||||
class SearchService:
|
||||
"""Search service performs search on Github."""
|
||||
|
||||
def __init__(self, github_client: Github):
|
||||
self._github_client = github_client
|
||||
|
||||
def search_repositories(self, query, limit):
|
||||
"""Search for repositories and return formatted data."""
|
||||
repositories = self._github_client.search_repositories(
|
||||
query=query,
|
||||
**{'in': 'name'},
|
||||
)
|
||||
return [
|
||||
self._format_repo(repository)
|
||||
for repository in repositories[:limit]
|
||||
]
|
||||
|
||||
def _format_repo(self, repository: Repository):
|
||||
commits = repository.get_commits()
|
||||
return {
|
||||
'url': repository.html_url,
|
||||
'name': repository.name,
|
||||
'owner': {
|
||||
'login': repository.owner.login,
|
||||
'url': repository.owner.html_url,
|
||||
'avatar_url': repository.owner.avatar_url,
|
||||
},
|
||||
'latest_commit': self._format_commit(commits[0]) if commits else {},
|
||||
}
|
||||
|
||||
def _format_commit(self, commit: Commit):
|
||||
return {
|
||||
'sha': commit.sha,
|
||||
'url': commit.html_url,
|
||||
'message': commit.commit.message,
|
||||
'author_name': commit.commit.author.name,
|
||||
}
|
|
@ -11,7 +11,9 @@ from .application import create_app
|
|||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
return create_app()
|
||||
app = create_app()
|
||||
yield app
|
||||
app.container.unwire()
|
||||
|
||||
|
||||
def test_index(client, app):
|
|
@ -1,14 +1,16 @@
|
|||
"""Views module."""
|
||||
|
||||
from flask import request, render_template
|
||||
from dependency_injector.wiring import Provide
|
||||
|
||||
from .services import SearchService
|
||||
from .containers import Container
|
||||
|
||||
|
||||
def index(
|
||||
search_service: SearchService,
|
||||
default_query: str,
|
||||
default_limit: int,
|
||||
search_service: SearchService = Provide[Container.search_service],
|
||||
default_query: str = Provide[Container.config.default.query],
|
||||
default_limit: int = Provide[Container.config.default.limit.as_int()],
|
||||
):
|
||||
query = request.args.get('query', default_query)
|
||||
limit = request.args.get('limit', default_limit, int)
|
BIN
examples/miniapps/flask/screenshot.png
Normal file
After Width: | Height: | Size: 647 KiB |
|
@ -1,5 +0,0 @@
|
|||
github:
|
||||
request_timeout: 10
|
||||
search:
|
||||
default_query: "Dependency Injector"
|
||||
default_limit: 10
|
|
@ -1,20 +0,0 @@
|
|||
"""Application module."""
|
||||
|
||||
from .containers import ApplicationContainer
|
||||
|
||||
|
||||
def create_app():
|
||||
"""Create and return Flask application."""
|
||||
container = ApplicationContainer()
|
||||
container.config.from_yaml('config.yml')
|
||||
container.config.github.auth_token.from_env('GITHUB_TOKEN')
|
||||
|
||||
app = container.app()
|
||||
app.container = container
|
||||
|
||||
bootstrap = container.bootstrap()
|
||||
bootstrap.init_app(app)
|
||||
|
||||
app.add_url_rule('/', view_func=container.index_view.as_view())
|
||||
|
||||
return app
|
|
@ -1,37 +0,0 @@
|
|||
"""Application containers module."""
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.ext import flask
|
||||
from flask import Flask
|
||||
from flask_bootstrap import Bootstrap
|
||||
from github import Github
|
||||
|
||||
from . import views, services
|
||||
|
||||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
|
||||
app = flask.Application(Flask, __name__)
|
||||
|
||||
bootstrap = flask.Extension(Bootstrap)
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
github_client = providers.Factory(
|
||||
Github,
|
||||
login_or_token=config.github.auth_token,
|
||||
timeout=config.github.request_timeout,
|
||||
)
|
||||
|
||||
search_service = providers.Factory(
|
||||
services.SearchService,
|
||||
github_client=github_client,
|
||||
)
|
||||
|
||||
index_view = flask.View(
|
||||
views.index,
|
||||
search_service=search_service,
|
||||
default_query=config.search.default_query,
|
||||
default_limit=config.search.default_limit,
|
||||
)
|
|
@ -1,5 +0,0 @@
|
|||
giphy:
|
||||
request_timeout: 10
|
||||
search:
|
||||
default_query: "Dependency Injector"
|
||||
default_limit: 10
|