fleet-python 0.1.0__py3-none-any.whl → 0.2.0__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.

@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env python3
2
+ """Example demonstrating browser control with Fleet Manager Client."""
3
+
4
+ import asyncio
5
+ import fleet as flt
6
+
7
+
8
+ async def main():
9
+ fleet = flt.AsyncFleet()
10
+
11
+ environments = await fleet.list_envs()
12
+ print("Environments:", len(environments))
13
+
14
+ instances = await fleet.instances(status="running")
15
+ print("Instances:", len(instances))
16
+
17
+ instance = await fleet.instance("16fdbc96")
18
+ print("Instance:", instance.instance_id)
19
+ print("Instance Environment:", instance.env_key)
20
+
21
+ environment = await fleet.environment(instance.env_key)
22
+ print("Environment Default Version:", environment.default_version)
23
+
24
+ response = await instance.env.reset()
25
+ print("Reset response:", response)
26
+
27
+ print(await instance.env.resources())
28
+
29
+ sqlite = instance.env.sqlite("current")
30
+ print("SQLite:", await sqlite.describe())
31
+
32
+ print("Query:", await sqlite.query("SELECT * FROM users"))
33
+
34
+ sqlite = await instance.env.state("sqlite://current").describe()
35
+ print("SQLite:", sqlite)
36
+
37
+ browser = await instance.env.browser("cdp").describe()
38
+ print("CDP URL:", browser.url)
39
+ print("CDP Devtools URL:", browser.devtools_url)
40
+
41
+ # Create a new instance
42
+ instance = await fleet.make(flt.InstanceRequest(env_key=instance.env_key))
43
+ print("New Instance:", instance.instance_id)
44
+
45
+ # Delete the instance
46
+ instance = await fleet.delete(instance.instance_id)
47
+ print("Instance deleted:", instance.terminated_at)
48
+
49
+
50
+ if __name__ == "__main__":
51
+ asyncio.run(main())
fleet/__init__.py CHANGED
@@ -14,29 +14,17 @@
14
14
 
15
15
  """Fleet Python SDK - Environment-based AI agent interactions."""
16
16
 
17
- from . import env
18
17
  from .exceptions import FleetError, FleetAPIError, FleetTimeoutError, FleetConfigurationError
19
- from .config import get_config, FleetConfig
20
- from .client import FleetAPIClient, InstanceRequest, InstanceResponse, EnvDetails as APIEnvironment, HealthResponse, ManagerURLs, InstanceURLs
21
- from .manager_client import FleetManagerClient, ManagerHealthResponse, TimestampResponse
18
+ from .client import Fleet, AsyncFleet, InstanceRequest
22
19
 
23
- __version__ = "0.1.0"
20
+ __version__ = "0.1.1"
24
21
  __all__ = [
25
22
  "env",
26
23
  "FleetError",
27
24
  "FleetAPIError",
28
25
  "FleetTimeoutError",
29
26
  "FleetConfigurationError",
30
- "get_config",
31
- "FleetConfig",
32
- "FleetAPIClient",
27
+ "Fleet",
28
+ "AsyncFleet",
33
29
  "InstanceRequest",
34
- "InstanceResponse",
35
- "APIEnvironment",
36
- "HealthResponse",
37
- "ManagerURLs",
38
- "InstanceURLs",
39
- "FleetManagerClient",
40
- "ManagerHealthResponse",
41
- "TimestampResponse",
42
30
  ]
