fleet-python 0.2.10__py3-none-any.whl → 0.2.12__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.
Potentially problematic release.
This version of fleet-python might be problematic. Click here for more details.
- examples/example.py +3 -0
- fleet/_async/base.py +112 -21
- fleet/_async/client.py +28 -9
- fleet/_async/config.py +8 -0
- fleet/_async/env/client.py +4 -0
- fleet/_async/exceptions.py +91 -12
- fleet/_async/instance/base.py +25 -1
- fleet/_async/instance/client.py +4 -4
- fleet/base.py +112 -21
- fleet/client.py +28 -9
- fleet/config.py +8 -0
- fleet/env/__init__.py +12 -4
- fleet/env/client.py +4 -0
- fleet/exceptions.py +91 -12
- fleet/instance/base.py +25 -1
- fleet/instance/client.py +4 -3
- {fleet_python-0.2.10.dist-info → fleet_python-0.2.12.dist-info}/METADATA +2 -1
- {fleet_python-0.2.10.dist-info → fleet_python-0.2.12.dist-info}/RECORD +22 -20
- scripts/fix_sync_imports.py +4 -0
- {fleet_python-0.2.10.dist-info → fleet_python-0.2.12.dist-info}/WHEEL +0 -0
- {fleet_python-0.2.10.dist-info → fleet_python-0.2.12.dist-info}/licenses/LICENSE +0 -0
- {fleet_python-0.2.10.dist-info → fleet_python-0.2.12.dist-info}/top_level.txt +0 -0
examples/example.py
CHANGED
fleet/_async/base.py
CHANGED
|
@@ -9,6 +9,13 @@ from .exceptions import (
|
|
|
9
9
|
FleetRateLimitError,
|
|
10
10
|
FleetInstanceLimitError,
|
|
11
11
|
FleetTimeoutError,
|
|
12
|
+
FleetTeamNotFoundError,
|
|
13
|
+
FleetEnvironmentAccessError,
|
|
14
|
+
FleetRegionError,
|
|
15
|
+
FleetEnvironmentNotFoundError,
|
|
16
|
+
FleetVersionNotFoundError,
|
|
17
|
+
FleetBadRequestError,
|
|
18
|
+
FleetPermissionError,
|
|
12
19
|
)
|
|
13
20
|
|
|
14
21
|
|
|
@@ -47,86 +54,170 @@ class AsyncWrapper(BaseWrapper):
|
|
|
47
54
|
url: str,
|
|
48
55
|
params: Optional[Dict[str, Any]] = None,
|
|
49
56
|
json: Optional[Any] = None,
|
|
57
|
+
base_url: Optional[str] = None,
|
|
50
58
|
**kwargs,
|
|
51
59
|
) -> httpx.Response:
|
|
60
|
+
base_url = base_url or self.base_url
|
|
52
61
|
try:
|
|
53
62
|
response = await self.httpx_client.request(
|
|
54
63
|
method,
|
|
55
|
-
f"{
|
|
64
|
+
f"{base_url}{url}",
|
|
56
65
|
headers=self.get_headers(),
|
|
57
66
|
params=params,
|
|
58
67
|
json=json,
|
|
59
68
|
**kwargs,
|
|
60
69
|
)
|
|
61
|
-
|
|
70
|
+
|
|
62
71
|
# Check for HTTP errors
|
|
63
72
|
if response.status_code >= 400:
|
|
64
73
|
self._handle_error_response(response)
|
|
65
|
-
|
|
74
|
+
|
|
66
75
|
return response
|
|
67
76
|
except httpx.TimeoutException as e:
|
|
68
77
|
raise FleetTimeoutError(f"Request timed out: {str(e)}")
|
|
69
78
|
except httpx.RequestError as e:
|
|
70
79
|
raise FleetAPIError(f"Request failed: {str(e)}")
|
|
71
|
-
|
|
80
|
+
|
|
72
81
|
def _handle_error_response(self, response: httpx.Response) -> None:
|
|
73
82
|
"""Handle HTTP error responses and convert to appropriate Fleet exceptions."""
|
|
74
83
|
status_code = response.status_code
|
|
75
|
-
|
|
84
|
+
|
|
76
85
|
# Try to parse error response as JSON
|
|
77
86
|
try:
|
|
78
87
|
error_data = response.json()
|
|
79
88
|
detail = error_data.get("detail", response.text)
|
|
80
|
-
|
|
89
|
+
|
|
81
90
|
# Handle structured error responses
|
|
82
91
|
if isinstance(detail, dict):
|
|
83
92
|
error_type = detail.get("error_type", "")
|
|
84
93
|
error_message = detail.get("message", str(detail))
|
|
85
|
-
|
|
94
|
+
|
|
86
95
|
if error_type == "instance_limit_exceeded":
|
|
87
96
|
raise FleetInstanceLimitError(
|
|
88
97
|
error_message,
|
|
89
98
|
running_instances=detail.get("running_instances"),
|
|
90
|
-
instance_limit=detail.get("instance_limit")
|
|
99
|
+
instance_limit=detail.get("instance_limit"),
|
|
91
100
|
)
|
|
92
101
|
else:
|
|
93
102
|
error_message = detail.get("message", str(detail))
|
|
94
103
|
else:
|
|
95
104
|
error_message = detail
|
|
96
|
-
|
|
105
|
+
|
|
97
106
|
except (json.JSONDecodeError, ValueError):
|
|
98
107
|
error_message = response.text
|
|
99
108
|
error_data = None
|
|
100
|
-
|
|
109
|
+
|
|
101
110
|
# Handle specific error types
|
|
102
111
|
if status_code == 401:
|
|
103
112
|
raise FleetAuthenticationError(error_message)
|
|
104
|
-
elif status_code ==
|
|
105
|
-
#
|
|
113
|
+
elif status_code == 403:
|
|
114
|
+
# Handle 403 errors - instance limit, permissions, team not found
|
|
106
115
|
if "instance limit" in error_message.lower():
|
|
107
116
|
# Try to extract instance counts from the error message
|
|
108
117
|
running_instances = None
|
|
109
118
|
instance_limit = None
|
|
110
|
-
if
|
|
119
|
+
if (
|
|
120
|
+
"You have" in error_message
|
|
121
|
+
and "running instances out of a maximum of" in error_message
|
|
122
|
+
):
|
|
111
123
|
try:
|
|
112
124
|
# Extract numbers from message like "You have 5 running instances out of a maximum of 10"
|
|
113
|
-
parts = error_message.split("You have ")[1].split(
|
|
125
|
+
parts = error_message.split("You have ")[1].split(
|
|
126
|
+
" running instances out of a maximum of "
|
|
127
|
+
)
|
|
114
128
|
if len(parts) == 2:
|
|
115
129
|
running_instances = int(parts[0])
|
|
116
130
|
instance_limit = int(parts[1].split(".")[0])
|
|
117
131
|
except (IndexError, ValueError):
|
|
118
132
|
pass
|
|
119
|
-
|
|
133
|
+
|
|
120
134
|
raise FleetInstanceLimitError(
|
|
121
|
-
error_message,
|
|
122
|
-
running_instances=running_instances,
|
|
123
|
-
instance_limit=instance_limit
|
|
135
|
+
error_message,
|
|
136
|
+
running_instances=running_instances,
|
|
137
|
+
instance_limit=instance_limit,
|
|
124
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)
|
|
125
153
|
else:
|
|
126
|
-
raise
|
|
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)
|
|
127
218
|
else:
|
|
128
219
|
raise FleetAPIError(
|
|
129
220
|
error_message,
|
|
130
221
|
status_code=status_code,
|
|
131
|
-
response_data=error_data if
|
|
132
|
-
)
|
|
222
|
+
response_data=error_data if "error_data" in locals() else None,
|
|
223
|
+
)
|
fleet/_async/client.py
CHANGED
|
@@ -29,6 +29,8 @@ from .instance import (
|
|
|
29
29
|
ValidatorType,
|
|
30
30
|
ExecuteFunctionResponse,
|
|
31
31
|
)
|
|
32
|
+
from .config import DEFAULT_MAX_RETRIES, 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
|
|
@@ -37,15 +39,17 @@ logger = logging.getLogger(__name__)
|
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
class AsyncEnvironment(EnvironmentBase):
|
|
40
|
-
def __init__(self,
|
|
42
|
+
def __init__(self, client: AsyncWrapper, **kwargs):
|
|
41
43
|
super().__init__(**kwargs)
|
|
42
|
-
self.
|
|
44
|
+
self._client = client
|
|
43
45
|
self._instance: Optional[AsyncInstanceClient] = None
|
|
44
46
|
|
|
45
47
|
@property
|
|
46
48
|
def instance(self) -> AsyncInstanceClient:
|
|
47
49
|
if self._instance is None:
|
|
48
|
-
self._instance = AsyncInstanceClient(
|
|
50
|
+
self._instance = AsyncInstanceClient(
|
|
51
|
+
self.manager_url, self._client.httpx_client
|
|
52
|
+
)
|
|
49
53
|
return self._instance
|
|
50
54
|
|
|
51
55
|
async def reset(
|
|
@@ -66,7 +70,10 @@ class AsyncEnvironment(EnvironmentBase):
|
|
|
66
70
|
return await self.instance.resources()
|
|
67
71
|
|
|
68
72
|
async def close(self) -> InstanceRecord:
|
|
69
|
-
|
|
73
|
+
response = await self._client.request(
|
|
74
|
+
"DELETE", f"/v1/env/instances/{self.instance_id}"
|
|
75
|
+
)
|
|
76
|
+
return InstanceRecord(**response.json())
|
|
70
77
|
|
|
71
78
|
async def verify(self, validator: ValidatorType) -> ExecuteFunctionResponse:
|
|
72
79
|
return await self.instance.verify(validator)
|
|
@@ -83,8 +90,9 @@ class AsyncFleet:
|
|
|
83
90
|
api_key: Optional[str] = os.getenv("FLEET_API_KEY"),
|
|
84
91
|
base_url: Optional[str] = None,
|
|
85
92
|
httpx_client: Optional[httpx.AsyncClient] = None,
|
|
93
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
86
94
|
):
|
|
87
|
-
self._httpx_client = httpx_client or
|
|
95
|
+
self._httpx_client = httpx_client or default_httpx_client(max_retries)
|
|
88
96
|
self.client = AsyncWrapper(
|
|
89
97
|
api_key=api_key,
|
|
90
98
|
base_url=base_url,
|
|
@@ -95,6 +103,10 @@ class AsyncFleet:
|
|
|
95
103
|
response = await self.client.request("GET", "/v1/env/")
|
|
96
104
|
return [EnvironmentModel(**env_data) for env_data in response.json()]
|
|
97
105
|
|
|
106
|
+
async def list_regions(self) -> List[str]:
|
|
107
|
+
response = await self.client.request("GET", "/v1/regions")
|
|
108
|
+
return response.json()
|
|
109
|
+
|
|
98
110
|
async def environment(self, env_key: str) -> EnvironmentModel:
|
|
99
111
|
response = await self.client.request("GET", f"/v1/env/{env_key}")
|
|
100
112
|
return EnvironmentModel(**response.json())
|
|
@@ -111,10 +123,14 @@ class AsyncFleet:
|
|
|
111
123
|
version = None
|
|
112
124
|
|
|
113
125
|
request = InstanceRequest(env_key=env_key_part, version=version, region=region)
|
|
126
|
+
region_base_url = REGION_BASE_URL.get(region)
|
|
114
127
|
response = await self.client.request(
|
|
115
|
-
"POST",
|
|
128
|
+
"POST",
|
|
129
|
+
"/v1/env/instances",
|
|
130
|
+
json=request.model_dump(),
|
|
131
|
+
base_url=region_base_url,
|
|
116
132
|
)
|
|
117
|
-
instance = AsyncEnvironment(**response.json())
|
|
133
|
+
instance = AsyncEnvironment(client=self.client, **response.json())
|
|
118
134
|
await instance.instance.load()
|
|
119
135
|
return instance
|
|
120
136
|
|
|
@@ -128,11 +144,14 @@ class AsyncFleet:
|
|
|
128
144
|
params["region"] = region
|
|
129
145
|
|
|
130
146
|
response = await self.client.request("GET", "/v1/env/instances", params=params)
|
|
131
|
-
return [
|
|
147
|
+
return [
|
|
148
|
+
AsyncEnvironment(client=self.client, **instance_data)
|
|
149
|
+
for instance_data in response.json()
|
|
150
|
+
]
|
|
132
151
|
|
|
133
152
|
async def instance(self, instance_id: str) -> AsyncEnvironment:
|
|
134
153
|
response = await self.client.request("GET", f"/v1/env/instances/{instance_id}")
|
|
135
|
-
instance = AsyncEnvironment(**response.json())
|
|
154
|
+
instance = AsyncEnvironment(client=self.client, **response.json())
|
|
136
155
|
await instance.instance.load()
|
|
137
156
|
return instance
|
|
138
157
|
|
fleet/_async/config.py
ADDED
fleet/_async/env/client.py
CHANGED
|
@@ -11,6 +11,10 @@ async def list_envs_async() -> List[EnvironmentModel]:
|
|
|
11
11
|
return await AsyncFleet().list_envs()
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
async def list_regions_async() -> List[str]:
|
|
15
|
+
return await AsyncFleet().list_regions()
|
|
16
|
+
|
|
17
|
+
|
|
14
18
|
async def list_instances_async(
|
|
15
19
|
status: Optional[str] = None, region: Optional[str] = None
|
|
16
20
|
) -> List[AsyncEnvironment]:
|
fleet/_async/exceptions.py
CHANGED
|
@@ -5,7 +5,7 @@ from typing import Any, Dict, Optional
|
|
|
5
5
|
|
|
6
6
|
class FleetError(Exception):
|
|
7
7
|
"""Base exception for all Fleet SDK errors."""
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
|
10
10
|
super().__init__(message)
|
|
11
11
|
self.message = message
|
|
@@ -14,7 +14,7 @@ class FleetError(Exception):
|
|
|
14
14
|
|
|
15
15
|
class FleetAPIError(FleetError):
|
|
16
16
|
"""Exception raised when Fleet API returns an error."""
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
def __init__(
|
|
19
19
|
self,
|
|
20
20
|
message: str,
|
|
@@ -29,7 +29,7 @@ class FleetAPIError(FleetError):
|
|
|
29
29
|
|
|
30
30
|
class FleetTimeoutError(FleetError):
|
|
31
31
|
"""Exception raised when a Fleet operation times out."""
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
def __init__(self, message: str, timeout_duration: Optional[float] = None):
|
|
34
34
|
super().__init__(message)
|
|
35
35
|
self.timeout_duration = timeout_duration
|
|
@@ -37,30 +37,109 @@ class FleetTimeoutError(FleetError):
|
|
|
37
37
|
|
|
38
38
|
class FleetAuthenticationError(FleetAPIError):
|
|
39
39
|
"""Exception raised when authentication fails."""
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
def __init__(self, message: str = "Authentication failed"):
|
|
42
42
|
super().__init__(message, status_code=401)
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
class FleetRateLimitError(FleetAPIError):
|
|
46
46
|
"""Exception raised when rate limit is exceeded."""
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
def __init__(self, message: str = "Rate limit exceeded"):
|
|
49
49
|
super().__init__(message, status_code=429)
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
class FleetInstanceLimitError(FleetAPIError):
|
|
53
53
|
"""Exception raised when team instance limit is exceeded."""
|
|
54
|
-
|
|
55
|
-
def __init__(
|
|
56
|
-
|
|
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)
|
|
57
62
|
self.running_instances = running_instances
|
|
58
63
|
self.instance_limit = instance_limit
|
|
59
64
|
|
|
60
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
|
+
|
|
61
140
|
class FleetEnvironmentError(FleetError):
|
|
62
141
|
"""Exception raised when environment operations fail."""
|
|
63
|
-
|
|
142
|
+
|
|
64
143
|
def __init__(self, message: str, environment_id: Optional[str] = None):
|
|
65
144
|
super().__init__(message)
|
|
66
145
|
self.environment_id = environment_id
|
|
@@ -68,7 +147,7 @@ class FleetEnvironmentError(FleetError):
|
|
|
68
147
|
|
|
69
148
|
class FleetFacetError(FleetError):
|
|
70
149
|
"""Exception raised when facet operations fail."""
|
|
71
|
-
|
|
150
|
+
|
|
72
151
|
def __init__(self, message: str, facet_type: Optional[str] = None):
|
|
73
152
|
super().__init__(message)
|
|
74
153
|
self.facet_type = facet_type
|
|
@@ -76,7 +155,7 @@ class FleetFacetError(FleetError):
|
|
|
76
155
|
|
|
77
156
|
class FleetConfigurationError(FleetError):
|
|
78
157
|
"""Exception raised when configuration is invalid."""
|
|
79
|
-
|
|
158
|
+
|
|
80
159
|
def __init__(self, message: str, config_key: Optional[str] = None):
|
|
81
160
|
super().__init__(message)
|
|
82
|
-
self.config_key = config_key
|
|
161
|
+
self.config_key = config_key
|
fleet/_async/instance/base.py
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
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) -> httpx.AsyncClient:
|
|
7
|
+
policy = httpx_retries.Retry(
|
|
8
|
+
total=max_retries,
|
|
9
|
+
status_forcelist=[
|
|
10
|
+
404,
|
|
11
|
+
429,
|
|
12
|
+
500,
|
|
13
|
+
502,
|
|
14
|
+
503,
|
|
15
|
+
504,
|
|
16
|
+
],
|
|
17
|
+
allowed_methods=["GET", "POST", "PATCH", "DELETE"],
|
|
18
|
+
backoff_factor=0.5,
|
|
19
|
+
)
|
|
20
|
+
retry = httpx_retries.RetryTransport(
|
|
21
|
+
transport=httpx.AsyncHTTPTransport(retries=2), retry=policy
|
|
22
|
+
)
|
|
23
|
+
return httpx.AsyncClient(
|
|
24
|
+
timeout=300.0,
|
|
25
|
+
transport=retry,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
5
29
|
class BaseWrapper:
|
|
6
30
|
def __init__(self, *, url: str):
|
|
7
31
|
self.url = url
|
|
@@ -34,4 +58,4 @@ class AsyncWrapper(BaseWrapper):
|
|
|
34
58
|
params=params,
|
|
35
59
|
json=json,
|
|
36
60
|
**kwargs,
|
|
37
|
-
)
|
|
61
|
+
)
|
fleet/_async/instance/client.py
CHANGED
|
@@ -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,9 +13,10 @@ 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
|
|
18
18
|
|
|
19
|
-
from .base import AsyncWrapper
|
|
19
|
+
from .base import AsyncWrapper, default_httpx_client
|
|
20
20
|
from .models import (
|
|
21
21
|
ResetRequest,
|
|
22
22
|
ResetResponse,
|
|
@@ -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),
|
|
55
55
|
)
|
|
56
56
|
self._resources: Optional[List[ResourceModel]] = None
|
|
57
57
|
self._resources_state: Dict[str, Dict[str, Resource]] = {
|
fleet/base.py
CHANGED
|
@@ -9,6 +9,13 @@ from .exceptions import (
|
|
|
9
9
|
FleetRateLimitError,
|
|
10
10
|
FleetInstanceLimitError,
|
|
11
11
|
FleetTimeoutError,
|
|
12
|
+
FleetTeamNotFoundError,
|
|
13
|
+
FleetEnvironmentAccessError,
|
|
14
|
+
FleetRegionError,
|
|
15
|
+
FleetEnvironmentNotFoundError,
|
|
16
|
+
FleetVersionNotFoundError,
|
|
17
|
+
FleetBadRequestError,
|
|
18
|
+
FleetPermissionError,
|
|
12
19
|
)
|
|
13
20
|
|
|
14
21
|
|
|
@@ -47,86 +54,170 @@ class SyncWrapper(BaseWrapper):
|
|
|
47
54
|
url: str,
|
|
48
55
|
params: Optional[Dict[str, Any]] = None,
|
|
49
56
|
json: Optional[Any] = None,
|
|
57
|
+
base_url: Optional[str] = None,
|
|
50
58
|
**kwargs,
|
|
51
59
|
) -> httpx.Response:
|
|
60
|
+
base_url = base_url or self.base_url
|
|
52
61
|
try:
|
|
53
62
|
response = self.httpx_client.request(
|
|
54
63
|
method,
|
|
55
|
-
f"{
|
|
64
|
+
f"{base_url}{url}",
|
|
56
65
|
headers=self.get_headers(),
|
|
57
66
|
params=params,
|
|
58
67
|
json=json,
|
|
59
68
|
**kwargs,
|
|
60
69
|
)
|
|
61
|
-
|
|
70
|
+
|
|
62
71
|
# Check for HTTP errors
|
|
63
72
|
if response.status_code >= 400:
|
|
64
73
|
self._handle_error_response(response)
|
|
65
|
-
|
|
74
|
+
|
|
66
75
|
return response
|
|
67
76
|
except httpx.TimeoutException as e:
|
|
68
77
|
raise FleetTimeoutError(f"Request timed out: {str(e)}")
|
|
69
78
|
except httpx.RequestError as e:
|
|
70
79
|
raise FleetAPIError(f"Request failed: {str(e)}")
|
|
71
|
-
|
|
80
|
+
|
|
72
81
|
def _handle_error_response(self, response: httpx.Response) -> None:
|
|
73
82
|
"""Handle HTTP error responses and convert to appropriate Fleet exceptions."""
|
|
74
83
|
status_code = response.status_code
|
|
75
|
-
|
|
84
|
+
|
|
76
85
|
# Try to parse error response as JSON
|
|
77
86
|
try:
|
|
78
87
|
error_data = response.json()
|
|
79
88
|
detail = error_data.get("detail", response.text)
|
|
80
|
-
|
|
89
|
+
|
|
81
90
|
# Handle structured error responses
|
|
82
91
|
if isinstance(detail, dict):
|
|
83
92
|
error_type = detail.get("error_type", "")
|
|
84
93
|
error_message = detail.get("message", str(detail))
|
|
85
|
-
|
|
94
|
+
|
|
86
95
|
if error_type == "instance_limit_exceeded":
|
|
87
96
|
raise FleetInstanceLimitError(
|
|
88
97
|
error_message,
|
|
89
98
|
running_instances=detail.get("running_instances"),
|
|
90
|
-
instance_limit=detail.get("instance_limit")
|
|
99
|
+
instance_limit=detail.get("instance_limit"),
|
|
91
100
|
)
|
|
92
101
|
else:
|
|
93
102
|
error_message = detail.get("message", str(detail))
|
|
94
103
|
else:
|
|
95
104
|
error_message = detail
|
|
96
|
-
|
|
105
|
+
|
|
97
106
|
except (json.JSONDecodeError, ValueError):
|
|
98
107
|
error_message = response.text
|
|
99
108
|
error_data = None
|
|
100
|
-
|
|
109
|
+
|
|
101
110
|
# Handle specific error types
|
|
102
111
|
if status_code == 401:
|
|
103
112
|
raise FleetAuthenticationError(error_message)
|
|
104
|
-
elif status_code ==
|
|
105
|
-
#
|
|
113
|
+
elif status_code == 403:
|
|
114
|
+
# Handle 403 errors - instance limit, permissions, team not found
|
|
106
115
|
if "instance limit" in error_message.lower():
|
|
107
116
|
# Try to extract instance counts from the error message
|
|
108
117
|
running_instances = None
|
|
109
118
|
instance_limit = None
|
|
110
|
-
if
|
|
119
|
+
if (
|
|
120
|
+
"You have" in error_message
|
|
121
|
+
and "running instances out of a maximum of" in error_message
|
|
122
|
+
):
|
|
111
123
|
try:
|
|
112
124
|
# Extract numbers from message like "You have 5 running instances out of a maximum of 10"
|
|
113
|
-
parts = error_message.split("You have ")[1].split(
|
|
125
|
+
parts = error_message.split("You have ")[1].split(
|
|
126
|
+
" running instances out of a maximum of "
|
|
127
|
+
)
|
|
114
128
|
if len(parts) == 2:
|
|
115
129
|
running_instances = int(parts[0])
|
|
116
130
|
instance_limit = int(parts[1].split(".")[0])
|
|
117
131
|
except (IndexError, ValueError):
|
|
118
132
|
pass
|
|
119
|
-
|
|
133
|
+
|
|
120
134
|
raise FleetInstanceLimitError(
|
|
121
|
-
error_message,
|
|
122
|
-
running_instances=running_instances,
|
|
123
|
-
instance_limit=instance_limit
|
|
135
|
+
error_message,
|
|
136
|
+
running_instances=running_instances,
|
|
137
|
+
instance_limit=instance_limit,
|
|
124
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)
|
|
125
153
|
else:
|
|
126
|
-
raise
|
|
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)
|
|
127
218
|
else:
|
|
128
219
|
raise FleetAPIError(
|
|
129
220
|
error_message,
|
|
130
221
|
status_code=status_code,
|
|
131
|
-
response_data=error_data if
|
|
132
|
-
)
|
|
222
|
+
response_data=error_data if "error_data" in locals() else None,
|
|
223
|
+
)
|
fleet/client.py
CHANGED
|
@@ -29,6 +29,8 @@ from .instance import (
|
|
|
29
29
|
ValidatorType,
|
|
30
30
|
ExecuteFunctionResponse,
|
|
31
31
|
)
|
|
32
|
+
from .config import DEFAULT_MAX_RETRIES, REGION_BASE_URL
|
|
33
|
+
from .instance.base import default_httpx_client
|
|
32
34
|
from .resources.base import Resource
|
|
33
35
|
from .resources.sqlite import SQLiteResource
|
|
34
36
|
from .resources.browser import BrowserResource
|
|
@@ -37,15 +39,17 @@ logger = logging.getLogger(__name__)
|
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
class Environment(EnvironmentBase):
|
|
40
|
-
def __init__(self,
|
|
42
|
+
def __init__(self, client: SyncWrapper, **kwargs):
|
|
41
43
|
super().__init__(**kwargs)
|
|
42
|
-
self.
|
|
44
|
+
self._client = client
|
|
43
45
|
self._instance: Optional[InstanceClient] = None
|
|
44
46
|
|
|
45
47
|
@property
|
|
46
48
|
def instance(self) -> InstanceClient:
|
|
47
49
|
if self._instance is None:
|
|
48
|
-
self._instance = InstanceClient(
|
|
50
|
+
self._instance = InstanceClient(
|
|
51
|
+
self.manager_url, self._client.httpx_client
|
|
52
|
+
)
|
|
49
53
|
return self._instance
|
|
50
54
|
|
|
51
55
|
def reset(
|
|
@@ -66,7 +70,10 @@ class Environment(EnvironmentBase):
|
|
|
66
70
|
return self.instance.resources()
|
|
67
71
|
|
|
68
72
|
def close(self) -> InstanceRecord:
|
|
69
|
-
|
|
73
|
+
response = self._client.request(
|
|
74
|
+
"DELETE", f"/v1/env/instances/{self.instance_id}"
|
|
75
|
+
)
|
|
76
|
+
return InstanceRecord(**response.json())
|
|
70
77
|
|
|
71
78
|
def verify(self, validator: ValidatorType) -> ExecuteFunctionResponse:
|
|
72
79
|
return self.instance.verify(validator)
|
|
@@ -83,8 +90,9 @@ class Fleet:
|
|
|
83
90
|
api_key: Optional[str] = os.getenv("FLEET_API_KEY"),
|
|
84
91
|
base_url: Optional[str] = None,
|
|
85
92
|
httpx_client: Optional[httpx.Client] = None,
|
|
93
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
86
94
|
):
|
|
87
|
-
self._httpx_client = httpx_client or
|
|
95
|
+
self._httpx_client = httpx_client or default_httpx_client(max_retries)
|
|
88
96
|
self.client = SyncWrapper(
|
|
89
97
|
api_key=api_key,
|
|
90
98
|
base_url=base_url,
|
|
@@ -95,6 +103,10 @@ class Fleet:
|
|
|
95
103
|
response = self.client.request("GET", "/v1/env/")
|
|
96
104
|
return [EnvironmentModel(**env_data) for env_data in response.json()]
|
|
97
105
|
|
|
106
|
+
def list_regions(self) -> List[str]:
|
|
107
|
+
response = self.client.request("GET", "/v1/regions")
|
|
108
|
+
return response.json()
|
|
109
|
+
|
|
98
110
|
def environment(self, env_key: str) -> EnvironmentModel:
|
|
99
111
|
response = self.client.request("GET", f"/v1/env/{env_key}")
|
|
100
112
|
return EnvironmentModel(**response.json())
|
|
@@ -111,10 +123,14 @@ class Fleet:
|
|
|
111
123
|
version = None
|
|
112
124
|
|
|
113
125
|
request = InstanceRequest(env_key=env_key_part, version=version, region=region)
|
|
126
|
+
region_base_url = REGION_BASE_URL.get(region)
|
|
114
127
|
response = self.client.request(
|
|
115
|
-
"POST",
|
|
128
|
+
"POST",
|
|
129
|
+
"/v1/env/instances",
|
|
130
|
+
json=request.model_dump(),
|
|
131
|
+
base_url=region_base_url,
|
|
116
132
|
)
|
|
117
|
-
instance = Environment(**response.json())
|
|
133
|
+
instance = Environment(client=self.client, **response.json())
|
|
118
134
|
instance.instance.load()
|
|
119
135
|
return instance
|
|
120
136
|
|
|
@@ -128,11 +144,14 @@ class Fleet:
|
|
|
128
144
|
params["region"] = region
|
|
129
145
|
|
|
130
146
|
response = self.client.request("GET", "/v1/env/instances", params=params)
|
|
131
|
-
return [
|
|
147
|
+
return [
|
|
148
|
+
Environment(client=self.client, **instance_data)
|
|
149
|
+
for instance_data in response.json()
|
|
150
|
+
]
|
|
132
151
|
|
|
133
152
|
def instance(self, instance_id: str) -> Environment:
|
|
134
153
|
response = self.client.request("GET", f"/v1/env/instances/{instance_id}")
|
|
135
|
-
instance = Environment(**response.json())
|
|
154
|
+
instance = Environment(client=self.client, **response.json())
|
|
136
155
|
instance.instance.load()
|
|
137
156
|
return instance
|
|
138
157
|
|
fleet/config.py
ADDED
fleet/env/__init__.py
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
"""Fleet env module - convenience functions for environment management."""
|
|
2
2
|
|
|
3
|
-
from .client import make, list_envs, get, list_instances
|
|
3
|
+
from .client import make, list_envs, list_regions, get, list_instances
|
|
4
4
|
|
|
5
5
|
# Import async versions from _async
|
|
6
|
-
from .._async.env.client import
|
|
6
|
+
from .._async.env.client import (
|
|
7
|
+
make_async,
|
|
8
|
+
list_envs_async,
|
|
9
|
+
list_regions_async,
|
|
10
|
+
get_async,
|
|
11
|
+
list_instances_async,
|
|
12
|
+
)
|
|
7
13
|
|
|
8
14
|
__all__ = [
|
|
9
15
|
"make",
|
|
10
|
-
"list_envs",
|
|
16
|
+
"list_envs",
|
|
17
|
+
"list_regions",
|
|
11
18
|
"list_instances",
|
|
12
19
|
"get",
|
|
13
20
|
"make_async",
|
|
14
21
|
"list_envs_async",
|
|
22
|
+
"list_regions_async",
|
|
15
23
|
"list_instances_async",
|
|
16
24
|
"get_async",
|
|
17
|
-
]
|
|
25
|
+
]
|
fleet/env/client.py
CHANGED
|
@@ -11,6 +11,10 @@ def list_envs() -> List[EnvironmentModel]:
|
|
|
11
11
|
return Fleet().list_envs()
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def list_regions() -> List[str]:
|
|
15
|
+
return Fleet().list_regions()
|
|
16
|
+
|
|
17
|
+
|
|
14
18
|
def list_instances(
|
|
15
19
|
status: Optional[str] = None, region: Optional[str] = None
|
|
16
20
|
) -> List[Environment]:
|
fleet/exceptions.py
CHANGED
|
@@ -5,7 +5,7 @@ from typing import Any, Dict, Optional
|
|
|
5
5
|
|
|
6
6
|
class FleetError(Exception):
|
|
7
7
|
"""Base exception for all Fleet SDK errors."""
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
|
10
10
|
super().__init__(message)
|
|
11
11
|
self.message = message
|
|
@@ -14,7 +14,7 @@ class FleetError(Exception):
|
|
|
14
14
|
|
|
15
15
|
class FleetAPIError(FleetError):
|
|
16
16
|
"""Exception raised when Fleet API returns an error."""
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
def __init__(
|
|
19
19
|
self,
|
|
20
20
|
message: str,
|
|
@@ -29,7 +29,7 @@ class FleetAPIError(FleetError):
|
|
|
29
29
|
|
|
30
30
|
class FleetTimeoutError(FleetError):
|
|
31
31
|
"""Exception raised when a Fleet operation times out."""
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
def __init__(self, message: str, timeout_duration: Optional[float] = None):
|
|
34
34
|
super().__init__(message)
|
|
35
35
|
self.timeout_duration = timeout_duration
|
|
@@ -37,30 +37,109 @@ class FleetTimeoutError(FleetError):
|
|
|
37
37
|
|
|
38
38
|
class FleetAuthenticationError(FleetAPIError):
|
|
39
39
|
"""Exception raised when authentication fails."""
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
def __init__(self, message: str = "Authentication failed"):
|
|
42
42
|
super().__init__(message, status_code=401)
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
class FleetRateLimitError(FleetAPIError):
|
|
46
46
|
"""Exception raised when rate limit is exceeded."""
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
def __init__(self, message: str = "Rate limit exceeded"):
|
|
49
49
|
super().__init__(message, status_code=429)
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
class FleetInstanceLimitError(FleetAPIError):
|
|
53
53
|
"""Exception raised when team instance limit is exceeded."""
|
|
54
|
-
|
|
55
|
-
def __init__(
|
|
56
|
-
|
|
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)
|
|
57
62
|
self.running_instances = running_instances
|
|
58
63
|
self.instance_limit = instance_limit
|
|
59
64
|
|
|
60
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
|
+
|
|
61
140
|
class FleetEnvironmentError(FleetError):
|
|
62
141
|
"""Exception raised when environment operations fail."""
|
|
63
|
-
|
|
142
|
+
|
|
64
143
|
def __init__(self, message: str, environment_id: Optional[str] = None):
|
|
65
144
|
super().__init__(message)
|
|
66
145
|
self.environment_id = environment_id
|
|
@@ -68,7 +147,7 @@ class FleetEnvironmentError(FleetError):
|
|
|
68
147
|
|
|
69
148
|
class FleetFacetError(FleetError):
|
|
70
149
|
"""Exception raised when facet operations fail."""
|
|
71
|
-
|
|
150
|
+
|
|
72
151
|
def __init__(self, message: str, facet_type: Optional[str] = None):
|
|
73
152
|
super().__init__(message)
|
|
74
153
|
self.facet_type = facet_type
|
|
@@ -76,7 +155,7 @@ class FleetFacetError(FleetError):
|
|
|
76
155
|
|
|
77
156
|
class FleetConfigurationError(FleetError):
|
|
78
157
|
"""Exception raised when configuration is invalid."""
|
|
79
|
-
|
|
158
|
+
|
|
80
159
|
def __init__(self, message: str, config_key: Optional[str] = None):
|
|
81
160
|
super().__init__(message)
|
|
82
|
-
self.config_key = config_key
|
|
161
|
+
self.config_key = config_key
|
fleet/instance/base.py
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
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) -> httpx.Client:
|
|
7
|
+
policy = httpx_retries.Retry(
|
|
8
|
+
total=max_retries,
|
|
9
|
+
status_forcelist=[
|
|
10
|
+
404,
|
|
11
|
+
429,
|
|
12
|
+
500,
|
|
13
|
+
502,
|
|
14
|
+
503,
|
|
15
|
+
504,
|
|
16
|
+
],
|
|
17
|
+
allowed_methods=["GET", "POST", "PATCH", "DELETE"],
|
|
18
|
+
backoff_factor=0.5,
|
|
19
|
+
)
|
|
20
|
+
retry = httpx_retries.RetryTransport(
|
|
21
|
+
transport=httpx.HTTPTransport(retries=2), retry=policy
|
|
22
|
+
)
|
|
23
|
+
return httpx.Client(
|
|
24
|
+
timeout=300.0,
|
|
25
|
+
transport=retry,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
5
29
|
class BaseWrapper:
|
|
6
30
|
def __init__(self, *, url: str):
|
|
7
31
|
self.url = url
|
|
@@ -34,4 +58,4 @@ class SyncWrapper(BaseWrapper):
|
|
|
34
58
|
params=params,
|
|
35
59
|
json=json,
|
|
36
60
|
**kwargs,
|
|
37
|
-
)
|
|
61
|
+
)
|
fleet/instance/client.py
CHANGED
|
@@ -13,9 +13,10 @@ from ..resources.base import Resource
|
|
|
13
13
|
|
|
14
14
|
from ..verifiers import DatabaseSnapshot
|
|
15
15
|
|
|
16
|
-
from ..exceptions import FleetEnvironmentError
|
|
16
|
+
from ..exceptions import FleetEnvironmentError
|
|
17
|
+
from ..config import DEFAULT_MAX_RETRIES
|
|
17
18
|
|
|
18
|
-
from .base import SyncWrapper
|
|
19
|
+
from .base import SyncWrapper, default_httpx_client
|
|
19
20
|
from .models import (
|
|
20
21
|
ResetRequest,
|
|
21
22
|
ResetResponse,
|
|
@@ -50,7 +51,7 @@ class InstanceClient:
|
|
|
50
51
|
self.base_url = url
|
|
51
52
|
self.client = SyncWrapper(
|
|
52
53
|
url=self.base_url,
|
|
53
|
-
httpx_client=httpx_client or
|
|
54
|
+
httpx_client=httpx_client or default_httpx_client(DEFAULT_MAX_RETRIES),
|
|
54
55
|
)
|
|
55
56
|
self._resources: Optional[List[ResourceModel]] = None
|
|
56
57
|
self._resources_state: Dict[str, Dict[str, Resource]] = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fleet-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.12
|
|
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"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
examples/dsl_example.py,sha256=3Eu5924a8x61nuSGXqGz8XjPLNKKH8Ye7lSYHSvixtk,5361
|
|
2
|
-
examples/example.py,sha256=
|
|
2
|
+
examples/example.py,sha256=62rjLU3LdGEW9QJZjMP44Gtdjalv-6oRnClDFk67Oec,1089
|
|
3
3
|
examples/example_client.py,sha256=70HKEhz_Gb79YcvKQauCPdS08AAwjo9unt2dh1jN_Oo,1030
|
|
4
4
|
examples/example_sync.py,sha256=_XaM3E0osxdt7AJQrJdtgE_I4-ChWuANQ5HIxCisbJk,983
|
|
5
5
|
examples/gemini_example.py,sha256=8mDXGGCaodyK6uXgpWhxi-DQ5OA-GFW12Gfwh0b3EDY,16177
|
|
@@ -9,32 +9,34 @@ examples/openai_example.py,sha256=I2vk_SJN9BkSRQCYRJfbtGJ-HJ2xzQj-lOjwqmLos5M,82
|
|
|
9
9
|
examples/openai_simple_example.py,sha256=I42ytIwv0INgDO39pp1MOQSqsJz2YYH8GeNNBaUtq3A,1748
|
|
10
10
|
examples/quickstart.py,sha256=1VT39IRRhemsJgxi0O0gprdpcw7HB4pYO97GAYagIcg,3788
|
|
11
11
|
fleet/__init__.py,sha256=-EFlLzHmyJIUGSZ4_XIts6OhXIXGPyu5PgI9JxgDzTg,2165
|
|
12
|
-
fleet/base.py,sha256=
|
|
13
|
-
fleet/client.py,sha256=
|
|
14
|
-
fleet/
|
|
12
|
+
fleet/base.py,sha256=E4gUv_eQEU4eSzTBy8gLGwl0s4t57OCTnsMYTKP3qI0,8782
|
|
13
|
+
fleet/client.py,sha256=5Cf9eGO_PaJaG_Um8WDo2b_Hv8HNZcPtuJ971e25QQU,5509
|
|
14
|
+
fleet/config.py,sha256=9JoDqeAOV7JPcivKSUsxm0EghAY8tFUzU8NyCH4SR1E,250
|
|
15
|
+
fleet/exceptions.py,sha256=fUmPwWhnT8SR97lYsRq0kLHQHKtSh2eJS0VQ2caSzEI,5055
|
|
15
16
|
fleet/models.py,sha256=Jf6Zmk689TPXhTSnVENK_VCw0VsujWzEWsN3T29MQ0k,3713
|
|
16
17
|
fleet/playwright.py,sha256=BmRvez5DUa0ttAQB084hPAyt9_8WxdzCGBGF-GZbTuQ,8593
|
|
17
18
|
fleet/_async/__init__.py,sha256=AJWCnuo7XKja4yBb8fK2wX7ntciLXQrpzdRHwjTRP6M,62
|
|
18
|
-
fleet/_async/base.py,sha256=
|
|
19
|
-
fleet/_async/client.py,sha256=
|
|
20
|
-
fleet/_async/
|
|
19
|
+
fleet/_async/base.py,sha256=c84QR8NBXeRjmdXkCFrK0cvpAiE3VeJm2ELysg4hL-o,8800
|
|
20
|
+
fleet/_async/client.py,sha256=5AiyAethOoNH19r1tJU63ANlKQ9AiuHLZyZqFtA7YIY,5753
|
|
21
|
+
fleet/_async/config.py,sha256=9JoDqeAOV7JPcivKSUsxm0EghAY8tFUzU8NyCH4SR1E,250
|
|
22
|
+
fleet/_async/exceptions.py,sha256=fUmPwWhnT8SR97lYsRq0kLHQHKtSh2eJS0VQ2caSzEI,5055
|
|
21
23
|
fleet/_async/models.py,sha256=Jf6Zmk689TPXhTSnVENK_VCw0VsujWzEWsN3T29MQ0k,3713
|
|
22
24
|
fleet/_async/playwright.py,sha256=2r4ywuv2ZqT0Qu3-k8A7V4YijeAOHnN8HiqJreLEYGI,8924
|
|
23
25
|
fleet/_async/env/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
-
fleet/_async/env/client.py,sha256=
|
|
26
|
+
fleet/_async/env/client.py,sha256=lBLcuEeQ9B2JMp6xUPb7ANgBYl9XTJMoVaOzGW7s9Pk,784
|
|
25
27
|
fleet/_async/instance/__init__.py,sha256=jIt-7EEJ0WM_ipheT_s0lniCbLei6yUdN0qQv1bMJ3E,524
|
|
26
|
-
fleet/_async/instance/base.py,sha256=
|
|
27
|
-
fleet/_async/instance/client.py,sha256=
|
|
28
|
+
fleet/_async/instance/base.py,sha256=wL-Lb33QjwKvBsK1borBg9jLIwCjEOh0il8XqFDfo7w,1531
|
|
29
|
+
fleet/_async/instance/client.py,sha256=S2MBIz4cilNm8u3yJWWIo8n5y8QE7we7COE74ykNxjk,5696
|
|
28
30
|
fleet/_async/instance/models.py,sha256=ZTiue0YOuhuwX8jYfJAoCzGfqjLqqXRLqK1LVFhq6rQ,4183
|
|
29
31
|
fleet/_async/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
32
|
fleet/_async/resources/base.py,sha256=203gD54NP1IvjuSqFo-f7FvrkhtjChggtzrxJK7xf2E,667
|
|
31
33
|
fleet/_async/resources/browser.py,sha256=x11y4aKHogIEv83FByHtExerjV-cDWI3U62349Guq_Q,1368
|
|
32
34
|
fleet/_async/resources/sqlite.py,sha256=sRiII_qJ8X6-FSemlBsXThz4ZPjkNy9wDT8g5UAz2XM,1501
|
|
33
|
-
fleet/env/__init__.py,sha256=
|
|
34
|
-
fleet/env/client.py,sha256=
|
|
35
|
+
fleet/env/__init__.py,sha256=yYk657kVDnMWYbuPS_2o0kpVC99AYD85o5WG1jCaWSY,531
|
|
36
|
+
fleet/env/client.py,sha256=HwJ-xCg3oZMJ48nP9_EaHk4Rao-na9myPX9X5JxYMbo,644
|
|
35
37
|
fleet/instance/__init__.py,sha256=Hr8xPPoqzKOViXZXWmaL6dQ7NOBn-GooTGzoIvGmiE4,514
|
|
36
|
-
fleet/instance/base.py,sha256=
|
|
37
|
-
fleet/instance/client.py,sha256=
|
|
38
|
+
fleet/instance/base.py,sha256=MMq3IoXFT5dl3lTxA0qzhw12OEe43b3h7qCQdUR1yvk,1498
|
|
39
|
+
fleet/instance/client.py,sha256=KBVIojg5-OfRWf0MkHeuSt3C3NyZNhDtHKJmJoLuQs8,5530
|
|
38
40
|
fleet/instance/models.py,sha256=ZTiue0YOuhuwX8jYfJAoCzGfqjLqqXRLqK1LVFhq6rQ,4183
|
|
39
41
|
fleet/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
42
|
fleet/resources/base.py,sha256=203gD54NP1IvjuSqFo-f7FvrkhtjChggtzrxJK7xf2E,667
|
|
@@ -44,10 +46,10 @@ fleet/verifiers/__init__.py,sha256=mRMN8x0gDWFJ1MRLqdBtQw0gn_q8kDV3lMLyoiEf1yY,2
|
|
|
44
46
|
fleet/verifiers/code.py,sha256=NJ4OLZnpqLkI1lXY7-5m2GuZklLxMzHUCnRMVyN2_OI,25
|
|
45
47
|
fleet/verifiers/db.py,sha256=tssmvJjDHuBIy8qlL_P5-UdmEFUw2DZcqLsWZ8ot3Xw,27766
|
|
46
48
|
fleet/verifiers/sql_differ.py,sha256=dmiGCFXVMEMbAX519OjhVqgA8ZvhnvdmC1BVpL7QCF0,6490
|
|
47
|
-
fleet_python-0.2.
|
|
48
|
-
scripts/fix_sync_imports.py,sha256=
|
|
49
|
+
fleet_python-0.2.12.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
50
|
+
scripts/fix_sync_imports.py,sha256=nIPK0xIIuH17sOa-SI1QPGGWkz5K1gBxuL1rTN_5PFU,3103
|
|
49
51
|
scripts/unasync.py,sha256=--Fmaae47o-dZ1HYgX1c3Nvi-rMjcFymTRlJcWWnmpw,725
|
|
50
|
-
fleet_python-0.2.
|
|
51
|
-
fleet_python-0.2.
|
|
52
|
-
fleet_python-0.2.
|
|
53
|
-
fleet_python-0.2.
|
|
52
|
+
fleet_python-0.2.12.dist-info/METADATA,sha256=E-uVjx2PGP2Es79eQ7VSLu_Nc5beqAwefdF7jng7OXE,4358
|
|
53
|
+
fleet_python-0.2.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
54
|
+
fleet_python-0.2.12.dist-info/top_level.txt,sha256=_3DSmTohvSDf3AIP_BYfGzhwO1ECFwuzg83X-wHCx3Y,23
|
|
55
|
+
fleet_python-0.2.12.dist-info/RECORD,,
|
scripts/fix_sync_imports.py
CHANGED
|
@@ -25,6 +25,9 @@ def fix_file(filepath: Path) -> bool:
|
|
|
25
25
|
# Fix any remaining AsyncFleetPlaywrightWrapper references in docstrings
|
|
26
26
|
content = content.replace('AsyncFleetPlaywrightWrapper', 'FleetPlaywrightWrapper')
|
|
27
27
|
|
|
28
|
+
# Fix httpx transport classes
|
|
29
|
+
content = content.replace('httpx.SyncHTTPTransport', 'httpx.HTTPTransport')
|
|
30
|
+
|
|
28
31
|
# Fix playwright imports for sync version
|
|
29
32
|
if 'playwright' in str(filepath):
|
|
30
33
|
# Fix the import statement
|
|
@@ -54,6 +57,7 @@ def main():
|
|
|
54
57
|
# Files to fix
|
|
55
58
|
files_to_fix = [
|
|
56
59
|
sync_dir / "instance" / "client.py",
|
|
60
|
+
sync_dir / "instance" / "base.py",
|
|
57
61
|
sync_dir / "playwright.py",
|
|
58
62
|
# Add other files here as needed
|
|
59
63
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|