Initial SFERA platform baseline
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from integration_topology import IntegrationEndpoint, build_integration_topology
|
||||
from projection_engine import InMemoryProjection
|
||||
from sir import EdgeKind, NodeKind, SemanticNode, SirSnapshot
|
||||
|
||||
|
||||
class RoutineImpactReport(BaseModel):
|
||||
routine_name: str
|
||||
callers: list[SemanticNode] = Field(default_factory=list)
|
||||
callees: list[SemanticNode] = Field(default_factory=list)
|
||||
query_tables: list[SemanticNode] = Field(default_factory=list)
|
||||
writes: list[SemanticNode] = Field(default_factory=list)
|
||||
|
||||
|
||||
class RoleAccessGrant(BaseModel):
|
||||
role: SemanticNode
|
||||
permissions: dict = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ObjectImpactReport(BaseModel):
|
||||
object_name: str
|
||||
object: SemanticNode
|
||||
modules: list[SemanticNode] = Field(default_factory=list)
|
||||
routines: list[SemanticNode] = Field(default_factory=list)
|
||||
forms: list[SemanticNode] = Field(default_factory=list)
|
||||
commands: list[SemanticNode] = Field(default_factory=list)
|
||||
attributes: list[SemanticNode] = Field(default_factory=list)
|
||||
tabular_sections: list[SemanticNode] = Field(default_factory=list)
|
||||
tabular_section_columns: dict[str, list[SemanticNode]] = Field(default_factory=dict)
|
||||
roles: list[SemanticNode] = Field(default_factory=list)
|
||||
role_access: list[RoleAccessGrant] = Field(default_factory=list)
|
||||
jobs: list[SemanticNode] = Field(default_factory=list)
|
||||
callees: list[SemanticNode] = Field(default_factory=list)
|
||||
query_tables: list[SemanticNode] = Field(default_factory=list)
|
||||
writes: list[SemanticNode] = Field(default_factory=list)
|
||||
integrations: list[IntegrationEndpoint] = Field(default_factory=list)
|
||||
|
||||
|
||||
def routine_impact(graph: InMemoryProjection, routine_name: str) -> RoutineImpactReport:
|
||||
return RoutineImpactReport(
|
||||
routine_name=routine_name,
|
||||
callers=graph.find_callers(routine_name),
|
||||
callees=graph.find_callees(routine_name),
|
||||
query_tables=graph.find_query_tables(routine_name),
|
||||
writes=graph.find_writes(routine_name),
|
||||
)
|
||||
|
||||
|
||||
def object_impact(graph: InMemoryProjection, object_name: str) -> ObjectImpactReport | None:
|
||||
owner = _find_object(graph, object_name)
|
||||
if owner is None:
|
||||
return None
|
||||
|
||||
modules = _targets(graph, owner, EdgeKind.CONTAINS, {NodeKind.MODULE})
|
||||
attributes = _targets(graph, owner, EdgeKind.HAS_ATTRIBUTE, {NodeKind.ATTRIBUTE})
|
||||
tabular_sections = _targets(graph, owner, EdgeKind.HAS_TABULAR_SECTION, {NodeKind.TABULAR_SECTION})
|
||||
tabular_section_columns = {
|
||||
section.lineage_id: _targets(graph, section, EdgeKind.HAS_ATTRIBUTE, {NodeKind.ATTRIBUTE})
|
||||
for section in tabular_sections
|
||||
}
|
||||
forms = _targets(graph, owner, EdgeKind.HAS_FORM, {NodeKind.FORM})
|
||||
commands = _dedupe_nodes(
|
||||
[
|
||||
*_targets(graph, owner, EdgeKind.HAS_COMMAND, {NodeKind.COMMAND}),
|
||||
*[
|
||||
command
|
||||
for form in forms
|
||||
for command in _targets(graph, form, EdgeKind.HAS_COMMAND, {NodeKind.COMMAND})
|
||||
],
|
||||
]
|
||||
)
|
||||
command_handlers = _dedupe_nodes(
|
||||
[
|
||||
handler
|
||||
for command in commands
|
||||
for handler in _targets(graph, command, EdgeKind.HANDLES, {NodeKind.PROCEDURE, NodeKind.FUNCTION})
|
||||
]
|
||||
)
|
||||
form_handlers = _dedupe_nodes(
|
||||
[
|
||||
handler
|
||||
for form in forms
|
||||
for handler in _targets(graph, form, EdgeKind.HANDLES, {NodeKind.PROCEDURE, NodeKind.FUNCTION})
|
||||
]
|
||||
)
|
||||
role_access = _role_access_grants(graph, owner)
|
||||
roles = _dedupe_nodes([grant.role for grant in role_access])
|
||||
job_routines = _targets(graph, owner, EdgeKind.RUNS, {NodeKind.PROCEDURE, NodeKind.FUNCTION})
|
||||
routines = _dedupe_nodes(
|
||||
job_routines
|
||||
+ command_handlers
|
||||
+ form_handlers
|
||||
+ [
|
||||
routine
|
||||
for module in modules
|
||||
for routine in _targets(graph, module, EdgeKind.DECLARES, {NodeKind.PROCEDURE, NodeKind.FUNCTION})
|
||||
]
|
||||
)
|
||||
routine_modules = _dedupe_nodes(
|
||||
[
|
||||
module
|
||||
for routine in routines
|
||||
for module in _sources(graph, routine, EdgeKind.DECLARES, {NodeKind.MODULE})
|
||||
]
|
||||
)
|
||||
modules = _dedupe_nodes([*modules, *routine_modules])
|
||||
callees = _dedupe_nodes(
|
||||
[
|
||||
callee
|
||||
for routine in routines
|
||||
for callee in _targets(graph, routine, EdgeKind.CALLS, {NodeKind.PROCEDURE, NodeKind.FUNCTION})
|
||||
]
|
||||
)
|
||||
query_nodes = [
|
||||
query
|
||||
for routine in routines
|
||||
for query in _targets(graph, routine, EdgeKind.OWNS_QUERY, {NodeKind.QUERY})
|
||||
]
|
||||
query_tables = _dedupe_nodes(
|
||||
[
|
||||
table
|
||||
for query in query_nodes
|
||||
for table in _targets(graph, query, EdgeKind.READS_TABLE, {NodeKind.TABLE, NodeKind.REGISTER})
|
||||
]
|
||||
)
|
||||
writes = _dedupe_nodes(
|
||||
[
|
||||
target
|
||||
for routine in routines
|
||||
for target in _targets(graph, routine, EdgeKind.WRITES, {NodeKind.REGISTER, NodeKind.TABLE})
|
||||
]
|
||||
)
|
||||
|
||||
return ObjectImpactReport(
|
||||
object_name=object_name,
|
||||
object=owner,
|
||||
modules=modules,
|
||||
routines=routines,
|
||||
forms=forms,
|
||||
commands=commands,
|
||||
attributes=attributes,
|
||||
tabular_sections=tabular_sections,
|
||||
tabular_section_columns=tabular_section_columns,
|
||||
roles=roles,
|
||||
role_access=role_access,
|
||||
jobs=[owner] if owner.kind == NodeKind.SCHEDULED_JOB else [],
|
||||
callees=callees,
|
||||
query_tables=query_tables,
|
||||
writes=writes,
|
||||
)
|
||||
|
||||
|
||||
def object_impact_from_snapshot(snapshot: SirSnapshot, object_name: str) -> ObjectImpactReport | None:
|
||||
graph = InMemoryProjection()
|
||||
graph.project_snapshot(snapshot)
|
||||
report = object_impact(graph, object_name)
|
||||
if report is None:
|
||||
return None
|
||||
routine_names = {routine.qualified_name for routine in report.routines}
|
||||
module_names = {module.qualified_name for module in report.modules}
|
||||
report.integrations = [
|
||||
endpoint
|
||||
for endpoint in build_integration_topology(snapshot).endpoints
|
||||
if endpoint.owner in routine_names or endpoint.owner in module_names
|
||||
]
|
||||
return report
|
||||
|
||||
|
||||
def _find_object(graph: InMemoryProjection, object_name: str) -> SemanticNode | None:
|
||||
wanted = object_name.lower()
|
||||
object_kinds = {
|
||||
NodeKind.CATALOG,
|
||||
NodeKind.DOCUMENT,
|
||||
NodeKind.CONSTANT,
|
||||
NodeKind.DOCUMENT_JOURNAL,
|
||||
NodeKind.ENUM,
|
||||
NodeKind.REPORT,
|
||||
NodeKind.DATA_PROCESSOR,
|
||||
NodeKind.CHART_OF_CHARACTERISTIC_TYPES,
|
||||
NodeKind.CHART_OF_ACCOUNTS,
|
||||
NodeKind.CHART_OF_CALCULATION_TYPES,
|
||||
NodeKind.REGISTER,
|
||||
NodeKind.COMMON_MODULE,
|
||||
NodeKind.EXCHANGE_PLAN,
|
||||
NodeKind.EXTERNAL_DATA_SOURCE,
|
||||
NodeKind.SCHEDULED_JOB,
|
||||
NodeKind.BUSINESS_PROCESS,
|
||||
NodeKind.TASK,
|
||||
}
|
||||
return next(
|
||||
(
|
||||
node
|
||||
for node in graph.nodes.values()
|
||||
if node.kind in object_kinds
|
||||
and (node.name.lower() == wanted or node.qualified_name.lower() == wanted)
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def _targets(
|
||||
graph: InMemoryProjection,
|
||||
source: SemanticNode,
|
||||
kind: EdgeKind,
|
||||
target_kinds: set[NodeKind],
|
||||
) -> list[SemanticNode]:
|
||||
return [
|
||||
graph.nodes[edge.target_lineage]
|
||||
for edge in graph.edges.values()
|
||||
if edge.kind == kind
|
||||
and edge.source_lineage == source.lineage_id
|
||||
and edge.target_lineage in graph.nodes
|
||||
and graph.nodes[edge.target_lineage].kind in target_kinds
|
||||
]
|
||||
|
||||
|
||||
def _sources(
|
||||
graph: InMemoryProjection,
|
||||
target: SemanticNode,
|
||||
kind: EdgeKind,
|
||||
source_kinds: set[NodeKind],
|
||||
) -> list[SemanticNode]:
|
||||
return [
|
||||
graph.nodes[edge.source_lineage]
|
||||
for edge in graph.edges.values()
|
||||
if edge.kind == kind
|
||||
and edge.target_lineage == target.lineage_id
|
||||
and edge.source_lineage in graph.nodes
|
||||
and graph.nodes[edge.source_lineage].kind in source_kinds
|
||||
]
|
||||
|
||||
|
||||
def _role_access_grants(graph: InMemoryProjection, target: SemanticNode) -> list[RoleAccessGrant]:
|
||||
grants: list[RoleAccessGrant] = []
|
||||
for edge in graph.edges.values():
|
||||
if edge.kind != EdgeKind.GRANTS_ACCESS or edge.target_lineage != target.lineage_id:
|
||||
continue
|
||||
role = graph.nodes.get(edge.source_lineage)
|
||||
if role is None or role.kind != NodeKind.ROLE:
|
||||
continue
|
||||
grants.append(RoleAccessGrant(role=role, permissions=edge.attributes))
|
||||
return grants
|
||||
|
||||
|
||||
def _dedupe_nodes(nodes: list[SemanticNode]) -> list[SemanticNode]:
|
||||
seen: dict[str, SemanticNode] = {}
|
||||
for node in nodes:
|
||||
seen.setdefault(node.lineage_id, node)
|
||||
return list(seen.values())
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ObjectImpactReport",
|
||||
"RoleAccessGrant",
|
||||
"RoutineImpactReport",
|
||||
"object_impact",
|
||||
"object_impact_from_snapshot",
|
||||
"routine_impact",
|
||||
]
|
||||
Reference in New Issue
Block a user