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.
Files changed (31) hide show
  1. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/PKG-INFO +2 -1
  2. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/pyproject.toml +3 -3
  3. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/__init__.py +0 -0
  4. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/_kafka.py +0 -0
  5. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/_serializers.py +92 -2
  6. haystack_ml_stack-0.4.13/src/haystack_ml_stack/_version.py +1 -0
  7. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/app.py +12 -1
  8. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/cache.py +0 -0
  9. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/dynamo.py +60 -23
  10. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/generated/v1/features_pb2.py +7 -1
  11. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/generated/v1/features_pb2.pyi +30 -0
  12. haystack_ml_stack-0.4.13/src/haystack_ml_stack/history.py +38 -0
  13. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/settings.py +0 -0
  14. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack.egg-info/PKG-INFO +2 -1
  15. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack.egg-info/SOURCES.txt +2 -2
  16. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack.egg-info/requires.txt +1 -0
  17. haystack_ml_stack-0.4.13/tests/test_history_fetch.py +226 -0
  18. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/tests/test_serializers.py +92 -0
  19. haystack_ml_stack-0.4.11/src/haystack_ml_stack/_version.py +0 -1
  20. haystack_ml_stack-0.4.11/src/haystack_ml_stack/tmp.py +0 -28
  21. haystack_ml_stack-0.4.11/src/haystack_ml_stack/tmp2.py +0 -28
  22. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/README.md +0 -0
  23. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/setup.cfg +0 -0
  24. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/exceptions.py +0 -0
  25. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/generated/__init__.py +0 -0
  26. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/generated/v1/__init__.py +0 -0
  27. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/model_store.py +0 -0
  28. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack/utils.py +0 -0
  29. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack.egg-info/dependency_links.txt +0 -0
  30. {haystack_ml_stack-0.4.11 → haystack_ml_stack-0.4.13}/src/haystack_ml_stack.egg-info/top_level.txt +0 -0
  31. {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.11
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.11"
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
+ ]
@@ -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(stats["capped_not_watched_secs"])
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(stats.get("capped_total_log_watched", 0.0))
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,
@@ -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
- all_feature_keys = [*stream_features, *user_features]
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) - stream_cache_misses
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][feature_name] = value
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
- return FeatureRetrievalMeta(
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][feature_name] = value
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[key] = obj
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.Channelb\x06proto3')
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haystack-ml-stack
3
- Version: 0.4.11
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"
@@ -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
@@ -1,5 +1,6 @@
1
1
  protobuf==6.33.2
2
2
  orjson==3.11.7
3
+ numpy>=1.26
3
4
 
4
5
  [server]
5
6
  pydantic==2.5.0
@@ -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)}")