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.
- examples/export_tasks.py +16 -5
- examples/export_tasks_filtered.py +245 -0
- examples/fetch_tasks.py +230 -0
- examples/import_tasks.py +140 -8
- examples/iterate_verifiers.py +725 -0
- fleet/__init__.py +128 -5
- fleet/_async/__init__.py +27 -3
- fleet/_async/base.py +24 -9
- fleet/_async/client.py +938 -41
- fleet/_async/env/client.py +60 -3
- fleet/_async/instance/client.py +52 -7
- fleet/_async/models.py +15 -0
- fleet/_async/resources/api.py +200 -0
- fleet/_async/resources/sqlite.py +1801 -46
- fleet/_async/tasks.py +122 -25
- fleet/_async/verifiers/bundler.py +22 -21
- fleet/_async/verifiers/verifier.py +25 -19
- fleet/agent/__init__.py +32 -0
- fleet/agent/gemini_cua/Dockerfile +45 -0
- fleet/agent/gemini_cua/__init__.py +10 -0
- fleet/agent/gemini_cua/agent.py +759 -0
- fleet/agent/gemini_cua/mcp/main.py +108 -0
- fleet/agent/gemini_cua/mcp_server/__init__.py +5 -0
- fleet/agent/gemini_cua/mcp_server/main.py +105 -0
- fleet/agent/gemini_cua/mcp_server/tools.py +178 -0
- fleet/agent/gemini_cua/requirements.txt +5 -0
- fleet/agent/gemini_cua/start.sh +30 -0
- fleet/agent/orchestrator.py +854 -0
- fleet/agent/types.py +49 -0
- fleet/agent/utils.py +34 -0
- fleet/base.py +34 -9
- fleet/cli.py +1061 -0
- fleet/client.py +1060 -48
- fleet/config.py +1 -1
- fleet/env/__init__.py +16 -0
- fleet/env/client.py +60 -3
- fleet/eval/__init__.py +15 -0
- fleet/eval/uploader.py +231 -0
- fleet/exceptions.py +8 -0
- fleet/instance/client.py +53 -8
- fleet/instance/models.py +1 -0
- fleet/models.py +303 -0
- fleet/proxy/__init__.py +25 -0
- fleet/proxy/proxy.py +453 -0
- fleet/proxy/whitelist.py +244 -0
- fleet/resources/api.py +200 -0
- fleet/resources/sqlite.py +1845 -46
- fleet/tasks.py +113 -20
- fleet/utils/__init__.py +7 -0
- fleet/utils/http_logging.py +178 -0
- fleet/utils/logging.py +13 -0
- fleet/utils/playwright.py +440 -0
- fleet/verifiers/bundler.py +22 -21
- fleet/verifiers/db.py +985 -1
- fleet/verifiers/decorator.py +1 -1
- fleet/verifiers/verifier.py +25 -19
- {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/METADATA +28 -1
- fleet_python-0.2.105.dist-info/RECORD +115 -0
- {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/WHEEL +1 -1
- fleet_python-0.2.105.dist-info/entry_points.txt +2 -0
- tests/test_app_method.py +85 -0
- tests/test_expect_exactly.py +4148 -0
- tests/test_expect_only.py +2593 -0
- tests/test_instance_dispatch.py +607 -0
- tests/test_sqlite_resource_dual_mode.py +263 -0
- tests/test_sqlite_shared_memory_behavior.py +117 -0
- fleet_python-0.2.66b2.dist-info/RECORD +0 -81
- tests/test_verifier_security.py +0 -427
- {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/licenses/LICENSE +0 -0
- {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/top_level.txt +0 -0
examples/export_tasks.py
CHANGED
|
@@ -17,8 +17,7 @@ def main():
|
|
|
17
17
|
parser.add_argument(
|
|
18
18
|
"--task-keys",
|
|
19
19
|
"-t",
|
|
20
|
-
|
|
21
|
-
help="Optional list of task keys to export (space-separated)",
|
|
20
|
+
help="Optional list of task keys to export (comma-separated)",
|
|
22
21
|
default=None,
|
|
23
22
|
)
|
|
24
23
|
parser.add_argument(
|
|
@@ -27,6 +26,12 @@ def main():
|
|
|
27
26
|
help="Optional task project key to filter tasks",
|
|
28
27
|
default=None,
|
|
29
28
|
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--env-key",
|
|
31
|
+
"-e",
|
|
32
|
+
help="Optional environment key to filter tasks",
|
|
33
|
+
default=None,
|
|
34
|
+
)
|
|
30
35
|
parser.add_argument(
|
|
31
36
|
"--output",
|
|
32
37
|
"-o",
|
|
@@ -42,12 +47,13 @@ def main():
|
|
|
42
47
|
args.project_key is not None,
|
|
43
48
|
args.task_keys is not None,
|
|
44
49
|
args.task_project_key is not None,
|
|
50
|
+
args.env_key is not None,
|
|
45
51
|
]
|
|
46
52
|
)
|
|
47
53
|
|
|
48
54
|
if filters_specified > 1:
|
|
49
55
|
parser.error(
|
|
50
|
-
"Cannot specify multiple filters. Use only one of --project-key, --task-keys,
|
|
56
|
+
"Cannot specify multiple filters. Use only one of --project-key, --task-keys, --task-project-key, or --env-key."
|
|
51
57
|
)
|
|
52
58
|
|
|
53
59
|
# Get account info
|
|
@@ -59,13 +65,18 @@ def main():
|
|
|
59
65
|
print(f"Loading tasks from project: {args.project_key}")
|
|
60
66
|
tasks = fleet.load_tasks(project_key=args.project_key)
|
|
61
67
|
elif args.task_keys:
|
|
68
|
+
# Split comma-separated task keys and strip whitespace
|
|
69
|
+
task_keys_list = [key.strip() for key in args.task_keys.split(",")]
|
|
62
70
|
print(
|
|
63
|
-
f"Loading {len(
|
|
71
|
+
f"Loading {len(task_keys_list)} specific task(s): {', '.join(task_keys_list)}"
|
|
64
72
|
)
|
|
65
|
-
tasks = fleet.load_tasks(keys=
|
|
73
|
+
tasks = fleet.load_tasks(keys=task_keys_list)
|
|
66
74
|
elif args.task_project_key:
|
|
67
75
|
print(f"Loading tasks from task project: {args.task_project_key}")
|
|
68
76
|
tasks = fleet.load_tasks(task_project_key=args.task_project_key)
|
|
77
|
+
elif args.env_key:
|
|
78
|
+
print(f"Loading tasks from environment: {args.env_key}")
|
|
79
|
+
tasks = fleet.load_tasks(env_key=args.env_key)
|
|
69
80
|
else:
|
|
70
81
|
print("Loading all tasks")
|
|
71
82
|
tasks = fleet.load_tasks()
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Export tasks to JSON, excluding tasks from targets marked as 'unused'.
|
|
3
|
+
|
|
4
|
+
This script filters out tasks whose task_project_target has status='unused',
|
|
5
|
+
ensuring that broken/invalid targets don't pollute exports.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python export_tasks_filtered.py --task-project-key my-project
|
|
9
|
+
python export_tasks_filtered.py --project-key my-project --output tasks.json
|
|
10
|
+
python export_tasks_filtered.py --env-key my-env
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
from typing import List, Set
|
|
17
|
+
|
|
18
|
+
import fleet
|
|
19
|
+
from dotenv import load_dotenv
|
|
20
|
+
from supabase import create_client, Client
|
|
21
|
+
|
|
22
|
+
load_dotenv()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_unused_target_ids(supabase: Client, team_id: str) -> Set[str]:
|
|
26
|
+
"""Fetch all target IDs that have status='unused' for the given team."""
|
|
27
|
+
# Get all task_projects for this team first
|
|
28
|
+
projects_response = (
|
|
29
|
+
supabase.table("task_projects")
|
|
30
|
+
.select("id")
|
|
31
|
+
.eq("team_id", team_id)
|
|
32
|
+
.execute()
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if not projects_response.data:
|
|
36
|
+
return set()
|
|
37
|
+
|
|
38
|
+
project_ids = [p["id"] for p in projects_response.data]
|
|
39
|
+
|
|
40
|
+
# Get all targets with status='unused' for these projects
|
|
41
|
+
targets_response = (
|
|
42
|
+
supabase.table("task_project_targets")
|
|
43
|
+
.select("id")
|
|
44
|
+
.in_("project_id", project_ids)
|
|
45
|
+
.eq("status", "unused")
|
|
46
|
+
.execute()
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if not targets_response.data:
|
|
50
|
+
return set()
|
|
51
|
+
|
|
52
|
+
return {t["id"] for t in targets_response.data}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_task_target_mapping(supabase: Client, task_keys: List[str], team_id: str) -> dict:
|
|
56
|
+
"""Fetch task_project_target_id for each task key."""
|
|
57
|
+
if not task_keys:
|
|
58
|
+
return {}
|
|
59
|
+
|
|
60
|
+
# Batch the queries to avoid hitting limits
|
|
61
|
+
BATCH_SIZE = 100
|
|
62
|
+
mapping = {}
|
|
63
|
+
|
|
64
|
+
for i in range(0, len(task_keys), BATCH_SIZE):
|
|
65
|
+
batch_keys = task_keys[i:i + BATCH_SIZE]
|
|
66
|
+
response = (
|
|
67
|
+
supabase.table("eval_tasks")
|
|
68
|
+
.select("key, task_project_target_id")
|
|
69
|
+
.in_("key", batch_keys)
|
|
70
|
+
.eq("team_id", team_id)
|
|
71
|
+
.execute()
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
for row in response.data or []:
|
|
75
|
+
mapping[row["key"]] = row.get("task_project_target_id")
|
|
76
|
+
|
|
77
|
+
return mapping
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main():
|
|
81
|
+
parser = argparse.ArgumentParser(
|
|
82
|
+
description="Export tasks to JSON, excluding tasks from 'unused' targets"
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--project-key",
|
|
86
|
+
"-p",
|
|
87
|
+
help="Optional project key to filter tasks",
|
|
88
|
+
default=None,
|
|
89
|
+
)
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"--task-keys",
|
|
92
|
+
"-t",
|
|
93
|
+
nargs="+",
|
|
94
|
+
help="Optional list of task keys to export (space-separated)",
|
|
95
|
+
default=None,
|
|
96
|
+
)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--task-project-key",
|
|
99
|
+
"-tpk",
|
|
100
|
+
help="Optional task project key to filter tasks",
|
|
101
|
+
default=None,
|
|
102
|
+
)
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"--env-key",
|
|
105
|
+
"-e",
|
|
106
|
+
help="Optional environment key to filter tasks",
|
|
107
|
+
default=None,
|
|
108
|
+
)
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
"--output",
|
|
111
|
+
"-o",
|
|
112
|
+
help="Output JSON filename (defaults to {team_id}_filtered.json)",
|
|
113
|
+
default=None,
|
|
114
|
+
)
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
"--include-unused",
|
|
117
|
+
action="store_true",
|
|
118
|
+
help="Include tasks from 'unused' targets (disables filtering)",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
args = parser.parse_args()
|
|
122
|
+
|
|
123
|
+
# Validate that only one filter is specified
|
|
124
|
+
filters_specified = sum(
|
|
125
|
+
[
|
|
126
|
+
args.project_key is not None,
|
|
127
|
+
args.task_keys is not None,
|
|
128
|
+
args.task_project_key is not None,
|
|
129
|
+
args.env_key is not None,
|
|
130
|
+
]
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if filters_specified > 1:
|
|
134
|
+
parser.error(
|
|
135
|
+
"Cannot specify multiple filters. Use only one of --project-key, --task-keys, --task-project-key, or --env-key."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Get account info
|
|
139
|
+
account = fleet.env.account()
|
|
140
|
+
print(f"Exporting from team: {account.team_name}")
|
|
141
|
+
|
|
142
|
+
# Initialize Supabase client for filtering
|
|
143
|
+
supabase_url = os.getenv("SUPABASE_URL") or os.getenv("NEXT_PUBLIC_SUPABASE_URL")
|
|
144
|
+
supabase_key = os.getenv("SUPABASE_SERVICE_ROLE_KEY") or os.getenv("SUPABASE_KEY")
|
|
145
|
+
|
|
146
|
+
if not supabase_url or not supabase_key:
|
|
147
|
+
print("⚠ Warning: SUPABASE_URL/SUPABASE_KEY not set - cannot filter by target status")
|
|
148
|
+
print(" Falling back to unfiltered export")
|
|
149
|
+
supabase = None
|
|
150
|
+
else:
|
|
151
|
+
supabase = create_client(supabase_url, supabase_key)
|
|
152
|
+
|
|
153
|
+
# Load tasks
|
|
154
|
+
if args.project_key:
|
|
155
|
+
print(f"Loading tasks from project: {args.project_key}")
|
|
156
|
+
tasks = fleet.load_tasks(project_key=args.project_key)
|
|
157
|
+
elif args.task_keys:
|
|
158
|
+
print(f"Loading {len(args.task_keys)} specific task(s): {', '.join(args.task_keys)}")
|
|
159
|
+
tasks = fleet.load_tasks(keys=args.task_keys)
|
|
160
|
+
elif args.task_project_key:
|
|
161
|
+
print(f"Loading tasks from task project: {args.task_project_key}")
|
|
162
|
+
tasks = fleet.load_tasks(task_project_key=args.task_project_key)
|
|
163
|
+
elif args.env_key:
|
|
164
|
+
print(f"Loading tasks from environment: {args.env_key}")
|
|
165
|
+
tasks = fleet.load_tasks(env_key=args.env_key)
|
|
166
|
+
else:
|
|
167
|
+
print("Loading all tasks")
|
|
168
|
+
tasks = fleet.load_tasks()
|
|
169
|
+
|
|
170
|
+
print(f"\nFound {len(tasks)} task(s) before filtering")
|
|
171
|
+
|
|
172
|
+
# Filter out tasks from unused targets
|
|
173
|
+
filtered_tasks = tasks
|
|
174
|
+
excluded_count = 0
|
|
175
|
+
|
|
176
|
+
if supabase and not args.include_unused:
|
|
177
|
+
print("\nFiltering out tasks from 'unused' targets...")
|
|
178
|
+
|
|
179
|
+
# Get unused target IDs
|
|
180
|
+
unused_target_ids = get_unused_target_ids(supabase, account.team_id)
|
|
181
|
+
|
|
182
|
+
if unused_target_ids:
|
|
183
|
+
print(f" Found {len(unused_target_ids)} unused target(s)")
|
|
184
|
+
|
|
185
|
+
# Get task -> target mapping
|
|
186
|
+
task_keys = [t.key for t in tasks]
|
|
187
|
+
task_target_map = get_task_target_mapping(supabase, task_keys, account.team_id)
|
|
188
|
+
|
|
189
|
+
# Filter tasks
|
|
190
|
+
filtered_tasks = []
|
|
191
|
+
for task in tasks:
|
|
192
|
+
target_id = task_target_map.get(task.key)
|
|
193
|
+
if target_id in unused_target_ids:
|
|
194
|
+
excluded_count += 1
|
|
195
|
+
else:
|
|
196
|
+
filtered_tasks.append(task)
|
|
197
|
+
|
|
198
|
+
print(f" Excluded {excluded_count} task(s) from unused targets")
|
|
199
|
+
else:
|
|
200
|
+
print(" No unused targets found - all tasks included")
|
|
201
|
+
|
|
202
|
+
tasks = filtered_tasks
|
|
203
|
+
print(f"\n{len(tasks)} task(s) after filtering")
|
|
204
|
+
|
|
205
|
+
# Validate that all tasks have verifier_func
|
|
206
|
+
print("\nValidating tasks have verifier_func...")
|
|
207
|
+
missing_verifier = []
|
|
208
|
+
for task in tasks:
|
|
209
|
+
if not task.verifier_func:
|
|
210
|
+
missing_verifier.append(task.key)
|
|
211
|
+
|
|
212
|
+
if missing_verifier:
|
|
213
|
+
print(f"\n✗ Error: {len(missing_verifier)} task(s) missing verifier_func:")
|
|
214
|
+
for key in missing_verifier[:10]: # Show first 10
|
|
215
|
+
print(f" - {key}")
|
|
216
|
+
if len(missing_verifier) > 10:
|
|
217
|
+
print(f" ... and {len(missing_verifier) - 10} more")
|
|
218
|
+
raise ValueError(
|
|
219
|
+
"All tasks must have a verifier_func. Cannot export tasks without verifiers."
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
print("✓ All tasks have verifier_func")
|
|
223
|
+
|
|
224
|
+
# Determine output filename
|
|
225
|
+
output_file = args.output or f"{account.team_id}_filtered.json"
|
|
226
|
+
|
|
227
|
+
# Export to JSON
|
|
228
|
+
print(f"\nExporting to: {output_file}")
|
|
229
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
230
|
+
json.dump(
|
|
231
|
+
[task.model_dump() for task in tasks],
|
|
232
|
+
f,
|
|
233
|
+
indent=2,
|
|
234
|
+
ensure_ascii=False,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
print(f"✓ Successfully exported {len(tasks)} task(s) to {output_file}")
|
|
238
|
+
if excluded_count > 0:
|
|
239
|
+
print(f" ({excluded_count} task(s) excluded from unused targets)")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
if __name__ == "__main__":
|
|
243
|
+
main()
|
|
244
|
+
|
|
245
|
+
|
examples/fetch_tasks.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
6
|
+
import fleet
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
9
|
+
load_dotenv()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def fetch_task(
|
|
13
|
+
task_key: str, semaphore: asyncio.Semaphore
|
|
14
|
+
) -> Tuple[str, Optional[Dict[str, Any]], Optional[str]]:
|
|
15
|
+
"""
|
|
16
|
+
Fetch a single task from the Fleet API.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
task_key: Task key to fetch
|
|
20
|
+
semaphore: Semaphore to limit concurrent requests
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Tuple of (task_key, task_data_dict, error_message)
|
|
24
|
+
"""
|
|
25
|
+
async with semaphore:
|
|
26
|
+
try:
|
|
27
|
+
# Use load_tasks with keys parameter to get Task objects
|
|
28
|
+
tasks = await fleet.load_tasks_async(keys=[task_key])
|
|
29
|
+
if tasks:
|
|
30
|
+
task = tasks[0]
|
|
31
|
+
# Convert to dict using model_dump() like export_tasks.py does
|
|
32
|
+
return task_key, task.model_dump(), None
|
|
33
|
+
else:
|
|
34
|
+
return task_key, None, "Task not found"
|
|
35
|
+
except Exception as e:
|
|
36
|
+
error_msg = f"{type(e).__name__}: {str(e)}"
|
|
37
|
+
return task_key, None, error_msg
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def fetch_tasks_batch(
|
|
41
|
+
task_keys: List[str], max_concurrent: int = 20
|
|
42
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
43
|
+
"""
|
|
44
|
+
Fetch multiple tasks concurrently from the Fleet API.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
task_keys: List of task keys to fetch
|
|
48
|
+
max_concurrent: Maximum number of concurrent requests
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Dictionary mapping task_key to task data
|
|
52
|
+
"""
|
|
53
|
+
print(f"\nFetching {len(task_keys)} task(s) from Fleet API...")
|
|
54
|
+
print(f"Max concurrent requests: {max_concurrent}")
|
|
55
|
+
|
|
56
|
+
semaphore = asyncio.Semaphore(max_concurrent)
|
|
57
|
+
|
|
58
|
+
# Fetch all tasks concurrently
|
|
59
|
+
results = await asyncio.gather(
|
|
60
|
+
*[fetch_task(key, semaphore) for key in task_keys],
|
|
61
|
+
return_exceptions=True,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Process results
|
|
65
|
+
fetched_tasks = {}
|
|
66
|
+
errors = []
|
|
67
|
+
|
|
68
|
+
for result in results:
|
|
69
|
+
if isinstance(result, Exception):
|
|
70
|
+
errors.append(f"Unexpected error: {result}")
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
task_key, task_data, error = result
|
|
74
|
+
|
|
75
|
+
if error:
|
|
76
|
+
errors.append(f"{task_key}: {error}")
|
|
77
|
+
print(f" ✗ {task_key}: {error}")
|
|
78
|
+
elif task_data:
|
|
79
|
+
# Task data is already a dict from model_dump()
|
|
80
|
+
fetched_tasks[task_key] = task_data
|
|
81
|
+
print(f" ✓ {task_key}")
|
|
82
|
+
|
|
83
|
+
print(f"\n✓ Successfully fetched {len(fetched_tasks)} task(s)")
|
|
84
|
+
|
|
85
|
+
if errors:
|
|
86
|
+
print(f"\n⚠ {len(errors)} error(s) occurred:")
|
|
87
|
+
for error in errors[:10]: # Show first 10
|
|
88
|
+
print(f" - {error}")
|
|
89
|
+
if len(errors) > 10:
|
|
90
|
+
print(f" ... and {len(errors) - 10} more")
|
|
91
|
+
|
|
92
|
+
return fetched_tasks
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def main():
|
|
96
|
+
parser = argparse.ArgumentParser(
|
|
97
|
+
description="Fetch tasks from Fleet API and update JSON file",
|
|
98
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
99
|
+
epilog="""
|
|
100
|
+
Examples:
|
|
101
|
+
# Fetch tasks and update in-place
|
|
102
|
+
%(prog)s tasks.json
|
|
103
|
+
|
|
104
|
+
# Fetch tasks and save to a new file
|
|
105
|
+
%(prog)s tasks.json --output updated_tasks.json
|
|
106
|
+
|
|
107
|
+
# Limit concurrent requests
|
|
108
|
+
%(prog)s tasks.json --max-concurrent 10
|
|
109
|
+
""",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
parser.add_argument("json_file", help="Path to JSON file containing tasks")
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
"--output",
|
|
115
|
+
"-o",
|
|
116
|
+
help="Path to output JSON file (defaults to overwriting input)",
|
|
117
|
+
default=None,
|
|
118
|
+
)
|
|
119
|
+
parser.add_argument(
|
|
120
|
+
"--max-concurrent",
|
|
121
|
+
"-c",
|
|
122
|
+
type=int,
|
|
123
|
+
default=20,
|
|
124
|
+
help="Maximum number of concurrent API requests (default: 20)",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
args = parser.parse_args()
|
|
128
|
+
|
|
129
|
+
# Load JSON file
|
|
130
|
+
print(f"Reading tasks from: {args.json_file}")
|
|
131
|
+
try:
|
|
132
|
+
with open(args.json_file, "r", encoding="utf-8") as f:
|
|
133
|
+
tasks_data = json.load(f)
|
|
134
|
+
except FileNotFoundError:
|
|
135
|
+
print(f"✗ Error: File '{args.json_file}' not found")
|
|
136
|
+
sys.exit(1)
|
|
137
|
+
except json.JSONDecodeError as e:
|
|
138
|
+
print(f"✗ Error: Invalid JSON in '{args.json_file}': {e}")
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
|
|
141
|
+
if not isinstance(tasks_data, list):
|
|
142
|
+
print("✗ Error: JSON file must contain an array of tasks")
|
|
143
|
+
sys.exit(1)
|
|
144
|
+
|
|
145
|
+
print(f"Found {len(tasks_data)} task(s) in file")
|
|
146
|
+
|
|
147
|
+
# Extract task keys
|
|
148
|
+
task_keys = []
|
|
149
|
+
missing_keys = []
|
|
150
|
+
|
|
151
|
+
for i, task in enumerate(tasks_data):
|
|
152
|
+
task_key = task.get("key") or task.get("id")
|
|
153
|
+
if task_key:
|
|
154
|
+
task_keys.append(task_key)
|
|
155
|
+
else:
|
|
156
|
+
missing_keys.append(f"Task at index {i}")
|
|
157
|
+
|
|
158
|
+
if missing_keys:
|
|
159
|
+
print(f"\n⚠ Warning: {len(missing_keys)} task(s) missing key/id:")
|
|
160
|
+
for key in missing_keys[:10]:
|
|
161
|
+
print(f" - {key}")
|
|
162
|
+
if len(missing_keys) > 10:
|
|
163
|
+
print(f" ... and {len(missing_keys) - 10} more")
|
|
164
|
+
|
|
165
|
+
if not task_keys:
|
|
166
|
+
print("\n✗ Error: No valid task keys found in JSON file")
|
|
167
|
+
sys.exit(1)
|
|
168
|
+
|
|
169
|
+
print(f"\nExtracted {len(task_keys)} task key(s)")
|
|
170
|
+
|
|
171
|
+
# Get account info
|
|
172
|
+
account = await fleet.env.account_async()
|
|
173
|
+
print(f"Fetching from team: {account.team_name}")
|
|
174
|
+
|
|
175
|
+
# Fetch tasks from API
|
|
176
|
+
fetched_tasks = await fetch_tasks_batch(task_keys, args.max_concurrent)
|
|
177
|
+
|
|
178
|
+
if not fetched_tasks:
|
|
179
|
+
print("\n✗ Error: No tasks were successfully fetched")
|
|
180
|
+
sys.exit(1)
|
|
181
|
+
|
|
182
|
+
# Update tasks in the original data
|
|
183
|
+
updated_count = 0
|
|
184
|
+
not_found = []
|
|
185
|
+
|
|
186
|
+
print("\nUpdating task data...")
|
|
187
|
+
for i, task in enumerate(tasks_data):
|
|
188
|
+
task_key = task.get("key") or task.get("id")
|
|
189
|
+
|
|
190
|
+
if task_key in fetched_tasks:
|
|
191
|
+
# Replace entire task with fetched data
|
|
192
|
+
tasks_data[i] = fetched_tasks[task_key]
|
|
193
|
+
updated_count += 1
|
|
194
|
+
else:
|
|
195
|
+
not_found.append(task_key or f"index {i}")
|
|
196
|
+
|
|
197
|
+
print(f"✓ Updated {updated_count} task(s)")
|
|
198
|
+
|
|
199
|
+
if not_found:
|
|
200
|
+
print(f"\n⚠ Warning: {len(not_found)} task(s) not fetched:")
|
|
201
|
+
for key in not_found[:10]:
|
|
202
|
+
print(f" - {key}")
|
|
203
|
+
if len(not_found) > 10:
|
|
204
|
+
print(f" ... and {len(not_found) - 10} more")
|
|
205
|
+
|
|
206
|
+
# Write output
|
|
207
|
+
output_file = args.output or args.json_file
|
|
208
|
+
print(f"\nWriting updated tasks to: {output_file}")
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
212
|
+
json.dump(tasks_data, f, indent=2, ensure_ascii=False)
|
|
213
|
+
print(f"✓ Successfully wrote {len(tasks_data)} task(s) to '{output_file}'")
|
|
214
|
+
except Exception as e:
|
|
215
|
+
print(f"✗ Error writing output file: {e}")
|
|
216
|
+
sys.exit(1)
|
|
217
|
+
|
|
218
|
+
# Summary
|
|
219
|
+
print("\n" + "=" * 60)
|
|
220
|
+
print("Summary:")
|
|
221
|
+
print(f" Total tasks in file: {len(tasks_data)}")
|
|
222
|
+
print(f" Successfully fetched: {len(fetched_tasks)}")
|
|
223
|
+
print(f" Updated in file: {updated_count}")
|
|
224
|
+
print(f" Failed to fetch: {len(task_keys) - len(fetched_tasks)}")
|
|
225
|
+
print("=" * 60)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
if __name__ == "__main__":
|
|
229
|
+
asyncio.run(main())
|
|
230
|
+
|