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 0000000..acffb03 Binary files /dev/null and b/022-burette/.magnum_opus/.coverage differ 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 1ca583a..41e4173 100644 Binary files a/021-conical-continued-again/magnum_opus/docker/storage/storage.db and b/022-burette/.magnum_opus/docker/storage/storage.db differ 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 0000000..0e5750b Binary files /dev/null and b/022-burette/.magnum_opus/tests/application/.coverage differ diff --git a/022-burette/.magnum_opus/tests/application/__init__.py b/022-burette/.magnum_opus/tests/application/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/022-burette/.magnum_opus/tests/application/test_alembic_instruction_resource.py b/022-burette/.magnum_opus/tests/application/test_alembic_instruction_resource.py new file mode 100644 index 0000000..063bb92 --- /dev/null +++ b/022-burette/.magnum_opus/tests/application/test_alembic_instruction_resource.py @@ -0,0 +1,47 @@ +import pytest +from magnumopus import create_app +from .. import is_dictionary_inside +from .test_substance_resource import create_substance_request + +def create_alembic_instruction_request(client, instruction_type, natures, action=None): + data = { + 'instruction_type': instruction_type, + 'natures': ','.join(natures) + } + + if action is not None: + data['action'] = action + + return client.post('/alembic_instruction', data=data) + +def test_create_alembic_mix_instruction(client): + create_substance_request(client, ['Mercury']) + create_substance_request(client, ['Salt']) + create_substance_request(client, ['Sulphur']) + + rv = create_alembic_instruction_request(client, 'mix', ['Mercury', 'Salt', 'Sulphur']) + + assert rv.status_code == 201 + + content = rv.get_json() + + assert is_dictionary_inside({ + 'nature': 'Gloop', + 'state': [], + 'is_philosophers_stone': False + }, content) + +def test_create_alembic_process_instruction(client): + create_substance_request(client, ['Mercury']) + + rv = create_alembic_instruction_request(client, 'process', ['Mercury'], 'cook') + + assert rv.status_code == 201 + + content = rv.get_json() + + assert is_dictionary_inside({ + 'nature': 'Mercury', + 'state': ['cooked'], + 'is_philosophers_stone': False + }, content) diff --git a/022-burette/.magnum_opus/tests/application/test_philosophers_stone.py b/022-burette/.magnum_opus/tests/application/test_philosophers_stone.py new file mode 100644 index 0000000..ca25fb7 --- /dev/null +++ b/022-burette/.magnum_opus/tests/application/test_philosophers_stone.py @@ -0,0 +1,7 @@ +import pytest +from magnumopus import create_app +from .. import is_dictionary_inside + + +def test_create_philosophers_stone(client): + pass diff --git a/022-burette/.magnum_opus/tests/application/test_substance_resource.py b/022-burette/.magnum_opus/tests/application/test_substance_resource.py new file mode 100644 index 0000000..ed85af9 --- /dev/null +++ b/022-burette/.magnum_opus/tests/application/test_substance_resource.py @@ -0,0 +1,50 @@ +import pytest +from magnumopus import create_app +from .. import is_dictionary_inside + +def create_substance_request(client, nature): + return client.post('/substance', data={ + 'nature': nature + }) + +def index_substances_request(client, nature): + return client.get(f'/substance?nature={nature}') + +def test_create_substance(client): + rv = create_substance_request(client, 'Sulphur') + + assert rv.status_code == 201 + + content = rv.get_json() + + assert type(content['id']) is int + + assert is_dictionary_inside({ + 'nature': 'Sulphur', + 'state': [], + 'is_philosophers_stone': False + }, content) + + +def test_get_substances(client): + create_substance_request(client, 'Sulphur') + + rv = index_substances_request(client, 'Sulphur') + + assert rv.status_code == 200 + + content = rv.get_json() + + assert type(content) is list + + assert len(content) == 1 + + assert type(content[0]['id']) is int + + assert is_dictionary_inside({ + 'nature': 'Sulphur', + 'state': [], + 'is_philosophers_stone': False + }, content[0]) + +# We also need non-happy paths! diff --git a/022-burette/.magnum_opus/tests/conftest.py b/022-burette/.magnum_opus/tests/conftest.py new file mode 100644 index 0000000..19d7e10 --- /dev/null +++ b/022-burette/.magnum_opus/tests/conftest.py @@ -0,0 +1,23 @@ +import pytest +from magnumopus import create_app +from magnumopus.models import db + +@pytest.fixture(scope='session') +def app(): + app = create_app() + return app + + +@pytest.fixture(scope='session') +def _db(app): + with app.app_context(): + db.create_all() + return db + +@pytest.fixture +def client(app): + with app.test_client() as client: + with app.app_context(): + db.create_all() + yield client + db.drop_all() diff --git a/022-burette/.magnum_opus/tests/domain/__init__.py b/022-burette/.magnum_opus/tests/domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/022-burette/.magnum_opus/tests/domain/test_alembic.py b/022-burette/.magnum_opus/tests/domain/test_alembic.py new file mode 100644 index 0000000..cd67657 --- /dev/null +++ b/022-burette/.magnum_opus/tests/domain/test_alembic.py @@ -0,0 +1,81 @@ +import pytest + +from magnumopus.services.alembic import Alembic, NotEnoughSubstancesToMixException, UnknownProcessException +from magnumopus.models.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/022-burette/.magnum_opus/tests/domain/test_alembic_instruction_handler.py b/022-burette/.magnum_opus/tests/domain/test_alembic_instruction_handler.py new file mode 100644 index 0000000..ccb2f96 --- /dev/null +++ b/022-burette/.magnum_opus/tests/domain/test_alembic_instruction_handler.py @@ -0,0 +1,37 @@ +import pytest + +from magnumopus.services.alembic_instruction_handler import AlembicInstruction, AlembicInstructionHandler +from magnumopus.repositories.list_pantry import ListPantry +from magnumopus.models.substance import Substance + +@pytest.fixture +def instruction_unknown(): + return AlembicInstruction( + 'saute', + ['Sulphur'], + 'cook' + ) + +@pytest.fixture +def instruction(): + return AlembicInstruction( + 'process', + ['Sludge'], + 'cook' + ) + +@pytest.fixture +def pantry(): + pantry = ListPantry() + + substance = Substance(nature='Sludge') + pantry.add_substance(substance) + + substance = Substance(nature='Sulphur') + pantry.add_substance(substance) + + return pantry + +def test_can_set_up_my_alembic_instruction_handler(instruction, pantry): + handler = AlembicInstructionHandler() + handler.handle(instruction, pantry) diff --git a/022-burette/.magnum_opus/tests/domain/test_pantry.py b/022-burette/.magnum_opus/tests/domain/test_pantry.py new file mode 100644 index 0000000..464ed15 --- /dev/null +++ b/022-burette/.magnum_opus/tests/domain/test_pantry.py @@ -0,0 +1,43 @@ +import pytest + +from magnumopus.repositories.list_pantry import ListPantry +from magnumopus.repositories.sqlalchemy_pantry import SQLAlchemyPantry +from magnumopus.models.substance import Substance + +@pytest.fixture +def list_pantry(): + return ListPantry() + +@pytest.fixture +def sqlalchemy_pantry(_db): + return SQLAlchemyPantry(_db) + +@pytest.fixture +def pantries(list_pantry, sqlalchemy_pantry): + return { + 'list': list_pantry, + 'sqlalchemy': sqlalchemy_pantry + } + +# We may want other pantry-specific tests, but bear in mind LSP +@pytest.mark.parametrize('pantry_type', ['list', 'sqlalchemy']) +def test_can_add_to_pantry(pantry_type, pantries): + pantry = pantries[pantry_type] + + substance = Substance() + + pantry.add_substance(substance) + + assert pantry.count_all_substances() == 1 + +@pytest.mark.parametrize('pantry_type', ['list', 'sqlalchemy']) +def test_can_retrieve_substance_from_pantry_by_nature(pantry_type, pantries): + pantry = pantries[pantry_type] + + substance = Substance(nature='Mercury') + + pantry.add_substance(substance) + + mercury = pantry.find_substances_by_nature('Mercury')[0] + + assert mercury.nature == 'Mercury' diff --git a/022-burette/.magnum_opus/tests/domain/test_substance.py b/022-burette/.magnum_opus/tests/domain/test_substance.py new file mode 100644 index 0000000..d00a6f7 --- /dev/null +++ b/022-burette/.magnum_opus/tests/domain/test_substance.py @@ -0,0 +1,49 @@ +from magnumopus.models.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/022-burette/.magnum_opus/tox.ini b/022-burette/.magnum_opus/tox.ini new file mode 100644 index 0000000..d815853 --- /dev/null +++ b/022-burette/.magnum_opus/tox.ini @@ -0,0 +1,6 @@ +[tox] +envlist = py38 + +[testenv] +deps = pytest +commands = pytest