haystack-ml-stack 0.2.4__tar.gz → 0.2.5__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 (17) hide show
  1. {haystack_ml_stack-0.2.4 → haystack_ml_stack-0.2.5}/PKG-INFO +1 -1
  2. {haystack_ml_stack-0.2.4 → haystack_ml_stack-0.2.5}/pyproject.toml +1 -1
  3. {haystack_ml_stack-0.2.4 → haystack_ml_stack-0.2.5}/src/haystack_ml_stack/__init__.py +1 -1
  4. {haystack_ml_stack-0.2.4 → haystack_ml_stack-0.2.5}/src/haystack_ml_stack/app.py +56 -26
  5. {haystack_ml_stack-0.2.4 → haystack_ml_stack-0.2.5}/src/haystack_ml_stack/cache.py +2 -2
  6. {haystack_ml_stack-0.2.4 → haystack_ml_stack-0.2.5}/src/haystack_ml_stack/dynamo.py +22 -22
  7. {haystack_ml_stack-0.2.4 → haystack_ml_stack-0.2.5}/src/haystack_ml_stack.egg-info/PKG-INFO +1 -1
  8. {haystack_ml_stack-0.2.4 → haystack_ml_stack-0.2.5}/README.md +0 -0
  9. {haystack_ml_stack-0.2.4 → haystack_ml_stack-0.2.5}/setup.cfg +0 -0
  10. {haystack_ml_stack-0.2.4 → haystack_ml_stack-0.2.5}/src/haystack_ml_stack/model_store.py +0 -0
  11. {haystack_ml_stack-0.2.4 → haystack_ml_stack-0.2.5}/src/haystack_ml_stack/settings.py +0 -0
  12. {haystack_ml_stack-0.2.4 → haystack_ml_stack-0.2.5}/src/haystack_ml_stack/utils.py +0 -0
  13. {haystack_ml_stack-0.2.4 → haystack_ml_stack-0.2.5}/src/haystack_ml_stack.egg-info/SOURCES.txt +0 -0
  14. {haystack_ml_stack-0.2.4 → haystack_ml_stack-0.2.5}/src/haystack_ml_stack.egg-info/dependency_links.txt +0 -0
  15. {haystack_ml_stack-0.2.4 → haystack_ml_stack-0.2.5}/src/haystack_ml_stack.egg-info/requires.txt +0 -0
  16. {haystack_ml_stack-0.2.4 → haystack_ml_stack-0.2.5}/src/haystack_ml_stack.egg-info/top_level.txt +0 -0
  17. {haystack_ml_stack-0.2.4 → haystack_ml_stack-0.2.5}/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.2.4
3
+ Version: 0.2.5
4
4
  Summary: Functions related to Haystack ML
5
5
  Author-email: Oscar Vega <oscar@haystack.tv>
6
6
  License: MIT
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "haystack-ml-stack"
8
- version = "0.2.4"
8
+ version = "0.2.5"
9
9
  description = "Functions related to Haystack ML"
10
10
  readme = "README.md"
11
11
  authors = [{ name = "Oscar Vega", email = "oscar@haystack.tv" }]
@@ -1,4 +1,4 @@
1
1
  from .app import create_app
2
2
 
3
3
  __all__ = ["create_app"]
4
- __version__ = "0.2.4"
4
+ __version__ = "0.2.5"
@@ -5,8 +5,10 @@ import sys
5
5
  from http import HTTPStatus
6
6
  from typing import Any, Dict, List, Optional
7
7
  import time
8
+ from contextlib import asynccontextmanager, AsyncExitStack
8
9
 
9
10
  import aiobotocore.session
11
+ from aiobotocore.config import AioConfig
10
12
  from fastapi import FastAPI, HTTPException, Request, Response
11
13
  from fastapi.encoders import jsonable_encoder
12
14
  import newrelic.agent
@@ -25,7 +27,7 @@ logging.basicConfig(
25
27
  )
26
28
 
27
29
  logger = logging.getLogger(__name__)
28
- APP_NAME = os.environ.get("NEW_RELIC_APP_NAME", None)
30
+ MAX_POOL_CONNECTIONS = int(os.environ.get("MAX_POOL_CONNECTIONS", 50))
29
31
 
30
32
 
