fleet-python 0.2.3__py3-none-any.whl → 0.2.5__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/dsl_example.py +2 -1
- examples/example.py +2 -2
- examples/json_tasks_example.py +1 -1
- examples/nova_act_example.py +1 -1
- examples/openai_example.py +17 -24
- examples/openai_simple_example.py +11 -12
- fleet/__init__.py +18 -3
- fleet/_async/base.py +51 -0
- fleet/_async/client.py +133 -0
- fleet/_async/env/__init__.py +0 -0
- fleet/_async/env/client.py +15 -0
- fleet/_async/exceptions.py +73 -0
- fleet/_async/instance/__init__.py +24 -0
- fleet/_async/instance/base.py +37 -0
- fleet/_async/instance/client.py +278 -0
- fleet/_async/instance/models.py +141 -0
- fleet/_async/models.py +109 -0
- fleet/_async/playwright.py +291 -0
- fleet/_async/resources/__init__.py +0 -0
- fleet/_async/resources/base.py +26 -0
- fleet/_async/resources/browser.py +41 -0
- fleet/_async/resources/sqlite.py +41 -0
- fleet/base.py +1 -24
- fleet/client.py +31 -99
- fleet/env/__init__.py +13 -1
- fleet/env/client.py +7 -7
- fleet/instance/__init__.py +3 -2
- fleet/instance/base.py +1 -24
- fleet/instance/client.py +40 -57
- fleet/playwright.py +45 -47
- fleet/resources/__init__.py +0 -0
- fleet/resources/browser.py +14 -14
- fleet/resources/sqlite.py +11 -11
- fleet/verifiers/__init__.py +5 -10
- fleet/verifiers/code.py +1 -132
- {fleet_python-0.2.3.dist-info → fleet_python-0.2.5.dist-info}/METADATA +12 -11
- fleet_python-0.2.5.dist-info/RECORD +48 -0
- {fleet_python-0.2.3.dist-info → fleet_python-0.2.5.dist-info}/top_level.txt +1 -0
- scripts/unasync.py +28 -0
- fleet_python-0.2.3.dist-info/RECORD +0 -31
- {fleet_python-0.2.3.dist-info → fleet_python-0.2.5.dist-info}/WHEEL +0 -0
- {fleet_python-0.2.3.dist-info → fleet_python-0.2.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from typing import List, Dict, Any
|
|
3
|
+
from playwright.async_api import async_playwright, Browser, Page
|
|
4
|
+
from .client import AsyncEnvironment
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Key mapping for computer use actions
|
|
8
|
+
CUA_KEY_TO_PLAYWRIGHT_KEY = {
|
|
9
|
+
"/": "Divide",
|
|
10
|
+
"\\": "Backslash",
|
|
11
|
+
"alt": "Alt",
|
|
12
|
+
"arrowdown": "ArrowDown",
|
|
13
|
+
"arrowleft": "ArrowLeft",
|
|
14
|
+
"arrowright": "ArrowRight",
|
|
15
|
+
"arrowup": "ArrowUp",
|
|
16
|
+
"backspace": "Backspace",
|
|
17
|
+
"capslock": "CapsLock",
|
|
18
|
+
"cmd": "Meta",
|
|
19
|
+
"ctrl": "Control",
|
|
20
|
+
"delete": "Delete",
|
|
21
|
+
"end": "End",
|
|
22
|
+
"enter": "Enter",
|
|
23
|
+
"esc": "Escape",
|
|
24
|
+
"home": "Home",
|
|
25
|
+
"insert": "Insert",
|
|
26
|
+
"option": "Alt",
|
|
27
|
+
"pagedown": "PageDown",
|
|
28
|
+
"pageup": "PageUp",
|
|
29
|
+
"shift": "Shift",
|
|
30
|
+
"space": " ",
|
|
31
|
+
"super": "Meta",
|
|
32
|
+
"tab": "Tab",
|
|
33
|
+
"win": "Meta",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FleetPlaywrightWrapper:
|
|
38
|
+
"""
|
|
39
|
+
A wrapper that adds Playwright browser automation to Fleet environment instances.
|
|
40
|
+
|
|
41
|
+
This class handles:
|
|
42
|
+
- Browser connection via CDP
|
|
43
|
+
- Computer actions (click, scroll, type, etc.)
|
|
44
|
+
- Screenshot capture
|
|
45
|
+
- Integration with OpenAI computer use API
|
|
46
|
+
|
|
47
|
+
Usage:
|
|
48
|
+
instance = await fleet.env.make(env_key="hubspot", version="v1.2.7")
|
|
49
|
+
browser = FleetPlaywrightWrapper(instance)
|
|
50
|
+
await browser.start()
|
|
51
|
+
|
|
52
|
+
# Use browser methods
|
|
53
|
+
screenshot = await browser.screenshot()
|
|
54
|
+
tools = [browser.openai_cua_tool]
|
|
55
|
+
|
|
56
|
+
# Clean up when done
|
|
57
|
+
await browser.close()
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def get_environment(self):
|
|
61
|
+
return "browser"
|
|
62
|
+
|
|
63
|
+
def get_dimensions(self):
|
|
64
|
+
return (1920, 1080)
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
env: AsyncEnvironment,
|
|
69
|
+
display_width: int = 1920,
|
|
70
|
+
display_height: int = 1080,
|
|
71
|
+
):
|
|
72
|
+
"""
|
|
73
|
+
Initialize the Fleet Playwright wrapper.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
env: Fleet environment instance
|
|
77
|
+
display_width: Browser viewport width
|
|
78
|
+
display_height: Browser viewport height
|
|
79
|
+
"""
|
|
80
|
+
self.env = env
|
|
81
|
+
self.display_width = display_width
|
|
82
|
+
self.display_height = display_height
|
|
83
|
+
|
|
84
|
+
self._playwright = None
|
|
85
|
+
self._browser: Browser | None = None
|
|
86
|
+
self._page: Page | None = None
|
|
87
|
+
self._started = False
|
|
88
|
+
|
|
89
|
+
async def start(self):
|
|
90
|
+
"""Start the browser and establish connection."""
|
|
91
|
+
if self._started:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
# Start Playwright
|
|
95
|
+
self._playwright = await async_playwright().start()
|
|
96
|
+
|
|
97
|
+
# Start browser on the Fleet instance
|
|
98
|
+
print("Starting browser...")
|
|
99
|
+
await self.env.browser().start()
|
|
100
|
+
cdp = await self.env.browser().describe()
|
|
101
|
+
|
|
102
|
+
# Connect to browser
|
|
103
|
+
self._browser = await self._playwright.chromium.connect_over_cdp(
|
|
104
|
+
cdp.cdp_browser_url
|
|
105
|
+
)
|
|
106
|
+
self._page = self._browser.contexts[0].pages[0]
|
|
107
|
+
await self._page.set_viewport_size(
|
|
108
|
+
{"width": self.display_width, "height": self.display_height}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
self._started = True
|
|
112
|
+
print(f"Track agent: {cdp.cdp_devtools_url}")
|
|
113
|
+
|
|
114
|
+
async def close(self):
|
|
115
|
+
"""Close the browser connection."""
|
|
116
|
+
if self._playwright:
|
|
117
|
+
await self._playwright.stop()
|
|
118
|
+
self._playwright = None
|
|
119
|
+
self._browser = None
|
|
120
|
+
self._page = None
|
|
121
|
+
self._started = False
|
|
122
|
+
|
|
123
|
+
def _ensure_started(self):
|
|
124
|
+
"""Ensure browser is started before operations."""
|
|
125
|
+
if not self._started:
|
|
126
|
+
raise RuntimeError("Browser not started. Call await browser.start() first.")
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def openai_cua_tool(self) -> Dict[str, Any]:
|
|
130
|
+
"""
|
|
131
|
+
Tool definition for OpenAI computer use API.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Tool definition dict for use with OpenAI responses API
|
|
135
|
+
"""
|
|
136
|
+
return {
|
|
137
|
+
"type": "computer_use_preview",
|
|
138
|
+
"display_width": self.display_width,
|
|
139
|
+
"display_height": self.display_height,
|
|
140
|
+
"environment": "browser",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async def screenshot(self) -> str:
|
|
144
|
+
"""
|
|
145
|
+
Take a screenshot and return base64 encoded string.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Base64 encoded PNG screenshot
|
|
149
|
+
"""
|
|
150
|
+
self._ensure_started()
|
|
151
|
+
|
|
152
|
+
png_bytes = await self._page.screenshot(full_page=False)
|
|
153
|
+
return base64.b64encode(png_bytes).decode("utf-8")
|
|
154
|
+
|
|
155
|
+
def get_current_url(self) -> str:
|
|
156
|
+
"""Get the current page URL."""
|
|
157
|
+
self._ensure_started()
|
|
158
|
+
return self._page.url
|
|
159
|
+
|
|
160
|
+
async def execute_computer_action(self, action: Dict[str, Any]) -> Dict[str, Any]:
|
|
161
|
+
"""
|
|
162
|
+
Execute a computer action and return the result for OpenAI API.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
action: Computer action dict from OpenAI response
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Result dict for computer_call_output
|
|
169
|
+
"""
|
|
170
|
+
self._ensure_started()
|
|
171
|
+
|
|
172
|
+
action_type = action["type"]
|
|
173
|
+
action_args = {k: v for k, v in action.items() if k != "type"}
|
|
174
|
+
|
|
175
|
+
print(f"Executing: {action_type}({action_args})")
|
|
176
|
+
|
|
177
|
+
# Execute the action
|
|
178
|
+
if hasattr(self, f"_{action_type}"):
|
|
179
|
+
method = getattr(self, f"_{action_type}")
|
|
180
|
+
await method(**action_args)
|
|
181
|
+
else:
|
|
182
|
+
raise ValueError(f"Unsupported action type: {action_type}")
|
|
183
|
+
|
|
184
|
+
# Take screenshot after action
|
|
185
|
+
screenshot_base64 = await self.screenshot()
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
"type": "input_image",
|
|
189
|
+
"image_url": f"data:image/png;base64,{screenshot_base64}",
|
|
190
|
+
"current_url": self.get_current_url(),
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Computer action implementations
|
|
194
|
+
async def _click(self, x: int, y: int, button: str = "left") -> None:
|
|
195
|
+
"""Click at coordinates."""
|
|
196
|
+
self._ensure_started()
|
|
197
|
+
await self._page.mouse.click(x, y, button=button)
|
|
198
|
+
|
|
199
|
+
async def _double_click(self, x: int, y: int) -> None:
|
|
200
|
+
"""Double-click at coordinates."""
|
|
201
|
+
self._ensure_started()
|
|
202
|
+
await self._page.mouse.dblclick(x, y)
|
|
203
|
+
|
|
204
|
+
async def _scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None:
|
|
205
|
+
"""Scroll from coordinates."""
|
|
206
|
+
self._ensure_started()
|
|
207
|
+
await self._page.mouse.move(x, y)
|
|
208
|
+
await self._page.evaluate(f"window.scrollBy({scroll_x}, {scroll_y})")
|
|
209
|
+
|
|
210
|
+
async def _type(self, text: str) -> None:
|
|
211
|
+
"""Type text."""
|
|
212
|
+
self._ensure_started()
|
|
213
|
+
await self._page.keyboard.type(text)
|
|
214
|
+
|
|
215
|
+
async def _keypress(self, keys: List[str]) -> None:
|
|
216
|
+
"""Press key combination."""
|
|
217
|
+
self._ensure_started()
|
|
218
|
+
mapped_keys = [CUA_KEY_TO_PLAYWRIGHT_KEY.get(key.lower(), key) for key in keys]
|
|
219
|
+
for key in mapped_keys:
|
|
220
|
+
await self._page.keyboard.down(key)
|
|
221
|
+
for key in reversed(mapped_keys):
|
|
222
|
+
await self._page.keyboard.up(key)
|
|
223
|
+
|
|
224
|
+
async def _move(self, x: int, y: int) -> None:
|
|
225
|
+
"""Move mouse to coordinates."""
|
|
226
|
+
self._ensure_started()
|
|
227
|
+
await self._page.mouse.move(x, y)
|
|
228
|
+
|
|
229
|
+
async def _drag(self, path: List[Dict[str, int]]) -> None:
|
|
230
|
+
"""Drag mouse along path."""
|
|
231
|
+
self._ensure_started()
|
|
232
|
+
if not path:
|
|
233
|
+
return
|
|
234
|
+
await self._page.mouse.move(path[0]["x"], path[0]["y"])
|
|
235
|
+
await self._page.mouse.down()
|
|
236
|
+
for point in path[1:]:
|
|
237
|
+
await self._page.mouse.move(point["x"], point["y"])
|
|
238
|
+
await self._page.mouse.up()
|
|
239
|
+
|
|
240
|
+
async def _wait(self, ms: int = 1000) -> None:
|
|
241
|
+
"""Wait for specified milliseconds."""
|
|
242
|
+
import asyncio
|
|
243
|
+
|
|
244
|
+
await asyncio.sleep(ms / 1000)
|
|
245
|
+
|
|
246
|
+
# Browser-specific actions
|
|
247
|
+
async def _goto(self, url: str) -> None:
|
|
248
|
+
"""Navigate to URL."""
|
|
249
|
+
self._ensure_started()
|
|
250
|
+
try:
|
|
251
|
+
await self._page.goto(url)
|
|
252
|
+
except Exception as e:
|
|
253
|
+
print(f"Error navigating to {url}: {e}")
|
|
254
|
+
|
|
255
|
+
async def _back(self) -> None:
|
|
256
|
+
"""Go back in browser history."""
|
|
257
|
+
self._ensure_started()
|
|
258
|
+
await self._page.go_back()
|
|
259
|
+
|
|
260
|
+
async def _forward(self) -> None:
|
|
261
|
+
"""Go forward in browser history."""
|
|
262
|
+
self._ensure_started()
|
|
263
|
+
await self._page.go_forward()
|
|
264
|
+
|
|
265
|
+
async def _refresh(self) -> None:
|
|
266
|
+
"""Refresh the page."""
|
|
267
|
+
self._ensure_started()
|
|
268
|
+
await self._page.reload()
|
|
269
|
+
|
|
270
|
+
# ------------------------------------------------------------------
|
|
271
|
+
# Public aliases (no leading underscore) expected by the Agent &
|
|
272
|
+
# OpenAI computer-use API. They forward directly to the underscored
|
|
273
|
+
# implementations above so the external interface matches the older
|
|
274
|
+
# BasePlaywrightComputer class.
|
|
275
|
+
# ------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
# Mouse / keyboard actions
|
|
278
|
+
click = _click
|
|
279
|
+
double_click = _double_click
|
|
280
|
+
scroll = _scroll
|
|
281
|
+
type = _type # noqa: A003 – shadowing built-in for API compatibility
|
|
282
|
+
keypress = _keypress
|
|
283
|
+
move = _move
|
|
284
|
+
drag = _drag
|
|
285
|
+
wait = _wait
|
|
286
|
+
|
|
287
|
+
# Browser navigation actions
|
|
288
|
+
goto = _goto
|
|
289
|
+
back = _back
|
|
290
|
+
forward = _forward
|
|
291
|
+
refresh = _refresh
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from ..instance.models import Resource as ResourceModel, ResourceType, ResourceMode
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Resource(ABC):
|
|
6
|
+
def __init__(self, resource: ResourceModel):
|
|
7
|
+
self.resource = resource
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def uri(self) -> str:
|
|
11
|
+
return f"{self.resource.type.value}://{self.resource.name}"
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def name(self) -> str:
|
|
15
|
+
return self.resource.name
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def type(self) -> ResourceType:
|
|
19
|
+
return self.resource.type
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def mode(self) -> ResourceMode:
|
|
23
|
+
return self.resource.mode
|
|
24
|
+
|
|
25
|
+
def __repr__(self) -> str:
|
|
26
|
+
return f"Resource(uri={self.uri}, mode={self.mode.value})"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from ..instance.models import (
|
|
3
|
+
Resource as ResourceModel,
|
|
4
|
+
CDPDescribeResponse,
|
|
5
|
+
ChromeStartRequest,
|
|
6
|
+
ChromeStartResponse,
|
|
7
|
+
)
|
|
8
|
+
from .base import Resource
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..instance.base import AsyncWrapper
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AsyncBrowserResource(Resource):
|
|
17
|
+
def __init__(self, resource: ResourceModel, client: "AsyncWrapper"):
|
|
18
|
+
super().__init__(resource)
|
|
19
|
+
self.client = client
|
|
20
|
+
|
|
21
|
+
async def start(self, width: int = 1920, height: int = 1080) -> CDPDescribeResponse:
|
|
22
|
+
response = await self.client.request(
|
|
23
|
+
"POST",
|
|
24
|
+
"/resources/cdp/start",
|
|
25
|
+
json=ChromeStartRequest(resolution=f"{width},{height}").model_dump(),
|
|
26
|
+
)
|
|
27
|
+
ChromeStartResponse(**response.json())
|
|
28
|
+
return await self.describe()
|
|
29
|
+
|
|
30
|
+
async def describe(self) -> CDPDescribeResponse:
|
|
31
|
+
response = await self.client.request("GET", "/resources/cdp/describe")
|
|
32
|
+
if response.status_code != 200:
|
|
33
|
+
await self.start()
|
|
34
|
+
response = await self.client.request("GET", "/resources/cdp/describe")
|
|
35
|
+
return CDPDescribeResponse(**response.json())
|
|
36
|
+
|
|
37
|
+
async def cdp_url(self) -> str:
|
|
38
|
+
return (await self.describe()).cdp_browser_url
|
|
39
|
+
|
|
40
|
+
async def devtools_url(self) -> str:
|
|
41
|
+
return (await self.describe()).cdp_devtools_url
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import Any, List, Optional
|
|
2
|
+
from ..instance.models import Resource as ResourceModel
|
|
3
|
+
from ..instance.models import DescribeResponse, QueryRequest, QueryResponse
|
|
4
|
+
from .base import Resource
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..instance.base import AsyncWrapper
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AsyncSQLiteResource(Resource):
|
|
13
|
+
def __init__(self, resource: ResourceModel, client: "AsyncWrapper"):
|
|
14
|
+
super().__init__(resource)
|
|
15
|
+
self.client = client
|
|
16
|
+
|
|
17
|
+
async def describe(self) -> DescribeResponse:
|
|
18
|
+
"""Describe the SQLite database schema."""
|
|
19
|
+
response = await self.client.request(
|
|
20
|
+
"GET", f"/resources/sqlite/{self.resource.name}/describe"
|
|
21
|
+
)
|
|
22
|
+
return DescribeResponse(**response.json())
|
|
23
|
+
|
|
24
|
+
async def query(
|
|
25
|
+
self, query: str, args: Optional[List[Any]] = None
|
|
26
|
+
) -> QueryResponse:
|
|
27
|
+
return await self._query(query, args, read_only=True)
|
|
28
|
+
|
|
29
|
+
async def exec(self, query: str, args: Optional[List[Any]] = None) -> QueryResponse:
|
|
30
|
+
return await self._query(query, args, read_only=False)
|
|
31
|
+
|
|
32
|
+
async def _query(
|
|
33
|
+
self, query: str, args: Optional[List[Any]] = None, read_only: bool = True
|
|
34
|
+
) -> QueryResponse:
|
|
35
|
+
request = QueryRequest(query=query, args=args, read_only=read_only)
|
|
36
|
+
response = await self.client.request(
|
|
37
|
+
"POST",
|
|
38
|
+
f"/resources/sqlite/{self.resource.name}/query",
|
|
39
|
+
json=request.model_dump(),
|
|
40
|
+
)
|
|
41
|
+
return QueryResponse(**response.json())
|
fleet/base.py
CHANGED
|
@@ -48,27 +48,4 @@ class SyncWrapper(BaseWrapper):
|
|
|
48
48
|
params=params,
|
|
49
49
|
json=json,
|
|
50
50
|
**kwargs,
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class AsyncWrapper(BaseWrapper):
|
|
55
|
-
def __init__(self, *, httpx_client: httpx.AsyncClient, **kwargs):
|
|
56
|
-
super().__init__(**kwargs)
|
|
57
|
-
self.httpx_client = httpx_client
|
|
58
|
-
|
|
59
|
-
async def request(
|
|
60
|
-
self,
|
|
61
|
-
method: str,
|
|
62
|
-
url: str,
|
|
63
|
-
params: Optional[Dict[str, Any]] = None,
|
|
64
|
-
json: Optional[Any] = None,
|
|
65
|
-
**kwargs,
|
|
66
|
-
) -> httpx.Response:
|
|
67
|
-
return await self.httpx_client.request(
|
|
68
|
-
method,
|
|
69
|
-
f"{self.base_url}{url}",
|
|
70
|
-
headers=self.get_headers(),
|
|
71
|
-
params=params,
|
|
72
|
-
json=json,
|
|
73
|
-
**kwargs,
|
|
74
|
-
)
|
|
51
|
+
)
|
fleet/client.py
CHANGED
|
@@ -14,26 +14,24 @@
|
|
|
14
14
|
|
|
15
15
|
"""Fleet API Client for making HTTP requests to Fleet services."""
|
|
16
16
|
|
|
17
|
-
import asyncio
|
|
18
17
|
import os
|
|
19
18
|
import httpx
|
|
20
19
|
import logging
|
|
21
20
|
from typing import Optional, List
|
|
22
21
|
|
|
23
|
-
from .base import EnvironmentBase,
|
|
22
|
+
from .base import EnvironmentBase, SyncWrapper
|
|
24
23
|
from .models import InstanceRequest, InstanceRecord, Environment as EnvironmentModel
|
|
25
24
|
|
|
26
25
|
from .instance import (
|
|
27
26
|
InstanceClient,
|
|
28
|
-
AsyncInstanceClient,
|
|
29
27
|
ResetRequest,
|
|
30
28
|
ResetResponse,
|
|
31
29
|
ValidatorType,
|
|
32
30
|
ExecuteFunctionResponse,
|
|
33
31
|
)
|
|
34
32
|
from .resources.base import Resource
|
|
35
|
-
from .resources.sqlite import
|
|
36
|
-
from .resources.browser import
|
|
33
|
+
from .resources.sqlite import SQLiteResource
|
|
34
|
+
from .resources.browser import BrowserResource
|
|
37
35
|
|
|
38
36
|
logger = logging.getLogger(__name__)
|
|
39
37
|
|
|
@@ -41,7 +39,7 @@ logger = logging.getLogger(__name__)
|
|
|
41
39
|
class Environment(EnvironmentBase):
|
|
42
40
|
def __init__(self, httpx_client: Optional[httpx.Client] = None, **kwargs):
|
|
43
41
|
super().__init__(**kwargs)
|
|
44
|
-
self._httpx_client = httpx_client or httpx.Client()
|
|
42
|
+
self._httpx_client = httpx_client or httpx.Client(timeout=60.0)
|
|
45
43
|
self._instance: Optional[InstanceClient] = None
|
|
46
44
|
|
|
47
45
|
@property
|
|
@@ -50,46 +48,33 @@ class Environment(EnvironmentBase):
|
|
|
50
48
|
self._instance = InstanceClient(self.manager_url, self._httpx_client)
|
|
51
49
|
return self._instance
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
class AsyncEnvironment(EnvironmentBase):
|
|
55
|
-
def __init__(self, httpx_client: Optional[httpx.AsyncClient] = None, **kwargs):
|
|
56
|
-
super().__init__(**kwargs)
|
|
57
|
-
self._httpx_client = httpx_client or httpx.AsyncClient(timeout=60.0)
|
|
58
|
-
self._instance: Optional[AsyncInstanceClient] = None
|
|
59
|
-
|
|
60
|
-
@property
|
|
61
|
-
def instance(self) -> AsyncInstanceClient:
|
|
62
|
-
if self._instance is None:
|
|
63
|
-
self._instance = AsyncInstanceClient(self.manager_url, self._httpx_client)
|
|
64
|
-
return self._instance
|
|
65
|
-
|
|
66
|
-
async def reset(
|
|
51
|
+
def reset(
|
|
67
52
|
self, seed: Optional[int] = None, timestamp: Optional[int] = None
|
|
68
53
|
) -> ResetResponse:
|
|
69
|
-
return
|
|
54
|
+
return self.instance.reset(ResetRequest(seed=seed, timestamp=timestamp))
|
|
70
55
|
|
|
71
|
-
def db(self, name: str = "current") ->
|
|
56
|
+
def db(self, name: str = "current") -> SQLiteResource:
|
|
72
57
|
return self.instance.db(name)
|
|
73
58
|
|
|
74
|
-
def browser(self, name: str = "cdp") ->
|
|
59
|
+
def browser(self, name: str = "cdp") -> BrowserResource:
|
|
75
60
|
return self.instance.browser(name)
|
|
76
61
|
|
|
77
62
|
def state(self, uri: str) -> Resource:
|
|
78
63
|
return self.instance.state(uri)
|
|
79
64
|
|
|
80
|
-
|
|
81
|
-
return
|
|
65
|
+
def resources(self) -> List[Resource]:
|
|
66
|
+
return self.instance.resources()
|
|
82
67
|
|
|
83
|
-
|
|
84
|
-
return
|
|
68
|
+
def close(self) -> InstanceRecord:
|
|
69
|
+
return Fleet().delete(self.instance_id)
|
|
85
70
|
|
|
86
|
-
|
|
87
|
-
return
|
|
71
|
+
def verify(self, validator: ValidatorType) -> ExecuteFunctionResponse:
|
|
72
|
+
return self.instance.verify(validator)
|
|
88
73
|
|
|
89
|
-
|
|
74
|
+
def verify_raw(
|
|
90
75
|
self, function_code: str, function_name: str
|
|
91
76
|
) -> ExecuteFunctionResponse:
|
|
92
|
-
return
|
|
77
|
+
return self.instance.verify_raw(function_code, function_name)
|
|
93
78
|
|
|
94
79
|
|
|
95
80
|
class Fleet:
|
|
@@ -106,7 +91,7 @@ class Fleet:
|
|
|
106
91
|
httpx_client=self._httpx_client,
|
|
107
92
|
)
|
|
108
93
|
|
|
109
|
-
def
|
|
94
|
+
def list_envs(self) -> List[EnvironmentModel]:
|
|
110
95
|
response = self.client.request("GET", "/v1/env/")
|
|
111
96
|
return [EnvironmentModel(**env_data) for env_data in response.json()]
|
|
112
97
|
|
|
@@ -114,52 +99,7 @@ class Fleet:
|
|
|
114
99
|
response = self.client.request("GET", f"/v1/env/{env_key}")
|
|
115
100
|
return EnvironmentModel(**response.json())
|
|
116
101
|
|
|
117
|
-
def make(self,
|
|
118
|
-
response = self.client.request(
|
|
119
|
-
"POST", "/v1/env/instances", json=request.model_dump()
|
|
120
|
-
)
|
|
121
|
-
return Environment(**response.json())
|
|
122
|
-
|
|
123
|
-
def instances(self, status: Optional[str] = None) -> List[Environment]:
|
|
124
|
-
params = {}
|
|
125
|
-
if status:
|
|
126
|
-
params["status"] = status
|
|
127
|
-
|
|
128
|
-
response = self.client.request("GET", "/v1/env/instances", params=params)
|
|
129
|
-
return [Environment(**instance_data) for instance_data in response.json()]
|
|
130
|
-
|
|
131
|
-
def instance(self, instance_id: str) -> Environment:
|
|
132
|
-
response = self.client.request("GET", f"/v1/env/instances/{instance_id}")
|
|
133
|
-
return Environment(**response.json())
|
|
134
|
-
|
|
135
|
-
def delete(self, instance_id: str) -> InstanceRecord:
|
|
136
|
-
response = self.client.request("DELETE", f"/v1/env/instances/{instance_id}")
|
|
137
|
-
return InstanceRecord(**response.json())
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
class AsyncFleet:
|
|
141
|
-
def __init__(
|
|
142
|
-
self,
|
|
143
|
-
api_key: Optional[str] = os.getenv("FLEET_API_KEY"),
|
|
144
|
-
base_url: Optional[str] = None,
|
|
145
|
-
httpx_client: Optional[httpx.AsyncClient] = None,
|
|
146
|
-
):
|
|
147
|
-
self._httpx_client = httpx_client or httpx.AsyncClient(timeout=60.0)
|
|
148
|
-
self.client = AsyncWrapper(
|
|
149
|
-
api_key=api_key,
|
|
150
|
-
base_url=base_url,
|
|
151
|
-
httpx_client=self._httpx_client,
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
async def list_envs(self) -> List[EnvironmentModel]:
|
|
155
|
-
response = await self.client.request("GET", "/v1/env/")
|
|
156
|
-
return [EnvironmentModel(**env_data) for env_data in response.json()]
|
|
157
|
-
|
|
158
|
-
async def environment(self, env_key: str) -> EnvironmentModel:
|
|
159
|
-
response = await self.client.request("GET", f"/v1/env/{env_key}")
|
|
160
|
-
return EnvironmentModel(**response.json())
|
|
161
|
-
|
|
162
|
-
async def make(self, env_key: str) -> AsyncEnvironment:
|
|
102
|
+
def make(self, env_key: str) -> Environment:
|
|
163
103
|
if ":" in env_key:
|
|
164
104
|
env_key_part, version = env_key.split(":", 1)
|
|
165
105
|
if not version.startswith("v"):
|
|
@@ -169,33 +109,25 @@ class AsyncFleet:
|
|
|
169
109
|
version = None
|
|
170
110
|
|
|
171
111
|
request = InstanceRequest(env_key=env_key_part, version=version)
|
|
172
|
-
response =
|
|
112
|
+
response = self.client.request(
|
|
173
113
|
"POST", "/v1/env/instances", json=request.model_dump()
|
|
174
114
|
)
|
|
175
|
-
instance =
|
|
176
|
-
|
|
115
|
+
instance = Environment(**response.json())
|
|
116
|
+
instance.instance.load()
|
|
177
117
|
return instance
|
|
178
118
|
|
|
179
|
-
|
|
119
|
+
def instances(self, status: Optional[str] = None) -> List[Environment]:
|
|
180
120
|
params = {}
|
|
181
121
|
if status:
|
|
182
122
|
params["status"] = status
|
|
183
123
|
|
|
184
|
-
response =
|
|
185
|
-
|
|
186
|
-
AsyncEnvironment(**instance_data) for instance_data in response.json()
|
|
187
|
-
]
|
|
188
|
-
await asyncio.gather(*[instance.instance.load() for instance in instances])
|
|
189
|
-
return instances
|
|
190
|
-
|
|
191
|
-
async def instance(self, instance_id: str) -> AsyncEnvironment:
|
|
192
|
-
response = await self.client.request("GET", f"/v1/env/instances/{instance_id}")
|
|
193
|
-
instance = AsyncEnvironment(**response.json())
|
|
194
|
-
await instance.instance.load()
|
|
195
|
-
return instance
|
|
124
|
+
response = self.client.request("GET", "/v1/env/instances", params=params)
|
|
125
|
+
return [Environment(**instance_data) for instance_data in response.json()]
|
|
196
126
|
|
|
197
|
-
|
|
198
|
-
response =
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
127
|
+
def instance(self, instance_id: str) -> Environment:
|
|
128
|
+
response = self.client.request("GET", f"/v1/env/instances/{instance_id}")
|
|
129
|
+
return Environment(**response.json())
|
|
130
|
+
|
|
131
|
+
def delete(self, instance_id: str) -> InstanceRecord:
|
|
132
|
+
response = self.client.request("DELETE", f"/v1/env/instances/{instance_id}")
|
|
133
|
+
return InstanceRecord(**response.json())
|
fleet/env/__init__.py
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
"""Fleet env module - convenience functions for environment management."""
|
|
2
|
+
|
|
1
3
|
from .client import make, list_envs, get
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
# Import async versions from _async
|
|
6
|
+
from .._async.env.client import make_async, list_envs_async, get_async
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"make",
|
|
10
|
+
"list_envs",
|
|
11
|
+
"get",
|
|
12
|
+
"make_async",
|
|
13
|
+
"list_envs_async",
|
|
14
|
+
"get_async",
|
|
15
|
+
]
|
fleet/env/client.py
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
from ..client import
|
|
1
|
+
from ..client import Fleet, Environment
|
|
2
2
|
from ..models import Environment as EnvironmentModel
|
|
3
3
|
from typing import List
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
return
|
|
6
|
+
def make(env_key: str) -> Environment:
|
|
7
|
+
return Fleet().make(env_key)
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
return
|
|
10
|
+
def list_envs() -> List[EnvironmentModel]:
|
|
11
|
+
return Fleet().list_envs()
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
return
|
|
14
|
+
def get(instance_id: str) -> Environment:
|
|
15
|
+
return Fleet().instance(instance_id)
|