fleet-python 0.2.107__tar.gz → 0.2.109__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.107/fleet_python.egg-info → fleet_python-0.2.109}/PKG-INFO +1 -1
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/export_tasks.py +5 -4
- fleet_python-0.2.109/examples/export_tasks_filtered.py +245 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/import_tasks.py +130 -13
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/__init__.py +1 -1
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/__init__.py +1 -1
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/base.py +1 -1
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/client.py +5 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/resources/sqlite.py +9 -4
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/base.py +1 -1
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/client.py +5 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/resources/sqlite.py +12 -7
- {fleet_python-0.2.107 → fleet_python-0.2.109/fleet_python.egg-info}/PKG-INFO +1 -1
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet_python.egg-info/SOURCES.txt +1 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/pyproject.toml +1 -1
- {fleet_python-0.2.107 → fleet_python-0.2.109}/LICENSE +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/README.md +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/diff_example.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/dsl_example.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/example.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/exampleResume.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/example_account.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/example_action_log.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/example_client.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/example_mcp_anthropic.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/example_mcp_openai.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/example_sync.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/example_task.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/example_tasks.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/example_verifier.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/fetch_tasks.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/gemini_example.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/iterate_verifiers.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/json_tasks_example.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/nova_act_example.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/openai_example.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/openai_simple_example.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/query_builder_example.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/quickstart.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/examples/test_cdp_logging.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/env/__init__.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/env/client.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/exceptions.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/global_client.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/instance/__init__.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/instance/base.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/instance/client.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/models.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/resources/__init__.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/resources/api.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/resources/base.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/resources/browser.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/resources/filesystem.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/resources/mcp.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/tasks.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/verifiers/__init__.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/verifiers/bundler.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/_async/verifiers/verifier.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/agent/__init__.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/agent/gemini_cua/Dockerfile +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/agent/gemini_cua/__init__.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/agent/gemini_cua/agent.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/agent/gemini_cua/mcp/main.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/agent/gemini_cua/mcp_server/__init__.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/agent/gemini_cua/mcp_server/main.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/agent/gemini_cua/mcp_server/tools.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/agent/gemini_cua/requirements.txt +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/agent/gemini_cua/start.sh +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/agent/orchestrator.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/agent/types.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/agent/utils.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/cli.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/config.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/env/__init__.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/env/client.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/eval/__init__.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/eval/uploader.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/exceptions.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/global_client.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/instance/__init__.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/instance/base.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/instance/client.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/instance/models.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/models.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/proxy/__init__.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/proxy/proxy.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/proxy/whitelist.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/resources/__init__.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/resources/api.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/resources/base.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/resources/browser.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/resources/filesystem.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/resources/mcp.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/tasks.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/types.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/utils/__init__.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/utils/http_logging.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/utils/logging.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/utils/playwright.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/verifiers/__init__.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/verifiers/bundler.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/verifiers/code.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/verifiers/db.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/verifiers/decorator.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/verifiers/parse.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/verifiers/sql_differ.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet/verifiers/verifier.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet_python.egg-info/dependency_links.txt +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet_python.egg-info/entry_points.txt +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet_python.egg-info/requires.txt +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/fleet_python.egg-info/top_level.txt +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/scripts/fix_sync_imports.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/scripts/unasync.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/setup.cfg +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/tests/__init__.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/tests/test_app_method.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/tests/test_expect_exactly.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/tests/test_expect_only.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/tests/test_instance_dispatch.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/tests/test_sqlite_resource_dual_mode.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/tests/test_sqlite_shared_memory_behavior.py +0 -0
- {fleet_python-0.2.107 → fleet_python-0.2.109}/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)
|
|
@@ -405,6 +405,11 @@ class AsyncEnv(EnvironmentBase):
|
|
|
405
405
|
base_url = self.urls.api
|
|
406
406
|
elif self.urls and self.urls.root:
|
|
407
407
|
base_url = f"{self.urls.root.rstrip('/')}/raw"
|
|
408
|
+
elif self._manager_url_override and self._manager_url_override != "local://":
|
|
409
|
+
# URL mode: strip /api/v1/env suffix to get root URL
|
|
410
|
+
base_url = self._manager_url_override.rstrip('/')
|
|
411
|
+
if base_url.endswith('/api/v1/env'):
|
|
412
|
+
base_url = base_url[:-len('/api/v1/env')]
|
|
408
413
|
else:
|
|
409
414
|
raise ValueError("No API URL configured for this environment")
|
|
410
415
|
return self.instance.api(name, base_url)
|
|
@@ -99,10 +99,11 @@ class AsyncDatabaseSnapshot:
|
|
|
99
99
|
self,
|
|
100
100
|
other: "AsyncDatabaseSnapshot",
|
|
101
101
|
ignore_config: Optional[IgnoreConfig] = None,
|
|
102
|
+
use_structured_diff: bool = False,
|
|
102
103
|
) -> "AsyncSnapshotDiff":
|
|
103
104
|
"""Compare this snapshot with another."""
|
|
104
105
|
# No need to fetch all data upfront - diff will fetch on demand
|
|
105
|
-
return AsyncSnapshotDiff(self, other, ignore_config)
|
|
106
|
+
return AsyncSnapshotDiff(self, other, ignore_config, use_structured_diff=use_structured_diff)
|
|
106
107
|
|
|
107
108
|
|
|
108
109
|
class AsyncSnapshotQueryBuilder:
|
|
@@ -275,10 +276,12 @@ class AsyncSnapshotDiff:
|
|
|
275
276
|
before: AsyncDatabaseSnapshot,
|
|
276
277
|
after: AsyncDatabaseSnapshot,
|
|
277
278
|
ignore_config: Optional[IgnoreConfig] = None,
|
|
279
|
+
use_structured_diff: bool = False,
|
|
278
280
|
):
|
|
279
281
|
self.before = before
|
|
280
282
|
self.after = after
|
|
281
283
|
self.ignore_config = ignore_config or IgnoreConfig()
|
|
284
|
+
self.use_structured_diff = use_structured_diff
|
|
282
285
|
self._cached: Optional[Dict[str, Any]] = None
|
|
283
286
|
self._targeted_mode = False # Flag to use targeted queries
|
|
284
287
|
|
|
@@ -1766,8 +1769,7 @@ class AsyncSnapshotDiff:
|
|
|
1766
1769
|
return await self._expect_no_changes()
|
|
1767
1770
|
|
|
1768
1771
|
resource = self.after.resource
|
|
1769
|
-
|
|
1770
|
-
if False and resource.client is not None and resource._mode == "http":
|
|
1772
|
+
if self.use_structured_diff and resource.client is not None and resource._mode == "http":
|
|
1771
1773
|
api_diff = None
|
|
1772
1774
|
try:
|
|
1773
1775
|
payload = {}
|
|
@@ -2478,12 +2480,15 @@ class AsyncSQLiteResource(Resource):
|
|
|
2478
2480
|
self,
|
|
2479
2481
|
other: "AsyncSQLiteResource",
|
|
2480
2482
|
ignore_config: Optional[IgnoreConfig] = None,
|
|
2483
|
+
use_structured_diff: bool = False,
|
|
2481
2484
|
) -> AsyncSnapshotDiff:
|
|
2482
2485
|
"""Compare this database with another AsyncSQLiteResource.
|
|
2483
2486
|
|
|
2484
2487
|
Args:
|
|
2485
2488
|
other: Another AsyncSQLiteResource to compare against
|
|
2486
2489
|
ignore_config: Optional configuration for ignoring specific tables/fields
|
|
2490
|
+
use_structured_diff: If True, use server-side structured diff API in expect_only_v2.
|
|
2491
|
+
Defaults to False (local diff only).
|
|
2487
2492
|
|
|
2488
2493
|
Returns:
|
|
2489
2494
|
AsyncSnapshotDiff: Object containing the differences between the two databases
|
|
@@ -2497,4 +2502,4 @@ class AsyncSQLiteResource(Resource):
|
|
|
2497
2502
|
)
|
|
2498
2503
|
|
|
2499
2504
|
# Return the diff between the snapshots
|
|
2500
|
-
return await before_snapshot.diff(after_snapshot, ignore_config)
|
|
2505
|
+
return await before_snapshot.diff(after_snapshot, ignore_config, use_structured_diff=use_structured_diff)
|
|
@@ -417,6 +417,11 @@ class SyncEnv(EnvironmentBase):
|
|
|
417
417
|
base_url = self.urls.api
|
|
418
418
|
elif self.urls and self.urls.root:
|
|
419
419
|
base_url = f"{self.urls.root.rstrip('/')}/raw"
|
|
420
|
+
elif self._manager_url_override and self._manager_url_override != "local://":
|
|
421
|
+
# URL mode: strip /api/v1/env suffix to get root URL
|
|
422
|
+
base_url = self._manager_url_override.rstrip('/')
|
|
423
|
+
if base_url.endswith('/api/v1/env'):
|
|
424
|
+
base_url = base_url[:-len('/api/v1/env')]
|
|
420
425
|
else:
|
|
421
426
|
raise ValueError("No API URL configured for this environment")
|
|
422
427
|
return self.instance.api(name, base_url)
|
|
@@ -98,10 +98,11 @@ class SyncDatabaseSnapshot:
|
|
|
98
98
|
self,
|
|
99
99
|
other: "SyncDatabaseSnapshot",
|
|
100
100
|
ignore_config: Optional[IgnoreConfig] = None,
|
|
101
|
+
use_structured_diff: bool = False,
|
|
101
102
|
) -> "SyncSnapshotDiff":
|
|
102
103
|
"""Compare this snapshot with another."""
|
|
103
104
|
# No need to fetch all data upfront - diff will fetch on demand
|
|
104
|
-
return SyncSnapshotDiff(self, other, ignore_config)
|
|
105
|
+
return SyncSnapshotDiff(self, other, ignore_config, use_structured_diff=use_structured_diff)
|
|
105
106
|
|
|
106
107
|
|
|
107
108
|
class SyncSnapshotQueryBuilder:
|
|
@@ -274,10 +275,12 @@ class SyncSnapshotDiff:
|
|
|
274
275
|
before: SyncDatabaseSnapshot,
|
|
275
276
|
after: SyncDatabaseSnapshot,
|
|
276
277
|
ignore_config: Optional[IgnoreConfig] = None,
|
|
278
|
+
use_structured_diff: bool = False,
|
|
277
279
|
):
|
|
278
280
|
self.before = before
|
|
279
281
|
self.after = after
|
|
280
282
|
self.ignore_config = ignore_config or IgnoreConfig()
|
|
283
|
+
self.use_structured_diff = use_structured_diff
|
|
281
284
|
self._cached: Optional[Dict[str, Any]] = None
|
|
282
285
|
self._targeted_mode = False # Flag to use targeted queries
|
|
283
286
|
|
|
@@ -1814,8 +1817,7 @@ class SyncSnapshotDiff:
|
|
|
1814
1817
|
return self._expect_no_changes()
|
|
1815
1818
|
|
|
1816
1819
|
resource = self.after.resource
|
|
1817
|
-
|
|
1818
|
-
if False and resource.client is not None and resource._mode == "http":
|
|
1820
|
+
if self.use_structured_diff and resource.client is not None and resource._mode == "http":
|
|
1819
1821
|
api_diff = None
|
|
1820
1822
|
try:
|
|
1821
1823
|
payload = {}
|
|
@@ -2516,19 +2518,22 @@ class SQLiteResource(Resource):
|
|
|
2516
2518
|
self,
|
|
2517
2519
|
other: "SQLiteResource",
|
|
2518
2520
|
ignore_config: Optional[IgnoreConfig] = None,
|
|
2521
|
+
use_structured_diff: bool = False,
|
|
2519
2522
|
) -> SyncSnapshotDiff:
|
|
2520
|
-
"""Compare this database with another
|
|
2523
|
+
"""Compare this database with another SQLiteResource.
|
|
2521
2524
|
|
|
2522
2525
|
Args:
|
|
2523
|
-
other: Another
|
|
2526
|
+
other: Another SQLiteResource to compare against
|
|
2524
2527
|
ignore_config: Optional configuration for ignoring specific tables/fields
|
|
2528
|
+
use_structured_diff: If True, use server-side structured diff API in expect_only_v2.
|
|
2529
|
+
Defaults to False (local diff only).
|
|
2525
2530
|
|
|
2526
2531
|
Returns:
|
|
2527
|
-
|
|
2532
|
+
SyncSnapshotDiff: Object containing the differences between the two databases
|
|
2528
2533
|
"""
|
|
2529
2534
|
# Create snapshots of both databases
|
|
2530
2535
|
before_snapshot = self.snapshot(name=f"before_{datetime.utcnow().isoformat()}")
|
|
2531
2536
|
after_snapshot = other.snapshot(name=f"after_{datetime.utcnow().isoformat()}")
|
|
2532
2537
|
|
|
2533
2538
|
# Return the diff between the snapshots
|
|
2534
|
-
return before_snapshot.diff(after_snapshot, ignore_config)
|
|
2539
|
+
return before_snapshot.diff(after_snapshot, ignore_config, use_structured_diff=use_structured_diff)
|
|
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
|
|
File without changes
|
|
File without changes
|