feat(generator): добавлены счётчики классов заказов
Зачем: - дать описи контрольные числа для закрытых дней заказного окна; - зафиксировать словарь наблюдаемых классов без изменения самого мира. Что: - посчитаны match, cancelled и amount_delta с приоритетом отмены; - счётчики добавлены только к дням с закрытым окном; - опись, тесты и документация приведены к новому формату. Проверка: - make lint; - make typecheck; - make test — 429 passed; - uv run pytest tests/test_inventory.py -q — 3 passed после ревью.
This commit is contained in:
1 parent
f1fee5f1d1
commit
1267b45f47
7 files changed
+61
-18
No files matched your search
+2
-1
@@ -86,7 +86,8 @@ D0 живёт предыстория, поэтому любой день соб
|
||||
из контракта. Документ руками не правят — пересобирают.
|
||||
- `src/clickstream_generator/inventory.py` — сборка описи мира
|
||||
([`data/world-inventory.json`](../data/world-inventory.json)): паспорт мира
|
||||
и хеши восьми дней, которыми наполняется стенд. Руками не правят —
|
||||
и хеши восьми дней, которыми наполняется стенд, плюс счётчики классов
|
||||
расхождений по заказам для дней с закрытым окном. Руками не правят —
|
||||
пересобирают целью `make inventory`.
|
||||
- `tests/` — инварианты контракта, свежесть описания и обещания мира:
|
||||
чистота от зерна, приток, гарантия двухкуковых пар, форма суточной волны
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Опись мира: чем стенд наполняется при подъёме и каким это обязано выйти.
|
||||
|
||||
Мир — чистая функция зерна (спека генератора, раздел 2), поэтому в git лежит не
|
||||
он сам, а опись: паспорт мира, число событий по дням и хеш байтов каждого дня.
|
||||
Сам мир пересчитывается когда угодно, а опись отвечает на единственный вопрос —
|
||||
**тот ли это мир, что был вчера**. Разошлись хеши — мир уехал, и дальше уже
|
||||
неважно, чего от него ждали проверки.
|
||||
он сам, а опись: паспорт мира, число событий и хеш байтов каждого дня, счётчики
|
||||
классов расхождений по заказам. Сам мир пересчитывается когда угодно, а опись
|
||||
отвечает на единственный вопрос — **тот ли это мир, что был вчера**. Разошлись
|
||||
хеши — мир уехал, и дальше уже неважно, чего от него ждали проверки.
|
||||
|
||||
Дней в описи восемь: столько заливается в стенд при `make up`. Понедельник по
|
||||
понедельник — полная неделя с выходными и первый замкнутый цикл окна K = 7.
|
||||
@@ -43,9 +43,9 @@ from importlib.metadata import version
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from clickstream_generator import commerce, serialize, world
|
||||
from clickstream_generator import day as day_module
|
||||
from clickstream_generator import orders as orders_module
|
||||
from clickstream_generator import serialize, world
|
||||
from clickstream_generator.catalog import CATALOG_PATH
|
||||
from clickstream_generator.seeds import CANONICAL_SEED
|
||||
|
||||
@@ -66,7 +66,10 @@ def build() -> dict[str, Any]:
|
||||
orders = []
|
||||
for number in range(STARTING_DAYS):
|
||||
today = day_module.stream(CANONICAL_SEED, number)
|
||||
days.append(_day(number, today))
|
||||
row = _day(number, today)
|
||||
if number < STARTING_DAYS - world.ORDER_WINDOW_DAYS:
|
||||
row["orders"] = _order_class_counts(today)
|
||||
days.append(row)
|
||||
orders.append(today.orders)
|
||||
|
||||
return {
|
||||
@@ -115,6 +118,25 @@ def _snapshot(number: int, orders: list[orders_module.Orders]) -> dict[str, Any]
|
||||
}
|
||||
|
||||
|
||||
def _order_class_counts(today: day_module.Day) -> dict[str, int]:
|
||||
"""Заказы дня по итоговому классу: отмена перевешивает дельту суммы."""
|
||||
purchase = today.columns["EventType"] == commerce.PURCHASE
|
||||
declared = today.columns["purchaseRevenue"][purchase]
|
||||
counts = {"match": 0, "cancelled": 0, "amount_delta": 0}
|
||||
|
||||
for outcome, items_total, revenue in zip(
|
||||
today.orders.outcome, today.orders.items_total, declared, strict=True
|
||||
):
|
||||
if outcome != orders_module.OrderOutcome.PAID:
|
||||
counts["cancelled"] += 1
|
||||
elif items_total != round(revenue[0] * commerce.KOPECKS):
|
||||
counts["amount_delta"] += 1
|
||||
else:
|
||||
counts["match"] += 1
|
||||
|
||||
return counts
|
||||
|
||||
|
||||
def _date(number: int) -> str:
|
||||
return (world.ORIGIN + timedelta(days=number)).isoformat()
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from clickstream_generator import cli
|
||||
from clickstream_generator import day as day_module
|
||||
from clickstream_generator.inventory import STARTING_DAYS, build
|
||||
from clickstream_generator.seeds import CANONICAL_SEED
|
||||
|
||||
INVENTORY_PATH = Path(__file__).resolve().parents[2] / "data" / "world-inventory.json"
|
||||
|
||||
@@ -41,6 +43,16 @@ def test_inventory_is_up_to_date(inventory: dict):
|
||||
)
|
||||
|
||||
|
||||
def test_order_class_counters_cover_only_closed_days(inventory: dict):
|
||||
"""Итог заказа становится опорой только после закрытия его окна."""
|
||||
rows = [row for row in inventory["days"] if "orders" in row]
|
||||
|
||||
assert [row["day"] for row in rows] == [0]
|
||||
assert set(rows[0]["orders"]) == {"match", "cancelled", "amount_delta"}
|
||||
today = day_module.stream(CANONICAL_SEED, rows[0]["day"])
|
||||
assert sum(rows[0]["orders"].values()) == len(today.orders)
|
||||
|
||||
|
||||
def test_the_inventory_holds_the_hash_of_every_sent_snapshot(inventory: dict, tmp_path):
|
||||
"""У каждого отправленного слепка — своя строка с хешем его байтов.
|
||||
|
||||
|
||||
Reference in new issue
Block a user