fleet-python 0.2.11__tar.gz → 0.2.12__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.

Files changed (64) hide show
  1. {fleet_python-0.2.11 → fleet_python-0.2.12}/PKG-INFO +2 -1
  2. fleet_python-0.2.12/fleet/_async/base.py +223 -0
  3. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/_async/client.py +24 -9
  4. fleet_python-0.2.12/fleet/_async/config.py +8 -0
  5. fleet_python-0.2.12/fleet/_async/exceptions.py +161 -0
  6. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/_async/instance/base.py +25 -1
  7. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/_async/instance/client.py +4 -4
  8. fleet_python-0.2.12/fleet/base.py +223 -0
  9. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/client.py +28 -9
  10. fleet_python-0.2.12/fleet/config.py +8 -0
  11. fleet_python-0.2.12/fleet/exceptions.py +161 -0
  12. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/instance/base.py +25 -1
  13. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/instance/client.py +4 -3
  14. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet_python.egg-info/PKG-INFO +2 -1
  15. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet_python.egg-info/SOURCES.txt +2 -0
  16. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet_python.egg-info/requires.txt +1 -0
  17. {fleet_python-0.2.11 → fleet_python-0.2.12}/pyproject.toml +4 -1
  18. {fleet_python-0.2.11 → fleet_python-0.2.12}/scripts/fix_sync_imports.py +4 -0
  19. fleet_python-0.2.11/fleet/_async/base.py +0 -132
  20. fleet_python-0.2.11/fleet/_async/exceptions.py +0 -82
  21. fleet_python-0.2.11/fleet/base.py +0 -132
  22. fleet_python-0.2.11/fleet/exceptions.py +0 -82
  23. {fleet_python-0.2.11 → fleet_python-0.2.12}/LICENSE +0 -0
  24. {fleet_python-0.2.11 → fleet_python-0.2.12}/README.md +0 -0
  25. {fleet_python-0.2.11 → fleet_python-0.2.12}/examples/dsl_example.py +0 -0
  26. {fleet_python-0.2.11 → fleet_python-0.2.12}/examples/example.py +0 -0
  27. {fleet_python-0.2.11 → fleet_python-0.2.12}/examples/example_client.py +0 -0
  28. {fleet_python-0.2.11 → fleet_python-0.2.12}/examples/example_sync.py +0 -0
  29. {fleet_python-0.2.11 → fleet_python-0.2.12}/examples/gemini_example.py +0 -0
  30. {fleet_python-0.2.11 → fleet_python-0.2.12}/examples/json_tasks_example.py +0 -0
  31. {fleet_python-0.2.11 → fleet_python-0.2.12}/examples/nova_act_example.py +0 -0
  32. {fleet_python-0.2.11 → fleet_python-0.2.12}/examples/openai_example.py +0 -0
  33. {fleet_python-0.2.11 → fleet_python-0.2.12}/examples/openai_simple_example.py +0 -0
  34. {fleet_python-0.2.11 → fleet_python-0.2.12}/examples/quickstart.py +0 -0
  35. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/__init__.py +0 -0
  36. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/_async/__init__.py +0 -0
  37. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/_async/env/__init__.py +0 -0
  38. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/_async/env/client.py +0 -0
  39. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/_async/instance/__init__.py +0 -0
  40. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/_async/instance/models.py +0 -0
  41. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/_async/models.py +0 -0
  42. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/_async/playwright.py +0 -0
  43. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/_async/resources/__init__.py +0 -0
  44. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/_async/resources/base.py +0 -0
  45. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/_async/resources/browser.py +0 -0
  46. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/_async/resources/sqlite.py +0 -0
  47. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/env/__init__.py +0 -0
  48. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/env/client.py +0 -0
  49. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/instance/__init__.py +0 -0
  50. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/instance/models.py +0 -0
  51. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/models.py +0 -0
  52. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/playwright.py +0 -0
  53. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/resources/__init__.py +0 -0
  54. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/resources/base.py +0 -0
  55. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/resources/browser.py +0 -0
  56. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/resources/sqlite.py +0 -0
  57. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/verifiers/__init__.py +0 -0
  58. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/verifiers/code.py +0 -0
  59. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/verifiers/db.py +0 -0
  60. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet/verifiers/sql_differ.py +0 -0
  61. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet_python.egg-info/dependency_links.txt +0 -0
  62. {fleet_python-0.2.11 → fleet_python-0.2.12}/fleet_python.egg-info/top_level.txt +0 -0
  63. {fleet_python-0.2.11 → fleet_python-0.2.12}/scripts/unasync.py +0 -0
  64. {fleet_python-0.2.11 → fleet_python-0.2.12}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.2.11
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"
@@ -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
+ )
@@ -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, httpx_client: Optional[httpx.AsyncClient] = None, **kwargs):
42
+ def __init__(self, client: AsyncWrapper, **kwargs):
41
43
  super().__init__(**kwargs)
42
- self._httpx_client = httpx_client or httpx.AsyncClient(timeout=180.0)
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(self.manager_url, self._httpx_client)
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
- return await AsyncFleet().delete(self.instance_id)
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 httpx.AsyncClient(timeout=180.0)
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,
@@ -115,10 +123,14 @@ class AsyncFleet:
115
123
  version = None
116
124
 
117
125
  request = InstanceRequest(env_key=env_key_part, version=version, region=region)
126
+ region_base_url = REGION_BASE_URL.get(region)
118
127
  response = await self.client.request(
119
- "POST", "/v1/env/instances", json=request.model_dump()
128
+ "POST",
129
+ "/v1/env/instances",
130
+ json=request.model_dump(),
131
+ base_url=region_base_url,
120
132
  )
121
- instance = AsyncEnvironment(**response.json())
133
+ instance = AsyncEnvironment(client=self.client, **response.json())
122
134
  await instance.instance.load()
123
135
  return instance
124
136
 
@@ -132,11 +144,14 @@ class AsyncFleet:
132
144
  params["region"] = region
133
145
 
134
146
  response = await self.client.request("GET", "/v1/env/instances", params=params)
135
- return [AsyncEnvironment(**instance_data) for instance_data in response.json()]
147
+ return [
148
+ AsyncEnvironment(client=self.client, **instance_data)
149
+ for instance_data in response.json()
150
+ ]
136
151
 
137
152
  async def instance(self, instance_id: str) -> AsyncEnvironment:
138
153
  response = await self.client.request("GET", f"/v1/env/instances/{instance_id}")
139
- instance = AsyncEnvironment(**response.json())
154
+ instance = AsyncEnvironment(client=self.client, **response.json())
140
155
  await instance.instance.load()
141
156
  return instance
142
157
 
@@ -0,0 +1,8 @@
1
+ DEFAULT_MAX_RETRIES = 5
2
+
3
+ GLOBAL_BASE_URL = "https://orchestrator.fleetai.com"
4
+ REGION_BASE_URL = {
5
+ "us-west-1": "https://us-west-1.fleetai.com",
6
+ "us-east-1": "https://us-east-1.fleetai.com",
7
+ "eu-west-2": "https://eu-west-2.fleetai.com",
8
+ }
@@ -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,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
+ )
@@ -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, FleetAPIError
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 httpx.AsyncClient(timeout=180.0),
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]] = {