Skip to content

Commit 3b29948

Browse files
Merge pull request #6 from weAreMusicAI/DEV-417-user-agent
DEV-417 Add custom User-Agent header and copyResultsTo method to Python SDK
2 parents 29dc5b5 + f08d378 commit 3b29948

5 files changed

Lines changed: 125 additions & 13 deletions

File tree

README.md

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,26 +81,60 @@ song_url = music_ai.upload_file(file_path)
8181

8282
### Add a job
8383

84-
Creates a new job and returns its correponding ID.
84+
Creates a new job and returns its corresponding ID.
8585

8686
```python
87-
def add_job(job_name: str, workflow_slug: str, params: Dict[str, str]) -> str
87+
def add_job(
88+
job_name: str,
89+
workflow_slug: str,
90+
params: Dict[str, Any],
91+
**options
92+
) -> str
8893
```
8994

9095
#### Example
9196

9297
```python
9398
song_url = "https://your-website.com/song.mp3"
99+
job_id = music_ai.add_job("job-1", "music-ai/isolate-drums", {
100+
"inputUrl": song_url,
101+
})
102+
```
103+
104+
Check the [documentation](https://music.ai/docs) for all the existing workflows and expected correspondent parameters.
105+
106+
#### Custom storage
107+
108+
You can optionally store outputs in your own storage by providing upload URLs. To do that, use the `copy_results_to` option, defining one upload URL for each output of the workflow.
109+
110+
```python
94111
job_id = music_ai.add_job(
95112
"job-1",
96-
"music-ai/generate-chords",
113+
"music-ai/isolate-drums",
97114
{
98-
"inputUrl": song_url
115+
"inputUrl": song_url,
116+
},
117+
copy_results_to={
118+
"Kick drum": "https://example.com/my-upload-url-1",
119+
"Snare drum": "https://example.com/my-upload-url-2"
99120
}
100121
)
101122
```
102123

103-
Check the [documentation](https://music.ai/docs) for all the existing workflows and expected correspondent parameters.
124+
The example above uses the `music-ai/isolate-drums` workflow, which has 3 outputs, Kick drum, Snare drum, and Other. We have provided upload URLs for the first two. Since we haven't provided a URL for the third output, it will be stored in Music AI's storage, as usual.
125+
126+
The JSON below contains the data for the job created above. Please note that Music AI doesn't provide download URLs for the outputs directed to your custom storage.
127+
128+
```json
129+
{
130+
// ...
131+
"result": {
132+
"Kick drum": "[custom storage]",
133+
"Snare drum": "[custom storage]",
134+
"Other": "https://cdn.music.ai/example/vocals.wav"
135+
}
136+
}
137+
```
104138

105139
### Get a job
106140

musicai_sdk/client.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,32 @@
66
import requests
77
from requests.exceptions import HTTPError
88

9-
from .utils import extract_file_extension_from_url, extract_name_from_url
9+
from .utils import (
10+
extract_file_extension_from_url,
11+
extract_name_from_url,
12+
get_user_agent,
13+
)
1014

1115

1216
class MusicAiClient:
13-
def __init__(self, api_key, job_monitor_interval=2, save_output_to_folder=True):
17+
def __init__(
18+
self,
19+
api_key,
20+
job_monitor_interval=2,
21+
save_output_to_folder=True,
22+
user_agent=None,
23+
):
1424
self.api_key = api_key
1525
self.base_url = "https://api.music.ai/api"
1626
self.job_monitor_interval = job_monitor_interval
1727
self.save_output_to_folder = save_output_to_folder
28+
self.user_agent = user_agent or get_user_agent()
1829

1930
def get_headers(self):
20-
return {"Authorization": self.api_key}
31+
return {
32+
"Authorization": self.api_key,
33+
"User-Agent": self.user_agent,
34+
}
2135

2236
def upload_file(self, file_path):
2337
response = requests.get(f"{self.base_url}/upload", headers=self.get_headers())
@@ -79,8 +93,19 @@ def list_jobs(self, filters=None):
7993

8094
return response.json()
8195

82-
def add_job(self, job_name, workflow_slug, params):
83-
data = {"name": job_name, "workflow": workflow_slug, "params": params}
96+
def add_job(self, job_name, workflow_slug, params, **options):
97+
data = {
98+
"name": job_name,
99+
"workflow": workflow_slug,
100+
"params": params,
101+
}
102+
103+
if "metadata" in options:
104+
data["metadata"] = options["metadata"]
105+
106+
if "copy_results_to" in options:
107+
data["copyResultsTo"] = options["copy_results_to"]
108+
84109
response = requests.post(
85110
f"{self.base_url}/job", headers=self.get_headers(), json=data
86111
)

musicai_sdk/utils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from urllib.parse import urlparse, unquote
2+
import platform
3+
from pathlib import Path
24

35

46
def extract_name_from_url(file_url):
@@ -10,3 +12,21 @@ def extract_name_from_url(file_url):
1012
def extract_file_extension_from_url(url):
1113
name_from_url = extract_name_from_url(url)
1214
return name_from_url.split(".")[-1]
15+
16+
17+
def get_version():
18+
package_path = Path(__file__).parent.parent / "pyproject.toml"
19+
with open(package_path, "r") as f:
20+
content = f.read()
21+
22+
for line in content.splitlines():
23+
if line.startswith("version = "):
24+
return line.split("=")[1].strip().strip("\"'")
25+
return "unknown"
26+
27+
28+
def get_user_agent(environment="SDK-Python", version=None):
29+
if version is None:
30+
version = get_version()
31+
platform_name = platform.system() or "unknown"
32+
return f"Music.AI/{environment}/{version} ({platform_name})"

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[tool.poetry]
22
name = "musicai-sdk"
3-
version = "1.0.0"
4-
description = "Python sdk for music.ai"
3+
version = "1.0.1"
4+
description = "Python SDK for Music.AI"
55
authors = ["Music.ai Support <support@music.ai>"]
66
license = "MIT"
77
readme = "README.md"

tests/test_client.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import shutil
44
import tempfile
55
import uuid
6+
import requests
67

78
import pytest
89

@@ -71,6 +72,38 @@ def test_add_job(self, client):
7172
assert job_id is not None
7273
assert re.match(UUID_REGEX, job_id)
7374

75+
def test_add_job_with_custom_storage(self, client):
76+
"""Test adding a job with custom storage"""
77+
# Arrange
78+
upload_response = requests.get(f"{client.base_url}/upload", headers=client.get_headers())
79+
upload_response.raise_for_status()
80+
upload_url = upload_response.json()["uploadUrl"]
81+
82+
# Act
83+
result = client.add_job(
84+
"sdk-test-custom-storage",
85+
"sdk-test",
86+
{
87+
"file": "https://music.ai/demo.ogg",
88+
},
89+
copy_results_to={
90+
"file": upload_url
91+
}
92+
)
93+
94+
# Assert
95+
assert isinstance(result, dict)
96+
job_id = result.get("id")
97+
assert job_id is not None
98+
assert re.match(UUID_REGEX, job_id)
99+
100+
# Additional verification for custom storage
101+
completed_job = client.wait_for_job_completion(job_id)
102+
assert completed_job["status"] == "SUCCEEDED"
103+
assert "result" in completed_job
104+
assert "file" in completed_job["result"]
105+
assert completed_job["result"]["file"] == "[custom storage]"
106+
74107
def test_get_job(self, client):
75108
"""Test getting job details"""
76109
# Arrange
@@ -208,4 +241,4 @@ def test_get_application_info(self, client):
208241
assert isinstance(result, dict)
209242
assert "id" in result
210243
assert re.match(UUID_REGEX, result["id"])
211-
assert "name" in result
244+
assert "name" in result

0 commit comments

Comments
 (0)