31
33
  def create_app(
@@ -39,12 +41,6 @@ def create_app(
39
41
  """
40
42
  cfg = settings or Settings()
41
43
 
42
- app = FastAPI(
43
- title="ML Stream Scorer",
44
- description="Scores video streams using a pre-trained ML model and DynamoDB features.",
45
- version="1.0.0",
46
- )
47
-
48
44
  # Mutable state: cache + model
49
45
  features_cache = make_features_cache(cfg.cache_maxsize)
50
46
  state: Dict[str, Any] = {
@@ -55,24 +51,59 @@ def create_app(
55
51
  ),
56
52
  }
57
53
 
58
- @app.on_event("startup")
59
- async def _startup() -> None:
60
- if state["model"] is not None:
61
- logger.info("Using preloaded model.")
62
- return
63
-
64
- if not cfg.s3_model_path:
65
- logger.critical("S3_MODEL_PATH not set; service will be unhealthy.")
66
- return
67
-
68
- try:
69
- state["model"] = await download_and_load_model(
70
- cfg.s3_model_path, aio_session=state["session"]
54
+ @asynccontextmanager
55
+ async def lifespan(app_server: FastAPI):
56
+ """
57
+ Handles startup and shutdown logic.
58
+ Everything before 'yield' runs on startup.
59
+ Everything after 'yield' runs on shutdown.
60
+ """
61
+ async with AsyncExitStack() as stack:
62
+ # 1. Initialize DynamoDB Client (Persistent Pool)
63
+ session = state["session"]
64
+ state["dynamo_client"] = await stack.enter_async_context(
65
+ session.create_client(
66
+ "dynamodb",
67
+ # Ensure the pool is large enough for ML concurrency
68
+ config=AioConfig(max_pool_connections=MAX_POOL_CONNECTIONS),
69
+ )
71
70
  )
72
- state["stream_features"] = state["model"].get("stream_features", [])
73
- logger.info("Model loaded on startup.")
74
- except Exception as e:
75
- logger.critical("Failed to load model: %s", e)
71
+ logger.info("DynamoDB persistent client initialized.")
72
+
73
+ # 2. Load ML Model
74
+ if state["model"] is None:
75
+ if not cfg.s3_model_path:
76
+ logger.critical("S3_MODEL_PATH not set; service will be unhealthy.")
77
+ else:
78
+ try:
79
+ # Pass the persistent session/client if needed
80
+ state["model"] = await download_and_load_model(
81
+ cfg.s3_model_path, aio_session=state["session"]
82
+ )
83
+ state["stream_features"] = state["model"].get(
84
+ "stream_features", []
85
+ )
86
+ state["user_features"] = state["model"].get("user_features", [])
87
+
88
+ newrelic.agent.add_custom_attribute(
89
+ "total_stream_features", len(state["stream_features"])
90
+ )
91
+ logger.info("Model loaded successfully.")
92
+ except Exception as e:
93
+ logger.critical("Failed to load model: %s", e)
94
+
95
+ yield
96
+
97
+ # 3. Shutdown Logic
98
+ # The AsyncExitStack automatically closes the DynamoDB client pool here
99
+ logger.info("Shutting down: Connection pools closed.")
100
+
101
+ app = FastAPI(
102
+ title="ML Stream Scorer",
103
+ description="Scores video streams using a pre-trained ML model and DynamoDB features.",
104
+ version="1.0.0",
105
+ lifespan=lifespan,
106
+ )
76
107
 
77
108
  @app.get("/health", status_code=HTTPStatus.OK)
78
109
  async def health():
@@ -130,7 +161,7 @@ def create_app(
130
161
  )
131
162
  if stream_features:
132
163
  retrieval_meta = await set_stream_features(
133
- aio_session=state["session"],
164
+ dynamo_client=state["dynamo_client"],
134
165
  streams=streams,
135
166
  stream_features=stream_features,
136
167
  features_cache=features_cache,
@@ -168,7 +199,6 @@ def create_app(
168
199
  newrelic.agent.record_custom_event(
169
200
  "Inference",
170
201
  {
171
- "app_name": APP_NAME,
172
202
  "cache_misses": retrieval_meta.cache_misses,
173
203
  "retrieval_success": int(retrieval_meta.success),
174
204
  "cache_delay_minutes": retrieval_meta.cache_delay_minutes,
@@ -5,14 +5,14 @@ from cachetools import TLRUCache
5
5
 
6
6
  def _ttu(_, value: Any, now: float) -> float:
7
7
  """Time-To-Use policy: allow per-item TTL via 'cache_ttl_in_seconds' or fallback."""
8
- ONE_YEAR = 365 * 24 * 60 * 60
8
+ ONE_WEEK = 7 * 24 * 60 * 60
9
9
  try:
10
10
  ttl = int(value.get("cache_ttl_in_seconds", -1))
11
11
  if ttl > 0:
12
12
  return now + ttl
13
13
  except Exception:
14
14
  pass
15
- return now + ONE_YEAR
15
+ return now + ONE_WEEK
16
16
 
17
17
 
18
18
  def make_features_cache(maxsize: int) -> TLRUCache:
@@ -2,7 +2,6 @@ from typing import Any, Dict, List, NamedTuple
2
2
  import logging
3
3
  import time
4
4
  import datetime
5
- import aiobotocore.session
6
5
  from boto3.dynamodb.types import TypeDeserializer
7
6
  import newrelic.agent
8
7
  import asyncio
@@ -38,10 +37,14 @@ async def async_batch_get(
38
37
  """
39
38
  # DynamoDB's BatchGetItem has a 100-item limit per request.
40
39
  CHUNK_SIZE = 100
41
- chunks = [keys[i : i + CHUNK_SIZE] for i in range(0, len(keys), CHUNK_SIZE)]
42
- tasks = [_fetch_chunk(dynamo_client, table_name, chunk) for chunk in chunks]
43
- results = await asyncio.gather(*tasks)
44
- all_items = [item for batch in results for item in batch]
40
+
41
+ if len(keys) <= CHUNK_SIZE:
42
+ all_items = await _fetch_chunk(dynamo_client, table_name, keys)
43
+ else:
44
+ chunks = [keys[i : i + CHUNK_SIZE] for i in range(0, len(keys), CHUNK_SIZE)]
45
+ tasks = [_fetch_chunk(dynamo_client, table_name, chunk) for chunk in chunks]
46
+ results = await asyncio.gather(*tasks)
47
+ all_items = [item for batch in results for item in batch]
45
48
  return all_items
46
49
 
47
50
 
@@ -76,7 +79,6 @@ async def _fetch_chunk(dynamo_client, table_name: str, chunk_keys):
76
79
  return items
77
80
 
78
81
 
79
- @newrelic.agent.function_trace()
80
82
  def parse_dynamo_item(item: Dict[str, Any]) -> Dict[str, Any]:
81
83
  """Parse a DynamoDB attribute map (low-level) to Python types."""
82
84
  # out: Dict[str, Any] = {}
@@ -92,7 +94,7 @@ async def set_stream_features(
92
94
  features_table: str,
93
95
  stream_pk_prefix: str,
94
96
  cache_sep: str,
95
- aio_session: aiobotocore.session.Session | None = None,
97
+ dynamo_client,
96
98
  ) -> FeatureRetrievalMeta:
97
99
  time_start = time.perf_counter_ns()
98
100
  """Fetch missing features for streams from DynamoDB and fill them into streams."""
@@ -145,22 +147,20 @@ async def set_stream_features(
145
147
  pk = f"{stream_pk_prefix}{stream_url}"
146
148
  keys.append({"pk": {"S": pk}, "sk": {"S": sk}})
147
149
 
148
- session = aio_session or aiobotocore.session.get_session()
149
150
  dynamo_start = time.perf_counter_ns()
150
- async with session.create_client("dynamodb") as dynamodb:
151
- try:
152
- items = await async_batch_get(dynamodb, features_table, keys)
153
- except Exception as e:
154
- logger.error("DynamoDB batch_get failed: %s", e)
155
- end_time = time.perf_counter_ns()
156
- return FeatureRetrievalMeta(
157
- cache_misses=cache_misses,
158
- retrieval_ms=(end_time - time_start) * 1e-6,
159
- success=False,
160
- cache_delay_minutes=cache_delay / 60,
161
- dynamo_ms=(end_time - dynamo_start) * 1e-6,
162
- parsing_ms=0,
163
- )
151
+ try:
152
+ items = await async_batch_get(dynamo_client, features_table, keys)
153
+ except Exception as e:
154
+ logger.error("DynamoDB batch_get failed: %s", e)
155
+ end_time = time.perf_counter_ns()
156
+ return FeatureRetrievalMeta(
157
+ cache_misses=cache_misses,
158
+ retrieval_ms=(end_time - time_start) * 1e-6,
159
+ success=False,
160
+ cache_delay_minutes=cache_delay / 60,
161
+ dynamo_ms=(end_time - dynamo_start) * 1e-6,
162
+ parsing_ms=0,
163
+ )
164
164
  dynamo_end = time.perf_counter_ns()
165
165
  updated_keys = set()
166
166
  for item in items:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haystack-ml-stack
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: Functions related to Haystack ML
5
5
  Author-email: Oscar Vega <oscar@haystack.tv>
6
6
  License: MIT