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.

fleet/env/base.py CHANGED
@@ -1,328 +1,60 @@
1
- """Fleet SDK Base Environment Classes."""
1
+ import httpx
2
+ from typing import Dict, Any, Optional
2
3
 
3
- from abc import ABC, abstractmethod
4
- from typing import Any, Dict, List, Optional, Tuple, Union
5
- from datetime import datetime
6
- import asyncio
7
- import time
8
- import logging
9
- from pydantic import BaseModel, Field
10
4
 
11
- from ..facets.base import Facet
12
- from ..exceptions import FleetEnvironmentError, FleetAPIError
13
- from ..client import FleetAPIClient, InstanceRequest, InstanceResponse
14
- from ..manager_client import FleetManagerClient, ManagerHealthResponse
15
- from ..config import FleetConfig, get_config
16
-
17
-
18
- logger = logging.getLogger(__name__)
5
+ class BaseWrapper:
6
+ def __init__(self, *, url: str):
7
+ self.url = url
19
8
 
9
+ def get_headers(self) -> Dict[str, str]:
10
+ headers: Dict[str, str] = {
11
+ "X-Fleet-SDK-Language": "Python",
12
+ "X-Fleet-SDK-Version": "1.0.0",
13
+ }
14
+ return headers
20
15
 
21
- class EnvironmentConfig(BaseModel):
22
- """Configuration for Fleet environments."""
23
-
24
- environment_type: str = Field(..., description="Type of environment (e.g., 'chrome-desktop-v1')")
25
- api_key: Optional[str] = Field(None, description="Fleet API key")
26
- base_url: str = Field(default="https://fleet.new", description="Fleet API base URL")
27
- metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional configuration")
28
-
29
- def to_dict(self) -> Dict[str, Any]:
30
- """Convert config to dictionary."""
31
- return self.model_dump()
32
16
 
17
+ class SyncWrapper(BaseWrapper):
18
+ def __init__(self, *, httpx_client: httpx.Client, **kwargs):
19
+ super().__init__(**kwargs)
20
+ self.httpx_client = httpx_client
33
21
 
34
- class Environment(ABC):
35
- """Base class for all Fleet environments."""
36
-
37
- def __init__(self, config: EnvironmentConfig):
38
- self.config = config
39
- self._facets: Dict[str, Facet] = {}
40
- self._session_id: Optional[str] = None
41
- self._instance_id: Optional[str] = None
42
- self._step_count: int = 0
43
-
44
- @abstractmethod
45
- async def reset(
22
+ def request(
46
23
  self,
47
- seed: Optional[int] = None,
48
- timestamp: Optional[Union[str, datetime]] = None,
49
- options: Optional[Dict[str, Any]] = None
50
- ) -> None:
51
- """Reset the environment.
52
-
53
- Args:
54
- seed: Integer seed for deterministic RNG in the env (physics, action noise, etc.)
55
- timestamp: ISO8601 string or datetime for the "current time" the sim should use
56
- options: Any additional flags the env impl supports (e.g. viewport size, login creds, feature flags)
57
- """
58
- pass
59
-
60
- @abstractmethod
61
- async def step(self, action: Dict[str, Any]) -> Tuple[Dict[str, Any], float, bool]:
62
- """Execute one step in the environment.
63
-
64
- Args:
65
- action: The action to execute as a dictionary
66
-
67
- Returns:
68
- Tuple of (state, reward, done)
69
- """
70
- pass
71
-
72
- @abstractmethod
73
- async def close(self) -> None:
74
- """Close the environment and clean up resources."""
75
- pass
76
-
77
- @abstractmethod
78
- def state(self, facet_uri: str) -> Facet:
79
- """Get a facet for accessing environment state.
80
-
81
- Args:
82
- facet_uri: URI identifying the facet (e.g., 'sqlite://crm', 'browser://dom')
83
-
84
- Returns:
85
- Facet instance for the requested state
86
- """
87
- pass
88
-
89
- @property
90
- def session_id(self) -> Optional[str]:
91
- """Get the current session ID."""
92
- return self._session_id
93
-
94
- @property
95
- def instance_id(self) -> Optional[str]:
96
- """Get the current instance ID."""
97
- return self._instance_id
98
-
99
- @property
100
- def step_count(self) -> int:
101
- """Get the current step count."""
102
- return self._step_count
103
-
104
- def _increment_step(self) -> None:
105
- """Increment the step counter."""
106
- self._step_count += 1
107
-
108
- def _reset_step_count(self) -> None:
109
- """Reset the step counter."""
110
- self._step_count = 0
111
-
112
- def _register_facet(self, uri: str, facet: Facet) -> None:
113
- """Register a facet for this environment."""
114
- self._facets[uri] = facet
115
-
116
- def _get_facet(self, uri: str) -> Optional[Facet]:
117
- """Get a registered facet."""
118
- return self._facets.get(uri)
24
+ method: str,
25
+ path: str,
26
+ params: Optional[Dict[str, Any]] = None,
27
+ json: Optional[Any] = None,
28
+ **kwargs,
29
+ ) -> httpx.Response:
30
+ return self.httpx_client.request(
31
+ method,
32
+ f"{self.url}{path}",
33
+ headers=self.get_headers(),
34
+ params=params,
35
+ json=json,
36
+ **kwargs,
37
+ )
119
38
 
