magic_hour 0.8.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.

Potentially problematic release.


This version of magic_hour might be problematic. Click here for more details.

Files changed (131) hide show
  1. magic_hour/__init__.py +6 -0
  2. magic_hour/client.py +61 -0
  3. magic_hour/core/__init__.py +53 -0
  4. magic_hour/core/api_error.py +48 -0
  5. magic_hour/core/auth.py +314 -0
  6. magic_hour/core/base_client.py +600 -0
  7. magic_hour/core/binary_response.py +23 -0
  8. magic_hour/core/request.py +158 -0
  9. magic_hour/core/response.py +293 -0
  10. magic_hour/core/type_utils.py +28 -0
  11. magic_hour/core/utils.py +38 -0
  12. magic_hour/environment.py +6 -0
  13. magic_hour/resources/v1/__init__.py +4 -0
  14. magic_hour/resources/v1/ai_clothes_changer/README.md +41 -0
  15. magic_hour/resources/v1/ai_clothes_changer/__init__.py +4 -0
  16. magic_hour/resources/v1/ai_clothes_changer/client.py +129 -0
  17. magic_hour/resources/v1/ai_headshot_generator/README.md +31 -0
  18. magic_hour/resources/v1/ai_headshot_generator/__init__.py +4 -0
  19. magic_hour/resources/v1/ai_headshot_generator/client.py +119 -0
  20. magic_hour/resources/v1/ai_image_generator/README.md +37 -0
  21. magic_hour/resources/v1/ai_image_generator/__init__.py +4 -0
  22. magic_hour/resources/v1/ai_image_generator/client.py +144 -0
  23. magic_hour/resources/v1/ai_image_upscaler/README.md +37 -0
  24. magic_hour/resources/v1/ai_image_upscaler/__init__.py +4 -0
  25. magic_hour/resources/v1/ai_image_upscaler/client.py +143 -0
  26. magic_hour/resources/v1/ai_photo_editor/README.md +53 -0
  27. magic_hour/resources/v1/ai_photo_editor/__init__.py +4 -0
  28. magic_hour/resources/v1/ai_photo_editor/client.py +167 -0
  29. magic_hour/resources/v1/ai_qr_code_generator/README.md +35 -0
  30. magic_hour/resources/v1/ai_qr_code_generator/__init__.py +4 -0
  31. magic_hour/resources/v1/ai_qr_code_generator/client.py +127 -0
  32. magic_hour/resources/v1/animation/README.md +63 -0
  33. magic_hour/resources/v1/animation/__init__.py +4 -0
  34. magic_hour/resources/v1/animation/client.py +179 -0
  35. magic_hour/resources/v1/client.py +153 -0
  36. magic_hour/resources/v1/face_swap/README.md +52 -0
  37. magic_hour/resources/v1/face_swap/__init__.py +4 -0
  38. magic_hour/resources/v1/face_swap/client.py +165 -0
  39. magic_hour/resources/v1/face_swap_photo/README.md +39 -0
  40. magic_hour/resources/v1/face_swap_photo/__init__.py +4 -0
  41. magic_hour/resources/v1/face_swap_photo/client.py +127 -0
  42. magic_hour/resources/v1/files/__init__.py +4 -0
  43. magic_hour/resources/v1/files/client.py +19 -0
  44. magic_hour/resources/v1/files/upload_urls/README.md +56 -0
  45. magic_hour/resources/v1/files/upload_urls/__init__.py +4 -0
  46. magic_hour/resources/v1/files/upload_urls/client.py +142 -0
  47. magic_hour/resources/v1/image_background_remover/README.md +31 -0
  48. magic_hour/resources/v1/image_background_remover/__init__.py +4 -0
  49. magic_hour/resources/v1/image_background_remover/client.py +121 -0
  50. magic_hour/resources/v1/image_projects/README.md +63 -0
  51. magic_hour/resources/v1/image_projects/__init__.py +4 -0
  52. magic_hour/resources/v1/image_projects/client.py +177 -0
  53. magic_hour/resources/v1/image_to_video/README.md +44 -0
  54. magic_hour/resources/v1/image_to_video/__init__.py +4 -0
  55. magic_hour/resources/v1/image_to_video/client.py +165 -0
  56. magic_hour/resources/v1/lip_sync/README.md +54 -0
  57. magic_hour/resources/v1/lip_sync/__init__.py +4 -0
  58. magic_hour/resources/v1/lip_sync/client.py +177 -0
  59. magic_hour/resources/v1/text_to_video/README.md +40 -0
  60. magic_hour/resources/v1/text_to_video/__init__.py +4 -0
  61. magic_hour/resources/v1/text_to_video/client.py +150 -0
  62. magic_hour/resources/v1/video_projects/README.md +63 -0
  63. magic_hour/resources/v1/video_projects/__init__.py +4 -0
  64. magic_hour/resources/v1/video_projects/client.py +177 -0
  65. magic_hour/resources/v1/video_to_video/README.md +60 -0
  66. magic_hour/resources/v1/video_to_video/__init__.py +4 -0
  67. magic_hour/resources/v1/video_to_video/client.py +204 -0
  68. magic_hour/types/models/__init__.py +60 -0
  69. magic_hour/types/models/get_v1_image_projects_id_response.py +82 -0
  70. magic_hour/types/models/get_v1_image_projects_id_response_downloads_item.py +19 -0
  71. magic_hour/types/models/get_v1_image_projects_id_response_error.py +25 -0
  72. magic_hour/types/models/get_v1_video_projects_id_response.py +114 -0
  73. magic_hour/types/models/get_v1_video_projects_id_response_download.py +19 -0
  74. magic_hour/types/models/get_v1_video_projects_id_response_downloads_item.py +19 -0
  75. magic_hour/types/models/get_v1_video_projects_id_response_error.py +25 -0
  76. magic_hour/types/models/post_v1_ai_clothes_changer_response.py +25 -0
  77. magic_hour/types/models/post_v1_ai_headshot_generator_response.py +25 -0
  78. magic_hour/types/models/post_v1_ai_image_generator_response.py +25 -0
  79. magic_hour/types/models/post_v1_ai_image_upscaler_response.py +25 -0
  80. magic_hour/types/models/post_v1_ai_photo_editor_response.py +25 -0
  81. magic_hour/types/models/post_v1_ai_qr_code_generator_response.py +25 -0
  82. magic_hour/types/models/post_v1_animation_response.py +25 -0
  83. magic_hour/types/models/post_v1_face_swap_photo_response.py +25 -0
  84. magic_hour/types/models/post_v1_face_swap_response.py +25 -0
  85. magic_hour/types/models/post_v1_files_upload_urls_response.py +21 -0
  86. magic_hour/types/models/post_v1_files_upload_urls_response_items_item.py +31 -0
  87. magic_hour/types/models/post_v1_image_background_remover_response.py +25 -0
  88. magic_hour/types/models/post_v1_image_to_video_response.py +25 -0
  89. magic_hour/types/models/post_v1_lip_sync_response.py +25 -0
  90. magic_hour/types/models/post_v1_text_to_video_response.py +25 -0
  91. magic_hour/types/models/post_v1_video_to_video_response.py +25 -0
  92. magic_hour/types/params/__init__.py +205 -0
  93. magic_hour/types/params/post_v1_ai_clothes_changer_body.py +40 -0
  94. magic_hour/types/params/post_v1_ai_clothes_changer_body_assets.py +45 -0
  95. magic_hour/types/params/post_v1_ai_headshot_generator_body.py +40 -0
  96. magic_hour/types/params/post_v1_ai_headshot_generator_body_assets.py +28 -0
  97. magic_hour/types/params/post_v1_ai_image_generator_body.py +54 -0
  98. magic_hour/types/params/post_v1_ai_image_generator_body_style.py +28 -0
  99. magic_hour/types/params/post_v1_ai_image_upscaler_body.py +54 -0
  100. magic_hour/types/params/post_v1_ai_image_upscaler_body_assets.py +28 -0
  101. magic_hour/types/params/post_v1_ai_image_upscaler_body_style.py +36 -0
  102. magic_hour/types/params/post_v1_ai_photo_editor_body.py +63 -0
  103. magic_hour/types/params/post_v1_ai_photo_editor_body_assets.py +28 -0
  104. magic_hour/types/params/post_v1_ai_photo_editor_body_style.py +67 -0
  105. magic_hour/types/params/post_v1_ai_qr_code_generator_body.py +45 -0
  106. magic_hour/types/params/post_v1_ai_qr_code_generator_body_style.py +28 -0
  107. magic_hour/types/params/post_v1_animation_body.py +84 -0
  108. magic_hour/types/params/post_v1_animation_body_assets.py +55 -0
  109. magic_hour/types/params/post_v1_animation_body_style.py +279 -0
  110. magic_hour/types/params/post_v1_face_swap_body.py +72 -0
  111. magic_hour/types/params/post_v1_face_swap_body_assets.py +52 -0
  112. magic_hour/types/params/post_v1_face_swap_photo_body.py +40 -0
  113. magic_hour/types/params/post_v1_face_swap_photo_body_assets.py +36 -0
  114. magic_hour/types/params/post_v1_files_upload_urls_body.py +31 -0
  115. magic_hour/types/params/post_v1_files_upload_urls_body_items_item.py +38 -0
  116. magic_hour/types/params/post_v1_image_background_remover_body.py +40 -0
  117. magic_hour/types/params/post_v1_image_background_remover_body_assets.py +28 -0
  118. magic_hour/types/params/post_v1_image_to_video_body.py +73 -0
  119. magic_hour/types/params/post_v1_image_to_video_body_assets.py +28 -0
  120. magic_hour/types/params/post_v1_image_to_video_body_style.py +29 -0
  121. magic_hour/types/params/post_v1_lip_sync_body.py +80 -0
  122. magic_hour/types/params/post_v1_lip_sync_body_assets.py +52 -0
  123. magic_hour/types/params/post_v1_text_to_video_body.py +57 -0
  124. magic_hour/types/params/post_v1_text_to_video_body_style.py +28 -0
  125. magic_hour/types/params/post_v1_video_to_video_body.py +93 -0
  126. magic_hour/types/params/post_v1_video_to_video_body_assets.py +44 -0
  127. magic_hour/types/params/post_v1_video_to_video_body_style.py +199 -0
  128. magic_hour-0.8.0.dist-info/LICENSE +21 -0
  129. magic_hour-0.8.0.dist-info/METADATA +138 -0
  130. magic_hour-0.8.0.dist-info/RECORD +131 -0
  131. magic_hour-0.8.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,158 @@
