Develop 4.0 (#298)
* Add wiring (#294) * Add wiring module * Fix code style * Fix package test * Add version fix * Try spike for 3.6 * Try another fix with metaclass * Downsample required version to 3.6 * Introduce concept with annotations * Fix bugs * Add debug message * Add extra tests * Add extra debugging * Update config resolving * Remove 3.6 generic meta fix * Fix Flake8 * Add spike for 3.6 * Add Python 3.6 spike * Add unwire functionality * Add support of corouting functions * Bump version to 4.0 * Updaet demo example * Add pydocstyle ignore for demo * Add flake8 ignore for demo * Update aiohttp example * Update flask example * Rename aiohttp example directory * Rename views module to handlers in aiohttp example * Add sanic example * Remove not needed images * Update demo * Implement wiring for Provide[foo.provider] * Implement Provide[foo.provided.bar.baz.call()] * Make flake8 happy * Wiring refactoring (#296) * Refactor wiring * Add todos to wiring * Implement wiring of config invariant * Implement sub containers wiring + add tests * Add test for wiring config invariant * Add container.unwire() typing stub * Deprecate ext package modules and remove types module * Deprecate provider.delegate() method * Add __all__ for wiring module * Add protection for wiring only declarative container instances * Bump version to 4.0.0a2 * Add wiring docs * Add wiring of class methods * Remove unused import * Add a note on individuals import to wiring docs * Add minor improvement to wiring doc * Update DI in Python page * Update key features * Update README concep and FAQ * Add files via upload * Update README.rst * Update README.rst * Update README.rst * Update docs index page * Update README * Remove API docs for flask and aiohttp ext * Add wiring API docs * Update docs index * Update README * Update readme and docs index * Change wording in README * Django example (#297) * Add rough django example * Remove sqlite db * Add gitignore * Fix flake8 and pydocstyle errors * Add tests * Refactor settings * Move web app to to the root of the project * Add bootstrap 4 * Add doc blocks for web app * Add coverage * Fix typo in flask * Remove not needed newlines * Add screenshot * Update django app naming * Add django example to the docs * Update changelog * Update Aiohttp example * Add sanic example to the docs * Make a little fix in django example docs page * Add flask example to the docs * Add aiohttp example to the docs * Update installation docs page * Fix .delegate() deprecation * Refactor movie lister to use wiring * Make micro cosmetic changes to flask, aiohttp & sanic examples * Refactor single container example to use wiring * Refactor multiple container example to use wiring * Add return type to main() in application examples * Refactor decoupled packages example to use wiring * Refactor code layout for DI demo example * Update wiring feature message * Add more links to the examples * Change code layout in miniapps * Update sanic example * Update miniapp READMEs * Update wiring docs * Refactor part of cli tutorial * Refactor CLI app tutorial * Update test coverage results in movie lister example and tutorial * Make some minor updates to aiohttp and cli tutorials * Refactor flask tutorial * Make cosmetic fix in flask example * Refactor Flask tutorial: Connect to the GitHub * Refactor Flask tutorial: Search service * Refactor Flask tutorial: Inject search service into view * Refactor Flask tutorial: Make some refactoring * Finish flask tutorial refactoring * Update tutorials * Refactor asyncio monitoring daemon example application * Fix tutorial links * Rename asyncio miniapp * Rename tutorial image dirs * Rename api docs tol-level page * Refactor initial sections of asyncio daemon tutorial * Refactor asyncio tutorial till Example.com monitor section * Refactor asyncio tutorial example.com monitor section * Refactor asyncio tutorial httpbin.org monitor tutorial * Refactor tests section of asyncio daemon tutorial * Update conclusion of asyncio daemon tutorial * Rename tutorial images * Make cosmetic update to flask tutorial * Refactor aiohttp tutorial: Minimal application section * Refactor aiohttp tutorial: Giphy API client secion * Refactor aiohttp tutorial secion: Make the search work * Refactor aiohttp tutorial tests section * Refactor aiohttp tutorial conclusion * Upgrade Cython to 0.29.21 * Update changelog * Update demo example * Update wording on index pages * Update changelog * Update code layout for main demo
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>`_.
|
See `Configuration provider <http://python-dependency-injector.ets-labs.org/providers/configuration.html>`_.
|
||||||
- **Containers**. Provides declarative and dynamic containers.
|
- **Containers**. Provides declarative and dynamic containers.
|
||||||
See `Containers <http://python-dependency-injector.ets-labs.org/containers/index.html>`_.
|
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.
|
- **Typing**. Provides typing stubs, ``mypy``-friendly.
|
||||||
See `Typing and mypy <http://python-dependency-injector.ets-labs.org/providers/typing_mypy.html>`_.
|
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.
|
- **Maturity**. Mature and production-ready. Well-tested, documented and supported.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from dependency_injector import containers, providers
|
from dependency_injector import containers, providers
|
||||||
|
from dependency_injector.wiring import Provide
|
||||||
|
|
||||||
|
|
||||||
class Container(containers.DeclarativeContainer):
|
class Container(containers.DeclarativeContainer):
|
||||||
|
@ -93,23 +97,38 @@ Key features of the ``Dependency Injector``:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main(service: Service = Provide[Container.service]):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
container = Container()
|
container = Container()
|
||||||
container.config.api_key.from_env('API_KEY')
|
container.config.api_key.from_env('API_KEY')
|
||||||
container.config.timeout.from_env('TIMEOUT')
|
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**.
|
with container.api_client.override(mock.Mock()):
|
||||||
This place is called **the container**. You use the container to manage all the components of the
|
main() # <-- overridden dependency is injected automatically
|
||||||
application. All the component dependencies are defined explicitly. This provides the control on
|
|
||||||
the application structure. It is **easy to understand and change** it.
|
|
||||||
|
|
||||||
.. 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
|
: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
|
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>`_.
|
`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 (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>`_
|
- `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>`_
|
- `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
|
Tutorials
|
||||||
---------
|
---------
|
||||||
|
@ -147,22 +170,16 @@ Choose one of the following:
|
||||||
Concept
|
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).
|
.. code-block:: plain
|
||||||
- Do no magic to your code.
|
|
||||||
|
|
||||||
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)*.
|
You need to specify how to assemble and where to inject the dependencies explicitly.
|
||||||
- **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.
|
|
||||||
|
|
||||||
``Dependency Injector`` makes a simple contract with you:
|
The power of the framework is in a simplicity.
|
||||||
|
``Dependency Injector`` is a simple tool for the powerful concept.
|
||||||
- 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.
|
|
||||||
|
|
||||||
Frequently asked questions
|
Frequently asked questions
|
||||||
--------------------------
|
--------------------------
|
||||||
|
@ -171,35 +188,17 @@ What is the dependency injection?
|
||||||
- dependency injection is a principle that decreases coupling and increases cohesion
|
- dependency injection is a principle that decreases coupling and increases cohesion
|
||||||
|
|
||||||
Why should I do the dependency injection?
|
Why should I do the dependency injection?
|
||||||
- your code becomes more flexible, testable and clear
|
- your code becomes more flexible, testable and clear 😎
|
||||||
- you have no problems when you need to understand how it works or change it 😎
|
|
||||||
|
|
||||||
How do I start doing the dependency injection?
|
How do I start doing the dependency injection?
|
||||||
- you start writing the code following the dependency injection principle
|
- you start writing the code following the dependency injection principle
|
||||||
- you register all of your application components and their dependencies in the container
|
- you register all of your application components and their dependencies in the container
|
||||||
- when you need a component, you get it from the container
|
- when you need a component, you specify where to inject it or 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
|
|
||||||
|
|
||||||
What price do I pay and what do I get?
|
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 be extra work in the beginning
|
||||||
- it will payoff when project grows or in two weeks 😊 (when you forget what project was about)
|
- it will payoff as the project grows
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Have a question?
|
Have a question?
|
||||||
- Open a `Github Issue <https://github.com/ets-labs/python-dependency-injector/issues>`_
|
- 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::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
top_level
|
top-level
|
||||||
providers
|
providers
|
||||||
containers
|
containers
|
||||||
|
wiring
|
||||||
errors
|
errors
|
||||||
aiohttpext
|
|
||||||
flaskext
|
|
||||||
|
|
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-single-container
|
||||||
application-multiple-containers
|
application-multiple-containers
|
||||||
decoupled-packages
|
decoupled-packages
|
||||||
|
django
|
||||||
|
flask
|
||||||
|
aiohttp
|
||||||
|
sanic
|
||||||
|
|
||||||
.. disqus::
|
.. 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
|
- **Configuration**. Read configuration from ``yaml`` & ``ini`` files, environment variables
|
||||||
and dictionaries. See :ref:`configuration-provider`.
|
and dictionaries. See :ref:`configuration-provider`.
|
||||||
- **Containers**. Provides declarative and dynamic containers. See :ref:`containers`.
|
- **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`.
|
- **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.
|
- **Maturity**. Mature and production-ready. Well-tested, documented and supported.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from dependency_injector import containers, providers
|
from dependency_injector import containers, providers
|
||||||
|
from dependency_injector.wiring import Provide
|
||||||
|
|
||||||
|
|
||||||
class Container(containers.DeclarativeContainer):
|
class Container(containers.DeclarativeContainer):
|
||||||
|
@ -102,23 +105,28 @@ Key features of the ``Dependency Injector``:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main(service: Service = Provide[Container.service]):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
container = Container()
|
container = Container()
|
||||||
container.config.api_key.from_env('API_KEY')
|
container.config.api_key.from_env('API_KEY')
|
||||||
container.config.timeout.from_env('TIMEOUT')
|
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**.
|
with container.api_client.override(mock.Mock()):
|
||||||
This place is called **the container**. You use the container to manage all the components of the
|
main() # <-- overridden dependency is injected automatically
|
||||||
application. All the component dependencies are defined explicitly. This provides the control on
|
|
||||||
the application structure. It is **easy to understand and change** it.
|
|
||||||
|
|
||||||
.. 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
|
: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``.
|
Explore the documentation to know more about the ``Dependency Injector``.
|
||||||
|
|
||||||
.. _contents:
|
.. _contents:
|
||||||
|
@ -134,6 +142,7 @@ Contents
|
||||||
tutorials/index
|
tutorials/index
|
||||||
providers/index
|
providers/index
|
||||||
containers/index
|
containers/index
|
||||||
|
wiring
|
||||||
examples-other/index
|
examples-other/index
|
||||||
api/index
|
api/index
|
||||||
main/feedback
|
main/feedback
|
||||||
|
|
|
@ -67,19 +67,23 @@ Before:
|
||||||
class ApiClient:
|
class ApiClient:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.api_key = os.getenv('API_KEY') # <-- the dependency
|
self.api_key = os.getenv('API_KEY') # <-- dependency
|
||||||
self.timeout = os.getenv('TIMEOUT') # <-- the dependency
|
self.timeout = os.getenv('TIMEOUT') # <-- dependency
|
||||||
|
|
||||||
|
|
||||||
class Service:
|
class Service:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.api_client = ApiClient() # <-- the dependency
|
self.api_client = ApiClient() # <-- dependency
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
service = Service() # <-- dependency
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
service = Service()
|
main()
|
||||||
|
|
||||||
|
|
||||||
After:
|
After:
|
||||||
|
|
||||||
|
@ -91,18 +95,29 @@ After:
|
||||||
class ApiClient:
|
class ApiClient:
|
||||||
|
|
||||||
def __init__(self, api_key: str, timeout: int):
|
def __init__(self, api_key: str, timeout: int):
|
||||||
self.api_key = api_key # <-- the dependency is injected
|
self.api_key = api_key # <-- dependency is injected
|
||||||
self.timeout = timeout # <-- the dependency is injected
|
self.timeout = timeout # <-- dependency is injected
|
||||||
|
|
||||||
|
|
||||||
class Service:
|
class Service:
|
||||||
|
|
||||||
def __init__(self, api_client: ApiClient):
|
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__':
|
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
|
``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.
|
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
|
``Service`` is decoupled from the ``ApiClient``. It does not create it anymore. You can provide a
|
||||||
stub or other compatible object.
|
stub or other compatible object.
|
||||||
|
|
||||||
|
Function ``main()`` is decoupled from ``Service``. It receives it as an argument.
|
||||||
|
|
||||||
Flexibility comes with a price.
|
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.
|
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?
|
What does the Dependency Injector do?
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
||||||
With the dependency injection pattern objects loose the responsibility of assembling the
|
With the dependency injection pattern objects loose the responsibility of assembling
|
||||||
dependencies. The ``Dependency Injector`` absorbs that responsibility.
|
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
|
It provides a container and providers that help you with the objects assembly.
|
||||||
need an object you get it from the container. The rest of the assembly work is done by the
|
When you need an object you place a ``Provide`` marker as a default value of a
|
||||||
framework:
|
function argument. When you call this function framework assembles and injects
|
||||||
|
the dependency.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from dependency_injector import containers, providers
|
from dependency_injector import containers, providers
|
||||||
|
from dependency_injector.wiring import Provide
|
||||||
|
|
||||||
|
|
||||||
class Container(containers.DeclarativeContainer):
|
class Container(containers.DeclarativeContainer):
|
||||||
|
@ -153,36 +181,34 @@ framework:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main(service: Service = Provide[Container.service]):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
container = Container()
|
container = Container()
|
||||||
container.config.api_key.from_env('API_KEY')
|
container.config.api_key.from_env('API_KEY')
|
||||||
container.config.timeout.from_env('TIMEOUT')
|
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::
|
|
||||||
|
|
||||||
service = container.service()
|
|
||||||
|
|
||||||
Objects assembling is consolidated in the container. When you need to make a change you do it in
|
|
||||||
one place.
|
|
||||||
|
|
||||||
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()):
|
with container.api_client.override(mock.Mock()):
|
||||||
service = container.service()
|
main() # <-- overridden dependency is injected automatically
|
||||||
|
|
||||||
|
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.
|
You can override any provider with another provider.
|
||||||
|
|
||||||
It also helps you in configuring project for the different environments: replace an API client
|
It also helps you in configuring project for the different environments: replace an API client
|
||||||
with a stub on the dev or stage.
|
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
|
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-single-container`
|
||||||
- :ref:`application-multiple-containers`
|
- :ref:`application-multiple-containers`
|
||||||
- :ref:`decoupled-packages`
|
- :ref:`decoupled-packages`
|
||||||
|
- :ref:`django-example`
|
||||||
|
- :ref:`flask-example`
|
||||||
|
- :ref:`aiohttp-example`
|
||||||
|
- :ref:`sanic-example`
|
||||||
- Pass the tutorials:
|
- Pass the tutorials:
|
||||||
- :ref:`flask-tutorial`
|
- :ref:`flask-tutorial`
|
||||||
- :ref:`aiohttp-tutorial`
|
- :ref:`aiohttp-tutorial`
|
||||||
|
@ -261,6 +291,7 @@ Choose one of the following as a next step:
|
||||||
- :ref:`cli-tutorial`
|
- :ref:`cli-tutorial`
|
||||||
- Know more about the ``Dependency Injector`` :ref:`key-features`
|
- Know more about the ``Dependency Injector`` :ref:`key-features`
|
||||||
- Know more about the :ref:`providers`
|
- Know more about the :ref:`providers`
|
||||||
|
- Know more about the :ref:`wiring`
|
||||||
- Go to the :ref:`contents`
|
- Go to the :ref:`contents`
|
||||||
|
|
||||||
Useful links
|
Useful links
|
||||||
|
|
|
@ -1,41 +1,42 @@
|
||||||
Installation
|
Installation
|
||||||
============
|
============
|
||||||
|
|
||||||
*Dependency Injector* framework is distributed by PyPi_.
|
``Dependency Injector`` is available on `PyPI <https://pypi.org/project/dependency-injector/>`_.
|
||||||
|
To install latest version you can use ``pip``:
|
||||||
Latest stable version (and all previous versions) of *Dependency Injector*
|
|
||||||
framework can be installed from PyPi_:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
pip install dependency-injector
|
pip install dependency-injector
|
||||||
|
|
||||||
.. note::
|
Some modules of the ``Dependency Injector`` are implemented as C extensions.
|
||||||
Some components of *Dependency Injector* are implemented as C extension types.
|
``Dependency Injector`` is distributed as a pre-compiled wheels. Wheels are
|
||||||
*Dependency Injector* is distributed as an archive with a source code, so
|
available for all supported Python versions on Linux, Windows and MacOS.
|
||||||
C compiler and Python header files are required for the installation.
|
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
|
To verify the installed version:
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
>>> import dependency_injector
|
>>> import dependency_injector
|
||||||
>>> dependency_injector.__version__
|
>>> dependency_injector.__version__
|
||||||
'3.43.0'
|
'4.0.0'
|
||||||
|
|
||||||
.. _PyPi: https://pypi.org/project/dependency-injector/
|
.. note::
|
||||||
.. _GitHub: https://github.com/ets-labs/python-dependency-injector
|
When add ``Dependency Injector`` to the ``requirements.txt`` don't forget to pin version
|
||||||
.. _GitHub releases page: https://github.com/ets-labs/python-dependency-injector/releases
|
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::
|
.. disqus::
|
||||||
|
|
|
@ -19,20 +19,21 @@ Key features of the ``Dependency Injector``:
|
||||||
- **Configuration**. Read configuration from ``yaml`` & ``ini`` files, environment variables
|
- **Configuration**. Read configuration from ``yaml`` & ``ini`` files, environment variables
|
||||||
and dictionaries. See :ref:`configuration-provider`.
|
and dictionaries. See :ref:`configuration-provider`.
|
||||||
- **Containers**. Provides declarative and dynamic containers. See :ref:`containers`.
|
- **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`.
|
- **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.
|
- **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)**.
|
.. code-block:: plain
|
||||||
- **Do not do any magic to your code**.
|
|
||||||
|
|
||||||
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)*.
|
You need to specify how to assemble and where to inject the dependencies explicitly.
|
||||||
- **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.
|
|
||||||
|
|
||||||
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::
|
.. disqus::
|
||||||
|
|
|
@ -7,6 +7,44 @@ that were made in every particular version.
|
||||||
From version 0.7.6 *Dependency Injector* framework strictly
|
From version 0.7.6 *Dependency Injector* framework strictly
|
||||||
follows `Semantic versioning`_
|
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
|
3.44.0
|
||||||
------
|
------
|
||||||
- Add native support of the generics to the providers: ``some_provider = providers.Provider[SomeClass]``.
|
- 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
|
:backlinks: none
|
||||||
|
|
||||||
You can find complete project on the
|
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?
|
What are we going to build?
|
||||||
---------------------------
|
---------------------------
|
||||||
|
@ -88,18 +88,18 @@ Prepare the environment
|
||||||
|
|
||||||
Let's create the environment for the project.
|
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
|
.. code-block:: bash
|
||||||
|
|
||||||
mkdir giphynav-aiohttp-tutorial
|
mkdir giphynav-aiohttp-tutorial
|
||||||
cd 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
|
.. code-block:: bash
|
||||||
|
|
||||||
|
python3 -m venv venv
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
|
|
||||||
Environment is ready and now we're going to create the layout of the project.
|
Environment is ready and now we're going to create the layout of the project.
|
||||||
|
@ -116,7 +116,7 @@ Initial project layout::
|
||||||
│ ├── __init__.py
|
│ ├── __init__.py
|
||||||
│ ├── application.py
|
│ ├── application.py
|
||||||
│ ├── containers.py
|
│ ├── containers.py
|
||||||
│ └── views.py
|
│ └── handlers.py
|
||||||
├── venv/
|
├── venv/
|
||||||
└── requirements.txt
|
└── requirements.txt
|
||||||
|
|
||||||
|
@ -164,14 +164,14 @@ The requirements are setup. Now we will build a minimal application.
|
||||||
Minimal application
|
Minimal application
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
In this section we will build a minimal application. It will have an endpoint that we can call.
|
In this section we will build a minimal application. It will have an endpoint that
|
||||||
The endpoint will answer in the right format and will have no data.
|
will answer our requests in json format. There will be no payload for now.
|
||||||
|
|
||||||
Edit ``views.py``:
|
Edit ``handlers.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
"""Views module."""
|
"""Handlers module."""
|
||||||
|
|
||||||
from aiohttp import web
|
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
|
Now let's create a container. Container will keep all of the application components and their dependencies.
|
||||||
application components and their dependencies. First two providers we need to add are
|
|
||||||
the ``aiohttp`` application provider and the view provider.
|
|
||||||
|
|
||||||
Put next into the ``containers.py``:
|
Edit ``containers.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
"""Application containers module."""
|
"""Containers module."""
|
||||||
|
|
||||||
from dependency_injector import containers
|
from dependency_injector import containers
|
||||||
from dependency_injector.ext import aiohttp
|
|
||||||
from aiohttp import web
|
|
||||||
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationContainer(containers.DeclarativeContainer):
|
class Container(containers.DeclarativeContainer):
|
||||||
"""Application container."""
|
...
|
||||||
|
|
||||||
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)
|
Finally we need to create ``aiohttp`` application factory. It will create and configure container
|
||||||
|
and ``web.Application``. It is traditionally called ``create_app()``.
|
||||||
At the last we need to create the ``aiohttp`` application factory. It is traditionally called
|
We will assign ``index`` handler to handle user requests to the root ``/`` of our web application.
|
||||||
``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.
|
|
||||||
|
|
||||||
Put next into the ``application.py``:
|
Put next into the ``application.py``:
|
||||||
|
|
||||||
|
@ -227,28 +218,20 @@ Put next into the ``application.py``:
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from .containers import ApplicationContainer
|
from .containers import Container
|
||||||
|
from . import handlers
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app() -> web.Application:
|
||||||
"""Create and return aiohttp application."""
|
container = Container()
|
||||||
container = ApplicationContainer()
|
|
||||||
|
|
||||||
app: web.Application = container.app()
|
app = web.Application()
|
||||||
app.container = container
|
app.container = container
|
||||||
|
|
||||||
app.add_routes([
|
app.add_routes([
|
||||||
web.get('/', container.index_view.as_view()),
|
web.get('/', handlers.index),
|
||||||
])
|
])
|
||||||
|
|
||||||
return app
|
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
|
Now we're ready to run our application
|
||||||
|
|
||||||
Do next in the terminal:
|
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 aux server at http://localhost:8001 ◆
|
||||||
[18:52:59] Starting dev server at http://localhost:8000 ●
|
[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
|
.. code-block:: bash
|
||||||
|
|
||||||
|
@ -306,7 +289,7 @@ Create ``giphy.py`` module in the ``giphynavigator`` package:
|
||||||
│ ├── application.py
|
│ ├── application.py
|
||||||
│ ├── containers.py
|
│ ├── containers.py
|
||||||
│ ├── giphy.py
|
│ ├── giphy.py
|
||||||
│ └── views.py
|
│ └── handlers.py
|
||||||
├── venv/
|
├── venv/
|
||||||
└── requirements.txt
|
└── requirements.txt
|
||||||
|
|
||||||
|
@ -351,21 +334,16 @@ providers from the ``dependency_injector.providers`` module:
|
||||||
Edit ``containers.py``:
|
Edit ``containers.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. 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 import containers, providers
|
||||||
from dependency_injector.ext import aiohttp
|
|
||||||
from aiohttp import web
|
|
||||||
|
|
||||||
from . import giphy, views
|
from . import giphy
|
||||||
|
|
||||||
|
|
||||||
class ApplicationContainer(containers.DeclarativeContainer):
|
class Container(containers.DeclarativeContainer):
|
||||||
"""Application container."""
|
|
||||||
|
|
||||||
app = aiohttp.Application(web.Application)
|
|
||||||
|
|
||||||
config = providers.Configuration()
|
config = providers.Configuration()
|
||||||
|
|
||||||
|
@ -375,8 +353,6 @@ Edit ``containers.py``:
|
||||||
timeout=config.giphy.request_timeout,
|
timeout=config.giphy.request_timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
index_view = aiohttp.View(views.index)
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
We have used the configuration value before it was defined. That's the principle how the
|
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
|
│ ├── application.py
|
||||||
│ ├── containers.py
|
│ ├── containers.py
|
||||||
│ ├── giphy.py
|
│ ├── giphy.py
|
||||||
│ └── views.py
|
│ └── handlers.py
|
||||||
├── venv/
|
├── venv/
|
||||||
├── config.yml
|
├── config.yml
|
||||||
└── requirements.txt
|
└── requirements.txt
|
||||||
|
@ -427,24 +403,23 @@ Edit ``application.py``:
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from .containers import ApplicationContainer
|
from .containers import Container
|
||||||
|
from . import handlers
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app() -> web.Application:
|
||||||
"""Create and return aiohttp application."""
|
container = Container()
|
||||||
container = ApplicationContainer()
|
|
||||||
container.config.from_yaml('config.yml')
|
container.config.from_yaml('config.yml')
|
||||||
container.config.giphy.api_key.from_env('GIPHY_API_KEY')
|
container.config.giphy.api_key.from_env('GIPHY_API_KEY')
|
||||||
|
|
||||||
app: web.Application = container.app()
|
app = web.Application()
|
||||||
app.container = container
|
app.container = container
|
||||||
|
|
||||||
app.add_routes([
|
app.add_routes([
|
||||||
web.get('/', container.index_view.as_view()),
|
web.get('/', handlers.index),
|
||||||
])
|
])
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
Now we need to create an API key and set it to the environment variable.
|
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:
|
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:
|
Create ``services.py`` module in the ``giphynavigator`` package:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
:emphasize-lines: 7
|
:emphasize-lines: 8
|
||||||
|
|
||||||
./
|
./
|
||||||
├── giphynavigator/
|
├── giphynavigator/
|
||||||
|
@ -481,9 +456,10 @@ Create ``services.py`` module in the ``giphynavigator`` package:
|
||||||
│ ├── application.py
|
│ ├── application.py
|
||||||
│ ├── containers.py
|
│ ├── containers.py
|
||||||
│ ├── giphy.py
|
│ ├── giphy.py
|
||||||
│ ├── services.py
|
│ ├── handlers.py
|
||||||
│ └── views.py
|
│ └── services.py
|
||||||
├── venv/
|
├── venv/
|
||||||
|
├── config.yml
|
||||||
└── requirements.txt
|
└── requirements.txt
|
||||||
|
|
||||||
and put next into it:
|
and put next into it:
|
||||||
|
@ -509,27 +485,22 @@ and put next into it:
|
||||||
|
|
||||||
return [{'url': gif['url']} for gif in result['data']]
|
return [{'url': gif['url']} for gif in result['data']]
|
||||||
|
|
||||||
The ``SearchService`` has a dependency on the ``GiphyClient``. This dependency will be injected.
|
The ``SearchService`` has a dependency on the ``GiphyClient``. This dependency will be
|
||||||
Let's add ``SearchService`` to the container.
|
injected when we add ``SearchService`` to the container.
|
||||||
|
|
||||||
Edit ``containers.py``:
|
Edit ``containers.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. 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 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):
|
class Container(containers.DeclarativeContainer):
|
||||||
"""Application container."""
|
|
||||||
|
|
||||||
app = aiohttp.Application(web.Application)
|
|
||||||
|
|
||||||
config = providers.Configuration()
|
config = providers.Configuration()
|
||||||
|
|
||||||
|
@ -544,31 +515,31 @@ Edit ``containers.py``:
|
||||||
giphy_client=giphy_client,
|
giphy_client=giphy_client,
|
||||||
)
|
)
|
||||||
|
|
||||||
index_view = aiohttp.View(views.index)
|
The search service is ready. In next section we're going to put it to work.
|
||||||
|
|
||||||
|
|
||||||
The search service is ready. In the next section we're going to make it work.
|
|
||||||
|
|
||||||
Make the search 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
|
.. 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 aiohttp import web
|
||||||
|
from dependency_injector.wiring import Provide
|
||||||
|
|
||||||
from .services import SearchService
|
from .services import SearchService
|
||||||
|
from .containers import Container
|
||||||
|
|
||||||
|
|
||||||
async def index(
|
async def index(
|
||||||
request: web.Request,
|
request: web.Request,
|
||||||
search_service: SearchService,
|
search_service: SearchService = Provide[Container.search_service],
|
||||||
) -> web.Response:
|
) -> web.Response:
|
||||||
query = request.query.get('query', 'Dependency Injector')
|
query = request.query.get('query', 'Dependency Injector')
|
||||||
limit = int(request.query.get('limit', 10))
|
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
|
.. 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 aiohttp import web
|
||||||
|
|
||||||
from . import giphy, services, views
|
from .containers import Container
|
||||||
|
from . import handlers
|
||||||
|
|
||||||
|
|
||||||
class ApplicationContainer(containers.DeclarativeContainer):
|
def create_app() -> web.Application:
|
||||||
"""Application container."""
|
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)
|
app = web.Application()
|
||||||
|
app.container = container
|
||||||
config = providers.Configuration()
|
app.add_routes([
|
||||||
|
web.get('/', handlers.index),
|
||||||
giphy_client = providers.Factory(
|
])
|
||||||
giphy.GiphyClient,
|
return app
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
Make sure the app is running or use:
|
Make sure the app is running or use:
|
||||||
|
|
||||||
|
@ -639,30 +601,30 @@ You should see:
|
||||||
.. code-block:: json
|
.. code-block:: json
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
HTTP/1.1 200 OK
|
||||||
Content-Length: 850
|
Content-Length: 492
|
||||||
Content-Type: application/json; charset=utf-8
|
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
|
Server: Python/3.8 aiohttp/3.6.2
|
||||||
|
|
||||||
{
|
{
|
||||||
"gifs": [
|
"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/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/readingrainbow-teamwork-levar-burton-reading-rainbow-3o7qE1EaTWLQGDSabK"
|
||||||
},
|
}
|
||||||
{
|
|
||||||
"url": "https://giphy.com/gifs/cat-massage-at-work-l46CzMaOlJXAFuO3u"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://giphy.com/gifs/everwhatproductions-fun-christmas-3oxHQCI8tKXoeW4IBq"
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
"limit": 10,
|
"limit": 5,
|
||||||
"query": "wow,it works"
|
"query": "wow,it works"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -673,30 +635,32 @@ The search works!
|
||||||
Make some refactoring
|
Make some refactoring
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
Our ``index`` view has two hardcoded config values:
|
Our ``index`` handler has two hardcoded config values:
|
||||||
|
|
||||||
- Default search query
|
- Default search query
|
||||||
- Default results limit
|
- Default results limit
|
||||||
|
|
||||||
Let's make some refactoring. We will move these values to the config.
|
Let's make some refactoring. We will move these values to the config.
|
||||||
|
|
||||||
Edit ``views.py``:
|
Edit ``handlers.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 11-12,14-15
|
:emphasize-lines: 13-14,16-17
|
||||||
|
|
||||||
"""Views module."""
|
"""Handlers module."""
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
from dependency_injector.wiring import Provide
|
||||||
|
|
||||||
from .services import SearchService
|
from .services import SearchService
|
||||||
|
from .containers import Container
|
||||||
|
|
||||||
|
|
||||||
async def index(
|
async def index(
|
||||||
request: web.Request,
|
request: web.Request,
|
||||||
search_service: SearchService,
|
search_service: SearchService = Provide[Container.search_service],
|
||||||
default_query: str,
|
default_query: str = Provide[Container.config.default.query],
|
||||||
default_limit: int,
|
default_limit: int = Provide[Container.config.default.limit.as_int()],
|
||||||
) -> web.Response:
|
) -> web.Response:
|
||||||
query = request.query.get('query', default_query)
|
query = request.query.get('query', default_query)
|
||||||
limit = int(request.query.get('limit', default_limit))
|
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.
|
Let's update the config.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Edit ``config.yml``:
|
Edit ``config.yml``:
|
||||||
|
|
||||||
|
@ -761,26 +684,21 @@ Edit ``config.yml``:
|
||||||
|
|
||||||
giphy:
|
giphy:
|
||||||
request_timeout: 10
|
request_timeout: 10
|
||||||
search:
|
default:
|
||||||
default_query: "Dependency Injector"
|
query: "Dependency Injector"
|
||||||
default_limit: 10
|
limit: 10
|
||||||
|
|
||||||
The refactoring is done. We've made it cleaner - hardcoded values are now moved to the config.
|
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
|
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/>`_.
|
|
||||||
|
|
||||||
Create ``tests.py`` module in the ``giphynavigator`` package:
|
Create ``tests.py`` module in the ``giphynavigator`` package:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
:emphasize-lines: 8
|
:emphasize-lines: 9
|
||||||
|
|
||||||
./
|
./
|
||||||
├── giphynavigator/
|
├── giphynavigator/
|
||||||
|
@ -788,16 +706,17 @@ Create ``tests.py`` module in the ``giphynavigator`` package:
|
||||||
│ ├── application.py
|
│ ├── application.py
|
||||||
│ ├── containers.py
|
│ ├── containers.py
|
||||||
│ ├── giphy.py
|
│ ├── giphy.py
|
||||||
|
│ ├── handlers.py
|
||||||
│ ├── services.py
|
│ ├── services.py
|
||||||
│ ├── tests.py
|
│ └── tests.py
|
||||||
│ └── views.py
|
|
||||||
├── venv/
|
├── venv/
|
||||||
|
├── config.yml
|
||||||
└── requirements.txt
|
└── requirements.txt
|
||||||
|
|
||||||
and put next into it:
|
and put next into it:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 30,57,71
|
:emphasize-lines: 32,59,73
|
||||||
|
|
||||||
"""Tests module."""
|
"""Tests module."""
|
||||||
|
|
||||||
|
@ -811,7 +730,9 @@ and put next into it:
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def app():
|
def app():
|
||||||
return create_app()
|
app = create_app()
|
||||||
|
yield app
|
||||||
|
app.container.unwire()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -874,8 +795,8 @@ and put next into it:
|
||||||
|
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
data = await response.json()
|
data = await response.json()
|
||||||
assert data['query'] == app.container.config.search.default_query()
|
assert data['query'] == app.container.config.default.query()
|
||||||
assert data['limit'] == app.container.config.search.default_limit()
|
assert data['limit'] == app.container.config.default.limit()
|
||||||
|
|
||||||
Now let's run it and check the coverage:
|
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:
|
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
|
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
|
plugins: cov-2.10.0, aiohttp-0.3.0, asyncio-0.14.0
|
||||||
|
@ -897,15 +818,14 @@ You should see:
|
||||||
Name Stmts Miss Cover
|
Name Stmts Miss Cover
|
||||||
---------------------------------------------------
|
---------------------------------------------------
|
||||||
giphynavigator/__init__.py 0 0 100%
|
giphynavigator/__init__.py 0 0 100%
|
||||||
giphynavigator/__main__.py 5 5 0%
|
giphynavigator/application.py 12 0 100%
|
||||||
giphynavigator/application.py 10 0 100%
|
giphynavigator/containers.py 6 0 100%
|
||||||
giphynavigator/containers.py 10 0 100%
|
|
||||||
giphynavigator/giphy.py 14 9 36%
|
giphynavigator/giphy.py 14 9 36%
|
||||||
|
giphynavigator/handlers.py 9 0 100%
|
||||||
giphynavigator/services.py 9 1 89%
|
giphynavigator/services.py 9 1 89%
|
||||||
giphynavigator/tests.py 35 0 100%
|
giphynavigator/tests.py 37 0 100%
|
||||||
giphynavigator/views.py 7 0 100%
|
|
||||||
---------------------------------------------------
|
---------------------------------------------------
|
||||||
TOTAL 90 15 83%
|
TOTAL 87 10 89%
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
@ -920,45 +840,19 @@ In this tutorial we've built an ``aiohttp`` REST API application following the d
|
||||||
injection principle.
|
injection principle.
|
||||||
We've used the ``Dependency Injector`` as a dependency injection framework.
|
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
|
:ref:`containers` and :ref:`providers` helped to specify how to assemble search service and
|
||||||
when you need to understand or change your application structure. It's easy with the container,
|
giphy client.
|
||||||
cause you have everything defined explicitly in one place:
|
|
||||||
|
|
||||||
.. 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
|
We kept all the dependencies injected explicitly. This will help when you need to add or
|
||||||
from dependency_injector.ext import aiohttp
|
change something in future.
|
||||||
from aiohttp import web
|
|
||||||
|
|
||||||
from . import giphy, services, views
|
You can find complete project on the
|
||||||
|
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/aiohttp>`_.
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
What's next?
|
What's next?
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ Start from the scratch or jump to the section:
|
||||||
:backlinks: none
|
:backlinks: none
|
||||||
|
|
||||||
You can find complete project on the
|
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?
|
What are we going to build?
|
||||||
---------------------------
|
---------------------------
|
||||||
|
@ -42,7 +42,7 @@ response it will log:
|
||||||
- The amount of bytes in the response
|
- The amount of bytes in the response
|
||||||
- The time took to complete the response
|
- The time took to complete the response
|
||||||
|
|
||||||
.. image:: asyncio_images/diagram.png
|
.. image:: asyncio-images/diagram.png
|
||||||
|
|
||||||
Prerequisites
|
Prerequisites
|
||||||
-------------
|
-------------
|
||||||
|
@ -79,8 +79,8 @@ Create the project root folder and set it as a working directory:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
mkdir monitoring-daemon-tutorial
|
mkdir asyncio-daemon-tutorial
|
||||||
cd monitoring-daemon-tutorial
|
cd asyncio-daemon-tutorial
|
||||||
|
|
||||||
Now we need to create the initial project structure. Create the files and folders following next
|
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.
|
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
|
.. code-block:: bash
|
||||||
|
|
||||||
Creating network "monitoring-daemon-tutorial_default" with the default driver
|
Creating network "asyncio-daemon-tutorial_default" with the default driver
|
||||||
Creating monitoring-daemon-tutorial_monitor_1 ... done
|
Creating asyncio-daemon-tutorial_monitor_1 ... done
|
||||||
Attaching to monitoring-daemon-tutorial_monitor_1
|
Attaching to asyncio-daemon-tutorial_monitor_1
|
||||||
monitoring-daemon-tutorial_monitor_1 exited with code 0
|
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``.
|
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
|
.. code-block:: python
|
||||||
|
|
||||||
"""Application containers module."""
|
"""Containers module."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
@ -222,8 +222,7 @@ Put next lines into the ``containers.py`` file:
|
||||||
from dependency_injector import containers, providers
|
from dependency_injector import containers, providers
|
||||||
|
|
||||||
|
|
||||||
class ApplicationContainer(containers.DeclarativeContainer):
|
class Container(containers.DeclarativeContainer):
|
||||||
"""Application container."""
|
|
||||||
|
|
||||||
config = providers.Configuration()
|
config = providers.Configuration()
|
||||||
|
|
||||||
|
@ -261,27 +260,25 @@ Put next lines into the ``__main__.py`` file:
|
||||||
|
|
||||||
"""Main module."""
|
"""Main module."""
|
||||||
|
|
||||||
from .containers import ApplicationContainer
|
from .containers import Container
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""Run the application."""
|
...
|
||||||
container = ApplicationContainer()
|
|
||||||
|
|
||||||
container.config.from_yaml('config.yml')
|
|
||||||
container.configure_logging()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
container = Container()
|
||||||
|
container.config.from_yaml('config.yml')
|
||||||
|
container.configure_logging()
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Container is the first object in the application.
|
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 next section we will create the monitoring
|
||||||
|
|
||||||
Logging and configuration parsing part is done. In the next section we will create the monitoring
|
|
||||||
checks dispatcher.
|
checks dispatcher.
|
||||||
|
|
||||||
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
|
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.
|
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.
|
Let's create dispatcher and the monitor base classes.
|
||||||
|
|
||||||
|
@ -336,7 +333,7 @@ and next into the ``dispatcher.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
""""Dispatcher module."""
|
"""Dispatcher module."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
@ -382,6 +379,7 @@ and next into the ``dispatcher.py``:
|
||||||
self._logger.info('Shutting down')
|
self._logger.info('Shutting down')
|
||||||
for task, monitor in zip(self._monitor_tasks, self._monitors):
|
for task, monitor in zip(self._monitor_tasks, self._monitors):
|
||||||
task.cancel()
|
task.cancel()
|
||||||
|
self._monitor_tasks.clear()
|
||||||
self._logger.info('Shutdown finished successfully')
|
self._logger.info('Shutdown finished successfully')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -407,9 +405,9 @@ Now we need to add the dispatcher to the container.
|
||||||
Edit ``containers.py``:
|
Edit ``containers.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 8,23-28
|
:emphasize-lines: 8,22-27
|
||||||
|
|
||||||
"""Application containers module."""
|
"""Containers module."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
@ -419,8 +417,7 @@ Edit ``containers.py``:
|
||||||
from . import dispatcher
|
from . import dispatcher
|
||||||
|
|
||||||
|
|
||||||
class ApplicationContainer(containers.DeclarativeContainer):
|
class Container(containers.DeclarativeContainer):
|
||||||
"""Application container."""
|
|
||||||
|
|
||||||
config = providers.Configuration()
|
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``:
|
Edit ``__main__.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 13-14
|
:emphasize-lines: 3-7,11-12,19
|
||||||
|
|
||||||
"""Main module."""
|
"""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:
|
def main(dispatcher: Dispatcher = Provide[Container.dispatcher]) -> None:
|
||||||
"""Run the application."""
|
|
||||||
container = ApplicationContainer()
|
|
||||||
|
|
||||||
container.config.from_yaml('config.yml')
|
|
||||||
container.configure_logging()
|
|
||||||
|
|
||||||
dispatcher = container.dispatcher()
|
|
||||||
dispatcher.run()
|
dispatcher.run()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
container = Container()
|
||||||
|
container.config.from_yaml('config.yml')
|
||||||
|
container.configure_logging()
|
||||||
|
container.wire(modules=[sys.modules[__name__]])
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
||||||
Finally let's start the daemon to check that all works.
|
Finally let's start the daemon to check that all works.
|
||||||
|
@ -481,22 +478,22 @@ The output should look like:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
Starting monitoring-daemon-tutorial_monitor_1 ... done
|
Starting asyncio-daemon-tutorial_monitor_1 ... done
|
||||||
Attaching to monitoring-daemon-tutorial_monitor_1
|
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,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]: Shutting down
|
||||||
monitor_1 | [2020-08-08 16:12:35,774] [INFO] [Dispatcher]: Shutdown finished successfully
|
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.
|
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.
|
add first monitoring task.
|
||||||
|
|
||||||
Example.com monitor
|
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>`_.
|
`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
|
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
|
will send the HTTP request to the specified URL. The http request sending will be delegated to
|
||||||
the ``HttpClient``.
|
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:
|
Create ``http.py`` in the ``monitoringdaemon`` package:
|
||||||
|
|
||||||
|
@ -549,9 +546,9 @@ Now we need to add the ``HttpClient`` to the container.
|
||||||
Edit ``containers.py``:
|
Edit ``containers.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 8, 23
|
:emphasize-lines: 8,22
|
||||||
|
|
||||||
"""Application containers module."""
|
"""Containers module."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
@ -561,8 +558,7 @@ Edit ``containers.py``:
|
||||||
from . import http, dispatcher
|
from . import http, dispatcher
|
||||||
|
|
||||||
|
|
||||||
class ApplicationContainer(containers.DeclarativeContainer):
|
class Container(containers.DeclarativeContainer):
|
||||||
"""Application container."""
|
|
||||||
|
|
||||||
config = providers.Configuration()
|
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``:
|
Edit ``monitors.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 4-5,7,20-56
|
:emphasize-lines: 4-7,20-56
|
||||||
|
|
||||||
"""Monitors module."""
|
"""Monitors module."""
|
||||||
|
|
||||||
|
@ -638,7 +634,7 @@ Edit ``monitors.py``:
|
||||||
' %s %s\n'
|
' %s %s\n'
|
||||||
' response code: %s\n'
|
' response code: %s\n'
|
||||||
' content length: %s\n'
|
' content length: %s\n'
|
||||||
' request took: %s seconds\n',
|
' request took: %s seconds',
|
||||||
self._method,
|
self._method,
|
||||||
self._url,
|
self._url,
|
||||||
response.status,
|
response.status,
|
||||||
|
@ -655,9 +651,9 @@ We make two changes in the container:
|
||||||
Edit ``containers.py``:
|
Edit ``containers.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 8,25-29,34
|
:emphasize-lines: 8,24-28,33
|
||||||
|
|
||||||
"""Application containers module."""
|
"""Containers module."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
@ -667,8 +663,7 @@ Edit ``containers.py``:
|
||||||
from . import http, monitors, dispatcher
|
from . import http, monitors, dispatcher
|
||||||
|
|
||||||
|
|
||||||
class ApplicationContainer(containers.DeclarativeContainer):
|
class Container(containers.DeclarativeContainer):
|
||||||
"""Application container."""
|
|
||||||
|
|
||||||
config = providers.Configuration()
|
config = providers.Configuration()
|
||||||
|
|
||||||
|
@ -727,15 +722,14 @@ You should see:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
Starting monitoring-daemon-tutorial_monitor_1 ... done
|
Starting asyncio-daemon-tutorial_monitor_1 ... done
|
||||||
Attaching to monitoring-daemon-tutorial_monitor_1
|
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:41,965] [INFO] [Dispatcher]: Starting up
|
||||||
monitor_1 | [2020-08-08 17:06:42,033] [INFO] [HttpMonitor]: Check
|
monitor_1 | [2020-08-08 17:06:42,033] [INFO] [HttpMonitor]: Check
|
||||||
monitor_1 | GET http://example.com
|
monitor_1 | GET http://example.com
|
||||||
monitor_1 | response code: 200
|
monitor_1 | response code: 200
|
||||||
monitor_1 | content length: 648
|
monitor_1 | content length: 648
|
||||||
monitor_1 | request took: 0.067 seconds
|
monitor_1 | request took: 0.067 seconds
|
||||||
monitor_1 |
|
|
||||||
monitor_1 | [2020-08-08 17:06:47,040] [INFO] [HttpMonitor]: Check
|
monitor_1 | [2020-08-08 17:06:47,040] [INFO] [HttpMonitor]: Check
|
||||||
monitor_1 | GET http://example.com
|
monitor_1 | GET http://example.com
|
||||||
monitor_1 | response code: 200
|
monitor_1 | response code: 200
|
||||||
|
@ -744,21 +738,21 @@ You should see:
|
||||||
|
|
||||||
Our daemon can monitor `http://example.com <http://example.com>`_ availability.
|
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
|
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
|
components ready. We just need to create a new provider in the container and update the
|
||||||
configuration.
|
configuration.
|
||||||
|
|
||||||
Edit ``containers.py``:
|
Edit ``containers.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 31-35,41
|
:emphasize-lines: 30-34,40
|
||||||
|
|
||||||
"""Application containers module."""
|
"""Containers module."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
@ -768,8 +762,7 @@ Edit ``containers.py``:
|
||||||
from . import http, monitors, dispatcher
|
from . import http, monitors, dispatcher
|
||||||
|
|
||||||
|
|
||||||
class ApplicationContainer(containers.DeclarativeContainer):
|
class Container(containers.DeclarativeContainer):
|
||||||
"""Application container."""
|
|
||||||
|
|
||||||
config = providers.Configuration()
|
config = providers.Configuration()
|
||||||
|
|
||||||
|
@ -837,27 +830,24 @@ You should see:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
Starting monitoring-daemon-tutorial_monitor_1 ... done
|
Starting asyncio-daemon-tutorial_monitor_1 ... done
|
||||||
Attaching to monitoring-daemon-tutorial_monitor_1
|
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,540] [INFO] [Dispatcher]: Starting up
|
||||||
monitor_1 | [2020-08-08 18:09:08,618] [INFO] [HttpMonitor]: Check
|
monitor_1 | [2020-08-08 18:09:08,618] [INFO] [HttpMonitor]: Check
|
||||||
monitor_1 | GET http://example.com
|
monitor_1 | GET http://example.com
|
||||||
monitor_1 | response code: 200
|
monitor_1 | response code: 200
|
||||||
monitor_1 | content length: 648
|
monitor_1 | content length: 648
|
||||||
monitor_1 | request took: 0.077 seconds
|
monitor_1 | request took: 0.077 seconds
|
||||||
monitor_1 |
|
|
||||||
monitor_1 | [2020-08-08 18:09:08,722] [INFO] [HttpMonitor]: Check
|
monitor_1 | [2020-08-08 18:09:08,722] [INFO] [HttpMonitor]: Check
|
||||||
monitor_1 | GET https://httpbin.org/get
|
monitor_1 | GET https://httpbin.org/get
|
||||||
monitor_1 | response code: 200
|
monitor_1 | response code: 200
|
||||||
monitor_1 | content length: 310
|
monitor_1 | content length: 310
|
||||||
monitor_1 | request took: 0.18 seconds
|
monitor_1 | request took: 0.18 seconds
|
||||||
monitor_1 |
|
|
||||||
monitor_1 | [2020-08-08 18:09:13,619] [INFO] [HttpMonitor]: Check
|
monitor_1 | [2020-08-08 18:09:13,619] [INFO] [HttpMonitor]: Check
|
||||||
monitor_1 | GET http://example.com
|
monitor_1 | GET http://example.com
|
||||||
monitor_1 | response code: 200
|
monitor_1 | response code: 200
|
||||||
monitor_1 | content length: 648
|
monitor_1 | content length: 648
|
||||||
monitor_1 | request took: 0.066 seconds
|
monitor_1 | request took: 0.066 seconds
|
||||||
monitor_1 |
|
|
||||||
monitor_1 | [2020-08-08 18:09:13,681] [INFO] [HttpMonitor]: Check
|
monitor_1 | [2020-08-08 18:09:13,681] [INFO] [HttpMonitor]: Check
|
||||||
monitor_1 | GET https://httpbin.org/get
|
monitor_1 | GET https://httpbin.org/get
|
||||||
monitor_1 | response code: 200
|
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
|
The functional part is done. Daemon monitors `http://example.com <http://example.com>`_ and
|
||||||
`https://httpbin.org <https://httpbin.org>`_.
|
`https://httpbin.org <https://httpbin.org>`_.
|
||||||
|
|
||||||
In the next section we will add some tests.
|
In next section we will add some tests.
|
||||||
|
|
||||||
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
|
We will use `pytest <https://docs.pytest.org/en/stable/>`_ and
|
||||||
`coverage <https://coverage.readthedocs.io/>`_.
|
`coverage <https://coverage.readthedocs.io/>`_.
|
||||||
|
@ -909,7 +899,7 @@ and put next into it:
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from .containers import ApplicationContainer
|
from .containers import Container
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
|
@ -920,7 +910,7 @@ and put next into it:
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def container():
|
def container():
|
||||||
container = ApplicationContainer()
|
container = Container()
|
||||||
container.config.from_dict({
|
container.config.from_dict({
|
||||||
'log': {
|
'log': {
|
||||||
'level': 'INFO',
|
'level': 'INFO',
|
||||||
|
@ -1002,14 +992,14 @@ You should see:
|
||||||
Name Stmts Miss Cover
|
Name Stmts Miss Cover
|
||||||
----------------------------------------------------
|
----------------------------------------------------
|
||||||
monitoringdaemon/__init__.py 0 0 100%
|
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/containers.py 11 0 100%
|
||||||
monitoringdaemon/dispatcher.py 43 5 88%
|
monitoringdaemon/dispatcher.py 44 5 89%
|
||||||
monitoringdaemon/http.py 6 3 50%
|
monitoringdaemon/http.py 6 3 50%
|
||||||
monitoringdaemon/monitors.py 23 1 96%
|
monitoringdaemon/monitors.py 23 1 96%
|
||||||
monitoringdaemon/tests.py 37 0 100%
|
monitoringdaemon/tests.py 37 0 100%
|
||||||
----------------------------------------------------
|
----------------------------------------------------
|
||||||
TOTAL 129 18 86%
|
TOTAL 133 21 84%
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
@ -1028,55 +1018,19 @@ In this tutorial we've built an ``asyncio`` monitoring daemon following the dep
|
||||||
injection principle.
|
injection principle.
|
||||||
We've used the ``Dependency Injector`` as a dependency injection framework.
|
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
|
With a help of :ref:`containers` and :ref:`providers` we have defined how to assemble application components.
|
||||||
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:
|
|
||||||
|
|
||||||
.. 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
|
We kept all the dependencies injected explicitly. This will help when you need to add or
|
||||||
import sys
|
change something in future.
|
||||||
|
|
||||||
from dependency_injector import containers, providers
|
You can find complete project on the
|
||||||
|
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/asyncio-daemon>`_.
|
||||||
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,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
What's next?
|
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:
|
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:
|
The responsibilities are split next way:
|
||||||
|
|
||||||
|
@ -63,18 +63,18 @@ Prepare the environment
|
||||||
|
|
||||||
Let's create the environment for the project.
|
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
|
.. code-block:: bash
|
||||||
|
|
||||||
mkdir movie-lister-tutorial
|
mkdir movie-lister-tutorial
|
||||||
cd 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
|
.. code-block:: bash
|
||||||
|
|
||||||
|
python3 -m venv venv
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
|
|
||||||
Project layout
|
Project layout
|
||||||
|
@ -245,13 +245,13 @@ Edit ``containers.py``:
|
||||||
from dependency_injector import containers
|
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.
|
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
|
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``:
|
Edit ``__main__.py``:
|
||||||
|
|
||||||
|
@ -259,22 +259,18 @@ Edit ``__main__.py``:
|
||||||
|
|
||||||
"""Main module."""
|
"""Main module."""
|
||||||
|
|
||||||
from .containers import ApplicationContainer
|
from .containers import Container
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main() -> None:
|
||||||
container = ApplicationContainer()
|
...
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
container = Container()
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Container is the first object in the application.
|
|
||||||
|
|
||||||
The container is used to create all other objects.
|
|
||||||
|
|
||||||
Csv finder
|
Csv finder
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
@ -289,7 +285,7 @@ We will add:
|
||||||
|
|
||||||
After each step we will add the provider to the container.
|
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:
|
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``:
|
Edit ``containers.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 3,5,9
|
:emphasize-lines: 3,5,10
|
||||||
|
|
||||||
"""Containers module."""
|
"""Containers module."""
|
||||||
|
|
||||||
|
@ -346,7 +342,8 @@ Edit ``containers.py``:
|
||||||
|
|
||||||
from . import entities
|
from . import entities
|
||||||
|
|
||||||
class ApplicationContainer(containers.DeclarativeContainer):
|
|
||||||
|
class Container(containers.DeclarativeContainer):
|
||||||
|
|
||||||
movie = providers.Factory(entities.Movie)
|
movie = providers.Factory(entities.Movie)
|
||||||
|
|
||||||
|
@ -420,7 +417,7 @@ Now let's add the csv finder into the container.
|
||||||
Edit ``containers.py``:
|
Edit ``containers.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 5,9,13-18
|
:emphasize-lines: 5,10,14-19
|
||||||
|
|
||||||
"""Containers module."""
|
"""Containers module."""
|
||||||
|
|
||||||
|
@ -428,7 +425,8 @@ Edit ``containers.py``:
|
||||||
|
|
||||||
from . import finders, entities
|
from . import finders, entities
|
||||||
|
|
||||||
class ApplicationContainer(containers.DeclarativeContainer):
|
|
||||||
|
class Container(containers.DeclarativeContainer):
|
||||||
|
|
||||||
config = providers.Configuration()
|
config = providers.Configuration()
|
||||||
|
|
||||||
|
@ -474,20 +472,21 @@ The configuration file is ready. Now let's update the ``main()`` function to sp
|
||||||
Edit ``__main__.py``:
|
Edit ``__main__.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 9
|
:emphasize-lines: 12
|
||||||
|
|
||||||
"""Main module."""
|
"""Main module."""
|
||||||
|
|
||||||
from .containers import ApplicationContainer
|
from .containers import Container
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main() -> None:
|
||||||
container = ApplicationContainer()
|
...
|
||||||
|
|
||||||
container.config.from_yaml('config.yml')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
container = Container()
|
||||||
|
container.config.from_yaml('config.yml')
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
||||||
Move on to the lister.
|
Move on to the lister.
|
||||||
|
@ -542,7 +541,7 @@ and put next into it:
|
||||||
and edit ``containers.py``:
|
and edit ``containers.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 5,20-23
|
:emphasize-lines: 5,21-24
|
||||||
|
|
||||||
"""Containers module."""
|
"""Containers module."""
|
||||||
|
|
||||||
|
@ -550,7 +549,8 @@ and edit ``containers.py``:
|
||||||
|
|
||||||
from . import finders, listers, entities
|
from . import finders, listers, entities
|
||||||
|
|
||||||
class ApplicationContainer(containers.DeclarativeContainer):
|
|
||||||
|
class Container(containers.DeclarativeContainer):
|
||||||
|
|
||||||
config = providers.Configuration()
|
config = providers.Configuration()
|
||||||
|
|
||||||
|
@ -570,36 +570,69 @@ and edit ``containers.py``:
|
||||||
|
|
||||||
All the components are created and added to the container.
|
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``:
|
Edit ``__main__.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 11-20
|
:emphasize-lines: 3-7,11,18
|
||||||
|
|
||||||
"""Main module."""
|
"""Main module."""
|
||||||
|
|
||||||
from .containers import ApplicationContainer
|
import sys
|
||||||
|
|
||||||
|
from dependency_injector.wiring import Provide
|
||||||
|
|
||||||
|
from .listers import MovieLister
|
||||||
|
from .containers import Container
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main(lister: MovieLister = Provide[Container.lister]) -> None:
|
||||||
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),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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()
|
main()
|
||||||
|
|
||||||
All set. Now we run the application.
|
All set. Now we run the application.
|
||||||
|
@ -612,12 +645,15 @@ Run in the terminal:
|
||||||
|
|
||||||
You should see:
|
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')]
|
Francis Lawrence movies:
|
||||||
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')]
|
- 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.
|
the sqlite format. We will deal with it in the next section.
|
||||||
|
|
||||||
Sqlite finder
|
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``:
|
Edit ``containers.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 20-24,28
|
:emphasize-lines: 21-25,29
|
||||||
|
|
||||||
"""Containers module."""
|
"""Containers module."""
|
||||||
|
|
||||||
|
@ -696,7 +732,8 @@ Edit ``containers.py``:
|
||||||
|
|
||||||
from . import finders, listers, entities
|
from . import finders, listers, entities
|
||||||
|
|
||||||
class ApplicationContainer(containers.DeclarativeContainer):
|
|
||||||
|
class Container(containers.DeclarativeContainer):
|
||||||
|
|
||||||
config = providers.Configuration()
|
config = providers.Configuration()
|
||||||
|
|
||||||
|
@ -747,10 +784,13 @@ Run in the terminal:
|
||||||
|
|
||||||
You should see:
|
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')]
|
Francis Lawrence movies:
|
||||||
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')]
|
- 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
|
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
|
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
|
from . import finders, listers, entities
|
||||||
|
|
||||||
|
|
||||||
class ApplicationContainer(containers.DeclarativeContainer):
|
class Container(containers.DeclarativeContainer):
|
||||||
|
|
||||||
config = providers.Configuration()
|
config = providers.Configuration()
|
||||||
|
|
||||||
|
@ -812,7 +852,7 @@ Edit ``containers.py``:
|
||||||
movie_finder=finder,
|
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``.
|
``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
|
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``:
|
Edit ``__main__.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 10
|
:emphasize-lines: 24
|
||||||
|
|
||||||
"""Main module."""
|
"""Main module."""
|
||||||
|
|
||||||
from .containers import ApplicationContainer
|
import sys
|
||||||
|
|
||||||
|
from dependency_injector.wiring import Provide
|
||||||
|
|
||||||
|
from .listers import MovieLister
|
||||||
|
from .containers import Container
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main(lister: MovieLister = Provide[Container.lister]) -> None:
|
||||||
container = ApplicationContainer()
|
print('Francis Lawrence movies:')
|
||||||
|
for movie in lister.movies_directed_by('Francis Lawrence'):
|
||||||
|
print('\t-', movie)
|
||||||
|
|
||||||
container.config.from_yaml('config.yml')
|
print('2016 movies:')
|
||||||
container.config.finder.type.from_env('MOVIE_FINDER_TYPE')
|
for movie in lister.movies_released_in(2016):
|
||||||
|
print('\t-', movie)
|
||||||
lister = container.lister()
|
|
||||||
|
|
||||||
print(
|
|
||||||
'Francis Lawrence movies:',
|
|
||||||
lister.movies_directed_by('Francis Lawrence'),
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
'2016 movies:',
|
|
||||||
lister.movies_released_in(2016),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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()
|
main()
|
||||||
|
|
||||||
Done.
|
Done.
|
||||||
|
@ -858,12 +900,15 @@ Run in the terminal line by line:
|
||||||
MOVIE_FINDER_TYPE=csv python -m movies
|
MOVIE_FINDER_TYPE=csv python -m movies
|
||||||
MOVIE_FINDER_TYPE=sqlite 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')]
|
Francis Lawrence movies:
|
||||||
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')]
|
- 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.
|
In the next section we will add some tests.
|
||||||
|
|
||||||
|
@ -908,12 +953,12 @@ and put next into it:
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from .containers import ApplicationContainer
|
from .containers import Container
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def container():
|
def container():
|
||||||
container = ApplicationContainer()
|
container = Container()
|
||||||
container.config.from_dict({
|
container.config.from_dict({
|
||||||
'finder': {
|
'finder': {
|
||||||
'type': 'csv',
|
'type': 'csv',
|
||||||
|
@ -966,7 +1011,7 @@ Run in the terminal:
|
||||||
|
|
||||||
You should see:
|
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
|
platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
|
||||||
plugins: cov-2.10.0
|
plugins: cov-2.10.0
|
||||||
|
@ -974,18 +1019,18 @@ You should see:
|
||||||
|
|
||||||
movies/tests.py .. [100%]
|
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
|
Name Stmts Miss Cover
|
||||||
------------------------------------------
|
------------------------------------------
|
||||||
movies/__init__.py 0 0 100%
|
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/containers.py 9 0 100%
|
||||||
movies/entities.py 7 1 86%
|
movies/entities.py 7 1 86%
|
||||||
movies/finders.py 26 13 50%
|
movies/finders.py 26 13 50%
|
||||||
movies/listers.py 8 0 100%
|
movies/listers.py 8 0 100%
|
||||||
movies/tests.py 24 0 100%
|
movies/tests.py 24 0 100%
|
||||||
------------------------------------------
|
------------------------------------------
|
||||||
TOTAL 84 24 71%
|
TOTAL 91 31 66%
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
@ -1002,48 +1047,19 @@ Conclusion
|
||||||
In this tutorial we've built a CLI application following the dependency injection principle.
|
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.
|
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
|
With a help of :ref:`containers` and :ref:`providers` we have defined how to assemble application components.
|
||||||
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:
|
|
||||||
|
|
||||||
.. 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
|
You can find complete project on the
|
||||||
|
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/movie-lister>`_.
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
What's next?
|
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
|
:backlinks: none
|
||||||
|
|
||||||
You can find complete project on the
|
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?
|
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
|
- User can click on the repository, the repository owner or the last commit to open its web page
|
||||||
on the Github.
|
on the Github.
|
||||||
|
|
||||||
.. image:: flask_images/screen_02.png
|
.. image:: flask-images/screen-02.png
|
||||||
|
|
||||||
Prepare the environment
|
Prepare the environment
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
Let's create the environment for the project.
|
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
|
.. code-block:: bash
|
||||||
|
|
||||||
mkdir ghnav-flask-tutorial
|
mkdir ghnav-flask-tutorial
|
||||||
cd 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
|
.. code-block:: bash
|
||||||
|
|
||||||
|
python3 -m venv venv
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
|
|
||||||
Project layout
|
Project layout
|
||||||
|
@ -110,13 +110,13 @@ You should see something like:
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
(venv) $ python -c "import dependency_injector; print(dependency_injector.__version__)"
|
(venv) $ python -c "import dependency_injector; print(dependency_injector.__version__)"
|
||||||
3.22.0
|
4.0.0
|
||||||
(venv) $ python -c "import flask; print(flask.__version__)"
|
(venv) $ python -c "import flask; print(flask.__version__)"
|
||||||
1.1.2
|
1.1.2
|
||||||
|
|
||||||
*Versions can be different. That's fine.*
|
*Versions can be different. That's fine.*
|
||||||
|
|
||||||
Hello world!
|
Hello World!
|
||||||
------------
|
------------
|
||||||
|
|
||||||
Let's create minimal application.
|
Let's create minimal application.
|
||||||
|
@ -133,34 +133,25 @@ Put next into the ``views.py``:
|
||||||
|
|
||||||
Ok, we have the view.
|
Ok, we have the view.
|
||||||
|
|
||||||
Now let's create the main part of our application - the container. Container will keep all of the
|
Now let's create a container. Container will keep all of the application components and their dependencies.
|
||||||
application components and their dependencies. First two providers we need to add are
|
|
||||||
the ``Flask`` application provider and the view provider.
|
|
||||||
|
|
||||||
Put next into the ``containers.py``:
|
Edit ``containers.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
"""Application containers module."""
|
"""Containers module."""
|
||||||
|
|
||||||
from dependency_injector import containers
|
from dependency_injector import containers
|
||||||
from dependency_injector.ext import flask
|
|
||||||
from flask import Flask
|
|
||||||
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationContainer(containers.DeclarativeContainer):
|
class Container(containers.DeclarativeContainer):
|
||||||
"""Application container."""
|
...
|
||||||
|
|
||||||
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 Flask application factory. It will create and configure container
|
||||||
|
and Flask application. It is traditionally called ``create_app()``.
|
||||||
Finally we need to create the Flask application factory. It is traditionally called
|
We will assign ``index`` view to handle user requests to the root ``/`` of our web application.
|
||||||
``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.
|
|
||||||
|
|
||||||
Put next into the ``application.py``:
|
Put next into the ``application.py``:
|
||||||
|
|
||||||
|
@ -168,26 +159,21 @@ Put next into the ``application.py``:
|
||||||
|
|
||||||
"""Application module."""
|
"""Application module."""
|
||||||
|
|
||||||
from .containers import ApplicationContainer
|
from flask import Flask
|
||||||
|
|
||||||
|
from .containers import Container
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app() -> Flask:
|
||||||
"""Create and return Flask application."""
|
container = Container()
|
||||||
container = ApplicationContainer()
|
|
||||||
|
|
||||||
app = container.app()
|
app = Flask(__name__)
|
||||||
app.container = container
|
app.container = container
|
||||||
|
app.add_url_rule('/', 'index', views.index)
|
||||||
app.add_url_rule('/', view_func=container.index_view.as_view())
|
|
||||||
|
|
||||||
return app
|
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!".
|
Ok. Now we're ready to say "Hello, World!".
|
||||||
|
|
||||||
Do next in the terminal:
|
Do next in the terminal:
|
||||||
|
@ -237,58 +223,34 @@ and run in the terminal:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
pip install --upgrade -r requirements.txt
|
pip install -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)
|
|
||||||
|
|
||||||
Let's initialize ``bootstrap-flask`` extension. We will need to modify ``create_app()``.
|
Let's initialize ``bootstrap-flask`` extension. We will need to modify ``create_app()``.
|
||||||
|
|
||||||
Edit ``application.py``:
|
Edit ``application.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 13-14
|
:emphasize-lines: 4,17-18
|
||||||
|
|
||||||
"""Application module."""
|
"""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():
|
def create_app() -> Flask:
|
||||||
"""Create and return Flask application."""
|
container = Container()
|
||||||
container = ApplicationContainer()
|
|
||||||
|
|
||||||
app = container.app()
|
app = Flask(__name__)
|
||||||
app.container = container
|
app.container = container
|
||||||
|
app.add_url_rule('/', 'index', views.index)
|
||||||
|
|
||||||
bootstrap = container.bootstrap()
|
bootstrap = Bootstrap()
|
||||||
bootstrap.init_app(app)
|
bootstrap.init_app(app)
|
||||||
|
|
||||||
app.add_url_rule('/', view_func=container.index_view.as_view())
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
Now we need to add the templates. For doing this we will need to add the folder ``templates/`` to
|
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:
|
You should see:
|
||||||
|
|
||||||
.. image:: flask_images/screen_01.png
|
.. image:: flask-images/screen-01.png
|
||||||
|
|
||||||
Connect to the GitHub
|
Connect to the GitHub
|
||||||
---------------------
|
---------------------
|
||||||
|
@ -477,7 +439,7 @@ and run in the terminal:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. 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
|
Now we need to add Github API client the container. We will need to add two more providers from
|
||||||
the ``dependency_injector.providers`` module:
|
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
|
- ``Configuration`` provider that will be used for providing the API token and the request timeout
|
||||||
for the ``Github`` client.
|
for the ``Github`` client.
|
||||||
|
|
||||||
Let's do it.
|
|
||||||
|
|
||||||
Edit ``containers.py``:
|
Edit ``containers.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. 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 import containers, providers
|
||||||
from dependency_injector.ext import flask
|
|
||||||
from flask import Flask
|
|
||||||
from flask_bootstrap import Bootstrap
|
|
||||||
from github import Github
|
from github import Github
|
||||||
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
|
class Container(containers.DeclarativeContainer):
|
||||||
class ApplicationContainer(containers.DeclarativeContainer):
|
|
||||||
"""Application container."""
|
|
||||||
|
|
||||||
app = flask.Application(Flask, __name__)
|
|
||||||
|
|
||||||
bootstrap = flask.Extension(Bootstrap)
|
|
||||||
|
|
||||||
config = providers.Configuration()
|
config = providers.Configuration()
|
||||||
|
|
||||||
|
@ -519,8 +469,6 @@ Edit ``containers.py``:
|
||||||
timeout=config.github.request_timeout,
|
timeout=config.github.request_timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
index_view = flask.View(views.index)
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
We have used the configuration value before it was defined. That's the principle how
|
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.
|
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.
|
Now let's add the configuration file.
|
||||||
|
|
||||||
We will use YAML.
|
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
|
.. code-block:: bash
|
||||||
:emphasize-lines: 11
|
:emphasize-lines: 11
|
||||||
|
@ -575,7 +528,7 @@ and install it:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. 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.
|
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``:
|
Edit ``application.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 9-10
|
:emphasize-lines: 12-13
|
||||||
|
|
||||||
"""Application module."""
|
"""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():
|
def create_app() -> Flask:
|
||||||
"""Create and return Flask application."""
|
container = Container()
|
||||||
container = ApplicationContainer()
|
|
||||||
container.config.from_yaml('config.yml')
|
container.config.from_yaml('config.yml')
|
||||||
container.config.github.auth_token.from_env('GITHUB_TOKEN')
|
container.config.github.auth_token.from_env('GITHUB_TOKEN')
|
||||||
|
|
||||||
app = container.app()
|
app = Flask(__name__)
|
||||||
app.container = container
|
app.container = container
|
||||||
|
app.add_url_rule('/', 'index', views.index)
|
||||||
|
|
||||||
bootstrap = container.bootstrap()
|
bootstrap = Bootstrap()
|
||||||
bootstrap.init_app(app)
|
bootstrap.init_app(app)
|
||||||
|
|
||||||
app.add_url_rule('/', view_func=container.index_view.as_view())
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
Now we need create an API token.
|
Now we need create an API token.
|
||||||
|
@ -636,7 +591,7 @@ Github API client setup is done.
|
||||||
Search service
|
Search service
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
Now it's time to add the ``SearchService``. It will:
|
Now it's time to add ``SearchService``. It will:
|
||||||
|
|
||||||
- Perform the search.
|
- Perform the search.
|
||||||
- Fetch commit extra data for each result.
|
- Fetch commit extra data for each result.
|
||||||
|
@ -717,25 +672,17 @@ Now let's add ``SearchService`` to the container.
|
||||||
Edit ``containers.py``:
|
Edit ``containers.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. 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 import containers, providers
|
||||||
from dependency_injector.ext import flask
|
|
||||||
from flask import Flask
|
|
||||||
from flask_bootstrap import Bootstrap
|
|
||||||
from github import Github
|
from github import Github
|
||||||
|
|
||||||
from . import services, views
|
from . import services
|
||||||
|
|
||||||
|
|
||||||
class ApplicationContainer(containers.DeclarativeContainer):
|
class Container(containers.DeclarativeContainer):
|
||||||
"""Application container."""
|
|
||||||
|
|
||||||
app = flask.Application(Flask, __name__)
|
|
||||||
|
|
||||||
bootstrap = flask.Extension(Bootstrap)
|
|
||||||
|
|
||||||
config = providers.Configuration()
|
config = providers.Configuration()
|
||||||
|
|
||||||
|
@ -750,26 +697,28 @@ Edit ``containers.py``:
|
||||||
github_client=github_client,
|
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``:
|
Edit ``views.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 5,8,12
|
:emphasize-lines: 4,6-7,10,14
|
||||||
|
|
||||||
"""Views module."""
|
"""Views module."""
|
||||||
|
|
||||||
from flask import request, render_template
|
from flask import request, render_template
|
||||||
|
from dependency_injector.wiring import Provide
|
||||||
|
|
||||||
from .services import SearchService
|
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')
|
query = request.args.get('query', 'Dependency Injector')
|
||||||
limit = request.args.get('limit', 10, int)
|
limit = request.args.get('limit', 10, int)
|
||||||
|
|
||||||
|
@ -782,54 +731,44 @@ Edit ``views.py``:
|
||||||
repositories=repositories,
|
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
|
.. 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 import Flask
|
||||||
from flask_bootstrap import Bootstrap
|
from flask_bootstrap import Bootstrap
|
||||||
from github import Github
|
|
||||||
|
|
||||||
from . import services, views
|
from .containers import Container
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
class ApplicationContainer(containers.DeclarativeContainer):
|
def create_app() -> Flask:
|
||||||
"""Application container."""
|
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()
|
return app
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
Make sure the app is running or use ``flask run`` and open ``http://127.0.0.1:5000/``.
|
Make sure the app is running or use ``flask run`` and open ``http://127.0.0.1:5000/``.
|
||||||
|
|
||||||
You should see:
|
You should see:
|
||||||
|
|
||||||
.. image:: flask_images/screen_02.png
|
.. image:: flask-images/screen-02.png
|
||||||
|
|
||||||
Make some refactoring
|
Make some refactoring
|
||||||
---------------------
|
---------------------
|
||||||
|
@ -844,19 +783,21 @@ Let's make some refactoring. We will move these values to the config.
|
||||||
Edit ``views.py``:
|
Edit ``views.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 8-14
|
:emphasize-lines: 10-16
|
||||||
|
|
||||||
"""Views module."""
|
"""Views module."""
|
||||||
|
|
||||||
from flask import request, render_template
|
from flask import request, render_template
|
||||||
|
from dependency_injector.wiring import Provide
|
||||||
|
|
||||||
from .services import SearchService
|
from .services import SearchService
|
||||||
|
from .containers import Container
|
||||||
|
|
||||||
|
|
||||||
def index(
|
def index(
|
||||||
search_service: SearchService,
|
search_service: SearchService = Provide[Container.search_service],
|
||||||
default_query: str,
|
default_query: str = Provide[Container.config.default.query],
|
||||||
default_limit: int,
|
default_limit: int = Provide[Container.config.default.limit.as_int()],
|
||||||
):
|
):
|
||||||
query = request.args.get('query', default_query)
|
query = request.args.get('query', default_query)
|
||||||
limit = request.args.get('limit', default_limit, int)
|
limit = request.args.get('limit', default_limit, int)
|
||||||
|
@ -870,53 +811,6 @@ Edit ``views.py``:
|
||||||
repositories=repositories,
|
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``:
|
Edit ``config.yml``:
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
@ -924,20 +818,18 @@ Edit ``config.yml``:
|
||||||
|
|
||||||
github:
|
github:
|
||||||
request_timeout: 10
|
request_timeout: 10
|
||||||
search:
|
default:
|
||||||
default_query: "Dependency Injector"
|
query: "Dependency Injector"
|
||||||
default_limit: 10
|
limit: 10
|
||||||
|
|
||||||
That's it.
|
That's it. The refactoring is done. We've made it cleaner.
|
||||||
|
|
||||||
The refactoring is done. We've made it cleaner.
|
|
||||||
|
|
||||||
Tests
|
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/>`_.
|
`coverage <https://coverage.readthedocs.io/>`_.
|
||||||
|
|
||||||
Edit ``requirements.txt``:
|
Edit ``requirements.txt``:
|
||||||
|
@ -953,7 +845,7 @@ Edit ``requirements.txt``:
|
||||||
pytest-flask
|
pytest-flask
|
||||||
pytest-cov
|
pytest-cov
|
||||||
|
|
||||||
And let's install it:
|
And install added packages:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
|
@ -982,7 +874,7 @@ Create empty file ``tests.py`` in the ``githubnavigator`` package:
|
||||||
and put next into it:
|
and put next into it:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 42,65
|
:emphasize-lines: 44,67
|
||||||
|
|
||||||
"""Tests module."""
|
"""Tests module."""
|
||||||
|
|
||||||
|
@ -997,7 +889,9 @@ and put next into it:
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def app():
|
def app():
|
||||||
return create_app()
|
app = create_app()
|
||||||
|
yield app
|
||||||
|
app.container.unwire()
|
||||||
|
|
||||||
|
|
||||||
def test_index(client, app):
|
def test_index(client, app):
|
||||||
|
@ -1074,13 +968,13 @@ You should see:
|
||||||
Name Stmts Miss Cover
|
Name Stmts Miss Cover
|
||||||
----------------------------------------------------
|
----------------------------------------------------
|
||||||
githubnavigator/__init__.py 0 0 100%
|
githubnavigator/__init__.py 0 0 100%
|
||||||
githubnavigator/application.py 11 0 100%
|
githubnavigator/application.py 15 0 100%
|
||||||
githubnavigator/containers.py 13 0 100%
|
githubnavigator/containers.py 7 0 100%
|
||||||
githubnavigator/services.py 14 0 100%
|
githubnavigator/services.py 14 0 100%
|
||||||
githubnavigator/tests.py 32 0 100%
|
githubnavigator/tests.py 34 0 100%
|
||||||
githubnavigator/views.py 7 0 100%
|
githubnavigator/views.py 9 0 100%
|
||||||
----------------------------------------------------
|
----------------------------------------------------
|
||||||
TOTAL 77 0 100%
|
TOTAL 79 0 100%
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
@ -1091,53 +985,22 @@ You should see:
|
||||||
Conclusion
|
Conclusion
|
||||||
----------
|
----------
|
||||||
|
|
||||||
We are done.
|
|
||||||
|
|
||||||
In this tutorial we've built a ``Flask`` application following the dependency injection principle.
|
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.
|
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
|
:ref:`containers` and :ref:`providers` helped to specify how to assemble search service and
|
||||||
their dependencies defined explicitly in one place:
|
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
|
We kept all the dependencies injected explicitly. This will help when you need to add or
|
||||||
from dependency_injector.ext import flask
|
change something in future.
|
||||||
from flask import Flask
|
|
||||||
from flask_bootstrap import Bootstrap
|
|
||||||
from github import Github
|
|
||||||
|
|
||||||
from . import services, views
|
You can find complete project on the
|
||||||
|
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/flask>`_.
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
What's next?
|
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 import containers, providers
|
||||||
|
from dependency_injector.wiring import Provide
|
||||||
|
|
||||||
|
from after import ApiClient, Service
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class Container(containers.DeclarativeContainer):
|
class Container(containers.DeclarativeContainer):
|
||||||
|
@ -30,9 +23,17 @@ class Container(containers.DeclarativeContainer):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main(service: Service = Provide[Container.service]):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
container = Container()
|
container = Container()
|
||||||
container.config.api_key.from_env('API_KEY')
|
container.config.api_key.from_env('API_KEY')
|
||||||
container.config.timeout.from_env('TIMEOUT')
|
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/>`_ +
|
This is an `Aiohttp <https://docs.aiohttp.org/>`_ +
|
||||||
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ application.
|
`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
|
Run
|
||||||
---
|
---
|
||||||
|
@ -106,12 +108,11 @@ The output should be something like:
|
||||||
Name Stmts Miss Cover
|
Name Stmts Miss Cover
|
||||||
---------------------------------------------------
|
---------------------------------------------------
|
||||||
giphynavigator/__init__.py 0 0 100%
|
giphynavigator/__init__.py 0 0 100%
|
||||||
giphynavigator/__main__.py 5 5 0%
|
giphynavigator/application.py 12 0 100%
|
||||||
giphynavigator/application.py 10 0 100%
|
giphynavigator/containers.py 6 0 100%
|
||||||
giphynavigator/containers.py 10 0 100%
|
|
||||||
giphynavigator/giphy.py 14 9 36%
|
giphynavigator/giphy.py 14 9 36%
|
||||||
|
giphynavigator/handlers.py 9 0 100%
|
||||||
giphynavigator/services.py 9 1 89%
|
giphynavigator/services.py 9 1 89%
|
||||||
giphynavigator/tests.py 35 0 100%
|
giphynavigator/tests.py 37 0 100%
|
||||||
giphynavigator/views.py 7 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 aiohttp import web
|
||||||
|
from dependency_injector.wiring import Provide
|
||||||
|
|
||||||
from .services import SearchService
|
from .services import SearchService
|
||||||
|
from .containers import Container
|
||||||
|
|
||||||
|
|
||||||
async def index(
|
async def index(
|
||||||
request: web.Request,
|
request: web.Request,
|
||||||
search_service: SearchService,
|
search_service: SearchService = Provide[Container.search_service],
|
||||||
default_query: str,
|
default_query: str = Provide[Container.config.default.query],
|
||||||
default_limit: int,
|
default_limit: int = Provide[Container.config.default.limit.as_int()],
|
||||||
) -> web.Response:
|
) -> web.Response:
|
||||||
query = request.query.get('query', default_query)
|
query = request.query.get('query', default_query)
|
||||||
limit = int(request.query.get('limit', default_limit))
|
limit = int(request.query.get('limit', default_limit))
|
|
@ -10,7 +10,9 @@ from giphynavigator.giphy import GiphyClient
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def app():
|
def app():
|
||||||
return create_app()
|
app = create_app()
|
||||||
|
yield app
|
||||||
|
app.container.unwire()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -73,5 +75,5 @@ async def test_index_default_params(client, app):
|
||||||
|
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
data = await response.json()
|
data = await response.json()
|
||||||
assert data['query'] == app.container.config.search.default_query()
|
assert data['query'] == app.container.config.default.query()
|
||||||
assert data['limit'] == app.container.config.search.default_limit()
|
assert data['limit'] == app.container.config.default.limit()
|
|
@ -24,6 +24,6 @@ You should see:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
[2020-09-04 16:06:00,750] [DEBUG] [example.services.UserService]: User user@example.com has been found in database
|
[2020-10-06 15:36:55,961] [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-10-06 15:36:55,961] [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.PhotoService]: Photo photo.jpg has been successfully uploaded by user user@example.com
|
||||||
|
|
|
@ -2,23 +2,29 @@
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from dependency_injector.wiring import Provide
|
||||||
|
|
||||||
|
from .services import UserService, AuthService, PhotoService
|
||||||
from .containers import Application
|
from .containers import Application
|
||||||
|
|
||||||
|
|
||||||
def main(email: str, password: str, photo: str) -> None:
|
def main(
|
||||||
application = Application()
|
email: str,
|
||||||
|
password: str,
|
||||||
application.config.from_yaml('config.yml')
|
photo: str,
|
||||||
application.core.configure_logging()
|
user_service: UserService = Provide[Application.services.user],
|
||||||
|
auth_service: AuthService = Provide[Application.services.auth],
|
||||||
user_service = application.services.user()
|
photo_service: PhotoService = Provide[Application.services.photo],
|
||||||
auth_service = application.services.auth()
|
) -> None:
|
||||||
photo_service = application.services.photo()
|
|
||||||
|
|
||||||
user = user_service.get_user(email)
|
user = user_service.get_user(email)
|
||||||
auth_service.authenticate(user, password)
|
auth_service.authenticate(user, password)
|
||||||
photo_service.upload_photo(user, photo)
|
photo_service.upload_photo(user, photo)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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:])
|
main(*sys.argv[1:])
|
||||||
|
|
|
@ -24,6 +24,6 @@ You should see:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
[2020-09-04 15:27:27,727] [DEBUG] [example.services.UserService]: User user@example.com has been found in database
|
[2020-10-06 15:32:33,195] [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-10-06 15:32:33,195] [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.PhotoService]: Photo photo.jpg has been successfully uploaded by user user@example.com
|
||||||
|
|
|
@ -2,23 +2,29 @@
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from dependency_injector.wiring import Provide
|
||||||
|
|
||||||
|
from .services import UserService, AuthService, PhotoService
|
||||||
from .containers import Container
|
from .containers import Container
|
||||||
|
|
||||||
|
|
||||||
def main(email: str, password: str, photo: str) -> None:
|
def main(
|
||||||
container = Container()
|
email: str,
|
||||||
|
password: str,
|
||||||
container.configure_logging()
|
photo: str,
|
||||||
container.config.from_ini('config.ini')
|
user_service: UserService = Provide[Container.user_service],
|
||||||
|
auth_service: AuthService = Provide[Container.auth_service],
|
||||||
user_service = container.user_service()
|
photo_service: PhotoService = Provide[Container.photo_service],
|
||||||
auth_service = container.auth_service()
|
) -> None:
|
||||||
photo_service = container.photo_service()
|
|
||||||
|
|
||||||
user = user_service.get_user(email)
|
user = user_service.get_user(email)
|
||||||
auth_service.authenticate(user, password)
|
auth_service.authenticate(user, password)
|
||||||
photo_service.upload_photo(user, photo)
|
photo_service.upload_photo(user, photo)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
container = Container()
|
||||||
|
container.configure_logging()
|
||||||
|
container.config.from_ini('config.ini')
|
||||||
|
container.wire(modules=[sys.modules[__name__]])
|
||||||
|
|
||||||
main(*sys.argv[1:])
|
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>`_
|
This is an `asyncio <https://docs.python.org/3/library/asyncio.html>`_ +
|
||||||
+ `Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ application.
|
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ example application.
|
||||||
|
|
||||||
|
The example application is a daemon that monitors availability of web services.
|
||||||
|
|
||||||
Run
|
Run
|
||||||
---
|
---
|
||||||
|
@ -31,19 +33,16 @@ The output should be something like:
|
||||||
monitor_1 | response code: 200
|
monitor_1 | response code: 200
|
||||||
monitor_1 | content length: 648
|
monitor_1 | content length: 648
|
||||||
monitor_1 | request took: 0.074 seconds
|
monitor_1 | request took: 0.074 seconds
|
||||||
monitor_1 |
|
|
||||||
monitor_1 | [2020-08-08 17:04:36,811] [INFO] [HttpMonitor]: Check
|
monitor_1 | [2020-08-08 17:04:36,811] [INFO] [HttpMonitor]: Check
|
||||||
monitor_1 | GET https://httpbin.org/get
|
monitor_1 | GET https://httpbin.org/get
|
||||||
monitor_1 | response code: 200
|
monitor_1 | response code: 200
|
||||||
monitor_1 | content length: 310
|
monitor_1 | content length: 310
|
||||||
monitor_1 | request took: 0.153 seconds
|
monitor_1 | request took: 0.153 seconds
|
||||||
monitor_1 |
|
|
||||||
monitor_1 | [2020-08-08 17:04:41,731] [INFO] [HttpMonitor]: Check
|
monitor_1 | [2020-08-08 17:04:41,731] [INFO] [HttpMonitor]: Check
|
||||||
monitor_1 | GET http://example.com
|
monitor_1 | GET http://example.com
|
||||||
monitor_1 | response code: 200
|
monitor_1 | response code: 200
|
||||||
monitor_1 | content length: 648
|
monitor_1 | content length: 648
|
||||||
monitor_1 | request took: 0.067 seconds
|
monitor_1 | request took: 0.067 seconds
|
||||||
monitor_1 |
|
|
||||||
monitor_1 | [2020-08-08 17:04:41,787] [INFO] [HttpMonitor]: Check
|
monitor_1 | [2020-08-08 17:04:41,787] [INFO] [HttpMonitor]: Check
|
||||||
monitor_1 | GET https://httpbin.org/get
|
monitor_1 | GET https://httpbin.org/get
|
||||||
monitor_1 | response code: 200
|
monitor_1 | response code: 200
|
||||||
|
@ -77,11 +76,11 @@ The output should be something like:
|
||||||
Name Stmts Miss Cover
|
Name Stmts Miss Cover
|
||||||
----------------------------------------------------
|
----------------------------------------------------
|
||||||
monitoringdaemon/__init__.py 0 0 100%
|
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/containers.py 11 0 100%
|
||||||
monitoringdaemon/dispatcher.py 43 5 88%
|
monitoringdaemon/dispatcher.py 44 5 89%
|
||||||
monitoringdaemon/http.py 6 3 50%
|
monitoringdaemon/http.py 6 3 50%
|
||||||
monitoringdaemon/monitors.py 23 1 96%
|
monitoringdaemon/monitors.py 23 1 96%
|
||||||
monitoringdaemon/tests.py 37 0 100%
|
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 logging
|
||||||
import sys
|
import sys
|
||||||
|
@ -8,8 +8,7 @@ from dependency_injector import containers, providers
|
||||||
from . import http, monitors, dispatcher
|
from . import http, monitors, dispatcher
|
||||||
|
|
||||||
|
|
||||||
class ApplicationContainer(containers.DeclarativeContainer):
|
class Container(containers.DeclarativeContainer):
|
||||||
"""Application container."""
|
|
||||||
|
|
||||||
config = providers.Configuration()
|
config = providers.Configuration()
|
||||||
|
|
|
@ -44,6 +44,7 @@ class Dispatcher:
|
||||||
self._logger.info('Shutting down')
|
self._logger.info('Shutting down')
|
||||||
for task, monitor in zip(self._monitor_tasks, self._monitors):
|
for task, monitor in zip(self._monitor_tasks, self._monitors):
|
||||||
task.cancel()
|
task.cancel()
|
||||||
|
self._monitor_tasks.clear()
|
||||||
self._logger.info('Shutdown finished successfully')
|
self._logger.info('Shutdown finished successfully')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
|
@ -47,7 +47,7 @@ class HttpMonitor(Monitor):
|
||||||
' %s %s\n'
|
' %s %s\n'
|
||||||
' response code: %s\n'
|
' response code: %s\n'
|
||||||
' content length: %s\n'
|
' content length: %s\n'
|
||||||
' request took: %s seconds\n',
|
' request took: %s seconds',
|
||||||
self._method,
|
self._method,
|
||||||
self._url,
|
self._url,
|
||||||
response.status,
|
response.status,
|
|
@ -6,7 +6,7 @@ from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from .containers import ApplicationContainer
|
from .containers import Container
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
|
@ -17,7 +17,7 @@ class RequestStub:
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def container():
|
def container():
|
||||||
container = ApplicationContainer()
|
container = Container()
|
||||||
container.config.from_dict({
|
container.config.from_dict({
|
||||||
'log': {
|
'log': {
|
||||||
'level': 'INFO',
|
'level': 'INFO',
|
|
@ -1,15 +1,26 @@
|
||||||
"""Main module."""
|
"""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
|
from .containers import ApplicationContainer
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main(
|
||||||
application = ApplicationContainer()
|
user_repository: UserRepository = Provide[
|
||||||
application.config.from_ini('config.ini')
|
ApplicationContainer.user_package.user_repository
|
||||||
|
],
|
||||||
user_repository = application.user_package.user_repository()
|
photo_repository: PhotoRepository = Provide[
|
||||||
photo_repository = application.photo_package.photo_repository()
|
ApplicationContainer.photo_package.photo_repository
|
||||||
|
],
|
||||||
|
aggregation_service: AggregationService = Provide[
|
||||||
|
ApplicationContainer.analytics_package.aggregation_service
|
||||||
|
],
|
||||||
|
) -> None:
|
||||||
user1 = user_repository.get(id=1)
|
user1 = user_repository.get(id=1)
|
||||||
user1_photos = photo_repository.get_photos(user1.id)
|
user1_photos = photo_repository.get_photos(user1.id)
|
||||||
print(f'Retrieve user id={user1.id}, photos count={len(user1_photos)}')
|
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)
|
user2_photos = photo_repository.get_photos(user2.id)
|
||||||
print(f'Retrieve user id={user2.id}, photos count={len(user2_photos)}')
|
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.user_repository is user_repository
|
||||||
assert aggregation_service.photo_repository is photo_repository
|
assert aggregation_service.photo_repository is photo_repository
|
||||||
print('Aggregate analytics from user and photo packages')
|
print('Aggregate analytics from user and photo packages')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
application = ApplicationContainer()
|
||||||
|
application.config.from_ini('config.ini')
|
||||||
|
application.wire(modules=[sys.modules[__name__]])
|
||||||
|
|
||||||
main()
|
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/>`_ +
|
This is a `Flask <https://flask.palletsprojects.com/>`_ +
|
||||||
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ application.
|
`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
|
.. image:: screenshot.png
|
||||||
|
|
||||||
|
@ -46,8 +48,7 @@ After that visit http://127.0.0.1:5000/ in your browser.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
Github has a rate limit. When the rate limit is exceed you will see an exception
|
||||||
Github has a rate limit. When thre rate limit is exceed you will see an exception
|
|
||||||
``github.GithubException.RateLimitExceededException``. For unauthenticated requests, the rate
|
``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
|
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.
|
need to set personal access token.
|
||||||
|
@ -90,10 +91,10 @@ The output should be something like:
|
||||||
Name Stmts Miss Cover
|
Name Stmts Miss Cover
|
||||||
----------------------------------------------------
|
----------------------------------------------------
|
||||||
githubnavigator/__init__.py 0 0 100%
|
githubnavigator/__init__.py 0 0 100%
|
||||||
githubnavigator/application.py 11 0 100%
|
githubnavigator/application.py 15 0 100%
|
||||||
githubnavigator/containers.py 13 0 100%
|
githubnavigator/containers.py 7 0 100%
|
||||||
githubnavigator/services.py 14 0 100%
|
githubnavigator/services.py 14 0 100%
|
||||||
githubnavigator/tests.py 32 0 100%
|
githubnavigator/tests.py 34 0 100%
|
||||||
githubnavigator/views.py 7 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
|
@pytest.fixture
|
||||||
def app():
|
def app():
|
||||||
return create_app()
|
app = create_app()
|
||||||
|
yield app
|
||||||
|
app.container.unwire()
|
||||||
|
|
||||||
|
|
||||||
def test_index(client, app):
|
def test_index(client, app):
|
|
@ -1,14 +1,16 @@
|
||||||
"""Views module."""
|
"""Views module."""
|
||||||
|
|
||||||
from flask import request, render_template
|
from flask import request, render_template
|
||||||
|
from dependency_injector.wiring import Provide
|
||||||
|
|
||||||
from .services import SearchService
|
from .services import SearchService
|
||||||
|
from .containers import Container
|
||||||
|
|
||||||
|
|
||||||
def index(
|
def index(
|
||||||
search_service: SearchService,
|
search_service: SearchService = Provide[Container.search_service],
|
||||||
default_query: str,
|
default_query: str = Provide[Container.config.default.query],
|
||||||
default_limit: int,
|
default_limit: int = Provide[Container.config.default.limit.as_int()],
|
||||||
):
|
):
|
||||||
query = request.args.get('query', default_query)
|
query = request.args.get('query', default_query)
|
||||||
limit = request.args.get('limit', default_limit, int)
|
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
|
|