fleet-python 0.1.1__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,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,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()