From 80c474b37bbc7a548a30fd577ba9e5141ba4fbc0 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 7 Jul 2026 11:58:41 +0000 Subject: [PATCH] =?UTF-8?q?Batches=20never=20split=20a=20postcode=20?= =?UTF-8?q?=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- backend/app/modelling/batching.py | 19 +++++++++++++ tests/backend/app/modelling/test_batching.py | 30 ++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 backend/app/modelling/batching.py create mode 100644 tests/backend/app/modelling/test_batching.py diff --git a/backend/app/modelling/batching.py b/backend/app/modelling/batching.py new file mode 100644 index 000000000..914746e24 --- /dev/null +++ b/backend/app/modelling/batching.py @@ -0,0 +1,19 @@ +"""Pack a Modelling Run's resolved properties into SQS-message-sized batches. + +Batches stay postcode-grouped — properties sharing a postcode ride the same +message so the workers' prediction cohort cache keeps paying (ADR-0055), the +same packing the trigger script has been running. +""" + +from backend.app.modelling.property_filters import FilteredProperty + +BATCH_SIZE = 50 + + +def pack_postcode_batches( + properties: list[FilteredProperty], batch_size: int = BATCH_SIZE +) -> list[list[FilteredProperty]]: + """Pack *properties* into batches of ~*batch_size*, never splitting a + postcode across batches. A single postcode larger than *batch_size* + becomes its own oversized batch.""" + raise NotImplementedError diff --git a/tests/backend/app/modelling/test_batching.py b/tests/backend/app/modelling/test_batching.py new file mode 100644 index 000000000..a7f196036 --- /dev/null +++ b/tests/backend/app/modelling/test_batching.py @@ -0,0 +1,30 @@ +from backend.app.modelling.batching import pack_postcode_batches +from backend.app.modelling.property_filters import FilteredProperty + + +def _properties(postcode: str, count: int, start_id: int) -> list[FilteredProperty]: + return [ + FilteredProperty(property_id=start_id + i, postcode=postcode) + for i in range(count) + ] + + +def test_postcodes_are_never_split_across_batches() -> None: + # arrange — 30 + 30 + 10 with a cap of 50: the second postcode won't fit + # alongside the first, the third rides with the second + properties = ( + _properties("B93 8SU", 30, start_id=0) + + _properties("M20 4TF", 30, start_id=100) + + _properties("SW1A 1AA", 10, start_id=200) + ) + + # act + batches = pack_postcode_batches(properties, batch_size=50) + + # assert + assert [len(b) for b in batches] == [30, 40] + for batch in batches: + for postcode in {p.postcode for p in batch}: + in_batch = [p for p in batch if p.postcode == postcode] + everywhere = [p for p in properties if p.postcode == postcode] + assert len(in_batch) == len(everywhere) # whole postcode, one batch