fleet-python 0.2.69b3__tar.gz → 0.2.70__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.

Potentially problematic release.


This version of fleet-python might be problematic. Click here for more details.

Files changed (90) hide show
  1. {fleet_python-0.2.69b3/fleet_python.egg-info → fleet_python-0.2.70}/PKG-INFO +1 -1
  2. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/__init__.py +3 -2
  3. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/__init__.py +26 -2
  4. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/base.py +21 -10
  5. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/client.py +131 -201
  6. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/env/client.py +38 -7
  7. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/instance/client.py +4 -19
  8. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/resources/sqlite.py +1 -150
  9. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/tasks.py +13 -7
  10. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/verifiers/bundler.py +22 -21
  11. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/verifiers/verifier.py +20 -19
  12. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/base.py +21 -10
  13. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/client.py +137 -219
  14. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/config.py +1 -1
  15. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/env/__init__.py +8 -0
  16. fleet_python-0.2.70/fleet/env/client.py +104 -0
  17. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/instance/client.py +5 -20
  18. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/models.py +33 -0
  19. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/resources/sqlite.py +1 -143
  20. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/tasks.py +15 -7
  21. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/verifiers/bundler.py +22 -21
  22. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/verifiers/decorator.py +1 -1
  23. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/verifiers/verifier.py +20 -19
  24. {fleet_python-0.2.69b3 → fleet_python-0.2.70/fleet_python.egg-info}/PKG-INFO +1 -1
  25. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet_python.egg-info/SOURCES.txt +0 -4
  26. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/pyproject.toml +1 -1
  27. fleet_python-0.2.69b3/fleet/env/client.py +0 -73
  28. fleet_python-0.2.69b3/tests/test_app_method.py +0 -85
  29. fleet_python-0.2.69b3/tests/test_instance_dispatch.py +0 -607
  30. fleet_python-0.2.69b3/tests/test_sqlite_resource_dual_mode.py +0 -263
  31. fleet_python-0.2.69b3/tests/test_sqlite_shared_memory_behavior.py +0 -117
  32. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/LICENSE +0 -0
  33. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/README.md +0 -0
  34. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/diff_example.py +0 -0
  35. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/dsl_example.py +0 -0
  36. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/example.py +0 -0
  37. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/exampleResume.py +0 -0
  38. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/example_account.py +0 -0
  39. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/example_action_log.py +0 -0
  40. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/example_client.py +0 -0
  41. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/example_mcp_anthropic.py +0 -0
  42. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/example_mcp_openai.py +0 -0
  43. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/example_sync.py +0 -0
  44. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/example_task.py +0 -0
  45. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/example_tasks.py +0 -0
  46. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/example_verifier.py +0 -0
  47. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/export_tasks.py +0 -0
  48. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/gemini_example.py +0 -0
  49. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/import_tasks.py +0 -0
  50. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/json_tasks_example.py +0 -0
  51. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/nova_act_example.py +0 -0
  52. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/openai_example.py +0 -0
  53. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/openai_simple_example.py +0 -0
  54. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/query_builder_example.py +0 -0
  55. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/quickstart.py +0 -0
  56. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/test_cdp_logging.py +0 -0
  57. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/env/__init__.py +0 -0
  58. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/exceptions.py +0 -0
  59. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/global_client.py +0 -0
  60. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/instance/__init__.py +0 -0
  61. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/instance/base.py +0 -0
  62. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/models.py +0 -0
  63. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/resources/__init__.py +0 -0
  64. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/resources/base.py +0 -0
  65. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/resources/browser.py +0 -0
  66. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/resources/mcp.py +0 -0
  67. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/verifiers/__init__.py +0 -0
  68. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/exceptions.py +0 -0
  69. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/global_client.py +0 -0
  70. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/instance/__init__.py +0 -0
  71. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/instance/base.py +0 -0
  72. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/instance/models.py +0 -0
  73. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/resources/__init__.py +0 -0
  74. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/resources/base.py +0 -0
  75. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/resources/browser.py +0 -0
  76. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/resources/mcp.py +0 -0
  77. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/types.py +0 -0
  78. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/verifiers/__init__.py +0 -0
  79. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/verifiers/code.py +0 -0
  80. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/verifiers/db.py +0 -0
  81. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/verifiers/parse.py +0 -0
  82. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/verifiers/sql_differ.py +0 -0
  83. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet_python.egg-info/dependency_links.txt +0 -0
  84. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet_python.egg-info/requires.txt +0 -0
  85. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet_python.egg-info/top_level.txt +0 -0
  86. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/scripts/fix_sync_imports.py +0 -0
  87. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/scripts/unasync.py +0 -0
  88. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/setup.cfg +0 -0
  89. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/tests/__init__.py +0 -0
  90. {fleet_python-0.2.69b3 → fleet_python-0.2.70}/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.69b3
