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.
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/CHANGELOG.md +8 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/PKG-INFO +1 -1
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/pyproject.toml +1 -1
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/resources/async_sessions.py +13 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/resources/telemetry_batcher.py +26 -7
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_telemetry_batcher.py +53 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/.gitignore +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/LICENSE +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/README.md +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/blackbox/README.md +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/blackbox/customer_app.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/blackbox/harness.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/blackbox/mock_centive_api.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/blackbox/test_blackbox.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/blackbox/test_blackbox_telemetry.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/examples/async_example.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/examples/fastapi_integration.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/examples/sync_example.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/examples/websocket_example.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/__init__.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/_http.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/_logging.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/_retry.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/async_client.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/client.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/config.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/exceptions.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/models/__init__.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/models/requests.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/models/responses.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/py.typed +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/resources/__init__.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/resources/async_telemetry.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/resources/message_accumulator.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/resources/sessions.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/resources/websocket_server.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/__init__.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/conftest.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_client_async.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_client_sync.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_config.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_error_mapping.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_logging_redaction.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_message_accumulator.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_retry_logic.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_websocket_security.py +0 -0
- {centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/tests/test_websocket_server.py +0 -0
- {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.
|
|
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
|
{centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/resources/async_sessions.py
RENAMED
|
@@ -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.
|
{centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/resources/telemetry_batcher.py
RENAMED
|
@@ -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:
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/resources/async_telemetry.py
RENAMED
|
File without changes
|
{centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/resources/message_accumulator.py
RENAMED
|
File without changes
|
|
File without changes
|
{centive_sdk-2.2.0.dev4 → centive_sdk-2.2.0.dev5}/src/centive_sdk/resources/websocket_server.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|