fleet/base.py ADDED
@@ -0,0 +1,74 @@
1
+ import httpx
2
+ from typing import Dict, Any, Optional
3
+
4
+ from .models import InstanceResponse
5
+
6
+
7
+ class InstanceBase(InstanceResponse):
8
+ @property
9
+ def manager_url(self) -> str:
10
+ return f"{self.urls.manager.api}"
11
+
12
+
13
+ class BaseWrapper:
14
+ def __init__(self, *, api_key: Optional[str], base_url: Optional[str]):
15
+ if api_key is None:
16
+ raise ValueError("api_key is required")
17
+ self.api_key = api_key
18
+ if base_url is None:
19
+ base_url = "https://fleet.new"
20
+ self.base_url = base_url
21
+
22
+ def get_headers(self) -> Dict[str, str]:
23
+ headers: Dict[str, str] = {
24
+ "X-Fleet-SDK-Language": "Python",
25
+ "X-Fleet-SDK-Version": "1.0.0",
26
+ }
27
+ headers["Authorization"] = f"Bearer {self.api_key}"
28
+ return headers
29
+
30
+
31
+ class SyncWrapper(BaseWrapper):
32
+ def __init__(self, *, httpx_client: httpx.Client, **kwargs):
33
+ super().__init__(**kwargs)
34
+ self.httpx_client = httpx_client
35
+
36
+ def request(
37
+ self,
38
+ method: str,
39
+ url: str,
40
+ params: Optional[Dict[str, Any]] = None,
41
+ json: Optional[Any] = None,
42
+ **kwargs,
43
+ ) -> httpx.Response:
44
+ return self.httpx_client.request(
45
+ method,
46
+ f"{self.base_url}{url}",
47
+ headers=self.get_headers(),
48
+ params=params,
49
+ json=json,
50
+ **kwargs,
51
+ )
52
+
53
+
54
+ class AsyncWrapper(BaseWrapper):
55
+ def __init__(self, *, httpx_client: httpx.AsyncClient, **kwargs):
56
+ super().__init__(**kwargs)
57
+ self.httpx_client = httpx_client
58
+
59
+ async def request(
60
+ self,
61
+ method: str,
62
+ url: str,
63
+ params: Optional[Dict[str, Any]] = None,
64
+ json: Optional[Any] = None,
65
+ **kwargs,
66
+ ) -> httpx.Response:
67
+ return await self.httpx_client.request(
68
+ method,
69
+ f"{self.base_url}{url}",
70
+ headers=self.get_headers(),
71
+ params=params,
72
+ json=json,
73
+ **kwargs,
74
+ )
fleet/client.py CHANGED
@@ -15,304 +15,140 @@
15
15
  """Fleet API Client for making HTTP requests to Fleet services."""
16
16
 
17
17
  import asyncio
18
+ import os
19
+ import httpx
18
20
  import logging
19
- from typing import Any, Dict, List, Optional, Union
20
- from datetime import datetime
21
- import aiohttp
22
- from pydantic import BaseModel, Field
21
+ from typing import Optional, List
23
22
 
24
- from .config import FleetConfig
25
- from .exceptions import (
26
- FleetAPIError,
27
- FleetAuthenticationError,
28
- FleetRateLimitError,
29
- FleetTimeoutError,
30
- FleetError,
31
- )
23
+ from .base import InstanceBase, AsyncWrapper, SyncWrapper
24
+ from .models import InstanceRequest, InstanceRecord, Environment as EnvironmentModel
32
25
 
26
+ from .env import Environment, AsyncEnvironment
33
27
 
34
28
  logger = logging.getLogger(__name__)
35
29
 
36
30
 
37
- class InstanceRequest(BaseModel):
38
- """Request model for creating instances."""
39
-
40
- env_key: str = Field(..., description="Environment key to create instance for")
41
- version: Optional[str] = Field(None, description="Version of the environment")
42
- region: Optional[str] = Field("us-west-1", description="AWS region")
43
- seed: Optional[int] = Field(None, description="Random seed for deterministic behavior")
44
- timestamp: Optional[int] = Field(None, description="Timestamp for environment state")
45
- p_error: Optional[float] = Field(None, description="Error probability")
46
- avg_latency: Optional[float] = Field(None, description="Average latency")
47
- run_id: Optional[str] = Field(None, description="Run ID for tracking")
48
- task_id: Optional[str] = Field(None, description="Task ID for tracking")
31
+ class Instance(InstanceBase):
32
+ def __init__(self, httpx_client: Optional[httpx.Client] = None, **kwargs):
33
+ super().__init__(**kwargs)
34
+ self._httpx_client = httpx_client or httpx.Client()
35
+ self._env: Optional[Environment] = None
49
36
 
