fleet-python 0.2.1__py3-none-any.whl → 0.2.2__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.

@@ -0,0 +1,258 @@
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 InstanceClient:
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 AsyncInstanceClient:
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
+ self._resources = [
116
+ ResourceModel(**resource) for resource in response.json()
117
+ ]
118
+ for resource in self._resources:
119
+ if resource.type not in self._resources_state:
120
+ self._resources_state[resource.type.value] = {}
121
+ self._resources_state[resource.type.value][resource.name] = (
122
+ RESOURCE_TYPES[resource.type](resource, self.client)
123
+ )
124
+
125
+ async def step(self, action: Dict[str, Any]) -> Tuple[Dict[str, Any], float, bool]:
126
+ """Execute one step in the environment."""
127
+ if not self._instance_id:
128
+ raise FleetEnvironmentError(
129
+ "Environment not initialized. Call reset() first."
130
+ )
131
+
132
+ try:
133
+ # Increment step count
134
+ self._increment_step()
135
+
136
+ # Execute action through instance manager API
137
+ # This is a placeholder - actual implementation depends on the manager API spec
138
+ state, reward, done = await self._execute_action(action)
139
+
140
+ return state, reward, done
141
+
142
+ except Exception as e:
143
+ raise FleetEnvironmentError(f"Failed to execute step: {e}")
144
+
145
+ async def close(self) -> None:
146
+ """Close the environment and clean up resources."""
147
+ try:
148
+ # Delete instance if it exists
149
+ if self._instance_id:
150
+ try:
151
+ await self._client.delete_instance(self._instance_id)
152
+ logger.info(f"Deleted instance: {self._instance_id}")
153
+ except FleetAPIError as e:
154
+ logger.warning(f"Failed to delete instance: {e}")
155
+ finally:
156
+ self._instance_id = None
157
+ self._instance_response = None
158
+
159
+ # Close manager client
160
+ if self._manager_client:
161
+ await self._manager_client.close()
162
+ self._manager_client = None
163
+
164
+ # Close API client
165
+ await self._client.close()
166
+
167
+ except Exception as e:
168
+ logger.error(f"Error closing environment: {e}")
169
+
170
+ async def manager_health_check(self) -> Optional[HealthResponse]:
171
+ response = await self.client.request("GET", "/health")
172
+ return HealthResponse(**response.json())
173
+
174
+ async def _wait_for_instance_ready(self, timeout: float = 300.0) -> None:
175
+ """Wait for instance to be ready.
176
+
177
+ Args:
178
+ timeout: Maximum time to wait in seconds
179
+ """
180
+ start_time = time.time()
181
+
182
+ while time.time() - start_time < timeout:
183
+ try:
184
+ instance = await self._client.get_instance(self._instance_id)
185
+ self._instance_response = instance
186
+
187
+ if instance.status == "running":
188
+ logger.info(f"Instance {self._instance_id} is ready")
189
+ return
190
+
191
+ elif instance.status == "error":
192
+ raise FleetEnvironmentError(
193
+ f"Instance {self._instance_id} failed to start"
194
+ )
195
+
196
+ # Wait before checking again
197
+ await asyncio.sleep(5)
198
+
199
+ except FleetAPIError as e:
200
+ if time.time() - start_time >= timeout:
201
+ raise FleetEnvironmentError(
202
+ f"Timeout waiting for instance to be ready: {e}"
203
+ )
204
+ await asyncio.sleep(5)
205
+
206
+ raise FleetEnvironmentError(
207
+ f"Timeout waiting for instance {self._instance_id} to be ready"
208
+ )
209
+
210
+ async def _execute_action(
211
+ self, action: Dict[str, Any]
212
+ ) -> Tuple[Dict[str, Any], float, bool]:
213
+ """Execute an action through the instance manager API.
214
+
215
+ This is a placeholder implementation that should be extended based on
216
+ the actual manager API specification.
217
+
218
+ Args:
219
+ action: The action to execute as a dictionary
220
+
221
+ Returns:
222
+ Tuple of (state, reward, done)
223
+ """
224
+ # Ensure manager client is available
225
+ await self._ensure_manager_client()
226
+
227
+ # TODO: In the future, this would use the manager API to execute actions
228
+ # For example: await self._manager_client.log_action(action)
229
+ # For now, return placeholder values
230
+
231
+ # Create a placeholder state
232
+ state = self._create_state_from_action(action)
233
+
234
+ # Create a placeholder reward
235
+ reward = 0.0
236
+
237
+ # Determine if episode is done (placeholder logic)
238
+ done = self._step_count >= 100 # Example: done after 100 steps
239
+
240
+ return state, reward, done
241
+
242
+ def _create_state_from_action(self, action: Dict[str, Any]) -> Dict[str, Any]:
243
+ """Create state based on executed action."""
244
+ return {
245
+ "instance_id": self._instance_id,
246
+ "step": self._step_count,
247
+ "last_action": action,
248
+ "timestamp": time.time(),
249
+ "status": "running",
250
+ }
251
+
252
+ async def __aenter__(self):
253
+ """Async context manager entry."""
254
+ return self
255
+
256
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
257
+ """Async context manager exit."""
258
+ await self.close()
fleet/resources/base.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from abc import ABC
2
- from ..env.models import Resource as ResourceModel, ResourceType, ResourceMode
2
+ from ..manager.models import Resource as ResourceModel, ResourceType, ResourceMode
3
3
 
