Metadata-Version: 2.4
Name: zensconnect
Version: 0.1.0
Summary: ZensConnect Embed Platform server SDK
Project-URL: Homepage, https://zensconnect.com
Project-URL: Repository, https://github.com/zensbot/zenszoom
Project-URL: Documentation, https://github.com/zensbot/zenszoom/blob/main/sdks/python/README.md
Project-URL: Issues, https://github.com/zensbot/zenszoom/issues
License: Proprietary
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Description-Content-Type: text/markdown

# zenszoom (Python SDK)

Server-side SDK for the ZenZoom Embed Platform.

## Install

```bash
pip install zensconnect
```
> **Installed as `zensconnect`; imported as `zenszoom`.** The module name is retained for
> backwards compatibility and will be aligned in a future major release.


## Quickstart

```python
from zenszoom import ZenszoomClient

zz = ZenszoomClient(api_key="zzemk_...", api_secret="...")

meeting = zz.create_meeting(title="Algebra 101")

entry = zz.meeting_entry(
    meeting.id,
    host={"external_user_id": "teacher-42", "name": "Ms. Khan"},
    participant={"external_user_id": "student-7", "name": "Ali"},
)

# Hand entry.start.url to the teacher, entry.join.url to the student,
# OR pass entry.start.token / entry.join.token to the browser embed SDK.
```

The default `base_url` is `https://api.zensconnect.com`. Override it if you're hitting
a staging or self-hosted instance:

```python
zz = ZenszoomClient(api_key="...", api_secret="...", base_url="https://api.staging.example.com")
```

## Meeting lifecycle helpers

```python
# Transition the meeting to "live" (host-start gate)
zz.start_meeting(meeting.id)

# Attendance — basic list and enriched engagement detail
attendance = zz.get_attendance(meeting.id)           # AttendanceOut(meeting_id, rows)
detail = zz.get_attendance_detail(meeting.id)        # AttendanceDetailOut (engagement metrics)

# Live roster (participants currently in-room)
roster = zz.get_live_participants(meeting.id)         # LiveRosterOut(live_count, participants)

# Recordings — cursor-paginated list + single recording with signed playback URLs
page = zz.get_recordings(meeting.id, limit=50)        # RecordingsOut(recordings, next_cursor)
rec = zz.get_recording(page.recordings[0].id)         # RecordingDetail(playback_url, download_url, ...)

# Org-level reports
overview = zz.get_reports_overview()                  # OrgMetrics
report = zz.get_attendance_report()                   # AttendanceOverview
```

## Sign in with ZensZoom (OAuth 2.0 + PKCE)

```python
from zenszoom import ZensZoomOAuth

# Public client (PKCE only) — or pass client_secret=... for a confidential client.
oauth = ZensZoomOAuth(
    client_id="zzoa_...",
    redirect_uri="https://yourapp.com/callback",
    # storage=... defaults to in-memory; inject your session/cache store to
    # persist {verifier, state, nonce} across the redirect (multi-process).
)

# Step 1 — redirect the user to ZensZoom
result = oauth.sign_in()
redirect_to(result.authorize_url)

# Step 2 — in your /callback route, validate state (CSRF) + exchange the code
tokens = oauth.handle_callback(request.url)           # OAuthTokens(access_token, ...)

# Step 3 — rotate an expired access token
tokens = oauth.refresh(tokens.refresh_token)

# Exchange an access token for a LiveKit join token
join = oauth.join_meeting(access_token=tokens.access_token, meeting_id=meeting.id)
connect_to_room(join.livekit_url, join.livekit_token)
```

Low-level PKCE helpers are also exported (`create_pkce_pair`, `build_authorize_url`,
`s256_challenge`, `random_verifier`/`random_state`/`random_nonce`) if you drive the
flow manually.

## Verifying webhooks

```python
from zenszoom.webhooks import verify_signature, parse_event

ok = verify_signature(
    secret=ENDPOINT_SECRET,
    body=request_body_bytes,
    signature=headers["X-ZensZoom-Signature"],
    timestamp=headers["X-ZensZoom-Timestamp"],
)
if ok:
    event = parse_event(request_body_bytes)
    if event["event_type"] == "recording.completed":
        store(event["payload"]["download_url"])
```

`verify_signature` accepts a comma-separated `v1=<hex>` header (dual-signing
during secret rotation) and enforces a 5-minute timestamp tolerance by default.

## API reference

| Method | Description |
|---|---|
| `ZenszoomClient(api_key, api_secret, *, base_url?)` | Construct a client |
| `create_meeting(*, title, mode?, class_id?)` | Create a meeting; returns `Meeting(id, title, status)` |
| `meeting_entry(meeting_id, *, host, participant)` | Mint entry tokens; returns `MeetingEntry` with `.start` and `.join` (`EntryPoint`) each having `.url` and `.token` |
| `start_meeting(meeting_id)` | Transition meeting to live |
| `get_attendance(meeting_id)` | Basic attendance — `AttendanceOut(meeting_id, rows)` |
| `get_attendance_detail(meeting_id)` | Enriched engagement detail — `AttendanceDetailOut` |
| `get_live_participants(meeting_id)` | Current live roster — `LiveRosterOut` |
| `get_recordings(meeting_id, *, limit?, cursor?, from_?, to?)` | Cursor-paginated recordings — `RecordingsOut` |
| `get_recording(recording_id)` | Single recording w/ signed URLs — `RecordingDetail` |
| `get_reports_overview()` | Org metrics — `OrgMetrics` |
| `get_attendance_report()` | Org-wide attendance summary — `AttendanceOverview` |
| `verify_signature(secret, *, body, signature, timestamp, tolerance?)` | Verify HMAC-SHA256 webhook signature |
| `parse_event(body)` | Deserialise raw webhook bytes to `dict` |
| `ZensZoomOAuth(*, client_id, redirect_uri?, client_secret?, base_url?, scope?, storage?)` | Sign-in-with-ZensZoom OAuth client |
| `.sign_in(...)` | Generate PKCE+state, persist, return authorize URL (`SignInResult`) |
| `.handle_callback(callback_url, ...)` | Validate state (CSRF) + exchange code → `OAuthTokens` |
| `.exchange_code(*, code, code_verifier, redirect_uri?)` | Exchange a code for tokens (public or confidential) |
| `.refresh(refresh_token)` | Rotate access token via refresh grant → `OAuthTokens` |
| `.join_meeting(*, access_token, meeting_id)` | Get a LiveKit join token → `JoinMeetingResult` |
