centive-sdk 2.2.0.dev4__tar.gz → 2.2.0.dev5__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 (48) hide show
  1. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/CHANGELOG.md +8 -0
  2. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/PKG-INFO +1 -1
  3. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/pyproject.toml +1 -1
  4. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/resources/async_sessions.py +13 -0
  5. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/resources/telemetry_batcher.py +26 -7
  6. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_telemetry_batcher.py +53 -0
  7. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/.gitignore +0 -0
  8. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/LICENSE +0 -0
  9. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/README.md +0 -0
  10. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/blackbox/README.md +0 -0
  11. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/blackbox/customer_app.py +0 -0
  12. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/blackbox/harness.py +0 -0
  13. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/blackbox/mock_centive_api.py +0 -0
  14. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/blackbox/test_blackbox.py +0 -0
  15. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/blackbox/test_blackbox_telemetry.py +0 -0
  16. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/examples/async_example.py +0 -0
  17. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/examples/fastapi_integration.py +0 -0
  18. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/examples/sync_example.py +0 -0
  19. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/examples/websocket_example.py +0 -0
  20. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/__init__.py +0 -0
  21. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/_http.py +0 -0
  22. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/_logging.py +0 -0
  23. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/_retry.py +0 -0
  24. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/async_client.py +0 -0
  25. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/client.py +0 -0
  26. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/config.py +0 -0
  27. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/exceptions.py +0 -0
  28. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/models/__init__.py +0 -0
  29. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/models/requests.py +0 -0
  30. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/models/responses.py +0 -0
  31. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/py.typed +0 -0
  32. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/resources/__init__.py +0 -0
  33. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/resources/async_telemetry.py +0 -0
  34. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/resources/message_accumulator.py +0 -0
  35. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/resources/sessions.py +0 -0
  36. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/resources/websocket_server.py +0 -0
  37. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/__init__.py +0 -0
  38. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/conftest.py +0 -0
  39. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_client_async.py +0 -0
  40. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_client_sync.py +0 -0
  41. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_config.py +0 -0
  42. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_error_mapping.py +0 -0
  43. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_logging_redaction.py +0 -0
  44. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_message_accumulator.py +0 -0
  45. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_retry_logic.py +0 -0
  46. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_websocket_security.py +0 -0
  47. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_websocket_server.py +0 -0
  48. {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_websocket_telemetry.py +0 -0
@@ -46,6 +46,14 @@ Centive batcher and the host callback).
46
46
  `ServerError`) for every request, since retrying the same body cannot help.
47
47
  - Log redaction now masks `page_path`, `page_title`, `path`, `title` and
48
48
  `referrer_path`.
49
+ - **A 404 is no longer retried.** It is not a transient failure: the route is
50
+ absent on that deployment. This is the shape of a Centive backend older than
51
+ the SDK — `/anam/events` does not exist before page tracking — and of a
52
+ `trigger-session` for a user with no session in the organization. Previously
53
+ both were retried three times and then opened a circuit breaker to reach the
54
+ same answer. Telemetry now drops the batch, says once that the deployment has
55
+ no product-events endpoint, and keeps everything else working; a rolling
56
+ upgrade that brings the endpoint up is picked up by the next batch.
49
57
 
50
58
  ## v2.1.0
51
59
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: centive-sdk
3
- Version: 2.2.0.dev4
3
+ Version: 2.2.0.dev5
4
4
  Summary: Python SDK for Centive backend integration
5
5
  Project-URL: Homepage, https://github.com/TheAgenticAI/centive-backend-sdk
6
6
  Project-URL: Repository, https://github.com/TheAgenticAI/centive-backend-sdk
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "centive-sdk"
7
- version = "2.2.0.dev4"
7
+ version = "2.2.0.dev5"
8
8
  description = "Python SDK for Centive backend integration"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -416,6 +416,19 @@ class AsyncSessions:
416
416
  error_detail = self._extract_error_message(response)
417
417
  raise AuthError(f"Authentication failed: {error_detail}", request_id=request_id)
418
418
 
419
+ elif response.status_code == 404:
420
+ # Not transient, so it must not be retried: the route is
421
+ # absent on this deployment. That is the shape of a Centive
422
+ # backend older than the SDK — /anam/events does not exist
423
+ # before page tracking — and of a trigger-session for a user
424
+ # with no session in this org. Retrying either one three
425
+ # times, then opening a circuit breaker, spends requests and
426
+ # fills logs to reach the same answer.
427
+ error_detail = self._extract_error_message(response)
428
+ raise ValidationError(
429
+ f"Not found (404): {error_detail}", request_id=request_id
430
+ )
431
+
419
432
  elif response.status_code in [400, 413, 422]:
420
433
  # 413 is "payload over the API's size caps": retrying the
421
434
  # same body cannot succeed, so it is a validation failure.
