Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/python-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: python lint

# Ruff lint gate for the published Python SDK. Enforces the E/F/I/N/W/UP/B/C4/SIM
# rule set (config lives in pyproject.toml [tool.ruff.lint]). The SDK's compressed
# one-liner house style (E701/E702) and public re-export imports are blessed there.
on:
push:
branches: [main]
pull_request:

permissions:
contents: read

concurrency:
group: python-lint-${{ github.ref }}
cancel-in-progress: true

jobs:
ruff:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- name: Install ruff
run: pip install ruff

@cubic-dev-ai cubic-dev-ai Bot Jun 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The lint workflow installs Ruff without a pinned version, making the required CI gate non-deterministic and prone to unrelated future breakages.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/python-lint.yml, line 30:

<comment>The lint workflow installs Ruff without a pinned version, making the required CI gate non-deterministic and prone to unrelated future breakages.</comment>

<file context>
@@ -0,0 +1,32 @@
+        with:
+          python-version: "3.12"
+      - name: Install ruff
+        run: pip install ruff
+      - name: Ruff check
+        run: ruff check
</file context>
Fix with cubic

- name: Ruff check
run: ruff check
14 changes: 13 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,20 @@ warn_unused_configs = true
[tool.ruff]
target-version = "py39"
line-length = 100

