Skip to content

Commit

Permalink
add dependency injection to improve separation of concerns and modula…
Browse files Browse the repository at this point in the history
…rity
  • Loading branch information
philtweir committed Jun 27, 2020
1 parent cf696b0 commit cb0b410
Show file tree
Hide file tree
Showing 45 changed files with 857 additions and 0 deletions.
Binary file added 022-burette/magnum_opus/.coverage
Binary file not shown.
4 changes: 4 additions & 0 deletions 022-burette/magnum_opus/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.egg-info
.tox
Pipfile
.env
24 changes: 24 additions & 0 deletions 022-burette/magnum_opus/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM python:3-alpine

RUN addgroup -S user && adduser user -S -G user

WORKDIR /home/user/

COPY requirements.txt .
COPY gunicorn_config.py .
COPY setup.py .
COPY setup.cfg .
COPY init_entrypoint.sh /

RUN pip install gunicorn
RUN pip install -r requirements.txt

USER user

EXPOSE 5000

ENTRYPOINT []

CMD gunicorn --config ./gunicorn_config.py magnumopus.index:app

COPY magnumopus magnumopus
1 change: 1 addition & 0 deletions 022-burette/magnum_opus/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

46 changes: 46 additions & 0 deletions 022-burette/magnum_opus/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
Magnum Opus
===========

Recipe maker for the philosopher's stone.


python3 -m pytest --cov magnumopus >> README.md

============================= test session starts ==============================
platform linux -- Python 3.8.2, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
rootdir: /home/philtweir/Work/Training/PythonCourse/python-course/022-burette/magnum_opus
plugins: pep8-1.0.6, cov-2.10.0
collected 15 items

tests/domain/test_alembic.py ....... [ 46%]
tests/domain/test_pantry.py .. [ 60%]
tests/domain/test_substance.py ...... [100%]

----------- coverage: platform linux, python 3.8.2-final-0 -----------
Name Stmts Miss Cover
------------------------------------------------------------------------
magnumopus/__init__.py 10 8 20%
magnumopus/config.py 4 4 0%
magnumopus/index.py 2 2 0%
magnumopus/initialize.py 6 6 0%
magnumopus/logger.py 4 4 0%
magnumopus/models/__init__.py 3 1 67%
magnumopus/models/base.py 2 0 100%
magnumopus/models/substance.py 24 0 100%
magnumopus/repositories/__init__.py 0 0 100%
magnumopus/repositories/pantry.py 12 1 92%
magnumopus/repositories/sqlalchemy_pantry.py 15 15 0%
magnumopus/resources/__init__.py 7 7 0%
magnumopus/resources/alembic_instruction.py 26 26 0%
magnumopus/resources/substance.py 28 28 0%
magnumopus/schemas/__init__.py 4 4 0%
magnumopus/schemas/substance_schema.py 11 11 0%
magnumopus/services/__init__.py 0 0 100%
magnumopus/services/alembic.py 32 2 94%
magnumopus/services/alembic_instruction_handler.py 20 20 0%
magnumopus/services/assessor.py 2 2 0%
------------------------------------------------------------------------
TOTAL 212 141 33%


============================== 15 passed in 0.38s ==============================
9 changes: 9 additions & 0 deletions 022-burette/magnum_opus/RULES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
* One unit of each of the substances Mercury, Salt and Sulphur are mixed, using my "Alembic" (mixing pot), giving one unit of another substance, Gloop
* Any attempt to mix anything other than those three substances, gives Sludge, another substance
* Substances can undergo several Processes in my Alembic - they can be Cooked, Washed, Pickled or Fermented
* If Gloop is Cooked, Washed, Pickled and Fermented, in that order, it is the Philosopher's Stone (panacea and cure of all ills)
[* To process a Substance, at least one unit must be in my Pantry, including Gloop - even when freshly processed/created, it must be stored there before re-use (to cool)]

