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.
- pymlsapi/__init__.py +103 -0
- pymlsapi/async_client.py +67 -0
- pymlsapi/async_http.py +104 -0
- pymlsapi/client.py +67 -0
- pymlsapi/config.py +58 -0
- pymlsapi/errors.py +95 -0
- pymlsapi/http.py +155 -0
- pymlsapi/models/__init__.py +100 -0
- pymlsapi/models/common.py +66 -0
- pymlsapi/models/content.py +92 -0
- pymlsapi/models/intelligence.py +65 -0
- pymlsapi/models/listings.py +91 -0
- pymlsapi/models/studio.py +171 -0
- pymlsapi/poller.py +102 -0
- pymlsapi/resources/__init__.py +20 -0
- pymlsapi/resources/account/__init__.py +38 -0
- pymlsapi/resources/account/billing.py +74 -0
- pymlsapi/resources/account/keys.py +46 -0
- pymlsapi/resources/content.py +77 -0
- pymlsapi/resources/intelligence.py +47 -0
- pymlsapi/resources/listings.py +179 -0
- pymlsapi/resources/studio/__init__.py +56 -0
- pymlsapi/resources/studio/creatives.py +124 -0
- pymlsapi/resources/studio/custom.py +100 -0
- pymlsapi/resources/studio/enhance.py +170 -0
- pymlsapi/resources/studio/floorplan.py +166 -0
- pymlsapi/resources/studio/jobs.py +86 -0
- pymlsapi/resources/studio/render.py +96 -0
- pymlsapi/resources/studio/social.py +57 -0
- pymlsapi/resources/studio/staging.py +694 -0
- pymlsapi/resources/studio/upload.py +93 -0
- pymlsapi/resources/studio/video.py +346 -0
- pymlsapi-0.1.0.dist-info/METADATA +570 -0
- pymlsapi-0.1.0.dist-info/RECORD +35 -0
- pymlsapi-0.1.0.dist-info/WHEEL +4 -0
|
@@ -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 {})
|