Rework movie lister example app

This commit is contained in:
Roman Mogylatov 2020-08-11 17:29:06 -04:00
parent ca18fea26c
commit a21c0c6160
26 changed files with 314 additions and 468 deletions

View File

@ -0,0 +1,67 @@
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 about dependency injection and inversion of control:
http://www.martinfowler.com/articles/injection.html
Create virtual environment:
.. code-block:: bash
virtualenv venv
. venv/bin/activate
Install requirements:
.. code-block:: bash
pip install -r requirements.txt
To run the application do:
.. code-block:: bash
MOVIE_STORAGE_TYPE=csv python -m movies
MOVIE_STORAGE_TYPE=sqlite python -m movies
The output should be something like:
.. code-block:: bash
[Movie(name='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')]
[Movie(name='The 33', year=2015, director='Patricia Riggen')]
[Movie(name='Star Wars: Episode VII - The Force Awakens', year=2015, director='JJ Abrams')]
[Movie(name='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence'), Movie(name='The 33', year=2015, director='Patricia Riggen'), Movie(name='Star Wars: Episode VII - The Force Awakens', year=2015, director='JJ Abrams')]
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 15 15 0%
movies/containers.py 9 0 100%
movies/finders.py 9 0 100%
movies/fixtures.py 1 0 100%
movies/listers.py 8 0 100%
movies/models.py 7 1 86%
movies/storages.py 32 17 47%
movies/tests.py 24 0 100%
------------------------------------------
TOTAL 105 33 69%

View File

@ -0,0 +1,8 @@
storage:
csv:
path: "data/movies.csv"
delimiter: ","
sqlite:
path: "data/movies.db"

View File

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

View File

@ -0,0 +1,23 @@
"""Main module."""
from .containers import ApplicationContainer
def main():
container = ApplicationContainer()
container.config.from_yaml('config.yml')
container.config.storage.type.from_env('MOVIE_STORAGE_TYPE')
storage = container.storage()
fixtures = container.fixtures()
storage.load_all(fixtures)
lister = container.lister()
print(lister.movies_directed_by('Francis Lawrence'))
print(lister.movies_directed_by('Patricia Riggen'))
print(lister.movies_directed_by('JJ Abrams'))
print(lister.movies_released_in(2015))
if __name__ == '__main__':
main()

View File

@ -0,0 +1,39 @@
"""Containers module."""
from dependency_injector import containers, providers
from . import finders, listers, storages, models, fixtures
class ApplicationContainer(containers.DeclarativeContainer):
config = providers.Configuration()
fixtures = providers.Object(fixtures.MOVIES_SAMPLE_DATA)
storage = providers.Singleton(
providers.Selector(
config.storage.type,
csv=providers.Factory(
storages.CsvMovieStorage,
options=config.storage[config.storage.type],
),
sqlite=providers.Factory(
storages.SqliteMovieStorage,
options=config.storage[config.storage.type],
),
),
)
movie = providers.Factory(models.Movie)
finder = providers.Factory(
finders.MovieFinder,
movie_factory=movie.provider,
movie_storage=storage,
)
lister = providers.Factory(
listers.MovieLister,
movie_finder=finder,
)

View File

@ -0,0 +1,23 @@
"""Movie finders module."""
from typing import Callable, List
from .models import Movie
from .storages import MovieStorage
class MovieFinder:
def __init__(
self,
movie_factory: Callable[..., Movie],
movie_storage: MovieStorage,
) -> None:
self._movie_factory = movie_factory
self._movie_storage = movie_storage
def find_all(self) -> List[Movie]:
return [
self._movie_factory(*row)
for row in self._movie_storage.get_all()
]

View File

