Skip to content

Commit d77ebf3

Browse files
committed
start on a Product model with an allocate fn [product_aggregate]
1 parent 2156971 commit d77ebf3

File tree

10 files changed

+75
-205
lines changed

10 files changed

+75
-205
lines changed

src/allocation/model.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,21 @@ class OutOfStock(Exception):
88
pass
99

1010

11-
def allocate(line: OrderLine, batches: List[Batch]) -> str:
12-
try:
13-
batch = next(
14-
b for b in sorted(batches) if b.can_allocate(line)
15-
)
16-
batch.allocate(line)
17-
return batch.reference
18-
except StopIteration:
19-
raise OutOfStock(f'Out of stock for sku {line.sku}')
11+
class Product:
12+
13+
def __init__(self, sku: str, batches: List[Batch]):
14+
self.sku = sku
15+
self.batches = batches
16+
17+
def allocate(self, line: OrderLine) -> str:
18+
try:
19+
batch = next(
20+
b for b in sorted(self.batches) if b.can_allocate(line)
21+
)
22+
batch.allocate(line)
23+
return batch.reference
24+
except StopIteration:
25+
raise OutOfStock(f'Out of stock for sku {line.sku}')
2026

2127

2228
@dataclass(unsafe_hash=True)

src/allocation/orm.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,17 @@
1717
Column('orderid', String(255)),
1818
)
1919

20+
products = Table(
21+
'products', metadata,
22+
Column('sku', String(255), primary_key=True),
23+
# Column('version_number', Integer, nullable=False, default=0),
24+
)
25+
2026
batches = Table(
2127
'batches', metadata,
2228
Column('id', Integer, primary_key=True, autoincrement=True),
2329
Column('reference', String(255)),
24-
Column('sku', String(255)),
30+
Column('sku', ForeignKey('products.sku')),
2531
Column('_purchased_quantity', Integer, nullable=False),
2632
Column('eta', Date, nullable=True),
2733
)
@@ -36,10 +42,13 @@
3642

