fleet-python 0.2.101__tar.gz → 0.2.103__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.
- {fleet_python-0.2.101/fleet_python.egg-info → fleet_python-0.2.103}/PKG-INFO +1 -1
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/export_tasks.py +5 -4
- fleet_python-0.2.103/examples/export_tasks_filtered.py +245 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/import_tasks.py +130 -13
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/__init__.py +1 -1
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/__init__.py +1 -1
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/base.py +1 -1
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/client.py +5 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/resources/sqlite.py +10 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/base.py +1 -1
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/client.py +5 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/resources/sqlite.py +10 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103/fleet_python.egg-info}/PKG-INFO +1 -1
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet_python.egg-info/SOURCES.txt +1 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/pyproject.toml +1 -1
- {fleet_python-0.2.101 → fleet_python-0.2.103}/LICENSE +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/README.md +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/diff_example.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/dsl_example.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/example.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/exampleResume.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/example_account.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/example_action_log.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/example_client.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/example_mcp_anthropic.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/example_mcp_openai.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/example_sync.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/example_task.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/example_tasks.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/example_verifier.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/fetch_tasks.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/gemini_example.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/iterate_verifiers.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/json_tasks_example.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/nova_act_example.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/openai_example.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/openai_simple_example.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/query_builder_example.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/quickstart.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/examples/test_cdp_logging.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/env/__init__.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/env/client.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/exceptions.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/global_client.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/instance/__init__.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/instance/base.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/instance/client.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/models.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/resources/__init__.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/resources/api.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/resources/base.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/resources/browser.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/resources/mcp.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/tasks.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/verifiers/__init__.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/verifiers/bundler.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/_async/verifiers/verifier.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/agent/__init__.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/agent/gemini_cua/Dockerfile +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/agent/gemini_cua/__init__.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/agent/gemini_cua/agent.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/agent/gemini_cua/mcp/main.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/agent/gemini_cua/mcp_server/__init__.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/agent/gemini_cua/mcp_server/main.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/agent/gemini_cua/mcp_server/tools.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/agent/gemini_cua/requirements.txt +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/agent/gemini_cua/start.sh +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/agent/orchestrator.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/agent/types.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/agent/utils.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/cli.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/config.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/env/__init__.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/env/client.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/eval/__init__.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/eval/uploader.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/exceptions.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/global_client.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/instance/__init__.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/instance/base.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/instance/client.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/instance/models.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/models.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/proxy/__init__.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/proxy/proxy.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/proxy/whitelist.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/resources/__init__.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/resources/api.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/resources/base.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/resources/browser.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/resources/mcp.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/tasks.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/types.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/utils/__init__.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/utils/http_logging.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/utils/logging.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/utils/playwright.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/verifiers/__init__.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/verifiers/bundler.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/verifiers/code.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/verifiers/db.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/verifiers/decorator.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/verifiers/parse.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/verifiers/sql_differ.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet/verifiers/verifier.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet_python.egg-info/dependency_links.txt +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet_python.egg-info/entry_points.txt +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet_python.egg-info/requires.txt +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/fleet_python.egg-info/top_level.txt +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/scripts/fix_sync_imports.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/scripts/unasync.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/setup.cfg +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/tests/__init__.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/tests/test_app_method.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/tests/test_expect_exactly.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/tests/test_expect_only.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/tests/test_instance_dispatch.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/tests/test_sqlite_resource_dual_mode.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/tests/test_sqlite_shared_memory_behavior.py +0 -0
- {fleet_python-0.2.101 → fleet_python-0.2.103}/tests/test_verifier_from_string.py +0 -0
|
@@ -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(
|
|
@@ -66,10 +65,12 @@ def main():
|
|
|
66
65
|
print(f"Loading tasks from project: {args.project_key}")
|
|
67
66
|
tasks = fleet.load_tasks(project_key=args.project_key)
|
|
68
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(",")]
|
|
69
70
|
print(
|
|
70
|
-
f"Loading {len(
|
|
71
|
+
f"Loading {len(task_keys_list)} specific task(s): {', '.join(task_keys_list)}"
|
|
71
72
|
)
|
|
72
|
-
tasks = fleet.load_tasks(keys=
|
|
73
|
+
tasks = fleet.load_tasks(keys=task_keys_list)
|
|
73
74
|
elif args.task_project_key:
|
|
74
75
|
print(f"Loading tasks from task project: {args.task_project_key}")
|
|
75
76
|
tasks = fleet.load_tasks(task_project_key=args.task_project_key)
|
|
@@ -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
|
+
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import argparse
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
4
5
|
import sys
|
|
6
|
+
import tempfile
|
|
5
7
|
from collections import defaultdict
|
|
6
8
|
from typing import Dict, List, Tuple
|
|
7
9
|
import fleet
|
|
@@ -183,7 +185,6 @@ async def run_verifier_sanity_check(
|
|
|
183
185
|
print(f" - {task_key}: {error_msg}")
|
|
184
186
|
if len(errors) > 10:
|
|
185
187
|
print(f" ... and {len(errors) - 10} more")
|
|
186
|
-
print("\nFix the verifiers and try again.")
|
|
187
188
|
return False, errors
|
|
188
189
|
else:
|
|
189
190
|
print("✓ All verifiers passed!")
|
|
@@ -237,6 +238,7 @@ async def main():
|
|
|
237
238
|
task_count = len(tasks_data)
|
|
238
239
|
task_keys = []
|
|
239
240
|
missing_verifier = []
|
|
241
|
+
tasks_with_output_schema = []
|
|
240
242
|
for task_data in tasks_data:
|
|
241
243
|
task_key = task_data.get("key") or task_data.get("id")
|
|
242
244
|
if task_key:
|
|
@@ -249,6 +251,10 @@ async def main():
|
|
|
249
251
|
if not verifier_code:
|
|
250
252
|
missing_verifier.append(task_key or "(no key)")
|
|
251
253
|
|
|
254
|
+
# Check for output_json_schema
|
|
255
|
+
if task_data.get("output_json_schema"):
|
|
256
|
+
tasks_with_output_schema.append(task_key or "(no key)")
|
|
257
|
+
|
|
252
258
|
# Validate all tasks have verifier_func
|
|
253
259
|
if missing_verifier:
|
|
254
260
|
print(f"✗ Error: {len(missing_verifier)} task(s) missing verifier_func:")
|
|
@@ -291,21 +297,64 @@ async def main():
|
|
|
291
297
|
print(f"✓ Loaded {len(tasks)} tasks")
|
|
292
298
|
|
|
293
299
|
# Run sanity check (unless skipped)
|
|
300
|
+
already_confirmed = False # Track if user already confirmed import
|
|
294
301
|
if not args.skip_sanity_check:
|
|
295
302
|
success, errors = await run_verifier_sanity_check(tasks, client)
|
|
296
|
-
if not success:
|
|
297
|
-
sys.exit(1)
|
|
298
303
|
|
|
299
|
-
# If only doing sanity check, exit
|
|
304
|
+
# If only doing sanity check, exit here
|
|
300
305
|
if args.sanity_check_only:
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
306
|
+
if success:
|
|
307
|
+
print("\n✓ Sanity check complete! (--sanity-check-only)")
|
|
308
|
+
print("Tasks are ready to import.")
|
|
309
|
+
sys.exit(0)
|
|
310
|
+
else:
|
|
311
|
+
print("\n✗ Sanity check failed (--sanity-check-only)")
|
|
312
|
+
print("Fix the verifiers and try again.")
|
|
313
|
+
sys.exit(1)
|
|
314
|
+
|
|
315
|
+
# Handle partial failures
|
|
316
|
+
if not success:
|
|
317
|
+
# Filter out failed tasks
|
|
318
|
+
failed_keys = set(errors.keys())
|
|
319
|
+
passed_tasks = [t for t in tasks if t.key not in failed_keys]
|
|
320
|
+
|
|
321
|
+
print("\n" + "=" * 60)
|
|
322
|
+
print("SANITY CHECK RESULTS")
|
|
323
|
+
print("=" * 60)
|
|
324
|
+
print(f"Passed: {len(passed_tasks)}/{len(tasks)} tasks")
|
|
325
|
+
print(f"Failed: {len(failed_keys)}/{len(tasks)} tasks")
|
|
326
|
+
|
|
327
|
+
if len(passed_tasks) == 0:
|
|
328
|
+
print("\n✗ No tasks passed the sanity check.")
|
|
329
|
+
print("Fix the verifiers and try again.")
|
|
330
|
+
sys.exit(1)
|
|
331
|
+
|
|
332
|
+
# Prompt user to import only passed tasks
|
|
333
|
+
if not args.yes:
|
|
334
|
+
print("\nWould you like to import only the tasks that passed?")
|
|
335
|
+
response = input("Type 'YES' to import passed tasks only: ")
|
|
336
|
+
if response != "YES":
|
|
337
|
+
print("Import cancelled.")
|
|
338
|
+
sys.exit(0)
|
|
339
|
+
already_confirmed = True # User already confirmed import
|
|
340
|
+
else:
|
|
341
|
+
print("\n⚠️ Auto-importing only tasks that passed (--yes flag)")
|
|
342
|
+
|
|
343
|
+
# Update tasks list and tasks_data to only include passed tasks
|
|
344
|
+
tasks = passed_tasks
|
|
345
|
+
passed_keys = {t.key for t in passed_tasks}
|
|
346
|
+
tasks_data = [td for td in tasks_data if td.get("key") in passed_keys]
|
|
347
|
+
# Also filter tasks_with_output_schema
|
|
348
|
+
tasks_with_output_schema = [
|
|
349
|
+
k for k in tasks_with_output_schema if k in passed_keys
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
print(f"\nProceeding with {len(tasks)} tasks that passed sanity check")
|
|
304
353
|
else:
|
|
305
354
|
print("\n⚠️ Skipping sanity check (--skip-sanity-check)")
|
|
306
355
|
|
|
307
|
-
# Confirmation prompt (unless --yes flag is provided)
|
|
308
|
-
if not args.yes:
|
|
356
|
+
# Confirmation prompt (unless --yes flag is provided or already confirmed)
|
|
357
|
+
if not args.yes and not already_confirmed:
|
|
309
358
|
print("\n" + "=" * 60)
|
|
310
359
|
response = input("Type 'YES' to proceed with import: ")
|
|
311
360
|
if response != "YES":
|
|
@@ -315,10 +364,78 @@ async def main():
|
|
|
315
364
|
# Import tasks
|
|
316
365
|
print("\nImporting tasks...")
|
|
317
366
|
try:
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
367
|
+
# If tasks were filtered, write to a temporary file for import
|
|
368
|
+
if len(tasks_data) < task_count:
|
|
369
|
+
# Create temporary file with filtered tasks
|
|
370
|
+
with tempfile.NamedTemporaryFile(
|
|
371
|
+
mode="w", suffix=".json", delete=False, encoding="utf-8"
|
|
372
|
+
) as temp_file:
|
|
373
|
+
json.dump(tasks_data, temp_file, indent=2, ensure_ascii=False)
|
|
374
|
+
temp_filename = temp_file.name
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
results = await fleet.import_tasks_async(
|
|
378
|
+
temp_filename, project_key=args.project_key
|
|
379
|
+
)
|
|
380
|
+
finally:
|
|
381
|
+
# Clean up temporary file
|
|
382
|
+
os.unlink(temp_filename)
|
|
383
|
+
else:
|
|
384
|
+
# Import from original file if no filtering occurred
|
|
385
|
+
results = await fleet.import_tasks_async(
|
|
386
|
+
args.json_file, project_key=args.project_key
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Print success summary
|
|
390
|
+
print("\n" + "=" * 60)
|
|
391
|
+
print("IMPORT COMPLETE")
|
|
392
|
+
print("=" * 60)
|
|
393
|
+
print(f"✓ Successfully imported {len(results)} task(s)")
|
|
394
|
+
|
|
395
|
+
if args.project_key:
|
|
396
|
+
print(f"✓ Associated with project: {args.project_key}")
|
|
397
|
+
|
|
398
|
+
print(f"✓ Team: {account.team_name}")
|
|
399
|
+
|
|
400
|
+
# Print HUGE warning if any tasks have output_json_schema
|
|
401
|
+
if tasks_with_output_schema:
|
|
402
|
+
print("\n")
|
|
403
|
+
print("!" * 80)
|
|
404
|
+
print("!" * 80)
|
|
405
|
+
print("!" * 80)
|
|
406
|
+
print(
|
|
407
|
+
"!!! !!!"
|
|
408
|
+
)
|
|
409
|
+
print(
|
|
410
|
+
"!!! ⚠️ WARNING WARNING WARNING ⚠️ !!!"
|
|
411
|
+
)
|
|
412
|
+
print(
|
|
413
|
+
"!!! !!!"
|
|
414
|
+
)
|
|
415
|
+
print(
|
|
416
|
+
f"!!! {len(tasks_with_output_schema)} TASK(S) HAVE OUTPUT_JSON_SCHEMA THAT NEED MANUAL COPYING! !!!"
|
|
417
|
+
)
|
|
418
|
+
print(
|
|
419
|
+
"!!! !!!"
|
|
420
|
+
)
|
|
421
|
+
print(
|
|
422
|
+
"!!! The output_json_schema field is NOT automatically imported! !!!"
|
|
423
|
+
)
|
|
424
|
+
print(
|
|
425
|
+
"!!! You MUST manually copy the output schemas to each task! !!!"
|
|
426
|
+
)
|
|
427
|
+
print(
|
|
428
|
+
"!!! !!!"
|
|
429
|
+
)
|
|
430
|
+
print("!" * 80)
|
|
431
|
+
print("!" * 80)
|
|
432
|
+
print("!" * 80)
|
|
433
|
+
print("\nTasks with output_json_schema:")
|
|
434
|
+
for i, key in enumerate(tasks_with_output_schema[:20], 1):
|
|
435
|
+
print(f" {i}. {key}")
|
|
436
|
+
if len(tasks_with_output_schema) > 20:
|
|
437
|
+
print(f" ... and {len(tasks_with_output_schema) - 20} more")
|
|
438
|
+
print("\n⚠️ REMEMBER TO MANUALLY COPY OUTPUT SCHEMAS! ⚠️\n")
|
|
322
439
|
except Exception as e:
|
|
323
440
|
print(f"\n✗ Error importing tasks: {e}")
|
|
324
441
|
sys.exit(1)
|
|
@@ -400,6 +400,11 @@ class AsyncEnv(EnvironmentBase):
|
|
|
400
400
|
base_url = self.urls.api
|
|
401
401
|
elif self.urls and self.urls.root:
|
|
402
402
|
base_url = f"{self.urls.root.rstrip('/')}/raw"
|
|
403
|
+
elif self._manager_url_override and self._manager_url_override != "local://":
|
|
404
|
+
# URL mode: strip /api/v1/env suffix to get root URL
|
|
405
|
+
base_url = self._manager_url_override.rstrip('/')
|
|
406
|
+
if base_url.endswith('/api/v1/env'):
|
|
407
|
+
base_url = base_url[:-len('/api/v1/env')]
|
|
403
408
|
else:
|
|
404
409
|
raise ValueError("No API URL configured for this environment")
|
|
405
410
|
return self.instance.api(name, base_url)
|
|
@@ -1733,6 +1733,11 @@ class AsyncSnapshotDiff:
|
|
|
1733
1733
|
|
|
1734
1734
|
async def expect_only(self, allowed_changes: List[Dict[str, Any]]):
|
|
1735
1735
|
"""Ensure only specified changes occurred."""
|
|
1736
|
+
# Normalize pk values: convert lists to tuples for hashability and consistency
|
|
1737
|
+
for change in allowed_changes:
|
|
1738
|
+
if "pk" in change and isinstance(change["pk"], list):
|
|
1739
|
+
change["pk"] = tuple(change["pk"])
|
|
1740
|
+
|
|
1736
1741
|
# Special case: empty allowed_changes means no changes should have occurred
|
|
1737
1742
|
if not allowed_changes:
|
|
1738
1743
|
return await self._expect_no_changes()
|
|
@@ -1751,6 +1756,11 @@ class AsyncSnapshotDiff:
|
|
|
1751
1756
|
This version supports field-level specifications for added/removed rows,
|
|
1752
1757
|
allowing users to specify expected field values instead of just whole-row specs.
|
|
1753
1758
|
"""
|
|
1759
|
+
# Normalize pk values: convert lists to tuples for hashability and consistency
|
|
1760
|
+
for change in allowed_changes:
|
|
1761
|
+
if "pk" in change and isinstance(change["pk"], list):
|
|
1762
|
+
change["pk"] = tuple(change["pk"])
|
|
1763
|
+
|
|
1754
1764
|
# Special case: empty allowed_changes means no changes should have occurred
|
|
1755
1765
|
if not allowed_changes:
|
|
1756
1766
|
return await self._expect_no_changes()
|
|
@@ -412,6 +412,11 @@ class SyncEnv(EnvironmentBase):
|
|
|
412
412
|
base_url = self.urls.api
|
|
413
413
|
elif self.urls and self.urls.root:
|
|
414
414
|
base_url = f"{self.urls.root.rstrip('/')}/raw"
|
|
415
|
+
elif self._manager_url_override and self._manager_url_override != "local://":
|
|
416
|
+
# URL mode: strip /api/v1/env suffix to get root URL
|
|
417
|
+
base_url = self._manager_url_override.rstrip('/')
|
|
418
|
+
if base_url.endswith('/api/v1/env'):
|
|
419
|
+
base_url = base_url[:-len('/api/v1/env')]
|
|
415
420
|
else:
|
|
416
421
|
raise ValueError("No API URL configured for this environment")
|
|
417
422
|
return self.instance.api(name, base_url)
|
|
@@ -1781,6 +1781,11 @@ class SyncSnapshotDiff:
|
|
|
1781
1781
|
|
|
1782
1782
|
def expect_only(self, allowed_changes: List[Dict[str, Any]]):
|
|
1783
1783
|
"""Ensure only specified changes occurred."""
|
|
1784
|
+
# Normalize pk values: convert lists to tuples for hashability and consistency
|
|
1785
|
+
for change in allowed_changes:
|
|
1786
|
+
if "pk" in change and isinstance(change["pk"], list):
|
|
1787
|
+
change["pk"] = tuple(change["pk"])
|
|
1788
|
+
|
|
1784
1789
|
# Special case: empty allowed_changes means no changes should have occurred
|
|
1785
1790
|
if not allowed_changes:
|
|
1786
1791
|
return self._expect_no_changes()
|
|
@@ -1799,6 +1804,11 @@ class SyncSnapshotDiff:
|
|
|
1799
1804
|
This version supports field-level specifications for added/removed rows,
|
|
1800
1805
|
allowing users to specify expected field values instead of just whole-row specs.
|
|
1801
1806
|
"""
|
|
1807
|
+
# Normalize pk values: convert lists to tuples for hashability and consistency
|
|
1808
|
+
for change in allowed_changes:
|
|
1809
|
+
if "pk" in change and isinstance(change["pk"], list):
|
|
1810
|
+
change["pk"] = tuple(change["pk"])
|
|
1811
|
+
|
|
1802
1812
|
# Special case: empty allowed_changes means no changes should have occurred
|
|
1803
1813
|
if not allowed_changes:
|
|
1804
1814
|
return self._expect_no_changes()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|