pymlsapi 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import BinaryIO, Optional, Union
5
+
6
+ from pymlsapi.async_http import AsyncHttpClient
7
+ from pymlsapi.http import HttpClient
8
+ from pymlsapi.models.studio import UploadResult
9
+
10
+
11
+ class UploadResource:
12
+ """Synchronous file upload utility to mlsapi global CDN."""
13
+
14
+ def __init__(self, http: HttpClient) -> None:
15
+ self._http = http
16
+
17
+ def upload(
18
+ self,
19
+ file: Union[str, Path, bytes, BinaryIO],
20
+ filename: Optional[str] = None,
21
+ content_type: Optional[str] = None,
22
+ ) -> UploadResult:
23
+ """Upload an image or video file to mlsapi CDN."""
24
+ if isinstance(file, (str, Path)):
25
+ path = Path(file)
26
+ filename = filename or path.name
27
+ with open(path, "rb") as f:
28
+ file_bytes = f.read()
29
+ elif isinstance(file, bytes):
30
+ file_bytes = file
31
+ filename = filename or "upload.jpg"
32
+ elif hasattr(file, "read"):
33
+ file_bytes = file.read()
34
+ filename = filename or getattr(file, "name", "upload.jpg")
35
+ else:
36
+ raise ValueError(
37
+ "Unsupported file type. Pass a Path, filepath string, bytes, or file object."
38
+ )
39
+
40
+ content_type = content_type or "image/jpeg"
41
+ files = {"file": (filename, file_bytes, content_type)}
42
+
43
+ data = self._http.post("/v1/studio/upload", files=files)
44
+ # Server may return { url: "..." } or { data: { url: "..." } }
45
+ url = data.get("url") or (data.get("data", {}).get("url", ""))
46
+ return UploadResult(
47
+ url=url,
48
+ filename=filename,
49
+ content_type=content_type,
50
+ bytes=len(file_bytes),
51
+ )
52
+
53
+
54
+ class AsyncUploadResource:
55
+ """Asynchronous file upload utility to mlsapi global CDN."""
56
+
57
+ def __init__(self, http: AsyncHttpClient) -> None:
58
+ self._http = http
59
+
60
+ async def upload(
61
+ self,
62
+ file: Union[str, Path, bytes, BinaryIO],
63
+ filename: Optional[str] = None,
64
+ content_type: Optional[str] = None,
65
+ ) -> UploadResult:
66
+ """Upload an image or video file to mlsapi CDN."""
67
+ if isinstance(file, (str, Path)):
68
+ path = Path(file)
69
+ filename = filename or path.name
70
+ with open(path, "rb") as f:
71
+ file_bytes = f.read()
72
+ elif isinstance(file, bytes):
73
+ file_bytes = file
74
+ filename = filename or "upload.jpg"
75
+ elif hasattr(file, "read"):
76
+ file_bytes = file.read()
77
+ filename = filename or getattr(file, "name", "upload.jpg")
78
+ else:
79
+ raise ValueError(
80
+ "Unsupported file type. Pass a Path, filepath string, bytes, or file object."
81
+ )
82
+
83
+ content_type = content_type or "image/jpeg"
84
+ files = {"file": (filename, file_bytes, content_type)}
85
+
86
+ data = await self._http.post("/v1/studio/upload", files=files)
87
+ url = data.get("url") or (data.get("data", {}).get("url", ""))
88
+ return UploadResult(
89
+ url=url,
90
+ filename=filename,
91
+ content_type=content_type,
92
+ bytes=len(file_bytes),
93
+ )
@@ -0,0 +1,346 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Dict, List, Optional
4
+
5
+ from pymlsapi.async_http import AsyncHttpClient
6
+ from pymlsapi.http import HttpClient
7
+ from pymlsapi.models.studio import (
8
+ HouseTourResult,
9
+ StudioJob,
10
+ VideoEnhanceResult,
11
+ VideoTransitionResult,
12
+ VideoWalkthroughResult,
13
+ )
14
+ from pymlsapi.resources.studio.jobs import AsyncStudioJobsResource, StudioJobsResource
15
+
16
+
17
+ class VideoResource:
18
+ """Synchronous video walkthrough, speech mastering, and tour automation."""
19
+
20
+ def __init__(self, http: HttpClient, jobs: StudioJobsResource) -> None:
21
+ self._http = http
22
+ self._jobs = jobs
23
+
24
+ def enhance(
25
+ self,
26
+ video_url: str,
27
+ features: Optional[Dict[str, bool]] = None,
28
+ subtitle_style: Optional[Dict[str, Any]] = None,
29
+ export_aspect_ratios: Optional[List[str]] = None,
30
+ webhook_url: Optional[str] = None,
31
+ ) -> StudioJob:
32
+ payload: Dict[str, Any] = {
33
+ "video_url": video_url,
34
+ "features": features
35
+ or {"studio_voice": True, "animated_subtitles": True, "smart_reframe": True},
36
+ }
37
+ if subtitle_style:
38
+ payload["subtitle_style"] = subtitle_style
39
+ if export_aspect_ratios:
40
+ payload["export_aspect_ratios"] = export_aspect_ratios
41
+ if webhook_url:
42
+ payload["webhook_url"] = webhook_url
43
+ data = self._http.post("/v1/studio/video/enhance", json=payload)
44
+ return StudioJob.model_validate(data)
45
+
46
+ def enhance_and_wait(
47
+ self,
48
+ video_url: str,
49
+ features: Optional[Dict[str, bool]] = None,
50
+ subtitle_style: Optional[Dict[str, Any]] = None,
51
+ export_aspect_ratios: Optional[List[str]] = None,
52
+ webhook_url: Optional[str] = None,
53
+ timeout_seconds: float = 120.0,
54
+ poll_interval: float = 2.0,
55
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
56
+ ) -> VideoEnhanceResult:
57
+ job = self.enhance(
58
+ video_url,
59
+ features=features,
60
+ subtitle_style=subtitle_style,
61
+ export_aspect_ratios=export_aspect_ratios,
62
+ webhook_url=webhook_url,
63
+ )
64
+ completed = self._jobs.wait_for(
65
+ job.job_id,
66
+ timeout_seconds=timeout_seconds,
67
+ poll_interval=poll_interval,
68
+ on_progress=on_progress,
69
+ )
70
+ return VideoEnhanceResult.model_validate(completed.result or {})
71
+
72
+ def walkthrough(
73
+ self,
74
+ photos: Optional[List[str]] = None,
75
+ mls_id: Optional[str] = None,
76
+ webhook_url: Optional[str] = None,
77
+ ) -> StudioJob:
78
+ payload: Dict[str, Any] = {}
79
+ if photos:
80
+ payload["photos"] = photos
81
+ if mls_id:
82
+ payload["mls_id"] = mls_id
83
+ if webhook_url:
84
+ payload["webhook_url"] = webhook_url
85
+ data = self._http.post("/v1/studio/video/walkthrough", json=payload)
86
+ return StudioJob.model_validate(data)
87
+
88
+ def walkthrough_and_wait(
89
+ self,
90
+ photos: Optional[List[str]] = None,
91
+ mls_id: Optional[str] = None,
92
+ webhook_url: Optional[str] = None,
93
+ timeout_seconds: float = 120.0,
94
+ poll_interval: float = 2.0,
95
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
96
+ ) -> VideoWalkthroughResult:
97
+ job = self.walkthrough(photos=photos, mls_id=mls_id, webhook_url=webhook_url)
98
+ completed = self._jobs.wait_for(
99
+ job.job_id,
100
+ timeout_seconds=timeout_seconds,
101
+ poll_interval=poll_interval,
102
+ on_progress=on_progress,
103
+ )
104
+ return VideoWalkthroughResult.model_validate(completed.result or {})
105
+
106
+ def transition(
107
+ self,
108
+ start_image_url: str,
109
+ end_image_url: str,
110
+ duration_seconds: float = 3.0,
111
+ webhook_url: Optional[str] = None,
112
+ ) -> StudioJob:
113
+ payload: Dict[str, Any] = {
114
+ "start_image_url": start_image_url,
115
+ "end_image_url": end_image_url,
116
+ "duration_seconds": duration_seconds,
117
+ }
118
+ if webhook_url:
119
+ payload["webhook_url"] = webhook_url
120
+ data = self._http.post("/v1/studio/video/transition", json=payload)
121
+ return StudioJob.model_validate(data)
122
+
123
+ def transition_and_wait(
124
+ self,
125
+ start_image_url: str,
126
+ end_image_url: str,
127
+ duration_seconds: float = 3.0,
128
+ webhook_url: Optional[str] = None,
129
+ timeout_seconds: float = 90.0,
130
+ poll_interval: float = 2.0,
131
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
132
+ ) -> VideoTransitionResult:
133
+ job = self.transition(
134
+ start_image_url,
135
+ end_image_url,
136
+ duration_seconds=duration_seconds,
137
+ webhook_url=webhook_url,
138
+ )
139
+ completed = self._jobs.wait_for(
140
+ job.job_id,
141
+ timeout_seconds=timeout_seconds,
142
+ poll_interval=poll_interval,
143
+ on_progress=on_progress,
144
+ )
145
+ return VideoTransitionResult.model_validate(completed.result or {})
146
+
147
+ def tour(
148
+ self,
149
+ ordered_photos: List[str],
150
+ music_mood: Optional[str] = "luxurious",
151
+ webhook_url: Optional[str] = None,
152
+ ) -> StudioJob:
153
+ payload: Dict[str, Any] = {
154
+ "ordered_photos": ordered_photos,
155
+ "music_mood": music_mood,
156
+ }
157
+ if webhook_url:
158
+ payload["webhook_url"] = webhook_url
159
+ data = self._http.post("/v1/studio/video/tour", json=payload)
160
+ return StudioJob.model_validate(data)
161
+
162
+ def tour_and_wait(
163
+ self,
164
+ ordered_photos: List[str],
165
+ music_mood: Optional[str] = "luxurious",
166
+ webhook_url: Optional[str] = None,
167
+ timeout_seconds: float = 120.0,
168
+ poll_interval: float = 2.0,
169
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
170
+ ) -> HouseTourResult:
171
+ job = self.tour(
172
+ ordered_photos=ordered_photos, music_mood=music_mood, webhook_url=webhook_url
173
+ )
174
+ completed = self._jobs.wait_for(
175
+ job.job_id,
176
+ timeout_seconds=timeout_seconds,
177
+ poll_interval=poll_interval,
178
+ on_progress=on_progress,
179
+ )
180
+ return HouseTourResult.model_validate(completed.result or {})
181
+
182
+
183
+ class AsyncVideoResource:
184
+ """Asynchronous video walkthrough, speech mastering, and tour automation."""
185
+
186
+ def __init__(self, http: AsyncHttpClient, jobs: AsyncStudioJobsResource) -> None:
187
+ self._http = http
188
+ self._jobs = jobs
189
+
190
+ async def enhance(
191
+ self,
192
+ video_url: str,
193
+ features: Optional[Dict[str, bool]] = None,
194
+ subtitle_style: Optional[Dict[str, Any]] = None,
195
+ export_aspect_ratios: Optional[List[str]] = None,
196
+ webhook_url: Optional[str] = None,
197
+ ) -> StudioJob:
198
+ payload: Dict[str, Any] = {
199
+ "video_url": video_url,
200
+ "features": features
201
+ or {"studio_voice": True, "animated_subtitles": True, "smart_reframe": True},
202
+ }
203
+ if subtitle_style:
204
+ payload["subtitle_style"] = subtitle_style
205
+ if export_aspect_ratios:
206
+ payload["export_aspect_ratios"] = export_aspect_ratios
207
+ if webhook_url:
208
+ payload["webhook_url"] = webhook_url
209
+ data = await self._http.post("/v1/studio/video/enhance", json=payload)
210
+ return StudioJob.model_validate(data)
211
+
212
+ async def enhance_and_wait(
213
+ self,
214
+ video_url: str,
215
+ features: Optional[Dict[str, bool]] = None,
216
+ subtitle_style: Optional[Dict[str, Any]] = None,
217
+ export_aspect_ratios: Optional[List[str]] = None,
218
+ webhook_url: Optional[str] = None,
219
+ timeout_seconds: float = 120.0,
220
+ poll_interval: float = 2.0,
221
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
222
+ ) -> VideoEnhanceResult:
223
+ job = await self.enhance(
224
+ video_url,
225
+ features=features,
226
+ subtitle_style=subtitle_style,
227
+ export_aspect_ratios=export_aspect_ratios,
228
+ webhook_url=webhook_url,
229
+ )
230
+ completed = await self._jobs.wait_for(
231
+ job.job_id,
232
+ timeout_seconds=timeout_seconds,
233
+ poll_interval=poll_interval,
234
+ on_progress=on_progress,
235
+ )
236
+ return VideoEnhanceResult.model_validate(completed.result or {})
237
+
238
+ async def walkthrough(
239
+ self,
240
+ photos: Optional[List[str]] = None,
241
+ mls_id: Optional[str] = None,
242
+ webhook_url: Optional[str] = None,
243
+ ) -> StudioJob:
244
+ payload: Dict[str, Any] = {}
245
+ if photos:
246
+ payload["photos"] = photos
247
+ if mls_id:
248
+ payload["mls_id"] = mls_id
249
+ if webhook_url:
250
+ payload["webhook_url"] = webhook_url
251
+ data = await self._http.post("/v1/studio/video/walkthrough", json=payload)
252
+ return StudioJob.model_validate(data)
253
+
254
+ async def walkthrough_and_wait(
255
+ self,
256
+ photos: Optional[List[str]] = None,
257
+ mls_id: Optional[str] = None,
258
+ webhook_url: Optional[str] = None,
259
+ timeout_seconds: float = 120.0,
260
+ poll_interval: float = 2.0,
261
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
262
+ ) -> VideoWalkthroughResult:
263
+ job = await self.walkthrough(photos=photos, mls_id=mls_id, webhook_url=webhook_url)
264
+ completed = await self._jobs.wait_for(
265
+ job.job_id,
266
+ timeout_seconds=timeout_seconds,
267
+ poll_interval=poll_interval,
268
+ on_progress=on_progress,
269
+ )
270
+ return VideoWalkthroughResult.model_validate(completed.result or {})
271
+
272
+ async def transition(
273
+ self,
274
+ start_image_url: str,
275
+ end_image_url: str,
276
+ duration_seconds: float = 3.0,
277
+ webhook_url: Optional[str] = None,
278
+ ) -> StudioJob:
279
+ payload: Dict[str, Any] = {
280
+ "start_image_url": start_image_url,
281
+ "end_image_url": end_image_url,
282
+ "duration_seconds": duration_seconds,
283
+ }
284
+ if webhook_url:
285
+ payload["webhook_url"] = webhook_url
286
+ data = await self._http.post("/v1/studio/video/transition", json=payload)
287
+ return StudioJob.model_validate(data)
288
+
289
+ async def transition_and_wait(
290
+ self,
291
+ start_image_url: str,
292
+ end_image_url: str,
293
+ duration_seconds: float = 3.0,
294
+ webhook_url: Optional[str] = None,
295
+ timeout_seconds: float = 90.0,
296
+ poll_interval: float = 2.0,
297
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
298
+ ) -> VideoTransitionResult:
299
+ job = await self.transition(
300
+ start_image_url,
301
+ end_image_url,
302
+ duration_seconds=duration_seconds,
303
+ webhook_url=webhook_url,
304
+ )
305
+ completed = await self._jobs.wait_for(
306
+ job.job_id,
307
+ timeout_seconds=timeout_seconds,
308
+ poll_interval=poll_interval,
309
+ on_progress=on_progress,
310
+ )
311
+ return VideoTransitionResult.model_validate(completed.result or {})
312
+
313
+ async def tour(
314
+ self,
315
+ ordered_photos: List[str],
316
+ music_mood: Optional[str] = "luxurious",
317
+ webhook_url: Optional[str] = None,
318
+ ) -> StudioJob:
319
+ payload: Dict[str, Any] = {
320
+ "ordered_photos": ordered_photos,
321
+ "music_mood": music_mood,
322
+ }
323
+ if webhook_url:
324
+ payload["webhook_url"] = webhook_url
325
+ data = await self._http.post("/v1/studio/video/tour", json=payload)
326
+ return StudioJob.model_validate(data)
327
+
328
+ async def tour_and_wait(
329
+ self,
330
+ ordered_photos: List[str],
331
+ music_mood: Optional[str] = "luxurious",
332
+ webhook_url: Optional[str] = None,
333
+ timeout_seconds: float = 120.0,
334
+ poll_interval: float = 2.0,
335
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
336
+ ) -> HouseTourResult:
337
+ job = await self.tour(
338
+ ordered_photos=ordered_photos, music_mood=music_mood, webhook_url=webhook_url
339
+ )
340
+ completed = await self._jobs.wait_for(
341
+ job.job_id,
342
+ timeout_seconds=timeout_seconds,
343
+ poll_interval=poll_interval,
344
+ on_progress=on_progress,
345
+ )
346
+ return HouseTourResult.model_validate(completed.result or {})