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

Files changed (47) hide show
  1. examples/dsl_example.py +108 -92
  2. examples/example.py +2 -2
  3. examples/json_tasks_example.py +82 -0
  4. examples/nova_act_example.py +18 -169
  5. examples/openai_example.py +89 -311
  6. examples/openai_simple_example.py +60 -0
  7. examples/quickstart.py +5 -5
  8. fleet/__init__.py +32 -3
  9. fleet/_async/base.py +51 -0
  10. fleet/_async/client.py +133 -0
  11. fleet/_async/env/__init__.py +0 -0
  12. fleet/_async/env/client.py +15 -0
  13. fleet/_async/exceptions.py +73 -0
  14. fleet/{manager → _async/instance}/__init__.py +4 -2
  15. fleet/{manager → _async/instance}/base.py +1 -24
  16. fleet/{manager → _async/instance}/client.py +44 -24
  17. fleet/{manager → _async/instance}/models.py +13 -0
  18. fleet/_async/models.py +109 -0
  19. fleet/_async/playwright.py +291 -0
  20. fleet/_async/resources/__init__.py +0 -0
  21. fleet/_async/resources/base.py +26 -0
  22. fleet/_async/resources/browser.py +41 -0
  23. fleet/_async/resources/sqlite.py +41 -0
  24. fleet/base.py +1 -24
  25. fleet/client.py +42 -95
  26. fleet/env/__init__.py +13 -1
  27. fleet/env/client.py +7 -7
  28. fleet/instance/__init__.py +26 -0
  29. fleet/instance/base.py +37 -0
  30. fleet/instance/client.py +278 -0
  31. fleet/instance/models.py +141 -0
  32. fleet/playwright.py +289 -0
  33. fleet/resources/__init__.py +0 -0
  34. fleet/resources/base.py +1 -1
  35. fleet/resources/browser.py +20 -23
  36. fleet/resources/sqlite.py +13 -13
  37. fleet/verifiers/__init__.py +10 -3
  38. fleet/verifiers/code.py +1 -0
  39. fleet/verifiers/{database_snapshot.py → db.py} +62 -22
  40. fleet/verifiers/sql_differ.py +1 -1
  41. {fleet_python-0.2.2.dist-info → fleet_python-0.2.4.dist-info}/METADATA +4 -1
  42. fleet_python-0.2.4.dist-info/RECORD +48 -0
  43. {fleet_python-0.2.2.dist-info → fleet_python-0.2.4.dist-info}/top_level.txt +1 -0
  44. scripts/unasync.py +28 -0
  45. fleet_python-0.2.2.dist-info/RECORD +0 -27
  46. {fleet_python-0.2.2.dist-info → fleet_python-0.2.4.dist-info}/WHEEL +0 -0
  47. {fleet_python-0.2.2.dist-info → fleet_python-0.2.4.dist-info}/licenses/LICENSE +0 -0
