fleet-python 0.2.66b2__py3-none-any.whl → 0.2.105__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.
Files changed (70) hide show
  1. examples/export_tasks.py +16 -5
  2. examples/export_tasks_filtered.py +245 -0
  3. examples/fetch_tasks.py +230 -0
  4. examples/import_tasks.py +140 -8
  5. examples/iterate_verifiers.py +725 -0
  6. fleet/__init__.py +128 -5
  7. fleet/_async/__init__.py +27 -3
  8. fleet/_async/base.py +24 -9
  9. fleet/_async/client.py +938 -41
  10. fleet/_async/env/client.py +60 -3
  11. fleet/_async/instance/client.py +52 -7
  12. fleet/_async/models.py +15 -0
  13. fleet/_async/resources/api.py +200 -0
  14. fleet/_async/resources/sqlite.py +1801 -46
  15. fleet/_async/tasks.py +122 -25
  16. fleet/_async/verifiers/bundler.py +22 -21
  17. fleet/_async/verifiers/verifier.py +25 -19
  18. fleet/agent/__init__.py +32 -0
  19. fleet/agent/gemini_cua/Dockerfile +45 -0
  20. fleet/agent/gemini_cua/__init__.py +10 -0
  21. fleet/agent/gemini_cua/agent.py +759 -0
  22. fleet/agent/gemini_cua/mcp/main.py +108 -0
  23. fleet/agent/gemini_cua/mcp_server/__init__.py +5 -0
  24. fleet/agent/gemini_cua/mcp_server/main.py +105 -0
  25. fleet/agent/gemini_cua/mcp_server/tools.py +178 -0
  26. fleet/agent/gemini_cua/requirements.txt +5 -0
  27. fleet/agent/gemini_cua/start.sh +30 -0
  28. fleet/agent/orchestrator.py +854 -0
  29. fleet/agent/types.py +49 -0
  30. fleet/agent/utils.py +34 -0
  31. fleet/base.py +34 -9
  32. fleet/cli.py +1061 -0
  33. fleet/client.py +1060 -48
  34. fleet/config.py +1 -1
  35. fleet/env/__init__.py +16 -0
  36. fleet/env/client.py +60 -3
  37. fleet/eval/__init__.py +15 -0
  38. fleet/eval/uploader.py +231 -0
  39. fleet/exceptions.py +8 -0
  40. fleet/instance/client.py +53 -8
  41. fleet/instance/models.py +1 -0
  42. fleet/models.py +303 -0
  43. fleet/proxy/__init__.py +25 -0
  44. fleet/proxy/proxy.py +453 -0
  45. fleet/proxy/whitelist.py +244 -0
  46. fleet/resources/api.py +200 -0
  47. fleet/resources/sqlite.py +1845 -46
  48. fleet/tasks.py +113 -20
  49. fleet/utils/__init__.py +7 -0
  50. fleet/utils/http_logging.py +178 -0
  51. fleet/utils/logging.py +13 -0
  52. fleet/utils/playwright.py +440 -0
  53. fleet/verifiers/bundler.py +22 -21
  54. fleet/verifiers/db.py +985 -1
  55. fleet/verifiers/decorator.py +1 -1
  56. fleet/verifiers/verifier.py +25 -19
  57. {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/METADATA +28 -1
  58. fleet_python-0.2.105.dist-info/RECORD +115 -0
  59. {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/WHEEL +1 -1
  60. fleet_python-0.2.105.dist-info/entry_points.txt +2 -0
  61. tests/test_app_method.py +85 -0
  62. tests/test_expect_exactly.py +4148 -0
  63. tests/test_expect_only.py +2593 -0
  64. tests/test_instance_dispatch.py +607 -0
  65. tests/test_sqlite_resource_dual_mode.py +263 -0
  66. tests/test_sqlite_shared_memory_behavior.py +117 -0
  67. fleet_python-0.2.66b2.dist-info/RECORD +0 -81
  68. tests/test_verifier_security.py +0 -427
  69. {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/licenses/LICENSE +0 -0
  70. {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,5 @@
1
1
  from ..client import AsyncFleet, AsyncEnv, Task
2
- from ...models import Environment as EnvironmentModel, AccountResponse
2
+ from ...models import Environment as EnvironmentModel, AccountResponse, InstanceResponse, Run, HeartbeatResponse
3
3
  from typing import List, Optional, Dict, Any
4
4
 
5
5
 
@@ -10,6 +10,8 @@ async def make_async(
10
10
  env_variables: Optional[Dict[str, Any]] = None,
11
11
  image_type: Optional[str] = None,
12
12
  ttl_seconds: Optional[int] = None,
13
+ run_id: Optional[str] = None,
14
+ heartbeat_interval: Optional[int] = None,
13
15
  ) -> AsyncEnv:
14
16
  return await AsyncFleet().make(
15
17
  env_key,
@@ -18,6 +20,8 @@ async def make_async(
18
20
  env_variables=env_variables,
19
21
  image_type=image_type,
20
22
  ttl_seconds=ttl_seconds,
23
+ run_id=run_id,
24
+ heartbeat_interval=heartbeat_interval,
21
25
  )
22
26
 
23
27
 
@@ -34,14 +38,67 @@ async def list_regions_async() -> List[str]:
34
38
 
35
39
 
36
40
  async def list_instances_async(
37
- status: Optional[str] = None, region: Optional[str] = None
41
+ status: Optional[str] = None, region: Optional[str] = None, run_id: Optional[str] = None, profile_id: Optional[str] = None
38
42
  ) -> List[AsyncEnv]:
39
- return await AsyncFleet().instances(status=status, region=region)
43
+ return await AsyncFleet().instances(status=status, region=region, run_id=run_id, profile_id=profile_id)
40
44
 
41
45
 
42
46
  async def get_async(instance_id: str) -> AsyncEnv:
43
47
  return await AsyncFleet().instance(instance_id)
44
48
 
45
49
 
50
+ async def close_async(instance_id: str) -> InstanceResponse:
51
+ """Close (delete) a specific instance by ID.
52
+
53
+ Args:
54
+ instance_id: The instance ID to close
55
+
56
+ Returns:
57
+ InstanceResponse containing the deleted instance details
58
+ """
59
+ return await AsyncFleet().close(instance_id)
60
+
61
+
62
+ async def close_all_async(run_id: Optional[str] = None, profile_id: Optional[str] = None) -> List[InstanceResponse]:
63
+ """Close (delete) instances using the batch delete endpoint.
64
+
65
+ Args:
66
+ run_id: Optional run ID to filter instances by
67
+ profile_id: Optional profile ID to filter instances by (use "self" for your own profile)
68
+
69
+ Returns:
70
+ List[InstanceResponse] containing the deleted instances
71
+
72
+ Note:
73
+ At least one of run_id or profile_id must be provided.
74
+ """
75
+ return await AsyncFleet().close_all(run_id=run_id, profile_id=profile_id)
76
+
77
+
78
+ async def list_runs_async(profile_id: Optional[str] = None, status: Optional[str] = "active") -> List[Run]:
79
+ """List all runs (groups of instances by run_id) with aggregated statistics.
80
+
81
+ Args:
82
+ profile_id: Optional profile ID to filter runs by (use "self" for your own profile)
83
+ status: Filter by run status - "active" (default), "inactive", or "all"
84
+
85
+ Returns:
86
+ List[Run] containing run information with instance counts and timestamps
87
+ """
88
+ return await AsyncFleet().list_runs(profile_id=profile_id, status=status)
89
+
90
+
91
+ async def heartbeat_async(instance_id: str) -> HeartbeatResponse:
92
+ """Send heartbeat to keep instance alive (if heartbeat monitoring is enabled).
93
+
94
+ Args:
95
+ instance_id: The instance ID to send heartbeat for
96
+
97
+ Returns:
98
+ HeartbeatResponse containing heartbeat status and deadline information
99
+ """
100
+ return await AsyncFleet().heartbeat(instance_id)
101
+
102
+
46
103
  async def account_async() -> AccountResponse:
47
104
  return await AsyncFleet().account()
@@ -9,6 +9,7 @@ from urllib.parse import urlparse
9
9
 
10
10
  from ..resources.sqlite import AsyncSQLiteResource
11
11
  from ..resources.browser import AsyncBrowserResource
12
+ from ..resources.api import AsyncAPIResource
12
13
  from ..resources.base import Resource
13
14
 
14
15
  from fleet.verifiers import DatabaseSnapshot
@@ -23,6 +24,7 @@ from ...instance.models import (
23
24
  ResetResponse,
24
25
  Resource as ResourceModel,
25
26
  ResourceType,
27
+ ResourceMode,
26
28
  HealthResponse,
27
29
  ExecuteFunctionRequest,
28
30
  ExecuteFunctionResponse,
@@ -35,6 +37,7 @@ logger = logging.getLogger(__name__)
35
37
  RESOURCE_TYPES = {
36
38
  ResourceType.db: AsyncSQLiteResource,
37
39
  ResourceType.cdp: AsyncBrowserResource,
40
+ ResourceType.api: AsyncAPIResource,
38
41
  }
39
42
 
40
43
  ValidatorType = Callable[
@@ -85,15 +88,46 @@ class AsyncInstanceClient:
85
88
  Returns:
86
89
  An SQLite database resource for the given database name
87
90
  """
88
- return AsyncSQLiteResource(
89
- self._resources_state[ResourceType.db.value][name], self.client
90
- )
91
+ resource_info = self._resources_state[ResourceType.db.value][name]
92
+ # Local mode - resource_info is a dict with creation parameters
93
+ if isinstance(resource_info, dict) and resource_info.get('type') == 'local':
94
+ # Create new instance each time (matching HTTP mode behavior)
95
+ return AsyncSQLiteResource(
96
+ resource_info['resource_model'],
97
+ client=None,
98
+ db_path=resource_info['db_path']
99
+ )
100
+ # HTTP mode - resource_info is a ResourceModel, create new wrapper
101
+ return AsyncSQLiteResource(resource_info, self.client)
91
102
 
92
103
  def browser(self, name: str) -> AsyncBrowserResource:
93
104
  return AsyncBrowserResource(
94
105
  self._resources_state[ResourceType.cdp.value][name], self.client
95
106
  )
96
107
 
108
+ def api(self, name: str, base_url: str) -> AsyncAPIResource:
109
+ """
110
+ Returns an API resource for making HTTP requests.
111
+
112
+ Args:
113
+ name: The name of the API resource
114
+ base_url: The base URL for API requests
115
+
116
+ Returns:
117
+ An AsyncAPIResource for making HTTP requests
118
+ """
119
+ # Create a minimal resource model for API
120
+ resource_model = ResourceModel(
121
+ name=name,
122
+ type=ResourceType.api,
123
+ mode=ResourceMode.rw,
124
+ )
125
+ return AsyncAPIResource(
126
+ resource_model,
127
+ base_url=base_url,
128
+ client=self.client.httpx_client if self.client else None,
129
+ )
130
+
97
131
  async def resources(self) -> List[Resource]:
98
132
  await self._load_resources()
99
133
  return [
@@ -130,10 +164,14 @@ class AsyncInstanceClient:
130
164
 
131
165
  async def _load_resources(self) -> None:
132
166
  if self._resources is None:
133
- response = await self.client.request("GET", "/resources", timeout=1.0)
167
+ response = await self.client.request("GET", "/resources")
134
168
  if response.status_code != 200:
135
- self._resources = []
136
- return
169
+ response_body = response.text[:500] if response.text else "empty"
170
+ self._resources = [] # Mark as loaded (empty) to prevent retries
171
+ raise FleetEnvironmentError(
172
+ f"Failed to load instance resources: status_code={response.status_code} "
173
+ f"(url={self.base_url}, response={response_body})"
174
+ )
137
175
 
138
176
  # Handle both old and new response formats
139
177
  response_data = response.json()
@@ -177,10 +215,17 @@ class AsyncInstanceClient:
177
215
  response = await self.client.request("GET", "/health")
178
216
  return HealthResponse(**response.json())
179
217
 
218
+ def close(self):
219
+ """Close anchor connections for in-memory databases."""
220
+ if hasattr(self, '_memory_anchors'):
221
+ for conn in self._memory_anchors.values():
222
+ conn.close()
223
+ self._memory_anchors.clear()
224
+
180
225
  async def __aenter__(self):
181
226
  """Async context manager entry."""
182
227
  return self
183
228
 
184
229
  async def __aexit__(self, exc_type, exc_val, exc_tb):
185
230
  """Async context manager exit."""
186
- await self.close()
231
+ self.close()
fleet/_async/models.py CHANGED
@@ -120,6 +120,7 @@ class ResourceMode(Enum):
120
120
  class ResourceType(Enum):
121
121
  sqlite = "sqlite"
122
122
  cdp = "cdp"
123
+ api = "api"
123
124
 
124
125
 
125
126
  class RestoreRequest(BaseModel):
@@ -155,12 +156,23 @@ class TaskRequest(BaseModel):
155
156
  verifier_id: Optional[str] = Field(None, title="Verifier Id")
156
157
  version: Optional[str] = Field(None, title="Version")
157
158
  env_variables: Optional[Dict[str, Any]] = Field(None, title="Env Variables")
159
+ metadata: Optional[Dict[str, Any]] = Field(None, title="Metadata")
160
+ writer_metadata: Optional[Dict[str, Any]] = Field(
161
+ None, title="Writer Metadata", description="Metadata filled by task writer"
162
+ )
158
163
  output_json_schema: Optional[Dict[str, Any]] = Field(None, title="Output Json Schema")
159
164
 
160
165
 
161
166
  class TaskUpdateRequest(BaseModel):
162
167
  prompt: Optional[str] = Field(None, title="Prompt")
163
168
  verifier_code: Optional[str] = Field(None, title="Verifier Code")
169
+ metadata: Optional[Dict[str, Any]] = Field(None, title="Metadata")
170
+ writer_metadata: Optional[Dict[str, Any]] = Field(
171
+ None, title="Writer Metadata", description="Metadata filled by task writer"
172
+ )
173
+ qa_metadata: Optional[Dict[str, Any]] = Field(
174
+ None, title="QA Metadata", description="Metadata filled by QA reviewer"
175
+ )
164
176
 
165
177
 
166
178
  class VerifierData(BaseModel):
@@ -186,6 +198,9 @@ class TaskResponse(BaseModel):
186
198
  data_version: Optional[str] = Field(None, title="Data Version")
187
199
  env_variables: Optional[Dict[str, Any]] = Field(None, title="Env Variables")
188
200
  verifier: Optional[VerifierData] = Field(None, title="Verifier")
201
+ metadata: Optional[Dict[str, Any]] = Field(None, title="Metadata")
202
+ writer_metadata: Optional[Dict[str, Any]] = Field(None, title="Writer Metadata")
203
+ qa_metadata: Optional[Dict[str, Any]] = Field(None, title="QA Metadata")
189
204
  output_json_schema: Optional[Dict[str, Any]] = Field(None, title="Output Json Schema")
190
205
 
191
206
 
@@ -0,0 +1,200 @@
1
+ """Async API Resource for making HTTP requests to the app's API endpoint."""
2
+
3
+ from typing import Any, Dict, Optional
4
+ import httpx
5
+
6
+ from .base import Resource
7
+ from ...instance.models import Resource as ResourceModel
8
+
9
+
10
+ class AsyncAPIResponse:
11
+ """Simple wrapper around httpx.Response with a requests-like interface."""
12
+
13
+ def __init__(self, response: httpx.Response):
14
+ self._response = response
15
+ self.status_code: int = response.status_code
16
+ self.headers: httpx.Headers = response.headers
17
+ self.text: str = response.text
18
+ self.content: bytes = response.content
19
+ self.url: str = str(response.url)
20
+ self.ok: bool = response.is_success
21
+
22
+ def json(self) -> Any:
23
+ """Parse response body as JSON."""
24
+ return self._response.json()
25
+
26
+ def raise_for_status(self) -> "AsyncAPIResponse":
27
+ """Raise an HTTPStatusError if the response has an error status code."""
28
+ self._response.raise_for_status()
29
+ return self
30
+
31
+ def __repr__(self) -> str:
32
+ return f"<AsyncAPIResponse [{self.status_code}]>"
33
+
34
+
35
+ class AsyncAPIResource(Resource):
36
+ """Async HTTP client for making requests to the app's API endpoint.
37
+
38
+ Provides a requests-like interface for interacting with the app's REST API.
39
+
40
+ Example:
41
+ api = env.api()
42
+ response = await api.get("/users/1")
43
+ print(response.status_code) # 200
44
+ print(response.json()) # {"id": 1, "name": "John"}
45
+
46
+ # With headers/auth
47
+ response = await api.post(
48
+ "/users",
49
+ json={"name": "Jane"},
50
+ headers={"Authorization": "Bearer xxx"}
51
+ )
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ resource: ResourceModel,
57
+ base_url: str,
58
+ client: Optional[httpx.AsyncClient] = None,
59
+ ):
60
+ super().__init__(resource)
61
+ self.base_url = base_url.rstrip("/")
62
+ self._client = client or httpx.AsyncClient()
63
+
64
+ def _build_url(self, path: str) -> str:
65
+ """Build full URL from base_url and path."""
66
+ if path.startswith("/"):
67
+ return f"{self.base_url}{path}"
68
+ return f"{self.base_url}/{path}"
69
+
70
+ async def request(
71
+ self,
72
+ method: str,
73
+ path: str,
74
+ *,
75
+ params: Optional[Dict[str, Any]] = None,
76
+ json: Optional[Any] = None,
77
+ data: Optional[Dict[str, Any]] = None,
78
+ headers: Optional[Dict[str, str]] = None,
79
+ cookies: Optional[Dict[str, str]] = None,
80
+ timeout: Optional[float] = None,
81
+ **kwargs: Any,
82
+ ) -> AsyncAPIResponse:
83
+ """Make an HTTP request.
84
+
85
+ Args:
86
+ method: HTTP method (GET, POST, PUT, PATCH, DELETE, etc.)
87
+ path: URL path (relative to base_url)
88
+ params: Query parameters
89
+ json: JSON body (will be serialized)
90
+ data: Form data
91
+ headers: Request headers
92
+ cookies: Request cookies
93
+ timeout: Request timeout in seconds
94
+ **kwargs: Additional arguments passed to httpx
95
+
96
+ Returns:
97
+ AsyncAPIResponse with status_code, headers, text, content, json() method
98
+ """
99
+ url = self._build_url(path)
100
+ response = await self._client.request(
101
+ method,
102
+ url,
103
+ params=params,
104
+ json=json,
105
+ data=data,
106
+ headers=headers,
107
+ cookies=cookies,
108
+ timeout=timeout,
109
+ **kwargs,
110
+ )
111
+ return AsyncAPIResponse(response)
112
+
113
+ async def get(
114
+ self,
115
+ path: str,
116
+ *,
117
+ params: Optional[Dict[str, Any]] = None,
118
+ headers: Optional[Dict[str, str]] = None,
119
+ **kwargs: Any,
120
+ ) -> AsyncAPIResponse:
121
+ """Make a GET request."""
122
+ return await self.request("GET", path, params=params, headers=headers, **kwargs)
123
+
124
+ async def post(
125
+ self,
126
+ path: str,
127
+ *,
128
+ json: Optional[Any] = None,
129
+ data: Optional[Dict[str, Any]] = None,
130
+ params: Optional[Dict[str, Any]] = None,
131
+ headers: Optional[Dict[str, str]] = None,
132
+ **kwargs: Any,
133
+ ) -> AsyncAPIResponse:
134
+ """Make a POST request."""
135
+ return await self.request(
136
+ "POST", path, json=json, data=data, params=params, headers=headers, **kwargs
137
+ )
138
+
139
+ async def put(
140
+ self,
141
+ path: str,
142
+ *,
143
+ json: Optional[Any] = None,
144
+ data: Optional[Dict[str, Any]] = None,
145
+ params: Optional[Dict[str, Any]] = None,
146
+ headers: Optional[Dict[str, str]] = None,
147
+ **kwargs: Any,
148
+ ) -> AsyncAPIResponse:
149
+ """Make a PUT request."""
150
+ return await self.request(
151
+ "PUT", path, json=json, data=data, params=params, headers=headers, **kwargs
152
+ )
153
+
154
+ async def patch(
155
+ self,
156
+ path: str,
157
+ *,
158
+ json: Optional[Any] = None,
159
+ data: Optional[Dict[str, Any]] = None,
160
+ params: Optional[Dict[str, Any]] = None,
161
+ headers: Optional[Dict[str, str]] = None,
162
+ **kwargs: Any,
163
+ ) -> AsyncAPIResponse:
164
+ """Make a PATCH request."""
165
+ return await self.request(
166
+ "PATCH", path, json=json, data=data, params=params, headers=headers, **kwargs
167
+ )
168
+
169
+ async def delete(
170
+ self,
171
+ path: str,
172
+ *,
173
+ params: Optional[Dict[str, Any]] = None,
174
+ headers: Optional[Dict[str, str]] = None,
175
+ **kwargs: Any,
176
+ ) -> AsyncAPIResponse:
177
+ """Make a DELETE request."""
178
+ return await self.request("DELETE", path, params=params, headers=headers, **kwargs)
179
+
180
+ async def head(
181
+ self,
182
+ path: str,
183
+ *,
184
+ params: Optional[Dict[str, Any]] = None,
185
+ headers: Optional[Dict[str, str]] = None,
186
+ **kwargs: Any,
187
+ ) -> AsyncAPIResponse:
188
+ """Make a HEAD request."""
189
+ return await self.request("HEAD", path, params=params, headers=headers, **kwargs)
190
+
191
+ async def options(
192
+ self,
193
+ path: str,
194
+ *,
195
+ params: Optional[Dict[str, Any]] = None,
196
+ headers: Optional[Dict[str, str]] = None,
197
+ **kwargs: Any,
198
+ ) -> AsyncAPIResponse:
199
+ """Make an OPTIONS request."""
200
+ return await self.request("OPTIONS", path, params=params, headers=headers, **kwargs)