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