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.
Files changed (99) hide show
  1. kaiten_cli/__init__.py +5 -0
  2. kaiten_cli/__main__.py +9 -0
  3. kaiten_cli/agent_gateway.py +577 -0
  4. kaiten_cli/app.py +1636 -0
  5. kaiten_cli/completion.py +570 -0
  6. kaiten_cli/discovery.py +39 -0
  7. kaiten_cli/errors.py +79 -0
  8. kaiten_cli/i18n.py +92 -0
  9. kaiten_cli/locales/ru.json +2967 -0
  10. kaiten_cli/localized_click.py +160 -0
  11. kaiten_cli/models.py +202 -0
  12. kaiten_cli/profiles.py +316 -0
  13. kaiten_cli/registry/__init__.py +260 -0
  14. kaiten_cli/registry/addons.py +392 -0
  15. kaiten_cli/registry/audit_and_analytics.py +451 -0
  16. kaiten_cli/registry/automation_schema.py +44 -0
  17. kaiten_cli/registry/automation_schemas.json +7417 -0
  18. kaiten_cli/registry/automations.py +342 -0
  19. kaiten_cli/registry/base.py +62 -0
  20. kaiten_cli/registry/blockers.py +320 -0
  21. kaiten_cli/registry/boards.py +520 -0
  22. kaiten_cli/registry/card_relations.py +376 -0
  23. kaiten_cli/registry/card_types.py +363 -0
  24. kaiten_cli/registry/cards.py +1329 -0
  25. kaiten_cli/registry/charts.py +539 -0
  26. kaiten_cli/registry/checklists.py +600 -0
  27. kaiten_cli/registry/columns.py +558 -0
  28. kaiten_cli/registry/comments.py +224 -0
  29. kaiten_cli/registry/custom_directories.py +980 -0
  30. kaiten_cli/registry/custom_properties.py +1460 -0
  31. kaiten_cli/registry/dashboards.py +564 -0
  32. kaiten_cli/registry/documents.py +761 -0
  33. kaiten_cli/registry/external_links.py +128 -0
  34. kaiten_cli/registry/files.py +738 -0
  35. kaiten_cli/registry/github_addon.py +613 -0
  36. kaiten_cli/registry/iterations.py +397 -0
  37. kaiten_cli/registry/lanes.py +205 -0
  38. kaiten_cli/registry/live_contracts.py +361 -0
  39. kaiten_cli/registry/members.py +336 -0
  40. kaiten_cli/registry/module_docs.py +108 -0
  41. kaiten_cli/registry/projects.py +369 -0
  42. kaiten_cli/registry/query.py +129 -0
  43. kaiten_cli/registry/restrictions.py +249 -0
  44. kaiten_cli/registry/roles_and_groups.py +1102 -0
  45. kaiten_cli/registry/scim.py +263 -0
  46. kaiten_cli/registry/service_desk.py +1270 -0
  47. kaiten_cli/registry/snapshot.py +187 -0
  48. kaiten_cli/registry/spaces.py +272 -0
  49. kaiten_cli/registry/subscribers.py +168 -0
  50. kaiten_cli/registry/tags.py +193 -0
  51. kaiten_cli/registry/time_logs.py +353 -0
  52. kaiten_cli/registry/tree.py +352 -0
  53. kaiten_cli/registry/utilities.py +287 -0
  54. kaiten_cli/registry/webhooks.py +265 -0
  55. kaiten_cli/runtime/__init__.py +1 -0
  56. kaiten_cli/runtime/automation_validation.py +115 -0
  57. kaiten_cli/runtime/behaviors.py +787 -0
  58. kaiten_cli/runtime/cache.py +733 -0
  59. kaiten_cli/runtime/client.py +387 -0
  60. kaiten_cli/runtime/endpoints.py +59 -0
  61. kaiten_cli/runtime/executor.py +269 -0
  62. kaiten_cli/runtime/fs_security.py +168 -0
  63. kaiten_cli/runtime/input.py +310 -0
  64. kaiten_cli/runtime/output.py +59 -0
  65. kaiten_cli/runtime/public_validation.py +35 -0
  66. kaiten_cli/runtime/snapshot_build.py +527 -0
  67. kaiten_cli/runtime/snapshot_common.py +363 -0
  68. kaiten_cli/runtime/snapshot_query.py +306 -0
  69. kaiten_cli/runtime/snapshot_store.py +1192 -0
  70. kaiten_cli/runtime/snapshots.py +62 -0
  71. kaiten_cli/runtime/sqlite_errors.py +23 -0
  72. kaiten_cli/runtime/support/__init__.py +1 -0
  73. kaiten_cli/runtime/support/addons.py +953 -0
  74. kaiten_cli/runtime/support/audit.py +70 -0
  75. kaiten_cli/runtime/support/batch.py +234 -0
  76. kaiten_cli/runtime/support/card_move_url.py +213 -0
  77. kaiten_cli/runtime/support/cards.py +174 -0
  78. kaiten_cli/runtime/support/checklists.py +21 -0
  79. kaiten_cli/runtime/support/company_users.py +117 -0
  80. kaiten_cli/runtime/support/dashboards.py +35 -0
  81. kaiten_cli/runtime/support/documents.py +175 -0
  82. kaiten_cli/runtime/support/files.py +561 -0
  83. kaiten_cli/runtime/support/markdown_export.py +525 -0
  84. kaiten_cli/runtime/support/pagination.py +167 -0
  85. kaiten_cli/runtime/support/projects.py +66 -0
  86. kaiten_cli/runtime/support/relations.py +86 -0
  87. kaiten_cli/runtime/support/restrictions.py +62 -0
  88. kaiten_cli/runtime/support/spaces.py +20 -0
  89. kaiten_cli/runtime/support/time_logs.py +61 -0
  90. kaiten_cli/runtime/support/tree.py +140 -0
  91. kaiten_cli/runtime/support/tree_sharing.py +447 -0
  92. kaiten_cli/runtime/trace.py +500 -0
  93. kaiten_cli/runtime/transforms.py +128 -0
  94. kaiten_cli/schema_docs.py +22 -0
  95. kaiten_cli/update_check.py +519 -0
  96. kaiten_cli-0.3.3.dist-info/METADATA +1110 -0
  97. kaiten_cli-0.3.3.dist-info/RECORD +99 -0
  98. kaiten_cli-0.3.3.dist-info/WHEEL +4 -0
  99. kaiten_cli-0.3.3.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,787 @@
