fleet-python 0.2.11__tar.gz → 0.2.13__tar.gz
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.
Potentially problematic release.
This version of fleet-python might be problematic. Click here for more details.
- {fleet_python-0.2.11 → fleet_python-0.2.13}/PKG-INFO +2 -1
- {fleet_python-0.2.11 → fleet_python-0.2.13}/examples/example.py +1 -1
- {fleet_python-0.2.11 → fleet_python-0.2.13}/examples/example_sync.py +1 -1
- fleet_python-0.2.13/fleet/_async/base.py +223 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/_async/client.py +29 -14
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/_async/env/client.py +1 -1
- fleet_python-0.2.13/fleet/_async/exceptions.py +161 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/_async/instance/__init__.py +1 -1
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/_async/instance/base.py +28 -1
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/_async/instance/client.py +5 -5
- {fleet_python-0.2.11/fleet → fleet_python-0.2.13/fleet/_async}/resources/base.py +1 -1
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/_async/resources/browser.py +1 -1
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/_async/resources/sqlite.py +2 -2
- fleet_python-0.2.13/fleet/base.py +223 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/client.py +32 -13
- fleet_python-0.2.13/fleet/config.py +9 -0
- fleet_python-0.2.13/fleet/exceptions.py +161 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/instance/base.py +28 -1
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/instance/client.py +4 -3
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet_python.egg-info/PKG-INFO +2 -1
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet_python.egg-info/SOURCES.txt +1 -2
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet_python.egg-info/requires.txt +1 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/pyproject.toml +4 -1
- {fleet_python-0.2.11 → fleet_python-0.2.13}/scripts/fix_sync_imports.py +33 -11
- fleet_python-0.2.11/fleet/_async/base.py +0 -132
- fleet_python-0.2.11/fleet/_async/exceptions.py +0 -82
- fleet_python-0.2.11/fleet/base.py +0 -132
- fleet_python-0.2.11/fleet/exceptions.py +0 -82
- fleet_python-0.2.11/fleet/instance/models.py +0 -141
- fleet_python-0.2.11/fleet/models.py +0 -109
- {fleet_python-0.2.11 → fleet_python-0.2.13}/LICENSE +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/README.md +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/examples/dsl_example.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/examples/example_client.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/examples/gemini_example.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/examples/json_tasks_example.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/examples/nova_act_example.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/examples/openai_example.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/examples/openai_simple_example.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/examples/quickstart.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/__init__.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/_async/__init__.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/_async/env/__init__.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/_async/playwright.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/_async/resources/__init__.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/env/__init__.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/env/client.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/instance/__init__.py +0 -0
- {fleet_python-0.2.11/fleet/_async → fleet_python-0.2.13/fleet}/instance/models.py +0 -0
- {fleet_python-0.2.11/fleet/_async → fleet_python-0.2.13/fleet}/models.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/playwright.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/resources/__init__.py +0 -0
- {fleet_python-0.2.11/fleet/_async → fleet_python-0.2.13/fleet}/resources/base.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/resources/browser.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/resources/sqlite.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/verifiers/__init__.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/verifiers/code.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/verifiers/db.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet/verifiers/sql_differ.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet_python.egg-info/dependency_links.txt +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/fleet_python.egg-info/top_level.txt +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/scripts/unasync.py +0 -0
- {fleet_python-0.2.11 → fleet_python-0.2.13}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fleet-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.13
|
|
4
4
|
Summary: Python SDK for Fleet environments
|
|
5
5
|
Author-email: Fleet AI <nic@fleet.so>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -23,6 +23,7 @@ License-File: LICENSE
|
|
|
23
23
|
Requires-Dist: aiohttp>=3.8.0
|
|
24
24
|
Requires-Dist: pydantic>=2.0.0
|
|
25
25
|
Requires-Dist: httpx>=0.27.0
|
|
26
|
+
Requires-Dist: httpx-retries>=0.4.0
|
|
26
27
|
Requires-Dist: typing-extensions>=4.0.0
|
|
27
28
|
Provides-Extra: dev
|
|
28
29
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
@@ -16,7 +16,7 @@ async def main():
|
|
|
16
16
|
print("Environments:", len(environments))
|
|
17
17
|
|
|
18
18
|
# Create a new instance
|
|
19
|
-
env = await flt.env.make_async("hubspot
|
|
19
|
+
env = await flt.env.make_async("hubspot")
|
|
20
20
|
print(f"New Instance: {env.instance_id} ({env.region})")
|
|
21
21
|
|
|
22
22
|
response = await env.reset(seed=42)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from typing import Dict, Any, Optional
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from ..models import InstanceResponse
|
|
6
|
+
from .exceptions import (
|
|
7
|
+
FleetAPIError,
|
|
8
|
+
FleetAuthenticationError,
|
|
9
|
+
FleetRateLimitError,
|
|
10
|
+
FleetInstanceLimitError,
|
|
11
|
+
FleetTimeoutError,
|
|
12
|
+
FleetTeamNotFoundError,
|
|
13
|
+
FleetEnvironmentAccessError,
|
|
14
|
+
FleetRegionError,
|
|
15
|
+
FleetEnvironmentNotFoundError,
|
|
16
|
+
FleetVersionNotFoundError,
|
|
17
|
+
FleetBadRequestError,
|
|
18
|
+
FleetPermissionError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EnvironmentBase(InstanceResponse):
|
|
23
|
+
@property
|
|
24
|
+
def manager_url(self) -> str:
|
|
25
|
+
return f"{self.urls.manager.api}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BaseWrapper:
|
|
29
|
+
def __init__(self, *, api_key: Optional[str], base_url: Optional[str]):
|
|
30
|
+
if api_key is None:
|
|
31
|
+
raise ValueError("api_key is required")
|
|
32
|
+
self.api_key = api_key
|
|
33
|
+
if base_url is None:
|
|
34
|
+
base_url = "https://orchestrator.fleetai.com"
|
|
35
|
+
self.base_url = base_url
|
|
36
|
+
|
|
37
|
+
def get_headers(self) -> Dict[str, str]:
|
|
38
|
+
headers: Dict[str, str] = {
|
|
39
|
+
"X-Fleet-SDK-Language": "Python",
|
|
40
|
+
"X-Fleet-SDK-Version": "1.0.0",
|
|
41
|
+
}
|
|
42
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
43
|
+
return headers
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AsyncWrapper(BaseWrapper):
|
|
47
|
+
def __init__(self, *, httpx_client: httpx.AsyncClient, **kwargs):
|
|
48
|
+
super().__init__(**kwargs)
|
|
49
|
+
self.httpx_client = httpx_client
|
|
50
|
+
|
|
51
|
+
async def request(
|
|
52
|
+
self,
|
|
53
|
+
method: str,
|
|
54
|
+
url: str,
|
|
55
|
+
params: Optional[Dict[str, Any]] = None,
|
|
56
|
+
json: Optional[Any] = None,
|
|
57
|
+
base_url: Optional[str] = None,
|
|
58
|
+
**kwargs,
|
|
59
|
+
) -> httpx.Response:
|
|
60
|
+
base_url = base_url or self.base_url
|
|
61
|
+
try:
|
|
62
|
+
response = await self.httpx_client.request(
|
|
63
|
+
method,
|
|
64
|
+
f"{base_url}{url}",
|
|
65
|
+
headers=self.get_headers(),
|
|
66
|
+
params=params,
|
|
67
|
+
json=json,
|
|
68
|
+
**kwargs,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Check for HTTP errors
|
|
72
|
+
if response.status_code >= 400:
|
|
73
|
+
self._handle_error_response(response)
|
|
74
|
+
|
|
75
|
+
return response
|
|
76
|
+
except httpx.TimeoutException as e:
|
|
77
|
+
raise FleetTimeoutError(f"Request timed out: {str(e)}")
|
|
78
|
+
except httpx.RequestError as e:
|
|
79
|
+
raise FleetAPIError(f"Request failed: {str(e)}")
|
|
80
|
+
|
|
81
|
+
def _handle_error_response(self, response: httpx.Response) -> None:
|
|
82
|
+
"""Handle HTTP error responses and convert to appropriate Fleet exceptions."""
|
|
83
|
+
status_code = response.status_code
|
|
84
|
+
|
|
85
|
+
# Try to parse error response as JSON
|
|
86
|
+
try:
|
|
87
|
+
error_data = response.json()
|
|
88
|
+
detail = error_data.get("detail", response.text)
|
|
89
|
+
|
|
90
|
+
# Handle structured error responses
|
|
91
|
+
if isinstance(detail, dict):
|
|
92
|
+
error_type = detail.get("error_type", "")
|
|
93
|
+
error_message = detail.get("message", str(detail))
|
|
94
|
+
|
|
95
|
+
if error_type == "instance_limit_exceeded":
|
|
96
|
+
raise FleetInstanceLimitError(
|
|
97
|
+
error_message,
|
|
98
|
+
running_instances=detail.get("running_instances"),
|
|
99
|
+
instance_limit=detail.get("instance_limit"),
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
error_message = detail.get("message", str(detail))
|
|
103
|
+
else:
|
|
104
|
+
error_message = detail
|
|
105
|
+
|
|
106
|
+
except (json.JSONDecodeError, ValueError):
|
|
107
|
+
error_message = response.text
|
|
108
|
+
error_data = None
|
|
109
|
+
|
|
110
|
+
# Handle specific error types
|
|
111
|
+
if status_code == 401:
|
|
112
|
+
raise FleetAuthenticationError(error_message)
|
|
113
|
+
elif status_code == 403:
|
|
114
|
+
# Handle 403 errors - instance limit, permissions, team not found
|
|
115
|
+
if "instance limit" in error_message.lower():
|
|
116
|
+
# Try to extract instance counts from the error message
|
|
117
|
+
running_instances = None
|
|
118
|
+
instance_limit = None
|
|
119
|
+
if (
|
|
120
|
+
"You have" in error_message
|
|
121
|
+
and "running instances out of a maximum of" in error_message
|
|
122
|
+
):
|
|
123
|
+
try:
|
|
124
|
+
# Extract numbers from message like "You have 5 running instances out of a maximum of 10"
|
|
125
|
+
parts = error_message.split("You have ")[1].split(
|
|
126
|
+
" running instances out of a maximum of "
|
|
127
|
+
)
|
|
128
|
+
if len(parts) == 2:
|
|
129
|
+
running_instances = int(parts[0])
|
|
130
|
+
instance_limit = int(parts[1].split(".")[0])
|
|
131
|
+
except (IndexError, ValueError):
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
raise FleetInstanceLimitError(
|
|
135
|
+
error_message,
|
|
136
|
+
running_instances=running_instances,
|
|
137
|
+
instance_limit=instance_limit,
|
|
138
|
+
)
|
|
139
|
+
elif "team not found" in error_message.lower():
|
|
140
|
+
raise FleetTeamNotFoundError(error_message)
|
|
141
|
+
elif (
|
|
142
|
+
"does not have permission" in error_message.lower()
|
|
143
|
+
and "environment" in error_message.lower()
|
|
144
|
+
):
|
|
145
|
+
# Extract environment key from error message if possible
|
|
146
|
+
env_key = None
|
|
147
|
+
if "'" in error_message:
|
|
148
|
+
# Look for quoted environment key
|
|
149
|
+
parts = error_message.split("'")
|
|
150
|
+
if len(parts) >= 2:
|
|
151
|
+
env_key = parts[1]
|
|
152
|
+
raise FleetEnvironmentAccessError(error_message, env_key=env_key)
|
|
153
|
+
else:
|
|
154
|
+
raise FleetPermissionError(error_message)
|
|
155
|
+
elif status_code == 400:
|
|
156
|
+
# Handle 400 errors - bad requests, region errors, environment/version not found
|
|
157
|
+
if "region" in error_message.lower() and (
|
|
158
|
+
"not supported" in error_message.lower()
|
|
159
|
+
or "unsupported" in error_message.lower()
|
|
160
|
+
):
|
|
161
|
+
# Extract region and supported regions if possible
|
|
162
|
+
region = None
|
|
163
|
+
supported_regions = []
|
|
164
|
+
if "Region" in error_message:
|
|
165
|
+
# Try to extract region from "Region X not supported"
|
|
166
|
+
try:
|
|
167
|
+
parts = error_message.split("Region ")[1].split(
|
|
168
|
+
" not supported"
|
|
169
|
+
)
|
|
170
|
+
if parts:
|
|
171
|
+
region = parts[0]
|
|
172
|
+
except (IndexError, ValueError):
|
|
173
|
+
pass
|
|
174
|
+
# Try to extract supported regions from "Please use [...]"
|
|
175
|
+
if "Please use" in error_message and "[" in error_message:
|
|
176
|
+
try:
|
|
177
|
+
regions_str = error_message.split("[")[1].split("]")[0]
|
|
178
|
+
supported_regions = [
|
|
179
|
+
r.strip().strip("'\"") for r in regions_str.split(",")
|
|
180
|
+
]
|
|
181
|
+
except (IndexError, ValueError):
|
|
182
|
+
pass
|
|
183
|
+
raise FleetRegionError(
|
|
184
|
+
error_message, region=region, supported_regions=supported_regions
|
|
185
|
+
)
|
|
186
|
+
elif (
|
|
187
|
+
"environment" in error_message.lower()
|
|
188
|
+
and "not found" in error_message.lower()
|
|
189
|
+
):
|
|
190
|
+
# Extract env_key if possible
|
|
191
|
+
env_key = None
|
|
192
|
+
if "'" in error_message:
|
|
193
|
+
parts = error_message.split("'")
|
|
194
|
+
if len(parts) >= 2:
|
|
195
|
+
env_key = parts[1]
|
|
196
|
+
raise FleetEnvironmentNotFoundError(error_message, env_key=env_key)
|
|
197
|
+
elif (
|
|
198
|
+
"version" in error_message.lower()
|
|
199
|
+
and "not found" in error_message.lower()
|
|
200
|
+
):
|
|
201
|
+
# Extract version and env_key if possible
|
|
202
|
+
version = None
|
|
203
|
+
env_key = None
|
|
204
|
+
if "'" in error_message:
|
|
205
|
+
parts = error_message.split("'")
|
|
206
|
+
if len(parts) >= 2:
|
|
207
|
+
version = parts[1]
|
|
208
|
+
if len(parts) >= 4:
|
|
209
|
+
env_key = parts[3]
|
|
210
|
+
raise FleetVersionNotFoundError(
|
|
211
|
+
error_message, version=version, env_key=env_key
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
raise FleetBadRequestError(error_message)
|
|
215
|
+
elif status_code == 429:
|
|
216
|
+
# Rate limit errors (not instance limit which is now 403)
|
|
217
|
+
raise FleetRateLimitError(error_message)
|
|
218
|
+
else:
|
|
219
|
+
raise FleetAPIError(
|
|
220
|
+
error_message,
|
|
221
|
+
status_code=status_code,
|
|
222
|
+
response_data=error_data if "error_data" in locals() else None,
|
|
223
|
+
)
|
|
@@ -20,7 +20,7 @@ import logging
|
|
|
20
20
|
from typing import Optional, List
|
|
21
21
|
|
|
22
22
|
from .base import EnvironmentBase, AsyncWrapper
|
|
23
|
-
from
|
|
23
|
+
from ..models import InstanceRequest, InstanceRecord, Environment as EnvironmentModel
|
|
24
24
|
|
|
25
25
|
from .instance import (
|
|
26
26
|
AsyncInstanceClient,
|
|
@@ -29,6 +29,8 @@ from .instance import (
|
|
|
29
29
|
ValidatorType,
|
|
30
30
|
ExecuteFunctionResponse,
|
|
31
31
|
)
|
|
32
|
+
from ..config import DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, REGION_BASE_URL
|
|
33
|
+
from .instance.base import default_httpx_client
|
|
32
34
|
from .resources.base import Resource
|
|
33
35
|
from .resources.sqlite import AsyncSQLiteResource
|
|
34
36
|
from .resources.browser import AsyncBrowserResource
|
|
@@ -36,16 +38,23 @@ from .resources.browser import AsyncBrowserResource
|
|
|
36
38
|
logger = logging.getLogger(__name__)
|
|
37
39
|
|
|
38
40
|
|
|
41
|
+
async def _delete_instance(client: AsyncWrapper, instance_id: str) -> InstanceRecord:
|
|
42
|
+
response = await client.request("DELETE", f"/v1/env/instances/{instance_id}")
|
|
43
|
+
return InstanceRecord(**response.json())
|
|
44
|
+
|
|
45
|
+
|
|
39
46
|
class AsyncEnvironment(EnvironmentBase):
|
|
40
|
-
def __init__(self,
|
|
47
|
+
def __init__(self, client: AsyncWrapper, **kwargs):
|
|
41
48
|
super().__init__(**kwargs)
|
|
42
|
-
self.
|
|
49
|
+
self._client = client
|
|
43
50
|
self._instance: Optional[AsyncInstanceClient] = None
|
|
44
51
|
|
|
45
52
|
@property
|
|
46
53
|
def instance(self) -> AsyncInstanceClient:
|
|
47
54
|
if self._instance is None:
|
|
48
|
-
self._instance = AsyncInstanceClient(
|
|
55
|
+
self._instance = AsyncInstanceClient(
|
|
56
|
+
self.manager_url, self._client.httpx_client
|
|
57
|
+
)
|
|
49
58
|
return self._instance
|
|
50
59
|
|
|
51
60
|
async def reset(
|
|
@@ -66,7 +75,7 @@ class AsyncEnvironment(EnvironmentBase):
|
|
|
66
75
|
return await self.instance.resources()
|
|
67
76
|
|
|
68
77
|
async def close(self) -> InstanceRecord:
|
|
69
|
-
return await
|
|
78
|
+
return await _delete_instance(self._client, self.instance_id)
|
|
70
79
|
|
|
71
80
|
async def verify(self, validator: ValidatorType) -> ExecuteFunctionResponse:
|
|
72
81
|
return await self.instance.verify(validator)
|
|
@@ -83,8 +92,10 @@ class AsyncFleet:
|
|
|
83
92
|
api_key: Optional[str] = os.getenv("FLEET_API_KEY"),
|
|
84
93
|
base_url: Optional[str] = None,
|
|
85
94
|
httpx_client: Optional[httpx.AsyncClient] = None,
|
|
95
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
96
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
86
97
|
):
|
|
87
|
-
self._httpx_client = httpx_client or
|
|
98
|
+
self._httpx_client = httpx_client or default_httpx_client(max_retries, timeout)
|
|
88
99
|
self.client = AsyncWrapper(
|
|
89
100
|
api_key=api_key,
|
|
90
101
|
base_url=base_url,
|
|
@@ -115,10 +126,14 @@ class AsyncFleet:
|
|
|
115
126
|
version = None
|
|
116
127
|
|
|
117
128
|
request = InstanceRequest(env_key=env_key_part, version=version, region=region)
|
|
129
|
+
region_base_url = REGION_BASE_URL.get(region)
|
|
118
130
|
response = await self.client.request(
|
|
119
|
-
"POST",
|
|
131
|
+
"POST",
|
|
132
|
+
"/v1/env/instances",
|
|
133
|
+
json=request.model_dump(),
|
|
134
|
+
base_url=region_base_url,
|
|
120
135
|
)
|
|
121
|
-
instance = AsyncEnvironment(**response.json())
|
|
136
|
+
instance = AsyncEnvironment(client=self.client, **response.json())
|
|
122
137
|
await instance.instance.load()
|
|
123
138
|
return instance
|
|
124
139
|
|
|
@@ -132,16 +147,16 @@ class AsyncFleet:
|
|
|
132
147
|
params["region"] = region
|
|
133
148
|
|
|
134
149
|
response = await self.client.request("GET", "/v1/env/instances", params=params)
|
|
135
|
-
return [
|
|
150
|
+
return [
|
|
151
|
+
AsyncEnvironment(client=self.client, **instance_data)
|
|
152
|
+
for instance_data in response.json()
|
|
153
|
+
]
|
|
136
154
|
|
|
137
155
|
async def instance(self, instance_id: str) -> AsyncEnvironment:
|
|
138
156
|
response = await self.client.request("GET", f"/v1/env/instances/{instance_id}")
|
|
139
|
-
instance = AsyncEnvironment(**response.json())
|
|
157
|
+
instance = AsyncEnvironment(client=self.client, **response.json())
|
|
140
158
|
await instance.instance.load()
|
|
141
159
|
return instance
|
|
142
160
|
|
|
143
161
|
async def delete(self, instance_id: str) -> InstanceRecord:
|
|
144
|
-
|
|
145
|
-
"DELETE", f"/v1/env/instances/{instance_id}"
|
|
146
|
-
)
|
|
147
|
-
return InstanceRecord(**response.json())
|
|
162
|
+
return await _delete_instance(self.client, instance_id)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Fleet SDK Exception Classes."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FleetError(Exception):
|
|
7
|
+
"""Base exception for all Fleet SDK errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
self.message = message
|
|
12
|
+
self.details = details or {}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FleetAPIError(FleetError):
|
|
16
|
+
"""Exception raised when Fleet API returns an error."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
message: str,
|
|
21
|
+
status_code: Optional[int] = None,
|
|
22
|
+
response_data: Optional[Dict[str, Any]] = None,
|
|
23
|
+
details: Optional[Dict[str, Any]] = None,
|
|
24
|
+
):
|
|
25
|
+
super().__init__(message, details)
|
|
26
|
+
self.status_code = status_code
|
|
27
|
+
self.response_data = response_data or {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FleetTimeoutError(FleetError):
|
|
31
|
+
"""Exception raised when a Fleet operation times out."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, message: str, timeout_duration: Optional[float] = None):
|
|
34
|
+
super().__init__(message)
|
|
35
|
+
self.timeout_duration = timeout_duration
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FleetAuthenticationError(FleetAPIError):
|
|
39
|
+
"""Exception raised when authentication fails."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, message: str = "Authentication failed"):
|
|
42
|
+
super().__init__(message, status_code=401)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class FleetRateLimitError(FleetAPIError):
|
|
46
|
+
"""Exception raised when rate limit is exceeded."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, message: str = "Rate limit exceeded"):
|
|
49
|
+
super().__init__(message, status_code=429)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class FleetInstanceLimitError(FleetAPIError):
|
|
53
|
+
"""Exception raised when team instance limit is exceeded."""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
message: str = "Instance limit exceeded",
|
|
58
|
+
running_instances: Optional[int] = None,
|
|
59
|
+
instance_limit: Optional[int] = None,
|
|
60
|
+
):
|
|
61
|
+
super().__init__(message, status_code=403)
|
|
62
|
+
self.running_instances = running_instances
|
|
63
|
+
self.instance_limit = instance_limit
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class FleetBadRequestError(FleetAPIError):
|
|
67
|
+
"""Exception raised for bad request errors (400)."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, message: str, error_type: Optional[str] = None):
|
|
70
|
+
super().__init__(message, status_code=400)
|
|
71
|
+
self.error_type = error_type
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class FleetPermissionError(FleetAPIError):
|
|
75
|
+
"""Exception raised when permission is denied (403)."""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
message: str,
|
|
80
|
+
resource_type: Optional[str] = None,
|
|
81
|
+
resource_id: Optional[str] = None,
|
|
82
|
+
):
|
|
83
|
+
super().__init__(message, status_code=403)
|
|
84
|
+
self.resource_type = resource_type
|
|
85
|
+
self.resource_id = resource_id
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class FleetRegionError(FleetBadRequestError):
|
|
89
|
+
"""Exception raised when an unsupported region is specified."""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
message: str,
|
|
94
|
+
region: Optional[str] = None,
|
|
95
|
+
supported_regions: Optional[list] = None,
|
|
96
|
+
):
|
|
97
|
+
super().__init__(message, error_type="unsupported_region")
|
|
98
|
+
self.region = region
|
|
99
|
+
self.supported_regions = supported_regions or []
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class FleetEnvironmentNotFoundError(FleetBadRequestError):
|
|
103
|
+
"""Exception raised when environment is not found."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, message: str, env_key: Optional[str] = None):
|
|
106
|
+
super().__init__(message, error_type="environment_not_found")
|
|
107
|
+
self.env_key = env_key
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class FleetVersionNotFoundError(FleetBadRequestError):
|
|
111
|
+
"""Exception raised when version is not found."""
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self, message: str, env_key: Optional[str] = None, version: Optional[str] = None
|
|
115
|
+
):
|
|
116
|
+
super().__init__(message, error_type="version_not_found")
|
|
117
|
+
self.env_key = env_key
|
|
118
|
+
self.version = version
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class FleetEnvironmentAccessError(FleetPermissionError):
|
|
122
|
+
"""Exception raised when team doesn't have access to an environment."""
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self, message: str, env_key: Optional[str] = None, version: Optional[str] = None
|
|
126
|
+
):
|
|
127
|
+
super().__init__(message, resource_type="environment", resource_id=env_key)
|
|
128
|
+
self.env_key = env_key
|
|
129
|
+
self.version = version
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class FleetTeamNotFoundError(FleetPermissionError):
|
|
133
|
+
"""Exception raised when team is not found."""
|
|
134
|
+
|
|
135
|
+
def __init__(self, message: str, team_id: Optional[str] = None):
|
|
136
|
+
super().__init__(message, resource_type="team", resource_id=team_id)
|
|
137
|
+
self.team_id = team_id
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class FleetEnvironmentError(FleetError):
|
|
141
|
+
"""Exception raised when environment operations fail."""
|
|
142
|
+
|
|
143
|
+
def __init__(self, message: str, environment_id: Optional[str] = None):
|
|
144
|
+
super().__init__(message)
|
|
145
|
+
self.environment_id = environment_id
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class FleetFacetError(FleetError):
|
|
149
|
+
"""Exception raised when facet operations fail."""
|
|
150
|
+
|
|
151
|
+
def __init__(self, message: str, facet_type: Optional[str] = None):
|
|
152
|
+
super().__init__(message)
|
|
153
|
+
self.facet_type = facet_type
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class FleetConfigurationError(FleetError):
|
|
157
|
+
"""Exception raised when configuration is invalid."""
|
|
158
|
+
|
|
159
|
+
def __init__(self, message: str, config_key: Optional[str] = None):
|
|
160
|
+
super().__init__(message)
|
|
161
|
+
self.config_key = config_key
|
|
@@ -1,7 +1,34 @@
|
|
|
1
1
|
import httpx
|
|
2
|
+
import httpx_retries
|
|
2
3
|
from typing import Dict, Any, Optional
|
|
3
4
|
|
|
4
5
|
|
|
6
|
+
def default_httpx_client(max_retries: int, timeout: float) -> httpx.AsyncClient:
|
|
7
|
+
if max_retries <= 0:
|
|
8
|
+
return httpx.AsyncClient(timeout=timeout)
|
|
9
|
+
|
|
10
|
+
policy = httpx_retries.Retry(
|
|
11
|
+
total=max_retries,
|
|
12
|
+
status_forcelist=[
|
|
13
|
+
404,
|
|
14
|
+
429,
|
|
15
|
+
500,
|
|
16
|
+
502,
|
|
17
|
+
503,
|
|
18
|
+
504,
|
|
19
|
+
],
|
|
20
|
+
allowed_methods=["GET", "POST", "PATCH", "DELETE"],
|
|
21
|
+
backoff_factor=0.5,
|
|
22
|
+
)
|
|
23
|
+
retry = httpx_retries.RetryTransport(
|
|
24
|
+
transport=httpx.AsyncHTTPTransport(retries=2), retry=policy
|
|
25
|
+
)
|
|
26
|
+
return httpx.AsyncClient(
|
|
27
|
+
timeout=timeout,
|
|
28
|
+
transport=retry,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
5
32
|
class BaseWrapper:
|
|
6
33
|
def __init__(self, *, url: str):
|
|
7
34
|
self.url = url
|
|
@@ -34,4 +61,4 @@ class AsyncWrapper(BaseWrapper):
|
|
|
34
61
|
params=params,
|
|
35
62
|
json=json,
|
|
36
63
|
**kwargs,
|
|
37
|
-
)
|
|
64
|
+
)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Fleet SDK Instance Client."""
|
|
2
2
|
|
|
3
3
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
4
|
-
import asyncio
|
|
5
4
|
import httpx
|
|
6
5
|
import inspect
|
|
7
6
|
import time
|
|
@@ -14,10 +13,11 @@ from ..resources.base import Resource
|
|
|
14
13
|
|
|
15
14
|
from fleet.verifiers import DatabaseSnapshot
|
|
16
15
|
|
|
17
|
-
from ..exceptions import FleetEnvironmentError
|
|
16
|
+
from ..exceptions import FleetEnvironmentError
|
|
17
|
+
from ...config import DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT
|
|
18
18
|
|
|
19
|
-
from .base import AsyncWrapper
|
|
20
|
-
from .models import (
|
|
19
|
+
from .base import AsyncWrapper, default_httpx_client
|
|
20
|
+
from ...instance.models import (
|
|
21
21
|
ResetRequest,
|
|
22
22
|
ResetResponse,
|
|
23
23
|
Resource as ResourceModel,
|
|
@@ -51,7 +51,7 @@ class AsyncInstanceClient:
|
|
|
51
51
|
self.base_url = url
|
|
52
52
|
self.client = AsyncWrapper(
|
|
53
53
|
url=self.base_url,
|
|
54
|
-
httpx_client=httpx_client or
|
|
54
|
+
httpx_client=httpx_client or default_httpx_client(DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT),
|
|
55
55
|
)
|
|
56
56
|
self._resources: Optional[List[ResourceModel]] = None
|
|
57
57
|
self._resources_state: Dict[str, Dict[str, Resource]] = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Any, List, Optional
|
|
2
|
-
from
|
|
3
|
-
from
|
|
2
|
+
from ...instance.models import Resource as ResourceModel
|
|
3
|
+
from ...instance.models import DescribeResponse, QueryRequest, QueryResponse
|
|
4
4
|
from .base import Resource
|
|
5
5
|
|
|
6
6
|
from typing import TYPE_CHECKING
|