From f0b2620f4431e68cdff36ffc0b0cc2c0a96b3e75 Mon Sep 17 00:00:00 2001 From: Phil Weir Date: Wed, 1 Jul 2020 14:03:41 +0100 Subject: [PATCH] add magnum opus cql example remove storage.db from git --- 021-conical-domain/magnum_opus/.gitignore | 3 + 021-conical-domain/magnum_opus/Dockerfile | 23 +++++ 021-conical-domain/magnum_opus/MANIFEST.in | 1 + 021-conical-domain/magnum_opus/README.md | 5 ++ 021-conical-domain/magnum_opus/RULES.md | 9 ++ .../magnum_opus/docker-compose.yml | 6 ++ .../magnum_opus/gunicorn_config.py | 5 ++ .../magnum_opus/magnumopus/__init__.py | 0 .../magnum_opus/magnumopus/alembic.py | 51 +++++++++++ .../magnum_opus/magnumopus/index.py | 3 + .../magnum_opus/magnumopus/logger.py | 5 ++ .../magnum_opus/magnumopus/models/__init__.py | 5 ++ .../magnum_opus/magnumopus/models/base.py | 3 + .../magnumopus/models/substance.py | 23 +++++ .../magnum_opus/magnumopus/pantry.py | 12 +++ .../magnumopus/repositories/__init__.py | 0 .../magnumopus/repositories/pantry.py | 14 +++ .../magnumopus/resources/__init__.py | 8 ++ .../resources/alembic_instruction.py | 49 +++++++++++ .../magnumopus/resources/substance.py | 37 ++++++++ .../magnumopus/schemas/__init__.py | 6 ++ .../magnumopus/schemas/substance_schema.py | 13 +++ .../magnumopus/services/__init__.py | 0 .../magnumopus/services/alembic.py | 51 +++++++++++ .../services/alembic_instruction_handler.py | 31 +++++++ .../magnumopus/services/assessor.py | 2 + .../magnum_opus/magnumopus/substance.py | 23 +++++ .../magnum_opus/requirements.txt | 5 ++ 021-conical-domain/magnum_opus/setup.cfg | 19 ++++ 021-conical-domain/magnum_opus/setup.py | 14 +++ .../magnum_opus/tests/test_alembic.py | 81 ++++++++++++++++++ .../magnum_opus/tests/test_pantry.py | 22 +++++ .../magnum_opus/tests/test_substance.py | 49 +++++++++++ 021-conical-domain/magnum_opus/tox.ini | 6 ++ 022-burette/.magnum_opus/.coverage | Bin 0 -> 53248 bytes 022-burette/.magnum_opus/.env.local | 5 ++ 022-burette/.magnum_opus/.gitignore | 4 + 022-burette/.magnum_opus/Dockerfile | 24 ++++++ 022-burette/.magnum_opus/MANIFEST.in | 1 + 022-burette/.magnum_opus/README.md | 46 ++++++++++ 022-burette/.magnum_opus/RULES.md | 9 ++ 022-burette/.magnum_opus/docker-compose.yml | 13 +++ .../.magnum_opus}/docker/storage/storage.db | Bin 8192 -> 8192 bytes 022-burette/.magnum_opus/gunicorn_config.py | 5 ++ 022-burette/.magnum_opus/init_containers.sh | 6 ++ 022-burette/.magnum_opus/init_entrypoint.sh | 3 + .../.magnum_opus/magnumopus/__init__.py | 22 +++++ 022-burette/.magnum_opus/magnumopus/config.py | 15 ++++ 022-burette/.magnum_opus/magnumopus/index.py | 3 + .../.magnum_opus/magnumopus/initialize.py | 9 ++ .../.magnum_opus/magnumopus/injection.py | 11 +++ 022-burette/.magnum_opus/magnumopus/logger.py | 6 ++ .../magnumopus/models/__init__.py | 12 +++ .../.magnum_opus/magnumopus/models/base.py | 3 + .../magnumopus/models/substance.py | 37 ++++++++ .../magnumopus/repositories/__init__.py | 20 +++++ .../repositories/cqlalchemy_pantry.py | 30 +++++++ .../magnumopus/repositories/list_pantry.py | 15 ++++ .../magnumopus/repositories/pantry.py | 21 +++++ .../repositories/sqlalchemy_pantry.py | 28 ++++++ .../magnumopus/resources/__init__.py | 9 ++ .../resources/alembic_instruction.py | 58 +++++++++++++ .../magnumopus/resources/substance.py | 62 ++++++++++++++ .../magnumopus/schemas/__init__.py | 22 +++++ .../magnumopus/schemas/substance_schema.py | 19 ++++ .../magnumopus/services/__init__.py | 9 ++ .../magnumopus/services/alembic.py | 51 +++++++++++ .../services/alembic_instruction_handler.py | 36 ++++++++ .../magnumopus/services/assessor.py | 2 + 022-burette/.magnum_opus/requirements.txt | 10 +++ 022-burette/.magnum_opus/setup.cfg | 37 ++++++++ 022-burette/.magnum_opus/setup.py | 14 +++ 022-burette/.magnum_opus/tests/__init__.py | 4 + .../.magnum_opus/tests/application/.coverage | Bin 0 -> 53248 bytes .../tests/application/__init__.py | 0 .../test_alembic_instruction_resource.py | 47 ++++++++++ .../application/test_philosophers_stone.py | 7 ++ .../application/test_substance_resource.py | 50 +++++++++++ 022-burette/.magnum_opus/tests/conftest.py | 23 +++++ .../.magnum_opus/tests/domain/__init__.py | 0 .../.magnum_opus/tests/domain/test_alembic.py | 81 ++++++++++++++++++ .../test_alembic_instruction_handler.py | 37 ++++++++ .../.magnum_opus/tests/domain/test_pantry.py | 43 ++++++++++ .../tests/domain/test_substance.py | 49 +++++++++++ 022-burette/.magnum_opus/tox.ini | 6 ++ 85 files changed, 1608 insertions(+) create mode 100644 021-conical-domain/magnum_opus/.gitignore create mode 100644 021-conical-domain/magnum_opus/Dockerfile create mode 100644 021-conical-domain/magnum_opus/MANIFEST.in create mode 100644 021-conical-domain/magnum_opus/README.md create mode 100644 021-conical-domain/magnum_opus/RULES.md create mode 100644 021-conical-domain/magnum_opus/docker-compose.yml create mode 100644 021-conical-domain/magnum_opus/gunicorn_config.py create mode 100644 021-conical-domain/magnum_opus/magnumopus/__init__.py create mode 100644 021-conical-domain/magnum_opus/magnumopus/alembic.py create mode 100644 021-conical-domain/magnum_opus/magnumopus/index.py create mode 100644 021-conical-domain/magnum_opus/magnumopus/logger.py create mode 100644 021-conical-domain/magnum_opus/magnumopus/models/__init__.py create mode 100644 021-conical-domain/magnum_opus/magnumopus/models/base.py create mode 100644 021-conical-domain/magnum_opus/magnumopus/models/substance.py create mode 100644 021-conical-domain/magnum_opus/magnumopus/pantry.py create mode 100644 021-conical-domain/magnum_opus/magnumopus/repositories/__init__.py create mode 100644 021-conical-domain/magnum_opus/magnumopus/repositories/pantry.py create mode 100644 021-conical-domain/magnum_opus/magnumopus/resources/__init__.py create mode 100644 021-conical-domain/magnum_opus/magnumopus/resources/alembic_instruction.py create mode 100644 021-conical-domain/magnum_opus/magnumopus/resources/substance.py create mode 100644 021-conical-domain/magnum_opus/magnumopus/schemas/__init__.py create mode 100644 021-conical-domain/magnum_opus/magnumopus/schemas/substance_schema.py create mode 100644 021-conical-domain/magnum_opus/magnumopus/services/__init__.py create mode 100644 021-conical-domain/magnum_opus/magnumopus/services/alembic.py create mode 100644 021-conical-domain/magnum_opus/magnumopus/services/alembic_instruction_handler.py create mode 100644 021-conical-domain/magnum_opus/magnumopus/services/assessor.py create mode 100644 021-conical-domain/magnum_opus/magnumopus/substance.py create mode 100644 021-conical-domain/magnum_opus/requirements.txt create mode 100644 021-conical-domain/magnum_opus/setup.cfg create mode 100644 021-conical-domain/magnum_opus/setup.py create mode 100644 021-conical-domain/magnum_opus/tests/test_alembic.py create mode 100644 021-conical-domain/magnum_opus/tests/test_pantry.py create mode 100644 021-conical-domain/magnum_opus/tests/test_substance.py create mode 100644 021-conical-domain/magnum_opus/tox.ini create mode 100644 022-burette/.magnum_opus/.coverage create mode 100644 022-burette/.magnum_opus/.env.local create mode 100644 022-burette/.magnum_opus/.gitignore create mode 100644 022-burette/.magnum_opus/Dockerfile create mode 100644 022-burette/.magnum_opus/MANIFEST.in create mode 100644 022-burette/.magnum_opus/README.md create mode 100644 022-burette/.magnum_opus/RULES.md create mode 100644 022-burette/.magnum_opus/docker-compose.yml rename {021-conical-continued-again/magnum_opus => 022-burette/.magnum_opus}/docker/storage/storage.db (95%) create mode 100644 022-burette/.magnum_opus/gunicorn_config.py create mode 100755 022-burette/.magnum_opus/init_containers.sh create mode 100755 022-burette/.magnum_opus/init_entrypoint.sh create mode 100644 022-burette/.magnum_opus/magnumopus/__init__.py create mode 100644 022-burette/.magnum_opus/magnumopus/config.py create mode 100644 022-burette/.magnum_opus/magnumopus/index.py create mode 100644 022-burette/.magnum_opus/magnumopus/initialize.py create mode 100644 022-burette/.magnum_opus/magnumopus/injection.py create mode 100644 022-burette/.magnum_opus/magnumopus/logger.py create mode 100644 022-burette/.magnum_opus/magnumopus/models/__init__.py create mode 100644 022-burette/.magnum_opus/magnumopus/models/base.py create mode 100644 022-burette/.magnum_opus/magnumopus/models/substance.py create mode 100644 022-burette/.magnum_opus/magnumopus/repositories/__init__.py create mode 100644 022-burette/.magnum_opus/magnumopus/repositories/cqlalchemy_pantry.py create mode 100644 022-burette/.magnum_opus/magnumopus/repositories/list_pantry.py create mode 100644 022-burette/.magnum_opus/magnumopus/repositories/pantry.py create mode 100644 022-burette/.magnum_opus/magnumopus/repositories/sqlalchemy_pantry.py create mode 100644 022-burette/.magnum_opus/magnumopus/resources/__init__.py create mode 100644 022-burette/.magnum_opus/magnumopus/resources/alembic_instruction.py create mode 100644 022-burette/.magnum_opus/magnumopus/resources/substance.py create mode 100644 022-burette/.magnum_opus/magnumopus/schemas/__init__.py create mode 100644 022-burette/.magnum_opus/magnumopus/schemas/substance_schema.py create mode 100644 022-burette/.magnum_opus/magnumopus/services/__init__.py create mode 100644 022-burette/.magnum_opus/magnumopus/services/alembic.py create mode 100644 022-burette/.magnum_opus/magnumopus/services/alembic_instruction_handler.py create mode 100644 022-burette/.magnum_opus/magnumopus/services/assessor.py create mode 100644 022-burette/.magnum_opus/requirements.txt create mode 100644 022-burette/.magnum_opus/setup.cfg create mode 100644 022-burette/.magnum_opus/setup.py create mode 100644 022-burette/.magnum_opus/tests/__init__.py create mode 100644 022-burette/.magnum_opus/tests/application/.coverage create mode 100644 022-burette/.magnum_opus/tests/application/__init__.py create mode 100644 022-burette/.magnum_opus/tests/application/test_alembic_instruction_resource.py create mode 100644 022-burette/.magnum_opus/tests/application/test_philosophers_stone.py create mode 100644 022-burette/.magnum_opus/tests/application/test_substance_resource.py create mode 100644 022-burette/.magnum_opus/tests/conftest.py create mode 100644 022-burette/.magnum_opus/tests/domain/__init__.py create mode 100644 022-burette/.magnum_opus/tests/domain/test_alembic.py create mode 100644 022-burette/.magnum_opus/tests/domain/test_alembic_instruction_handler.py create mode 100644 022-burette/.magnum_opus/tests/domain/test_pantry.py create mode 100644 022-burette/.magnum_opus/tests/domain/test_substance.py create mode 100644 022-burette/.magnum_opus/tox.ini diff --git a/021-conical-domain/magnum_opus/.gitignore b/021-conical-domain/magnum_opus/.gitignore new file mode 100644 index 0000000..9fb668e --- /dev/null +++ b/021-conical-domain/magnum_opus/.gitignore @@ -0,0 +1,3 @@ +*.egg-info +.tox +Pipfile diff --git a/021-conical-domain/magnum_opus/Dockerfile b/021-conical-domain/magnum_opus/Dockerfile new file mode 100644 index 0000000..e8b07e3 --- /dev/null +++ b/021-conical-domain/magnum_opus/Dockerfile @@ -0,0 +1,23 @@ +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 . + +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 diff --git a/021-conical-domain/magnum_opus/MANIFEST.in b/021-conical-domain/magnum_opus/MANIFEST.in new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/021-conical-domain/magnum_opus/MANIFEST.in @@ -0,0 +1 @@ + diff --git a/021-conical-domain/magnum_opus/README.md b/021-conical-domain/magnum_opus/README.md new file mode 100644 index 0000000..b87be82 --- /dev/null +++ b/021-conical-domain/magnum_opus/README.md @@ -0,0 +1,5 @@ +Magnum Opus +=========== + +This recipe maker +and averaged over repeated attempts, returning an average round trip time. diff --git a/021-conical-domain/magnum_opus/RULES.md b/021-conical-domain/magnum_opus/RULES.md new file mode 100644 index 0000000..4438311 --- /dev/null +++ b/021-conical-domain/magnum_opus/RULES.md @@ -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) diff --git a/021-conical-domain/magnum_opus/docker-compose.yml b/021-conical-domain/magnum_opus/docker-compose.yml new file mode 100644 index 0000000..a234fe9 --- /dev/null +++ b/021-conical-domain/magnum_opus/docker-compose.yml @@ -0,0 +1,6 @@ +version: "3" +services: + web: + build: . + ports: + - 5000:5000 diff --git a/021-conical-domain/magnum_opus/gunicorn_config.py b/021-conical-domain/magnum_opus/gunicorn_config.py new file mode 100644 index 0000000..29db9d9 --- /dev/null +++ b/021-conical-domain/magnum_opus/gunicorn_config.py @@ -0,0 +1,5 @@ +port = '5000' +bind = "0.0.0.0:%s" % port +workers = 1 +timeout = 600 +reload = False diff --git a/021-conical-domain/magnum_opus/magnumopus/__init__.py b/021-conical-domain/magnum_opus/magnumopus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/021-conical-domain/magnum_opus/magnumopus/alembic.py b/021-conical-domain/magnum_opus/magnumopus/alembic.py new file mode 100644 index 0000000..39f02a4 --- /dev/null +++ b/021-conical-domain/magnum_opus/magnumopus/alembic.py @@ -0,0 +1,51 @@ +from .substance import Substance + +class NotEnoughSubstancesToMixException(Exception): + pass + +class UnknownProcessException(Exception): + pass + + +MIXTURES = { + ('Mercury', 'Salt', 'Sulphur'): 'Gloop' +} + + +class Alembic: + _nature_of_unknown_mixture = 'Sludge' + + @staticmethod + def _produce(nature): + return Substance(nature=nature) + + def mix(self, *substances): + if len(substances) < 2: + raise NotEnoughSubstancesToMixException() + + constituents = [substance.nature for substance in substances] + + # This gives us a canonical, ordered way of expressing our + # constituents that we can use as a recipe look-up + ingredient_list = tuple(sorted(constituents)) + + try: + nature = MIXTURES[ingredient_list] + except KeyError: + nature = self._nature_of_unknown_mixture + + return self._produce(nature) + + def process(self, process_name, substance): + if process_name == 'ferment': + result = substance.ferment() + elif process_name == 'cook': + result = substance.cook() + elif process_name == 'wash': + result = substance.wash() + elif process_name == 'pickle': + result = substance.pickle() + else: + raise UnknownProcessException() + + return result diff --git a/021-conical-domain/magnum_opus/magnumopus/index.py b/021-conical-domain/magnum_opus/magnumopus/index.py new file mode 100644 index 0000000..183c186 --- /dev/null +++ b/021-conical-domain/magnum_opus/magnumopus/index.py @@ -0,0 +1,3 @@ +from . import create_app + +app = create_app() diff --git a/021-conical-domain/magnum_opus/magnumopus/logger.py b/021-conical-domain/magnum_opus/magnumopus/logger.py new file mode 100644 index 0000000..9b7f97a --- /dev/null +++ b/021-conical-domain/magnum_opus/magnumopus/logger.py @@ -0,0 +1,5 @@ +import logging + +def init_app(app): + app.logger.addHandler(logging.StreamHandler()) + app.logger.setLevel(logging.INFO) diff --git a/021-conical-domain/magnum_opus/magnumopus/models/__init__.py b/021-conical-domain/magnum_opus/magnumopus/models/__init__.py new file mode 100644 index 0000000..2e94a1e --- /dev/null +++ b/021-conical-domain/magnum_opus/magnumopus/models/__init__.py @@ -0,0 +1,5 @@ +from .base import db + +def init_app(app): + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db' + db.init_app(app) diff --git a/021-conical-domain/magnum_opus/magnumopus/models/base.py b/021-conical-domain/magnum_opus/magnumopus/models/base.py new file mode 100644 index 0000000..f0b13d6 --- /dev/null +++ b/021-conical-domain/magnum_opus/magnumopus/models/base.py @@ -0,0 +1,3 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() diff --git a/021-conical-domain/magnum_opus/magnumopus/models/substance.py b/021-conical-domain/magnum_opus/magnumopus/models/substance.py new file mode 100644 index 0000000..828b4c5 --- /dev/null +++ b/021-conical-domain/magnum_opus/magnumopus/models/substance.py @@ -0,0 +1,23 @@ +class SubstanceMustBeFreshToProcessException(Exception): + pass + +class Substance: + def __init__(self, nature='Unknown'): + self.nature = nature + self.state = [] + + 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') diff --git a/021-conical-domain/magnum_opus/magnumopus/pantry.py b/021-conical-domain/magnum_opus/magnumopus/pantry.py new file mode 100644 index 0000000..9e7a1d6 --- /dev/null +++ b/021-conical-domain/magnum_opus/magnumopus/pantry.py @@ -0,0 +1,12 @@ +class Pantry: + def __init__(self): + self._cupboard = [] + + def add_substance(self, substance): + self._cupboard.append(substance) + + def find_substance_by_nature(self, nature): + return [substance for substance in self._cupboard if substance.nature == nature][0] + + def count_all_substances(self): + return len(self._cupboard) diff --git a/021-conical-domain/magnum_opus/magnumopus/repositories/__init__.py b/021-conical-domain/magnum_opus/magnumopus/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/021-conical-domain/magnum_opus/magnumopus/repositories/pantry.py b/021-conical-domain/magnum_opus/magnumopus/repositories/pantry.py new file mode 100644 index 0000000..90ac0d9 --- /dev/null +++ b/021-conical-domain/magnum_opus/magnumopus/repositories/pantry.py @@ -0,0 +1,14 @@ +class Pantry: + _cupboard = [] + + def __init__(self): + pass + + 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) diff --git a/021-conical-domain/magnum_opus/magnumopus/resources/__init__.py b/021-conical-domain/magnum_opus/magnumopus/resources/__init__.py new file mode 100644 index 0000000..85683b9 --- /dev/null +++ b/021-conical-domain/magnum_opus/magnumopus/resources/__init__.py @@ -0,0 +1,8 @@ +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) diff --git a/021-conical-domain/magnum_opus/magnumopus/resources/alembic_instruction.py b/021-conical-domain/magnum_opus/magnumopus/resources/alembic_instruction.py new file mode 100644 index 0000000..1843acf --- /dev/null +++ b/021-conical-domain/magnum_opus/magnumopus/resources/alembic_instruction.py @@ -0,0 +1,49 @@ +from flask_restful import Resource, reqparse +from ..repositories.pantry import Pantry +from ..models.substance import Substance +from ..schemas.substance_schema import SubstanceSchema +from ..services.alembic_instruction_handler import AlembicInstructionHandler, AlembicInstruction + +parser = reqparse.RequestParser() +parser.add_argument('instruction_type') +parser.add_argument('action') +parser.add_argument('natures') + +substance_schema = SubstanceSchema() + +class AlembicInstructionResource(Resource): + 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 + """ + + args = parser.parse_args() + instruction_type = args['instruction_type'] + + pantry = 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) + + return substance_schema.dump(result) + + +def init_app(app, api): + api.add_resource(AlembicInstructionResource, '/alembic_instruction') diff --git a/021-conical-domain/magnum_opus/magnumopus/resources/substance.py b/021-conical-domain/magnum_opus/magnumopus/resources/substance.py new file mode 100644 index 0000000..770dcac --- /dev/null +++ b/021-conical-domain/magnum_opus/magnumopus/resources/substance.py @@ -0,0 +1,37 @@ +from flask_restful import Resource, reqparse +from ..repositories.pantry import Pantry +from ..models.substance import Substance +from ..schemas.substance_schema import SubstanceSchema + +parser = reqparse.RequestParser() +parser.add_argument('nature') + +substance_schema = SubstanceSchema() +substances_schema = SubstanceSchema(many=True) + + +class SubstanceResource(Resource): + def get(self): + args = parser.parse_args() + nature = args['nature'] + pantry = Pantry() + + substances = pantry.find_substances_by_nature(nature) + + return substances_schema.dump(substances) + + def post(self): + args = parser.parse_args() + nature = args['nature'] + + pantry = Pantry() + + substance = Substance(nature=nature) + + pantry.add_substance(substance) + + return substance_schema.dump(substance) + + +def init_app(app, api): + api.add_resource(SubstanceResource, '/substance') diff --git a/021-conical-domain/magnum_opus/magnumopus/schemas/__init__.py b/021-conical-domain/magnum_opus/magnumopus/schemas/__init__.py new file mode 100644 index 0000000..34395e3 --- /dev/null +++ b/021-conical-domain/magnum_opus/magnumopus/schemas/__init__.py @@ -0,0 +1,6 @@ +from flask_marshmallow import Marshmallow + +ma = Marshmallow() + +def init_app(app): + ma.init_app(app) diff --git a/021-conical-domain/magnum_opus/magnumopus/schemas/substance_schema.py b/021-conical-domain/magnum_opus/magnumopus/schemas/substance_schema.py new file mode 100644 index 0000000..430e96c --- /dev/null +++ b/021-conical-domain/magnum_opus/magnumopus/schemas/substance_schema.py @@ -0,0 +1,13 @@ +from marshmallow import fields + +from . import ma + +from ..services.assessor import assess_whether_substance_is_philosophers_stone + +class SubstanceSchema(ma.Schema): + is_philosophers_stone = fields.Function( + assess_whether_substance_is_philosophers_stone + ) + + class Meta: + fields = ("nature", "is_philosophers_stone") diff --git a/021-conical-domain/magnum_opus/magnumopus/services/__init__.py b/021-conical-domain/magnum_opus/magnumopus/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/021-conical-domain/magnum_opus/magnumopus/services/alembic.py b/021-conical-domain/magnum_opus/magnumopus/services/alembic.py new file mode 100644 index 0000000..5fc05b2 --- /dev/null +++ b/021-conical-domain/magnum_opus/magnumopus/services/alembic.py @@ -0,0 +1,51 @@ +from ..models.substance import Substance + +class NotEnoughSubstancesToMixException(Exception): + pass + +class UnknownProcessException(Exception): + pass + + +MIXTURES = { + ('Mercury', 'Salt', 'Sulphur'): 'Gloop' +} + + +class Alembic: + _nature_of_unknown_mixture = 'Sludge' + + @staticmethod + def _produce(nature): + return Substance(nature=nature) + + def mix(self, *substances): + if len(substances) < 2: + raise NotEnoughSubstancesToMixException() + + constituents = [substance.nature for substance in substances] + + # This gives us a canonical, ordered way of expressing our + # constituents that we can use as a recipe look-up + ingredient_list = tuple(sorted(constituents)) + + try: + nature = MIXTURES[ingredient_list] + except KeyError: + nature = self._nature_of_unknown_mixture + + return self._produce(nature) + + def process(self, process_name, substance): + if process_name == 'ferment': + result = substance.ferment() + elif process_name == 'cook': + result = substance.cook() + elif process_name == 'wash': + result = substance.wash() + elif process_name == 'pickle': + result = substance.pickle() + else: + raise UnknownProcessException() + + return result diff --git a/021-conical-domain/magnum_opus/magnumopus/services/alembic_instruction_handler.py b/021-conical-domain/magnum_opus/magnumopus/services/alembic_instruction_handler.py new file mode 100644 index 0000000..556bbe2 --- /dev/null +++ b/021-conical-domain/magnum_opus/magnumopus/services/alembic_instruction_handler.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass + +from .alembic import Alembic +from ..repositories.pantry import Pantry + +@dataclass +class AlembicInstruction: + instruction_type: str + natures: list + action: str = '' + +class AlembicInstructionHandler: + def handle(self, instruction: AlembicInstruction, pantry: Pantry): + natures = instruction.natures + action = instruction.action + instruction_type = instruction.instruction_type + + # Clearly need some validation here! + substances = [pantry.find_substances_by_nature(nature)[0] for nature in natures] + + alembic = Alembic() + + if instruction_type == 'mix': + result = alembic.mix(*substances) + elif instruction_type == 'process': + result = alembic.process(action, substances[0]) + else: + pass + # a sensible error + + return result diff --git a/021-conical-domain/magnum_opus/magnumopus/services/assessor.py b/021-conical-domain/magnum_opus/magnumopus/services/assessor.py new file mode 100644 index 0000000..94d564d --- /dev/null +++ b/021-conical-domain/magnum_opus/magnumopus/services/assessor.py @@ -0,0 +1,2 @@ +def assess_whether_substance_is_philosophers_stone(substance): + return substance.nature == 'Gloop' and substance.state == ['cooked', 'washed', 'pickled', 'fermented'] diff --git a/021-conical-domain/magnum_opus/magnumopus/substance.py b/021-conical-domain/magnum_opus/magnumopus/substance.py new file mode 100644 index 0000000..828b4c5 --- /dev/null +++ b/021-conical-domain/magnum_opus/magnumopus/substance.py @@ -0,0 +1,23 @@ +class SubstanceMustBeFreshToProcessException(Exception): + pass + +class Substance: + def __init__(self, nature='Unknown'): + self.nature = nature + self.state = [] + + 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') diff --git a/021-conical-domain/magnum_opus/requirements.txt b/021-conical-domain/magnum_opus/requirements.txt new file mode 100644 index 0000000..99f65af --- /dev/null +++ b/021-conical-domain/magnum_opus/requirements.txt @@ -0,0 +1,5 @@ +numpy +pylint +pyyaml +Click +appdirs \ No newline at end of file diff --git a/021-conical-domain/magnum_opus/setup.cfg b/021-conical-domain/magnum_opus/setup.cfg new file mode 100644 index 0000000..144266f --- /dev/null +++ b/021-conical-domain/magnum_opus/setup.cfg @@ -0,0 +1,19 @@ +[metadata] +name = magnumopus +version = 0.0.1 +author = Phil Weir +author_email = phil.weir@flaxandteal.co.uk +license = GPL +description = Service for cooking up a philosopher's stone +long-description = file:README.md + +[options] +include_package_data = True +packages = find: +python_requires = >=3.6 +install_requires = + appdirs + +[options.packages.find] +exclude = + tests diff --git a/021-conical-domain/magnum_opus/setup.py b/021-conical-domain/magnum_opus/setup.py new file mode 100644 index 0000000..1767837 --- /dev/null +++ b/021-conical-domain/magnum_opus/setup.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" +Magnum Opus + +This tool performs alchemical reactions to create +the Philosopher's Stone. + +@author: Phil Weir +""" + +from setuptools import setup + +if __name__ == '__main__': + setup() diff --git a/021-conical-domain/magnum_opus/tests/test_alembic.py b/021-conical-domain/magnum_opus/tests/test_alembic.py new file mode 100644 index 0000000..01fd341 --- /dev/null +++ b/021-conical-domain/magnum_opus/tests/test_alembic.py @@ -0,0 +1,81 @@ +import pytest + +from magnumopus.alembic import Alembic, NotEnoughSubstancesToMixException, UnknownProcessException +from magnumopus.substance import Substance + +def test_can_set_up_my_alembic(): + Alembic() + +def test_can_mix_multiple_substances_in_my_alembic(): + alembic = Alembic() + substance = [Substance() for _ in range(3)] + alembic.mix(*substance) + + substance = [Substance() for _ in range(6)] + alembic.mix(*substance) + +def test_cannot_mix_one_substance_in_my_alembic(): + alembic = Alembic() + substance = Substance() + + with pytest.raises(NotEnoughSubstancesToMixException): + alembic.mix(substance) + +def test_mixing_sulphur_salt_and_mercury_gives_gloop(): + alembic = Alembic() + + sulphur = Substance(nature='Sulphur') + salt = Substance(nature='Salt') + mercury = Substance(nature='Mercury') + + result = alembic.mix(sulphur, salt, mercury) + + assert result.nature == 'Gloop' + + result = alembic.mix(mercury, sulphur, salt) + + assert result.nature == 'Gloop' + +def test_mixing_other_recipes_gives_sludge(): + alembic = Alembic() + + sulphur = Substance(nature='Sulphur') + salt = Substance(nature='Salt') + mercury = Substance(nature='Mercury') + gloop = Substance(nature='Gloop') + + result = alembic.mix(sulphur, salt, mercury, sulphur) + + assert result.nature == 'Sludge' + + result = alembic.mix(salt, mercury) + + assert result.nature == 'Sludge' + + result = alembic.mix(gloop, salt, mercury) + + assert result.nature == 'Sludge' + +def test_can_process_substance(): + alembic = Alembic() + + substance = Substance() + result = alembic.process('cook', substance) + + substance = Substance() + cooked_substance = substance.cook() + + assert result.state == cooked_substance.state + + result = alembic.process('ferment', substance) + cooked_fermented_substance = cooked_substance.ferment() + + assert result.state == cooked_fermented_substance.state + +def test_cannot_perform_unknown_process(): + alembic = Alembic() + + substance = Substance() + + with pytest.raises(UnknownProcessException): + alembic.process('boil', substance) diff --git a/021-conical-domain/magnum_opus/tests/test_pantry.py b/021-conical-domain/magnum_opus/tests/test_pantry.py new file mode 100644 index 0000000..84dbd7e --- /dev/null +++ b/021-conical-domain/magnum_opus/tests/test_pantry.py @@ -0,0 +1,22 @@ +from magnumopus.pantry import Pantry +from magnumopus.substance import Substance + +def test_can_add_to_pantry(): + pantry = Pantry() + + substance = Substance() + + pantry.add_substance(substance) + + assert pantry.count_all_substances() == 1 + +def test_can_retrieve_substance_from_pantry_by_nature(): + pantry = Pantry() + + substance = Substance(nature='Mercury') + + pantry.add_substance(substance) + + mercury = pantry.find_substance_by_nature('Mercury') + + assert mercury.nature == 'Mercury' diff --git a/021-conical-domain/magnum_opus/tests/test_substance.py b/021-conical-domain/magnum_opus/tests/test_substance.py new file mode 100644 index 0000000..342cf50 --- /dev/null +++ b/021-conical-domain/magnum_opus/tests/test_substance.py @@ -0,0 +1,49 @@ +from magnumopus.substance import Substance + +def test_can_cook_substance(): + substance = Substance() + + result = substance.cook() + + assert substance.state == ['cooked'] + +def test_can_wash_substance(): + substance = Substance() + + result = substance.wash() + + assert result.state == ['washed'] + +def test_can_pickle_substance(): + substance = Substance() + + result = substance.pickle() + + assert result.state == ['pickled'] + +def test_can_ferment_substance(): + substance = Substance() + + result = substance.ferment() + + assert substance.state == ['fermented'] + +def test_can_cook_and_ferment_substance(): + substance = Substance() + + result = substance.cook() + result = result.ferment() + + assert substance.state == ['cooked', 'fermented'] + +def test_the_order_of_processes_applied_to_a_substance_matters(): + substance1 = Substance() + result1 = substance1.cook() + result1 = result1.ferment() + + substance2 = Substance() + result2 = substance2.ferment() + result2 = result2.cook() + + assert result1.state != result2.state + assert result1.state == result2.state[::-1] diff --git a/021-conical-domain/magnum_opus/tox.ini b/021-conical-domain/magnum_opus/tox.ini new file mode 100644 index 0000000..d815853 --- /dev/null +++ b/021-conical-domain/magnum_opus/tox.ini @@ -0,0 +1,6 @@ +[tox] +envlist = py38 + +[testenv] +deps = pytest +commands = pytest diff --git a/022-burette/.magnum_opus/.coverage b/022-burette/.magnum_opus/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..acffb036006a2fd0428756bfafc1028b3539f7f7 GIT binary patch literal 53248 zcmeI4TWlOx8GvVYW@mQxcGg~Bl%~NWB;;7Jy|Kfk4M-sbKeP#I8`1`mhRy8y*q(TI z#+jL2+f4%+w^AOs2(<`Q-~l9{4@k5RG(r&)541?|fFJ<`Z9^qMln01Zgt({(@&9x2 zjuShI1miZzKiWO#%*;9GKmYgt=ggeBee|ItrpNU;+i4q~zE7wKq9}Y&*9Ae);9r7& zyr__n;}wX-M0%^FCQKgqO^H1s6qPRu>=UJrv5De0OSQtw#jf^r;aT;x)`A_h5CI}U z1c(3;2nhHO71iM#JH)kfp3!J=&oP?Z3B%H%qcaDO&FIGte(1=I9&Xcj=XF>nCiH{4 zV=wDV+|lRE7S~N{)@&M{Y0c~20*`ZdTt16OJlF=uJj^&5l*d%voP|;zpNA@z9J6gW ztNJ2e9S<@@(mcN6g&Uv}Zq8enL*E@0AJZK^#~p4pxf`}(w>dkOAA90cd3Cs270>fP zC&OvNudqQI*w9nuzB#C&Ci}Ro1g3WpCOxo7l3a?*?F{;qkL$gX z&>JngvC-E8k8wG45Eu*MY;zr}iCx7-9h!ERJH|YpjP{1Z!dvY~;5t*9 zGCVV~mFuj#Kz(XGnfG@n>hR8;;+ht?QDm(sQa89zY{^ZA(Rf#z8*aym2a+2Nl5vUN zC>i&WXb-3}p(?`%cW$Lll&?M&C33zhtHXnXq967M9GCE)-Jk?+>2-l?4g@#f{3XL% z(64VhZia@BW|iUh4{p_PY(jksL*5h%#M05Pki@=s>RycNOcH?~VpdiH|8bx*vnS7I!O1wA+R? z8y%+b=o{7wLq7INloO5}P*5L*zUV^Fyr#~$(r7OZRB`>2OMn7vZ5y*;FGpJ>YFu96 z*HsrDgg$I|+%wx;$1Fi(0)JXIT)pXV*a?Twu~F$p)<+}Nr!d4>at=>q{1H(d-m^#a zJ5e7DhnKnw$MUujH#=I*Z19b1cJ|!l0T^fDBq!+}$S#2cS3?OnriB)4gcRaj(J4-T z3@7*^dsBcPEku9_5CI}U1c(3;AOb{y2oM1xKm>@u?MFZoGop&~f0^wN*im+fjY5JJ zB0vO)01+SpM1Tko0U|&IhyW2F0(TODqAcxV@uLxY?~Yn}6)9@>0q@C61#`=3Er~Eylyld@n{6xfw(b|ID=C!2-v*j&w)2SV|oyFQQ$1p8; zvaj~ws<&WUhwP5yLZ)D4q8YC3ot~a(z(Y5l$7^k49-c|6+e;m{Hp#7Stz|Z%?E5C~ zslnrZd}7IHF2ci0E^grCnlt-9pYugCbtGkP6aHF}g;P zDM-7t`0@na{~yY}BCw~~Ncp+)y`>jR4-{W29xhxiJd(eZKcW3zvvP0b&Z_@ZKdX)? z-&O91%(M^zB0vO)z}uNXdq@`i`v3jvbMNYL4X$8&rqMDV<8X<2VGzVUoUS-{ZyF?- z10d;mnv&qf(>ZeSUUtOzSicUO-00`qNgdWzWNx4M3DH^vS|nHcibO?!tWx z_zFVJXmNNayID6a*K<0twq6CaZKv4+mpffEXn^Z-*R@gcvkau3O;u{}a;IB!I}O)^ z&o=Pr{Z0_>mO=R9b_lNnY|{(w{d7wp^h}yUdzN0;8I=euH2G?M$*??U6`d@fwv#zL(S(-idOhJ?Eq!|@gs&^Tk^{M&ZR4Ia zxvGN6t7(ev4JFr)W%UJkr@a*ntF8hJQtjYvdzQBngA0x-*|ddTt33$yWDvSNqj$p6 zmIP9bGz}f`Z5S?|)HoTCXKsVMgpP|M$UKpz%!Dw!|38#{RbXFGUQiBaUsZMWb4)Kk zUwW{3q;S0Oi~QyMqw2r3KWKLDujT!vmx@2iJ^pq+>7}+20U|&Ih=3qivR@a|JdSLb z{|}k6|8UCU!ukK;Y1u!Xs-*S#|3F*zPo^p=IsdPwuGsqgzuy<-wQQ=I^=5^e&;R>6 zvfoa%1-;YO-u%Dvxa^-zSL)XDf9A>l;dCt$tYt>Xm_lA-i&i~ca zJNPE^e01+SpM1Tko0U|&I zhyW2F0z`la+=c`ssifik{{i-Hf&GJBWq)M9Wj|vtvhT6)uy3%hu+OveY=v2DmOaWo z!e-b9AR{eAfCvx)B0vO)01+SpM1Tko0U|&I?hpdXfGld|fs1Mt;!5>mPU*)D{g0LU zAilfrlj=K>grZb%b0w2w5cjjwuPeDSrpmu+zoS&btfl`5cUOdra7r!0YNdEp(h86m zC|ppN3u+#><*gGr4HEs@m!H{@%V8>awI$_MNDZkM&K>w=F0Wu(xp1ZPic-u%`mXHD zpSd#gTpf16T3NndkuZir8Hg*HuBwW%ruBdP(s8{1Kft;IdlSC@{};Fd@EZFy`#Jjw z`yu;2djYNkJj=euo?%b1PqQc4Imk#05g-CYfCvx)B0vO)01+SpM1Tko0U~gd1aQJG z4h#fQwHieI{Xx{%7etjx5HS`+<#G^}N>Y5g-CYfCvzQTap0Y|EKx? zEonMxG!Y;IM1Tko0U|&IhyW2F0z`la5P_Q}fbahgv8V9)|G%?0*q_g!( zq#^h$zch%!00vbI`Z4Ikpn?Izpo~EYgCYh64DuLg800WeF;FnbVjyE6VUWQ9zyJSV D&<%nw literal 0 HcmV?d00001 diff --git a/022-burette/.magnum_opus/.env.local b/022-burette/.magnum_opus/.env.local new file mode 100644 index 0000000..463337d --- /dev/null +++ b/022-burette/.magnum_opus/.env.local @@ -0,0 +1,5 @@ +CASSANDRA_HOSTS=35.234.129.40 +CASSANDRA_KEYSPACE=pythoncourse +CASSANDRA_PASSWORD=Le2V2gZ0nk +CASSANDRA_PORT=30531 +CQLENG_ALLOW_SCHEMA_MANAGEMENT=1 diff --git a/022-burette/.magnum_opus/.gitignore b/022-burette/.magnum_opus/.gitignore new file mode 100644 index 0000000..8a3f0fc --- /dev/null +++ b/022-burette/.magnum_opus/.gitignore @@ -0,0 +1,4 @@ +*.egg-info +.tox +Pipfile +.env diff --git a/022-burette/.magnum_opus/Dockerfile b/022-burette/.magnum_opus/Dockerfile new file mode 100644 index 0000000..0ff1387 --- /dev/null +++ b/022-burette/.magnum_opus/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3 + +RUN addgroup user && adduser --ingroup user 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 diff --git a/022-burette/.magnum_opus/MANIFEST.in b/022-burette/.magnum_opus/MANIFEST.in new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/022-burette/.magnum_opus/MANIFEST.in @@ -0,0 +1 @@ + diff --git a/022-burette/.magnum_opus/README.md b/022-burette/.magnum_opus/README.md new file mode 100644 index 0000000..94ce180 --- /dev/null +++ b/022-burette/.magnum_opus/README.md @@ -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 ============================== diff --git a/022-burette/.magnum_opus/RULES.md b/022-burette/.magnum_opus/RULES.md new file mode 100644 index 0000000..4438311 --- /dev/null +++ b/022-burette/.magnum_opus/RULES.md @@ -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) diff --git a/022-burette/.magnum_opus/docker-compose.yml b/022-burette/.magnum_opus/docker-compose.yml new file mode 100644 index 0000000..0a35c30 --- /dev/null +++ b/022-burette/.magnum_opus/docker-compose.yml @@ -0,0 +1,13 @@ +version: "3" +services: + web_cql: + build: . + env_file: + - .env.local + environment: + DATABASE_URI: 'sqlite:////docker/storage/storage.db' + volumes: + - ./docker:/docker + - ./magnumopus:/home/user/magnumopus + ports: + - 5000:5000 diff --git a/021-conical-continued-again/magnum_opus/docker/storage/storage.db b/022-burette/.magnum_opus/docker/storage/storage.db similarity index 95% rename from 021-conical-continued-again/magnum_opus/docker/storage/storage.db rename to 022-burette/.magnum_opus/docker/storage/storage.db index 1ca583a44b1c65b9c5aee6b06168dcfc0b816ca9..41e4173e74b7ef47d0eaff4eba95ac3eb2ab418f 100644 GIT binary patch delta 130 zcmZp0XmFSy&B!rP#+i|0W5N=CA$I=N4E&$@pYh+~Kg)lZe;fbm&4vPU{1Y3>Lpj-5 z7{qzqbMo^GxY<}3q8adIND|zPNSXl~%OVH> delta 234 zcmZp0XmFSy&B#Ad#+i|SW5N=C1wMX%2L8|d&-ichpW)xnzlnb-{|x?a{yP3b{uKTw ze*evi0^0oboO~<{;=JxT`S}GLyetf&yupb%CEPqL4AQ*8r8xx|rA6G_AdYWpQF3Wf sB{vr`hZDpJ&CAZqFVExVK<2O`bJ&nMtjHV|BV-;kGKUG7!-&KI0E=`x0ssI2 diff --git a/022-burette/.magnum_opus/gunicorn_config.py b/022-burette/.magnum_opus/gunicorn_config.py new file mode 100644 index 0000000..29db9d9 --- /dev/null +++ b/022-burette/.magnum_opus/gunicorn_config.py @@ -0,0 +1,5 @@ +port = '5000' +bind = "0.0.0.0:%s" % port +workers = 1 +timeout = 600 +reload = False diff --git a/022-burette/.magnum_opus/init_containers.sh b/022-burette/.magnum_opus/init_containers.sh new file mode 100755 index 0000000..0a64c94 --- /dev/null +++ b/022-burette/.magnum_opus/init_containers.sh @@ -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 diff --git a/022-burette/.magnum_opus/init_entrypoint.sh b/022-burette/.magnum_opus/init_entrypoint.sh new file mode 100755 index 0000000..8366386 --- /dev/null +++ b/022-burette/.magnum_opus/init_entrypoint.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +chown -R user:user /docker/storage diff --git a/022-burette/.magnum_opus/magnumopus/__init__.py b/022-burette/.magnum_opus/magnumopus/__init__.py new file mode 100644 index 0000000..e410774 --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/__init__.py @@ -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 diff --git a/022-burette/.magnum_opus/magnumopus/config.py b/022-burette/.magnum_opus/magnumopus/config.py new file mode 100644 index 0000000..6e584a9 --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/config.py @@ -0,0 +1,15 @@ +import os +from cassandra.auth import PlainTextAuthProvider + +class Config: + CASSANDRA_HOSTS = os.environ.get('CASSANDRA_HOSTS', 'cassandra').split() + CASSANDRA_KEYSPACE = os.environ.get('CASSANDRA_KEYSPACE', 'pythoncourse') + CASSANDRA_SETUP_KWARGS = { + 'protocol_version': 3, + 'port': os.environ.get('CASSANDRA_PORT', 9042), + 'auth_provider': PlainTextAuthProvider( + username='cassandra', + password=os.environ.get('CASSANDRA_PASSWORD') + ) + } + PANTRY_STORE = os.environ.get('MAGNUMOPUS_PANTRY_STORE', 'cqlalchemy') diff --git a/022-burette/.magnum_opus/magnumopus/index.py b/022-burette/.magnum_opus/magnumopus/index.py new file mode 100644 index 0000000..183c186 --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/index.py @@ -0,0 +1,3 @@ +from . import create_app + +app = create_app() diff --git a/022-burette/.magnum_opus/magnumopus/initialize.py b/022-burette/.magnum_opus/magnumopus/initialize.py new file mode 100644 index 0000000..3616c54 --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/initialize.py @@ -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.sync_db() diff --git a/022-burette/.magnum_opus/magnumopus/injection.py b/022-burette/.magnum_opus/magnumopus/injection.py new file mode 100644 index 0000000..8d74458 --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/injection.py @@ -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 diff --git a/022-burette/.magnum_opus/magnumopus/logger.py b/022-burette/.magnum_opus/magnumopus/logger.py new file mode 100644 index 0000000..b5c4b4f --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/logger.py @@ -0,0 +1,6 @@ +import logging + +def init_app(app): + app.logger.addHandler(logging.StreamHandler()) + app.logger.setLevel(logging.INFO) + return [] diff --git a/022-burette/.magnum_opus/magnumopus/models/__init__.py b/022-burette/.magnum_opus/magnumopus/models/__init__.py new file mode 100644 index 0000000..0894cc8 --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/models/__init__.py @@ -0,0 +1,12 @@ +from .base import db +from flask_cqlalchemy import CQLAlchemy + + +def init_app(app): + db.init_app(app) + return [configure_injector] + +def configure_injector(binding): + binding.bind( + CQLAlchemy, to=db + ) diff --git a/022-burette/.magnum_opus/magnumopus/models/base.py b/022-burette/.magnum_opus/magnumopus/models/base.py new file mode 100644 index 0000000..762d085 --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/models/base.py @@ -0,0 +1,3 @@ +from flask_cqlalchemy import CQLAlchemy + +db = CQLAlchemy() diff --git a/022-burette/.magnum_opus/magnumopus/models/substance.py b/022-burette/.magnum_opus/magnumopus/models/substance.py new file mode 100644 index 0000000..35349a9 --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/models/substance.py @@ -0,0 +1,37 @@ +from . import db +import uuid +from sqlalchemy_utils import ScalarListType + +class SubstanceMustBeFreshToProcessException(Exception): + pass + + +class Substance(db.Model): + id = db.columns.UUID(primary_key=True, default=uuid.uuid4) + nature = db.columns.Text() + state = db.columns.List(db.columns.Text()) + + def __init__(self, nature='Unknown'): + super(Substance, self).__init__(nature=nature, state=[]) + + def _process(self, process_name): + # Example of leakage of persistence behaviour into + # domain, due to db.Model -- we must copy the state list to + # ensure it is seen to change... + state = self.state[:] + state.append(process_name) + self.state = state + + 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') diff --git a/022-burette/.magnum_opus/magnumopus/repositories/__init__.py b/022-burette/.magnum_opus/magnumopus/repositories/__init__.py new file mode 100644 index 0000000..aaab290 --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/repositories/__init__.py @@ -0,0 +1,20 @@ +from .pantry import Pantry +from .list_pantry import ListPantry +from .sqlalchemy_pantry import SQLAlchemyPantry +from .cqlalchemy_pantry import CQLAlchemyPantry + +PANTRY_STORES = { + 'sqlalchemy': SQLAlchemyPantry, + 'cqlalchemy': CQLAlchemyPantry, + '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 + ) diff --git a/022-burette/.magnum_opus/magnumopus/repositories/cqlalchemy_pantry.py b/022-burette/.magnum_opus/magnumopus/repositories/cqlalchemy_pantry.py new file mode 100644 index 0000000..1b0cac1 --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/repositories/cqlalchemy_pantry.py @@ -0,0 +1,30 @@ +from flask_cqlalchemy import CQLAlchemy +from injector import inject + +from ..models.substance import Substance +from ..models import db + +class CQLAlchemyPantry: + @inject + def __init__(self, db: CQLAlchemy): + self._db = db + self._add_buffer = [] + + # 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._add_buffer.append(substance) + return substance + + def find_substances_by_nature(self, nature): + substances = Substance.objects().filter(nature=nature).all() + return substances + + def count_all_substances(self): + return Substance.objects().count() + + def commit(self): + for substance in self._add_buffer: + substance.save() diff --git a/022-burette/.magnum_opus/magnumopus/repositories/list_pantry.py b/022-burette/.magnum_opus/magnumopus/repositories/list_pantry.py new file mode 100644 index 0000000..e14711a --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/repositories/list_pantry.py @@ -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 diff --git a/022-burette/.magnum_opus/magnumopus/repositories/pantry.py b/022-burette/.magnum_opus/magnumopus/repositories/pantry.py new file mode 100644 index 0000000..a2cb730 --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/repositories/pantry.py @@ -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 diff --git a/022-burette/.magnum_opus/magnumopus/repositories/sqlalchemy_pantry.py b/022-burette/.magnum_opus/magnumopus/repositories/sqlalchemy_pantry.py new file mode 100644 index 0000000..996fe76 --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/repositories/sqlalchemy_pantry.py @@ -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() diff --git a/022-burette/.magnum_opus/magnumopus/resources/__init__.py b/022-burette/.magnum_opus/magnumopus/resources/__init__.py new file mode 100644 index 0000000..11345e8 --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/resources/__init__.py @@ -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 [] diff --git a/022-burette/.magnum_opus/magnumopus/resources/alembic_instruction.py b/022-burette/.magnum_opus/magnumopus/resources/alembic_instruction.py new file mode 100644 index 0000000..2d39fc4 --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/resources/alembic_instruction.py @@ -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), 201 + + +def init_app(app, api): + api.add_resource(AlembicInstructionResource, '/alembic_instruction') diff --git a/022-burette/.magnum_opus/magnumopus/resources/substance.py b/022-burette/.magnum_opus/magnumopus/resources/substance.py new file mode 100644 index 0000000..5e4aafe --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/resources/substance.py @@ -0,0 +1,62 @@ +from flask_restful import Resource, reqparse +from injector import inject +from typing import Dict +from ..repositories.pantry import Pantry +from ..models import db +from ..models.substance import Substance +from ..schemas.substance_schema import SubstanceSchema + +# parser = reqparse.RequestParser() +# parser.add_argument('nature') + +# substance_schema = SubstanceSchema() +# substances_schema = SubstanceSchema(many=True) + +# pantry_cls = SQLAlchemyPantry + + +class SubstanceResource(Resource): + @inject + def __init__(self, pantry: Pantry, substance_schemas: Dict[str, SubstanceSchema], parser_fcty=reqparse.RequestParser): + super(SubstanceResource, self).__init__() + + self._pantry = pantry + self._substance_schema = substance_schemas['one'] + self._substances_schema = substance_schemas['many'] + self._parser_fcty = parser_fcty + + def get(self): + parser = self._parser_fcty() + + parser.add_argument('nature') + + args = parser.parse_args() + + nature = args['nature'] + + substances = self._pantry.find_substances_by_nature(nature) + + return self._substances_schema.dump(substances) + + def post(self): + parser = self._parser_fcty() + + parser.add_argument('nature') + + args = parser.parse_args() + + nature = args['nature'] + + pantry = self._pantry + + substance = Substance(nature=nature) + + pantry.add_substance(substance) + + pantry.commit() + + return self._substance_schema.dump(substance), 201 + + +def init_app(app, api): + api.add_resource(SubstanceResource, '/substance') diff --git a/022-burette/.magnum_opus/magnumopus/schemas/__init__.py b/022-burette/.magnum_opus/magnumopus/schemas/__init__.py new file mode 100644 index 0000000..7b27dd7 --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/schemas/__init__.py @@ -0,0 +1,22 @@ +from typing import Dict +from flask_marshmallow import Marshmallow + +ma = Marshmallow() + +def init_app(app): + ma.init_app(app) + return [configure_injector] + +def configure_injector(binder): + from .substance_schema import SubstanceSchema + + binder.bind( + SubstanceSchema, to=SubstanceSchema() + ) + + binder.multibind( + Dict[str, SubstanceSchema], to={ + 'one': SubstanceSchema(), + 'many': SubstanceSchema(many=True) + } + ) diff --git a/022-burette/.magnum_opus/magnumopus/schemas/substance_schema.py b/022-burette/.magnum_opus/magnumopus/schemas/substance_schema.py new file mode 100644 index 0000000..6b292e3 --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/schemas/substance_schema.py @@ -0,0 +1,19 @@ +from marshmallow import fields + +from . import ma + +from ..models.substance import Substance +from ..services.assessor import assess_whether_substance_is_philosophers_stone + +class SubstanceSchema(ma.SQLAlchemySchema): + is_philosophers_stone = fields.Function( + assess_whether_substance_is_philosophers_stone + ) + + class Meta: + model = Substance + fields = ('id', 'nature', 'is_philosophers_stone', 'state') + + id = fields.UUID() + nature = fields.String() + state = fields.Function(lambda model: model.state or []) diff --git a/022-burette/.magnum_opus/magnumopus/services/__init__.py b/022-burette/.magnum_opus/magnumopus/services/__init__.py new file mode 100644 index 0000000..561fd25 --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/services/__init__.py @@ -0,0 +1,9 @@ +from .alembic import Alembic + +def init_app(app): + return [configure_injector] + +def configure_injector(binder): + binder.bind( + Alembic, to=Alembic + ) diff --git a/022-burette/.magnum_opus/magnumopus/services/alembic.py b/022-burette/.magnum_opus/magnumopus/services/alembic.py new file mode 100644 index 0000000..5fc05b2 --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/services/alembic.py @@ -0,0 +1,51 @@ +from ..models.substance import Substance + +class NotEnoughSubstancesToMixException(Exception): + pass + +class UnknownProcessException(Exception): + pass + + +MIXTURES = { + ('Mercury', 'Salt', 'Sulphur'): 'Gloop' +} + + +class Alembic: + _nature_of_unknown_mixture = 'Sludge' + + @staticmethod + def _produce(nature): + return Substance(nature=nature) + + def mix(self, *substances): + if len(substances) < 2: + raise NotEnoughSubstancesToMixException() + + constituents = [substance.nature for substance in substances] + + # This gives us a canonical, ordered way of expressing our + # constituents that we can use as a recipe look-up + ingredient_list = tuple(sorted(constituents)) + + try: + nature = MIXTURES[ingredient_list] + except KeyError: + nature = self._nature_of_unknown_mixture + + return self._produce(nature) + + def process(self, process_name, substance): + if process_name == 'ferment': + result = substance.ferment() + elif process_name == 'cook': + result = substance.cook() + elif process_name == 'wash': + result = substance.wash() + elif process_name == 'pickle': + result = substance.pickle() + else: + raise UnknownProcessException() + + return result diff --git a/022-burette/.magnum_opus/magnumopus/services/alembic_instruction_handler.py b/022-burette/.magnum_opus/magnumopus/services/alembic_instruction_handler.py new file mode 100644 index 0000000..54fbb40 --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/services/alembic_instruction_handler.py @@ -0,0 +1,36 @@ +from dataclasses import dataclass +from injector import inject + +from .alembic import Alembic +from ..repositories.pantry import Pantry + +class UnknownAlembicInstructionException(Exception): + pass + +@dataclass +class AlembicInstruction: + instruction_type: str + natures: list + action: str = '' + +class AlembicInstructionHandler: + @inject + def handle(self, instruction: AlembicInstruction, pantry: Pantry): + natures = instruction.natures + action = instruction.action + instruction_type = instruction.instruction_type + + # Clearly need some validation here! + substances = [pantry.find_substances_by_nature(nature)[0] for nature in natures] + + alembic = Alembic() + + if instruction_type == 'mix': + result = alembic.mix(*substances) + elif instruction_type == 'process': + result = alembic.process(action, substances[0]) + else: + raise UnknownAlembicInstructionException(f'Unknown instruction: {action}') + + print(result.state, result.id, 'x') + return result diff --git a/022-burette/.magnum_opus/magnumopus/services/assessor.py b/022-burette/.magnum_opus/magnumopus/services/assessor.py new file mode 100644 index 0000000..94d564d --- /dev/null +++ b/022-burette/.magnum_opus/magnumopus/services/assessor.py @@ -0,0 +1,2 @@ +def assess_whether_substance_is_philosophers_stone(substance): + return substance.nature == 'Gloop' and substance.state == ['cooked', 'washed', 'pickled', 'fermented'] diff --git a/022-burette/.magnum_opus/requirements.txt b/022-burette/.magnum_opus/requirements.txt new file mode 100644 index 0000000..0599a73 --- /dev/null +++ b/022-burette/.magnum_opus/requirements.txt @@ -0,0 +1,10 @@ +appdirs +flask +flask-restful +flask-marshmallow +flask-sqlalchemy +flask-injector +marshmallow-sqlalchemy +sqlalchemy-utils +flask-cqlalchemy +cassandra-driver diff --git a/022-burette/.magnum_opus/setup.cfg b/022-burette/.magnum_opus/setup.cfg new file mode 100644 index 0000000..b3f3250 --- /dev/null +++ b/022-burette/.magnum_opus/setup.cfg @@ -0,0 +1,37 @@ +[metadata] +name = magnumopus +version = 0.0.1 +author = Phil Weir +author_email = phil.weir@flaxandteal.co.uk +license = GPL +description = Service for cooking up a philosopher's stone +long-description = file:README.md + +[options] +include_package_data = True +packages = find: +python_requires = >=3.6 +install_requires = + appdirs + flask-restful + flask-marshmallow + flask-sqlalchemy + flask-injector + sqlalchemy-utils + flask-cqlalchemy + cassandra-driver + marshmallow-sqlalchemy + wheel + flask + +[options.extras_require] +dev = + pytest + pytest-pep8 + pytest-cov + pytest-flask + pytest-flask-sqlalchemy + +[options.packages.find] +exclude = + tests diff --git a/022-burette/.magnum_opus/setup.py b/022-burette/.magnum_opus/setup.py new file mode 100644 index 0000000..1767837 --- /dev/null +++ b/022-burette/.magnum_opus/setup.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" +Magnum Opus + +This tool performs alchemical reactions to create +the Philosopher's Stone. + +@author: Phil Weir +""" + +from setuptools import setup + +if __name__ == '__main__': + setup() diff --git a/022-burette/.magnum_opus/tests/__init__.py b/022-burette/.magnum_opus/tests/__init__.py new file mode 100644 index 0000000..c09f99d --- /dev/null +++ b/022-burette/.magnum_opus/tests/__init__.py @@ -0,0 +1,4 @@ + +def is_dictionary_inside(d1, d2): + return d1.items() <= d2.items() + diff --git a/022-burette/.magnum_opus/tests/application/.coverage b/022-burette/.magnum_opus/tests/application/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..0e5750b04d16c86d92dbe9e012e32b8f135b4fbd GIT binary patch literal 53248 zcmeI4O^h5z6@a^^d#2}iYwh*!%B(i-3mUJyv$MPACzwMp_8x*QL5zcgEYsJ5_bF@g&h4&X=}$R(F>fJnJ;2@oWHgOIq81Cc;NNGKe5uYYEG>|GNH zdJ=D5Yo?}us_NDI-m9v4{rAOZt{Og9Hyzi~ef7LBEr_D<8C4Yop#==1-p%55Df}EtC7M-sWzU)-GM!xOja-y?*htS2xsXpL({e!qRA{7gg8UReRi3 zHw}}khTSpRx^LK9s^8^F?!L!6IOD-SnDa2>N?0D(G@}Ehe7*%$^jyQz-FDknGx^uP$%7O!>2N) z4eS`H^7JND)OOqsw!Y=+cDu{HWmO+)*mj_udw!&?>sU?SQBA`R)wDgsHym5#d%WHE zdFN0Cml0aB0KBv|P}lRQ(DPBrSXPS8sPG{rE%j&4)a;S#wEUTyl zc)&+1h51j-hyr*e-}81%_~WJNecuVUH88%m21)gsCyL7anKRHF$!*jQ+T z+A^KixUYpC<8kO9Fcu`)Hv4uPyNZW8w4DKW^)0>D7I7*#3pSBjS6az(8=4WTcAnFk~F5x#nrUdR8b%6&ag7@D1p6++mhue<3q2cv> zVgA#zM>QOqpsitAc;Q+uZ1P^z39o|0Hu{EfNO-bey9Nn)&UrV|btXP(Acj9RZXWyt+81k`C zqMT^#fP!ir`l1It^MN{(O5>wAP$l)R^Z*6cYU!P*m!mCGHSTu#L)Ar-&}H4{zF~0{ zvxJQa{b^VC)V9mvAWWf4b?H&oMwC==ZV#HK(TcO5UwN_#xBKx%Pj7F-%}XBc z=bqoxGz)}i8r&99Usmf4sCR%XQC(P7{Z<)lDzi5Q_9puWdz<~~!(?L8l?V_4B0vO) z01+SpM1Tko0U|&Ih`{4Zpeikj$$1O8D9ws^+Cr*G3u1hZB3F?XOUda8y#7Cz|AWBp zu`^RYnz~&3b#1#QR$r_xR_<38%lFIcrB_Rria#x0QQlBKSNKKYImk>45g-CYfCzk$ z3EY{Jh2YNr{p|DT>~YiOJqOM_JFWpIolV2@wVrPK?mnDw9?XL9orwt_%H2B!a{CjL zt7&i+-w#i*2jYw@94HeL4G)m(CO${FKcar;WY#8+o-6d4Uccq}@J<3AKfDMg_fCND z?JV_Agr}(s(?C6+rBs)Da6Jaz=Frql4$qdi!RDUt_QS`VjuP`tqaden;cu> zmnWuw%Ei1=)~2?c4mVSy38-onL8z7`a~!e_-88<+L0ne>anEKe4qo>Gl8gdKx{;+M zcwfS%u?3>cJczoLr6|L`&65Gpkh50IbhhAyJfN5@fhavo#bS?a=^lEAn*({q1mvZ9 z-L?oaZ)PboB@D0s&*k3|*tcqCEmyr)`_0q_Q>R|8K2i8({;k57l`Eyo#n(zdE?!lB zUU{i}zVbx*r4RC-UTPZ=AOb{y2nfR0H@F?#n@ca&l8M z>8T(mA1IR(eOUZIlex*05PTRBj^h85z8q|4s(&IpMg2q|$_M#OrT(}0e|jJX&V*W! z(wt48haDFGPi1fBVJIHO#PC17)|Ue-Q^tqI|J9%?7m<%H;GXdwbbfCvx)B0vO)01+SpM1Tko0U|&Ijv)a_s+I8izr@}a*uU7{;D;6> zKm>>Y5g-CYfCvx)B0vO)01+SpMBp(dP>^J?G$oal9Q;n_=ES@xmr7G*y#6n-cLeq> zy#N25$5=sXIuRfOM1Tko0U|&IhyW2F0z`la5CI|(5hy_HFG^DQlgr6)0wBEpFH934 zB0vO)01+SpM1Tko0U|&IhyW2F0z}|A6Ts{L6#pM*3zKd{fCvx)B0vO)01+SpM1Tko z0U|&I1OcD_pJOlL{r~@De`kMXe_?-Se`LR9zhZB&pRw23PuQ#M74|awA^QRQ9{Vo) z4rHc<2oM1xKm>>Y5g-CYfCvx)B0vO)01@~f2}pDBT7GF3lT(