3743
def start_mappers():
3844
lines_mapper = mapper(model.OrderLine, order_lines)
39-
mapper(model.Batch, batches, properties={
45+
batches_mapper = mapper(model.Batch, batches, properties={
4046
'_allocations': relationship(
4147
lines_mapper,
4248
secondary=allocations,
4349
collection_class=set,
4450
)
4551
})
52+
mapper(model.Product, products, properties={
53+
'batches': relationship(batches_mapper)
54+
})

src/allocation/repository.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import abc
22
from allocation import model
33

4-
54
class AbstractRepository(abc.ABC):
65

76
@abc.abstractmethod
8-
def add(self, batch: model.Batch):
7+
def add(self, product: model.Product):
98
raise NotImplementedError
109

1110
@abc.abstractmethod
12-
def get(self, reference) -> model.Batch:
11+
def get(self, sku) -> model.Product:
1312
raise NotImplementedError
1413

1514

@@ -19,11 +18,8 @@ class SqlAlchemyRepository(AbstractRepository):
1918
def __init__(self, session):
2019
self.session = session
2120

22-
def add(self, batch):
23-
self.session.add(batch)
24-
25-
def get(self, reference):
26-
return self.session.query(model.Batch).filter_by(reference=reference).one()
21+
def add(self, product):
22+
self.session.add(product)
2723

28-
def list(self):
29-
return self.session.query(model.Batch).all()
24+
def get(self, sku):
25+
return self.session.query(model.Product).filter_by(sku=sku).first()

src/allocation/services.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ class InvalidSku(Exception):
1111
pass
1212

1313

14-
def is_valid_sku(sku, batches):
15-
return sku in {b.sku for b in batches}
16-
17-
1814
def add_batch(
1915
ref: str, sku: str, qty: int, eta: Optional[date],
2016
uow: unit_of_work.AbstractUnitOfWork
2117
):
2218
with uow:
23-
uow.batches.add(model.Batch(ref, sku, qty, eta))
19+
product = uow.products.get(sku=sku)
20+
if product is None:
21+
product = model.Product(sku, batches=[])
22+
uow.products.add(product)
23+
product.batches.append(model.Batch(ref, sku, qty, eta))
2424
uow.commit()
2525

2626

@@ -30,9 +30,9 @@ def allocate(
3030
) -> str:
3131
line = OrderLine(orderid, sku, qty)
3232
with uow:
33-
batches = uow.batches.list()
34-
if not is_valid_sku(line.sku, batches):
33+
product = uow.products.get(sku=line.sku)
34+
if product is None:
3535
raise InvalidSku(f'Invalid sku {line.sku}')
36-
batchref = model.allocate(line, batches)
36+
batchref = product.allocate(line)
3737
uow.commit()
3838
return batchref

src/allocation/unit_of_work.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# pylint: disable=attribute-defined-outside-init
22
from __future__ import annotations
33
import abc
4-
from typing import Callable
54
from sqlalchemy import create_engine
65
from sqlalchemy.orm import sessionmaker
76
from sqlalchemy.orm.session import Session
@@ -11,7 +10,7 @@
1110

1211

1312
class AbstractUnitOfWork(abc.ABC):
14-
batches: repository.AbstractRepository
13+
products: repository.AbstractRepository
1514

1615
def __enter__(self) -> AbstractUnitOfWork:
1716
return self
@@ -40,7 +39,7 @@ def __init__(self, session_factory=DEFAULT_SESSION_FACTORY):
4039

4140
def __enter__(self):
4241
self.session = self.session_factory() # type: Session
43-
self.batches = repository.SqlAlchemyRepository(self.session)
42+
self.products = repository.SqlAlchemyRepository(self.session)
4443
return super().__enter__()
4544

4645
def __exit__(self, *args):

tests/integration/test_orm.py

Lines changed: 0 additions & 90 deletions
This file was deleted.

tests/integration/test_repository.py

Lines changed: 0 additions & 62 deletions
This file was deleted.

tests/integration/test_uow.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
from allocation import unit_of_work
44

55
def insert_batch(session, ref, sku, qty, eta):
6+
session.execute(
7+
'INSERT INTO products (sku) VALUES (:sku)',
8+
dict(sku=sku),
9+
)
610
session.execute(
711
'INSERT INTO batches (reference, sku, _purchased_quantity, eta)'
812
' VALUES (:ref, :sku, :qty, :eta)',
@@ -29,9 +33,9 @@ def test_uow_can_retrieve_a_batch_and_allocate_to_it(session_factory):
2933

3034
uow = unit_of_work.SqlAlchemyUnitOfWork(session_factory)
3135
with uow:
32-
batch = uow.batches.get(reference='batch1')
36+
product = uow.products.get(sku='HIPSTER-WORKBENCH')
3337
line = model.OrderLine('o1', 'HIPSTER-WORKBENCH', 10)
34-
batch.allocate(line)
38+
product.allocate(line)
3539
uow.commit()
3640

3741
batchref = get_allocated_batch_ref(session, 'o1', 'HIPSTER-WORKBENCH')
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
from datetime import date, timedelta
22
import pytest
3-
from allocation.model import allocate, OrderLine, Batch, OutOfStock
3+
from allocation.model import Product, OrderLine, Batch, OutOfStock
44

55
today = date.today()
66
tomorrow = today + timedelta(days=1)
77
later = tomorrow + timedelta(days=10)
88

9-
def test_prefers_current_stock_batches_to_shipments():
9+
def test_prefers_warehouse_batches_to_shipments():
1010
in_stock_batch = Batch("in-stock-batch", "RETRO-CLOCK", 100, eta=None)
1111
shipment_batch = Batch("shipment-batch", "RETRO-CLOCK", 100, eta=tomorrow)
12+
product = Product(sku="RETRO-CLOCK", batches=[in_stock_batch, shipment_batch])
1213
line = OrderLine("oref", "RETRO-CLOCK", 10)
1314

14-
allocate(line, [in_stock_batch, shipment_batch])
15+
product.allocate(line)
1516

1617
assert in_stock_batch.available_quantity == 90
1718
assert shipment_batch.available_quantity == 100
@@ -21,9 +22,10 @@ def test_prefers_earlier_batches():
2122
earliest = Batch("speedy-batch", "MINIMALIST-SPOON", 100, eta=today)
2223
medium = Batch("normal-batch", "MINIMALIST-SPOON", 100, eta=tomorrow)
2324
latest = Batch("slow-batch", "MINIMALIST-SPOON", 100, eta=later)
25+
product = Product(sku="MINIMALIST-SPOON", batches=[medium, earliest, latest])
2426
line = OrderLine("order1", "MINIMALIST-SPOON", 10)
2527

26-
allocate(line, [medium, earliest, latest])
28+
product.allocate(line)
2729

2830
assert earliest.available_quantity == 90
2931
assert medium.available_quantity == 100
@@ -34,13 +36,15 @@ def test_returns_allocated_batch_ref():
3436
in_stock_batch = Batch("in-stock-batch-ref", "HIGHBROW-POSTER", 100, eta=None)
3537
shipment_batch = Batch("shipment-batch-ref", "HIGHBROW-POSTER", 100, eta=tomorrow)
3638
line = OrderLine("oref", "HIGHBROW-POSTER", 10)
37-
allocation = allocate(line, [in_stock_batch, shipment_batch])
39+
product = Product(sku="HIGHBROW-POSTER", batches=[in_stock_batch, shipment_batch])
40+
allocation = product.allocate(line)
3841
assert allocation == in_stock_batch.reference
3942

4043

4144
def test_raises_out_of_stock_exception_if_cannot_allocate():
4245
batch = Batch('batch1', 'SMALL-FORK', 10, eta=today)
43-
allocate(OrderLine('order1', 'SMALL-FORK', 10), [batch])
46+
product = Product(sku="SMALL-FORK", batches=[batch])
47+
product.allocate(OrderLine('order1', 'SMALL-FORK', 10))
4448

4549
with pytest.raises(OutOfStock, match='SMALL-FORK'):
46-
allocate(OrderLine('order2', 'SMALL-FORK', 1), [batch])
50+
product.allocate(OrderLine('order2', 'SMALL-FORK', 1))

0 commit comments

Comments
 (0)