@ -1,8 +1,8 @@
"""Fixtures module.""" """Fixtures module."""
MOVIES_SAMPLE_DATA = ( MOVIES_SAMPLE_DATA = [
('The Hunger Games: Mockingjay - Part 2', 2015, 'Francis Lawrence'), ('The Hunger Games: Mockingjay - Part 2', 2015, 'Francis Lawrence'),
('The 33', 2015, 'Patricia Riggen'), ('The 33', 2015, 'Patricia Riggen'),
('Star Wars: Episode VII - The Force Awakens', 2015, 'JJ Abrams'), ('Star Wars: Episode VII - The Force Awakens', 2015, 'JJ Abrams'),
) ]

View File

@ -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
]

View File

@ -0,0 +1,16 @@
"""Movie module."""
class Movie:
def __init__(self, name: str, year: int, director: str):
self.name = str(name)
self.year = int(year)
self.director = str(director)
def __repr__(self):
return '{0}(name={1}, year={2}, director={3})'.format(
self.__class__.__name__,
repr(self.name),
repr(self.year),
repr(self.director))

View File

@ -0,0 +1,55 @@
"""Movie storages module."""
import csv
import sqlite3
from typing import List, Tuple, Any
Row = Tuple[Any]
class MovieStorage:
def load_all(self, movie_data: List[Row]):
raise NotImplementedError()
def get_all(self) -> List[Row]:
raise NotImplementedError()
class CsvMovieStorage(MovieStorage):
def __init__(self, options) -> None:
self._csv_file_path = options.pop('path')
self._delimiter = options.pop('delimiter')
def load_all(self, movie_data: List[Row]) -> None:
with open(self._csv_file_path, 'w') as csv_file:
csv.writer(csv_file, delimiter=self._delimiter).writerows(movie_data)
def get_all(self) -> List[Row]:
with open(self._csv_file_path) as csv_file:
csv_reader = csv.reader(csv_file, delimiter=self._delimiter)
return [row for row in csv_reader]
class SqliteMovieStorage(MovieStorage):
def __init__(self, options) -> None:
self._database = sqlite3.connect(database=options.pop('path'))
def load_all(self, movie_data: List[Row]) -> None:
with self._database as db:
db.execute(
'CREATE TABLE IF NOT EXISTS movies '
'(name text, year int, director text)',
)
db.execute('DELETE FROM movies')
db.executemany('INSERT INTO movies VALUES (?,?,?)', movie_data)
def get_all(self) -> List[Row]:
with self._database as db:
rows = db.execute(
'SELECT name, year, director '
'FROM movies',
)
return [row for row in rows]

View File

@ -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({
'storage': {
'type': 'csv',
'csv': {
'path': '/fake-movies.csv',
'delimiter': ',',
},
'sqlite': {
'path': '/fake-movies.db',
},
},
})
return container
def test_movies_directed_by(container):
storage_mock = mock.Mock()
storage_mock.get_all.return_value = [
('The 33', 2015, 'Patricia Riggen'),
('The Jungle Book', 2016, 'Jon Favreau'),
]
with container.storage.override(storage_mock):
lister = container.lister()
movies = lister.movies_directed_by('Jon Favreau')
assert len(movies) == 1
assert movies[0].name == 'The Jungle Book'
def test_movies_released_in(container):
storage_mock = mock.Mock()
storage_mock.get_all.return_value = [
('The 33', 2015, 'Patricia Riggen'),
('The Jungle Book', 2016, 'Jon Favreau'),
]
with container.storage.override(storage_mock):
lister = container.lister()
movies = lister.movies_released_in(2015)
assert len(movies) == 1
assert movies[0].name == 'The 33'

View File

@ -0,0 +1,4 @@
dependency-injector
pyyaml
pytest
pytest-cov

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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
1 The Hunger Games: Mockingjay - Part 2 2015 Francis Lawrence
2 The 33 2015 Patricia Riggen
3 Star Wars: Episode VII - The Force Awakens 2015 JJ Abrams

View File

@ -1 +0,0 @@
"""Example top-level package."""

View File

@ -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)

View File

@ -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))

View File

@ -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)

View File

@ -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]

View File

@ -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]

View File

@ -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))

View File

@ -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'