Extract metadata tree controller
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-17 19:51:30 +03:00
parent 8d206a3bf2
commit bb3e70f1e5
2 changed files with 165 additions and 66 deletions
+35 -66
View File
@@ -94,6 +94,12 @@ from api_server.html5_setup_controller import (
html5_setup_source as _html5_setup_source, html5_setup_source as _html5_setup_source,
html5_setup_summary as _html5_setup_summary, html5_setup_summary as _html5_setup_summary,
) )
from api_server.metadata_tree_controller import (
metadata_tree as _metadata_tree,
metadata_tree_children as _metadata_tree_children,
metadata_tree_path as _metadata_tree_path,
metadata_tree_search as _metadata_tree_search,
)
from impact_engine import object_impact, routine_impact from impact_engine import object_impact, routine_impact
from incremental_indexer import rebuild_changed_file from incremental_indexer import rebuild_changed_file
from integration_topology import IntegrationKind, build_integration_topology from integration_topology import IntegrationKind, build_integration_topology
@@ -3204,16 +3210,14 @@ async def metadata_catalog() -> MetadataCatalogResponse:
@app.get("/projects/{project_id}/metadata/tree", response_model=ProjectMetadataTreeResponse) @app.get("/projects/{project_id}/metadata/tree", response_model=ProjectMetadataTreeResponse)
async def project_metadata_tree(project_id: str, object_limit_per_branch: int = 200) -> ProjectMetadataTreeResponse: async def project_metadata_tree(project_id: str, object_limit_per_branch: int = 200) -> ProjectMetadataTreeResponse:
snapshot = _project_snapshot_or_404(project_id) return _metadata_tree(
normalized = _load_normalized_project(project_id)
root = (
_project_metadata_tree_response_from_normalized(normalized, object_limit_per_branch=max(0, object_limit_per_branch))
if normalized is not None
else _project_metadata_tree_response(snapshot, object_limit_per_branch=max(0, object_limit_per_branch))
)
return ProjectMetadataTreeResponse(
project_id=project_id, project_id=project_id,
root=root, object_limit_per_branch=object_limit_per_branch,
project_snapshot=_project_snapshot_or_404,
normalized_project=_load_normalized_project,
normalized_tree=_project_metadata_tree_response_from_normalized,
snapshot_tree=_project_metadata_tree_response,
response_model=ProjectMetadataTreeResponse,
) )
@@ -3224,78 +3228,43 @@ async def project_metadata_tree_children(
offset: int = 0, offset: int = 0,
limit: int = 50, limit: int = 50,
) -> MetadataTreeChildrenResponse: ) -> MetadataTreeChildrenResponse:
snapshot = _project_snapshot_or_404(project_id) return _metadata_tree_children(
normalized = _load_normalized_project(project_id)
normalized_offset = max(0, offset)
normalized_limit = min(max(1, limit), 250)
normalized_children = (
_normalized_metadata_tree_children_for_node(
normalized,
node_id=node_id,
offset=normalized_offset,
limit=normalized_limit,
)
if normalized is not None
else None
)
if normalized_children is None:
children, total = _metadata_tree_children_for_node(
snapshot,
node_id=node_id,
offset=normalized_offset,
limit=normalized_limit,
)
else:
children, total = normalized_children
return MetadataTreeChildrenResponse(
project_id=project_id, project_id=project_id,
parent_id=node_id, node_id=node_id,
offset=normalized_offset, offset=offset,
limit=normalized_limit, limit=limit,
total=total, project_snapshot=_project_snapshot_or_404,
has_more=normalized_offset + len(children) < total, normalized_project=_load_normalized_project,
children=children, normalized_children_for_node=_normalized_metadata_tree_children_for_node,
snapshot_children_for_node=_metadata_tree_children_for_node,
response_model=MetadataTreeChildrenResponse,
) )
@app.get("/projects/{project_id}/metadata/tree/search", response_model=MetadataTreeSearchResponse) @app.get("/projects/{project_id}/metadata/tree/search", response_model=MetadataTreeSearchResponse)
async def project_metadata_tree_search(project_id: str, q: str, limit: int = 80) -> MetadataTreeSearchResponse: async def project_metadata_tree_search(project_id: str, q: str, limit: int = 80) -> MetadataTreeSearchResponse:
snapshot = _project_snapshot_or_404(project_id) return _metadata_tree_search(
normalized_query = q.strip().casefold()
normalized_limit = min(max(1, limit), 250)
if len(normalized_query) < 2:
return MetadataTreeSearchResponse(project_id=project_id, q=q, total=0, results=[])
matches = [
node
for node in snapshot.nodes
if _is_metadata_tree_search_node(node)
and (
normalized_query in node.name.casefold()
or normalized_query in node.qualified_name.casefold()
)
]
matches.sort(key=lambda item: _metadata_search_rank(item, normalized_query))
page = matches[:normalized_limit]
child_count_index = _metadata_child_count_index(snapshot, [node.lineage_id for node in page])
return MetadataTreeSearchResponse(
project_id=project_id, project_id=project_id,
q=q, q=q,
total=len(matches), limit=limit,
results=[_metadata_node_for_search_result(snapshot, node, child_count_index) for node in page], project_snapshot=_project_snapshot_or_404,
is_search_node=_is_metadata_tree_search_node,
search_rank=_metadata_search_rank,
child_count_index=_metadata_child_count_index,
node_for_search_result=_metadata_node_for_search_result,
response_model=MetadataTreeSearchResponse,
) )
@app.get("/projects/{project_id}/metadata/tree/path", response_model=MetadataTreePathResponse) @app.get("/projects/{project_id}/metadata/tree/path", response_model=MetadataTreePathResponse)
async def project_metadata_tree_path(project_id: str, node_id: str) -> MetadataTreePathResponse: async def project_metadata_tree_path(project_id: str, node_id: str) -> MetadataTreePathResponse:
snapshot = _project_snapshot_or_404(project_id) return _metadata_tree_path(
path = _metadata_tree_path_for_node(snapshot, node_id)
if not path:
raise HTTPException(status_code=404, detail=f"Metadata tree path not found: {node_id}")
return MetadataTreePathResponse(
project_id=project_id, project_id=project_id,
node_id=node_id, node_id=node_id,
path=path, project_snapshot=_project_snapshot_or_404,
steps=_metadata_tree_path_steps(snapshot, path), tree_path_for_node=_metadata_tree_path_for_node,
tree_path_steps=_metadata_tree_path_steps,
response_model=MetadataTreePathResponse,
) )
@@ -0,0 +1,130 @@
from __future__ import annotations
from collections.abc import Callable
from typing import Any
from fastapi import HTTPException
def metadata_tree(
*,
project_id: str,
object_limit_per_branch: int,
project_snapshot: Callable[[str], object],
normalized_project: Callable[[str], object | None],
normalized_tree: Callable[..., object],
snapshot_tree: Callable[..., object],
response_model: Callable[..., object],
) -> object:
snapshot = project_snapshot(project_id)
normalized = normalized_project(project_id)
root = (
normalized_tree(normalized, object_limit_per_branch=max(0, object_limit_per_branch))
if normalized is not None
else snapshot_tree(snapshot, object_limit_per_branch=max(0, object_limit_per_branch))
)
return response_model(project_id=project_id, root=root)
def metadata_tree_children(
*,
project_id: str,
node_id: str,
offset: int,
limit: int,
project_snapshot: Callable[[str], object],
normalized_project: Callable[[str], object | None],
normalized_children_for_node: Callable[..., tuple[list[object], int] | None],
snapshot_children_for_node: Callable[..., tuple[list[object], int]],
response_model: Callable[..., object],
) -> object:
snapshot = project_snapshot(project_id)
normalized = normalized_project(project_id)
normalized_offset = max(0, offset)
normalized_limit = min(max(1, limit), 250)
normalized_children = (
normalized_children_for_node(
normalized,
node_id=node_id,
offset=normalized_offset,
limit=normalized_limit,
)
if normalized is not None
else None
)
if normalized_children is None:
children, total = snapshot_children_for_node(
snapshot,
node_id=node_id,
offset=normalized_offset,
limit=normalized_limit,
)
else:
children, total = normalized_children
return response_model(
project_id=project_id,
parent_id=node_id,
offset=normalized_offset,
limit=normalized_limit,
total=total,
has_more=normalized_offset + len(children) < total,
children=children,
)
def metadata_tree_search(
*,
project_id: str,
q: str,
limit: int,
project_snapshot: Callable[[str], object],
is_search_node: Callable[[Any], bool],
search_rank: Callable[[Any, str], object],
child_count_index: Callable[[object, list[str]], object],
node_for_search_result: Callable[[object, Any, object], object],
response_model: Callable[..., object],
) -> object:
snapshot = project_snapshot(project_id)
normalized_query = q.strip().casefold()
normalized_limit = min(max(1, limit), 250)
if len(normalized_query) < 2:
return response_model(project_id=project_id, q=q, total=0, results=[])
matches = [
node
for node in snapshot.nodes
if is_search_node(node)
and (
normalized_query in node.name.casefold()
or normalized_query in node.qualified_name.casefold()
)
]
matches.sort(key=lambda item: search_rank(item, normalized_query))
page = matches[:normalized_limit]
counts = child_count_index(snapshot, [node.lineage_id for node in page])
return response_model(
project_id=project_id,
q=q,
total=len(matches),
results=[node_for_search_result(snapshot, node, counts) for node in page],
)
def metadata_tree_path(
*,
project_id: str,
node_id: str,
project_snapshot: Callable[[str], object],
tree_path_for_node: Callable[[object, str], list[str]],
tree_path_steps: Callable[[object, list[str]], list[object]],
response_model: Callable[..., object],
) -> object:
snapshot = project_snapshot(project_id)
path = tree_path_for_node(snapshot, node_id)
if not path:
raise HTTPException(status_code=404, detail=f"Metadata tree path not found: {node_id}")
return response_model(
project_id=project_id,
node_id=node_id,
path=path,
steps=tree_path_steps(snapshot, path),
)