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.
- {fleet_python-0.2.69b3/fleet_python.egg-info → fleet_python-0.2.70}/PKG-INFO +1 -1
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/__init__.py +3 -2
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/__init__.py +26 -2
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/base.py +21 -10
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/client.py +131 -201
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/env/client.py +38 -7
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/instance/client.py +4 -19
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/resources/sqlite.py +1 -150
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/tasks.py +13 -7
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/verifiers/bundler.py +22 -21
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/verifiers/verifier.py +20 -19
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/base.py +21 -10
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/client.py +137 -219
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/config.py +1 -1
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/env/__init__.py +8 -0
- fleet_python-0.2.70/fleet/env/client.py +104 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/instance/client.py +5 -20
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/models.py +33 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/resources/sqlite.py +1 -143
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/tasks.py +15 -7
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/verifiers/bundler.py +22 -21
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/verifiers/decorator.py +1 -1
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/verifiers/verifier.py +20 -19
- {fleet_python-0.2.69b3 → fleet_python-0.2.70/fleet_python.egg-info}/PKG-INFO +1 -1
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet_python.egg-info/SOURCES.txt +0 -4
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/pyproject.toml +1 -1
- fleet_python-0.2.69b3/fleet/env/client.py +0 -73
- fleet_python-0.2.69b3/tests/test_app_method.py +0 -85
- fleet_python-0.2.69b3/tests/test_instance_dispatch.py +0 -607
- fleet_python-0.2.69b3/tests/test_sqlite_resource_dual_mode.py +0 -263
- fleet_python-0.2.69b3/tests/test_sqlite_shared_memory_behavior.py +0 -117
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/LICENSE +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/README.md +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/diff_example.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/dsl_example.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/example.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/exampleResume.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/example_account.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/example_action_log.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/example_client.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/example_mcp_anthropic.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/example_mcp_openai.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/example_sync.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/example_task.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/example_tasks.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/example_verifier.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/export_tasks.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/gemini_example.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/import_tasks.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/json_tasks_example.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/nova_act_example.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/openai_example.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/openai_simple_example.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/query_builder_example.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/quickstart.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/examples/test_cdp_logging.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/env/__init__.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/exceptions.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/global_client.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/instance/__init__.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/instance/base.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/models.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/resources/__init__.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/resources/base.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/resources/browser.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/resources/mcp.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/_async/verifiers/__init__.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/exceptions.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/global_client.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/instance/__init__.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/instance/base.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/instance/models.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/resources/__init__.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/resources/base.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/resources/browser.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/resources/mcp.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/types.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/verifiers/__init__.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/verifiers/code.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/verifiers/db.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/verifiers/parse.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet/verifiers/sql_differ.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet_python.egg-info/dependency_links.txt +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet_python.egg-info/requires.txt +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/fleet_python.egg-info/top_level.txt +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/scripts/fix_sync_imports.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/scripts/unasync.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/setup.cfg +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/tests/__init__.py +0 -0
- {fleet_python-0.2.69b3 → fleet_python-0.2.70}/tests/test_verifier_from_string.py +0 -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.
|
|
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.
|
|
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":
|
|
51
|
+
"X-Fleet-SDK-Version": __version__,
|
|
45
52
|
}
|
|
46
53
|
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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:
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
478
|
-
"""
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
668
|
-
|
|
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
|
-
|
|
676
|
-
|
|
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
|
-
|
|
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
|
|
998
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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)
|