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,100 @@
1
+ from __future__ import annotations
2
+
3
+ from pymlsapi.models.common import InteriorStyle, JobStatus, RoomType, SocialPlatform
4
+ from pymlsapi.models.content import (
5
+ ContentGenerationResponse,
6
+ GeneratedContentBundle,
7
+ InstagramContent,
8
+ SocialPlatformBundle,
9
+ VideoScriptContent,
10
+ )
11
+ from pymlsapi.models.intelligence import (
12
+ FinancialFlag,
13
+ InvestorInsights,
14
+ LlmIntelligence,
15
+ PropertyIntelligence,
16
+ SystemCondition,
17
+ SystemsAndCapex,
18
+ )
19
+ from pymlsapi.models.listings import (
20
+ Address,
21
+ AgentInfo,
22
+ BaseListing,
23
+ Coordinates,
24
+ IngestJob,
25
+ ListingMetadata,
26
+ PropertyFeatures,
27
+ PropertySpecifications,
28
+ )
29
+ from pymlsapi.models.studio import (
30
+ AdCreativesResult,
31
+ ArchitecturalRenderResult,
32
+ CustomStudioResult,
33
+ DeclutterResult,
34
+ DeStageEmptyResult,
35
+ ExteriorEnhanceResult,
36
+ FloorPlanAnalysisResponse,
37
+ HouseTourResult,
38
+ Render3dResult,
39
+ ReplaceFurnitureResult,
40
+ ReplaceMaterialResult,
41
+ RestyleResult,
42
+ SocialPublishResult,
43
+ StagingResult,
44
+ StudioJob,
45
+ TwilightResult,
46
+ UploadResult,
47
+ UpscaleResult,
48
+ VideoEnhanceResult,
49
+ VideoTransitionResult,
50
+ VideoWalkthroughResult,
51
+ WallColorsResult,
52
+ )
53
+
54
+ __all__ = [
55
+ "AdCreativesResult",
56
+ "Address",
57
+ "AgentInfo",
58
+ "ArchitecturalRenderResult",
59
+ "BaseListing",
60
+ "ContentGenerationResponse",
61
+ "Coordinates",
62
+ "CustomStudioResult",
63
+ "DeStageEmptyResult",
64
+ "DeclutterResult",
65
+ "ExteriorEnhanceResult",
66
+ "FinancialFlag",
67
+ "FloorPlanAnalysisResponse",
68
+ "GeneratedContentBundle",
69
+ "HouseTourResult",
70
+ "IngestJob",
71
+ "InstagramContent",
72
+ "InteriorStyle",
73
+ "InvestorInsights",
74
+ "JobStatus",
75
+ "ListingMetadata",
76
+ "LlmIntelligence",
77
+ "PropertyFeatures",
78
+ "PropertyIntelligence",
79
+ "PropertySpecifications",
80
+ "Render3dResult",
81
+ "ReplaceFurnitureResult",
82
+ "ReplaceMaterialResult",
83
+ "RestyleResult",
84
+ "RoomType",
85
+ "SocialPlatform",
86
+ "SocialPlatformBundle",
87
+ "SocialPublishResult",
88
+ "StagingResult",
89
+ "StudioJob",
90
+ "SystemCondition",
91
+ "SystemsAndCapex",
92
+ "TwilightResult",
93
+ "UploadResult",
94
+ "UpscaleResult",
95
+ "VideoEnhanceResult",
96
+ "VideoScriptContent",
97
+ "VideoTransitionResult",
98
+ "VideoWalkthroughResult",
99
+ "WallColorsResult",
100
+ ]
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class InteriorStyle(str, Enum):
7
+ MODERN = "modern"
8
+ LUXURY = "luxury"
9
+ SCANDINAVIAN = "scandinavian"
10
+ JAPANDI = "japandi"
11
+ INDUSTRIAL = "industrial"
12
+ BOHEMIAN = "bohemian"
13
+ MINIMALIST = "minimalist"
14
+ COASTAL = "coastal"
15
+ MID_CENTURY_MODERN = "mid_century_modern"
16
+ ART_DECO = "art_deco"
17
+ FARMHOUSE = "farmhouse"
18
+ MEDITERRANEAN = "mediterranean"
19
+ CONTEMPORARY = "contemporary"
20
+ RUSTIC = "rustic"
21
+ TRANSITIONAL = "transitional"
22
+ FRENCH_COUNTRY = "french_country"
23
+ HOLLYWOOD_REGENCY = "hollywood_regency"
24
+ ECLECTIC = "eclectic"
25
+ ZEN = "zen"
26
+ BAUHAUS = "bauhaus"
27
+ VICTORIAN = "victorian"
28
+ TROPICAL = "tropical"
29
+ MODERN_CRAFTSMAN = "modern_craftsman"
30
+ SOUTHWESTERN = "southwestern"
31
+ WABI_SABI = "wabi_sabi"
32
+ SHABBY_CHIC = "shabby_chic"
33
+ CHALET = "chalet"
34
+ URBAN_LOFT = "urban_loft"
35
+ CUSTOM = "custom"
36
+
37
+
38
+ class RoomType(str, Enum):
39
+ LIVING_ROOM = "living_room"
40
+ BEDROOM = "bedroom"
41
+ PRIMARY_BEDROOM = "primary_bedroom"
42
+ DINING_ROOM = "dining_room"
43
+ KITCHEN = "kitchen"
44
+ BATHROOM = "bathroom"
45
+ PATIO = "patio"
46
+ OUTDOOR_PATIO = "outdoor_patio"
47
+ HOME_OFFICE = "home_office"
48
+ ENTRYWAY = "entryway"
49
+ BASEMENT = "basement"
50
+ COMMERCIAL_LOBBY = "commercial_lobby"
51
+
52
+
53
+ class SocialPlatform(str, Enum):
54
+ INSTAGRAM = "instagram"
55
+ FACEBOOK = "facebook"
56
+ LINKEDIN = "linkedin"
57
+ X_TWITTER = "x_twitter"
58
+ TIKTOK = "tiktok"
59
+ YOUTUBE = "youtube"
60
+
61
+
62
+ class JobStatus(str, Enum):
63
+ QUEUED = "queued"
64
+ PROCESSING = "processing"
65
+ COMPLETED = "completed"
66
+ FAILED = "failed"
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class InstagramContent(BaseModel):
9
+ hook_above_fold: Optional[str] = None
10
+ caption: str
11
+ carousel_prompt: Optional[str] = None
12
+ call_to_action: Optional[str] = None
13
+ hashtags: List[str] = Field(default_factory=list)
14
+ character_count: Optional[int] = None
15
+
16
+
17
+ class FacebookContent(BaseModel):
18
+ headline: Optional[str] = None
19
+ post_copy: str
20
+ link_preview: Optional[Dict[str, str]] = None
21
+ hashtags: List[str] = Field(default_factory=list)
22
+
23
+
24
+ class LinkedInContent(BaseModel):
25
+ post_copy: str
26
+ hashtags: List[str] = Field(default_factory=list)
27
+
28
+
29
+ class XTwitterContent(BaseModel):
30
+ single_tweet: Optional[str] = None
31
+ thread: List[str] = Field(default_factory=list)
32
+
33
+
34
+ class TikTokContent(BaseModel):
35
+ caption: str
36
+ on_screen_hook_text: Optional[str] = None
37
+ call_to_action_cue: Optional[str] = None
38
+ sound_recommendation: Optional[str] = None
39
+ hashtags: List[str] = Field(default_factory=list)
40
+
41
+
42
+ class YouTubeContent(BaseModel):
43
+ video_title_options: List[str] = Field(default_factory=list)
44
+ description: Optional[str] = None
45
+ tags: List[str] = Field(default_factory=list)
46
+ shorts: Optional[Dict[str, str]] = None
47
+
48
+
49
+ class SocialPlatformBundle(BaseModel):
50
+ instagram: Optional[InstagramContent] = None
51
+ facebook: Optional[FacebookContent] = None
52
+ linkedin: Optional[LinkedInContent] = None
53
+ x_twitter: Optional[XTwitterContent] = None
54
+ tiktok: Optional[TikTokContent] = None
55
+ youtube: Optional[YouTubeContent] = None
56
+
57
+
58
+ class VideoScriptScene(BaseModel):
59
+ second_range: str
60
+ visual: str
61
+ voiceover: str
62
+
63
+
64
+ class VideoScriptContent(BaseModel):
65
+ duration_seconds: int = 30
66
+ format: str = "vertical_9_16"
67
+ hook: str
68
+ scenes: List[VideoScriptScene] = Field(default_factory=list)
69
+
70
+
71
+ class EmailBlastContent(BaseModel):
72
+ subject_lines: List[str] = Field(default_factory=list)
73
+ preview_text: Optional[str] = None
74
+ body_html: Optional[str] = None
75
+ body_markdown: Optional[str] = None
76
+
77
+
78
+ class GeneratedContentBundle(BaseModel):
79
+ social: Optional[SocialPlatformBundle] = None
80
+ email_blast: Optional[EmailBlastContent] = None
81
+ video_script: Optional[VideoScriptContent] = None
82
+ flyer_bullets: Optional[List[str]] = None
83
+ mls_remarks: Optional[str] = None
84
+ investor_pitch: Optional[Dict[str, Any]] = None
85
+
86
+
87
+ class ContentGenerationResponse(BaseModel):
88
+ mls_id: str
89
+ tone: str
90
+ target_audience: Optional[str] = None
91
+ generated_at: Optional[str] = None
92
+ content: GeneratedContentBundle = Field(default_factory=GeneratedContentBundle)
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class SystemCondition(BaseModel):
9
+ age_years: Optional[int] = None
10
+ material: Optional[str] = None
11
+ condition: Optional[str] = None
12
+ estimated_replacement_horizon: Optional[str] = None
13
+ confidence: Optional[float] = None
14
+
15
+
16
+ class StormProtection(BaseModel):
17
+ has_impact_windows: bool = False
18
+ has_impact_doors: bool = False
19
+ insurance_premium_impact: Optional[str] = None
20
+
21
+
22
+ class SystemsAndCapex(BaseModel):
23
+ roof: Optional[SystemCondition] = None
24
+ hvac: Optional[SystemCondition] = None
25
+ water_heater: Optional[SystemCondition] = None
26
+ appliances: Optional[Dict[str, Any]] = None
27
+ storm_protection: Optional[StormProtection] = None
28
+
29
+
30
+ class RentRange(BaseModel):
31
+ low: float = 0.0
32
+ median: float = 0.0
33
+ high: float = 0.0
34
+
35
+
36
+ class InvestorInsights(BaseModel):
37
+ estimated_monthly_rent: RentRange = Field(default_factory=RentRange)
38
+ estimated_gross_yield_pct: float = 0.0
39
+ hoa_present: bool = False
40
+ rental_restrictions: Optional[str] = None
41
+ tenant_suitability: Optional[str] = None
42
+
43
+
44
+ class FinancialFlag(BaseModel):
45
+ type: str
46
+ severity: str
47
+ summary: str
48
+ explanation: str
49
+
50
+
51
+ class LlmIntelligence(BaseModel):
52
+ systems_and_capex: SystemsAndCapex = Field(default_factory=SystemsAndCapex)
53
+ investor_insights: InvestorInsights = Field(default_factory=InvestorInsights)
54
+ financial_flags: List[FinancialFlag] = Field(default_factory=list)
55
+ property_strengths: List[str] = Field(default_factory=list)
56
+ watch_items: List[str] = Field(default_factory=list)
57
+
58
+
59
+ class PropertyIntelligence(BaseModel):
60
+ mls_id: str
61
+ parcel_id: Optional[str] = None
62
+ summary: Dict[str, Any] = Field(default_factory=dict)
63
+ public_records: Dict[str, Any] = Field(default_factory=dict)
64
+ llm_derived_intelligence: Optional[LlmIntelligence] = None
65
+ generated_at: Optional[str] = None
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, List, Optional
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class Address(BaseModel):
9
+ street: str
10
+ city: str
11
+ state: str
12
+ zip: str
13
+ county: Optional[str] = None
14
+ country: Optional[str] = None
15
+ formatted: str
16
+
17
+
18
+ class Coordinates(BaseModel):
19
+ latitude: float
20
+ longitude: float
21
+
22
+
23
+ class PropertySpecifications(BaseModel):
24
+ beds: int
25
+ baths_full: float
26
+ baths_half: float = 0.0
27
+ sqft: int
28
+ lot_sqft: Optional[int] = None
29
+ year_built: Optional[int] = None
30
+ stories: Optional[int] = None
31
+ property_type: str
32
+ parking_spaces: Optional[int] = None
33
+ garage: Optional[bool] = None
34
+
35
+
36
+ class PropertyFeatures(BaseModel):
37
+ heating: List[str] = Field(default_factory=list)
38
+ cooling: List[str] = Field(default_factory=list)
39
+ appliances: List[str] = Field(default_factory=list)
40
+ flooring: List[str] = Field(default_factory=list)
41
+ roof_type: Optional[str] = None
42
+ water_source: Optional[str] = None
43
+ sewer: Optional[str] = None
44
+ view: List[str] = Field(default_factory=list)
45
+
46
+
47
+ class AgentInfo(BaseModel):
48
+ name: str
49
+ phone: Optional[str] = None
50
+ email: Optional[str] = None
51
+ license: Optional[str] = None
52
+ brokerage: Optional[str] = None
53
+
54
+
55
+ class ListingMetadata(BaseModel):
56
+ days_on_market: Optional[int] = None
57
+ listed_date: Optional[str] = None
58
+ mls_board: Optional[str] = None
59
+ updated_at: Optional[str] = None
60
+
61
+
62
+ class BaseListing(BaseModel):
63
+ source: str = "cache"
64
+ mls_id: str
65
+ status: str
66
+ price: float
67
+ currency: str = "USD"
68
+ price_per_sqft: Optional[float] = None
69
+ address: Address
70
+ coordinates: Optional[Coordinates] = None
71
+ specifications: PropertySpecifications
72
+ features: PropertyFeatures = Field(default_factory=PropertyFeatures)
73
+ photos: List[str] = Field(default_factory=list)
74
+ photo_count: int = 0
75
+ agent: Optional[AgentInfo] = None
76
+ description: str = ""
77
+ meta: Optional[ListingMetadata] = None
78
+
79
+
80
+ class IngestJob(BaseModel):
81
+ job_id: Optional[str] = Field(None, alias="jobId")
82
+ mls_id: Optional[str] = Field(None, alias="mlsId")
83
+ status: str
84
+ step: Optional[str] = None
85
+ status_url: Optional[str] = Field(None, alias="statusUrl")
86
+ lookup_url: Optional[str] = Field(None, alias="lookupUrl")
87
+ progress_percentage: Optional[int] = 0
88
+ error: Optional[str] = None
89
+ result: Optional[Any] = None
90
+
91
+ model_config = ConfigDict(populate_by_name=True)
@@ -0,0 +1,171 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class StudioJob(BaseModel):
9
+ job_id: str
10
+ status: str
11
+ progress_percentage: int = 0
12
+ current_step: Optional[str] = None
13
+ status_url: Optional[str] = None
14
+ result: Optional[Dict[str, Any]] = None
15
+ error: Optional[str] = None
16
+ created_at: Optional[str] = None
17
+ completed_at: Optional[str] = None
18
+
19
+
20
+ class StagingResult(BaseModel):
21
+ staged_photo_url: str
22
+ before_after_comparison_url: Optional[str] = None
23
+ room_type: Optional[str] = None
24
+ style: Optional[str] = None
25
+ staging_manifest: List[str] = Field(default_factory=list)
26
+
27
+
28
+ class DeclutterResult(BaseModel):
29
+ decluttered_photo_url: str
30
+ before_after_comparison_url: Optional[str] = None
31
+ items_removed: List[str] = Field(default_factory=list)
32
+
33
+
34
+ class TwilightResult(BaseModel):
35
+ enhanced_photo_url: str
36
+ before_after_comparison_url: Optional[str] = None
37
+ mode: Optional[str] = None
38
+
39
+
40
+ class DeStageEmptyResult(BaseModel):
41
+ empty_photo_url: str
42
+ before_after_comparison_url: Optional[str] = None
43
+ flooring_restored: Optional[str] = None
44
+
45
+
46
+ class RestyleResult(BaseModel):
47
+ restyled_photo_url: str
48
+ before_after_comparison_url: Optional[str] = None
49
+ target_style: Optional[str] = None
50
+
51
+
52
+ class ReplaceFurnitureResult(BaseModel):
53
+ result_photo_url: str
54
+ target_furniture: Optional[str] = None
55
+
56
+
57
+ class ReplaceMaterialResult(BaseModel):
58
+ result_photo_url: str
59
+ surface_type: Optional[str] = None
60
+ material_applied: Optional[str] = None
61
+
62
+
63
+ class SwatchItem(BaseModel):
64
+ color_name: str
65
+ hex: str
66
+ image_url: str
67
+
68
+
69
+ class WallColorsResult(BaseModel):
70
+ comparison_grid_3x3_url: str
71
+ swatch_results: List[SwatchItem] = Field(default_factory=list)
72
+
73
+
74
+ class ExteriorEnhanceResult(BaseModel):
75
+ enhanced_photo_url: str
76
+ enhancements_applied: List[str] = Field(default_factory=list)
77
+
78
+
79
+ class UpscaleResult(BaseModel):
80
+ upscaled_image_url: str
81
+ target_resolution: Optional[str] = None
82
+ scale_factor: int = 4
83
+
84
+
85
+ class RoomCloseup(BaseModel):
86
+ room_name: str
87
+ image_url: str
88
+
89
+
90
+ class Render3dResult(BaseModel):
91
+ isometric_3d_dollhouse_url: str
92
+ thumbnail_url: Optional[str] = None
93
+ room_renders: List[RoomCloseup] = Field(default_factory=list)
94
+
95
+
96
+ class SpatialSummary(BaseModel):
97
+ total_rooms_detected: int = 0
98
+ stories: int = 1
99
+ layout_type: Optional[str] = None
100
+
101
+
102
+ class FloorPlanAnalysisResponse(BaseModel):
103
+ style: Optional[str] = None
104
+ spatial_summary: SpatialSummary = Field(default_factory=SpatialSummary)
105
+ rooms: List[Dict[str, Any]] = Field(default_factory=list)
106
+ isometric_3d_prompt: Optional[str] = None
107
+ render_3d_url: Optional[str] = None
108
+
109
+
110
+ class ArchitecturalRenderResult(BaseModel):
111
+ rendered_image_url: str
112
+ source_type: Optional[str] = None
113
+
114
+
115
+ class CreativePlacement(BaseModel):
116
+ placement: str
117
+ image_url: str
118
+ aspect_ratio: Optional[str] = None
119
+
120
+
121
+ class AdCreativesCompliance(BaseModel):
122
+ fair_housing_passed: bool = True
123
+ flags: List[str] = Field(default_factory=list)
124
+
125
+
126
+ class AdCreativesResult(BaseModel):
127
+ mls_id: Optional[str] = None
128
+ creatives: Dict[str, CreativePlacement] = Field(default_factory=dict)
129
+ compliance: AdCreativesCompliance = Field(default_factory=AdCreativesCompliance)
130
+
131
+
132
+ class SocialPublishResult(BaseModel):
133
+ status: str
134
+ destinations_dispatched: List[str] = Field(default_factory=list)
135
+ results: Dict[str, Any] = Field(default_factory=dict)
136
+
137
+
138
+ class MasteredVideo(BaseModel):
139
+ aspect_ratio: str
140
+ url: str
141
+
142
+
143
+ class VideoEnhanceResult(BaseModel):
144
+ mastered_videos: List[MasteredVideo] = Field(default_factory=list)
145
+
146
+
147
+ class VideoWalkthroughResult(BaseModel):
148
+ video_url: str
149
+ duration_seconds: Optional[float] = None
150
+
151
+
152
+ class VideoTransitionResult(BaseModel):
153
+ video_url: str
154
+ transition_type: Optional[str] = None
155
+
156
+
157
+ class HouseTourResult(BaseModel):
158
+ video_tour_url: str
159
+ rooms_included: List[str] = Field(default_factory=list)
160
+
161
+
162
+ class CustomStudioResult(BaseModel):
163
+ output_url: str
164
+ prompt_used: str
165
+
166
+
167
+ class UploadResult(BaseModel):
168
+ url: str
169
+ filename: Optional[str] = None
170
+ content_type: Optional[str] = None
171
+ bytes: Optional[int] = None
pymlsapi/poller.py ADDED
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import time
5
+ from typing import Any, Callable, Optional, TypeVar
6
+
7
+ from pymlsapi.errors import JobTimeoutError, StudioJobFailedError
8
+
9
+ T = TypeVar("T")
10
+
11
+
12
+ def poll_job_sync(
13
+ fetch_fn: Callable[[], Any],
14
+ is_done_fn: Callable[[Any], bool],
15
+ is_failed_fn: Callable[[Any], bool],
16
+ timeout_seconds: float = 90.0,
17
+ initial_interval: float = 1.5,
18
+ max_interval: float = 5.0,
19
+ backoff_multiplier: float = 1.25,
20
+ on_progress: Optional[Callable[[Any], None]] = None,
21
+ ) -> Any:
22
+ """Poll a job synchronously until it is finished, fails, or times out."""
23
+ start_time = time.monotonic()
24
+ current_interval = initial_interval
25
+
26
+ while True:
27
+ job = fetch_fn()
28
+
29
+ if on_progress:
30
+ try:
31
+ on_progress(job)
32
+ except Exception:
33
+ pass
34
+
35
+ if is_done_fn(job):
36
+ return job
37
+
38
+ if is_failed_fn(job):
39
+ job_id = getattr(job, "job_id", getattr(job, "id", "unknown"))
40
+ error_msg = getattr(job, "error", "Job processing failed on worker")
41
+ raise StudioJobFailedError(
42
+ job_id=str(job_id), message=str(error_msg), error_details=job
43
+ )
44
+
45
+ elapsed = time.monotonic() - start_time
46
+ if elapsed >= timeout_seconds:
47
+ job_id = getattr(job, "job_id", getattr(job, "id", "unknown"))
48
+ raise JobTimeoutError(job_id=str(job_id), timeout_seconds=timeout_seconds)
49
+
50
+ sleep_time = min(current_interval, timeout_seconds - elapsed)
51
+ if sleep_time > 0:
52
+ time.sleep(sleep_time)
53
+
54
+ current_interval = min(current_interval * backoff_multiplier, max_interval)
55
+
56
+
57
+ async def poll_job_async(
58
+ fetch_fn: Callable[[], Any], # async function
59
+ is_done_fn: Callable[[Any], bool],
60
+ is_failed_fn: Callable[[Any], bool],
61
+ timeout_seconds: float = 90.0,
62
+ initial_interval: float = 1.5,
63
+ max_interval: float = 5.0,
64
+ backoff_multiplier: float = 1.25,
65
+ on_progress: Optional[Callable[[Any], None]] = None,
66
+ ) -> Any:
67
+ """Poll a job asynchronously until it is finished, fails, or times out."""
68
+ start_time = time.monotonic()
69
+ current_interval = initial_interval
70
+
71
+ while True:
72
+ job = await fetch_fn()
73
+
74
+ if on_progress:
75
+ try:
76
+ if asyncio.iscoroutinefunction(on_progress):
77
+ await on_progress(job)
78
+ else:
79
+ on_progress(job)
80
+ except Exception:
81
+ pass
82
+
83
+ if is_done_fn(job):
84
+ return job
85
+
86
+ if is_failed_fn(job):
87
+ job_id = getattr(job, "job_id", getattr(job, "id", "unknown"))
88
+ error_msg = getattr(job, "error", "Job processing failed on worker")
89
+ raise StudioJobFailedError(
90
+ job_id=str(job_id), message=str(error_msg), error_details=job
91
+ )
92
+
93
+ elapsed = time.monotonic() - start_time
94
+ if elapsed >= timeout_seconds:
95
+ job_id = getattr(job, "job_id", getattr(job, "id", "unknown"))
96
+ raise JobTimeoutError(job_id=str(job_id), timeout_seconds=timeout_seconds)
97
+
98
+ sleep_time = min(current_interval, timeout_seconds - elapsed)
99
+ if sleep_time > 0:
100
+ await asyncio.sleep(sleep_time)
101
+
102
+ current_interval = min(current_interval * backoff_multiplier, max_interval)
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from pymlsapi.resources.account import AccountResource, AsyncAccountResource
4
+ from pymlsapi.resources.content import AsyncContentResource, ContentResource
5
+ from pymlsapi.resources.intelligence import AsyncIntelligenceResource, IntelligenceResource
6
+ from pymlsapi.resources.listings import AsyncListingsResource, ListingsResource
7
+ from pymlsapi.resources.studio import AsyncStudioResource, StudioResource
8
+
9
+ __all__ = [
10
+ "AccountResource",
11
+ "AsyncAccountResource",
12
+ "AsyncContentResource",
13
+ "AsyncIntelligenceResource",
14
+ "AsyncListingsResource",
15
+ "AsyncStudioResource",
16
+ "ContentResource",
17
+ "IntelligenceResource",
18
+ "ListingsResource",
19
+ "StudioResource",
20
+ ]