4
4
 
5
5
  class Resource(ABC):
@@ -8,7 +8,7 @@ class Resource(ABC):
8
8
 
9
9
  @property
10
10
  def uri(self) -> str:
11
- return f"{self.resource.type}://{self.resource.name}"
11
+ return f"{self.resource.type.value}://{self.resource.name}"
12
12
 
13
13
  @property
14
14
  def name(self) -> str:
@@ -21,3 +21,6 @@ class Resource(ABC):
21
21
  @property
22
22
  def mode(self) -> ResourceMode:
23
23
  return self.resource.mode
24
+
25
+ def __repr__(self) -> str:
26
+ return f"Resource(uri={self.uri}, mode={self.mode.value})"
@@ -1,6 +1,5 @@
1
1
  from typing import Optional
2
-
3
- from ..env.models import (
2
+ from ..manager.models import (
4
3
  Resource as ResourceModel,
5
4
  CDPDescribeResponse,
6
5
  ChromeStartRequest,
@@ -11,24 +10,35 @@ from .base import Resource
11
10
  from typing import TYPE_CHECKING
12
11
 
13
12
  if TYPE_CHECKING:
14
- from ..env.base import AsyncWrapper
13
+ from ..manager.base import AsyncWrapper
15
14
 
16
15
 
17
16
  class AsyncBrowserResource(Resource):
18
17
  def __init__(self, resource: ResourceModel, client: "AsyncWrapper"):
19
18
  super().__init__(resource)
20
19
  self.client = client
20
+ self._describe: Optional[CDPDescribeResponse] = None
21
21
 
22
- async def start(
23
- self, start_request: Optional[ChromeStartRequest] = None
24
- ) -> ChromeStartResponse:
22
+ async def start(self, width: int = 1920, height: int = 1080) -> CDPDescribeResponse:
25
23
  response = await self.client.request(
26
24
  "POST",
27
25
  "/resources/cdp/start",
28
- json=start_request.model_dump() if start_request else None,
26
+ json=ChromeStartRequest(resolution=f"{width},{height}").model_dump(),
29
27
  )
30
- return ChromeStartResponse(**response.json())
28
+ ChromeStartResponse(**response.json())
29
+ return await self.describe()
31
30
 
32
31
  async def describe(self) -> CDPDescribeResponse:
33
- response = await self.client.request("GET", "/resources/cdp/describe")
34
- return CDPDescribeResponse(**response.json())
32
+ if self._describe is None:
33
+ response = await self.client.request("GET", "/resources/cdp/describe")
34
+ if response.status_code != 200:
35
+ await self.start()
36
+ response = await self.client.request("GET", "/resources/cdp/describe")
37
+ self._describe = CDPDescribeResponse(**response.json())
38
+ return self._describe
39
+
40
+ async def cdp_url(self) -> str:
41
+ return (await self.describe()).cdp_browser_url
42
+
43
+ async def devtools_url(self) -> str:
44
+ return (await self.describe()).cdp_devtools_url
fleet/resources/sqlite.py CHANGED
@@ -1,12 +1,12 @@
1
1
  from typing import Any, List, Optional
2
- from ..env.models import Resource as ResourceModel
3
- from ..env.models import DescribeResponse, QueryRequest, QueryResponse
2
+ from ..manager.models import Resource as ResourceModel
3
+ from ..manager.models import DescribeResponse, QueryRequest, QueryResponse
4
4
  from .base import Resource
5
5
 
6
6
  from typing import TYPE_CHECKING
7
7
 
8
8
  if TYPE_CHECKING:
9
- from ..env.base import AsyncWrapper
9
+ from ..manager.base import AsyncWrapper
10
10
 
11
11
 
12
12
  class AsyncSQLiteResource(Resource):
@@ -0,0 +1,4 @@
1
+ from .database_snapshot import QueryBuilder, DatabaseSnapshot
2
+ from .sql_differ import SQLiteDiffer
3
+
4
+ __all__ = ["QueryBuilder", "DatabaseSnapshot", "SQLiteDiffer"]