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,66 @@
1
+ """Project-specific execution helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from kaiten_cli.errors import ApiError
8
+ from kaiten_cli.i18n import tr
9
+ from kaiten_cli.models import DebugReporter
10
+ from kaiten_cli.runtime.client import KaitenClient
11
+
12
+
13
+ def extract_project_cards(payload: Any) -> list[Any] | None:
14
+ if not isinstance(payload, dict):
15
+ return None
16
+
17
+ direct_cards = payload.get("cards")
18
+ if isinstance(direct_cards, list):
19
+ return direct_cards
20
+
21
+ direct_cards_data = payload.get("cards_data")
22
+ if isinstance(direct_cards_data, list):
23
+ return direct_cards_data
24
+
25
+ project_payload = payload.get("project")
26
+ if isinstance(project_payload, dict):
27
+ nested_cards = project_payload.get("cards")
28
+ if isinstance(nested_cards, list):
29
+ return nested_cards
30
+ nested_cards_data = project_payload.get("cards_data")
31
+ if isinstance(nested_cards_data, list):
32
+ return nested_cards_data
33
+
34
+ return None
35
+
36
+
37
+ async def fetch_project_cards(
38
+ client: KaitenClient,
39
+ project_id: str,
40
+ *,
41
+ timeout: float,
42
+ reporter: DebugReporter | None = None,
43
+ ) -> list[Any]:
44
+ try:
45
+ result = await client.get(f"/projects/{project_id}/cards", timeout=timeout)
46
+ if isinstance(result, list):
47
+ return result
48
+ return []
49
+ except ApiError as exc:
50
+ if exc.status_code != 405:
51
+ raise
52
+ if reporter is not None:
53
+ reporter(
54
+ tr(
55
+ "fallback: GET /projects/{id}/cards returned 405, retrying with with_cards_data=true"
56
+ )
57
+ )
58
+ project = await client.get(
59
+ f"/projects/{project_id}",
60
+ params={"with_cards_data": True},
61
+ timeout=timeout,
62
+ )
63
+ cards = extract_project_cards(project)
64
+ if cards is None:
65
+ raise exc
66
+ return cards
@@ -0,0 +1,86 @@
1
+ """Support helpers for relation- and comment-heavy investigations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from kaiten_cli.runtime.support.batch import (
8
+ DEFAULT_BATCH_WORKERS,
9
+ fetch_card_collection_batch,
10
+ )
11
+ from kaiten_cli.runtime.support.pagination import (
12
+ API_MAX_PAGE_SIZE,
13
+ DEFAULT_COLLECTION_MAX_PAGES,
14
+ )
15
+ from kaiten_cli.runtime.transforms import compact_response, select_fields
16
+
17
+
18
+ def _shape_nested_items(items: list[Any], *, compact: bool, fields: str | None) -> list[Any]:
19
+ shaped = compact_response(items, compact)
20
+ return select_fields(shaped, fields)
21
+
22
+
23
+ async def fetch_card_children_batch(
24
+ *,
25
+ domain: str,
26
+ token: str,
27
+ card_ids: list[int],
28
+ workers: int = DEFAULT_BATCH_WORKERS,
29
+ page_size: int = API_MAX_PAGE_SIZE,
30
+ max_pages: int = DEFAULT_COLLECTION_MAX_PAGES,
31
+ compact: bool = False,
32
+ fields: str | None = None,
33
+ timeout: float,
34
+ reporter,
35
+ execution_context=None,
36
+ cache_policy: str = "request_scope",
37
+ ) -> dict[str, Any]:
38
+ return await fetch_card_collection_batch(
39
+ domain=domain,
40
+ token=token,
41
+ card_ids=card_ids,
42
+ workers=workers,
43
+ timeout=timeout,
44
+ reporter=reporter,
45
+ execution_context=execution_context,
46
+ cache_policy=cache_policy,
47
+ path_for_card=lambda card_id: f"/cards/{card_id}/children",
48
+ page_size=page_size,
49
+ max_pages=max_pages,
50
+ result_field="children",
51
+ transform_items=lambda items: _shape_nested_items(items, compact=compact, fields=fields),
52
+ worker_label="batch-children",
53
+ )
54
+
55
+
56
+ async def fetch_comments_batch(
57
+ *,
58
+ domain: str,
59
+ token: str,
60
+ card_ids: list[int],
61
+ workers: int = DEFAULT_BATCH_WORKERS,
62
+ page_size: int = API_MAX_PAGE_SIZE,
63
+ max_pages: int = DEFAULT_COLLECTION_MAX_PAGES,
64
+ compact: bool = False,
65
+ fields: str | None = None,
66
+ timeout: float,
67
+ reporter,
68
+ execution_context=None,
69
+ cache_policy: str = "request_scope",
70
+ ) -> dict[str, Any]:
71
+ return await fetch_card_collection_batch(
72
+ domain=domain,
73
+ token=token,
74
+ card_ids=card_ids,
75
+ workers=workers,
76
+ timeout=timeout,
77
+ reporter=reporter,
78
+ execution_context=execution_context,
79
+ cache_policy=cache_policy,
80
+ path_for_card=lambda card_id: f"/cards/{card_id}/comments",
81
+ page_size=page_size,
82
+ max_pages=max_pages,
83
+ result_field="comments",
84
+ transform_items=lambda items: _shape_nested_items(items, compact=compact, fields=fields),
85
+ worker_label="batch-comments",
86
+ )
@@ -0,0 +1,62 @@
1
+ """Validation and selection for the undocumented restrictions API extension."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+ from uuid import UUID
7
+
8
+ from kaiten_cli.errors import ApiError, TransportError, ValidationError
9
+ from kaiten_cli.i18n import tr
10
+
11
+
12
+ def validate_restriction_payload(tool, payload: dict[str, Any]) -> None:
13
+ if "restriction_id" in payload:
14
+ try:
15
+ identifier = UUID(payload["restriction_id"])
16
+ if payload["restriction_id"].lower() not in (str(identifier), identifier.hex):
17
+ raise ValueError("Unsupported UUID spelling")
18
+ except ValueError as exc:
19
+ raise ValidationError(tr("Restriction ID must be a UUID.")) from exc
20
+ if tool.canonical_name == "restrictions.update" and not any(
21
+ key in payload for key in ("name", "conditions", "restrictions", "status")
22
+ ):
23
+ raise ValidationError(
24
+ tr(
25
+ "Restriction update requires name, conditions, restrictions or status; error_text alone is not accepted."
26
+ )
27
+ )
28
+
29
+
30
+ def select_restriction(data: Any, restriction_id: str) -> dict[str, Any]:
31
+ if not isinstance(data, list):
32
+ raise TransportError(
33
+ tr("Restriction list response must be an array of objects with UUID identifiers.")
34
+ )
35
+ target_id = UUID(restriction_id)
36
+ selected = None
37
+ for item in data:
38
+ if not isinstance(item, dict) or not isinstance(item.get("id"), str):
39
+ raise TransportError(
40
+ tr("Restriction list response must be an array of objects with UUID identifiers.")
41
+ )
42
+ try:
43
+ item_id = UUID(item["id"])
44
+ except ValueError as exc:
45
+ raise TransportError(
46
+ tr("Restriction list response must be an array of objects with UUID identifiers.")
47
+ ) from exc
48
+ if (
49
+ item_id == target_id
50
+ and item.get("type") != "on_workflow"
51
+ and item.get("status") != "removed"
52
+ ):
53
+ selected = item
54
+ if selected is None:
55
+ raise ApiError(
56
+ 404,
57
+ tr(
58
+ "Restriction {restriction_id} was not found in this space.",
59
+ restriction_id=restriction_id,
60
+ ),
61
+ )
62
+ return selected
@@ -0,0 +1,20 @@
1
+ """Support helpers for space-scoped aggregated reads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ async def fetch_space_topology(client, space_id: int, *, timeout: float) -> dict[str, Any]:
9
+ boards = await client.get(f"/spaces/{space_id}/boards", timeout=timeout)
10
+ topology_boards: list[dict[str, Any]] = []
11
+ for board in boards if isinstance(boards, list) else []:
12
+ if not isinstance(board, dict) or "id" not in board:
13
+ continue
14
+ detail = await client.get(f"/boards/{board['id']}", timeout=timeout)
15
+ if not isinstance(detail, dict):
16
+ continue
17
+ detail.setdefault("columns", [])
18
+ detail.setdefault("lanes", [])
19
+ topology_boards.append(detail)
20
+ return {"space_id": space_id, "boards": topology_boards}
@@ -0,0 +1,61 @@
1
+ """Support helpers for batch time-log reads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from kaiten_cli.runtime.support.batch import DEFAULT_BATCH_WORKERS, fetch_card_collection_batch
8
+ from kaiten_cli.runtime.support.pagination import (
9
+ API_MAX_PAGE_SIZE,
10
+ DEFAULT_COLLECTION_MAX_PAGES,
11
+ )
12
+ from kaiten_cli.runtime.transforms import compact_response, select_fields
13
+
14
+
15
+ def _shape_time_log_items(items: list[Any], *, compact: bool, fields: str | None) -> list[Any]:
16
+ shaped = compact_response(items, compact)
17
+ return select_fields(shaped, fields)
18
+
19
+
20
+ async def fetch_time_logs_batch(
21
+ *,
22
+ domain: str,
23
+ token: str,
24
+ card_ids: list[int],
25
+ workers: int = DEFAULT_BATCH_WORKERS,
26
+ page_size: int = API_MAX_PAGE_SIZE,
27
+ max_pages: int = DEFAULT_COLLECTION_MAX_PAGES,
28
+ for_date: str | None = None,
29
+ personal: bool | None = None,
30
+ compact: bool = False,
31
+ fields: str | None = None,
32
+ timeout: float,
33
+ reporter,
34
+ execution_context=None,
35
+ cache_policy: str = "request_scope",
36
+ ) -> dict[str, Any]:
37
+ def query_for_card(card_id: int) -> dict[str, Any] | None:
38
+ params: dict[str, Any] = {}
39
+ if for_date:
40
+ params["for_date"] = for_date
41
+ if personal is not None:
42
+ params["personal"] = personal
43
+ return params or None
44
+
45
+ return await fetch_card_collection_batch(
46
+ domain=domain,
47
+ token=token,
48
+ card_ids=card_ids,
49
+ workers=workers,
50
+ timeout=timeout,
51
+ reporter=reporter,
52
+ execution_context=execution_context,
53
+ cache_policy=cache_policy,
54
+ path_for_card=lambda card_id: f"/cards/{card_id}/time-logs",
55
+ query_for_card=query_for_card,
56
+ page_size=page_size,
57
+ max_pages=max_pages,
58
+ result_field="time_logs",
59
+ transform_items=lambda items: _shape_time_log_items(items, compact=compact, fields=fields),
60
+ worker_label="batch-time-logs",
61
+ )
@@ -0,0 +1,140 @@
1
+ """Helpers for entity tree commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from kaiten_cli.errors import ConfigError
8
+ from kaiten_cli.i18n import tr
9
+ from kaiten_cli.runtime.support.pagination import fetch_all_offset_pages
10
+
11
+ TREE_PAGE_LIMIT = 100
12
+ TREE_MAX_PAGES = 200
13
+
14
+
15
+ def sort_entities(entities: list[dict[str, Any]]) -> list[dict[str, Any]]:
16
+ type_order = {"document_group": 0, "space": 1, "document": 2}
17
+ return sorted(
18
+ entities, key=lambda entity: (type_order.get(entity["type"], 99), entity.get("title", ""))
19
+ )
20
+
21
+
22
+ def strip_id_none(entity: dict[str, Any]) -> dict[str, Any]:
23
+ return {key: value for key, value in entity.items() if not (key == "id" and value is None)}
24
+
25
+
26
+ async def fetch_paginated_entities(
27
+ client,
28
+ path: str,
29
+ *,
30
+ timeout: float,
31
+ limit: int = TREE_PAGE_LIMIT,
32
+ max_pages: int = TREE_MAX_PAGES,
33
+ reporter=None,
34
+ ) -> list[dict[str, Any]]:
35
+ rows = await fetch_all_offset_pages(
36
+ client,
37
+ path,
38
+ timeout=timeout,
39
+ page_size=limit,
40
+ max_pages=max_pages,
41
+ reporter=reporter,
42
+ )
43
+ return [row for row in rows if isinstance(row, dict)]
44
+
45
+
46
+ async def fetch_all_entities(client, *, timeout: float, reporter=None) -> list[dict[str, Any]]:
47
+ spaces_resp = await fetch_paginated_entities(
48
+ client, "/spaces", timeout=timeout, reporter=reporter
49
+ )
50
+ docs_resp = await fetch_paginated_entities(
51
+ client, "/documents", timeout=timeout, reporter=reporter
52
+ )
53
+ groups_resp = await fetch_paginated_entities(
54
+ client, "/document-groups", timeout=timeout, reporter=reporter
55
+ )
56
+
57
+ entities: list[dict[str, Any]] = []
58
+
59
+ for space in spaces_resp if isinstance(spaces_resp, list) else []:
60
+ uid = space.get("uid")
61
+ if uid is None:
62
+ continue
63
+ entities.append(
64
+ {
65
+ "type": "space",
66
+ "uid": uid,
67
+ "id": space.get("id"),
68
+ "title": space.get("title", ""),
69
+ "parent_entity_uid": space.get("parent_entity_uid"),
70
+ }
71
+ )
72
+
73
+ for document in docs_resp if isinstance(docs_resp, list) else []:
74
+ entities.append(
75
+ {
76
+ "type": "document",
77
+ "uid": document.get("uid", ""),
78
+ "id": None,
79
+ "title": document.get("title", ""),
80
+ "parent_entity_uid": document.get("parent_entity_uid"),
81
+ }
82
+ )
83
+
84
+ for group in groups_resp if isinstance(groups_resp, list) else []:
85
+ entities.append(
86
+ {
87
+ "type": "document_group",
88
+ "uid": group.get("uid", ""),
89
+ "id": None,
90
+ "title": group.get("title", ""),
91
+ "parent_entity_uid": group.get("parent_entity_uid"),
92
+ }
93
+ )
94
+
95
+ return entities
96
+
97
+
98
+ def list_children(entities: list[dict[str, Any]], parent_uid: str | None) -> list[dict[str, Any]]:
99
+ if parent_uid is None:
100
+ known_uids = {entity.get("uid") for entity in entities}
101
+ children = [
102
+ entity for entity in entities if effective_parent_uid(entity, known_uids) is None
103
+ ]
104
+ else:
105
+ children = [entity for entity in entities if entity.get("parent_entity_uid") == parent_uid]
106
+ return [strip_id_none(entity) for entity in sort_entities(children)]
107
+
108
+
109
+ def effective_parent_uid(entity: dict[str, Any], known_uids: set[Any]) -> Any:
110
+ parent_uid = entity.get("parent_entity_uid")
111
+ if parent_uid is not None and parent_uid not in known_uids:
112
+ return None
113
+ return parent_uid
114
+
115
+
116
+ def build_tree(
117
+ entities: list[dict[str, Any]], root_uid: str | None, max_depth: int
118
+ ) -> list[dict[str, Any]]:
119
+ if root_uid is not None and not any(entity["uid"] == root_uid for entity in entities):
120
+ raise ConfigError(tr("Entity with uid '{value_0}' not found", value_0=root_uid))
121
+
122
+ known_uids = {entity.get("uid") for entity in entities}
123
+ by_parent: dict[str | None, list[dict[str, Any]]] = {}
124
+ for entity in entities:
125
+ by_parent.setdefault(effective_parent_uid(entity, known_uids), []).append(entity)
126
+
127
+ def recurse(parent_uid: str | None, depth: int) -> list[dict[str, Any]]:
128
+ children = sort_entities(by_parent.get(parent_uid, []))
129
+ result = []
130
+ for child in children:
131
+ node = strip_id_none(child)
132
+ node.pop("parent_entity_uid", None)
133
+ if max_depth == 0 or depth < max_depth:
134
+ node["children"] = recurse(child["uid"], depth + 1)
135
+ else:
136
+ node["children"] = []
137
+ result.append(node)
138
+ return result
139
+
140
+ return recurse(root_uid, 0)