plato-sdk-v2 2.0.50__py3-none-any.whl → 2.2.4__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.
Files changed (158) hide show
  1. plato/__init__.py +7 -6
  2. plato/_generated/__init__.py +1 -1
  3. plato/_generated/api/v1/env/evaluate_session.py +3 -3
  4. plato/_generated/api/v1/env/log_state_mutation.py +4 -4
  5. plato/_generated/api/v1/sandbox/checkpoint_vm.py +3 -3
  6. plato/_generated/api/v1/sandbox/save_vm_snapshot.py +3 -3
  7. plato/_generated/api/v1/sandbox/setup_sandbox.py +8 -8
  8. plato/_generated/api/v1/session/__init__.py +2 -0
  9. plato/_generated/api/v1/session/get_sessions_for_archival.py +100 -0
  10. plato/_generated/api/v1/testcases/__init__.py +6 -2
  11. plato/_generated/api/v1/testcases/get_mutation_groups_for_testcase.py +98 -0
  12. plato/_generated/api/v1/testcases/{get_next_output_testcase_for_scoring.py → get_next_testcase_for_scoring.py} +23 -10
  13. plato/_generated/api/v1/testcases/get_testcase_metadata_for_scoring.py +74 -0
  14. plato/_generated/api/v2/__init__.py +2 -1
  15. plato/_generated/api/v2/jobs/__init__.py +4 -0
  16. plato/_generated/api/v2/jobs/checkpoint.py +3 -3
  17. plato/_generated/api/v2/jobs/disk_snapshot.py +3 -3
  18. plato/_generated/api/v2/jobs/log_for_job.py +4 -39
  19. plato/_generated/api/v2/jobs/make.py +4 -4
  20. plato/_generated/api/v2/jobs/setup_sandbox.py +97 -0
  21. plato/_generated/api/v2/jobs/snapshot.py +3 -3
  22. plato/_generated/api/v2/jobs/snapshot_store.py +91 -0
  23. plato/_generated/api/v2/sessions/__init__.py +4 -0
  24. plato/_generated/api/v2/sessions/checkpoint.py +3 -3
  25. plato/_generated/api/v2/sessions/disk_snapshot.py +3 -3
  26. plato/_generated/api/v2/sessions/evaluate.py +3 -3
  27. plato/_generated/api/v2/sessions/log_job_mutation.py +4 -39
  28. plato/_generated/api/v2/sessions/make.py +4 -4
  29. plato/_generated/api/v2/sessions/setup_sandbox.py +98 -0
  30. plato/_generated/api/v2/sessions/snapshot.py +3 -3
  31. plato/_generated/api/v2/sessions/snapshot_store.py +94 -0
  32. plato/_generated/api/v2/user/__init__.py +7 -0
  33. plato/_generated/api/v2/user/get_current_user.py +76 -0
  34. plato/_generated/models/__init__.py +174 -23
  35. plato/_sims_generator/__init__.py +19 -4
  36. plato/_sims_generator/instruction.py +203 -0
  37. plato/_sims_generator/templates/instruction/helpers.py.jinja +161 -0
  38. plato/_sims_generator/templates/instruction/init.py.jinja +43 -0
  39. plato/agents/__init__.py +107 -517
  40. plato/agents/base.py +145 -0
  41. plato/agents/build.py +61 -0
  42. plato/agents/config.py +160 -0
  43. plato/agents/logging.py +401 -0
  44. plato/agents/runner.py +161 -0
  45. plato/agents/trajectory.py +266 -0
  46. plato/chronos/__init__.py +37 -0
  47. plato/chronos/api/__init__.py +3 -0
  48. plato/chronos/api/agents/__init__.py +13 -0
  49. plato/chronos/api/agents/create_agent.py +63 -0
  50. plato/chronos/api/agents/delete_agent.py +61 -0
  51. plato/chronos/api/agents/get_agent.py +62 -0
  52. plato/chronos/api/agents/get_agent_schema.py +72 -0
  53. plato/chronos/api/agents/get_agent_versions.py +62 -0
  54. plato/chronos/api/agents/list_agents.py +57 -0
  55. plato/chronos/api/agents/lookup_agent.py +74 -0
  56. plato/chronos/api/auth/__init__.py +9 -0
  57. plato/chronos/api/auth/debug_auth_api_auth_debug_get.py +43 -0
  58. plato/chronos/api/auth/get_auth_status_api_auth_status_get.py +61 -0
  59. plato/chronos/api/auth/get_current_user_route_api_auth_me_get.py +60 -0
  60. plato/chronos/api/callback/__init__.py +11 -0
  61. plato/chronos/api/callback/push_agent_logs.py +61 -0
  62. plato/chronos/api/callback/update_agent_status.py +57 -0
  63. plato/chronos/api/callback/upload_artifacts.py +59 -0
  64. plato/chronos/api/callback/upload_logs_zip.py +57 -0
  65. plato/chronos/api/callback/upload_trajectory.py +57 -0
  66. plato/chronos/api/default/__init__.py +7 -0
  67. plato/chronos/api/default/health.py +43 -0
  68. plato/chronos/api/jobs/__init__.py +7 -0
  69. plato/chronos/api/jobs/launch_job.py +63 -0
  70. plato/chronos/api/registry/__init__.py +19 -0
  71. plato/chronos/api/registry/get_agent_schema_api_registry_agents__agent_name__schema_get.py +62 -0
  72. plato/chronos/api/registry/get_agent_versions_api_registry_agents__agent_name__versions_get.py +52 -0
  73. plato/chronos/api/registry/get_world_schema_api_registry_worlds__package_name__schema_get.py +68 -0
  74. plato/chronos/api/registry/get_world_versions_api_registry_worlds__package_name__versions_get.py +52 -0
  75. plato/chronos/api/registry/list_registry_agents_api_registry_agents_get.py +44 -0
  76. plato/chronos/api/registry/list_registry_worlds_api_registry_worlds_get.py +44 -0
  77. plato/chronos/api/runtimes/__init__.py +11 -0
  78. plato/chronos/api/runtimes/create_runtime.py +63 -0
  79. plato/chronos/api/runtimes/delete_runtime.py +61 -0
  80. plato/chronos/api/runtimes/get_runtime.py +62 -0
  81. plato/chronos/api/runtimes/list_runtimes.py +57 -0
  82. plato/chronos/api/runtimes/test_runtime.py +67 -0
  83. plato/chronos/api/secrets/__init__.py +11 -0
  84. plato/chronos/api/secrets/create_secret.py +63 -0
  85. plato/chronos/api/secrets/delete_secret.py +61 -0
  86. plato/chronos/api/secrets/get_secret.py +62 -0
  87. plato/chronos/api/secrets/list_secrets.py +57 -0
  88. plato/chronos/api/secrets/update_secret.py +68 -0
  89. plato/chronos/api/sessions/__init__.py +10 -0
  90. plato/chronos/api/sessions/get_session.py +62 -0
  91. plato/chronos/api/sessions/get_session_logs.py +72 -0
  92. plato/chronos/api/sessions/get_session_logs_download.py +62 -0
  93. plato/chronos/api/sessions/list_sessions.py +57 -0
  94. plato/chronos/api/status/__init__.py +8 -0
  95. plato/chronos/api/status/get_status_api_status_get.py +44 -0
  96. plato/chronos/api/status/get_version_info_api_version_get.py +44 -0
  97. plato/chronos/api/templates/__init__.py +11 -0
  98. plato/chronos/api/templates/create_template.py +63 -0
  99. plato/chronos/api/templates/delete_template.py +61 -0
  100. plato/chronos/api/templates/get_template.py +62 -0
  101. plato/chronos/api/templates/list_templates.py +57 -0
  102. plato/chronos/api/templates/update_template.py +68 -0
  103. plato/chronos/api/trajectories/__init__.py +8 -0
  104. plato/chronos/api/trajectories/get_trajectory.py +62 -0
  105. plato/chronos/api/trajectories/list_trajectories.py +62 -0
  106. plato/chronos/api/worlds/__init__.py +10 -0
  107. plato/chronos/api/worlds/create_world.py +63 -0
  108. plato/chronos/api/worlds/delete_world.py +61 -0
  109. plato/chronos/api/worlds/get_world.py +62 -0
  110. plato/chronos/api/worlds/list_worlds.py +57 -0
  111. plato/chronos/client.py +171 -0
  112. plato/chronos/errors.py +141 -0
  113. plato/chronos/models/__init__.py +647 -0
  114. plato/chronos/py.typed +0 -0
  115. plato/sims/cli.py +299 -123
  116. plato/sims/registry.py +77 -4
  117. plato/v1/cli/agent.py +88 -84
  118. plato/v1/cli/main.py +2 -0
  119. plato/v1/cli/pm.py +441 -119
  120. plato/v1/cli/sandbox.py +747 -191
  121. plato/v1/cli/sim.py +11 -0
  122. plato/v1/cli/verify.py +1269 -0
  123. plato/v1/cli/world.py +3 -0
  124. plato/v1/flow_executor.py +21 -17
  125. plato/v1/models/env.py +11 -11
  126. plato/v1/sdk.py +2 -2
  127. plato/v1/sync_env.py +11 -11
  128. plato/v1/sync_flow_executor.py +21 -17
  129. plato/v1/sync_sdk.py +4 -2
  130. plato/v2/__init__.py +2 -0
  131. plato/v2/async_/environment.py +20 -1
  132. plato/v2/async_/session.py +54 -3
  133. plato/v2/sync/environment.py +2 -1
  134. plato/v2/sync/session.py +52 -2
  135. plato/worlds/README.md +218 -0
  136. plato/worlds/__init__.py +54 -18
  137. plato/worlds/base.py +304 -93
  138. plato/worlds/config.py +239 -73
  139. plato/worlds/runner.py +391 -80
  140. {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/METADATA +1 -3
  141. {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/RECORD +143 -68
  142. {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/entry_points.txt +1 -0
  143. plato/_generated/api/v2/interfaces/__init__.py +0 -27
  144. plato/_generated/api/v2/interfaces/v2_interface_browser_create.py +0 -68
  145. plato/_generated/api/v2/interfaces/v2_interface_cdp_url.py +0 -65
  146. plato/_generated/api/v2/interfaces/v2_interface_click.py +0 -64
  147. plato/_generated/api/v2/interfaces/v2_interface_close.py +0 -59
  148. plato/_generated/api/v2/interfaces/v2_interface_computer_create.py +0 -68
  149. plato/_generated/api/v2/interfaces/v2_interface_cursor.py +0 -64
  150. plato/_generated/api/v2/interfaces/v2_interface_key.py +0 -68
  151. plato/_generated/api/v2/interfaces/v2_interface_screenshot.py +0 -65
  152. plato/_generated/api/v2/interfaces/v2_interface_scroll.py +0 -70
  153. plato/_generated/api/v2/interfaces/v2_interface_type.py +0 -64
  154. plato/world/__init__.py +0 -44
  155. plato/world/base.py +0 -267
  156. plato/world/config.py +0 -139
  157. plato/world/types.py +0 -47
  158. {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/WHEEL +0 -0
@@ -0,0 +1,62 @@
1
+ """Get World"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from plato.chronos.errors import raise_for_status
10
+ from plato.chronos.models import WorldResponse
11
+
12
+
13
+ def _build_request_args(
14
+ public_id: str,
15
+ x_api_key: str | None = None,
16
+ ) -> dict[str, Any]:
17
+ """Build request arguments."""
18
+ url = f"/api/worlds/{public_id}"
19
+
20
+ headers: dict[str, str] = {}
21
+ if x_api_key is not None:
22
+ headers["X-API-Key"] = x_api_key
23
+
24
+ return {
25
+ "method": "GET",
26
+ "url": url,
27
+ "headers": headers,
28
+ }
29
+
30
+
31
+ def sync(
32
+ client: httpx.Client,
33
+ public_id: str,
34
+ x_api_key: str | None = None,
35
+ ) -> WorldResponse:
36
+ """Get a world by public ID."""
37
+
38
+ request_args = _build_request_args(
39
+ public_id=public_id,
40
+ x_api_key=x_api_key,
41
+ )
42
+
43
+ response = client.request(**request_args)
44
+ raise_for_status(response)
45
+ return WorldResponse.model_validate(response.json())
46
+
47
+
48
+ async def asyncio(
49
+ client: httpx.AsyncClient,
50
+ public_id: str,
51
+ x_api_key: str | None = None,
52
+ ) -> WorldResponse:
53
+ """Get a world by public ID."""
54
+
55
+ request_args = _build_request_args(
56
+ public_id=public_id,
57
+ x_api_key=x_api_key,
58
+ )
59
+
60
+ response = await client.request(**request_args)
61
+ raise_for_status(response)
62
+ return WorldResponse.model_validate(response.json())
@@ -0,0 +1,57 @@
1
+ """List Worlds"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from plato.chronos.errors import raise_for_status
10
+ from plato.chronos.models import WorldListResponse
11
+
12
+
13
+ def _build_request_args(
14
+ x_api_key: str | None = None,
15
+ ) -> dict[str, Any]:
16
+ """Build request arguments."""
17
+ url = "/api/worlds"
18
+
19
+ headers: dict[str, str] = {}
20
+ if x_api_key is not None:
21
+ headers["X-API-Key"] = x_api_key
22
+
23
+ return {
24
+ "method": "GET",
25
+ "url": url,
26
+ "headers": headers,
27
+ }
28
+
29
+
30
+ def sync(
31
+ client: httpx.Client,
32
+ x_api_key: str | None = None,
33
+ ) -> WorldListResponse:
34
+ """List all worlds for the org."""
35
+
36
+ request_args = _build_request_args(
37
+ x_api_key=x_api_key,
38
+ )
39
+
40
+ response = client.request(**request_args)
41
+ raise_for_status(response)
42
+ return WorldListResponse.model_validate(response.json())
43
+
44
+
45
+ async def asyncio(
46
+ client: httpx.AsyncClient,
47
+ x_api_key: str | None = None,
48
+ ) -> WorldListResponse:
49
+ """List all worlds for the org."""
50
+
51
+ request_args = _build_request_args(
52
+ x_api_key=x_api_key,
53
+ )
54
+
55
+ response = await client.request(**request_args)
56
+ raise_for_status(response)
57
+ return WorldListResponse.model_validate(response.json())
@@ -0,0 +1,171 @@
1
+ """HTTP client for Chronos API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from collections.abc import Callable
7
+ from typing import Any
8
+
9
+ import httpx
10
+
11
+
12
+ class Client:
13
+ """Sync HTTP client for Chronos API."""
14
+
15
+ def __init__(
16
+ self,
17
+ base_url: str,
18
+ timeout: float = 30.0,
19
+ headers: dict[str, str] | None = None,
20
+ max_retries: int = 3,
21
+ retry_on_status: tuple[int, ...] = (429, 500, 502, 503, 504),
22
+ on_request: Callable[[httpx.Request], None] | None = None,
23
+ on_response: Callable[[httpx.Response], None] | None = None,
24
+ **kwargs: Any,
25
+ ):
26
+ """Initialize the HTTP client.
27
+
28
+ Args:
29
+ base_url: Base URL for API requests
30
+ timeout: Request timeout in seconds
31
+ headers: Default headers to include in all requests
32
+ max_retries: Maximum number of retry attempts for failed requests
33
+ retry_on_status: HTTP status codes that trigger a retry
34
+ on_request: Hook called before each request
35
+ on_response: Hook called after each response
36
+ **kwargs: Additional arguments passed to httpx.Client
37
+ """
38
+ self._base_url = base_url.rstrip("/")
39
+ self._headers = headers or {}
40
+ self._max_retries = max_retries
41
+ self._retry_on_status = retry_on_status
42
+ self._closed = False
43
+
44
+ event_hooks: dict[str, list[Callable]] = {"request": [], "response": []}
45
+ if on_request:
46
+ event_hooks["request"].append(on_request)
47
+ if on_response:
48
+ event_hooks["response"].append(on_response)
49
+
50
+ self._client = httpx.Client(
51
+ base_url=self._base_url,
52
+ timeout=timeout,
53
+ headers=self._headers,
54
+ event_hooks=event_hooks if any(event_hooks.values()) else None,
55
+ **kwargs,
56
+ )
57
+
58
+ @property
59
+ def httpx(self) -> httpx.Client:
60
+ """Access the underlying httpx client."""
61
+ return self._client
62
+
63
+ @property
64
+ def max_retries(self) -> int:
65
+ """Maximum number of retry attempts."""
66
+ return self._max_retries
67
+
68
+ @property
69
+ def retry_on_status(self) -> tuple[int, ...]:
70
+ """HTTP status codes that trigger a retry."""
71
+ return self._retry_on_status
72
+
73
+ def close(self) -> None:
74
+ """Close the client."""
75
+ self._closed = True
76
+ self._client.close()
77
+
78
+ def __enter__(self) -> Client:
79
+ return self
80
+
81
+ def __exit__(self, *args: Any) -> None:
82
+ self.close()
83
+
84
+ def __del__(self) -> None:
85
+ if not self._closed:
86
+ warnings.warn(
87
+ f"{self.__class__.__name__} was not closed. Use 'with' statement or call 'client.close()'",
88
+ ResourceWarning,
89
+ stacklevel=2,
90
+ )
91
+
92
+
93
+ class AsyncClient:
94
+ """Async HTTP client for Chronos API."""
95
+
96
+ def __init__(
97
+ self,
98
+ base_url: str,
99
+ timeout: float = 30.0,
100
+ headers: dict[str, str] | None = None,
101
+ max_retries: int = 3,
102
+ retry_on_status: tuple[int, ...] = (429, 500, 502, 503, 504),
103
+ on_request: Callable[[httpx.Request], None] | None = None,
104
+ on_response: Callable[[httpx.Response], None] | None = None,
105
+ **kwargs: Any,
106
+ ):
107
+ """Initialize the async HTTP client.
108
+
109
+ Args:
110
+ base_url: Base URL for API requests
111
+ timeout: Request timeout in seconds
112
+ headers: Default headers to include in all requests
113
+ max_retries: Maximum number of retry attempts for failed requests
114
+ retry_on_status: HTTP status codes that trigger a retry
115
+ on_request: Hook called before each request
116
+ on_response: Hook called after each response
117
+ **kwargs: Additional arguments passed to httpx.AsyncClient
118
+ """
119
+ self._base_url = base_url.rstrip("/")
120
+ self._headers = headers or {}
121
+ self._max_retries = max_retries
122
+ self._retry_on_status = retry_on_status
123
+ self._closed = False
124
+
125
+ event_hooks: dict[str, list[Callable]] = {"request": [], "response": []}
126
+ if on_request:
127
+ event_hooks["request"].append(on_request)
128
+ if on_response:
129
+ event_hooks["response"].append(on_response)
130
+
131
+ self._client = httpx.AsyncClient(
132
+ base_url=self._base_url,
133
+ timeout=timeout,
134
+ headers=self._headers,
135
+ event_hooks=event_hooks if any(event_hooks.values()) else None,
136
+ **kwargs,
137
+ )
138
+
139
+ @property
140
+ def httpx(self) -> httpx.AsyncClient:
141
+ """Access the underlying httpx client."""
142
+ return self._client
143
+
144
+ @property
145
+ def max_retries(self) -> int:
146
+ """Maximum number of retry attempts."""
147
+ return self._max_retries
148
+
149
+ @property
150
+ def retry_on_status(self) -> tuple[int, ...]:
151
+ """HTTP status codes that trigger a retry."""
152
+ return self._retry_on_status
153
+
154
+ async def close(self) -> None:
155
+ """Close the client."""
156
+ self._closed = True
157
+ await self._client.aclose()
158
+
159
+ async def __aenter__(self) -> AsyncClient:
160
+ return self
161
+
162
+ async def __aexit__(self, *args: Any) -> None:
163
+ await self.close()
164
+
165
+ def __del__(self) -> None:
166
+ if not self._closed:
167
+ warnings.warn(
168
+ f"{self.__class__.__name__} was not closed. Use 'async with' statement or call 'await client.close()'",
169
+ ResourceWarning,
170
+ stacklevel=2,
171
+ )
@@ -0,0 +1,141 @@
1
+ """API error types for Chronos API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ class APIError(Exception):
9
+ """Base exception for API errors."""
10
+
11
+ def __init__(
12
+ self,
13
+ status_code: int,
14
+ message: str | None = None,
15
+ body: dict[str, Any] | None = None,
16
+ ):
17
+ self.status_code = status_code
18
+ self.message = message or f"API request failed with status {status_code}"
19
+ self.body = body
20
+ super().__init__(self.message)
21
+
22
+ def __repr__(self) -> str:
23
+ return f"{self.__class__.__name__}(status_code={self.status_code}, message={self.message!r})"
24
+
25
+
26
+ class BadRequestError(APIError):
27
+ """400 Bad Request - Invalid request parameters."""
28
+
29
+ def __init__(self, message: str | None = None, body: dict[str, Any] | None = None):
30
+ super().__init__(400, message or "Bad request", body)
31
+
32
+
33
+ class UnauthorizedError(APIError):
34
+ """401 Unauthorized - Invalid or missing authentication."""
35
+
36
+ def __init__(self, message: str | None = None, body: dict[str, Any] | None = None):
37
+ super().__init__(401, message or "Unauthorized", body)
38
+
39
+
40
+ class ForbiddenError(APIError):
41
+ """403 Forbidden - Insufficient permissions."""
42
+
43
+ def __init__(self, message: str | None = None, body: dict[str, Any] | None = None):
44
+ super().__init__(403, message or "Forbidden", body)
45
+
46
+
47
+ class NotFoundError(APIError):
48
+ """404 Not Found - Resource not found."""
49
+
50
+ def __init__(self, message: str | None = None, body: dict[str, Any] | None = None):
51
+ super().__init__(404, message or "Not found", body)
52
+
53
+
54
+ class ConflictError(APIError):
55
+ """409 Conflict - Resource conflict."""
56
+
57
+ def __init__(self, message: str | None = None, body: dict[str, Any] | None = None):
58
+ super().__init__(409, message or "Conflict", body)
59
+
60
+
61
+ class UnprocessableEntityError(APIError):
62
+ """422 Unprocessable Entity - Validation error."""
63
+
64
+ def __init__(self, message: str | None = None, body: dict[str, Any] | None = None):
65
+ super().__init__(422, message or "Unprocessable entity", body)
66
+
67
+
68
+ class RateLimitError(APIError):
69
+ """429 Too Many Requests - Rate limit exceeded."""
70
+
71
+ def __init__(
72
+ self,
73
+ message: str | None = None,
74
+ body: dict[str, Any] | None = None,
75
+ retry_after: float | None = None,
76
+ ):
77
+ super().__init__(429, message or "Rate limit exceeded", body)
78
+ self.retry_after = retry_after
79
+
80
+
81
+ class InternalServerError(APIError):
82
+ """500 Internal Server Error - Server error."""
83
+
84
+ def __init__(self, message: str | None = None, body: dict[str, Any] | None = None):
85
+ super().__init__(500, message or "Internal server error", body)
86
+
87
+
88
+ class ServiceUnavailableError(APIError):
89
+ """503 Service Unavailable - Service temporarily unavailable."""
90
+
91
+ def __init__(self, message: str | None = None, body: dict[str, Any] | None = None):
92
+ super().__init__(503, message or "Service unavailable", body)
93
+
94
+
95
+ def raise_for_status(response) -> None:
96
+ """Raise appropriate error based on response status code.
97
+
98
+ Args:
99
+ response: httpx.Response object
100
+
101
+ Raises:
102
+ APIError: Appropriate subclass based on status code
103
+ """
104
+ if response.is_success:
105
+ return
106
+
107
+ status = response.status_code
108
+
109
+ try:
110
+ body = response.json()
111
+ message = body.get("message") or body.get("error") or body.get("detail")
112
+ except Exception:
113
+ body = None
114
+ message = response.text or None
115
+
116
+ error_map = {
117
+ 400: BadRequestError,
118
+ 401: UnauthorizedError,
119
+ 403: ForbiddenError,
120
+ 404: NotFoundError,
121
+ 409: ConflictError,
122
+ 422: UnprocessableEntityError,
123
+ 429: RateLimitError,
124
+ 500: InternalServerError,
125
+ 503: ServiceUnavailableError,
126
+ }
127
+
128
+ error_class = error_map.get(status, APIError)
129
+
130
+ if status == 429:
131
+ retry_after = response.headers.get("retry-after")
132
+ raise RateLimitError(
133
+ message=message,
134
+ body=body,
135
+ retry_after=float(retry_after) if retry_after else None,
136
+ )
137
+
138
+ if error_class == APIError:
139
+ raise APIError(status, message, body)
140
+
141
+ raise error_class(message, body)