@@ -95,6 +95,7 @@ class TelemetryBatcher:
95
95
  self._wake = asyncio.Event()
96
96
  self._running = False
97
97
  self._auth_error_logged = False
98
+ self._missing_endpoint_logged = False
98
99
  self.on_context: Optional[ContextCallable] = None
99
100
 
100
101
  # ------------------------------------------------------------------ state
@@ -246,15 +247,33 @@ class TelemetryBatcher:
246
247
  safe_log(self._logger, "error", f"telemetry rejected: invalid API key ({e})")
247
248
  return False
248
249
  except ValidationError as e:
249
- # 400 / 413 / 422: our payload was wrong; retrying cannot help.
250
+ # 400 / 404 / 413 / 422: the payload or the route was wrong, and
251
+ # retrying cannot change either.
250
252
  self._stats.dropped_non_retryable += len(batch)
251
253
  self._stats.last_error = "ValidationError"
252
- safe_log(
253
- self._logger,
254
- "warning",
255
- f"telemetry batch rejected by API, dropped: {e}",
256
- {"events": len(batch)},
257
- )
254
+ if "(404)" in str(e):
255
+ # A Centive deployment older than this SDK: page tracking has
256
+ # no endpoint to reach there. Said once, plainly, because the
257
+ # fix is a backend upgrade and not something a retry reaches.
258
+ # Not fatal: everything else the SDK does still works, and a
259
+ # rolling upgrade can bring the endpoint up under us, so later
260
+ # batches keep trying quietly.
261
+ if not self._missing_endpoint_logged:
262
+ self._missing_endpoint_logged = True
263
+ safe_log(
264
+ self._logger,
265
+ "warning",
266
+ "this Centive deployment has no product-events endpoint; page "
267
+ "events are being discarded. Everything else is unaffected. "
268
+ "Set telemetry_enabled=False to stop sending them.",
269
+ )
270
+ else:
271
+ safe_log(
272
+ self._logger,
273
+ "warning",
274
+ f"telemetry batch rejected by API, dropped: {e}",
275
+ {"events": len(batch)},
276
+ )
258
277
  return True
259
278
  except Exception as e: # unknown: treat as retryable but bounded by the breaker
260
279
  self._breaker.record_failure()
@@ -265,3 +265,56 @@ async def test_context_relay_failure_does_not_break_flush():
265
265
  assert batcher.queue_depth == 0
266
266
  assert batcher.stats()["accepted"] == 1
267
267
  assert batcher.circuit_breaker.state == "closed"
268
+
269
+
270
+ @pytest.mark.asyncio
271
+ async def test_missing_endpoint_drops_without_retrying_or_tripping_the_breaker():
272
+ """A Centive deployment older than this SDK has no /anam/events.
273
+
274
+ This is the rollout shape that matters: the SDK ships ahead of the backend
275
+ for a couple of weeks. A 404 must cost the page events and nothing else —
276
+ no retry storm, no circuit breaker, no effect on transcript saves.
277
+ """
278
+ logged: list[str] = []
279
+
280
+ class Recorder:
281
+ def warning(self, message, *args, **kwargs):
282
+ logged.append(str(message))
283
+
284
+ def __getattr__(self, _name):
285
+ return lambda *a, **k: None
286
+
287
+ sender = RecordingSender([ValidationError("Not found (404): Not Found")])
288
+ batcher = TelemetryBatcher(sender, make_config(telemetry_flush_max_events=10), logger=Recorder())
289
+ batcher.enqueue("alice", None, [make_event(), make_event()])
290
+
291
+ await batcher.flush()
292
+
293
+ assert batcher.queue_depth == 0 # dropped, not requeued
294
+ assert batcher.circuit_breaker.state == "closed"
295
+ assert batcher.stats()["dropped_non_retryable"] == 2
296
+ assert len(sender.requests) == 1 # not retried
297
+ assert any("no product-events endpoint" in line for line in logged)
298
+
299
+
300
+ @pytest.mark.asyncio
301
+ async def test_the_missing_endpoint_is_reported_once_not_per_batch():
302
+ logged: list[str] = []
303
+
304
+ class Recorder:
305
+ def warning(self, message, *args, **kwargs):
306
+ logged.append(str(message))
307
+
308
+ def __getattr__(self, _name):
309
+ return lambda *a, **k: None
310
+
311
+ sender = RecordingSender(
312
+ [ValidationError("Not found (404): Not Found"), ValidationError("Not found (404): Not Found")]
313
+ )
314
+ batcher = TelemetryBatcher(sender, make_config(telemetry_flush_max_events=1), logger=Recorder())
315
+ batcher.enqueue("alice", None, [make_event(), make_event()])
316
+
317
+ await batcher.flush()
318
+
319
+ assert len(sender.requests) == 2
320
+ assert sum("no product-events endpoint" in line for line in logged) == 1