37
+ @property
38
+ def env(self) -> Environment:
39
+ if self._env is None:
40
+ self._env = Environment(self.manager_url, self._httpx_client)
41
+ return self._env
50
42
 
51
- class ManagerURLs(BaseModel):
52
- """Model for manager API URLs."""
53
-
54
- api: str = Field(..., description="Manager API URL")
55
- docs: str = Field(..., description="Manager docs URL")
56
- reset: str = Field(..., description="Reset URL")
57
- diff: str = Field(..., description="Diff URL")
58
- snapshot: str = Field(..., description="Snapshot URL")
59
- execute_verifier_function: str = Field(..., description="Execute verifier function URL")
60
- execute_verifier_function_with_upload: str = Field(..., description="Execute verifier function with upload URL")
61
43
 
44
+ class AsyncInstance(InstanceBase):
45
+ def __init__(self, httpx_client: Optional[httpx.AsyncClient] = None, **kwargs):
46
+ super().__init__(**kwargs)
47
+ self._httpx_client = httpx_client or httpx.AsyncClient()
48
+ self._env: Optional[AsyncEnvironment] = None
62
49
 
63
- class InstanceURLs(BaseModel):
64
- """Model for instance URLs."""
65
-
66
- root: str = Field(..., description="Root URL")
67
- app: str = Field(..., description="App URL")
68
- api: Optional[str] = Field(None, description="API URL")
69
- health: Optional[str] = Field(None, description="Health check URL")
70
- api_docs: Optional[str] = Field(None, description="API documentation URL")
71
- manager: ManagerURLs = Field(..., description="Manager API URLs")
50
+ @property
51
+ def env(self) -> AsyncEnvironment:
52
+ if self._env is None:
53
+ self._env = AsyncEnvironment(self.manager_url, self._httpx_client)
54
+ return self._env
72
55
 
73
56
 
74
- class InstanceResponse(BaseModel):
75
- """Response model for instance operations."""
76
-
77
- instance_id: str = Field(..., description="Instance ID")
78
- env_key: str = Field(..., description="Environment key")
79
- version: str = Field(..., description="Environment version")
80
- status: str = Field(..., description="Instance status")
81
- subdomain: str = Field(..., description="Instance subdomain")
82
- created_at: str = Field(..., description="Creation timestamp")
83
- updated_at: str = Field(..., description="Last update timestamp")
84
- terminated_at: Optional[str] = Field(None, description="Termination timestamp")
85
- team_id: str = Field(..., description="Team ID")
86
- region: str = Field(..., description="AWS region")
87
- urls: InstanceURLs = Field(..., description="Instance URLs")
88
- health: Optional[bool] = Field(None, description="Health status")
89
-
57
+ class Fleet:
58
+ def __init__(
59
+ self,
60
+ api_key: Optional[str] = os.getenv("FLEET_API_KEY"),
61
+ base_url: Optional[str] = None,
62
+ httpx_client: Optional[httpx.Client] = None,
63
+ ):
64
+ self._httpx_client = httpx_client or httpx.Client(timeout=60.0)
65
+ self.client = SyncWrapper(
66
+ api_key=api_key,
67
+ base_url=base_url,
68
+ httpx_client=self._httpx_client,
69
+ )
70
+
71
+ def environments(self) -> List[EnvironmentModel]:
72
+ response = self.client.request("GET", "/v1/env/")
73
+ return [EnvironmentModel(**env_data) for env_data in response.json()]
74
+
75
+ def environment(self, env_key: str) -> EnvironmentModel:
76
+ response = self.client.request("GET", f"/v1/env/{env_key}")
77
+ return EnvironmentModel(**response.json())
78
+
79
+ def make(self, request: InstanceRequest) -> Instance:
80
+ response = self.client.request(
81
+ "POST", "/v1/env/instances", json=request.model_dump()
82
+ )
83
+ return Instance(**response.json())
84
+
85
+ def instances(self, status: Optional[str] = None) -> List[Instance]:
86
+ params = {}
87
+ if status:
88
+ params["status"] = status
90
89
 
