cartograph-python 0.2.0__cp38-abi3-win_amd64.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.
- cartograph/__init__.py +69 -0
- cartograph/__init__.pyi +483 -0
- cartograph/_cartograph.pyd +0 -0
- cartograph/aio.py +238 -0
- cartograph/codegraph.py +247 -0
- cartograph/dataframes.py +42 -0
- cartograph/igraph_adapter.py +51 -0
- cartograph/jupyter.py +38 -0
- cartograph/models.py +64 -0
- cartograph/networkx_adapter.py +35 -0
- cartograph/py.typed +1 -0
- cartograph_python-0.2.0.dist-info/METADATA +96 -0
- cartograph_python-0.2.0.dist-info/RECORD +15 -0
- cartograph_python-0.2.0.dist-info/WHEEL +4 -0
- cartograph_python-0.2.0.dist-info/sboms/cartograph-python.cyclonedx.json +9324 -0
cartograph/aio.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Asynchronous CodeGraph engine interface using asyncio and thread offloading."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
7
|
+
|
|
8
|
+
from cartograph.codegraph import CodeGraph
|
|
9
|
+
from cartograph.models import (
|
|
10
|
+
ComplexityReport,
|
|
11
|
+
DeadCodeReport,
|
|
12
|
+
DiagnosisReport,
|
|
13
|
+
DiagramResult,
|
|
14
|
+
GraphStats,
|
|
15
|
+
ImpactResult,
|
|
16
|
+
IndexStats,
|
|
17
|
+
LintArchReport,
|
|
18
|
+
LocateResult,
|
|
19
|
+
LogicReport,
|
|
20
|
+
RouteTrace,
|
|
21
|
+
TestSelectorResult,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
import igraph as ig
|
|
26
|
+
import networkx as nx
|
|
27
|
+
import pandas as pd
|
|
28
|
+
import polars as pl
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AsyncCodeGraph:
|
|
32
|
+
"""Asynchronous CodeGraph engine interface for async services, agents, and FastAPI."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
path: str | Path | None = None,
|
|
37
|
+
profile: str = "default",
|
|
38
|
+
in_memory: bool = False,
|
|
39
|
+
) -> None:
|
|
40
|
+
self._sync_graph = CodeGraph(path=path, profile=profile, in_memory=in_memory)
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
async def open(
|
|
44
|
+
cls,
|
|
45
|
+
path: str | Path | None = None,
|
|
46
|
+
profile: str = "default",
|
|
47
|
+
in_memory: bool = False,
|
|
48
|
+
) -> AsyncCodeGraph:
|
|
49
|
+
"""Asynchronously open or initialize an AsyncCodeGraph instance."""
|
|
50
|
+
return await asyncio.to_thread(cls, path=path, profile=profile, in_memory=in_memory)
|
|
51
|
+
|
|
52
|
+
async def __aenter__(self) -> AsyncCodeGraph:
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
56
|
+
await self.close()
|
|
57
|
+
|
|
58
|
+
async def close(self) -> None:
|
|
59
|
+
"""Asynchronously close database connections and release resources."""
|
|
60
|
+
await asyncio.to_thread(self._sync_graph.close)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def is_open(self) -> bool:
|
|
64
|
+
"""Check if database connection is active."""
|
|
65
|
+
return self._sync_graph.is_open
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def profile(self) -> str:
|
|
69
|
+
"""Active storage profile name."""
|
|
70
|
+
return self._sync_graph.profile
|
|
71
|
+
|
|
72
|
+
async def index(
|
|
73
|
+
self,
|
|
74
|
+
force: bool = False,
|
|
75
|
+
staged: bool = False,
|
|
76
|
+
branch: str | None = None,
|
|
77
|
+
threads: int | None = None,
|
|
78
|
+
) -> IndexStats:
|
|
79
|
+
"""Scan workspace and populate the knowledge graph asynchronously."""
|
|
80
|
+
return await asyncio.to_thread(
|
|
81
|
+
self._sync_graph.index, force=force, staged=staged, branch=branch, threads=threads
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
async def status(self) -> GraphStats:
|
|
85
|
+
"""Return aggregate statistics and health of the graph store asynchronously."""
|
|
86
|
+
return await asyncio.to_thread(self._sync_graph.status)
|
|
87
|
+
|
|
88
|
+
async def locate(
|
|
89
|
+
self,
|
|
90
|
+
query: str,
|
|
91
|
+
code: bool = False,
|
|
92
|
+
limit: int = 10,
|
|
93
|
+
) -> LocateResult:
|
|
94
|
+
"""Natural language or exact code subgraph localization asynchronously."""
|
|
95
|
+
return await asyncio.to_thread(
|
|
96
|
+
self._sync_graph.locate, query=query, code=code, limit=limit
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
async def impact(
|
|
100
|
+
self,
|
|
101
|
+
target: str,
|
|
102
|
+
branch: str | None = None,
|
|
103
|
+
staged: bool = False,
|
|
104
|
+
depth: int = 3,
|
|
105
|
+
) -> ImpactResult:
|
|
106
|
+
"""Perform topological blast-radius and risk analysis asynchronously."""
|
|
107
|
+
return await asyncio.to_thread(
|
|
108
|
+
self._sync_graph.impact, target=target, branch=branch, staged=staged, depth=depth
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
async def test_selector(
|
|
112
|
+
self,
|
|
113
|
+
target: str,
|
|
114
|
+
branch: str | None = None,
|
|
115
|
+
) -> TestSelectorResult:
|
|
116
|
+
"""Predict minimal test set exercising the changed target asynchronously."""
|
|
117
|
+
return await asyncio.to_thread(
|
|
118
|
+
self._sync_graph.test_selector, target=target, branch=branch
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
async def routes(
|
|
122
|
+
self,
|
|
123
|
+
filter_query: str | None = None,
|
|
124
|
+
) -> list[RouteTrace]:
|
|
125
|
+
"""Discover API routes, controller endpoints, and full-stack traces asynchronously."""
|
|
126
|
+
return await asyncio.to_thread(
|
|
127
|
+
self._sync_graph.routes, filter_query=filter_query
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
async def lint_arch(
|
|
131
|
+
self,
|
|
132
|
+
strict: bool = False,
|
|
133
|
+
) -> LintArchReport:
|
|
134
|
+
"""Detect architecture boundary violations and circular dependencies asynchronously."""
|
|
135
|
+
return await asyncio.to_thread(
|
|
136
|
+
self._sync_graph.lint_arch, strict=strict
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def complexity(
|
|
140
|
+
self,
|
|
141
|
+
top: int = 15,
|
|
142
|
+
danger_zone: bool = False,
|
|
143
|
+
) -> ComplexityReport:
|
|
144
|
+
"""Analyze cyclomatic/cognitive complexity and test deficits asynchronously."""
|
|
145
|
+
return await asyncio.to_thread(
|
|
146
|
+
self._sync_graph.complexity, top=top, danger_zone=danger_zone
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
async def deadcode(
|
|
150
|
+
self,
|
|
151
|
+
min_lines: int = 20,
|
|
152
|
+
) -> DeadCodeReport:
|
|
153
|
+
"""Detect unreachable symbols and ghost dependencies asynchronously."""
|
|
154
|
+
return await asyncio.to_thread(
|
|
155
|
+
self._sync_graph.deadcode, min_lines=min_lines
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
async def extract_logic(
|
|
159
|
+
self,
|
|
160
|
+
query: str | None = None,
|
|
161
|
+
symbol: str | None = None,
|
|
162
|
+
concept: str | None = None,
|
|
163
|
+
deep: bool = False,
|
|
164
|
+
) -> LogicReport:
|
|
165
|
+
"""Extract business rules, invariants, and constraints asynchronously."""
|
|
166
|
+
return await asyncio.to_thread(
|
|
167
|
+
self._sync_graph.extract_logic, query=query, symbol=symbol, concept=concept, deep=deep
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
async def diagnose(
|
|
171
|
+
self,
|
|
172
|
+
trace_or_log: str,
|
|
173
|
+
file_path: str | None = None,
|
|
174
|
+
deep: bool = False,
|
|
175
|
+
offline: bool = True,
|
|
176
|
+
) -> DiagnosisReport:
|
|
177
|
+
"""Perform 8-stage topological root-cause fault diagnosis asynchronously."""
|
|
178
|
+
return await asyncio.to_thread(
|
|
179
|
+
self._sync_graph.diagnose,
|
|
180
|
+
trace_or_log=trace_or_log,
|
|
181
|
+
file_path=file_path,
|
|
182
|
+
deep=deep,
|
|
183
|
+
offline=offline,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
async def diagram(
|
|
187
|
+
self,
|
|
188
|
+
symbol_or_concept: str | None = None,
|
|
189
|
+
diagram_type: str = "architecture",
|
|
190
|
+
format: str = "mermaid",
|
|
191
|
+
depth: int = 2,
|
|
192
|
+
direction: str = "both",
|
|
193
|
+
max_nodes: int = 30,
|
|
194
|
+
collapse_modules: bool = False,
|
|
195
|
+
theme: str = "dark",
|
|
196
|
+
) -> DiagramResult:
|
|
197
|
+
"""Render architecture, call graph, and component diagrams asynchronously."""
|
|
198
|
+
return await asyncio.to_thread(
|
|
199
|
+
self._sync_graph.diagram,
|
|
200
|
+
symbol_or_concept=symbol_or_concept,
|
|
201
|
+
diagram_type=diagram_type,
|
|
202
|
+
format=format,
|
|
203
|
+
depth=depth,
|
|
204
|
+
direction=direction,
|
|
205
|
+
max_nodes=max_nodes,
|
|
206
|
+
collapse_modules=collapse_modules,
|
|
207
|
+
theme=theme,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# ── Graph Data Science Integrations ─────────────────────────
|
|
211
|
+
|
|
212
|
+
async def to_networkx(self) -> nx.DiGraph:
|
|
213
|
+
"""Stream codebase graph into a NetworkX DiGraph asynchronously."""
|
|
214
|
+
return await asyncio.to_thread(self._sync_graph.to_networkx)
|
|
215
|
+
|
|
216
|
+
async def to_igraph(self) -> ig.Graph:
|
|
217
|
+
"""Stream codebase graph into an igraph Graph asynchronously."""
|
|
218
|
+
return await asyncio.to_thread(self._sync_graph.to_igraph)
|
|
219
|
+
|
|
220
|
+
async def nodes_df(self, engine: Literal["polars", "pandas"] = "polars") -> Any:
|
|
221
|
+
"""Return nodes dataframe using Polars or Pandas asynchronously."""
|
|
222
|
+
return await asyncio.to_thread(self._sync_graph.nodes_df, engine=engine)
|
|
223
|
+
|
|
224
|
+
async def edges_df(self, engine: Literal["polars", "pandas"] = "polars") -> Any:
|
|
225
|
+
"""Return edges dataframe using Polars or Pandas asynchronously."""
|
|
226
|
+
return await asyncio.to_thread(self._sync_graph.edges_df, engine=engine)
|
|
227
|
+
|
|
228
|
+
async def complexity_df(self, engine: Literal["polars", "pandas"] = "polars") -> Any:
|
|
229
|
+
"""Return complexity hotspots dataframe using Polars or Pandas asynchronously."""
|
|
230
|
+
return await asyncio.to_thread(self._sync_graph.complexity_df, engine=engine)
|
|
231
|
+
|
|
232
|
+
async def to_polars(self) -> dict[str, pl.DataFrame]:
|
|
233
|
+
"""Export all primary tables to Polars DataFrames asynchronously."""
|
|
234
|
+
return await asyncio.to_thread(self._sync_graph.to_polars)
|
|
235
|
+
|
|
236
|
+
async def to_pandas(self) -> dict[str, pd.DataFrame]:
|
|
237
|
+
"""Export all primary tables to Pandas DataFrames asynchronously."""
|
|
238
|
+
return await asyncio.to_thread(self._sync_graph.to_pandas)
|
cartograph/codegraph.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Synchronous CodeGraph engine interface."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
6
|
+
|
|
7
|
+
from cartograph._cartograph import (
|
|
8
|
+
CodeGraph as _NativeCodeGraph,
|
|
9
|
+
ComplexityReport,
|
|
10
|
+
DeadCodeReport,
|
|
11
|
+
DiagnosisReport,
|
|
12
|
+
DiagramResult,
|
|
13
|
+
GraphStats,
|
|
14
|
+
ImpactResult,
|
|
15
|
+
IndexStats,
|
|
16
|
+
LintArchReport,
|
|
17
|
+
LocateResult,
|
|
18
|
+
LogicReport,
|
|
19
|
+
Node,
|
|
20
|
+
RouteTrace,
|
|
21
|
+
TestSelectorResult,
|
|
22
|
+
)
|
|
23
|
+
from cartograph.dataframes import columnar_to_pandas, columnar_to_polars
|
|
24
|
+
from cartograph.igraph_adapter import stream_to_igraph
|
|
25
|
+
from cartograph.networkx_adapter import stream_to_networkx
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
import igraph as ig
|
|
29
|
+
import networkx as nx
|
|
30
|
+
import pandas as pd
|
|
31
|
+
import polars as pl
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CodeGraph:
|
|
35
|
+
"""High-performance code graph engine and knowledge graph for codebases."""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
path: str | Path | None = None,
|
|
40
|
+
profile: str = "default",
|
|
41
|
+
in_memory: bool = False,
|
|
42
|
+
) -> None:
|
|
43
|
+
path_str = str(Path(path).resolve()) if path is not None else None
|
|
44
|
+
self._inner = _NativeCodeGraph.open(path=path_str, profile=profile, in_memory=in_memory)
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def open(
|
|
48
|
+
cls,
|
|
49
|
+
path: str | Path | None = None,
|
|
50
|
+
profile: str = "default",
|
|
51
|
+
in_memory: bool = False,
|
|
52
|
+
) -> CodeGraph:
|
|
53
|
+
"""Open or initialize a CodeGraph instance."""
|
|
54
|
+
return cls(path=path, profile=profile, in_memory=in_memory)
|
|
55
|
+
|
|
56
|
+
def __enter__(self) -> CodeGraph:
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
60
|
+
self.close()
|
|
61
|
+
|
|
62
|
+
def close(self) -> None:
|
|
63
|
+
"""Close database connections and release resources."""
|
|
64
|
+
self._inner.close()
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def is_open(self) -> bool:
|
|
68
|
+
"""Check if database connection is active."""
|
|
69
|
+
return self._inner.is_open()
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def profile(self) -> str:
|
|
73
|
+
"""Active storage profile name."""
|
|
74
|
+
return self._inner.profile
|
|
75
|
+
|
|
76
|
+
def index(
|
|
77
|
+
self,
|
|
78
|
+
force: bool = False,
|
|
79
|
+
staged: bool = False,
|
|
80
|
+
branch: str | None = None,
|
|
81
|
+
threads: int | None = None,
|
|
82
|
+
) -> IndexStats:
|
|
83
|
+
"""Scan workspace and populate the knowledge graph."""
|
|
84
|
+
return self._inner.index(force=force, staged=staged, branch=branch, threads=threads)
|
|
85
|
+
|
|
86
|
+
def status(self) -> GraphStats:
|
|
87
|
+
"""Return aggregate statistics and health of the graph store."""
|
|
88
|
+
return self._inner.status()
|
|
89
|
+
|
|
90
|
+
def locate(
|
|
91
|
+
self,
|
|
92
|
+
query: str,
|
|
93
|
+
code: bool = False,
|
|
94
|
+
limit: int = 10,
|
|
95
|
+
) -> LocateResult:
|
|
96
|
+
"""Natural language or exact code subgraph localization."""
|
|
97
|
+
return self._inner.locate(query=query, code=code, limit=limit)
|
|
98
|
+
|
|
99
|
+
def impact(
|
|
100
|
+
self,
|
|
101
|
+
target: str,
|
|
102
|
+
branch: str | None = None,
|
|
103
|
+
staged: bool = False,
|
|
104
|
+
depth: int = 3,
|
|
105
|
+
) -> ImpactResult:
|
|
106
|
+
"""Perform topological blast-radius and risk analysis."""
|
|
107
|
+
return self._inner.impact(target=target, branch=branch, staged=staged, depth=depth)
|
|
108
|
+
|
|
109
|
+
def test_selector(
|
|
110
|
+
self,
|
|
111
|
+
target: str,
|
|
112
|
+
branch: str | None = None,
|
|
113
|
+
) -> TestSelectorResult:
|
|
114
|
+
"""Predict minimal test set exercising the changed target."""
|
|
115
|
+
return self._inner.test_selector(target=target, branch=branch)
|
|
116
|
+
|
|
117
|
+
def routes(
|
|
118
|
+
self,
|
|
119
|
+
filter_query: str | None = None,
|
|
120
|
+
) -> list[RouteTrace]:
|
|
121
|
+
"""Discover API routes, controller endpoints, and full-stack traces."""
|
|
122
|
+
return self._inner.routes(filter_query=filter_query)
|
|
123
|
+
|
|
124
|
+
def lint_arch(
|
|
125
|
+
self,
|
|
126
|
+
strict: bool = False,
|
|
127
|
+
) -> LintArchReport:
|
|
128
|
+
"""Detect architecture boundary violations and circular dependencies."""
|
|
129
|
+
return self._inner.lint_arch(strict=strict)
|
|
130
|
+
|
|
131
|
+
def complexity(
|
|
132
|
+
self,
|
|
133
|
+
top: int = 15,
|
|
134
|
+
danger_zone: bool = False,
|
|
135
|
+
) -> ComplexityReport:
|
|
136
|
+
"""Analyze cyclomatic/cognitive complexity and test deficits."""
|
|
137
|
+
return self._inner.complexity(top=top, danger_zone=danger_zone)
|
|
138
|
+
|
|
139
|
+
def deadcode(
|
|
140
|
+
self,
|
|
141
|
+
min_lines: int = 20,
|
|
142
|
+
) -> DeadCodeReport:
|
|
143
|
+
"""Detect unreachable symbols and ghost dependencies."""
|
|
144
|
+
return self._inner.deadcode(min_lines=min_lines)
|
|
145
|
+
|
|
146
|
+
def extract_logic(
|
|
147
|
+
self,
|
|
148
|
+
query: str | None = None,
|
|
149
|
+
symbol: str | None = None,
|
|
150
|
+
concept: str | None = None,
|
|
151
|
+
deep: bool = False,
|
|
152
|
+
) -> LogicReport:
|
|
153
|
+
"""Extract business rules, invariants, and constraints."""
|
|
154
|
+
return self._inner.extract_logic(query=query, symbol=symbol, concept=concept, deep=deep)
|
|
155
|
+
|
|
156
|
+
def diagnose(
|
|
157
|
+
self,
|
|
158
|
+
trace_or_log: str,
|
|
159
|
+
file_path: str | None = None,
|
|
160
|
+
deep: bool = False,
|
|
161
|
+
offline: bool = True,
|
|
162
|
+
) -> DiagnosisReport:
|
|
163
|
+
"""Perform 8-stage topological root-cause fault diagnosis."""
|
|
164
|
+
return self._inner.diagnose(
|
|
165
|
+
trace_or_log=trace_or_log, file_path=file_path, deep=deep, offline=offline
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def diagram(
|
|
169
|
+
self,
|
|
170
|
+
symbol_or_concept: str | None = None,
|
|
171
|
+
diagram_type: str = "architecture",
|
|
172
|
+
format: str = "mermaid",
|
|
173
|
+
depth: int = 2,
|
|
174
|
+
direction: str = "both",
|
|
175
|
+
max_nodes: int = 30,
|
|
176
|
+
collapse_modules: bool = False,
|
|
177
|
+
theme: str = "dark",
|
|
178
|
+
) -> DiagramResult:
|
|
179
|
+
"""Render architecture, call graph, and component diagrams."""
|
|
180
|
+
return self._inner.diagram(
|
|
181
|
+
symbol_or_concept=symbol_or_concept,
|
|
182
|
+
diagram_type=diagram_type,
|
|
183
|
+
format=format,
|
|
184
|
+
depth=depth,
|
|
185
|
+
direction=direction,
|
|
186
|
+
max_nodes=max_nodes,
|
|
187
|
+
collapse_modules=collapse_modules,
|
|
188
|
+
theme=theme,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# ── Graph Data Science Integrations ─────────────────────────
|
|
192
|
+
|
|
193
|
+
def to_networkx(self) -> nx.DiGraph:
|
|
194
|
+
"""Stream codebase graph into a NetworkX DiGraph."""
|
|
195
|
+
streamer = self._inner.get_streamer()
|
|
196
|
+
return stream_to_networkx(streamer)
|
|
197
|
+
|
|
198
|
+
def to_igraph(self) -> ig.Graph:
|
|
199
|
+
"""Stream codebase graph into an igraph Graph."""
|
|
200
|
+
streamer = self._inner.get_streamer()
|
|
201
|
+
return stream_to_igraph(streamer)
|
|
202
|
+
|
|
203
|
+
def nodes_df(self, engine: Literal["polars", "pandas"] = "polars") -> Any:
|
|
204
|
+
"""Return nodes dataframe using Polars or Pandas."""
|
|
205
|
+
col_dict = self._inner.nodes_columnar()
|
|
206
|
+
if engine == "polars":
|
|
207
|
+
return columnar_to_polars(col_dict)
|
|
208
|
+
elif engine == "pandas":
|
|
209
|
+
return columnar_to_pandas(col_dict)
|
|
210
|
+
else:
|
|
211
|
+
raise ValueError(f"Unsupported engine: {engine}. Choose 'polars' or 'pandas'")
|
|
212
|
+
|
|
213
|
+
def edges_df(self, engine: Literal["polars", "pandas"] = "polars") -> Any:
|
|
214
|
+
"""Return edges dataframe using Polars or Pandas."""
|
|
215
|
+
col_dict = self._inner.edges_columnar()
|
|
216
|
+
if engine == "polars":
|
|
217
|
+
return columnar_to_polars(col_dict)
|
|
218
|
+
elif engine == "pandas":
|
|
219
|
+
return columnar_to_pandas(col_dict)
|
|
220
|
+
else:
|
|
221
|
+
raise ValueError(f"Unsupported engine: {engine}. Choose 'polars' or 'pandas'")
|
|
222
|
+
|
|
223
|
+
def complexity_df(self, engine: Literal["polars", "pandas"] = "polars") -> Any:
|
|
224
|
+
"""Return complexity hotspots dataframe using Polars or Pandas."""
|
|
225
|
+
col_dict = self._inner.complexity_columnar()
|
|
226
|
+
if engine == "polars":
|
|
227
|
+
return columnar_to_polars(col_dict)
|
|
228
|
+
elif engine == "pandas":
|
|
229
|
+
return columnar_to_pandas(col_dict)
|
|
230
|
+
else:
|
|
231
|
+
raise ValueError(f"Unsupported engine: {engine}. Choose 'polars' or 'pandas'")
|
|
232
|
+
|
|
233
|
+
def to_polars(self) -> dict[str, pl.DataFrame]:
|
|
234
|
+
"""Export all primary tables (nodes, edges, complexity) to Polars DataFrames."""
|
|
235
|
+
return {
|
|
236
|
+
"nodes": self.nodes_df("polars"),
|
|
237
|
+
"edges": self.edges_df("polars"),
|
|
238
|
+
"complexity": self.complexity_df("polars"),
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
def to_pandas(self) -> dict[str, pd.DataFrame]:
|
|
242
|
+
"""Export all primary tables (nodes, edges, complexity) to Pandas DataFrames."""
|
|
243
|
+
return {
|
|
244
|
+
"nodes": self.nodes_df("pandas"),
|
|
245
|
+
"edges": self.edges_df("pandas"),
|
|
246
|
+
"complexity": self.complexity_df("pandas"),
|
|
247
|
+
}
|
cartograph/dataframes.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Dataframe conversions for Polars, Pandas, and PyArrow."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import polars as pl
|
|
9
|
+
import pyarrow as pa
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def columnar_to_polars(col_dict: dict[str, Any]) -> pl.DataFrame:
|
|
13
|
+
"""Convert a columnar dictionary directly into a Polars DataFrame."""
|
|
14
|
+
try:
|
|
15
|
+
import polars as pl
|
|
16
|
+
except ImportError as e:
|
|
17
|
+
raise ImportError(
|
|
18
|
+
"polars is not installed. Install it with: pip install 'cartograph[polars]'"
|
|
19
|
+
) from e
|
|
20
|
+
return pl.DataFrame(col_dict)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def columnar_to_pandas(col_dict: dict[str, Any]) -> pd.DataFrame:
|
|
24
|
+
"""Convert a columnar dictionary directly into a Pandas DataFrame."""
|
|
25
|
+
try:
|
|
26
|
+
import pandas as pd
|
|
27
|
+
except ImportError as e:
|
|
28
|
+
raise ImportError(
|
|
29
|
+
"pandas is not installed. Install it with: pip install 'cartograph[pandas]'"
|
|
30
|
+
) from e
|
|
31
|
+
return pd.DataFrame(col_dict)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def columnar_to_arrow(col_dict: dict[str, Any]) -> pa.Table:
|
|
35
|
+
"""Convert a columnar dictionary directly into a PyArrow Table."""
|
|
36
|
+
try:
|
|
37
|
+
import pyarrow as pa
|
|
38
|
+
except ImportError as e:
|
|
39
|
+
raise ImportError(
|
|
40
|
+
"pyarrow is not installed. Install it with: pip install pyarrow"
|
|
41
|
+
) from e
|
|
42
|
+
return pa.Table.from_pydict(col_dict)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""igraph conversion adapter for cartograph."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
import igraph as ig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def stream_to_igraph(streamer: Any) -> ig.Graph:
|
|
11
|
+
"""Stream graph nodes and edges directly into an igraph Graph."""
|
|
12
|
+
try:
|
|
13
|
+
import igraph as ig
|
|
14
|
+
except ImportError as e:
|
|
15
|
+
raise ImportError(
|
|
16
|
+
"igraph is not installed. Install it with: pip install 'cartograph[igraph]'"
|
|
17
|
+
) from e
|
|
18
|
+
|
|
19
|
+
nodes = streamer.get_nodes()
|
|
20
|
+
edges = streamer.get_edges()
|
|
21
|
+
|
|
22
|
+
node_indices = {n[0]: i for i, n in enumerate(nodes)}
|
|
23
|
+
edge_tuples = []
|
|
24
|
+
edge_kinds = []
|
|
25
|
+
edge_weights = []
|
|
26
|
+
|
|
27
|
+
for src, tgt, kind, weight in edges:
|
|
28
|
+
if src in node_indices and tgt in node_indices:
|
|
29
|
+
edge_tuples.append((node_indices[src], node_indices[tgt]))
|
|
30
|
+
edge_kinds.append(kind)
|
|
31
|
+
edge_weights.append(weight)
|
|
32
|
+
|
|
33
|
+
g = ig.Graph(
|
|
34
|
+
n=len(nodes),
|
|
35
|
+
edges=edge_tuples,
|
|
36
|
+
directed=True,
|
|
37
|
+
vertex_attrs={
|
|
38
|
+
"name": [n[0] for n in nodes],
|
|
39
|
+
"symbol_name": [n[1] for n in nodes],
|
|
40
|
+
"kind": [n[2] for n in nodes],
|
|
41
|
+
"filepath": [n[3] for n in nodes],
|
|
42
|
+
"start_line": [n[4] for n in nodes],
|
|
43
|
+
"end_line": [n[5] for n in nodes],
|
|
44
|
+
"complexity": [n[6] for n in nodes],
|
|
45
|
+
},
|
|
46
|
+
edge_attrs={
|
|
47
|
+
"kind": edge_kinds,
|
|
48
|
+
"weight": edge_weights,
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
return g
|
cartograph/jupyter.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Jupyter and IPython display integrations for cartograph."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import html
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def render_mermaid_html(mermaid_code: str) -> str:
|
|
9
|
+
"""Render a mermaid diagram inside Jupyter / IPython notebook via HTML."""
|
|
10
|
+
escaped = html.escape(mermaid_code)
|
|
11
|
+
return f"""
|
|
12
|
+
<div class="cartograph-mermaid-container" style="background: #1e1e2e; padding: 16px; border-radius: 8px;">
|
|
13
|
+
<pre class="mermaid" style="background: transparent;">
|
|
14
|
+
{escaped}
|
|
15
|
+
</pre>
|
|
16
|
+
<script type="module">
|
|
17
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
|
|
18
|
+
mermaid.initialize({{ startOnLoad: true, theme: 'dark' }});
|
|
19
|
+
mermaid.run();
|
|
20
|
+
</script>
|
|
21
|
+
</div>
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RichMermaidDisplay:
|
|
26
|
+
"""Wrapper that displays mermaid diagrams natively in Jupyter."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, content: str):
|
|
29
|
+
self.content = content
|
|
30
|
+
|
|
31
|
+
def _repr_html_(self) -> str:
|
|
32
|
+
return render_mermaid_html(self.content)
|
|
33
|
+
|
|
34
|
+
def __str__(self) -> str:
|
|
35
|
+
return self.content
|
|
36
|
+
|
|
37
|
+
def __repr__(self) -> str:
|
|
38
|
+
return self.content
|
cartograph/models.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Data models and exception classes for Cartograph."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from cartograph._cartograph import (
|
|
7
|
+
AnalysisError,
|
|
8
|
+
BoundaryViolation,
|
|
9
|
+
BusinessRule,
|
|
10
|
+
CartographError,
|
|
11
|
+
ComplexityRecord,
|
|
12
|
+
ComplexityReport,
|
|
13
|
+
DatabaseError,
|
|
14
|
+
DeadCodeReport,
|
|
15
|
+
DeadSymbolItem,
|
|
16
|
+
DependencyCycle,
|
|
17
|
+
DiagnosisReport,
|
|
18
|
+
DiagramResult,
|
|
19
|
+
Edge,
|
|
20
|
+
FaultCandidate,
|
|
21
|
+
GraphStats,
|
|
22
|
+
GraphStreamer,
|
|
23
|
+
ImpactResult,
|
|
24
|
+
ImpactRoot,
|
|
25
|
+
IndexStats,
|
|
26
|
+
LintArchReport,
|
|
27
|
+
LocateResult,
|
|
28
|
+
LogicReport,
|
|
29
|
+
Node,
|
|
30
|
+
RouteTrace,
|
|
31
|
+
SelectedTest,
|
|
32
|
+
SymbolNotFoundError,
|
|
33
|
+
TestSelectorResult,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"AnalysisError",
|
|
38
|
+
"BoundaryViolation",
|
|
39
|
+
"BusinessRule",
|
|
40
|
+
"CartographError",
|
|
41
|
+
"ComplexityRecord",
|
|
42
|
+
"ComplexityReport",
|
|
43
|
+
"DatabaseError",
|
|
44
|
+
"DeadCodeReport",
|
|
45
|
+
"DeadSymbolItem",
|
|
46
|
+
"DependencyCycle",
|
|
47
|
+
"DiagnosisReport",
|
|
48
|
+
"DiagramResult",
|
|
49
|
+
"Edge",
|
|
50
|
+
"FaultCandidate",
|
|
51
|
+
"GraphStats",
|
|
52
|
+
"GraphStreamer",
|
|
53
|
+
"ImpactResult",
|
|
54
|
+
"ImpactRoot",
|
|
55
|
+
"IndexStats",
|
|
56
|
+
"LintArchReport",
|
|
57
|
+
"LocateResult",
|
|
58
|
+
"LogicReport",
|
|
59
|
+
"Node",
|
|
60
|
+
"RouteTrace",
|
|
61
|
+
"SelectedTest",
|
|
62
|
+
"SymbolNotFoundError",
|
|
63
|
+
"TestSelectorResult",
|
|
64
|
+
]
|