[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP", "B", "C4", "SIM"]
ignore = ["E501"]
# E501: line length is left to the formatter, not the linter.
# E701/E702: the SDK deliberately uses a compressed one-liner house style
# (multiple statements per line). Blessed by the maintainers.
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"]
# 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"]

[tool.black]
line-length = 100
Expand Down
43 changes: 34 additions & 9 deletions tests/test_sdk_exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,39 @@
def test_all_modules_import():
"""All 33 API modules should be importable from wave package."""
from wave import (
ClipsAPI, EditorAPI, VoiceAPI, PhoneAPI, CollabAPI,
CaptionsAPI, ChaptersAPI, StudioAIAPI, TranscribeAPI,
SentimentAPI, SearchAPI, SceneAPI,
PipelineAPI, StudioAPI,
FleetAPI, GhostAPI, MeshAPI, EdgeAPI, PulseAPI, PrismAPI, ZoomAPI,
VaultAPI, MarketplaceAPI, ConnectAPI, DistributionAPI,
DesktopAPI, SignageAPI, QrAPI, AudienceAPI, CreatorAPI,
PodcastAPI, SlidesAPI, UsbAPI,
AudienceAPI,
CaptionsAPI,
ChaptersAPI,
ClipsAPI,
CollabAPI,
ConnectAPI,
CreatorAPI,
DesktopAPI,
DistributionAPI,
EdgeAPI,
EditorAPI,
FleetAPI,
GhostAPI,
MarketplaceAPI,
MeshAPI,
PhoneAPI,
PipelineAPI,
PodcastAPI,
PrismAPI,
PulseAPI,
QrAPI,
SceneAPI,
SearchAPI,
SentimentAPI,
SignageAPI,
SlidesAPI,
StudioAIAPI,
StudioAPI,
TranscribeAPI,
UsbAPI,
VaultAPI,
VoiceAPI,
ZoomAPI,
)
# All should be classes
assert callable(ClipsAPI)
Expand All @@ -29,7 +54,7 @@ def test_all_modules_import():

def test_wave_client_import():
"""Core client classes should import."""
from wave import WaveClient, WaveError, RateLimitError
from wave import RateLimitError, WaveClient, WaveError
assert callable(WaveClient)
assert issubclass(WaveError, Exception)
assert issubclass(RateLimitError, WaveError)
Expand Down
66 changes: 33 additions & 33 deletions wave/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,55 +10,55 @@
>>> clips = client.clips.list()
"""

from wave.client import WaveClient, WaveError, RateLimitError
from wave.audience import AudienceAPI
from wave.captions import CaptionsAPI
from wave.chapters import ChaptersAPI
from wave.client import RateLimitError, WaveClient, WaveError

# Existing P3 modules
from wave.clips import ClipsAPI
from wave.editor import EditorAPI
from wave.voice import VoiceAPI
from wave.phone import PhoneAPI
from wave.collab import CollabAPI
from wave.captions import CaptionsAPI
from wave.chapters import ChaptersAPI
from wave.studio_ai import StudioAIAPI
from wave.transcribe import TranscribeAPI
from wave.sentiment import SentimentAPI
from wave.search import SearchAPI
from wave.scene import SceneAPI

# P1 modules
from wave.pipeline import PipelineAPI
from wave.studio import StudioAPI
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

# P2 modules
from wave.fleet import FleetAPI
from wave.ghost import GhostAPI
from wave.marketplace import MarketplaceAPI
from wave.mesh import MeshAPI
from wave.edge import EdgeAPI
from wave.pulse import PulseAPI
from wave.prism import PrismAPI
from wave.zoom import ZoomAPI

# P3 new modules
from wave.vault import VaultAPI
from wave.marketplace import MarketplaceAPI
from wave.connect import ConnectAPI
from wave.distribution import DistributionAPI
from wave.desktop import DesktopAPI
from wave.signage import SignageAPI
from wave.qr import QrAPI
from wave.audience import AudienceAPI
from wave.creator import CreatorAPI
# Cross-cutting
from wave.notifications import NotificationsAPI
from wave.phone import PhoneAPI

# P1 modules
from wave.pipeline import PipelineAPI

# P4 modules
from wave.podcast import PodcastAPI
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.usb import UsbAPI

# Cross-cutting
from wave.notifications import NotificationsAPI
from wave.drm import DrmAPI
from wave.realtime import RealtimeAPI, RealtimeChannel
# P3 new modules
from wave.vault import VaultAPI
from wave.voice import VoiceAPI
from wave.zoom import ZoomAPI

__version__ = "2.1.0"
__all__ = [
Expand Down
7 changes: 4 additions & 3 deletions wave/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from __future__ import annotations

from typing import Any, Callable, Optional
from typing import Any, Callable

import httpx


Expand Down Expand Up @@ -64,9 +65,9 @@ def __init__(
self,
api_key: str,
name: str = "stream-monitor",
stream_ids: Optional[list[str]] = None,
stream_ids: list[str] | None = None,
auto_remediate: bool = False,
on_quality_drop: Optional[Callable[..., Any]] = None,
on_quality_drop: Callable[..., Any] | None = None,
) -> None:
super().__init__(api_key=api_key, name=name, agent_type="stream_monitor")
self.stream_ids = stream_ids or []
Expand Down
5 changes: 4 additions & 1 deletion wave/audience.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"""WAVE SDK - Audience API. Polls, Q&A, reactions, and engagement."""
from __future__ import annotations

from typing import Any
from pydantic import BaseModel
from wave.client import WaveClient

from pydantic import BaseModel


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

Expand Down
5 changes: 4 additions & 1 deletion wave/captions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"""WAVE SDK - Captions API. Auto-generate, translate, burn-in, and manage captions."""
from __future__ import annotations

import time
from typing import Any
from pydantic import BaseModel
from wave.client import WaveClient

from pydantic import BaseModel


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

Expand Down
5 changes: 4 additions & 1 deletion wave/chapters.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"""WAVE SDK - Chapters API. Auto-generate and manage video chapters."""
from __future__ import annotations

import time
from typing import Any
from pydantic import BaseModel
from wave.client import WaveClient

from pydantic import BaseModel


class Chapter(BaseModel):
id: str; title: str; start_time: float; end_time: float; thumbnail_url: str | None = None; description: str | None = None

Expand Down
11 changes: 4 additions & 7 deletions wave/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@

from __future__ import annotations

import time
import logging
from typing import Any, TypeVar, Generic
from urllib.parse import urlencode
import time
from typing import Any, Generic, TypeVar

import httpx
from pydantic import BaseModel
Expand Down Expand Up @@ -43,9 +42,7 @@ def _is_retryable(self, status_code: int, code: str) -> bool:
return True
if 500 <= status_code < 600:
return True
if code in ("TIMEOUT", "NETWORK_ERROR", "SERVICE_UNAVAILABLE"):
return True
return False
return code in ("TIMEOUT", "NETWORK_ERROR", "SERVICE_UNAVAILABLE")

def __str__(self) -> str:
return f"WaveError({self.code}): {self.message}"
Expand Down Expand Up @@ -302,7 +299,7 @@ def close(self) -> None:
"""Close the HTTP client."""
self._client.close()

def __enter__(self) -> "WaveClient":
def __enter__(self) -> WaveClient:
return self

def __exit__(self, *args: Any) -> None:
Expand Down
4 changes: 2 additions & 2 deletions wave/clips.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@

import time
from typing import Any, Literal
from pydantic import BaseModel

from wave.client import WaveClient

from pydantic import BaseModel


class ClipSource(BaseModel):
"""Clip source reference."""
Expand Down
5 changes: 4 additions & 1 deletion wave/collab.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"""WAVE SDK - Collab API. Real-time collaboration rooms, participants, comments, and annotations."""
from __future__ import annotations

from typing import Any
from pydantic import BaseModel
from wave.client import WaveClient

from pydantic import BaseModel


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

Expand Down
5 changes: 4 additions & 1 deletion wave/connect.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"""WAVE SDK - Connect API. Third-party integration and webhook management."""
from __future__ import annotations

from typing import Any
from pydantic import BaseModel
from wave.client import WaveClient

from pydantic import BaseModel


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

Expand Down
5 changes: 4 additions & 1 deletion wave/creator.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"""WAVE SDK - Creator API. Monetization, subscriptions, tips, and payouts."""
from __future__ import annotations

from typing import Any
from pydantic import BaseModel
from wave.client import WaveClient

from pydantic import BaseModel


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

Expand Down
2 changes: 2 additions & 0 deletions wave/desktop.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""WAVE SDK - Desktop API. Desktop Node application management."""
from __future__ import annotations

from typing import Any
from wave.client import WaveClient


class DesktopAPI:
def __init__(self, client: WaveClient): self._client = client; self._base = "/v1/desktop"
def get_info(self, node_id: str) -> dict: return self._client.get(f"{self._base}/nodes/{node_id}")
Expand Down
5 changes: 4 additions & 1 deletion wave/distribution.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"""WAVE SDK - Distribution API. Social simulcasting and scheduled publishing."""
from __future__ import annotations

from typing import Any
from pydantic import BaseModel
from wave.client import WaveClient

from pydantic import BaseModel


class Destination(BaseModel):
id: str; organization_id: str; name: str; type: str; status: str; auto_start: bool = False; created_at: str; updated_at: str

Expand Down
5 changes: 4 additions & 1 deletion wave/drm.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"""WAVE SDK - DRM API. Content protection with Widevine, FairPlay, and PlayReady."""
from __future__ import annotations

from typing import Any
from pydantic import BaseModel
from wave.client import WaveClient

from pydantic import BaseModel


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

Expand Down
Loading
Loading