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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "sonilo"
version = "0.5.0"
version = "0.5.1"
description = "Official Python client for the Sonilo API"
readme = "README.md"
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion sonilo-cli/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "sonilo-cli"
version = "0.1.0"
version = "0.1.1"
description = "Command-line interface for the Sonilo API: generate music and sound effects from text or video"
readme = "README.md"
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion sonilo-cli/src/sonilo_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__version__ = "0.1.0"
__version__ = "0.1.1"

__all__ = ["__version__"]
4 changes: 3 additions & 1 deletion sonilo-cli/src/sonilo_cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ def build_client(api_key: Optional[str]) -> Sonilo:
"no API key — pass --api-key <key> or set the "
"SONILO_API_KEY environment variable"
)
return Sonilo(api_key=key)
# Identify as the CLI rather than inheriting the SDK's own name, so CLI
# traffic stays separable from direct SDK use in server-side analytics.
return Sonilo(api_key=key, client_name="cli-python", client_version=__version__)


def cmd_account(client: Sonilo, args: argparse.Namespace) -> None:
Expand Down
13 changes: 13 additions & 0 deletions sonilo-cli/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,3 +369,16 @@ def test_video_to_video_sound_requires_a_video_source():
with pytest.raises(SystemExit) as exc:
run(["video-to-video-sound"])
assert exc.value.code == 1


def test_cli_identifies_itself_not_the_sdk():
"""CLI traffic must be separable from direct SDK use in analytics."""
import sonilo_cli
from sonilo_cli.__main__ import build_client

client = build_client("sk-test")
try:
assert client._http.headers["x-sonilo-client"] == "cli-python"
assert client._http.headers["x-sonilo-client-version"] == sonilo_cli.__version__
finally:
client.close()
2 changes: 1 addition & 1 deletion sonilo-video-kit/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "sonilo-video-kit"
version = "0.1.1"
version = "0.1.2"
description = "Video helpers for the Sonilo API: generate a soundtrack and mix it into your video with ffmpeg"
readme = "README.md"
license = "MIT"
Expand Down
1 change: 1 addition & 0 deletions sonilo-video-kit/src/sonilo_video_kit/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.1.2"
6 changes: 5 additions & 1 deletion sonilo-video-kit/src/sonilo_video_kit/duck.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ def effective_download_cap(output_bytes: Optional[int]) -> int:
def _default_client() -> DuckingClient:
from sonilo import Sonilo

return Sonilo()
from sonilo_video_kit._version import __version__

# Only the kit's own default client is tagged; a caller-supplied client
# keeps whatever identity its owner gave it.
return Sonilo(client_name="videokit-python", client_version=__version__)


def _paid_note(task_id: str) -> str:
Expand Down
6 changes: 5 additions & 1 deletion sonilo-video-kit/src/sonilo_video_kit/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,9 @@ def generate_music_for_video(
) -> Any:
if client is None:
from sonilo import Sonilo
client = Sonilo()

from sonilo_video_kit._version import __version__
# Only the kit's own default client is tagged; a caller-supplied client
# keeps whatever identity its owner gave it.
client = Sonilo(client_name="videokit-python", client_version=__version__)
return client.video_to_music.generate(video=video, prompt=prompt, segments=segments)
17 changes: 17 additions & 0 deletions sonilo-video-kit/tests/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,20 @@

def test_import():
assert hasattr(sonilo_video_kit, "__all__")


def test_default_clients_identify_as_the_video_kit(monkeypatch):
"""Only the kit's own default client is tagged; a caller-supplied client
keeps its owner's identity."""
from sonilo_video_kit._version import __version__
from sonilo_video_kit.duck import _default_client

# _default_client() builds Sonilo() with no explicit key, so the SDK reads
# the environment — set one, or this fails wherever no key is configured.
monkeypatch.setenv("SONILO_API_KEY", "sk-test")
client = _default_client()
try:
assert client._http.headers["x-sonilo-client"] == "videokit-python"
assert client._http.headers["x-sonilo-client-version"] == __version__
finally:
client.close()
8 changes: 7 additions & 1 deletion src/sonilo/_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,17 @@ def __init__(
api_key: Optional[str] = None,
base_url: Optional[str] = None,
timeout: float = DEFAULT_TIMEOUT,
client_name: Optional[str] = None,
client_version: Optional[str] = None,
) -> None:
"""`client_name`/`client_version` identify a wrapper built on this SDK
(the CLI, the video kit) in the `X-Sonilo-Client` headers. Leave both
unset for direct SDK use.
"""
key = _resolve_api_key(api_key)
self._http = httpx.AsyncClient(
base_url=(base_url or DEFAULT_BASE_URL).rstrip("/"),
headers=_default_headers(key, "sdk-python"),
headers=_default_headers(key, client_name, client_version),
timeout=timeout,
)
self.text_to_music = AsyncTextToMusic(self)
Expand Down
23 changes: 19 additions & 4 deletions src/sonilo/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
DEFAULT_BASE_URL = "https://api.sonilo.com"
DEFAULT_TIMEOUT = 600.0

