breeze-blue 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ Initial release.
6
+
7
+ - ElevenLabs-compatible client surface for text-to-speech, voices, text-to-voice, history, models, and account APIs.
8
+ - Typed errors for authentication, rate limiting, validation, billing, upstream, and service-unavailable failures.
9
+ - Audio playback helpers using ffplay, afplay on macOS, and mpv for streaming-style playback.
10
+ - `xi-api-key` auth header and `x-breeze-sdk` telemetry header.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Breeze Blue
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ include CHANGELOG.md
2
+ include LICENSE
3
+ include README.md
4
+ prune tests
5
+ prune src/*.egg-info
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: breeze-blue
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the Breeze Blue Developer API.
5
+ Author-email: Breeze Blue <support@breeze.blue>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://breeze.blue
8
+ Project-URL: Documentation, https://breeze.blue/docs
9
+ Keywords: audio,breeze,breeze-blue,elevenlabs,elevenlabs-alternative,sdk,text-to-speech,tts,voice
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: httpx<1.0.0,>=0.28.0
21
+ Dynamic: license-file
22
+
23
+ # Breeze Blue Python SDK
24
+
25
+ Local v0.1 Python SDK for the Breeze Blue Developer API.
26
+
27
+ ## Install
28
+
29
+ Before the package is published to PyPI, install it from this monorepo:
30
+
31
+ ```bash
32
+ uv sync
33
+ uv pip install -e ./packages/breeze-python
34
+ ```
35
+
36
+ After the first public release, install the same SDK from PyPI:
37
+
38
+ ```bash
39
+ uv add breeze-blue
40
+ ```
41
+
42
+ ```python
43
+ from breeze_blue import BreezeBlue, play, save, stream
44
+
45
+ client = BreezeBlue() # Reads BREEZE_API_KEY and defaults to https://api.breeze.blue
46
+
47
+ audio = client.text_to_speech.convert(
48
+ "voc_demo",
49
+ text="Hello from Breeze Blue.",
50
+ output_format="mp3_44100_128",
51
+ )
52
+ play(audio)
53
+ save(audio, "hello.mp3")
54
+ ```
55
+
56
+ ## Client
57
+
58
+ ```python
59
+ client = BreezeBlue(
60
+ api_key="brz_secret",
61
+ base_url="https://api.breeze.blue",
62
+ )
63
+ ```
64
+
65
+ Authentication uses the ElevenLabs-compatible `xi-api-key` header to make migration easier. If `api_key` is omitted, the SDK reads `BREEZE_API_KEY`. If `base_url` is omitted, the SDK reads `BREEZE_BASE_URL` and then falls back to `https://api.breeze.blue`.
66
+
67
+ For local development, pass `base_url="http://127.0.0.1:8000"` or set `BREEZE_BASE_URL`.
68
+
69
+ Set a default client timeout with `BreezeBlue(timeout=120.0)`, or override a single call with `timeout=120.0`.
70
+
71
+ ## Resources
72
+
73
+ - `client.text_to_speech.convert(voice_id, text=...)`
74
+ - `client.text_to_speech.stream(voice_id, text=...)`
75
+ - `client.voices.search(...)`
76
+ - `client.voices.get(voice_id)`
77
+ - `client.voices.ivc.create_preview(name=..., file=...)`
78
+ - `client.voices.ivc.edit(voice_id, name=..., file=...)`
79
+ - `client.voices.ivc.get_settings(voice_id)` / `client.voices.ivc.update_settings(voice_id, guidance_scale=...)`
80
+ - `client.voices.ivc.delete(voice_id)`
81
+ - `client.text_to_voice.design(voice_description=...)`
82
+ - `client.text_to_voice.create(generated_voice_id=..., voice_name=...)`
83
+ - `client.text_to_voice.stream_preview(generated_voice_id)`
84
+ - `client.history.list(...)`
85
+ - `client.history.get(history_item_id)`
86
+ - `client.history.download_audio(history_item_id)`
87
+ - `client.history.delete(history_item_id)`
88
+ - `client.models.list()`
89
+ - `client.account.balance()`
90
+ - `client.account.usage(...)`
91
+
92
+ All resource methods accept `timeout=<seconds>` to override the client default for a single call.
93
+ Common response shapes are exported as `TypedDict` types from `breeze_blue.types` and from the package root.
94
+
95
+ ## Audio Helpers
96
+
97
+ Audio endpoints return `AudioResponse` with:
98
+
99
+ - `content`
100
+ - `content_type`
101
+ - `history_item_id`
102
+
103
+ Helpers:
104
+
105
+ - `audio.save(path)`
106
+ - `audio.play()` uses `ffplay`, with macOS `afplay` fallback, and waits for playback to finish.
107
+ - `audio.stream()` pipes the buffered response content to `mpv` and waits for playback to finish.
108
+ - `play(audio)`, `save(audio, path)`, and `stream(audio)` are also exported
109
+ from `breeze_blue` for ElevenLabs-style examples.
110
+ - `stream(audio)` also accepts an iterable of byte chunks for chunked playback.
111
+ - `start_play(audio)` and `start_stream(audio)` are available for advanced
112
+ fire-and-forget process control.
113
+
114
+ ## Errors
115
+
116
+ All API errors inherit from `BreezeBlueError`. HTTP and Breeze error codes are mapped to typed exceptions including `BadRequestError`, `AuthenticationError`, `ForbiddenError`, `NotFoundError`, `ConflictError`, `ValidationError`, `RateLimitError`, `BillingInsufficientCreditsError`, `UpstreamError`, and `ServiceUnavailableError`.
117
+
118
+ ## Local release workflow
119
+
120
+ The Python and TypeScript SDKs share one SemVer version. From the monorepo root:
121
+
122
+ ```bash
123
+ mise exec -- pnpm sdk:status
124
+ mise exec -- pnpm sdk:version patch
125
+ mise exec -- pnpm sdk:check
126
+ mise exec -- pnpm sdk:build
127
+ mise exec -- pnpm sdk:smoke
128
+ mise exec -- pnpm sdk:publish -- --dry-run
129
+ ```
130
+
131
+ Use `patch` for compatible bug fixes, `minor` for compatible new features, and `major` only for breaking public API changes. Actual PyPI publishing uses local `uv publish` credentials, such as `UV_PUBLISH_TOKEN`; do not store tokens in the repo.
@@ -0,0 +1,109 @@
1
+ # Breeze Blue Python SDK
2
+
3
+ Local v0.1 Python SDK for the Breeze Blue Developer API.
4
+
5
+ ## Install
6
+
7
+ Before the package is published to PyPI, install it from this monorepo:
8
+
9
+ ```bash
10
+ uv sync
11
+ uv pip install -e ./packages/breeze-python
12
+ ```
13
+
14
+ After the first public release, install the same SDK from PyPI:
15
+
16
+ ```bash
17
+ uv add breeze-blue
18
+ ```
19
+
20
+ ```python
21
+ from breeze_blue import BreezeBlue, play, save, stream
22
+
23
+ client = BreezeBlue() # Reads BREEZE_API_KEY and defaults to https://api.breeze.blue
24
+
25
+ audio = client.text_to_speech.convert(
26
+ "voc_demo",
27
+ text="Hello from Breeze Blue.",
28
+ output_format="mp3_44100_128",
29
+ )
30
+ play(audio)
31
+ save(audio, "hello.mp3")
32
+ ```
33
+
34
+ ## Client
35
+
36
+ ```python
37
+ client = BreezeBlue(
38
+ api_key="brz_secret",
39
+ base_url="https://api.breeze.blue",
40
+ )
41
+ ```
42
+
43
+ Authentication uses the ElevenLabs-compatible `xi-api-key` header to make migration easier. If `api_key` is omitted, the SDK reads `BREEZE_API_KEY`. If `base_url` is omitted, the SDK reads `BREEZE_BASE_URL` and then falls back to `https://api.breeze.blue`.
44
+
45
+ For local development, pass `base_url="http://127.0.0.1:8000"` or set `BREEZE_BASE_URL`.
46
+
47
+ Set a default client timeout with `BreezeBlue(timeout=120.0)`, or override a single call with `timeout=120.0`.
48
+
49
+ ## Resources
50
+
51
+ - `client.text_to_speech.convert(voice_id, text=...)`
52
+ - `client.text_to_speech.stream(voice_id, text=...)`
53
+ - `client.voices.search(...)`
54
+ - `client.voices.get(voice_id)`
55
+ - `client.voices.ivc.create_preview(name=..., file=...)`
56
+ - `client.voices.ivc.edit(voice_id, name=..., file=...)`
57
+ - `client.voices.ivc.get_settings(voice_id)` / `client.voices.ivc.update_settings(voice_id, guidance_scale=...)`
58
+ - `client.voices.ivc.delete(voice_id)`
59
+ - `client.text_to_voice.design(voice_description=...)`
60
+ - `client.text_to_voice.create(generated_voice_id=..., voice_name=...)`
61
+ - `client.text_to_voice.stream_preview(generated_voice_id)`
62
+ - `client.history.list(...)`
63
+ - `client.history.get(history_item_id)`
64
+ - `client.history.download_audio(history_item_id)`
65
+ - `client.history.delete(history_item_id)`
66
+ - `client.models.list()`
67
+ - `client.account.balance()`
68
+ - `client.account.usage(...)`
69
+
70
+ All resource methods accept `timeout=<seconds>` to override the client default for a single call.
71
+ Common response shapes are exported as `TypedDict` types from `breeze_blue.types` and from the package root.
72
+
73
+ ## Audio Helpers
74
+
75
+ Audio endpoints return `AudioResponse` with:
76
+
77
+ - `content`
78
+ - `content_type`
79
+ - `history_item_id`
80
+
81
+ Helpers:
82
+
83
+ - `audio.save(path)`
84
+ - `audio.play()` uses `ffplay`, with macOS `afplay` fallback, and waits for playback to finish.
85
+ - `audio.stream()` pipes the buffered response content to `mpv` and waits for playback to finish.
86
+ - `play(audio)`, `save(audio, path)`, and `stream(audio)` are also exported
87
+ from `breeze_blue` for ElevenLabs-style examples.
88
+ - `stream(audio)` also accepts an iterable of byte chunks for chunked playback.
89
+ - `start_play(audio)` and `start_stream(audio)` are available for advanced
90
+ fire-and-forget process control.
91
+
92
+ ## Errors
93
+
94
+ All API errors inherit from `BreezeBlueError`. HTTP and Breeze error codes are mapped to typed exceptions including `BadRequestError`, `AuthenticationError`, `ForbiddenError`, `NotFoundError`, `ConflictError`, `ValidationError`, `RateLimitError`, `BillingInsufficientCreditsError`, `UpstreamError`, and `ServiceUnavailableError`.
95
+
96
+ ## Local release workflow
97
+
98
+ The Python and TypeScript SDKs share one SemVer version. From the monorepo root:
99
+
100
+ ```bash
101
+ mise exec -- pnpm sdk:status
102
+ mise exec -- pnpm sdk:version patch
103
+ mise exec -- pnpm sdk:check
104
+ mise exec -- pnpm sdk:build
105
+ mise exec -- pnpm sdk:smoke
106
+ mise exec -- pnpm sdk:publish -- --dry-run
107
+ ```
108
+
109
+ Use `patch` for compatible bug fixes, `minor` for compatible new features, and `major` only for breaking public API changes. Actual PyPI publishing uses local `uv publish` credentials, such as `UV_PUBLISH_TOKEN`; do not store tokens in the repo.
@@ -0,0 +1,55 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "breeze-blue"
7
+ version = "0.1.0"
8
+ description = "Python SDK for the Breeze Blue Developer API."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ { name = "Breeze Blue", email = "support@breeze.blue" },
15
+ ]
16
+ keywords = [
17
+ "audio",
18
+ "breeze",
19
+ "breeze-blue",
20
+ "elevenlabs",
21
+ "elevenlabs-alternative",
22
+ "sdk",
23
+ "text-to-speech",
24
+ "tts",
25
+ "voice",
26
+ ]
27
+ classifiers = [
28
+ "Development Status :: 4 - Beta",
29
+ "Intended Audience :: Developers",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.10",
32
+ "Programming Language :: Python :: 3.11",
33
+ "Programming Language :: Python :: 3.12",
34
+ "Topic :: Multimedia :: Sound/Audio :: Speech",
35
+ ]
36
+ dependencies = [
37
+ "httpx>=0.28.0,<1.0.0",
38
+ ]
39
+
40
+ [project.urls]
41
+ Homepage = "https://breeze.blue"
42
+ Documentation = "https://breeze.blue/docs"
43
+
44
+ [dependency-groups]
45
+ dev = [
46
+ "exceptiongroup>=1.2.0,<2.0.0; python_version < '3.11'",
47
+ "pyright>=1.1.407,<2.0.0",
48
+ "pytest>=8.4.2,<9.0.0",
49
+ ]
50
+
51
+ [tool.pytest.ini_options]
52
+ pythonpath = ["src"]
53
+
54
+ [tool.setuptools.packages.find]
55
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ from ._audio import AudioResponse, play, save, start_play, start_stream, stream
6
+ from ._client import BreezeBlue
7
+ from ._errors import (
8
+ APIError,
9
+ AuthenticationError,
10
+ BadRequestError,
11
+ BillingInsufficientCreditsError,
12
+ BreezeBlueError,
13
+ ConflictError,
14
+ ForbiddenError,
15
+ NotFoundError,
16
+ RateLimitError,
17
+ ServiceUnavailableError,
18
+ UpstreamError,
19
+ ValidationError,
20
+ )
21
+ from .types import (
22
+ Balance,
23
+ GenericStatus,
24
+ HistoryItem,
25
+ HistoryList,
26
+ Model,
27
+ Usage,
28
+ Voice,
29
+ VoiceClonePreview,
30
+ VoiceDesignResponse,
31
+ VoiceList,
32
+ VoiceSettings,
33
+ )
34
+
35
+ try:
36
+ __version__ = version("breeze-blue")
37
+ except PackageNotFoundError:
38
+ __version__ = "0.0.0"
39
+
40
+ __all__ = [
41
+ "APIError",
42
+ "AudioResponse",
43
+ "AuthenticationError",
44
+ "BadRequestError",
45
+ "BillingInsufficientCreditsError",
46
+ "BreezeBlue",
47
+ "BreezeBlueError",
48
+ "ConflictError",
49
+ "ForbiddenError",
50
+ "NotFoundError",
51
+ "RateLimitError",
52
+ "ServiceUnavailableError",
53
+ "UpstreamError",
54
+ "ValidationError",
55
+ "Balance",
56
+ "GenericStatus",
57
+ "HistoryItem",
58
+ "HistoryList",
59
+ "Model",
60
+ "Usage",
61
+ "Voice",
62
+ "VoiceClonePreview",
63
+ "VoiceDesignResponse",
64
+ "VoiceList",
65
+ "VoiceSettings",
66
+ "__version__",
67
+ "play",
68
+ "save",
69
+ "start_play",
70
+ "start_stream",
71
+ "stream",
72
+ ]
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ import platform
7
+ import shutil
8
+ import subprocess
9
+
10
+
11
+ class PlayerNotFoundError(RuntimeError):
12
+ pass
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class AudioResponse:
17
+ content: bytes
18
+ content_type: str | None = None
19
+ history_item_id: str | None = None
20
+
21
+ def save(self, path: str | Path) -> Path:
22
+ target = Path(path)
23
+ target.write_bytes(self.content)
24
+ return target
25
+
26
+ def play(self) -> None:
27
+ ffplay = shutil.which("ffplay")
28
+ if ffplay:
29
+ _pipe_to_player([ffplay, "-autoexit", "-nodisp", "-"], self.content)
30
+ return
31
+
32
+ if platform.system() == "Darwin":
33
+ afplay = shutil.which("afplay")
34
+ if afplay:
35
+ _pipe_to_player([afplay, "-"], self.content)
36
+ return
37
+
38
+ raise PlayerNotFoundError("Install ffplay, or afplay on macOS, to play audio.")
39
+
40
+ def stream(self) -> None:
41
+ mpv = shutil.which("mpv")
42
+ if not mpv:
43
+ raise PlayerNotFoundError("Install mpv to stream audio.")
44
+ _pipe_to_player([mpv, "--no-terminal", "-"], self.content)
45
+
46
+
47
+ def save(audio: AudioResponse | bytes | bytearray, path: str | Path) -> Path:
48
+ return _coerce_audio_response(audio).save(path)
49
+
50
+
51
+ def play(audio: AudioResponse | bytes | bytearray) -> None:
52
+ return _coerce_audio_response(audio).play()
53
+
54
+
55
+ def stream(audio: AudioResponse | bytes | bytearray | Iterable[bytes]) -> None:
56
+ mpv = shutil.which("mpv")
57
+ if not mpv:
58
+ raise PlayerNotFoundError("Install mpv to stream audio.")
59
+ _pipe_chunks_to_player([mpv, "--no-terminal", "-"], _iter_audio_chunks(audio))
60
+
61
+
62
+ def start_play(audio: AudioResponse | bytes | bytearray) -> subprocess.Popen[bytes]:
63
+ response = _coerce_audio_response(audio)
64
+ ffplay = shutil.which("ffplay")
65
+ if ffplay:
66
+ return _start_player([ffplay, "-autoexit", "-nodisp", "-"], response.content)
67
+ if platform.system() == "Darwin":
68
+ afplay = shutil.which("afplay")
69
+ if afplay:
70
+ return _start_player([afplay, "-"], response.content)
71
+ raise PlayerNotFoundError("Install ffplay, or afplay on macOS, to play audio.")
72
+
73
+
74
+ def start_stream(audio: AudioResponse | bytes | bytearray | Iterable[bytes]) -> subprocess.Popen[bytes]:
75
+ mpv = shutil.which("mpv")
76
+ if not mpv:
77
+ raise PlayerNotFoundError("Install mpv to stream audio.")
78
+ return _start_player_with_chunks([mpv, "--no-terminal", "-"], _iter_audio_chunks(audio))
79
+
80
+
81
+ def _coerce_audio_response(audio: AudioResponse | bytes | bytearray) -> AudioResponse:
82
+ if isinstance(audio, AudioResponse):
83
+ return audio
84
+ return AudioResponse(content=bytes(audio))
85
+
86
+
87
+ def _iter_audio_chunks(audio: AudioResponse | bytes | bytearray | Iterable[bytes]) -> Iterable[bytes]:
88
+ if isinstance(audio, AudioResponse):
89
+ yield audio.content
90
+ return
91
+ if isinstance(audio, bytes | bytearray):
92
+ yield bytes(audio)
93
+ return
94
+ yield from audio
95
+
96
+
97
+ def _pipe_to_player(command: list[str], audio: bytes) -> None:
98
+ process = _start_player(command, audio)
99
+ process.wait()
100
+
101
+
102
+ def _pipe_chunks_to_player(command: list[str], chunks: Iterable[bytes]) -> None:
103
+ process = _start_player_with_chunks(command, chunks)
104
+ process.wait()
105
+
106
+
107
+ def _start_player(command: list[str], audio: bytes) -> subprocess.Popen[bytes]:
108
+ return _start_player_with_chunks(command, [audio])
109
+
110
+
111
+ def _start_player_with_chunks(command: list[str], chunks: Iterable[bytes]) -> subprocess.Popen[bytes]:
112
+ process = subprocess.Popen(command, stdin=subprocess.PIPE)
113
+ if process.stdin is not None:
114
+ for chunk in chunks:
115
+ if chunk:
116
+ process.stdin.write(chunk)
117
+ process.stdin.close()
118
+ return process
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from importlib.metadata import PackageNotFoundError, version
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from ._audio import AudioResponse
10
+ from ._errors import raise_for_response
11
+ from ._resources import AccountResource, HistoryResource, ModelsResource, TextToSpeechResource, TextToVoiceResource, VoicesResource
12
+
13
+
14
+ DEFAULT_BASE_URL = "https://api.breeze.blue"
15
+ try:
16
+ SDK_VERSION = version("breeze-blue")
17
+ except PackageNotFoundError:
18
+ SDK_VERSION = "0.0.0"
19
+
20
+
21
+ class BreezeBlue:
22
+ def __init__(
23
+ self,
24
+ *,
25
+ api_key: str | None = None,
26
+ base_url: str | None = None,
27
+ timeout: float | httpx.Timeout = 60.0,
28
+ http_client: httpx.Client | None = None,
29
+ ) -> None:
30
+ self.api_key = api_key if api_key is not None else os.getenv("BREEZE_API_KEY")
31
+ resolved_base_url = base_url or os.getenv("BREEZE_BASE_URL") or DEFAULT_BASE_URL
32
+ self.base_url = resolved_base_url.rstrip("/")
33
+ self._owns_client = http_client is None
34
+ self._client = http_client or httpx.Client(timeout=timeout)
35
+
36
+ self.text_to_speech = TextToSpeechResource(self)
37
+ self.voices = VoicesResource(self)
38
+ self.text_to_voice = TextToVoiceResource(self)
39
+ self.history = HistoryResource(self)
40
+ self.models = ModelsResource(self)
41
+ self.account = AccountResource(self)
42
+
43
+ def close(self) -> None:
44
+ if self._owns_client:
45
+ self._client.close()
46
+
47
+ def __enter__(self) -> BreezeBlue:
48
+ return self
49
+
50
+ def __exit__(self, *_exc: object) -> None:
51
+ self.close()
52
+
53
+ def request(
54
+ self,
55
+ method: str,
56
+ path: str,
57
+ *,
58
+ params: dict[str, Any] | None = None,
59
+ json: dict[str, Any] | None = None,
60
+ data: dict[str, Any] | None = None,
61
+ files: dict[str, Any] | list[tuple[str, Any]] | None = None,
62
+ timeout: float | httpx.Timeout | None = None,
63
+ ) -> httpx.Response:
64
+ request_kwargs: dict[str, Any] = {
65
+ "params": _drop_none(params),
66
+ "json": json,
67
+ "data": _drop_none(data),
68
+ "files": files,
69
+ "headers": self._headers(),
70
+ }
71
+ if timeout is not None:
72
+ request_kwargs["timeout"] = timeout
73
+ response = self._client.request(method, self._url(path), **request_kwargs)
74
+ raise_for_response(response)
75
+ return response
76
+
77
+ def json_request(
78
+ self,
79
+ method: str,
80
+ path: str,
81
+ *,
82
+ params: dict[str, Any] | None = None,
83
+ json: dict[str, Any] | None = None,
84
+ data: dict[str, Any] | None = None,
85
+ files: dict[str, Any] | list[tuple[str, Any]] | None = None,
86
+ timeout: float | httpx.Timeout | None = None,
87
+ ) -> Any:
88
+ response = self.request(method, path, params=params, json=json, data=data, files=files, timeout=timeout)
89
+ if not response.content:
90
+ return None
91
+ return response.json()
92
+
93
+ def audio_request(
94
+ self,
95
+ method: str,
96
+ path: str,
97
+ *,
98
+ params: dict[str, Any] | None = None,
99
+ json: dict[str, Any] | None = None,
100
+ timeout: float | httpx.Timeout | None = None,
101
+ ) -> AudioResponse:
102
+ response = self.request(method, path, params=params, json=json, timeout=timeout)
103
+ return AudioResponse(
104
+ content=response.content,
105
+ content_type=response.headers.get("content-type"),
106
+ history_item_id=response.headers.get("history-item-id"),
107
+ )
108
+
109
+ def _url(self, path: str) -> str:
110
+ normalized = path if path.startswith("/") else f"/{path}"
111
+ return f"{self.base_url}{normalized}"
112
+
113
+ def _headers(self) -> dict[str, str]:
114
+ headers = {
115
+ "user-agent": f"breeze-blue-python/{SDK_VERSION}",
116
+ "x-breeze-sdk": f"breeze-blue-python/{SDK_VERSION}",
117
+ }
118
+ if self.api_key:
119
+ headers["xi-api-key"] = self.api_key
120
+ return headers
121
+
122
+
123
+ def _drop_none(payload: dict[str, Any] | None) -> dict[str, Any] | None:
124
+ if payload is None:
125
+ return None
126
+ return {key: value for key, value in payload.items() if value is not None}