kbve 0.1.0__tar.gz
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.
- kbve-0.1.0/.flake8 +14 -0
- kbve-0.1.0/.python-version +1 -0
- kbve-0.1.0/PKG-INFO +57 -0
- kbve-0.1.0/README.md +19 -0
- kbve-0.1.0/kbve/__init__.py +10 -0
- kbve-0.1.0/kbve/api/__init__.py +1 -0
- kbve-0.1.0/kbve/api/clients/__init__.py +1 -0
- kbve-0.1.0/kbve/api/utils/__init__.py +1 -0
- kbve-0.1.0/kbve/mdx/__init__.py +4 -0
- kbve-0.1.0/kbve/mdx/escape.py +14 -0
- kbve-0.1.0/kbve/mdx/renderer.py +220 -0
- kbve-0.1.0/kbve/models/__init__.py +3 -0
- kbve-0.1.0/kbve/models/server_models.py +13 -0
- kbve-0.1.0/kbve/nx/__init__.py +20 -0
- kbve-0.1.0/kbve/nx/graph.py +131 -0
- kbve-0.1.0/kbve/nx/security.py +258 -0
- kbve-0.1.0/kbve/proto/__init__.py +1 -0
- kbve-0.1.0/kbve/proto/kbve_pb2.py +48 -0
- kbve-0.1.0/kbve/proto/kbve_pb2.pyi +36 -0
- kbve-0.1.0/kbve/proto/kbve_pb2_grpc.py +169 -0
- kbve-0.1.0/kbve/server/__init__.py +5 -0
- kbve-0.1.0/kbve/server/app_server.py +90 -0
- kbve-0.1.0/kbve/server/grpc_server.py +49 -0
- kbve-0.1.0/kbve/server/http_server.py +45 -0
- kbve-0.1.0/kbve/server/services/__init__.py +4 -0
- kbve-0.1.0/kbve/server/services/echo_service.py +13 -0
- kbve-0.1.0/kbve/server/services/health_service.py +13 -0
- kbve-0.1.0/kbve/utils/__init__.py +11 -0
- kbve-0.1.0/kbve/utils/json_utils.py +55 -0
- kbve-0.1.0/kbve/utils/module_info.py +64 -0
- kbve-0.1.0/project.json +79 -0
- kbve-0.1.0/pyproject.toml +57 -0
- kbve-0.1.0/uv.lock +1081 -0
kbve-0.1.0/.flake8
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[flake8]
|
|
2
|
+
exclude =
|
|
3
|
+
.git,
|
|
4
|
+
__pycache__,
|
|
5
|
+
build,
|
|
6
|
+
dist,
|
|
7
|
+
.tox,
|
|
8
|
+
venv,
|
|
9
|
+
.venv,
|
|
10
|
+
.pytest_cache
|
|
11
|
+
max-line-length = 120
|
|
12
|
+
extend-ignore = E203, E221
|
|
13
|
+
per-file-ignores =
|
|
14
|
+
kbve/proto/*_pb2*.py: E501, E303, F401, E261, W504, E302, W291, E271, E211, E111, E225, E231, F541, E114, E128
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
kbve-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kbve
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: KBVE - API layer and complexity handler for async HTTP, WebSocket, gRPC, and broadcasting
|
|
5
|
+
License-Expression: LicenseRef-Proprietary
|
|
6
|
+
Requires-Python: <3.14,>=3.12
|
|
7
|
+
Requires-Dist: aiohappyeyeballs==2.6.1
|
|
8
|
+
Requires-Dist: aiohttp==3.13.3
|
|
9
|
+
Requires-Dist: aiosignal==1.4.0
|
|
10
|
+
Requires-Dist: annotated-doc==0.0.4
|
|
11
|
+
Requires-Dist: annotated-types==0.7.0
|
|
12
|
+
Requires-Dist: anyio==4.12.1
|
|
13
|
+
Requires-Dist: attrs==25.4.0
|
|
14
|
+
Requires-Dist: click==8.3.1
|
|
15
|
+
Requires-Dist: colorama==0.4.6; sys_platform == 'win32'
|
|
16
|
+
Requires-Dist: fastapi==0.128.7
|
|
17
|
+
Requires-Dist: frozenlist==1.8.0
|
|
18
|
+
Requires-Dist: grpcio==1.78.0
|
|
19
|
+
Requires-Dist: h11==0.16.0
|
|
20
|
+
Requires-Dist: httptools==0.7.1
|
|
21
|
+
Requires-Dist: idna==3.11
|
|
22
|
+
Requires-Dist: multidict==6.7.1
|
|
23
|
+
Requires-Dist: propcache==0.4.1
|
|
24
|
+
Requires-Dist: protobuf==5.29.6
|
|
25
|
+
Requires-Dist: pydantic-core==2.41.5
|
|
26
|
+
Requires-Dist: pydantic==2.12.5
|
|
27
|
+
Requires-Dist: python-dotenv==1.2.1
|
|
28
|
+
Requires-Dist: pyyaml==6.0.3
|
|
29
|
+
Requires-Dist: starlette==0.52.1
|
|
30
|
+
Requires-Dist: typing-extensions==4.15.0
|
|
31
|
+
Requires-Dist: typing-inspection==0.4.2
|
|
32
|
+
Requires-Dist: uvicorn==0.40.0
|
|
33
|
+
Requires-Dist: uvloop==0.22.1; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'
|
|
34
|
+
Requires-Dist: watchfiles==1.1.1
|
|
35
|
+
Requires-Dist: websockets==16.0
|
|
36
|
+
Requires-Dist: yarl==1.22.0
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# kbve
|
|
40
|
+
|
|
41
|
+
API layer and complexity handler for async HTTP, WebSocket, gRPC, and broadcasting.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install kbve
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Development
|
|
50
|
+
|
|
51
|
+
This package is part of the [KBVE](https://github.com/kbve/kbve) monorepo, managed with Nx and Poetry.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pnpm nx test python-kbve
|
|
55
|
+
pnpm nx lint python-kbve
|
|
56
|
+
pnpm nx build python-kbve
|
|
57
|
+
```
|
kbve-0.1.0/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# kbve
|
|
2
|
+
|
|
3
|
+
API layer and complexity handler for async HTTP, WebSocket, gRPC, and broadcasting.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install kbve
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Development
|
|
12
|
+
|
|
13
|
+
This package is part of the [KBVE](https://github.com/kbve/kbve) monorepo, managed with Nx and Poetry.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm nx test python-kbve
|
|
17
|
+
pnpm nx lint python-kbve
|
|
18
|
+
pnpm nx build python-kbve
|
|
19
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""KBVE - Core library for async servers, Nx workspace tooling, and MDX rendering."""
|
|
2
|
+
|
|
3
|
+
from .models.server_models import ServerConfig # noqa: F401
|
|
4
|
+
from .server.app_server import AppServer # noqa: F401
|
|
5
|
+
from .server.grpc_server import GrpcServer # noqa: F401
|
|
6
|
+
from .server.http_server import HttpServer # noqa: F401
|
|
7
|
+
from .server.services.health_service import HealthServicer # noqa: F401
|
|
8
|
+
from .server.services.echo_service import EchoServicer # noqa: F401
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""KBVE API utilities."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""KBVE API clients."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""KBVE API utilities."""
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""MDX/JSX text escaping utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def escape_mdx(text: str) -> str:
|
|
7
|
+
"""Escape characters that MDX/JSX would interpret as markup."""
|
|
8
|
+
return (text
|
|
9
|
+
.replace("&", "&")
|
|
10
|
+
.replace("<", "<")
|
|
11
|
+
.replace(">", ">")
|
|
12
|
+
.replace("{", "{")
|
|
13
|
+
.replace("}", "}")
|
|
14
|
+
.replace("|", "\\|"))
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Starlight MDX rendering primitives.
|
|
2
|
+
|
|
3
|
+
Provides ``MdxWriter``, a composable builder for generating Astro Starlight
|
|
4
|
+
MDX pages with frontmatter, cards, tabs, mermaid diagrams, tables, and
|
|
5
|
+
admonitions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import io
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MdxWriter:
|
|
15
|
+
"""Composable builder for Starlight MDX content.
|
|
16
|
+
|
|
17
|
+
Usage::
|
|
18
|
+
|
|
19
|
+
w = MdxWriter()
|
|
20
|
+
w.frontmatter(title="My Page", description="A page.")
|
|
21
|
+
w.imports("Card", "CardGrid", source="@astrojs/starlight/components")
|
|
22
|
+
w.heading("Hello", level=2)
|
|
23
|
+
w.admonition("note", "Auto-generated", "Some body text.")
|
|
24
|
+
w.card_grid_start()
|
|
25
|
+
w.card("3 Apps", icon="rocket", body="app1, app2, app3")
|
|
26
|
+
w.card_grid_end()
|
|
27
|
+
w.mermaid_pie("Title", {"Apps": 3, "Libs": 5})
|
|
28
|
+
w.table(["Col A", "Col B"], [["a", "b"]])
|
|
29
|
+
content = w.render()
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
self._buf = io.StringIO()
|
|
34
|
+
|
|
35
|
+
# ── Frontmatter ──────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
def frontmatter(self, **kwargs: Any) -> "MdxWriter":
|
|
38
|
+
"""Write YAML frontmatter block.
|
|
39
|
+
|
|
40
|
+
Supports flat keys and one level of nesting via dict values::
|
|
41
|
+
|
|
42
|
+
w.frontmatter(
|
|
43
|
+
title="Page",
|
|
44
|
+
sidebar={"label": "Nav", "order": 1},
|
|
45
|
+
)
|
|
46
|
+
"""
|
|
47
|
+
colon = ":"
|
|
48
|
+
self._buf.write("---\n")
|
|
49
|
+
for key, val in kwargs.items():
|
|
50
|
+
if isinstance(val, dict):
|
|
51
|
+
self._buf.write(f"{key}{colon}\n")
|
|
52
|
+
for k, v in val.items():
|
|
53
|
+
if isinstance(v, str) and "\n" in v:
|
|
54
|
+
self._buf.write(f" {k}{colon} |\n")
|
|
55
|
+
for line in v.splitlines():
|
|
56
|
+
self._buf.write(f" {line}\n")
|
|
57
|
+
else:
|
|
58
|
+
self._buf.write(f" {k}{colon} {v}\n")
|
|
59
|
+
elif isinstance(val, bool):
|
|
60
|
+
bval = "true" if val else "false"
|
|
61
|
+
self._buf.write(f"{key}{colon} {bval}\n")
|
|
62
|
+
else:
|
|
63
|
+
self._buf.write(f"{key}{colon} {val}\n")
|
|
64
|
+
self._buf.write("---\n\n")
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
# ── Imports ──────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
def imports(self, *components: str, source: str) -> "MdxWriter":
|
|
70
|
+
"""Write an ESM import statement."""
|
|
71
|
+
names = ", ".join(components)
|
|
72
|
+
lbrace = "{"
|
|
73
|
+
rbrace = "}"
|
|
74
|
+
semi = ";"
|
|
75
|
+
self._buf.write(
|
|
76
|
+
f"import {lbrace} {names} {rbrace} from '{source}'{semi}\n\n"
|
|
77
|
+
)
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
# ── Headings & text ──────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
def heading(self, text: str, level: int = 2) -> "MdxWriter":
|
|
83
|
+
prefix = "#" * level
|
|
84
|
+
self._buf.write(f"{prefix} {text}\n\n")
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
def text(self, content: str) -> "MdxWriter":
|
|
88
|
+
self._buf.write(f"{content}\n\n")
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
def raw(self, content: str) -> "MdxWriter":
|
|
92
|
+
"""Write raw content without trailing newlines."""
|
|
93
|
+
self._buf.write(content)
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
# ── Admonitions ──────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
def admonition(
|
|
99
|
+
self, kind: str, title: str, body: str,
|
|
100
|
+
) -> "MdxWriter":
|
|
101
|
+
"""Write a Starlight admonition (note, tip, caution, danger)."""
|
|
102
|
+
prefix = ":" * 3
|
|
103
|
+
self._buf.write(
|
|
104
|
+
f"{prefix}{kind}[{title}]\n{body}\n{prefix}\n\n"
|
|
105
|
+
)
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
# ── Cards ────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
def card_grid_start(self) -> "MdxWriter":
|
|
111
|
+
self._buf.write("<CardGrid>\n")
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
def card_grid_end(self) -> "MdxWriter":
|
|
115
|
+
self._buf.write("</CardGrid>\n\n")
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
def card(
|
|
119
|
+
self, title: str, icon: str, body: str,
|
|
120
|
+
) -> "MdxWriter":
|
|
121
|
+
line1 = f' <Card title="{title}" icon="{icon}">'
|
|
122
|
+
line2 = f" {body}"
|
|
123
|
+
self._buf.write(f"{line1}\n{line2}\n </Card>\n")
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
# ── Tabs ─────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
def tabs_start(self) -> "MdxWriter":
|
|
129
|
+
self._buf.write("<Tabs>\n")
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
def tabs_end(self) -> "MdxWriter":
|
|
133
|
+
self._buf.write("</Tabs>\n\n")
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
def tab_start(self, label: str) -> "MdxWriter":
|
|
137
|
+
tag = f' <TabItem label="{label}">'
|
|
138
|
+
self._buf.write(f"{tag}\n\n")
|
|
139
|
+
return self
|
|
140
|
+
|
|
141
|
+
def tab_end(self) -> "MdxWriter":
|
|
142
|
+
self._buf.write(" </TabItem>\n")
|
|
143
|
+
return self
|
|
144
|
+
|
|
145
|
+
# ── Mermaid ──────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
def mermaid_pie(
|
|
148
|
+
self, title: str, data: dict[str, int | float],
|
|
149
|
+
) -> "MdxWriter":
|
|
150
|
+
"""Write a Mermaid pie chart."""
|
|
151
|
+
self._buf.write("```mermaid\npie showData\n")
|
|
152
|
+
self._buf.write(f" title {title}\n")
|
|
153
|
+
for label, value in data.items():
|
|
154
|
+
if value > 0:
|
|
155
|
+
self._buf.write(
|
|
156
|
+
' "' + label + '" : ' + str(value) + '\n'
|
|
157
|
+
)
|
|
158
|
+
self._buf.write("```\n\n")
|
|
159
|
+
return self
|
|
160
|
+
|
|
161
|
+
def mermaid_graph(self, lines: list[str]) -> "MdxWriter":
|
|
162
|
+
"""Write a raw Mermaid graph block."""
|
|
163
|
+
self._buf.write("```mermaid\n")
|
|
164
|
+
self._buf.write("\n".join(lines))
|
|
165
|
+
self._buf.write("\n```\n\n")
|
|
166
|
+
return self
|
|
167
|
+
|
|
168
|
+
# ── Tables ───────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
def table(
|
|
171
|
+
self,
|
|
172
|
+
headers: list[str],
|
|
173
|
+
rows: list[list[str]],
|
|
174
|
+
alignments: list[str] | None = None,
|
|
175
|
+
) -> "MdxWriter":
|
|
176
|
+
"""Write a Markdown table.
|
|
177
|
+
|
|
178
|
+
``alignments`` can contain ``"left"``, ``"center"``, or ``"right"``
|
|
179
|
+
per column. Defaults to left-aligned.
|
|
180
|
+
"""
|
|
181
|
+
if alignments is None:
|
|
182
|
+
alignments = ["left"] * len(headers)
|
|
183
|
+
|
|
184
|
+
sep_map = {
|
|
185
|
+
"left": "---",
|
|
186
|
+
"center": ":---:",
|
|
187
|
+
"right": "---:",
|
|
188
|
+
}
|
|
189
|
+
header_line = "| " + " | ".join(headers) + " |"
|
|
190
|
+
sep_line = "| " + " | ".join(
|
|
191
|
+
sep_map.get(a, "---") for a in alignments
|
|
192
|
+
) + " |"
|
|
193
|
+
|
|
194
|
+
self._buf.write(header_line + "\n")
|
|
195
|
+
self._buf.write(sep_line + "\n")
|
|
196
|
+
for row in rows:
|
|
197
|
+
self._buf.write("| " + " | ".join(row) + " |\n")
|
|
198
|
+
self._buf.write("\n")
|
|
199
|
+
return self
|
|
200
|
+
|
|
201
|
+
# ── Details / collapsible ────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
def details_start(self, summary: str) -> "MdxWriter":
|
|
204
|
+
self._buf.write(f"<details>\n<summary>{summary}</summary>\n\n")
|
|
205
|
+
return self
|
|
206
|
+
|
|
207
|
+
def details_end(self) -> "MdxWriter":
|
|
208
|
+
self._buf.write("\n</details>\n\n")
|
|
209
|
+
return self
|
|
210
|
+
|
|
211
|
+
# ── Render ───────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
def render(self) -> str:
|
|
214
|
+
"""Return the accumulated MDX content as a string."""
|
|
215
|
+
return self._buf.getvalue()
|
|
216
|
+
|
|
217
|
+
def write_to(self, path: str) -> None:
|
|
218
|
+
"""Write the accumulated MDX content to a file."""
|
|
219
|
+
with open(path, "w") as f:
|
|
220
|
+
f.write(self.render())
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Server configuration models."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ServerConfig(BaseModel):
|
|
7
|
+
"""Configuration for the unified HTTP + gRPC server."""
|
|
8
|
+
|
|
9
|
+
http_host: str = "0.0.0.0"
|
|
10
|
+
http_port: int = 8000
|
|
11
|
+
grpc_host: str = "0.0.0.0"
|
|
12
|
+
grpc_port: int = 50051
|
|
13
|
+
log_level: str = "info"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""KBVE Nx workspace integrations — graph analysis and security auditing."""
|
|
2
|
+
|
|
3
|
+
from .graph import ( # noqa: F401
|
|
4
|
+
parse_graph,
|
|
5
|
+
group_projects_by_type,
|
|
6
|
+
collect_edges,
|
|
7
|
+
top_hubs,
|
|
8
|
+
ProjectRow,
|
|
9
|
+
)
|
|
10
|
+
from .security import ( # noqa: F401
|
|
11
|
+
normalize_severity,
|
|
12
|
+
parse_npm,
|
|
13
|
+
parse_cargo,
|
|
14
|
+
parse_python,
|
|
15
|
+
parse_codeql,
|
|
16
|
+
parse_dependabot,
|
|
17
|
+
build_summary,
|
|
18
|
+
parse_all_ecosystems,
|
|
19
|
+
SEVERITY_ORDER,
|
|
20
|
+
)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Nx project graph parsing and analysis.
|
|
2
|
+
|
|
3
|
+
Parses Nx graph JSON into structured data for downstream rendering
|
|
4
|
+
(MDX, JSON reports, API responses, etc.).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ProjectRow:
|
|
18
|
+
"""A single project extracted from the Nx graph."""
|
|
19
|
+
|
|
20
|
+
name: str
|
|
21
|
+
project_type: str
|
|
22
|
+
root: str
|
|
23
|
+
dep_count: int
|
|
24
|
+
dependent_count: int
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class GraphData:
|
|
29
|
+
"""Parsed and analysed Nx project graph."""
|
|
30
|
+
|
|
31
|
+
nodes: dict[str, Any]
|
|
32
|
+
deps: dict[str, list[dict[str, str]]]
|
|
33
|
+
rows: list[ProjectRow]
|
|
34
|
+
by_type: dict[str, list[str]]
|
|
35
|
+
edges: set[tuple[str, str]]
|
|
36
|
+
edges_by_source: dict[str, list[str]]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def mermaid_id(name: str) -> str:
|
|
40
|
+
"""Sanitize a project name into a valid Mermaid node ID."""
|
|
41
|
+
return re.sub(r"[^a-zA-Z0-9_]", "_", name)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parse_graph(source: str | Path | dict) -> GraphData:
|
|
45
|
+
"""Parse an Nx graph from a file path, JSON string, or dict.
|
|
46
|
+
|
|
47
|
+
Returns a fully analysed ``GraphData`` with rows, groupings, and edges
|
|
48
|
+
ready for rendering.
|
|
49
|
+
"""
|
|
50
|
+
if isinstance(source, dict):
|
|
51
|
+
data = source
|
|
52
|
+
elif isinstance(source, Path) or (
|
|
53
|
+
isinstance(source, str) and not source.lstrip().startswith("{")
|
|
54
|
+
):
|
|
55
|
+
with open(source) as f:
|
|
56
|
+
data = json.load(f)
|
|
57
|
+
else:
|
|
58
|
+
data = json.loads(source)
|
|
59
|
+
|
|
60
|
+
nodes: dict[str, Any] = data["graph"]["nodes"]
|
|
61
|
+
deps: dict[str, list[dict[str, str]]] = data["graph"]["dependencies"]
|
|
62
|
+
|
|
63
|
+
by_type = group_projects_by_type(nodes)
|
|
64
|
+
rows = _build_rows(nodes, deps)
|
|
65
|
+
edges, edges_by_source = collect_edges(deps)
|
|
66
|
+
|
|
67
|
+
return GraphData(
|
|
68
|
+
nodes=nodes,
|
|
69
|
+
deps=deps,
|
|
70
|
+
rows=rows,
|
|
71
|
+
by_type=by_type,
|
|
72
|
+
edges=edges,
|
|
73
|
+
edges_by_source=edges_by_source,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def group_projects_by_type(
|
|
78
|
+
nodes: dict[str, Any],
|
|
79
|
+
) -> dict[str, list[str]]:
|
|
80
|
+
"""Group project names by their Nx project type."""
|
|
81
|
+
by_type: dict[str, list[str]] = {}
|
|
82
|
+
for name, node in sorted(nodes.items()):
|
|
83
|
+
ptype = node.get("type", "unknown")
|
|
84
|
+
by_type.setdefault(ptype, []).append(name)
|
|
85
|
+
return by_type
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def collect_edges(
|
|
89
|
+
deps: dict[str, list[dict[str, str]]],
|
|
90
|
+
) -> tuple[set[tuple[str, str]], dict[str, list[str]]]:
|
|
91
|
+
"""Collect unique directed edges from Nx dependency data.
|
|
92
|
+
|
|
93
|
+
Returns ``(edge_set, edges_by_source)``.
|
|
94
|
+
"""
|
|
95
|
+
seen: set[tuple[str, str]] = set()
|
|
96
|
+
by_source: dict[str, list[str]] = {}
|
|
97
|
+
for dep_list in deps.values():
|
|
98
|
+
for d in dep_list:
|
|
99
|
+
edge = (d["source"], d["target"])
|
|
100
|
+
if edge not in seen:
|
|
101
|
+
seen.add(edge)
|
|
102
|
+
by_source.setdefault(d["source"], []).append(d["target"])
|
|
103
|
+
return seen, by_source
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def top_hubs(rows: list[ProjectRow], n: int = 5) -> list[ProjectRow]:
|
|
107
|
+
"""Return the *n* most-depended-on projects."""
|
|
108
|
+
return sorted(rows, key=lambda r: r.dependent_count, reverse=True)[:n]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _build_rows(
|
|
112
|
+
nodes: dict[str, Any],
|
|
113
|
+
deps: dict[str, list[dict[str, str]]],
|
|
114
|
+
) -> list[ProjectRow]:
|
|
115
|
+
"""Build a sorted list of ``ProjectRow`` from raw graph data."""
|
|
116
|
+
rows: list[ProjectRow] = []
|
|
117
|
+
for name in sorted(nodes):
|
|
118
|
+
node = nodes[name]
|
|
119
|
+
dep_count = len(deps.get(name, []))
|
|
120
|
+
dependents = sum(
|
|
121
|
+
1 for d_list in deps.values() for d in d_list
|
|
122
|
+
if d["target"] == name
|
|
123
|
+
)
|
|
124
|
+
rows.append(ProjectRow(
|
|
125
|
+
name=name,
|
|
126
|
+
project_type=node.get("type", "unknown"),
|
|
127
|
+
root=node.get("data", {}).get("root", ""),
|
|
128
|
+
dep_count=dep_count,
|
|
129
|
+
dependent_count=dependents,
|
|
130
|
+
))
|
|
131
|
+
return rows
|