1
+ from typing import Any, Dict, Type, Union, Sequence, List
2
+ from urllib.parse import quote_plus
3
+ import httpx
4
+ from typing_extensions import TypedDict, Required, NotRequired
5
+ from pydantic import TypeAdapter, BaseModel
6
+ from .type_utils import NotGiven
7
+
8
+ """
9
+ Request configuration and utility functions for handling HTTP requests.
10
+ This module provides type definitions and helper functions for building
11
+ and processing HTTP requests in a type-safe manner.
12
+ """
13
+
14
+ # Type alias for query parameters that can handle both primitive data and sequences
15
+ QueryParams = Dict[
16
+ str, Union[httpx._types.PrimitiveData, Sequence[httpx._types.PrimitiveData]]
17
+ ]
18
+
19
+
20
+ class RequestConfig(TypedDict):
21
+ """
22
+ Configuration for HTTP requests.
23
+
24
+ Defines all possible parameters that can be passed to an HTTP request,
25
+ including required method and URL, as well as optional parameters like
26
+ content, headers, authentication, etc.
27
+ """
28
+
29
+ method: Required[str]
30
+ url: Required[httpx._types.URLTypes]
31
+ content: NotRequired[httpx._types.RequestContent]
32
+ data: NotRequired[httpx._types.RequestData]
33
+ files: NotRequired[httpx._types.RequestFiles]
34
+ json: NotRequired[Any]
35
+ params: NotRequired[QueryParams]
36
+ headers: NotRequired[Dict[str, str]]
37
+ cookies: NotRequired[Dict[str, str]]
38
+ auth: NotRequired[httpx._types.AuthTypes]
39
+ follow_redirects: NotRequired[bool]
40
+ timeout: NotRequired[httpx._types.TimeoutTypes]
41
+ extensions: NotRequired[httpx._types.RequestExtensions]
42
+
43
+
44
+ class RequestOptions(TypedDict):
45
+ """
46
+ Additional options for customizing request behavior.
47
+
48
+ Provides configuration for timeouts and additional headers/parameters
49
+ that should be included with requests.
50
+
51
+ Attributes:
52
+ timeout: Number of seconds to await an API call before timing out
53
+ additional_headers: Extra headers to include in the request
54
+ additional_params: Extra query parameters to include in the request
55
+ """
56
+
57
+ timeout: NotRequired[int]
58
+ additional_headers: NotRequired[Dict[str, str]]
59
+ additional_params: NotRequired[QueryParams]
60
+
61
+
62
+ def default_request_options() -> RequestOptions:
63
+ """
64
+ Provides default request options.
65
+
66
+ Returns an empty dictionary as the base configuration, allowing defaults
67
+ to be handled by the underlying HTTP client.
68
+ """
69
+ return {}
70
+
71
+
72
+ def model_dump(item: Any) -> Any:
73
+ """
74
+ Recursively converts Pydantic models to dictionaries.
75
+
76
+ Handles nested structures including lists and individual models,
77
+ preserving alias information and excluding unset values.
78
+ """
79
+ if isinstance(item, list):
80
+ return [model_dump(i) for i in item]
81
+ if isinstance(item, BaseModel):
82
+ return item.model_dump(exclude_unset=True, by_alias=True)
83
+ else:
84
+ return item
85
+
86
+
87
+ def to_encodable(
88
+ *, item: Any, dump_with: Union[Type, Union[Type, Any], List[Type]]
89
+ ) -> Any:
90
+ """
91
+ Validates and converts an item to an encodable format using a specified type.
92
+ Uses Pydantic's TypeAdapter for validation and converts the result
93
+ to a format suitable for encoding in requests.
94
+ """
95
+ filtered_item = filter_not_given(item)
96
+ adapter: TypeAdapter = TypeAdapter(dump_with)
97
+ validated_item = adapter.validate_python(filtered_item)
98
+ return model_dump(validated_item)
99
+
100
+
101
+ def to_content(*, file: httpx._types.FileTypes) -> httpx._types.RequestContent:
102
+ """
103
+ Converts the various ways files can be provided to something that is accepted by
104
+ the httpx.request content kwarg
105
+ """
106
+ if isinstance(file, tuple):
107
+ file_content: httpx._types.FileContent = file[1]
108
+ else:
109
+ file_content = file
110
+
111
+ if hasattr(file_content, "read") and callable(file_content.read):
112
+ return file_content.read()
113
+ else:
114
+ return file_content
115
+
116
+
117
+ def encode_param(
118
+ value: Any, explode: bool
119
+ ) -> Union[httpx._types.PrimitiveData, Sequence[httpx._types.PrimitiveData]]:
120
+ """
121
+ Encodes parameter values for use in URLs.
122
+
123
+ Handles both simple values and collections, with special handling for
124
+ unexploded collections (combining them with commas) versus exploded ones.
125
+
126
+ Args:
127
+ explode: Whether to explode collections into separate parameters
128
+ """
129
+ if isinstance(value, (list, dict)) and not explode:
130
+ return quote_plus(",".join(map(str, value)))
131
+ else:
132
+ return value
133
+
134
+
135
+ def filter_not_given(value: Any) -> Any:
136
+ """Helper function to recursively filter out NotGiven values"""
137
+ if isinstance(value, NotGiven):
138
+ return None # This will trigger filtering at the container level
139
+ elif isinstance(value, dict):
140
+ return {
141
+ k: filter_not_given(v)
142
+ for k, v in value.items()
143
+ if not isinstance(v, NotGiven)
144
+ }
145
+ elif isinstance(value, (list, tuple)):
146
+ return type(value)(
147
+ filter_not_given(item) for item in value if not isinstance(item, NotGiven)
148
+ )
149
+ return value
150
+
151
+
152
+ def _get_default_for_type(value_type: Any) -> Any:
153
+ """Helper to provide appropriate default values for required fields"""
154
+ if value_type is dict or isinstance(value_type, dict):
155
+ return {}
156
+ elif value_type is list or isinstance(value_type, list):
157
+ return []
158
+ return None
@@ -0,0 +1,293 @@
1
+ import json
2
+ from typing import Any, Union, Dict, Type, TypeVar, List, Generic, Optional
3
+ from pydantic import BaseModel
4
+ import httpx
5
+
6
+ """
7
+ Provides functionality for handling Server-Sent Events (SSE) streams and response data encoding.
8
+ Includes utilities for both synchronous and asynchronous stream processing.
9
+ """
10
+
11
+ EncodableT = TypeVar(
12
+ "EncodableT",
13
+ bound=Union[
14
+ object,
15
+ str,
16
+ int,
17
+ float,
18
+ None,
19
+ BaseModel,
20
+ List[Any],
21
+ Dict[str, Any],
22
+ ],
23
+ )
24
+
25
+
26
+ def from_encodable(*, data: Any, load_with: Type[EncodableT]) -> Any:
27
+ """
28
+ Converts raw data into a specified type using Pydantic validation.
29
+
30
+ Uses a dynamic Pydantic model to validate and convert incoming data
31
+ into the specified target type.
32
+ """
33
+
34
+ class Caster(BaseModel):
35
+ data: load_with # type: ignore
36
+
37
+ return Caster(data=data).data
38
+
39
+
40
+ T = TypeVar("T")
41
+
42
+
43
+ class StreamResponse(Generic[T]):
44
+ """
45
+ Handles synchronous streaming of Server-Sent Events (SSE).
46
+
47
+ Processes a streaming HTTP response by buffering chunks of data
48
+ and parsing them according to SSE format, converting each event
49
+ into the specified type.
50
+ """
51
+
52
+ def __init__(self, response: httpx.Response, stream_context, cast_to: Type[T]):
53
+ """
54
+ Initialize the stream processor with response and conversion settings.
55
+
56
+ Args:
57
+ response: The HTTP response containing the SSE stream
58
+ stream_context: Context manager for the stream
59
+ cast_to: Target type for converting parsed events
60
+ """
61
+ self.response = response
62
+ self._context = stream_context
63
+ self.cast_to = cast_to
64
+ self.iterator = response.iter_bytes()
65
+ self.buffer = bytearray()
66
+ self.position = 0
67
+
68
+ def __iter__(self):
69
+ """Enables iteration over the stream events."""
70
+ return self
71
+
72
+ def __next__(self) -> T:
73
+ """
74
+ Retrieves and processes the next event from the stream.
75
+
76
+ Buffers incoming data and processes it according to SSE format,
77
+ converting each complete event into the specified type.
78
+
79
+ Raises:
80
+ StopIteration: When the stream is exhausted
81
+ """
82
+ try:
83
+ while True:
84
+ event = self._process_buffer()
85
+ if event:
86
+ return event
87
+
88
+ chunk = next(self.iterator)
89
+ self.buffer += chunk
90
+
91
+ except StopIteration:
92
+ event = self._process_buffer(final=True)
93
+ if event:
94
+ return event
95
+ self._context.__exit__(None, None, None)
96
+ raise
97
+
98
+ def _process_buffer(self, final=False) -> Optional[T]:
99
+ """
100
+ Processes the current buffer to extract complete SSE events.
101
+
102
+ Searches for event boundaries and parses complete events,
103
+ handling both JSON and non-JSON payloads.
104
+
105
+ Args:
106
+ final: Whether this is the final processing of the buffer
107
+ """
108
+ while self.position < len(self.buffer):
109
+ for boundary in [b"\r\n\r\n", b"\n\n", b"\r\r"]:
110
+ if (self.position + len(boundary)) <= len(self.buffer):
111
+ if (
112
+ self.buffer[self.position : self.position + len(boundary)]
113
+ == boundary
114
+ ):
115
+ message = self.buffer[: self.position].decode()
116
+ self.buffer = self.buffer[self.position + len(boundary) :]
117
+ self.position = 0
118
+
119
+ data = self._parse_sse(message)
120
+ if data:
121
+ try:
122
+ parsed_data = json.loads(data)
123
+ if (
124
+ not isinstance(parsed_data, dict)
125
+ or "data" not in parsed_data
126
+ ):
127
+ parsed_data = {"data": parsed_data}
128
+ return from_encodable(
129
+ data=parsed_data, load_with=self.cast_to
130
+ )
131
+ except json.JSONDecodeError:
132
+ return from_encodable(
133
+ data={"data": data}, load_with=self.cast_to
134
+ )
135
+ return None
136
+
137
+ self.position += 1
138
+
139
+ if final and self.buffer:
140
+ message = self.buffer.decode()
141
+ data = self._parse_sse(message)
142
+ if data:
143
+ try:
144
+ parsed_data = json.loads(data)
145
+ if not isinstance(parsed_data, dict) or "data" not in parsed_data:
146
+ parsed_data = {"data": parsed_data}
147
+ return from_encodable(data=parsed_data, load_with=self.cast_to)
148
+ except json.JSONDecodeError:
149
+ return from_encodable(data={"data": data}, load_with=self.cast_to)
150
+
151
+ return None
152
+
153
+ def _parse_sse(self, message: str) -> Optional[str]:
154
+ """
155
+ Parses an SSE message to extract the data field.
156
+
157
+ Handles multi-line data fields and empty data fields according
158
+ to the SSE specification.
159
+ """
160
+ data = []
161
+ for line in message.split("\n"):
162
+ if line.startswith("data:"):
163
+ data.append(line[5:].strip())
164
+ elif line.strip() == "data:": # Handle empty data field
165
+ data.append("")
166
+
167
+ if data:
168
+ return "\n".join(data)
169
+ return None
170
+
171
+
172
+ class AsyncStreamResponse(Generic[T]):
173
+ """
174
+ Handles asynchronous streaming of Server-Sent Events (SSE).
175
+
176
+ Asynchronous version of StreamResponse, providing the same functionality
177
+ but compatible with async/await syntax.
178
+ """
179
+
180
+ def __init__(self, response: httpx.Response, stream_context, cast_to: Type[T]):
181
+ """
182
+ Initialize the async stream processor.
183
+
184
+ Args:
185
+ response: The HTTP response containing the SSE stream
186
+ stream_context: Async context manager for the stream
187
+ cast_to: Target type for converting parsed events
188
+ """
189
+ self.response = response
190
+ self._context = stream_context
191
+ self.cast_to = cast_to
192
+ self.iterator = response.aiter_bytes()
193
+ self.buffer = bytearray()
194
+ self.position = 0
195
+
196
+ def __aiter__(self):
197
+ """Enables async iteration over the stream events."""
198
+ return self
199
+
200
+ async def __anext__(self) -> T:
201
+ """
202
+ Asynchronously retrieves and processes the next event from the stream.
203
+
204
+ Similar to synchronous version but uses async/await syntax for
205
+ iteration and context management.
206
+
207
+ Raises:
208
+ StopAsyncIteration: When the stream is exhausted
209
+ """
210
+ try:
211
+ while True:
212
+ event = self._process_buffer()
213
+ if event:
214
+ return event
215
+
216
+ chunk = await self.iterator.__anext__()
217
+ self.buffer += chunk
218
+
219
+ except StopAsyncIteration:
220
+ event = self._process_buffer(final=True)
221
+ if event:
222
+ return event
223
+ await self._context.__aexit__(None, None, None)
224
+ raise
225
+
226
+ def _process_buffer(self, final=False) -> Optional[T]:
227
+ """
228
+ Processes the current buffer to extract complete SSE events.
229
+
230
+ Identical to the synchronous version's buffer processing.
231
+ """
232
+ while self.position < len(self.buffer):
233
+ for boundary in [b"\r\n\r\n", b"\n\n", b"\r\r"]:
234
+ if (self.position + len(boundary)) <= len(self.buffer):
235
+ if (
236
+ self.buffer[self.position : self.position + len(boundary)]
237
+ == boundary
238
+ ):
239
+ message = self.buffer[: self.position].decode()
240
+ self.buffer = self.buffer[self.position + len(boundary) :]
241
+ self.position = 0
242
+
243
+ data = self._parse_sse(message)
244
+ if data:
245
+ try:
246
+ parsed_data = json.loads(data)
247
+ if (
248
+ not isinstance(parsed_data, dict)
249
+ or "data" not in parsed_data
250
+ ):
251
+ parsed_data = {"data": parsed_data}
252
+ return from_encodable(
253
+ data=parsed_data, load_with=self.cast_to
254
+ )
255
+ except json.JSONDecodeError:
256
+ return from_encodable(
257
+ data={"data": data}, load_with=self.cast_to
258
+ )
259
+ return None
260
+
261
+ self.position += 1
262
+
263
+ if final and self.buffer:
264
+ message = self.buffer.decode()
265
+ data = self._parse_sse(message)
266
+ if data:
267
+ try:
268
+ parsed_data = json.loads(data)
269
+ if not isinstance(parsed_data, dict) or "data" not in parsed_data:
270
+ parsed_data = {"data": parsed_data}
271
+ return from_encodable(data=parsed_data, load_with=self.cast_to)
272
+ except json.JSONDecodeError:
273
+ return from_encodable(data={"data": data}, load_with=self.cast_to)
274
+
275
+ return None
276
+
277
+ def _parse_sse(self, message: str) -> Optional[str]:
278
+ """
279
+ Parses an SSE message to extract the data field.
280
+
281
+ Identical to the synchronous version's SSE parsing.
282
+ """
283
+ data = []
284
+ for line in message.split("\n"):
285
+ line = line.strip()
286
+ if line.startswith("data:"):
287
+ data.append(line[5:].strip())
288
+ elif line == "data:": # Handle empty data field
289
+ data.append("")
290
+
291
+ if data:
292
+ return "\n".join(data)
293
+ return None
@@ -0,0 +1,28 @@
1
+ from typing import (
2
+ Union,
3
+ )
4
+ from typing_extensions import (
5
+ Literal,
6
+ TypeVar,
7
+ override,
8
+ )
9
+
10
+ _T = TypeVar("_T")
11
+
12
+
13
+ class NotGiven:
14
+ """
15
+ Used to distinguish omitted keyword arguments from those passed explicitly
16
+ with the value None.
17
+ """
18
+
19
+ def __bool__(self) -> Literal[False]:
20
+ return False
21
+
22
+ @override
23
+ def __repr__(self) -> str:
24
+ return "NOT_GIVEN"
25
+
26
+
27
+ NotGivenOr = Union[_T, NotGiven]
28
+ NOT_GIVEN = NotGiven()
@@ -0,0 +1,38 @@
1
+ import typing
2
+ import httpx
3
+
4
+
5
+ def remove_none_from_dict(
6
+ original: typing.Dict[str, typing.Optional[typing.Any]],
7
+ ) -> typing.Dict[str, typing.Any]:
8
+ new: typing.Dict[str, typing.Any] = {}
9
+ for key, value in original.items():
10
+ if value is not None:
11
+ new[key] = value
12
+ return new
13
+
14
+
15
+ def is_binary_content_type(content_type: str) -> bool:
16
+ """Check if the content type indicates binary data."""
17
+ binary_types = [
18
+ "application/octet-stream",
19
+ "application/pdf",
20
+ "application/zip",
21
+ "image/",
22
+ "audio/",
23
+ "video/",
24
+ "application/msword",
25
+ "application/vnd.openxmlformats-officedocument",
26
+ "application/x-binary",
27
+ "application/vnd.ms-excel",
28
+ "application/vnd.ms-powerpoint",
29
+ ]
30
+ return any(binary_type in content_type for binary_type in binary_types)
31
+
32
+
33
+ def get_content_type(headers: httpx.Headers) -> str:
34
+ """Get content type in a case-insensitive manner."""
35
+ for key, value in headers.items():
36
+ if key.lower() == "content-type":
37
+ return value.lower()
38
+ return ""
@@ -0,0 +1,6 @@
1
+ import enum
2
+
3
+
4
+ class Environment(enum.Enum):
5
+ ENVIRONMENT = "https://api.magichour.ai"
6
+ MOCK_SERVER = "https://api.sideko.dev/v1/mock/magichour/magic-hour/0.8.0"
@@ -0,0 +1,4 @@
1
+ from .client import AsyncV1Client, V1Client
2
+
3
+
4
+ __all__ = ["AsyncV1Client", "V1Client"]
@@ -0,0 +1,41 @@
1
+
2
+ ### create <a name="create"></a>
3
+ AI Clothes Changer
4
+
5
+ Change outfits in photos in seconds with just a photo reference. Each photo costs 25 frames.
6
+
7
+ **API Endpoint**: `POST /v1/ai-clothes-changer`
8
+
9
+ #### Synchronous Client
10
+
11
+ ```python
12
+ from magic_hour import Client
13
+ from os import getenv
14
+
15
+ client = Client(token=getenv("API_TOKEN"))
16
+ res = client.v1.ai_clothes_changer.create(
17
+ assets={
18
+ "garment_file_path": "api-assets/id/outfit.png",
19
+ "garment_type": "dresses",
20
+ "person_file_path": "api-assets/id/model.png",
21
+ },
22
+ name="Clothes Changer image",
23
+ )
24
+ ```
25
+
26
+ #### Asynchronous Client
27
+
28
+ ```python
29
+ from magic_hour import AsyncClient
30
+ from os import getenv
31
+
32
+ client = AsyncClient(token=getenv("API_TOKEN"))
33
+ res = await client.v1.ai_clothes_changer.create(
34
+ assets={
35
+ "garment_file_path": "api-assets/id/outfit.png",
36
+ "garment_type": "dresses",
37
+ "person_file_path": "api-assets/id/model.png",
38
+ },
39
+ name="Clothes Changer image",
40
+ )
41
+ ```
@@ -0,0 +1,4 @@
1
+ from .client import AiClothesChangerClient, AsyncAiClothesChangerClient
2
+
3
+
4
+ __all__ = ["AiClothesChangerClient", "AsyncAiClothesChangerClient"]