fleet-python 0.2.125__py3-none-any.whl → 0.2.127__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.125.dist-info → fleet_python-0.2.127.dist-info}/METADATA +1 -1
- {fleet_python-0.2.125.dist-info → fleet_python-0.2.127.dist-info}/RECORD +16 -14
- {fleet_python-0.2.125.dist-info → fleet_python-0.2.127.dist-info}/WHEEL +0 -0
- {fleet_python-0.2.125.dist-info → fleet_python-0.2.127.dist-info}/entry_points.txt +0 -0
- {fleet_python-0.2.125.dist-info → fleet_python-0.2.127.dist-info}/licenses/LICENSE +0 -0
- {fleet_python-0.2.125.dist-info → fleet_python-0.2.127.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.127"
|
|
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."""
|
|
@@ -601,7 +649,9 @@ class AsyncFleet:
|
|
|
601
649
|
)
|
|
602
650
|
|
|
603
651
|
instance = AsyncEnv(client=self.client, **response.json())
|
|
604
|
-
|
|
652
|
+
# Resources are loaded lazily on first `db()`/`browser()`/`resources()` access
|
|
653
|
+
# via `_load_resources()`, so we don't preload here. Eagerly loading would
|
|
654
|
+
# fail-fast with a 502 while the container is still warming up.
|
|
605
655
|
return instance
|
|
606
656
|
|
|
607
657
|
async def make_for_task(self, task: Task) -> AsyncEnv:
|
|
@@ -653,7 +703,7 @@ class AsyncFleet:
|
|
|
653
703
|
else:
|
|
654
704
|
response = await self.client.request("GET", f"/v1/env/instances/{instance_id}")
|
|
655
705
|
instance = AsyncEnv(client=self.client, **response.json())
|
|
656
|
-
|
|
706
|
+
# Resources load lazily on first `db()`/`browser()`/`resources()` access.
|
|
657
707
|
return instance
|
|
658
708
|
|
|
659
709
|
def _create_url_instance(self, base_url: str) -> AsyncEnv:
|
|
@@ -794,6 +844,45 @@ class AsyncFleet:
|
|
|
794
844
|
self.client, bundle_data, args, kwargs, timeout
|
|
795
845
|
)
|
|
796
846
|
|
|
847
|
+
async def create_browser(
|
|
848
|
+
self,
|
|
849
|
+
ttl_seconds: int = 300,
|
|
850
|
+
*,
|
|
851
|
+
lease_id: Optional[str] = None,
|
|
852
|
+
allowed_hosts: Optional[List[str]] = None,
|
|
853
|
+
request_timestamp_ms: Optional[int] = None,
|
|
854
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
855
|
+
jwt_token: Optional[str] = None,
|
|
856
|
+
team_id: Optional[str] = None,
|
|
857
|
+
wait_until_running: bool = False,
|
|
858
|
+
wait_timeout: float = 60.0,
|
|
859
|
+
) -> AsyncBrowserLease:
|
|
860
|
+
"""Create a Fleet Browser lease (``POST /v1/browser``)."""
|
|
861
|
+
return await _create_browser_lease(
|
|
862
|
+
self.client,
|
|
863
|
+
ttl_seconds=ttl_seconds,
|
|
864
|
+
lease_id=lease_id,
|
|
865
|
+
allowed_hosts=allowed_hosts,
|
|
866
|
+
request_timestamp_ms=request_timestamp_ms,
|
|
867
|
+
extra=extra,
|
|
868
|
+
jwt_token=jwt_token,
|
|
869
|
+
team_id=team_id,
|
|
870
|
+
wait_until_running=wait_until_running,
|
|
871
|
+
wait_timeout=wait_timeout,
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
async def get_browser(
|
|
875
|
+
self,
|
|
876
|
+
lease_id: str,
|
|
877
|
+
*,
|
|
878
|
+
jwt_token: Optional[str] = None,
|
|
879
|
+
team_id: Optional[str] = None,
|
|
880
|
+
) -> AsyncBrowserLease:
|
|
881
|
+
"""Inspect an existing browser lease (``GET /v1/browser/{lease_id}``)."""
|
|
882
|
+
return await _get_browser_lease(
|
|
883
|
+
self.client, lease_id, jwt_token=jwt_token, team_id=team_id
|
|
884
|
+
)
|
|
885
|
+
|
|
797
886
|
async def delete(self, instance_id: str) -> InstanceResponse:
|
|
798
887
|
return await _delete_instance(self.client, instance_id)
|
|
799
888
|
|
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):
|
|
@@ -357,6 +358,7 @@ class InstanceResponse(BaseModel):
|
|
|
357
358
|
data_version: Optional[str] = Field(None, title="Data Version")
|
|
358
359
|
urls: Optional[InstanceURLs] = Field(None, title="Urls")
|
|
359
360
|
health: Optional[bool] = Field(None, title="Health")
|
|
361
|
+
multi_env_list: Optional[List[str]] = Field(None, title="Multi Env List")
|
|
360
362
|
|
|
361
363
|
|
|
362
364
|
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
|
@@ -177,10 +177,15 @@ from .instance.base import default_httpx_client
|
|
|
177
177
|
from .instance.client import ValidatorType
|
|
178
178
|
from .resources.base import Resource
|
|
179
179
|
from .resources.sqlite import SQLiteResource
|
|
180
|
-
from .resources.browser import BrowserResource
|
|
181
180
|
from .resources.filesystem import FilesystemResource
|
|
182
181
|
from .resources.mcp import SyncMCPResource
|
|
183
182
|
from .resources.api import APIResource
|
|
183
|
+
from .browser import (
|
|
184
|
+
BrowserLease,
|
|
185
|
+
create_browser as _create_browser_lease,
|
|
186
|
+
get_browser as _get_browser_lease,
|
|
187
|
+
host_from_url,
|
|
188
|
+
)
|
|
184
189
|
|
|
185
190
|
logger = logging.getLogger(__name__)
|
|
186
191
|
|
|
@@ -398,8 +403,51 @@ class SyncEnv(EnvironmentBase):
|
|
|
398
403
|
def db(self, name: str = "current") -> SQLiteResource:
|
|
399
404
|
return self.instance.db(name)
|
|
400
405
|
|
|
401
|
-
def browser(
|
|
402
|
-
|
|
406
|
+
def browser(
|
|
407
|
+
self,
|
|
408
|
+
ttl_seconds: int = 300,
|
|
409
|
+
*,
|
|
410
|
+
lease_id: Optional[str] = None,
|
|
411
|
+
allowed_hosts: Optional[List[str]] = None,
|
|
412
|
+
include_root_host: bool = True,
|
|
413
|
+
wait_until_running: bool = False,
|
|
414
|
+
wait_timeout: float = 60.0,
|
|
415
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
416
|
+
jwt_token: Optional[str] = None,
|
|
417
|
+
team_id: Optional[str] = None,
|
|
418
|
+
) -> BrowserLease:
|
|
419
|
+
"""Spin up an orchestrator-managed Fleet Browser lease for this env.
|
|
420
|
+
|
|
421
|
+
``env.browser()`` posts to ``/v1/browser`` and returns a
|
|
422
|
+
:class:`fleet.browser.BrowserLease` with ``cdp_url`` / ``mcp_url`` /
|
|
423
|
+
``stream_url`` and a ``mcp_tools()`` accessor. By default the host
|
|
424
|
+
from ``self.urls.root`` is prepended to ``allowed_hosts`` so the
|
|
425
|
+
browser can reach the instance — pass ``include_root_host=False``
|
|
426
|
+
to opt out.
|
|
427
|
+
"""
|
|
428
|
+
hosts: Optional[List[str]] = list(allowed_hosts) if allowed_hosts else None
|
|
429
|
+
if include_root_host and self.urls and self.urls.root:
|
|
430
|
+
root_host = host_from_url(self.urls.root)
|
|
431
|
+
if root_host:
|
|
432
|
+
hosts = hosts or []
|
|
433
|
+
if root_host not in hosts:
|
|
434
|
+
hosts.insert(0, root_host)
|
|
435
|
+
return _create_browser_lease(
|
|
436
|
+
self._load_client,
|
|
437
|
+
ttl_seconds=ttl_seconds,
|
|
438
|
+
lease_id=lease_id,
|
|
439
|
+
allowed_hosts=hosts,
|
|
440
|
+
extra=extra,
|
|
441
|
+
jwt_token=jwt_token,
|
|
442
|
+
team_id=team_id,
|
|
443
|
+
wait_until_running=wait_until_running,
|
|
444
|
+
wait_timeout=wait_timeout,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
@property
|
|
448
|
+
def root_url(self) -> Optional[str]:
|
|
449
|
+
"""Convenience: ``self.urls.root`` if available (handy paired with spawn_browser)."""
|
|
450
|
+
return self.urls.root if self.urls else None
|
|
403
451
|
|
|
404
452
|
def fs(self) -> FilesystemResource:
|
|
405
453
|
"""Get a filesystem diff resource for inspecting file changes."""
|
|
@@ -613,7 +661,9 @@ class Fleet:
|
|
|
613
661
|
)
|
|
614
662
|
|
|
615
663
|
instance = SyncEnv(client=self.client, **response.json())
|
|
616
|
-
|
|
664
|
+
# Resources load lazily on first `db()`/`browser()`/`resources()` access via
|
|
665
|
+
# `_load_resources()`. Skipping the eager preload avoids fail-fast 502s while
|
|
666
|
+
# the container is still warming up.
|
|
617
667
|
return instance
|
|
618
668
|
|
|
619
669
|
def make_for_task(self, task: Task) -> SyncEnv:
|
|
@@ -665,7 +715,7 @@ class Fleet:
|
|
|
665
715
|
else:
|
|
666
716
|
response = self.client.request("GET", f"/v1/env/instances/{instance_id}")
|
|
667
717
|
instance = SyncEnv(client=self.client, **response.json())
|
|
668
|
-
|
|
718
|
+
# Resources load lazily on first `db()`/`browser()`/`resources()` access.
|
|
669
719
|
return instance
|
|
670
720
|
|
|
671
721
|
def _create_url_instance(self, base_url: str) -> SyncEnv:
|
|
@@ -806,6 +856,50 @@ class Fleet:
|
|
|
806
856
|
) -> VerifiersExecuteResponse:
|
|
807
857
|
return _execute_verifier_remote(self.client, bundle_data, args, kwargs, timeout)
|
|
808
858
|
|
|
859
|
+
def create_browser(
|
|
860
|
+
self,
|
|
861
|
+
ttl_seconds: int = 300,
|
|
862
|
+
*,
|
|
863
|
+
lease_id: Optional[str] = None,
|
|
864
|
+
allowed_hosts: Optional[List[str]] = None,
|
|
865
|
+
request_timestamp_ms: Optional[int] = None,
|
|
866
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
867
|
+
jwt_token: Optional[str] = None,
|
|
868
|
+
team_id: Optional[str] = None,
|
|
869
|
+
wait_until_running: bool = False,
|
|
870
|
+
wait_timeout: float = 60.0,
|
|
871
|
+
) -> BrowserLease:
|
|
872
|
+
"""Create a Fleet Browser lease (``POST /v1/browser``).
|
|
873
|
+
|
|
874
|
+
Freeform — pass any of ``allowed_hosts`` / ``lease_id`` /
|
|
875
|
+
``request_timestamp_ms`` directly, or use ``extra`` for keys this
|
|
876
|
+
SDK version doesn't surface yet.
|
|
877
|
+
"""
|
|
878
|
+
return _create_browser_lease(
|
|
879
|
+
self.client,
|
|
880
|
+
ttl_seconds=ttl_seconds,
|
|
881
|
+
lease_id=lease_id,
|
|
882
|
+
allowed_hosts=allowed_hosts,
|
|
883
|
+
request_timestamp_ms=request_timestamp_ms,
|
|
884
|
+
extra=extra,
|
|
885
|
+
jwt_token=jwt_token,
|
|
886
|
+
team_id=team_id,
|
|
887
|
+
wait_until_running=wait_until_running,
|
|
888
|
+
wait_timeout=wait_timeout,
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
def get_browser(
|
|
892
|
+
self,
|
|
893
|
+
lease_id: str,
|
|
894
|
+
*,
|
|
895
|
+
jwt_token: Optional[str] = None,
|
|
896
|
+
team_id: Optional[str] = None,
|
|
897
|
+
) -> BrowserLease:
|
|
898
|
+
"""Inspect an existing browser lease (``GET /v1/browser/{lease_id}``)."""
|
|
899
|
+
return _get_browser_lease(
|
|
900
|
+
self.client, lease_id, jwt_token=jwt_token, team_id=team_id
|
|
901
|
+
)
|
|
902
|
+
|
|
809
903
|
def delete(self, instance_id: str) -> InstanceResponse:
|
|
810
904
|
return _delete_instance(self.client, instance_id)
|
|
811
905
|
|
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):
|
|
@@ -369,6 +370,7 @@ class InstanceResponse(BaseModel):
|
|
|
369
370
|
profile_id: Optional[str] = Field(None, title="Profile Id")
|
|
370
371
|
heartbeat_interval: Optional[int] = Field(None, title="Heartbeat Interval")
|
|
371
372
|
heartbeat_region: Optional[str] = Field(None, title="Heartbeat Region")
|
|
373
|
+
multi_env_list: Optional[List[str]] = Field(None, title="Multi Env List")
|
|
372
374
|
|
|
373
375
|
|
|
374
376
|
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=vxo5Rc8G5_nLKQfUmAdRjwF8VsYMDD4Ml9aCZb9oFBc,8193
|
|
28
|
+
fleet/base.py,sha256=HoszFAHJI5ppu3qshLqV8FR7Q_remz57j6Cu1j0bzLM,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=ljLkd8ebn7MKD5tLgh-dS0ZjK4w1DLNWBsx8boxWyis,72321
|
|
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=3IMbL_BCTSmOKhiVNuguTJ-y4mVrrcGawRd4_ZnFvXc,25329
|
|
36
37
|
fleet/tasks.py,sha256=8pEzXmgC7RslqsMC_0s6shhr_t2WGIRpTRqo-MAQjdg,20778
|
|
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=pto8pgZsakpTBmqKytR2wmS2ogmAf8TbzilpnlsgbCQ,9116
|
|
40
|
+
fleet/_async/base.py,sha256=N0twsinB-fmJqBQKYMXDJSTMPaMI55ejKojkr9YHxbk,9774
|
|
41
|
+
fleet/_async/browser.py,sha256=_lU23nuoSg8gwtiGGd4gGvD9ZRQ-gJFo42zxvDdM_ic,6388
|
|
42
|
+
fleet/_async/client.py,sha256=OI3cD7CJokyJqI0IN7_cwns5CEVmZILV9Ow2JmLJcb8,68619
|
|
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=6B_Umf9DYToWENHrvt1ExkBt03X0Th11GDNFL7MEldU,13743
|
|
45
47
|
fleet/_async/tasks.py,sha256=rlY6SmWl3ZABXe5-ky63wfwYETd2K-Na_m7g5KI1GoI,20602
|
|
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=iqGevW7dSd0J5RdRQjpu-zioy_FYAXnzMfkuB3-QmO0,14601
|
|
104
|
-
fleet_python-0.2.
|
|
106
|
+
fleet_python-0.2.127.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.127.dist-info/METADATA,sha256=zGHs5kaIfn_Pw4svle84Ro_KgURnnKUxe9TAJ8Wdrog,4240
|
|
118
|
+
fleet_python-0.2.127.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
119
|
+
fleet_python-0.2.127.dist-info/entry_points.txt,sha256=qKIQ326cHR5WyCd16QnrW-1DpcT0YyxVRDb3IlTyzTA,39
|
|
120
|
+
fleet_python-0.2.127.dist-info/top_level.txt,sha256=qb1zIbtEktyhRFZdqVytwg54l64qtoZL0wjHB4bUg3c,29
|
|
121
|
+
fleet_python-0.2.127.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|