Final rule:
GROUP 1: When I process a substance, using any process, it becomes a different substance
GROUP 2: When I process a substance, its state changes but is essentially the same substance (NB: mixing is not a process)
11 changes: 11 additions & 0 deletions 022-burette/magnum_opus/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: "3"
services:
web:
build: .
environment:
DATABASE_URI: 'sqlite:////docker/storage/storage.db'
volumes:
- ./docker:/docker
- ./magnumopus:/home/user/magnumopus
ports:
- 5000:5000
Binary file not shown.
5 changes: 5 additions & 0 deletions 022-burette/magnum_opus/gunicorn_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
port = '5000'
bind = "0.0.0.0:%s" % port
workers = 1
timeout = 600
reload = False
6 changes: 6 additions & 0 deletions 022-burette/magnum_opus/init_containers.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh

mkdir -p docker/storage

docker-compose run --user root web /init_entrypoint.sh
docker-compose run web python3 -m magnumopus.initialize
3 changes: 3 additions & 0 deletions 022-burette/magnum_opus/init_entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh

chown -R user:user /docker/storage
22 changes: 22 additions & 0 deletions 022-burette/magnum_opus/magnumopus/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from flask import Flask

def create_app():
from . import models, resources, schemas, logger, repositories, injection

app = Flask(__name__)

# This could be used to separate by environment
app.config.from_object('magnumopus.config.Config')

# This helps avoid cyclic dependencies
modules = []

modules += logger.init_app(app)
modules += models.init_app(app)
modules += resources.init_app(app)
modules += repositories.init_app(app)
modules += schemas.init_app(app)

injection.init_app(app, modules)

return app
7 changes: 7 additions & 0 deletions 022-burette/magnum_opus/magnumopus/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import os

class Config:
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI', 'sqlite:///:memory:')
SQLALCHEMY_TRACK_MODIFICATIONS = False

PANTRY_STORE = os.environ.get('MAGNUMOPUS_PANTRY_STORE', 'sqlalchemy')
3 changes: 3 additions & 0 deletions 022-burette/magnum_opus/magnumopus/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import create_app

app = create_app()
9 changes: 9 additions & 0 deletions 022-burette/magnum_opus/magnumopus/initialize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from . import create_app
from .models import db

if __name__ == "__main__":
# There are nicer ways around this, but this keeps it clear for an example
app = create_app()

with app.app_context():
db.create_all()
11 changes: 11 additions & 0 deletions 022-burette/magnum_opus/magnumopus/injection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from flask_injector import FlaskInjector

def init_app(app, modules):
modules.append(configure_injector)
FlaskInjector(app=app, modules=modules)

def configure_injector(binder):
"""
Add any general purpose injectables, not included in a submodule
"""
pass
6 changes: 6 additions & 0 deletions 022-burette/magnum_opus/magnumopus/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import logging

def init_app(app):
app.logger.addHandler(logging.StreamHandler())
app.logger.setLevel(logging.INFO)
return []
11 changes: 11 additions & 0 deletions 022-burette/magnum_opus/magnumopus/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from .base import db
from flask_sqlalchemy import SQLAlchemy

def init_app(app):
db.init_app(app)
return [configure_injector]

def configure_injector(binding):
binding.bind(
SQLAlchemy, to=db
)
3 changes: 3 additions & 0 deletions 022-burette/magnum_opus/magnumopus/models/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
34 changes: 34 additions & 0 deletions 022-burette/magnum_opus/magnumopus/models/substance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from . import db
from sqlalchemy_utils.types.scalar_list import ScalarListType

class SubstanceMustBeFreshToProcessException(Exception):
pass

class Substance(db.Model):
__tablename__ = 'substances'

id = db.Column(db.Integer, primary_key=True)
nature = db.Column(db.String(32), default='Unknown')
state = db.Column(ScalarListType())

def __init__(self, nature='Unknown'):
self.state = []
self.nature = nature

super(Substance, self).__init__()

def _process(self, process_name):
self.state.append(process_name)
return self

def cook(self):
return self._process('cooked')

def pickle(self):
return self._process('pickled')

def ferment(self):
return self._process('fermented')

