Skip to content

v0.2.0: server sync — async traces, dual auth, scrubber-proxy mode, rater client - #1

Closed
UltraInstinct0x wants to merge 3 commits into
mainfrom
v0.2.0-server-sync
Closed

v0.2.0: server sync — async traces, dual auth, scrubber-proxy mode, rater client#1
UltraInstinct0x wants to merge 3 commits into
mainfrom
v0.2.0-server-sync

Conversation

@UltraInstinct0x

@UltraInstinct0x UltraInstinct0x commented May 27, 2026

Copy link
Copy Markdown
Owner

Summary

This PR syncs the Python SDK to panel server surface v0.2.0.

New/updated methods

  • PanelClient.ingest_trace(source_agent, blob, trace_id=None) -> TraceResult
  • PanelClient.ingest_trace_and_wait(source_agent, blob, trace_id=None, max_wait_seconds=60, poll_interval_seconds=1.5)
  • PanelClient.fetch_trace(trace_id)
  • PanelClient.ingest_units(units)
  • PanelClient.score_unit(ref=None, unit_id=None)
  • PanelClient.skill_review(skill_name, diff, ...)
  • PanelClient.verify_token(token) (signature unchanged)
  • Async parity for all Panel methods via AsyncPanelClient
  • RaterClient.next_unit(pool, rater_id)
  • RaterClient.submit_judgment(unit_id, choice)
  • Async parity via AsyncRaterClient

Surface changes

  • Package split into: client.py, rater.py, _signing.py, _scrubber.py, _retry.py, errors.py, types.py
  • from panel_sdk import PanelClient remains valid
  • Version bumped to 0.2.0

Auth + scrubber behavior

  • HMAC signing centralized in _signing.py
  • Dual-secret mode via site_secret_source="raw" (x-panel-ingest-secret)
  • Scrubber modes: off, self-sign, proxy

Error types

  • PanelError
  • PanelRateLimitError (scope, retry_after_s)
  • PanelScrubberError

Retry/backoff

  • 429 retry using Retry-After / payload retry_after_s (capped)
  • 5xx exponential backoff

Tests

Added tests (pytest + respx) for:

  1. HMAC over canonical ingest body
  2. HMAC over canonical score query string
  3. Scrubber self-sign JWT structure
  4. Async trace polling completion (ingest_trace_and_wait)
  5. 429 retry path with Retry-After

Breaking changes

  • Removed old ingest_unit/fetch_unit convenience methods in favor of spec-aligned ingest_units and server-sync surface.

Copilot AI review requested due to automatic review settings May 27, 2026 02:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR syncs the Python SDK to Panel server surface v0.2.0 by refactoring the SDK into modular modules, expanding the operator API surface (including async parity), and adding retry/scrubber/auth behaviors plus updated tests and docs.

Changes:

  • Refactors the package from a monolithic __init__.py client into modular client/errors/types/signing/scrubber/retry modules and bumps version to 0.2.0.
  • Adds new operator methods (e.g., ingest_units, score_unit, skill_review, ingest_trace_and_wait) with async equivalents and introduces rater clients.
  • Adds pytest/respx coverage for signing, scrubber JWT structure, trace polling, and 429 retry behavior; updates README + adds CHANGELOG.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
tests/test_sdk_sync.py New tests for v0.2.0 signing/scrubber/retry/async trace ingest behaviors.
tests/test_client.py Removes the previous test suite superseded by the new sync suite.
README.md Updates install + documents v0.2.0 options/methods/errors.
pyproject.toml Bumps version and defines test dependencies + pytest asyncio mode.
panel_sdk/types.py Adds typed VerifyResult and TraceResult dataclasses.
panel_sdk/rater.py Adds sync/async rater clients for pool fetch and judgment submission.
panel_sdk/errors.py Introduces typed exceptions (PanelError, PanelRateLimitError, PanelScrubberError).
panel_sdk/client.py Implements sync + async Panel clients with signing, scrubber modes, retry/backoff, and new endpoints.
panel_sdk/_signing.py Adds canonicalization + HMAC/JWT helpers and score canonical string builder.
panel_sdk/_scrubber.py Implements scrubber header/body handling for off/self-sign/proxy modes.
panel_sdk/_retry.py Adds retry decision/delay helpers and typed error parsing.
panel_sdk/__init__.py Updates public exports for the new modular package layout and version.
CHANGELOG.md Adds a v0.2.0 changelog entry describing the refactor and new surface.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread panel_sdk/_retry.py
def retry_delay_seconds(status_code: int, attempt: int, retry_after_s: float | None) -> float:
"""Return capped sleep duration before next retry."""
if status_code == 429:
return min(30.0, retry_after_s or 1.0)
Comment thread panel_sdk/_signing.py

