kaiten-cli 0.3.3__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.
- kaiten_cli/__init__.py +5 -0
- kaiten_cli/__main__.py +9 -0
- kaiten_cli/agent_gateway.py +577 -0
- kaiten_cli/app.py +1636 -0
- kaiten_cli/completion.py +570 -0
- kaiten_cli/discovery.py +39 -0
- kaiten_cli/errors.py +79 -0
- kaiten_cli/i18n.py +92 -0
- kaiten_cli/locales/ru.json +2967 -0
- kaiten_cli/localized_click.py +160 -0
- kaiten_cli/models.py +202 -0
- kaiten_cli/profiles.py +316 -0
- kaiten_cli/registry/__init__.py +260 -0
- kaiten_cli/registry/addons.py +392 -0
- kaiten_cli/registry/audit_and_analytics.py +451 -0
- kaiten_cli/registry/automation_schema.py +44 -0
- kaiten_cli/registry/automation_schemas.json +7417 -0
- kaiten_cli/registry/automations.py +342 -0
- kaiten_cli/registry/base.py +62 -0
- kaiten_cli/registry/blockers.py +320 -0
- kaiten_cli/registry/boards.py +520 -0
- kaiten_cli/registry/card_relations.py +376 -0
- kaiten_cli/registry/card_types.py +363 -0
- kaiten_cli/registry/cards.py +1329 -0
- kaiten_cli/registry/charts.py +539 -0
- kaiten_cli/registry/checklists.py +600 -0
- kaiten_cli/registry/columns.py +558 -0
- kaiten_cli/registry/comments.py +224 -0
- kaiten_cli/registry/custom_directories.py +980 -0
- kaiten_cli/registry/custom_properties.py +1460 -0
- kaiten_cli/registry/dashboards.py +564 -0
- kaiten_cli/registry/documents.py +761 -0
- kaiten_cli/registry/external_links.py +128 -0
- kaiten_cli/registry/files.py +738 -0
- kaiten_cli/registry/github_addon.py +613 -0
- kaiten_cli/registry/iterations.py +397 -0
- kaiten_cli/registry/lanes.py +205 -0
- kaiten_cli/registry/live_contracts.py +361 -0
- kaiten_cli/registry/members.py +336 -0
- kaiten_cli/registry/module_docs.py +108 -0
- kaiten_cli/registry/projects.py +369 -0
- kaiten_cli/registry/query.py +129 -0
- kaiten_cli/registry/restrictions.py +249 -0
- kaiten_cli/registry/roles_and_groups.py +1102 -0
- kaiten_cli/registry/scim.py +263 -0
- kaiten_cli/registry/service_desk.py +1270 -0
- kaiten_cli/registry/snapshot.py +187 -0
- kaiten_cli/registry/spaces.py +272 -0
- kaiten_cli/registry/subscribers.py +168 -0
- kaiten_cli/registry/tags.py +193 -0
- kaiten_cli/registry/time_logs.py +353 -0
- kaiten_cli/registry/tree.py +352 -0
- kaiten_cli/registry/utilities.py +287 -0
- kaiten_cli/registry/webhooks.py +265 -0
- kaiten_cli/runtime/__init__.py +1 -0
- kaiten_cli/runtime/automation_validation.py +115 -0
- kaiten_cli/runtime/behaviors.py +787 -0
- kaiten_cli/runtime/cache.py +733 -0
- kaiten_cli/runtime/client.py +387 -0
- kaiten_cli/runtime/endpoints.py +59 -0
- kaiten_cli/runtime/executor.py +269 -0
- kaiten_cli/runtime/fs_security.py +168 -0
- kaiten_cli/runtime/input.py +310 -0
- kaiten_cli/runtime/output.py +59 -0
- kaiten_cli/runtime/public_validation.py +35 -0
- kaiten_cli/runtime/snapshot_build.py +527 -0
- kaiten_cli/runtime/snapshot_common.py +363 -0
- kaiten_cli/runtime/snapshot_query.py +306 -0
- kaiten_cli/runtime/snapshot_store.py +1192 -0
- kaiten_cli/runtime/snapshots.py +62 -0
- kaiten_cli/runtime/sqlite_errors.py +23 -0
- kaiten_cli/runtime/support/__init__.py +1 -0
- kaiten_cli/runtime/support/addons.py +953 -0
- kaiten_cli/runtime/support/audit.py +70 -0
- kaiten_cli/runtime/support/batch.py +234 -0
- kaiten_cli/runtime/support/card_move_url.py +213 -0
- kaiten_cli/runtime/support/cards.py +174 -0
- kaiten_cli/runtime/support/checklists.py +21 -0
- kaiten_cli/runtime/support/company_users.py +117 -0
- kaiten_cli/runtime/support/dashboards.py +35 -0
- kaiten_cli/runtime/support/documents.py +175 -0
- kaiten_cli/runtime/support/files.py +561 -0
- kaiten_cli/runtime/support/markdown_export.py +525 -0
- kaiten_cli/runtime/support/pagination.py +167 -0
- kaiten_cli/runtime/support/projects.py +66 -0
- kaiten_cli/runtime/support/relations.py +86 -0
- kaiten_cli/runtime/support/restrictions.py +62 -0
- kaiten_cli/runtime/support/spaces.py +20 -0
- kaiten_cli/runtime/support/time_logs.py +61 -0
- kaiten_cli/runtime/support/tree.py +140 -0
- kaiten_cli/runtime/support/tree_sharing.py +447 -0
- kaiten_cli/runtime/trace.py +500 -0
- kaiten_cli/runtime/transforms.py +128 -0
- kaiten_cli/schema_docs.py +22 -0
- kaiten_cli/update_check.py +519 -0
- kaiten_cli-0.3.3.dist-info/METADATA +1110 -0
- kaiten_cli-0.3.3.dist-info/RECORD +99 -0
- kaiten_cli-0.3.3.dist-info/WHEEL +4 -0
- kaiten_cli-0.3.3.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
"""Remote data collection and lifecycle executors for local snapshots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from kaiten_cli.errors import ConfigError, ValidationError
|
|
9
|
+
from kaiten_cli.i18n import tr
|
|
10
|
+
from kaiten_cli.models import CACHE_POLICY_PERSISTENT_HEAVY
|
|
11
|
+
from kaiten_cli.runtime.snapshot_common import (
|
|
12
|
+
SNAPSHOT_PRESETS,
|
|
13
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
14
|
+
WINDOW_PRESETS,
|
|
15
|
+
_now_iso,
|
|
16
|
+
_parse_timestamp,
|
|
17
|
+
_sorted_history,
|
|
18
|
+
_stats_delta,
|
|
19
|
+
_stats_snapshot,
|
|
20
|
+
)
|
|
21
|
+
from kaiten_cli.runtime.snapshot_store import SnapshotStore
|
|
22
|
+
from kaiten_cli.runtime.support.audit import (
|
|
23
|
+
DEFAULT_HISTORY_WORKERS,
|
|
24
|
+
fetch_all_space_activity,
|
|
25
|
+
fetch_card_location_histories,
|
|
26
|
+
)
|
|
27
|
+
from kaiten_cli.runtime.support.cards import fetch_all_cards, fetch_cards_batch_get
|
|
28
|
+
from kaiten_cli.runtime.support.relations import fetch_card_children_batch, fetch_comments_batch
|
|
29
|
+
from kaiten_cli.runtime.support.spaces import fetch_space_topology
|
|
30
|
+
from kaiten_cli.runtime.support.time_logs import fetch_time_logs_batch
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _validate_snapshot_build_payload(payload: dict[str, Any]) -> None:
|
|
34
|
+
preset = payload.get("preset", "basic")
|
|
35
|
+
if preset not in SNAPSHOT_PRESETS:
|
|
36
|
+
allowed = ", ".join(sorted(SNAPSHOT_PRESETS))
|
|
37
|
+
raise ValidationError(tr("Field preset must be one of: {value_0}.", value_0=allowed))
|
|
38
|
+
if preset in WINDOW_PRESETS:
|
|
39
|
+
if not payload.get("window_start") or not payload.get("window_end"):
|
|
40
|
+
raise ValidationError(
|
|
41
|
+
tr(
|
|
42
|
+
"Fields window_start and window_end are required for analytics and full snapshots."
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
start = _parse_timestamp(payload.get("window_start"))
|
|
46
|
+
end = _parse_timestamp(payload.get("window_end"))
|
|
47
|
+
if start is not None and end is not None and start > end:
|
|
48
|
+
raise ValidationError(tr("window_start must be <= window_end."))
|
|
49
|
+
board_ids = payload.get("board_ids")
|
|
50
|
+
if board_ids is not None and (not isinstance(board_ids, list) or not board_ids):
|
|
51
|
+
raise ValidationError(tr("Field board_ids must be a non-empty array when provided."))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def validate_snapshot_build(tool, payload: dict[str, Any]) -> None:
|
|
55
|
+
_validate_snapshot_build_payload(payload)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _board_ids_for_snapshot(
|
|
59
|
+
topology: dict[str, Any], requested_board_ids: list[int] | None
|
|
60
|
+
) -> list[int]:
|
|
61
|
+
boards = []
|
|
62
|
+
for board in topology.get("boards", []):
|
|
63
|
+
if not isinstance(board, dict) or "id" not in board:
|
|
64
|
+
continue
|
|
65
|
+
if requested_board_ids and board["id"] not in requested_board_ids:
|
|
66
|
+
continue
|
|
67
|
+
boards.append(board)
|
|
68
|
+
topology["boards"] = boards
|
|
69
|
+
return [int(board["id"]) for board in boards]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _dedupe_cards(cards: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
73
|
+
by_id: dict[int, dict[str, Any]] = {}
|
|
74
|
+
for card in cards:
|
|
75
|
+
if not isinstance(card, dict) or "id" not in card:
|
|
76
|
+
continue
|
|
77
|
+
by_id[int(card["id"])] = card
|
|
78
|
+
return [by_id[card_id] for card_id in sorted(by_id)]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def _measure_stage(client, reporter, name: str, callback):
|
|
82
|
+
stats = client.execution_context.stats
|
|
83
|
+
before = _stats_snapshot(stats)
|
|
84
|
+
started = time.perf_counter()
|
|
85
|
+
data = await callback()
|
|
86
|
+
after = _stats_snapshot(stats)
|
|
87
|
+
delta = _stats_delta(after, before)
|
|
88
|
+
stage = {
|
|
89
|
+
"name": name,
|
|
90
|
+
"duration_ms": round((time.perf_counter() - started) * 1000.0, 2),
|
|
91
|
+
"http_request_count": delta["http_request_count"],
|
|
92
|
+
"http_response_count": delta["http_response_count"],
|
|
93
|
+
"http_error_count": delta["http_error_count"],
|
|
94
|
+
"api_wait_ms": round(delta["api_wait_ms"], 2),
|
|
95
|
+
"http_wait_ms": round(delta["http_wait_ms"], 2),
|
|
96
|
+
"retry_count": delta["retry_count"],
|
|
97
|
+
"cache_hits": {
|
|
98
|
+
"request": delta["request_cache_hits"],
|
|
99
|
+
"inflight_dedup": delta["inflight_dedup_hits"],
|
|
100
|
+
"disk": delta["disk_cache_hits"],
|
|
101
|
+
},
|
|
102
|
+
"cache_misses": {
|
|
103
|
+
"request": delta["request_cache_misses"],
|
|
104
|
+
"disk": delta["disk_cache_misses"] + delta["disk_cache_expired"],
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
if reporter is not None:
|
|
108
|
+
reporter(
|
|
109
|
+
tr(
|
|
110
|
+
"snapshot-stage: name={value_0} duration_ms={value_1:.2f} http_requests={value_2}",
|
|
111
|
+
value_0=name,
|
|
112
|
+
value_1=stage["duration_ms"],
|
|
113
|
+
value_2=stage["http_request_count"],
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
return data, stage
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def _fetch_snapshot_cards(
|
|
120
|
+
client, space_id: int, board_ids: list[int], *, timeout: float
|
|
121
|
+
) -> list[dict[str, Any]]:
|
|
122
|
+
args = {"relations": "none"}
|
|
123
|
+
if board_ids:
|
|
124
|
+
cards: list[dict[str, Any]] = []
|
|
125
|
+
for board_id in board_ids:
|
|
126
|
+
result = await fetch_all_cards(client, {**args, "board_id": board_id}, timeout=timeout)
|
|
127
|
+
cards.extend(item for item in result if isinstance(item, dict))
|
|
128
|
+
return _dedupe_cards(cards)
|
|
129
|
+
result = await fetch_all_cards(client, {**args, "space_id": space_id}, timeout=timeout)
|
|
130
|
+
return [item for item in result if isinstance(item, dict)]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _children_map(result: dict[str, Any]) -> tuple[dict[int, list[dict[str, Any]]], int]:
|
|
134
|
+
mapping: dict[int, list[dict[str, Any]]] = {}
|
|
135
|
+
for item in result.get("items", []):
|
|
136
|
+
if isinstance(item, dict) and "card_id" in item:
|
|
137
|
+
mapping[int(item["card_id"])] = [
|
|
138
|
+
row for row in item.get("children", []) if isinstance(row, dict)
|
|
139
|
+
]
|
|
140
|
+
return mapping, len(result.get("errors", []))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _comments_map(result: dict[str, Any]) -> tuple[dict[int, list[dict[str, Any]]], int]:
|
|
144
|
+
mapping: dict[int, list[dict[str, Any]]] = {}
|
|
145
|
+
for item in result.get("items", []):
|
|
146
|
+
if isinstance(item, dict) and "card_id" in item:
|
|
147
|
+
mapping[int(item["card_id"])] = [
|
|
148
|
+
row for row in item.get("comments", []) if isinstance(row, dict)
|
|
149
|
+
]
|
|
150
|
+
return mapping, len(result.get("errors", []))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _history_map(result: dict[str, Any]) -> tuple[dict[int, list[dict[str, Any]]], int]:
|
|
154
|
+
mapping: dict[int, list[dict[str, Any]]] = {}
|
|
155
|
+
for item in result.get("items", []):
|
|
156
|
+
if isinstance(item, dict) and "card_id" in item:
|
|
157
|
+
mapping[int(item["card_id"])] = _sorted_history(item.get("history", []))
|
|
158
|
+
return mapping, len(result.get("errors", []))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _cards_map(result: dict[str, Any]) -> tuple[dict[int, dict[str, Any]], int]:
|
|
162
|
+
mapping: dict[int, dict[str, Any]] = {}
|
|
163
|
+
for item in result.get("items", []):
|
|
164
|
+
if isinstance(item, dict) and "card_id" in item and isinstance(item.get("card"), dict):
|
|
165
|
+
mapping[int(item["card_id"])] = item["card"]
|
|
166
|
+
return mapping, len(result.get("errors", []))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _time_logs_map(result: dict[str, Any]) -> tuple[dict[int, list[dict[str, Any]]], int]:
|
|
170
|
+
mapping: dict[int, list[dict[str, Any]]] = {}
|
|
171
|
+
for item in result.get("items", []):
|
|
172
|
+
if isinstance(item, dict) and "card_id" in item:
|
|
173
|
+
mapping[int(item["card_id"])] = [
|
|
174
|
+
row for row in item.get("time_logs", []) if isinstance(row, dict)
|
|
175
|
+
]
|
|
176
|
+
return mapping, len(result.get("errors", []))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _merge_card_details(
|
|
180
|
+
cards: list[dict[str, Any]], details_map: dict[int, dict[str, Any]]
|
|
181
|
+
) -> list[dict[str, Any]]:
|
|
182
|
+
merged: list[dict[str, Any]] = []
|
|
183
|
+
for card in cards:
|
|
184
|
+
if not isinstance(card, dict) or "id" not in card:
|
|
185
|
+
continue
|
|
186
|
+
card_id = int(card["id"])
|
|
187
|
+
detail = details_map.get(card_id)
|
|
188
|
+
if detail:
|
|
189
|
+
payload = dict(card)
|
|
190
|
+
payload.update(detail)
|
|
191
|
+
merged.append(payload)
|
|
192
|
+
else:
|
|
193
|
+
merged.append(card)
|
|
194
|
+
return merged
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
async def _build_snapshot(
|
|
198
|
+
*,
|
|
199
|
+
client,
|
|
200
|
+
payload: dict[str, Any],
|
|
201
|
+
reporter,
|
|
202
|
+
timeout: float,
|
|
203
|
+
spec: dict[str, Any],
|
|
204
|
+
) -> dict[str, Any]:
|
|
205
|
+
store = SnapshotStore(reporter=reporter)
|
|
206
|
+
store.ensure_writable()
|
|
207
|
+
client.cache_policy = CACHE_POLICY_PERSISTENT_HEAVY
|
|
208
|
+
requested_board_ids = payload.get("board_ids")
|
|
209
|
+
topology, topology_stage = await _measure_stage(
|
|
210
|
+
client,
|
|
211
|
+
reporter,
|
|
212
|
+
"topology",
|
|
213
|
+
lambda: fetch_space_topology(client, int(spec["space_id"]), timeout=timeout),
|
|
214
|
+
)
|
|
215
|
+
board_ids = _board_ids_for_snapshot(topology, requested_board_ids)
|
|
216
|
+
cards, cards_stage = await _measure_stage(
|
|
217
|
+
client,
|
|
218
|
+
reporter,
|
|
219
|
+
"cards",
|
|
220
|
+
lambda: _fetch_snapshot_cards(client, int(spec["space_id"]), board_ids, timeout=timeout),
|
|
221
|
+
)
|
|
222
|
+
card_ids = [int(card["id"]) for card in cards if isinstance(card, dict) and "id" in card]
|
|
223
|
+
|
|
224
|
+
activity_rows: list[dict[str, Any]] = []
|
|
225
|
+
history_map: dict[int, list[dict[str, Any]]] = {}
|
|
226
|
+
children_map: dict[int, list[dict[str, Any]]] = {}
|
|
227
|
+
comments_map: dict[int, list[dict[str, Any]]] = {}
|
|
228
|
+
time_logs_map: dict[int, list[dict[str, Any]]] = {}
|
|
229
|
+
dataset_errors = {
|
|
230
|
+
"card_detail_errors": 0,
|
|
231
|
+
"history_errors": 0,
|
|
232
|
+
"time_log_errors": 0,
|
|
233
|
+
"relation_errors": 0,
|
|
234
|
+
"comment_errors": 0,
|
|
235
|
+
}
|
|
236
|
+
stages = [topology_stage, cards_stage]
|
|
237
|
+
|
|
238
|
+
if spec["preset"] in {"evidence", "full"} and card_ids:
|
|
239
|
+
card_details_result, card_details_stage = await _measure_stage(
|
|
240
|
+
client,
|
|
241
|
+
reporter,
|
|
242
|
+
"card-details",
|
|
243
|
+
lambda: fetch_cards_batch_get(
|
|
244
|
+
domain=client.domain,
|
|
245
|
+
token=client.token,
|
|
246
|
+
card_ids=card_ids,
|
|
247
|
+
workers=DEFAULT_HISTORY_WORKERS,
|
|
248
|
+
compact=False,
|
|
249
|
+
fields=None,
|
|
250
|
+
timeout=timeout,
|
|
251
|
+
reporter=reporter,
|
|
252
|
+
execution_context=client.execution_context,
|
|
253
|
+
cache_policy=CACHE_POLICY_PERSISTENT_HEAVY,
|
|
254
|
+
),
|
|
255
|
+
)
|
|
256
|
+
card_details_map, dataset_errors["card_detail_errors"] = _cards_map(card_details_result)
|
|
257
|
+
cards = _merge_card_details(cards, card_details_map)
|
|
258
|
+
card_details_stage["rows"] = len(card_details_map)
|
|
259
|
+
card_details_stage["errors"] = dataset_errors["card_detail_errors"]
|
|
260
|
+
stages.append(card_details_stage)
|
|
261
|
+
|
|
262
|
+
if spec["preset"] in {"analytics", "full"}:
|
|
263
|
+
activity_rows, activity_stage = await _measure_stage(
|
|
264
|
+
client,
|
|
265
|
+
reporter,
|
|
266
|
+
"activity",
|
|
267
|
+
lambda: fetch_all_space_activity(
|
|
268
|
+
client,
|
|
269
|
+
{
|
|
270
|
+
"space_id": int(spec["space_id"]),
|
|
271
|
+
"created_after": spec.get("window_start"),
|
|
272
|
+
"created_before": spec.get("window_end"),
|
|
273
|
+
},
|
|
274
|
+
timeout=timeout,
|
|
275
|
+
),
|
|
276
|
+
)
|
|
277
|
+
history_result, history_stage = await _measure_stage(
|
|
278
|
+
client,
|
|
279
|
+
reporter,
|
|
280
|
+
"history",
|
|
281
|
+
lambda: fetch_card_location_histories(
|
|
282
|
+
domain=client.domain,
|
|
283
|
+
token=client.token,
|
|
284
|
+
card_ids=card_ids,
|
|
285
|
+
workers=DEFAULT_HISTORY_WORKERS,
|
|
286
|
+
fields=None,
|
|
287
|
+
timeout=timeout,
|
|
288
|
+
reporter=reporter,
|
|
289
|
+
execution_context=client.execution_context,
|
|
290
|
+
cache_policy=CACHE_POLICY_PERSISTENT_HEAVY,
|
|
291
|
+
),
|
|
292
|
+
)
|
|
293
|
+
history_map, dataset_errors["history_errors"] = _history_map(history_result)
|
|
294
|
+
history_stage["rows"] = sum(len(rows) for rows in history_map.values())
|
|
295
|
+
history_stage["errors"] = dataset_errors["history_errors"]
|
|
296
|
+
time_logs_result, time_logs_stage = await _measure_stage(
|
|
297
|
+
client,
|
|
298
|
+
reporter,
|
|
299
|
+
"time-logs",
|
|
300
|
+
lambda: fetch_time_logs_batch(
|
|
301
|
+
domain=client.domain,
|
|
302
|
+
token=client.token,
|
|
303
|
+
card_ids=card_ids,
|
|
304
|
+
workers=DEFAULT_HISTORY_WORKERS,
|
|
305
|
+
compact=False,
|
|
306
|
+
fields=None,
|
|
307
|
+
timeout=timeout,
|
|
308
|
+
reporter=reporter,
|
|
309
|
+
execution_context=client.execution_context,
|
|
310
|
+
cache_policy=CACHE_POLICY_PERSISTENT_HEAVY,
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
time_logs_map, dataset_errors["time_log_errors"] = _time_logs_map(time_logs_result)
|
|
314
|
+
time_logs_stage["rows"] = sum(len(rows) for rows in time_logs_map.values())
|
|
315
|
+
time_logs_stage["errors"] = dataset_errors["time_log_errors"]
|
|
316
|
+
stages.extend([activity_stage, history_stage, time_logs_stage])
|
|
317
|
+
|
|
318
|
+
if spec["preset"] in {"evidence", "full"} and card_ids:
|
|
319
|
+
relation_result, relations_stage = await _measure_stage(
|
|
320
|
+
client,
|
|
321
|
+
reporter,
|
|
322
|
+
"relations",
|
|
323
|
+
lambda: fetch_card_children_batch(
|
|
324
|
+
domain=client.domain,
|
|
325
|
+
token=client.token,
|
|
326
|
+
card_ids=card_ids,
|
|
327
|
+
workers=DEFAULT_HISTORY_WORKERS,
|
|
328
|
+
compact=False,
|
|
329
|
+
fields=None,
|
|
330
|
+
timeout=timeout,
|
|
331
|
+
reporter=reporter,
|
|
332
|
+
execution_context=client.execution_context,
|
|
333
|
+
cache_policy=CACHE_POLICY_PERSISTENT_HEAVY,
|
|
334
|
+
),
|
|
335
|
+
)
|
|
336
|
+
children_map, dataset_errors["relation_errors"] = _children_map(relation_result)
|
|
337
|
+
relations_stage["rows"] = sum(len(rows) for rows in children_map.values())
|
|
338
|
+
relations_stage["errors"] = dataset_errors["relation_errors"]
|
|
339
|
+
comments_result, comments_stage = await _measure_stage(
|
|
340
|
+
client,
|
|
341
|
+
reporter,
|
|
342
|
+
"comments",
|
|
343
|
+
lambda: fetch_comments_batch(
|
|
344
|
+
domain=client.domain,
|
|
345
|
+
token=client.token,
|
|
346
|
+
card_ids=card_ids,
|
|
347
|
+
workers=DEFAULT_HISTORY_WORKERS,
|
|
348
|
+
compact=False,
|
|
349
|
+
fields=None,
|
|
350
|
+
timeout=timeout,
|
|
351
|
+
reporter=reporter,
|
|
352
|
+
execution_context=client.execution_context,
|
|
353
|
+
cache_policy=CACHE_POLICY_PERSISTENT_HEAVY,
|
|
354
|
+
),
|
|
355
|
+
)
|
|
356
|
+
comments_map, dataset_errors["comment_errors"] = _comments_map(comments_result)
|
|
357
|
+
comments_stage["rows"] = sum(len(rows) for rows in comments_map.values())
|
|
358
|
+
comments_stage["errors"] = dataset_errors["comment_errors"]
|
|
359
|
+
stages.extend([relations_stage, comments_stage])
|
|
360
|
+
|
|
361
|
+
dataset_counts = {
|
|
362
|
+
"boards": len(topology.get("boards", [])),
|
|
363
|
+
"columns": sum(
|
|
364
|
+
len(board.get("columns", []))
|
|
365
|
+
for board in topology.get("boards", [])
|
|
366
|
+
if isinstance(board, dict)
|
|
367
|
+
),
|
|
368
|
+
"lanes": sum(
|
|
369
|
+
len(board.get("lanes", []))
|
|
370
|
+
for board in topology.get("boards", [])
|
|
371
|
+
if isinstance(board, dict)
|
|
372
|
+
),
|
|
373
|
+
"cards": len(cards),
|
|
374
|
+
"activity_rows": len(activity_rows),
|
|
375
|
+
"history_cards": len(history_map),
|
|
376
|
+
"history_rows": sum(len(rows) for rows in history_map.values()),
|
|
377
|
+
"time_log_cards": len(time_logs_map),
|
|
378
|
+
"time_logs": sum(len(rows) for rows in time_logs_map.values()),
|
|
379
|
+
"child_relations": sum(len(rows) for rows in children_map.values()),
|
|
380
|
+
"comments": sum(len(rows) for rows in comments_map.values()),
|
|
381
|
+
**dataset_errors,
|
|
382
|
+
}
|
|
383
|
+
build_trace = {
|
|
384
|
+
"total_http_request_count": client.execution_context.stats.http_request_count,
|
|
385
|
+
"total_retry_count": client.execution_context.stats.retry_count,
|
|
386
|
+
"stages": stages,
|
|
387
|
+
}
|
|
388
|
+
store.replace_snapshot(
|
|
389
|
+
name=spec["name"],
|
|
390
|
+
profile_name=client.execution_context.profile.name,
|
|
391
|
+
domain=client.domain,
|
|
392
|
+
space_id=int(spec["space_id"]),
|
|
393
|
+
board_ids=board_ids,
|
|
394
|
+
preset=spec["preset"],
|
|
395
|
+
window_start=spec.get("window_start"),
|
|
396
|
+
window_end=spec.get("window_end"),
|
|
397
|
+
spec=spec,
|
|
398
|
+
dataset_counts=dataset_counts,
|
|
399
|
+
build_trace=build_trace,
|
|
400
|
+
topology=topology,
|
|
401
|
+
cards=cards,
|
|
402
|
+
history_map=history_map,
|
|
403
|
+
children_map=children_map,
|
|
404
|
+
comments_map=comments_map,
|
|
405
|
+
time_logs_map=time_logs_map,
|
|
406
|
+
activity_rows=activity_rows,
|
|
407
|
+
)
|
|
408
|
+
return {
|
|
409
|
+
"name": spec["name"],
|
|
410
|
+
"profile_name": client.execution_context.profile.name,
|
|
411
|
+
"domain": client.domain,
|
|
412
|
+
"space_id": int(spec["space_id"]),
|
|
413
|
+
"board_ids": board_ids,
|
|
414
|
+
"preset": spec["preset"],
|
|
415
|
+
"window": {"start": spec.get("window_start"), "end": spec.get("window_end")},
|
|
416
|
+
"schema_version": SNAPSHOT_SCHEMA_VERSION,
|
|
417
|
+
"built_at": _now_iso(),
|
|
418
|
+
"datasets": dataset_counts,
|
|
419
|
+
"meta": {"trace": build_trace},
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
async def execute_snapshot_build(
|
|
424
|
+
client,
|
|
425
|
+
tool,
|
|
426
|
+
payload: dict[str, Any],
|
|
427
|
+
path: str,
|
|
428
|
+
query: dict[str, Any] | None,
|
|
429
|
+
body: dict[str, Any] | None,
|
|
430
|
+
timeout: float,
|
|
431
|
+
reporter,
|
|
432
|
+
) -> Any:
|
|
433
|
+
if client is None or client.execution_context is None:
|
|
434
|
+
raise ConfigError(tr("snapshot.build requires an active Kaiten profile."))
|
|
435
|
+
spec = {
|
|
436
|
+
"name": payload["name"],
|
|
437
|
+
"space_id": int(payload["space_id"]),
|
|
438
|
+
"board_ids": payload.get("board_ids"),
|
|
439
|
+
"preset": payload.get("preset", "basic"),
|
|
440
|
+
"window_start": payload.get("window_start"),
|
|
441
|
+
"window_end": payload.get("window_end"),
|
|
442
|
+
}
|
|
443
|
+
_validate_snapshot_build_payload(spec)
|
|
444
|
+
if reporter is not None:
|
|
445
|
+
reporter(
|
|
446
|
+
tr(
|
|
447
|
+
"execution: local snapshot build name={value_0} preset={value_1} space_id={value_2}",
|
|
448
|
+
value_0=spec["name"],
|
|
449
|
+
value_1=spec["preset"],
|
|
450
|
+
value_2=spec["space_id"],
|
|
451
|
+
)
|
|
452
|
+
)
|
|
453
|
+
return await _build_snapshot(
|
|
454
|
+
client=client, payload=payload, reporter=reporter, timeout=timeout, spec=spec
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
async def execute_snapshot_refresh(
|
|
459
|
+
client,
|
|
460
|
+
tool,
|
|
461
|
+
payload: dict[str, Any],
|
|
462
|
+
path: str,
|
|
463
|
+
query: dict[str, Any] | None,
|
|
464
|
+
body: dict[str, Any] | None,
|
|
465
|
+
timeout: float,
|
|
466
|
+
reporter,
|
|
467
|
+
) -> Any:
|
|
468
|
+
if client is None or client.execution_context is None:
|
|
469
|
+
raise ConfigError(tr("snapshot.refresh requires an active Kaiten profile."))
|
|
470
|
+
store = SnapshotStore(reporter=reporter)
|
|
471
|
+
store.ensure_writable()
|
|
472
|
+
existing = store.get_snapshot(payload["name"])
|
|
473
|
+
if existing["domain"] and existing["domain"] != client.domain:
|
|
474
|
+
raise ConfigError(
|
|
475
|
+
tr(
|
|
476
|
+
"Snapshot {value_0} was built for domain {value_1}, current profile uses {value_2}.",
|
|
477
|
+
value_0=payload["name"],
|
|
478
|
+
value_1=existing["domain"],
|
|
479
|
+
value_2=client.domain,
|
|
480
|
+
)
|
|
481
|
+
)
|
|
482
|
+
spec = dict(existing["spec"])
|
|
483
|
+
if reporter is not None:
|
|
484
|
+
reporter(tr("execution: local snapshot refresh name={value_0}", value_0=spec["name"]))
|
|
485
|
+
await client.execution_context.clear_cache_scope(reason="snapshot-refresh")
|
|
486
|
+
return await _build_snapshot(
|
|
487
|
+
client=client, payload=payload, reporter=reporter, timeout=timeout, spec=spec
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
async def execute_snapshot_list(
|
|
492
|
+
client,
|
|
493
|
+
tool,
|
|
494
|
+
payload: dict[str, Any],
|
|
495
|
+
path: str,
|
|
496
|
+
query: dict[str, Any] | None,
|
|
497
|
+
body: dict[str, Any] | None,
|
|
498
|
+
timeout: float,
|
|
499
|
+
reporter,
|
|
500
|
+
) -> Any:
|
|
501
|
+
return {"items": SnapshotStore(reporter=reporter).list_snapshots()}
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
async def execute_snapshot_show(
|
|
505
|
+
client,
|
|
506
|
+
tool,
|
|
507
|
+
payload: dict[str, Any],
|
|
508
|
+
path: str,
|
|
509
|
+
query: dict[str, Any] | None,
|
|
510
|
+
body: dict[str, Any] | None,
|
|
511
|
+
timeout: float,
|
|
512
|
+
reporter,
|
|
513
|
+
) -> Any:
|
|
514
|
+
return SnapshotStore(reporter=reporter).get_snapshot(payload["name"])
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
async def execute_snapshot_delete(
|
|
518
|
+
client,
|
|
519
|
+
tool,
|
|
520
|
+
payload: dict[str, Any],
|
|
521
|
+
path: str,
|
|
522
|
+
query: dict[str, Any] | None,
|
|
523
|
+
body: dict[str, Any] | None,
|
|
524
|
+
timeout: float,
|
|
525
|
+
reporter,
|
|
526
|
+
) -> Any:
|
|
527
|
+
return SnapshotStore(reporter=reporter).delete_snapshot(payload["name"])
|