loudly-py-sdk 0.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.
@@ -0,0 +1,2 @@
1
+ from .client import LoudlyClient
2
+ from .exceptions import LoudlyAPIError
@@ -0,0 +1,401 @@
1
+ import os
2
+ import requests
3
+ from typing import Optional, Dict, Any, List
4
+ from dotenv import load_dotenv
5
+ from .exceptions import LoudlyAPIError
6
+
7
+ # Load .env variables automatically
8
+ load_dotenv()
9
+
10
+
11
+ class LoudlyClient:
12
+ DEFAULT_BASE_URL = "https://api.loudly.com/v1"
13
+
14
+ def __init__(
15
+ self,
16
+ api_key: Optional[str] = None,
17
+ config: Optional[Dict[str, Any]] = None,
18
+ timeout: float = 10.0,
19
+ base_url: Optional[str] = None,
20
+ ):
21
+ if config:
22
+ api_key = config.get("apiKey", api_key)
23
+
24
+ self.api_key = api_key or os.getenv("LOUDLY_API_KEY")
25
+ self.timeout = timeout
26
+ self.base_url = base_url or self.DEFAULT_BASE_URL
27
+
28
+ self.session = requests.Session()
29
+ if self.api_key:
30
+ self._set_auth_header(self.api_key)
31
+
32
+ # -------------------
33
+ # Fluent setters
34
+ # -------------------
35
+ def with_api_key(self, api_key: str) -> "LoudlyClient":
36
+ self._set_auth_header(api_key)
37
+ return self
38
+
39
+ def with_timeout(self, timeout: float) -> "LoudlyClient":
40
+ self.timeout = timeout
41
+ return self
42
+
43
+ def with_base_url(self, base_url: str) -> "LoudlyClient":
44
+ self.base_url = base_url
45
+ return self
46
+
47
+ # -------------------
48
+ # Internal helpers
49
+ # -------------------
50
+ def _set_auth_header(self, api_key: str):
51
+ self.api_key = api_key
52
+ self.session.headers.update({
53
+ "Authorization": f"Bearer {self.api_key}",
54
+ "Content-Type": "application/json",
55
+ "Accept": "application/json",
56
+ })
57
+
58
+ def _ensure_api_key(self):
59
+ if not self.api_key:
60
+ raise ValueError(
61
+ "No API key set. Use api_key=..., config={'apiKey': ...}, "
62
+ "LOUDLY_API_KEY in a .env file, or .with_api_key()."
63
+ )
64
+
65
+ def _request(self, method: str, path: str, params=None, json=None, headers=None):
66
+ self._ensure_api_key()
67
+ url = f"{self.base_url}{path}"
68
+ try:
69
+ resp = self.session.request(
70
+ method=method,
71
+ url=url,
72
+ params=params,
73
+ json=json,
74
+ headers=headers,
75
+ timeout=self.timeout
76
+ )
77
+ except requests.RequestException as e:
78
+ raise LoudlyAPIError(-1, f"Network error: {e}")
79
+
80
+ if not resp.ok:
81
+ try:
82
+ err = resp.json()
83
+ message = err.get("error", err.get("message", resp.text))
84
+ except ValueError:
85
+ message = resp.text
86
+ raise LoudlyAPIError(resp.status_code, message, response=resp)
87
+
88
+ try:
89
+ return resp.json()
90
+ except ValueError:
91
+ return {"raw": resp.text}
92
+
93
+ # -------------------
94
+ # API Methods
95
+ # -------------------
96
+
97
+ # 1. Genres
98
+ def list_genres(self) -> List[Dict[str, Any]]:
99
+ self._ensure_api_key()
100
+ headers = {
101
+ "API-KEY": self.api_key,
102
+ "Accept": "application/json"
103
+ }
104
+ url = "https://soundtracks.loudly.com/api/ai/genres"
105
+
106
+ try:
107
+ resp = requests.get(url, headers=headers, timeout=self.timeout)
108
+ except requests.RequestException as e:
109
+ raise LoudlyAPIError(-1, f"Network error: {e}")
110
+
111
+ if not resp.ok:
112
+ try:
113
+ err = resp.json()
114
+ message = err.get("error", err.get("message", resp.text))
115
+ except ValueError:
116
+ message = resp.text
117
+ raise LoudlyAPIError(resp.status_code, message, response=resp)
118
+
119
+ try:
120
+ return resp.json()
121
+ except ValueError:
122
+ return {"raw": resp.text}
123
+
124
+ # 2. Structures
125
+ def list_structures(self) -> List[Dict[str, Any]]:
126
+ self._ensure_api_key()
127
+ headers = {
128
+ "API-KEY": self.api_key,
129
+ "Accept": "application/json"
130
+ }
131
+ url = "https://soundtracks.loudly.com/api/ai/structures"
132
+
133
+ try:
134
+ resp = requests.get(url, headers=headers, timeout=self.timeout)
135
+ except requests.RequestException as e:
136
+ raise LoudlyAPIError(-1, f"Network error: {e}")
137
+
138
+ if not resp.ok:
139
+ try:
140
+ err = resp.json()
141
+ message = err.get("error", err.get("message", resp.text))
142
+ except ValueError:
143
+ message = resp.text
144
+ raise LoudlyAPIError(resp.status_code, message, response=resp)
145
+
146
+ try:
147
+ return resp.json()
148
+ except ValueError:
149
+ return {"raw": resp.text}
150
+
151
+ # 3. Random prompt
152
+ def get_random_prompt(self) -> Dict[str, Any]:
153
+ self._ensure_api_key()
154
+ headers = {
155
+ "API-KEY": self.api_key,
156
+ "Accept": "application/json"
157
+ }
158
+ url = "https://soundtracks.loudly.com/api/ai/prompt/random"
159
+
160
+ try:
161
+ resp = requests.get(url, headers=headers, timeout=self.timeout)
162
+ except requests.RequestException as e:
163
+ raise LoudlyAPIError(-1, f"Network error: {e}")
164
+
165
+ if not resp.ok:
166
+ try:
167
+ err = resp.json()
168
+ message = err.get("error", err.get("message", resp.text))
169
+ except ValueError:
170
+ message = resp.text
171
+ raise LoudlyAPIError(resp.status_code, message, response=resp)
172
+
173
+ try:
174
+ return resp.json()
175
+ except ValueError:
176
+ return {"raw": resp.text}
177
+
178
+ # 4. Song tags
179
+ def get_song_tags(
180
+ self,
181
+ mood: Optional[List[str]] = None,
182
+ genre: Optional[List[str]] = None,
183
+ key: Optional[List[str]] = None
184
+ ) -> Dict[str, Any]:
185
+ self._ensure_api_key()
186
+ headers = {
187
+ "API-KEY": self.api_key,
188
+ "Accept": "application/json"
189
+ }
190
+ url = "https://soundtracks.loudly.com/api/songs/tags"
191
+ payload = {}
192
+ if mood: payload["mood"] = mood
193
+ if genre: payload["genre"] = genre
194
+ if key: payload["key"] = key
195
+
196
+ try:
197
+ resp = requests.get(url, headers=headers, json=payload, timeout=self.timeout)
198
+ except requests.RequestException as e:
199
+ raise LoudlyAPIError(-1, f"Network error: {e}")
200
+
201
+ if not resp.ok:
202
+ try:
203
+ err = resp.json()
204
+ message = err.get("error", err.get("message", resp.text))
205
+ except ValueError:
206
+ message = resp.text
207
+ raise LoudlyAPIError(resp.status_code, message, response=resp)
208
+
209
+ try:
210
+ return resp.json()
211
+ except ValueError:
212
+ return {"raw": resp.text}
213
+
214
+ # 5. List songs
215
+ def list_songs(
216
+ self,
217
+ page: int = 1,
218
+ per_page: int = 20
219
+ ) -> Dict[str, Any]:
220
+ self._ensure_api_key()
221
+ headers = {
222
+ "API-KEY": self.api_key,
223
+ "Accept": "application/json"
224
+ }
225
+ params = {"page": page, "per_page": per_page}
226
+ url = "https://soundtracks.loudly.com/api/songs"
227
+
228
+ try:
229
+ resp = requests.get(url, headers=headers, params=params, timeout=self.timeout)
230
+ except requests.RequestException as e:
231
+ raise LoudlyAPIError(-1, f"Network error: {e}")
232
+
233
+ if not resp.ok:
234
+ try:
235
+ err = resp.json()
236
+ message = err.get("error", err.get("message", resp.text))
237
+ except ValueError:
238
+ message = resp.text
239
+ raise LoudlyAPIError(resp.status_code, message, response=resp)
240
+
241
+ try:
242
+ return resp.json()
243
+ except ValueError:
244
+ return {"raw": resp.text}
245
+
246
+ # 6. Song Generation
247
+ def generate_ai_song(
248
+ self,
249
+ genre: str,
250
+ genre_blend: str = "",
251
+ duration: Optional[int] = None,
252
+ energy: Optional[str] = "",
253
+ bpm: Optional[int] = None,
254
+ key_root: Optional[str] = "",
255
+ key_quality: Optional[str] = "",
256
+ instruments: Optional[str] = "",
257
+ structure_id: Optional[int] = None,
258
+ test: Optional[bool] = False
259
+ ) -> Dict[str, Any]:
260
+ """Generate AI song using form data"""
261
+
262
+ if not genre:
263
+ raise ValueError("genre is required")
264
+
265
+ self._ensure_api_key()
266
+
267
+ url = "https://soundtracks.loudly.com/api/ai/songs"
268
+
269
+ # Prepare form data - only include non-empty values
270
+ data = {"genre": genre}
271
+
272
+ if genre_blend:
273
+ data["genre_blend"] = genre_blend
274
+ if duration is not None:
275
+ data["duration"] = str(duration)
276
+ if energy:
277
+ data["energy"] = energy
278
+ if bpm is not None:
279
+ data["bpm"] = str(bpm)
280
+ if key_root:
281
+ data["key_root"] = key_root
282
+ if key_quality:
283
+ data["key_quality"] = key_quality
284
+ if instruments:
285
+ data["instruments"] = instruments
286
+ if structure_id is not None:
287
+ data["structure_id"] = str(structure_id)
288
+ if test is not None:
289
+ data["test"] = str(test).lower()
290
+
291
+ headers = {
292
+ "API-KEY": self.api_key,
293
+ "Accept": "application/json"
294
+ # Don't set Content-Type - requests will set it automatically for form data
295
+ }
296
+
297
+ try:
298
+ resp = requests.post(url, headers=headers, data=data, timeout=self.timeout)
299
+ except requests.RequestException as e:
300
+ raise LoudlyAPIError(-1, f"Network error: {e}")
301
+
302
+ if not resp.ok:
303
+ try:
304
+ err = resp.json()
305
+ message = err.get("error", err.get("message", resp.text))
306
+ except ValueError:
307
+ message = resp.text
308
+ raise LoudlyAPIError(resp.status_code, message, response=resp)
309
+
310
+ try:
311
+ return resp.json()
312
+ except ValueError:
313
+ return {"raw": resp.text}
314
+
315
+ # 7. Song generation from prompt
316
+ def generate_song_from_prompt(
317
+ self,
318
+ prompt: str,
319
+ duration: Optional[int] = None,
320
+ test: Optional[bool] = False,
321
+ structure_id: Optional[int] = None
322
+ ) -> Dict[str, Any]:
323
+
324
+ if not prompt:
325
+ raise ValueError("Prompt is required")
326
+
327
+ self._ensure_api_key()
328
+
329
+ url = "https://soundtracks.loudly.com/api/ai/prompt/songs"
330
+
331
+ data = {"prompt": prompt}
332
+
333
+ if duration is not None:
334
+ data["duration"] = str(duration)
335
+
336
+ if test is not None:
337
+ data["test"] = str(test).lower()
338
+
339
+ if structure_id is not None:
340
+ data["structure_id"] = str(structure_id)
341
+
342
+ headers = {
343
+ "API-KEY": self.api_key,
344
+ "Accept": "application/json"
345
+ }
346
+
347
+ try:
348
+ resp = requests.post(url, headers=headers, data=data, timeout=self.timeout)
349
+ except requests.RequestException as e:
350
+ raise LoudlyAPIError(-1, f"Network error: {e}")
351
+
352
+ if not resp.ok:
353
+ try:
354
+ err = resp.json()
355
+ message = err.get("error", err.get("message", resp.text))
356
+ except ValueError:
357
+ message = resp.text
358
+ raise LoudlyAPIError(resp.status_code, message, response=resp)
359
+
360
+ try:
361
+ return resp.json()
362
+ except ValueError:
363
+ return {"raw": resp.text}
364
+
365
+ # 8. Limits Account
366
+ def get_limits(
367
+ self,
368
+ date_from: Optional[str] = None,
369
+ date_to: Optional[str] = None
370
+ ) -> List[Dict[str, Any]]:
371
+ self._ensure_api_key()
372
+ headers = {
373
+ "API-KEY": self.api_key,
374
+ "Accept": "application/json"
375
+ }
376
+ # Use the soundtracks.loudly.com domain for limits as well
377
+ url = "https://soundtracks.loudly.com/api/account/limits"
378
+
379
+ params: Dict[str, Any] = {}
380
+ if date_from:
381
+ params["date_from"] = date_from
382
+ if date_to:
383
+ params["date_to"] = date_to
384
+
385
+ try:
386
+ resp = requests.get(url, headers=headers, params=params, timeout=self.timeout)
387
+ except requests.RequestException as e:
388
+ raise LoudlyAPIError(-1, f"Network error: {e}")
389
+
390
+ if not resp.ok:
391
+ try:
392
+ err = resp.json()
393
+ message = err.get("error", err.get("message", resp.text))
394
+ except ValueError:
395
+ message = resp.text
396
+ raise LoudlyAPIError(resp.status_code, message, response=resp)
397
+
398
+ try:
399
+ return resp.json()
400
+ except ValueError:
401
+ return {"raw": resp.text}
@@ -0,0 +1,7 @@
1
+ class LoudlyAPIError(Exception):
2
+ """Custom exception for Loudly API errors."""
3
+
4
+ def __init__(self, status_code: int, message: str, response=None):
5
+ super().__init__(f"HTTP {status_code}: {message}")
6
+ self.status_code = status_code
7
+ self.response = response
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: loudly-py-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for Loudly Music API
5
+ License: MIT
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: requests>=2.25.0
9
+ Requires-Dist: python-dotenv>=1.0.0
@@ -0,0 +1,7 @@
1
+ loudly_py_sdk/__init__.py,sha256=QhxhC1MDaPN25pqKU170TltaBmR3NyagbV7Rn9ZXwUU,72
2
+ loudly_py_sdk/client.py,sha256=xcyLGktA2Qf9ATqTrA8IFa6I7o2LWYJPNfLfBLkDLtE,12636
3
+ loudly_py_sdk/exceptions.py,sha256=NDZQzVDikQPAUyWOaw7_FZNRTxyVVNWVrNzxlMrc7Uk,286
4
+ loudly_py_sdk-0.1.0.dist-info/METADATA,sha256=N0GSbEZCI6pOgZbPtxYa-8w0IJTqkp9D-WG1mBbkEus,242
5
+ loudly_py_sdk-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ loudly_py_sdk-0.1.0.dist-info/top_level.txt,sha256=7uCtoDw4K0bxopM0acT-c7sNLOx_-1FiMYvthMx0Rk8,14
7
+ loudly_py_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ loudly_py_sdk