def wash(self):
return self._process('washed')
18 changes: 18 additions & 0 deletions 022-burette/magnum_opus/magnumopus/repositories/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from .pantry import Pantry
from .list_pantry import ListPantry
from .sqlalchemy_pantry import SQLAlchemyPantry

PANTRY_STORES = {
'sqlalchemy': SQLAlchemyPantry,
'list': ListPantry() # we instantiate this, as we want a singleton cupboard
}

def init_app(app):
return [lambda binder: configure_injector(binder, app)]

def configure_injector(binder, app):
pantry_cls = PANTRY_STORES[app.config['PANTRY_STORE']]

binder.bind(
Pantry, to=pantry_cls
)
15 changes: 15 additions & 0 deletions 022-burette/magnum_opus/magnumopus/repositories/list_pantry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class ListPantry:
def __init__(self):
self._cupboard = []

def add_substance(self, substance):
self._cupboard.append(substance)

def find_substances_by_nature(self, nature):
return [substance for substance in self._cupboard if substance.nature == nature]

def count_all_substances(self):
return len(self._cupboard)

def commit(self):
pass
21 changes: 21 additions & 0 deletions 022-burette/magnum_opus/magnumopus/repositories/pantry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from abc import ABC, abstractmethod

class Pantry(ABC):
def __init__(self):
pass

@abstractmethod
def add_substance(self, substance):
pass

@abstractmethod
def find_substances_by_nature(self, nature):
pass

@abstractmethod
def count_all_substances(self):
pass

@abstractmethod
def commit(self):
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from flask_sqlalchemy import SQLAlchemy
from injector import inject

from ..models.substance import Substance
from ..models import db

class SQLAlchemyPantry:
@inject
def __init__(self, db: SQLAlchemy):
self._db = db

# A more involved solution would open and close the pantry... like
# a cupboard door, or a Unit of Work

# Note how we're committing too frequently?
def add_substance(self, substance):
self._db.session.add(substance)
return substance

def find_substances_by_nature(self, nature):
substances = Substance.query.filter_by(nature=nature).all()
return substances

def count_all_substances(self):
return Substance.query.count()

def commit(self):
self._db.session.commit()
9 changes: 9 additions & 0 deletions 022-burette/magnum_opus/magnumopus/resources/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from flask_restful import Api
from . import substance
from . import alembic_instruction

def init_app(app):
api = Api(app)
substance.init_app(app, api)
alembic_instruction.init_app(app, api)
return []
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from flask_restful import Resource, reqparse
from injector import inject
from ..repositories.pantry import Pantry
from ..models.substance import Substance
from ..schemas.substance_schema import SubstanceSchema
from ..services.alembic_instruction_handler import AlembicInstructionHandler, AlembicInstruction

class AlembicInstructionResource(Resource):
@inject
def __init__(self, pantry: Pantry, substance_schema: SubstanceSchema, parser_fcty=reqparse.RequestParser):
super(AlembicInstructionResource, self).__init__()

self._pantry = pantry
self._substance_schema = substance_schema
self._parser_fcty = parser_fcty

def get(self):
"""This should return past requests/commands."""
pass

def post(self):
"""
Add an instruction for the alembic.
Note that POST is _not_ assumed to be idempotent, unlike PUT
"""

parser = self._parser_fcty()
parser.add_argument('instruction_type')
parser.add_argument('action')
parser.add_argument('natures')

args = parser.parse_args()
instruction_type = args['instruction_type']

pantry = self._pantry

instruction_handler = AlembicInstructionHandler()

# This could do with deserialization...
instruction = AlembicInstruction(
instruction_type=args.instruction_type,
natures=args.natures.split(','),
action=args.action
)

# Crude start at DI... see flask-injector
result = instruction_handler.handle(instruction, pantry)

pantry.add_substance(result)

pantry.commit()

return self._substance_schema.dump(result)


def init_app(app, api):
api.add_resource(AlembicInstructionResource, '/alembic_instruction')
Loading

0 comments on commit cb0b410

Please sign in to comment.