fleet-python 0.2.126__tar.gz → 0.2.127__tar.gz

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.
Files changed (126) hide show
  1. {fleet_python-0.2.126/fleet_python.egg-info → fleet_python-0.2.127}/PKG-INFO +1 -1
  2. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/__init__.py +7 -1
  3. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/__init__.py +1 -1
  4. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/base.py +1 -1
  5. fleet_python-0.2.127/fleet/_async/browser.py +191 -0
  6. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/client.py +90 -3
  7. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/base.py +1 -1
  8. fleet_python-0.2.127/fleet/browser.py +216 -0
  9. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/client.py +95 -3
  10. {fleet_python-0.2.126 → fleet_python-0.2.127/fleet_python.egg-info}/PKG-INFO +1 -1
  11. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet_python.egg-info/SOURCES.txt +2 -0
  12. {fleet_python-0.2.126 → fleet_python-0.2.127}/pyproject.toml +1 -1
  13. {fleet_python-0.2.126 → fleet_python-0.2.127}/LICENSE +0 -0
  14. {fleet_python-0.2.126 → fleet_python-0.2.127}/README.md +0 -0
  15. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/diff_example.py +0 -0
  16. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/dsl_example.py +0 -0
  17. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/example.py +0 -0
  18. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/exampleResume.py +0 -0
  19. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/example_account.py +0 -0
  20. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/example_action_log.py +0 -0
  21. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/example_client.py +0 -0
  22. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/example_mcp_anthropic.py +0 -0
  23. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/example_mcp_openai.py +0 -0
  24. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/example_sync.py +0 -0
  25. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/example_task.py +0 -0
  26. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/example_tasks.py +0 -0
  27. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/example_verifier.py +0 -0
  28. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/export_tasks.py +0 -0
  29. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/export_tasks_filtered.py +0 -0
  30. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/fetch_tasks.py +0 -0
  31. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/gemini_example.py +0 -0
  32. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/import_tasks.py +0 -0
  33. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/iterate_verifiers.py +0 -0
  34. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/json_tasks_example.py +0 -0
  35. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/nova_act_example.py +0 -0
  36. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/openai_example.py +0 -0
  37. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/openai_simple_example.py +0 -0
  38. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/query_builder_example.py +0 -0
  39. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/quickstart.py +0 -0
  40. {fleet_python-0.2.126 → fleet_python-0.2.127}/examples/test_cdp_logging.py +0 -0
  41. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/env/__init__.py +0 -0
  42. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/env/client.py +0 -0
  43. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/exceptions.py +0 -0
  44. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/global_client.py +0 -0
  45. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/instance/__init__.py +0 -0
  46. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/instance/base.py +0 -0
  47. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/instance/client.py +0 -0
  48. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/judge.py +0 -0
  49. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/models.py +0 -0
  50. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/resources/__init__.py +0 -0
  51. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/resources/api.py +0 -0
  52. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/resources/base.py +0 -0
  53. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/resources/browser.py +0 -0
  54. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/resources/filesystem.py +0 -0
  55. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/resources/mcp.py +0 -0
  56. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/resources/sqlite.py +0 -0
  57. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/tasks.py +0 -0
  58. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/verifiers/__init__.py +0 -0
  59. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/verifiers/bundler.py +0 -0
  60. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/_async/verifiers/verifier.py +0 -0
  61. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/agent/__init__.py +0 -0
  62. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/agent/gemini_cua/Dockerfile +0 -0
  63. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/agent/gemini_cua/__init__.py +0 -0
  64. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/agent/gemini_cua/agent.py +0 -0
  65. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/agent/gemini_cua/mcp/main.py +0 -0
  66. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/agent/gemini_cua/mcp_server/__init__.py +0 -0
  67. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/agent/gemini_cua/mcp_server/main.py +0 -0
  68. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/agent/gemini_cua/mcp_server/tools.py +0 -0
  69. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/agent/gemini_cua/requirements.txt +0 -0
  70. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/agent/gemini_cua/start.sh +0 -0
  71. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/agent/orchestrator.py +0 -0
  72. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/agent/types.py +0 -0
  73. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/agent/utils.py +0 -0
  74. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/cli.py +0 -0
  75. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/config.py +0 -0
  76. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/env/__init__.py +0 -0
  77. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/env/client.py +0 -0
  78. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/eval/__init__.py +0 -0
  79. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/eval/uploader.py +0 -0
  80. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/exceptions.py +0 -0
  81. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/global_client.py +0 -0
  82. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/instance/__init__.py +0 -0
  83. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/instance/base.py +0 -0
  84. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/instance/client.py +0 -0
  85. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/instance/models.py +0 -0
  86. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/judge.py +0 -0
  87. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/models.py +0 -0
  88. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/proxy/__init__.py +0 -0
  89. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/proxy/proxy.py +0 -0
  90. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/proxy/whitelist.py +0 -0
  91. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/resources/__init__.py +0 -0
  92. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/resources/api.py +0 -0
  93. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/resources/base.py +0 -0
  94. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/resources/browser.py +0 -0
  95. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/resources/filesystem.py +0 -0
  96. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/resources/mcp.py +0 -0
  97. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/resources/sqlite.py +0 -0
  98. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/tasks.py +0 -0
  99. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/types.py +0 -0
  100. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/utils/__init__.py +0 -0
  101. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/utils/http_logging.py +0 -0
  102. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/utils/logging.py +0 -0
  103. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/utils/playwright.py +0 -0
  104. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/verifiers/__init__.py +0 -0
  105. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/verifiers/bundler.py +0 -0
  106. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/verifiers/code.py +0 -0
  107. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/verifiers/db.py +0 -0
  108. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/verifiers/decorator.py +0 -0
  109. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/verifiers/parse.py +0 -0
  110. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/verifiers/sql_differ.py +0 -0
  111. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet/verifiers/verifier.py +0 -0
  112. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet_python.egg-info/dependency_links.txt +0 -0
  113. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet_python.egg-info/entry_points.txt +0 -0
  114. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet_python.egg-info/requires.txt +0 -0
  115. {fleet_python-0.2.126 → fleet_python-0.2.127}/fleet_python.egg-info/top_level.txt +0 -0
  116. {fleet_python-0.2.126 → fleet_python-0.2.127}/scripts/fix_sync_imports.py +0 -0
  117. {fleet_python-0.2.126 → fleet_python-0.2.127}/scripts/unasync.py +0 -0
  118. {fleet_python-0.2.126 → fleet_python-0.2.127}/setup.cfg +0 -0
  119. {fleet_python-0.2.126 → fleet_python-0.2.127}/tests/__init__.py +0 -0
  120. {fleet_python-0.2.126 → fleet_python-0.2.127}/tests/test_app_method.py +0 -0
  121. {fleet_python-0.2.126 → fleet_python-0.2.127}/tests/test_expect_exactly.py +0 -0
  122. {fleet_python-0.2.126 → fleet_python-0.2.127}/tests/test_expect_only.py +0 -0
  123. {fleet_python-0.2.126 → fleet_python-0.2.127}/tests/test_instance_dispatch.py +0 -0
  124. {fleet_python-0.2.126 → fleet_python-0.2.127}/tests/test_sqlite_resource_dual_mode.py +0 -0
  125. {fleet_python-0.2.126 → fleet_python-0.2.127}/tests/test_sqlite_shared_memory_behavior.py +0 -0
  126. {fleet_python-0.2.126 → fleet_python-0.2.127}/tests/test_verifier_from_string.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.2.126
