haystack-ml-stack 0.4.11__tar.gz → 0.4.13__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/PKG-INFO +2 -1
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/pyproject.toml +3 -3
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/__init__.py +0 -0
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/_kafka.py +0 -0
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/_serializers.py +92 -2
- haystack_ml_stack-0.4.13/src/haystack_ml_stack/_version.py +1 -0
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/app.py +12 -1
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/cache.py +0 -0
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/dynamo.py +60 -23
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/generated/v1/features_pb2.py +7 -1
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/generated/v1/features_pb2.pyi +30 -0
- haystack_ml_stack-0.4.13/src/haystack_ml_stack/history.py +38 -0
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/settings.py +0 -0
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack.egg-info/PKG-INFO +2 -1
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack.egg-info/SOURCES.txt +2 -2
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack.egg-info/requires.txt +1 -0
- haystack_ml_stack-0.4.13/tests/test_history_fetch.py +226 -0
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/tests/test_serializers.py +92 -0
- haystack_ml_stack-0.4.11/src/haystack_ml_stack/_version.py +0 -1
- haystack_ml_stack-0.4.11/src/haystack_ml_stack/tmp.py +0 -28
- haystack_ml_stack-0.4.11/src/haystack_ml_stack/tmp2.py +0 -28
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/README.md +0 -0
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/setup.cfg +0 -0
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/exceptions.py +0 -0
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/generated/__init__.py +0 -0
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/generated/v1/__init__.py +0 -0
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/model_store.py +0 -0
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/utils.py +0 -0
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack.egg-info/dependency_links.txt +0 -0
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack.egg-info/top_level.txt +0 -0
- {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: haystack-ml-stack
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.13
|
|
4
4
|
Summary: Functions related to Haystack ML
|
|
5
5
|
Author-email: Oscar Vega <oscar@haystack.tv>
|
|
6
6
|
License: MIT
|
|
@@ -8,6 +8,7 @@ Requires-Python: >=3.11
|
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Requires-Dist: protobuf==6.33.2
|
|
10
10
|
Requires-Dist: orjson==3.11.7
|
|
11
|
+
Requires-Dist: numpy>=1.26
|
|
11
12
|
Provides-Extra: server
|
|
12
13
|
Requires-Dist: pydantic==2.5.0; extra == "server"
|
|
13
14
|
Requires-Dist: cachetools==5.5.2; extra == "server"
|
|
@@ -5,13 +5,13 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "haystack-ml-stack"
|
|
8
|
-
version = "0.4.
|
|
8
|
+
version = "0.4.13"
|
|
9
9
|
description = "Functions related to Haystack ML"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
authors = [{ name = "Oscar Vega", email = "oscar@haystack.tv" }]
|
|
12
12
|
requires-python = ">=3.11"
|
|
13
13
|
dependencies = [
|
|
14
|
-
"protobuf==6.33.2", "orjson==3.11.7"
|
|
14
|
+
"protobuf==6.33.2", "orjson==3.11.7", "numpy>=1.26"
|
|
15
15
|
]
|
|
16
16
|
license = { text = "MIT" }
|
|
17
17
|
|
|
@@ -25,4 +25,4 @@ server = [
|
|
|
25
25
|
"pydantic-settings==2.2",
|
|
26
26
|
"newrelic==11.1.0",
|
|
27
27
|
"confluent-kafka==2.13.0"
|
|
28
|
-
]
|
|
28
|
+
]
|
|
File without changes
|
|
File without changes
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from .generated.v1 import features_pb2 as features_pb2_v1
|
|
2
|
+
from .history import CONTEXT_CODES, STREAM_URL_PREFIX
|
|
2
3
|
from google.protobuf.message import Message
|
|
3
4
|
from google.protobuf.json_format import ParseDict as ProtoParseDict
|
|
5
|
+
import numpy as np
|
|
4
6
|
import typing as _t
|
|
5
7
|
from abc import ABC, abstractmethod
|
|
6
8
|
|
|
@@ -249,7 +251,9 @@ class GlobalPlaylistStatsSerializerV1(SimpleSerializer):
|
|
|
249
251
|
category_msg.watched_count = int(stats["watched_count"])
|
|
250
252
|
category_msg.not_watched_count = int(stats["not_watched_count"])
|
|
251
253
|
category_msg.capped_watched_secs = float(stats["capped_watched_secs"])
|
|
252
|
-
category_msg.capped_not_watched_secs = float(
|
|
254
|
+
category_msg.capped_not_watched_secs = float(
|
|
255
|
+
stats["capped_not_watched_secs"]
|
|
256
|
+
)
|
|
253
257
|
category_msg.watched_secs = float(stats["watched_secs"])
|
|
254
258
|
category_msg.not_watched_secs = float(stats["not_watched_secs"])
|
|
255
259
|
return root_msg
|
|
@@ -275,7 +279,9 @@ class UserPlaylistStatsSerializerV1(SimpleSerializer):
|
|
|
275
279
|
category_msg.active_days = int(stats["active_days"])
|
|
276
280
|
category_msg.total_watched = float(stats["total_watched"])
|
|
277
281
|
category_msg.capped_total_watched = float(stats["capped_total_watched"])
|
|
278
|
-
category_msg.capped_total_log_watched = float(
|
|
282
|
+
category_msg.capped_total_log_watched = float(
|
|
283
|
+
stats.get("capped_total_log_watched", 0.0)
|
|
284
|
+
)
|
|
279
285
|
category_msg.first_active_date = str(stats.get("first_active_date", ""))
|
|
280
286
|
return root_msg
|
|
281
287
|
|
|
@@ -301,6 +307,64 @@ class GlobalChannelsSerializerV1(SimpleSerializer):
|
|
|
301
307
|
return root_msg
|
|
302
308
|
|
|
303
309
|
|
|
310
|
+
class StreamEmbeddingSerializerV1(SimpleSerializer):
|
|
311
|
+
"""The raw 512-coordinate prefix, normalized in float32 then stored as float16."""
|
|
312
|
+
|
|
313
|
+
def __init__(self):
|
|
314
|
+
super().__init__(msg_class=features_pb2_v1.StreamEmbedding)
|
|
315
|
+
|
|
316
|
+
def serialize(self, value) -> bytes:
|
|
317
|
+
return self.build_msg(value).SerializeToString()
|
|
318
|
+
|
|
319
|
+
def build_msg(self, value) -> features_pb2_v1.StreamEmbedding:
|
|
320
|
+
assert value["version"] == 1, "Wrong version given!"
|
|
321
|
+
data = np.asarray(value["data"], dtype=np.float32)
|
|
322
|
+
assert data.shape == (512,), "Expected the first 512 coordinates"
|
|
323
|
+
norm = np.linalg.norm(data)
|
|
324
|
+
if not np.isfinite(norm) or norm <= 0:
|
|
325
|
+
raise ValueError("Expected a finite, nonzero 512-coordinate vector norm")
|
|
326
|
+
message = self.msg_class()
|
|
327
|
+
message.version = 1
|
|
328
|
+
message.data = (data / norm).astype("<f2").tobytes()
|
|
329
|
+
return message
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class UserHistorySerializerV1(SimpleSerializer):
|
|
333
|
+
"""Pack the SQL's selected states."""
|
|
334
|
+
|
|
335
|
+
def __init__(self):
|
|
336
|
+
super().__init__(msg_class=features_pb2_v1.UserHistory)
|
|
337
|
+
|
|
338
|
+
def serialize(self, value) -> bytes:
|
|
339
|
+
return self.build_msg(value).SerializeToString()
|
|
340
|
+
|
|
341
|
+
def build_msg(self, value) -> features_pb2_v1.UserHistory:
|
|
342
|
+
assert value["version"] == 1, "Wrong version given!"
|
|
343
|
+
data = value["data"]
|
|
344
|
+
columns = ["stream_urls", "event_at_ms", "seconds_watched", "contexts", "browsed"]
|
|
345
|
+
lengths = {len(data[column]) for column in columns}
|
|
346
|
+
assert len(lengths) == 1, "History columns must have equal lengths"
|
|
347
|
+
message = self.msg_class()
|
|
348
|
+
message.version = 1
|
|
349
|
+
message.data.snapshot_at_ms = data["snapshot_at_ms"]
|
|
350
|
+
count = len(data["stream_urls"])
|
|
351
|
+
assert count <= 256, "Expected at most 256 history states"
|
|
352
|
+
flags = bytearray()
|
|
353
|
+
for url, at_ms, context, browsed in zip(
|
|
354
|
+
data["stream_urls"], data["event_at_ms"], data["contexts"], data["browsed"]
|
|
355
|
+
):
|
|
356
|
+
suffix = url.removeprefix(STREAM_URL_PREFIX)
|
|
357
|
+
message.data.stream_ids.append(
|
|
358
|
+
suffix if url.startswith(STREAM_URL_PREFIX) and not suffix.startswith(":")
|
|
359
|
+
else ":" + url
|
|
360
|
+
)
|
|
361
|
+
message.data.event_age_ms.append(data["snapshot_at_ms"] - at_ms)
|
|
362
|
+
flags.append(CONTEXT_CODES.get(context, 1) | (128 if browsed else 0))
|
|
363
|
+
message.data.seconds_watched.extend(data["seconds_watched"])
|
|
364
|
+
message.data.context_browsed = bytes(flags)
|
|
365
|
+
return message
|
|
366
|
+
|
|
367
|
+
|
|
304
368
|
class PassThroughSerializer(Serializer):
|
|
305
369
|
def serialize(self, value):
|
|
306
370
|
return value
|
|
@@ -322,6 +386,8 @@ stream_similarity_scores_serializer_v1 = StreamSimilaritySerializerV1()
|
|
|
322
386
|
global_playlist_stats_serializer_v1 = GlobalPlaylistStatsSerializerV1()
|
|
323
387
|
user_playlist_stats_serializer_v1 = UserPlaylistStatsSerializerV1()
|
|
324
388
|
global_channels_serializer_v1 = GlobalChannelsSerializerV1()
|
|
389
|
+
stream_embedding_serializer_v1 = StreamEmbeddingSerializerV1()
|
|
390
|
+
user_history_serializer_v1 = UserHistorySerializerV1()
|
|
325
391
|
|
|
326
392
|
|
|
327
393
|
class FeatureRegistryId(_t.NamedTuple):
|
|
@@ -344,6 +410,11 @@ stream_pwatched_v1_features: list[FeatureRegistryId] = [
|
|
|
344
410
|
FeatureRegistryId(
|
|
345
411
|
entity_type="STREAM", feature_id="PWATCHED#24H#MOBILE", version="v1"
|
|
346
412
|
),
|
|
413
|
+
FeatureRegistryId(entity_type="STREAM", feature_id="PWATCHED#6H", version="v1"),
|
|
414
|
+
FeatureRegistryId(entity_type="STREAM", feature_id="PWATCHED#6H#TV", version="v1"),
|
|
415
|
+
FeatureRegistryId(
|
|
416
|
+
entity_type="STREAM", feature_id="PWATCHED#6H#MOBILE", version="v1"
|
|
417
|
+
),
|
|
347
418
|
]
|
|
348
419
|
|
|
349
420
|
stream_pselect_v0_features: list[FeatureRegistryId] = [
|
|
@@ -360,6 +431,11 @@ stream_pselect_v1_features: list[FeatureRegistryId] = [
|
|
|
360
431
|
entity_type="STREAM", feature_id="PSELECT#24H#MOBILE", version="v1"
|
|
361
432
|
),
|
|
362
433
|
FeatureRegistryId(entity_type="STREAM", feature_id="PSELECT#24H#TV", version="v1"),
|
|
434
|
+
FeatureRegistryId(entity_type="STREAM", feature_id="PSELECT#6H", version="v1"),
|
|
435
|
+
FeatureRegistryId(entity_type="STREAM", feature_id="PSELECT#6H#TV", version="v1"),
|
|
436
|
+
FeatureRegistryId(
|
|
437
|
+
entity_type="STREAM", feature_id="PSELECT#6H#MOBILE", version="v1"
|
|
438
|
+
),
|
|
363
439
|
]
|
|
364
440
|
|
|
365
441
|
stream_similarity_v0_features: list[FeatureRegistryId] = [
|
|
@@ -454,6 +530,18 @@ global_channels_v1_features: list[FeatureRegistryId] = [
|
|
|
454
530
|
),
|
|
455
531
|
]
|
|
456
532
|
|
|
533
|
+
gemini_title_transcript_512_v1_features: list[FeatureRegistryId] = [
|
|
534
|
+
FeatureRegistryId(
|
|
535
|
+
entity_type="STREAM",
|
|
536
|
+
feature_id="EMBEDDING#GEMINI_2_TITLE_TRANSCRIPT#512",
|
|
537
|
+
version="v1",
|
|
538
|
+
),
|
|
539
|
+
]
|
|
540
|
+
|
|
541
|
+
user_history_v1_features: list[FeatureRegistryId] = [
|
|
542
|
+
FeatureRegistryId(entity_type="USER", feature_id="HISTORY#5M#256", version="v1"),
|
|
543
|
+
]
|
|
544
|
+
|
|
457
545
|
features_serializer_tuples: list[tuple[list[FeatureRegistryId], Serializer]] = [
|
|
458
546
|
(stream_pwatched_v0_features, stream_pwatched_serializer_v0),
|
|
459
547
|
(stream_pwatched_v1_features, stream_pwatched_serializer_v1),
|
|
@@ -480,6 +568,8 @@ features_serializer_tuples: list[tuple[list[FeatureRegistryId], Serializer]] = [
|
|
|
480
568
|
global_channels_v1_features,
|
|
481
569
|
global_channels_serializer_v1,
|
|
482
570
|
),
|
|
571
|
+
(gemini_title_transcript_512_v1_features, stream_embedding_serializer_v1),
|
|
572
|
+
(user_history_v1_features, user_history_serializer_v1),
|
|
483
573
|
]
|
|
484
574
|
|
|
485
575
|
SerializerRegistry: dict[FeatureRegistryId, Serializer] = {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.13"
|
|
@@ -70,9 +70,14 @@ async def load_model(state, cfg: Settings) -> None:
|
|
|
70
70
|
(entity_type, feature_id)
|
|
71
71
|
for entity_type, feature_id, _ in SerializerRegistry.keys()
|
|
72
72
|
)
|
|
73
|
+
# Optional {"user_feature": ..., "stream_features": [...]}: the streams
|
|
74
|
+
# in that history record get these stream features.
|
|
75
|
+
history = state["model"].get("history")
|
|
76
|
+
history_features = history["stream_features"] if history else []
|
|
73
77
|
all_features = set(
|
|
74
78
|
[("STREAM", feature_name) for feature_name in state["stream_features"]]
|
|
75
79
|
+ [("USER", feature_name) for feature_name in state["user_features"]]
|
|
80
|
+
+ [("STREAM", feature_name) for feature_name in history_features]
|
|
76
81
|
)
|
|
77
82
|
invalid_features = all_features.difference(valid_features)
|
|
78
83
|
if invalid_features:
|
|
@@ -230,6 +235,8 @@ def create_app(
|
|
|
230
235
|
model = state["model"]
|
|
231
236
|
stream_features = model.get("stream_features", []) or []
|
|
232
237
|
user_features = model.get("user_features", []) or []
|
|
238
|
+
history = model.get("history")
|
|
239
|
+
history_streams: List[Dict[str, Any]] = []
|
|
233
240
|
retrieval_meta = FeatureRetrievalMeta(
|
|
234
241
|
cache_misses=0,
|
|
235
242
|
stream_cache_misses=0,
|
|
@@ -240,7 +247,7 @@ def create_app(
|
|
|
240
247
|
dynamo_ms=0,
|
|
241
248
|
parsing_ms=0,
|
|
242
249
|
)
|
|
243
|
-
if stream_features:
|
|
250
|
+
if stream_features or user_features:
|
|
244
251
|
try:
|
|
245
252
|
retrieval_meta = await set_all_features(
|
|
246
253
|
dynamo_client=state["dynamo_client"],
|
|
@@ -252,6 +259,8 @@ def create_app(
|
|
|
252
259
|
user_features_cache=user_features_cache,
|
|
253
260
|
features_table=cfg.features_table,
|
|
254
261
|
cache_sep=cfg.cache_separator,
|
|
262
|
+
history=history,
|
|
263
|
+
history_streams=history_streams,
|
|
255
264
|
)
|
|
256
265
|
except exceptions.InvalidFeaturesException as e:
|
|
257
266
|
logger.error(
|
|
@@ -280,6 +289,8 @@ def create_app(
|
|
|
280
289
|
try:
|
|
281
290
|
preprocess_start = time.perf_counter_ns()
|
|
282
291
|
model["params"]["query_params"] = query_params
|
|
292
|
+
# Outside `user`, so that the history vectors are not logged.
|
|
293
|
+
model["params"]["history_streams"] = history_streams
|
|
283
294
|
model_input = model["preprocess"](
|
|
284
295
|
user,
|
|
285
296
|
streams,
|
|
File without changes
|
|
@@ -9,6 +9,7 @@ from boto3.dynamodb.types import TypeDeserializer
|
|
|
9
9
|
|
|
10
10
|
from . import exceptions
|
|
11
11
|
from ._serializers import FeatureRegistryId, SerializerRegistry
|
|
12
|
+
from .history import history_stream_urls
|
|
12
13
|
from .utils import _complete_features_for_channels, DEFAULT_CHANNELS
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
@@ -106,7 +107,13 @@ async def set_all_features(
|
|
|
106
107
|
features_table: str,
|
|
107
108
|
cache_sep: str,
|
|
108
109
|
dynamo_client,
|
|
110
|
+
history: Dict[str, Any] | None = None,
|
|
111
|
+
history_streams: List[Dict[str, Any]] | None = None,
|
|
109
112
|
) -> FeatureRetrievalMeta:
|
|
113
|
+
"""Fetch features in parallel, filling history_streams when history is configured.
|
|
114
|
+
|
|
115
|
+
Cached history vectors join the primary fetch; cold history uses a second stage.
|
|
116
|
+
"""
|
|
110
117
|
time_start = time.perf_counter_ns()
|
|
111
118
|
if not streams or (not stream_features and not user_features):
|
|
112
119
|
return FeatureRetrievalMeta(
|
|
@@ -119,24 +126,13 @@ async def set_all_features(
|
|
|
119
126
|
dynamo_ms=0,
|
|
120
127
|
parsing_ms=0,
|
|
121
128
|
)
|
|
122
|
-
cache_miss: Dict[str, Dict[str, Any]] = {}
|
|
123
|
-
|
|
129
|
+
cache_miss: Dict[str, List[Dict[str, Any]]] = {}
|
|
130
|
+
history_features = history["stream_features"] if history else []
|
|
131
|
+
if history_streams is None:
|
|
132
|
+
history_streams = []
|
|
133
|
+
all_feature_keys = [*stream_features, *user_features, *history_features]
|
|
124
134
|
cache_delay_obj: dict[str, float] = {f: 0 for f in all_feature_keys}
|
|
125
135
|
now = datetime.datetime.utcnow()
|
|
126
|
-
for f in stream_features:
|
|
127
|
-
for s in streams:
|
|
128
|
-
cache_miss, cache_delay_obj = _check_cache(
|
|
129
|
-
obj=s,
|
|
130
|
-
id_type="STREAM",
|
|
131
|
-
id_key=s["streamUrl"],
|
|
132
|
-
feature_key=f,
|
|
133
|
-
cache_sep=cache_sep,
|
|
134
|
-
features_cache=stream_features_cache,
|
|
135
|
-
cache_miss=cache_miss,
|
|
136
|
-
cache_delay=cache_delay_obj,
|
|
137
|
-
now=now,
|
|
138
|
-
)
|
|
139
|
-
stream_cache_misses = len(cache_miss)
|
|
140
136
|
for f in user_features:
|
|
141
137
|
cache_miss, cache_delay_obj = _check_cache(
|
|
142
138
|
obj=user,
|
|
@@ -149,7 +145,26 @@ async def set_all_features(
|
|
|
149
145
|
cache_delay=cache_delay_obj,
|
|
150
146
|
now=now,
|
|
151
147
|
)
|
|
152
|
-
user_cache_misses = len(cache_miss)
|
|
148
|
+
user_cache_misses = len(cache_miss)
|
|
149
|
+
cached_history = user.get(history["user_feature"]) if history else None
|
|
150
|
+
if history:
|
|
151
|
+
history_streams[:] = [{"streamUrl": url} for url in history_stream_urls(cached_history)]
|
|
152
|
+
# A cached history reveals its vector keys before the first parallel fetch.
|
|
153
|
+
for targets, features in ((streams, stream_features), (history_streams, history_features)):
|
|
154
|
+
for f in features:
|
|
155
|
+
for s in targets:
|
|
156
|
+
cache_miss, cache_delay_obj = _check_cache(
|
|
157
|
+
obj=s,
|
|
158
|
+
id_type="STREAM",
|
|
159
|
+
id_key=s["streamUrl"],
|
|
160
|
+
feature_key=f,
|
|
161
|
+
cache_sep=cache_sep,
|
|
162
|
+
features_cache=stream_features_cache,
|
|
163
|
+
cache_miss=cache_miss,
|
|
164
|
+
cache_delay=cache_delay_obj,
|
|
165
|
+
now=now,
|
|
166
|
+
)
|
|
167
|
+
stream_cache_misses = len(cache_miss) - user_cache_misses
|
|
153
168
|
valid_cache_delays = list(v for v in cache_delay_obj.values() if v > 0)
|
|
154
169
|
cache_delay = min(valid_cache_delays) if valid_cache_delays else 0
|
|
155
170
|
if not cache_miss:
|
|
@@ -198,7 +213,7 @@ async def set_all_features(
|
|
|
198
213
|
updated_keys = set()
|
|
199
214
|
for item in items:
|
|
200
215
|
full_id = item["pk"]["S"]
|
|
201
|
-
id_type, id_key = full_id.split("#")
|
|
216
|
+
id_type, id_key = full_id.split("#", 1)
|
|
202
217
|
feature_name = item["sk"]["S"]
|
|
203
218
|
if id_type == "STREAM":
|
|
204
219
|
cache_to_use = stream_features_cache
|
|
@@ -242,7 +257,8 @@ async def set_all_features(
|
|
|
242
257
|
}
|
|
243
258
|
|
|
244
259
|
if cache_key in cache_miss:
|
|
245
|
-
cache_miss[cache_key]
|
|
260
|
+
for obj in cache_miss[cache_key]:
|
|
261
|
+
obj[feature_name] = value
|
|
246
262
|
updated_keys.add(cache_key)
|
|
247
263
|
parsing_end = time.perf_counter_ns()
|
|
248
264
|
# Save keys that were not found in DynamoDB with None value
|
|
@@ -258,7 +274,7 @@ async def set_all_features(
|
|
|
258
274
|
"cache_ttl_in_seconds": 6 * 3600,
|
|
259
275
|
}
|
|
260
276
|
end_time = time.perf_counter_ns()
|
|
261
|
-
|
|
277
|
+
meta = FeatureRetrievalMeta(
|
|
262
278
|
cache_misses=user_cache_misses + stream_cache_misses,
|
|
263
279
|
user_cache_misses=user_cache_misses,
|
|
264
280
|
stream_cache_misses=stream_cache_misses,
|
|
@@ -268,6 +284,26 @@ async def set_all_features(
|
|
|
268
284
|
dynamo_ms=_perf_counter_ns_delta_in_ms(dynamo_start, dynamo_end),
|
|
269
285
|
parsing_ms=_perf_counter_ns_delta_in_ms(dynamo_end, parsing_end),
|
|
270
286
|
)
|
|
287
|
+
if history and cached_history is None:
|
|
288
|
+
record = user.get(history["user_feature"])
|
|
289
|
+
history_streams[:] = [{"streamUrl": url} for url in history_stream_urls(record)]
|
|
290
|
+
# An uncached history needs a second stage; its batches are still parallel.
|
|
291
|
+
history_meta = await set_all_features(
|
|
292
|
+
user=user, streams=history_streams, stream_features=history_features,
|
|
293
|
+
user_features=[], stream_features_cache=stream_features_cache,
|
|
294
|
+
user_features_cache=user_features_cache, features_table=features_table,
|
|
295
|
+
cache_sep=cache_sep, dynamo_client=dynamo_client,
|
|
296
|
+
)
|
|
297
|
+
meta = meta._replace(
|
|
298
|
+
cache_misses=meta.cache_misses + history_meta.cache_misses,
|
|
299
|
+
stream_cache_misses=meta.stream_cache_misses + history_meta.stream_cache_misses,
|
|
300
|
+
retrieval_ms=_perf_counter_ns_delta_in_ms(time_start, time.perf_counter_ns()),
|
|
301
|
+
success=meta.success and history_meta.success,
|
|
302
|
+
cache_delay_minutes=max(meta.cache_delay_minutes, history_meta.cache_delay_minutes),
|
|
303
|
+
dynamo_ms=meta.dynamo_ms + history_meta.dynamo_ms,
|
|
304
|
+
parsing_ms=meta.parsing_ms + history_meta.parsing_ms,
|
|
305
|
+
)
|
|
306
|
+
return meta
|
|
271
307
|
|
|
272
308
|
|
|
273
309
|
_MOBILE_OS = {"ios", "android", "iphone", "galaxy"}
|
|
@@ -296,7 +332,7 @@ async def create_channel_candidates(
|
|
|
296
332
|
os_cat = _get_os_cat(user.get("clientOs", ""))
|
|
297
333
|
|
|
298
334
|
global_holder: Dict[str, Any] = {}
|
|
299
|
-
cache_miss: Dict[str, Dict[str, Any]] = {}
|
|
335
|
+
cache_miss: Dict[str, List[Dict[str, Any]]] = {}
|
|
300
336
|
all_feature_keys = [*global_features, *user_features]
|
|
301
337
|
cache_delay_obj: dict[str, float] = {f: 0 for f in all_feature_keys}
|
|
302
338
|
now = datetime.datetime.utcnow()
|
|
@@ -437,7 +473,8 @@ async def create_channel_candidates(
|
|
|
437
473
|
}
|
|
438
474
|
|
|
439
475
|
if cache_key in cache_miss:
|
|
440
|
-
cache_miss[cache_key]
|
|
476
|
+
for obj in cache_miss[cache_key]:
|
|
477
|
+
obj[feature_name] = value
|
|
441
478
|
updated_keys.add(cache_key)
|
|
442
479
|
|
|
443
480
|
parsing_end = time.perf_counter_ns()
|
|
@@ -594,7 +631,7 @@ def _check_cache(
|
|
|
594
631
|
(now - cached["inserted_at"]).total_seconds(),
|
|
595
632
|
)
|
|
596
633
|
else:
|
|
597
|
-
cache_miss[
|
|
634
|
+
cache_miss.setdefault(key, []).append(obj)
|
|
598
635
|
return cache_miss, cache_delay
|
|
599
636
|
|
|
600
637
|
|
|
@@ -24,7 +24,7 @@ _sym_db = _symbol_database.Default()
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0e\x66\x65\x61tures.proto\x12\x1ahaystack_ml_stack.features\"7\n\x12\x45ntryContextCounts\x12\x10\n\x08\x61ttempts\x18\x01 \x01(\x05\x12\x0f\n\x07watched\x18\x02 \x01(\x05\"_\n\x0cSelectCounts\x12\x15\n\rtotal_selects\x18\x01 \x01(\x05\x12!\n\x19total_selects_and_watched\x18\x02 \x01(\x05\x12\x15\n\rtotal_browsed\x18\x03 \x01(\x05\"\xf3\x02\n\x14\x45ntryContextPWatched\x12@\n\x08\x61utoplay\x18\x01 \x01(\x0b\x32..haystack_ml_stack.features.EntryContextCounts\x12\x41\n\tsel_thumb\x18\x02 \x01(\x0b\x32..haystack_ml_stack.features.EntryContextCounts\x12\x43\n\x0b\x63hoose_next\x18\x03 \x01(\x0b\x32..haystack_ml_stack.features.EntryContextCounts\x12@\n\x08\x63h_swtch\x18\x04 \x01(\x0b\x32..haystack_ml_stack.features.EntryContextCounts\x12O\n\x17launch_first_in_session\x18\x05 \x01(\x0b\x32..haystack_ml_stack.features.EntryContextCounts\"\x85\x02\n\x0fPositionPSelect\x12;\n\tfirst_pos\x18\x01 \x01(\x0b\x32(.haystack_ml_stack.features.SelectCounts\x12<\n\nsecond_pos\x18\x02 \x01(\x0b\x32(.haystack_ml_stack.features.SelectCounts\x12;\n\tthird_pos\x18\x03 \x01(\x0b\x32(.haystack_ml_stack.features.SelectCounts\x12:\n\x08rest_pos\x18\x04 \x01(\x0b\x32(.haystack_ml_stack.features.SelectCounts\"\xa9\x01\n\x1f\x42rowsedDebiasedPositionPSelects\x12\x44\n\x0fup_to_4_browsed\x18\x01 \x01(\x0b\x32+.haystack_ml_stack.features.PositionPSelect\x12@\n\x0b\x61ll_browsed\x18\x02 \x01(\x0b\x32+.haystack_ml_stack.features.PositionPSelect\"\xb8\x01\n\x16PlaylistStatsForGlobal\x12\x15\n\rwatched_count\x18\x01 \x01(\x05\x12\x19\n\x11not_watched_count\x18\x02 \x01(\x05\x12\x1b\n\x13\x63\x61pped_watched_secs\x18\x03 \x01(\x02\x12\x1f\n\x17\x63\x61pped_not_watched_secs\x18\x04 \x01(\x02\x12\x14\n\x0cwatched_secs\x18\x05 \x01(\x02\x12\x18\n\x10not_watched_secs\x18\x06 \x01(\x02\"\xc5\x01\n\x14PlaylistStatsForUser\x12\x12\n\ntotal_days\x18\x01 \x01(\x05\x12\x12\n\nstart_days\x18\x02 \x01(\x05\x12\x13\n\x0b\x61\x63tive_days\x18\x03 \x01(\x05\x12\x15\n\rtotal_watched\x18\x04 \x01(\x02\x12\x1c\n\x14\x63\x61pped_total_watched\x18\x05 \x01(\x02\x12 \n\x18\x63\x61pped_total_log_watched\x18\x06 \x01(\x02\x12\x19\n\x11\x66irst_active_date\x18\x07 \x01(\t\"^\n\x07\x43hannel\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x16\n\x0e\x63\x61tegory_group\x18\x02 \x01(\t\x12\x12\n\nstart_date\x18\x03 \x01(\x05\x12\x19\n\x11required_features\x18\x04 \x03(\t\"k\n\rStreamPSelect\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12I\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32;.haystack_ml_stack.features.BrowsedDebiasedPositionPSelects\"a\n\x0eStreamPWatched\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12>\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x30.haystack_ml_stack.features.EntryContextPWatched\"_\n\x0cUserPWatched\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12>\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x30.haystack_ml_stack.features.EntryContextPWatched\"\xda\x01\n\x19UserPersonalizingPWatched\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12M\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32?.haystack_ml_stack.features.UserPersonalizingPWatched.DataEntry\x1a]\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12?\n\x05value\x18\x02 \x01(\x0b\x32\x30.haystack_ml_stack.features.EntryContextPWatched:\x02\x38\x01\"i\n\x0bUserPSelect\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12I\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32;.haystack_ml_stack.features.BrowsedDebiasedPositionPSelects\"\xe3\x01\n\x18UserPersonalizingPSelect\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12L\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32>.haystack_ml_stack.features.UserPersonalizingPSelect.DataEntry\x1ah\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12J\n\x05value\x18\x02 \x01(\x0b\x32;.haystack_ml_stack.features.BrowsedDebiasedPositionPSelects:\x02\x38\x01\"\xa2\x01\n\x16StreamSimilarityScores\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12J\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32<.haystack_ml_stack.features.StreamSimilarityScores.DataEntry\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x01:\x02\x38\x01\"\xd0\x01\n\x13GlobalPlaylistStats\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12G\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32\x39.haystack_ml_stack.features.GlobalPlaylistStats.DataEntry\x1a_\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x41\n\x05value\x18\x02 \x01(\x0b\x32\x32.haystack_ml_stack.features.PlaylistStatsForGlobal:\x02\x38\x01\"\xca\x01\n\x11UserPlaylistStats\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x45\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32\x37.haystack_ml_stack.features.UserPlaylistStats.DataEntry\x1a]\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12?\n\x05value\x18\x02 \x01(\x0b\x32\x30.haystack_ml_stack.features.PlaylistStatsForUser:\x02\x38\x01\"T\n\x0eGlobalChannels\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x31\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32#.haystack_ml_stack.features.
|
|
27
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0e\x66\x65\x61tures.proto\x12\x1ahaystack_ml_stack.features\"7\n\x12\x45ntryContextCounts\x12\x10\n\x08\x61ttempts\x18\x01 \x01(\x05\x12\x0f\n\x07watched\x18\x02 \x01(\x05\"_\n\x0cSelectCounts\x12\x15\n\rtotal_selects\x18\x01 \x01(\x05\x12!\n\x19total_selects_and_watched\x18\x02 \x01(\x05\x12\x15\n\rtotal_browsed\x18\x03 \x01(\x05\"\xf3\x02\n\x14\x45ntryContextPWatched\x12@\n\x08\x61utoplay\x18\x01 \x01(\x0b\x32..haystack_ml_stack.features.EntryContextCounts\x12\x41\n\tsel_thumb\x18\x02 \x01(\x0b\x32..haystack_ml_stack.features.EntryContextCounts\x12\x43\n\x0b\x63hoose_next\x18\x03 \x01(\x0b\x32..haystack_ml_stack.features.EntryContextCounts\x12@\n\x08\x63h_swtch\x18\x04 \x01(\x0b\x32..haystack_ml_stack.features.EntryContextCounts\x12O\n\x17launch_first_in_session\x18\x05 \x01(\x0b\x32..haystack_ml_stack.features.EntryContextCounts\"\x85\x02\n\x0fPositionPSelect\x12;\n\tfirst_pos\x18\x01 \x01(\x0b\x32(.haystack_ml_stack.features.SelectCounts\x12<\n\nsecond_pos\x18\x02 \x01(\x0b\x32(.haystack_ml_stack.features.SelectCounts\x12;\n\tthird_pos\x18\x03 \x01(\x0b\x32(.haystack_ml_stack.features.SelectCounts\x12:\n\x08rest_pos\x18\x04 \x01(\x0b\x32(.haystack_ml_stack.features.SelectCounts\"\xa9\x01\n\x1f\x42rowsedDebiasedPositionPSelects\x12\x44\n\x0fup_to_4_browsed\x18\x01 \x01(\x0b\x32+.haystack_ml_stack.features.PositionPSelect\x12@\n\x0b\x61ll_browsed\x18\x02 \x01(\x0b\x32+.haystack_ml_stack.features.PositionPSelect\"\xb8\x01\n\x16PlaylistStatsForGlobal\x12\x15\n\rwatched_count\x18\x01 \x01(\x05\x12\x19\n\x11not_watched_count\x18\x02 \x01(\x05\x12\x1b\n\x13\x63\x61pped_watched_secs\x18\x03 \x01(\x02\x12\x1f\n\x17\x63\x61pped_not_watched_secs\x18\x04 \x01(\x02\x12\x14\n\x0cwatched_secs\x18\x05 \x01(\x02\x12\x18\n\x10not_watched_secs\x18\x06 \x01(\x02\"\xc5\x01\n\x14PlaylistStatsForUser\x12\x12\n\ntotal_days\x18\x01 \x01(\x05\x12\x12\n\nstart_days\x18\x02 \x01(\x05\x12\x13\n\x0b\x61\x63tive_days\x18\x03 \x01(\x05\x12\x15\n\rtotal_watched\x18\x04 \x01(\x02\x12\x1c\n\x14\x63\x61pped_total_watched\x18\x05 \x01(\x02\x12 \n\x18\x63\x61pped_total_log_watched\x18\x06 \x01(\x02\x12\x19\n\x11\x66irst_active_date\x18\x07 \x01(\t\"^\n\x07\x43hannel\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x16\n\x0e\x63\x61tegory_group\x18\x02 \x01(\t\x12\x12\n\nstart_date\x18\x03 \x01(\x05\x12\x19\n\x11required_features\x18\x04 \x03(\t\"k\n\rStreamPSelect\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12I\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32;.haystack_ml_stack.features.BrowsedDebiasedPositionPSelects\"a\n\x0eStreamPWatched\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12>\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x30.haystack_ml_stack.features.EntryContextPWatched\"_\n\x0cUserPWatched\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12>\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x30.haystack_ml_stack.features.EntryContextPWatched\"\xda\x01\n\x19UserPersonalizingPWatched\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12M\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32?.haystack_ml_stack.features.UserPersonalizingPWatched.DataEntry\x1a]\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12?\n\x05value\x18\x02 \x01(\x0b\x32\x30.haystack_ml_stack.features.EntryContextPWatched:\x02\x38\x01\"i\n\x0bUserPSelect\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12I\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32;.haystack_ml_stack.features.BrowsedDebiasedPositionPSelects\"\xe3\x01\n\x18UserPersonalizingPSelect\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12L\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32>.haystack_ml_stack.features.UserPersonalizingPSelect.DataEntry\x1ah\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12J\n\x05value\x18\x02 \x01(\x0b\x32;.haystack_ml_stack.features.BrowsedDebiasedPositionPSelects:\x02\x38\x01\"\xa2\x01\n\x16StreamSimilarityScores\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12J\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32<.haystack_ml_stack.features.StreamSimilarityScores.DataEntry\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x01:\x02\x38\x01\"\xd0\x01\n\x13GlobalPlaylistStats\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12G\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32\x39.haystack_ml_stack.features.GlobalPlaylistStats.DataEntry\x1a_\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x41\n\x05value\x18\x02 \x01(\x0b\x32\x32.haystack_ml_stack.features.PlaylistStatsForGlobal:\x02\x38\x01\"\xca\x01\n\x11UserPlaylistStats\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x45\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32\x37.haystack_ml_stack.features.UserPlaylistStats.DataEntry\x1a]\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12?\n\x05value\x18\x02 \x01(\x0b\x32\x30.haystack_ml_stack.features.PlaylistStatsForUser:\x02\x38\x01\"T\n\x0eGlobalChannels\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x31\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32#.haystack_ml_stack.features.Channel\"0\n\x0fStreamEmbedding\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"\x83\x01\n\rHistoryStates\x12\x12\n\nstream_ids\x18\x01 \x03(\t\x12\x14\n\x0c\x65vent_age_ms\x18\x02 \x03(\x04\x12\x17\n\x0fseconds_watched\x18\x03 \x03(\x02\x12\x17\n\x0f\x63ontext_browsed\x18\x04 \x01(\x0c\x12\x16\n\x0esnapshot_at_ms\x18\x06 \x01(\x03\"]\n\x0bUserHistory\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x37\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32).haystack_ml_stack.features.HistoryStatesJ\x04\x08\x03\x10\x04\x62\x06proto3')
|
|
28
28
|
|
|
29
29
|
_globals = globals()
|
|
30
30
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
@@ -87,4 +87,10 @@ if not _descriptor._USE_C_DESCRIPTORS:
|
|
|
87
87
|
_globals['_USERPLAYLISTSTATS_DATAENTRY']._serialized_end=2935
|
|
88
88
|
_globals['_GLOBALCHANNELS']._serialized_start=2937
|
|
89
89
|
_globals['_GLOBALCHANNELS']._serialized_end=3021
|
|
90
|
+
_globals['_STREAMEMBEDDING']._serialized_start=3023
|
|
91
|
+
_globals['_STREAMEMBEDDING']._serialized_end=3071
|
|
92
|
+
_globals['_HISTORYSTATES']._serialized_start=3074
|
|
93
|
+
_globals['_HISTORYSTATES']._serialized_end=3205
|
|
94
|
+
_globals['_USERHISTORY']._serialized_start=3207
|
|
95
|
+
_globals['_USERHISTORY']._serialized_end=3300
|
|
90
96
|
# @@protoc_insertion_point(module_scope)
|
|
@@ -218,3 +218,33 @@ class GlobalChannels(_message.Message):
|
|
|
218
218
|
version: int
|
|
219
219
|
data: _containers.RepeatedCompositeFieldContainer[Channel]
|
|
220
220
|
def __init__(self, version: _Optional[int] = ..., data: _Optional[_Iterable[_Union[Channel, _Mapping]]] = ...) -> None: ...
|
|
221
|
+
|
|
222
|
+
class StreamEmbedding(_message.Message):
|
|
223
|
+
__slots__ = ()
|
|
224
|
+
VERSION_FIELD_NUMBER: _ClassVar[int]
|
|
225
|
+
DATA_FIELD_NUMBER: _ClassVar[int]
|
|
226
|
+
version: int
|
|
227
|
+
data: bytes
|
|
228
|
+
def __init__(self, version: _Optional[int] = ..., data: _Optional[bytes] = ...) -> None: ...
|
|
229
|
+
|
|
230
|
+
class HistoryStates(_message.Message):
|
|
231
|
+
__slots__ = ()
|
|
232
|
+
STREAM_IDS_FIELD_NUMBER: _ClassVar[int]
|
|
233
|
+
EVENT_AGE_MS_FIELD_NUMBER: _ClassVar[int]
|
|
234
|
+
SECONDS_WATCHED_FIELD_NUMBER: _ClassVar[int]
|
|
235
|
+
CONTEXT_BROWSED_FIELD_NUMBER: _ClassVar[int]
|
|
236
|
+
SNAPSHOT_AT_MS_FIELD_NUMBER: _ClassVar[int]
|
|
237
|
+
stream_ids: _containers.RepeatedScalarFieldContainer[str]
|
|
238
|
+
event_age_ms: _containers.RepeatedScalarFieldContainer[int]
|
|
239
|
+
seconds_watched: _containers.RepeatedScalarFieldContainer[float]
|
|
240
|
+
context_browsed: bytes
|
|
241
|
+
snapshot_at_ms: int
|
|
242
|
+
def __init__(self, stream_ids: _Optional[_Iterable[str]] = ..., event_age_ms: _Optional[_Iterable[int]] = ..., seconds_watched: _Optional[_Iterable[float]] = ..., context_browsed: _Optional[bytes] = ..., snapshot_at_ms: _Optional[int] = ...) -> None: ...
|
|
243
|
+
|
|
244
|
+
class UserHistory(_message.Message):
|
|
245
|
+
__slots__ = ()
|
|
246
|
+
VERSION_FIELD_NUMBER: _ClassVar[int]
|
|
247
|
+
DATA_FIELD_NUMBER: _ClassVar[int]
|
|
248
|
+
version: int
|
|
249
|
+
data: HistoryStates
|
|
250
|
+
def __init__(self, version: _Optional[int] = ..., data: _Optional[_Union[HistoryStates, _Mapping]] = ...) -> None: ...
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Compact history v1: shared URL and state decoding for serving and replay."""
|
|
2
|
+
|
|
3
|
+
STREAM_URL_PREFIX = "http://haystack.tv/id/"
|
|
4
|
+
# Wire codes are append-only. Missing and unsupported contexts are distinct.
|
|
5
|
+
CONTEXTS = ("", "__unsupported__", "autoplay", "sel thumb", "pres nxt",
|
|
6
|
+
"swpe nxt", "ch swtch", "launch", "pl refrsh")
|
|
7
|
+
CONTEXT_CODES = {context: code for code, context in enumerate(CONTEXTS)}
|
|
8
|
+
CONTEXT_CODES.update({"null": 0, "__missing__": 0})
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def history_stream_urls(record):
|
|
12
|
+
"""Rebuild exact vector keys; ':' explicitly escapes a full URL."""
|
|
13
|
+
if record is None:
|
|
14
|
+
return []
|
|
15
|
+
return [value[1:] if value.startswith(":") else STREAM_URL_PREFIX + value
|
|
16
|
+
for value in record.data.stream_ids]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def history_entries(record):
|
|
20
|
+
"""Yield URL, timestamp ms, seconds, context, browsed, watched (over 25 s, not browsed)."""
|
|
21
|
+
if record is None:
|
|
22
|
+
return
|
|
23
|
+
data = record.data
|
|
24
|
+
count = len(data.stream_ids)
|
|
25
|
+
if not (record.version == 1 and count <= 256
|
|
26
|
+
and len(data.event_age_ms) == len(data.seconds_watched)
|
|
27
|
+
== len(data.context_browsed) == count):
|
|
28
|
+
raise ValueError("Invalid compact history version or column lengths")
|
|
29
|
+
for url, age, seconds, flags in zip(
|
|
30
|
+
history_stream_urls(record), data.event_age_ms,
|
|
31
|
+
data.seconds_watched, data.context_browsed,
|
|
32
|
+
):
|
|
33
|
+
context = flags & 127
|
|
34
|
+
if context >= len(CONTEXTS):
|
|
35
|
+
raise ValueError("Unknown compact history context code")
|
|
36
|
+
browsed = bool(flags & 128)
|
|
37
|
+
yield (url, data.snapshot_at_ms - age, seconds, CONTEXTS[context],
|
|
38
|
+
browsed, seconds > 25 and not browsed)
|
|
File without changes
|
{haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: haystack-ml-stack
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.13
|
|
4
4
|
Summary: Functions related to Haystack ML
|
|
5
5
|
Author-email: Oscar Vega <oscar@haystack.tv>
|
|
6
6
|
License: MIT
|
|
@@ -8,6 +8,7 @@ Requires-Python: >=3.11
|
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Requires-Dist: protobuf==6.33.2
|
|
10
10
|
Requires-Dist: orjson==3.11.7
|
|
11
|
+
Requires-Dist: numpy>=1.26
|
|
11
12
|
Provides-Extra: server
|
|
12
13
|
Requires-Dist: pydantic==2.5.0; extra == "server"
|
|
13
14
|
Requires-Dist: cachetools==5.5.2; extra == "server"
|
{haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack.egg-info/SOURCES.txt
RENAMED
|
@@ -8,10 +8,9 @@ src/haystack_ml_stack/app.py
|
|
|
8
8
|
src/haystack_ml_stack/cache.py
|
|
9
9
|
src/haystack_ml_stack/dynamo.py
|
|
10
10
|
src/haystack_ml_stack/exceptions.py
|
|
11
|
+
src/haystack_ml_stack/history.py
|
|
11
12
|
src/haystack_ml_stack/model_store.py
|
|
12
13
|
src/haystack_ml_stack/settings.py
|
|
13
|
-
src/haystack_ml_stack/tmp.py
|
|
14
|
-
src/haystack_ml_stack/tmp2.py
|
|
15
14
|
src/haystack_ml_stack/utils.py
|
|
16
15
|
src/haystack_ml_stack.egg-info/PKG-INFO
|
|
17
16
|
src/haystack_ml_stack.egg-info/SOURCES.txt
|
|
@@ -22,5 +21,6 @@ src/haystack_ml_stack/generated/__init__.py
|
|
|
22
21
|
src/haystack_ml_stack/generated/v1/__init__.py
|
|
23
22
|
src/haystack_ml_stack/generated/v1/features_pb2.py
|
|
24
23
|
src/haystack_ml_stack/generated/v1/features_pb2.pyi
|
|
24
|
+
tests/test_history_fetch.py
|
|
25
25
|
tests/test_serializers.py
|
|
26
26
|
tests/test_utils.py
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import base64
|
|
3
|
+
import datetime
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
import aiobotocore.session
|
|
7
|
+
import newrelic.agent
|
|
8
|
+
import pytest
|
|
9
|
+
from fastapi import BackgroundTasks, Response
|
|
10
|
+
from starlette.requests import Request
|
|
11
|
+
|
|
12
|
+
from haystack_ml_stack import FeatureRegistryId, SerializerRegistry
|
|
13
|
+
from haystack_ml_stack.app import create_app
|
|
14
|
+
from haystack_ml_stack.cache import make_features_cache
|
|
15
|
+
from haystack_ml_stack.dynamo import _build_cache_key, set_all_features
|
|
16
|
+
from haystack_ml_stack._kafka import default_serialization
|
|
17
|
+
from haystack_ml_stack.settings import Settings
|
|
18
|
+
|
|
19
|
+
EMBEDDING = "EMBEDDING#GEMINI_2_TITLE_TRANSCRIPT#512"
|
|
20
|
+
HISTORY = "HISTORY#5M#256"
|
|
21
|
+
CANDIDATE = "http://haystack.tv/id/candidate"
|
|
22
|
+
WATCHED = "https://example.com/watch,video#clip"
|
|
23
|
+
NO_VECTOR = "http://haystack.tv/id/no-vector"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FakeDynamo:
|
|
27
|
+
"""batch_get_item over in-memory items."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, items=()):
|
|
30
|
+
self.items = {(i["pk"]["S"], i["sk"]["S"]): i for i in items}
|
|
31
|
+
self.calls = 0
|
|
32
|
+
self.batches = []
|
|
33
|
+
self.active = 0
|
|
34
|
+
self.max_active = 0
|
|
35
|
+
|
|
36
|
+
async def batch_get_item(self, RequestItems):
|
|
37
|
+
self.calls += 1
|
|
38
|
+
((table, request),) = RequestItems.items()
|
|
39
|
+
keys = [(k["pk"]["S"], k["sk"]["S"]) for k in request["Keys"]]
|
|
40
|
+
self.batches.append(keys)
|
|
41
|
+
self.active += 1
|
|
42
|
+
self.max_active = max(self.max_active, self.active)
|
|
43
|
+
try:
|
|
44
|
+
await asyncio.sleep(0)
|
|
45
|
+
return {"Responses": {table: [self.items[k] for k in keys if k in self.items]}}
|
|
46
|
+
finally:
|
|
47
|
+
self.active -= 1
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class FakeSession:
|
|
51
|
+
def __init__(self, client):
|
|
52
|
+
self.client = client
|
|
53
|
+
|
|
54
|
+
def create_client(self, *args, **kwargs):
|
|
55
|
+
client = self.client
|
|
56
|
+
|
|
57
|
+
class Context:
|
|
58
|
+
async def __aenter__(self):
|
|
59
|
+
return client
|
|
60
|
+
|
|
61
|
+
async def __aexit__(self, *exc):
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
return Context()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def item(pk, sk, value):
|
|
68
|
+
return {
|
|
69
|
+
"pk": {"S": pk},
|
|
70
|
+
"sk": {"S": sk},
|
|
71
|
+
"value": {"B": value},
|
|
72
|
+
"version": {"S": "v1"},
|
|
73
|
+
"cache_ttl_in_seconds": {"N": "3600"},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.mark.parametrize("candidate_features", [[EMBEDDING], []])
|
|
78
|
+
def test_history_vectors_reach_the_model_outside_the_logged_user(candidate_features):
|
|
79
|
+
history = SerializerRegistry[FeatureRegistryId("USER", HISTORY, "v1")].serialize(
|
|
80
|
+
{
|
|
81
|
+
"version": 1,
|
|
82
|
+
"data": {
|
|
83
|
+
"snapshot_at_ms": 1_758_499_200_000,
|
|
84
|
+
"stream_urls": [WATCHED, NO_VECTOR],
|
|
85
|
+
"event_at_ms": [1_758_400_000_000, 1_758_450_000_000],
|
|
86
|
+
"seconds_watched": [120.0, 0.0],
|
|
87
|
+
"contexts": ["sel thumb", "sel thumb"],
|
|
88
|
+
"browsed": [False, True],
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
vector = SerializerRegistry[FeatureRegistryId("STREAM", EMBEDDING, "v1")].serialize(
|
|
93
|
+
{"version": 1, "data": [0.01] * 512}
|
|
94
|
+
)
|
|
95
|
+
client = FakeDynamo(
|
|
96
|
+
[
|
|
97
|
+
item("USER#u1", HISTORY, history),
|
|
98
|
+
item(f"STREAM#{CANDIDATE}", EMBEDDING, vector),
|
|
99
|
+
item(f"STREAM#{WATCHED}", EMBEDDING, vector),
|
|
100
|
+
]
|
|
101
|
+
)
|
|
102
|
+
seen = {}
|
|
103
|
+
|
|
104
|
+
def preprocess(user, streams, playlist, params):
|
|
105
|
+
seen["user"] = dict(user)
|
|
106
|
+
seen["history_streams"] = params["history_streams"]
|
|
107
|
+
return streams
|
|
108
|
+
|
|
109
|
+
model = {
|
|
110
|
+
"preprocess": preprocess,
|
|
111
|
+
"predict": lambda streams, params: {s["streamUrl"]: 0.5 for s in streams},
|
|
112
|
+
"params": {},
|
|
113
|
+
"stream_features": candidate_features,
|
|
114
|
+
"user_features": [HISTORY],
|
|
115
|
+
"history": {"user_feature": HISTORY, "stream_features": [EMBEDDING]},
|
|
116
|
+
}
|
|
117
|
+
events = []
|
|
118
|
+
original_session = aiobotocore.session.get_session
|
|
119
|
+
original_record = newrelic.agent.record_custom_event
|
|
120
|
+
aiobotocore.session.get_session = lambda: FakeSession(client)
|
|
121
|
+
newrelic.agent.record_custom_event = lambda name, meta: events.append(meta)
|
|
122
|
+
try:
|
|
123
|
+
app = create_app(Settings(), preloaded_model=model)
|
|
124
|
+
score = next(r for r in app.routes if getattr(r, "path", "") == "/score")
|
|
125
|
+
body = json.dumps({"user": {"userid": "u1"}, "streams": [{"streamUrl": CANDIDATE}]}).encode()
|
|
126
|
+
|
|
127
|
+
async def receive():
|
|
128
|
+
return {"type": "http.request", "body": body, "more_body": False}
|
|
129
|
+
|
|
130
|
+
async def call():
|
|
131
|
+
scope = {"type": "http", "method": "POST", "path": "/score",
|
|
132
|
+
"headers": [], "query_string": b""}
|
|
133
|
+
async with app.router.lifespan_context(app):
|
|
134
|
+
return await score.endpoint(Request(scope, receive), Response(), BackgroundTasks())
|
|
135
|
+
|
|
136
|
+
output = asyncio.run(call())
|
|
137
|
+
finally:
|
|
138
|
+
aiobotocore.session.get_session = original_session
|
|
139
|
+
newrelic.agent.record_custom_event = original_record
|
|
140
|
+
|
|
141
|
+
assert output == {CANDIDATE: 0.5}
|
|
142
|
+
assert [s["streamUrl"] for s in seen["history_streams"]] == [WATCHED, NO_VECTOR]
|
|
143
|
+
assert len(seen["history_streams"][0][EMBEDDING].data) == 1024 # 512 float16
|
|
144
|
+
assert EMBEDDING not in seen["history_streams"][1]
|
|
145
|
+
assert set(seen["user"]) == {"userid", HISTORY}
|
|
146
|
+
# The retrieval metrics count both reads: the candidate's vector and the history
|
|
147
|
+
# record, then the vectors of the history's two streams.
|
|
148
|
+
(event,) = events
|
|
149
|
+
expected = (4, 3, 1) if candidate_features else (3, 2, 1)
|
|
150
|
+
assert (event["cache_misses"], event["stream_cache_misses"], event["user_cache_misses"]) == expected
|
|
151
|
+
assert event["retrieval_success"] == 1
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@pytest.mark.parametrize("cache_state", ["cold", "history", "all"])
|
|
155
|
+
def test_parallel_history_fetch_and_shared_vector_assignment(cache_state):
|
|
156
|
+
import orjson
|
|
157
|
+
|
|
158
|
+
candidates = [f"http://haystack.tv/id/c{i}" for i in range(100)]
|
|
159
|
+
urls = candidates[:50] + [f"http://haystack.tv/id/h{i}" for i in range(206)]
|
|
160
|
+
history_serializer = SerializerRegistry[FeatureRegistryId("USER", HISTORY, "v1")]
|
|
161
|
+
vector_serializer = SerializerRegistry[FeatureRegistryId("STREAM", EMBEDDING, "v1")]
|
|
162
|
+
history = history_serializer.serialize({
|
|
163
|
+
"version": 1,
|
|
164
|
+
"data": {"snapshot_at_ms": 1_758_499_200_000,
|
|
165
|
+
"stream_urls": urls, "event_at_ms": [1_758_400_000_000] * 256,
|
|
166
|
+
"seconds_watched": [30.] * 256, "contexts": ["pres nxt"] * 256,
|
|
167
|
+
"browsed": [False] * 256},
|
|
168
|
+
})
|
|
169
|
+
vector = vector_serializer.serialize({"version": 1, "data": [.01] * 512})
|
|
170
|
+
rows = [item("USER#u1", HISTORY, history)] + [
|
|
171
|
+
item("STREAM#" + url, EMBEDDING, vector) for url in dict.fromkeys(candidates + urls)
|
|
172
|
+
]
|
|
173
|
+
client = FakeDynamo(rows)
|
|
174
|
+
user_cache, stream_cache = make_features_cache(1000), make_features_cache(1000)
|
|
175
|
+
def cache_value(cache, kind, entity, feature, value):
|
|
176
|
+
cache[_build_cache_key(kind, entity, feature, "--")] = {
|
|
177
|
+
"value": value, "cache_ttl_in_seconds": 3600,
|
|
178
|
+
"inserted_at": datetime.datetime.utcnow(),
|
|
179
|
+
}
|
|
180
|
+
if cache_state != "cold":
|
|
181
|
+
cache_value(user_cache, "USER", "u1", HISTORY, history_serializer.deserialize(history))
|
|
182
|
+
if cache_state == "all":
|
|
183
|
+
for url in dict.fromkeys(candidates + urls):
|
|
184
|
+
cache_value(stream_cache, "STREAM", url, EMBEDDING, vector_serializer.deserialize(vector))
|
|
185
|
+
user, streams, history_streams = {"userid": "u1"}, [{"streamUrl": u} for u in candidates], []
|
|
186
|
+
meta = asyncio.run(set_all_features(
|
|
187
|
+
user=user, streams=streams, stream_features=[EMBEDDING], user_features=[HISTORY],
|
|
188
|
+
history={"user_feature": HISTORY, "stream_features": [EMBEDDING]},
|
|
189
|
+
history_streams=history_streams, stream_features_cache=stream_cache,
|
|
190
|
+
user_features_cache=user_cache, features_table="features", cache_sep="--", dynamo_client=client,
|
|
191
|
+
))
|
|
192
|
+
assert meta.success
|
|
193
|
+
assert [s["streamUrl"] for s in history_streams] == urls
|
|
194
|
+
assert all(s[EMBEDDING].data == vector_serializer.deserialize(vector).data for s in streams + history_streams)
|
|
195
|
+
assert set(user) == {"userid", HISTORY}
|
|
196
|
+
# Both a candidate and its history reference receive the one fetched object.
|
|
197
|
+
assert streams[0][EMBEDDING] is history_streams[0][EMBEDDING]
|
|
198
|
+
batches = [len(batch) for batch in client.batches]
|
|
199
|
+
assert batches == {"cold": [100, 1, 100, 100, 6], "history": [100, 100, 100, 6], "all": []}[cache_state]
|
|
200
|
+
assert client.max_active == {"cold": 3, "history": 4, "all": 0}[cache_state]
|
|
201
|
+
keys = [key for batch in client.batches for key in batch]
|
|
202
|
+
assert len(keys) == len(set(keys)) # no repeat lookups across either stage
|
|
203
|
+
assert meta.stream_cache_misses == (0 if cache_state == "all" else 306)
|
|
204
|
+
# Exercise the real Kafka encoder; vectors of history stay outside user/streams.
|
|
205
|
+
logged = orjson.loads(orjson.dumps({"user": user, "streams": streams}, default=default_serialization))
|
|
206
|
+
assert set(logged["user"]) == {"userid", HISTORY}
|
|
207
|
+
restored = history_serializer.deserialize(base64.b64decode(logged["user"][HISTORY]["proto"]))
|
|
208
|
+
assert restored == user[HISTORY]
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_missing_history_is_cached_only_after_a_successful_read():
|
|
212
|
+
client = FakeDynamo()
|
|
213
|
+
user_cache, stream_cache = make_features_cache(10), make_features_cache(10)
|
|
214
|
+
async def run():
|
|
215
|
+
for _ in range(2):
|
|
216
|
+
history_streams = []
|
|
217
|
+
meta = await set_all_features(
|
|
218
|
+
user={"userid": "u1"}, streams=[{"streamUrl": CANDIDATE}],
|
|
219
|
+
stream_features=[], user_features=[HISTORY],
|
|
220
|
+
history={"user_feature": HISTORY, "stream_features": [EMBEDDING]},
|
|
221
|
+
history_streams=history_streams, stream_features_cache=stream_cache,
|
|
222
|
+
user_features_cache=user_cache, features_table="features", cache_sep="--", dynamo_client=client,
|
|
223
|
+
)
|
|
224
|
+
assert meta.success and history_streams == []
|
|
225
|
+
asyncio.run(run())
|
|
226
|
+
assert client.calls == 1
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import struct
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
1
6
|
from google.protobuf.json_format import ParseDict as ProtoParseDict
|
|
2
7
|
from haystack_ml_stack import SerializerRegistry
|
|
3
8
|
from haystack_ml_stack.generated.v1 import features_pb2 as features_pb2_v1
|
|
9
|
+
from haystack_ml_stack.history import history_entries, history_stream_urls
|
|
4
10
|
|
|
5
11
|
|
|
6
12
|
def test_v0_serializers():
|
|
@@ -211,3 +217,89 @@ def test_user_playlist_stats_serializer_backward_compatibility():
|
|
|
211
217
|
assert result.data["sports"].total_days == 30
|
|
212
218
|
assert result.data["sports"].capped_total_log_watched == 0.0
|
|
213
219
|
assert result.data["sports"].first_active_date == ""
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_stream_embedding_serializer():
|
|
223
|
+
values = [math.sin(0.37 * (i + 1)) for i in range(512)]
|
|
224
|
+
serializer = SerializerRegistry[
|
|
225
|
+
("STREAM", "EMBEDDING#GEMINI_2_TITLE_TRANSCRIPT#512", "v1")
|
|
226
|
+
]
|
|
227
|
+
message = serializer.deserialize(serializer.serialize({"version": 1, "data": values}))
|
|
228
|
+
vector = struct.unpack("<512e", message.data)
|
|
229
|
+
norm = math.sqrt(sum(x * x for x in values))
|
|
230
|
+
assert message.version == 1
|
|
231
|
+
assert all(abs(a - b / norm) < 1e-3 for a, b in zip(vector, values))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_stream_embedding_normalizes_before_float16_storage():
|
|
235
|
+
import numpy as np
|
|
236
|
+
values = [math.sin(0.37 * (i + 1)) * (1 + i % 7) for i in range(512)]
|
|
237
|
+
first = np.asarray(values, dtype=np.float32)
|
|
238
|
+
expected = (first / np.linalg.norm(first)).astype("<f2")
|
|
239
|
+
serializer = SerializerRegistry[
|
|
240
|
+
("STREAM", "EMBEDDING#GEMINI_2_TITLE_TRANSCRIPT#512", "v1")
|
|
241
|
+
]
|
|
242
|
+
message = serializer.deserialize(serializer.serialize({"version": 1, "data": values}))
|
|
243
|
+
assert message.data == expected.tobytes()
|
|
244
|
+
assert len(message.data) == 1024
|
|
245
|
+
rounded = first.astype(np.float16).astype(np.float32)
|
|
246
|
+
assert message.data != (rounded / np.linalg.norm(rounded)).astype("<f2").tobytes()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@pytest.mark.parametrize("count", [511, 513])
|
|
250
|
+
def test_stream_embedding_requires_512_coordinates(count):
|
|
251
|
+
serializer = SerializerRegistry[("STREAM", "EMBEDDING#GEMINI_2_TITLE_TRANSCRIPT#512", "v1")]
|
|
252
|
+
with pytest.raises(AssertionError, match="512 coordinates"):
|
|
253
|
+
serializer.serialize({"version": 1, "data": [1.] * count})
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_user_history_serializer():
|
|
257
|
+
data = {
|
|
258
|
+
"snapshot_at_ms": 1758499200000,
|
|
259
|
+
"stream_urls": ["http://haystack.tv/id/a", "https://example.com/a,b#c",
|
|
260
|
+
"http://haystack.tv/id/:escape", "relative/path", "http://haystack.tv/id/e"],
|
|
261
|
+
"event_at_ms": [1758400000001 + i for i in range(5)],
|
|
262
|
+
"seconds_watched": [25.7, 3600.0, 30.0, 0.0, 25.0],
|
|
263
|
+
"contexts": ["pres nxt", "null", "sel thumb", "unsupported", "__missing__"],
|
|
264
|
+
"browsed": [False, False, True, False, False],
|
|
265
|
+
}
|
|
266
|
+
serializer = SerializerRegistry[("USER", "HISTORY#5M#256", "v1")]
|
|
267
|
+
message = serializer.deserialize(
|
|
268
|
+
serializer.serialize({"version": 1, "data": data})
|
|
269
|
+
)
|
|
270
|
+
assert message.version == 1
|
|
271
|
+
assert message.data.snapshot_at_ms == data["snapshot_at_ms"]
|
|
272
|
+
assert list(message.data.stream_ids) == ["a", ":https://example.com/a,b#c",
|
|
273
|
+
":http://haystack.tv/id/:escape", ":relative/path", "e"]
|
|
274
|
+
assert history_stream_urls(message) == data["stream_urls"]
|
|
275
|
+
rows = list(history_entries(message))
|
|
276
|
+
assert [r[1] for r in rows] == data["event_at_ms"]
|
|
277
|
+
assert [r[2] for r in rows] == pytest.approx(data["seconds_watched"])
|
|
278
|
+
assert [r[3] for r in rows] == ["pres nxt", "", "sel thumb", "__unsupported__", ""]
|
|
279
|
+
assert [r[4] for r in rows] == data["browsed"]
|
|
280
|
+
assert [r[5] for r in rows] == [True, True, False, False, False]
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_empty_history_keeps_snapshot_inside_data():
|
|
284
|
+
serializer = SerializerRegistry[("USER", "HISTORY#5M#256", "v1")]
|
|
285
|
+
data = dict(snapshot_at_ms=1758499200000, stream_urls=[], event_at_ms=[],
|
|
286
|
+
seconds_watched=[], contexts=[], browsed=[])
|
|
287
|
+
message = serializer.deserialize(serializer.serialize({"version": 1, "data": data}))
|
|
288
|
+
assert message.data.snapshot_at_ms == data["snapshot_at_ms"]
|
|
289
|
+
assert history_stream_urls(message) == []
|
|
290
|
+
assert list(history_entries(message)) == []
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@pytest.mark.parametrize("value", [0.0, float("nan"), float("inf")])
|
|
294
|
+
def test_invalid_vector_norm_has_a_clear_error(value):
|
|
295
|
+
serializer = SerializerRegistry[("STREAM", "EMBEDDING#GEMINI_2_TITLE_TRANSCRIPT#512", "v1")]
|
|
296
|
+
with pytest.raises(ValueError, match="finite, nonzero"):
|
|
297
|
+
serializer.serialize({"version": 1, "data": [value] * 512})
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def test_history_decoder_rejects_misaligned_columns():
|
|
301
|
+
message = features_pb2_v1.UserHistory(version=1)
|
|
302
|
+
message.data.snapshot_at_ms = 1000
|
|
303
|
+
message.data.stream_ids.append("a")
|
|
304
|
+
with pytest.raises(ValueError, match="column lengths"):
|
|
305
|
+
list(history_entries(message))
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.11"
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import base64
|
|
3
|
-
|
|
4
|
-
b64 = """
|
|
5
|
-
CAESHAoEa3VzYRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa25vZRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2Z0dhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIwoLZm94NWF0bGFudGESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHd0bHYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEh4KBndjdmJ0dhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3RpdhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd3BzZBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSGwoDa2d3Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3Z2VtEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrdnVlEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3Y21oEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRraWlpEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dHhsEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3aWZyEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrb2NvEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrYnR4Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3YnJjEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dmlyEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIlCgpuZXdzbmF0aW9uEhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIcCgR3bmNuEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIkCgxjYnNtaW5uZXNvdGESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEikKEWZveDU5aW5kaWFuYXBvbGlzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3d2xwEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3c2ZhEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIhCgljYnNkZW52ZXISDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEh0KBTEzYWJjEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIeCgZrb2F0dHYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiMKC2NiY21hbml0b2JhEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrYW1yEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3bGF4Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRra2NvEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3Y2F4Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRra3R2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrZW5zEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrdG52Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3cmVnEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIeCgZrc2J3dHYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiUKDXRoZW5ld3NjZW50ZXISDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiMKCG5iY3RvZGF5EhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIcCgR3ZWh0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIbCgN3b2kSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdmeHQSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdhbmUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGttdnQSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiIKCmZveDdhdXN0aW4SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiQKCW5ld3NtYXh0dhIRbmF0aW9uYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2J0dxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIQoJY2JzYm9zdG9uEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrbGZ5Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3ZmFhEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrYXJkEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3eHl6Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrb2xvEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrdHNtEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3YXZ5Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrZ3VuEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrdHZxEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIiCgd0aGVoaWxsEhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIcCgR3ZGJqEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3eGlpEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3cHhpEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3c3ltEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3eG1pEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3YXZlEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3YXRlEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIpChFmb3gyMW5ld3Njb2xvcmFkbxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3JleBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2F0YxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSJwoPZm94MTFsb3NhbmdlbGVzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3c29jEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3emR4Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrc253Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRInCgx5YWhvb2ZpbmFuY2USEW5hdGlvbmFsX2Zhdm9yaXRlGIDCr/AFEhwKBHdjYmQSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEh4KA2NubhIRbmF0aW9uYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3RyaxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3BueBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSLwoUZGV1dHNjaGV3ZWxsZWVuZ2xpc2gSEW5hdGlvbmFsX2Zhdm9yaXRlGIDCr/AFEhwKBHdueXcSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiUKDWNic3NhY3JhbWVudG8SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdwcmkSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtud2ESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdhYmkSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHduZHUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEh4KBjFjYnNsYRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2dtYhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3JxZRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd3BpeBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3JlbRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2R2bRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2dhbBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2hzdhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2JuZxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd3FhZBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHQoFbmJjMTUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiIKCmNiY3Rvcm9udG8SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGt2dnUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiIKB2FiY25ld3MSEW5hdGlvbmFsX2Zhdm9yaXRlGIDCr/AFEhwKBHd0aWMSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHd0dnkSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHd4aXgSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdzYXcSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtpcm8SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiAKBXZldWVyEhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIcCgR3cm9jEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dHR2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3d2J0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIgCghhemZhbWlseRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSGwoDd3dsEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3aGJmEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dHZmEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIkCglibG9vbWJlcmcSEW5hdGlvbmFsX2Zhdm9yaXRlGIDCr/AFEhwKBHdibnMSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHd0a3ISDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGttYmMSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGt0dXUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiYKDmZveDRrYW5zYXNjaXR5Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3d2F5Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIqChJoYXlzdGFja192YW5jb3V2ZXISDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdka3kSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEioKD2Fzc29jaWF0ZWRwcmVzcxIRbmF0aW9uYWxfZmF2b3JpdGUYgMKv8AUSHgoGa25vcHR2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3YmtvEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3eGlhEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3aXNuEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dGFlEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3YXB0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIeCgZmb3g1ZGMSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiAKCGZveDhuZXdzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3amNsEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3aHRtEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3ZWF1Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIeCgZrZnlydHYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdib2MSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdrcmcSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdkc3USDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdvd2sSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHd0b2wSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtianISDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtjcmcSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiMKCG5ld3N5aHViEhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIcCgRrc250Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIlCgpsaXZlbm93Zm94EhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIcCgR3bmVwEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIhCgZncmF5ZGMSEW5hdGlvbmFsX2Zhdm9yaXRlGIDCr/AFEhwKBGt5dHgSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHd1c2ESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtvYWESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdkaG4SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGt0dmISDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEh0KBXdpc3R2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3bG94Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIiCgdjYmNuZXdzEhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIcCgRrZ3R2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3cHZpEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3b3d0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIdCgVraG9uMhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2VybxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2ZveBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHQoFa2N0djUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtzdHASDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtnbnMSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEh4KBm5ld3MxMhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2ZvchIObG9jYWxfZmF2b3JpdGUYgMKv8AUSGwoDbmNtEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIkCgxjYnNiYWx0aW1vcmUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdyZGUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiQKDGZveDVzYW5kaWVnbxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2FsYhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2l0bhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3RhYhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa21naBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHgoGd210d3R2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIiCgpjYnNjaGljYWdvEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dG9rEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3amJmEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3Y3BvEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3aGFzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRInCgxyZXV0ZXJzdmlkZW8SEW5hdGlvbmFsX2Zhdm9yaXRlGIDCr/AFEhwKBHdzYXYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHduZW0SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtyb24SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdjdHYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhoKDGNubi9hbWFucG91chIEc2hvdxiAwq/wBRIcCgR3ZXdzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3aGJxEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIeCgZhYmM3bnkSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEh0KBXdnbnR2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrZmRhEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRuYmM2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3Y2piEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dHdvEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIjCghldXJvbmV3cxIRbmF0aW9uYWxfZmF2b3JpdGUYgMKv8AUSHAoEd3N5chIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2NhdRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIwoLZm94MmRldHJvaXQSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdvb2QSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtjY2kSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiIKCmNic2RldHJvaXQSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtpdmkSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEh0KBXdobzEzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dGFqEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrZXljEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR0bWo0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3Z2hwEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIlCg1jYnNwaXR0c2J1cmdoEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrc3dvEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRramN0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIjCgtuYmNzYW5kaWVnbxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIwoLbmJjbmVicmFza2ESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiMKC2NiY2VkbW9udG9uEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrdHZpEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3ZGFtEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3Z3J6Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIlCg1mb3g2bWlsd2F1a2VlEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3ZmZmEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3bWJiEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dmVjEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3bG5zEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrdGFsEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrbnh2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRranJoEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRInCg9jYnNzYW5mcmFuY2lzY28SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdtZ3QSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiMKC2ZveGNhcm9saW5hEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIfCgdmb3gxMHR2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrcmlzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrcHR2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3ZXNoEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRraW5nEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dnRtEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3eHh2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3bWFyEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrc2Z5Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3bWJkEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIgCghjYnNtaWFtaRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoIcG9saXRpY3MSCmhzX2NoYW5uZWwYxIba8AUSHAoEa2FsYhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSJAoMZm94MzVvcmxhbmRvEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrdHhsEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIkCglhYmNuZXdzMjQSEW5hdGlvbmFsX2Zhdm9yaXRlGIDCr/AFEhwKBHdsa3kSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdjYmkSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtpZHkSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtlbG8SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEicKD3dlc3Rlcm5tYXNzbmV3cxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3R2eBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSJAoMY2JzNnBvcnRsYW5kEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3bmN0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIeCgZ3aXZidHYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiIKCmNiY2NhbGdhcnkSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtldm4SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGFiYzcSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtsdHYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdpbHgSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdzcGESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdoaW8SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdtYmYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdsZXgSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGt3cWMSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdnYmESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiQKDGZveDMyY2hpY2FnbxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIgoKY2JzbmV3eW9yaxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2p6eRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3VschIObG9jYWxfZmF2b3JpdGUYgMKv8AUSGwoDa3NsEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dGVuEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3ZnRzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dHZtEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dmx0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrZm1iEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrYXJrEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3YnJlEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIiCgdjb3VydHR2EhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIcCgR3YmFsEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRImChJ3aWxkZmlyZS1uZXdzLTIwMjYSCmhzX2NoYW5uZWwYmJTP0wYSHAoEd3BiZhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd3JibBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2FpdBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3htYRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2hudBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3NlZRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd3NtdhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3hhbhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2R0bhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2ZzYhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIQoGY25ldHR2EhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIdCgV3bWN0dhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSGwoDa29iEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3a3l0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dHZyEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRreGlpEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrc2xhEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dGhyEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3bHd0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3YnR2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrZnZzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dnVlEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dm5zEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrdGxhEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3aWJ3Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrd2NoEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIrChBhbGphemVlcmFlbmdsaXNoEhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIjCgthYmM3Y2hpY2FnbxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIwoLZm94MzFkZW52ZXISDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdsdHgSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdhZmYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdwbXQSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdiaXISDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHd0dmQSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHd0cmYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGthcmUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtmc24SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdjaWESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGttdHYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdwdHYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdtdXISDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdzYXoSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiQKCW55cHNwb3J0cxIRbmF0aW9uYWxfZmF2b3JpdGUYgMKv8AUSJgoObmV3czJuYXNodmlsbGUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtmZHgSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEh4KBmtldHZ0dhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSKQoRZm94MjlwaGlsYWRlbHBoaWESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEh4KBmNic2RmdxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2NlbhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIAoIZm94NG5ld3MSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdjbmMSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtzdHUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGt4eHYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdiYXkSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGttaWQSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiQKEGlyYW4tY3Jpc2lzLW5ld3MSCmhzX2NoYW5uZWwY6LuLzQYSHAoEa3NkaxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2ZydhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2hlYxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2pldBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2JtdBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2hicxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2ZzbRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3R0YxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSJwoPZm94OW1pbm5lYXBvbGlzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3enptEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIkCgxmb3gxMHBob2VuaXgSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdkaW8SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiQKDGZveDI2aG91c3RvbhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3d0eBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIwoLY2JjbW9udHJlYWwSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdyaWMSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtobmwSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdib3kSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiAKCGFiYzduZXdzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3cmR3Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3bGJ0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIiCgpxMTNzZWF0dGxlEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIdCgVjYnM0NhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIwoIYW1hemVsYWISEW5hdGlvbmFsX2Zhdm9yaXRlGIDCr/AFEhwKBHdwdHoSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiUKDWZveDEzdGFtcGFiYXkSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiIKB25iY25ld3MSEW5hdGlvbmFsX2Zhdm9yaXRlGIDCr/AFEhwKBHd0b2MSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhsKA2tocRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2dldBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3RodhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd21hehIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3l0dhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa21vdhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2ZteRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIgoHaTI0bmV3cxIRbmF0aW9uYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2VjdBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2p0dhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd3FyZhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3lsZRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSKAoQY2hhbm5lbDhsYXN2ZWdhcxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd3dueRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3BheBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2RlZhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSKgoPZnJhbmNlMjRlbmdsaXNoEhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIcCgRrc2hiEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIbCgN3c2ISDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdmbGESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiIKB2NoZWRkYXISEW5hdGlvbmFsX2Zhdm9yaXRlGIDCr/AFEiEKBmJ1eno2MBIRbmF0aW9uYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2JiaBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3BsYxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3NieRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2R0dhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd3lmZhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd3dzYhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSJQoKZm94d2VhdGhlchIRbmF0aW9uYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2NiZBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSJgoOdmFsbGV5bmV3c2xpdmUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtjcmESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiAKCDEwMTFuZXdzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrdHZ1Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrc25mEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3bHVjEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3b2lvEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIoChBjYnNwaGlsYWRlbHBoaWExEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRraG91Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrbmR1Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIhCg1pbnRlcm5hdGlvbmFsEgpoc19jaGFubmVsGOzSs/AFEi4KDWRvY3VtZW50YXJpZXMSCmhzX2NoYW5uZWwYovTQ0QYiC3ByZXZpZXdHYXRlEh4KCnlvdXItbW9uZXkSCmhzX2NoYW5uZWwYid3H0wY=
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
data = base64.b64decode(b64.strip().replace(' ', '').replace('\n', ''))
|
|
9
|
-
print(f"Binary size: {len(data)} bytes")
|
|
10
|
-
|
|
11
|
-
# --- 0.4.10 ---
|
|
12
|
-
sys.path.insert(0, '/Users/admin/miniconda3/envs/py311/lib/python3.11/site-packages')
|
|
13
|
-
from haystack_ml_stack.generated.v1 import features_pb2 as pb2_v10
|
|
14
|
-
|
|
15
|
-
msg10 = pb2_v10.GlobalChannels()
|
|
16
|
-
msg10.ParseFromString(data)
|
|
17
|
-
|
|
18
|
-
empty10 = [c.name for c in msg10.data if not c.category_group]
|
|
19
|
-
print(f"\n=== 0.4.10 ===")
|
|
20
|
-
print(f"version={msg10.version}, channels={len(msg10.data)}")
|
|
21
|
-
print(f"Empty category_group: {len(empty10)}")
|
|
22
|
-
if empty10:
|
|
23
|
-
print(f" {empty10}")
|
|
24
|
-
for c in msg10.data:
|
|
25
|
-
if c.category_group == 'hs_channel':
|
|
26
|
-
print(f" [hs] {c.name:<30} start_date={c.start_date} required_features={list(c.required_features)}")
|
|
27
|
-
|
|
28
|
-
print([c for c in msg10.data if c.name in ('wbns', 'wdsu', 'kcau', 'weau', 'kndu', 'ktuu')])
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# decode_v49_only.py — run with: /tmp/hms_v49_env/bin/python3 decode_v49_only.py
|
|
2
|
-
import sys
|
|
3
|
-
import base64
|
|
4
|
-
|
|
5
|
-
# No 0.4.10 path here at all
|
|
6
|
-
b64 = """
|
|
7
|
-
CAESHAoEa3VzYRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa25vZRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2Z0dhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIwoLZm94NWF0bGFudGESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHd0bHYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEh4KBndjdmJ0dhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3RpdhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd3BzZBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSGwoDa2d3Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3Z2VtEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrdnVlEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3Y21oEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRraWlpEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dHhsEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3aWZyEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrb2NvEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrYnR4Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3YnJjEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dmlyEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIlCgpuZXdzbmF0aW9uEhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIcCgR3bmNuEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIkCgxjYnNtaW5uZXNvdGESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEikKEWZveDU5aW5kaWFuYXBvbGlzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3d2xwEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3c2ZhEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIhCgljYnNkZW52ZXISDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEh0KBTEzYWJjEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIeCgZrb2F0dHYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiMKC2NiY21hbml0b2JhEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrYW1yEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3bGF4Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRra2NvEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3Y2F4Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRra3R2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrZW5zEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrdG52Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3cmVnEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIeCgZrc2J3dHYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiUKDXRoZW5ld3NjZW50ZXISDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiMKCG5iY3RvZGF5EhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIcCgR3ZWh0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIbCgN3b2kSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdmeHQSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdhbmUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGttdnQSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiIKCmZveDdhdXN0aW4SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiQKCW5ld3NtYXh0dhIRbmF0aW9uYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2J0dxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIQoJY2JzYm9zdG9uEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrbGZ5Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3ZmFhEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrYXJkEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3eHl6Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrb2xvEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrdHNtEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3YXZ5Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrZ3VuEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrdHZxEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIiCgd0aGVoaWxsEhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIcCgR3ZGJqEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3eGlpEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3cHhpEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3c3ltEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3eG1pEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3YXZlEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3YXRlEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIpChFmb3gyMW5ld3Njb2xvcmFkbxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3JleBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2F0YxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSJwoPZm94MTFsb3NhbmdlbGVzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3c29jEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3emR4Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrc253Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRInCgx5YWhvb2ZpbmFuY2USEW5hdGlvbmFsX2Zhdm9yaXRlGIDCr/AFEhwKBHdjYmQSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEh4KA2NubhIRbmF0aW9uYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3RyaxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3BueBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSLwoUZGV1dHNjaGV3ZWxsZWVuZ2xpc2gSEW5hdGlvbmFsX2Zhdm9yaXRlGIDCr/AFEhwKBHdueXcSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiUKDWNic3NhY3JhbWVudG8SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdwcmkSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtud2ESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdhYmkSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHduZHUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEh4KBjFjYnNsYRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2dtYhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3JxZRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd3BpeBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3JlbRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2R2bRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2dhbBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2hzdhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2JuZxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd3FhZBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHQoFbmJjMTUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiIKCmNiY3Rvcm9udG8SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGt2dnUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiIKB2FiY25ld3MSEW5hdGlvbmFsX2Zhdm9yaXRlGIDCr/AFEhwKBHd0aWMSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHd0dnkSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHd4aXgSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdzYXcSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtpcm8SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiAKBXZldWVyEhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIcCgR3cm9jEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dHR2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3d2J0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIgCghhemZhbWlseRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSGwoDd3dsEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3aGJmEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dHZmEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIkCglibG9vbWJlcmcSEW5hdGlvbmFsX2Zhdm9yaXRlGIDCr/AFEhwKBHdibnMSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHd0a3ISDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGttYmMSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGt0dXUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiYKDmZveDRrYW5zYXNjaXR5Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3d2F5Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIqChJoYXlzdGFja192YW5jb3V2ZXISDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdka3kSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEioKD2Fzc29jaWF0ZWRwcmVzcxIRbmF0aW9uYWxfZmF2b3JpdGUYgMKv8AUSHgoGa25vcHR2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3YmtvEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3eGlhEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3aXNuEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dGFlEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3YXB0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIeCgZmb3g1ZGMSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiAKCGZveDhuZXdzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3amNsEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3aHRtEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3ZWF1Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIeCgZrZnlydHYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdib2MSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdrcmcSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdkc3USDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdvd2sSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHd0b2wSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtianISDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtjcmcSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiMKCG5ld3N5aHViEhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIcCgRrc250Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIlCgpsaXZlbm93Zm94EhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIcCgR3bmVwEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIhCgZncmF5ZGMSEW5hdGlvbmFsX2Zhdm9yaXRlGIDCr/AFEhwKBGt5dHgSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHd1c2ESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtvYWESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdkaG4SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGt0dmISDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEh0KBXdpc3R2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3bG94Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIiCgdjYmNuZXdzEhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIcCgRrZ3R2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3cHZpEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3b3d0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIdCgVraG9uMhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2VybxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2ZveBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHQoFa2N0djUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtzdHASDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtnbnMSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEh4KBm5ld3MxMhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2ZvchIObG9jYWxfZmF2b3JpdGUYgMKv8AUSGwoDbmNtEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIkCgxjYnNiYWx0aW1vcmUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdyZGUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiQKDGZveDVzYW5kaWVnbxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2FsYhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2l0bhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3RhYhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa21naBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHgoGd210d3R2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIiCgpjYnNjaGljYWdvEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dG9rEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3amJmEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3Y3BvEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3aGFzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRInCgxyZXV0ZXJzdmlkZW8SEW5hdGlvbmFsX2Zhdm9yaXRlGIDCr/AFEhwKBHdzYXYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHduZW0SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtyb24SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdjdHYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhoKDGNubi9hbWFucG91chIEc2hvdxiAwq/wBRIcCgR3ZXdzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3aGJxEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIeCgZhYmM3bnkSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEh0KBXdnbnR2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrZmRhEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRuYmM2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3Y2piEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dHdvEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIjCghldXJvbmV3cxIRbmF0aW9uYWxfZmF2b3JpdGUYgMKv8AUSHAoEd3N5chIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2NhdRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIwoLZm94MmRldHJvaXQSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdvb2QSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtjY2kSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiIKCmNic2RldHJvaXQSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtpdmkSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEh0KBXdobzEzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dGFqEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrZXljEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR0bWo0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3Z2hwEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIlCg1jYnNwaXR0c2J1cmdoEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrc3dvEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRramN0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIjCgtuYmNzYW5kaWVnbxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIwoLbmJjbmVicmFza2ESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiMKC2NiY2VkbW9udG9uEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrdHZpEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3ZGFtEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3Z3J6Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIlCg1mb3g2bWlsd2F1a2VlEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3ZmZmEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3bWJiEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dmVjEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3bG5zEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrdGFsEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrbnh2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRranJoEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRInCg9jYnNzYW5mcmFuY2lzY28SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdtZ3QSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiMKC2ZveGNhcm9saW5hEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIfCgdmb3gxMHR2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrcmlzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrcHR2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3ZXNoEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRraW5nEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dnRtEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3eHh2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3bWFyEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrc2Z5Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3bWJkEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIgCghjYnNtaWFtaRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoIcG9saXRpY3MSCmhzX2NoYW5uZWwYxIba8AUSHAoEa2FsYhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSJAoMZm94MzVvcmxhbmRvEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrdHhsEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIkCglhYmNuZXdzMjQSEW5hdGlvbmFsX2Zhdm9yaXRlGIDCr/AFEhwKBHdsa3kSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdjYmkSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtpZHkSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtlbG8SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEicKD3dlc3Rlcm5tYXNzbmV3cxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3R2eBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSJAoMY2JzNnBvcnRsYW5kEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3bmN0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIeCgZ3aXZidHYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiIKCmNiY2NhbGdhcnkSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtldm4SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGFiYzcSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtsdHYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdpbHgSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdzcGESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdoaW8SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdtYmYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdsZXgSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGt3cWMSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdnYmESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiQKDGZveDMyY2hpY2FnbxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIgoKY2JzbmV3eW9yaxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2p6eRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3VschIObG9jYWxfZmF2b3JpdGUYgMKv8AUSGwoDa3NsEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dGVuEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3ZnRzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dHZtEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dmx0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrZm1iEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrYXJrEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3YnJlEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIiCgdjb3VydHR2EhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIcCgR3YmFsEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRImChJ3aWxkZmlyZS1uZXdzLTIwMjYSCmhzX2NoYW5uZWwYmJTP0wYSHAoEd3BiZhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd3JibBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2FpdBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3htYRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2hudBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3NlZRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd3NtdhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3hhbhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2R0bhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2ZzYhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIQoGY25ldHR2EhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIdCgV3bWN0dhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSGwoDa29iEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3a3l0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dHZyEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRreGlpEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrc2xhEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dGhyEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3bHd0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3YnR2Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrZnZzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dnVlEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3dm5zEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrdGxhEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3aWJ3Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrd2NoEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIrChBhbGphemVlcmFlbmdsaXNoEhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIjCgthYmM3Y2hpY2FnbxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIwoLZm94MzFkZW52ZXISDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdsdHgSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdhZmYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdwbXQSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdiaXISDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHd0dmQSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHd0cmYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGthcmUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtmc24SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdjaWESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGttdHYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdwdHYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdtdXISDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdzYXoSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiQKCW55cHNwb3J0cxIRbmF0aW9uYWxfZmF2b3JpdGUYgMKv8AUSJgoObmV3czJuYXNodmlsbGUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtmZHgSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEh4KBmtldHZ0dhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSKQoRZm94MjlwaGlsYWRlbHBoaWESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEh4KBmNic2RmdxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2NlbhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIAoIZm94NG5ld3MSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdjbmMSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtzdHUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGt4eHYSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdiYXkSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGttaWQSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiQKEGlyYW4tY3Jpc2lzLW5ld3MSCmhzX2NoYW5uZWwY6LuLzQYSHAoEa3NkaxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2ZydhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2hlYxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2pldBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2JtdBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2hicxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2ZzbRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3R0YxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSJwoPZm94OW1pbm5lYXBvbGlzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3enptEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIkCgxmb3gxMHBob2VuaXgSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdkaW8SDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiQKDGZveDI2aG91c3RvbhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3d0eBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIwoLY2JjbW9udHJlYWwSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdyaWMSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtobmwSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdib3kSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiAKCGFiYzduZXdzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3cmR3Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3bGJ0Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIiCgpxMTNzZWF0dGxlEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIdCgVjYnM0NhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIwoIYW1hemVsYWISEW5hdGlvbmFsX2Zhdm9yaXRlGIDCr/AFEhwKBHdwdHoSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiUKDWZveDEzdGFtcGFiYXkSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiIKB25iY25ld3MSEW5hdGlvbmFsX2Zhdm9yaXRlGIDCr/AFEhwKBHd0b2MSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhsKA2tocRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2dldBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3RodhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd21hehIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3l0dhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa21vdhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2ZteRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSIgoHaTI0bmV3cxIRbmF0aW9uYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2VjdBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2p0dhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd3FyZhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3lsZRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSKAoQY2hhbm5lbDhsYXN2ZWdhcxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd3dueRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3BheBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2RlZhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSKgoPZnJhbmNlMjRlbmdsaXNoEhFuYXRpb25hbF9mYXZvcml0ZRiAwq/wBRIcCgRrc2hiEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIbCgN3c2ISDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBHdmbGESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiIKB2NoZWRkYXISEW5hdGlvbmFsX2Zhdm9yaXRlGIDCr/AFEiEKBmJ1eno2MBIRbmF0aW9uYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2JiaBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3BsYxIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEa3NieRIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd2R0dhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd3lmZhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSHAoEd3dzYhIObG9jYWxfZmF2b3JpdGUYgMKv8AUSJQoKZm94d2VhdGhlchIRbmF0aW9uYWxfZmF2b3JpdGUYgMKv8AUSHAoEa2NiZBIObG9jYWxfZmF2b3JpdGUYgMKv8AUSJgoOdmFsbGV5bmV3c2xpdmUSDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEhwKBGtjcmESDmxvY2FsX2Zhdm9yaXRlGIDCr/AFEiAKCDEwMTFuZXdzEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrdHZ1Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrc25mEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3bHVjEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgR3b2lvEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIoChBjYnNwaGlsYWRlbHBoaWExEg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRraG91Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIcCgRrbmR1Eg5sb2NhbF9mYXZvcml0ZRiAwq/wBRIhCg1pbnRlcm5hdGlvbmFsEgpoc19jaGFubmVsGOzSs/AFEi4KDWRvY3VtZW50YXJpZXMSCmhzX2NoYW5uZWwYovTQ0QYiC3ByZXZpZXdHYXRlEh4KCnlvdXItbW9uZXkSCmhzX2NoYW5uZWwYid3H0wY=
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
s = b64.strip().replace(' ', '').replace('\n', '')
|
|
11
|
-
data = base64.b64decode(s + '=' * (-len(s) % 4))
|
|
12
|
-
|
|
13
|
-
# -len(s) % 4 computes exactly how many = characters are needed (0, 1, or 2), so it handles any input correctly.
|
|
14
|
-
#data = base64.b64decode(b64.strip().replace(' ', '').replace('\n', ''))
|
|
15
|
-
|
|
16
|
-
from haystack_ml_stack.generated.v1 import features_pb2 as pb2
|
|
17
|
-
|
|
18
|
-
msg = pb2.GlobalChannels()
|
|
19
|
-
msg.ParseFromString(data)
|
|
20
|
-
|
|
21
|
-
empty = [c.name for c in msg.data if not c.category_group]
|
|
22
|
-
print(f"version={msg.version}, channels={len(msg.data)}")
|
|
23
|
-
print(f"Empty category_group: {len(empty)}")
|
|
24
|
-
if empty:
|
|
25
|
-
print(f" {empty}")
|
|
26
|
-
for c in msg.data:
|
|
27
|
-
if c.category_group == 'hs_channel':
|
|
28
|
-
print(f" [hs] {c.name:<30} start_date={c.start_date} required_features={list(c.required_features)}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/generated/__init__.py
RENAMED
|
File without changes
|
{haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/generated/v1/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|