audiopod 1.5.0__py3-none-any.whl → 2.1.0__py3-none-any.whl

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.
audiopod/__init__.py CHANGED
@@ -1,33 +1,34 @@
1
1
  """
2
2
  AudioPod SDK for Python
3
3
  Professional Audio Processing powered by AI
4
+
5
+ Example:
6
+ from audiopod import AudioPod
7
+
8
+ client = AudioPod(api_key="ap_...")
9
+
10
+ # Transcribe audio
11
+ job = client.transcription.create(url="https://...")
12
+ result = client.transcription.wait_for_completion(job.id)
4
13
  """
5
14
 
6
- __version__ = "1.5.0"
15
+ __version__ = "2.1.0"
7
16
 
8
- from .client import Client, AsyncClient
17
+ from .client import AudioPod
9
18
  from .exceptions import (
10
19
  AudioPodError,
11
20
  AuthenticationError,
12
21
  APIError,
13
22
  RateLimitError,
14
- ValidationError,
15
23
  InsufficientBalanceError,
16
24
  )
17
25
 
18
- # Alias for consistency with documentation and Node.js SDK
19
- AudioPod = Client
20
-
21
26
  __all__ = [
22
- "Client",
23
- "AsyncClient",
24
- "AudioPod", # Alias for Client
27
+ "AudioPod",
25
28
  "AudioPodError",
26
- "AuthenticationError",
29
+ "AuthenticationError",
27
30
  "APIError",
28
31
  "RateLimitError",
29
- "ValidationError",
30
32
  "InsufficientBalanceError",
31
33
  "__version__",
32
34
  ]
33
-
audiopod/client.py CHANGED
@@ -1,84 +1,102 @@
1
1
  """
2
2
  AudioPod API Client
3
+
4
+ Clean, minimal API inspired by OpenAI's SDK design.
3
5
  """
4
6
 
5
7
  import os
6
- import logging
7
- from typing import Optional, Dict, Any
8
+ from typing import Optional, Dict, Any, BinaryIO
8
9
  from urllib.parse import urljoin
9
10
 
10
11
  import requests
11
- import aiohttp
12
12
  from requests.adapters import HTTPAdapter
13
13
  from urllib3.util.retry import Retry
14
14
 
