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.
- plato/__init__.py +7 -6
- plato/_generated/__init__.py +1 -1
- plato/_generated/api/v1/env/evaluate_session.py +3 -3
- plato/_generated/api/v1/env/log_state_mutation.py +4 -4
- plato/_generated/api/v1/sandbox/checkpoint_vm.py +3 -3
- plato/_generated/api/v1/sandbox/save_vm_snapshot.py +3 -3
- plato/_generated/api/v1/sandbox/setup_sandbox.py +8 -8
- plato/_generated/api/v1/session/__init__.py +2 -0
- plato/_generated/api/v1/session/get_sessions_for_archival.py +100 -0
- plato/_generated/api/v1/testcases/__init__.py +6 -2
- plato/_generated/api/v1/testcases/get_mutation_groups_for_testcase.py +98 -0
- plato/_generated/api/v1/testcases/{get_next_output_testcase_for_scoring.py → get_next_testcase_for_scoring.py} +23 -10
- plato/_generated/api/v1/testcases/get_testcase_metadata_for_scoring.py +74 -0
- plato/_generated/api/v2/__init__.py +2 -1
- plato/_generated/api/v2/jobs/__init__.py +4 -0
- plato/_generated/api/v2/jobs/checkpoint.py +3 -3
- plato/_generated/api/v2/jobs/disk_snapshot.py +3 -3
- plato/_generated/api/v2/jobs/log_for_job.py +4 -39
- plato/_generated/api/v2/jobs/make.py +4 -4
- plato/_generated/api/v2/jobs/setup_sandbox.py +97 -0
- plato/_generated/api/v2/jobs/snapshot.py +3 -3
- plato/_generated/api/v2/jobs/snapshot_store.py +91 -0
- plato/_generated/api/v2/sessions/__init__.py +4 -0
- plato/_generated/api/v2/sessions/checkpoint.py +3 -3
- plato/_generated/api/v2/sessions/disk_snapshot.py +3 -3
- plato/_generated/api/v2/sessions/evaluate.py +3 -3
- plato/_generated/api/v2/sessions/log_job_mutation.py +4 -39
- plato/_generated/api/v2/sessions/make.py +4 -4
- plato/_generated/api/v2/sessions/setup_sandbox.py +98 -0
- plato/_generated/api/v2/sessions/snapshot.py +3 -3
- plato/_generated/api/v2/sessions/snapshot_store.py +94 -0
- plato/_generated/api/v2/user/__init__.py +7 -0
- plato/_generated/api/v2/user/get_current_user.py +76 -0
- plato/_generated/models/__init__.py +174 -23
- plato/_sims_generator/__init__.py +19 -4
- plato/_sims_generator/instruction.py +203 -0
- plato/_sims_generator/templates/instruction/helpers.py.jinja +161 -0
- plato/_sims_generator/templates/instruction/init.py.jinja +43 -0
- plato/agents/__init__.py +107 -517
- plato/agents/base.py +145 -0
- plato/agents/build.py +61 -0
- plato/agents/config.py +160 -0
- plato/agents/logging.py +401 -0
- plato/agents/runner.py +161 -0
- plato/agents/trajectory.py +266 -0
- plato/chronos/__init__.py +37 -0
- plato/chronos/api/__init__.py +3 -0
- plato/chronos/api/agents/__init__.py +13 -0
- plato/chronos/api/agents/create_agent.py +63 -0
- plato/chronos/api/agents/delete_agent.py +61 -0
- plato/chronos/api/agents/get_agent.py +62 -0
- plato/chronos/api/agents/get_agent_schema.py +72 -0
- plato/chronos/api/agents/get_agent_versions.py +62 -0
- plato/chronos/api/agents/list_agents.py +57 -0
- plato/chronos/api/agents/lookup_agent.py +74 -0
- plato/chronos/api/auth/__init__.py +9 -0
- plato/chronos/api/auth/debug_auth_api_auth_debug_get.py +43 -0
- plato/chronos/api/auth/get_auth_status_api_auth_status_get.py +61 -0
- plato/chronos/api/auth/get_current_user_route_api_auth_me_get.py +60 -0
- plato/chronos/api/callback/__init__.py +11 -0
- plato/chronos/api/callback/push_agent_logs.py +61 -0
- plato/chronos/api/callback/update_agent_status.py +57 -0
- plato/chronos/api/callback/upload_artifacts.py +59 -0
- plato/chronos/api/callback/upload_logs_zip.py +57 -0
- plato/chronos/api/callback/upload_trajectory.py +57 -0
- plato/chronos/api/default/__init__.py +7 -0
- plato/chronos/api/default/health.py +43 -0
- plato/chronos/api/jobs/__init__.py +7 -0
- plato/chronos/api/jobs/launch_job.py +63 -0
- plato/chronos/api/registry/__init__.py +19 -0
- plato/chronos/api/registry/get_agent_schema_api_registry_agents__agent_name__schema_get.py +62 -0
- plato/chronos/api/registry/get_agent_versions_api_registry_agents__agent_name__versions_get.py +52 -0
- plato/chronos/api/registry/get_world_schema_api_registry_worlds__package_name__schema_get.py +68 -0
- plato/chronos/api/registry/get_world_versions_api_registry_worlds__package_name__versions_get.py +52 -0
- plato/chronos/api/registry/list_registry_agents_api_registry_agents_get.py +44 -0
- plato/chronos/api/registry/list_registry_worlds_api_registry_worlds_get.py +44 -0
- plato/chronos/api/runtimes/__init__.py +11 -0
- plato/chronos/api/runtimes/create_runtime.py +63 -0
- plato/chronos/api/runtimes/delete_runtime.py +61 -0
- plato/chronos/api/runtimes/get_runtime.py +62 -0
- plato/chronos/api/runtimes/list_runtimes.py +57 -0
- plato/chronos/api/runtimes/test_runtime.py +67 -0
- plato/chronos/api/secrets/__init__.py +11 -0
- plato/chronos/api/secrets/create_secret.py +63 -0
- plato/chronos/api/secrets/delete_secret.py +61 -0
- plato/chronos/api/secrets/get_secret.py +62 -0
- plato/chronos/api/secrets/list_secrets.py +57 -0
- plato/chronos/api/secrets/update_secret.py +68 -0
- plato/chronos/api/sessions/__init__.py +10 -0
- plato/chronos/api/sessions/get_session.py +62 -0
- plato/chronos/api/sessions/get_session_logs.py +72 -0
- plato/chronos/api/sessions/get_session_logs_download.py +62 -0
- plato/chronos/api/sessions/list_sessions.py +57 -0
- plato/chronos/api/status/__init__.py +8 -0
- plato/chronos/api/status/get_status_api_status_get.py +44 -0
- plato/chronos/api/status/get_version_info_api_version_get.py +44 -0
- plato/chronos/api/templates/__init__.py +11 -0
- plato/chronos/api/templates/create_template.py +63 -0
- plato/chronos/api/templates/delete_template.py +61 -0
- plato/chronos/api/templates/get_template.py +62 -0
- plato/chronos/api/templates/list_templates.py +57 -0
- plato/chronos/api/templates/update_template.py +68 -0
- plato/chronos/api/trajectories/__init__.py +8 -0
- plato/chronos/api/trajectories/get_trajectory.py +62 -0
- plato/chronos/api/trajectories/list_trajectories.py +62 -0
- plato/chronos/api/worlds/__init__.py +10 -0
- plato/chronos/api/worlds/create_world.py +63 -0
- plato/chronos/api/worlds/delete_world.py +61 -0
- plato/chronos/api/worlds/get_world.py +62 -0
- plato/chronos/api/worlds/list_worlds.py +57 -0
- plato/chronos/client.py +171 -0
- plato/chronos/errors.py +141 -0
- plato/chronos/models/__init__.py +647 -0
- plato/chronos/py.typed +0 -0
- plato/sims/cli.py +299 -123
- plato/sims/registry.py +77 -4
- plato/v1/cli/agent.py +88 -84
- plato/v1/cli/main.py +2 -0
- plato/v1/cli/pm.py +441 -119
- plato/v1/cli/sandbox.py +747 -191
- plato/v1/cli/sim.py +11 -0
- plato/v1/cli/verify.py +1269 -0
- plato/v1/cli/world.py +3 -0
- plato/v1/flow_executor.py +21 -17
- plato/v1/models/env.py +11 -11
- plato/v1/sdk.py +2 -2
- plato/v1/sync_env.py +11 -11
- plato/v1/sync_flow_executor.py +21 -17
- plato/v1/sync_sdk.py +4 -2
- plato/v2/__init__.py +2 -0
- plato/v2/async_/environment.py +20 -1
- plato/v2/async_/session.py +54 -3
- plato/v2/sync/environment.py +2 -1
- plato/v2/sync/session.py +52 -2
- plato/worlds/README.md +218 -0
- plato/worlds/__init__.py +54 -18
- plato/worlds/base.py +304 -93
- plato/worlds/config.py +239 -73
- plato/worlds/runner.py +391 -80
- {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/METADATA +1 -3
- {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/RECORD +143 -68
- {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/entry_points.txt +1 -0
- plato/_generated/api/v2/interfaces/__init__.py +0 -27
- plato/_generated/api/v2/interfaces/v2_interface_browser_create.py +0 -68
- plato/_generated/api/v2/interfaces/v2_interface_cdp_url.py +0 -65
- plato/_generated/api/v2/interfaces/v2_interface_click.py +0 -64
- plato/_generated/api/v2/interfaces/v2_interface_close.py +0 -59
- plato/_generated/api/v2/interfaces/v2_interface_computer_create.py +0 -68
- plato/_generated/api/v2/interfaces/v2_interface_cursor.py +0 -64
- plato/_generated/api/v2/interfaces/v2_interface_key.py +0 -68
- plato/_generated/api/v2/interfaces/v2_interface_screenshot.py +0 -65
- plato/_generated/api/v2/interfaces/v2_interface_scroll.py +0 -70
- plato/_generated/api/v2/interfaces/v2_interface_type.py +0 -64
- plato/world/__init__.py +0 -44
- plato/world/base.py +0 -267
- plato/world/config.py +0 -139
- plato/world/types.py +0 -47
- {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())
|
plato/chronos/client.py
ADDED
|
@@ -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
|
+
)
|
plato/chronos/errors.py
ADDED
|
@@ -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)
|