mirror of
https://github.com/ets-labs/python-dependency-injector.git
synced 2025-01-30 19:24:31 +03:00
Merge branch 'release/4.37.0' into master
This commit is contained in:
commit
541131e338
33
.github/workflows/publishing.yml
vendored
33
.github/workflows/publishing.yml
vendored
|
@ -9,20 +9,20 @@ jobs:
|
|||
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: "3.10"
|
||||
- run: pip install tox
|
||||
- run: tox
|
||||
env:
|
||||
TOXENV: 3.9
|
||||
TOXENV: "3.10"
|
||||
|
||||
linters:
|
||||
name: Run linters
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
toxenv: [flake8, pydocstyle, mypy, pylint]
|
||||
|
@ -30,7 +30,7 @@ jobs:
|
|||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: "3.10"
|
||||
- run: pip install tox
|
||||
- run: tox
|
||||
env:
|
||||
|
@ -39,12 +39,12 @@ jobs:
|
|||
build-sdist:
|
||||
name: Build source tarball
|
||||
needs: [tests, linters]
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: "3.10"
|
||||
- run: python setup.py sdist
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
|
@ -56,15 +56,15 @@ jobs:
|
|||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-18.04, windows-latest, macos-latest]
|
||||
os: [ubuntu-20.04, windows-2019, macOS-10.15]
|
||||
env:
|
||||
CIBW_SKIP: cp27-win*
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
- run: pip install cibuildwheel==1.8.0
|
||||
python-version: "3.10"
|
||||
- run: pip install cibuildwheel==2.1.3
|
||||
- run: cibuildwheel --output-dir wheelhouse
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
|
@ -73,15 +73,15 @@ jobs:
|
|||
build-wheels-linux-aarch64:
|
||||
name: Build wheels (ubuntu-latest-aarch64)
|
||||
needs: [tests, linters]
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
- run: pip install cibuildwheel==1.8.0
|
||||
python-version: "3.10"
|
||||
- run: pip install cibuildwheel==2.1.3
|
||||
- run: cibuildwheel --archs aarch64 --output-dir wheelhouse
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
|
@ -90,7 +90,7 @@ jobs:
|
|||
publish:
|
||||
name: Publish on PyPI
|
||||
needs: [build-sdist, build-wheels, build-wheels-linux-aarch64]
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
|
@ -100,6 +100,9 @@ jobs:
|
|||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
# For publishing to Test PyPI, uncomment next two lines:
|
||||
# password: ${{ secrets.TEST_PYPI_API_TOKEN }}
|
||||
# repository_url: https://test.pypi.org/legacy/
|
||||
|
||||
publish-docs:
|
||||
name: Publish docs
|
||||
|
@ -109,7 +112,7 @@ jobs:
|
|||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: "3.10"
|
||||
- run: pip install -r requirements-doc.txt
|
||||
- run: pip install awscli
|
||||
- run: pip install -e .
|
||||
|
|
8
.github/workflows/tests-and-linters.yml
vendored
8
.github/workflows/tests-and-linters.yml
vendored
|
@ -6,10 +6,10 @@ jobs:
|
|||
|
||||
test-on-different-versions:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [2.7, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, pypy2, pypy3]
|
||||
python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, "3.10", pypy2, pypy3]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
|
@ -31,7 +31,7 @@ jobs:
|
|||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: "3.10"
|
||||
- run: pip install tox cython
|
||||
- run: make cythonize
|
||||
- run: tox
|
||||
|
@ -48,7 +48,7 @@ jobs:
|
|||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: "3.10"
|
||||
- run: pip install tox
|
||||
- run: tox
|
||||
env:
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -36,6 +36,7 @@ reports/
|
|||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
@ -54,7 +55,7 @@ target/
|
|||
.idea/
|
||||
|
||||
# Virtualenv
|
||||
venv/
|
||||
venv*/
|
||||
|
||||
# SQLite
|
||||
*.db
|
||||
|
|
11
Makefile
11
Makefile
|
@ -45,17 +45,10 @@ install: uninstall clean cythonize
|
|||
uninstall:
|
||||
- pip uninstall -y -q dependency-injector 2> /dev/null
|
||||
|
||||
test-py2: build
|
||||
test:
|
||||
# Unit tests with coverage report
|
||||
coverage erase
|
||||
coverage run --rcfile=./.coveragerc -m unittest discover -s tests/unit/ -p test_*_py2_py3.py
|
||||
coverage report --rcfile=./.coveragerc
|
||||
coverage html --rcfile=./.coveragerc
|
||||
|
||||
test: build
|
||||
# Unit tests with coverage report
|
||||
coverage erase
|
||||
coverage run --rcfile=./.coveragerc -m unittest discover -s tests/unit/ -p test_*py3*.py
|
||||
coverage run --rcfile=./.coveragerc -m pytest -c tests/.configs/pytest.ini
|
||||
coverage report --rcfile=./.coveragerc
|
||||
coverage html --rcfile=./.coveragerc
|
||||
|
||||
|
|
14
README.rst
14
README.rst
|
@ -80,7 +80,7 @@ Key features of the ``Dependency Injector``:
|
|||
.. code-block:: python
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
@ -104,11 +104,11 @@ Key features of the ``Dependency Injector``:
|
|||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
container.config.api_key.from_env('API_KEY')
|
||||
container.config.timeout.from_env('TIMEOUT')
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
container.config.api_key.from_env("API_KEY")
|
||||
container.config.timeout.from_env("TIMEOUT")
|
||||
container.wire(modules=[__name__])
|
||||
|
||||
main() # <-- dependency is injected automatically
|
||||
|
||||
|
@ -195,7 +195,7 @@ What is the dependency injection?
|
|||
- dependency injection is a principle that decreases coupling and increases cohesion
|
||||
|
||||
Why should I do the dependency injection?
|
||||
- your code becomes more flexible, testable and clear 😎
|
||||
- your code becomes more flexible, testable, and clear 😎
|
||||
|
||||
How do I start doing the dependency injection?
|
||||
- you start writing the code following the dependency injection principle
|
||||
|
@ -204,7 +204,7 @@ How do I start doing the dependency injection?
|
|||
|
||||
What price do I pay and what do I get?
|
||||
- you need to explicitly specify the dependencies
|
||||
- it will be extra work in the beginning
|
||||
- it will be an extra work in the beginning
|
||||
- it will payoff as the project grows
|
||||
|
||||
Have a question?
|
||||
|
|
124
docs/conf.py
124
docs/conf.py
|
@ -20,49 +20,49 @@ import alabaster
|
|||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
sys.path.insert(0, os.path.abspath(".."))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
#needs_sphinx = "1.0"
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# extensions coming with Sphinx (named "sphinx.ext.*") or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'alabaster',
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinxcontrib.disqus',
|
||||
"alabaster",
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinxcontrib.disqus",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
# source_suffix = [".rst", ".md"]
|
||||
source_suffix = ".rst"
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
#source_encoding = "utf-8-sig"
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = u'Dependency Injector'
|
||||
copyright = u'2021, Roman Mogylatov'
|
||||
author = u'Roman Mogylatov'
|
||||
project = "Dependency Injector"
|
||||
copyright = "2021, Roman Mogylatov"
|
||||
author = "Roman Mogylatov"
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# The version info for the project you"re documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
# Getting version:
|
||||
with open('../src/dependency_injector/__init__.py') as init_file:
|
||||
version = re.search('__version__ = \'(.*?)\'', init_file.read()).group(1)
|
||||
with open("../src/dependency_injector/__init__.py") as init_file:
|
||||
version = re.search("__version__ = \"(.*?)\"", init_file.read()).group(1)
|
||||
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = version
|
||||
|
@ -76,19 +76,19 @@ language = None
|
|||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
#today = ""
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
#today_fmt = "%B %d, %Y"
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
exclude_patterns = ["_build"]
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
# If true, "()" will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
|
@ -100,7 +100,7 @@ exclude_patterns = ['_build']
|
|||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
pygments_style = "sphinx"
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
@ -116,8 +116,8 @@ todo_include_todos = False
|
|||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
# html_theme = 'sphinx_rtd_theme'
|
||||
html_theme = 'alabaster'
|
||||
# html_theme = "sphinx_rtd_theme"
|
||||
html_theme = "alabaster"
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
|
@ -141,21 +141,21 @@ html_theme_path = [alabaster.get_path()]
|
|||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
html_favicon = 'favicon.ico'
|
||||
html_favicon = "favicon.ico"
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
html_static_path = ["_static"]
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
#html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# If not "", a "Last updated on:" timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
#html_last_updated_fmt = "%b %d, %Y"
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
|
@ -189,50 +189,50 @@ html_static_path = ['_static']
|
|||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
#html_use_opensearch = ""
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Language to be used for generating the HTML full-text search index.
|
||||
# Sphinx supports the following languages:
|
||||
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
|
||||
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
|
||||
#html_search_language = 'en'
|
||||
# "da", "de", "en", "es", "fi", "fr", "hu", "it", "ja"
|
||||
# "nl", "no", "pt", "ro", "ru", "sv", "tr"
|
||||
#html_search_language = "en"
|
||||
|
||||
# A dictionary with options for the search language support, empty by default.
|
||||
# Now only 'ja' uses this config value
|
||||
#html_search_options = {'type': 'default'}
|
||||
# Now only "ja" uses this config value
|
||||
#html_search_options = {"type": "default"}
|
||||
|
||||
# The name of a javascript file (relative to the configuration directory) that
|
||||
# implements a search results scorer. If empty, the default will be used.
|
||||
#html_search_scorer = 'scorer.js'
|
||||
#html_search_scorer = "scorer.js"
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'dependency_injectordoc'
|
||||
htmlhelp_basename = "dependency_injectordoc"
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
# The paper size ("letterpaper" or "a4paper").
|
||||
#"papersize": "letterpaper",
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
# The font size ("10pt", "11pt" or "12pt").
|
||||
#"pointsize": "10pt",
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
#"preamble": "",
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#'figure_align': 'htbp',
|
||||
#"figure_align": "htbp",
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'dependency_injector.tex', u'Dependency Injector Documentation',
|
||||
u'Roman Mogylatov', 'manual'),
|
||||
(master_doc, "dependency_injector.tex", u"Dependency Injector Documentation",
|
||||
u"Roman Mogylatov", "manual"),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
|
@ -261,7 +261,7 @@ latex_documents = [
|
|||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'Dependency Injector', u'Dependency Injector Documentation',
|
||||
(master_doc, "Dependency Injector", u"Dependency Injector Documentation",
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
@ -275,9 +275,9 @@ man_pages = [
|
|||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'Dependency Injector', u'Dependency Injector Documentation',
|
||||
author, 'Dependency Injector', 'Dependency injection microframework for Python',
|
||||
'Miscellaneous'),
|
||||
(master_doc, "Dependency Injector", u"Dependency Injector Documentation",
|
||||
author, "Dependency Injector", "Dependency injection microframework for Python",
|
||||
"Miscellaneous"),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
|
@ -286,24 +286,24 @@ texinfo_documents = [
|
|||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
# How to display URL addresses: "footnote", "no", or "inline".
|
||||
#texinfo_show_urls = "footnote"
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
# If true, do not generate a @detailmenu in the "Top" node"s menu.
|
||||
#texinfo_no_detailmenu = False
|
||||
|
||||
autodoc_member_order = 'bysource'
|
||||
autodoc_member_order = "bysource"
|
||||
|
||||
disqus_shortname = 'python-dependency-injector'
|
||||
disqus_shortname = "python-dependency-injector"
|
||||
|
||||
html_theme_options = {
|
||||
'github_user': 'ets-labs',
|
||||
'github_repo': 'python-dependency-injector',
|
||||
'github_type': 'star',
|
||||
'github_button': True,
|
||||
'github_banner': True,
|
||||
'logo': 'logo.svg',
|
||||
'description': 'Dependency injection framework for Python by Roman Mogylatov',
|
||||
'code_font_size': '10pt',
|
||||
'analytics_id': 'UA-67012059-1',
|
||||
"github_user": "ets-labs",
|
||||
"github_repo": "python-dependency-injector",
|
||||
"github_type": "star",
|
||||
"github_button": True,
|
||||
"github_banner": True,
|
||||
"logo": "logo.svg",
|
||||
"description": "Dependency injection framework for Python by Roman Mogylatov",
|
||||
"code_font_size": "10pt",
|
||||
"analytics_id": "UA-67012059-1",
|
||||
}
|
||||
|
|
|
@ -34,10 +34,39 @@ Injections in the declarative container are done the usual way:
|
|||
:language: python
|
||||
:lines: 3-
|
||||
|
||||
You can override the container providers when you create the container instance:
|
||||
You can override container providers while creating a container instance:
|
||||
|
||||
.. literalinclude:: ../../examples/containers/declarative_override_providers.py
|
||||
:language: python
|
||||
:lines: 3-
|
||||
:emphasize-lines: 13
|
||||
|
||||
Alternatively, you can call ``container.override_providers()`` method when the container instance
|
||||
already exists:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 3
|
||||
|
||||
container = Container()
|
||||
|
||||
container.override_providers(foo=mock.Mock(Foo), bar=mock.Mock(Bar))
|
||||
|
||||
assert isinstance(container.foo(), mock.Mock)
|
||||
assert isinstance(container.bar(), mock.Mock)
|
||||
|
||||
You can also use ``container.override_providers()`` with a context manager to reset
|
||||
provided overriding after the context is closed:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 3
|
||||
|
||||
container = Container()
|
||||
|
||||
with container.override_providers(foo=mock.Mock(Foo), bar=mock.Mock(Bar)):
|
||||
assert isinstance(container.foo(), mock.Mock)
|
||||
assert isinstance(container.bar(), mock.Mock)
|
||||
|
||||
assert isinstance(container.foo(), Foo)
|
||||
assert isinstance(container.bar(), Bar)
|
||||
|
||||
.. disqus::
|
||||
|
|
|
@ -19,7 +19,7 @@ additional arguments.
|
|||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
instance = concrete_factory()
|
||||
# Same as: # instance = SomeClass(base_argument=1, extra_argument=2)
|
||||
|
||||
|
@ -43,21 +43,21 @@ Passing of the arguments works the same way like for any other :ref:`factory-pro
|
|||
providers.Factory(dict, arg1=1),
|
||||
arg2=2,
|
||||
)
|
||||
print(chained_dict_factory()) # prints: {'arg1': 1, 'arg2': 2}
|
||||
print(chained_dict_factory()) # prints: {"arg1": 1, "arg2": 2}
|
||||
|
||||
# 2. Keyword arguments of upper level factory have priority
|
||||
chained_dict_factory = providers.Factory(
|
||||
providers.Factory(dict, arg1=1),
|
||||
arg1=2,
|
||||
)
|
||||
print(chained_dict_factory()) # prints: {'arg1': 2}
|
||||
print(chained_dict_factory()) # prints: {"arg1": 2}
|
||||
|
||||
# 3. Keyword arguments provided from context have the most priority
|
||||
chained_dict_factory = providers.Factory(
|
||||
providers.Factory(dict, arg1=1),
|
||||
arg1=2,
|
||||
)
|
||||
print(chained_dict_factory(arg1=3)) # prints: {'arg1': 3}
|
||||
print(chained_dict_factory(arg1=3)) # prints: {"arg1": 3}
|
||||
|
||||
|
||||
Credits
|
||||
|
|
|
@ -20,7 +20,7 @@ additional arguments.
|
|||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
instance = concrete_factory()
|
||||
# Same as: # instance = SomeClass(base_argument=1, extra_argument=2)
|
||||
|
||||
|
@ -46,7 +46,7 @@ Passing of the arguments works the same way like for any other :ref:`factory-pro
|
|||
arg1=1,
|
||||
)
|
||||
dict_factory = factory_of_dict_factories(arg2=2)
|
||||
print(dict_factory()) # prints: {'arg1': 1, 'arg2': 2}
|
||||
print(dict_factory()) # prints: {"arg1": 1, "arg2": 2}
|
||||
|
||||
# 2. Keyword arguments of upper level factory have priority
|
||||
factory_of_dict_factories = providers.Factory(
|
||||
|
@ -55,7 +55,7 @@ Passing of the arguments works the same way like for any other :ref:`factory-pro
|
|||
arg1=1,
|
||||
)
|
||||
dict_factory = factory_of_dict_factories(arg1=2)
|
||||
print(dict_factory()) # prints: {'arg1': 2}
|
||||
print(dict_factory()) # prints: {"arg1": 2}
|
||||
|
||||
# 3. Keyword arguments provided from context have the most priority
|
||||
factory_of_dict_factories = providers.Factory(
|
||||
|
@ -64,7 +64,7 @@ Passing of the arguments works the same way like for any other :ref:`factory-pro
|
|||
arg1=1,
|
||||
)
|
||||
dict_factory = factory_of_dict_factories(arg1=2)
|
||||
print(dict_factory(arg1=3)) # prints: {'arg1': 3}
|
||||
print(dict_factory(arg1=3)) # prints: {"arg1": 3}
|
||||
|
||||
Credits
|
||||
-------
|
||||
|
|
|
@ -70,7 +70,7 @@ Tests use :ref:`provider-overriding` feature to replace giphy client with a mock
|
|||
|
||||
.. literalinclude:: ../../examples/miniapps/sanic/giphynavigator/tests.py
|
||||
:language: python
|
||||
:emphasize-lines: 27,54,68
|
||||
:emphasize-lines: 34,61,75
|
||||
|
||||
Sources
|
||||
-------
|
||||
|
|
|
@ -86,7 +86,7 @@ Key features of the ``Dependency Injector``:
|
|||
.. code-block:: python
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
@ -110,11 +110,11 @@ Key features of the ``Dependency Injector``:
|
|||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
container.config.api_key.from_env('API_KEY')
|
||||
container.config.timeout.from_env('TIMEOUT')
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
container.config.api_key.from_env("API_KEY")
|
||||
container.config.timeout.from_env("TIMEOUT")
|
||||
container.wire(modules=[__name__])
|
||||
|
||||
main() # <-- dependency is injected automatically
|
||||
|
||||
|
|
|
@ -67,8 +67,8 @@ Before:
|
|||
class ApiClient:
|
||||
|
||||
def __init__(self):
|
||||
self.api_key = os.getenv('API_KEY') # <-- dependency
|
||||
self.timeout = os.getenv('TIMEOUT') # <-- dependency
|
||||
self.api_key = os.getenv("API_KEY") # <-- dependency
|
||||
self.timeout = os.getenv("TIMEOUT") # <-- dependency
|
||||
|
||||
|
||||
class Service:
|
||||
|
@ -82,7 +82,7 @@ Before:
|
|||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
After:
|
||||
|
@ -109,12 +109,12 @@ After:
|
|||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main(
|
||||
service=Service(
|
||||
api_client=ApiClient(
|
||||
api_key=os.getenv('API_KEY'),
|
||||
timeout=os.getenv('TIMEOUT'),
|
||||
api_key=os.getenv("API_KEY"),
|
||||
timeout=os.getenv("TIMEOUT"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -136,8 +136,8 @@ Now you need to assemble and inject the objects like this:
|
|||
main(
|
||||
service=Service(
|
||||
api_client=ApiClient(
|
||||
api_key=os.getenv('API_KEY'),
|
||||
timeout=os.getenv('TIMEOUT'),
|
||||
api_key=os.getenv("API_KEY"),
|
||||
timeout=os.getenv("TIMEOUT"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -162,7 +162,7 @@ the dependency.
|
|||
.. code-block:: python
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
@ -186,11 +186,11 @@ the dependency.
|
|||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
container.config.api_key.from_env('API_KEY')
|
||||
container.config.timeout.from_env('TIMEOUT')
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
container.config.api_key.from_env("API_KEY")
|
||||
container.config.timeout.from_env("TIMEOUT")
|
||||
container.wire(modules=[__name__])
|
||||
|
||||
main() # <-- dependency is injected automatically
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ To verify the installed version:
|
|||
|
||||
>>> import dependency_injector
|
||||
>>> dependency_injector.__version__
|
||||
'4.0.0'
|
||||
'4.37.0'
|
||||
|
||||
.. note::
|
||||
When add ``Dependency Injector`` to the ``requirements.txt`` don't forget to pin version
|
||||
|
|
|
@ -7,6 +7,25 @@ that were made in every particular version.
|
|||
From version 0.7.6 *Dependency Injector* framework strictly
|
||||
follows `Semantic versioning`_
|
||||
|
||||
4.37.0
|
||||
------
|
||||
- Add support of Python 3.10.
|
||||
- Improve wiring with adding importing modules and packages from a string
|
||||
``container.wire(modules=["yourapp.module1"])``.
|
||||
- Add container wiring configuration ``wiring_config = containers.WiringConfiguration()``.
|
||||
- Add support of ``with`` statement for ``container.override_providers()`` method.
|
||||
- Add ``Configuration(yaml_files=[...])`` argument.
|
||||
- Add ``Configuration(ini_files=[...])`` argument.
|
||||
- Add ``Configuration(pydantic_settings=[...])`` argument.
|
||||
- Drop support of Python 3.4. There are no immediate breaking changes, but Dependency Injector
|
||||
will no longer be tested on Python 3.4 and any bugs will not be fixed.
|
||||
- Announce the date of dropping Python 3.5 support (Jan 1st 2022).
|
||||
- Fix ``Dependency.is_defined`` attribute to always return boolean value.
|
||||
- Fix ``envs_required=False`` behavior in ``Configuration.from_*()`` methods
|
||||
to give a priority to the explicitly provided value.
|
||||
- Update documentation and fix typos.
|
||||
- Regenerate C sources using Cython 0.29.24.
|
||||
- Migrate tests to ``pytest``.
|
||||
|
||||
4.36.2
|
||||
------
|
||||
|
|
|
@ -45,6 +45,21 @@ where ``examples/providers/configuration/config.ini`` is:
|
|||
.. literalinclude:: ../../examples/providers/configuration/config.ini
|
||||
:language: ini
|
||||
|
||||
Alternatively, you can provide a path to the INI file over the configuration provider argument. In that case,
|
||||
the container will call ``config.from_ini()`` automatically:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 3
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration(ini_files=["./config.ini"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
container = Container() # Config is loaded from ./config.ini
|
||||
|
||||
|
||||
:py:meth:`Configuration.from_ini` method supports environment variables interpolation.
|
||||
|
||||
.. code-block:: ini
|
||||
|
@ -72,6 +87,20 @@ where ``examples/providers/configuration/config.yml`` is:
|
|||
.. literalinclude:: ../../examples/providers/configuration/config.yml
|
||||
:language: ini
|
||||
|
||||
Alternatively, you can provide a path to the YAML file over the configuration provider argument. In that case,
|
||||
the container will call ``config.from_yaml()`` automatically:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 3
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration(yaml_files=["./config.yml"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
container = Container() # Config is loaded from ./config.yml
|
||||
|
||||
:py:meth:`Configuration.from_yaml` method supports environment variables interpolation.
|
||||
|
||||
.. code-block:: ini
|
||||
|
@ -91,7 +120,7 @@ To use another loader use ``loader`` argument:
|
|||
import yaml
|
||||
|
||||
|
||||
container.config.from_yaml('config.yml', loader=yaml.UnsafeLoader)
|
||||
container.config.from_yaml("config.yml", loader=yaml.UnsafeLoader)
|
||||
|
||||
.. note::
|
||||
|
||||
|
@ -123,7 +152,22 @@ If you need to pass an argument to this call, use ``.from_pydantic()`` keyword a
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
container.config.from_pydantic(Settings(), exclude={'optional'})
|
||||
container.config.from_pydantic(Settings(), exclude={"optional"})
|
||||
|
||||
Alternatively, you can provide a ``pydantic`` settings object over the configuration provider argument. In that case,
|
||||
the container will call ``config.from_pydantic()`` automatically:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 3
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration(pydantic_settings=[Settings()])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
container = Container() # Config is loaded from Settings()
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
|
@ -225,7 +269,7 @@ undefined environment variable that doesn't have a default value, pass argument
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
container.config.from_yaml('config.yml', envs_required=True)
|
||||
container.config.from_yaml("config.yml", envs_required=True)
|
||||
|
||||
See also: :ref:`configuration-strict-mode`.
|
||||
|
||||
|
@ -270,13 +314,13 @@ Mandatory YAML file:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
container.config.from_yaml('config.yaml', required=True)
|
||||
container.config.from_yaml("config.yaml", required=True)
|
||||
|
||||
Mandatory INI file:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
container.config.from_ini('config.ini', required=True)
|
||||
container.config.from_ini("config.ini", required=True)
|
||||
|
||||
Mandatory dictionary:
|
||||
|
||||
|
@ -288,7 +332,7 @@ Mandatory environment variable:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
container.config.api_key.from_env('API_KEY', required=True)
|
||||
container.config.api_key.from_env("API_KEY", required=True)
|
||||
|
||||
See also: :ref:`configuration-strict-mode`.
|
||||
|
||||
|
@ -346,16 +390,16 @@ configuration data is undefined:
|
|||
config = providers.Configuration(strict=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
|
||||
try:
|
||||
container.config.from_yaml('does-not_exist.yml') # raise exception
|
||||
container.config.from_yaml("does-not_exist.yml") # raise exception
|
||||
except FileNotFoundError:
|
||||
...
|
||||
|
||||
try:
|
||||
container.config.from_ini('does-not_exist.ini') # raise exception
|
||||
container.config.from_ini("does-not_exist.ini") # raise exception
|
||||
except FileNotFoundError:
|
||||
...
|
||||
|
||||
|
@ -365,7 +409,7 @@ configuration data is undefined:
|
|||
...
|
||||
|
||||
try:
|
||||
container.config.from_env('UNDEFINED_ENV_VAR') # raise exception
|
||||
container.config.from_env("UNDEFINED_ENV_VAR") # raise exception
|
||||
except ValueError:
|
||||
...
|
||||
|
||||
|
@ -385,7 +429,7 @@ an undefined environment variable without a default value.
|
|||
.. code-block:: python
|
||||
|
||||
try:
|
||||
container.config.from_yaml('undefined_env.yml') # raise exception
|
||||
container.config.from_yaml("undefined_env.yml") # raise exception
|
||||
except ValueError:
|
||||
...
|
||||
|
||||
|
@ -398,11 +442,11 @@ You can override ``.from_*()`` methods behaviour in strict mode using ``required
|
|||
config = providers.Configuration(strict=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
|
||||
container.config.from_yaml('config.yml')
|
||||
container.config.from_yaml('config.local.yml', required=False)
|
||||
container.config.from_yaml("config.yml")
|
||||
container.config.from_yaml("config.local.yml", required=False)
|
||||
|
||||
You can also use ``.required()`` option modifier when making an injection. It does not require to switch
|
||||
configuration provider to strict mode.
|
||||
|
|
|
@ -23,8 +23,8 @@ To use non-string keys or keys with ``.`` and ``-`` provide a dictionary as a po
|
|||
|
||||
providers.Dict({
|
||||
SomeClass: providers.Factory(...),
|
||||
'key.with.periods': providers.Factory(...),
|
||||
'key-with-dashes': providers.Factory(...),
|
||||
"key.with.periods": providers.Factory(...),
|
||||
"key-with-dashes": providers.Factory(...),
|
||||
})
|
||||
|
||||
Example:
|
||||
|
|
|
@ -98,7 +98,7 @@ you configure global resource:
|
|||
|
||||
configure_logging = providers.Resource(
|
||||
logging.config.fileConfig,
|
||||
fname='logging.ini',
|
||||
fname="logging.ini",
|
||||
)
|
||||
|
||||
Function initializer does not provide a way to specify custom resource shutdown.
|
||||
|
@ -210,8 +210,8 @@ first argument.
|
|||
|
||||
.. _resource-provider-wiring-closing:
|
||||
|
||||
Resources, wiring and per-function execution scope
|
||||
--------------------------------------------------
|
||||
Resources, wiring, and per-function execution scope
|
||||
---------------------------------------------------
|
||||
|
||||
You can compound ``Resource`` provider with :ref:`wiring` to implement per-function
|
||||
execution scope. For doing this you need to use additional ``Closing`` marker from
|
||||
|
@ -220,7 +220,7 @@ execution scope. For doing this you need to use additional ``Closing`` marker fr
|
|||
.. literalinclude:: ../../examples/wiring/flask_resource_closing.py
|
||||
:language: python
|
||||
:lines: 3-
|
||||
:emphasize-lines: 24
|
||||
:emphasize-lines: 22
|
||||
|
||||
Framework initializes and injects the resource into the function. With the ``Closing`` marker
|
||||
framework calls resource ``shutdown()`` method when function execution is over.
|
||||
|
@ -325,7 +325,7 @@ When you use resource provider with asynchronous initializer you need to call it
|
|||
connection = await container.connection.shutdown()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
Container ``init_resources()`` and ``shutdown_resources()`` methods should be used asynchronously if there is
|
||||
|
@ -349,7 +349,7 @@ at least one asynchronous resource provider:
|
|||
await container.shutdown_resources()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
See also:
|
||||
|
|
|
@ -30,7 +30,7 @@ IDE.
|
|||
provider = providers.Factory(Cat)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
animal = provider() # mypy knows that animal is of type "Cat"
|
||||
|
||||
|
||||
|
@ -54,5 +54,7 @@ function or method.
|
|||
provider: providers.Provider[Animal] = providers.Factory(Cat)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
animal = provider() # mypy knows that animal is of type "Animal"
|
||||
|
||||
.. disqus::
|
||||
|
|
|
@ -127,8 +127,6 @@ Now it's time to install the project requirements. We will use next packages:
|
|||
|
||||
- ``dependency-injector`` - the dependency injection framework
|
||||
- ``aiohttp`` - the web framework
|
||||
- ``aiohttp-devtools`` - the helper library that will provide a development server with live
|
||||
reloading
|
||||
- ``pyyaml`` - the YAML files parsing library, used for the reading of the configuration files
|
||||
- ``pytest-aiohttp`` - the helper library for the testing of the ``aiohttp`` application
|
||||
- ``pytest-cov`` - the helper library for measuring the test coverage
|
||||
|
@ -139,7 +137,6 @@ Put next lines into the ``requirements.txt`` file:
|
|||
|
||||
dependency-injector
|
||||
aiohttp
|
||||
aiohttp-devtools
|
||||
pyyaml
|
||||
pytest-aiohttp
|
||||
pytest-cov
|
||||
|
@ -177,16 +174,16 @@ Edit ``handlers.py``:
|
|||
|
||||
|
||||
async def index(request: web.Request) -> web.Response:
|
||||
query = request.query.get('query', 'Dependency Injector')
|
||||
limit = int(request.query.get('limit', 10))
|
||||
query = request.query.get("query", "Dependency Injector")
|
||||
limit = int(request.query.get("limit", 10))
|
||||
|
||||
gifs = []
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
'query': query,
|
||||
'limit': limit,
|
||||
'gifs': gifs,
|
||||
"query": query,
|
||||
"limit": limit,
|
||||
"gifs": gifs,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -228,30 +225,35 @@ Put next into the ``application.py``:
|
|||
app = web.Application()
|
||||
app.container = container
|
||||
app.add_routes([
|
||||
web.get('/', handlers.index),
|
||||
web.get("/", handlers.index),
|
||||
])
|
||||
return app
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = create_app()
|
||||
web.run_app(app)
|
||||
|
||||
Now we're ready to run our application
|
||||
|
||||
Do next in the terminal:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
adev runserver giphynavigator/application.py --livereload
|
||||
python -m giphynavigator.application
|
||||
|
||||
The output should be something like:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
[18:52:59] Starting aux server at http://localhost:8001 ◆
|
||||
[18:52:59] Starting dev server at http://localhost:8000 ●
|
||||
======== Running on http://0.0.0.0:8080 ========
|
||||
(Press CTRL+C to quit)
|
||||
|
||||
Let's check that it works. Open another terminal session and use ``httpie``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
http http://127.0.0.1:8000/
|
||||
http http://0.0.0.0:8080/
|
||||
|
||||
You should see:
|
||||
|
||||
|
@ -261,7 +263,7 @@ You should see:
|
|||
Content-Length: 844
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Date: Wed, 29 Jul 2020 21:01:50 GMT
|
||||
Server: Python/3.8 aiohttp/3.6.2
|
||||
Server: Python/3.10 aiohttp/3.6.2
|
||||
|
||||
{
|
||||
"gifs": [],
|
||||
|
@ -304,7 +306,7 @@ and put next into it:
|
|||
|
||||
class GiphyClient:
|
||||
|
||||
API_URL = 'https://api.giphy.com/v1'
|
||||
API_URL = "https://api.giphy.com/v1"
|
||||
|
||||
def __init__(self, api_key, timeout):
|
||||
self._api_key = api_key
|
||||
|
@ -312,11 +314,11 @@ and put next into it:
|
|||
|
||||
async def search(self, query, limit):
|
||||
"""Make search API call and return result."""
|
||||
url = f'{self.API_URL}/gifs/search'
|
||||
url = f"{self.API_URL}/gifs/search"
|
||||
params = {
|
||||
'q': query,
|
||||
'api_key': self._api_key,
|
||||
'limit': limit,
|
||||
"q": query,
|
||||
"api_key": self._api_key,
|
||||
"limit": limit,
|
||||
}
|
||||
async with ClientSession(timeout=self._timeout) as session:
|
||||
async with session.get(url, params=params) as response:
|
||||
|
@ -328,8 +330,10 @@ Now we need to add ``GiphyClient`` into the container. The ``GiphyClient`` has t
|
|||
that have to be injected: the API key and the request timeout. We will need to use two more
|
||||
providers from the ``dependency_injector.providers`` module:
|
||||
|
||||
- ``Factory`` provider that will create the ``GiphyClient`` client.
|
||||
- ``Configuration`` provider that will provide the API key and the request timeout.
|
||||
- ``Factory`` provider. It will create a ``GiphyClient`` client.
|
||||
- ``Configuration`` provider. It will provide an API key and a request timeout for the ``GiphyClient``
|
||||
client. We will specify the location of the configuration file. The configuration provider will parse
|
||||
the configuration file when we create a container instance.
|
||||
|
||||
Edit ``containers.py``:
|
||||
|
||||
|
@ -345,7 +349,7 @@ Edit ``containers.py``:
|
|||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
config = providers.Configuration(yaml_files=["config.yml"])
|
||||
|
||||
giphy_client = providers.Factory(
|
||||
giphy.GiphyClient,
|
||||
|
@ -353,18 +357,8 @@ Edit ``containers.py``:
|
|||
timeout=config.giphy.request_timeout,
|
||||
)
|
||||
|
||||
.. note::
|
||||
|
||||
We have used the configuration value before it was defined. That's the principle how the
|
||||
``Configuration`` provider works.
|
||||
|
||||
Use first, define later.
|
||||
|
||||
Now let's add the configuration file.
|
||||
|
||||
We will use YAML.
|
||||
|
||||
Create an empty file ``config.yml`` in the root root of the project:
|
||||
Now let's add the configuration file. We will use YAML. Create an empty file ``config.yml`` in
|
||||
the root root of the project:
|
||||
|
||||
.. code-block:: bash
|
||||
:emphasize-lines: 9
|
||||
|
@ -387,17 +381,14 @@ and put next into it:
|
|||
giphy:
|
||||
request_timeout: 10
|
||||
|
||||
We will use an environment variable ``GIPHY_API_KEY`` to provide the API key.
|
||||
|
||||
Now we need to edit ``create_app()`` to make two things when application starts:
|
||||
|
||||
- Load the configuration file the ``config.yml``.
|
||||
- Load the API key from the ``GIPHY_API_KEY`` environment variable.
|
||||
We will use the ``GIPHY_API_KEY`` environment variable to provide the API key. Let’s edit
|
||||
``create_app()`` to fetch the key value from it.
|
||||
|
||||
Edit ``application.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 11-12
|
||||
:emphasize-lines: 11
|
||||
|
||||
"""Application module."""
|
||||
|
||||
|
@ -409,17 +400,20 @@ Edit ``application.py``:
|
|||
|
||||
def create_app() -> web.Application:
|
||||
container = Container()
|
||||
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()
|
||||
app.container = container
|
||||
app.add_routes([
|
||||
web.get('/', handlers.index),
|
||||
web.get("/", handlers.index),
|
||||
])
|
||||
return app
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = create_app()
|
||||
web.run_app(app)
|
||||
|
||||
Now we need to create an API key and set it to the environment variable.
|
||||
|
||||
As for now, don’t worry, just take this one:
|
||||
|
@ -483,7 +477,7 @@ and put next into it:
|
|||
|
||||
result = await self._giphy_client.search(query, limit)
|
||||
|
||||
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 when we add ``SearchService`` to the container.
|
||||
|
@ -502,7 +496,7 @@ Edit ``containers.py``:
|
|||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
config = providers.Configuration(yaml_files=["config.yml"])
|
||||
|
||||
giphy_client = providers.Factory(
|
||||
giphy.GiphyClient,
|
||||
|
@ -531,7 +525,7 @@ Edit ``handlers.py``:
|
|||
"""Handlers module."""
|
||||
|
||||
from aiohttp import web
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
|
||||
from .services import SearchService
|
||||
from .containers import Container
|
||||
|
@ -542,60 +536,63 @@ Edit ``handlers.py``:
|
|||
request: web.Request,
|
||||
search_service: SearchService = Provide[Container.search_service],
|
||||
) -> web.Response:
|
||||
query = request.query.get('query', 'Dependency Injector')
|
||||
limit = int(request.query.get('limit', 10))
|
||||
query = request.query.get("query", "Dependency Injector")
|
||||
limit = int(request.query.get("limit", 10))
|
||||
|
||||
gifs = await search_service.search(query, limit)
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
'query': query,
|
||||
'limit': limit,
|
||||
'gifs': gifs,
|
||||
"query": query,
|
||||
"limit": limit,
|
||||
"gifs": gifs,
|
||||
},
|
||||
)
|
||||
|
||||
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.
|
||||
To make the injection work we need to wire the container with the ``handlers`` module.
|
||||
Let's configure the container to automatically make wiring with the ``handlers`` module when we
|
||||
create a container instance.
|
||||
|
||||
Edit ``application.py``:
|
||||
Edit ``containers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 13
|
||||
:emphasize-lines: 10
|
||||
|
||||
"""Application module."""
|
||||
"""Containers module."""
|
||||
|
||||
from aiohttp import web
|
||||
from dependency_injector import containers, providers
|
||||
|
||||
from .containers import Container
|
||||
from . import handlers
|
||||
from . import giphy, services
|
||||
|
||||
|
||||
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])
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
app = web.Application()
|
||||
app.container = container
|
||||
app.add_routes([
|
||||
web.get('/', handlers.index),
|
||||
])
|
||||
return app
|
||||
wiring_config = containers.WiringConfiguration(modules=[".handlers"])
|
||||
|
||||
Make sure the app is running or use:
|
||||
config = providers.Configuration(yaml_files=["config.yml"])
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
Make sure the app is running:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
adev runserver giphynavigator/application.py --livereload
|
||||
python -m giphynavigator.application
|
||||
|
||||
and make a request to the API in the terminal:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
http http://localhost:8000/ query=="wow,it works" limit==5
|
||||
http http://0.0.0.0:8080/ query=="wow,it works" limit==5
|
||||
|
||||
You should see:
|
||||
|
||||
|
@ -605,7 +602,7 @@ You should see:
|
|||
Content-Length: 492
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Date: Fri, 09 Oct 2020 01:35:48 GMT
|
||||
Server: Python/3.8 aiohttp/3.6.2
|
||||
Server: Python/3.10 aiohttp/3.6.2
|
||||
|
||||
{
|
||||
"gifs": [
|
||||
|
@ -651,7 +648,7 @@ Edit ``handlers.py``:
|
|||
"""Handlers module."""
|
||||
|
||||
from aiohttp import web
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
|
||||
from .services import SearchService
|
||||
from .containers import Container
|
||||
|
@ -664,16 +661,16 @@ Edit ``handlers.py``:
|
|||
default_query: str = Provide[Container.config.default.query],
|
||||
default_limit: int = Provide[Container.config.default.limit.as_int()],
|
||||
) -> web.Response:
|
||||
query = request.query.get('query', default_query)
|
||||
limit = int(request.query.get('limit', default_limit))
|
||||
query = request.query.get("query", default_query)
|
||||
limit = int(request.query.get("limit", default_limit))
|
||||
|
||||
gifs = await search_service.search(query, limit)
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
'query': query,
|
||||
'limit': limit,
|
||||
'gifs': gifs,
|
||||
"query": query,
|
||||
"limit": limit,
|
||||
"gifs": gifs,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -745,29 +742,29 @@ and put next into it:
|
|||
async def test_index(client, app):
|
||||
giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
|
||||
giphy_client_mock.search.return_value = {
|
||||
'data': [
|
||||
{'url': 'https://giphy.com/gif1.gif'},
|
||||
{'url': 'https://giphy.com/gif2.gif'},
|
||||
"data": [
|
||||
{"url": "https://giphy.com/gif1.gif"},
|
||||
{"url": "https://giphy.com/gif2.gif"},
|
||||
],
|
||||
}
|
||||
|
||||
with app.container.giphy_client.override(giphy_client_mock):
|
||||
response = await client.get(
|
||||
'/',
|
||||
"/",
|
||||
params={
|
||||
'query': 'test',
|
||||
'limit': 10,
|
||||
"query": "test",
|
||||
"limit": 10,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status == 200
|
||||
data = await response.json()
|
||||
assert data == {
|
||||
'query': 'test',
|
||||
'limit': 10,
|
||||
'gifs': [
|
||||
{'url': 'https://giphy.com/gif1.gif'},
|
||||
{'url': 'https://giphy.com/gif2.gif'},
|
||||
"query": "test",
|
||||
"limit": 10,
|
||||
"gifs": [
|
||||
{"url": "https://giphy.com/gif1.gif"},
|
||||
{"url": "https://giphy.com/gif2.gif"},
|
||||
],
|
||||
}
|
||||
|
||||
|
@ -775,30 +772,30 @@ and put next into it:
|
|||
async def test_index_no_data(client, app):
|
||||
giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
|
||||
giphy_client_mock.search.return_value = {
|
||||
'data': [],
|
||||
"data": [],
|
||||
}
|
||||
|
||||
with app.container.giphy_client.override(giphy_client_mock):
|
||||
response = await client.get('/')
|
||||
response = await client.get("/")
|
||||
|
||||
assert response.status == 200
|
||||
data = await response.json()
|
||||
assert data['gifs'] == []
|
||||
assert data["gifs"] == []
|
||||
|
||||
|
||||
async def test_index_default_params(client, app):
|
||||
giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
|
||||
giphy_client_mock.search.return_value = {
|
||||
'data': [],
|
||||
"data": [],
|
||||
}
|
||||
|
||||
with app.container.giphy_client.override(giphy_client_mock):
|
||||
response = await client.get('/')
|
||||
response = await client.get("/")
|
||||
|
||||
assert response.status == 200
|
||||
data = await response.json()
|
||||
assert data['query'] == app.container.config.default.query()
|
||||
assert data['limit'] == app.container.config.default.limit()
|
||||
assert data["query"] == app.container.config.default.query()
|
||||
assert data["limit"] == app.container.config.default.limit()
|
||||
|
||||
Now let's run it and check the coverage:
|
||||
|
||||
|
@ -810,24 +807,24 @@ You should see:
|
|||
|
||||
.. code-block::
|
||||
|
||||
platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
|
||||
plugins: cov-2.10.0, aiohttp-0.3.0, asyncio-0.14.0
|
||||
platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
|
||||
plugins: asyncio-0.16.0, anyio-3.3.4, aiohttp-0.3.0, cov-3.0.0
|
||||
collected 3 items
|
||||
|
||||
giphynavigator/tests.py ... [100%]
|
||||
|
||||
---------- coverage: platform darwin, python 3.8.3-final-0 -----------
|
||||
---------- coverage: platform darwin, python 3.10.0-final-0 ----------
|
||||
Name Stmts Miss Cover
|
||||
---------------------------------------------------
|
||||
giphynavigator/__init__.py 0 0 100%
|
||||
giphynavigator/application.py 12 0 100%
|
||||
giphynavigator/containers.py 6 0 100%
|
||||
giphynavigator/application.py 13 2 85%
|
||||
giphynavigator/containers.py 7 0 100%
|
||||
giphynavigator/giphy.py 14 9 36%
|
||||
giphynavigator/handlers.py 10 0 100%
|
||||
giphynavigator/services.py 9 1 89%
|
||||
giphynavigator/tests.py 37 0 100%
|
||||
---------------------------------------------------
|
||||
TOTAL 88 10 89%
|
||||
TOTAL 90 12 87%
|
||||
|
||||
.. note::
|
||||
|
||||
|
|
|
@ -59,8 +59,8 @@ The output should look something like:
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
Docker version 19.03.12, build 48a66213fe
|
||||
docker-compose version 1.26.2, build eefe0d31
|
||||
Docker version 20.10.5, build 55c4c88
|
||||
docker-compose version 1.29.0, build 07737305
|
||||
|
||||
.. note::
|
||||
|
||||
|
@ -129,13 +129,13 @@ Put next lines into the ``requirements.txt`` file:
|
|||
pytest-cov
|
||||
|
||||
Second, we need to create the ``Dockerfile``. It will describe the daemon's build process and
|
||||
specify how to run it. We will use ``python:3.8-buster`` as a base image.
|
||||
specify how to run it. We will use ``python:3.9-buster`` as a base image.
|
||||
|
||||
Put next lines into the ``Dockerfile`` file:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
FROM python:3.8-buster
|
||||
FROM python:3.10-buster
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
|
@ -204,11 +204,11 @@ Logging and configuration
|
|||
|
||||
In this section we will configure the logging and configuration file parsing.
|
||||
|
||||
Let's start with the the main part of our application - the container. Container will keep all of
|
||||
Let's start with the the main part of our application – the container. Container will keep all of
|
||||
the application components and their dependencies.
|
||||
|
||||
First two components that we're going to add are the config object and the provider for
|
||||
configuring the logging.
|
||||
First two components that we're going to add are the configuration provider and the resource provider
|
||||
for configuring the logging.
|
||||
|
||||
Put next lines into the ``containers.py`` file:
|
||||
|
||||
|
@ -224,7 +224,7 @@ Put next lines into the ``containers.py`` file:
|
|||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
config = providers.Configuration(yaml_files=["config.yml"])
|
||||
|
||||
logging = providers.Resource(
|
||||
logging.basicConfig,
|
||||
|
@ -233,16 +233,7 @@ Put next lines into the ``containers.py`` file:
|
|||
format=config.log.format,
|
||||
)
|
||||
|
||||
.. note::
|
||||
|
||||
We have used the configuration value before it was defined. That's the principle how the
|
||||
``Configuration`` provider works.
|
||||
|
||||
Use first, define later.
|
||||
|
||||
The configuration file will keep the logging settings.
|
||||
|
||||
Put next lines into the ``config.yml`` file:
|
||||
The configuration file will keep the logging settings. Put next lines into the ``config.yml`` file:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
@ -250,9 +241,10 @@ Put next lines into the ``config.yml`` file:
|
|||
level: "INFO"
|
||||
format: "[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s"
|
||||
|
||||
Now let's create the function that will run our daemon. It's traditionally called
|
||||
``main()``. The ``main()`` function will create the container. Then it will use the container
|
||||
to parse the ``config.yml`` file and call the logging configuration provider.
|
||||
Now let's create the function that will run our daemon. It's traditionally called ``main()``.
|
||||
The ``main()`` function will start the dispatcher, but we will keep it empty for now.
|
||||
We will create the container instance before calling ``main()`` in ``if __name__ == "__main__"``.
|
||||
Container instance will parse ``config.yml`` and then we will call the logging configuration provider.
|
||||
|
||||
Put next lines into the ``__main__.py`` file:
|
||||
|
||||
|
@ -267,9 +259,8 @@ Put next lines into the ``__main__.py`` file:
|
|||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
container.init_resources()
|
||||
|
||||
main()
|
||||
|
@ -356,7 +347,7 @@ and next into the ``dispatcher.py``:
|
|||
asyncio.run(self.start())
|
||||
|
||||
async def start(self) -> None:
|
||||
self._logger.info('Starting up')
|
||||
self._logger.info("Starting up")
|
||||
|
||||
for monitor in self._monitors:
|
||||
self._monitor_tasks.append(
|
||||
|
@ -376,11 +367,11 @@ and next into the ``dispatcher.py``:
|
|||
|
||||
self._stopping = True
|
||||
|
||||
self._logger.info('Shutting down')
|
||||
self._logger.info("Shutting down")
|
||||
for task, monitor in zip(self._monitor_tasks, self._monitors):
|
||||
task.cancel()
|
||||
self._monitor_tasks.clear()
|
||||
self._logger.info('Shutdown finished successfully')
|
||||
self._logger.info("Shutdown finished successfully")
|
||||
|
||||
@staticmethod
|
||||
async def _run_monitor(monitor: Monitor) -> None:
|
||||
|
@ -396,7 +387,7 @@ and next into the ``dispatcher.py``:
|
|||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception:
|
||||
monitor.logger.exception('Error executing monitor check')
|
||||
monitor.logger.exception("Error executing monitor check")
|
||||
|
||||
await asyncio.sleep(_until_next(last=time_start))
|
||||
|
||||
|
@ -419,7 +410,7 @@ Edit ``containers.py``:
|
|||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
config = providers.Configuration(yaml_files=["config.yml"])
|
||||
|
||||
logging = providers.Resource(
|
||||
logging.basicConfig,
|
||||
|
@ -442,13 +433,11 @@ and call the ``run()`` method. We will use :ref:`wiring` feature.
|
|||
Edit ``__main__.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 3-7,11-13,20
|
||||
:emphasize-lines: 3-5,9-11,17
|
||||
|
||||
"""Main module."""
|
||||
|
||||
import sys
|
||||
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
|
||||
from .dispatcher import Dispatcher
|
||||
from .containers import Container
|
||||
|
@ -459,11 +448,10 @@ Edit ``__main__.py``:
|
|||
dispatcher.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
container.init_resources()
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
container.wire(modules=[__name__])
|
||||
|
||||
main()
|
||||
|
||||
|
@ -561,7 +549,7 @@ Edit ``containers.py``:
|
|||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
config = providers.Configuration(yaml_files=["config.yml"])
|
||||
|
||||
logging = providers.Resource(
|
||||
logging.basicConfig,
|
||||
|
@ -613,10 +601,10 @@ Edit ``monitors.py``:
|
|||
options: Dict[str, Any],
|
||||
) -> None:
|
||||
self._client = http_client
|
||||
self._method = options.pop('method')
|
||||
self._url = options.pop('url')
|
||||
self._timeout = options.pop('timeout')
|
||||
super().__init__(check_every=options.pop('check_every'))
|
||||
self._method = options.pop("method")
|
||||
self._url = options.pop("url")
|
||||
self._timeout = options.pop("timeout")
|
||||
super().__init__(check_every=options.pop("check_every"))
|
||||
|
||||
async def check(self) -> None:
|
||||
time_start = time.time()
|
||||
|
@ -631,11 +619,11 @@ Edit ``monitors.py``:
|
|||
time_took = time_end - time_start
|
||||
|
||||
self.logger.info(
|
||||
'Check\n'
|
||||
' %s %s\n'
|
||||
' response code: %s\n'
|
||||
' content length: %s\n'
|
||||
' request took: %s seconds',
|
||||
"Check\n"
|
||||
" %s %s\n"
|
||||
" response code: %s\n"
|
||||
" content length: %s\n"
|
||||
" request took: %s seconds",
|
||||
self._method,
|
||||
self._url,
|
||||
response.status,
|
||||
|
@ -666,7 +654,7 @@ Edit ``containers.py``:
|
|||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
config = providers.Configuration(yaml_files=["config.yml"])
|
||||
|
||||
logging = providers.Resource(
|
||||
logging.basicConfig,
|
||||
|
@ -765,7 +753,7 @@ Edit ``containers.py``:
|
|||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
config = providers.Configuration(yaml_files=["config.yml"])
|
||||
|
||||
logging = providers.Resource(
|
||||
logging.basicConfig,
|
||||
|
@ -890,7 +878,7 @@ Create ``tests.py`` in the ``monitoringdaemon`` package:
|
|||
and put next into it:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 54,70-71
|
||||
:emphasize-lines: 54,70-73
|
||||
|
||||
"""Tests module."""
|
||||
|
||||
|
@ -911,33 +899,33 @@ and put next into it:
|
|||
|
||||
@pytest.fixture
|
||||
def container():
|
||||
container = Container()
|
||||
container.config.from_dict({
|
||||
'log': {
|
||||
'level': 'INFO',
|
||||
'formant': '[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s',
|
||||
},
|
||||
'monitors': {
|
||||
'example': {
|
||||
'method': 'GET',
|
||||
'url': 'http://fake-example.com',
|
||||
'timeout': 1,
|
||||
'check_every': 1,
|
||||
return Container(
|
||||
config={
|
||||
"log": {
|
||||
"level": "INFO",
|
||||
"formant": "[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s",
|
||||
},
|
||||
'httpbin': {
|
||||
'method': 'GET',
|
||||
'url': 'https://fake-httpbin.org/get',
|
||||
'timeout': 1,
|
||||
'check_every': 1,
|
||||
"monitors": {
|
||||
"example": {
|
||||
"method": "GET",
|
||||
"url": "http://fake-example.com",
|
||||
"timeout": 1,
|
||||
"check_every": 1,
|
||||
},
|
||||
"httpbin": {
|
||||
"method": "GET",
|
||||
"url": "https://fake-httpbin.org/get",
|
||||
"timeout": 1,
|
||||
"check_every": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return container
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_example_monitor(container, caplog):
|
||||
caplog.set_level('INFO')
|
||||
caplog.set_level("INFO")
|
||||
|
||||
http_client_mock = mock.AsyncMock()
|
||||
http_client_mock.request.return_value = RequestStub(
|
||||
|
@ -949,21 +937,22 @@ and put next into it:
|
|||
example_monitor = container.example_monitor()
|
||||
await example_monitor.check()
|
||||
|
||||
assert 'http://fake-example.com' in caplog.text
|
||||
assert 'response code: 200' in caplog.text
|
||||
assert 'content length: 635' in caplog.text
|
||||
assert "http://fake-example.com" in caplog.text
|
||||
assert "response code: 200" in caplog.text
|
||||
assert "content length: 635" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispatcher(container, caplog, event_loop):
|
||||
caplog.set_level('INFO')
|
||||
caplog.set_level("INFO")
|
||||
|
||||
example_monitor_mock = mock.AsyncMock()
|
||||
httpbin_monitor_mock = mock.AsyncMock()
|
||||
|
||||
with container.example_monitor.override(example_monitor_mock), \
|
||||
container.httpbin_monitor.override(httpbin_monitor_mock):
|
||||
|
||||
with container.override_providers(
|
||||
example_monitor=example_monitor_mock,
|
||||
httpbin_monitor=httpbin_monitor_mock,
|
||||
):
|
||||
dispatcher = container.dispatcher()
|
||||
event_loop.create_task(dispatcher.start())
|
||||
await asyncio.sleep(0.1)
|
||||
|
@ -982,25 +971,25 @@ You should see:
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
platform linux -- Python 3.8.3, pytest-6.0.1, py-1.9.0, pluggy-0.13.1
|
||||
platform linux -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
|
||||
rootdir: /code
|
||||
plugins: asyncio-0.14.0, cov-2.10.0
|
||||
plugins: asyncio-0.16.0, cov-3.0.0
|
||||
collected 2 items
|
||||
|
||||
monitoringdaemon/tests.py .. [100%]
|
||||
|
||||
----------- coverage: platform linux, python 3.8.3-final-0 -----------
|
||||
---------- coverage: platform linux, python 3.10.0-final-0 -----------
|
||||
Name Stmts Miss Cover
|
||||
----------------------------------------------------
|
||||
monitoringdaemon/__init__.py 0 0 100%
|
||||
monitoringdaemon/__main__.py 13 13 0%
|
||||
monitoringdaemon/__main__.py 11 11 0%
|
||||
monitoringdaemon/containers.py 11 0 100%
|
||||
monitoringdaemon/dispatcher.py 44 5 89%
|
||||
monitoringdaemon/dispatcher.py 45 5 89%
|
||||
monitoringdaemon/http.py 6 3 50%
|
||||
monitoringdaemon/monitors.py 23 1 96%
|
||||
monitoringdaemon/tests.py 37 0 100%
|
||||
monitoringdaemon/tests.py 35 0 100%
|
||||
----------------------------------------------------
|
||||
TOTAL 134 22 84%
|
||||
TOTAL 131 20 85%
|
||||
|
||||
.. note::
|
||||
|
||||
|
|
|
@ -160,19 +160,19 @@ Second put next in the ``fixtures.py``:
|
|||
|
||||
|
||||
SAMPLE_DATA = [
|
||||
('The Hunger Games: Mockingjay - Part 2', 2015, 'Francis Lawrence'),
|
||||
('Rogue One: A Star Wars Story', 2016, 'Gareth Edwards'),
|
||||
('The Jungle Book', 2016, 'Jon Favreau'),
|
||||
("The Hunger Games: Mockingjay - Part 2", 2015, "Francis Lawrence"),
|
||||
("Rogue One: A Star Wars Story", 2016, "Gareth Edwards"),
|
||||
("The Jungle Book", 2016, "Jon Favreau"),
|
||||
]
|
||||
|
||||
FILE = pathlib.Path(__file__)
|
||||
DIR = FILE.parent
|
||||
CSV_FILE = DIR / 'movies.csv'
|
||||
SQLITE_FILE = DIR / 'movies.db'
|
||||
CSV_FILE = DIR / "movies.csv"
|
||||
SQLITE_FILE = DIR / "movies.db"
|
||||
|
||||
|
||||
def create_csv(movies_data, path):
|
||||
with open(path, 'w') as opened_file:
|
||||
with open(path, "w") as opened_file:
|
||||
writer = csv.writer(opened_file)
|
||||
for row in movies_data:
|
||||
writer.writerow(row)
|
||||
|
@ -181,20 +181,20 @@ Second put next in the ``fixtures.py``:
|
|||
def create_sqlite(movies_data, path):
|
||||
with sqlite3.connect(path) as db:
|
||||
db.execute(
|
||||
'CREATE TABLE IF NOT EXISTS movies '
|
||||
'(title text, year int, director text)'
|
||||
"CREATE TABLE IF NOT EXISTS movies "
|
||||
"(title text, year int, director text)"
|
||||
)
|
||||
db.execute('DELETE FROM movies')
|
||||
db.executemany('INSERT INTO movies VALUES (?,?,?)', movies_data)
|
||||
db.execute("DELETE FROM movies")
|
||||
db.executemany("INSERT INTO movies VALUES (?,?,?)", movies_data)
|
||||
|
||||
|
||||
def main():
|
||||
create_csv(SAMPLE_DATA, CSV_FILE)
|
||||
create_sqlite(SAMPLE_DATA, SQLITE_FILE)
|
||||
print('OK')
|
||||
print("OK")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Now run in the terminal:
|
||||
|
@ -266,7 +266,7 @@ Edit ``__main__.py``:
|
|||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
|
||||
main()
|
||||
|
@ -321,7 +321,7 @@ and put next into it:
|
|||
self.director = str(director)
|
||||
|
||||
def __repr__(self):
|
||||
return '{0}(title={1}, year={2}, director={3})'.format(
|
||||
return "{0}(title={1}, year={2}, director={3})".format(
|
||||
self.__class__.__name__,
|
||||
repr(self.title),
|
||||
repr(self.year),
|
||||
|
@ -428,7 +428,7 @@ Edit ``containers.py``:
|
|||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
config = providers.Configuration(yaml_files=["config.yml"])
|
||||
|
||||
movie = providers.Factory(entities.Movie)
|
||||
|
||||
|
@ -445,15 +445,9 @@ This is also called the delegation of the provider. If we just pass the movie fa
|
|||
as the dependency, it will be called when csv finder is created and the ``Movie`` instance will
|
||||
be injected. With the ``.provider`` attribute the provider itself will be injected.
|
||||
|
||||
The csv finder also has a few dependencies on the configuration options. We added configuration
|
||||
provider to provide these dependencies.
|
||||
|
||||
.. note::
|
||||
|
||||
We have used the configuration value before it was defined. That's the principle how the
|
||||
Configuration provider works.
|
||||
|
||||
Use first, define later.
|
||||
The csv finder also has a few dependencies on the configuration options. We added a configuration
|
||||
provider to provide these dependencies and specified the location of the configuration file.
|
||||
The configuration provider will parse the configuration file when we create a container instance.
|
||||
|
||||
Not let's define the configuration values.
|
||||
|
||||
|
@ -467,29 +461,7 @@ Edit ``config.yml``:
|
|||
path: "data/movies.csv"
|
||||
delimiter: ","
|
||||
|
||||
The configuration file is ready. Now let's update the ``main()`` function to specify its location.
|
||||
|
||||
Edit ``__main__.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 12
|
||||
|
||||
"""Main module."""
|
||||
|
||||
from .containers import Container
|
||||
|
||||
|
||||
def main() -> None:
|
||||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
|
||||
main()
|
||||
|
||||
Move on to the lister.
|
||||
The configuration file is ready. Move on to the lister.
|
||||
|
||||
Create the ``listers.py`` in the ``movies`` package:
|
||||
|
||||
|
@ -552,7 +524,7 @@ and edit ``containers.py``:
|
|||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
config = providers.Configuration(yaml_files=["config.yml"])
|
||||
|
||||
movie = providers.Factory(entities.Movie)
|
||||
|
||||
|
@ -575,13 +547,11 @@ Let's inject the ``lister`` into the ``main()`` function.
|
|||
Edit ``__main__.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 3-7,11-12,19
|
||||
:emphasize-lines: 3-5,9-10,16
|
||||
|
||||
"""Main module."""
|
||||
|
||||
import sys
|
||||
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
|
||||
from .listers import MovieLister
|
||||
from .containers import Container
|
||||
|
@ -592,10 +562,9 @@ Edit ``__main__.py``:
|
|||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
container.wire(modules=[__name__])
|
||||
|
||||
main()
|
||||
|
||||
|
@ -607,13 +576,11 @@ Francis Lawrence and movies released in 2016.
|
|||
Edit ``__main__.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 13-19
|
||||
:emphasize-lines: 11-17
|
||||
|
||||
"""Main module."""
|
||||
|
||||
import sys
|
||||
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
|
||||
from .listers import MovieLister
|
||||
from .containers import Container
|
||||
|
@ -621,19 +588,18 @@ Edit ``__main__.py``:
|
|||
|
||||
@inject
|
||||
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("Francis Lawrence movies:")
|
||||
for movie in lister.movies_directed_by("Francis Lawrence"):
|
||||
print("\t-", movie)
|
||||
|
||||
print('2016 movies:')
|
||||
print("2016 movies:")
|
||||
for movie in lister.movies_released_in(2016):
|
||||
print('\t-', movie)
|
||||
print("\t-", movie)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
container.wire(modules=[__name__])
|
||||
|
||||
main()
|
||||
|
||||
|
@ -718,7 +684,7 @@ Edit ``finders.py``:
|
|||
|
||||
def find_all(self) -> List[Movie]:
|
||||
with self._database as db:
|
||||
rows = db.execute('SELECT title, year, director FROM movies')
|
||||
rows = db.execute("SELECT title, year, director FROM movies")
|
||||
return [self._movie_factory(*row) for row in rows]
|
||||
|
||||
Now we need to add the sqlite finder to the container and update lister's dependency to use it.
|
||||
|
@ -737,7 +703,7 @@ Edit ``containers.py``:
|
|||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
config = providers.Configuration(yaml_files=["config.yml"])
|
||||
|
||||
movie = providers.Factory(entities.Movie)
|
||||
|
||||
|
@ -826,7 +792,7 @@ Edit ``containers.py``:
|
|||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
config = providers.Configuration(yaml_files=["config.yml"])
|
||||
|
||||
movie = providers.Factory(entities.Movie)
|
||||
|
||||
|
@ -863,13 +829,11 @@ Now we need to read the value of the ``config.finder.type`` option from the envi
|
|||
Edit ``__main__.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 25
|
||||
:emphasize-lines: 22
|
||||
|
||||
"""Main module."""
|
||||
|
||||
import sys
|
||||
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
|
||||
from .listers import MovieLister
|
||||
from .containers import Container
|
||||
|
@ -877,19 +841,18 @@ Edit ``__main__.py``:
|
|||
|
||||
@inject
|
||||
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("Francis Lawrence movies:")
|
||||
for movie in lister.movies_directed_by("Francis Lawrence"):
|
||||
print("\t-", movie)
|
||||
|
||||
print('2016 movies:')
|
||||
print("2016 movies:")
|
||||
for movie in lister.movies_released_in(2016):
|
||||
print('\t-', movie)
|
||||
print("\t-", movie)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
container.config.finder.type.from_env('MOVIE_FINDER_TYPE')
|
||||
container.config.finder.type.from_env("MOVIE_FINDER_TYPE")
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
|
||||
main()
|
||||
|
@ -948,7 +911,7 @@ Create ``tests.py`` in the ``movies`` package:
|
|||
and put next into it:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 35,50
|
||||
:emphasize-lines: 36,51
|
||||
|
||||
"""Tests module."""
|
||||
|
||||
|
@ -961,42 +924,43 @@ and put next into it:
|
|||
|
||||
@pytest.fixture
|
||||
def container():
|
||||
container = Container()
|
||||
container.config.from_dict({
|
||||
'finder': {
|
||||
'type': 'csv',
|
||||
'csv': {
|
||||
'path': '/fake-movies.csv',
|
||||
'delimiter': ',',
|
||||
},
|
||||
'sqlite': {
|
||||
'path': '/fake-movies.db',
|
||||
container = Container(
|
||||
config={
|
||||
"finder": {
|
||||
"type": "csv",
|
||||
"csv": {
|
||||
"path": "/fake-movies.csv",
|
||||
"delimiter": ",",
|
||||
},
|
||||
"sqlite": {
|
||||
"path": "/fake-movies.db",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
return container
|
||||
|
||||
|
||||
def test_movies_directed_by(container):
|
||||
finder_mock = mock.Mock()
|
||||
finder_mock.find_all.return_value = [
|
||||
container.movie('The 33', 2015, 'Patricia Riggen'),
|
||||
container.movie('The Jungle Book', 2016, 'Jon Favreau'),
|
||||
container.movie("The 33", 2015, "Patricia Riggen"),
|
||||
container.movie("The Jungle Book", 2016, "Jon Favreau"),
|
||||
]
|
||||
|
||||
with container.finder.override(finder_mock):
|
||||
lister = container.lister()
|
||||
movies = lister.movies_directed_by('Jon Favreau')
|
||||
movies = lister.movies_directed_by("Jon Favreau")
|
||||
|
||||
assert len(movies) == 1
|
||||
assert movies[0].title == 'The Jungle Book'
|
||||
assert movies[0].title == "The Jungle Book"
|
||||
|
||||
|
||||
def test_movies_released_in(container):
|
||||
finder_mock = mock.Mock()
|
||||
finder_mock.find_all.return_value = [
|
||||
container.movie('The 33', 2015, 'Patricia Riggen'),
|
||||
container.movie('The Jungle Book', 2016, 'Jon Favreau'),
|
||||
container.movie("The 33", 2015, "Patricia Riggen"),
|
||||
container.movie("The Jungle Book", 2016, "Jon Favreau"),
|
||||
]
|
||||
|
||||
with container.finder.override(finder_mock):
|
||||
|
@ -1004,7 +968,7 @@ and put next into it:
|
|||
movies = lister.movies_released_in(2015)
|
||||
|
||||
assert len(movies) == 1
|
||||
assert movies[0].title == 'The 33'
|
||||
assert movies[0].title == "The 33"
|
||||
|
||||
Run in the terminal:
|
||||
|
||||
|
@ -1016,24 +980,24 @@ You should see:
|
|||
|
||||
.. code-block::
|
||||
|
||||
platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
|
||||
plugins: cov-2.10.0
|
||||
platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
|
||||
plugins: cov-3.0.0
|
||||
collected 2 items
|
||||
|
||||
movies/tests.py .. [100%]
|
||||
|
||||
---------- coverage: platform darwin, python 3.8.5-final-0 -----------
|
||||
---------- coverage: platform darwin, python 3.10 -----------
|
||||
Name Stmts Miss Cover
|
||||
------------------------------------------
|
||||
movies/__init__.py 0 0 100%
|
||||
movies/__main__.py 18 18 0%
|
||||
movies/__main__.py 16 16 0%
|
||||
movies/containers.py 9 0 100%
|
||||
movies/entities.py 7 1 86%
|
||||
movies/finders.py 26 13 50%
|
||||
movies/listers.py 8 0 100%
|
||||
movies/tests.py 24 0 100%
|
||||
movies/tests.py 23 0 100%
|
||||
------------------------------------------
|
||||
TOTAL 92 32 65%
|
||||
TOTAL 89 30 66%
|
||||
|
||||
.. note::
|
||||
|
||||
|
@ -1053,7 +1017,7 @@ We've used the ``Dependency Injector`` as a dependency injection framework.
|
|||
With a help of :ref:`containers` and :ref:`providers` we have defined how to assemble application components.
|
||||
|
||||
``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.
|
||||
:ref:`configuration-provider` helped to deal with reading a YAML file and environment variables.
|
||||
|
||||
We used :ref:`wiring` feature to inject the dependencies into the ``main()`` function.
|
||||
:ref:`provider-overriding` feature helped in testing.
|
||||
|
|
|
@ -110,9 +110,9 @@ You should see something like:
|
|||
.. code-block:: bash
|
||||
|
||||
(venv) $ python -c "import dependency_injector; print(dependency_injector.__version__)"
|
||||
4.0.0
|
||||
4.37.0
|
||||
(venv) $ python -c "import flask; print(flask.__version__)"
|
||||
1.1.2
|
||||
2.0.2
|
||||
|
||||
*Versions can be different. That's fine.*
|
||||
|
||||
|
@ -129,7 +129,7 @@ Put next into the ``views.py``:
|
|||
|
||||
|
||||
def index():
|
||||
return 'Hello, World!'
|
||||
return "Hello, World!"
|
||||
|
||||
Ok, we have the view.
|
||||
|
||||
|
@ -170,7 +170,7 @@ Put next into the ``application.py``:
|
|||
|
||||
app = Flask(__name__)
|
||||
app.container = container
|
||||
app.add_url_rule('/', 'index', views.index)
|
||||
app.add_url_rule("/", "index", views.index)
|
||||
|
||||
return app
|
||||
|
||||
|
@ -246,7 +246,7 @@ Edit ``application.py``:
|
|||
|
||||
app = Flask(__name__)
|
||||
app.container = container
|
||||
app.add_url_rule('/', 'index', views.index)
|
||||
app.add_url_rule("/", "index", views.index)
|
||||
|
||||
bootstrap = Bootstrap()
|
||||
bootstrap.init_app(app)
|
||||
|
@ -398,13 +398,13 @@ Edit ``views.py``:
|
|||
|
||||
|
||||
def index():
|
||||
query = request.args.get('query', 'Dependency Injector')
|
||||
limit = request.args.get('limit', 10, int)
|
||||
query = request.args.get("query", "Dependency Injector")
|
||||
limit = request.args.get("limit", 10, int)
|
||||
|
||||
repositories = []
|
||||
|
||||
return render_template(
|
||||
'index.html',
|
||||
"index.html",
|
||||
query=query,
|
||||
limit=limit,
|
||||
repositories=repositories,
|
||||
|
@ -444,9 +444,10 @@ and run in the terminal:
|
|||
Now we need to add Github API client the container. We will need to add two more providers from
|
||||
the ``dependency_injector.providers`` module:
|
||||
|
||||
- ``Factory`` provider that will create ``Github`` client.
|
||||
- ``Configuration`` provider that will be used for providing the API token and the request timeout
|
||||
for the ``Github`` client.
|
||||
- ``Factory`` provider. It will create a ``Github`` client.
|
||||
- ``Configuration`` provider. It will provide an API token and a request timeout for the ``Github`` client.
|
||||
We will specify the location of the configuration file. The configuration provider will parse
|
||||
the configuration file when we create a container instance.
|
||||
|
||||
Edit ``containers.py``:
|
||||
|
||||
|
@ -461,7 +462,7 @@ Edit ``containers.py``:
|
|||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
config = providers.Configuration(yaml_files=["config.yml"])
|
||||
|
||||
github_client = providers.Factory(
|
||||
Github,
|
||||
|
@ -469,23 +470,14 @@ Edit ``containers.py``:
|
|||
timeout=config.github.request_timeout,
|
||||
)
|
||||
|
||||
.. note::
|
||||
|
||||
We have used the configuration value before it was defined. That's the principle how
|
||||
``Configuration`` provider works.
|
||||
|
||||
Use first, define later.
|
||||
|
||||
.. note::
|
||||
|
||||
Don't forget to remove the Ellipsis ``...`` from the container. We don't need it anymore
|
||||
since we container is not empty.
|
||||
|
||||
Now let's add the configuration file.
|
||||
|
||||
We will use YAML.
|
||||
|
||||
Create an empty file ``config.yml`` in the root of the project:
|
||||
Now let's add the configuration file. We will use YAML. Create an empty file ``config.yml``
|
||||
in the root of the project:
|
||||
|
||||
.. code-block:: bash
|
||||
:emphasize-lines: 11
|
||||
|
@ -530,17 +522,13 @@ and install it:
|
|||
|
||||
pip install -r requirements.txt
|
||||
|
||||
We will use environment variable ``GITHUB_TOKEN`` to provide the API token.
|
||||
|
||||
Now we need to edit ``create_app()`` to make two things when application starts:
|
||||
|
||||
- Load the configuration file the ``config.yml``.
|
||||
- Load the API token from the ``GITHUB_TOKEN`` environment variable.
|
||||
We will use the ``GITHUB_TOKEN`` environment variable to provide the API token. Let's edit
|
||||
``create_app()`` to fetch the token value from it.
|
||||
|
||||
Edit ``application.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 12-13
|
||||
:emphasize-lines: 12
|
||||
|
||||
"""Application module."""
|
||||
|
||||
|
@ -553,12 +541,11 @@ Edit ``application.py``:
|
|||
|
||||
def create_app() -> Flask:
|
||||
container = Container()
|
||||
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 = Flask(__name__)
|
||||
app.container = container
|
||||
app.add_url_rule('/', 'index', views.index)
|
||||
app.add_url_rule("/", "index", views.index)
|
||||
|
||||
bootstrap = Bootstrap()
|
||||
bootstrap.init_app(app)
|
||||
|
@ -639,7 +626,7 @@ and put next into it:
|
|||
"""Search for repositories and return formatted data."""
|
||||
repositories = self._github_client.search_repositories(
|
||||
query=query,
|
||||
**{'in': 'name'},
|
||||
**{"in": "name"},
|
||||
)
|
||||
return [
|
||||
self._format_repo(repository)
|
||||
|
@ -649,22 +636,22 @@ and put next into it:
|
|||
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,
|
||||
"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 {},
|
||||
"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,
|
||||
"sha": commit.sha,
|
||||
"url": commit.html_url,
|
||||
"message": commit.commit.message,
|
||||
"author_name": commit.commit.author.name,
|
||||
}
|
||||
|
||||
Now let's add ``SearchService`` to the container.
|
||||
|
@ -684,7 +671,7 @@ Edit ``containers.py``:
|
|||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
config = providers.Configuration(yaml_files=["config.yml"])
|
||||
|
||||
github_client = providers.Factory(
|
||||
Github,
|
||||
|
@ -720,50 +707,51 @@ Edit ``views.py``:
|
|||
|
||||
@inject
|
||||
def index(search_service: SearchService = Provide[Container.search_service]):
|
||||
query = request.args.get('query', 'Dependency Injector')
|
||||
limit = request.args.get('limit', 10, int)
|
||||
query = request.args.get("query", "Dependency Injector")
|
||||
limit = request.args.get("limit", 10, int)
|
||||
|
||||
repositories = search_service.search_repositories(query, limit)
|
||||
|
||||
return render_template(
|
||||
'index.html',
|
||||
"index.html",
|
||||
query=query,
|
||||
limit=limit,
|
||||
repositories=repositories,
|
||||
)
|
||||
|
||||
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.
|
||||
To make the injection work we need to wire the container with the ``views`` module.
|
||||
Let's configure the container to automatically make wiring with the ``views`` module when we
|
||||
create a container instance.
|
||||
|
||||
Edit ``application.py``:
|
||||
Edit ``containers.py``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 14
|
||||
:emphasize-lines: 11
|
||||
|
||||
"""Application module."""
|
||||
"""Containers module."""
|
||||
|
||||
from flask import Flask
|
||||
from flask_bootstrap import Bootstrap
|
||||
from dependency_injector import containers, providers
|
||||
from github import Github
|
||||
|
||||
from .containers import Container
|
||||
from . import views
|
||||
from . import services
|
||||
|
||||
|
||||
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])
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
app = Flask(__name__)
|
||||
app.container = container
|
||||
app.add_url_rule('/', 'index', views.index)
|
||||
wiring_config = containers.WiringConfiguration(modules=[".views"])
|
||||
|
||||
bootstrap = Bootstrap()
|
||||
bootstrap.init_app(app)
|
||||
config = providers.Configuration(yaml_files=["config.yml"])
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
Make sure the app is running or use ``flask run`` and open ``http://127.0.0.1:5000/``.
|
||||
|
||||
|
@ -801,13 +789,13 @@ Edit ``views.py``:
|
|||
default_query: str = Provide[Container.config.default.query],
|
||||
default_limit: int = Provide[Container.config.default.limit.as_int()],
|
||||
):
|
||||
query = request.args.get('query', default_query)
|
||||
limit = request.args.get('limit', default_limit, int)
|
||||
query = request.args.get("query", default_query)
|
||||
limit = request.args.get("limit", default_limit, int)
|
||||
|
||||
repositories = search_service.search_repositories(query, limit)
|
||||
|
||||
return render_template(
|
||||
'index.html',
|
||||
"index.html",
|
||||
query=query,
|
||||
limit=limit,
|
||||
repositories=repositories,
|
||||
|
@ -900,44 +888,44 @@ and put next into it:
|
|||
github_client_mock = mock.Mock(spec=Github)
|
||||
github_client_mock.search_repositories.return_value = [
|
||||
mock.Mock(
|
||||
html_url='repo1-url',
|
||||
name='repo1-name',
|
||||
html_url="repo1-url",
|
||||
name="repo1-name",
|
||||
owner=mock.Mock(
|
||||
login='owner1-login',
|
||||
html_url='owner1-url',
|
||||
avatar_url='owner1-avatar-url',
|
||||
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',
|
||||
html_url="repo2-url",
|
||||
name="repo2-name",
|
||||
owner=mock.Mock(
|
||||
login='owner2-login',
|
||||
html_url='owner2-url',
|
||||
avatar_url='owner2-avatar-url',
|
||||
login="owner2-login",
|
||||
html_url="owner2-url",
|
||||
avatar_url="owner2-avatar-url",
|
||||
),
|
||||
get_commits=mock.Mock(return_value=[mock.Mock()]),
|
||||
),
|
||||
]
|
||||
|
||||
with app.container.github_client.override(github_client_mock):
|
||||
response = client.get(url_for('index'))
|
||||
response = client.get(url_for("index"))
|
||||
|
||||
assert response.status_code == 200
|
||||
assert b'Results found: 2' in response.data
|
||||
assert b"Results found: 2" in response.data
|
||||
|
||||
assert b'repo1-url' in response.data
|
||||
assert b'repo1-name' in response.data
|
||||
assert b'owner1-login' in response.data
|
||||
assert b'owner1-url' in response.data
|
||||
assert b'owner1-avatar-url' in response.data
|
||||
assert b"repo1-url" in response.data
|
||||
assert b"repo1-name" in response.data
|
||||
assert b"owner1-login" in response.data
|
||||
assert b"owner1-url" in response.data
|
||||
assert b"owner1-avatar-url" in response.data
|
||||
|
||||
assert b'repo2-url' in response.data
|
||||
assert b'repo2-name' in response.data
|
||||
assert b'owner2-login' in response.data
|
||||
assert b'owner2-url' in response.data
|
||||
assert b'owner2-avatar-url' in response.data
|
||||
assert b"repo2-url" in response.data
|
||||
assert b"repo2-name" in response.data
|
||||
assert b"owner2-login" in response.data
|
||||
assert b"owner2-url" in response.data
|
||||
assert b"owner2-avatar-url" in response.data
|
||||
|
||||
|
||||
def test_index_no_results(client, app):
|
||||
|
@ -945,10 +933,10 @@ and put next into it:
|
|||
github_client_mock.search_repositories.return_value = []
|
||||
|
||||
with app.container.github_client.override(github_client_mock):
|
||||
response = client.get(url_for('index'))
|
||||
response = client.get(url_for("index"))
|
||||
|
||||
assert response.status_code == 200
|
||||
assert b'Results found: 0' in response.data
|
||||
assert b"Results found: 0" in response.data
|
||||
|
||||
Now let's run it and check the coverage:
|
||||
|
||||
|
@ -960,23 +948,23 @@ You should see:
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
|
||||
plugins: flask-1.0.0, cov-2.10.0
|
||||
platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
|
||||
plugins: cov-3.0.0, flask-1.2.0
|
||||
collected 2 items
|
||||
|
||||
githubnavigator/tests.py .. [100%]
|
||||
|
||||
---------- coverage: platform darwin, python 3.8.3-final-0 -----------
|
||||
---------- coverage: platform darwin, python 3.10.0-final-0 ----------
|
||||
Name Stmts Miss Cover
|
||||
----------------------------------------------------
|
||||
githubnavigator/__init__.py 0 0 100%
|
||||
githubnavigator/application.py 15 0 100%
|
||||
githubnavigator/containers.py 7 0 100%
|
||||
githubnavigator/application.py 13 0 100%
|
||||
githubnavigator/containers.py 8 0 100%
|
||||
githubnavigator/services.py 14 0 100%
|
||||
githubnavigator/tests.py 34 0 100%
|
||||
githubnavigator/views.py 10 0 100%
|
||||
----------------------------------------------------
|
||||
TOTAL 80 0 100%
|
||||
TOTAL 79 0 100%
|
||||
|
||||
.. note::
|
||||
|
||||
|
|
167
docs/wiring.rst
167
docs/wiring.rst
|
@ -95,17 +95,17 @@ Also you can use ``Provide`` marker to inject a container.
|
|||
|
||||
.. literalinclude:: ../examples/wiring/example_container.py
|
||||
:language: python
|
||||
:emphasize-lines: 16-19
|
||||
:emphasize-lines: 14-17
|
||||
:lines: 3-
|
||||
|
||||
Strings identifiers
|
||||
-------------------
|
||||
String identifiers
|
||||
------------------
|
||||
|
||||
You can use wiring with string identifiers. String identifier should match provider name in the container:
|
||||
|
||||
.. literalinclude:: ../examples/wiring/example_string_id.py
|
||||
:language: python
|
||||
:emphasize-lines: 17
|
||||
:emphasize-lines: 15
|
||||
:lines: 3-
|
||||
|
||||
With string identifiers you don't need to use a container to specify an injection.
|
||||
|
@ -115,7 +115,7 @@ To specify an injection from a nested container use point ``.`` as a separator:
|
|||
.. code-block:: python
|
||||
|
||||
@inject
|
||||
def foo(service: UserService = Provide['services.user']) -> None:
|
||||
def foo(service: UserService = Provide["services.user"]) -> None:
|
||||
...
|
||||
|
||||
You can also use injection modifiers:
|
||||
|
@ -135,34 +135,34 @@ You can also use injection modifiers:
|
|||
|
||||
|
||||
@inject
|
||||
def foo(value: int = Provide['config.option', as_int()]) -> None:
|
||||
def foo(value: int = Provide["config.option", as_int()]) -> None:
|
||||
...
|
||||
|
||||
|
||||
@inject
|
||||
def foo(value: float = Provide['config.option', as_float()]) -> None:
|
||||
def foo(value: float = Provide["config.option", as_float()]) -> None:
|
||||
...
|
||||
|
||||
|
||||
@inject
|
||||
def foo(value: Decimal = Provide['config.option', as_(Decimal)]) -> None:
|
||||
def foo(value: Decimal = Provide["config.option", as_(Decimal)]) -> None:
|
||||
...
|
||||
|
||||
@inject
|
||||
def foo(value: str = Provide['config.option', required()]) -> None:
|
||||
def foo(value: str = Provide["config.option", required()]) -> None:
|
||||
...
|
||||
|
||||
@inject
|
||||
def foo(value: int = Provide['config.option', required().as_int()]) -> None:
|
||||
def foo(value: int = Provide["config.option", required().as_int()]) -> None:
|
||||
...
|
||||
|
||||
|
||||
@inject
|
||||
def foo(value: int = Provide['config.option', invariant('config.switch')]) -> None:
|
||||
def foo(value: int = Provide["config.option", invariant("config.switch")]) -> None:
|
||||
...
|
||||
|
||||
@inject
|
||||
def foo(value: int = Provide['service', provided().foo['bar'].call()]) -> None:
|
||||
def foo(value: int = Provide["service", provided().foo["bar"].call()]) -> None:
|
||||
...
|
||||
|
||||
|
||||
|
@ -171,7 +171,7 @@ To inject a container use special identifier ``<container>``:
|
|||
.. code-block:: python
|
||||
|
||||
@inject
|
||||
def foo(container: Container = Provide['<container>']) -> None:
|
||||
def foo(container: Container = Provide["<container>"]) -> None:
|
||||
...
|
||||
|
||||
|
||||
|
@ -183,25 +183,63 @@ You can use wiring to make injections into modules and class attributes.
|
|||
.. literalinclude:: ../examples/wiring/example_attribute.py
|
||||
:language: python
|
||||
:lines: 3-
|
||||
:emphasize-lines: 16,21
|
||||
:emphasize-lines: 14,19
|
||||
|
||||
You could also use string identifiers to avoid a dependency on a container:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 1,6
|
||||
|
||||
service: Service = Provide['service']
|
||||
service: Service = Provide["service"]
|
||||
|
||||
|
||||
class Main:
|
||||
|
||||
service: Service = Provide['service']
|
||||
service: Service = Provide["service"]
|
||||
|
||||
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.
|
||||
To wire a container with the modules you need to call ``container.wire()`` method:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
container.wire(
|
||||
modules=[
|
||||
"yourapp.module1",
|
||||
"yourapp.module2",
|
||||
],
|
||||
)
|
||||
|
||||
Method ``container.wire()`` can resolve relative imports:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# In module "yourapp.main":
|
||||
|
||||
container.wire(
|
||||
modules=[
|
||||
".module1", # Resolved to: "yourapp.module1"
|
||||
".module2", # Resolved to: "yourapp.module2"
|
||||
],
|
||||
)
|
||||
|
||||
You can also manually specify a base package for resolving relative imports with
|
||||
the ``from_package`` argument:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# In module "yourapp.main":
|
||||
|
||||
container.wire(
|
||||
modules=[
|
||||
".module1", # Resolved to: "anotherapp.module1"
|
||||
".module2", # Resolved to: "anotherapp.module2"
|
||||
],
|
||||
from_package="anotherapp",
|
||||
)
|
||||
|
||||
Argument ``modules`` can also take already imported modules:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -211,15 +249,16 @@ To wire a container with a module you need to call ``container.wire(modules=[...
|
|||
container = Container()
|
||||
container.wire(modules=[module1, module2])
|
||||
|
||||
You can wire container with a package. Container walks recursively over package modules.
|
||||
You can wire container with a package. Container walks recursively over the package modules:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from yourapp import package1, package2
|
||||
|
||||
|
||||
container = Container()
|
||||
container.wire(packages=[package1, package2])
|
||||
container.wire(
|
||||
packages=[
|
||||
"yourapp.package1",
|
||||
"yourapp.package2",
|
||||
],
|
||||
)
|
||||
|
||||
Arguments ``modules`` and ``packages`` can be used together.
|
||||
|
||||
|
@ -233,7 +272,7 @@ When wiring is done functions and methods with the markers are patched to provid
|
|||
|
||||
|
||||
container = Container()
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
container.wire(modules=[__name__])
|
||||
|
||||
foo() # <--- Argument "bar" is injected
|
||||
|
||||
|
@ -267,7 +306,7 @@ You can use that in testing to re-create and re-wire a container before each tes
|
|||
|
||||
def setUp(self):
|
||||
self.container = Container()
|
||||
self.container.wire(modules=[module1, module2])
|
||||
self.container.wire(modules=["yourapp.module1", "yourapp.module2"])
|
||||
self.addCleanup(self.container.unwire)
|
||||
|
||||
.. code-block:: python
|
||||
|
@ -278,7 +317,7 @@ You can use that in testing to re-create and re-wire a container before each tes
|
|||
@pytest.fixture
|
||||
def container():
|
||||
container = Container()
|
||||
container.wire(modules=[module1, module2])
|
||||
container.wire(modules=["yourapp.module1", "yourapp.module2"])
|
||||
yield container
|
||||
container.unwire()
|
||||
|
||||
|
@ -309,6 +348,76 @@ You can use that in testing to re-create and re-wire a container before each tes
|
|||
|
||||
module.fn()
|
||||
|
||||
Wiring configuration
|
||||
--------------------
|
||||
|
||||
You can specify wiring configuration in the container. When wiring configuration is defined,
|
||||
container will call method ``.wire()`` automatically when you create an instance:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
wiring_config = containers.WiringConfiguration(
|
||||
modules=[
|
||||
"yourapp.module1",
|
||||
"yourapp.module2",
|
||||
],
|
||||
packages=[
|
||||
"yourapp.package1",
|
||||
"yourapp.package2",
|
||||
],
|
||||
)
|
||||
|
||||
...
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
container = Container() # container.wire() is called automatically
|
||||
...
|
||||
|
||||
You can also use relative imports. Container will resolve them corresponding
|
||||
to the module of the container class:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# In module "yourapp.container":
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
wiring_config = containers.WiringConfiguration(
|
||||
modules=[
|
||||
".module1", # Resolved to: "yourapp.module1"
|
||||
".module2", # Resolved to: "yourapp.module2"
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# In module "yourapp.foo.bar.main":
|
||||
|
||||
if __name__ == "__main__":
|
||||
container = Container() # wire to "yourapp.module1" and "yourapp.module2"
|
||||
...
|
||||
|
||||
To use wiring configuration and call method ``.wire()`` manually, set flag ``auto_wire=False``:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 5
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
wiring_config = containers.WiringConfiguration(
|
||||
modules=["yourapp.module1"],
|
||||
auto_wire=False,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
container = Container() # container.wire() is NOT called automatically
|
||||
container.wire() # wire to "yourapp.module1"
|
||||
...
|
||||
|
||||
.. _async-injections-wiring:
|
||||
|
||||
Asynchronous injections
|
||||
|
@ -402,11 +511,11 @@ This is useful when you import modules dynamically.
|
|||
from .containers import Container
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
register_loader_containers(container) # <--- installs import hook
|
||||
|
||||
module = importlib.import_module('package.module')
|
||||
module = importlib.import_module("package.module")
|
||||
module.foo()
|
||||
|
||||
You can register multiple containers in the import hook. For doing this call register function
|
||||
|
|
|
@ -9,7 +9,7 @@ class Container(containers.DeclarativeContainer):
|
|||
service2 = providers.Dependency()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
container.check_dependencies() # <-- raises error:
|
||||
# Container has undefined dependencies: "Container.service1", "Container.service2"
|
||||
|
|
|
@ -10,7 +10,7 @@ class Container(containers.DeclarativeContainer):
|
|||
factory2 = providers.Factory(object)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
|
||||
object1 = container.factory1()
|
||||
|
@ -18,6 +18,6 @@ if __name__ == '__main__':
|
|||
|
||||
print(container.providers)
|
||||
# {
|
||||
# 'factory1': <dependency_injector.providers.Factory(...),
|
||||
# 'factory2': <dependency_injector.providers.Factory(...),
|
||||
# "factory1": <dependency_injector.providers.Factory(...),
|
||||
# "factory2": <dependency_injector.providers.Factory(...),
|
||||
# }
|
||||
|
|
|
@ -13,7 +13,7 @@ class Service:
|
|||
|
||||
class SourceContainer(containers.DeclarativeContainer):
|
||||
|
||||
database = providers.Singleton(sqlite3.connect, ':memory:')
|
||||
database = providers.Singleton(sqlite3.connect, ":memory:")
|
||||
service = providers.Factory(Service, db=database)
|
||||
|
||||
|
||||
|
@ -24,7 +24,7 @@ class DestinationContainer(SourceContainer):
|
|||
database = providers.Singleton(mock.Mock)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = DestinationContainer()
|
||||
|
||||
service = container.service()
|
||||
|
|
|
@ -9,21 +9,21 @@ class Service:
|
|||
|
||||
|
||||
class Base(containers.DeclarativeContainer):
|
||||
dependency = providers.Dependency(instance_of=str, default='Default value')
|
||||
dependency = providers.Dependency(instance_of=str, default="Default value")
|
||||
service = providers.Factory(Service, dependency=dependency)
|
||||
|
||||
|
||||
@containers.copy(Base)
|
||||
class Derived1(Base):
|
||||
dependency = providers.Dependency(instance_of=str, default='Derived 1')
|
||||
dependency = providers.Dependency(instance_of=str, default="Derived 1")
|
||||
|
||||
|
||||
# @containers.copy(Base) # <-- No @copy decorator
|
||||
class Derived2(Base):
|
||||
dependency = providers.Dependency(instance_of=str, default='Derived 2')
|
||||
dependency = providers.Dependency(instance_of=str, default="Derived 2")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container1 = Derived1()
|
||||
service1 = container1.service()
|
||||
print(service1.dependency) # Derived 1
|
||||
|
|
|
@ -14,21 +14,21 @@ class ContainerB(ContainerA):
|
|||
|
||||
|
||||
assert ContainerA.providers == {
|
||||
'provider1': ContainerA.provider1,
|
||||
"provider1": ContainerA.provider1,
|
||||
}
|
||||
assert ContainerB.providers == {
|
||||
'provider1': ContainerA.provider1,
|
||||
'provider2': ContainerB.provider2,
|
||||
"provider1": ContainerA.provider1,
|
||||
"provider2": ContainerB.provider2,
|
||||
}
|
||||
|
||||
assert ContainerA.cls_providers == {
|
||||
'provider1': ContainerA.provider1,
|
||||
"provider1": ContainerA.provider1,
|
||||
}
|
||||
assert ContainerB.cls_providers == {
|
||||
'provider2': ContainerB.provider2,
|
||||
"provider2": ContainerB.provider2,
|
||||
}
|
||||
|
||||
assert ContainerA.inherited_providers == {}
|
||||
assert ContainerB.inherited_providers == {
|
||||
'provider1': ContainerA.provider1,
|
||||
"provider1": ContainerA.provider1,
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ class AuthService:
|
|||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
database = providers.Singleton(sqlite3.connect, ':memory:')
|
||||
database = providers.Singleton(sqlite3.connect, ":memory:")
|
||||
|
||||
user_service = providers.Factory(
|
||||
UserService,
|
||||
|
@ -32,7 +32,7 @@ class Container(containers.DeclarativeContainer):
|
|||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
|
||||
user_service = container.user_service()
|
||||
|
|
|
@ -8,7 +8,7 @@ from dependency_injector import containers, providers
|
|||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
database = providers.Singleton(sqlite3.connect, ':memory:')
|
||||
database = providers.Singleton(sqlite3.connect, ":memory:")
|
||||
|
||||
|
||||
# Overriding ``Container`` with ``OverridingContainer``:
|
||||
|
@ -18,7 +18,7 @@ class OverridingContainer(containers.DeclarativeContainer):
|
|||
database = providers.Singleton(mock.Mock)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
|
||||
database = container.database()
|
||||
|
|
|
@ -8,10 +8,10 @@ from dependency_injector import containers, providers
|
|||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
database = providers.Singleton(sqlite3.connect, ':memory:')
|
||||
database = providers.Singleton(sqlite3.connect, ":memory:")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container(database=mock.Mock(sqlite3.Connection))
|
||||
|
||||
database = container.database()
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
from dependency_injector import containers, providers
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = containers.DynamicContainer()
|
||||
container.factory1 = providers.Factory(object)
|
||||
container.factory2 = providers.Factory(object)
|
||||
|
@ -13,6 +13,6 @@ if __name__ == '__main__':
|
|||
|
||||
print(container.providers)
|
||||
# {
|
||||
# 'factory1': <dependency_injector.providers.Factory(...),
|
||||
# 'factory2': <dependency_injector.providers.Factory(...),
|
||||
# "factory1": <dependency_injector.providers.Factory(...),
|
||||
# "factory2": <dependency_injector.providers.Factory(...),
|
||||
# }
|
||||
|
|
|
@ -13,20 +13,20 @@ class AuthService:
|
|||
|
||||
def populate_container(container, providers_config):
|
||||
for provider_name, provider_info in providers_config.items():
|
||||
provided_cls = globals().get(provider_info['class'])
|
||||
provider_cls = getattr(providers, provider_info['provider_class'])
|
||||
provided_cls = globals().get(provider_info["class"])
|
||||
provider_cls = getattr(providers, provider_info["provider_class"])
|
||||
setattr(container, provider_name, provider_cls(provided_cls))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
services_config = {
|
||||
'user': {
|
||||
'class': 'UserService',
|
||||
'provider_class': 'Factory',
|
||||
"user": {
|
||||
"class": "UserService",
|
||||
"provider_class": "Factory",
|
||||
},
|
||||
'auth': {
|
||||
'class': 'AuthService',
|
||||
'provider_class': 'Factory',
|
||||
"auth": {
|
||||
"class": "AuthService",
|
||||
"provider_class": "Factory",
|
||||
},
|
||||
}
|
||||
services = containers.DynamicContainer()
|
||||
|
|
|
@ -21,14 +21,14 @@ class Container(containers.DeclarativeContainer):
|
|||
|
||||
__self__ = providers.Self()
|
||||
|
||||
service1 = providers.Factory(Service, name='Service 1')
|
||||
service2 = providers.Factory(Service, name='Service 2')
|
||||
service3 = providers.Factory(Service, name='Service 3')
|
||||
service1 = providers.Factory(Service, name="Service 1")
|
||||
service2 = providers.Factory(Service, name="Service 2")
|
||||
service3 = providers.Factory(Service, name="Service 3")
|
||||
|
||||
dispatcher = providers.Singleton(ServiceDispatcher, __self__)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
|
||||
dispatcher = container.dispatcher()
|
||||
|
|
|
@ -21,7 +21,7 @@ class OverridingContainer(containers.DeclarativeContainer):
|
|||
service = providers.Factory(ServiceStub)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
overriding_container = OverridingContainer()
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ class Container(containers.DeclarativeContainer):
|
|||
service2 = providers.Singleton(object)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
|
||||
service1 = container.service1()
|
||||
|
|
|
@ -14,7 +14,7 @@ class Container(containers.DeclarativeContainer):
|
|||
sub = providers.Container(SubContainer)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
|
||||
service1 = container.service()
|
||||
|
|
|
@ -8,7 +8,7 @@ class Container(containers.DeclarativeContainer):
|
|||
service = providers.Singleton(object)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
|
||||
service1 = container.service()
|
||||
|
|
|
@ -34,15 +34,15 @@ class Container(containers.DeclarativeContainer):
|
|||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
|
||||
for provider in container.traverse():
|
||||
print(provider)
|
||||
|
||||
# <dependency_injector.providers.Configuration('config') at 0x10d37d200>
|
||||
# <dependency_injector.providers.Factory(<class '__main__.Service'>) at 0x10d3a2820>
|
||||
# <dependency_injector.providers.Configuration("config") at 0x10d37d200>
|
||||
# <dependency_injector.providers.Factory(<class "__main__.Service">) at 0x10d3a2820>
|
||||
# <dependency_injector.providers.Resource(<function init_database at 0x10bd2cb80>) at 0x10d346b40>
|
||||
# <dependency_injector.providers.ConfigurationOption('config.cache_hosts') at 0x10d37d350>
|
||||
# <dependency_injector.providers.ConfigurationOption("config.cache_hosts") at 0x10d37d350>
|
||||
# <dependency_injector.providers.Resource(<function init_cache at 0x10be373a0>) at 0x10d346bc0>
|
||||
# <dependency_injector.providers.ConfigurationOption('config.database_url') at 0x10d37d2e0>
|
||||
# <dependency_injector.providers.ConfigurationOption("config.database_url") at 0x10d37d2e0>
|
||||
|
|
|
@ -18,12 +18,12 @@ def main(service: Service): # <-- dependency is injected
|
|||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main(
|
||||
service=Service(
|
||||
api_client=ApiClient(
|
||||
api_key=os.getenv('API_KEY'),
|
||||
timeout=os.getenv('TIMEOUT'),
|
||||
api_key=os.getenv("API_KEY"),
|
||||
timeout=os.getenv("TIMEOUT"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -4,8 +4,8 @@ import os
|
|||
class ApiClient:
|
||||
|
||||
def __init__(self):
|
||||
self.api_key = os.getenv('API_KEY') # <-- dependency
|
||||
self.timeout = os.getenv('TIMEOUT') # <-- dependency
|
||||
self.api_key = os.getenv("API_KEY") # <-- dependency
|
||||
self.timeout = os.getenv("TIMEOUT") # <-- dependency
|
||||
|
||||
|
||||
class Service:
|
||||
|
@ -19,5 +19,5 @@ def main() -> None:
|
|||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import sys
|
||||
from unittest import mock
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
|
||||
from after import ApiClient, Service
|
||||
|
||||
|
@ -28,11 +27,11 @@ def main(service: Service = Provide[Container.service]):
|
|||
...
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
container.config.api_key.from_env('API_KEY')
|
||||
container.config.timeout.from_env('TIMEOUT')
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
container.config.api_key.from_env("API_KEY")
|
||||
container.config.timeout.from_env("TIMEOUT")
|
||||
container.wire(modules=[__name__])
|
||||
|
||||
main() # <-- dependency is injected automatically
|
||||
|
||||
|
|
|
@ -27,16 +27,16 @@ To run the application do:
|
|||
.. code-block:: bash
|
||||
|
||||
export GIPHY_API_KEY=wBJ2wZG7SRqfrU9nPgPiWvORmloDyuL0
|
||||
adev runserver giphynavigator/application.py --livereload
|
||||
python -m giphynavigator.application
|
||||
|
||||
The output should be something like:
|
||||
|
||||
.. code-block::
|
||||
|
||||
[18:52:59] Starting aux server at http://localhost:8001 ◆
|
||||
[18:52:59] Starting dev server at http://localhost:8000 ●
|
||||
======== Running on http://0.0.0.0:8080 ========
|
||||
(Press CTRL+C to quit)
|
||||
|
||||
After that visit http://127.0.0.1:8000/ in your browser or use CLI command (``curl``, ``httpie``,
|
||||
After that visit http://0.0.0.0:8080/ in your browser or use CLI command (``curl``, ``httpie``,
|
||||
etc). You should see something like:
|
||||
|
||||
.. code-block:: json
|
||||
|
@ -98,21 +98,21 @@ The output should be something like:
|
|||
|
||||
.. code-block::
|
||||
|
||||
platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
|
||||
plugins: cov-2.10.0, aiohttp-0.3.0, asyncio-0.14.0
|
||||
platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
|
||||
plugins: asyncio-0.16.0, anyio-3.3.4, aiohttp-0.3.0, cov-3.0.0
|
||||
collected 3 items
|
||||
|
||||
giphynavigator/tests.py ... [100%]
|
||||
|
||||
---------- coverage: platform darwin, python 3.8.3-final-0 -----------
|
||||
---------- coverage: platform darwin, python 3.10.0-final-0 ----------
|
||||
Name Stmts Miss Cover
|
||||
---------------------------------------------------
|
||||
giphynavigator/__init__.py 0 0 100%
|
||||
giphynavigator/application.py 12 0 100%
|
||||
giphynavigator/containers.py 6 0 100%
|
||||
giphynavigator/application.py 13 2 85%
|
||||
giphynavigator/containers.py 7 0 100%
|
||||
giphynavigator/giphy.py 14 9 36%
|
||||
giphynavigator/handlers.py 10 0 100%
|
||||
giphynavigator/services.py 9 1 89%
|
||||
giphynavigator/tests.py 37 0 100%
|
||||
---------------------------------------------------
|
||||
TOTAL 88 10 89%
|
||||
TOTAL 90 12 87%
|
||||
|
|
|
@ -8,13 +8,16 @@ 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])
|
||||
container.config.giphy.api_key.from_env("GIPHY_API_KEY")
|
||||
|
||||
app = web.Application()
|
||||
app.container = container
|
||||
app.add_routes([
|
||||
web.get('/', handlers.index),
|
||||
web.get("/", handlers.index),
|
||||
])
|
||||
return app
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = create_app()
|
||||
web.run_app(app)
|
||||
|
|
|
@ -7,7 +7,9 @@ from . import giphy, services
|
|||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
wiring_config = containers.WiringConfiguration(modules=[".handlers"])
|
||||
|
||||
config = providers.Configuration(yaml_files=["config.yml"])
|
||||
|
||||
giphy_client = providers.Factory(
|
||||
giphy.GiphyClient,
|
||||
|
|
|
@ -5,7 +5,7 @@ from aiohttp import ClientSession, ClientTimeout
|
|||
|
||||
class GiphyClient:
|
||||
|
||||
API_URL = 'https://api.giphy.com/v1'
|
||||
API_URL = "https://api.giphy.com/v1"
|
||||
|
||||
def __init__(self, api_key, timeout):
|
||||
self._api_key = api_key
|
||||
|
@ -13,11 +13,11 @@ class GiphyClient:
|
|||
|
||||
async def search(self, query, limit):
|
||||
"""Make search API call and return result."""
|
||||
url = f'{self.API_URL}/gifs/search'
|
||||
url = f"{self.API_URL}/gifs/search"
|
||||
params = {
|
||||
'q': query,
|
||||
'api_key': self._api_key,
|
||||
'limit': limit,
|
||||
"q": query,
|
||||
"api_key": self._api_key,
|
||||
"limit": limit,
|
||||
}
|
||||
async with ClientSession(timeout=self._timeout) as session:
|
||||
async with session.get(url, params=params) as response:
|
||||
|
|
|
@ -14,15 +14,15 @@ async def index(
|
|||
default_query: str = Provide[Container.config.default.query],
|
||||
default_limit: int = Provide[Container.config.default.limit.as_int()],
|
||||
) -> web.Response:
|
||||
query = request.query.get('query', default_query)
|
||||
limit = int(request.query.get('limit', default_limit))
|
||||
query = request.query.get("query", default_query)
|
||||
limit = int(request.query.get("limit", default_limit))
|
||||
|
||||
gifs = await search_service.search(query, limit)
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
'query': query,
|
||||
'limit': limit,
|
||||
'gifs': gifs,
|
||||
"query": query,
|
||||
"limit": limit,
|
||||
"gifs": gifs,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -15,4 +15,4 @@ class SearchService:
|
|||
|
||||
result = await self._giphy_client.search(query, limit)
|
||||
|
||||
return [{'url': gif['url']} for gif in result['data']]
|
||||
return [{"url": gif["url"]} for gif in result["data"]]
|
||||
|
|
|
@ -23,29 +23,29 @@ def client(app, aiohttp_client, loop):
|
|||
async def test_index(client, app):
|
||||
giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
|
||||
giphy_client_mock.search.return_value = {
|
||||
'data': [
|
||||
{'url': 'https://giphy.com/gif1.gif'},
|
||||
{'url': 'https://giphy.com/gif2.gif'},
|
||||
"data": [
|
||||
{"url": "https://giphy.com/gif1.gif"},
|
||||
{"url": "https://giphy.com/gif2.gif"},
|
||||
],
|
||||
}
|
||||
|
||||
with app.container.giphy_client.override(giphy_client_mock):
|
||||
response = await client.get(
|
||||
'/',
|
||||
"/",
|
||||
params={
|
||||
'query': 'test',
|
||||
'limit': 10,
|
||||
"query": "test",
|
||||
"limit": 10,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status == 200
|
||||
data = await response.json()
|
||||
assert data == {
|
||||
'query': 'test',
|
||||
'limit': 10,
|
||||
'gifs': [
|
||||
{'url': 'https://giphy.com/gif1.gif'},
|
||||
{'url': 'https://giphy.com/gif2.gif'},
|
||||
"query": "test",
|
||||
"limit": 10,
|
||||
"gifs": [
|
||||
{"url": "https://giphy.com/gif1.gif"},
|
||||
{"url": "https://giphy.com/gif2.gif"},
|
||||
],
|
||||
}
|
||||
|
||||
|
@ -53,27 +53,27 @@ async def test_index(client, app):
|
|||
async def test_index_no_data(client, app):
|
||||
giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
|
||||
giphy_client_mock.search.return_value = {
|
||||
'data': [],
|
||||
"data": [],
|
||||
}
|
||||
|
||||
with app.container.giphy_client.override(giphy_client_mock):
|
||||
response = await client.get('/')
|
||||
response = await client.get("/")
|
||||
|
||||
assert response.status == 200
|
||||
data = await response.json()
|
||||
assert data['gifs'] == []
|
||||
assert data["gifs"] == []
|
||||
|
||||
|
||||
async def test_index_default_params(client, app):
|
||||
giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
|
||||
giphy_client_mock.search.return_value = {
|
||||
'data': [],
|
||||
"data": [],
|
||||
}
|
||||
|
||||
with app.container.giphy_client.override(giphy_client_mock):
|
||||
response = await client.get('/')
|
||||
response = await client.get("/")
|
||||
|
||||
assert response.status == 200
|
||||
data = await response.json()
|
||||
assert data['query'] == app.container.config.default.query()
|
||||
assert data['limit'] == app.container.config.default.limit()
|
||||
assert data["query"] == app.container.config.default.query()
|
||||
assert data["limit"] == app.container.config.default.limit()
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
dependency-injector
|
||||
aiohttp
|
||||
aiohttp-devtools
|
||||
pyyaml
|
||||
pytest-aiohttp
|
||||
pytest-cov
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
from .containers import Application
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
application = Application()
|
||||
config = application.service.config()
|
||||
config.build()
|
||||
|
|
|
@ -6,17 +6,17 @@ from .services import ConfigService
|
|||
|
||||
|
||||
class Core(containers.DeclarativeContainer):
|
||||
config = providers.Configuration('config')
|
||||
config = providers.Configuration("config")
|
||||
|
||||
|
||||
class Storage(containers.DeclarativeContainer):
|
||||
queue = providers.Singleton(lambda: 'Some storage')
|
||||
queue = providers.Singleton(lambda: "Some storage")
|
||||
|
||||
|
||||
class Adapter(containers.DeclarativeContainer):
|
||||
core = providers.DependenciesContainer(config=providers.Configuration())
|
||||
tinydb = providers.Singleton(
|
||||
lambda db_path: f'DB Path=[{db_path}]',
|
||||
lambda db_path: f"DB Path=[{db_path}]",
|
||||
db_path=core.config.default.db_path,
|
||||
)
|
||||
|
||||
|
@ -25,7 +25,7 @@ class Repository(containers.DeclarativeContainer):
|
|||
adapter = providers.DependenciesContainer()
|
||||
storage = providers.DependenciesContainer()
|
||||
site = providers.Singleton(
|
||||
lambda adapter, queue: f'Adapter=[{adapter}], queue=[{queue}]',
|
||||
lambda adapter, queue: f"Adapter=[{adapter}], queue=[{queue}]",
|
||||
adapter=adapter.tinydb,
|
||||
queue=storage.queue,
|
||||
)
|
||||
|
|
|
@ -6,4 +6,4 @@ class ConfigService:
|
|||
self._config = config
|
||||
|
||||
def build(self):
|
||||
self._config.from_dict({'default': {'db_path': '~/test'}})
|
||||
self._config.from_dict({"default": {"db_path": "~/test"}})
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import sys
|
||||
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
|
||||
from .services import UserService, AuthService, PhotoService
|
||||
from .containers import Application
|
||||
|
@ -22,10 +22,9 @@ def main(
|
|||
photo_service.upload_photo(user, photo)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
application = Application()
|
||||
application.config.from_yaml('config.yml')
|
||||
application.core.init_resources()
|
||||
application.wire(modules=[sys.modules[__name__]])
|
||||
application.wire(modules=[__name__])
|
||||
|
||||
main(*sys.argv[1:])
|
||||
|
|
|
@ -30,7 +30,7 @@ class Gateways(containers.DeclarativeContainer):
|
|||
|
||||
s3_client = providers.Singleton(
|
||||
boto3.client,
|
||||
service_name='s3',
|
||||
service_name="s3",
|
||||
aws_access_key_id=config.aws.access_key_id,
|
||||
aws_secret_access_key=config.aws.secret_access_key,
|
||||
)
|
||||
|
@ -61,7 +61,7 @@ class Services(containers.DeclarativeContainer):
|
|||
|
||||
class Application(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
config = providers.Configuration(yaml_files=["config.yml"])
|
||||
|
||||
core = providers.Container(
|
||||
Core,
|
||||
|
|
|
@ -11,7 +11,7 @@ class BaseService:
|
|||
|
||||
def __init__(self) -> None:
|
||||
self.logger = logging.getLogger(
|
||||
f'{__name__}.{self.__class__.__name__}',
|
||||
f"{__name__}.{self.__class__.__name__}",
|
||||
)
|
||||
|
||||
|
||||
|
@ -22,8 +22,8 @@ class UserService(BaseService):
|
|||
super().__init__()
|
||||
|
||||
def get_user(self, email: str) -> Dict[str, str]:
|
||||
self.logger.debug('User %s has been found in database', email)
|
||||
return {'email': email, 'password_hash': '...'}
|
||||
self.logger.debug("User %s has been found in database", email)
|
||||
return {"email": email, "password_hash": "..."}
|
||||
|
||||
|
||||
class AuthService(BaseService):
|
||||
|
@ -36,8 +36,8 @@ class AuthService(BaseService):
|
|||
def authenticate(self, user: Dict[str, str], password: str) -> None:
|
||||
assert password is not None
|
||||
self.logger.debug(
|
||||
'User %s has been successfully authenticated',
|
||||
user['email'],
|
||||
"User %s has been successfully authenticated",
|
||||
user["email"],
|
||||
)
|
||||
|
||||
|
||||
|
@ -50,7 +50,7 @@ class PhotoService(BaseService):
|
|||
|
||||
def upload_photo(self, user: Dict[str, str], photo_path: str) -> None:
|
||||
self.logger.debug(
|
||||
'Photo %s has been successfully uploaded by user %s',
|
||||
"Photo %s has been successfully uploaded by user %s",
|
||||
photo_path,
|
||||
user['email'],
|
||||
user["email"],
|
||||
)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import sys
|
||||
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
|
||||
from .services import UserService, AuthService, PhotoService
|
||||
from .containers import Container
|
||||
|
@ -22,10 +22,9 @@ def main(
|
|||
photo_service.upload_photo(user, photo)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
container.init_resources()
|
||||
container.config.from_ini('config.ini')
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
container.wire(modules=[__name__])
|
||||
|
||||
main(*sys.argv[1:])
|
||||
|
|
|
@ -11,11 +11,11 @@ from . import services
|
|||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
config = providers.Configuration(ini_files=["config.ini"])
|
||||
|
||||
logging = providers.Resource(
|
||||
logging.config.fileConfig,
|
||||
fname='logging.ini',
|
||||
fname="logging.ini",
|
||||
)
|
||||
|
||||
# Gateways
|
||||
|
@ -27,7 +27,7 @@ class Container(containers.DeclarativeContainer):
|
|||
|
||||
s3_client = providers.Singleton(
|
||||
boto3.client,
|
||||
service_name='s3',
|
||||
service_name="s3",
|
||||
aws_access_key_id=config.aws.access_key_id,
|
||||
aws_secret_access_key=config.aws.secret_access_key,
|
||||
)
|
||||
|
|
|
@ -11,7 +11,7 @@ class BaseService:
|
|||
|
||||
def __init__(self) -> None:
|
||||
self.logger = logging.getLogger(
|
||||
f'{__name__}.{self.__class__.__name__}',
|
||||
f"{__name__}.{self.__class__.__name__}",
|
||||
)
|
||||
|
||||
|
||||
|
@ -22,8 +22,8 @@ class UserService(BaseService):
|
|||
super().__init__()
|
||||
|
||||
def get_user(self, email: str) -> Dict[str, str]:
|
||||
self.logger.debug('User %s has been found in database', email)
|
||||
return {'email': email, 'password_hash': '...'}
|
||||
self.logger.debug("User %s has been found in database", email)
|
||||
return {"email": email, "password_hash": "..."}
|
||||
|
||||
|
||||
class AuthService(BaseService):
|
||||
|
@ -36,8 +36,8 @@ class AuthService(BaseService):
|
|||
def authenticate(self, user: Dict[str, str], password: str) -> None:
|
||||
assert password is not None
|
||||
self.logger.debug(
|
||||
'User %s has been successfully authenticated',
|
||||
user['email'],
|
||||
"User %s has been successfully authenticated",
|
||||
user["email"],
|
||||
)
|
||||
|
||||
|
||||
|
@ -50,7 +50,7 @@ class PhotoService(BaseService):
|
|||
|
||||
def upload_photo(self, user: Dict[str, str], photo_path: str) -> None:
|
||||
self.logger.debug(
|
||||
'Photo %s has been successfully uploaded by user %s',
|
||||
"Photo %s has been successfully uploaded by user %s",
|
||||
photo_path,
|
||||
user['email'],
|
||||
user["email"],
|
||||
)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.8-buster
|
||||
FROM python:3.10-buster
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
|
|
|
@ -65,22 +65,22 @@ The output should be something like:
|
|||
|
||||
.. code-block::
|
||||
|
||||
platform linux -- Python 3.8.3, pytest-6.0.1, py-1.9.0, pluggy-0.13.1
|
||||
platform linux -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
|
||||
rootdir: /code
|
||||
plugins: asyncio-0.14.0, cov-2.10.0
|
||||
plugins: asyncio-0.16.0, cov-3.0.0
|
||||
collected 2 items
|
||||
|
||||
monitoringdaemon/tests.py .. [100%]
|
||||
|
||||
----------- coverage: platform linux, python 3.8.3-final-0 -----------
|
||||
---------- coverage: platform linux, python 3.10.0-final-0 -----------
|
||||
Name Stmts Miss Cover
|
||||
----------------------------------------------------
|
||||
monitoringdaemon/__init__.py 0 0 100%
|
||||
monitoringdaemon/__main__.py 13 13 0%
|
||||
monitoringdaemon/__main__.py 11 11 0%
|
||||
monitoringdaemon/containers.py 11 0 100%
|
||||
monitoringdaemon/dispatcher.py 44 5 89%
|
||||
monitoringdaemon/dispatcher.py 45 5 89%
|
||||
monitoringdaemon/http.py 6 3 50%
|
||||
monitoringdaemon/monitors.py 23 1 96%
|
||||
monitoringdaemon/tests.py 37 0 100%
|
||||
monitoringdaemon/tests.py 35 0 100%
|
||||
----------------------------------------------------
|
||||
TOTAL 134 22 84%
|
||||
TOTAL 131 20 85%
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
"""Main module."""
|
||||
|
||||
import sys
|
||||
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
|
||||
from .dispatcher import Dispatcher
|
||||
|
@ -13,10 +11,9 @@ def main(dispatcher: Dispatcher = Provide[Container.dispatcher]) -> None:
|
|||
dispatcher.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
container.init_resources()
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
container.wire(modules=[__name__])
|
||||
|
||||
main()
|
||||
|
|
|
@ -10,7 +10,7 @@ from . import http, monitors, dispatcher
|
|||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
config = providers.Configuration(yaml_files=["config.yml"])
|
||||
|
||||
logging = providers.Resource(
|
||||
logging.basicConfig,
|
||||
|
|
|
@ -21,7 +21,7 @@ class Dispatcher:
|
|||
asyncio.run(self.start())
|
||||
|
||||
async def start(self) -> None:
|
||||
self._logger.info('Starting up')
|
||||
self._logger.info("Starting up")
|
||||
|
||||
for monitor in self._monitors:
|
||||
self._monitor_tasks.append(
|
||||
|
@ -41,11 +41,11 @@ class Dispatcher:
|
|||
|
||||
self._stopping = True
|
||||
|
||||
self._logger.info('Shutting down')
|
||||
self._logger.info("Shutting down")
|
||||
for task, monitor in zip(self._monitor_tasks, self._monitors):
|
||||
task.cancel()
|
||||
self._monitor_tasks.clear()
|
||||
self._logger.info('Shutdown finished successfully')
|
||||
self._logger.info("Shutdown finished successfully")
|
||||
|
||||
@staticmethod
|
||||
async def _run_monitor(monitor: Monitor) -> None:
|
||||
|
@ -61,6 +61,6 @@ class Dispatcher:
|
|||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception:
|
||||
monitor.logger.exception('Error executing monitor check')
|
||||
monitor.logger.exception("Error executing monitor check")
|
||||
|
||||
await asyncio.sleep(_until_next(last=time_start))
|
||||
|
|
|
@ -25,10 +25,10 @@ class HttpMonitor(Monitor):
|
|||
options: Dict[str, Any],
|
||||
) -> None:
|
||||
self._client = http_client
|
||||
self._method = options.pop('method')
|
||||
self._url = options.pop('url')
|
||||
self._timeout = options.pop('timeout')
|
||||
super().__init__(check_every=options.pop('check_every'))
|
||||
self._method = options.pop("method")
|
||||
self._url = options.pop("url")
|
||||
self._timeout = options.pop("timeout")
|
||||
super().__init__(check_every=options.pop("check_every"))
|
||||
|
||||
async def check(self) -> None:
|
||||
time_start = time.time()
|
||||
|
@ -43,11 +43,11 @@ class HttpMonitor(Monitor):
|
|||
time_took = time_end - time_start
|
||||
|
||||
self.logger.info(
|
||||
'Check\n'
|
||||
' %s %s\n'
|
||||
' response code: %s\n'
|
||||
' content length: %s\n'
|
||||
' request took: %s seconds',
|
||||
"Check\n"
|
||||
" %s %s\n"
|
||||
" response code: %s\n"
|
||||
" content length: %s\n"
|
||||
" request took: %s seconds",
|
||||
self._method,
|
||||
self._url,
|
||||
response.status,
|
||||
|
|
|
@ -17,33 +17,33 @@ class RequestStub:
|
|||
|
||||
@pytest.fixture
|
||||
def container():
|
||||
container = Container()
|
||||
container.config.from_dict({
|
||||
'log': {
|
||||
'level': 'INFO',
|
||||
'formant': '[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s',
|
||||
},
|
||||
'monitors': {
|
||||
'example': {
|
||||
'method': 'GET',
|
||||
'url': 'http://fake-example.com',
|
||||
'timeout': 1,
|
||||
'check_every': 1,
|
||||
return Container(
|
||||
config={
|
||||
"log": {
|
||||
"level": "INFO",
|
||||
"formant": "[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s",
|
||||
},
|
||||
'httpbin': {
|
||||
'method': 'GET',
|
||||
'url': 'https://fake-httpbin.org/get',
|
||||
'timeout': 1,
|
||||
'check_every': 1,
|
||||
"monitors": {
|
||||
"example": {
|
||||
"method": "GET",
|
||||
"url": "http://fake-example.com",
|
||||
"timeout": 1,
|
||||
"check_every": 1,
|
||||
},
|
||||
"httpbin": {
|
||||
"method": "GET",
|
||||
"url": "https://fake-httpbin.org/get",
|
||||
"timeout": 1,
|
||||
"check_every": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return container
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_example_monitor(container, caplog):
|
||||
caplog.set_level('INFO')
|
||||
caplog.set_level("INFO")
|
||||
|
||||
http_client_mock = mock.AsyncMock()
|
||||
http_client_mock.request.return_value = RequestStub(
|
||||
|
@ -55,21 +55,22 @@ async def test_example_monitor(container, caplog):
|
|||
example_monitor = container.example_monitor()
|
||||
await example_monitor.check()
|
||||
|
||||
assert 'http://fake-example.com' in caplog.text
|
||||
assert 'response code: 200' in caplog.text
|
||||
assert 'content length: 635' in caplog.text
|
||||
assert "http://fake-example.com" in caplog.text
|
||||
assert "response code: 200" in caplog.text
|
||||
assert "content length: 635" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispatcher(container, caplog, event_loop):
|
||||
caplog.set_level('INFO')
|
||||
caplog.set_level("INFO")
|
||||
|
||||
example_monitor_mock = mock.AsyncMock()
|
||||
httpbin_monitor_mock = mock.AsyncMock()
|
||||
|
||||
with container.example_monitor.override(example_monitor_mock), \
|
||||
container.httpbin_monitor.override(httpbin_monitor_mock):
|
||||
|
||||
with container.override_providers(
|
||||
example_monitor=example_monitor_mock,
|
||||
httpbin_monitor=httpbin_monitor_mock,
|
||||
):
|
||||
dispatcher = container.dispatcher()
|
||||
event_loop.create_task(dispatcher.start())
|
||||
await asyncio.sleep(0.1)
|
||||
|
|
|
@ -23,12 +23,12 @@ class Container(containers.DeclarativeContainer):
|
|||
|
||||
s3_client = providers.Resource(
|
||||
session.provided.client.call(),
|
||||
service_name='s3',
|
||||
service_name="s3",
|
||||
)
|
||||
|
||||
sqs_client = providers.Resource(
|
||||
providers.MethodCaller(session.provided.client), # Alternative syntax
|
||||
service_name='sqs',
|
||||
service_name="sqs",
|
||||
)
|
||||
|
||||
service1 = providers.Factory(
|
||||
|
@ -39,16 +39,16 @@ class Container(containers.DeclarativeContainer):
|
|||
|
||||
service2 = providers.Factory(
|
||||
Service,
|
||||
s3_client=session.provided.client.call(service_name='s3'), # Alternative inline syntax
|
||||
sqs_client=session.provided.client.call(service_name='sqs'), # Alternative inline syntax
|
||||
s3_client=session.provided.client.call(service_name="s3"), # Alternative inline syntax
|
||||
sqs_client=session.provided.client.call(service_name="sqs"), # Alternative inline syntax
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
container = Container()
|
||||
container.config.aws_access_key_id.from_env('AWS_ACCESS_KEY_ID')
|
||||
container.config.aws_secret_access_key.from_env('AWS_SECRET_ACCESS_KEY')
|
||||
container.config.aws_session_token.from_env('AWS_SESSION_TOKEN')
|
||||
container.config.aws_access_key_id.from_env("AWS_ACCESS_KEY_ID")
|
||||
container.config.aws_secret_access_key.from_env("AWS_SECRET_ACCESS_KEY")
|
||||
container.config.aws_session_token.from_env("AWS_SESSION_TOKEN")
|
||||
container.init_resources()
|
||||
|
||||
s3_client = container.s3_client()
|
||||
|
@ -62,11 +62,11 @@ def main():
|
|||
assert service1.s3_client is s3_client
|
||||
assert service1.sqs_client is sqs_client
|
||||
|
||||
service2 = container.service1()
|
||||
service2 = container.service2()
|
||||
print(service2, service2.s3_client, service2.sqs_client)
|
||||
assert service2.s3_client is s3_client
|
||||
assert service2.sqs_client is sqs_client
|
||||
assert service2.s3_client.__class__.__name__ == "S3"
|
||||
assert service2.sqs_client.__class__.__name__ == "SQS"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -4,7 +4,7 @@ from .containers import Container
|
|||
from .commands import SaveRating, DoSomethingElse
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
message_bus = container.message_bus()
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ class CommandHandler:
|
|||
self.rating_repo = rating_repo
|
||||
|
||||
def save_rating(self):
|
||||
print('Saving rating')
|
||||
print("Saving rating")
|
||||
|
||||
def something_else(self):
|
||||
print('Doing something else')
|
||||
print("Doing something else")
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
"""Main module."""
|
||||
|
||||
import sys
|
||||
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
|
||||
from .user.repositories import UserRepository
|
||||
from .photo.repositories import PhotoRepository
|
||||
|
@ -24,20 +22,19 @@ def main(
|
|||
) -> None:
|
||||
user1 = user_repository.get(id=1)
|
||||
user1_photos = photo_repository.get_photos(user1.id)
|
||||
print(f'Retrieve user id={user1.id}, photos count={len(user1_photos)}')
|
||||
print(f"Retrieve user id={user1.id}, photos count={len(user1_photos)}")
|
||||
|
||||
user2 = user_repository.get(id=2)
|
||||
user2_photos = photo_repository.get_photos(user2.id)
|
||||
print(f'Retrieve user id={user2.id}, photos count={len(user2_photos)}')
|
||||
print(f"Retrieve user id={user2.id}, photos count={len(user2_photos)}")
|
||||
|
||||
assert aggregation_service.user_repository is user_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__]])
|
||||
application.wire(modules=[__name__])
|
||||
|
||||
main()
|
||||
|
|
|
@ -12,13 +12,13 @@ from .analytics.containers import AnalyticsContainer
|
|||
|
||||
class ApplicationContainer(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
config = providers.Configuration(ini_files=["config.ini"])
|
||||
|
||||
sqlite = providers.Singleton(sqlite3.connect, config.database.dsn)
|
||||
|
||||
s3 = providers.Singleton(
|
||||
boto3.client,
|
||||
service_name='s3',
|
||||
service_name="s3",
|
||||
aws_access_key_id=config.aws.access_key_id,
|
||||
aws_secret_access_key=config.aws.secret_access_key,
|
||||
)
|
||||
|
|
|
@ -105,9 +105,9 @@ The output should be something like:
|
|||
githubnavigator/wsgi.py 4 4 0%
|
||||
manage.py 12 2 83%
|
||||
web/__init__.py 0 0 100%
|
||||
web/apps.py 7 0 100%
|
||||
web/apps.py 6 0 100%
|
||||
web/tests.py 28 0 100%
|
||||
web/urls.py 3 0 100%
|
||||
web/views.py 12 0 100%
|
||||
---------------------------------------------------
|
||||
TOTAL 121 10 92%
|
||||
TOTAL 120 10 92%
|
||||
|
|
|
@ -10,6 +10,6 @@ import os
|
|||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'githubnavigator.settings')
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "githubnavigator.settings")
|
||||
|
||||
application = get_asgi_application()
|
||||
|
|
|
@ -15,7 +15,7 @@ class SearchService:
|
|||
"""Search for repositories and return formatted data."""
|
||||
repositories = self._github_client.search_repositories(
|
||||
query=query,
|
||||
**{'in': 'name'},
|
||||
**{"in": "name"},
|
||||
)
|
||||
return [
|
||||
self._format_repo(repository)
|
||||
|
@ -25,20 +25,20 @@ class SearchService:
|
|||
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,
|
||||
"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 {},
|
||||
"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,
|
||||
"sha": commit.sha,
|
||||
"url": commit.html_url,
|
||||
"message": commit.commit.message,
|
||||
"author_name": commit.commit.author.name,
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|||
# 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'
|
||||
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
|
||||
|
@ -31,54 +31,54 @@ 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',
|
||||
"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',
|
||||
"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'
|
||||
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',
|
||||
"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'
|
||||
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'),
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,16 +88,16 @@ DATABASES = {
|
|||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -105,9 +105,9 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/3.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
|
@ -119,13 +119,13 @@ USE_TZ = True
|
|||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/3.0/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_URL = "/static/"
|
||||
|
||||
# Github client settings
|
||||
GITHUB_TOKEN = os.getenv('GITHUB_TOKEN')
|
||||
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
|
||||
GITHUB_REQUEST_TIMEOUT = 10
|
||||
|
||||
# Search settings
|
||||
DEFAULT_LIMIT = 5
|
||||
DEFAULT_QUERY = 'Dependency Injector'
|
||||
DEFAULT_QUERY = "Dependency Injector"
|
||||
LIMIT_OPTIONS = [5, 10, 20]
|
||||
|
|
|
@ -17,6 +17,6 @@ from django.contrib import admin
|
|||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('', include('web.urls')),
|
||||
path('admin/', admin.site.urls),
|
||||
path("", include("web.urls")),
|
||||
path("admin/", admin.site.urls),
|
||||
]
|
||||
|
|
|
@ -11,6 +11,6 @@ import os
|
|||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'githubnavigator.settings')
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "githubnavigator.settings")
|
||||
|
||||
application = get_wsgi_application()
|
||||
|
|
|
@ -5,7 +5,7 @@ import sys
|
|||
|
||||
|
||||
def main():
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'githubnavigator.settings')
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "githubnavigator.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
|
@ -17,5 +17,5 @@ def main():
|
|||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -3,11 +3,10 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
from githubnavigator import container
|
||||
from . import views
|
||||
|
||||
|
||||
class WebConfig(AppConfig):
|
||||
name = 'web'
|
||||
name = "web"
|
||||
|
||||
def ready(self):
|
||||
container.wire(modules=[views])
|
||||
container.wire(modules=[".views"])
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'bootstrap4/bootstrap4.html' %}
|
||||
{% extends "bootstrap4/bootstrap4.html" %}
|
||||
|
||||
{% load bootstrap4 %}
|
||||
|
||||
|
|
|
@ -15,49 +15,49 @@ class IndexTests(TestCase):
|
|||
github_client_mock = mock.Mock(spec=Github)
|
||||
github_client_mock.search_repositories.return_value = [
|
||||
mock.Mock(
|
||||
html_url='repo1-url',
|
||||
name='repo1-name',
|
||||
html_url="repo1-url",
|
||||
name="repo1-name",
|
||||
owner=mock.Mock(
|
||||
login='owner1-login',
|
||||
html_url='owner1-url',
|
||||
avatar_url='owner1-avatar-url',
|
||||
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',
|
||||
html_url="repo2-url",
|
||||
name="repo2-name",
|
||||
owner=mock.Mock(
|
||||
login='owner2-login',
|
||||
html_url='owner2-url',
|
||||
avatar_url='owner2-avatar-url',
|
||||
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'))
|
||||
response = self.client.get(reverse("index"))
|
||||
|
||||
self.assertContains(response, 'Results found: 2')
|
||||
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, "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')
|
||||
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'))
|
||||
response = self.client.get(reverse("index"))
|
||||
|
||||
self.assertContains(response, 'Results found: 0')
|
||||
self.assertContains(response, "Results found: 0")
|
||||
|
|
|
@ -5,5 +5,5 @@ from django.urls import path
|
|||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
path("", views.index, name="index"),
|
||||
]
|
||||
|
|
|
@ -18,18 +18,18 @@ def index(
|
|||
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))
|
||||
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',
|
||||
template_name="index.html",
|
||||
context={
|
||||
'query': query,
|
||||
'limit': limit,
|
||||
'limit_options': limit_options,
|
||||
'repositories': repositories,
|
||||
"query": query,
|
||||
"limit": limit,
|
||||
"limit_options": limit_options,
|
||||
"repositories": repositories,
|
||||
}
|
||||
)
|
||||
|
|
|
@ -63,7 +63,7 @@ class Container(containers.DeclarativeContainer):
|
|||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
|
||||
token_service = container.token_service()
|
||||
|
|
|
@ -58,7 +58,7 @@ class Container(containers.DeclarativeContainer):
|
|||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
|
||||
token_service = container.token_service()
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.8-buster
|
||||
FROM python:3.9-buster
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
|
|
|
@ -69,14 +69,14 @@ The output should be something like:
|
|||
|
||||
.. code-block::
|
||||
|
||||
platform linux -- Python 3.8.6, pytest-6.2.1, py-1.10.0, pluggy-0.13.1
|
||||
platform linux -- Python 3.9, pytest-6.2.1, py-1.10.0, pluggy-0.13.1
|
||||
rootdir: /code
|
||||
plugins: cov-2.10.1, asyncio-0.14.0
|
||||
collected 1 item
|
||||
|
||||
fastapiredis/tests.py . [100%]
|
||||
|
||||
----------- coverage: platform linux, python 3.8.6-final-0 -----------
|
||||
----------- coverage: platform linux, python 3.9 -----------
|
||||
Name Stmts Miss Cover
|
||||
-------------------------------------------------
|
||||
fastapiredis/__init__.py 0 0 100%
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
"""Application module."""
|
||||
|
||||
import sys
|
||||
|
||||
from fastapi import FastAPI, Depends
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from fastapi import FastAPI, Depends
|
||||
|
||||
from .containers import Container
|
||||
from .services import Service
|
||||
|
@ -12,14 +10,14 @@ from .services import Service
|
|||
app = FastAPI()
|
||||
|
||||
|
||||
@app.api_route('/')
|
||||
@app.api_route("/")
|
||||
@inject
|
||||
async def index(service: Service = Depends(Provide[Container.service])):
|
||||
value = await service.process()
|
||||
return {'result': value}
|
||||
return {"result": value}
|
||||
|
||||
|
||||
container = Container()
|
||||
container.config.redis_host.from_env('REDIS_HOST', 'localhost')
|
||||
container.config.redis_password.from_env('REDIS_PASSWORD', 'password')
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
container.config.redis_host.from_env("REDIS_HOST", "localhost")
|
||||
container.config.redis_password.from_env("REDIS_PASSWORD", "password")
|
||||
container.wire(modules=[__name__])
|
||||
|
|
|
@ -6,7 +6,7 @@ from aioredis import create_redis_pool, Redis
|
|||
|
||||
|
||||
async def init_redis_pool(host: str, password: str) -> AsyncIterator[Redis]:
|
||||
pool = await create_redis_pool(f'redis://{host}', password=password)
|
||||
pool = await create_redis_pool(f"redis://{host}", password=password)
|
||||
yield pool
|
||||
pool.close()
|
||||
await pool.wait_closed()
|
||||
|
|
|
@ -8,5 +8,5 @@ class Service:
|
|||
self._redis = redis
|
||||
|
||||
async def process(self) -> str:
|
||||
await self._redis.set('my-key', 'value')
|
||||
return await self._redis.get('my-key', encoding='utf-8')
|
||||
await self._redis.set("my-key", "value")
|
||||
return await self._redis.get("my-key", encoding="utf-8")
|
||||
|
|
|
@ -11,7 +11,7 @@ from .services import Service
|
|||
|
||||
@pytest.fixture
|
||||
def client(event_loop):
|
||||
client = AsyncClient(app=app, base_url='http://test')
|
||||
client = AsyncClient(app=app, base_url="http://test")
|
||||
yield client
|
||||
event_loop.run_until_complete(client.aclose())
|
||||
|
||||
|
@ -19,10 +19,10 @@ def client(event_loop):
|
|||
@pytest.mark.asyncio
|
||||
async def test_index(client):
|
||||
service_mock = mock.AsyncMock(spec=Service)
|
||||
service_mock.process.return_value = 'Foo'
|
||||
service_mock.process.return_value = "Foo"
|
||||
|
||||
with container.service.override(service_mock):
|
||||
response = await client.get('/')
|
||||
response = await client.get("/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {'result': 'Foo'}
|
||||
assert response.json() == {"result": "Foo"}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
dependency-injector
|
||||
fastapi
|
||||
uvicorn
|
||||
aioredis
|
||||
aioredis<2 # TODO: Update example to work with aioredis >= 2.0
|
||||
|
||||
# For testing:
|
||||
pytest
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import sys
|
||||
|
||||
from fastapi import FastAPI, Depends
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.wiring import inject, Provide
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
|
||||
|
||||
class Service:
|
||||
async def process(self) -> str:
|
||||
return 'Ok'
|
||||
return "OK"
|
||||
|
||||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
@ -18,12 +16,12 @@ class Container(containers.DeclarativeContainer):
|
|||
app = FastAPI()
|
||||
|
||||
|
||||
@app.api_route('/')
|
||||
@app.api_route("/")
|
||||
@inject
|
||||
async def index(service: Service = Depends(Provide[Container.service])):
|
||||
result = await service.process()
|
||||
return {'result': result}
|
||||
return {"result": result}
|
||||
|
||||
|
||||
container = Container()
|
||||
container.wire(modules=[sys.modules[__name__]])
|
||||
container.wire(modules=[__name__])
|
||||
|
|
|
@ -7,19 +7,18 @@ from fastapi_di_example import app, container, Service
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def client(event_loop):
|
||||
client = AsyncClient(app=app, base_url='http://test')
|
||||
yield client
|
||||
event_loop.run_until_complete(client.aclose())
|
||||
async def client(event_loop):
|
||||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_index(client):
|
||||
service_mock = mock.AsyncMock(spec=Service)
|
||||
service_mock.process.return_value = 'Foo'
|
||||
service_mock.process.return_value = "Foo"
|
||||
|
||||
with container.service.override(service_mock):
|
||||
response = await client.get('/')
|
||||
response = await client.get("/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {'result': 'Foo'}
|
||||
assert response.json() == {"result": "Foo"}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.9-buster
|
||||
FROM python:3.10-buster
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV HOST=0.0.0.0
|
||||
|
|
|
@ -73,19 +73,19 @@ The output should be something like:
|
|||
|
||||
.. code-block::
|
||||
|
||||
platform linux -- Python 3.9.1, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
|
||||
platform linux -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
|
||||
rootdir: /code
|
||||
plugins: cov-2.11.1
|
||||
plugins: cov-3.0.0
|
||||
collected 7 items
|
||||
|
||||
webapp/tests.py ....... [100%]
|
||||
|
||||
----------- coverage: platform linux, python 3.9.1-final-0 -----------
|
||||
---------- coverage: platform linux, python 3.10.0-final-0 ----------
|
||||
Name Stmts Miss Cover
|
||||
--------------------------------------------
|
||||
webapp/__init__.py 0 0 100%
|
||||
webapp/application.py 14 0 100%
|
||||
webapp/containers.py 9 0 100%
|
||||
webapp/application.py 12 0 100%
|
||||
webapp/containers.py 10 0 100%
|
||||
webapp/database.py 24 8 67%
|
||||
webapp/endpoints.py 32 0 100%
|
||||
webapp/models.py 10 1 90%
|
||||
|
@ -93,4 +93,4 @@ The output should be something like:
|
|||
webapp/services.py 16 0 100%
|
||||
webapp/tests.py 59 0 100%
|
||||
--------------------------------------------
|
||||
TOTAL 200 29 86%
|
||||
TOTAL 199 29 85%
|
||||
|
|
|
@ -8,8 +8,6 @@ from . import endpoints
|
|||
|
||||
def create_app() -> FastAPI:
|
||||
container = Container()
|
||||
container.config.from_yaml('config.yml')
|
||||
container.wire(modules=[endpoints])
|
||||
|
||||
db = container.db()
|
||||
db.create_database()
|
||||
|
|
|
@ -9,7 +9,9 @@ from .services import UserService
|
|||
|
||||
class Container(containers.DeclarativeContainer):
|
||||
|
||||
config = providers.Configuration()
|
||||
wiring_config = containers.WiringConfiguration(modules=[".endpoints"])
|
||||
|
||||
config = providers.Configuration(yaml_files=["config.yml"])
|
||||
|
||||
db = providers.Singleton(Database, db_url=config.db.url)
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ class Database:
|
|||
try:
|
||||
yield session
|
||||
except Exception:
|
||||
logger.exception('Session rollback because of exception')
|
||||
logger.exception("Session rollback because of exception")
|
||||
session.rollback()
|
||||
raise
|
||||
finally:
|
||||
|
|
|
@ -10,7 +10,7 @@ from .repositories import NotFoundError
|
|||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/users')
|
||||
@router.get("/users")
|
||||
@inject
|
||||
def get_list(
|
||||
user_service: UserService = Depends(Provide[Container.user_service]),
|
||||
|
@ -18,7 +18,7 @@ def get_list(
|
|||
return user_service.get_users()
|
||||
|
||||
|
||||
@router.get('/users/{user_id}')
|
||||
@router.get("/users/{user_id}")
|
||||
@inject
|
||||
def get_by_id(
|
||||
user_id: int,
|
||||
|
@ -30,7 +30,7 @@ def get_by_id(
|
|||
return Response(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
@router.post('/users', status_code=status.HTTP_201_CREATED)
|
||||
@router.post("/users", status_code=status.HTTP_201_CREATED)
|
||||
@inject
|
||||
def add(
|
||||
user_service: UserService = Depends(Provide[Container.user_service]),
|
||||
|
@ -38,7 +38,7 @@ def add(
|
|||
return user_service.create_user()
|
||||
|
||||
|
||||
@router.delete('/users/{user_id}', status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@inject
|
||||
def remove(
|
||||
user_id: int,
|
||||
|
@ -52,6 +52,6 @@ def remove(
|
|||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@router.get('/status')
|
||||
@router.get("/status")
|
||||
def get_status():
|
||||
return {'status': 'OK'}
|
||||
return {"status": "OK"}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user