magic_hour 0.9.5__py3-none-any.whl → 0.44.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.
- magic_hour/README.md +34 -0
- magic_hour/__init__.py +1 -1
- magic_hour/client.py +8 -17
- magic_hour/environment.py +13 -1
- magic_hour/helpers/__init__.py +4 -0
- magic_hour/helpers/download.py +77 -0
- magic_hour/helpers/logger.py +8 -0
- magic_hour/resources/v1/README.md +32 -0
- magic_hour/resources/v1/ai_clothes_changer/README.md +94 -5
- magic_hour/resources/v1/ai_clothes_changer/client.py +161 -16
- magic_hour/resources/v1/ai_face_editor/README.md +195 -0
- magic_hour/resources/v1/ai_face_editor/__init__.py +4 -0
- magic_hour/resources/v1/ai_face_editor/client.py +324 -0
- magic_hour/resources/v1/ai_gif_generator/README.md +116 -0
- magic_hour/resources/v1/ai_gif_generator/__init__.py +4 -0
- magic_hour/resources/v1/ai_gif_generator/client.py +257 -0
- magic_hour/resources/v1/ai_headshot_generator/README.md +81 -3
- magic_hour/resources/v1/ai_headshot_generator/client.py +167 -18
- magic_hour/resources/v1/ai_image_editor/README.md +125 -0
- magic_hour/resources/v1/ai_image_editor/__init__.py +4 -0
- magic_hour/resources/v1/ai_image_editor/client.py +290 -0
- magic_hour/resources/v1/ai_image_generator/README.md +99 -5
- magic_hour/resources/v1/ai_image_generator/client.py +170 -24
- magic_hour/resources/v1/ai_image_upscaler/README.md +89 -3
- magic_hour/resources/v1/ai_image_upscaler/client.py +173 -20
- magic_hour/resources/v1/ai_meme_generator/README.md +129 -0
- magic_hour/resources/v1/ai_meme_generator/__init__.py +4 -0
- magic_hour/resources/v1/ai_meme_generator/client.py +253 -0
- magic_hour/resources/v1/ai_photo_editor/README.md +119 -4
- magic_hour/resources/v1/ai_photo_editor/client.py +199 -18
- magic_hour/resources/v1/ai_qr_code_generator/README.md +84 -3
- magic_hour/resources/v1/ai_qr_code_generator/client.py +140 -18
- magic_hour/resources/v1/ai_talking_photo/README.md +137 -0
- magic_hour/resources/v1/ai_talking_photo/__init__.py +4 -0
- magic_hour/resources/v1/ai_talking_photo/client.py +326 -0
- magic_hour/resources/v1/ai_voice_cloner/README.md +62 -0
- magic_hour/resources/v1/ai_voice_cloner/__init__.py +4 -0
- magic_hour/resources/v1/ai_voice_cloner/client.py +272 -0
- magic_hour/resources/v1/ai_voice_generator/README.md +112 -0
- magic_hour/resources/v1/ai_voice_generator/__init__.py +4 -0
- magic_hour/resources/v1/ai_voice_generator/client.py +241 -0
- magic_hour/resources/v1/animation/README.md +128 -6
- magic_hour/resources/v1/animation/client.py +247 -22
- magic_hour/resources/v1/audio_projects/README.md +135 -0
- magic_hour/resources/v1/audio_projects/__init__.py +12 -0
- magic_hour/resources/v1/audio_projects/client.py +310 -0
- magic_hour/resources/v1/audio_projects/client_test.py +520 -0
- magic_hour/resources/v1/auto_subtitle_generator/README.md +128 -0
- magic_hour/resources/v1/auto_subtitle_generator/__init__.py +4 -0
- magic_hour/resources/v1/auto_subtitle_generator/client.py +346 -0
- magic_hour/resources/v1/client.py +75 -1
- magic_hour/resources/v1/face_detection/README.md +157 -0
- magic_hour/resources/v1/face_detection/__init__.py +12 -0
- magic_hour/resources/v1/face_detection/client.py +380 -0
- magic_hour/resources/v1/face_swap/README.md +137 -9
- magic_hour/resources/v1/face_swap/client.py +329 -38
- magic_hour/resources/v1/face_swap_photo/README.md +118 -3
- magic_hour/resources/v1/face_swap_photo/client.py +199 -14
- magic_hour/resources/v1/files/README.md +39 -0
- magic_hour/resources/v1/files/client.py +351 -1
- magic_hour/resources/v1/files/client_test.py +414 -0
- magic_hour/resources/v1/files/upload_urls/README.md +38 -17
- magic_hour/resources/v1/files/upload_urls/client.py +38 -34
- magic_hour/resources/v1/image_background_remover/README.md +96 -5
- magic_hour/resources/v1/image_background_remover/client.py +151 -16
- magic_hour/resources/v1/image_projects/README.md +82 -10
- magic_hour/resources/v1/image_projects/__init__.py +10 -2
- magic_hour/resources/v1/image_projects/client.py +154 -16
- magic_hour/resources/v1/image_projects/client_test.py +527 -0
- magic_hour/resources/v1/image_to_video/README.md +96 -11
- magic_hour/resources/v1/image_to_video/client.py +282 -38
- magic_hour/resources/v1/lip_sync/README.md +112 -9
- magic_hour/resources/v1/lip_sync/client.py +288 -34
- magic_hour/resources/v1/photo_colorizer/README.md +107 -0
- magic_hour/resources/v1/photo_colorizer/__init__.py +4 -0
- magic_hour/resources/v1/photo_colorizer/client.py +248 -0
- magic_hour/resources/v1/text_to_video/README.md +96 -7
- magic_hour/resources/v1/text_to_video/client.py +204 -18
- magic_hour/resources/v1/video_projects/README.md +81 -9
- magic_hour/resources/v1/video_projects/__init__.py +10 -2
- magic_hour/resources/v1/video_projects/client.py +151 -14
- magic_hour/resources/v1/video_projects/client_test.py +527 -0
- magic_hour/resources/v1/video_to_video/README.md +119 -15
- magic_hour/resources/v1/video_to_video/client.py +299 -46
- magic_hour/types/models/__init__.py +92 -56
- magic_hour/types/models/v1_ai_clothes_changer_create_response.py +33 -0
- magic_hour/types/models/v1_ai_face_editor_create_response.py +33 -0
- magic_hour/types/models/v1_ai_gif_generator_create_response.py +33 -0
- magic_hour/types/models/v1_ai_headshot_generator_create_response.py +33 -0
- magic_hour/types/models/v1_ai_image_editor_create_response.py +33 -0
- magic_hour/types/models/v1_ai_image_generator_create_response.py +33 -0
- magic_hour/types/models/v1_ai_image_upscaler_create_response.py +33 -0
- magic_hour/types/models/v1_ai_meme_generator_create_response.py +33 -0
- magic_hour/types/models/v1_ai_photo_editor_create_response.py +33 -0
- magic_hour/types/models/v1_ai_qr_code_generator_create_response.py +33 -0
- magic_hour/types/models/v1_ai_talking_photo_create_response.py +35 -0
- magic_hour/types/models/v1_ai_voice_cloner_create_response.py +27 -0
- magic_hour/types/models/v1_ai_voice_generator_create_response.py +27 -0
- magic_hour/types/models/v1_animation_create_response.py +35 -0
- magic_hour/types/models/v1_audio_projects_get_response.py +72 -0
- magic_hour/types/models/v1_audio_projects_get_response_downloads_item.py +19 -0
- magic_hour/types/models/{get_v1_image_projects_id_response_error.py → v1_audio_projects_get_response_error.py} +2 -2
- magic_hour/types/models/v1_auto_subtitle_generator_create_response.py +35 -0
- magic_hour/types/models/v1_face_detection_create_response.py +25 -0
- magic_hour/types/models/v1_face_detection_get_response.py +45 -0
- magic_hour/types/models/v1_face_detection_get_response_faces_item.py +25 -0
- magic_hour/types/models/v1_face_swap_create_response.py +35 -0
- magic_hour/types/models/v1_face_swap_photo_create_response.py +33 -0
- magic_hour/types/models/v1_files_upload_urls_create_response.py +24 -0
- magic_hour/types/models/{post_v1_files_upload_urls_response_items_item.py → v1_files_upload_urls_create_response_items_item.py} +2 -2
- magic_hour/types/models/v1_image_background_remover_create_response.py +33 -0
- magic_hour/types/models/{get_v1_image_projects_id_response.py → v1_image_projects_get_response.py} +20 -18
- magic_hour/types/models/{get_v1_video_projects_id_response_downloads_item.py → v1_image_projects_get_response_downloads_item.py} +1 -1
- magic_hour/types/models/{get_v1_video_projects_id_response_error.py → v1_image_projects_get_response_error.py} +2 -2
- magic_hour/types/models/v1_image_to_video_create_response.py +35 -0
- magic_hour/types/models/v1_lip_sync_create_response.py +35 -0
- magic_hour/types/models/v1_photo_colorizer_create_response.py +33 -0
- magic_hour/types/models/v1_text_to_video_create_response.py +35 -0
- magic_hour/types/models/{get_v1_video_projects_id_response.py → v1_video_projects_get_response.py} +26 -23
- magic_hour/types/models/{get_v1_video_projects_id_response_download.py → v1_video_projects_get_response_download.py} +1 -1
- magic_hour/types/models/{get_v1_image_projects_id_response_downloads_item.py → v1_video_projects_get_response_downloads_item.py} +1 -1
- magic_hour/types/models/v1_video_projects_get_response_error.py +25 -0
- magic_hour/types/models/v1_video_to_video_create_response.py +35 -0
- magic_hour/types/params/__init__.py +422 -176
- magic_hour/types/params/v1_ai_clothes_changer_create_body.py +40 -0
- magic_hour/types/params/v1_ai_clothes_changer_create_body_assets.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_create_body.py +52 -0
- magic_hour/types/params/v1_ai_face_editor_create_body_assets.py +33 -0
- magic_hour/types/params/v1_ai_face_editor_create_body_style.py +137 -0
- magic_hour/types/params/v1_ai_face_editor_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_ai_gif_generator_create_body.py +47 -0
- magic_hour/types/params/{post_v1_ai_image_generator_body_style.py → v1_ai_gif_generator_create_body_style.py} +5 -5
- magic_hour/types/params/v1_ai_headshot_generator_create_body.py +49 -0
- magic_hour/types/params/v1_ai_headshot_generator_create_body_assets.py +33 -0
- magic_hour/types/params/v1_ai_headshot_generator_create_body_style.py +27 -0
- magic_hour/types/params/v1_ai_headshot_generator_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_ai_image_editor_create_body.py +49 -0
- magic_hour/types/params/v1_ai_image_editor_create_body_assets.py +47 -0
- magic_hour/types/params/v1_ai_image_editor_create_body_style.py +41 -0
- magic_hour/types/params/v1_ai_image_editor_generate_body_assets.py +28 -0
- magic_hour/types/params/{post_v1_ai_image_generator_body.py → v1_ai_image_generator_create_body.py} +17 -11
- magic_hour/types/params/v1_ai_image_generator_create_body_style.py +127 -0
- magic_hour/types/params/v1_ai_image_upscaler_create_body.py +59 -0
- magic_hour/types/params/v1_ai_image_upscaler_create_body_assets.py +33 -0
- magic_hour/types/params/{post_v1_ai_image_upscaler_body_style.py → v1_ai_image_upscaler_create_body_style.py} +4 -4
- magic_hour/types/params/v1_ai_image_upscaler_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_ai_meme_generator_create_body.py +37 -0
- magic_hour/types/params/v1_ai_meme_generator_create_body_style.py +73 -0
- magic_hour/types/params/{post_v1_ai_photo_editor_body.py → v1_ai_photo_editor_create_body.py} +15 -15
- magic_hour/types/params/v1_ai_photo_editor_create_body_assets.py +33 -0
- magic_hour/types/params/{post_v1_ai_photo_editor_body_style.py → v1_ai_photo_editor_create_body_style.py} +20 -4
- magic_hour/types/params/v1_ai_photo_editor_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_ai_qr_code_generator_create_body.py +45 -0
- magic_hour/types/params/{post_v1_ai_qr_code_generator_body_style.py → v1_ai_qr_code_generator_create_body_style.py} +4 -4
- magic_hour/types/params/v1_ai_talking_photo_create_body.py +68 -0
- magic_hour/types/params/v1_ai_talking_photo_create_body_assets.py +46 -0
- magic_hour/types/params/v1_ai_talking_photo_create_body_style.py +44 -0
- magic_hour/types/params/v1_ai_talking_photo_generate_body_assets.py +26 -0
- magic_hour/types/params/v1_ai_voice_cloner_create_body.py +49 -0
- magic_hour/types/params/v1_ai_voice_cloner_create_body_assets.py +33 -0
- magic_hour/types/params/v1_ai_voice_cloner_create_body_style.py +28 -0
- magic_hour/types/params/v1_ai_voice_cloner_generate_body_assets.py +28 -0
- magic_hour/types/params/v1_ai_voice_generator_create_body.py +40 -0
- magic_hour/types/params/v1_ai_voice_generator_create_body_style.py +440 -0
- magic_hour/types/params/{post_v1_animation_body.py → v1_animation_create_body.py} +16 -16
- magic_hour/types/params/{post_v1_animation_body_assets.py → v1_animation_create_body_assets.py} +15 -5
- magic_hour/types/params/{post_v1_animation_body_style.py → v1_animation_create_body_style.py} +13 -10
- magic_hour/types/params/v1_animation_generate_body_assets.py +39 -0
- magic_hour/types/params/v1_auto_subtitle_generator_create_body.py +78 -0
- magic_hour/types/params/v1_auto_subtitle_generator_create_body_assets.py +33 -0
- magic_hour/types/params/v1_auto_subtitle_generator_create_body_style.py +56 -0
- magic_hour/types/params/v1_auto_subtitle_generator_create_body_style_custom_config.py +86 -0
- magic_hour/types/params/v1_auto_subtitle_generator_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_face_detection_create_body.py +44 -0
- magic_hour/types/params/v1_face_detection_create_body_assets.py +33 -0
- magic_hour/types/params/v1_face_detection_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_face_swap_create_body.py +92 -0
- magic_hour/types/params/v1_face_swap_create_body_assets.py +91 -0
- magic_hour/types/params/v1_face_swap_create_body_assets_face_mappings_item.py +44 -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_create_body.py +40 -0
- magic_hour/types/params/v1_face_swap_photo_create_body_assets.py +76 -0
- magic_hour/types/params/v1_face_swap_photo_create_body_assets_face_mappings_item.py +44 -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_files_upload_urls_create_body.py +36 -0
- magic_hour/types/params/v1_files_upload_urls_create_body_items_item.py +38 -0
- magic_hour/types/params/v1_image_background_remover_create_body.py +40 -0
- magic_hour/types/params/v1_image_background_remover_create_body_assets.py +49 -0
- magic_hour/types/params/v1_image_background_remover_generate_body_assets.py +27 -0
- magic_hour/types/params/v1_image_to_video_create_body.py +101 -0
- magic_hour/types/params/v1_image_to_video_create_body_assets.py +33 -0
- magic_hour/types/params/v1_image_to_video_create_body_style.py +53 -0
- magic_hour/types/params/v1_image_to_video_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_lip_sync_create_body.py +100 -0
- magic_hour/types/params/{post_v1_lip_sync_body_assets.py → v1_lip_sync_create_body_assets.py} +15 -5
- magic_hour/types/params/v1_lip_sync_create_body_style.py +37 -0
- magic_hour/types/params/v1_lip_sync_generate_body_assets.py +36 -0
- magic_hour/types/params/v1_photo_colorizer_create_body.py +40 -0
- magic_hour/types/params/v1_photo_colorizer_create_body_assets.py +33 -0
- magic_hour/types/params/v1_photo_colorizer_generate_body_assets.py +17 -0
- magic_hour/types/params/v1_text_to_video_create_body.py +78 -0
- magic_hour/types/params/v1_text_to_video_create_body_style.py +43 -0
- magic_hour/types/params/v1_video_to_video_create_body.py +101 -0
- magic_hour/types/params/{post_v1_video_to_video_body_assets.py → v1_video_to_video_create_body_assets.py} +9 -4
- magic_hour/types/params/{post_v1_video_to_video_body_style.py → v1_video_to_video_create_body_style.py} +68 -26
- magic_hour/types/params/v1_video_to_video_generate_body_assets.py +27 -0
- magic_hour-0.44.0.dist-info/METADATA +328 -0
- magic_hour-0.44.0.dist-info/RECORD +231 -0
- magic_hour/core/__init__.py +0 -52
- magic_hour/core/api_error.py +0 -56
- magic_hour/core/auth.py +0 -314
- magic_hour/core/base_client.py +0 -618
- magic_hour/core/binary_response.py +0 -23
- magic_hour/core/query.py +0 -106
- magic_hour/core/request.py +0 -156
- magic_hour/core/response.py +0 -293
- magic_hour/core/type_utils.py +0 -28
- magic_hour/core/utils.py +0 -55
- magic_hour/types/models/post_v1_ai_clothes_changer_response.py +0 -25
- magic_hour/types/models/post_v1_ai_headshot_generator_response.py +0 -25
- magic_hour/types/models/post_v1_ai_image_generator_response.py +0 -25
- magic_hour/types/models/post_v1_ai_image_upscaler_response.py +0 -25
- magic_hour/types/models/post_v1_ai_photo_editor_response.py +0 -25
- magic_hour/types/models/post_v1_ai_qr_code_generator_response.py +0 -25
- magic_hour/types/models/post_v1_animation_response.py +0 -25
- magic_hour/types/models/post_v1_face_swap_photo_response.py +0 -25
- magic_hour/types/models/post_v1_face_swap_response.py +0 -25
- magic_hour/types/models/post_v1_files_upload_urls_response.py +0 -21
- magic_hour/types/models/post_v1_image_background_remover_response.py +0 -25
- magic_hour/types/models/post_v1_image_to_video_response.py +0 -25
- magic_hour/types/models/post_v1_lip_sync_response.py +0 -25
- magic_hour/types/models/post_v1_text_to_video_response.py +0 -25
- magic_hour/types/models/post_v1_video_to_video_response.py +0 -25
- magic_hour/types/params/post_v1_ai_clothes_changer_body.py +0 -40
- magic_hour/types/params/post_v1_ai_clothes_changer_body_assets.py +0 -45
- magic_hour/types/params/post_v1_ai_headshot_generator_body.py +0 -40
- magic_hour/types/params/post_v1_ai_headshot_generator_body_assets.py +0 -28
- magic_hour/types/params/post_v1_ai_image_upscaler_body.py +0 -57
- magic_hour/types/params/post_v1_ai_image_upscaler_body_assets.py +0 -28
- magic_hour/types/params/post_v1_ai_photo_editor_body_assets.py +0 -28
- magic_hour/types/params/post_v1_ai_qr_code_generator_body.py +0 -45
- magic_hour/types/params/post_v1_face_swap_body.py +0 -72
- magic_hour/types/params/post_v1_face_swap_body_assets.py +0 -52
- magic_hour/types/params/post_v1_face_swap_photo_body.py +0 -40
- magic_hour/types/params/post_v1_face_swap_photo_body_assets.py +0 -36
- magic_hour/types/params/post_v1_files_upload_urls_body.py +0 -31
- magic_hour/types/params/post_v1_files_upload_urls_body_items_item.py +0 -38
- magic_hour/types/params/post_v1_image_background_remover_body.py +0 -40
- magic_hour/types/params/post_v1_image_background_remover_body_assets.py +0 -28
- magic_hour/types/params/post_v1_image_to_video_body.py +0 -73
- magic_hour/types/params/post_v1_image_to_video_body_assets.py +0 -28
- magic_hour/types/params/post_v1_image_to_video_body_style.py +0 -37
- magic_hour/types/params/post_v1_lip_sync_body.py +0 -80
- magic_hour/types/params/post_v1_text_to_video_body.py +0 -57
- magic_hour/types/params/post_v1_text_to_video_body_style.py +0 -28
- magic_hour/types/params/post_v1_video_to_video_body.py +0 -93
- magic_hour-0.9.5.dist-info/METADATA +0 -133
- magic_hour-0.9.5.dist-info/RECORD +0 -132
- {magic_hour-0.9.5.dist-info → magic_hour-0.44.0.dist-info}/LICENSE +0 -0
- {magic_hour-0.9.5.dist-info → magic_hour-0.44.0.dist-info}/WHEEL +0 -0
|
@@ -1,17 +1,367 @@
|
|
|
1
|
-
|
|
1
|
+
import httpx
|
|
2
|
+
import io
|
|
3
|
+
import mimetypes
|
|
4
|
+
import os
|
|
5
|
+
import pathlib
|
|
6
|
+
import typing
|
|
7
|
+
import typing_extensions
|
|
8
|
+
|
|
9
|
+
from magic_hour.helpers.logger import get_sdk_logger
|
|
2
10
|
from magic_hour.resources.v1.files.upload_urls import (
|
|
3
11
|
AsyncUploadUrlsClient,
|
|
4
12
|
UploadUrlsClient,
|
|
5
13
|
)
|
|
14
|
+
from magic_hour.types.params.v1_files_upload_urls_create_body_items_item import (
|
|
15
|
+
V1FilesUploadUrlsCreateBodyItemsItem,
|
|
16
|
+
)
|
|
17
|
+
from make_api_request import AsyncBaseClient, SyncBaseClient
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
logger = get_sdk_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_file_type_and_extension(file_path: str):
|
|
25
|
+
"""
|
|
26
|
+
Determine file type and extension from file path.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
file_path: Path to the file
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Tuple of (file_type, extension) where file_type is one of "video", "audio", or "image"
|
|
33
|
+
and extension is the lowercase file extension without the dot
|
|
34
|
+
"""
|
|
35
|
+
ext = Path(file_path).suffix.lower()
|
|
36
|
+
if ext.startswith("."):
|
|
37
|
+
ext = ext[1:] # Remove the leading dot
|
|
38
|
+
|
|
39
|
+
file_type: typing.Union[
|
|
40
|
+
typing_extensions.Literal["audio", "image", "video"], None
|
|
41
|
+
] = None
|
|
42
|
+
mime, _ = mimetypes.guess_type(file_path)
|
|
43
|
+
if mime:
|
|
44
|
+
if mime.startswith("video/"):
|
|
45
|
+
file_type = "video"
|
|
46
|
+
elif mime.startswith("audio/"):
|
|
47
|
+
file_type = "audio"
|
|
48
|
+
elif mime.startswith("image/"):
|
|
49
|
+
file_type = "image"
|
|
50
|
+
|
|
51
|
+
if not file_type:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"Could not determine file type for {file_path}. "
|
|
54
|
+
"Supported types: video (mp4, m4v, mov, webm), "
|
|
55
|
+
"audio (mp3, mpeg, wav, aac, aiff, flac), "
|
|
56
|
+
"image (png, jpg, jpeg, webp, avif, jp2, tiff, bmp)"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return file_type, ext
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _process_file_input(
|
|
63
|
+
file: typing.Union[str, pathlib.Path, typing.BinaryIO, io.IOBase],
|
|
64
|
+
):
|
|
65
|
+
"""
|
|
66
|
+
Process different file input types and return standardized information.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
file: Path to the local file to upload, or a file-like object
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Tuple of (file_path, file_to_upload, file_type, extension)
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
FileNotFoundError: If the local file is not found
|
|
76
|
+
ValueError: If the file type is not supported or file-like object is invalid
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
if isinstance(file, pathlib.Path):
|
|
80
|
+
file_path = str(file)
|
|
81
|
+
file_to_upload = None
|
|
82
|
+
elif isinstance(file, (io.IOBase, typing.BinaryIO)):
|
|
83
|
+
file_path = None
|
|
84
|
+
file_to_upload = file
|
|
85
|
+
else:
|
|
86
|
+
file_path = file
|
|
87
|
+
file_to_upload = None
|
|
88
|
+
|
|
89
|
+
if file_path is not None:
|
|
90
|
+
if not os.path.isfile(file_path):
|
|
91
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
92
|
+
file_type, extension = _get_file_type_and_extension(file_path)
|
|
93
|
+
else:
|
|
94
|
+
if file_to_upload is None:
|
|
95
|
+
raise ValueError("file_to_upload is None for file-like object case.")
|
|
96
|
+
file_name = getattr(file_to_upload, "name", None)
|
|
97
|
+
if not isinstance(file_name, str):
|
|
98
|
+
raise ValueError(
|
|
99
|
+
"File-like object must have a 'name' attribute of type str for extension detection."
|
|
100
|
+
)
|
|
101
|
+
file_type, extension = _get_file_type_and_extension(file_name)
|
|
102
|
+
|
|
103
|
+
return file_path, file_to_upload, file_type, extension
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _prepare_file_for_upload(
|
|
107
|
+
file_path: typing.Union[str, None],
|
|
108
|
+
file_to_upload: typing.Union[typing.BinaryIO, io.IOBase, None],
|
|
109
|
+
) -> bytes:
|
|
110
|
+
"""
|
|
111
|
+
Read file content for upload, handling both file paths and file-like objects.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
file_path: Path to the file (if using file path)
|
|
115
|
+
file_to_upload: File-like object (if using file-like object)
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
File content as bytes
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
ValueError: If both or neither parameters are provided
|
|
122
|
+
"""
|
|
123
|
+
if file_path is not None:
|
|
124
|
+
with open(file_path, "rb") as f:
|
|
125
|
+
return f.read()
|
|
126
|
+
else:
|
|
127
|
+
if file_to_upload is None:
|
|
128
|
+
raise ValueError("file_to_upload is None for file-like object case.")
|
|
129
|
+
pos = file_to_upload.tell() if hasattr(file_to_upload, "tell") else None
|
|
130
|
+
if hasattr(file_to_upload, "seek"):
|
|
131
|
+
file_to_upload.seek(0)
|
|
132
|
+
content = file_to_upload.read()
|
|
133
|
+
if pos is not None and hasattr(file_to_upload, "seek"):
|
|
134
|
+
file_to_upload.seek(pos)
|
|
135
|
+
return content
|
|
6
136
|
|
|
7
137
|
|
|
8
138
|
class FilesClient:
|
|
139
|
+
"""
|
|
140
|
+
Client for uploading files to Magic Hour's storage.
|
|
141
|
+
|
|
142
|
+
The Files client provides functionality to upload media files (images, videos, audio)
|
|
143
|
+
to Magic Hour's secure storage. Once uploaded, files can be referenced in other API
|
|
144
|
+
calls using the returned file path.
|
|
145
|
+
|
|
146
|
+
Supported file types:
|
|
147
|
+
- **Images**: PNG, JPG, JPEG, WebP, AVIF, JP2, TIFF, BMP
|
|
148
|
+
- **Videos**: MP4, M4V, MOV, WebM
|
|
149
|
+
- **Audio**: MP3, MPEG, WAV, AAC, AIFF, FLAC
|
|
150
|
+
"""
|
|
151
|
+
|
|
9
152
|
def __init__(self, *, base_client: SyncBaseClient):
|
|
10
153
|
self._base_client = base_client
|
|
11
154
|
self.upload_urls = UploadUrlsClient(base_client=self._base_client)
|
|
12
155
|
|
|
156
|
+
def upload_file(
|
|
157
|
+
self,
|
|
158
|
+
file: typing.Union[str, pathlib.Path, typing.BinaryIO, io.IOBase],
|
|
159
|
+
) -> str:
|
|
160
|
+
"""
|
|
161
|
+
Upload a file to Magic Hour's storage.
|
|
162
|
+
|
|
163
|
+
This method uploads a file to Magic Hour's secure cloud storage and returns
|
|
164
|
+
a file path that can be used as input for other Magic Hour API endpoints.
|
|
165
|
+
The file type is automatically detected from the file extension or MIME type.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
file: The file to upload. Can be:
|
|
169
|
+
- **str**: Path to a local file (e.g., "/path/to/image.jpg")
|
|
170
|
+
- **str**: URL of the file to upload, this will be skipped and the URL will be returned as is
|
|
171
|
+
- **str**: if the string begins with "api-assets", the file will be assumed to be a blob path and already uploaded to Magic Hour's storage
|
|
172
|
+
- **pathlib.Path**: Path object to a local file
|
|
173
|
+
- **typing.BinaryIO or io.IOBase**: File-like object (must have a 'name' attribute)
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
str: The uploaded file's path in Magic Hour's storage system.
|
|
177
|
+
This path can be used as input for other API endpoints, such for `.assets.image_file_path`.
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
FileNotFoundError: If the specified local file doesn't exist.
|
|
181
|
+
ValueError: If the file type is not supported or file-like object is invalid.
|
|
182
|
+
httpx.HTTPStatusError: If the upload request fails (network/server errors).
|
|
183
|
+
|
|
184
|
+
Examples:
|
|
185
|
+
Upload a local image file:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from magic_hour import Client
|
|
189
|
+
from os import getenv
|
|
190
|
+
|
|
191
|
+
client = Client(token=getenv("MAGIC_HOUR_API_TOKEN"))
|
|
192
|
+
|
|
193
|
+
# Upload from file path
|
|
194
|
+
file_path = client.v1.files.upload_file("/path/to/your/image.jpg")
|
|
195
|
+
print(f"Uploaded file: {file_path}")
|
|
196
|
+
|
|
197
|
+
# Use the uploaded file in other API calls
|
|
198
|
+
result = client.v1.ai_image_upscaler.create(
|
|
199
|
+
assets={"image_file_path": file_path}, style={"upscale_factor": 2}
|
|
200
|
+
)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Upload using pathlib.Path:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from pathlib import Path
|
|
207
|
+
|
|
208
|
+
image_path = Path("./assets/photo.png")
|
|
209
|
+
file_path = client.v1.files.upload_file(image_path)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Upload from a file-like object:
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
with open("video.mp4", "rb") as video_file:
|
|
216
|
+
file_path = client.v1.files.upload_file(video_file)
|
|
217
|
+
```
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
if isinstance(file, str) and file.startswith(("http://", "https://")):
|
|
221
|
+
logger.info(f"{file} is a url. Skipping upload and returning the URL.")
|
|
222
|
+
return file
|
|
223
|
+
elif isinstance(file, str) and file.startswith("api-assets"):
|
|
224
|
+
logger.info(
|
|
225
|
+
f"{file} is begins with api-assets, assuming it's a blob path.. Skipping upload and returning the path."
|
|
226
|
+
)
|
|
227
|
+
return file
|
|
228
|
+
|
|
229
|
+
file_path, file_to_upload, file_type, extension = _process_file_input(file)
|
|
230
|
+
|
|
231
|
+
response = self.upload_urls.create(
|
|
232
|
+
items=[
|
|
233
|
+
V1FilesUploadUrlsCreateBodyItemsItem(
|
|
234
|
+
extension=extension, type_=file_type
|
|
235
|
+
)
|
|
236
|
+
]
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if not response.items:
|
|
240
|
+
raise ValueError("No upload URL was returned from the server")
|
|
241
|
+
|
|
242
|
+
upload_info = response.items[0]
|
|
243
|
+
|
|
244
|
+
with httpx.Client(timeout=None) as client:
|
|
245
|
+
content = _prepare_file_for_upload(
|
|
246
|
+
file_path=file_path, file_to_upload=file_to_upload
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
upload_response = client.put(url=upload_info.upload_url, content=content)
|
|
250
|
+
upload_response.raise_for_status()
|
|
251
|
+
|
|
252
|
+
logger.info(
|
|
253
|
+
f"Uploaded {file_path} to Magic Hour storage at {upload_info.file_path}."
|
|
254
|
+
)
|
|
255
|
+
return upload_info.file_path
|
|
256
|
+
|
|
13
257
|
|
|
14
258
|
class AsyncFilesClient:
|
|
259
|
+
"""
|
|
260
|
+
Async client for uploading files to Magic Hour's storage.
|
|
261
|
+
|
|
262
|
+
The AsyncFilesClient provides asynchronous functionality to upload media files
|
|
263
|
+
(images, videos, audio) to Magic Hour's secure storage. This is ideal for
|
|
264
|
+
applications that need to handle multiple file uploads concurrently or integrate
|
|
265
|
+
with async frameworks like FastAPI or aiohttp.
|
|
266
|
+
|
|
267
|
+
Supported file types:
|
|
268
|
+
- **Images**: PNG, JPG, JPEG, WebP, AVIF, JP2, TIFF, BMP
|
|
269
|
+
- **Videos**: MP4, M4V, MOV, WebM
|
|
270
|
+
- **Audio**: MP3, MPEG, WAV, AAC, AIFF, FLAC
|
|
271
|
+
"""
|
|
272
|
+
|
|
15
273
|
def __init__(self, *, base_client: AsyncBaseClient):
|
|
16
274
|
self._base_client = base_client
|
|
17
275
|
self.upload_urls = AsyncUploadUrlsClient(base_client=self._base_client)
|
|
276
|
+
|
|
277
|
+
async def upload_file(
|
|
278
|
+
self,
|
|
279
|
+
file: typing.Union[str, pathlib.Path, typing.BinaryIO, io.IOBase],
|
|
280
|
+
) -> str:
|
|
281
|
+
"""
|
|
282
|
+
Upload a file to Magic Hour's storage asynchronously.
|
|
283
|
+
|
|
284
|
+
This method asynchronously uploads a file to Magic Hour's secure cloud storage
|
|
285
|
+
and returns a file path that can be used as input for other Magic Hour API endpoints.
|
|
286
|
+
The file type is automatically detected from the file extension or MIME type.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
file: The file to upload. Can be:
|
|
290
|
+
- **str**: Path to a local file (e.g., "/path/to/image.jpg")
|
|
291
|
+
- **str**: URL of the file to upload, this will be skipped and the URL will be returned as is
|
|
292
|
+
- **str**: if the string begins with "api-assets", the file will be assumed to be a blob path and already uploaded to Magic Hour's storage
|
|
293
|
+
- **pathlib.Path**: Path object to a local file
|
|
294
|
+
- **typing.BinaryIO or io.IOBase**: File-like object (must have a 'name' attribute)
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
str: The uploaded file's path in Magic Hour's storage system.
|
|
298
|
+
This path can be used as input for other API endpoints, such for `.assets.image_file_path`.
|
|
299
|
+
|
|
300
|
+
Raises:
|
|
301
|
+
FileNotFoundError: If the specified local file doesn't exist.
|
|
302
|
+
ValueError: If the file type is not supported or file-like object is invalid.
|
|
303
|
+
httpx.HTTPStatusError: If the upload request fails (network/server errors).
|
|
304
|
+
|
|
305
|
+
Examples:
|
|
306
|
+
Basic async upload:
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
import asyncio
|
|
310
|
+
from magic_hour import AsyncClient
|
|
311
|
+
from os import getenv
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
async def upload_example():
|
|
315
|
+
client = AsyncClient(token=getenv("MAGIC_HOUR_API_TOKEN"))
|
|
316
|
+
|
|
317
|
+
# Upload from file path
|
|
318
|
+
file_path = await client.v1.files.upload_file("/path/to/your/image.jpg")
|
|
319
|
+
print(f"Uploaded file: {file_path}")
|
|
320
|
+
|
|
321
|
+
# Use the uploaded file in other API calls
|
|
322
|
+
result = await client.v1.ai_image_upscaler.create(
|
|
323
|
+
assets={"image_file_path": file_path}, style={"upscale_factor": 2}
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
asyncio.run(upload_example())
|
|
328
|
+
```
|
|
329
|
+
"""
|
|
330
|
+
if isinstance(file, str) and file.startswith(("http://", "https://")):
|
|
331
|
+
logger.info(f"{file} is a url. Skipping upload and returning the URL.")
|
|
332
|
+
return file
|
|
333
|
+
elif isinstance(file, str) and file.startswith("api-assets"):
|
|
334
|
+
logger.info(
|
|
335
|
+
f"{file} is begins with api-assets, assuming it's a blob path.. Skipping upload and returning the path."
|
|
336
|
+
)
|
|
337
|
+
return file
|
|
338
|
+
|
|
339
|
+
file_path, file_to_upload, file_type, extension = _process_file_input(file)
|
|
340
|
+
|
|
341
|
+
response = await self.upload_urls.create(
|
|
342
|
+
items=[
|
|
343
|
+
V1FilesUploadUrlsCreateBodyItemsItem(
|
|
344
|
+
extension=extension, type_=file_type
|
|
345
|
+
)
|
|
346
|
+
]
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
if not response.items:
|
|
350
|
+
raise ValueError("No upload URL was returned from the server")
|
|
351
|
+
|
|
352
|
+
upload_info = response.items[0]
|
|
353
|
+
|
|
354
|
+
async with httpx.AsyncClient(timeout=None) as client:
|
|
355
|
+
content = _prepare_file_for_upload(
|
|
356
|
+
file_path=file_path, file_to_upload=file_to_upload
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
upload_response = await client.put(
|
|
360
|
+
url=upload_info.upload_url, content=content
|
|
361
|
+
)
|
|
362
|
+
upload_response.raise_for_status()
|
|
363
|
+
|
|
364
|
+
logger.info(
|
|
365
|
+
f"Uploaded {file_path} to Magic Hour storage at {upload_info.file_path}."
|
|
366
|
+
)
|
|
367
|
+
return upload_info.file_path
|