Опись мира: счётчики классов A и C #160
No files matched your search
@@ -209,3 +209,8 @@ _Избегать_: описание схемы, документация кон
|
|||||||
потоковый приём событий.
|
потоковый приём событий.
|
||||||
_Избегать_: путать с приёмником и пакетным режимом — те про сторону
|
_Избегать_: путать с приёмником и пакетным режимом — те про сторону
|
||||||
генератора, про отправку; забор — про сторону хранилища, про чтение.
|
генератора, про отправку; забор — про сторону хранилища, про чтение.
|
||||||
|
|
||||||
|
**Техническая проверка**:
|
||||||
|
Проверка, чей оракул — внутренний эталон стенда (опись мира), которого у
|
||||||
|
настоящего магазина нет. За учебный урок DQ не выдаётся.
|
||||||
|
_Избегать_: DQ-проверка — та работает на наблюдаемых данных магазина.
|
||||||
@@ -151,8 +151,9 @@ Airflow его явную пересборку: удалит прикладны
|
|||||||
репозитории значило бы держать там кэш. Вместо него лежит **опись мира**,
|
репозитории значило бы держать там кэш. Вместо него лежит **опись мира**,
|
||||||
[`data/world-inventory.json`](data/world-inventory.json): зерно, версия
|
[`data/world-inventory.json`](data/world-inventory.json): зерно, версия
|
||||||
генератора, хеш каталога товаров и по строке на каждый день — дата, число
|
генератора, хеш каталога товаров и по строке на каждый день — дата, число
|
||||||
событий и хеш его байтов. Опись отвечает на единственный вопрос: тот ли это
|
событий и хеш его байтов. У дней с закрытым окном там же лежат счётчики
|
||||||
мир, что был вчера.
|
классов расхождений по заказам. Опись отвечает на единственный вопрос: тот ли
|
||||||
|
это мир, что был вчера.
|
||||||
|
|
||||||
Спрашивают её двое. `make test` в `generator/` сверяет опись с тем, что
|
Спрашивают её двое. `make test` в `generator/` сверяет опись с тем, что
|
||||||
собирается из кода сегодня: правка генератора меняет мир, и опись надо
|
собирается из кода сегодня: правка генератора меняет мир, и опись надо
|
||||||
|
|||||||
@@ -7,7 +7,12 @@
|
|||||||
"day": 0,
|
"day": 0,
|
||||||
"date": "2026-06-01",
|
"date": "2026-06-01",
|
||||||
"events": 50626,
|
"events": 50626,
|
||||||
"sha256": "81ad7f402bdd4c8b56425f6439a024b9996e5ca95dd207d444290d20a70ed407"
|
"sha256": "81ad7f402bdd4c8b56425f6439a024b9996e5ca95dd207d444290d20a70ed407",
|
||||||
|
"orders": {
|
||||||
|
"match": 197,
|
||||||
|
"cancelled": 20,
|
||||||
|
"amount_delta": 6
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"day": 1,
|
"day": 1,
|
||||||
|
|||||||
@@ -87,7 +87,11 @@ Airflow. Партиция — день данных; прогон звена DQ
|
|||||||
- счётная сверка сессий с эталоном `VisitID` — кука × день, эталон читается
|
- счётная сверка сессий с эталоном `VisitID` — кука × день, эталон читается
|
||||||
из `ods.event_v` ([модель сессии](dds/session.md));
|
из `ods.event_v` ([модель сессии](dds/session.md));
|
||||||
- счётчики классов расхождений против описи мира — по дням с закрытым
|
- счётчики классов расхождений против описи мира — по дням с закрытым
|
||||||
окном;
|
окном. Проверка техническая, не урок DQ: её оракул — опись генератора,
|
||||||
|
а у настоящего магазина оракула нет. Обещание узкое и честное: до
|
||||||
|
витрин доехал тот мир, который родился, — одна растяжка через весь
|
||||||
|
путь вместо сторожей на каждом шаге. Верно ли сам мир рождён, она
|
||||||
|
не видит: опись считает тот же генератор;
|
||||||
- счётные сверки витрин стенда с их DDS-источниками.
|
- счётные сверки витрин стенда с их DDS-источниками.
|
||||||
|
|
||||||
Результаты ложатся в `dm.dq_summary`. Звено — не только сторож конвейера,
|
Результаты ложатся в `dm.dq_summary`. Звено — не только сторож конвейера,
|
||||||
|
|||||||
@@ -16,11 +16,11 @@
|
|||||||
артефакт мира. Побайтовое обещание («слепок переснимается и даёт те же
|
артефакт мира. Побайтовое обещание («слепок переснимается и даёт те же
|
||||||
байты») опись начинает сторожить.
|
байты») опись начинает сторожить.
|
||||||
- **Счётчики классов расхождений — наблюдаемых, после приоритета**, по
|
- **Счётчики классов расхождений — наблюдаемых, после приоритета**, по
|
||||||
итоговой судьбе заказов дня; единица счёта — заказ, и опись называет её
|
итоговой судьбе заказов дня. В строке закрытого дня поле `orders` хранит
|
||||||
словом. Сойтись с запросом менти они могут только на днях с закрытым окном:
|
число заказов каждого класса; пока окно открыто, поля нет. При N сыгранных
|
||||||
при N сыгранных днях таких N − 7 (день d закрывается слепком d + 6, а
|
днях закрыты N − 7 (день d закрывается слепком d + 6, а последний
|
||||||
последний отправленный слепок несёт день N − 2). Читатель у них придёт
|
отправленный слепок несёт день N − 2). Их читатель — техническая
|
||||||
этапом 4 — проверка сверки; не окажется читателя — та же бритва режет и их.
|
проверка `classes_vs_inventory` из DQ-звена этапа 4.
|
||||||
- **Контрольные числа идентичности не заводятся вовсе**: uniq известных
|
- **Контрольные числа идентичности не заводятся вовсе**: uniq известных
|
||||||
пользователей и число двухкуковых пар растут, пока мир едет, — опоры из них
|
пользователей и число двухкуковых пар растут, пока мир едет, — опоры из них
|
||||||
не выходит; генератор сторожит хеш, транспорт — счёт событий.
|
не выходит; генератор сторожит хеш, транспорт — счёт событий.
|
||||||
|
|||||||
@@ -535,9 +535,12 @@ Kafka день переигрывается генератором заново:
|
|||||||
|
|
||||||
- по строке на каждый отправленный слепок — хеш байтов: побайтовое обещание
|
- по строке на каждый отправленный слепок — хеш байтов: побайтовое обещание
|
||||||
«слепок переснимается и даёт те же байты» опись сторожит наравне с днями;
|
«слепок переснимается и даёт те же байты» опись сторожит наравне с днями;
|
||||||
- заказная сторона: заказы, выручка и счётчики наблюдаемых классов
|
- заказная сторона: счётчики наблюдаемых классов расхождений после
|
||||||
расхождений после приоритета, в заказах — по дням с закрытым окном
|
приоритета; каждый показывает число заказов и хранится только для дня с
|
||||||
(самопроверка лабы сверки; при N сыгранных днях таких дней N − 7);
|
закрытым окном (читатель — техническая проверка `classes_vs_inventory`
|
||||||
|
звена DQ; при N сыгранных днях таких дней N − 7). Отдельных полей для
|
||||||
|
общего числа заказов и выручки в описи пока нет: для них не назван
|
||||||
|
читатель;
|
||||||
- контрольные числа идентичности не заводятся: uniq известных пользователей
|
- контрольные числа идентичности не заводятся: uniq известных пользователей
|
||||||
и число двухкуковых покупателей растут, пока мир едет, а счётчик без
|
и число двухкуковых покупателей растут, пока мир едет, а счётчик без
|
||||||
названного читателя в опись не кладётся
|
названного читателя в опись не кладётся
|
||||||
|
|||||||
+2
-1
@@ -86,7 +86,8 @@ D0 живёт предыстория, поэтому любой день соб
|
|||||||
из контракта. Документ руками не правят — пересобирают.
|
из контракта. Документ руками не правят — пересобирают.
|
||||||
- `src/clickstream_generator/inventory.py` — сборка описи мира
|
- `src/clickstream_generator/inventory.py` — сборка описи мира
|
||||||
([`data/world-inventory.json`](../data/world-inventory.json)): паспорт мира
|
([`data/world-inventory.json`](../data/world-inventory.json)): паспорт мира
|
||||||
и хеши восьми дней, которыми наполняется стенд. Руками не правят —
|
и хеши восьми дней, которыми наполняется стенд, плюс счётчики классов
|
||||||
|
расхождений по заказам для дней с закрытым окном. Руками не правят —
|
||||||
пересобирают целью `make inventory`.
|
пересобирают целью `make inventory`.
|
||||||
- `tests/` — инварианты контракта, свежесть описания и обещания мира:
|
- `tests/` — инварианты контракта, свежесть описания и обещания мира:
|
||||||
чистота от зерна, приток, гарантия двухкуковых пар, форма суточной волны
|
чистота от зерна, приток, гарантия двухкуковых пар, форма суточной волны
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"""Опись мира: чем стенд наполняется при подъёме и каким это обязано выйти.
|
"""Опись мира: чем стенд наполняется при подъёме и каким это обязано выйти.
|
||||||
|
|
||||||
Мир — чистая функция зерна (спека генератора, раздел 2), поэтому в git лежит не
|
Мир — чистая функция зерна (спека генератора, раздел 2), поэтому в git лежит не
|
||||||
он сам, а опись: паспорт мира, число событий по дням и хеш байтов каждого дня.
|
он сам, а опись: паспорт мира, число событий и хеш байтов каждого дня, счётчики
|
||||||
Сам мир пересчитывается когда угодно, а опись отвечает на единственный вопрос —
|
классов расхождений по заказам. Сам мир пересчитывается когда угодно, а опись
|
||||||
**тот ли это мир, что был вчера**. Разошлись хеши — мир уехал, и дальше уже
|
отвечает на единственный вопрос — **тот ли это мир, что был вчера**. Разошлись
|
||||||
неважно, чего от него ждали проверки.
|
хеши — мир уехал, и дальше уже неважно, чего от него ждали проверки.
|
||||||
|
|
||||||
Дней в описи восемь: столько заливается в стенд при `make up`. Понедельник по
|
Дней в описи восемь: столько заливается в стенд при `make up`. Понедельник по
|
||||||
понедельник — полная неделя с выходными и первый замкнутый цикл окна K = 7.
|
понедельник — полная неделя с выходными и первый замкнутый цикл окна K = 7.
|
||||||
@@ -43,9 +43,9 @@ from importlib.metadata import version
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from clickstream_generator import commerce, serialize, world
|
||||||
from clickstream_generator import day as day_module
|
from clickstream_generator import day as day_module
|
||||||
from clickstream_generator import orders as orders_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.catalog import CATALOG_PATH
|
||||||
from clickstream_generator.seeds import CANONICAL_SEED
|
from clickstream_generator.seeds import CANONICAL_SEED
|
||||||
|
|
||||||
@@ -66,7 +66,10 @@ def build() -> dict[str, Any]:
|
|||||||
orders = []
|
orders = []
|
||||||
for number in range(STARTING_DAYS):
|
for number in range(STARTING_DAYS):
|
||||||
today = day_module.stream(CANONICAL_SEED, number)
|
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)
|
orders.append(today.orders)
|
||||||
|
|
||||||
return {
|
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:
|
def _date(number: int) -> str:
|
||||||
return (world.ORIGIN + timedelta(days=number)).isoformat()
|
return (world.ORIGIN + timedelta(days=number)).isoformat()
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ from pathlib import Path
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from clickstream_generator import cli
|
from clickstream_generator import cli
|
||||||
|
from clickstream_generator import day as day_module
|
||||||
from clickstream_generator.inventory import STARTING_DAYS, build
|
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"
|
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):
|
def test_the_inventory_holds_the_hash_of_every_sent_snapshot(inventory: dict, tmp_path):
|
||||||
"""У каждого отправленного слепка — своя строка с хешем его байтов.
|
"""У каждого отправленного слепка — своя строка с хешем его байтов.
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user