91
- class EnvDetails(BaseModel):
92
- """Model for environment details and metadata."""
93
-
94
- env_key: str = Field(..., description="Environment key")
95
- name: str = Field(..., description="Environment name")
96
- description: Optional[str] = Field(..., description="Environment description")
97
- default_version: Optional[str] = Field(..., description="Default version")
98
- versions: Dict[str, str] = Field(..., description="Available versions")
90
+ response = self.client.request("GET", "/v1/env/instances", params=params)
91
+ return [Instance(**instance_data) for instance_data in response.json()]
99
92
 
93
+ def instance(self, instance_id: str) -> Instance:
94
+ response = self.client.request("GET", f"/v1/env/instances/{instance_id}")
95
+ return Instance(**response.json())
100
96
 
101
- class HealthResponse(BaseModel):
102
- """Response model for health checks."""
103
-
104
- status: str = Field(..., description="Health status")
105
- timestamp: str = Field(..., description="Timestamp")
106
- mode: str = Field(..., description="Operation mode")
107
- region: str = Field(..., description="AWS region")
108
- docker_status: str = Field(..., description="Docker status")
109
- docker_error: Optional[str] = Field(None, description="Docker error if any")
110
- instances: int = Field(..., description="Number of instances")
111
- regions: Optional[Dict[str, "HealthResponse"]] = Field(None, description="Regional health info")
97
+ def delete(self, instance_id: str) -> InstanceRecord:
98
+ response = self.client.request("DELETE", f"/v1/env/instances/{instance_id}")
99
+ return InstanceRecord(**response.json())
112
100
 
113
101
 
