Initial SFERA platform baseline

This commit is contained in:
2026-05-16 19:03:49 +03:00
commit 3b845c8fce
282 changed files with 55045 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# sfera-integration-topology
Integration endpoint inventory stored as semantic nodes and dependencies.
@@ -0,0 +1,11 @@
[project]
name = "sfera-integration-topology"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"pydantic>=2.0",
"sfera-sir",
]
[tool.uv]
package = true
@@ -0,0 +1,128 @@
from __future__ import annotations
from enum import Enum
import re
from pydantic import BaseModel, Field
from sir import EdgeKind, NodeKind, SirSnapshot
class IntegrationKind(str, Enum):
HTTP_SERVICE = "HTTP_SERVICE"
WEB_SERVICE = "WEB_SERVICE"
EXCHANGE_PLAN = "EXCHANGE_PLAN"
FILE_EXCHANGE = "FILE_EXCHANGE"
COM_CONNECTOR = "COM_CONNECTOR"
UNKNOWN = "UNKNOWN"
class IntegrationEndpoint(BaseModel):
endpoint_id: str
name: str
kind: IntegrationKind = IntegrationKind.UNKNOWN
direction: str = "UNKNOWN"
owner: str | None = None
attributes: dict = Field(default_factory=dict)
class IntegrationTopology(BaseModel):
project_id: str
endpoints: list[IntegrationEndpoint] = Field(default_factory=list)
def by_kind(self, kind: IntegrationKind) -> list[IntegrationEndpoint]:
return [endpoint for endpoint in self.endpoints if endpoint.kind == kind]
_URL_RE = re.compile(r"https?://[^\"'\s;]+", re.IGNORECASE)
def build_integration_topology(snapshot: SirSnapshot) -> IntegrationTopology:
endpoints: list[IntegrationEndpoint] = []
nodes = {node.lineage_id: node for node in snapshot.nodes}
graph_endpoint_lineages = set()
for edge in snapshot.edges:
if edge.kind != EdgeKind.USES_INTEGRATION or edge.target_lineage not in nodes:
continue
endpoint_node = nodes[edge.target_lineage]
if endpoint_node.kind != NodeKind.INTEGRATION_ENDPOINT:
continue
owner = nodes.get(edge.source_lineage)
graph_endpoint_lineages.add(endpoint_node.lineage_id)
endpoints.append(
IntegrationEndpoint(
endpoint_id=f"integration.{endpoint_node.lineage_id}",
name=endpoint_node.name,
kind=IntegrationKind(endpoint_node.attributes.get("integration_kind", "UNKNOWN")),
direction=str(endpoint_node.attributes.get("direction", "UNKNOWN")),
owner=owner.qualified_name if owner is not None else None,
attributes=endpoint_node.attributes,
)
)
for node in snapshot.nodes:
if node.kind == NodeKind.MODULE and not graph_endpoint_lineages:
endpoints.extend(_module_integrations(node))
elif node.kind == NodeKind.EXCHANGE_PLAN:
endpoints.append(
IntegrationEndpoint(
endpoint_id=f"integration.{node.lineage_id}",
name=node.name,
kind=IntegrationKind.EXCHANGE_PLAN,
direction="BIDIRECTIONAL",
owner=node.qualified_name,
attributes={"qualified_name": node.qualified_name},
)
)
return IntegrationTopology(project_id=snapshot.project_id, endpoints=_dedupe_endpoints(endpoints))
def _module_integrations(node) -> list[IntegrationEndpoint]:
text = str(node.attributes.get("source_text", ""))
if not text:
return []
endpoints: list[IntegrationEndpoint] = []
for index, url in enumerate(_URL_RE.findall(text), start=1):
endpoints.append(
IntegrationEndpoint(
endpoint_id=f"integration.{node.lineage_id}.url.{index}",
name=url,
kind=IntegrationKind.HTTP_SERVICE,
direction="OUTBOUND",
owner=node.qualified_name,
attributes={"url": url},
)
)
if "HTTPСоединение" in text or "HTTPConnection" in text:
endpoints.append(_code_endpoint(node, "HTTPConnection", IntegrationKind.HTTP_SERVICE, "OUTBOUND"))
if "WSПрокси" in text or "WSProxy" in text or "WSСсылка" in text:
endpoints.append(_code_endpoint(node, "WSProxy", IntegrationKind.WEB_SERVICE, "OUTBOUND"))
if "FTPСоединение" in text or "FTPConnection" in text:
endpoints.append(_code_endpoint(node, "FTPConnection", IntegrationKind.FILE_EXCHANGE, "OUTBOUND"))
if "COMОбъект" in text or "COMObject" in text:
endpoints.append(_code_endpoint(node, "COMObject", IntegrationKind.COM_CONNECTOR, "OUTBOUND"))
return endpoints
def _code_endpoint(node, name: str, kind: IntegrationKind, direction: str) -> IntegrationEndpoint:
return IntegrationEndpoint(
endpoint_id=f"integration.{node.lineage_id}.{name.casefold()}",
name=name,
kind=kind,
direction=direction,
owner=node.qualified_name,
)
def _dedupe_endpoints(endpoints: list[IntegrationEndpoint]) -> list[IntegrationEndpoint]:
seen: dict[tuple[str, str, str | None], IntegrationEndpoint] = {}
for endpoint in endpoints:
seen.setdefault((endpoint.name, endpoint.kind.value, endpoint.owner), endpoint)
return sorted(seen.values(), key=lambda endpoint: (endpoint.kind.value, endpoint.name))
__all__ = [
"IntegrationEndpoint",
"IntegrationKind",
"IntegrationTopology",
"build_integration_topology",
]
@@ -0,0 +1,47 @@
from integration_topology import IntegrationEndpoint, IntegrationKind, IntegrationTopology
from integration_topology import build_integration_topology
from semantic_kernel import index_project
def test_integration_topology_filters_by_kind():
topology = IntegrationTopology(
project_id="demo",
endpoints=[
IntegrationEndpoint(
endpoint_id="endpoint.1",
name="OrdersApi",
kind=IntegrationKind.HTTP_SERVICE,
)
],
)
assert topology.by_kind(IntegrationKind.HTTP_SERVICE)[0].name == "OrdersApi"
def test_build_integration_topology_from_bsl_and_exchange_plan(tmp_path):
module = tmp_path / "integration.bsl"
module.write_text(
"""
Процедура ОтправитьЗаказ()
Соединение = Новый HTTPСоединение("api.example.local");
Адрес = "https://api.example.local/orders";
Объект = Новый COMОбъект("V83.Application");
КонецПроцедуры
""",
encoding="utf-8",
)
(tmp_path / "metadata.xml").write_text(
"""
<Configuration>
<ExchangePlan name="ОбменСКассой" qualifiedName="ПланОбмена.ОбменСКассой" />
</Configuration>
""",
encoding="utf-8",
)
snapshot = index_project(tmp_path, project_id="integrations")
topology = build_integration_topology(snapshot)
assert topology.by_kind(IntegrationKind.HTTP_SERVICE)
assert topology.by_kind(IntegrationKind.COM_CONNECTOR)[0].name == "COMObject"
assert topology.by_kind(IntegrationKind.EXCHANGE_PLAN)[0].name == "ОбменСКассой"