fleet/playwright.py ADDED
@@ -0,0 +1,289 @@
1
+ import base64
2
+ from typing import List, Dict, Any
3
+ from playwright.sync_api import sync_playwright, Browser, Page
4
+ from .client import Environment
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: Environment,
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
+ def start(self):
90
+ """Start the browser and establish connection."""
91
+ if self._started:
92
+ return
93
+
94
+ # Start Playwright
95
+ self._playwright = sync_playwright().start()
96
+
97
+ # Start browser on the Fleet instance
98
+ print("Starting browser...")
99
+ self.env.browser().start()
100
+ cdp = self.env.browser().describe()
101
+
102
+ # Connect to browser
103
+ self._browser = self._playwright.chromium.connect_over_cdp(cdp.cdp_browser_url)
104
+ self._page = self._browser.contexts[0].pages[0]
105
+ self._page.set_viewport_size(
106
+ {"width": self.display_width, "height": self.display_height}
107
+ )
108
+
109
+ self._started = True
110
+ print(f"Track agent: {cdp.cdp_devtools_url}")
111
+
112
+ def close(self):
113
+ """Close the browser connection."""
114
+ if self._playwright:
115
+ self._playwright.stop()
116
+ self._playwright = None
117
+ self._browser = None
118
+ self._page = None
119
+ self._started = False
120
+
121
+ def _ensure_started(self):
122
+ """Ensure browser is started before operations."""
123
+ if not self._started:
124
+ raise RuntimeError("Browser not started. Call await browser.start() first.")
125
+
126
+ @property
127
+ def openai_cua_tool(self) -> Dict[str, Any]:
128
+ """
129
+ Tool definition for OpenAI computer use API.
130
+
131
+ Returns:
132
+ Tool definition dict for use with OpenAI responses API
133
+ """
134
+ return {
135
+ "type": "computer_use_preview",
136
+ "display_width": self.display_width,
137
+ "display_height": self.display_height,
138
+ "environment": "browser",
139
+ }
140
+
141
+ def screenshot(self) -> str:
142
+ """
143
+ Take a screenshot and return base64 encoded string.
144
+
145
+ Returns:
146
+ Base64 encoded PNG screenshot
147
+ """
148
+ self._ensure_started()
149
+
150
+ png_bytes = self._page.screenshot(full_page=False)
151
+ return base64.b64encode(png_bytes).decode("utf-8")
152
+
153
+ def get_current_url(self) -> str:
154
+ """Get the current page URL."""
155
+ self._ensure_started()
156
+ return self._page.url
157
+
158
+ def execute_computer_action(self, action: Dict[str, Any]) -> Dict[str, Any]:
159
+ """
160
+ Execute a computer action and return the result for OpenAI API.
161
+
162
+ Args:
163
+ action: Computer action dict from OpenAI response
164
+
165
+ Returns:
166
+ Result dict for computer_call_output
167
+ """
168
+ self._ensure_started()
169
+
170
+ action_type = action["type"]
171
+ action_args = {k: v for k, v in action.items() if k != "type"}
172
+
173
+ print(f"Executing: {action_type}({action_args})")
174
+
175
+ # Execute the action
176
+ if hasattr(self, f"_{action_type}"):
177
+ method = getattr(self, f"_{action_type}")
178
+ method(**action_args)
179
+ else:
180
+ raise ValueError(f"Unsupported action type: {action_type}")
181
+
182
+ # Take screenshot after action
183
+ screenshot_base64 = self.screenshot()
184
+
185
+ return {
186
+ "type": "input_image",
187
+ "image_url": f"data:image/png;base64,{screenshot_base64}",
188
+ "current_url": self.get_current_url(),
189
+ }
190
+
191
+ # Computer action implementations
192
+ def _click(self, x: int, y: int, button: str = "left") -> None:
193
+ """Click at coordinates."""
194
+ self._ensure_started()
195
+ self._page.mouse.click(x, y, button=button)
196
+
197
+ def _double_click(self, x: int, y: int) -> None:
198
+ """Double-click at coordinates."""
199
+ self._ensure_started()
200
+ self._page.mouse.dblclick(x, y)
201
+
202
+ def _scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None:
203
+ """Scroll from coordinates."""
204
+ self._ensure_started()
205
+ self._page.mouse.move(x, y)
206
+ self._page.evaluate(f"window.scrollBy({scroll_x}, {scroll_y})")
207
+
208
+ def _type(self, text: str) -> None:
209
+ """Type text."""
210
+ self._ensure_started()
211
+ self._page.keyboard.type(text)
212
+
213
+ def _keypress(self, keys: List[str]) -> None:
214
+ """Press key combination."""
215
+ self._ensure_started()
216
+ mapped_keys = [CUA_KEY_TO_PLAYWRIGHT_KEY.get(key.lower(), key) for key in keys]
217
+ for key in mapped_keys:
218
+ self._page.keyboard.down(key)
219
+ for key in reversed(mapped_keys):
220
+ self._page.keyboard.up(key)
221
+
222
+ def _move(self, x: int, y: int) -> None:
223
+ """Move mouse to coordinates."""
224
+ self._ensure_started()
225
+ self._page.mouse.move(x, y)
226
+
227
+ def _drag(self, path: List[Dict[str, int]]) -> None:
228
+ """Drag mouse along path."""
229
+ self._ensure_started()
230
+ if not path:
231
+ return
232
+ self._page.mouse.move(path[0]["x"], path[0]["y"])
233
+ self._page.mouse.down()
234
+ for point in path[1:]:
235
+ self._page.mouse.move(point["x"], point["y"])
236
+ self._page.mouse.up()
237
+
238
+ def _wait(self, ms: int = 1000) -> None:
239
+ """Wait for specified milliseconds."""
240
+ import asyncio
241
+
242
+ asyncio.sleep(ms / 1000)
243
+
244
+ # Browser-specific actions
245
+ def _goto(self, url: str) -> None:
246
+ """Navigate to URL."""
247
+ self._ensure_started()
248
+ try:
249
+ self._page.goto(url)
250
+ except Exception as e:
251
+ print(f"Error navigating to {url}: {e}")
252
+
253
+ def _back(self) -> None:
254
+ """Go back in browser history."""
255
+ self._ensure_started()
256
+ self._page.go_back()
257
+
258
+ def _forward(self) -> None:
259
+ """Go forward in browser history."""
260
+ self._ensure_started()
261
+ self._page.go_forward()
262
+
263
+ def _refresh(self) -> None:
264
+ """Refresh the page."""
265
+ self._ensure_started()
266
+ self._page.reload()
267
+
268
+ # ------------------------------------------------------------------
269
+ # Public aliases (no leading underscore) expected by the Agent &
270
+ # OpenAI computer-use API. They forward directly to the underscored
271
+ # implementations above so the external interface matches the older
272
+ # BasePlaywrightComputer class.
273
+ # ------------------------------------------------------------------
274
+
275
+ # Mouse / keyboard actions
276
+ click = _click
277
+ double_click = _double_click
278
+ scroll = _scroll
279
+ type = _type # noqa: A003 – shadowing built-in for API compatibility
280
+ keypress = _keypress
281
+ move = _move
282
+ drag = _drag
283
+ wait = _wait
284
+
285
+ # Browser navigation actions
286
+ goto = _goto
287
+ back = _back
288
+ forward = _forward
289
+ refresh = _refresh
File without changes
fleet/resources/base.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from abc import ABC
2
- from ..manager.models import Resource as ResourceModel, ResourceType, ResourceMode
2
+ from ..instance.models import Resource as ResourceModel, ResourceType, ResourceMode
3
3
 
4
4
 
5
5
  class Resource(ABC):
@@ -1,5 +1,5 @@
1
1
  from typing import Optional
2
- from ..manager.models import (
2
+ from ..instance.models import (
3
3
  Resource as ResourceModel,
4
4
  CDPDescribeResponse,
5
5
  ChromeStartRequest,
@@ -10,35 +10,32 @@ from .base import Resource
10
10
  from typing import TYPE_CHECKING
11
11
 
12
12
  if TYPE_CHECKING:
13
- from ..manager.base import AsyncWrapper
13
+ from ..instance.base import SyncWrapper
14
14
 
15
15
 
16
- class AsyncBrowserResource(Resource):
17
- def __init__(self, resource: ResourceModel, client: "AsyncWrapper"):
16
+ class BrowserResource(Resource):
17
+ def __init__(self, resource: ResourceModel, client: "SyncWrapper"):
18
18
  super().__init__(resource)
19
19
  self.client = client
20
- self._describe: Optional[CDPDescribeResponse] = None
21
20
 
22
- async def start(self, width: int = 1920, height: int = 1080) -> CDPDescribeResponse:
23
- response = await self.client.request(
21
+ def start(self, width: int = 1920, height: int = 1080) -> CDPDescribeResponse:
22
+ response = self.client.request(
24
23
  "POST",
25
24
  "/resources/cdp/start",
26
25
  json=ChromeStartRequest(resolution=f"{width},{height}").model_dump(),
27
26
  )
28
27
  ChromeStartResponse(**response.json())
29
- return await self.describe()
30
-
31
- async def describe(self) -> CDPDescribeResponse:
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
28
+ return self.describe()
29
+
30
+ def describe(self) -> CDPDescribeResponse:
31
+ response = self.client.request("GET", "/resources/cdp/describe")
32
+ if response.status_code != 200:
33
+ self.start()
34
+ response = self.client.request("GET", "/resources/cdp/describe")
35
+ return CDPDescribeResponse(**response.json())
36
+
37
+ def cdp_url(self) -> str:
38
+ return (self.describe()).cdp_browser_url
39
+
40
+ def devtools_url(self) -> str:
41
+ return (self.describe()).cdp_devtools_url
fleet/resources/sqlite.py CHANGED
@@ -1,39 +1,39 @@
1
1
  from typing import Any, List, Optional
2
- from ..manager.models import Resource as ResourceModel
3
- from ..manager.models import DescribeResponse, QueryRequest, QueryResponse
2
+ from ..instance.models import Resource as ResourceModel
3
+ from ..instance.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 ..manager.base import AsyncWrapper
9
+ from ..instance.base import SyncWrapper
10
10
 
11
11
 
12
- class AsyncSQLiteResource(Resource):
13
- def __init__(self, resource: ResourceModel, client: "AsyncWrapper"):
12
+ class SQLiteResource(Resource):
13
+ def __init__(self, resource: ResourceModel, client: "SyncWrapper"):
14
14
  super().__init__(resource)
15
15
  self.client = client
16
16
 
17
- async def describe(self) -> DescribeResponse:
17
+ def describe(self) -> DescribeResponse:
18
18
  """Describe the SQLite database schema."""
19
- response = await self.client.request(
19
+ response = self.client.request(
20
20
  "GET", f"/resources/sqlite/{self.resource.name}/describe"
21
21
  )
22
22
  return DescribeResponse(**response.json())
23
23
 
24
- async def query(
24
+ def query(
25
25
  self, query: str, args: Optional[List[Any]] = None
26
26
  ) -> QueryResponse:
27
- return await self._query(query, args, read_only=True)
27
+ return self._query(query, args, read_only=True)
28
28
 
29
- async def exec(self, query: str, args: Optional[List[Any]] = None) -> QueryResponse:
30
- return await self._query(query, args, read_only=False)
29
+ def exec(self, query: str, args: Optional[List[Any]] = None) -> QueryResponse:
30
+ return self._query(query, args, read_only=False)
31
31
 
32
- async def _query(
32
+ def _query(
33
33
  self, query: str, args: Optional[List[Any]] = None, read_only: bool = True
34
34
  ) -> QueryResponse:
35
35
  request = QueryRequest(query=query, args=args, read_only=read_only)
36
- response = await self.client.request(
36
+ response = self.client.request(
37
37
  "POST",
38
38
  f"/resources/sqlite/{self.resource.name}/query",
39
39
  json=request.model_dump(),
@@ -1,4 +1,11 @@
1
- from .database_snapshot import QueryBuilder, DatabaseSnapshot
2
- from .sql_differ import SQLiteDiffer
1
+ """Fleet verifiers module - database snapshot validation utilities."""
3
2
 
4
- __all__ = ["QueryBuilder", "DatabaseSnapshot", "SQLiteDiffer"]
3
+ from .db import DatabaseSnapshot, IgnoreConfig, SnapshotDiff
4
+ from .code import TASK_SUCCESSFUL_SCORE
5
+
6
+ __all__ = [
7
+ "DatabaseSnapshot",
8
+ "IgnoreConfig",
9
+ "SnapshotDiff",
10
+ "TASK_SUCCESSFUL_SCORE",
11
+ ]
@@ -0,0 +1 @@
1
+ TASK_SUCCESSFUL_SCORE = 1
@@ -1,4 +1,3 @@
1
- # database_dsl.py
2
1
  """A schema‑agnostic, SQL‑native DSL for snapshot validation and diff invariants.
3
2
 
4
3
  The module extends your original `DatabaseSnapshot` implementation with
@@ -11,11 +10,13 @@ The module extends your original `DatabaseSnapshot` implementation with
11
10
  The public API stays tiny yet composable; everything else is built on
12
11
  orthogonal primitives so it works for *any* relational schema.
13
12
  """
13
+
14
14
  from __future__ import annotations
15
15
 
16
16
  import sqlite3
17
17
  from datetime import datetime
18
18
  from typing import Any
19
+ import json
19
20
 
20
21
  ################################################################################
21
22
  # Low‑level helpers
@@ -26,6 +27,36 @@ Condition = tuple[str, str, SQLValue] # (column, op, value)
26
27
  JoinSpec = tuple[str, dict[str, str]] # (table, on mapping)
27
28
 
28
29
 
30
+ def _is_json_string(value: Any) -> bool:
31
+ """Check if a value looks like a JSON string."""
32
+ if not isinstance(value, str):
33
+ return False
34
+ value = value.strip()
35
+ return (value.startswith("{") and value.endswith("}")) or (
36
+ value.startswith("[") and value.endswith("]")
37
+ )
38
+
39
+
40
+ def _values_equivalent(val1: Any, val2: Any) -> bool:
41
+ """Compare two values, using JSON semantic comparison for JSON strings."""
42
+ # If both are exactly equal, return True
43
+ if val1 == val2:
44
+ return True
45
+
46
+ # If both look like JSON strings, try semantic comparison
47
+ if _is_json_string(val1) and _is_json_string(val2):
48
+ try:
49
+ parsed1 = json.loads(val1)
50
+ parsed2 = json.loads(val2)
51
+ return parsed1 == parsed2
52
+ except (json.JSONDecodeError, TypeError):
53
+ # If parsing fails, fall back to string comparison
54
+ pass
55
+
56
+ # Default to exact comparison
57
+ return val1 == val2
58
+
59
+
29
60
  class _CountResult:
30
61
  """Wraps an integer count so we can chain assertions fluently."""
31
62
 
@@ -96,9 +127,7 @@ class QueryBuilder:
96
127
  # ---------------------------------------------------------------------
97
128
  # WHERE helpers (SQL‑like)
98
129
  # ---------------------------------------------------------------------
99
- def _add_condition(
100
- self, column: str, op: str, value: SQLValue
101
- ) -> "QueryBuilder": # noqa: UP037
130
+ def _add_condition(self, column: str, op: str, value: SQLValue) -> "QueryBuilder": # noqa: UP037
102
131
  qb = self._clone()
103
132
  qb._conditions.append((column, op, value))
104
133
  return qb
@@ -126,9 +155,7 @@ class QueryBuilder:
126
155
  qb._conditions.append((column, "IN", tuple(values)))
127
156
  return qb
128
157
 
129
- def not_in(
130
- self, column: str, values: list[SQLValue]
131
- ) -> "QueryBuilder": # noqa: UP037
158
+ def not_in(self, column: str, values: list[SQLValue]) -> "QueryBuilder": # noqa: UP037
132
159
  qb = self._clone()
133
160
  qb._conditions.append((column, "NOT IN", tuple(values)))
134
161
  return qb
@@ -147,9 +174,7 @@ class QueryBuilder:
147
174
  # ---------------------------------------------------------------------
148
175
  # JOIN (simple inner join)
149
176
  # ---------------------------------------------------------------------
150
- def join(
151
- self, other_table: str, on: dict[str, str]
152
- ) -> "QueryBuilder": # noqa: UP037
177
+ def join(self, other_table: str, on: dict[str, str]) -> "QueryBuilder": # noqa: UP037
153
178
  """`on` expects {local_col: remote_col}."""
154
179
  qb = self._clone()
155
180
  qb._joins.append((other_table, on))
@@ -166,7 +191,8 @@ class QueryBuilder:
166
191
  # Joins -------------------------------------------------------------
167
192
  for tbl, onmap in self._joins:
168
193
  join_clauses = [
169
- f"{self._table}.{l} = {tbl}.{r}" for l, r in onmap.items() # noqa: E741
194
+ f"{self._table}.{l} = {tbl}.{r}"
195
+ for l, r in onmap.items() # noqa: E741
170
196
  ]
171
197
  sql.append(f"JOIN {tbl} ON {' AND '.join(join_clauses)}")
172
198
 
@@ -430,10 +456,27 @@ class SnapshotDiff:
430
456
  def expect_only(self, allowed_changes: list[dict[str, Any]]):
431
457
  """Allowed changes is a list of {table, pk, field, after} (before optional)."""
432
458
  diff = self._collect()
433
- allowed_set = {
434
- (c["table"], c.get("pk"), c.get("field"), c.get("after"))
435
- for c in allowed_changes
436
- }
459
+
460
+ def _is_change_allowed(
461
+ table: str, row_id: str, field: str | None, after_value: Any
462
+ ) -> bool:
463
+ """Check if a change is in the allowed list using semantic comparison."""
464
+ for allowed in allowed_changes:
465
+ allowed_pk = allowed.get("pk")
466
+ # Handle type conversion for primary key comparison
467
+ # Convert both to strings for comparison to handle int/string mismatches
468
+ pk_match = (
469
+ str(allowed_pk) == str(row_id) if allowed_pk is not None else False
470
+ )
471
+
472
+ if (
473
+ allowed["table"] == table
474
+ and pk_match
475
+ and allowed.get("field") == field
476
+ and _values_equivalent(allowed.get("after"), after_value)
477
+ ):
478
+ return True
479
+ return False
437
480
 
438
481
  # Collect all unexpected changes for detailed reporting
439
482
  unexpected_changes = []
@@ -443,8 +486,7 @@ class SnapshotDiff:
443
486
  for f, vals in row["changes"].items():
444
487
  if self.ignore_config.should_ignore_field(tbl, f):
445
488
  continue
446
- tup = (tbl, row["row_id"], f, vals["after"])
447
- if tup not in allowed_set:
489
+ if not _is_change_allowed(tbl, row["row_id"], f, vals["after"]):
448
490
  unexpected_changes.append(
449
491
  {
450
492
  "type": "modification",
@@ -458,8 +500,7 @@ class SnapshotDiff:
458
500
  )
459
501
 
460
502
  for row in report.get("added_rows", []):
461
- tup = (tbl, row["row_id"], None, "__added__")
462
- if tup not in allowed_set:
503
+ if not _is_change_allowed(tbl, row["row_id"], None, "__added__"):
463
504
  unexpected_changes.append(
464
505
  {
465
506
  "type": "insertion",
@@ -472,8 +513,7 @@ class SnapshotDiff:
472
513
  )
473
514
 
474
515
  for row in report.get("removed_rows", []):
475
- tup = (tbl, row["row_id"], None, "__removed__")
476
- if tup not in allowed_set:
516
+ if not _is_change_allowed(tbl, row["row_id"], None, "__removed__"):
477
517
  unexpected_changes.append(
478
518
  {
479
519
  "type": "deletion",
@@ -663,4 +703,4 @@ class DatabaseSnapshot:
663
703
 
664
704
  # ---------------------------------------------------------------------
665
705
  def __repr__(self):
666
- return f"<DatabaseSnapshot {self.name} at {self.db_path}>"
706
+ return f"<DatabaseSnapshot {self.name} at {self.db_path}>"
@@ -184,4 +184,4 @@ class SQLiteDiffer:
184
184
  except Exception as e:
185
185
  results[table] = {"error": str(e)}
186
186
 
187
- return results
187
+ return results
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -31,6 +31,9 @@ Requires-Dist: black>=22.0.0; extra == "dev"
31
31
  Requires-Dist: isort>=5.0.0; extra == "dev"
32
32
  Requires-Dist: mypy>=1.0.0; extra == "dev"
33
33
  Requires-Dist: ruff>=0.1.0; extra == "dev"
34
+ Requires-Dist: unasync>=0.6.0; extra == "dev"
35
+ Provides-Extra: playwright
36
+ Requires-Dist: playwright>=1.40.0; extra == "playwright"
34
37
  Dynamic: license-file
35
38
 
36
39
  # Fleet SDK
@@ -0,0 +1,48 @@
1
+ examples/dsl_example.py,sha256=nBfKdv4u6XuM97TG-ugfVStPdtoYq86ildFzLcQqGIk,5315
2
+ examples/example.py,sha256=B1ERVudPsPx1D1od3gCQU8MnG4FxuSBm0p7otkvIaLw,945
3
+ examples/json_tasks_example.py,sha256=_y4w94FfAaMBGgIqhVlrDU0PXbinNDWrT28DsUMvd9Y,2354
4
+ examples/nova_act_example.py,sha256=YMfx_7O1_6pqsy0Rf6Yabyqyg4_kI3cgMBlMJjAQXW4,790
5
+ examples/openai_example.py,sha256=5ZvBC0dcw8gPHoC6Gn-1KZx3CmUQBlBmPJgOrcgMSVM,8188
6
+ examples/openai_simple_example.py,sha256=pS2z7Q3s7tml5hbahc1NGgMXoF0mWwhi7VeLDTA4sE4,1702
7
+ examples/quickstart.py,sha256=lVRzbnWIweU9ioe7uk6R2Rm7oSpt4mt8Jq_VUUp1zKg,4696
8
+ fleet/__init__.py,sha256=CxDokocjQsv_JUNwhSqnijpQVXhQx73CXLu2Cp-rtgk,2087
9
+ fleet/base.py,sha256=t4xkgazl8kEP05JFjNByyf39RvvASRP0GsvxuoqKPY0,1395
10
+ fleet/client.py,sha256=SQEUoj4DMuXa4m5wfPPCkBEhQc7f6941K9V8Qk6ZF0U,4675
11
+ fleet/exceptions.py,sha256=yG3QWprCw1OnF-vdFBFJWE4m3ftBLBng31Dr__VbjI4,2249
12
+ fleet/models.py,sha256=Jf6Zmk689TPXhTSnVENK_VCw0VsujWzEWsN3T29MQ0k,3713
13
+ fleet/playwright.py,sha256=LWx_1UlNZPo117Lf7qnBlWT4RJA0OcwqRVRdFE98bSQ,8627
14
+ fleet/_async/base.py,sha256=hUch1I5oUPgaCXR3IpJ8f_PjigifAZg2-LR7BJdZSo8,1413
15
+ fleet/_async/client.py,sha256=pL0csdrAJKltA9km6DHDRogPpTEzi8AH32mS1efxNgg,4920
16
+ fleet/_async/exceptions.py,sha256=yG3QWprCw1OnF-vdFBFJWE4m3ftBLBng31Dr__VbjI4,2249
17
+ fleet/_async/models.py,sha256=Jf6Zmk689TPXhTSnVENK_VCw0VsujWzEWsN3T29MQ0k,3713
18
+ fleet/_async/playwright.py,sha256=5MLFPE5P_-MpzAQ3EJ6GsLthuJiWwYkNuvhPE_rwe_E,8914
19
+ fleet/_async/env/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ fleet/_async/env/client.py,sha256=Gft6pNkUK4PYMV8VE3KyS5R_-HuqGO5HEjAUuCPObiw,440
21
+ fleet/_async/instance/__init__.py,sha256=jIt-7EEJ0WM_ipheT_s0lniCbLei6yUdN0qQv1bMJ3E,524
22
+ fleet/_async/instance/base.py,sha256=QgcCTHdcqhi5VQi6_a1uuR-uO2_2Z19-RwVPp1k266A,947
23
+ fleet/_async/instance/client.py,sha256=Llc-yefnbL3fkBsayU4ANEqy_fFB2Uy6jNhTkfGj1mU,9362
24
+ fleet/_async/instance/models.py,sha256=ZTiue0YOuhuwX8jYfJAoCzGfqjLqqXRLqK1LVFhq6rQ,4183
25
+ fleet/_async/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
+ fleet/_async/resources/base.py,sha256=203gD54NP1IvjuSqFo-f7FvrkhtjChggtzrxJK7xf2E,667
27
+ fleet/_async/resources/browser.py,sha256=x11y4aKHogIEv83FByHtExerjV-cDWI3U62349Guq_Q,1368
28
+ fleet/_async/resources/sqlite.py,sha256=sRiII_qJ8X6-FSemlBsXThz4ZPjkNy9wDT8g5UAz2XM,1501
29
+ fleet/env/__init__.py,sha256=_lvYBqieXWmvU_dyPi2seSpLO3AZh5kdprdqFeefkzk,338
30
+ fleet/env/client.py,sha256=UGPrRlu89NM2RFdY7m6cGwUjOHJdtiPe4vkz-f2ByRg,351
31
+ fleet/instance/__init__.py,sha256=cdVC50HLLp2y7yf1Ga5wpLiy-hmamxmyibH0NDG7xI4,597
32
+ fleet/instance/base.py,sha256=U-qW1EQVBo6yvMpP1JeKiPRhCjZ3y3aTsYFhLPNOTtQ,929
33
+ fleet/instance/client.py,sha256=RFp5R703pP3tjKd_HBozJJ2qNie7LpMUx9NYjyPnPOI,9128
34
+ fleet/instance/models.py,sha256=ZTiue0YOuhuwX8jYfJAoCzGfqjLqqXRLqK1LVFhq6rQ,4183
35
+ fleet/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
+ fleet/resources/base.py,sha256=203gD54NP1IvjuSqFo-f7FvrkhtjChggtzrxJK7xf2E,667
37
+ fleet/resources/browser.py,sha256=hRNM0YMsVQUAraZGNi_B-KXxLpuddy4ntoEDFSw7czU,1295
38
+ fleet/resources/sqlite.py,sha256=sZVWsuq46ger-ta6PSlqcXGJG8iWpNQVj0CPmDNBXv8,1446
39
+ fleet/verifiers/__init__.py,sha256=mRMN8x0gDWFJ1MRLqdBtQw0gn_q8kDV3lMLyoiEf1yY,281
40
+ fleet/verifiers/code.py,sha256=NJ4OLZnpqLkI1lXY7-5m2GuZklLxMzHUCnRMVyN2_OI,25
41
+ fleet/verifiers/db.py,sha256=tssmvJjDHuBIy8qlL_P5-UdmEFUw2DZcqLsWZ8ot3Xw,27766
42
+ fleet/verifiers/sql_differ.py,sha256=dmiGCFXVMEMbAX519OjhVqgA8ZvhnvdmC1BVpL7QCF0,6490
43
+ fleet_python-0.2.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
44
+ scripts/unasync.py,sha256=--Fmaae47o-dZ1HYgX1c3Nvi-rMjcFymTRlJcWWnmpw,725
45
+ fleet_python-0.2.4.dist-info/METADATA,sha256=WbcSC3GkduODoS30lm4TE1YMX6WEJ92HdMwHb-igUR4,3199
46
+ fleet_python-0.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
47
+ fleet_python-0.2.4.dist-info/top_level.txt,sha256=_3DSmTohvSDf3AIP_BYfGzhwO1ECFwuzg83X-wHCx3Y,23
48
+ fleet_python-0.2.4.dist-info/RECORD,,
@@ -1,2 +1,3 @@
1
1
  examples
2
2
  fleet
3
+ scripts