From 0260c2c59364cd7fd6a21f4c8b841a55f08bf773 Mon Sep 17 00:00:00 2001 From: Jake Fineman Date: Thu, 3 Sep 2026 04:12:04 -0400 Subject: [PATCH 1/2] fix(sdk): rename wave -> wave_sdk (stdlib shadow), fix quickstart routes, add fresh-install CI The published wave-sdk installs a top-level package literally named `wave`, which collides with the Python standard library's own `wave` module (WAV audio I/O). In a fresh `pip install wave-sdk` + `from wave import Wave` (the README's own quickstart), stdlib always wins (it precedes site-packages on sys.path), so the import raises ImportError on every supported Python version, in every environment except the repo checkout itself (where cwd masks the collision). Renamed the installable package to `wave_sdk`; `pip install wave-sdk` is unchanged. Also: the README/CHANGELOG quickstart called wave.pipeline / wave.prism / wave.pulse / wave.mail / wave.meter, none of which exist in the live public OpenAPI spec at api.wave.online/v1 (54 paths, verified live) - rewritten to search.search(), pricing.list_manifests(), transcribe.create(), captions.generate(), all confirmed against the real gateway. Entity corrected to "WAVE Online, LLC" (README, NOTICE, LICENSE, pyproject); CHANGELOG gains a 2.0.0 entry with the real PyPI upload date. User-Agent version no longer hardcoded, now derived from a single __version__. No duplicate PyPI publish workflow exists in this repo (re-verified: no publish/ release/twine/pypi workflow in .github/workflows or git history) - noted in the PR body rather than fabricated. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01K9mRh8G2ugbUt2kaXvFvF6 --- .github/workflows/smoke-install.yml | 78 +++++++++++++++++++++ CHANGELOG.md | 33 ++++++++- LICENSE | 2 +- NOTICE | 6 +- README.md | 48 +++++-------- pyproject.toml | 7 +- scripts/smoke_quickstart.py | 74 ++++++++++++++++++++ tests/conftest.py | 2 +- tests/test_contract_coverage.py | 2 +- tests/test_parity_apis.py | 23 +++--- tests/test_readme_quickstart.py | 12 ++-- tests/test_sdk_exports.py | 28 ++++---- tests/test_x402.py | 4 +- {wave => wave_sdk}/__init__.py | 104 ++++++++++++++-------------- {wave => wave_sdk}/agents.py | 0 {wave => wave_sdk}/audience.py | 3 +- {wave => wave_sdk}/captions.py | 3 +- {wave => wave_sdk}/chapters.py | 3 +- {wave => wave_sdk}/client.py | 9 ++- {wave => wave_sdk}/clips.py | 3 +- {wave => wave_sdk}/collab.py | 3 +- {wave => wave_sdk}/connect.py | 3 +- {wave => wave_sdk}/creator.py | 3 +- {wave => wave_sdk}/desktop.py | 3 +- {wave => wave_sdk}/distribution.py | 3 +- {wave => wave_sdk}/drm.py | 3 +- {wave => wave_sdk}/edge.py | 3 +- {wave => wave_sdk}/editor.py | 3 +- {wave => wave_sdk}/fleet.py | 3 +- {wave => wave_sdk}/ghost.py | 3 +- {wave => wave_sdk}/inference.py | 3 +- {wave => wave_sdk}/mail.py | 3 +- {wave => wave_sdk}/marketplace.py | 3 +- {wave => wave_sdk}/mesh.py | 3 +- {wave => wave_sdk}/meter.py | 3 +- {wave => wave_sdk}/notifications.py | 3 +- {wave => wave_sdk}/perception.py | 3 +- {wave => wave_sdk}/phone.py | 3 +- {wave => wave_sdk}/pipeline.py | 3 +- {wave => wave_sdk}/podcast.py | 3 +- {wave => wave_sdk}/pricing.py | 3 +- {wave => wave_sdk}/prism.py | 3 +- {wave => wave_sdk}/pulse.py | 3 +- {wave => wave_sdk}/py.typed | 0 {wave => wave_sdk}/qr.py | 3 +- {wave => wave_sdk}/realtime.py | 3 +- {wave => wave_sdk}/scene.py | 3 +- {wave => wave_sdk}/search.py | 3 +- {wave => wave_sdk}/sentiment.py | 3 +- {wave => wave_sdk}/signage.py | 3 +- {wave => wave_sdk}/slides.py | 3 +- {wave => wave_sdk}/studio.py | 3 +- {wave => wave_sdk}/studio_ai.py | 3 +- {wave => wave_sdk}/transcribe.py | 3 +- {wave => wave_sdk}/transcripts.py | 3 +- {wave => wave_sdk}/usb.py | 3 +- {wave => wave_sdk}/vault.py | 3 +- {wave => wave_sdk}/voice.py | 3 +- {wave => wave_sdk}/x402.py | 0 {wave => wave_sdk}/zoom.py | 3 +- 60 files changed, 388 insertions(+), 170 deletions(-) create mode 100644 .github/workflows/smoke-install.yml create mode 100644 scripts/smoke_quickstart.py rename {wave => wave_sdk}/__init__.py (66%) rename {wave => wave_sdk}/agents.py (100%) rename {wave => wave_sdk}/audience.py (98%) rename {wave => wave_sdk}/captions.py (99%) rename {wave => wave_sdk}/chapters.py (99%) rename {wave => wave_sdk}/client.py (96%) rename {wave => wave_sdk}/clips.py (99%) rename {wave => wave_sdk}/collab.py (99%) rename {wave => wave_sdk}/connect.py (98%) rename {wave => wave_sdk}/creator.py (98%) rename {wave => wave_sdk}/desktop.py (97%) rename {wave => wave_sdk}/distribution.py (98%) rename {wave => wave_sdk}/drm.py (98%) rename {wave => wave_sdk}/edge.py (98%) rename {wave => wave_sdk}/editor.py (99%) rename {wave => wave_sdk}/fleet.py (98%) rename {wave => wave_sdk}/ghost.py (97%) rename {wave => wave_sdk}/inference.py (99%) rename {wave => wave_sdk}/mail.py (98%) rename {wave => wave_sdk}/marketplace.py (97%) rename {wave => wave_sdk}/mesh.py (98%) rename {wave => wave_sdk}/meter.py (98%) rename {wave => wave_sdk}/notifications.py (97%) rename {wave => wave_sdk}/perception.py (99%) rename {wave => wave_sdk}/phone.py (99%) rename {wave => wave_sdk}/pipeline.py (98%) rename {wave => wave_sdk}/podcast.py (98%) rename {wave => wave_sdk}/pricing.py (98%) rename {wave => wave_sdk}/prism.py (98%) rename {wave => wave_sdk}/pulse.py (98%) rename {wave => wave_sdk}/py.typed (100%) rename {wave => wave_sdk}/qr.py (97%) rename {wave => wave_sdk}/realtime.py (99%) rename {wave => wave_sdk}/scene.py (99%) rename {wave => wave_sdk}/search.py (98%) rename {wave => wave_sdk}/sentiment.py (98%) rename {wave => wave_sdk}/signage.py (98%) rename {wave => wave_sdk}/slides.py (97%) rename {wave => wave_sdk}/studio.py (99%) rename {wave => wave_sdk}/studio_ai.py (99%) rename {wave => wave_sdk}/transcribe.py (99%) rename {wave => wave_sdk}/transcripts.py (97%) rename {wave => wave_sdk}/usb.py (97%) rename {wave => wave_sdk}/vault.py (98%) rename {wave => wave_sdk}/voice.py (99%) rename {wave => wave_sdk}/x402.py (100%) rename {wave => wave_sdk}/zoom.py (98%) diff --git a/.github/workflows/smoke-install.yml b/.github/workflows/smoke-install.yml new file mode 100644 index 0000000..d4a3d05 --- /dev/null +++ b/.github/workflows/smoke-install.yml @@ -0,0 +1,78 @@ +name: smoke install + +# Regression guard for the fresh-install class of bug: builds the wheel from +# this checkout, installs it (no `-e`, no repo on sys.path) into a throwaway +# venv, and proves the installed package imports and reaches the live WAVE +# gateway with the README's own quickstart. Running `pytest` from the repo +# checkout does NOT catch this class of bug (the checkout dir is first on +# sys.path and hides an import collision that only appears once the package +# is actually installed and run from elsewhere) — this workflow is the one +# gate that runs it the way a real `pip install wave-sdk` user does. +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: smoke-install-${{ github.ref }} + cancel-in-progress: true + +jobs: + smoke: + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.12", "3.13"] + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: ${{ matrix.python-version }} + + - name: Build wheel + run: | + python -m pip install --upgrade pip + pip install build + python -m build --wheel + + - name: Create fresh venv (no repo on sys.path) + run: python -m venv "$RUNNER_TEMP/smoke" + + - name: Install the built wheel + run: | + WHEEL=$(ls dist/*.whl) + "$RUNNER_TEMP/smoke/bin/pip" install --upgrade pip + "$RUNNER_TEMP/smoke/bin/pip" install "$WHEEL" + + - name: Import check (installed wheel, run away from the repo) + working-directory: ${{ runner.temp }}/smoke + run: | + bin/python -c " + import wave_sdk + print('wave_sdk', wave_sdk.__version__, 'imported from', wave_sdk.__file__) + from wave_sdk import Wave + print('Wave facade OK,', len([n for n in dir(wave_sdk) if n.endswith('API')]), 'API classes') + " + + - name: Copy quickstart into the smoke venv's working directory + run: cp scripts/smoke_quickstart.py "$RUNNER_TEMP/smoke/smoke_quickstart.py" + + - name: README quickstart (live gateway, real credentials) + working-directory: ${{ runner.temp }}/smoke + env: + WAVE_GATEWAY_API_KEY: ${{ secrets.WAVE_GATEWAY_API_KEY }} + run: | + if [ -z "$WAVE_GATEWAY_API_KEY" ]; then + echo "skipped: WAVE_GATEWAY_API_KEY absent (fork or unset)" + exit 0 + fi + bin/python smoke_quickstart.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 211234c..bfc016a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,26 @@ All notable changes to this project are documented here. The format is based on ## [Unreleased] -## [2.1.0] - 2026-09-01 +## [2.1.0] - 2026-09-01 (not yet published to PyPI) + +### Fixed + +- **Critical**: the top-level installable package was named `wave`, which + collides with the Python standard library's own `wave` module (WAV audio + I/O, `Lib/wave.py`, present in every CPython install). Because the stdlib + is earlier on `sys.path` than `site-packages`, a fresh `pip install + wave-sdk` followed by the README's own `from wave import Wave` resolved + to the STDLIB module and raised `ImportError: cannot import name 'Wave' + from 'wave'` — on every supported Python version, in every environment + except the SDK's own repo checkout (where the checkout directory being + first on `sys.path` masked the collision during development and in the + test suite). Verified live against the published 2.0.0 wheel from PyPI in + two isolated interpreters (3.14, 3.12); see the accompanying PR's LIVE + RECEIPTS. The installable package is renamed `wave_sdk` (`pip install + wave-sdk` still works; `from wave_sdk import Wave` now actually resolves + to the SDK). This does not change the 2.0.0 contract on PyPI — 2.0.0 was + never fixable in place and 2.1.0 has not shipped yet, so this lands before + the collision reaches a published release. ### Added @@ -41,7 +60,7 @@ to 42, matching the TS facade 1:1. 2026-09-01) has a corresponding Python method, or is in a justified allowlist (new backend surfaces neither SDK wraps yet, or pre-existing studio-ai drift that predates this release). -- `tests/test_readme_quickstart.py` - asserts every `wave..` +- `tests/test_readme_quickstart.py` - asserts every `client..` call in the README's quickstart resolves to a real SDK method. - Updated `tests/test_sdk_exports.py` for the new API count (42 + client) and version (2.1.0). @@ -50,3 +69,13 @@ to 42, matching the TS facade 1:1. - Bumped to 2.1.0 (additive, semver-minor): no existing method signature changed. + +## [2.0.0] - 2026-04-03 + +Initial public release of the WAVE Python SDK on PyPI as `wave-sdk`: 35 `*API` +classes covering streaming, production, analytics, and content workflows +(verified against the published wheel's `wave/__init__.py`; the PyPI package +`Summary` metadata for this release says "33 API modules", which undercounts +by 2 — a pre-existing metadata typo baked into the immutable 2.0.0 upload, +noted here rather than fixed retroactively since PyPI release metadata for a +published version cannot be edited). diff --git a/LICENSE b/LICENSE index 6b79e20..3345e13 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2026 WAVE, Inc. + Copyright 2026 WAVE Online, LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/NOTICE b/NOTICE index 018e56a..0bd6207 100644 --- a/NOTICE +++ b/NOTICE @@ -1,11 +1,11 @@ WAVE -Copyright 2026 WAVE, Inc. +Copyright 2026 WAVE Online, LLC -This product includes software developed at WAVE, Inc. +This product includes software developed at WAVE Online, LLC (https://wave.online). The names "WAVE" and "WAVE Surfer", the WAVE wordmark, and the WAVE logo -are trademarks of WAVE, Inc. and are NOT licensed under the Apache License, +are trademarks of WAVE Online, LLC and are NOT licensed under the Apache License, Version 2.0. The Apache License grants rights to the software in this repository only; it does not grant permission to use the WAVE marks except as required for reasonable and customary use in describing the origin of the diff --git a/README.md b/README.md index e7611fe..b577f9f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # WAVE SDK for Python -Official Python SDK for the WAVE API by WAVE Inc. +Media infrastructure for the agentic internet. Official Python SDK for WAVE, by +WAVE Online, LLC. ## Installation @@ -11,32 +12,19 @@ pip install wave-sdk ## Quick start ```python -from wave import Wave - -wave = Wave(api_key="your-api-key", organization_id="org_123") - -# Create and start a live stream -stream = wave.pipeline.create(title="My Stream", protocol="webrtc") -wave.pipeline.start(stream.id) -health = wave.pipeline.get_health(stream.id) -print(f"Viewers: {health['viewer_count']}") - -# Create a virtual camera from NDI -device = wave.prism.create_device( - name="PTZ Camera 1", - type="camera", - source_protocol="ndi", - source_endpoint="NDI-CAM-1", - node_id="node_abc", - ptz_enabled=True, -) - -# Get analytics -viewers = wave.pulse.get_viewer_analytics(time_range="24h") - -# Send a transcript email (mail:write) and read the usage ledger (meter:read) -wave.mail.transcript_email(to="alice@example.com", transcript="...") -ledger = wave.meter.ledger(channel="mail") +from wave_sdk import Wave + +client = Wave(api_key="your-api-key", organization_id="org_123") + +# Search your organization's indexed media +results = client.search.search(query="product launch") + +# List your org's published pricing tiers (requires the pricing:read scope) +manifests = client.pricing.list_manifests() + +# Transcribe a recording and auto-generate captions for it +transcription = client.transcribe.create(source_url="https://example.com/clip.mp4") +captions = client.captions.generate(media_id=transcription.id, media_type="video") ``` ## All 42 APIs @@ -116,10 +104,10 @@ ledger = wave.meter.ledger(channel="mail") ## Error handling ```python -from wave import WaveError, RateLimitError +from wave_sdk import WaveError, RateLimitError try: - wave.pipeline.get("invalid-id") + client.clips.get("invalid-id") except RateLimitError as e: print(f"Rate limited. Retry after {e.retry_after}s") except WaveError as e: @@ -134,4 +122,4 @@ except WaveError as e: ## License -MIT - WAVE Inc. +MIT - WAVE Online, LLC diff --git a/pyproject.toml b/pyproject.toml index 87eda6d..32fd0ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ readme = "README.md" license = {text = "MIT"} requires-python = ">=3.9" authors = [ - {name = "WAVE Inc.", email = "sdk@wave.online"} + {name = "WAVE Online, LLC", email = "sdk@wave.online"} ] keywords = [ "wave", @@ -44,6 +44,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Multimedia :: Video", "Topic :: Multimedia :: Sound/Audio", "Topic :: Software Development :: Libraries :: Python Modules", @@ -78,7 +79,7 @@ Issues = "https://github.com/wave-av/sdk-python/issues" [tool.setuptools.packages.find] where = ["."] -include = ["wave*"] +include = ["wave_sdk*"] [tool.mypy] python_version = "3.9" @@ -99,7 +100,7 @@ ignore = ["E501", "E701", "E702"] [tool.ruff.lint.per-file-ignores] # Public re-exports: the package __init__ exists to re-export the API surface. -"wave/__init__.py" = ["F401"] +"wave_sdk/__init__.py" = ["F401"] # Export-verification test imports every public symbol to assert it exists # (checked via __all__ / hasattr), so the names are intentionally "unused". "tests/test_sdk_exports.py" = ["F401"] diff --git a/scripts/smoke_quickstart.py b/scripts/smoke_quickstart.py new file mode 100644 index 0000000..faacae7 --- /dev/null +++ b/scripts/smoke_quickstart.py @@ -0,0 +1,74 @@ +"""CI fresh-install smoke: proves the INSTALLED WHEEL imports and reaches the +live WAVE gateway (https://api.wave.online), using the first two calls from +README.md's quickstart (search + pricing) — the ones that resolve to routes +confirmed live in the public OpenAPI spec and that respond deterministically +(200, or an auth/scope error) without depending on a real, fetchable media +URL. Never mocked: this is a real HTTP round trip against production. + +Exit 0 when the SDK reaches the gateway, whether or not the call is fully +authorized (a 402 Payment Required or a 403 SCOPE_INSUFFICIENT both prove the +request landed on a real, authenticating route). Exit 1 on anything that +indicates the *installed package itself* is broken (ImportError, or any +response that is not a recognized "reached the gateway" shape). + +Invoked by .github/workflows/smoke-install.yml against a wheel built from +this checkout, installed into a throwaway venv with no repo source on +sys.path — the class of bug this guards against (the SDK's own top-level +package shadowing Python's stdlib `wave` module) is invisible to `pytest` +run from the repo checkout, because the checkout directory being first on +sys.path masks the collision. Only an install-from-wheel-elsewhere run like +this one, or a real end user's environment, sees it. +""" +from __future__ import annotations + +import os +import sys + + +def main() -> int: + api_key = os.environ.get("WAVE_GATEWAY_API_KEY") + if not api_key: + print("skipped: WAVE_GATEWAY_API_KEY absent (fork or unset)") + return 0 + + # Import happens after the env-var short-circuit so a fork PR (no secret) + # still exercises the import path, which is the cheapest and most common + # way this class of bug shows up. + from wave_sdk import Wave, WaveError + + client = Wave(api_key=api_key, organization_id="org_123") + + reached_gateway = False + + try: + results = client.search.search(query="product launch") + print(f"OK: search.search() -> {len(results.get('results', []))} results") + reached_gateway = True + except WaveError as e: + if e.status_code in (402, 403): + print(f"OK (reached gateway, gated): search.search() -> {e.status_code} {e.code}") + reached_gateway = True + else: + print(f"FAIL: search.search() -> {e.status_code} {e.code}: {e.message}", file=sys.stderr) + + try: + client.pricing.list_manifests() + print("OK: pricing.list_manifests() -> 200") + reached_gateway = True + except WaveError as e: + if e.status_code in (402, 403): + print(f"OK (reached gateway, gated): pricing.list_manifests() -> {e.status_code} {e.code}") + reached_gateway = True + else: + print(f"FAIL: pricing.list_manifests() -> {e.status_code} {e.code}: {e.message}", file=sys.stderr) + + if not reached_gateway: + print("FAIL: neither quickstart call reached the gateway", file=sys.stderr) + return 1 + + print("QUICKSTART OK") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/conftest.py b/tests/conftest.py index 417a394..7c1e788 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,5 +9,5 @@ def api_key(): @pytest.fixture def wave_client(): - from wave import Wave + from wave_sdk import Wave return Wave(api_key="test-api-key", organization_id="org_test") diff --git a/tests/test_contract_coverage.py b/tests/test_contract_coverage.py index 1537d1c..2e793fb 100644 --- a/tests/test_contract_coverage.py +++ b/tests/test_contract_coverage.py @@ -145,7 +145,7 @@ def test_no_stale_mapping_or_allowlist_entries(): def test_mapped_methods_exist_on_wave(): - from wave import Wave + from wave_sdk import Wave w = Wave(api_key="test-key") missing = [] for op_id, (namespace, method) in MAPPING.items(): diff --git a/tests/test_parity_apis.py b/tests/test_parity_apis.py index 4434d99..e7f64b4 100644 --- a/tests/test_parity_apis.py +++ b/tests/test_parity_apis.py @@ -5,16 +5,17 @@ from __future__ import annotations from unittest.mock import MagicMock -from wave.inference import InferenceAPI -from wave.mail import MailAPI -from wave.meter import MeterAPI -from wave.perception import PerceptionAPI -from wave.pricing import ManifestCreateResult, PricingAPI, PricingManifest, PricingTier -from wave.transcripts import TranscriptAPI import httpx import pytest +from wave_sdk.inference import InferenceAPI +from wave_sdk.mail import MailAPI +from wave_sdk.meter import MeterAPI +from wave_sdk.perception import PerceptionAPI +from wave_sdk.pricing import ManifestCreateResult, PricingAPI, PricingManifest, PricingTier +from wave_sdk.transcripts import TranscriptAPI + @pytest.fixture def mock_client(): @@ -223,7 +224,7 @@ def fake_post(url, headers=None, json=None, timeout=None): "usage": {"cost": 0.0001, "total_tokens": 12}, }) - monkeypatch.setattr("wave.inference.httpx.post", fake_post) + monkeypatch.setattr("wave_sdk.inference.httpx.post", fake_post) api = InferenceAPI(FakeClient()) result = api.complete("claude-haiku", [{"role": "user", "content": "hi"}]) assert result.model == "claude-haiku" @@ -238,9 +239,9 @@ class FakeClient: def fake_post(url, headers=None, json=None, timeout=None): return httpx.Response(500, text="funnel down") - monkeypatch.setattr("wave.inference.httpx.post", fake_post) + monkeypatch.setattr("wave_sdk.inference.httpx.post", fake_post) api = InferenceAPI(FakeClient()) - from wave.client import WaveError + from wave_sdk.client import WaveError with pytest.raises(WaveError): api.complete("claude-haiku", [{"role": "user", "content": "hi"}]) @@ -250,7 +251,7 @@ class FakeClient: api_key = "test-key" api = InferenceAPI(FakeClient()) - from wave.client import WaveError + from wave_sdk.client import WaveError with pytest.raises(WaveError, match="registry_url"): api.models() @@ -263,7 +264,7 @@ def fake_get(url, headers=None, timeout=None): assert url.startswith("https://registry.example.com/rest/v1/models") return httpx.Response(200, json=[{"id": "m1", "rail": "openai", "cost_input_per_m": 1.0, "cost_output_per_m": 2.0}]) - monkeypatch.setattr("wave.inference.httpx.get", fake_get) + monkeypatch.setattr("wave_sdk.inference.httpx.get", fake_get) api = InferenceAPI(FakeClient(), registry_url="https://registry.example.com", registry_key="k") models = api.models() assert models[0].id == "m1" diff --git a/tests/test_readme_quickstart.py b/tests/test_readme_quickstart.py index 08b0a42..24a46ab 100644 --- a/tests/test_readme_quickstart.py +++ b/tests/test_readme_quickstart.py @@ -1,4 +1,4 @@ -"""Verifies every `wave..(` call referenced in README.md's +"""Verifies every `client..(` call referenced in README.md's quickstart resolves to a real attribute on the Wave facade — the README is documentation the way the SDK actually behaves, not a wish list.""" from __future__ import annotations @@ -7,19 +7,19 @@ from pathlib import Path README = (Path(__file__).parent.parent / "README.md").read_text() -CALL_RE = re.compile(r"\bwave\.(\w+)\.(\w+)\(") +CALL_RE = re.compile(r"\bclient\.(\w+)\.(\w+)\(") def test_readme_quickstart_calls_are_real(): - from wave import Wave + from wave_sdk import Wave w = Wave(api_key="test-key") calls = sorted(set(CALL_RE.findall(README))) - assert calls, "expected at least one wave..(...) call in README.md" + assert calls, "expected at least one client..(...) call in README.md" missing = [] for namespace, method in calls: ns = getattr(w, namespace, None) if ns is None: - missing.append(f"wave.{namespace} (namespace does not exist)") + missing.append(f"client.{namespace} (namespace does not exist)") elif not hasattr(ns, method): - missing.append(f"wave.{namespace}.{method} (method does not exist)") + missing.append(f"client.{namespace}.{method} (method does not exist)") assert not missing, f"README references methods that don't exist: {missing}" diff --git a/tests/test_sdk_exports.py b/tests/test_sdk_exports.py index 8520b15..42290cc 100644 --- a/tests/test_sdk_exports.py +++ b/tests/test_sdk_exports.py @@ -1,7 +1,7 @@ """ SDK Export Verification Tests -Validates that all 39 SDK modules import correctly, all API classes +Validates that all 42 SDK modules import correctly, all API classes instantiate with WaveClient, and the Wave convenience class wires everything. """ @@ -9,8 +9,8 @@ def test_all_modules_import(): - """All 39 API modules should be importable from wave package.""" - from wave import ( + """All 42 API modules should be importable from the wave_sdk package.""" + from wave_sdk import ( AudienceAPI, CaptionsAPI, ChaptersAPI, @@ -62,7 +62,7 @@ def test_all_modules_import(): def test_wave_client_import(): """Core client classes should import.""" - from wave import RateLimitError, WaveClient, WaveError + from wave_sdk import RateLimitError, WaveClient, WaveError assert callable(WaveClient) assert issubclass(WaveError, Exception) assert issubclass(RateLimitError, WaveError) @@ -70,14 +70,14 @@ def test_wave_client_import(): def test_wave_client_requires_api_key(): """WaveClient should raise ValueError without api_key.""" - from wave import WaveClient + from wave_sdk import WaveClient with pytest.raises(ValueError, match="api_key"): WaveClient(api_key="") def test_wave_convenience_class(): """Wave class should instantiate with all 33 API modules.""" - from wave import Wave + from wave_sdk import Wave w = Wave(api_key="test-key") # Existing P3 @@ -136,7 +136,7 @@ def test_wave_convenience_class(): def test_api_count(): """Wave class should have exactly 42 API bindings (+ client) — parity with the TS SDK's 42 Wave-facade namespaces (43 including the base client).""" - from wave import Wave + from wave_sdk import Wave w = Wave(api_key="test-key") api_attrs = [a for a in dir(w) if not a.startswith('_') and a != 'client'] assert len(api_attrs) == 42, f"Expected 42 APIs, got {len(api_attrs)}: {api_attrs}" @@ -144,7 +144,7 @@ def test_api_count(): def test_pipeline_has_methods(): """PipelineAPI should have expected methods.""" - from wave import Wave + from wave_sdk import Wave w = Wave(api_key="test-key") for method in ['create', 'get', 'list', 'start', 'stop', 'get_health', 'wait_for_live']: assert hasattr(w.pipeline, method), f"PipelineAPI missing {method}" @@ -152,7 +152,7 @@ def test_pipeline_has_methods(): def test_prism_has_methods(): """PrismAPI should have expected methods.""" - from wave import Wave + from wave_sdk import Wave w = Wave(api_key="test-key") for method in ['create_device', 'start_device', 'stop_device', 'discover_sources', 'get_presets', 'set_preset', 'recall_preset']: assert hasattr(w.prism, method), f"PrismAPI missing {method}" @@ -160,7 +160,7 @@ def test_prism_has_methods(): def test_studio_has_methods(): """StudioAPI should have expected methods.""" - from wave import Wave + from wave_sdk import Wave w = Wave(api_key="test-key") for method in ['create', 'start', 'stop', 'add_source', 'activate_scene', 'transition', 'set_program', 'get_audio_mix']: assert hasattr(w.studio, method), f"StudioAPI missing {method}" @@ -168,13 +168,13 @@ def test_studio_has_methods(): def test_version(): """SDK version should be 2.1.0.""" - import wave - assert wave.__version__ == "2.1.0" + import wave_sdk + assert wave_sdk.__version__ == "2.1.0" def test_all_exports(): """__all__ should contain all API classes.""" - import wave + import wave_sdk expected = [ "ClipsAPI", "EditorAPI", "VoiceAPI", "PhoneAPI", "CollabAPI", "CaptionsAPI", "ChaptersAPI", "StudioAIAPI", "TranscribeAPI", @@ -188,4 +188,4 @@ def test_all_exports(): "TranscriptAPI", "MailAPI", "MeterAPI", "PricingAPI", "PerceptionAPI", "InferenceAPI", ] for cls in expected: - assert cls in wave.__all__, f"{cls} missing from __all__" + assert cls in wave_sdk.__all__, f"{cls} missing from __all__" diff --git a/tests/test_x402.py b/tests/test_x402.py index 03a9397..33cd990 100644 --- a/tests/test_x402.py +++ b/tests/test_x402.py @@ -1,4 +1,4 @@ -"""Conformance tests for wave.x402 (EIP-3009 "exact" scheme signing). +"""Conformance tests for wave_sdk.x402 (EIP-3009 "exact" scheme signing). These assert the Python signer is byte-for-byte compatible with WAVE's reference TypeScript x402 signer (the same viem stack the WAVE facilitator verifies against). The expected values live in @@ -14,7 +14,7 @@ pytest.importorskip("eth_account", reason='x402 signing needs the extra: pip install "wave-sdk[x402]"') -from wave.x402 import ( # noqa: E402 (after importorskip) +from wave_sdk.x402 import ( # noqa: E402 (after importorskip) encode_exact_payment_header, get_network_config, random_nonce, diff --git a/wave/__init__.py b/wave_sdk/__init__.py similarity index 66% rename from wave/__init__.py rename to wave_sdk/__init__.py index 13e8a2e..588d01a 100644 --- a/wave/__init__.py +++ b/wave_sdk/__init__.py @@ -1,75 +1,75 @@ """ WAVE SDK for Python -Official Python SDK for the WAVE API by WAVE Inc. +Official Python SDK for WAVE, media infrastructure for the agentic internet, by +WAVE Online, LLC. Example: - >>> from wave import Wave + >>> from wave_sdk import Wave >>> client = Wave(api_key="your-api-key") >>> streams = client.pipeline.list() >>> clips = client.clips.list() """ -from wave.audience import AudienceAPI -from wave.captions import CaptionsAPI -from wave.chapters import ChaptersAPI -from wave.client import RateLimitError, WaveClient, WaveError +from wave_sdk.audience import AudienceAPI +from wave_sdk.captions import CaptionsAPI +from wave_sdk.chapters import ChaptersAPI +from wave_sdk.client import RateLimitError, WaveClient, WaveError, __version__ # Existing P3 modules -from wave.clips import ClipsAPI -from wave.collab import CollabAPI -from wave.connect import ConnectAPI -from wave.creator import CreatorAPI -from wave.desktop import DesktopAPI -from wave.distribution import DistributionAPI -from wave.drm import DrmAPI -from wave.edge import EdgeAPI -from wave.editor import EditorAPI +from wave_sdk.clips import ClipsAPI +from wave_sdk.collab import CollabAPI +from wave_sdk.connect import ConnectAPI +from wave_sdk.creator import CreatorAPI +from wave_sdk.desktop import DesktopAPI +from wave_sdk.distribution import DistributionAPI +from wave_sdk.drm import DrmAPI +from wave_sdk.edge import EdgeAPI +from wave_sdk.editor import EditorAPI # P2 modules -from wave.fleet import FleetAPI -from wave.ghost import GhostAPI -from wave.inference import InferenceAPI -from wave.mail import MailAPI -from wave.marketplace import MarketplaceAPI -from wave.mesh import MeshAPI -from wave.meter import MeterAPI +from wave_sdk.fleet import FleetAPI +from wave_sdk.ghost import GhostAPI +from wave_sdk.inference import InferenceAPI +from wave_sdk.mail import MailAPI +from wave_sdk.marketplace import MarketplaceAPI +from wave_sdk.mesh import MeshAPI +from wave_sdk.meter import MeterAPI # Cross-cutting -from wave.notifications import NotificationsAPI -from wave.perception import PerceptionAPI -from wave.phone import PhoneAPI +from wave_sdk.notifications import NotificationsAPI +from wave_sdk.perception import PerceptionAPI +from wave_sdk.phone import PhoneAPI # P1 modules -from wave.pipeline import PipelineAPI +from wave_sdk.pipeline import PipelineAPI # P4 modules -from wave.podcast import PodcastAPI -from wave.pricing import PricingAPI -from wave.prism import PrismAPI -from wave.pulse import PulseAPI -from wave.qr import QrAPI -from wave.realtime import RealtimeAPI, RealtimeChannel -from wave.scene import SceneAPI -from wave.search import SearchAPI -from wave.sentiment import SentimentAPI -from wave.signage import SignageAPI -from wave.slides import SlidesAPI -from wave.studio import StudioAPI -from wave.studio_ai import StudioAIAPI -from wave.transcribe import TranscribeAPI -from wave.transcripts import TranscriptAPI -from wave.usb import UsbAPI +from wave_sdk.podcast import PodcastAPI +from wave_sdk.pricing import PricingAPI +from wave_sdk.prism import PrismAPI +from wave_sdk.pulse import PulseAPI +from wave_sdk.qr import QrAPI +from wave_sdk.realtime import RealtimeAPI, RealtimeChannel +from wave_sdk.scene import SceneAPI +from wave_sdk.search import SearchAPI +from wave_sdk.sentiment import SentimentAPI +from wave_sdk.signage import SignageAPI +from wave_sdk.slides import SlidesAPI +from wave_sdk.studio import StudioAPI +from wave_sdk.studio_ai import StudioAIAPI +from wave_sdk.transcribe import TranscribeAPI +from wave_sdk.transcripts import TranscriptAPI +from wave_sdk.usb import UsbAPI # P3 new modules -from wave.vault import VaultAPI -from wave.voice import VoiceAPI +from wave_sdk.vault import VaultAPI +from wave_sdk.voice import VoiceAPI # x402 agent payments (signing needs the optional [x402] extra; the import itself is dependency-free) -from wave.x402 import encode_exact_payment_header, sign_exact_authorization -from wave.zoom import ZoomAPI +from wave_sdk.x402 import encode_exact_payment_header, sign_exact_authorization +from wave_sdk.zoom import ZoomAPI -__version__ = "2.1.0" __all__ = [ "Wave", "WaveClient", @@ -105,11 +105,11 @@ class Wave: Full WAVE SDK client with all APIs attached. Example: - >>> from wave import Wave - >>> wave = Wave(api_key="your-api-key", organization_id="org_123") - >>> streams = wave.pipeline.list() - >>> clips = wave.clips.list() - >>> wave.prism.discover_sources() + >>> from wave_sdk import Wave + >>> client = Wave(api_key="your-api-key", organization_id="org_123") + >>> streams = client.pipeline.list() + >>> clips = client.clips.list() + >>> client.prism.discover_sources() """ def __init__( diff --git a/wave/agents.py b/wave_sdk/agents.py similarity index 100% rename from wave/agents.py rename to wave_sdk/agents.py diff --git a/wave/audience.py b/wave_sdk/audience.py similarity index 98% rename from wave/audience.py rename to wave_sdk/audience.py index 435bf3a..5db1d96 100644 --- a/wave/audience.py +++ b/wave_sdk/audience.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class Poll(BaseModel): id: str; stream_id: str; question: str; options: list[dict]; status: str; total_votes: int = 0; created_at: str; updated_at: str diff --git a/wave/captions.py b/wave_sdk/captions.py similarity index 99% rename from wave/captions.py rename to wave_sdk/captions.py index f5b7f21..d5dd7f5 100644 --- a/wave/captions.py +++ b/wave_sdk/captions.py @@ -3,10 +3,11 @@ import time from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class CaptionTrack(BaseModel): id: str; asset_id: str; language: str; status: str; format: str; cue_count: int = 0; download_url: str | None = None; created_at: str; updated_at: str diff --git a/wave/chapters.py b/wave_sdk/chapters.py similarity index 99% rename from wave/chapters.py rename to wave_sdk/chapters.py index 427d7bc..7824a81 100644 --- a/wave/chapters.py +++ b/wave_sdk/chapters.py @@ -3,10 +3,11 @@ import time from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class Chapter(BaseModel): id: str; title: str; start_time: float; end_time: float; thumbnail_url: str | None = None; description: str | None = None diff --git a/wave/client.py b/wave_sdk/client.py similarity index 96% rename from wave/client.py rename to wave_sdk/client.py index 6739708..7f02a27 100644 --- a/wave/client.py +++ b/wave_sdk/client.py @@ -13,7 +13,12 @@ import httpx from pydantic import BaseModel -logger = logging.getLogger("wave") +logger = logging.getLogger("wave_sdk") + +# Single source of truth for the SDK version, used both in the package's +# public __version__ (re-exported from wave_sdk/__init__.py) and here in the +# default User-Agent header, so the two can never drift. +__version__ = "2.1.0" T = TypeVar("T") @@ -126,7 +131,7 @@ def _build_headers(self) -> dict[str, str]: "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", "Accept": "application/json", - "User-Agent": "wave-sdk-python/1.0.0", + "User-Agent": f"wave-sdk-python/{__version__}", } if self.organization_id: headers["X-Organization-Id"] = self.organization_id diff --git a/wave/clips.py b/wave_sdk/clips.py similarity index 99% rename from wave/clips.py rename to wave_sdk/clips.py index e39a37d..6668f36 100644 --- a/wave/clips.py +++ b/wave_sdk/clips.py @@ -8,10 +8,11 @@ import time from typing import Any, Literal -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class ClipSource(BaseModel): """Clip source reference.""" diff --git a/wave/collab.py b/wave_sdk/collab.py similarity index 99% rename from wave/collab.py rename to wave_sdk/collab.py index f5d458c..38dc36f 100644 --- a/wave/collab.py +++ b/wave_sdk/collab.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class CollabRoom(BaseModel): id: str; organization_id: str; name: str; status: str; participant_count: int = 0; max_participants: int = 50; settings: dict | None = None; created_at: str; updated_at: str diff --git a/wave/connect.py b/wave_sdk/connect.py similarity index 98% rename from wave/connect.py rename to wave_sdk/connect.py index 1b1eed7..c90077a 100644 --- a/wave/connect.py +++ b/wave_sdk/connect.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class Integration(BaseModel): id: str; organization_id: str; name: str; type: str; provider: str; status: str; scopes: list[str] | None = None; last_sync_at: str | None = None; error_message: str | None = None; created_at: str; updated_at: str diff --git a/wave/creator.py b/wave_sdk/creator.py similarity index 98% rename from wave/creator.py rename to wave_sdk/creator.py index fdb8ee0..d137be0 100644 --- a/wave/creator.py +++ b/wave_sdk/creator.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class CreatorProfile(BaseModel): id: str; user_id: str; display_name: str; subscriber_count: int = 0; total_revenue_cents: int = 0; verified: bool = False; tier: str = "starter"; created_at: str; updated_at: str diff --git a/wave/desktop.py b/wave_sdk/desktop.py similarity index 97% rename from wave/desktop.py rename to wave_sdk/desktop.py index 7b4b4e1..ad18923 100644 --- a/wave/desktop.py +++ b/wave_sdk/desktop.py @@ -2,7 +2,8 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient + +from wave_sdk.client import WaveClient class DesktopAPI: diff --git a/wave/distribution.py b/wave_sdk/distribution.py similarity index 98% rename from wave/distribution.py rename to wave_sdk/distribution.py index eba165f..ce12e75 100644 --- a/wave/distribution.py +++ b/wave_sdk/distribution.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class Destination(BaseModel): id: str; organization_id: str; name: str; type: str; status: str; auto_start: bool = False; created_at: str; updated_at: str diff --git a/wave/drm.py b/wave_sdk/drm.py similarity index 98% rename from wave/drm.py rename to wave_sdk/drm.py index d167305..452370f 100644 --- a/wave/drm.py +++ b/wave_sdk/drm.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class DRMPolicy(BaseModel): id: str; organization_id: str; name: str; providers: list[str]; allow_offline: bool = False; max_devices: int = 1; output_protection: str = "none"; persistent_license: bool = False; created_at: str; updated_at: str diff --git a/wave/edge.py b/wave_sdk/edge.py similarity index 98% rename from wave/edge.py rename to wave_sdk/edge.py index fc9dbaf..61bcc3d 100644 --- a/wave/edge.py +++ b/wave_sdk/edge.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class EdgeNode(BaseModel): id: str; name: str; region: str; provider: str; status: str; latency_ms: int; capacity_percent: float; active_workers: int; bandwidth_mbps: float; created_at: str; updated_at: str diff --git a/wave/editor.py b/wave_sdk/editor.py similarity index 99% rename from wave/editor.py rename to wave_sdk/editor.py index 283a0d2..069a4c5 100644 --- a/wave/editor.py +++ b/wave_sdk/editor.py @@ -3,10 +3,11 @@ import time from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class EditorProject(BaseModel): id: str; organization_id: str; title: str; status: str; duration: float = 0; track_count: int = 0; created_at: str; updated_at: str diff --git a/wave/fleet.py b/wave_sdk/fleet.py similarity index 98% rename from wave/fleet.py rename to wave_sdk/fleet.py index 87ce5d7..eb29f1d 100644 --- a/wave/fleet.py +++ b/wave_sdk/fleet.py @@ -3,10 +3,11 @@ import time from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class FleetNode(BaseModel): id: str; organization_id: str; name: str; status: str; health: str; ip_address: str | None = None; version: str | None = None; os: str | None = None; cpu_usage: float = 0; memory_usage: float = 0; device_count: int = 0; last_seen_at: str | None = None; created_at: str; updated_at: str diff --git a/wave/ghost.py b/wave_sdk/ghost.py similarity index 97% rename from wave/ghost.py rename to wave_sdk/ghost.py index b836d38..c79dd57 100644 --- a/wave/ghost.py +++ b/wave_sdk/ghost.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class GhostSession(BaseModel): id: str; production_id: str; mode: str; style: str; status: str; confidence_threshold: float = 0.7; created_at: str | None = None diff --git a/wave/inference.py b/wave_sdk/inference.py similarity index 99% rename from wave/inference.py rename to wave_sdk/inference.py index 401d52f..ad2d576 100644 --- a/wave/inference.py +++ b/wave_sdk/inference.py @@ -13,11 +13,12 @@ from __future__ import annotations from typing import Any, Literal -from wave.client import WaveClient, WaveError import httpx from pydantic import BaseModel +from wave_sdk.client import WaveClient, WaveError + class InferenceMessage(BaseModel): role: Literal["system", "user", "assistant", "tool"] diff --git a/wave/mail.py b/wave_sdk/mail.py similarity index 98% rename from wave/mail.py rename to wave_sdk/mail.py index 0fabc95..62241c2 100644 --- a/wave/mail.py +++ b/wave_sdk/mail.py @@ -9,10 +9,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class SendResult(BaseModel): message_id: str | None = None diff --git a/wave/marketplace.py b/wave_sdk/marketplace.py similarity index 97% rename from wave/marketplace.py rename to wave_sdk/marketplace.py index 607cff1..8e6e78f 100644 --- a/wave/marketplace.py +++ b/wave_sdk/marketplace.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class MarketplaceItem(BaseModel): id: str; name: str; description: str; type: str; status: str; author_name: str; version: str; price_cents: int = 0; downloads: int = 0; rating: float = 0; tags: list[str] | None = None; category: str; created_at: str; updated_at: str diff --git a/wave/mesh.py b/wave_sdk/mesh.py similarity index 98% rename from wave/mesh.py rename to wave_sdk/mesh.py index dedb788..221a5a7 100644 --- a/wave/mesh.py +++ b/wave_sdk/mesh.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class MeshRegion(BaseModel): id: str; name: str; provider: str; location: str; status: str; latency_ms: int; capacity_percent: float; stream_count: int; viewer_count: int; is_primary: bool; created_at: str; updated_at: str diff --git a/wave/meter.py b/wave_sdk/meter.py similarity index 98% rename from wave/meter.py rename to wave_sdk/meter.py index a9b86fc..7f41cd9 100644 --- a/wave/meter.py +++ b/wave_sdk/meter.py @@ -7,10 +7,11 @@ from __future__ import annotations from typing import Literal -from wave.client import WaveClient from pydantic import BaseModel, ConfigDict, Field +from wave_sdk.client import WaveClient + class MeterMailChannel(BaseModel): ops: int; usdc: str; errors: int diff --git a/wave/notifications.py b/wave_sdk/notifications.py similarity index 97% rename from wave/notifications.py rename to wave_sdk/notifications.py index 50c7fb5..c118c9a 100644 --- a/wave/notifications.py +++ b/wave_sdk/notifications.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class Notification(BaseModel): id: str; user_id: str; type: str; title: str; body: str; status: str; priority: str; channel: str; action_url: str | None = None; read_at: str | None = None; created_at: str; updated_at: str diff --git a/wave/perception.py b/wave_sdk/perception.py similarity index 99% rename from wave/perception.py rename to wave_sdk/perception.py index c8a91d0..805b7eb 100644 --- a/wave/perception.py +++ b/wave_sdk/perception.py @@ -19,10 +19,11 @@ from __future__ import annotations from typing import Any, Literal -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + PerceptionTransport = Literal["whep", "srt"] PerceptionSampleMode = Literal["adaptive", "fixed", "keyframe"] PerceptionAudioMode = Literal["transcribe", "raw", "off"] diff --git a/wave/phone.py b/wave_sdk/phone.py similarity index 99% rename from wave/phone.py rename to wave_sdk/phone.py index 01c1fd5..5c4f1f9 100644 --- a/wave/phone.py +++ b/wave_sdk/phone.py @@ -3,10 +3,11 @@ import time from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class PhoneNumber(BaseModel): id: str; number: str; type: str; capabilities: list[str] | None = None; status: str; region: str | None = None; created_at: str; updated_at: str diff --git a/wave/pipeline.py b/wave_sdk/pipeline.py similarity index 98% rename from wave/pipeline.py rename to wave_sdk/pipeline.py index e645794..040ecae 100644 --- a/wave/pipeline.py +++ b/wave_sdk/pipeline.py @@ -3,10 +3,11 @@ import time from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class Stream(BaseModel): id: str; organization_id: str; title: str; description: str | None = None; status: str; protocol: str; ingest_url: str | None = None; playback_url: str | None = None; stream_key: str | None = None; resolution: str | None = None; frame_rate: int | None = None; bitrate_kbps: int | None = None; viewer_count: int = 0; recording_enabled: bool = False; tags: list[str] | None = None; metadata: dict[str, Any] | None = None; started_at: str | None = None; ended_at: str | None = None; created_at: str; updated_at: str diff --git a/wave/podcast.py b/wave_sdk/podcast.py similarity index 98% rename from wave/podcast.py rename to wave_sdk/podcast.py index 31a3b3e..9e25b56 100644 --- a/wave/podcast.py +++ b/wave_sdk/podcast.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class Podcast(BaseModel): id: str; organization_id: str; title: str; description: str; category: str; language: str = "en"; episode_count: int = 0; subscriber_count: int = 0; rss_url: str | None = None; created_at: str; updated_at: str diff --git a/wave/pricing.py b/wave_sdk/pricing.py similarity index 98% rename from wave/pricing.py rename to wave_sdk/pricing.py index ab1b063..c16b196 100644 --- a/wave/pricing.py +++ b/wave_sdk/pricing.py @@ -9,10 +9,11 @@ from __future__ import annotations from typing import Literal -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class PricingTier(BaseModel): id: str diff --git a/wave/prism.py b/wave_sdk/prism.py similarity index 98% rename from wave/prism.py rename to wave_sdk/prism.py index 06f1690..d599c94 100644 --- a/wave/prism.py +++ b/wave_sdk/prism.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class VirtualDevice(BaseModel): id: str; organization_id: str; name: str; type: str; status: str; source_protocol: str; source_endpoint: str; node_id: str; resolution: dict | None = None; frame_rate: int | None = None; health_score: float = 100; ptz_enabled: bool = False; created_at: str; updated_at: str diff --git a/wave/pulse.py b/wave_sdk/pulse.py similarity index 98% rename from wave/pulse.py rename to wave_sdk/pulse.py index 4548582..34f538d 100644 --- a/wave/pulse.py +++ b/wave_sdk/pulse.py @@ -2,7 +2,8 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient + +from wave_sdk.client import WaveClient class PulseAPI: diff --git a/wave/py.typed b/wave_sdk/py.typed similarity index 100% rename from wave/py.typed rename to wave_sdk/py.typed diff --git a/wave/qr.py b/wave_sdk/qr.py similarity index 97% rename from wave/qr.py rename to wave_sdk/qr.py index 099ac40..ff41e2f 100644 --- a/wave/qr.py +++ b/wave_sdk/qr.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class QRCode(BaseModel): id: str; organization_id: str; type: str; content: str; short_url: str; image_url: str; scan_count: int = 0; status: str = "active"; expires_at: str | None = None; created_at: str; updated_at: str diff --git a/wave/realtime.py b/wave_sdk/realtime.py similarity index 99% rename from wave/realtime.py rename to wave_sdk/realtime.py index 3f3de2d..363b1ca 100644 --- a/wave/realtime.py +++ b/wave_sdk/realtime.py @@ -14,10 +14,11 @@ import json from collections.abc import Iterator from typing import Any, Callable -from wave.client import WaveClient import httpx +from wave_sdk.client import WaveClient + _DEFAULT_WS = "wss://realtime.wave.online" diff --git a/wave/scene.py b/wave_sdk/scene.py similarity index 99% rename from wave/scene.py rename to wave_sdk/scene.py index 0498ec7..682c867 100644 --- a/wave/scene.py +++ b/wave_sdk/scene.py @@ -3,10 +3,11 @@ import time from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class Scene(BaseModel): id: str; start_time: float; end_time: float; scene_type: str; shot_type: str | None = None; confidence: float; labels: list[str] | None = None; thumbnail_url: str | None = None diff --git a/wave/search.py b/wave_sdk/search.py similarity index 98% rename from wave/search.py rename to wave_sdk/search.py index c8a54cd..fbb6f25 100644 --- a/wave/search.py +++ b/wave_sdk/search.py @@ -3,10 +3,11 @@ import time from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class SearchResult(BaseModel): id: str; type: str; title: str; score: float; highlights: list[dict] | None = None; thumbnail_url: str | None = None; created_at: str diff --git a/wave/sentiment.py b/wave_sdk/sentiment.py similarity index 98% rename from wave/sentiment.py rename to wave_sdk/sentiment.py index a8e21c7..6df80ab 100644 --- a/wave/sentiment.py +++ b/wave_sdk/sentiment.py @@ -3,10 +3,11 @@ import time from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class SentimentAnalysis(BaseModel): id: str; asset_id: str; status: str; overall_sentiment: str | None = None; overall_score: float = 0; segments_count: int = 0; created_at: str; updated_at: str diff --git a/wave/signage.py b/wave_sdk/signage.py similarity index 98% rename from wave/signage.py rename to wave_sdk/signage.py index 57c6ecb..127e1e6 100644 --- a/wave/signage.py +++ b/wave_sdk/signage.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class Display(BaseModel): id: str; organization_id: str; name: str; status: str; resolution: str | None = None; orientation: str = "landscape"; location: str | None = None; current_playlist_id: str | None = None; last_seen_at: str | None = None; created_at: str; updated_at: str diff --git a/wave/slides.py b/wave_sdk/slides.py similarity index 97% rename from wave/slides.py rename to wave_sdk/slides.py index ec7bb6c..c941bdf 100644 --- a/wave/slides.py +++ b/wave_sdk/slides.py @@ -3,10 +3,11 @@ import time from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class Conversion(BaseModel): id: str; organization_id: str; title: str; status: str; input_format: str; input_url: str; output_url: str | None = None; slide_count: int = 0; duration_seconds: float | None = None; progress_percent: int = 0; error: str | None = None; created_at: str; updated_at: str diff --git a/wave/studio.py b/wave_sdk/studio.py similarity index 99% rename from wave/studio.py rename to wave_sdk/studio.py index c271078..b1a2e97 100644 --- a/wave/studio.py +++ b/wave_sdk/studio.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class Production(BaseModel): id: str; organization_id: str; title: str; description: str | None = None; status: str; program_source_id: str | None = None; preview_source_id: str | None = None; active_scene_id: str | None = None; recording_enabled: bool = False; streaming_enabled: bool = False; viewer_count: int = 0; started_at: str | None = None; ended_at: str | None = None; created_at: str; updated_at: str diff --git a/wave/studio_ai.py b/wave_sdk/studio_ai.py similarity index 99% rename from wave/studio_ai.py rename to wave_sdk/studio_ai.py index ab9188f..47ba745 100644 --- a/wave/studio_ai.py +++ b/wave_sdk/studio_ai.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class AIAssistant(BaseModel): id: str; production_id: str | None = None; stream_id: str | None = None; mode: str; status: str; config: dict | None = None; stats: dict | None = None; started_at: str | None = None; created_at: str; updated_at: str diff --git a/wave/transcribe.py b/wave_sdk/transcribe.py similarity index 99% rename from wave/transcribe.py rename to wave_sdk/transcribe.py index 2415fee..2995e23 100644 --- a/wave/transcribe.py +++ b/wave_sdk/transcribe.py @@ -3,10 +3,11 @@ import time from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class Speaker(BaseModel): id: str; label: str; segments_count: int = 0; total_duration: float = 0 diff --git a/wave/transcripts.py b/wave_sdk/transcripts.py similarity index 97% rename from wave/transcripts.py rename to wave_sdk/transcripts.py index e1b618f..0030090 100644 --- a/wave/transcripts.py +++ b/wave_sdk/transcripts.py @@ -4,10 +4,11 @@ from __future__ import annotations from typing import Any, Literal -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class TranscriptMessage(BaseModel): role: Literal["system", "user", "assistant"] diff --git a/wave/usb.py b/wave_sdk/usb.py similarity index 97% rename from wave/usb.py rename to wave_sdk/usb.py index 4510fb7..d196e1b 100644 --- a/wave/usb.py +++ b/wave_sdk/usb.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class USBDevice(BaseModel): id: str; node_id: str; name: str; vendor_id: str; product_id: str; device_class: str; status: str; manufacturer: str | None = None; speed: str; capabilities: list[str] | None = None; connected_at: str; updated_at: str diff --git a/wave/vault.py b/wave_sdk/vault.py similarity index 98% rename from wave/vault.py rename to wave_sdk/vault.py index 672301c..d4437af 100644 --- a/wave/vault.py +++ b/wave_sdk/vault.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class Recording(BaseModel): id: str; organization_id: str; stream_id: str | None = None; title: str; status: str; duration_seconds: float = 0; file_size_bytes: int = 0; format: str | None = None; storage_tier: str = "hot"; playback_url: str | None = None; download_url: str | None = None; tags: list[str] | None = None; created_at: str; updated_at: str diff --git a/wave/voice.py b/wave_sdk/voice.py similarity index 99% rename from wave/voice.py rename to wave_sdk/voice.py index bc4ccd3..fa7d02d 100644 --- a/wave/voice.py +++ b/wave_sdk/voice.py @@ -3,10 +3,11 @@ import time from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class Voice(BaseModel): id: str; name: str; language: str; gender: str | None = None; model_type: str; preview_url: str | None = None; is_custom: bool = False; created_at: str; updated_at: str diff --git a/wave/x402.py b/wave_sdk/x402.py similarity index 100% rename from wave/x402.py rename to wave_sdk/x402.py diff --git a/wave/zoom.py b/wave_sdk/zoom.py similarity index 98% rename from wave/zoom.py rename to wave_sdk/zoom.py index b7d1d72..f306a2d 100644 --- a/wave/zoom.py +++ b/wave_sdk/zoom.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import Any -from wave.client import WaveClient from pydantic import BaseModel +from wave_sdk.client import WaveClient + class ZoomMeeting(BaseModel): id: str; topic: str; type: str; status: str; start_url: str; join_url: str; host_id: str; duration_minutes: int; participants_count: int = 0; recording_enabled: bool = False; rtms_enabled: bool = False; created_at: str From 0497ef8ce7d88f0166f89537ca76c6984c412a4f Mon Sep 17 00:00:00 2001 From: Jake Fineman Date: Thu, 3 Sep 2026 04:19:13 -0400 Subject: [PATCH 2/2] fix(sdk): py3.9 install was broken too (missing eval-type-backport + future annotations) The new smoke-install CI (this PR) caught a second, independent fresh-install defect on its first real run, on the Python 3.9 leg of the matrix: `pip install wave-sdk` + `import wave_sdk` raised TypeError on 3.9 (both stock CPython 3.9.6 and the CI runner's 3.9.25), because: 1. pydantic 2.x models use `str | None`-style PEP 604 unions under `from __future__ import annotations`, which on Python <3.10 requires the optional `eval-type-backport` package to resolve at runtime - it was not declared as a dependency despite requires-python allowing 3.9. 2. `wave_sdk/__init__.py` itself (the `Wave` facade class, not a pydantic model) used `str | None` in a plain function signature with no `from __future__ import annotations` in that file, so Python evaluated the annotation eagerly at class-definition time and hit the same TypeError - the pydantic backport does not help here since this code path never goes through pydantic. Verified against a real Python 3.9.6 interpreter: import + Wave facade + live quickstart (search.search -> 200, pricing.list_manifests -> 403 SCOPE_INSUFFICIENT) all pass post-fix. Confirmed the smoke (3.9) CI job on this PR failed before this commit and is expected to pass after it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01K9mRh8G2ugbUt2kaXvFvF6 --- pyproject.toml | 5 +++++ wave_sdk/__init__.py | 1 + 2 files changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 32fd0ea..eaa7500 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,11 @@ classifiers = [ dependencies = [ "httpx>=0.25.0", "pydantic>=2.0.0", + # pydantic 2.x needs this to resolve `str | None`-style PEP 604 unions on + # Python 3.9, where `str | None` cannot be `eval()`'d natively (the SDK's + # models use `from __future__ import annotations` + the new union syntax + # for readability; requires-python allows 3.9, so this isn't optional). + "eval-type-backport>=0.2.0; python_version < '3.10'", ] [project.optional-dependencies] diff --git a/wave_sdk/__init__.py b/wave_sdk/__init__.py index 588d01a..0d9dff4 100644 --- a/wave_sdk/__init__.py +++ b/wave_sdk/__init__.py @@ -10,6 +10,7 @@ >>> streams = client.pipeline.list() >>> clips = client.clips.list() """ +from __future__ import annotations from wave_sdk.audience import AudienceAPI from wave_sdk.captions import CaptionsAPI