From d38124263af241d1f622be1425c1868c5f5a9658 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Thu, 11 Jun 2026 11:00:39 +0300 Subject: [PATCH] =?UTF-8?q?feat(generator):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=20=D1=81=D0=B2=D1=8F=D0=B7=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D0=B2=D0=B8=D0=B7=D0=B8=D1=82=20=D0=B2=20?= =?UTF-8?q?=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=82=D0=BE=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - нужен первый проверяемый срез новой модели, где один визит содержит несколько связанных событий. - Что: - добавлен индекс browser-событий по click_id для выбора сид-визита как основы. - generate_batch теперь выпускает несколько browser-событий с одним новым click_id и общим device/geo-контекстом. - добавлен тест публичного контракта связанного визита. - Проверка: - make generator-test. --- generator/generator.py | 37 ++++++++++++++++++++++++++---- generator/tests/test_generation.py | 37 ++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/generator/generator.py b/generator/generator.py index c937d5e..1ae06ce 100644 --- a/generator/generator.py +++ b/generator/generator.py @@ -153,12 +153,15 @@ class EventDictionary: 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) device_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): # Строим индексы для связности + for browser in self.browser_events: + self.browser_by_click_id.setdefault(browser["click_id"], []).append(browser) for loc in self.location_events: self.location_by_event_id[loc["event_id"]] = loc for dev in self.device_events: @@ -281,16 +284,40 @@ class EventGenerator: "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_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_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 new_event_id = self._new_uuid() - new_click_id = self._new_uuid() new_timestamp = self._current_timestamp() # Создаём новое браузерное событие diff --git a/generator/tests/test_generation.py b/generator/tests/test_generation.py index 5ad968d..43762cc 100644 --- a/generator/tests/test_generation.py +++ b/generator/tests/test_generation.py @@ -61,6 +61,43 @@ class TestEventGeneration: assert len(batch["device_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): """event_id и click_id — новые UUID, не из оригинальных данных.""" generator = EventGenerator(event_dictionary, base_config)