114
- class FleetAPIClient:
115
- """Client for making requests to the Fleet API."""
116
-
117
- def __init__(self, config: FleetConfig):
118
- """Initialize the Fleet API client.
119
-
120
- Args:
121
- config: Fleet configuration with API key and base URL
122
- """
123
- self.config = config
124
- self._session: Optional[aiohttp.ClientSession] = None
125
- self._base_url = config.base_url
126
-
127
- async def __aenter__(self):
128
- """Async context manager entry."""
129
- await self._ensure_session()
130
- return self
131
-
132
- async def __aexit__(self, exc_type, exc_val, exc_tb):
133
- """Async context manager exit."""
134
- await self.close()
135
-
136
- async def _ensure_session(self):
137
- """Ensure HTTP session is created."""
138
- if self._session is None or self._session.closed:
139
- headers = {}
140
- if self.config.api_key:
141
- headers["Authorization"] = f"Bearer {self.config.api_key}"
142
-
143
- timeout = aiohttp.ClientTimeout(total=60)
144
- self._session = aiohttp.ClientSession(
145
- headers=headers,
146
- timeout=timeout,
147
- connector=aiohttp.TCPConnector(limit=100),
148
- )
149
-
150
- async def close(self):
151
- """Close the HTTP session."""
152
- if self._session and not self._session.closed:
153
- await self._session.close()
154
- self._session = None
155
-
156
- async def _request(
102
+ class AsyncFleet:
103
+ def __init__(
157
104
  self,
158
- method: str,
159
- path: str,
160
- data: Optional[Dict[str, Any]] = None,
161
- params: Optional[Dict[str, Any]] = None,
162
- headers: Optional[Dict[str, str]] = None,
163
- timeout: Optional[float] = None,
164
- ) -> Dict[str, Any]:
165
- """Make an HTTP request to the Fleet API.
166
-
167
- Args:
168
- method: HTTP method (GET, POST, DELETE, etc.)
169
- path: API endpoint path
170
- data: Request body data
171
- params: Query parameters
172
- headers: Additional headers
173
- timeout: Request timeout in seconds
174
-
175
- Returns:
176
- Response data as dictionary
177
-
178
- Raises:
179
- FleetAPIError: If the API returns an error
180
- FleetAuthenticationError: If authentication fails
181
- FleetRateLimitError: If rate limit is exceeded
182
- FleetTimeoutError: If request times out
183
- """
184
- await self._ensure_session()
185
-
186
- url = f"{self._base_url}{path}"
187
- request_headers = headers or {}
188
-
189
- try:
190
- logger.debug(f"Making {method} request to {url}")
191
-
192
- async with self._session.request(
193
- method=method,
194
- url=url,
195
- json=data,
196
- params=params,
197
- headers=request_headers,
198
- timeout=aiohttp.ClientTimeout(total=timeout or 60),
199
- ) as response:
200
- response_data = await response.json() if response.content_type == "application/json" else {}
201
-
202
- if response.status == 200:
203
- logger.debug(f"Request successful: {response.status}")
204
- return response_data
205
-
206
- elif response.status == 401:
207
- raise FleetAuthenticationError("Authentication failed - check your API key")
208
-
209
- elif response.status == 429:
210
- raise FleetRateLimitError("Rate limit exceeded - please retry later")
211
-
212
- else:
213
- error_message = response_data.get("detail", f"API request failed with status {response.status}")
214
- raise FleetAPIError(
215
- error_message,
216
- status_code=response.status,
217
- response_data=response_data,
218
- )
219
-
220
- except asyncio.TimeoutError:
221
- raise FleetTimeoutError(f"Request to {url} timed out")
222
-
223
- except aiohttp.ClientError as e:
224
- raise FleetAPIError(f"HTTP client error: {e}")
225
-
226
- # Environment operations
227
- async def list_environments(self) -> List[EnvDetails]:
228
- """List all available environments.
229
-
230
- Returns:
231
- List of EnvDetails objects
232
- """
233
- response = await self._request("GET", "/v1/env/")
234
- return [EnvDetails(**env_data) for env_data in response]
235
-
236
- async def get_environment(self, env_key: str) -> EnvDetails:
237
- """Get details for a specific environment.
238
-
239
- Args:
240
- env_key: Environment key
241
-
242
- Returns:
243
- EnvDetails object
244
- """
245
- response = await self._request("GET", f"/v1/env/{env_key}")
246
- return EnvDetails(**response)
247
-
248
- # Instance operations
249
- async def create_instance(self, request: InstanceRequest) -> InstanceResponse:
250
- """Create a new environment instance.
251
-
252
- Args:
253
- request: Instance creation request
254
-
255
- Returns:
256
- InstanceResponse object
257
- """
258
- response = await self._request("POST", "/v1/env/instances", data=request.model_dump(exclude_none=True))
259
- return InstanceResponse(**response)
260
-
261
- async def list_instances(self, status: Optional[str] = None) -> List[InstanceResponse]:
262
- """List all instances, optionally filtered by status.
263
-
264
- Args:
265
- status: Optional status filter (pending, running, stopped, error)
266
-
267
- Returns:
268
- List of InstanceResponse objects
269
- """
105
+ api_key: Optional[str] = os.getenv("FLEET_API_KEY"),
106
+ base_url: Optional[str] = None,
107
+ httpx_client: Optional[httpx.AsyncClient] = None,
108
+ ):
109
+ self._httpx_client = httpx_client or httpx.AsyncClient(timeout=60.0)
110
+ self.client = AsyncWrapper(
111
+ api_key=api_key,
112
+ base_url=base_url,
113
+ httpx_client=self._httpx_client,
114
+ )
115
+
116
+ async def list_envs(self) -> List[EnvironmentModel]:
117
+ response = await self.client.request("GET", "/v1/env/")
118
+ return [EnvironmentModel(**env_data) for env_data in response.json()]
119
+
120
+ async def environment(self, env_key: str) -> EnvironmentModel:
121
+ response = await self.client.request("GET", f"/v1/env/{env_key}")
122
+ return EnvironmentModel(**response.json())
123
+
124
+ async def make(self, request: InstanceRequest) -> AsyncInstance:
125
+ response = await self.client.request(
126
+ "POST", "/v1/env/instances", json=request.model_dump()
127
+ )
128
+ instance = AsyncInstance(**response.json())
129
+ await instance.env.load()
130
+ return instance
131
+
132
+ async def instances(self, status: Optional[str] = None) -> List[AsyncInstance]:
270
133
  params = {}
