feat(generator): добавлен связанный визит в генератор
- Зачем: - нужен первый проверяемый срез новой модели, где один визит содержит несколько связанных событий. - Что: - добавлен индекс browser-событий по click_id для выбора сид-визита как основы. - generate_batch теперь выпускает несколько browser-событий с одним новым click_id и общим device/geo-контекстом. - добавлен тест публичного контракта связанного визита. - Проверка: - make generator-test.
This commit is contained in:
+32
-5
@@ -153,12 +153,15 @@ class EventDictionary:
|
|||||||
geo_events: list[dict[str, Any]]
|
geo_events: list[dict[str, Any]]
|
||||||
|
|
||||||
# Индексы для быстрого поиска
|
# Индексы для быстрого поиска
|
||||||
|
browser_by_click_id: dict[str, list[dict]] = field(default_factory=dict)
|
||||||
location_by_event_id: dict[str, dict] = field(default_factory=dict)
|
location_by_event_id: dict[str, dict] = field(default_factory=dict)
|
||||||
device_by_click_id: dict[str, dict] = field(default_factory=dict)
|
device_by_click_id: dict[str, dict] = field(default_factory=dict)
|
||||||
geo_by_click_id: dict[str, dict] = field(default_factory=dict)
|
geo_by_click_id: dict[str, dict] = field(default_factory=dict)
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
# Строим индексы для связности
|
# Строим индексы для связности
|
||||||
|
for browser in self.browser_events:
|
||||||
|
self.browser_by_click_id.setdefault(browser["click_id"], []).append(browser)
|
||||||
for loc in self.location_events:
|
for loc in self.location_events:
|
||||||
self.location_by_event_id[loc["event_id"]] = loc
|
self.location_by_event_id[loc["event_id"]] = loc
|
||||||
for dev in self.device_events:
|
for dev in self.device_events:
|
||||||
@@ -281,16 +284,40 @@ class EventGenerator:
|
|||||||
"geo_events": [],
|
"geo_events": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
for _ in range(batch_size):
|
if batch_size <= 0:
|
||||||
# Выбираем случайное браузерное событие как базу
|
return batch
|
||||||
|
|
||||||
|
visit_candidates = [
|
||||||
|
click_id for click_id, browser_events in self.dictionary.browser_by_click_id.items()
|
||||||
|
if (
|
||||||
|
len(browser_events) >= batch_size
|
||||||
|
and click_id in self.dictionary.device_by_click_id
|
||||||
|
and click_id in self.dictionary.geo_by_click_id
|
||||||
|
and all(
|
||||||
|
event["event_id"] in self.dictionary.location_by_event_id
|
||||||
|
for event in browser_events[:batch_size]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
if visit_candidates:
|
||||||
|
base_click_id = self.rng.choice(visit_candidates)
|
||||||
|
base_browser_events = self.dictionary.browser_by_click_id[base_click_id][:batch_size]
|
||||||
|
else:
|
||||||
|
# Крайний случай для очень малого сида: сохраняем форму визита,
|
||||||
|
# даже если приходится брать события с повторением.
|
||||||
base_browser = self.rng.choice(self.dictionary.browser_events)
|
base_browser = self.rng.choice(self.dictionary.browser_events)
|
||||||
|
base_click_id = base_browser["click_id"]
|
||||||
|
base_browser_events = [base_browser for _ in range(batch_size)]
|
||||||
|
|
||||||
|
base_device = self.dictionary.device_by_click_id.get(base_click_id)
|
||||||
|
base_geo = self.dictionary.geo_by_click_id.get(base_click_id)
|
||||||
|
new_click_id = self._new_uuid()
|
||||||
|
|
||||||
|
for base_browser in base_browser_events:
|
||||||
base_location = self.dictionary.location_by_event_id.get(base_browser["event_id"])
|
base_location = self.dictionary.location_by_event_id.get(base_browser["event_id"])
|
||||||
base_device = self.dictionary.device_by_click_id.get(base_browser["click_id"])
|
|
||||||
base_geo = self.dictionary.geo_by_click_id.get(base_browser["click_id"])
|
|
||||||
|
|
||||||
# Генерируем новые ID
|
# Генерируем новые ID
|
||||||
new_event_id = self._new_uuid()
|
new_event_id = self._new_uuid()
|
||||||
new_click_id = self._new_uuid()
|
|
||||||
new_timestamp = self._current_timestamp()
|
new_timestamp = self._current_timestamp()
|
||||||
|
|
||||||
# Создаём новое браузерное событие
|
# Создаём новое браузерное событие
|
||||||
|
|||||||
@@ -61,6 +61,43 @@ class TestEventGeneration:
|
|||||||
assert len(batch["device_events"]) == 10
|
assert len(batch["device_events"]) == 10
|
||||||
assert len(batch["geo_events"]) == 10
|
assert len(batch["geo_events"]) == 10
|
||||||
|
|
||||||
|
def test_generate_batch_creates_one_connected_visit(self, event_dictionary, base_config):
|
||||||
|
"""Публичный вызов генератора создаёт один связанный визит."""
|
||||||
|
generator = EventGenerator(event_dictionary, base_config)
|
||||||
|
batch = generator.generate_batch(3)
|
||||||
|
|
||||||
|
original_event_ids = {e["event_id"] for e in event_dictionary.browser_events}
|
||||||
|
original_click_ids = {e["click_id"] for e in event_dictionary.browser_events}
|
||||||
|
|
||||||
|
browser_events = batch["browser_events"]
|
||||||
|
location_events = batch["location_events"]
|
||||||
|
device_events = batch["device_events"]
|
||||||
|
geo_events = batch["geo_events"]
|
||||||
|
|
||||||
|
click_ids = {event["click_id"] for event in browser_events}
|
||||||
|
event_ids = [event["event_id"] for event in browser_events]
|
||||||
|
|
||||||
|
assert len(browser_events) > 1
|
||||||
|
assert len(click_ids) == 1
|
||||||
|
click_id = next(iter(click_ids))
|
||||||
|
assert click_id not in original_click_ids
|
||||||
|
uuid.UUID(click_id)
|
||||||
|
|
||||||
|
assert len(set(event_ids)) == len(event_ids)
|
||||||
|
assert all(event_id not in original_event_ids for event_id in event_ids)
|
||||||
|
for event_id in event_ids:
|
||||||
|
uuid.UUID(event_id)
|
||||||
|
|
||||||
|
assert {event["event_id"] for event in location_events} == set(event_ids)
|
||||||
|
assert {event["click_id"] for event in device_events} == {click_id}
|
||||||
|
assert {event["click_id"] for event in geo_events} == {click_id}
|
||||||
|
|
||||||
|
device_context = [{k: v for k, v in event.items() if k != "click_id"} for event in device_events]
|
||||||
|
geo_context = [{k: v for k, v in event.items() if k != "click_id"} for event in geo_events]
|
||||||
|
assert len({event["user_domain_id"] for event in device_events}) == 1
|
||||||
|
assert all(context == device_context[0] for context in device_context)
|
||||||
|
assert all(context == geo_context[0] for context in geo_context)
|
||||||
|
|
||||||
def test_event_ids_are_new_uuids(self, event_dictionary, base_config):
|
def test_event_ids_are_new_uuids(self, event_dictionary, base_config):
|
||||||
"""event_id и click_id — новые UUID, не из оригинальных данных."""
|
"""event_id и click_id — новые UUID, не из оригинальных данных."""
|
||||||
generator = EventGenerator(event_dictionary, base_config)
|
generator = EventGenerator(event_dictionary, base_config)
|
||||||
|
|||||||
Reference in New Issue
Block a user