def canonical_json(value: Any) -> str:
"""Return compact canonical JSON string."""
return json.dumps(value, separators=(",", ":"), sort_keys=False)
Comment thread panel_sdk/client.py
except ValueError:
retry_after_s = None
if isinstance(payload, dict) and payload.get("retry_after_s") is not None:
retry_after_s = float(payload["retry_after_s"])
Comment thread panel_sdk/client.py
Comment on lines +97 to +113
retry_after_s: float | None = None
if response.status_code == 429:
ra = response.headers.get("Retry-After")
if ra:
try:
retry_after_s = float(ra)
except ValueError:
retry_after_s = None
if isinstance(payload, dict) and payload.get("retry_after_s") is not None:
retry_after_s = float(payload["retry_after_s"])
if should_retry(response.status_code, attempt, self.max_retries):
delay = retry_delay_seconds(response.status_code, attempt, retry_after_s)
blocking_sleep(delay)
attempt += 1
continue
parse_error_response(response.status_code, payload, response.text)

Comment thread panel_sdk/client.py
Comment on lines +128 to +135
while time.time() < deadline:
polled = self.fetch_trace(result.trace_id)
status = str(polled.get("status", ""))
if status and status != "pending":
return polled
blocking_sleep(poll_interval_seconds)
raise TimeoutError("trace polling timed out")

Comment thread panel_sdk/types.py
Comment on lines +51 to +53
return cls(
status=status,
trace_id=str(payload.get("trace_id", "")),
Comment thread panel_sdk/client.py
from panel_sdk._retry import blocking_sleep, parse_error_response, retry_delay_seconds, should_retry
from panel_sdk._scrubber import build_scrubber_headers, build_scrubber_headers_async
from panel_sdk._signing import canonical_json, canonical_score_string, hmac_sha256_hex
from panel_sdk.errors import PanelRateLimitError
Comment thread tests/test_sdk_sync.py
Comment on lines +82 to +90
respx.post(f"{BASE}/api/v1/traces").mock(
return_value=httpx.Response(202, json={"trace_id": "tr_2", "status": "pending", "poll": "/v1/traces/tr_2"})
)
route = respx.get(f"{BASE}/api/v1/traces/tr_2").mock(
side_effect=[
httpx.Response(202, json={"trace_id": "tr_2", "status": "pending"}),
httpx.Response(200, json={"trace_id": "tr_2", "status": "done", "unit_ids": ["u1"]}),
]
)
Comment thread panel_sdk/rater.py
Comment on lines +19 to +50
class RaterClient:
"""Synchronous rater client using site-key auth only."""

def __init__(self, *, base_url: str, site_key: str, timeout_seconds: float = 10.0, client: httpx.Client | None = None) -> None:
"""Initialize RaterClient."""
self.base_url = base_url.rstrip("/")
self.site_key = site_key
self._client = client or httpx.Client(timeout=timeout_seconds)

def next_unit(self, pool: str, rater_id: str) -> dict[str, Any]:
"""Fetch next unit from rater pool."""
response = self._client.get(
f"{self.base_url}/api/rater/next",
headers={"x-panel-site-key": self.site_key},
params={"pool": pool, "rater_id": rater_id},
)
payload = _json_or_raw(response)
if not (200 <= response.status_code < 300):
parse_error_response(response.status_code, payload, response.text)
return payload

def submit_judgment(self, unit_id: str, choice: str) -> dict[str, Any]:
"""Submit a rater judgment choice."""
response = self._client.post(
f"{self.base_url}/api/rater/judgment",
headers={"x-panel-site-key": self.site_key, "content-type": "application/json"},
json={"unit_id": unit_id, "choice": choice},
)
payload = _json_or_raw(response)
if not (200 <= response.status_code < 300):
parse_error_response(response.status_code, payload, response.text)
return payload
@UltraInstinct0x

Copy link
Copy Markdown
Owner Author

superseded by #2 (same v0.2.0 server sync, merged as 158a27f via parallel workflow)

@UltraInstinct0x
UltraInstinct0x deleted the v0.2.0-server-sync branch May 27, 2026 03:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants