fleet-python 0.2.128__py3-none-any.whl → 0.2.129__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.
- fleet/__init__.py +7 -1
- fleet/_async/__init__.py +1 -1
- fleet/_async/base.py +1 -1
- fleet/_async/browser.py +191 -0
- fleet/_async/client.py +94 -5
- fleet/_async/models.py +2 -0
- fleet/base.py +1 -1
- fleet/browser.py +216 -0
- fleet/client.py +99 -5
- fleet/models.py +2 -0
- {fleet_python-0.2.128.dist-info → fleet_python-0.2.129.dist-info}/METADATA +1 -1
- {fleet_python-0.2.128.dist-info → fleet_python-0.2.129.dist-info}/RECORD +16 -14
- {fleet_python-0.2.128.dist-info → fleet_python-0.2.129.dist-info}/WHEEL +0 -0
- {fleet_python-0.2.128.dist-info → fleet_python-0.2.129.dist-info}/entry_points.txt +0 -0
- {fleet_python-0.2.128.dist-info → fleet_python-0.2.129.dist-info}/licenses/LICENSE +0 -0
- {fleet_python-0.2.128.dist-info → fleet_python-0.2.129.dist-info}/top_level.txt +0 -0
fleet/__init__.py
CHANGED
|
@@ -26,6 +26,8 @@ from .exceptions import (
|
|
|
26
26
|
)
|
|
27
27
|
from .client import Fleet, SyncEnv, Session
|
|
28
28
|
from ._async.client import AsyncFleet, AsyncEnv, AsyncSession
|
|
29
|
+
from .browser import BrowserLease, host_from_url
|
|
30
|
+
from ._async.browser import AsyncBrowserLease
|
|
29
31
|
from .models import InstanceResponse, Environment, Run
|
|
30
32
|
from .instance.models import Resource, ResetResponse
|
|
31
33
|
|
|
@@ -76,7 +78,7 @@ from . import env
|
|
|
76
78
|
from . import global_client as _global_client
|
|
77
79
|
from ._async import global_client as _async_global_client
|
|
78
80
|
|
|
79
|
-
__version__ = "0.2.
|
|
81
|
+
__version__ = "0.2.129"
|
|
80
82
|
|
|
81
83
|
__all__ = [
|
|
82
84
|
# Core classes
|
|
@@ -84,6 +86,10 @@ __all__ = [
|
|
|
84
86
|
"SyncEnv",
|
|
85
87
|
"AsyncFleet",
|
|
86
88
|
"AsyncEnv",
|
|
89
|
+
# Browser lease (orchestrator-managed /v1/browser)
|
|
90
|
+
"BrowserLease",
|
|
91
|
+
"AsyncBrowserLease",
|
|
92
|
+
"host_from_url",
|
|
87
93
|
# Models
|
|
88
94
|
"InstanceResponse",
|
|
89
95
|
"SyncEnv",
|
fleet/_async/__init__.py
CHANGED
fleet/_async/base.py
CHANGED
fleet/_async/browser.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Copyright 2025 Fleet AI
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
"""Async mirror of ``fleet.browser``.
|
|
10
|
+
|
|
11
|
+
See :mod:`fleet.browser` for design notes. Kept as a separate file so the
|
|
12
|
+
async surface stays cleanly importable without dragging the sync wrapper
|
|
13
|
+
through any asyncio shims.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
from ..browser import host_from_url # re-export, identical logic
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from .base import AsyncWrapper
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_BROWSER_PATH = "/v1/browser"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _extra_headers(
|
|
31
|
+
jwt_token: Optional[str] = None, team_id: Optional[str] = None
|
|
32
|
+
) -> Optional[Dict[str, str]]:
|
|
33
|
+
headers: Dict[str, str] = {}
|
|
34
|
+
if jwt_token:
|
|
35
|
+
headers["X-JWT-Token"] = jwt_token
|
|
36
|
+
if team_id:
|
|
37
|
+
headers["X-Team-ID"] = team_id
|
|
38
|
+
return headers or None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AsyncBrowserLease:
|
|
42
|
+
"""Async counterpart of :class:`fleet.browser.BrowserLease`."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, client: "AsyncWrapper", data: Dict[str, Any]):
|
|
45
|
+
self._client = client
|
|
46
|
+
self._raw: Dict[str, Any] = dict(data)
|
|
47
|
+
self._jwt_token: Optional[str] = None
|
|
48
|
+
self._team_id: Optional[str] = None
|
|
49
|
+
self._apply(data)
|
|
50
|
+
|
|
51
|
+
def _apply(self, data: Dict[str, Any]) -> None:
|
|
52
|
+
self.id: str = data.get("id") or data["lease_id"]
|
|
53
|
+
self.lease_id: str = data["lease_id"]
|
|
54
|
+
self.browser_id: Optional[str] = data.get("browser_id")
|
|
55
|
+
self.host_domain: Optional[str] = data.get("host_domain")
|
|
56
|
+
self.mcp_url: Optional[str] = data.get("mcp_url")
|
|
57
|
+
self.cdp_url: Optional[str] = data.get("cdp_url")
|
|
58
|
+
self.stream_url: Optional[str] = data.get("stream_url")
|
|
59
|
+
self.status: Optional[str] = data.get("status")
|
|
60
|
+
self.stream_ready: Optional[bool] = data.get("stream_ready")
|
|
61
|
+
self.allowed_hosts: Optional[List[str]] = data.get("allowed_hosts")
|
|
62
|
+
self.created_timestamp_ms: Optional[int] = data.get("created_timestamp_ms")
|
|
63
|
+
self.expires_timestamp_ms: Optional[int] = data.get("expires_timestamp_ms")
|
|
64
|
+
self.age_duration_ms: Optional[int] = data.get("age_duration_ms")
|
|
65
|
+
self.cluster_name: Optional[str] = data.get("cluster_name")
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def raw(self) -> Dict[str, Any]:
|
|
69
|
+
return self._raw
|
|
70
|
+
|
|
71
|
+
async def refresh(self) -> "AsyncBrowserLease":
|
|
72
|
+
response = await self._client.request(
|
|
73
|
+
"GET",
|
|
74
|
+
f"{_BROWSER_PATH}/{self.lease_id}",
|
|
75
|
+
extra_headers=_extra_headers(self._jwt_token, self._team_id),
|
|
76
|
+
)
|
|
77
|
+
self._raw = response.json()
|
|
78
|
+
self._apply(self._raw)
|
|
79
|
+
return self
|
|
80
|
+
|
|
81
|
+
async def wait_until_running(
|
|
82
|
+
self,
|
|
83
|
+
timeout: float = 60.0,
|
|
84
|
+
poll_interval: float = 1.0,
|
|
85
|
+
require_stream_ready: bool = False,
|
|
86
|
+
) -> "AsyncBrowserLease":
|
|
87
|
+
loop = asyncio.get_event_loop()
|
|
88
|
+
deadline = loop.time() + timeout
|
|
89
|
+
while True:
|
|
90
|
+
await self.refresh()
|
|
91
|
+
running = self.status == "running"
|
|
92
|
+
if running and (not require_stream_ready or self.stream_ready):
|
|
93
|
+
return self
|
|
94
|
+
if loop.time() >= deadline:
|
|
95
|
+
raise TimeoutError(
|
|
96
|
+
f"Browser lease {self.lease_id} not running after "
|
|
97
|
+
f"{timeout}s (status={self.status}, stream_ready={self.stream_ready})"
|
|
98
|
+
)
|
|
99
|
+
await asyncio.sleep(poll_interval)
|
|
100
|
+
|
|
101
|
+
async def mcp_tools(self) -> Dict[str, Any]:
|
|
102
|
+
response = await self._client.request(
|
|
103
|
+
"GET",
|
|
104
|
+
f"{_BROWSER_PATH}/{self.lease_id}/mcp-tools",
|
|
105
|
+
extra_headers=_extra_headers(self._jwt_token, self._team_id),
|
|
106
|
+
)
|
|
107
|
+
return response.json()
|
|
108
|
+
|
|
109
|
+
async def delete(self) -> None:
|
|
110
|
+
from ..exceptions import FleetAPIError
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
await self._client.request(
|
|
114
|
+
"DELETE",
|
|
115
|
+
f"{_BROWSER_PATH}/{self.lease_id}",
|
|
116
|
+
extra_headers=_extra_headers(self._jwt_token, self._team_id),
|
|
117
|
+
)
|
|
118
|
+
except FleetAPIError as exc:
|
|
119
|
+
status = getattr(exc, "status_code", None)
|
|
120
|
+
if status not in (404, 410):
|
|
121
|
+
raise
|
|
122
|
+
|
|
123
|
+
async def __aenter__(self) -> "AsyncBrowserLease":
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
async def __aexit__(self, exc_type, exc, tb) -> None:
|
|
127
|
+
await self.delete()
|
|
128
|
+
|
|
129
|
+
def __repr__(self) -> str:
|
|
130
|
+
return (
|
|
131
|
+
f"AsyncBrowserLease(lease_id={self.lease_id!r}, status={self.status!r}, "
|
|
132
|
+
f"cdp_url={self.cdp_url!r})"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
async def create_browser(
|
|
137
|
+
client: "AsyncWrapper",
|
|
138
|
+
*,
|
|
139
|
+
ttl_seconds: int = 300,
|
|
140
|
+
lease_id: Optional[str] = None,
|
|
141
|
+
allowed_hosts: Optional[List[str]] = None,
|
|
142
|
+
request_timestamp_ms: Optional[int] = None,
|
|
143
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
144
|
+
jwt_token: Optional[str] = None,
|
|
145
|
+
team_id: Optional[str] = None,
|
|
146
|
+
wait_until_running: bool = False,
|
|
147
|
+
wait_timeout: float = 60.0,
|
|
148
|
+
) -> AsyncBrowserLease:
|
|
149
|
+
payload: Dict[str, Any] = {"ttl_seconds": ttl_seconds}
|
|
150
|
+
if lease_id is not None:
|
|
151
|
+
payload["lease_id"] = lease_id
|
|
152
|
+
if allowed_hosts is not None:
|
|
153
|
+
payload["allowed_hosts"] = allowed_hosts
|
|
154
|
+
if request_timestamp_ms is not None:
|
|
155
|
+
payload["request_timestamp_ms"] = request_timestamp_ms
|
|
156
|
+
if extra:
|
|
157
|
+
payload.update(extra)
|
|
158
|
+
|
|
159
|
+
response = await client.request(
|
|
160
|
+
"POST",
|
|
161
|
+
_BROWSER_PATH,
|
|
162
|
+
json=payload,
|
|
163
|
+
extra_headers=_extra_headers(jwt_token, team_id),
|
|
164
|
+
)
|
|
165
|
+
lease = AsyncBrowserLease(client, response.json())
|
|
166
|
+
lease._jwt_token = jwt_token
|
|
167
|
+
lease._team_id = team_id
|
|
168
|
+
if wait_until_running:
|
|
169
|
+
await lease.wait_until_running(timeout=wait_timeout)
|
|
170
|
+
return lease
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async def get_browser(
|
|
174
|
+
client: "AsyncWrapper",
|
|
175
|
+
lease_id: str,
|
|
176
|
+
*,
|
|
177
|
+
jwt_token: Optional[str] = None,
|
|
178
|
+
team_id: Optional[str] = None,
|
|
179
|
+
) -> AsyncBrowserLease:
|
|
180
|
+
response = await client.request(
|
|
181
|
+
"GET",
|
|
182
|
+
f"{_BROWSER_PATH}/{lease_id}",
|
|
183
|
+
extra_headers=_extra_headers(jwt_token, team_id),
|
|
184
|
+
)
|
|
185
|
+
lease = AsyncBrowserLease(client, response.json())
|
|
186
|
+
lease._jwt_token = jwt_token
|
|
187
|
+
lease._team_id = team_id
|
|
188
|
+
return lease
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
__all__ = ["AsyncBrowserLease", "create_browser", "get_browser", "host_from_url"]
|
fleet/_async/client.py
CHANGED
|
@@ -171,10 +171,15 @@ from .instance.base import default_httpx_client
|
|
|
171
171
|
from .instance.client import ValidatorType
|
|
172
172
|
from .resources.base import Resource
|
|
173
173
|
from .resources.sqlite import AsyncSQLiteResource
|
|
174
|
-
from .resources.browser import AsyncBrowserResource
|
|
175
174
|
from .resources.filesystem import AsyncFilesystemResource
|
|
176
175
|
from .resources.mcp import AsyncMCPResource
|
|
177
176
|
from .resources.api import AsyncAPIResource
|
|
177
|
+
from .browser import (
|
|
178
|
+
AsyncBrowserLease,
|
|
179
|
+
create_browser as _create_browser_lease,
|
|
180
|
+
get_browser as _get_browser_lease,
|
|
181
|
+
host_from_url,
|
|
182
|
+
)
|
|
178
183
|
|
|
179
184
|
logger = logging.getLogger(__name__)
|
|
180
185
|
|
|
@@ -386,8 +391,51 @@ class AsyncEnv(EnvironmentBase):
|
|
|
386
391
|
def db(self, name: str = "current") -> AsyncSQLiteResource:
|
|
387
392
|
return self.instance.db(name)
|
|
388
393
|
|
|
389
|
-
def browser(
|
|
390
|
-
|
|
394
|
+
async def browser(
|
|
395
|
+
self,
|
|
396
|
+
ttl_seconds: int = 300,
|
|
397
|
+
*,
|
|
398
|
+
lease_id: Optional[str] = None,
|
|
399
|
+
allowed_hosts: Optional[List[str]] = None,
|
|
400
|
+
include_root_host: bool = True,
|
|
401
|
+
wait_until_running: bool = False,
|
|
402
|
+
wait_timeout: float = 60.0,
|
|
403
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
404
|
+
jwt_token: Optional[str] = None,
|
|
405
|
+
team_id: Optional[str] = None,
|
|
406
|
+
) -> AsyncBrowserLease:
|
|
407
|
+
"""Spin up an orchestrator-managed Fleet Browser lease for this env.
|
|
408
|
+
|
|
409
|
+
``await env.browser()`` posts to ``/v1/browser`` and returns an
|
|
410
|
+
:class:`fleet._async.browser.AsyncBrowserLease` with ``cdp_url`` /
|
|
411
|
+
``mcp_url`` / ``stream_url`` and a ``mcp_tools()`` accessor. By
|
|
412
|
+
default the host derived from ``self.urls.root`` is prepended to
|
|
413
|
+
``allowed_hosts`` so the browser can reach the instance — pass
|
|
414
|
+
``include_root_host=False`` to opt out.
|
|
415
|
+
"""
|
|
416
|
+
hosts: Optional[List[str]] = list(allowed_hosts) if allowed_hosts else None
|
|
417
|
+
if include_root_host and self.urls and self.urls.root:
|
|
418
|
+
root_host = host_from_url(self.urls.root)
|
|
419
|
+
if root_host:
|
|
420
|
+
hosts = hosts or []
|
|
421
|
+
if root_host not in hosts:
|
|
422
|
+
hosts.insert(0, root_host)
|
|
423
|
+
return await _create_browser_lease(
|
|
424
|
+
self._load_client,
|
|
425
|
+
ttl_seconds=ttl_seconds,
|
|
426
|
+
lease_id=lease_id,
|
|
427
|
+
allowed_hosts=hosts,
|
|
428
|
+
extra=extra,
|
|
429
|
+
jwt_token=jwt_token,
|
|
430
|
+
team_id=team_id,
|
|
431
|
+
wait_until_running=wait_until_running,
|
|
432
|
+
wait_timeout=wait_timeout,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
@property
|
|
436
|
+
def root_url(self) -> Optional[str]:
|
|
437
|
+
"""Convenience: ``self.urls.root`` if available."""
|
|
438
|
+
return self.urls.root if self.urls else None
|
|
391
439
|
|
|
392
440
|
def fs(self) -> AsyncFilesystemResource:
|
|
393
441
|
"""Get a filesystem diff resource for inspecting file changes."""
|
|
@@ -605,7 +653,9 @@ class AsyncFleet:
|
|
|
605
653
|
)
|
|
606
654
|
|
|
607
655
|
instance = AsyncEnv(client=self.client, **response.json())
|
|
608
|
-
|
|
656
|
+
# Resources are loaded lazily on first `db()`/`browser()`/`resources()` access
|
|
657
|
+
# via `_load_resources()`, so we don't preload here. Eagerly loading would
|
|
658
|
+
# fail-fast with a 502 while the container is still warming up.
|
|
609
659
|
return instance
|
|
610
660
|
|
|
611
661
|
async def make_for_task(self, task: Task) -> AsyncEnv:
|
|
@@ -657,7 +707,7 @@ class AsyncFleet:
|
|
|
657
707
|
else:
|
|
658
708
|
response = await self.client.request("GET", f"/v1/env/instances/{instance_id}")
|
|
659
709
|
instance = AsyncEnv(client=self.client, **response.json())
|
|
660
|
-
|
|
710
|
+
# Resources load lazily on first `db()`/`browser()`/`resources()` access.
|
|
661
711
|
return instance
|
|
662
712
|
|
|
663
713
|
def _create_url_instance(self, base_url: str) -> AsyncEnv:
|
|
@@ -798,6 +848,45 @@ class AsyncFleet:
|
|
|
798
848
|
self.client, bundle_data, args, kwargs, timeout
|
|
799
849
|
)
|
|
800
850
|
|
|
851
|
+
async def create_browser(
|
|
852
|
+
self,
|
|
853
|
+
ttl_seconds: int = 300,
|
|
854
|
+
*,
|
|
855
|
+
lease_id: Optional[str] = None,
|
|
856
|
+
allowed_hosts: Optional[List[str]] = None,
|
|
857
|
+
request_timestamp_ms: Optional[int] = None,
|
|
858
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
859
|
+
jwt_token: Optional[str] = None,
|
|
860
|
+
team_id: Optional[str] = None,
|
|
861
|
+
wait_until_running: bool = False,
|
|
862
|
+
wait_timeout: float = 60.0,
|
|
863
|
+
) -> AsyncBrowserLease:
|
|
864
|
+
"""Create a Fleet Browser lease (``POST /v1/browser``)."""
|
|
865
|
+
return await _create_browser_lease(
|
|
866
|
+
self.client,
|
|
867
|
+
ttl_seconds=ttl_seconds,
|
|
868
|
+
lease_id=lease_id,
|
|
869
|
+
allowed_hosts=allowed_hosts,
|
|
870
|
+
request_timestamp_ms=request_timestamp_ms,
|
|
871
|
+
extra=extra,
|
|
872
|
+
jwt_token=jwt_token,
|
|
873
|
+
team_id=team_id,
|
|
874
|
+
wait_until_running=wait_until_running,
|
|
875
|
+
wait_timeout=wait_timeout,
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
async def get_browser(
|
|
879
|
+
self,
|
|
880
|
+
lease_id: str,
|
|
881
|
+
*,
|
|
882
|
+
jwt_token: Optional[str] = None,
|
|
883
|
+
team_id: Optional[str] = None,
|
|
884
|
+
) -> AsyncBrowserLease:
|
|
885
|
+
"""Inspect an existing browser lease (``GET /v1/browser/{lease_id}``)."""
|
|
886
|
+
return await _get_browser_lease(
|
|
887
|
+
self.client, lease_id, jwt_token=jwt_token, team_id=team_id
|
|
888
|
+
)
|
|
889
|
+
|
|
801
890
|
async def delete(self, instance_id: str) -> InstanceResponse:
|
|
802
891
|
return await _delete_instance(self.client, instance_id)
|
|
803
892
|
|
fleet/_async/models.py
CHANGED
|
@@ -51,6 +51,7 @@ class Instance(BaseModel):
|
|
|
51
51
|
team_id: str = Field(..., title="Team Id")
|
|
52
52
|
region: str = Field(..., title="Region")
|
|
53
53
|
env_variables: Optional[Dict[str, Any]] = Field(None, title="Env Variables")
|
|
54
|
+
multi_env_list: Optional[List[str]] = Field(None, title="Multi Env List")
|
|
54
55
|
|
|
55
56
|
|
|
56
57
|
class InstanceRequest(BaseModel):
|
|
@@ -373,6 +374,7 @@ class InstanceResponse(BaseModel):
|
|
|
373
374
|
data_version: Optional[str] = Field(None, title="Data Version")
|
|
374
375
|
urls: Optional[InstanceURLs] = Field(None, title="Urls")
|
|
375
376
|
health: Optional[bool] = Field(None, title="Health")
|
|
377
|
+
multi_env_list: Optional[List[str]] = Field(None, title="Multi Env List")
|
|
376
378
|
|
|
377
379
|
|
|
378
380
|
class AccountResponse(BaseModel):
|
fleet/base.py
CHANGED
fleet/browser.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Copyright 2025 Fleet AI
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
"""Fleet Browser API — orchestrator-managed remote browser leases.
|
|
10
|
+
|
|
11
|
+
Thin wrapper around the `/v1/browser` HTTP surface documented at
|
|
12
|
+
`https://orchestrator.fleetai.com/docs`. A ``BrowserLease`` exposes the
|
|
13
|
+
``cdp_url`` / ``mcp_url`` / ``stream_url`` returned by the create call and
|
|
14
|
+
lets you poll, list MCP tools, and release the lease.
|
|
15
|
+
|
|
16
|
+
This is intentionally independent of ``fleet.resources.browser`` (the
|
|
17
|
+
in-instance CDP resource); they solve different problems.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import time
|
|
23
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
24
|
+
from urllib.parse import urlparse
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from .base import SyncWrapper
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_BROWSER_PATH = "/v1/browser"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _extra_headers(
|
|
34
|
+
jwt_token: Optional[str] = None, team_id: Optional[str] = None
|
|
35
|
+
) -> Optional[Dict[str, str]]:
|
|
36
|
+
headers: Dict[str, str] = {}
|
|
37
|
+
if jwt_token:
|
|
38
|
+
headers["X-JWT-Token"] = jwt_token
|
|
39
|
+
if team_id:
|
|
40
|
+
headers["X-Team-ID"] = team_id
|
|
41
|
+
return headers or None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def host_from_url(url: str) -> str:
|
|
45
|
+
"""Return the bare hostname for a URL (no scheme, no port, no path).
|
|
46
|
+
|
|
47
|
+
Useful when populating ``allowed_hosts`` from ``env.urls.root``.
|
|
48
|
+
"""
|
|
49
|
+
parsed = urlparse(url if "://" in url else f"https://{url}")
|
|
50
|
+
return parsed.hostname or url
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BrowserLease:
|
|
54
|
+
"""A lease created via ``POST /v1/browser``.
|
|
55
|
+
|
|
56
|
+
Attribute names mirror the JSON response. Methods that hit the API
|
|
57
|
+
reuse the same :class:`SyncWrapper` that created the lease so auth /
|
|
58
|
+
base URL / retries stay consistent.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, client: "SyncWrapper", data: Dict[str, Any]):
|
|
62
|
+
self._client = client
|
|
63
|
+
self._raw: Dict[str, Any] = dict(data)
|
|
64
|
+
self._jwt_token: Optional[str] = None
|
|
65
|
+
self._team_id: Optional[str] = None
|
|
66
|
+
self._apply(data)
|
|
67
|
+
|
|
68
|
+
def _apply(self, data: Dict[str, Any]) -> None:
|
|
69
|
+
self.id: str = data.get("id") or data["lease_id"]
|
|
70
|
+
self.lease_id: str = data["lease_id"]
|
|
71
|
+
self.browser_id: Optional[str] = data.get("browser_id")
|
|
72
|
+
self.host_domain: Optional[str] = data.get("host_domain")
|
|
73
|
+
self.mcp_url: Optional[str] = data.get("mcp_url")
|
|
74
|
+
self.cdp_url: Optional[str] = data.get("cdp_url")
|
|
75
|
+
self.stream_url: Optional[str] = data.get("stream_url")
|
|
76
|
+
self.status: Optional[str] = data.get("status")
|
|
77
|
+
self.stream_ready: Optional[bool] = data.get("stream_ready")
|
|
78
|
+
self.allowed_hosts: Optional[List[str]] = data.get("allowed_hosts")
|
|
79
|
+
self.created_timestamp_ms: Optional[int] = data.get("created_timestamp_ms")
|
|
80
|
+
self.expires_timestamp_ms: Optional[int] = data.get("expires_timestamp_ms")
|
|
81
|
+
self.age_duration_ms: Optional[int] = data.get("age_duration_ms")
|
|
82
|
+
self.cluster_name: Optional[str] = data.get("cluster_name")
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def raw(self) -> Dict[str, Any]:
|
|
86
|
+
"""Last server response payload (for fields not surfaced as attrs)."""
|
|
87
|
+
return self._raw
|
|
88
|
+
|
|
89
|
+
def refresh(self) -> "BrowserLease":
|
|
90
|
+
"""Re-fetch the lease (``GET /v1/browser/{lease_id}``)."""
|
|
91
|
+
response = self._client.request(
|
|
92
|
+
"GET",
|
|
93
|
+
f"{_BROWSER_PATH}/{self.lease_id}",
|
|
94
|
+
extra_headers=_extra_headers(self._jwt_token, self._team_id),
|
|
95
|
+
)
|
|
96
|
+
self._raw = response.json()
|
|
97
|
+
self._apply(self._raw)
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
def wait_until_running(
|
|
101
|
+
self,
|
|
102
|
+
timeout: float = 60.0,
|
|
103
|
+
poll_interval: float = 1.0,
|
|
104
|
+
require_stream_ready: bool = False,
|
|
105
|
+
) -> "BrowserLease":
|
|
106
|
+
"""Poll until ``status == 'running'`` (or stream_ready, if requested)."""
|
|
107
|
+
deadline = time.monotonic() + timeout
|
|
108
|
+
while True:
|
|
109
|
+
self.refresh()
|
|
110
|
+
running = self.status == "running"
|
|
111
|
+
if running and (not require_stream_ready or self.stream_ready):
|
|
112
|
+
return self
|
|
113
|
+
if time.monotonic() >= deadline:
|
|
114
|
+
raise TimeoutError(
|
|
115
|
+
f"Browser lease {self.lease_id} not running after "
|
|
116
|
+
f"{timeout}s (status={self.status}, stream_ready={self.stream_ready})"
|
|
117
|
+
)
|
|
118
|
+
time.sleep(poll_interval)
|
|
119
|
+
|
|
120
|
+
def mcp_tools(self) -> Dict[str, Any]:
|
|
121
|
+
"""List MCP tools the browser exposes."""
|
|
122
|
+
response = self._client.request(
|
|
123
|
+
"GET",
|
|
124
|
+
f"{_BROWSER_PATH}/{self.lease_id}/mcp-tools",
|
|
125
|
+
extra_headers=_extra_headers(self._jwt_token, self._team_id),
|
|
126
|
+
)
|
|
127
|
+
return response.json()
|
|
128
|
+
|
|
129
|
+
def delete(self) -> None:
|
|
130
|
+
"""Release the lease. Idempotent — ignores 404 if already gone."""
|
|
131
|
+
from .exceptions import FleetAPIError
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
self._client.request(
|
|
135
|
+
"DELETE",
|
|
136
|
+
f"{_BROWSER_PATH}/{self.lease_id}",
|
|
137
|
+
extra_headers=_extra_headers(self._jwt_token, self._team_id),
|
|
138
|
+
)
|
|
139
|
+
except FleetAPIError as exc:
|
|
140
|
+
status = getattr(exc, "status_code", None)
|
|
141
|
+
if status not in (404, 410):
|
|
142
|
+
raise
|
|
143
|
+
|
|
144
|
+
# Context manager so `with client.create_browser(...) as br: ...` cleans up.
|
|
145
|
+
def __enter__(self) -> "BrowserLease":
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
149
|
+
self.delete()
|
|
150
|
+
|
|
151
|
+
def __repr__(self) -> str:
|
|
152
|
+
return (
|
|
153
|
+
f"BrowserLease(lease_id={self.lease_id!r}, status={self.status!r}, "
|
|
154
|
+
f"cdp_url={self.cdp_url!r})"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def create_browser(
|
|
159
|
+
client: "SyncWrapper",
|
|
160
|
+
*,
|
|
161
|
+
ttl_seconds: int = 300,
|
|
162
|
+
lease_id: Optional[str] = None,
|
|
163
|
+
allowed_hosts: Optional[List[str]] = None,
|
|
164
|
+
request_timestamp_ms: Optional[int] = None,
|
|
165
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
166
|
+
jwt_token: Optional[str] = None,
|
|
167
|
+
team_id: Optional[str] = None,
|
|
168
|
+
wait_until_running: bool = False,
|
|
169
|
+
wait_timeout: float = 60.0,
|
|
170
|
+
) -> BrowserLease:
|
|
171
|
+
"""POST ``/v1/browser`` and return a :class:`BrowserLease`.
|
|
172
|
+
|
|
173
|
+
``extra`` is merged into the request body so callers can pass
|
|
174
|
+
fields that aren't first-class kwargs yet — keeps this freeform.
|
|
175
|
+
"""
|
|
176
|
+
payload: Dict[str, Any] = {"ttl_seconds": ttl_seconds}
|
|
177
|
+
if lease_id is not None:
|
|
178
|
+
payload["lease_id"] = lease_id
|
|
179
|
+
if allowed_hosts is not None:
|
|
180
|
+
payload["allowed_hosts"] = allowed_hosts
|
|
181
|
+
if request_timestamp_ms is not None:
|
|
182
|
+
payload["request_timestamp_ms"] = request_timestamp_ms
|
|
183
|
+
if extra:
|
|
184
|
+
payload.update(extra)
|
|
185
|
+
|
|
186
|
+
response = client.request(
|
|
187
|
+
"POST",
|
|
188
|
+
_BROWSER_PATH,
|
|
189
|
+
json=payload,
|
|
190
|
+
extra_headers=_extra_headers(jwt_token, team_id),
|
|
191
|
+
)
|
|
192
|
+
lease = BrowserLease(client, response.json())
|
|
193
|
+
lease._jwt_token = jwt_token
|
|
194
|
+
lease._team_id = team_id
|
|
195
|
+
if wait_until_running:
|
|
196
|
+
lease.wait_until_running(timeout=wait_timeout)
|
|
197
|
+
return lease
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def get_browser(
|
|
201
|
+
client: "SyncWrapper",
|
|
202
|
+
lease_id: str,
|
|
203
|
+
*,
|
|
204
|
+
jwt_token: Optional[str] = None,
|
|
205
|
+
team_id: Optional[str] = None,
|
|
206
|
+
) -> BrowserLease:
|
|
207
|
+
"""Inspect an existing lease (``GET /v1/browser/{lease_id}``)."""
|
|
208
|
+
response = client.request(
|
|
209
|
+
"GET",
|
|
210
|
+
f"{_BROWSER_PATH}/{lease_id}",
|
|
211
|
+
extra_headers=_extra_headers(jwt_token, team_id),
|
|
212
|
+
)
|
|
213
|
+
lease = BrowserLease(client, response.json())
|
|
214
|
+
lease._jwt_token = jwt_token
|
|
215
|
+
lease._team_id = team_id
|
|
216
|
+
return lease
|
fleet/client.py
CHANGED
|
@@ -178,10 +178,15 @@ from .instance.base import default_httpx_client
|
|
|
178
178
|
from .instance.client import ValidatorType
|
|
179
179
|
from .resources.base import Resource
|
|
180
180
|
from .resources.sqlite import SQLiteResource
|
|
181
|
-
from .resources.browser import BrowserResource
|
|
182
181
|
from .resources.filesystem import FilesystemResource
|
|
183
182
|
from .resources.mcp import SyncMCPResource
|
|
184
183
|
from .resources.api import APIResource
|
|
184
|
+
from .browser import (
|
|
185
|
+
BrowserLease,
|
|
186
|
+
create_browser as _create_browser_lease,
|
|
187
|
+
get_browser as _get_browser_lease,
|
|
188
|
+
host_from_url,
|
|
189
|
+
)
|
|
185
190
|
|
|
186
191
|
logger = logging.getLogger(__name__)
|
|
187
192
|
|
|
@@ -399,8 +404,51 @@ class SyncEnv(EnvironmentBase):
|
|
|
399
404
|
def db(self, name: str = "current") -> SQLiteResource:
|
|
400
405
|
return self.instance.db(name)
|
|
401
406
|
|
|
402
|
-
def browser(
|
|
403
|
-
|
|
407
|
+
def browser(
|
|
408
|
+
self,
|
|
409
|
+
ttl_seconds: int = 300,
|
|
410
|
+
*,
|
|
411
|
+
lease_id: Optional[str] = None,
|
|
412
|
+
allowed_hosts: Optional[List[str]] = None,
|
|
413
|
+
include_root_host: bool = True,
|
|
414
|
+
wait_until_running: bool = False,
|
|
415
|
+
wait_timeout: float = 60.0,
|
|
416
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
417
|
+
jwt_token: Optional[str] = None,
|
|
418
|
+
team_id: Optional[str] = None,
|
|
419
|
+
) -> BrowserLease:
|
|
420
|
+
"""Spin up an orchestrator-managed Fleet Browser lease for this env.
|
|
421
|
+
|
|
422
|
+
``env.browser()`` posts to ``/v1/browser`` and returns a
|
|
423
|
+
:class:`fleet.browser.BrowserLease` with ``cdp_url`` / ``mcp_url`` /
|
|
424
|
+
``stream_url`` and a ``mcp_tools()`` accessor. By default the host
|
|
425
|
+
from ``self.urls.root`` is prepended to ``allowed_hosts`` so the
|
|
426
|
+
browser can reach the instance — pass ``include_root_host=False``
|
|
427
|
+
to opt out.
|
|
428
|
+
"""
|
|
429
|
+
hosts: Optional[List[str]] = list(allowed_hosts) if allowed_hosts else None
|
|
430
|
+
if include_root_host and self.urls and self.urls.root:
|
|
431
|
+
root_host = host_from_url(self.urls.root)
|
|
432
|
+
if root_host:
|
|
433
|
+
hosts = hosts or []
|
|
434
|
+
if root_host not in hosts:
|
|
435
|
+
hosts.insert(0, root_host)
|
|
436
|
+
return _create_browser_lease(
|
|
437
|
+
self._load_client,
|
|
438
|
+
ttl_seconds=ttl_seconds,
|
|
439
|
+
lease_id=lease_id,
|
|
440
|
+
allowed_hosts=hosts,
|
|
441
|
+
extra=extra,
|
|
442
|
+
jwt_token=jwt_token,
|
|
443
|
+
team_id=team_id,
|
|
444
|
+
wait_until_running=wait_until_running,
|
|
445
|
+
wait_timeout=wait_timeout,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def root_url(self) -> Optional[str]:
|
|
450
|
+
"""Convenience: ``self.urls.root`` if available (handy paired with spawn_browser)."""
|
|
451
|
+
return self.urls.root if self.urls else None
|
|
404
452
|
|
|
405
453
|
def fs(self) -> FilesystemResource:
|
|
406
454
|
"""Get a filesystem diff resource for inspecting file changes."""
|
|
@@ -618,7 +666,9 @@ class Fleet:
|
|
|
618
666
|
)
|
|
619
667
|
|
|
620
668
|
instance = SyncEnv(client=self.client, **response.json())
|
|
621
|
-
|
|
669
|
+
# Resources load lazily on first `db()`/`browser()`/`resources()` access via
|
|
670
|
+
# `_load_resources()`. Skipping the eager preload avoids fail-fast 502s while
|
|
671
|
+
# the container is still warming up.
|
|
622
672
|
return instance
|
|
623
673
|
|
|
624
674
|
def make_for_task(self, task: Task) -> SyncEnv:
|
|
@@ -670,7 +720,7 @@ class Fleet:
|
|
|
670
720
|
else:
|
|
671
721
|
response = self.client.request("GET", f"/v1/env/instances/{instance_id}")
|
|
672
722
|
instance = SyncEnv(client=self.client, **response.json())
|
|
673
|
-
|
|
723
|
+
# Resources load lazily on first `db()`/`browser()`/`resources()` access.
|
|
674
724
|
return instance
|
|
675
725
|
|
|
676
726
|
def _create_url_instance(self, base_url: str) -> SyncEnv:
|
|
@@ -811,6 +861,50 @@ class Fleet:
|
|
|
811
861
|
) -> VerifiersExecuteResponse:
|
|
812
862
|
return _execute_verifier_remote(self.client, bundle_data, args, kwargs, timeout)
|
|
813
863
|
|
|
864
|
+
def create_browser(
|
|
865
|
+
self,
|
|
866
|
+
ttl_seconds: int = 300,
|
|
867
|
+
*,
|
|
868
|
+
lease_id: Optional[str] = None,
|
|
869
|
+
allowed_hosts: Optional[List[str]] = None,
|
|
870
|
+
request_timestamp_ms: Optional[int] = None,
|
|
871
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
872
|
+
jwt_token: Optional[str] = None,
|
|
873
|
+
team_id: Optional[str] = None,
|
|
874
|
+
wait_until_running: bool = False,
|
|
875
|
+
wait_timeout: float = 60.0,
|
|
876
|
+
) -> BrowserLease:
|
|
877
|
+
"""Create a Fleet Browser lease (``POST /v1/browser``).
|
|
878
|
+
|
|
879
|
+
Freeform — pass any of ``allowed_hosts`` / ``lease_id`` /
|
|
880
|
+
``request_timestamp_ms`` directly, or use ``extra`` for keys this
|
|
881
|
+
SDK version doesn't surface yet.
|
|
882
|
+
"""
|
|
883
|
+
return _create_browser_lease(
|
|
884
|
+
self.client,
|
|
885
|
+
ttl_seconds=ttl_seconds,
|
|
886
|
+
lease_id=lease_id,
|
|
887
|
+
allowed_hosts=allowed_hosts,
|
|
888
|
+
request_timestamp_ms=request_timestamp_ms,
|
|
889
|
+
extra=extra,
|
|
890
|
+
jwt_token=jwt_token,
|
|
891
|
+
team_id=team_id,
|
|
892
|
+
wait_until_running=wait_until_running,
|
|
893
|
+
wait_timeout=wait_timeout,
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
def get_browser(
|
|
897
|
+
self,
|
|
898
|
+
lease_id: str,
|
|
899
|
+
*,
|
|
900
|
+
jwt_token: Optional[str] = None,
|
|
901
|
+
team_id: Optional[str] = None,
|
|
902
|
+
) -> BrowserLease:
|
|
903
|
+
"""Inspect an existing browser lease (``GET /v1/browser/{lease_id}``)."""
|
|
904
|
+
return _get_browser_lease(
|
|
905
|
+
self.client, lease_id, jwt_token=jwt_token, team_id=team_id
|
|
906
|
+
)
|
|
907
|
+
|
|
814
908
|
def delete(self, instance_id: str) -> InstanceResponse:
|
|
815
909
|
return _delete_instance(self.client, instance_id)
|
|
816
910
|
|
fleet/models.py
CHANGED
|
@@ -52,6 +52,7 @@ class Instance(BaseModel):
|
|
|
52
52
|
region: str = Field(..., title="Region")
|
|
53
53
|
env_variables: Optional[Dict[str, Any]] = Field(None, title="Env Variables")
|
|
54
54
|
run_id: Optional[str] = Field(None, title="Run Id")
|
|
55
|
+
multi_env_list: Optional[List[str]] = Field(None, title="Multi Env List")
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
class InstanceRequest(BaseModel):
|
|
@@ -385,6 +386,7 @@ class InstanceResponse(BaseModel):
|
|
|
385
386
|
profile_id: Optional[str] = Field(None, title="Profile Id")
|
|
386
387
|
heartbeat_interval: Optional[int] = Field(None, title="Heartbeat Interval")
|
|
387
388
|
heartbeat_region: Optional[str] = Field(None, title="Heartbeat Region")
|
|
389
|
+
multi_env_list: Optional[List[str]] = Field(None, title="Multi Env List")
|
|
388
390
|
|
|
389
391
|
|
|
390
392
|
class Run(BaseModel):
|
|
@@ -24,24 +24,26 @@ examples/openai_simple_example.py,sha256=HmiufucrAZne7tHq9uoEsDWlEhjNC265bQAyIGB
|
|
|
24
24
|
examples/query_builder_example.py,sha256=-cOMfWGNifYfYEt_Ds73XpwATZvFDL6F4KTkVxdMjzg,3951
|
|
25
25
|
examples/quickstart.py,sha256=1VT39IRRhemsJgxi0O0gprdpcw7HB4pYO97GAYagIcg,3788
|
|
26
26
|
examples/test_cdp_logging.py,sha256=AkCwQCgOTQEI8w3v0knWK_4eXMph7L9x07wj9yIYM10,2836
|
|
27
|
-
fleet/__init__.py,sha256=
|
|
28
|
-
fleet/base.py,sha256=
|
|
27
|
+
fleet/__init__.py,sha256=THC4oStKpTQC6zNjZRS0TEDqzGiU4Mst_jftJW1hXko,8193
|
|
28
|
+
fleet/base.py,sha256=yVB5d1xFN-1KxRVSjFCQpRSQ1IhVW5FjtoHOXzkM5Yo,10209
|
|
29
|
+
fleet/browser.py,sha256=6259Hgsygu9i_txbufOmQJYCyVb6qjPw_2-0aCLTLFU,7511
|
|
29
30
|
fleet/cli.py,sha256=arX1E-fjLXtcV3tVVkPHfEXxl7FDn4zRmf0ssXlXaMg,40104
|
|
30
|
-
fleet/client.py,sha256=
|
|
31
|
+
fleet/client.py,sha256=AMpRyKdDhMwQZQbHVepFkUVmVzJ3aURQRkYsNwkGul8,73370
|
|
31
32
|
fleet/config.py,sha256=n_wh9Sahu3gGE7nHJ7kqNFUH1qDiBtF4bgZq9MvIBMU,319
|
|
32
33
|
fleet/exceptions.py,sha256=YqhQonZlxGdLP1HD0DNdKs9Q9BuyeYvK3pc6Glhpl14,5352
|
|
33
34
|
fleet/global_client.py,sha256=frrDAFNM2ywN0JHLtlm9qbE1dQpnQJsavJpb7xSR_bU,1072
|
|
34
35
|
fleet/judge.py,sha256=c41hAPFMfZvA0LLGPmp6BjaOsIKPf5Wz90UNUvBorRg,35044
|
|
35
|
-
fleet/models.py,sha256=
|
|
36
|
+
fleet/models.py,sha256=CS-sL84W50eH21MgtuFC0Oz1V7fdRyeqdb_txsmai3Q,25858
|
|
36
37
|
fleet/tasks.py,sha256=7-bXf-H2EpnKMAT7t0XDiAePn6R76vi-OgTlD1bDt6Q,22500
|
|
37
38
|
fleet/types.py,sha256=L4Y82xICf1tzyCLqhLYUgEoaIIS5h9T05TyFNHSWs3s,652
|
|
38
|
-
fleet/_async/__init__.py,sha256=
|
|
39
|
-
fleet/_async/base.py,sha256=
|
|
40
|
-
fleet/_async/
|
|
39
|
+
fleet/_async/__init__.py,sha256=oTpKiIljz2_Tr5oWLsJqvfANVcD9PnsCB9xn2KZOtqo,9116
|
|
40
|
+
fleet/_async/base.py,sha256=zvMNIz1ctckeTMB4aIN5ELLBgPmv6u4mr95VVXxyNLk,9774
|
|
41
|
+
fleet/_async/browser.py,sha256=_lU23nuoSg8gwtiGGd4gGvD9ZRQ-gJFo42zxvDdM_ic,6388
|
|
42
|
+
fleet/_async/client.py,sha256=-mLVJ_ur1MFJZRb0vUBHyBPNlf1zYxT7Un67vLWWsWw,69693
|
|
41
43
|
fleet/_async/exceptions.py,sha256=fUmPwWhnT8SR97lYsRq0kLHQHKtSh2eJS0VQ2caSzEI,5055
|
|
42
44
|
fleet/_async/global_client.py,sha256=4WskpLHbsDEgWW7hXMD09W-brkp4euy8w2ZJ88594rQ,1103
|
|
43
45
|
fleet/_async/judge.py,sha256=Ape82WAveHB9ApbslmoiUnuj5KL-DkdQLThUlOsuxgk,6026
|
|
44
|
-
fleet/_async/models.py,sha256=
|
|
46
|
+
fleet/_async/models.py,sha256=wXvXhSk6XeCVFhF5q7YHQM-DgS4tAbm0mCaPW6MZPUg,14272
|
|
45
47
|
fleet/_async/tasks.py,sha256=r8HPRchH354hUauf3PruMY4i-jqjfwbIfafwrWEFlZo,22310
|
|
46
48
|
fleet/_async/env/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
47
49
|
fleet/_async/env/client.py,sha256=FqetvDlABeHLaAc8UF_m_2OFEhXy4ZqH7ly_Nh8p3uA,3479
|
|
@@ -101,7 +103,7 @@ fleet/verifiers/decorator.py,sha256=RuTjjDijbicNfMSjA7HcTpKueEki5dzNOdTuHS7UoZs,
|
|
|
101
103
|
fleet/verifiers/parse.py,sha256=qz9AfJrTbjlg-LU-lE8Ciqi7Yt2a8-cs17FdpjTLhMk,8550
|
|
102
104
|
fleet/verifiers/sql_differ.py,sha256=TqTLWyK3uOyLbitT6HYzYEzuSFC39wcyhgk3rcm__k8,6525
|
|
103
105
|
fleet/verifiers/verifier.py,sha256=DQ2m90AOEAhgNT2X7wl5NGDJY6NTSkkMsmLNXhOfs-A,15789
|
|
104
|
-
fleet_python-0.2.
|
|
106
|
+
fleet_python-0.2.129.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
105
107
|
scripts/fix_sync_imports.py,sha256=X9fWLTpiPGkSHsjyQUDepOJkxOqw1DPj7nd8wFlFqLQ,8368
|
|
106
108
|
scripts/unasync.py,sha256=vWVQxRWX8SRZO5cmzEhpvnG_REhCWXpidIGIpWmEcvI,696
|
|
107
109
|
tests/__init__.py,sha256=Re1SdyxH8NfyL1kjhi7SQkGP1mYeWB-D6UALqdIMd8I,35
|
|
@@ -112,8 +114,8 @@ tests/test_instance_dispatch.py,sha256=CvU4C3LBIqsYZdEsEFfontGjyxAZfVYyXnGwxyIvX
|
|
|
112
114
|
tests/test_sqlite_resource_dual_mode.py,sha256=Mh8jBd-xsIGDYFsOACKKK_5DXMUYlFFS7W-jaY6AjG4,8734
|
|
113
115
|
tests/test_sqlite_shared_memory_behavior.py,sha256=fKx_1BmLS3b8x-9pMgjMycpnaHWY8P-2ZuXEspx6Sbw,4082
|
|
114
116
|
tests/test_verifier_from_string.py,sha256=Lxi3TpFHFb-hG4-UhLKZJkqo84ax9YJY8G6beO-1erM,13581
|
|
115
|
-
fleet_python-0.2.
|
|
116
|
-
fleet_python-0.2.
|
|
117
|
-
fleet_python-0.2.
|
|
118
|
-
fleet_python-0.2.
|
|
119
|
-
fleet_python-0.2.
|
|
117
|
+
fleet_python-0.2.129.dist-info/METADATA,sha256=E5styAa9s0pNU03jBq-ncZaeUsNnDGvbpjsTXjUHjWw,4240
|
|
118
|
+
fleet_python-0.2.129.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
119
|
+
fleet_python-0.2.129.dist-info/entry_points.txt,sha256=qKIQ326cHR5WyCd16QnrW-1DpcT0YyxVRDb3IlTyzTA,39
|
|
120
|
+
fleet_python-0.2.129.dist-info/top_level.txt,sha256=qb1zIbtEktyhRFZdqVytwg54l64qtoZL0wjHB4bUg3c,29
|
|
121
|
+
fleet_python-0.2.129.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|