3
+ Version: 0.2.127
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -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.126"
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",
@@ -44,7 +44,7 @@ from ..types import VerifierFunction
44
44
  from .. import env
45
45
  from . import global_client as _async_global_client
46
46
 
47
- __version__ = "0.2.126"
47
+ __version__ = "0.2.127"
48
48
 
49
49
  __all__ = [
50
50
  # Core classes
@@ -26,7 +26,7 @@ from .exceptions import (
26
26
  try:
27
27
  from .. import __version__
28
28
  except ImportError:
29
- __version__ = "0.2.126"
29
+ __version__ = "0.2.127"
30
30
 
31
31
  logger = logging.getLogger(__name__)
32
32
 
@@ -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"]
@@ -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(self, name: str = "cdp") -> AsyncBrowserResource:
390
- return self.instance.browser(name)
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."""
@@ -796,6 +844,45 @@ class AsyncFleet:
796
844
  self.client, bundle_data, args, kwargs, timeout
797
845
  )
798
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
+
799
886
  async def delete(self, instance_id: str) -> InstanceResponse:
800
887
  return await _delete_instance(self.client, instance_id)
801
888
 
@@ -27,7 +27,7 @@ from .exceptions import (
27
27
  try:
28
28
  from . import __version__
29
29
  except ImportError:
30
- __version__ = "0.2.126"
30
+ __version__ = "0.2.127"
31
31
 
32
32
  logger = logging.getLogger(__name__)
33
33
 
@@ -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
@@ -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(self, name: str = "cdp") -> BrowserResource:
402
- return self.instance.browser(name)
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."""
@@ -808,6 +856,50 @@ class Fleet:
808
856
  ) -> VerifiersExecuteResponse:
809
857
  return _execute_verifier_remote(self.client, bundle_data, args, kwargs, timeout)
810
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
+
811
903
  def delete(self, instance_id: str) -> InstanceResponse:
812
904
  return _delete_instance(self.client, instance_id)
813
905
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.2.126
3
+ Version: 0.2.127
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -29,6 +29,7 @@ examples/quickstart.py
29
29
  examples/test_cdp_logging.py
30
30
  fleet/__init__.py
31
31
  fleet/base.py
32
+ fleet/browser.py
32
33
  fleet/cli.py
33
34
  fleet/client.py
34
35
  fleet/config.py
@@ -40,6 +41,7 @@ fleet/tasks.py
40
41
  fleet/types.py
41
42
  fleet/_async/__init__.py
42
43
  fleet/_async/base.py
44
+ fleet/_async/browser.py
43
45
  fleet/_async/client.py
44
46
  fleet/_async/exceptions.py
45
47
  fleet/_async/global_client.py
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "fleet-python"
7
7
 
8
- version = "0.2.126"
8
+ version = "0.2.127"
9
9
  description = "Python SDK for Fleet environments"
10
10
  authors = [
11
11
  {name = "Fleet AI", email = "nic@fleet.so"},
File without changes
File without changes
File without changes