feat: Python telemetry client SDK v0.1.0

Standalone Python package (mosaicstack-telemetry) for reporting
task-completion telemetry and querying predictions from the Mosaic
Stack Telemetry server.

- Sync/async TelemetryClient with context manager support
- Thread-safe EventQueue with bounded deque
- BatchSubmitter with httpx, exponential backoff, Retry-After
- PredictionCache with TTL
- EventBuilder convenience class
- All types standalone (no server dependency)
- 55 tests, 90% coverage, mypy strict clean

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 23:25:27 -06:00
parent 0b29302f43
commit f02207e33c
7 changed files with 752 additions and 56 deletions

View File

@@ -38,32 +38,28 @@ from mosaicstack_telemetry.types.predictions import (
__version__ = "0.1.0"
__all__ = [
# Client
"TelemetryClient",
"TelemetryConfig",
"EventBuilder",
"EventQueue",
"PredictionCache",
# Types - Events
"TaskCompletionEvent",
"TaskType",
"Complexity",
"Harness",
"Provider",
"QualityGate",
"Outcome",
"RepoSizeCategory",
# Types - Predictions
"PredictionQuery",
"PredictionResponse",
"PredictionData",
"PredictionMetadata",
"TokenDistribution",
"CorrectionFactors",
"QualityPrediction",
# Types - Common
"BatchEventRequest",
"BatchEventResponse",
"BatchEventResult",
"Complexity",
"CorrectionFactors",
"EventBuilder",
"EventQueue",
"Harness",
"Outcome",
"PredictionCache",
"PredictionData",
"PredictionMetadata",
"PredictionQuery",
"PredictionResponse",
"Provider",
"QualityGate",
"QualityPrediction",
"RepoSizeCategory",
"TaskCompletionEvent",
"TaskType",
"TelemetryClient",
"TelemetryConfig",
"TelemetryError",
"TokenDistribution",
]

View File

@@ -123,7 +123,7 @@ class TelemetryClient:
if response.status_code == 200:
data = response.json()
results = data.get("results", [])
for query, result_data in zip(queries, results):
for query, result_data in zip(queries, results, strict=False):
pred = PredictionResponse.model_validate(result_data)
self._prediction_cache.put(query, pred)
logger.debug("Refreshed %d predictions", len(results))
@@ -153,7 +153,7 @@ class TelemetryClient:
if response.status_code == 200:
data = response.json()
results = data.get("results", [])
for query, result_data in zip(queries, results):
for query, result_data in zip(queries, results, strict=False):
pred = PredictionResponse.model_validate(result_data)
self._prediction_cache.put(query, pred)
logger.debug("Refreshed %d predictions", len(results))

View File

@@ -20,8 +20,8 @@ logger = logging.getLogger("mosaicstack_telemetry")
def _backoff_delay(attempt: int, base: float = 1.0, maximum: float = 60.0) -> float:
"""Calculate exponential backoff with jitter."""
delay = min(base * (2**attempt), maximum)
jitter = random.uniform(0, delay * 0.5) # noqa: S311
delay: float = min(base * (2**attempt), maximum)
jitter: float = random.uniform(0, delay * 0.5) # noqa: S311
return delay + jitter
@@ -78,9 +78,7 @@ def submit_batch_sync(
continue
if response.status_code == 403:
logger.error(
"Authentication failed (403): API key may not match instance_id"
)
logger.error("Authentication failed (403): API key may not match instance_id")
return None
logger.warning(
@@ -110,7 +108,11 @@ def submit_batch_sync(
logger.debug("Backing off for %.1f seconds before retry", delay)
time.sleep(delay)
logger.error("All %d attempts failed for batch of %d events", config.max_retries + 1, len(events))
logger.error(
"All %d attempts failed for batch of %d events",
config.max_retries + 1,
len(events),
)
return None
@@ -169,9 +171,7 @@ async def submit_batch_async(
continue
if response.status_code == 403:
logger.error(
"Authentication failed (403): API key may not match instance_id"
)
logger.error("Authentication failed (403): API key may not match instance_id")
return None
logger.warning(
@@ -201,5 +201,9 @@ async def submit_batch_async(
logger.debug("Backing off for %.1f seconds before retry", delay)
await asyncio.sleep(delay)
logger.error("All %d attempts failed for batch of %d events", config.max_retries + 1, len(events))
logger.error(
"All %d attempts failed for batch of %d events",
config.max_retries + 1,
len(events),
)
return None