15
- from .config import ClientConfig
16
- from .exceptions import AuthenticationError, APIError, RateLimitError, InsufficientBalanceError
17
- from .services import (
18
- VoiceService,
19
- MusicService,
20
- TranscriptionService,
21
- TranslationService,
22
- SpeakerService,
23
- DenoiserService,
24
- CreditService,
25
- StemExtractionService,
26
- WalletService,
27
- VideoService,
15
+ from .exceptions import (
16
+ AuthenticationError,
17
+ APIError,
18
+ RateLimitError,
19
+ InsufficientBalanceError,
28
20
  )
21
+ from .resources.transcription import Transcription
22
+ from .resources.voice import Voice
23
+ from .resources.music import Music
24
+ from .resources.stems import StemExtraction
25
+ from .resources.denoiser import Denoiser
26
+ from .resources.speaker import Speaker
27
+ from .resources.wallet import Wallet
28
+
29
+ VERSION = "2.1.0"
30
+ DEFAULT_BASE_URL = "https://api.audiopod.ai"
31
+ DEFAULT_TIMEOUT = 60
32
+
29
33
 
30
- logger = logging.getLogger(__name__)
34
+ class AudioPod:
35
+ """
36
+ AudioPod API Client.
31
37
 
38
+ Args:
39
+ api_key: Your AudioPod API key (starts with 'ap_').
40
+ If not provided, reads from AUDIOPOD_API_KEY env var.
41
+ base_url: Base URL for the API (default: https://api.audiopod.ai)
42
+ timeout: Request timeout in seconds (default: 60)
43
+ max_retries: Maximum retries for failed requests (default: 3)
32
44
 
33
- class BaseClient:
34
- """Base client with common functionality"""
45
+ Example:
46
+ >>> from audiopod import AudioPod
47
+ >>> client = AudioPod(api_key="ap_...")
48
+ >>> result = client.transcription.transcribe(url="https://...")
49
+ """
35
50
 
36
51
  def __init__(
37
52
  self,
38
53
  api_key: Optional[str] = None,
39
54
  base_url: Optional[str] = None,
40
- timeout: int = 30,
55
+ timeout: int = DEFAULT_TIMEOUT,
41
56
  max_retries: int = 3,
42
- verify_ssl: bool = True,
43
- debug: bool = False,
44
57
  ):
45
- """
46
- Initialize the AudioPod API client.
47
-
48
- Args:
49
- api_key: Your AudioPod API key. If None, reads from AUDIOPOD_API_KEY env var.
50
- base_url: API base URL. Defaults to https://api.audiopod.ai
51
- timeout: Request timeout in seconds.
52
- max_retries: Maximum retries for failed requests.
53
- verify_ssl: Whether to verify SSL certificates.
54
- debug: Enable debug logging.
55
- """
56
- if debug:
57
- logging.basicConfig(level=logging.DEBUG)
58
-
59
58
  self.api_key = api_key or os.getenv("AUDIOPOD_API_KEY")
59
+
60
60
  if not self.api_key:
61
61
  raise AuthenticationError(
62
- "API key required. Pass api_key or set AUDIOPOD_API_KEY environment variable."
62
+ "API key is required. Pass api_key or set AUDIOPOD_API_KEY environment variable."
63
63
  )
64
64
 
65
65
  if not self.api_key.startswith("ap_"):
66
- raise AuthenticationError("Invalid API key format. Keys start with 'ap_'")
66
+ raise AuthenticationError(
67
+ "Invalid API key format. AudioPod API keys start with 'ap_'"
68
+ )
67
69
 
68
- self.config = ClientConfig(
69
- base_url=base_url or "https://api.audiopod.ai",
70
- timeout=timeout,
71
- max_retries=max_retries,
72
- verify_ssl=verify_ssl,
73
- debug=debug,
70
+ self.base_url = base_url or DEFAULT_BASE_URL
71
+ self.timeout = timeout
72
+
73
+ # Configure session with retries
74
+ self._session = requests.Session()
75
+ retry_strategy = Retry(
76
+ total=max_retries,
77
+ status_forcelist=[429, 500, 502, 503, 504],
78
+ allowed_methods=["HEAD", "GET", "OPTIONS", "POST", "DELETE"],
79
+ backoff_factor=1,
74
80
  )
81
+ adapter = HTTPAdapter(max_retries=retry_strategy)
82
+ self._session.mount("http://", adapter)
83
+ self._session.mount("https://", adapter)
84
+
85
+ # Initialize services
86
+ self.transcription = Transcription(self)
87
+ self.voice = Voice(self)
88
+ self.music = Music(self)
89
+ self.stems = StemExtraction(self)
90
+ self.denoiser = Denoiser(self)
91
+ self.speaker = Speaker(self)
92
+ self.wallet = Wallet(self)
75
93
 
76
94
  def _get_headers(self) -> Dict[str, str]:
77
95
  return {
78
96
  "Authorization": f"Bearer {self.api_key}",
79
97
  "X-API-Key": self.api_key,
80
98
  "Content-Type": "application/json",
81
- "User-Agent": f"audiopod-python/{self.config.version}",
99
+ "User-Agent": f"audiopod-python/{VERSION}",
82
100
  "Accept": "application/json",
83
101
  }
84
102
 
@@ -88,225 +106,93 @@ class BaseClient:
88
106
  if response.status_code == 204:
89
107
  return {}
90
108
  return response.json()
91
- except requests.exceptions.HTTPError as e:
92
- if response.status_code == 401:
93
- raise AuthenticationError("Invalid API key")
94
- elif response.status_code == 402:
109
+ except requests.exceptions.HTTPError:
110
+ status = response.status_code
111
+ try:
112
+ data = response.json()
113
+ message = data.get("detail") or data.get("message") or str(data)
114
+ except Exception:
115
+ message = response.text or f"HTTP {status}"
116
+
117
+ if status == 401:
118
+ raise AuthenticationError(message)
119
+ elif status == 402:
95
120
  try:
96
121
  data = response.json()
97
122
  raise InsufficientBalanceError(
98
- data.get("message", "Insufficient balance"),
123
+ message,
99
124
  required_cents=data.get("required_cents"),
100
125
  available_cents=data.get("available_cents"),
101
126
  )
102
127
  except (ValueError, KeyError):
103
- raise InsufficientBalanceError("Insufficient wallet balance")
104
- elif response.status_code == 429:
105
- raise RateLimitError("Rate limit exceeded")
128
+ raise InsufficientBalanceError(message)
129
+ elif status == 429:
130
+ raise RateLimitError(message)
106
131
  else:
107
- try:
108
- error_data = response.json()
109
- message = error_data.get("detail", str(e))
110
- except:
111
- message = str(e)
112
- raise APIError(f"API error: {message}", status_code=response.status_code)
113
-
114
-
115
- class Client(BaseClient):
116
- """
117
- Synchronous AudioPod API Client.
132
+ raise APIError(message, status_code=status)
118
133
 
119
- Example:
120
- ```python
121
- from audiopod import Client
122
-
123
- client = Client(api_key="ap_your_key")
124
-
125
- # Check wallet balance
126
- balance = client.wallet.get_balance()
127
- print(f"Balance: {balance['balance_usd']}")
128
-
129
- # Extract stems
130
- job = client.stem_extraction.extract_stems(
131
- audio_file="song.mp3",
132
- stem_types=["vocals", "drums", "bass", "other"]
133
- )
134
- ```
135
- """
136
-
137
- def __init__(self, **kwargs):
138
- super().__init__(**kwargs)
139
-
140
- self.session = requests.Session()
141
- retry_strategy = Retry(
142
- total=self.config.max_retries,
143
- status_forcelist=[429, 500, 502, 503, 504],
144
- allowed_methods=["HEAD", "GET", "OPTIONS", "POST", "DELETE"],
145
- backoff_factor=1,
134
+ def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
135
+ """Make a GET request."""
136
+ url = urljoin(self.base_url, endpoint)
137
+ headers = self._get_headers()
138
+ response = self._session.get(
139
+ url, headers=headers, params=params, timeout=self.timeout
146
140
  )
147
- adapter = HTTPAdapter(max_retries=retry_strategy)
148
- self.session.mount("http://", adapter)
149
- self.session.mount("https://", adapter)
150
-
151
- # Services
152
- self.voice = VoiceService(self)
153
- self.music = MusicService(self)
154
- self.transcription = TranscriptionService(self)
155
- self.translation = TranslationService(self)
156
- self.speaker = SpeakerService(self)
157
- self.denoiser = DenoiserService(self)
158
- self.credits = CreditService(self)
159
- self.stem_extraction = StemExtractionService(self)
160
- self.stems = self.stem_extraction # Alias for consistency with Node.js SDK
161
- self.wallet = WalletService(self)
162
- self.video = VideoService(self)
141
+ return self._handle_response(response)
163
142
 
164
- def request(
143
+ def post(
165
144
  self,
166
- method: str,
167
145
  endpoint: str,
168
146
  data: Optional[Dict[str, Any]] = None,
169
147
  json_data: Optional[Dict[str, Any]] = None,
170
- files: Optional[Dict[str, Any]] = None,
171
- params: Optional[Dict[str, Any]] = None,
172
148
  ) -> Dict[str, Any]:
173
- """Make API request."""
174
- url = urljoin(self.config.base_url, endpoint)
149
+ """Make a POST request."""
150
+ url = urljoin(self.base_url, endpoint)
175
151
  headers = self._get_headers()
176
-
177
- if files:
178
- headers.pop("Content-Type", None)
179
-
180
- response = self.session.request(
181
- method=method,
182
- url=url,
183
- headers=headers,
184
- data=data,
185
- json=json_data,
186
- files=files,
187
- params=params,
188
- timeout=self.config.timeout,
189
- verify=self.config.verify_ssl,
152
+ response = self._session.post(
153
+ url, headers=headers, data=data, json=json_data, timeout=self.timeout
190
154
  )
191
155
  return self._handle_response(response)
192
156
 
193
- def get_user_info(self) -> Dict[str, Any]:
194
- """Get current user information."""
195
- return self.request("GET", "/api/v1/auth/me")
196
-
197
- def close(self):
198
- """Close client session."""
199
- self.session.close()
200
-
201
- def __enter__(self):
202
- return self
203
-
204
- def __exit__(self, exc_type, exc_val, exc_tb):
205
- self.close()
206
-
207
-
208
- class AsyncClient(BaseClient):
209
- """
210
- Asynchronous AudioPod API Client.
211
-
212
- Example:
213
- ```python
214
- import asyncio
215
- from audiopod import AsyncClient
216
-
217
- async def main():
218
- async with AsyncClient(api_key="ap_your_key") as client:
219
- balance = await client.wallet.get_balance()
220
- print(f"Balance: {balance['balance_usd']}")
221
-
222
- asyncio.run(main())
223
- ```
224
- """
225
-
226
- def __init__(self, **kwargs):
227
- super().__init__(**kwargs)
228
- self._session: Optional[aiohttp.ClientSession] = None
229
-
230
- # Services
231
- self.voice = VoiceService(self, async_mode=True)
232
- self.music = MusicService(self, async_mode=True)
233
- self.transcription = TranscriptionService(self, async_mode=True)
234
- self.translation = TranslationService(self, async_mode=True)
235
- self.speaker = SpeakerService(self, async_mode=True)
236
- self.denoiser = DenoiserService(self, async_mode=True)
237
- self.credits = CreditService(self, async_mode=True)
238
- self.stem_extraction = StemExtractionService(self, async_mode=True)
239
- self.stems = self.stem_extraction # Alias for consistency with Node.js SDK
240
- self.wallet = WalletService(self, async_mode=True)
241
- self.video = VideoService(self, async_mode=True)
242
-
243
- @property
244
- def session(self) -> aiohttp.ClientSession:
245
- if self._session is None or self._session.closed:
246
- timeout = aiohttp.ClientTimeout(total=self.config.timeout)
247
- connector = aiohttp.TCPConnector(ssl=self.config.verify_ssl)
248
- self._session = aiohttp.ClientSession(
249
- timeout=timeout, connector=connector, headers=self._get_headers()
250
- )
251
- return self._session
157
+ def delete(self, endpoint: str) -> Dict[str, Any]:
158
+ """Make a DELETE request."""
159
+ url = urljoin(self.base_url, endpoint)
160
+ headers = self._get_headers()
161
+ response = self._session.delete(url, headers=headers, timeout=self.timeout)
162
+ return self._handle_response(response)
252
163
 
253
- async def request(
164
+ def upload(
254
165
  self,
255
- method: str,
256
166
  endpoint: str,
257
- data: Optional[Dict[str, Any]] = None,
258
- json_data: Optional[Dict[str, Any]] = None,
259
- files: Optional[Dict[str, Any]] = None,
260
- params: Optional[Dict[str, Any]] = None,
167
+ file_path: str,
168
+ field_name: str = "file",
169
+ additional_fields: Optional[Dict[str, Any]] = None,
261
170
  ) -> Dict[str, Any]:
262
- """Make async API request."""
263
- url = urljoin(self.config.base_url, endpoint)
264
-
265
- async with self.session.request(
266
- method=method,
267
- url=url,
268
- json=json_data,
269
- data=data,
270
- params=params,
271
- ) as response:
272
- return await self._handle_async_response(response)
273
-
274
- async def _handle_async_response(self, response: aiohttp.ClientResponse) -> Dict[str, Any]:
275
- if response.status == 204:
276
- return {}
277
- try:
278
- response.raise_for_status()
279
- return await response.json()
280
- except aiohttp.ClientResponseError as e:
281
- if response.status == 401:
282
- raise AuthenticationError("Invalid API key")
283
- elif response.status == 402:
284
- try:
285
- data = await response.json()
286
- raise InsufficientBalanceError(
287
- data.get("message", "Insufficient balance"),
288
- required_cents=data.get("required_cents"),
289
- available_cents=data.get("available_cents"),
290
- )
291
- except:
292
- raise InsufficientBalanceError("Insufficient wallet balance")
293
- elif response.status == 429:
294
- raise RateLimitError("Rate limit exceeded")
295
- else:
296
- raise APIError(f"API error: {e}", status_code=response.status)
171
+ """Upload a file."""
172
+ url = urljoin(self.base_url, endpoint)
173
+ headers = self._get_headers()
174
+ headers.pop("Content-Type", None) # Let requests set multipart boundary
175
+
176
+ with open(file_path, "rb") as f:
177
+ files = {field_name: f}
178
+ data = {}
179
+ if additional_fields:
180
+ for key, value in additional_fields.items():
181
+ if value is not None:
182
+ data[key] = str(value) if not isinstance(value, str) else value
183
+
184
+ response = self._session.post(
185
+ url, headers=headers, files=files, data=data, timeout=self.timeout
186
+ )
297
187
 
298
- async def get_user_info(self) -> Dict[str, Any]:
299
- """Get current user information."""
300
- return await self.request("GET", "/api/v1/auth/me")
188
+ return self._handle_response(response)
301
189
 
302
- async def close(self):
303
- """Close async client session."""
304
- if self._session and not self._session.closed:
305
- await self._session.close()
190
+ def close(self):
191
+ """Close the client session."""
192
+ self._session.close()
306
193
 
307
- async def __aenter__(self):
194
+ def __enter__(self):
308
195
  return self
309
196
 
310
- async def __aexit__(self, exc_type, exc_val, exc_tb):
311
- await self.close()
312
-
197
+ def __exit__(self, exc_type, exc_val, exc_tb):
198
+ self.close()
audiopod/exceptions.py CHANGED
@@ -2,40 +2,48 @@
2
2
  AudioPod SDK Exceptions
3
3
  """
4
4
 
5
+ from typing import Optional
6
+
5
7
 
6
8
  class AudioPodError(Exception):
7
- """Base exception for AudioPod SDK"""
8
- pass
9
+ """Base exception for AudioPod SDK."""
10
+
11
+ def __init__(self, message: str = "An error occurred"):
12
+ self.message = message
13
+ super().__init__(self.message)
9
14
 
10
15
 
11
16
  class AuthenticationError(AudioPodError):
12
- """Raised when authentication fails"""
13
- pass
17
+ """Raised when authentication fails."""
18
+
19
+ def __init__(self, message: str = "Authentication failed"):
20
+ super().__init__(message)
14
21
 
15
22
 
16
23
  class APIError(AudioPodError):
17
- """Raised when API returns an error"""
18
-
19
- def __init__(self, message: str, status_code: int = None):
20
- super().__init__(message)
24
+ """Raised when an API request fails."""
25
+
26
+ def __init__(self, message: str = "API request failed", status_code: Optional[int] = None):
21
27
  self.status_code = status_code
28
+ super().__init__(message)
22
29
 
23
30
 
24
31
  class RateLimitError(AudioPodError):
25
- """Raised when rate limit is exceeded"""
26
- pass
27
-
32
+ """Raised when rate limit is exceeded."""
28
33
 
29
- class ValidationError(AudioPodError):
30
- """Raised when input validation fails"""
31
- pass
34
+ def __init__(self, message: str = "Rate limit exceeded"):
35
+ super().__init__(message)
32
36
 
33
37
 
34
38
  class InsufficientBalanceError(AudioPodError):
35
- """Raised when wallet balance is insufficient"""
36
-
37
- def __init__(self, message: str, required_cents: int = None, available_cents: int = None):
38
- super().__init__(message)
39
+ """Raised when wallet balance is insufficient."""
40
+
41
+ def __init__(
42
+ self,
43
+ message: str = "Insufficient wallet balance",
44
+ required_cents: Optional[int] = None,
45
+ available_cents: Optional[int] = None,
46
+ ):
39
47
  self.required_cents = required_cents
40
48
  self.available_cents = available_cents
41
-
49
+ super().__init__(message)
@@ -0,0 +1,23 @@
1
+ """AudioPod SDK Resources"""
2
+
3
+ from .transcription import Transcription
4
+ from .voice import Voice
5
+ from .music import Music
6
+ from .stems import StemExtraction
7
+ from .denoiser import Denoiser
8
+ from .speaker import Speaker
9
+ from .wallet import Wallet
10
+
11
+ __all__ = [
12
+ "Transcription",
13
+ "Voice",
14
+ "Music",
15
+ "StemExtraction",
16
+ "Denoiser",
17
+ "Speaker",
18
+ "Wallet",
19
+ ]
20
+
21
+
22
+
23
+