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 +130 -0
- fleet/__init__.py +42 -0
- fleet/client.py +318 -0
- fleet/config.py +125 -0
- fleet/env/__init__.py +30 -0
- fleet/env/base.py +328 -0
- fleet/env/factory.py +446 -0
- fleet/exceptions.py +73 -0
- fleet/facets/__init__.py +7 -0
- fleet/facets/base.py +223 -0
- fleet/facets/factory.py +29 -0
- fleet/manager_client.py +177 -0
- fleet_python-0.1.0.dist-info/METADATA +110 -0
- fleet_python-0.1.0.dist-info/RECORD +17 -0
- fleet_python-0.1.0.dist-info/WHEEL +5 -0
- fleet_python-0.1.0.dist-info/licenses/LICENSE +201 -0
- fleet_python-0.1.0.dist-info/top_level.txt +2 -0
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
|
+
]
|