3
+ Version: 0.2.70
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -26,7 +26,7 @@ from .exceptions import (
26
26
  )
27
27
  from .client import Fleet, SyncEnv
28
28
  from ._async.client import AsyncFleet, AsyncEnv
29
- from .models import InstanceResponse, Environment
29
+ from .models import InstanceResponse, Environment, Run
30
30
  from .instance.models import Resource, ResetResponse
31
31
 
32
32
  # Import sync verifiers with explicit naming
@@ -73,7 +73,7 @@ from . import env
73
73
  from . import global_client as _global_client
74
74
  from ._async import global_client as _async_global_client
75
75
 
76
- __version__ = "0.1.0"
76
+ __version__ = "0.2.70"
77
77
 
78
78
  __all__ = [
79
79
  # Core classes
@@ -86,6 +86,7 @@ __all__ = [
86
86
  "SyncEnv",
87
87
  "Resource",
88
88
  "ResetResponse",
89
+ "Run",
89
90
  # Task models
90
91
  "Task",
91
92
  "VerifierFunction",
@@ -25,7 +25,7 @@ from ..exceptions import (
25
25
  FleetConfigurationError,
26
26
  )
27
27
  from .client import AsyncFleet, AsyncEnv
28
- from ..models import InstanceResponse, Environment, AccountResponse
28
+ from ..models import InstanceResponse, Environment, AccountResponse, Run
29
29
  from ..instance.models import Resource, ResetResponse
30
30
 
31
31
  # Import async verifiers
@@ -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.1.0"
47
+ __version__ = "0.2.70"
48
48
 
49
49
  __all__ = [
50
50
  # Core classes
@@ -54,6 +54,7 @@ __all__ = [
54
54
  "InstanceResponse",
55
55
  "Resource",
56
56
  "ResetResponse",
57
+ "Run",
57
58
  # Task models
58
59
  "Task",
59
60
  "VerifierFunction",
@@ -90,6 +91,7 @@ __all__ = [
90
91
  "import_tasks",
91
92
  "account",
92
93
  "get_task",
94
+ "list_runs",
93
95
  # Version
94
96
  "__version__",
95
97
  ]
@@ -255,6 +257,28 @@ async def get_task(task_key: str, version_id: Optional[str] = None):
255
257
  )
256
258
 
257
259
 
260
+ async def list_runs(
261
+ profile_id: Optional[str] = None, status: Optional[str] = "active"
262
+ ) -> List[Run]:
263
+ """List all runs (groups of instances by run_id) with aggregated statistics.
264
+
265
+ Args:
266
+ profile_id: Optional profile ID to filter runs by (use "self" for your own profile)
267
+ status: Filter by run status - "active" (default), "inactive", or "all"
268
+
269
+ Returns:
270
+ List[Run] containing run information with instance counts and timestamps
271
+
272
+ Example:
273
+ runs = await fleet.list_runs()
274
+ my_runs = await fleet.list_runs(profile_id="self")
275
+ all_runs = await fleet.list_runs(status="all")
276
+ """
277
+ return await _async_global_client.get_client().list_runs(
278
+ profile_id=profile_id, status=status
279
+ )
280
+
281
+
258
282
  def configure(
259
283
  api_key: Optional[str] = None,
260
284
  base_url: Optional[str] = None,
@@ -2,6 +2,7 @@ import httpx
2
2
  from typing import Dict, Any, Optional
3
3
  import json
4
4
  import logging
5
+ import uuid
5
6
 
6
7
  from ..models import InstanceResponse
7
8
  from ..config import GLOBAL_BASE_URL
@@ -20,6 +21,12 @@ from .exceptions import (
20
21
  FleetPermissionError,
21
22
  )
22
23
 
24
+ # Import version
25
+ try:
26
+ from .. import __version__
27
+ except ImportError:
28
+ __version__ = "0.2.70"
29
+
23
30
  logger = logging.getLogger(__name__)
24
31
 
25
32
 
@@ -38,17 +45,17 @@ class BaseWrapper:
38
45
  base_url = GLOBAL_BASE_URL
39
46
  self.base_url = base_url
40
47
 
41
- def get_headers(self) -> Dict[str, str]:
48
+ def get_headers(self, request_id: Optional[str] = None) -> Dict[str, str]:
42
49
  headers: Dict[str, str] = {
43
50
  "X-Fleet-SDK-Language": "Python",
44
- "X-Fleet-SDK-Version": "1.0.0",
51
+ "X-Fleet-SDK-Version": __version__,
45
52
  }
46
53
  headers["Authorization"] = f"Bearer {self.api_key}"
47
- # Debug log
48
- import logging
49
-
50
- logger = logging.getLogger(__name__)
51
- logger.debug(f"Headers being sent: {headers}")
54
+
55
+ # Add request ID for idempotency (persists across retries)
56
+ if request_id:
57
+ headers["X-Request-ID"] = request_id
58
+
52
59
  return headers
53
60
 
54
61
 
@@ -67,11 +74,14 @@ class AsyncWrapper(BaseWrapper):
67
74
  **kwargs,
68
75
  ) -> httpx.Response:
69
76
  base_url = base_url or self.base_url
77
+ # Generate unique request ID that persists across retries
78
+ request_id = str(uuid.uuid4())
79
+
70
80
  try:
71
81
  response = await self.httpx_client.request(
72
82
  method,
73
83
  f"{base_url}{url}",
74
- headers=self.get_headers(),
84
+ headers=self.get_headers(request_id=request_id),
75
85
  params=params,
76
86
  json=json,
77
87
  **kwargs,
@@ -93,8 +103,9 @@ class AsyncWrapper(BaseWrapper):
93
103
 
94
104
  # Debug log 500 errors
95
105
  if status_code == 500:
96
- logger.error(f"Got 500 error from {response.url}")
97
- logger.error(f"Response text: {response.text}")
106
+ # logger.error(f"Got 500 error from {response.url}")
107
+ # logger.error(f"Response text: {response.text}")
108
+ pass
98
109
 
99
110
  # Try to parse error response as JSON
100
111
  try:
@@ -21,7 +21,7 @@ import httpx
21
21
  import json
22
22
  import logging
23
23
  import os
24
- from typing import List, Optional, Dict, Any, TYPE_CHECKING, Union
24
+ from typing import List, Optional, Dict, Any, TYPE_CHECKING
25
25
 
26
26
  from .base import EnvironmentBase, AsyncWrapper
27
27
  from ..models import (
@@ -35,6 +35,8 @@ from ..models import (
35
35
  TaskRequest,
36
36
  TaskResponse,
37
37
  TaskUpdateRequest,
38
+ Run,
39
+ HeartbeatResponse,
38
40
  )
39
41
  from .tasks import Task
40
42
 
@@ -47,11 +49,6 @@ from .instance import (
47
49
  ResetResponse,
48
50
  ExecuteFunctionResponse,
49
51
  )
50
- from ..instance.models import (
51
- Resource as ResourceModel,
52
- ResourceType,
53
- ResourceMode,
54
- )
55
52
  from ..config import (
56
53
  DEFAULT_MAX_RETRIES,
57
54
  DEFAULT_TIMEOUT,
@@ -131,6 +128,23 @@ class AsyncEnv(EnvironmentBase):
131
128
  async def close(self) -> InstanceResponse:
132
129
  return await _delete_instance(self._load_client, self.instance_id)
133
130
 
131
+ async def heartbeat(self) -> HeartbeatResponse:
132
+ """Send heartbeat to keep instance alive (if heartbeat monitoring is enabled).
133
+
134
+ Returns:
135
+ HeartbeatResponse containing heartbeat status and deadline information
136
+ """
137
+ body = {}
138
+ if self.heartbeat_region:
139
+ body["region"] = self.heartbeat_region
140
+
141
+ response = await self._load_client.request(
142
+ "POST",
143
+ f"/v1/env/instances/{self.instance_id}/heartbeat",
144
+ json=body
145
+ )
146
+ return HeartbeatResponse(**response.json())
147
+
134
148
  async def verify(self, validator: ValidatorType) -> ExecuteFunctionResponse:
135
149
  return await self.instance.verify(validator)
136
150
 
@@ -218,6 +232,7 @@ class AsyncFleet:
218
232
  image_type: Optional[str] = None,
219
233
  ttl_seconds: Optional[int] = None,
220
234
  run_id: Optional[str] = None,
235
+ heartbeat_interval: Optional[int] = None,
221
236
  ) -> AsyncEnv:
222
237
  if ":" in env_key:
223
238
  env_key_part, env_version = env_key.split(":", 1)
@@ -254,6 +269,7 @@ class AsyncFleet:
254
269
  created_from="sdk",
255
270
  ttl_seconds=ttl_seconds,
256
271
  run_id=run_id,
272
+ heartbeat_interval=heartbeat_interval,
257
273
  )
258
274
 
259
275
  # Only use region-specific base URL if no custom base URL is set
@@ -276,7 +292,7 @@ class AsyncFleet:
276
292
  return await self.make(env_key=f"{task.env_id}:{task.version}")
277
293
 
278
294
  async def instances(
279
- self, status: Optional[str] = None, region: Optional[str] = None, run_id: Optional[str] = None
295
+ self, status: Optional[str] = None, region: Optional[str] = None, run_id: Optional[str] = None, profile_id: Optional[str] = None
280
296
  ) -> List[AsyncEnv]:
281
297
  params = {}
282
298
  if status:
@@ -285,6 +301,8 @@ class AsyncFleet:
285
301
  params["region"] = region
286
302
  if run_id:
287
303
  params["run_id"] = run_id
304
+ if profile_id:
305
+ params["profile_id"] = profile_id
288
306
 
289
307
  response = await self.client.request("GET", "/v1/env/instances", params=params)
290
308
  return [
@@ -292,163 +310,11 @@ class AsyncFleet:
292
310
  for instance_data in response.json()
293
311
  ]
294
312
 
295
- async def instance(self, instance_id: Union[str, Dict[str, str]]) -> AsyncEnv:
296
- """Create or connect to an environment instance.
297
-
298
- Supports three modes based on input type:
299
- 1. dict: Local filesystem mode - {"current": "./data.db", "seed": "./seed.db"}
300
- 2. str starting with http:// or https://: Localhost/URL mode
301
- 3. str (other): Remote cloud instance mode
302
-
303
- Args:
304
- instance_id: Instance identifier (str), URL (str starting with http://),
305
- or local db mapping (dict)
306
-
307
- Returns:
308
- AsyncEnv: Environment instance
309
- """
310
- # Local filesystem mode - dict of resource names to file paths
311
- if isinstance(instance_id, dict):
312
- return self._create_local_instance(instance_id)
313
-
314
- # Localhost/direct URL mode - string starting with http:// or https://
315
- elif isinstance(instance_id, str) and instance_id.startswith(("http://", "https://")):
316
- return self._create_url_instance(instance_id)
317
-
318
- # Remote mode - existing behavior
319
- else:
320
- response = await self.client.request("GET", f"/v1/env/instances/{instance_id}")
321
- instance = AsyncEnv(client=self.client, **response.json())
322
- await instance.instance.load()
323
- return instance
324
-
325
- def _create_url_instance(self, base_url: str) -> AsyncEnv:
326
- """Create instance connected to a direct URL (localhost or custom).
327
-
328
- Args:
329
- base_url: URL of the instance manager API
330
-
331
- Returns:
332
- AsyncEnv: Environment instance configured for URL mode
333
- """
334
- instance_client = AsyncInstanceClient(url=base_url, httpx_client=self._httpx_client)
335
-
336
- # Create a minimal environment for URL mode
337
- env = AsyncEnv(
338
- client=self.client,
339
- instance_id=base_url,
340
- env_key="localhost",
341
- version="",
342
- status="running",
343
- subdomain="localhost",
344
- created_at="",
345
- updated_at="",
346
- terminated_at=None,
347
- team_id="",
348
- region="localhost",
349
- env_variables=None,
350
- data_key=None,
351
- data_version=None,
352
- urls=None,
353
- health=None,
354
- )
355
- env._instance = instance_client
356
- return env
357
-
358
- @staticmethod
359
- def _normalize_db_path(path: str) -> tuple[str, bool]:
360
- """Normalize database path and detect if it's in-memory.
361
-
362
- Args:
363
- path: Database path - can be:
364
- - File path: "./data.db"
365
- - Plain memory: ":memory:"
366
- - Named memory: ":memory:namespace"
367
- - URI: "file:name?mode=memory&cache=shared"
368
-
369
- Returns:
370
- Tuple of (normalized_path, is_memory)
371
- """
372
- import uuid
373
- import sqlite3
374
-
375
- if path == ":memory:":
376
- # Plain :memory: - create unique namespace
377
- name = f"mem_{uuid.uuid4().hex[:8]}"
378
- return f"file:{name}?mode=memory&cache=shared", True
379
- elif path.startswith(":memory:"):
380
- # Named memory: :memory:current -> file:current?mode=memory&cache=shared
381
- namespace = path[8:] # Remove ":memory:" prefix
382
- return f"file:{namespace}?mode=memory&cache=shared", True
383
- elif "mode=memory" in path:
384
- # Already a proper memory URI
385
- return path, True
386
- else:
387
- # Regular file path
388
- return path, False
389
-
390
- def _create_local_instance(self, dbs: Dict[str, str]) -> AsyncEnv:
391
- """Create instance with local file-based or in-memory SQLite resources.
392
-
393
- Args:
394
- dbs: Map of resource names to paths (e.g., {"current": "./data.db"} or
395
- {"current": ":memory:current"})
396
-
397
- Returns:
398
- AsyncEnv: Environment instance configured for local mode
399
- """
400
- import sqlite3
401
-
402
- instance_client = AsyncInstanceClient(url="local://", httpx_client=None)
403
- instance_client._resources = [] # Mark as loaded
404
- instance_client._memory_anchors = {} # Store anchor connections for in-memory DBs
405
-
406
- # Store creation parameters for local AsyncSQLiteResources
407
- # This allows db() to create new instances each time (matching HTTP mode behavior)
408
- for name, path in dbs.items():
409
- # Normalize path and detect if it's in-memory
410
- normalized_path, is_memory = self._normalize_db_path(path)
411
-
412
- # Create anchor connection for in-memory databases
413
- # This keeps the database alive as long as the env exists
414
- if is_memory:
415
- anchor_conn = sqlite3.connect(normalized_path, uri=True)
416
- instance_client._memory_anchors[name] = anchor_conn
417
-
418
- resource_model = ResourceModel(
419
- name=name,
420
- type=ResourceType.db,
421
- mode=ResourceMode.rw,
422
- label=f"Local: {path}",
423
- )
424
- instance_client._resources_state[ResourceType.db.value][name] = {
425
- 'type': 'local',
426
- 'resource_model': resource_model,
427
- 'db_path': normalized_path,
428
- 'is_memory': is_memory
429
- }
430
-
431
- # Create a minimal environment for local mode
432
- env = AsyncEnv(
433
- client=self.client,
434
- instance_id="local",
435
- env_key="local",
436
- version="",
437
- status="running",
438
- subdomain="local",
439
- created_at="",
440
- updated_at="",
441
- terminated_at=None,
442
- team_id="",
443
- region="local",
444
- env_variables=None,
445
- data_key=None,
446
- data_version=None,
447
- urls=None,
448
- health=None,
449
- )
450
- env._instance = instance_client
451
- return env
313
+ async def instance(self, instance_id: str) -> AsyncEnv:
314
+ response = await self.client.request("GET", f"/v1/env/instances/{instance_id}")
315
+ instance = AsyncEnv(client=self.client, **response.json())
316
+ await instance.instance.load()
317
+ return instance
452
318
 
453
319
  async def check_bundle_exists(self, bundle_hash: str) -> VerifiersCheckResponse:
454
320
  return await _check_bundle_exists(self.client, bundle_hash)
@@ -474,16 +340,53 @@ class AsyncFleet:
474
340
  """
475
341
  return await _delete_instance(self.client, instance_id)
476
342
 
477
- async def close_all(self, run_id: str) -> List[InstanceResponse]:
478
- """Close (delete) all instances associated with a run_id.
343
+ async def heartbeat(self, instance_id: str, region: Optional[str] = None) -> HeartbeatResponse:
344
+ """Send heartbeat to keep instance alive (if heartbeat monitoring is enabled).
345
+
346
+ Args:
347
+ instance_id: The instance ID to send heartbeat for
348
+ region: Optional region override for cross-region heartbeats
349
+
350
+ Returns:
351
+ HeartbeatResponse containing heartbeat status and deadline information
352
+ """
353
+ return await _send_heartbeat(self.client, instance_id, region)
354
+
355
+ async def close_all(self, run_id: Optional[str] = None, profile_id: Optional[str] = None) -> List[InstanceResponse]:
356
+ """Close (delete) instances using the batch delete endpoint.
479
357
 
480
358
  Args:
481
- run_id: The run ID whose instances should be closed
359
+ run_id: Optional run ID to filter instances by
360
+ profile_id: Optional profile ID to filter instances by (use "self" for your own profile)
482
361
 
483
362
  Returns:
484
363
  List[InstanceResponse] containing the deleted instances
364
+
365
+ Note:
366
+ At least one of run_id or profile_id must be provided.
485
367
  """
486
- return await _delete_instances_by_run_id(self.client, run_id)
368
+ return await _delete_instances_batch(self.client, run_id=run_id, profile_id=profile_id)
369
+
370
+ async def list_runs(
371
+ self, profile_id: Optional[str] = None, status: Optional[str] = "active"
372
+ ) -> List[Run]:
373
+ """List all runs (groups of instances by run_id) with aggregated statistics.
374
+
375
+ Args:
376
+ profile_id: Optional profile ID to filter runs by (use "self" for your own profile)
377
+ status: Filter by run status - "active" (default), "inactive", or "all"
378
+
379
+ Returns:
380
+ List[Run] containing run information with instance counts and timestamps
381
+ """
382
+ params = {}
383
+ if profile_id:
384
+ params["profile_id"] = profile_id
385
+ if status:
386
+ params["active"] = status
387
+
388
+ response = await self.client.request("GET", "/v1/env/runs", params=params)
389
+ return [Run(**run_data) for run_data in response.json()]
487
390
 
488
391
  async def load_tasks_from_file(self, filename: str) -> List[Task]:
489
392
  with open(filename, "r", encoding="utf-8") as f:
@@ -563,8 +466,8 @@ class AsyncFleet:
563
466
  error_msg = f"Failed to create verifier {task_json.get('key', task_json.get('id'))}: {e}"
564
467
  if raise_on_verifier_error:
565
468
  raise ValueError(error_msg) from e
566
- else:
567
- logger.warning(error_msg)
469
+ # else:
470
+ # logger.warning(error_msg)
568
471
 
569
472
  task = Task(
570
473
  key=task_json.get("key", task_json.get("id")),
@@ -656,25 +559,25 @@ class AsyncFleet:
656
559
  verifier_sha=tr.verifier.sha256,
657
560
  )
658
561
  except Exception as e:
659
- logger.warning(
660
- f"Failed to create verifier {tr.verifier.key}: {e}"
661
- )
562
+ # logger.warning(
563
+ # f"Failed to create verifier {tr.verifier.key}: {e}"
564
+ # )
662
565
  return None
663
566
  else:
664
567
  # Fallback: try fetching by ID
665
568
  try:
666
- logger.warning(
667
- f"Embedded verifier code missing for {tr.verifier.key} (NoSuchKey). "
668
- f"Attempting to refetch by id {tr.verifier.verifier_id}"
669
- )
569
+ # logger.warning(
570
+ # f"Embedded verifier code missing for {tr.verifier.key} (NoSuchKey). "
571
+ # f"Attempting to refetch by id {tr.verifier.verifier_id}"
572
+ # )
670
573
  return await self._load_verifier(
671
574
  tr.verifier.verifier_id
672
575
  )
673
576
  except Exception as e:
674
- logger.warning(
675
- f"Refetch by verifier id failed for {tr.verifier.key}: {e}. "
676
- "Leaving verifier unset."
677
- )
577
+ # logger.warning(
578
+ # f"Refetch by verifier id failed for {tr.verifier.key}: {e}. "
579
+ # "Leaving verifier unset."
580
+ # )
678
581
  return None
679
582
 
680
583
  # Add the coroutine for parallel execution
@@ -713,9 +616,10 @@ class AsyncFleet:
713
616
  if task_response.verifier:
714
617
  # Process verifier result
715
618
  if isinstance(verifier_result, Exception):
716
- logger.warning(
717
- f"Verifier loading failed for {task_response.key}: {verifier_result}"
718
- )
619
+ # logger.warning(
620
+ # f"Verifier loading failed for {task_response.key}: {verifier_result}"
621
+ # )
622
+ pass
719
623
  elif verifier_result is not None:
720
624
  verifier = verifier_result
721
625
  embedded_code = task_response.verifier.code or ""
@@ -789,10 +693,10 @@ class AsyncFleet:
789
693
  with open(filename, "w", encoding="utf-8") as f:
790
694
  json.dump(tasks_data, f, indent=2, default=str)
791
695
 
792
- logger.info(f"Exported {len(tasks)} tasks to {filename}")
696
+ # logger.info(f"Exported {len(tasks)} tasks to {filename}")
793
697
  return filename
794
698
  else:
795
- logger.info("No tasks found to export")
699
+ # logger.info("No tasks found to export")
796
700
  return None
797
701
 
798
702
  async def import_single_task(self, task: Task, project_key: Optional[str] = None):
@@ -821,7 +725,7 @@ class AsyncFleet:
821
725
  )
822
726
  return response
823
727
  except Exception as e:
824
- logger.error(f"Failed to import task {task.key}: {e}")
728
+ # logger.error(f"Failed to import task {task.key}: {e}")
825
729
  return None
826
730
 
827
731
  async def import_tasks(self, filename: str, project_key: Optional[str] = None):
@@ -994,8 +898,34 @@ async def _delete_instance(client: AsyncWrapper, instance_id: str) -> InstanceRe
994
898
  return InstanceResponse(**response.json())
995
899
 
996
900
 
997
- async def _delete_instances_by_run_id(client: AsyncWrapper, run_id: str) -> List[InstanceResponse]:
998
- response = await client.request("DELETE", f"/v1/env/instances/run/{run_id}")
901
+ async def _send_heartbeat(client: AsyncWrapper, instance_id: str, region: Optional[str] = None) -> HeartbeatResponse:
902
+ """Send heartbeat to keep instance alive."""
903
+ body = {}
904
+ if region:
905
+ body["region"] = region
906
+
907
+ response = await client.request(
908
+ "POST",
909
+ f"/v1/env/instances/{instance_id}/heartbeat",
910
+ json=body
911
+ )
912
+ return HeartbeatResponse(**response.json())
913
+
914
+
915
+ async def _delete_instances_batch(
916
+ client: AsyncWrapper, run_id: Optional[str] = None, profile_id: Optional[str] = None
917
+ ) -> List[InstanceResponse]:
918
+ """Delete instances using the batch endpoint with flexible filtering."""
919
+ params = {}
920
+ if run_id:
921
+ params["run_id"] = run_id
922
+ if profile_id:
923
+ params["profile_id"] = profile_id
924
+
925
+ if not params:
926
+ raise ValueError("At least one of run_id or profile_id must be provided")
927
+
928
+ response = await client.request("DELETE", "/v1/env/instances/batch", params=params)
999
929
  return [InstanceResponse(**instance_data) for instance_data in response.json()]
1000
930
 
1001
931
 
@@ -1041,17 +971,17 @@ async def _execute_verifier_remote(
1041
971
  request_data["bundle"] = bundle_b64
1042
972
 
1043
973
  # Debug logging
1044
- logger.debug(
1045
- f"Sending verifier execute request: key={key}, sha256={bundle_sha[:8]}..., function_name={function_name}"
1046
- )
1047
- logger.debug(f"Request has bundle: {needs_upload}")
1048
- logger.debug(f"Using client with base_url: {client.base_url}")
1049
- logger.debug(f"Request data keys: {list(request_data.keys())}")
1050
- logger.debug(
1051
- f"Bundle size: {len(request_data.get('bundle', ''))} chars"
1052
- if "bundle" in request_data
1053
- else "No bundle"
1054
- )
974
+ # logger.debug(
975
+ # f"Sending verifier execute request: key={key}, sha256={bundle_sha[:8]}..., function_name={function_name}"
976
+ # )
977
+ # logger.debug(f"Request has bundle: {needs_upload}")
978
+ # logger.debug(f"Using client with base_url: {client.base_url}")
979
+ # logger.debug(f"Request data keys: {list(request_data.keys())}")
980
+ # logger.debug(
981
+ # f"Bundle size: {len(request_data.get('bundle', ''))} chars"
982
+ # if "bundle" in request_data
983
+ # else "No bundle"
984
+ # )
1055
985
 
1056
986
  # Note: This should be called on the instance URL, not the orchestrator
1057
987
  # The instance has manager URLs for verifier execution
@@ -1059,6 +989,6 @@ async def _execute_verifier_remote(
1059
989
 
1060
990
  # Debug the response
1061
991
  response_json = response.json()
1062
- logger.debug(f"Verifier execute response: {response_json}")
992
+ # logger.debug(f"Verifier execute response: {response_json}")
1063
993
 
1064
994
  return VerifiersExecuteResponse(**response_json)