#: Reported in `X-Sonilo-Client` unless a wrapper overrides it. First-party
#: wrappers (the CLI, the video kit) pass their own name so their traffic stays
#: distinguishable from direct SDK use in server-side analytics.
DEFAULT_CLIENT_NAME = "sdk-python"


def _resolve_api_key(api_key: Optional[str]) -> str:
key = api_key or os.environ.get("SONILO_API_KEY")
Expand All @@ -33,11 +38,15 @@ def _resolve_api_key(api_key: Optional[str]) -> str:
return key


def _default_headers(api_key: str, client_name: str) -> Dict[str, str]:
def _default_headers(
api_key: str,
client_name: Optional[str] = None,
client_version: Optional[str] = None,
) -> Dict[str, str]:
return {
"Authorization": f"Bearer {api_key}",
"X-Sonilo-Client": client_name,
"X-Sonilo-Client-Version": __version__,
"X-Sonilo-Client": client_name or DEFAULT_CLIENT_NAME,
"X-Sonilo-Client-Version": client_version or __version__,
}


Expand All @@ -49,11 +58,17 @@ def __init__(
api_key: Optional[str] = None,
base_url: Optional[str] = None,
timeout: float = DEFAULT_TIMEOUT,
client_name: Optional[str] = None,
client_version: Optional[str] = None,
) -> None:
"""`client_name`/`client_version` identify a wrapper built on this SDK
(the CLI, the video kit) in the `X-Sonilo-Client` headers. Leave both
unset for direct SDK use.
"""
key = _resolve_api_key(api_key)
self._http = httpx.Client(
base_url=(base_url or DEFAULT_BASE_URL).rstrip("/"),
headers=_default_headers(key, "sdk-python"),
headers=_default_headers(key, client_name, client_version),
timeout=timeout,
)
self.text_to_music = TextToMusic(self)
Expand Down
2 changes: 1 addition & 1 deletion src/sonilo/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.5.0"
__version__ = "0.5.1"
82 changes: 82 additions & 0 deletions tests/test_client_identity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Client-identity headers.

First-party wrappers (the CLI, the video kit) sit on top of this SDK, so
without an override every one of their calls reports as `sdk-python` and
becomes indistinguishable from direct SDK use in server-side analytics.
"""

import httpx
import pytest
import respx

from sonilo import AsyncSonilo, Sonilo
from sonilo._client import DEFAULT_CLIENT_NAME
from sonilo._version import __version__

BASE = "https://api.sonilo.com"
SERVICES = {"available_services": []}


def _stub():
return respx.get(f"{BASE}/v1/account/services").mock(
return_value=httpx.Response(200, json=SERVICES)
)


@respx.mock
def test_defaults_to_sdk_python():
route = _stub()
Sonilo(api_key="sk-test").account.services()
headers = route.calls.last.request.headers
assert headers["x-sonilo-client"] == "sdk-python"
assert headers["x-sonilo-client-version"] == __version__


def test_default_client_name_constant():
assert DEFAULT_CLIENT_NAME == "sdk-python"


@respx.mock
def test_client_name_and_version_are_overridable():
route = _stub()
Sonilo(api_key="sk-test", client_name="cli-python", client_version="1.2.3").account.services()
headers = route.calls.last.request.headers
assert headers["x-sonilo-client"] == "cli-python"
assert headers["x-sonilo-client-version"] == "1.2.3"


@respx.mock
def test_overrides_are_independent():
"""Naming a wrapper must not force it to also restate a version, and vice versa."""
route = _stub()
Sonilo(api_key="sk-test", client_name="videokit-python").account.services()
headers = route.calls.last.request.headers
assert headers["x-sonilo-client"] == "videokit-python"
assert headers["x-sonilo-client-version"] == __version__

Sonilo(api_key="sk-test", client_version="9.9.9").account.services()
headers = route.calls.last.request.headers
assert headers["x-sonilo-client"] == "sdk-python"
assert headers["x-sonilo-client-version"] == "9.9.9"


@respx.mock
@pytest.mark.asyncio
async def test_async_client_honours_the_same_overrides():
route = _stub()
await AsyncSonilo(
api_key="sk-test", client_name="cli-python", client_version="1.2.3"
).account.services()
headers = route.calls.last.request.headers
assert headers["x-sonilo-client"] == "cli-python"
assert headers["x-sonilo-client-version"] == "1.2.3"


@respx.mock
@pytest.mark.asyncio
async def test_async_defaults_to_sdk_python():
route = _stub()
await AsyncSonilo(api_key="sk-test").account.services()
headers = route.calls.last.request.headers
assert headers["x-sonilo-client"] == "sdk-python"
assert headers["x-sonilo-client-version"] == __version__
Loading