diff --git a/README.rst b/README.rst index 0bb2f8db..596c6c28 100644 --- a/README.rst +++ b/README.rst @@ -157,6 +157,7 @@ Choose one of the following: - `Flask web application tutorial `_ - `Aiohttp REST API tutorial `_ - `Asyncio monitoring daemon tutorial `_ +- `CLI application tutorial `_ Installation ============ diff --git a/docs/examples/index.rst b/docs/examples/index.rst index c61c4009..244569b2 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -15,7 +15,6 @@ and powered by *Dependency Injector* framework. .. toctree:: :maxdepth: 2 - movie_lister services_miniapp_v1 services_miniapp_v2 bundles_miniapp diff --git a/docs/examples/movie_lister.rst b/docs/examples/movie_lister.rst deleted file mode 100644 index 594a67f1..00000000 --- a/docs/examples/movie_lister.rst +++ /dev/null @@ -1,124 +0,0 @@ -Movie lister naive example --------------------------- - -.. meta:: - :description: Movie lister - is a naive example of dependency injection and - inversion of control containers on Python. Original example - was taken from Martin Fowler's article about dependency - injection and inversion of control. - -This naive example was taken from Martin Fowler's article about dependency -injection and inversion of control: http://www.martinfowler.com/articles/injection.html - -Like Martin says: - -.. pull-quote:: - - *Like all of my examples it's one of those super-simple examples; - small enough to be unreal, but hopefully enough for you to visualize - what's going on without falling into the bog of a real example.* - -While original Martin's MovieLister example was a bit modified here, it -makes sense to provide some description. So, the idea of this example is to -create ``movies`` library that can be configured to work with different -movie databases (csv, sqlite, etc...) and provide 2 main features: - -1. List all movies that were directed by certain person. -2. List all movies that were released in certain year. - -Also this example contains 3 mini applications that are based on ``movies`` -library: - -1. ``app_csv.py`` - list movies by certain criteria from csv file database. -2. ``app_db.py`` - list movies by certain criteria from sqlite database. -3. ``app_db_csv.py`` - list movies by certain criteria from csv file and - sqlite databases. - -Instructions for running: - -.. code-block:: bash - - python app_csv.py - python app_db.py - python app_db_csv.py - - -Full code of example could be found on GitHub_. - -Movies library -~~~~~~~~~~~~~~ - -Classes diagram: - -.. image:: /images/miniapps/movie_lister/classes.png - :width: 100% - :align: center - - -Movies library structure: - -.. code-block:: bash - - /movies - /__init__.py - /finders.py - /listers.py - /models.py - - -Listing of ``movies/__init__.py``: - -.. literalinclude:: ../../examples/miniapps/movie_lister/movies/__init__.py - :language: python - -Example application -~~~~~~~~~~~~~~~~~~~ - -Example application structure: - -.. code-block:: bash - - /example - /__init__.py - /db.py - /main.py - -Listing of ``examples/main.py``: - -.. literalinclude:: ../../examples/miniapps/movie_lister/example/main.py - :language: python - -Listing of ``examples/db.py``: - -.. literalinclude:: ../../examples/miniapps/movie_lister/example/db.py - :language: python - -Csv application -~~~~~~~~~~~~~~~ - -Listing of ``app_csv.py``: - -.. literalinclude:: ../../examples/miniapps/movie_lister/app_csv.py - :language: python - -Database application -~~~~~~~~~~~~~~~~~~~~ - -Listing of ``app_db.py``: - -.. literalinclude:: ../../examples/miniapps/movie_lister/app_db.py - :language: python - -Csv and database application -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Listing of ``app_db_csv.py``: - -.. literalinclude:: ../../examples/miniapps/movie_lister/app_db_csv.py - :language: python - - -.. disqus:: - - -.. _GitHub: https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/movie_lister diff --git a/docs/images/miniapps/movie_lister/classes.png b/docs/images/miniapps/movie_lister/classes.png deleted file mode 100644 index 78db720d..00000000 Binary files a/docs/images/miniapps/movie_lister/classes.png and /dev/null differ diff --git a/docs/introduction/di_in_python.rst b/docs/introduction/di_in_python.rst index ad1dc7a9..644fef9f 100644 --- a/docs/introduction/di_in_python.rst +++ b/docs/introduction/di_in_python.rst @@ -135,8 +135,9 @@ Choose one of the following as a next step: + :ref:`flask-tutorial` + :ref:`aiohttp-tutorial` + :ref:`asyncio-daemon-tutorial` -+ Know more about the :ref:`providers`. -+ Go to the :ref:`contents`. + + :ref:`cli-tutorial` ++ Know more about the :ref:`providers` ++ Go to the :ref:`contents` Useful links ~~~~~~~~~~~~ diff --git a/docs/providers/selector.rst b/docs/providers/selector.rst index 883e4fa0..29867217 100644 --- a/docs/providers/selector.rst +++ b/docs/providers/selector.rst @@ -1,3 +1,5 @@ +.. _selector-provider: + Selector providers ------------------ diff --git a/docs/tutorials/aiohttp.rst b/docs/tutorials/aiohttp.rst index 3c90e6e8..623d417b 100644 --- a/docs/tutorials/aiohttp.rst +++ b/docs/tutorials/aiohttp.rst @@ -922,7 +922,7 @@ We've used the ``Dependency Injector`` as a dependency injection framework. The benefit you get with the ``Dependency Injector`` is the container. It starts to payoff when you need to understand or change your application structure. It's easy with the container, -cause you have everything in one place: +cause you have everything defined explicitly in one place: .. code-block:: python @@ -962,8 +962,8 @@ cause you have everything in one place: What's next? -- Look at the other :ref:`tutorials`. -- Know more about the :ref:`providers`. -- Go to the :ref:`contents`. +- Look at the other :ref:`tutorials` +- Know more about the :ref:`providers` +- Go to the :ref:`contents` .. disqus:: diff --git a/docs/tutorials/asyncio-daemon.rst b/docs/tutorials/asyncio-daemon.rst index a6b6443a..38e1c460 100644 --- a/docs/tutorials/asyncio-daemon.rst +++ b/docs/tutorials/asyncio-daemon.rst @@ -1030,7 +1030,7 @@ We've used the ``Dependency Injector`` as a dependency injection framework. The benefit you get with the ``Dependency Injector`` is the container. It starts to payoff when you need to understand or change your application structure. It's easy with the container, -cause you have everything in one place: +cause you have everything defined explicitly in one place: .. code-block:: python @@ -1080,8 +1080,8 @@ cause you have everything in one place: What's next? -- Look at the other :ref:`tutorials`. -- Know more about the :ref:`providers`. -- Go to the :ref:`contents`. +- Look at the other :ref:`tutorials` +- Know more about the :ref:`providers` +- Go to the :ref:`contents` .. disqus:: diff --git a/docs/tutorials/cli-images/classes_01.png b/docs/tutorials/cli-images/classes_01.png new file mode 100644 index 00000000..ee4575a5 Binary files /dev/null and b/docs/tutorials/cli-images/classes_01.png differ diff --git a/docs/tutorials/cli-images/classes_02.png b/docs/tutorials/cli-images/classes_02.png new file mode 100644 index 00000000..f768d187 Binary files /dev/null and b/docs/tutorials/cli-images/classes_02.png differ diff --git a/docs/tutorials/cli.rst b/docs/tutorials/cli.rst new file mode 100644 index 00000000..d852b140 --- /dev/null +++ b/docs/tutorials/cli.rst @@ -0,0 +1,1054 @@ +.. _cli-tutorial: + +CLI application tutorial +======================== + +.. meta:: + :keywords: Python,CLI,Tutorial,Education,Web,Example,DI,Dependency injection,IoC, + Inversion of control,Refactoring,Tests,Unit tests,Pytest,py.test + :description: This tutorial shows how to build a CLI application following the dependency + injection principle. You will create the CLI script, use CSV files and sqlite + database, cover the application with the unit the tests and make some refactoring. + +This tutorial shows how to build a CLI application following the dependency injection +principle. + +Start from the scratch or jump to the section: + +.. contents:: + :local: + :backlinks: none + +You can find complete project on the +`Github `_. + +What are we going to build? +--------------------------- + +We will build a CLI application that helps to search for the movies. Let's call it Movie Lister. + +How does Movie Lister work? + +- There is a movies database +- Each movie has next fields: + - Title + - Year of the release + - Director's name +- The database is distributed in two formats: + - Csv + - Sqlite +- Application uses the movies database to search for the movies +- Application can search for the movies by: + - Director's name + - Year of the release +- Other database formats can be added later + +Movie Lister is a naive example from Martin Fowler's article about the dependency injection and +inversion of control: + + http://www.martinfowler.com/articles/injection.html + +Here is a class diagram of the Movie Lister application: + +.. image:: cli-images/classes_01.png + +The responsibilities are split next way: + +- ``MovieLister`` - is responsible for the search +- ``MovieFinder`` - is responsible for the fetching from the database +- ``Movie`` - the movie entity + +Prepare the environment +----------------------- + +Let's create the environment for the project. + +First we need to create a project folder and the virtual environment: + +.. code-block:: bash + + mkdir movie-lister-tutorial + cd movie-lister-tutorial + python3 -m venv venv + +Now let's activate the virtual environment: + +.. code-block:: bash + + . venv/bin/activate + +Project layout +-------------- + +Create next structure in the project root directory. All files are empty. That's ok for now. + +Initial project layout: + +.. code-block:: bash + + ./ + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ └── containers.py + ├── venv/ + ├── config.yml + └── requirements.txt + +Move on to the project requirements. + +Install the requirements +------------------------ + +Now it's time to install the project requirements. We will use next packages: + +- ``dependency-injector`` - the dependency injection framework +- ``pyyaml`` - the YAML files parsing library, used for the reading of the configuration files +- ``pytest`` - the test framework +- ``pytest-cov`` - the helper library for measuring the test coverage + +Put next lines into the ``requirements.txt`` file: + +.. code-block:: bash + + dependency-injector + pyyaml + pytest + pytest-cov + +and run next in the terminal: + +.. code-block:: bash + + pip install -r requirements.txt + +The requirements are setup. Now we will add the fixtures. + +Fixtures +-------- + +In this section we will add the fixtures. + +We will create a script that creates database files. + +First add the folder ``data/`` in the root of the project and then add the file +``fixtures.py`` inside of it: + +.. code-block:: bash + :emphasize-lines: 2-3 + + ./ + ├── data/ + │ └── fixtures.py + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ └── containers.py + ├── venv/ + ├── config.yml + └── requirements.txt + +Second put next in the ``fixtures.py``: + +.. code-block:: python + + """Fixtures module.""" + + import csv + import sqlite3 + import pathlib + + + 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'), + ] + + FILE = pathlib.Path(__file__) + DIR = FILE.parent + CSV_FILE = DIR / 'movies.csv' + SQLITE_FILE = DIR / 'movies.db' + + + def create_csv(movies_data, path): + with open(path, 'w') as opened_file: + writer = csv.writer(opened_file) + for row in movies_data: + writer.writerow(row) + + + 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)' + ) + 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') + + + if __name__ == '__main__': + main() + +Now run in the terminal: + +.. code-block:: bash + + python data/fixtures.py + +You should see: + +.. code-block:: bash + + OK + +Check that files ``movies.csv`` and ``movies.db`` have appeared in the ``data/`` folder: + +.. code-block:: bash + :emphasize-lines: 4-5 + + ./ + ├── data/ + │ ├── fixtures.py + │ ├── movies.csv + │ └── movies.db + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ └── containers.py + ├── venv/ + ├── config.yml + └── requirements.txt + +Fixtures are created. Let's move on. + +Container +--------- + +In this section we will add the main part of our application - the container. + +Container will keep all of the application components and their dependencies. + +Edit ``containers.py``: + +.. code-block:: python + + """Containers module.""" + + from dependency_injector import containers + + + class ApplicationContainer(containers.DeclarativeContainer): + ... + +Container is empty for now. We will add the providers in the following sections. + +Let's also create the ``main()`` function. Its responsibility is to run our application. For now +it will just create the container. + +Edit ``__main__.py``: + +.. code-block:: python + + """Main module.""" + + from .containers import ApplicationContainer + + + def main(): + container = ApplicationContainer() + + + if __name__ == '__main__': + main() + +.. note:: + + Container is the first object in the application. + + The container is used to create all other objects. + +Csv finder +---------- + +In this section we will build everything we need for working with the csv file formats. + +We will add: + +- The ``Movie`` entity +- The ``MovieFinder`` base class +- The ``CsvMovieFinder`` finder implementation +- The ``MovieLister`` class + +After each step we will add the provider to the container. + +.. image:: cli-images/classes_02.png + +Create the ``entities.py`` in the ``movies`` package: + +.. code-block:: bash + :emphasize-lines: 10 + + ./ + ├── data/ + │ ├── fixtures.py + │ ├── movies.csv + │ └── movies.db + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ ├── containers.py + │ └── entities.py + ├── venv/ + ├── config.yml + └── requirements.txt + +and put next into it: + +.. code-block:: python + + """Movie entities module.""" + + + class Movie: + + def __init__(self, title: str, year: int, director: str): + self.title = str(title) + self.year = int(year) + self.director = str(director) + + def __repr__(self): + return '{0}(title={1}, year={2}, director={3})'.format( + self.__class__.__name__, + repr(self.title), + repr(self.year), + repr(self.director), + ) + +Now we need to add the ``Movie`` factory to the container. We need to add import of the +``providers`` module from the ``dependency_injector`` package, import ``entities`` module. + +Edit ``containers.py``: + +.. code-block:: python + :emphasize-lines: 3,5,9 + + """Containers module.""" + + from dependency_injector import containers, providers + + from . import entities + + class ApplicationContainer(containers.DeclarativeContainer): + + movie = providers.Factory(entities.Movie) + +.. note:: + + Don't forget to remove the Ellipsis ``...`` from the container. We don't need it anymore + since we container is not empty. + +Let's move on to the finders. + +Create the ``finders.py`` in the ``movies`` package: + +.. code-block:: bash + :emphasize-lines: 11 + + ./ + ├── data/ + │ ├── fixtures.py + │ ├── movies.csv + │ └── movies.db + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ ├── containers.py + │ ├── entities.py + │ └── finders.py + ├── venv/ + ├── config.yml + └── requirements.txt + +and put next into it: + +.. code-block:: python + + """Movie finders module.""" + + import csv + from typing import Callable, List + + from .entities import Movie + + + class MovieFinder: + + def __init__(self, movie_factory: Callable[..., Movie]) -> None: + self._movie_factory = movie_factory + + def find_all(self) -> List[Movie]: + raise NotImplementedError() + + + class CsvMovieFinder(MovieFinder): + + def __init__( + self, + movie_factory: Callable[..., Movie], + path: str, + delimiter: str, + ) -> None: + self._csv_file_path = path + self._delimiter = delimiter + super().__init__(movie_factory) + + def find_all(self) -> List[Movie]: + with open(self._csv_file_path) as csv_file: + csv_reader = csv.reader(csv_file, delimiter=self._delimiter) + return [self._movie_factory(*row) for row in csv_reader] + +Now let's add the csv finder into the container. + +Edit ``containers.py``: + +.. code-block:: python + :emphasize-lines: 5,9,13-18 + + """Containers module.""" + + from dependency_injector import containers, providers + + from . import finders, entities + + class ApplicationContainer(containers.DeclarativeContainer): + + config = providers.Configuration() + + movie = providers.Factory(entities.Movie) + + csv_finder = providers.Singleton( + finders.CsvMovieFinder, + movie_factory=movie.provider, + path=config.finder.csv.path, + delimiter=config.finder.csv.delimiter, + ) + +The csv finder needs the movie factory. It needs it to create the ``Movie`` entities when +reads the csv rows. To provide the factory we use ``.provider`` factory attribute. +This is also called the delegation of the provider. If we just pass the movie factory +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. + +Not let's define the configuration values. + +Edit ``config.yml``: + +.. code-block:: yaml + + finder: + + csv: + 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: 9 + + """Main module.""" + + from .containers import ApplicationContainer + + + def main(): + container = ApplicationContainer() + + container.config.from_yaml('config.yml') + + + if __name__ == '__main__': + main() + +Move on to the lister. + +Create the ``listers.py`` in the ``movies`` package: + +.. code-block:: bash + :emphasize-lines: 12 + + ./ + ├── data/ + │ ├── fixtures.py + │ ├── movies.csv + │ └── movies.db + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ ├── containers.py + │ ├── entities.py + │ ├── finders.py + │ └── listers.py + ├── venv/ + ├── config.yml + └── requirements.txt + +and put next into it: + +.. code-block:: python + + """Movie listers module.""" + + from .finders import MovieFinder + + + class MovieLister: + + def __init__(self, movie_finder: MovieFinder): + self._movie_finder = movie_finder + + def movies_directed_by(self, director): + return [ + movie for movie in self._movie_finder.find_all() + if movie.director == director + ] + + def movies_released_in(self, year): + return [ + movie for movie in self._movie_finder.find_all() + if movie.year == year + ] + +and edit ``containers.py``: + +.. code-block:: python + :emphasize-lines: 5,20-23 + + """Containers module.""" + + from dependency_injector import containers, providers + + from . import finders, listers, entities + + class ApplicationContainer(containers.DeclarativeContainer): + + config = providers.Configuration() + + movie = providers.Factory(entities.Movie) + + csv_finder = providers.Singleton( + finders.CsvMovieFinder, + movie_factory=movie.provider, + path=config.finder.csv.path, + delimiter=config.finder.csv.delimiter, + ) + + lister = providers.Factory( + listers.MovieLister, + movie_finder=csv_finder, + ) + +All the components are created and added to the container. + +Finally let's update the ``main()`` function. + +Edit ``__main__.py``: + +.. code-block:: python + :emphasize-lines: 11-20 + + """Main module.""" + + from .containers import ApplicationContainer + + + def main(): + container = ApplicationContainer() + + container.config.from_yaml('config.yml') + + lister = container.lister() + + print( + 'Francis Lawrence movies:', + lister.movies_directed_by('Francis Lawrence'), + ) + print( + '2016 movies:', + lister.movies_released_in(2016), + ) + + + if __name__ == '__main__': + main() + +All set. Now we run the application. + +Run in the terminal: + +.. code-block:: bash + + python -m movies + +You should see: + +.. code-block:: bash + + Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')] + 2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')] + +Our application can work with the movies database in the csv format. We also need to support +the sqlite format. We will deal with it in the next section. + +Sqlite finder +------------- + +In this section we will add another type of the finder - the sqlite finder. + +Let's get to work. + +Edit ``finders.py``: + +.. code-block:: python + :emphasize-lines: 4,37-50 + + """Movie finders module.""" + + import csv + import sqlite3 + from typing import Callable, List + + from .entities import Movie + + + class MovieFinder: + + def __init__(self, movie_factory: Callable[..., Movie]) -> None: + self._movie_factory = movie_factory + + def find_all(self) -> List[Movie]: + raise NotImplementedError() + + + class CsvMovieFinder(MovieFinder): + + def __init__( + self, + movie_factory: Callable[..., Movie], + path: str, + delimiter: str, + ) -> None: + self._csv_file_path = path + self._delimiter = delimiter + super().__init__(movie_factory) + + def find_all(self) -> List[Movie]: + with open(self._csv_file_path) as csv_file: + csv_reader = csv.reader(csv_file, delimiter=self._delimiter) + return [self._movie_factory(*row) for row in csv_reader] + + + class SqliteMovieFinder(MovieFinder): + + def __init__( + self, + movie_factory: Callable[..., Movie], + path: str, + ) -> None: + self._database = sqlite3.connect(path) + super().__init__(movie_factory) + + def find_all(self) -> List[Movie]: + with self._database as db: + 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. + +Edit ``containers.py``: + +.. code-block:: python + :emphasize-lines: 20-24,28 + + """Containers module.""" + + from dependency_injector import containers, providers + + from . import finders, listers, entities + + class ApplicationContainer(containers.DeclarativeContainer): + + config = providers.Configuration() + + movie = providers.Factory(entities.Movie) + + csv_finder = providers.Singleton( + finders.CsvMovieFinder, + movie_factory=movie.provider, + path=config.finder.csv.path, + delimiter=config.finder.csv.delimiter, + ) + + sqlite_finder = providers.Singleton( + finders.SqliteMovieFinder, + movie_factory=movie.provider, + path=config.finder.sqlite.path, + ) + + lister = providers.Factory( + listers.MovieLister, + movie_finder=sqlite_finder, + ) + +The sqlite finder has a dependency on the configuration option. Let's update the configuration +file. + +Edit ``config.yml``: + +.. code-block:: yaml + :emphasize-lines: 7-8 + + finder: + + csv: + path: "data/movies.csv" + delimiter: "," + + sqlite: + path: "data/movies.db" + +All is ready. Let's check. + +Run in the terminal: + +.. code-block:: bash + + python -m movies + +You should see: + +.. code-block:: bash + + Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')] + 2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')] + +Our application now supports both formats: csv files and sqlite databases. Every time when we +need to work with the different format we need to make a code change in the container. We will +improve this in the next section. + +Selector +-------- + +In this section we will make our application more flexible. + +The code change will not be needed to switch between csv and sqlite formats. We implement the +switch based on the environment variable ``MOVIE_FINDER_TYPE``: + +- When ``MOVIE_FINDER_TYPE=csv`` application uses csv finder. +- When ``MOVIE_FINDER_TYPE=sqlite`` application uses sqlite finder. + +We will use the ``Selector`` provider. It selects the provider based on the configuration option +(docs - :ref:`selector-provider`). + +Edit ``containers.py``: + +.. code-block:: python + :emphasize-lines: 27-31,35 + + """Containers module.""" + + from dependency_injector import containers, providers + + from . import finders, listers, entities + + + class ApplicationContainer(containers.DeclarativeContainer): + + config = providers.Configuration() + + movie = providers.Factory(entities.Movie) + + csv_finder = providers.Singleton( + finders.CsvMovieFinder, + movie_factory=movie.provider, + path=config.finder.csv.path, + delimiter=config.finder.csv.delimiter, + ) + + sqlite_finder = providers.Singleton( + finders.SqliteMovieFinder, + movie_factory=movie.provider, + path=config.finder.sqlite.path, + ) + + finder = providers.Selector( + config.finder.type, + csv=csv_finder, + sqlite=sqlite_finder, + ) + + lister = providers.Factory( + listers.MovieLister, + movie_finder=finder, + ) + +The switch is the ``config.finder.type`` option. When its value is ``csv``, the provider under +``csv`` key is used. The same is for ``sqlite``. + +Now we need to read the value of the ``config.finder.type`` option from the environment variable +``MOVIE_FINDER_TYPE``. + +Edit ``__main__.py``: + +.. code-block:: python + :emphasize-lines: 10 + + """Main module.""" + + from .containers import ApplicationContainer + + + def main(): + container = ApplicationContainer() + + container.config.from_yaml('config.yml') + container.config.finder.type.from_env('MOVIE_FINDER_TYPE') + + lister = container.lister() + + print( + 'Francis Lawrence movies:', + lister.movies_directed_by('Francis Lawrence'), + ) + print( + '2016 movies:', + lister.movies_released_in(2016), + ) + + + if __name__ == '__main__': + main() + +Done. + +Run in the terminal line by line: + +.. code-block:: bash + + MOVIE_FINDER_TYPE=csv python -m movies + MOVIE_FINDER_TYPE=sqlite python -m movies + +The output should be something like this for each command: + +.. code-block:: bash + + Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')] + 2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')] + +In the next section we will add some tests. + +Tests +----- + +It would be nice to add some tests. Let's do it. + +We will use `pytest `_ and +`coverage `_. + +Create ``tests.py`` in the ``movies`` package: + +.. code-block:: bash + :emphasize-lines: 13 + + ./ + ├── data/ + │ ├── fixtures.py + │ ├── movies.csv + │ └── movies.db + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ ├── containers.py + │ ├── entities.py + │ ├── finders.py + │ ├── listers.py + │ └── tests.py + ├── venv/ + ├── config.yml + └── requirements.txt + +and put next into it: + +.. code-block:: python + :emphasize-lines: 35,50 + + """Tests module.""" + + from unittest import mock + + import pytest + + from .containers import ApplicationContainer + + + @pytest.fixture + def container(): + container = ApplicationContainer() + container.config.from_dict({ + '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'), + ] + + with container.finder.override(finder_mock): + lister = container.lister() + movies = lister.movies_directed_by('Jon Favreau') + + assert len(movies) == 1 + 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'), + ] + + with container.finder.override(finder_mock): + lister = container.lister() + movies = lister.movies_released_in(2015) + + assert len(movies) == 1 + assert movies[0].title == 'The 33' + +Run in the terminal: + +.. code-block:: bash + + pytest movies/tests.py --cov=movies + +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: cov-2.10.0 + collected 2 items + + movies/tests.py .. [100%] + + ---------- coverage: platform darwin, python 3.8.3-final-0 ----------- + Name Stmts Miss Cover + ------------------------------------------ + movies/__init__.py 0 0 100% + movies/__main__.py 10 10 0% + movies/containers.py 9 0 100% + movies/entities.py 7 1 86% + movies/finders.py 26 13 50% + movies/listers.py 8 0 100% + movies/tests.py 24 0 100% + ------------------------------------------ + TOTAL 84 24 71% + +.. note:: + + Take a look at the highlights in the ``tests.py``. + + We use ``.override()`` method of the ``finder`` provider. Provider is overridden by the mock. + Every time when any other provider will request ``finder`` provider to provide the dependency, + the mock will be returned. So when we call the ``lister`` provider, the ``MovieLister`` + instance is created with the mock, not an actual ``MovieFinder``. + +Conclusion +---------- + +In this tutorial we've built a CLI application following the dependency injection principle. +We've used the ``Dependency Injector`` as a dependency injection framework. + +The benefit you get with the ``Dependency Injector`` is the container. It starts to payoff +when you need to understand or change your application structure. It's easy with the container, +cause you have everything defined explicitly in one place: + +.. code-block:: python + + """Containers module.""" + + from dependency_injector import containers, providers + + from . import finders, listers, entities + + + class ApplicationContainer(containers.DeclarativeContainer): + + config = providers.Configuration() + + movie = providers.Factory(entities.Movie) + + csv_finder = providers.Singleton( + finders.CsvMovieFinder, + movie_factory=movie.provider, + path=config.finder.csv.path, + delimiter=config.finder.csv.delimiter, + ) + + sqlite_finder = providers.Singleton( + finders.SqliteMovieFinder, + movie_factory=movie.provider, + path=config.finder.sqlite.path, + ) + + finder = providers.Selector( + config.finder.type, + csv=csv_finder, + sqlite=sqlite_finder, + ) + + lister = providers.Factory( + listers.MovieLister, + movie_finder=finder, + ) + +What's next? + +- Look at the other :ref:`tutorials` +- Know more about the :ref:`providers` +- Go to the :ref:`contents` + +.. disqus:: diff --git a/docs/tutorials/flask.rst b/docs/tutorials/flask.rst index af3a222e..e36b17c9 100644 --- a/docs/tutorials/flask.rst +++ b/docs/tutorials/flask.rst @@ -1097,7 +1097,7 @@ In this tutorial we've built a ``Flask`` application following the dependency in We've used the ``Dependency Injector`` as a dependency injection framework. The main part of this application is the container. It keeps all the application components and -their dependencies in one place: +their dependencies defined explicitly in one place: .. code-block:: python @@ -1141,9 +1141,9 @@ their dependencies in one place: What's next? -- Look at the other :ref:`tutorials`. -- Know more about the :ref:`providers`. -- Go to the :ref:`contents`. +- Look at the other :ref:`tutorials` +- Know more about the :ref:`providers` +- Go to the :ref:`contents` .. disqus:: diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst index bfe6a2c2..f686fe4f 100644 --- a/docs/tutorials/index.rst +++ b/docs/tutorials/index.rst @@ -12,5 +12,6 @@ frameworks. flask aiohttp asyncio-daemon + cli .. disqus:: diff --git a/examples/.pydocstylerc b/examples/.pydocstylerc index c5b7bc0f..4d4d1367 100644 --- a/examples/.pydocstylerc +++ b/examples/.pydocstylerc @@ -1,2 +1,2 @@ [pydocstyle] -ignore = D100,D101,D102,D103,D107,D203,D213 +ignore = D100,D101,D102,D103,D105,D107,D203,D213 diff --git a/examples/miniapps/movie-lister/README.rst b/examples/miniapps/movie-lister/README.rst new file mode 100644 index 00000000..54a8e3a1 --- /dev/null +++ b/examples/miniapps/movie-lister/README.rst @@ -0,0 +1,75 @@ +Movie lister - a naive example of dependency injection in Python +================================================================ + +This is a Python implementation of the dependency injection example from Martin Fowler's +article: + + http://www.martinfowler.com/articles/injection.html + +Run +--- + +Create a virtual environment: + +.. code-block:: bash + + virtualenv venv + . venv/bin/activate + +Install the requirements: + +.. code-block:: bash + + pip install -r requirements.txt + +To create the fixtures do: + +.. code-block:: bash + + python data/fixtures.py + +To run the application do: + +.. code-block:: bash + + MOVIE_FINDER_TYPE=csv python -m movies + MOVIE_FINDER_TYPE=sqlite python -m movies + +The output should be something like this for each command: + +.. code-block:: bash + + Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')] + 2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')] + +Test +---- + +To run the tests do: + +.. code-block:: bash + + pytest movies/tests.py --cov=movies + +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 + collected 2 items + + movies/tests.py .. [100%] + + ---------- coverage: platform darwin, python 3.8.3-final-0 ----------- + Name Stmts Miss Cover + ------------------------------------------ + movies/__init__.py 0 0 100% + movies/__main__.py 10 10 0% + movies/containers.py 9 0 100% + movies/entities.py 7 1 86% + movies/finders.py 26 13 50% + movies/listers.py 8 0 100% + movies/tests.py 24 0 100% + ------------------------------------------ + TOTAL 84 24 71% diff --git a/examples/miniapps/movie-lister/config.yml b/examples/miniapps/movie-lister/config.yml new file mode 100644 index 00000000..35421f1b --- /dev/null +++ b/examples/miniapps/movie-lister/config.yml @@ -0,0 +1,8 @@ +finder: + + csv: + path: "data/movies.csv" + delimiter: "," + + sqlite: + path: "data/movies.db" diff --git a/examples/miniapps/movie-lister/data/.gitignore b/examples/miniapps/movie-lister/data/.gitignore new file mode 100644 index 00000000..ba68614f --- /dev/null +++ b/examples/miniapps/movie-lister/data/.gitignore @@ -0,0 +1,6 @@ +# Everything +* + +# Except this file: +!.gitignore +!fixtures.py diff --git a/examples/miniapps/movie-lister/data/fixtures.py b/examples/miniapps/movie-lister/data/fixtures.py new file mode 100644 index 00000000..2870d04d --- /dev/null +++ b/examples/miniapps/movie-lister/data/fixtures.py @@ -0,0 +1,44 @@ +"""Fixtures module.""" + +import csv +import sqlite3 +import pathlib + + +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'), +] + +FILE = pathlib.Path(__file__) +DIR = FILE.parent +CSV_FILE = DIR / 'movies.csv' +SQLITE_FILE = DIR / 'movies.db' + + +def create_csv(movies_data, path): + with open(path, 'w') as opened_file: + writer = csv.writer(opened_file) + for row in movies_data: + writer.writerow(row) + + +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)' + ) + 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') + + +if __name__ == '__main__': + main() diff --git a/examples/miniapps/movie-lister/movies/__init__.py b/examples/miniapps/movie-lister/movies/__init__.py new file mode 100644 index 00000000..1c744ca5 --- /dev/null +++ b/examples/miniapps/movie-lister/movies/__init__.py @@ -0,0 +1 @@ +"""Top-level package.""" diff --git a/examples/miniapps/movie-lister/movies/__main__.py b/examples/miniapps/movie-lister/movies/__main__.py new file mode 100644 index 00000000..b51c4010 --- /dev/null +++ b/examples/miniapps/movie-lister/movies/__main__.py @@ -0,0 +1,25 @@ +"""Main module.""" + +from .containers import ApplicationContainer + + +def main(): + container = ApplicationContainer() + + container.config.from_yaml('config.yml') + container.config.finder.type.from_env('MOVIE_FINDER_TYPE') + + lister = container.lister() + + print( + 'Francis Lawrence movies:', + lister.movies_directed_by('Francis Lawrence'), + ) + print( + '2016 movies:', + lister.movies_released_in(2016), + ) + + +if __name__ == '__main__': + main() diff --git a/examples/miniapps/movie-lister/movies/containers.py b/examples/miniapps/movie-lister/movies/containers.py new file mode 100644 index 00000000..0ffd7f16 --- /dev/null +++ b/examples/miniapps/movie-lister/movies/containers.py @@ -0,0 +1,36 @@ +"""Containers module.""" + +from dependency_injector import containers, providers + +from . import finders, listers, entities + + +class ApplicationContainer(containers.DeclarativeContainer): + + config = providers.Configuration() + + movie = providers.Factory(entities.Movie) + + csv_finder = providers.Singleton( + finders.CsvMovieFinder, + movie_factory=movie.provider, + path=config.finder.csv.path, + delimiter=config.finder.csv.delimiter, + ) + + sqlite_finder = providers.Singleton( + finders.SqliteMovieFinder, + movie_factory=movie.provider, + path=config.finder.sqlite.path, + ) + + finder = providers.Selector( + config.finder.type, + csv=csv_finder, + sqlite=sqlite_finder, + ) + + lister = providers.Factory( + listers.MovieLister, + movie_finder=finder, + ) diff --git a/examples/miniapps/movie-lister/movies/entities.py b/examples/miniapps/movie-lister/movies/entities.py new file mode 100644 index 00000000..ccd27256 --- /dev/null +++ b/examples/miniapps/movie-lister/movies/entities.py @@ -0,0 +1,17 @@ +"""Movie entities module.""" + + +class Movie: + + def __init__(self, title: str, year: int, director: str): + self.title = str(title) + self.year = int(year) + self.director = str(director) + + def __repr__(self): + return '{0}(title={1}, year={2}, director={3})'.format( + self.__class__.__name__, + repr(self.title), + repr(self.year), + repr(self.director), + ) diff --git a/examples/miniapps/movie-lister/movies/finders.py b/examples/miniapps/movie-lister/movies/finders.py new file mode 100644 index 00000000..3485d8c3 --- /dev/null +++ b/examples/miniapps/movie-lister/movies/finders.py @@ -0,0 +1,50 @@ +"""Movie finders module.""" + +import csv +import sqlite3 +from typing import Callable, List + +from .entities import Movie + + +class MovieFinder: + + def __init__(self, movie_factory: Callable[..., Movie]) -> None: + self._movie_factory = movie_factory + + def find_all(self) -> List[Movie]: + raise NotImplementedError() + + +class CsvMovieFinder(MovieFinder): + + def __init__( + self, + movie_factory: Callable[..., Movie], + path: str, + delimiter: str, + ) -> None: + self._csv_file_path = path + self._delimiter = delimiter + super().__init__(movie_factory) + + def find_all(self) -> List[Movie]: + with open(self._csv_file_path) as csv_file: + csv_reader = csv.reader(csv_file, delimiter=self._delimiter) + return [self._movie_factory(*row) for row in csv_reader] + + +class SqliteMovieFinder(MovieFinder): + + def __init__( + self, + movie_factory: Callable[..., Movie], + path: str, + ) -> None: + self._database = sqlite3.connect(path) + super().__init__(movie_factory) + + def find_all(self) -> List[Movie]: + with self._database as db: + rows = db.execute('SELECT title, year, director FROM movies') + return [self._movie_factory(*row) for row in rows] diff --git a/examples/miniapps/movie-lister/movies/listers.py b/examples/miniapps/movie-lister/movies/listers.py new file mode 100644 index 00000000..36d254c2 --- /dev/null +++ b/examples/miniapps/movie-lister/movies/listers.py @@ -0,0 +1,21 @@ +"""Movie listers module.""" + +from .finders import MovieFinder + + +class MovieLister: + + def __init__(self, movie_finder: MovieFinder): + self._movie_finder = movie_finder + + def movies_directed_by(self, director): + return [ + movie for movie in self._movie_finder.find_all() + if movie.director == director + ] + + def movies_released_in(self, year): + return [ + movie for movie in self._movie_finder.find_all() + if movie.year == year + ] diff --git a/examples/miniapps/movie-lister/movies/tests.py b/examples/miniapps/movie-lister/movies/tests.py new file mode 100644 index 00000000..f6bfe81f --- /dev/null +++ b/examples/miniapps/movie-lister/movies/tests.py @@ -0,0 +1,55 @@ +"""Tests module.""" + +from unittest import mock + +import pytest + +from .containers import ApplicationContainer + + +@pytest.fixture +def container(): + container = ApplicationContainer() + container.config.from_dict({ + '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'), + ] + + with container.finder.override(finder_mock): + lister = container.lister() + movies = lister.movies_directed_by('Jon Favreau') + + assert len(movies) == 1 + 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'), + ] + + with container.finder.override(finder_mock): + lister = container.lister() + movies = lister.movies_released_in(2015) + + assert len(movies) == 1 + assert movies[0].title == 'The 33' diff --git a/examples/miniapps/movie-lister/requirements.txt b/examples/miniapps/movie-lister/requirements.txt new file mode 100644 index 00000000..72ff627e --- /dev/null +++ b/examples/miniapps/movie-lister/requirements.txt @@ -0,0 +1,4 @@ +dependency-injector +pyyaml +pytest +pytest-cov diff --git a/examples/miniapps/movie_lister/README.rst b/examples/miniapps/movie_lister/README.rst deleted file mode 100644 index 616b9106..00000000 --- a/examples/miniapps/movie_lister/README.rst +++ /dev/null @@ -1,16 +0,0 @@ -A naive example of dependency injection in Python -================================================= - -Example implementation of dependency injection on Python from Martin Fowler's -article about dependency injection and inversion of control: - -http://www.martinfowler.com/articles/injection.html - - -Instructions for running: - -.. code-block:: bash - - python app_csv.py - python app_db.py - python app_db_csv.py diff --git a/examples/miniapps/movie_lister/app_csv.py b/examples/miniapps/movie_lister/app_csv.py deleted file mode 100644 index 052492bc..00000000 --- a/examples/miniapps/movie_lister/app_csv.py +++ /dev/null @@ -1,49 +0,0 @@ -"""A naive example of dependency injection on Python. - -Example implementation of dependency injection in Python from Martin Fowler's -article about dependency injection and inversion of control: - -http://www.martinfowler.com/articles/injection.html - -This mini application uses ``movies`` library, that is configured to work with -csv file movies database. -""" - -import movies -import movies.finders - -import example.db -import example.main - -import settings -import fixtures - -import dependency_injector.containers as containers -import dependency_injector.providers as providers - - -@containers.override(movies.MoviesModule) -class MyMoviesModule(containers.DeclarativeContainer): - """IoC container for overriding movies module component providers.""" - - finder = providers.Factory(movies.finders.CsvMovieFinder, - csv_file_path=settings.MOVIES_CSV_PATH, - delimiter=',', - **movies.MoviesModule.finder.kwargs) - - -class CsvApplication(containers.DeclarativeContainer): - """IoC container of csv application component providers.""" - - main = providers.Callable(example.main.main, - movie_lister=movies.MoviesModule.lister) - - init_db = providers.Callable(example.db.init_csv, - movies_data=fixtures.MOVIES_SAMPLE_DATA, - csv_file_path=settings.MOVIES_CSV_PATH, - delimiter=',') - - -if __name__ == '__main__': - CsvApplication.init_db() - CsvApplication.main() diff --git a/examples/miniapps/movie_lister/app_db.py b/examples/miniapps/movie_lister/app_db.py deleted file mode 100644 index dee6bdae..00000000 --- a/examples/miniapps/movie_lister/app_db.py +++ /dev/null @@ -1,55 +0,0 @@ -"""A naive example of dependency injection on Python. - -Example implementation of dependency injection in Python from Martin Fowler's -article about dependency injection and inversion of control: - -http://www.martinfowler.com/articles/injection.html - -This mini application uses ``movies`` library, that is configured to work with -sqlite movies database. -""" - -import sqlite3 - -import movies -import movies.finders - -import example.db -import example.main - -import settings -import fixtures - -import dependency_injector.containers as containers -import dependency_injector.providers as providers - - -class ResourcesModule(containers.DeclarativeContainer): - """IoC container of application resource providers.""" - - database = providers.Singleton(sqlite3.connect, settings.MOVIES_DB_PATH) - - -@containers.override(movies.MoviesModule) -class MyMoviesModule(containers.DeclarativeContainer): - """IoC container for overriding movies module component providers.""" - - finder = providers.Factory(movies.finders.SqliteMovieFinder, - database=ResourcesModule.database, - **movies.MoviesModule.finder.kwargs) - - -class DbApplication(containers.DeclarativeContainer): - """IoC container of database application component providers.""" - - main = providers.Callable(example.main.main, - movie_lister=movies.MoviesModule.lister) - - init_db = providers.Callable(example.db.init_sqlite, - movies_data=fixtures.MOVIES_SAMPLE_DATA, - database=ResourcesModule.database) - - -if __name__ == '__main__': - DbApplication.init_db() - DbApplication.main() diff --git a/examples/miniapps/movie_lister/app_db_csv.py b/examples/miniapps/movie_lister/app_db_csv.py deleted file mode 100644 index 9822ff1d..00000000 --- a/examples/miniapps/movie_lister/app_db_csv.py +++ /dev/null @@ -1,80 +0,0 @@ -"""A naive example of dependency injection on Python. - -Example implementation of dependency injection in Python from Martin Fowler's -article about dependency injection and inversion of control: - -http://www.martinfowler.com/articles/injection.html - -This mini application uses ``movies`` library, that is configured to work with -sqlite movies database and csv file movies database. -""" - -import sqlite3 - -import movies -import movies.finders - -import example.db -import example.main - -import settings -import fixtures - -import dependency_injector.containers as containers -import dependency_injector.providers as providers - - -class ResourcesModule(containers.DeclarativeContainer): - """IoC container of application resource providers.""" - - database = providers.Singleton(sqlite3.connect, settings.MOVIES_DB_PATH) - - -@containers.copy(movies.MoviesModule) -class DbMoviesModule(movies.MoviesModule): - """IoC container for overriding movies module component providers.""" - - finder = providers.Factory(movies.finders.SqliteMovieFinder, - database=ResourcesModule.database, - **movies.MoviesModule.finder.kwargs) - - -@containers.copy(movies.MoviesModule) -class CsvMoviesModule(movies.MoviesModule): - """IoC container for overriding movies module component providers.""" - - finder = providers.Factory(movies.finders.CsvMovieFinder, - csv_file_path=settings.MOVIES_CSV_PATH, - delimiter=',', - **movies.MoviesModule.finder.kwargs) - - -class DbApplication(containers.DeclarativeContainer): - """IoC container of database application component providers.""" - - main = providers.Callable(example.main.main, - movie_lister=DbMoviesModule.lister) - - init_db = providers.Callable(example.db.init_sqlite, - movies_data=fixtures.MOVIES_SAMPLE_DATA, - database=ResourcesModule.database) - - -class CsvApplication(containers.DeclarativeContainer): - """IoC container of csv application component providers.""" - - main = providers.Callable(example.main.main, - movie_lister=CsvMoviesModule.lister) - - init_db = providers.Callable(example.db.init_csv, - movies_data=fixtures.MOVIES_SAMPLE_DATA, - csv_file_path=settings.MOVIES_CSV_PATH, - delimiter=',') - - -if __name__ == '__main__': - DbApplication.init_db() - DbApplication.main() - - CsvApplication.init_db() - CsvApplication.main() diff --git a/examples/miniapps/movie_lister/data/.gitignore b/examples/miniapps/movie_lister/data/.gitignore deleted file mode 100644 index bc208b8f..00000000 --- a/examples/miniapps/movie_lister/data/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Ignore everything in this directory -* - -# Except this file: -!.gitignore diff --git a/examples/miniapps/movie_lister/data/movies.csv b/examples/miniapps/movie_lister/data/movies.csv deleted file mode 100644 index 6cb91ddf..00000000 --- a/examples/miniapps/movie_lister/data/movies.csv +++ /dev/null @@ -1,3 +0,0 @@ -The Hunger Games: Mockingjay - Part 2,2015,Francis Lawrence -The 33,2015,Patricia Riggen -Star Wars: Episode VII - The Force Awakens,2015,JJ Abrams diff --git a/examples/miniapps/movie_lister/example/__init__.py b/examples/miniapps/movie_lister/example/__init__.py deleted file mode 100644 index bfa99aa2..00000000 --- a/examples/miniapps/movie_lister/example/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Example top-level package.""" diff --git a/examples/miniapps/movie_lister/example/db.py b/examples/miniapps/movie_lister/example/db.py deleted file mode 100644 index d519fb03..00000000 --- a/examples/miniapps/movie_lister/example/db.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Example database module.""" - -import csv - - -def init_sqlite(movies_data, database): - """Initialize sqlite3 movies database. - - :param movies_data: Data about movies - :type movies_data: tuple[tuple] - - :param database: Connection to sqlite database with movies data - :type database: sqlite3.Connection - """ - with database: - database.execute('CREATE TABLE IF NOT EXISTS movies ' - '(name text, year int, director text)') - database.execute('DELETE FROM movies') - database.executemany('INSERT INTO movies VALUES (?,?,?)', movies_data) - - -def init_csv(movies_data, csv_file_path, delimiter): - """Initialize csv movies database. - - :param movies_data: Data about movies - :type movies_data: tuple[tuple] - - :param csv_file_path: Path to csv file with movies data - :type csv_file_path: str - - :param delimiter: Csv file's delimiter - :type delimiter: str - """ - with open(csv_file_path, 'w') as csv_file: - csv.writer(csv_file, delimiter=delimiter).writerows(movies_data) diff --git a/examples/miniapps/movie_lister/example/main.py b/examples/miniapps/movie_lister/example/main.py deleted file mode 100644 index c029226b..00000000 --- a/examples/miniapps/movie_lister/example/main.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Example main module.""" - - -def main(movie_lister): - """Run application. - - This program prints info about all movies that were directed by different - persons and then prints all movies that were released in 2015. - - :param movie_lister: Movie lister instance - :type movie_lister: movies.listers.MovieLister - """ - print(movie_lister.movies_directed_by('Francis Lawrence')) - print(movie_lister.movies_directed_by('Patricia Riggen')) - print(movie_lister.movies_directed_by('JJ Abrams')) - - print(movie_lister.movies_released_in(2015)) diff --git a/examples/miniapps/movie_lister/fixtures.py b/examples/miniapps/movie_lister/fixtures.py deleted file mode 100644 index 0e8805be..00000000 --- a/examples/miniapps/movie_lister/fixtures.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Fixtures module.""" - - -MOVIES_SAMPLE_DATA = ( - ('The Hunger Games: Mockingjay - Part 2', 2015, 'Francis Lawrence'), - ('The 33', 2015, 'Patricia Riggen'), - ('Star Wars: Episode VII - The Force Awakens', 2015, 'JJ Abrams'), -) diff --git a/examples/miniapps/movie_lister/movies/__init__.py b/examples/miniapps/movie_lister/movies/__init__.py deleted file mode 100644 index 73331b9a..00000000 --- a/examples/miniapps/movie_lister/movies/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Movies package. - -Top-level package of movies library. This package contains IoC container of -movies module component providers - ``MoviesModule``. It is recommended to use -movies library functionality by fetching required instances from -``MoviesModule`` providers. - -``MoviesModule.finder`` is a factory that provides abstract component -``finders.MovieFinder``. This provider should be overridden by provider of -concrete finder implementation in terms of library configuration. - -Each of ``MoviesModule`` providers could be overridden. -""" - -import movies.finders -import movies.listers -import movies.models - -import dependency_injector.containers as containers -import dependency_injector.providers as providers - - -class MoviesModule(containers.DeclarativeContainer): - """IoC container of movies module component providers.""" - - movie = providers.Factory(movies.models.Movie) - - finder = providers.AbstractFactory(movies.finders.MovieFinder, - movie_model=movie.provider) - - lister = providers.Factory(movies.listers.MovieLister, - movie_finder=finder) diff --git a/examples/miniapps/movie_lister/movies/finders.py b/examples/miniapps/movie_lister/movies/finders.py deleted file mode 100644 index 557dcbdb..00000000 --- a/examples/miniapps/movie_lister/movies/finders.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Movie finders module. - -This module contains all finder implementations. -""" - -import csv - - -class MovieFinder: - """Movie finder component. - - Movie finder component is responsible for fetching movies data from - various storage. - """ - - def __init__(self, movie_model): - """Initialize instance. - - :param movie_model: Movie model's factory - :type movie_model: movies.models.Movie - """ - self._movie_model = movie_model - - def find_all(self): - """Return all found movies. - - :rtype: list[movies.models.Movie] - :return: List of movie instances. - """ - raise NotImplementedError() - - -class CsvMovieFinder(MovieFinder): - """Movie finder that fetches movies data from csv file.""" - - def __init__(self, movie_model, csv_file_path, delimiter): - """Initialize instance. - - :param movie_model: Movie model's factory - :type movie_model: movies.models.Movie - - :param csv_file_path: Path to csv file with movies data - :type csv_file_path: str - - :param delimiter: Csv file's delimiter - :type delimiter: str - """ - self._csv_file_path = csv_file_path - self._delimiter = delimiter - super().__init__(movie_model) - - def find_all(self): - """Return all found movies. - - :rtype: list[movies.models.Movie] - :return: List of movie instances. - """ - with open(self._csv_file_path) as csv_file: - csv_reader = csv.reader(csv_file, delimiter=self._delimiter) - return [self._movie_model(*row) for row in csv_reader] - - -class SqliteMovieFinder(MovieFinder): - """Movie finder that fetches movies data from sqlite database.""" - - def __init__(self, movie_model, database): - """Initialize instance. - - :param movie_model: Movie model's factory - :type movie_model: (object) -> movies.models.Movie - - :param database: Connection to sqlite database with movies data - :type database: sqlite3.Connection - """ - self._database = database - super().__init__(movie_model) - - def find_all(self): - """Return all found movies. - - :rtype: list[movies.models.Movie] - :return: List of movie instances. - """ - with self._database: - rows = self._database.execute('SELECT name, year, director ' - 'FROM movies') - return [self._movie_model(*row) for row in rows] diff --git a/examples/miniapps/movie_lister/movies/listers.py b/examples/miniapps/movie_lister/movies/listers.py deleted file mode 100644 index f00b22ba..00000000 --- a/examples/miniapps/movie_lister/movies/listers.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Movie listers module. - -This module contains all lister implementations. -""" - - -class MovieLister: - """Movie lister component. - - Movie lister component provides several methods for filtering movies by - specific criteria. - """ - - def __init__(self, movie_finder): - """Initialize instance. - - :param movie_finder: Movie finder instance - :type movie_finder: movies.finders.MovieFinder - """ - self._movie_finder = movie_finder - - def movies_directed_by(self, director): - """Return list of movies that were directed by certain person. - - :param director: Director's name - :type director: str - - :rtype: list[movies.models.Movie] - :return: List of movie instances. - """ - return [movie for movie in self._movie_finder.find_all() - if movie.director == director] - - def movies_released_in(self, year): - """Return list of movies that were released in certain year. - - :param year: Release year - :type year: int - - :rtype: list[movies.models.Movie] - :return: List of movie instances. - """ - return [movie for movie in self._movie_finder.find_all() - if movie.year == year] diff --git a/examples/miniapps/movie_lister/movies/models.py b/examples/miniapps/movie_lister/movies/models.py deleted file mode 100644 index c42d6903..00000000 --- a/examples/miniapps/movie_lister/movies/models.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Movie models module. - -This module contains all model implementations. -""" - - -class Movie: - """Base movie model.""" - - def __init__(self, name, year, director): - """Initialize instance. - - :param name: Movie's name - :type name: str - - :param year: Year, when movie was released - :type year: int - - :param director: Name of person, that directed the movie - :type director: str - """ - self.name = str(name) - self.year = int(year) - self.director = str(director) - - def __repr__(self): - """Return string representation of movie instance. - - :rtype: str - :return: Movie's string representation. - """ - return '{0}(name={1}, year={2}, director={3})'.format( - self.__class__.__name__, - repr(self.name), - repr(self.year), - repr(self.director)) diff --git a/examples/miniapps/movie_lister/settings.py b/examples/miniapps/movie_lister/settings.py deleted file mode 100644 index 5aab4184..00000000 --- a/examples/miniapps/movie_lister/settings.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Settings module. - -This module contains application's settings and constants. -""" - -import os - - -DATA_DIR = os.path.abspath(os.path.dirname(__file__) + '/data') -MOVIES_CSV_PATH = DATA_DIR + '/movies.csv' -MOVIES_DB_PATH = DATA_DIR + '/movies.db'