fleet-python 0.1.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.

examples/quickstart.py ADDED
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Fleet SDK Quickstart Example.
4
+
5
+ This example demonstrates basic usage of the Fleet SDK for environment management.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ from typing import Dict, Any
11
+
12
+ import fleet
13
+
14
+
15
+ # Configure logging
16
+ logging.basicConfig(level=logging.INFO)
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ async def main():
21
+ """Main example function."""
22
+
23
+ # Check API health
24
+ print("๐Ÿ” Checking Fleet API health...")
25
+ try:
26
+ config = fleet.get_config()
27
+ client = fleet.FleetAPIClient(config)
28
+ health = await client.health_check()
29
+ print(f"โœ… API Status: {health.status}")
30
+ await client.close()
31
+ except Exception as e:
32
+ print(f"โŒ API Health Check failed: {e}")
33
+ return
34
+
35
+ # 1. List available environments
36
+ print("\n๐Ÿ“‹ Available environments:")
37
+ try:
38
+ environments = await fleet.env.list_envs()
39
+ for env in environments:
40
+ print(f" - {env.env_key}: {env.name}")
41
+ print(f" Description: {env.description}")
42
+ print(f" Default version: {env.default_version}")
43
+ print(f" Available versions: {', '.join(env.versions.keys())}")
44
+ except Exception as e:
45
+ print(f"โŒ Failed to list environments: {e}")
46
+ return
47
+
48
+ # 2. Create a new environment instance
49
+ print("\n๐Ÿš€ Creating new environment...")
50
+ try:
51
+ env = await fleet.env.make("fira:v1.2.5", region="us-west-1")
52
+ print(f"โœ… Environment created with instance ID: {env.instance_id}")
53
+
54
+ # Execute a simple action
55
+ print("\nโšก Executing a simple action...")
56
+ action = {"type": "test", "data": {"message": "Hello Fleet!"}}
57
+ state, reward, done = await env.step(action)
58
+ print(f"โœ… Action executed successfully!")
59
+ print(f" Reward: {reward}")
60
+ print(f" Done: {done}")
61
+ print(f" State keys: {list(state.keys())}")
62
+
63
+ # Check manager API health
64
+ print("\n๐Ÿฅ Checking manager API health...")
65
+ try:
66
+ manager_health = await env.manager_health_check()
67
+ if manager_health:
68
+ print(f"โœ… Manager API Status: {manager_health.status}")
69
+ print(f" Service: {manager_health.service}")
70
+ print(f" Timestamp: {manager_health.timestamp}")
71
+ else:
72
+ print("โŒ Manager API not available")
73
+ except Exception as e:
74
+ print(f"โŒ Manager health check failed: {e}")
75
+
76
+ # Clean up
77
+ print("\n๐Ÿงน Cleaning up...")
78
+ await env.close()
79
+ print("โœ… Environment closed")
80
+
81
+ except Exception as e:
82
+ print(f"โŒ Environment creation failed: {e}")
83
+ return
84
+
85
+ # 3. List running instances
86
+ print("\n๐Ÿƒ Listing running instances...")
87
+ try:
88
+ instances = await fleet.env.list_instances(status="running")
89
+ if instances:
90
+ print(f"Found {len(instances)} running instances:")
91
+ for instance in instances:
92
+ print(f" - {instance.instance_id}: {instance.env_key} ({instance.status})")
93
+ else:
94
+ print("No running instances found")
95
+ except Exception as e:
96
+ print(f"โŒ Failed to list instances: {e}")
97
+
98
+ # 4. Connect to an existing instance (if any)
99
+ print("\n๐Ÿ”— Connecting to existing instance...")
100
+ try:
101
+ # Only get running instances
102
+ running_instances = await fleet.env.list_instances(status="running")
103
+ if running_instances:
104
+ # Find a running instance that's not the one we just created/deleted
105
+ target_instance = running_instances[0]
106
+ print(f"Connecting to running instance: {target_instance.instance_id}")
107
+
108
+ env = await fleet.env.get(target_instance.instance_id)
109
+ print(f"โœ… Connected to instance: {env.instance_id}")
110
+
111
+ # Execute an action on the existing instance
112
+ action = {"type": "ping", "data": {"timestamp": "2024-01-01T00:00:00Z"}}
113
+ state, reward, done = await env.step(action)
114
+ print(f"โœ… Action executed on existing instance!")
115
+ print(f" Reward: {reward}")
116
+ print(f" Done: {done}")
117
+
118
+ # Clean up (this will delete the instance)
119
+ await env.close()
120
+ print("โœ… Connection closed (instance deleted)")
121
+ else:
122
+ print("No running instances to connect to")
123
+ except Exception as e:
124
+ print(f"โŒ Failed to connect to existing instance: {e}")
125
+
126
+ print("\n๐ŸŽ‰ Quickstart complete!")
127
+
128
+
129
+ if __name__ == "__main__":
130
+ asyncio.run(main())
fleet/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ # Copyright 2025 Fleet AI
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Fleet Python SDK - Environment-based AI agent interactions."""
16
+
17
+ from . import env
18
+ 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
22
+
23
+ __version__ = "0.1.0"
24
+ __all__ = [
25
+ "env",
26
+ "FleetError",
27
+ "FleetAPIError",
28
+ "FleetTimeoutError",
29
+ "FleetConfigurationError",
30
+ "get_config",
31
+ "FleetConfig",
32
+ "FleetAPIClient",
33
+ "InstanceRequest",
34
+ "InstanceResponse",
35
+ "APIEnvironment",
36
+ "HealthResponse",
37
+ "ManagerURLs",
38
+ "InstanceURLs",
39
+ "FleetManagerClient",
40
+ "ManagerHealthResponse",
41
+ "TimestampResponse",
42
+ ]
fleet/client.py ADDED
@@ -0,0 +1,318 @@
1
+ # Copyright 2025 Fleet AI
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Fleet API Client for making HTTP requests to Fleet services."""
16
+
17
+ import asyncio
18
+ 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
23
+
24
+ from .config import FleetConfig
25
+ from .exceptions import (
26
+ FleetAPIError,
27
+ FleetAuthenticationError,
28
+ FleetRateLimitError,
29
+ FleetTimeoutError,
30
+ FleetError,
31
+ )
32
+
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
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")
49
+
50
+
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
+
62
+
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")
72
+
73
+
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
+
90
+
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")
99
+
100
+
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")
112
+
113
+
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(
157
+ 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
+ """
270
+ params = {}
271
+ if status:
272
+ 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)
fleet/config.py ADDED
@@ -0,0 +1,125 @@
1
+ """Fleet SDK Configuration Management."""
2
+
3
+ import os
4
+ import logging
5
+ from typing import Optional, Dict, Any
6
+ from pydantic import BaseModel, Field, validator
7
+ from .exceptions import FleetAuthenticationError, FleetConfigurationError
8
+
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class FleetConfig(BaseModel):
14
+ """Fleet SDK Configuration."""
15
+
16
+ api_key: Optional[str] = Field(None, description="Fleet API key")
17
+ base_url: str = Field(default="https://fleet.new", description="Fleet API base URL (hardcoded)")
18
+
19
+ @validator('api_key')
20
+ def validate_api_key(cls, v):
21
+ """Validate API key format."""
22
+ if v is not None and not _is_valid_api_key(v):
23
+ raise FleetAuthenticationError(
24
+ "Invalid API key format. Fleet API keys should start with 'sk_' followed by alphanumeric characters."
25
+ )
26
+ return v
27
+
28
+ @validator('base_url')
29
+ def validate_base_url(cls, v):
30
+ """Validate base URL format."""
31
+ if not v.startswith(('http://', 'https://')):
32
+ raise FleetConfigurationError("Base URL must start with 'http://' or 'https://'")
33
+ return v.rstrip('/')
34
+
35
+ def mask_sensitive_data(self) -> Dict[str, Any]:
36
+ """Return config dict with sensitive data masked."""
37
+ data = self.dict()
38
+ if data.get('api_key'):
39
+ data['api_key'] = _mask_api_key(data['api_key'])
40
+ return data
41
+
42
+ class Config:
43
+ """Pydantic configuration."""
44
+ extra = 'allow'
45
+
46
+
47
+ def get_config(**kwargs: Any) -> FleetConfig:
48
+ """Get Fleet configuration from environment variables.
49
+
50
+ Loads FLEET_API_KEY from environment variables. The base URL is hardcoded to https://fleet.new.
51
+
52
+ Args:
53
+ **kwargs: Override specific configuration values
54
+
55
+ Returns:
56
+ FleetConfig instance
57
+
58
+ Raises:
59
+ FleetAuthenticationError: If API key is invalid
60
+ FleetConfigurationError: If configuration is invalid
61
+ """
62
+ # Load from environment variables
63
+ config_data = _load_env_config()
64
+
65
+ # Apply any overrides
66
+ config_data.update(kwargs)
67
+
68
+ # Create and validate configuration
69
+ try:
70
+ config = FleetConfig(**config_data)
71
+ return config
72
+
73
+ except Exception as e:
74
+ if isinstance(e, (FleetAuthenticationError, FleetConfigurationError)):
75
+ raise
76
+ raise FleetConfigurationError(f"Invalid configuration: {e}")
77
+
78
+
79
+ def _load_env_config() -> Dict[str, Any]:
80
+ """Load configuration from environment variables."""
81
+ env_mapping = {
82
+ 'FLEET_API_KEY': 'api_key',
83
+ # base_url is hardcoded, not configurable via env var
84
+ }
85
+
86
+ config = {}
87
+ for env_var, config_key in env_mapping.items():
88
+ value = os.getenv(env_var)
89
+ if value is not None:
90
+ config[config_key] = value
91
+
92
+ return config
93
+
94
+
95
+ def _is_valid_api_key(api_key: str) -> bool:
96
+ """Validate API key format."""
97
+ if not api_key:
98
+ return False
99
+
100
+ # Fleet API keys start with 'sk_' followed by alphanumeric characters
101
+ # This is a basic format check - actual validation happens on the server
102
+ if not api_key.startswith('sk_'):
103
+ return False
104
+
105
+ # Check if the rest contains only alphanumeric characters and underscores
106
+ key_part = api_key[3:] # Remove 'sk_' prefix
107
+ if not key_part or not key_part.replace('_', '').isalnum():
108
+ return False
109
+
110
+ # Minimum length check
111
+ if len(api_key) < 20:
112
+ return False
113
+
114
+ return True
115
+
116
+
117
+ def _mask_api_key(api_key: str) -> str:
118
+ """Mask API key for logging."""
119
+ if not api_key:
120
+ return api_key
121
+
122
+ if len(api_key) < 8:
123
+ return '*' * len(api_key)
124
+
125
+ return api_key[:4] + '*' * (len(api_key) - 8) + api_key[-4:]
fleet/env/__init__.py ADDED
@@ -0,0 +1,30 @@
1
+ """Fleet SDK Environment Module."""
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
+ )
16
+
17
+ __all__ = [
18
+ "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
+ ]