Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add missing method in abstract base class #29

Open
wants to merge 39 commits into
base: chapter_06_uow
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
fff68bd
ORM for _allocations set on Batch
hjwp Mar 11, 2019
8dae126
repository tests
hjwp Mar 11, 2019
b635a15
repository for batches [chapter_02_repository_ends]
hjwp Mar 11, 2019
fda077d
first api tests [first_api_test]
hjwp Mar 11, 2019
15902a6
all the dockerfile gubbins
hjwp Mar 19, 2019
8fc086e
first cut of flask app [first_cut_flask_app]
hjwp Mar 11, 2019
158e760
test persistence by double-allocating. [second_api_test]
hjwp Mar 19, 2019
d0ee7ad
need to commit [flask_commit]
hjwp Mar 19, 2019
f20c479
test some 400 error cases [test_error_cases]
hjwp Mar 19, 2019
80dece9
flask now does error handling [flask_error_handling]
hjwp Mar 19, 2019
a1903fa
first tests for the services layer [first_services_tests]
hjwp Mar 19, 2019
9fe9a3d
FakeRepository [fake_repo]
hjwp Jan 3, 2020
15b9bf7
FakeSession [fake_session]
hjwp Jan 3, 2020
eded7eb
test commmits [second_services_test]
hjwp Apr 23, 2019
d1e2e6e
services layer with valid-sku check [service_function]
hjwp Mar 19, 2019
cf2f52b
modify flask app to use service layer [flask_app_using_service_layer]
hjwp Mar 19, 2019
b537364
strip out unecessary tests from e2e layer [fewer_e2e_tests]
hjwp Mar 19, 2019
952a3d2
fix conftest waits and travis config [chapter_04_service_layer_ends]
hjwp Mar 26, 2019
bdf8fe9
move to a more nested folder structure
hjwp Dec 23, 2019
1bb572a
nest the tests too
hjwp Dec 31, 2019
db89218
get all tests passing
hjwp Dec 31, 2019
c5822aa
rewrite service layer to take primitives [service_takes_primitives]
hjwp Apr 23, 2019
f341bee
services tests partially converted to primitives [tests_call_with_pri…
hjwp Apr 23, 2019
1e1f238
fixture function for batches [services_factory_function]
hjwp Apr 23, 2019
5c953da
new service to add a batch [add_batch_service]
hjwp Apr 23, 2019
262eec0
service-layer test for add batch [test_add_batch]
hjwp Apr 23, 2019
c8fbb60
all service-layer tests now services [services_tests_all_services]
hjwp Apr 23, 2019
96301d2
modify flask app to use new service layer api [api_uses_modified_serv…
hjwp Apr 23, 2019
6b404b5
add api endpoint for add_batch [api_for_add_batch]
hjwp Apr 24, 2019
fd45a6f
api tests no longer need hardcoded sql fixture [chapter_05_high_gear_…
hjwp Apr 24, 2019
d9c340c
start moving files into src folder and add setup.py
hjwp Feb 27, 2019
c843a10
fix all the imports, get it all working
hjwp Feb 27, 2019
12d2bc2
get tests working in docker container
hjwp Feb 27, 2019
28afa9a
make mypy slightly stricter
hjwp May 23, 2019
53ad798
better requirements.txt [appendix_project_structure_ends]
hjwp Mar 11, 2019
833d48b
basic uow test, uow and conftest.py changes
hjwp Mar 11, 2019
658e61a
use uow in services, flask app
hjwp Mar 20, 2019
7526014
two more tests for rollback behaviour [chapter_06_uow_ends]
hjwp Mar 12, 2019
8a5f64a
Add missing method in abstract base class
mardiros Mar 21, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@ dist: xenial
language: python
python: 3.8

install:
- pip3 install sqlalchemy

script:
- make test
- make all

branches:
except:
Expand Down
15 changes: 15 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM python:3.9-slim-buster

# RUN apt install gcc libpq (no longer needed bc we use psycopg2-binary)

COPY requirements.txt /tmp/
RUN pip install -r /tmp/requirements.txt

RUN mkdir -p /src
COPY src/ /src/
RUN pip install -e /src
COPY tests/ /tests/

WORKDIR /src
ENV FLASK_APP=allocation/entrypoints/flask_app.py FLASK_DEBUG=1 PYTHONUNBUFFERED=1
CMD flask run --host=0.0.0.0 --port=80
32 changes: 28 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
test:
pytest --tb=short
# these will speed up builds, for docker-compose >= 1.25
export COMPOSE_DOCKER_CLI_BUILD=1
export DOCKER_BUILDKIT=1

watch-tests:
ls *.py | entr pytest --tb=short
all: down build up test

build:
docker-compose build

up:
docker-compose up -d app

down:
docker-compose down --remove-orphans

test: up
docker-compose run --rm --no-deps --entrypoint=pytest app /tests/unit /tests/integration /tests/e2e

unit-tests:
docker-compose run --rm --no-deps --entrypoint=pytest app /tests/unit

integration-tests: up
docker-compose run --rm --no-deps --entrypoint=pytest app /tests/integration

e2e-tests: up
docker-compose run --rm --no-deps --entrypoint=pytest app /tests/e2e

logs:
docker-compose logs app | tail -100

black:
black -l 86 $$(find * -name '*.py')
19 changes: 0 additions & 19 deletions conftest.py

This file was deleted.

29 changes: 29 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
version: "3"
services:

app:
build:
context: .
dockerfile: Dockerfile
depends_on:
- postgres
environment:
- DB_HOST=postgres
- DB_PASSWORD=abc123
- API_HOST=app
- PYTHONDONTWRITEBYTECODE=1
volumes:
- ./src:/src
- ./tests:/tests
ports:
- "5005:80"


postgres:
image: postgres:9.6
environment:
- POSTGRES_USER=allocation
- POSTGRES_PASSWORD=abc123
ports:
- "54321:5432"

8 changes: 3 additions & 5 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
[mypy]
ignore_missing_imports = False
mypy_path = ./src
check_untyped_defs = True

[mypy-pytest.*]
[mypy-pytest.*,sqlalchemy.*]
ignore_missing_imports = True

[mypy-sqlalchemy.*]
ignore_missing_imports = True

29 changes: 0 additions & 29 deletions orm.py

This file was deleted.

10 changes: 10 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# app
sqlalchemy
flask
psycopg2-binary

# tests
pytest
pytest-icdiff
mypy
requests
Empty file added src/allocation/__init__.py
Empty file.
Empty file.
49 changes: 49 additions & 0 deletions src/allocation/adapters/orm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from sqlalchemy import Table, MetaData, Column, Integer, String, Date, ForeignKey
from sqlalchemy.orm import mapper, relationship

from allocation.domain import model


metadata = MetaData()

order_lines = Table(
"order_lines",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("sku", String(255)),
Column("qty", Integer, nullable=False),
Column("orderid", String(255)),
)

batches = Table(
"batches",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("reference", String(255)),
Column("sku", String(255)),
Column("_purchased_quantity", Integer, nullable=False),
Column("eta", Date, nullable=True),
)

allocations = Table(
"allocations",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("orderline_id", ForeignKey("order_lines.id")),
Column("batch_id", ForeignKey("batches.id")),
)


def start_mappers():
lines_mapper = mapper(model.OrderLine, order_lines)
mapper(
model.Batch,
batches,
properties={
"_allocations": relationship(
lines_mapper,
secondary=allocations,
collection_class=set,
)
},
)
30 changes: 30 additions & 0 deletions src/allocation/adapters/repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import abc
from allocation.domain import model


class AbstractRepository(abc.ABC):
@abc.abstractmethod
def add(self, batch: model.Batch):
raise NotImplementedError

@abc.abstractmethod
def get(self, reference) -> model.Batch:
raise NotImplementedError

@abc.abstractmethod
def list(self):
raise NotImplementedError


class SqlAlchemyRepository(AbstractRepository):
def __init__(self, session):
self.session = session

def add(self, batch):
self.session.add(batch)

def get(self, reference):
return self.session.query(model.Batch).filter_by(reference=reference).one()

def list(self):
return self.session.query(model.Batch).all()
15 changes: 15 additions & 0 deletions src/allocation/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import os


def get_postgres_uri():
host = os.environ.get("DB_HOST", "localhost")
port = 54321 if host == "localhost" else 5432
password = os.environ.get("DB_PASSWORD", "abc123")
user, db_name = "allocation", "allocation"
return f"postgresql://{user}:{password}@{host}:{port}/{db_name}"


def get_api_url():
host = os.environ.get("API_HOST", "localhost")
port = 5005 if host == "localhost" else 80
return f"http://{host}:{port}"
Empty file.
File renamed without changes.
Empty file.
41 changes: 41 additions & 0 deletions src/allocation/entrypoints/flask_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from datetime import datetime
from flask import Flask, request
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from allocation.domain import model
from allocation.adapters import orm
from allocation.service_layer import services, unit_of_work

app = Flask(__name__)
orm.start_mappers()


@app.route("/add_batch", methods=["POST"])
def add_batch():
eta = request.json["eta"]
if eta is not None:
eta = datetime.fromisoformat(eta).date()
services.add_batch(
request.json["ref"],
request.json["sku"],
request.json["qty"],
eta,
unit_of_work.SqlAlchemyUnitOfWork(),
)
return "OK", 201


@app.route("/allocate", methods=["POST"])
def allocate_endpoint():
try:
batchref = services.allocate(
request.json["orderid"],
request.json["sku"],
request.json["qty"],
unit_of_work.SqlAlchemyUnitOfWork(),
)
except (model.OutOfStock, services.InvalidSku) as e:
return {"message": str(e)}, 400

return {"batchref": batchref}, 201
Empty file.
38 changes: 38 additions & 0 deletions src/allocation/service_layer/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from __future__ import annotations
from typing import Optional
from datetime import date

from allocation.domain import model
from allocation.domain.model import OrderLine
from allocation.service_layer import unit_of_work


class InvalidSku(Exception):
pass


def is_valid_sku(sku, batches):
return sku in {b.sku for b in batches}


def add_batch(
ref: str, sku: str, qty: int, eta: Optional[date],
uow: unit_of_work.AbstractUnitOfWork,
):
with uow:
uow.batches.add(model.Batch(ref, sku, qty, eta))
uow.commit()


def allocate(
orderid: str, sku: str, qty: int,
uow: unit_of_work.AbstractUnitOfWork,
) -> str:
line = OrderLine(orderid, sku, qty)
with uow:
batches = uow.batches.list()
if not is_valid_sku(line.sku, batches):
raise InvalidSku(f"Invalid sku {line.sku}")
batchref = model.allocate(line, batches)
uow.commit()
return batchref
Loading