magic_hour 0.35.0__py3-none-any.whl → 0.36.1__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.
Potentially problematic release.
This version of magic_hour might be problematic. Click here for more details.
- magic_hour/README.md +35 -0
- magic_hour/core/base_client.py +6 -5
- magic_hour/core/query.py +12 -6
- magic_hour/core/request.py +3 -3
- magic_hour/core/response.py +18 -14
- magic_hour/core/utils.py +3 -3
- magic_hour/environment.py +1 -1
- magic_hour/helpers/__init__.py +3 -0
- magic_hour/helpers/download.py +75 -0
- magic_hour/resources/v1/README.md +33 -0
- magic_hour/resources/v1/ai_clothes_changer/README.md +73 -0
- magic_hour/resources/v1/ai_clothes_changer/client.py +146 -0
- magic_hour/resources/v1/ai_face_editor/README.md +110 -0
- magic_hour/resources/v1/ai_face_editor/client.py +168 -0
- magic_hour/resources/v1/ai_gif_generator/README.md +59 -0
- magic_hour/resources/v1/ai_gif_generator/client.py +119 -0
- magic_hour/resources/v1/ai_headshot_generator/README.md +60 -0
- magic_hour/resources/v1/ai_headshot_generator/client.py +140 -0
- magic_hour/resources/v1/ai_image_editor/README.md +64 -0
- magic_hour/resources/v1/ai_image_editor/client.py +136 -0
- magic_hour/resources/v1/ai_image_generator/README.md +66 -0
- magic_hour/resources/v1/ai_image_generator/client.py +139 -0
- magic_hour/resources/v1/ai_image_upscaler/README.md +67 -0
- magic_hour/resources/v1/ai_image_upscaler/client.py +150 -0
- magic_hour/resources/v1/ai_meme_generator/README.md +71 -0
- magic_hour/resources/v1/ai_meme_generator/client.py +127 -0
- magic_hour/resources/v1/ai_photo_editor/README.md +98 -7
- magic_hour/resources/v1/ai_photo_editor/client.py +174 -0
- magic_hour/resources/v1/ai_qr_code_generator/README.md +63 -0
- magic_hour/resources/v1/ai_qr_code_generator/client.py +123 -0
- magic_hour/resources/v1/ai_talking_photo/README.md +74 -0
- magic_hour/resources/v1/ai_talking_photo/client.py +170 -0
- magic_hour/resources/v1/animation/README.md +100 -0
- magic_hour/resources/v1/animation/client.py +218 -0
- magic_hour/resources/v1/auto_subtitle_generator/README.md +69 -0
- magic_hour/resources/v1/auto_subtitle_generator/client.py +178 -0
- magic_hour/resources/v1/face_detection/README.md +59 -0
- magic_hour/resources/v1/face_detection/__init__.py +10 -2
- magic_hour/resources/v1/face_detection/client.py +179 -0
- magic_hour/resources/v1/face_swap/README.md +105 -8
- magic_hour/resources/v1/face_swap/client.py +242 -0
- magic_hour/resources/v1/face_swap_photo/README.md +84 -0
- magic_hour/resources/v1/face_swap_photo/client.py +172 -0
- magic_hour/resources/v1/files/README.md +40 -0
- magic_hour/resources/v1/files/client.py +350 -0
- magic_hour/resources/v1/files/client_test.py +414 -0
- magic_hour/resources/v1/files/upload_urls/README.md +8 -0
- magic_hour/resources/v1/image_background_remover/README.md +68 -0
- magic_hour/resources/v1/image_background_remover/client.py +130 -0
- magic_hour/resources/v1/image_projects/README.md +52 -0
- magic_hour/resources/v1/image_projects/__init__.py +10 -2
- magic_hour/resources/v1/image_projects/client.py +138 -0
- magic_hour/resources/v1/image_projects/client_test.py +527 -0
- magic_hour/resources/v1/image_to_video/README.md +77 -9
- magic_hour/resources/v1/image_to_video/client.py +186 -0
- magic_hour/resources/v1/lip_sync/README.md +87 -9
- magic_hour/resources/v1/lip_sync/client.py +210 -0
- magic_hour/resources/v1/photo_colorizer/README.md +59 -0
- magic_hour/resources/v1/photo_colorizer/client.py +130 -0
- magic_hour/resources/v1/text_to_video/README.md +68 -0
- magic_hour/resources/v1/text_to_video/client.py +151 -0
- magic_hour/resources/v1/video_projects/README.md +52 -0
- magic_hour/resources/v1/video_projects/__init__.py +10 -2
- magic_hour/resources/v1/video_projects/client.py +137 -0
- magic_hour/resources/v1/video_projects/client_test.py +527 -0
- magic_hour/resources/v1/video_to_video/README.md +98 -10
- magic_hour/resources/v1/video_to_video/client.py +222 -0
- magic_hour/types/params/__init__.py +58 -0
- magic_hour/types/params/v1_ai_clothes_changer_generate_body_assets.py +33 -0
- magic_hour/types/params/v1_ai_face_editor_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_ai_headshot_generator_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_ai_image_editor_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_ai_image_upscaler_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_ai_photo_editor_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_ai_talking_photo_generate_body_assets.py +26 -0
- magic_hour/types/params/v1_animation_generate_body_assets.py +39 -0
- magic_hour/types/params/v1_auto_subtitle_generator_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_face_detection_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_face_swap_create_body.py +12 -0
- magic_hour/types/params/v1_face_swap_create_body_style.py +33 -0
- magic_hour/types/params/v1_face_swap_generate_body_assets.py +56 -0
- magic_hour/types/params/v1_face_swap_generate_body_assets_face_mappings_item.py +25 -0
- magic_hour/types/params/v1_face_swap_photo_generate_body_assets.py +47 -0
- magic_hour/types/params/v1_face_swap_photo_generate_body_assets_face_mappings_item.py +25 -0
- magic_hour/types/params/v1_image_background_remover_generate_body_assets.py +27 -0
- magic_hour/types/params/v1_image_to_video_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_lip_sync_generate_body_assets.py +36 -0
- magic_hour/types/params/v1_photo_colorizer_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_video_to_video_generate_body_assets.py +27 -0
- magic_hour-0.36.1.dist-info/METADATA +306 -0
- {magic_hour-0.35.0.dist-info → magic_hour-0.36.1.dist-info}/RECORD +93 -65
- magic_hour-0.35.0.dist-info/METADATA +0 -166
- {magic_hour-0.35.0.dist-info → magic_hour-0.36.1.dist-info}/LICENSE +0 -0
- {magic_hour-0.35.0.dist-info → magic_hour-0.36.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import tempfile
|
|
3
|
+
import os
|
|
4
|
+
import io
|
|
5
|
+
import pathlib
|
|
6
|
+
from unittest import mock
|
|
7
|
+
|
|
8
|
+
from magic_hour import AsyncClient, Client
|
|
9
|
+
from magic_hour.environment import Environment
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_upload_file_local():
|
|
13
|
+
data = b"test data"
|
|
14
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp:
|
|
15
|
+
tmp.write(data)
|
|
16
|
+
tmp_path = tmp.name
|
|
17
|
+
|
|
18
|
+
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
19
|
+
|
|
20
|
+
with mock.patch("httpx.Client.put") as mock_put:
|
|
21
|
+
mock_put.return_value = mock.Mock(
|
|
22
|
+
status_code=200, raise_for_status=lambda: None
|
|
23
|
+
)
|
|
24
|
+
result = client.v1.files.upload_file(tmp_path)
|
|
25
|
+
assert result == "api-assets/id/video.mp4"
|
|
26
|
+
mock_put.assert_called_once_with(url=mock.ANY, content=data)
|
|
27
|
+
|
|
28
|
+
os.remove(tmp_path)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.asyncio
|
|
32
|
+
async def test_async_upload_file_local():
|
|
33
|
+
data = b"test data"
|
|
34
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp:
|
|
35
|
+
tmp.write(data)
|
|
36
|
+
tmp_path = tmp.name
|
|
37
|
+
|
|
38
|
+
client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
39
|
+
|
|
40
|
+
with mock.patch("httpx.AsyncClient.put", new_callable=mock.AsyncMock) as mock_put:
|
|
41
|
+
mock_put.return_value = mock.Mock(
|
|
42
|
+
status_code=200, raise_for_status=lambda: None
|
|
43
|
+
)
|
|
44
|
+
result = await client.v1.files.upload_file(tmp_path)
|
|
45
|
+
assert result == "api-assets/id/video.mp4"
|
|
46
|
+
mock_put.assert_awaited_once_with(url=mock.ANY, content=data)
|
|
47
|
+
|
|
48
|
+
os.remove(tmp_path)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_upload_file_with_binary_io():
|
|
52
|
+
data = b"test image data"
|
|
53
|
+
file_obj = io.BytesIO(data)
|
|
54
|
+
file_obj.name = "test.png" # Required for extension detection
|
|
55
|
+
|
|
56
|
+
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
57
|
+
|
|
58
|
+
with mock.patch("httpx.Client.put") as mock_put:
|
|
59
|
+
mock_put.return_value = mock.Mock(
|
|
60
|
+
status_code=200, raise_for_status=lambda: None
|
|
61
|
+
)
|
|
62
|
+
result = client.v1.files.upload_file(file_obj)
|
|
63
|
+
assert result == "api-assets/id/video.mp4"
|
|
64
|
+
mock_put.assert_called_once_with(
|
|
65
|
+
url=mock.ANY,
|
|
66
|
+
content=data,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.mark.asyncio
|
|
71
|
+
async def test_async_upload_file_with_binary_io():
|
|
72
|
+
data = b"test audio data"
|
|
73
|
+
file_obj = io.BytesIO(data)
|
|
74
|
+
file_obj.name = "test.wav"
|
|
75
|
+
|
|
76
|
+
client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
77
|
+
|
|
78
|
+
with mock.patch("httpx.AsyncClient.put", new_callable=mock.AsyncMock) as mock_put:
|
|
79
|
+
mock_put.return_value = mock.Mock(
|
|
80
|
+
status_code=200, raise_for_status=lambda: None
|
|
81
|
+
)
|
|
82
|
+
result = await client.v1.files.upload_file(file_obj)
|
|
83
|
+
assert result == "api-assets/id/video.mp4"
|
|
84
|
+
mock_put.assert_awaited_once_with(url=mock.ANY, content=data)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# Test pathlib.Path input
|
|
88
|
+
def test_upload_file_with_pathlib_path():
|
|
89
|
+
data = b"test data"
|
|
90
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as tmp:
|
|
91
|
+
tmp.write(data)
|
|
92
|
+
tmp_path = pathlib.Path(tmp.name)
|
|
93
|
+
|
|
94
|
+
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
95
|
+
|
|
96
|
+
with mock.patch("httpx.Client.put") as mock_put:
|
|
97
|
+
mock_put.return_value = mock.Mock(
|
|
98
|
+
status_code=200, raise_for_status=lambda: None
|
|
99
|
+
)
|
|
100
|
+
result = client.v1.files.upload_file(tmp_path)
|
|
101
|
+
assert result == "api-assets/id/video.mp4"
|
|
102
|
+
mock_put.assert_called_once_with(url=mock.ANY, content=data)
|
|
103
|
+
|
|
104
|
+
os.remove(tmp_path)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Test error cases
|
|
108
|
+
def test_upload_file_nonexistent_file():
|
|
109
|
+
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
110
|
+
|
|
111
|
+
with pytest.raises(
|
|
112
|
+
FileNotFoundError, match="File not found: /nonexistent/file.mp4"
|
|
113
|
+
):
|
|
114
|
+
client.v1.files.upload_file("/nonexistent/file.mp4")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_upload_file_unsupported_file_type():
|
|
118
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".txt") as tmp:
|
|
119
|
+
tmp.write(b"test data")
|
|
120
|
+
tmp_path = tmp.name
|
|
121
|
+
|
|
122
|
+
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
123
|
+
|
|
124
|
+
with pytest.raises(ValueError, match="Could not determine file type"):
|
|
125
|
+
client.v1.files.upload_file(tmp_path)
|
|
126
|
+
|
|
127
|
+
os.remove(tmp_path)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_upload_file_binary_io_without_name():
|
|
131
|
+
data = b"test data"
|
|
132
|
+
file_obj = io.BytesIO(data)
|
|
133
|
+
# Intentionally not setting name attribute
|
|
134
|
+
|
|
135
|
+
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
136
|
+
|
|
137
|
+
with pytest.raises(
|
|
138
|
+
ValueError, match="File-like object must have a 'name' attribute"
|
|
139
|
+
):
|
|
140
|
+
client.v1.files.upload_file(file_obj)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_upload_file_binary_io_with_non_string_name():
|
|
144
|
+
data = b"test data"
|
|
145
|
+
file_obj = io.BytesIO(data)
|
|
146
|
+
file_obj.name = 123 # Non-string name
|
|
147
|
+
|
|
148
|
+
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
149
|
+
|
|
150
|
+
with pytest.raises(
|
|
151
|
+
ValueError, match="File-like object must have a 'name' attribute of type str"
|
|
152
|
+
):
|
|
153
|
+
client.v1.files.upload_file(file_obj)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_upload_file_no_upload_url_returned():
|
|
157
|
+
data = b"test data"
|
|
158
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp:
|
|
159
|
+
tmp.write(data)
|
|
160
|
+
tmp_path = tmp.name
|
|
161
|
+
|
|
162
|
+
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
163
|
+
|
|
164
|
+
# Mock the upload_urls.create to return empty items
|
|
165
|
+
with mock.patch.object(client.v1.files.upload_urls, "create") as mock_create:
|
|
166
|
+
mock_create.return_value = mock.Mock(items=[])
|
|
167
|
+
|
|
168
|
+
with pytest.raises(
|
|
169
|
+
ValueError, match="No upload URL was returned from the server"
|
|
170
|
+
):
|
|
171
|
+
client.v1.files.upload_file(tmp_path)
|
|
172
|
+
|
|
173
|
+
os.remove(tmp_path)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_upload_file_http_error():
|
|
177
|
+
data = b"test data"
|
|
178
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp:
|
|
179
|
+
tmp.write(data)
|
|
180
|
+
tmp_path = tmp.name
|
|
181
|
+
|
|
182
|
+
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
183
|
+
|
|
184
|
+
with mock.patch("httpx.Client.put") as mock_put:
|
|
185
|
+
# Mock HTTP error response
|
|
186
|
+
mock_response = mock.Mock()
|
|
187
|
+
mock_response.raise_for_status.side_effect = Exception("Upload failed")
|
|
188
|
+
mock_put.return_value = mock_response
|
|
189
|
+
|
|
190
|
+
with pytest.raises(Exception, match="Upload failed"):
|
|
191
|
+
client.v1.files.upload_file(tmp_path)
|
|
192
|
+
|
|
193
|
+
os.remove(tmp_path)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# Test different file types to ensure proper type detection
|
|
197
|
+
def test_upload_different_file_types():
|
|
198
|
+
test_cases = [
|
|
199
|
+
(".mp4", "video"),
|
|
200
|
+
(".mov", "video"),
|
|
201
|
+
(".webm", "video"),
|
|
202
|
+
(".mp3", "audio"),
|
|
203
|
+
(".wav", "audio"),
|
|
204
|
+
(".png", "image"),
|
|
205
|
+
(".jpg", "image"),
|
|
206
|
+
(".jpeg", "image"),
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
210
|
+
|
|
211
|
+
for extension, expected_type in test_cases:
|
|
212
|
+
data = b"test data"
|
|
213
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=extension) as tmp:
|
|
214
|
+
tmp.write(data)
|
|
215
|
+
tmp_path = tmp.name
|
|
216
|
+
|
|
217
|
+
with mock.patch("httpx.Client.put") as mock_put:
|
|
218
|
+
mock_put.return_value = mock.Mock(
|
|
219
|
+
status_code=200, raise_for_status=lambda: None
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Mock the upload_urls.create to verify correct type is passed
|
|
223
|
+
with mock.patch.object(
|
|
224
|
+
client.v1.files.upload_urls, "create"
|
|
225
|
+
) as mock_create:
|
|
226
|
+
mock_create.return_value = mock.Mock(
|
|
227
|
+
items=[
|
|
228
|
+
mock.Mock(
|
|
229
|
+
upload_url="https://test.com/upload",
|
|
230
|
+
file_path="api-assets/id/video.mp4",
|
|
231
|
+
)
|
|
232
|
+
]
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
result = client.v1.files.upload_file(tmp_path)
|
|
236
|
+
assert result == "api-assets/id/video.mp4"
|
|
237
|
+
|
|
238
|
+
# Verify the correct type and extension were passed
|
|
239
|
+
call_args = mock_create.call_args
|
|
240
|
+
items = call_args.kwargs["items"]
|
|
241
|
+
assert len(items) == 1
|
|
242
|
+
assert items[0]["type_"] == expected_type
|
|
243
|
+
assert items[0]["extension"] == extension[1:] # without the dot
|
|
244
|
+
|
|
245
|
+
os.remove(tmp_path)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# Test URL handling - should skip upload and return URL as is
|
|
249
|
+
def test_upload_file_with_http_url():
|
|
250
|
+
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
251
|
+
|
|
252
|
+
http_url = "http://example.com/image.jpg"
|
|
253
|
+
result = client.v1.files.upload_file(http_url)
|
|
254
|
+
|
|
255
|
+
assert result == http_url
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def test_upload_file_with_https_url():
|
|
259
|
+
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
260
|
+
|
|
261
|
+
https_url = "https://example.com/video.mp4"
|
|
262
|
+
result = client.v1.files.upload_file(https_url)
|
|
263
|
+
|
|
264
|
+
assert result == https_url
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@pytest.mark.asyncio
|
|
268
|
+
async def test_async_upload_file_with_http_url():
|
|
269
|
+
client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
270
|
+
|
|
271
|
+
http_url = "http://example.com/audio.mp3"
|
|
272
|
+
result = await client.v1.files.upload_file(http_url)
|
|
273
|
+
|
|
274
|
+
assert result == http_url
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@pytest.mark.asyncio
|
|
278
|
+
async def test_async_upload_file_with_https_url():
|
|
279
|
+
client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
280
|
+
|
|
281
|
+
https_url = "https://example.com/document.pdf"
|
|
282
|
+
result = await client.v1.files.upload_file(https_url)
|
|
283
|
+
|
|
284
|
+
assert result == https_url
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# Test blob path handling - should skip upload and return blob path as is
|
|
288
|
+
def test_upload_file_with_blob_path():
|
|
289
|
+
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
290
|
+
|
|
291
|
+
blob_path = "api-assets/user123/image.jpg"
|
|
292
|
+
result = client.v1.files.upload_file(blob_path)
|
|
293
|
+
|
|
294
|
+
assert result == blob_path
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def test_upload_file_with_blob_path_different_format():
|
|
298
|
+
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
299
|
+
|
|
300
|
+
blob_path = "api-assets/project456/video.mp4"
|
|
301
|
+
result = client.v1.files.upload_file(blob_path)
|
|
302
|
+
|
|
303
|
+
assert result == blob_path
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@pytest.mark.asyncio
|
|
307
|
+
async def test_async_upload_file_with_blob_path():
|
|
308
|
+
client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
309
|
+
|
|
310
|
+
blob_path = "api-assets/user789/audio.wav"
|
|
311
|
+
result = await client.v1.files.upload_file(blob_path)
|
|
312
|
+
|
|
313
|
+
assert result == blob_path
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@pytest.mark.asyncio
|
|
317
|
+
async def test_async_upload_file_with_blob_path_different_format():
|
|
318
|
+
client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
319
|
+
|
|
320
|
+
blob_path = "api-assets/session101/photo.png"
|
|
321
|
+
result = await client.v1.files.upload_file(blob_path)
|
|
322
|
+
|
|
323
|
+
assert result == blob_path
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# Test that URL and blob path handling doesn't make HTTP requests
|
|
327
|
+
def test_upload_file_with_url_does_not_make_http_requests():
|
|
328
|
+
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
329
|
+
|
|
330
|
+
with mock.patch("httpx.Client.put") as mock_put:
|
|
331
|
+
with mock.patch.object(client.v1.files.upload_urls, "create") as mock_create:
|
|
332
|
+
result = client.v1.files.upload_file("https://example.com/file.jpg")
|
|
333
|
+
|
|
334
|
+
# Should not call upload_urls.create or make HTTP PUT request
|
|
335
|
+
mock_create.assert_not_called()
|
|
336
|
+
mock_put.assert_not_called()
|
|
337
|
+
|
|
338
|
+
assert result == "https://example.com/file.jpg"
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def test_upload_file_with_blob_path_does_not_make_http_requests():
|
|
342
|
+
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
343
|
+
|
|
344
|
+
with mock.patch("httpx.Client.put") as mock_put:
|
|
345
|
+
with mock.patch.object(client.v1.files.upload_urls, "create") as mock_create:
|
|
346
|
+
result = client.v1.files.upload_file("api-assets/user123/file.mp4")
|
|
347
|
+
|
|
348
|
+
# Should not call upload_urls.create or make HTTP PUT request
|
|
349
|
+
mock_create.assert_not_called()
|
|
350
|
+
mock_put.assert_not_called()
|
|
351
|
+
|
|
352
|
+
assert result == "api-assets/user123/file.mp4"
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@pytest.mark.asyncio
|
|
356
|
+
async def test_async_upload_file_with_url_does_not_make_http_requests():
|
|
357
|
+
client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
358
|
+
|
|
359
|
+
with mock.patch("httpx.AsyncClient.put", new_callable=mock.AsyncMock) as mock_put:
|
|
360
|
+
with mock.patch.object(
|
|
361
|
+
client.v1.files.upload_urls, "create", new_callable=mock.AsyncMock
|
|
362
|
+
) as mock_create:
|
|
363
|
+
result = await client.v1.files.upload_file("http://example.com/file.mp3")
|
|
364
|
+
|
|
365
|
+
# Should not call upload_urls.create or make HTTP PUT request
|
|
366
|
+
mock_create.assert_not_awaited()
|
|
367
|
+
mock_put.assert_not_awaited()
|
|
368
|
+
|
|
369
|
+
assert result == "http://example.com/file.mp3"
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@pytest.mark.asyncio
|
|
373
|
+
async def test_async_upload_file_with_blob_path_does_not_make_http_requests():
|
|
374
|
+
client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
375
|
+
|
|
376
|
+
with mock.patch("httpx.AsyncClient.put", new_callable=mock.AsyncMock) as mock_put:
|
|
377
|
+
with mock.patch.object(
|
|
378
|
+
client.v1.files.upload_urls, "create", new_callable=mock.AsyncMock
|
|
379
|
+
) as mock_create:
|
|
380
|
+
result = await client.v1.files.upload_file("api-assets/user456/file.wav")
|
|
381
|
+
|
|
382
|
+
# Should not call upload_urls.create or make HTTP PUT request
|
|
383
|
+
mock_create.assert_not_awaited()
|
|
384
|
+
mock_put.assert_not_awaited()
|
|
385
|
+
|
|
386
|
+
assert result == "api-assets/user456/file.wav"
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
# Test file position preservation for file-like objects
|
|
390
|
+
def test_upload_file_preserves_file_position():
|
|
391
|
+
data = b"test data for position preservation"
|
|
392
|
+
file_obj = io.BytesIO(data)
|
|
393
|
+
file_obj.name = "test.mp4"
|
|
394
|
+
|
|
395
|
+
# Move to middle of file
|
|
396
|
+
file_obj.seek(5)
|
|
397
|
+
original_position = file_obj.tell()
|
|
398
|
+
|
|
399
|
+
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)
|
|
400
|
+
|
|
401
|
+
with mock.patch("httpx.Client.put") as mock_put:
|
|
402
|
+
mock_put.return_value = mock.Mock(
|
|
403
|
+
status_code=200, raise_for_status=lambda: None
|
|
404
|
+
)
|
|
405
|
+
client.v1.files.upload_file(file_obj)
|
|
406
|
+
|
|
407
|
+
# Verify position was restored
|
|
408
|
+
assert file_obj.tell() == original_position
|
|
409
|
+
|
|
410
|
+
# Verify the full content was uploaded (not just from position 5)
|
|
411
|
+
mock_put.assert_called_once_with(
|
|
412
|
+
url=mock.ANY,
|
|
413
|
+
content=data, # Full data, not data[5:]
|
|
414
|
+
)
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# v1_files_upload_urls
|
|
2
|
+
|
|
3
|
+
## Module Functions
|
|
4
|
+
|
|
5
|
+
<!-- CUSTOM DOCS START -->
|
|
6
|
+
|
|
7
|
+
<!-- CUSTOM DOCS END -->
|
|
1
8
|
|
|
2
9
|
### Generate asset upload urls <a name="create"></a>
|
|
3
10
|
|
|
@@ -69,3 +76,4 @@ res = await client.v1.files.upload_urls.create(
|
|
|
69
76
|
|
|
70
77
|
##### Example
|
|
71
78
|
`{"items": [{"expires_at": "2024-07-25T16:56:21.932Z", "file_path": "api-assets/id/video.mp4", "upload_url": "https://videos.magichour.ai/api-assets/id/video.mp4?auth-value=1234567890"}, {"expires_at": "2024-07-25T16:56:21.932Z", "file_path": "api-assets/id/audio.mp3", "upload_url": "https://videos.magichour.ai/api-assets/id/audio.mp3?auth-value=1234567890"}]}`
|
|
79
|
+
|
|
@@ -1,3 +1,68 @@
|
|
|
1
|
+
# v1_image_background_remover
|
|
2
|
+
|
|
3
|
+
## Module Functions
|
|
4
|
+
|
|
5
|
+
<!-- CUSTOM DOCS START -->
|
|
6
|
+
|
|
7
|
+
### Image Background Remover Generate Workflow <a name="generate"></a>
|
|
8
|
+
|
|
9
|
+
The workflow performs the following action
|
|
10
|
+
|
|
11
|
+
1. upload local assets to Magic Hour storage. So you can pass in a local path instead of having to upload files yourself
|
|
12
|
+
2. trigger a generation
|
|
13
|
+
3. poll for a completion status. This is configurable
|
|
14
|
+
4. if success, download the output to local directory
|
|
15
|
+
|
|
16
|
+
> [!TIP]
|
|
17
|
+
> This is the recommended way to use the SDK unless you have specific needs where it is necessary to split up the actions.
|
|
18
|
+
|
|
19
|
+
#### Parameters
|
|
20
|
+
|
|
21
|
+
In Additional to the parameters listed in the `.create` section below, `.generate` introduces 3 new parameters:
|
|
22
|
+
|
|
23
|
+
- `wait_for_completion` (bool, default True): Whether to wait for the project to complete.
|
|
24
|
+
- `download_outputs` (bool, default True): Whether to download the generated files
|
|
25
|
+
- `download_directory` (str, optional): Directory to save downloaded files (defaults to current directory)
|
|
26
|
+
|
|
27
|
+
#### Synchronous Client
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from magic_hour import Client
|
|
31
|
+
from os import getenv
|
|
32
|
+
|
|
33
|
+
client = Client(token=getenv("API_TOKEN"))
|
|
34
|
+
res = client.v1.image_background_remover.generate(
|
|
35
|
+
assets={
|
|
36
|
+
"background_image_file_path": "/path/to/1234.png",
|
|
37
|
+
"image_file_path": "/path/to/1234.png",
|
|
38
|
+
},
|
|
39
|
+
name="Background Remover image",
|
|
40
|
+
wait_for_completion=True,
|
|
41
|
+
download_outputs=True,
|
|
42
|
+
download_directory="outputs"
|
|
43
|
+
)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
#### Asynchronous Client
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from magic_hour import AsyncClient
|
|
50
|
+
from os import getenv
|
|
51
|
+
|
|
52
|
+
client = AsyncClient(token=getenv("API_TOKEN"))
|
|
53
|
+
res = await client.v1.image_background_remover.generate(
|
|
54
|
+
assets={
|
|
55
|
+
"background_image_file_path": "/path/to/1234.png",
|
|
56
|
+
"image_file_path": "/path/to/1234.png",
|
|
57
|
+
},
|
|
58
|
+
name="Background Remover image",
|
|
59
|
+
wait_for_completion=True,
|
|
60
|
+
download_outputs=True,
|
|
61
|
+
download_directory="outputs"
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
<!-- CUSTOM DOCS END -->
|
|
1
66
|
|
|
2
67
|
### Image Background Remover <a name="create"></a>
|
|
3
68
|
|
|
@@ -10,6 +75,8 @@ Remove background from image. Each image costs 5 credits.
|
|
|
10
75
|
| Parameter | Required | Description | Example |
|
|
11
76
|
|-----------|:--------:|-------------|--------|
|
|
12
77
|
| `assets` | ✓ | Provide the assets for background removal | `{"background_image_file_path": "api-assets/id/1234.png", "image_file_path": "api-assets/id/1234.png"}` |
|
|
78
|
+
| `└─ background_image_file_path` | ✗ | The image used as the new background for the image_file_path. This image will be resized to match the image in image_file_path. Please make sure the resolution between the images are similar. This value is either - a direct URL to the video file - `file_path` field from the response of the [upload urls API](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls). Please refer to the [Input File documentation](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls#input-file) to learn more. | `"api-assets/id/1234.png"` |
|
|
79
|
+
| `└─ image_file_path` | ✓ | The image to remove the background. This value is either - a direct URL to the video file - `file_path` field from the response of the [upload urls API](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls). Please refer to the [Input File documentation](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls#input-file) to learn more. | `"api-assets/id/1234.png"` |
|
|
13
80
|
| `name` | ✗ | The name of image. This value is mainly used for your own identification of the image. | `"Background Remover image"` |
|
|
14
81
|
|
|
15
82
|
#### Synchronous Client
|
|
@@ -53,3 +120,4 @@ res = await client.v1.image_background_remover.create(
|
|
|
53
120
|
|
|
54
121
|
##### Example
|
|
55
122
|
`{"credits_charged": 5, "frame_cost": 5, "id": "cuid-example"}`
|
|
123
|
+
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import typing
|
|
2
3
|
|
|
3
4
|
from magic_hour.core import (
|
|
@@ -8,13 +9,82 @@ from magic_hour.core import (
|
|
|
8
9
|
to_encodable,
|
|
9
10
|
type_utils,
|
|
10
11
|
)
|
|
12
|
+
from magic_hour.resources.v1.files.client import AsyncFilesClient, FilesClient
|
|
13
|
+
from magic_hour.resources.v1.image_projects.client import (
|
|
14
|
+
AsyncImageProjectsClient,
|
|
15
|
+
ImageProjectsClient,
|
|
16
|
+
)
|
|
11
17
|
from magic_hour.types import models, params
|
|
12
18
|
|
|
13
19
|
|
|
20
|
+
logging.basicConfig(level=logging.INFO)
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
14
24
|
class ImageBackgroundRemoverClient:
|
|
15
25
|
def __init__(self, *, base_client: SyncBaseClient):
|
|
16
26
|
self._base_client = base_client
|
|
17
27
|
|
|
28
|
+
def generate(
|
|
29
|
+
self,
|
|
30
|
+
*,
|
|
31
|
+
assets: params.V1ImageBackgroundRemoverGenerateBodyAssets,
|
|
32
|
+
name: typing.Union[
|
|
33
|
+
typing.Optional[str], type_utils.NotGiven
|
|
34
|
+
] = type_utils.NOT_GIVEN,
|
|
35
|
+
wait_for_completion: bool = True,
|
|
36
|
+
download_outputs: bool = True,
|
|
37
|
+
download_directory: typing.Optional[str] = None,
|
|
38
|
+
request_options: typing.Optional[RequestOptions] = None,
|
|
39
|
+
):
|
|
40
|
+
"""
|
|
41
|
+
Generate background removed image (alias for create with additional functionality).
|
|
42
|
+
|
|
43
|
+
Remove background from image. Each removal costs 5 credits.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
name: The name of image. This value is mainly used for your own identification of the image.
|
|
47
|
+
assets: Provide the assets for background removal
|
|
48
|
+
wait_for_completion: Whether to wait for the image project to complete
|
|
49
|
+
download_outputs: Whether to download the outputs
|
|
50
|
+
download_directory: The directory to download the outputs to. If not provided, the outputs will be downloaded to the current working directory
|
|
51
|
+
request_options: Additional options to customize the HTTP request
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
V1ImageProjectsGetResponseWithDownloads: The response from the Image Background Remover API with the downloaded paths if `download_outputs` is True.
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
```py
|
|
58
|
+
response = client.v1.image_background_remover.generate(
|
|
59
|
+
assets={"image_file_path": "path/to/image.png"},
|
|
60
|
+
name="Background Removed Image",
|
|
61
|
+
wait_for_completion=True,
|
|
62
|
+
download_outputs=True,
|
|
63
|
+
download_directory="outputs/",
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
file_client = FilesClient(base_client=self._base_client)
|
|
69
|
+
|
|
70
|
+
image_file_path = assets["image_file_path"]
|
|
71
|
+
assets["image_file_path"] = file_client.upload_file(file=image_file_path)
|
|
72
|
+
|
|
73
|
+
create_response = self.create(
|
|
74
|
+
assets=assets, name=name, request_options=request_options
|
|
75
|
+
)
|
|
76
|
+
logger.info(f"Image Background Remover response: {create_response}")
|
|
77
|
+
|
|
78
|
+
image_projects_client = ImageProjectsClient(base_client=self._base_client)
|
|
79
|
+
response = image_projects_client.check_result(
|
|
80
|
+
id=create_response.id,
|
|
81
|
+
wait_for_completion=wait_for_completion,
|
|
82
|
+
download_outputs=download_outputs,
|
|
83
|
+
download_directory=download_directory,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return response
|
|
87
|
+
|
|
18
88
|
def create(
|
|
19
89
|
self,
|
|
20
90
|
*,
|
|
@@ -72,6 +142,66 @@ class AsyncImageBackgroundRemoverClient:
|
|
|
72
142
|
def __init__(self, *, base_client: AsyncBaseClient):
|
|
73
143
|
self._base_client = base_client
|
|
74
144
|
|
|
145
|
+
async def generate(
|
|
146
|
+
self,
|
|
147
|
+
*,
|
|
148
|
+
assets: params.V1ImageBackgroundRemoverGenerateBodyAssets,
|
|
149
|
+
name: typing.Union[
|
|
150
|
+
typing.Optional[str], type_utils.NotGiven
|
|
151
|
+
] = type_utils.NOT_GIVEN,
|
|
152
|
+
wait_for_completion: bool = True,
|
|
153
|
+
download_outputs: bool = True,
|
|
154
|
+
download_directory: typing.Optional[str] = None,
|
|
155
|
+
request_options: typing.Optional[RequestOptions] = None,
|
|
156
|
+
):
|
|
157
|
+
"""
|
|
158
|
+
Generate background removed image (alias for create with additional functionality).
|
|
159
|
+
|
|
160
|
+
Remove background from image. Each removal costs 5 credits.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
name: The name of image. This value is mainly used for your own identification of the image.
|
|
164
|
+
assets: Provide the assets for background removal
|
|
165
|
+
wait_for_completion: Whether to wait for the image project to complete
|
|
166
|
+
download_outputs: Whether to download the outputs
|
|
167
|
+
download_directory: The directory to download the outputs to. If not provided, the outputs will be downloaded to the current working directory
|
|
168
|
+
request_options: Additional options to customize the HTTP request
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
V1ImageProjectsGetResponseWithDownloads: The response from the Image Background Remover API with the downloaded paths if `download_outputs` is True.
|
|
172
|
+
|
|
173
|
+
Examples:
|
|
174
|
+
```py
|
|
175
|
+
response = await client.v1.image_background_remover.generate(
|
|
176
|
+
assets={"image_file_path": "path/to/image.png"},
|
|
177
|
+
name="Background Removed Image",
|
|
178
|
+
wait_for_completion=True,
|
|
179
|
+
download_outputs=True,
|
|
180
|
+
download_directory="outputs/",
|
|
181
|
+
)
|
|
182
|
+
```
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
file_client = AsyncFilesClient(base_client=self._base_client)
|
|
186
|
+
|
|
187
|
+
image_file_path = assets["image_file_path"]
|
|
188
|
+
assets["image_file_path"] = await file_client.upload_file(file=image_file_path)
|
|
189
|
+
|
|
190
|
+
create_response = await self.create(
|
|
191
|
+
assets=assets, name=name, request_options=request_options
|
|
192
|
+
)
|
|
193
|
+
logger.info(f"Image Background Remover response: {create_response}")
|
|
194
|
+
|
|
195
|
+
image_projects_client = AsyncImageProjectsClient(base_client=self._base_client)
|
|
196
|
+
response = await image_projects_client.check_result(
|
|
197
|
+
id=create_response.id,
|
|
198
|
+
wait_for_completion=wait_for_completion,
|
|
199
|
+
download_outputs=download_outputs,
|
|
200
|
+
download_directory=download_directory,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return response
|
|
204
|
+
|
|
75
205
|
async def create(
|
|
76
206
|
self,
|
|
77
207
|
*,
|