1
+ """Declarative runtime behavior for tools with non-trivial execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from kaiten_cli.errors import BatchExecutionError, ValidationError
9
+ from kaiten_cli.i18n import tr
10
+ from kaiten_cli.runtime.support.audit import (
11
+ DEFAULT_HISTORY_WORKERS,
12
+ fetch_all_space_activity,
13
+ fetch_card_location_histories,
14
+ )
15
+ from kaiten_cli.runtime.support.batch import MAX_BATCH_WORKERS
16
+ from kaiten_cli.runtime.support.card_move_url import (
17
+ parse_card_url,
18
+ parse_target_url,
19
+ resolve_move_target,
20
+ validate_url_domain,
21
+ )
22
+ from kaiten_cli.runtime.support.cards import fetch_all_cards, fetch_cards_batch_get
23
+ from kaiten_cli.runtime.support.checklists import extract_card_checklists, extract_checklist_items
24
+ from kaiten_cli.runtime.support.documents import prepare_document_body
25
+ from kaiten_cli.runtime.support.pagination import (
26
+ API_MAX_PAGE_SIZE,
27
+ DEFAULT_COLLECTION_MAX_PAGES,
28
+ )
29
+ from kaiten_cli.runtime.support.projects import fetch_project_cards
30
+ from kaiten_cli.runtime.support.relations import fetch_card_children_batch, fetch_comments_batch
31
+ from kaiten_cli.runtime.support.restrictions import select_restriction
32
+ from kaiten_cli.runtime.support.spaces import fetch_space_topology
33
+ from kaiten_cli.runtime.support.time_logs import fetch_time_logs_batch
34
+ from kaiten_cli.runtime.support.tree import build_tree, fetch_all_entities, list_children
35
+ from kaiten_cli.runtime.transforms import compact_response, select_fields, strip_base64
36
+
37
+ Query = dict[str, Any] | None
38
+ Body = dict[str, Any] | None
39
+
40
+
41
+ def archive_card_request(
42
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
43
+ ) -> tuple[str, Query, Body]:
44
+ return path, query, {"condition": 2}
45
+
46
+
47
+ def planned_relation_add_request(
48
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
49
+ ) -> tuple[str, Query, Body]:
50
+ shaped = dict(body or {})
51
+ shaped.setdefault("type", "end-start")
52
+ return path, query, shaped
53
+
54
+
55
+ def project_title_to_name_request(
56
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
57
+ ) -> tuple[str, Query, Body]:
58
+ shaped = dict(body or {})
59
+ if "title" in shaped:
60
+ shaped["name"] = shaped.pop("title")
61
+ return path, query, shaped
62
+
63
+
64
+ def card_child_add_request(
65
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
66
+ ) -> tuple[str, Query, Body]:
67
+ shaped = dict(body or {})
68
+ return path, query, {"card_id": shaped["child_card_id"]}
69
+
70
+
71
+ def card_parent_add_request(
72
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
73
+ ) -> tuple[str, Query, Body]:
74
+ shaped = dict(body or {})
75
+ return path, query, {"card_id": shaped["parent_card_id"]}
76
+
77
+
78
+ def default_role_time_log_request(
79
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
80
+ ) -> tuple[str, Query, Body]:
81
+ shaped = dict(body or {})
82
+ shaped.setdefault("role_id", -1)
83
+ return path, query, shaped
84
+
85
+
86
+ def prepare_document_request(
87
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
88
+ ) -> tuple[str, Query, Body]:
89
+ return path, query, prepare_document_body(tool.canonical_name, dict(body or {}))
90
+
91
+
92
+ def document_list_search_request(
93
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
94
+ ) -> tuple[str, Query, Body]:
95
+ shaped_query = dict(query or {})
96
+ if "search_fields" in payload:
97
+ shaped_query["fields"] = payload["search_fields"]
98
+ return path, shaped_query or None, body
99
+
100
+
101
+ def prevent_redirect_request(
102
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
103
+ ) -> tuple[str, Query, Body]:
104
+ shaped_query = dict(query or {})
105
+ shaped_query["prevent_redirect"] = True
106
+ return path, shaped_query, body
107
+
108
+
109
+ def checklist_item_compat_request(
110
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
111
+ ) -> tuple[str, Query, Body]:
112
+ card_id = payload.get("card_id")
113
+ if card_id is None:
114
+ return checklist_relocation_request(tool, payload, path, query, body)
115
+ checklist_id = payload["checklist_id"]
116
+ compatibility_path = f"/cards/{card_id}/checklists/{checklist_id}/items"
117
+ if "item_id" in payload:
118
+ compatibility_path += f"/{payload['item_id']}"
119
+ return checklist_relocation_request(tool, payload, compatibility_path, query, body)
120
+
121
+
122
+ def checklist_relocation_request(tool, payload, path, query, body):
123
+ shaped = dict(body or {})
124
+ for source, target in (
125
+ ("target_card_id", "card_id"),
126
+ ("target_checklist_id", "checklist_id"),
127
+ ("target_space_uid", "space_uid"),
128
+ ):
129
+ if source in payload:
130
+ shaped[target] = payload[source]
131
+ return path, query, shaped or None
132
+
133
+
134
+ def column_subscriber_default_type_request(
135
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
136
+ ) -> tuple[str, Query, Body]:
137
+ shaped = dict(body or {})
138
+ shaped.setdefault("type", 1)
139
+ return path, query, shaped
140
+
141
+
142
+ def automation_copy_request(
143
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
144
+ ) -> tuple[str, Query, Body]:
145
+ shaped = dict(body or {})
146
+ if "target_space_id" in shaped:
147
+ shaped["targetSpaceId"] = shaped.pop("target_space_id")
148
+ return path, query, shaped
149
+
150
+
151
+ def restriction_copy_request(tool, payload, path, query, body):
152
+ return path, query, {"source_restriction_id": payload["restriction_id"]}
153
+
154
+
155
+ async def execute_restriction_get(client, tool, payload, path, query, body, timeout, reporter):
156
+ if reporter:
157
+ reporter(tr("execution: synthetic restriction read from the space restriction list"))
158
+ data = await client.get(path, timeout=timeout)
159
+ return select_restriction(data, payload["restriction_id"])
160
+
161
+
162
+ def archive_service_request(
163
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
164
+ ) -> tuple[str, Query, Body]:
165
+ return path, query, {"archived": True}
166
+
167
+
168
+ def board_delete_force_request(
169
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
170
+ ) -> tuple[str, Query, Body]:
171
+ if "force" not in payload:
172
+ return path, query, body
173
+ shaped_query = dict(query or {})
174
+ shaped_query["force"] = payload["force"]
175
+ return path, shaped_query, {"force": payload["force"]}
176
+
177
+
178
+ def board_get_scoped_request(
179
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
180
+ ) -> tuple[str, Query, Body]:
181
+ space_id = payload.get("space_id")
182
+ if space_id is None:
183
+ return path, query, body
184
+ return f"/spaces/{space_id}/boards/{payload['board_id']}", query, body
185
+
186
+
187
+ def board_place_existing_request(
188
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
189
+ ) -> tuple[str, Query, Body]:
190
+ shaped = dict(body or {})
191
+ shaped.setdefault("top", 0)
192
+ shaped.setdefault("left", 0)
193
+ return path, query, shaped
194
+
195
+
196
+ def saved_filter_title_request(
197
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
198
+ ) -> tuple[str, Query, Body]:
199
+ shaped = dict(body or {})
200
+ if "name" in shaped:
201
+ shaped["title"] = shaped.pop("name")
202
+ return path, query, shaped
203
+
204
+
205
+ def service_desk_stats_query_request(
206
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
207
+ ) -> tuple[str, Query, Body]:
208
+ shaped_query = dict(query or {})
209
+ if "date_from" in shaped_query:
210
+ shaped_query["date-from"] = shaped_query.pop("date_from")
211
+ if "date_to" in shaped_query:
212
+ shaped_query["date-to"] = shaped_query.pop("date_to")
213
+ return path, shaped_query, body
214
+
215
+
216
+ def company_members_section_request(
217
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
218
+ ) -> tuple[str, Query, Body]:
219
+ shaped_query = dict(query or {})
220
+ shaped_query.setdefault("for_members_section", True)
221
+ shaped_query.setdefault("offset", 0)
222
+ return path, shaped_query, body
223
+
224
+
225
+ def comment_format_request(
226
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
227
+ ) -> tuple[str, Query, Body]:
228
+ shaped = dict(body or {})
229
+ format_value = shaped.pop("format", None)
230
+ if format_value == "html":
231
+ shaped["type"] = 2
232
+ elif format_value == "markdown":
233
+ shaped["type"] = 1
234
+ return path, query, shaped
235
+
236
+
237
+ def payload_body_request(
238
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
239
+ ) -> tuple[str, Query, Body]:
240
+ shaped = dict(body or {})
241
+ payload_body = shaped.pop("payload", None)
242
+ if payload_body is not None:
243
+ if not isinstance(payload_body, dict):
244
+ raise ValidationError(tr("Field payload must be a JSON object."))
245
+ shaped.update(payload_body)
246
+ return path, query, shaped or None
247
+
248
+
249
+ def encode_object_query_request(
250
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
251
+ ) -> tuple[str, Query, Body]:
252
+ shaped_query = dict(query or {})
253
+ for key, value in tuple(shaped_query.items()):
254
+ if isinstance(value, dict):
255
+ shaped_query[key] = json.dumps(value, ensure_ascii=False)
256
+ return path, shaped_query or None, body
257
+
258
+
259
+ def reject_custom_property_include_values(tool, payload: dict[str, Any]) -> None:
260
+ if payload.get("include_values") is True:
261
+ raise ValidationError(
262
+ tr(
263
+ "Field include_values is no longer supported by Kaiten Public API. List Select options with `kaiten custom-properties select-values list` and Catalog entries with `kaiten custom-properties catalog-values list`."
264
+ )
265
+ )
266
+
267
+
268
+ def scim_query_request(
269
+ tool, payload: dict[str, Any], path: str, query: Query, body: Body
270
+ ) -> tuple[str, Query, Body]:
271
+ shaped_query = dict(query or {})
272
+ if "start_index" in shaped_query:
273
+ shaped_query["startIndex"] = shaped_query.pop("start_index")
274
+ return path, shaped_query or None, body
275
+
276
+
277
+ def validate_cards_list_all_selection(tool, payload: dict[str, Any]) -> None:
278
+ selection = payload.get("selection")
279
+ if selection is None:
280
+ return
281
+ if "archived" in payload or "condition" in payload:
282
+ raise ValidationError(tr("Field selection cannot be combined with archived or condition."))
283
+
284
+
285
+ def validate_card_id_batch(tool, payload: dict[str, Any]) -> None:
286
+ card_ids = payload.get("card_ids")
287
+ if not isinstance(card_ids, list) or not card_ids:
288
+ raise ValidationError(tr("Field card_ids must be a non-empty array."))
289
+ workers = payload.get("workers", DEFAULT_HISTORY_WORKERS)
290
+ if workers < 1 or workers > MAX_BATCH_WORKERS:
291
+ raise ValidationError(
292
+ tr("Field workers must be between 1 and {value_0}.", value_0=MAX_BATCH_WORKERS)
293
+ )
294
+
295
+
296
+ validate_history_batch_get = validate_card_id_batch
297
+ validate_cards_batch_get = validate_card_id_batch
298
+ validate_time_logs_batch_list = validate_card_id_batch
299
+
300
+
301
+ async def execute_blockers_get(
302
+ client,
303
+ tool,
304
+ payload: dict[str, Any],
305
+ path: str,
306
+ query: Query,
307
+ body: Body,
308
+ timeout: float,
309
+ reporter,
310
+ ) -> Any:
311
+ if reporter:
312
+ reporter(tr("execution: custom read by filtering the blockers list"))
313
+ blockers = await client.get(path, params=query, timeout=timeout)
314
+ if not isinstance(blockers, list):
315
+ return blockers
316
+ blocker_id = payload["blocker_id"]
317
+ return next(
318
+ (item for item in blockers if isinstance(item, dict) and item.get("id") == blocker_id),
319
+ None,
320
+ )
321
+
322
+
323
+ async def execute_tree_children_list(
324
+ client,
325
+ tool,
326
+ payload: dict[str, Any],
327
+ path: str,
328
+ query: Query,
329
+ body: Body,
330
+ timeout: float,
331
+ reporter,
332
+ ) -> Any:
333
+ if reporter:
334
+ reporter(tr("execution: aggregated read from spaces, documents, and document groups"))
335
+ entities = await fetch_all_entities(client, timeout=timeout, reporter=reporter)
336
+ return list_children(entities, payload.get("parent_entity_uid"))
337
+
338
+
339
+ async def execute_tree_get(
340
+ client,
341
+ tool,
342
+ payload: dict[str, Any],
343
+ path: str,
344
+ query: Query,
345
+ body: Body,
346
+ timeout: float,
347
+ reporter,
348
+ ) -> Any:
349
+ if reporter:
350
+ reporter(tr("execution: aggregated tree build from spaces, documents, and document groups"))
351
+ entities = await fetch_all_entities(client, timeout=timeout, reporter=reporter)
352
+ return build_tree(entities, payload.get("root_uid"), payload.get("depth", 0))
353
+
354
+
355
+ async def execute_project_cards_list(
356
+ client,
357
+ tool,
358
+ payload: dict[str, Any],
359
+ path: str,
360
+ query: Query,
361
+ body: Body,
362
+ timeout: float,
363
+ reporter,
364
+ ) -> Any:
365
+ if reporter:
366
+ reporter(tr("execution: synthetic read with direct endpoint and embedded project fallback"))
367
+ return await fetch_project_cards(
368
+ client, str(payload["project_id"]), timeout=timeout, reporter=reporter
369
+ )
370
+
371
+
372
+ async def execute_cards_list_all(
373
+ client,
374
+ tool,
375
+ payload: dict[str, Any],
376
+ path: str,
377
+ query: Query,
378
+ body: Body,
379
+ timeout: float,
380
+ reporter,
381
+ ) -> Any:
382
+ if reporter:
383
+ reporter(tr("execution: aggregated bounded pagination over /cards"))
384
+ return await fetch_all_cards(client, payload, timeout=timeout)
385
+
386
+
387
+ def _shape_move_by_url_card(card: Any, payload: dict[str, Any]) -> Any:
388
+ shaped = compact_response(card, bool(payload.get("compact", False)))
389
+ shaped = select_fields(shaped, payload.get("fields"))
390
+ shaped, _ = strip_base64(shaped)
391
+ return shaped
392
+
393
+
394
+ def _verify_moved_card(card: Any, target: dict[str, Any]) -> None:
395
+ if not isinstance(card, dict):
396
+ raise ValidationError(tr("Move verification failed: final card response is not an object."))
397
+
398
+ expected = {
399
+ "board_id": target["board_id"],
400
+ "column_id": target["column_id"],
401
+ }
402
+ if target.get("lane_id") is not None:
403
+ expected["lane_id"] = target["lane_id"]
404
+
405
+ mismatches = [
406
+ tr(
407
+ "{key}: expected {expected}, got {actual}",
408
+ key=key,
409
+ expected=value,
410
+ actual=card.get(key),
411
+ )
412
+ for key, value in expected.items()
413
+ if card.get(key) != value
414
+ ]
415
+ if mismatches:
416
+ raise ValidationError(
417
+ tr("Move verification failed: {details}", details="; ".join(mismatches))
418
+ )
419
+
420
+
421
+ async def execute_cards_move_by_url(
422
+ client,
423
+ tool,
424
+ payload: dict[str, Any],
425
+ path: str,
426
+ query: Query,
427
+ body: Body,
428
+ timeout: float,
429
+ reporter,
430
+ ) -> Any:
431
+ if reporter:
432
+ reporter(tr("execution: aggregated card move by Kaiten UI URLs"))
433
+
434
+ card_url = parse_card_url(payload["card_url"])
435
+ target_url = parse_target_url(payload["target_url"])
436
+ validate_url_domain(card_url.domain, client.domain, label=tr("Card URL"))
437
+ validate_url_domain(target_url.domain, client.domain, label=tr("Target URL"))
438
+
439
+ target = await resolve_move_target(
440
+ client,
441
+ target_url,
442
+ lane_id=payload.get("lane_id"),
443
+ timeout=timeout,
444
+ )
445
+ patch_body: dict[str, Any] = {
446
+ "board_id": target["board_id"],
447
+ "column_id": target["column_id"],
448
+ }
449
+ if target.get("lane_id") is not None:
450
+ patch_body["lane_id"] = target["lane_id"]
451
+ if "sort_order" in payload:
452
+ patch_body["sort_order"] = payload["sort_order"]
453
+
454
+ if payload.get("dry_run", False):
455
+ return {
456
+ "card_ref": card_url.card_ref,
457
+ "card_space_id": card_url.space_id,
458
+ "target": target,
459
+ "dry_run": True,
460
+ "would_patch": patch_body,
461
+ }
462
+
463
+ moved_card = await client.patch(f"/cards/{card_url.card_ref}", json=patch_body, timeout=timeout)
464
+ verified = False
465
+ card = moved_card
466
+ if payload.get("verify", True):
467
+ card = await client.get(f"/cards/{card_url.card_ref}", timeout=timeout)
468
+ _verify_moved_card(card, target)
469
+ verified = True
470
+
471
+ return {
472
+ "card_ref": card_url.card_ref,
473
+ "target": target,
474
+ "card": _shape_move_by_url_card(card, payload),
475
+ "verified": verified,
476
+ }
477
+
478
+
479
+ async def execute_card_location_history_batch_get(
480
+ client,
481
+ tool,
482
+ payload: dict[str, Any],
483
+ path: str,
484
+ query: Query,
485
+ body: Body,
486
+ timeout: float,
487
+ reporter,
488
+ ) -> Any:
489
+ workers = payload.get("workers", DEFAULT_HISTORY_WORKERS)
490
+ if reporter:
491
+ reporter(
492
+ tr(
493
+ "execution: aggregated batch history fetch over /cards/{{card_id}}/location-history with workers={value_0}",
494
+ value_0=workers,
495
+ )
496
+ )
497
+ result = await fetch_card_location_histories(
498
+ domain=client.domain,
499
+ token=client.token,
500
+ card_ids=list(payload["card_ids"]),
501
+ workers=workers,
502
+ fields=payload.get("fields"),
503
+ timeout=timeout,
504
+ reporter=reporter,
505
+ execution_context=client.execution_context,
506
+ cache_policy=client.cache_policy,
507
+ )
508
+ if reporter:
509
+ meta = result["meta"]
510
+ reporter(
511
+ tr(
512
+ "batch-history: requested={value_0} unique={value_1} succeeded={value_2} failed={value_3}",
513
+ value_0=meta["requested_count"],
514
+ value_1=meta["unique_count"],
515
+ value_2=meta["succeeded"],
516
+ value_3=meta["failed"],
517
+ )
518
+ )
519
+ if result["meta"]["succeeded"] == 0:
520
+ raise BatchExecutionError(
521
+ tr("Failed to fetch location history for all requested cards."), result
522
+ )
523
+ return result
524
+
525
+
526
+ async def execute_card_children_batch_list(
527
+ client,
528
+ tool,
529
+ payload: dict[str, Any],
530
+ path: str,
531
+ query: Query,
532
+ body: Body,
533
+ timeout: float,
534
+ reporter,
535
+ ) -> Any:
536
+ workers = payload.get("workers", DEFAULT_HISTORY_WORKERS)
537
+ if reporter:
538
+ reporter(
539
+ tr(
540
+ "execution: aggregated batch relation fetch over /cards/{{card_id}}/children with workers={value_0}",
541
+ value_0=workers,
542
+ )
543
+ )
544
+ result = await fetch_card_children_batch(
545
+ domain=client.domain,
546
+ token=client.token,
547
+ card_ids=list(payload["card_ids"]),
548
+ workers=workers,
549
+ page_size=payload.get("page_size", API_MAX_PAGE_SIZE),
550
+ max_pages=payload.get("max_pages", DEFAULT_COLLECTION_MAX_PAGES),
551
+ compact=bool(payload.get("compact", False)),
552
+ fields=payload.get("fields"),
553
+ timeout=timeout,
554
+ reporter=reporter,
555
+ execution_context=client.execution_context,
556
+ cache_policy=client.cache_policy,
557
+ )
558
+ if reporter:
559
+ meta = result["meta"]
560
+ reporter(
561
+ tr(
562
+ "batch-children: requested={value_0} unique={value_1} succeeded={value_2} failed={value_3}",
563
+ value_0=meta["requested_count"],
564
+ value_1=meta["unique_count"],
565
+ value_2=meta["succeeded"],
566
+ value_3=meta["failed"],
567
+ )
568
+ )
569
+ if result["meta"]["succeeded"] == 0:
570
+ raise BatchExecutionError(
571
+ tr("Failed to fetch child cards for all requested cards."), result
572
+ )
573
+ return result
574
+
575
+
576
+ async def execute_cards_batch_get(
577
+ client,
578
+ tool,
579
+ payload: dict[str, Any],
580
+ path: str,
581
+ query: Query,
582
+ body: Body,
583
+ timeout: float,
584
+ reporter,
585
+ ) -> Any:
586
+ workers = payload.get("workers", DEFAULT_HISTORY_WORKERS)
587
+ if reporter:
588
+ reporter(
589
+ tr(
590
+ "execution: aggregated batch card read over /cards/{{card_id}} with workers={value_0}",
591
+ value_0=workers,
592
+ )
593
+ )
594
+ result = await fetch_cards_batch_get(
595
+ domain=client.domain,
596
+ token=client.token,
597
+ card_ids=list(payload["card_ids"]),
598
+ workers=workers,
599
+ compact=bool(payload.get("compact", False)),
600
+ fields=payload.get("fields"),
601
+ timeout=timeout,
602
+ reporter=reporter,
603
+ execution_context=client.execution_context,
604
+ cache_policy=client.cache_policy,
605
+ )
606
+ if reporter:
607
+ meta = result["meta"]
608
+ reporter(
609
+ tr(
610
+ "batch-cards: requested={value_0} unique={value_1} succeeded={value_2} failed={value_3}",
611
+ value_0=meta["requested_count"],
612
+ value_1=meta["unique_count"],
613
+ value_2=meta["succeeded"],
614
+ value_3=meta["failed"],
615
+ )
616
+ )
617
+ if result["meta"]["succeeded"] == 0:
618
+ raise BatchExecutionError(tr("Failed to fetch cards for all requested card IDs."), result)
619
+ return result
620
+
621
+
622
+ async def execute_checklists_list(
623
+ client,
624
+ tool,
625
+ payload: dict[str, Any],
626
+ path: str,
627
+ query: Query,
628
+ body: Body,
629
+ timeout: float,
630
+ reporter,
631
+ ) -> Any:
632
+ if reporter:
633
+ reporter(tr("execution: synthetic checklist read from /cards/{card_id} payload"))
634
+ card = await client.get(path, timeout=timeout)
635
+ return extract_card_checklists(card if isinstance(card, dict) else {})
636
+
637
+
638
+ async def execute_checklist_items_list(
639
+ client,
640
+ tool,
641
+ payload: dict[str, Any],
642
+ path: str,
643
+ query: Query,
644
+ body: Body,
645
+ timeout: float,
646
+ reporter,
647
+ ) -> Any:
648
+ if reporter:
649
+ reporter(
650
+ tr(
651
+ "execution: synthetic checklist item read from /cards/{card_id}/checklists/{checklist_id} payload"
652
+ )
653
+ )
654
+ checklist = await client.get(path, timeout=timeout)
655
+ return extract_checklist_items(checklist)
656
+
657
+
658
+ async def execute_comments_batch_list(
659
+ client,
660
+ tool,
661
+ payload: dict[str, Any],
662
+ path: str,
663
+ query: Query,
664
+ body: Body,
665
+ timeout: float,
666
+ reporter,
667
+ ) -> Any:
668
+ workers = payload.get("workers", DEFAULT_HISTORY_WORKERS)
669
+ if reporter:
670
+ reporter(
671
+ tr(
672
+ "execution: aggregated batch comment fetch over /cards/{{card_id}}/comments with workers={value_0}",
673
+ value_0=workers,
674
+ )
675
+ )
676
+ result = await fetch_comments_batch(
677
+ domain=client.domain,
678
+ token=client.token,
679
+ card_ids=list(payload["card_ids"]),
680
+ workers=workers,
681
+ page_size=payload.get("page_size", API_MAX_PAGE_SIZE),
682
+ max_pages=payload.get("max_pages", DEFAULT_COLLECTION_MAX_PAGES),
683
+ compact=bool(payload.get("compact", False)),
684
+ fields=payload.get("fields"),
685
+ timeout=timeout,
686
+ reporter=reporter,
687
+ execution_context=client.execution_context,
688
+ cache_policy=client.cache_policy,
689
+ )
690
+ if reporter:
691
+ meta = result["meta"]
692
+ reporter(
693
+ tr(
694
+ "batch-comments: requested={value_0} unique={value_1} succeeded={value_2} failed={value_3}",
695
+ value_0=meta["requested_count"],
696
+ value_1=meta["unique_count"],
697
+ value_2=meta["succeeded"],
698
+ value_3=meta["failed"],
699
+ )
700
+ )
701
+ if result["meta"]["succeeded"] == 0:
702
+ raise BatchExecutionError(tr("Failed to fetch comments for all requested cards."), result)
703
+ return result
704
+
705
+
706
+ async def execute_time_logs_batch_list(
707
+ client,
708
+ tool,
709
+ payload: dict[str, Any],
710
+ path: str,
711
+ query: Query,
712
+ body: Body,
713
+ timeout: float,
714
+ reporter,
715
+ ) -> Any:
716
+ workers = payload.get("workers", DEFAULT_HISTORY_WORKERS)
717
+ if reporter:
718
+ reporter(
719
+ tr(
720
+ "execution: aggregated batch time-log fetch over /cards/{{card_id}}/time-logs with workers={value_0}",
721
+ value_0=workers,
722
+ )
723
+ )
724
+ result = await fetch_time_logs_batch(
725
+ domain=client.domain,
726
+ token=client.token,
727
+ card_ids=list(payload["card_ids"]),
728
+ workers=workers,
729
+ page_size=payload.get("page_size", API_MAX_PAGE_SIZE),
730
+ max_pages=payload.get("max_pages", DEFAULT_COLLECTION_MAX_PAGES),
731
+ for_date=payload.get("for_date"),
732
+ personal=payload.get("personal"),
733
+ compact=bool(payload.get("compact", False)),
734
+ fields=payload.get("fields"),
735
+ timeout=timeout,
736
+ reporter=reporter,
737
+ execution_context=client.execution_context,
738
+ cache_policy=client.cache_policy,
739
+ )
740
+ if reporter:
741
+ meta = result["meta"]
742
+ reporter(
743
+ tr(
744
+ "batch-time-logs: requested={value_0} unique={value_1} succeeded={value_2} failed={value_3}",
745
+ value_0=meta["requested_count"],
746
+ value_1=meta["unique_count"],
747
+ value_2=meta["succeeded"],
748
+ value_3=meta["failed"],
749
+ )
750
+ )
751
+ if result["meta"]["succeeded"] == 0:
752
+ raise BatchExecutionError(tr("Failed to fetch time logs for all requested cards."), result)
753
+ return result
754
+
755
+
756
+ async def execute_space_activity_all(
757
+ client,
758
+ tool,
759
+ payload: dict[str, Any],
760
+ path: str,
761
+ query: Query,
762
+ body: Body,
763
+ timeout: float,
764
+ reporter,
765
+ ) -> Any:
766
+ if reporter:
767
+ reporter(tr("execution: aggregated bounded pagination over /spaces/{space_id}/activity"))
768
+ return await fetch_all_space_activity(client, payload, timeout=timeout)
769
+
770
+
771
+ async def execute_space_topology_get(
772
+ client,
773
+ tool,
774
+ payload: dict[str, Any],
775
+ path: str,
776
+ query: Query,
777
+ body: Body,
778
+ timeout: float,
779
+ reporter,
780
+ ) -> Any:
781
+ if reporter:
782
+ reporter(
783
+ tr(
784
+ "execution: aggregated topology read over /spaces/{space_id}/boards and /boards/{board_id}"
785
+ )
786
+ )
787
+ return await fetch_space_topology(client, payload["space_id"], timeout=timeout)