120
39
 
121
- class RemoteEnvironment(Environment):
122
- """Environment that connects to a remote Fleet API."""
123
-
124
- def __init__(self, config: EnvironmentConfig, instance_response: Optional[InstanceResponse] = None, instance_id: Optional[str] = None):
125
- super().__init__(config)
126
-
127
- # Create Fleet config from environment config
128
- self._fleet_config = FleetConfig(
129
- api_key=config.api_key,
130
- base_url=config.base_url,
131
- )
132
-
133
- # Initialize API client
134
- self._client = FleetAPIClient(self._fleet_config)
135
-
136
- # Set instance details
137
- if instance_response:
138
- self._instance_response = instance_response
139
- self._instance_id = instance_response.instance_id
140
- else:
141
- self._instance_id = instance_id
142
- self._instance_response = None
143
-
144
- # Initialize manager client (will be set when instance URLs are available)
145
- self._manager_client: Optional[FleetManagerClient] = None
146
-
147
- async def reset(
40
+ class AsyncWrapper(BaseWrapper):
41
+ def __init__(self, *, httpx_client: httpx.AsyncClient, **kwargs):
42
+ super().__init__(**kwargs)
43
+ self.httpx_client = httpx_client
44
+
45
+ async def request(
148
46
  self,
149
- seed: Optional[int] = None,
150
- timestamp: Optional[Union[str, datetime]] = None,
151
- options: Optional[Dict[str, Any]] = None
152
- ) -> None:
153
- """Reset the environment state.
154
-
155
- Args:
156
- seed: Integer seed for deterministic RNG in the env (physics, action noise, etc.)
157
- timestamp: ISO8601 string or datetime for the "current time" the sim should use
158
- options: Any additional flags the env impl supports (e.g. viewport size, login creds, feature flags)
159
- """
160
- raise NotImplementedError("reset() is not implemented yet.")
161
-
162
- async def step(self, action: Dict[str, Any]) -> Tuple[Dict[str, Any], float, bool]:
163
- """Execute one step in the environment."""
164
- if not self._instance_id:
165
- raise FleetEnvironmentError("Environment not initialized. Call reset() first.")
166
-
167
- try:
168
- # Increment step count
169
- self._increment_step()
170
-
171
- # Execute action through instance manager API
172
- # This is a placeholder - actual implementation depends on the manager API spec
173
- state, reward, done = await self._execute_action(action)
174
-
175
- return state, reward, done
176
-
177
- except Exception as e:
178
- raise FleetEnvironmentError(f"Failed to execute step: {e}")
179
-
180
- async def close(self) -> None:
181
- """Close the environment and clean up resources."""
182
- try:
183
- # Delete instance if it exists
184
- if self._instance_id:
185
- try:
186
- await self._client.delete_instance(self._instance_id)
187
- logger.info(f"Deleted instance: {self._instance_id}")
188
- except FleetAPIError as e:
189
- logger.warning(f"Failed to delete instance: {e}")
190
- finally:
191
- self._instance_id = None
192
- self._instance_response = None
193
-
194
- # Close manager client
195
- if self._manager_client:
196
- await self._manager_client.close()
197
- self._manager_client = None
198
-
199
- # Close API client
200
- await self._client.close()
201
-
202
- except Exception as e:
203
- logger.error(f"Error closing environment: {e}")
204
-
205
- def state(self, facet_uri: str) -> Facet:
206
- """Get a facet for accessing environment state."""
207
- # Check if facet is already registered
208
- facet = self._get_facet(facet_uri)
209
- if facet:
210
- return facet
211
-
212
- # Create new facet based on URI
213
- from ..facets.factory import create_facet
214
- facet = create_facet(facet_uri, self)
215
- self._register_facet(facet_uri, facet)
216
- return facet
217
-
218
- async def manager_health_check(self) -> Optional[ManagerHealthResponse]:
219
- """Check the health of the manager API.
220
-
221
- Returns:
222
- ManagerHealthResponse if manager is available, None otherwise
223
- """
224
- await self._ensure_manager_client()
225
- if not self._manager_client:
226
- return None
227
-
228
- try:
229
- return await self._manager_client.health_check()
230
- except Exception as e:
231
- logger.warning(f"Manager health check failed: {e}")
232
- return None
233
-
234
- async def _ensure_manager_client(self) -> None:
235
- """Ensure manager client is initialized if instance URLs are available."""
236
- if self._manager_client is not None:
237
- return
238
-
239
- # Need instance response to get manager URLs
240
- if not self._instance_response and self._instance_id:
241
- try:
242
- self._instance_response = await self._client.get_instance(self._instance_id)
243
- except Exception as e:
244
- logger.warning(f"Failed to get instance details for manager client: {e}")
245
- return
246
-
247
- if self._instance_response and self._instance_response.urls.manager:
248
- manager_base_url = self._instance_response.urls.manager.api
249
- self._manager_client = FleetManagerClient(manager_base_url)
250
- logger.debug(f"Initialized manager client for {manager_base_url}")
251
-
252
- async def _wait_for_instance_ready(self, timeout: float = 300.0) -> None:
253
- """Wait for instance to be ready.
254
-
255
- Args:
256
- timeout: Maximum time to wait in seconds
257
- """
258
- start_time = time.time()
259
-
260
- while time.time() - start_time < timeout:
261
- try:
262
- instance = await self._client.get_instance(self._instance_id)
263
- self._instance_response = instance
264
-
265
- if instance.status == "running":
266
- logger.info(f"Instance {self._instance_id} is ready")
267
- return
268
-
269
- elif instance.status == "error":
270
- raise FleetEnvironmentError(f"Instance {self._instance_id} failed to start")
271
-
272
- # Wait before checking again
273
- await asyncio.sleep(5)
274
-
275
- except FleetAPIError as e:
276
- if time.time() - start_time >= timeout:
277
- raise FleetEnvironmentError(f"Timeout waiting for instance to be ready: {e}")
278
- await asyncio.sleep(5)
279
-
280
- raise FleetEnvironmentError(f"Timeout waiting for instance {self._instance_id} to be ready")
281
-
282
- async def _execute_action(self, action: Dict[str, Any]) -> Tuple[Dict[str, Any], float, bool]:
283
- """Execute an action through the instance manager API.
284
-
285
- This is a placeholder implementation that should be extended based on
286
- the actual manager API specification.
287
-
288
- Args:
289
- action: The action to execute as a dictionary
290
-
291
- Returns:
292
- Tuple of (state, reward, done)
293
- """
294
- # Ensure manager client is available
295
- await self._ensure_manager_client()
296
-
297
- # TODO: In the future, this would use the manager API to execute actions
298
- # For example: await self._manager_client.log_action(action)
299
- # For now, return placeholder values
300
-
301
- # Create a placeholder state
302
- state = self._create_state_from_action(action)
303
-
304
- # Create a placeholder reward
305
- reward = 0.0
306
-
307
- # Determine if episode is done (placeholder logic)
308
- done = self._step_count >= 100 # Example: done after 100 steps
309
-
310
- return state, reward, done
311
-
312
- def _create_state_from_action(self, action: Dict[str, Any]) -> Dict[str, Any]:
313
- """Create state based on executed action."""
314
- return {
315
- "instance_id": self._instance_id,
316
- "step": self._step_count,
317
- "last_action": action,
318
- "timestamp": time.time(),
319
- "status": "running"
320
- }
321
-
322
- async def __aenter__(self):
323
- """Async context manager entry."""
324
- return self
325
-
326
- async def __aexit__(self, exc_type, exc_val, exc_tb):
327
- """Async context manager exit."""
328
- await self.close()
47
+ method: str,
48
+ path: str,
49
+ params: Optional[Dict[str, Any]] = None,
50
+ json: Optional[Any] = None,
51
+ **kwargs,
52
+ ) -> httpx.Response:
53
+ return await self.httpx_client.request(
54
+ method,
55
+ f"{self.url}{path}",
56
+ headers=self.get_headers(),
57
+ params=params,
58
+ json=json,
59
+ **kwargs,
60
+ )
fleet/env/client.py ADDED
@@ -0,0 +1,241 @@
1
+ """Fleet SDK Base Environment Classes."""
2
+
3
+ from typing import Any, Dict, List, Optional, Tuple
4
+ import asyncio
5
+ import httpx
6
+ import time
7
+ import logging
8
+ from urllib.parse import urlparse
9
+
10
+ from ..resources.sqlite import AsyncSQLiteResource
11
+ from ..resources.browser import AsyncBrowserResource
12
+ from ..resources.base import Resource
13
+
14
+ from ..exceptions import FleetEnvironmentError, FleetAPIError
15
+
16
+ from .base import SyncWrapper, AsyncWrapper
17
+ from .models import (
18
+ ResetResponse,
19
+ Resource as ResourceModel,
20
+ ResourceType,
21
+ HealthResponse,
22
+ )
23
+
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ RESOURCE_TYPES = {
29
+ ResourceType.sqlite: AsyncSQLiteResource,
30
+ ResourceType.cdp: AsyncBrowserResource,
31
+ }
32
+
33
+
34
+ class Environment:
35
+ def __init__(
36
+ self,
37
+ url: str,
38
+ httpx_client: Optional[httpx.Client] = None,
39
+ ):
40
+ self.base_url = url
41
+ self.client = SyncWrapper(
42
+ url=self.base_url, httpx_client=httpx_client or httpx.Client()
43
+ )
44
+ raise NotImplementedError("SyncManager is not implemented")
45
+
46
+ def reset(self) -> ResetResponse:
47
+ response = self.client.request("POST", "/reset")
48
+ return ResetResponse(**response.json())
49
+
50
+
51
+ class AsyncEnvironment:
52
+ def __init__(
53
+ self,
54
+ url: str,
55
+ httpx_client: Optional[httpx.AsyncClient] = None,
56
+ ):
57
+ self.base_url = url
58
+ self.client = AsyncWrapper(
59
+ url=self.base_url, httpx_client=httpx_client or httpx.AsyncClient()
60
+ )
61
+ self._resources: Optional[List[ResourceModel]] = None
62
+ self._resources_state: Dict[str, Dict[str, Resource]] = {
63
+ resource_type.value: {} for resource_type in ResourceType
64
+ }
65
+
66
+ async def load(self) -> None:
67
+ await self._load_resources()
68
+
69
+ async def reset(self) -> ResetResponse:
70
+ response = await self.client.request("POST", "/reset")
71
+ return ResetResponse(**response.json())
72
+
73
+ def state(self, uri: str) -> Resource:
74
+ url = urlparse(uri)
75
+ return self._resources_state[url.scheme][url.netloc]
76
+
77
+ def sqlite(self, name: str) -> AsyncSQLiteResource:
78
+ return AsyncSQLiteResource(
79
+ self._resources_state[ResourceType.sqlite.value][name], self.client
80
+ )
81
+
82
+ def browser(self, name: str) -> AsyncBrowserResource:
83
+ return AsyncBrowserResource(
84
+ self._resources_state[ResourceType.cdp.value][name], self.client
85
+ )
86
+
87
+ async def resources(self) -> List[ResourceModel]:
88
+ await self._load_resources()
89
+ return self._resources
90
+
91
+ async def _load_resources(self) -> None:
92
+ if self._resources is None:
93
+ response = await self.client.request("GET", "/resources")
94
+ if response.status_code != 200:
95
+ self._resources = []
96
+ return
97
+ data = response.json()
98
+ self._resources = [
99
+ ResourceModel(**resource) for resource in data["resources"]
100
+ ]
101
+ for resource in self._resources:
102
+ if resource.type not in self._resources_state:
103
+ self._resources_state[resource.type.value] = {}
104
+ self._resources_state[resource.type.value][resource.name] = (
105
+ RESOURCE_TYPES[resource.type](resource, self.client)
106
+ )
107
+
108
+ async def step(self, action: Dict[str, Any]) -> Tuple[Dict[str, Any], float, bool]:
109
+ """Execute one step in the environment."""
110
+ if not self._instance_id:
111
+ raise FleetEnvironmentError(
112
+ "Environment not initialized. Call reset() first."
113
+ )
114
+
115
+ try:
116
+ # Increment step count
117
+ self._increment_step()
118
+
119
+ # Execute action through instance manager API
120
+ # This is a placeholder - actual implementation depends on the manager API spec
121
+ state, reward, done = await self._execute_action(action)
122
+
123
+ return state, reward, done
124
+
125
+ except Exception as e:
126
+ raise FleetEnvironmentError(f"Failed to execute step: {e}")
127
+
128
+ async def close(self) -> None:
129
+ """Close the environment and clean up resources."""
130
+ try:
131
+ # Delete instance if it exists
132
+ if self._instance_id:
133
+ try:
134
+ await self._client.delete_instance(self._instance_id)
135
+ logger.info(f"Deleted instance: {self._instance_id}")
136
+ except FleetAPIError as e:
137
+ logger.warning(f"Failed to delete instance: {e}")
138
+ finally:
139
+ self._instance_id = None
140
+ self._instance_response = None
141
+
142
+ # Close manager client
143
+ if self._manager_client:
144
+ await self._manager_client.close()
145
+ self._manager_client = None
146
+
147
+ # Close API client
148
+ await self._client.close()
149
+
150
+ except Exception as e:
151
+ logger.error(f"Error closing environment: {e}")
152
+
153
+ async def manager_health_check(self) -> Optional[HealthResponse]:
154
+ response = await self.client.request("GET", "/health")
155
+ return HealthResponse(**response.json())
156
+
157
+ async def _wait_for_instance_ready(self, timeout: float = 300.0) -> None:
158
+ """Wait for instance to be ready.
159
+
160
+ Args:
161
+ timeout: Maximum time to wait in seconds
162
+ """
163
+ start_time = time.time()
164
+
165
+ while time.time() - start_time < timeout:
166
+ try:
167
+ instance = await self._client.get_instance(self._instance_id)
168
+ self._instance_response = instance
169
+
170
+ if instance.status == "running":
171
+ logger.info(f"Instance {self._instance_id} is ready")
172
+ return
173
+
174
+ elif instance.status == "error":
175
+ raise FleetEnvironmentError(
176
+ f"Instance {self._instance_id} failed to start"
177
+ )
178
+
179
+ # Wait before checking again
180
+ await asyncio.sleep(5)
181
+
182
+ except FleetAPIError as e:
183
+ if time.time() - start_time >= timeout:
184
+ raise FleetEnvironmentError(
185
+ f"Timeout waiting for instance to be ready: {e}"
186
+ )
187
+ await asyncio.sleep(5)
188
+
189
+ raise FleetEnvironmentError(
190
+ f"Timeout waiting for instance {self._instance_id} to be ready"
191
+ )
192
+
193
+ async def _execute_action(
194
+ self, action: Dict[str, Any]
195
+ ) -> Tuple[Dict[str, Any], float, bool]:
196
+ """Execute an action through the instance manager API.
197
+
198
+ This is a placeholder implementation that should be extended based on
199
+ the actual manager API specification.
200
+
201
+ Args:
202
+ action: The action to execute as a dictionary
203
+
204
+ Returns:
205
+ Tuple of (state, reward, done)
206
+ """
207
+ # Ensure manager client is available
208
+ await self._ensure_manager_client()
209
+
210
+ # TODO: In the future, this would use the manager API to execute actions
211
+ # For example: await self._manager_client.log_action(action)
212
+ # For now, return placeholder values
213
+
214
+ # Create a placeholder state
215
+ state = self._create_state_from_action(action)
216
+
217
+ # Create a placeholder reward
218
+ reward = 0.0
219
+
220
+ # Determine if episode is done (placeholder logic)
221
+ done = self._step_count >= 100 # Example: done after 100 steps
222
+
223
+ return state, reward, done
224
+
225
+ def _create_state_from_action(self, action: Dict[str, Any]) -> Dict[str, Any]:
226
+ """Create state based on executed action."""
227
+ return {
228
+ "instance_id": self._instance_id,
229
+ "step": self._step_count,
230
+ "last_action": action,
231
+ "timestamp": time.time(),
232
+ "status": "running",
233
+ }
234
+
235
+ async def __aenter__(self):
236
+ """Async context manager entry."""
237
+ return self
238
+
239
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
240
+ """Async context manager exit."""
241
+ await self.close()