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,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)
|