271
134
  if status:
272
135
  params["status"] = status
273
-
274
- response = await self._request("GET", "/v1/env/instances", params=params)
275
- return [InstanceResponse(**instance_data) for instance_data in response]
276
-
277
- async def get_instance(self, instance_id: str) -> InstanceResponse:
278
- """Get details for a specific instance.
279
-
280
- Args:
281
- instance_id: Instance ID
282
-
283
- Returns:
284
- InstanceResponse object
285
- """
286
- response = await self._request("GET", f"/v1/env/instances/{instance_id}")
287
- return InstanceResponse(**response)
288
-
289
- async def delete_instance(self, instance_id: str) -> Dict[str, Any]:
290
- """Delete an instance.
291
-
292
- Args:
293
- instance_id: Instance ID
294
-
295
- Returns:
296
- Deletion response data
297
- """
298
- response = await self._request("DELETE", f"/v1/env/instances/{instance_id}")
299
- return response
300
-
301
- # Health check operations
302
- async def health_check(self) -> HealthResponse:
303
- """Check the health of the Fleet API.
304
-
305
- Returns:
306
- HealthResponse object
307
- """
308
- response = await self._request("GET", "/health")
309
- return HealthResponse(**response)
310
-
311
- async def health_check_simple(self) -> HealthResponse:
312
- """Simple health check without authentication.
313
-
314
- Returns:
315
- HealthResponse object
316
- """
317
- response = await self._request("GET", "/health-check")
318
- return HealthResponse(**response)
136
+
137
+ response = await self.client.request("GET", "/v1/env/instances", params=params)
138
+ instances = [
139
+ AsyncInstance(**instance_data) for instance_data in response.json()
140
+ ]
141
+ await asyncio.gather(*[instance.env.load() for instance in instances])
142
+ return instances
143
+
144
+ async def instance(self, instance_id: str) -> AsyncInstance:
145
+ response = await self.client.request("GET", f"/v1/env/instances/{instance_id}")
146
+ instance = AsyncInstance(**response.json())
147
+ await instance.env.load()
148
+ return instance
149
+
150
+ async def delete(self, instance_id: str) -> InstanceRecord:
151
+ response = await self.client.request(
152
+ "DELETE", f"/v1/env/instances/{instance_id}"
153
+ )
154
+ return InstanceRecord(**response.json())
fleet/env/__init__.py CHANGED
@@ -1,30 +1,8 @@
1
1
  """Fleet SDK Environment Module."""
2
2
 
3
- from .base import Environment, EnvironmentConfig
4
- from .factory import (
5
- make,
6
- get,
7
- list_instances,
8
- list_envs,
9
- list_environments,
10
- list_categories,
11
- list_names,
12
- list_versions,
13
- is_environment_supported,
14
- EnvironmentInstance
15
- )
3
+ from .client import Environment, AsyncEnvironment
16
4
 
17
5
  __all__ = [
18
6
  "Environment",
19
- "EnvironmentConfig",
20
- "EnvironmentInstance",
21
- "make",
22
- "get",
23
- "list_instances",
24
- "list_envs",
25
- "list_environments",
26
- "list_categories",
27
- "list_names",
28
- "list_versions",
29
- "is_environment_supported",
30
- ]
7
+ "AsyncEnvironment",
8
+ ]