pycode-kg 0.25.0__tar.gz → 0.26.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.
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/PKG-INFO +5 -5
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/README.md +4 -4
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/pyproject.toml +1 -1
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/__init__.py +1 -1
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/cli/cmd_snapshot.py +21 -14
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/mcp_server.py +1 -1
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/pycodekg_thorough_analysis.py +8 -1
- pycode_kg-0.26.0/src/pycode_kg/snapshots.py +357 -0
- pycode_kg-0.25.0/src/pycode_kg/snapshots.py +0 -531
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/LICENSE +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/.DS_Store +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/__main__.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/analysis/__init__.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/analysis/bridge.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/analysis/centrality.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/analysis/framework_detector.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/app.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/architecture.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/build_pycodekg_sqlite.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/cli/__init__.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/cli/cmd_analyze.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/cli/cmd_architecture.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/cli/cmd_bridges.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/cli/cmd_build.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/cli/cmd_build_full.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/cli/cmd_centrality.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/cli/cmd_explain.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/cli/cmd_framework_nodes.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/cli/cmd_hooks.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/cli/cmd_init.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/cli/cmd_mcp.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/cli/cmd_model.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/cli/cmd_query.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/cli/cmd_quilt.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/cli/cmd_viz.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/cli/main.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/cli/options.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/config.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/explain.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/graph.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/graph_html.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/index.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/kg.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/layout3d.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/module/__init__.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/module/base.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/module/extractor.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/module/types.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/pycodekg.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/ranking/__init__.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/ranking/cli_rank.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/ranking/coderank.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/render.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/report.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/resolution.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/scene3d.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/sql/004_add_centrality_table.sql +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/store.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/theme.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/utils.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/visitor.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/viz3d.py +0 -0
- {pycode_kg-0.25.0 → pycode_kg-0.26.0}/src/pycode_kg/viz3d_timeline.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pycode-kg
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.26.0
|
|
4
4
|
Summary: A tool to build a searchable knowledge graph from Python repositories
|
|
5
5
|
License-Expression: Elastic-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -60,7 +60,7 @@ Description-Content-Type: text/markdown
|
|
|
60
60
|
|
|
61
61
|
[](https://www.python.org/)
|
|
62
62
|
[](https://www.elastic.co/licensing/elastic-license)
|
|
63
|
-
[](https://github.com/Flux-Frontiers/pycode_kg/releases)
|
|
64
64
|
[](https://github.com/Flux-Frontiers/pycode_kg/actions/workflows/ci.yml)
|
|
65
65
|
[](https://python-poetry.org/)
|
|
66
66
|
[](https://zenodo.org/badge/latestdoi/1202379010)
|
|
@@ -255,7 +255,7 @@ src/pycode_kg/
|
|
|
255
255
|
└── viz3d_timeline.py # Metric history timeline
|
|
256
256
|
```
|
|
257
257
|
|
|
258
|
-
The MCP server, the CLI, and the Streamlit app are thin wrappers over the same store + index + ranking core — there is exactly one code path for each capability. The latest architectural deep-dive is in [docs/analysis_v0.
|
|
258
|
+
The MCP server, the CLI, and the Streamlit app are thin wrappers over the same store + index + ranking core — there is exactly one code path for each capability. The latest architectural deep-dive is in [docs/analysis_v0.26.0.md](docs/analysis_v0.26.0.md), produced (of course) by `pycodekg analyze` against this very repo.
|
|
259
259
|
|
|
260
260
|
---
|
|
261
261
|
|
|
@@ -282,13 +282,13 @@ If you use PyCodeKG in your research or project, please cite it:
|
|
|
282
282
|
|
|
283
283
|
[](https://zenodo.org/badge/latestdoi/1202379010)
|
|
284
284
|
|
|
285
|
-
> Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.
|
|
285
|
+
> Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.26.0) [Software]. Flux-Frontiers. https://doi.org/10.5281/zenodo.19737993
|
|
286
286
|
|
|
287
287
|
```bibtex
|
|
288
288
|
@software{suchanek_pycode_kg,
|
|
289
289
|
author = {Suchanek, Eric G.},
|
|
290
290
|
title = {{PyCodeKG}: A Knowledge Graph for Python Codebases},
|
|
291
|
-
version = {0.
|
|
291
|
+
version = {0.26.0},
|
|
292
292
|
year = {2026},
|
|
293
293
|
publisher = {Flux-Frontiers},
|
|
294
294
|
url = {https://github.com/Flux-Frontiers/pycode_kg},
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
[](https://www.python.org/)
|
|
7
7
|
[](https://www.elastic.co/licensing/elastic-license)
|
|
8
|
-
[](https://github.com/Flux-Frontiers/pycode_kg/releases)
|
|
9
9
|
[](https://github.com/Flux-Frontiers/pycode_kg/actions/workflows/ci.yml)
|
|
10
10
|
[](https://python-poetry.org/)
|
|
11
11
|
[](https://zenodo.org/badge/latestdoi/1202379010)
|
|
@@ -200,7 +200,7 @@ src/pycode_kg/
|
|
|
200
200
|
└── viz3d_timeline.py # Metric history timeline
|
|
201
201
|
```
|
|
202
202
|
|
|
203
|
-
The MCP server, the CLI, and the Streamlit app are thin wrappers over the same store + index + ranking core — there is exactly one code path for each capability. The latest architectural deep-dive is in [docs/analysis_v0.
|
|
203
|
+
The MCP server, the CLI, and the Streamlit app are thin wrappers over the same store + index + ranking core — there is exactly one code path for each capability. The latest architectural deep-dive is in [docs/analysis_v0.26.0.md](docs/analysis_v0.26.0.md), produced (of course) by `pycodekg analyze` against this very repo.
|
|
204
204
|
|
|
205
205
|
---
|
|
206
206
|
|
|
@@ -227,13 +227,13 @@ If you use PyCodeKG in your research or project, please cite it:
|
|
|
227
227
|
|
|
228
228
|
[](https://zenodo.org/badge/latestdoi/1202379010)
|
|
229
229
|
|
|
230
|
-
> Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.
|
|
230
|
+
> Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.26.0) [Software]. Flux-Frontiers. https://doi.org/10.5281/zenodo.19737993
|
|
231
231
|
|
|
232
232
|
```bibtex
|
|
233
233
|
@software{suchanek_pycode_kg,
|
|
234
234
|
author = {Suchanek, Eric G.},
|
|
235
235
|
title = {{PyCodeKG}: A Knowledge Graph for Python Codebases},
|
|
236
|
-
version = {0.
|
|
236
|
+
version = {0.26.0},
|
|
237
237
|
year = {2026},
|
|
238
238
|
publisher = {Flux-Frontiers},
|
|
239
239
|
url = {https://github.com/Flux-Frontiers/pycode_kg},
|
|
@@ -70,7 +70,7 @@ torch = [
|
|
|
70
70
|
# ---------------------------------------------------------------------------
|
|
71
71
|
[project]
|
|
72
72
|
name = "pycode-kg"
|
|
73
|
-
version = "0.
|
|
73
|
+
version = "0.26.0"
|
|
74
74
|
description = "A tool to build a searchable knowledge graph from Python repositories"
|
|
75
75
|
readme = "README.md"
|
|
76
76
|
license = "Elastic-2.0"
|
|
@@ -25,7 +25,12 @@ from pycode_kg.cli.options import sqlite_option
|
|
|
25
25
|
from pycode_kg.kg import PyCodeKG
|
|
26
26
|
from pycode_kg.pycodekg import DEFAULT_MODEL
|
|
27
27
|
from pycode_kg.pycodekg_thorough_analysis import PyCodeKGAnalyzer
|
|
28
|
-
from pycode_kg.snapshots import
|
|
28
|
+
from pycode_kg.snapshots import (
|
|
29
|
+
SnapshotDelta,
|
|
30
|
+
SnapshotManager,
|
|
31
|
+
delta_from_dict,
|
|
32
|
+
metrics_from_dict,
|
|
33
|
+
)
|
|
29
34
|
from pycode_kg.store import GraphStore
|
|
30
35
|
|
|
31
36
|
|
|
@@ -174,9 +179,10 @@ def save_snapshot(
|
|
|
174
179
|
click.echo(f"OK Snapshot saved: {snapshot_file}")
|
|
175
180
|
click.echo(f" Key: {snapshot_obj.key}")
|
|
176
181
|
click.echo(f" Version: {snapshot_obj.version}")
|
|
177
|
-
|
|
178
|
-
click.echo(f"
|
|
179
|
-
click.echo(f"
|
|
182
|
+
saved_metrics = metrics_from_dict(snapshot_obj.metrics)
|
|
183
|
+
click.echo(f" Nodes: {saved_metrics.total_nodes}")
|
|
184
|
+
click.echo(f" Edges: {saved_metrics.total_edges}")
|
|
185
|
+
click.echo(f" Coverage: {saved_metrics.docstring_coverage:.1%}")
|
|
180
186
|
|
|
181
187
|
|
|
182
188
|
@snapshot.command("list")
|
|
@@ -270,19 +276,20 @@ def show_snapshot(key: str, snapshots_dir: str | None) -> None:
|
|
|
270
276
|
click.echo()
|
|
271
277
|
|
|
272
278
|
click.echo("Metrics:")
|
|
273
|
-
|
|
274
|
-
click.echo(f" Total
|
|
275
|
-
click.echo(f"
|
|
276
|
-
click.echo(f"
|
|
277
|
-
click.echo(f"
|
|
278
|
-
click.echo(f"
|
|
279
|
+
metrics = metrics_from_dict(snapshot_obj.metrics)
|
|
280
|
+
click.echo(f" Total Nodes: {metrics.total_nodes}")
|
|
281
|
+
click.echo(f" Total Edges: {metrics.total_edges}")
|
|
282
|
+
click.echo(f" Meaningful Nodes: {metrics.meaningful_nodes}")
|
|
283
|
+
click.echo(f" Docstring Coverage: {metrics.docstring_coverage:.1%}")
|
|
284
|
+
click.echo(f" Critical Issues: {metrics.critical_issues}")
|
|
285
|
+
click.echo(f" Complexity Median: {metrics.complexity_median:.2f}")
|
|
279
286
|
click.echo()
|
|
280
287
|
|
|
281
288
|
click.echo("Node/Edge Breakdown:")
|
|
282
|
-
for kind, count in sorted(
|
|
289
|
+
for kind, count in sorted(metrics.node_counts.items()):
|
|
283
290
|
click.echo(f" {kind}: {count}")
|
|
284
291
|
click.echo()
|
|
285
|
-
for rel, count in sorted(
|
|
292
|
+
for rel, count in sorted(metrics.edge_counts.items()):
|
|
286
293
|
click.echo(f" {rel}: {count}")
|
|
287
294
|
click.echo()
|
|
288
295
|
|
|
@@ -296,7 +303,7 @@ def show_snapshot(key: str, snapshots_dir: str | None) -> None:
|
|
|
296
303
|
|
|
297
304
|
if snapshot_obj.vs_previous:
|
|
298
305
|
click.echo("Delta vs. Previous:")
|
|
299
|
-
delta = snapshot_obj.vs_previous
|
|
306
|
+
delta = delta_from_dict(snapshot_obj.vs_previous) or SnapshotDelta()
|
|
300
307
|
click.echo(f" Nodes: {delta.nodes:+d}")
|
|
301
308
|
click.echo(f" Edges: {delta.edges:+d}")
|
|
302
309
|
click.echo(f" Coverage: {delta.coverage_delta:+.1%}")
|
|
@@ -305,7 +312,7 @@ def show_snapshot(key: str, snapshots_dir: str | None) -> None:
|
|
|
305
312
|
|
|
306
313
|
if snapshot_obj.vs_baseline:
|
|
307
314
|
click.echo("Delta vs. Baseline:")
|
|
308
|
-
delta = snapshot_obj.vs_baseline
|
|
315
|
+
delta = delta_from_dict(snapshot_obj.vs_baseline) or SnapshotDelta()
|
|
309
316
|
click.echo(f" Nodes: {delta.nodes:+d}")
|
|
310
317
|
click.echo(f" Edges: {delta.edges:+d}")
|
|
311
318
|
click.echo(f" Coverage: {delta.coverage_delta:+.1%}")
|
|
@@ -1653,7 +1653,7 @@ def snapshot_show(key: str = "latest") -> str:
|
|
|
1653
1653
|
if snapshot is None:
|
|
1654
1654
|
return json.dumps({"error": f"Snapshot not found for key: {key!r}"})
|
|
1655
1655
|
out = snapshot.to_dict()
|
|
1656
|
-
out["freshness"] = _snapshot_freshness(snapshot.metrics.total_nodes)
|
|
1656
|
+
out["freshness"] = _snapshot_freshness(snapshot.metrics.get("total_nodes", 0))
|
|
1657
1657
|
return json.dumps(out, indent=2, ensure_ascii=False)
|
|
1658
1658
|
|
|
1659
1659
|
|
|
@@ -333,12 +333,19 @@ def _has_property_decorator(source_lines: list[str], def_lineno: int) -> bool:
|
|
|
333
333
|
found, matching ``@property``, ``@cached_property`` (bare or via
|
|
334
334
|
``functools``), and ``@<name>.setter/getter/deleter``.
|
|
335
335
|
|
|
336
|
+
``def_lineno`` comes from the graph and ``source_lines`` from the file on
|
|
337
|
+
disk, so the two disagree whenever the graph is stale: a line number past
|
|
338
|
+
the end of the current file is normal after an edit, not a bug. Treat that
|
|
339
|
+
as "no property decorator" rather than indexing past the end -- a node
|
|
340
|
+
wrongly listed as an orphan is a report to re-read, an ``IndexError`` here
|
|
341
|
+
aborts the whole analysis phase.
|
|
342
|
+
|
|
336
343
|
:param source_lines: Module source split into lines.
|
|
337
344
|
:param def_lineno: 1-based line number of the ``def`` statement.
|
|
338
345
|
:return: True when a property-family decorator precedes the definition.
|
|
339
346
|
"""
|
|
340
347
|
i = def_lineno - 2 # 0-based index of the line above the def
|
|
341
|
-
while i
|
|
348
|
+
while 0 <= i < len(source_lines):
|
|
342
349
|
stripped = source_lines[i].strip()
|
|
343
350
|
if not stripped.startswith("@"):
|
|
344
351
|
break
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""
|
|
2
|
+
snapshots.py — Temporal Snapshots of PyCodeKG Metrics
|
|
3
|
+
|
|
4
|
+
Thin layer over the shared ``kg_utils.snapshots`` module.
|
|
5
|
+
|
|
6
|
+
``Snapshot``, ``SnapshotManifest`` and ``PruneResult`` are re-exported from
|
|
7
|
+
``kg_utils.snapshots`` unchanged. A snapshot's ``metrics``, ``vs_previous``
|
|
8
|
+
and ``vs_baseline`` are plain dicts, which is what the shared manager reads
|
|
9
|
+
and writes.
|
|
10
|
+
|
|
11
|
+
This module adds:
|
|
12
|
+
|
|
13
|
+
- ``SnapshotMetrics`` / ``SnapshotDelta`` — domain dataclasses, used as
|
|
14
|
+
converters by callers that want attribute access. Convert with
|
|
15
|
+
``metrics_from_dict`` / ``metrics_to_dict`` and ``delta_from_dict`` /
|
|
16
|
+
``delta_to_dict``; a ``Snapshot`` never holds one.
|
|
17
|
+
- a ``SnapshotManager`` subclass that sets ``package_name="pycode-kg"``,
|
|
18
|
+
names the pycode-kg metric fields in ``capture()``, adds
|
|
19
|
+
``coverage_delta`` and ``critical_issues_delta`` to deltas, collects
|
|
20
|
+
per-module node counts from SQLite, and extends a diff with
|
|
21
|
+
``module_node_counts_delta``, ``issues_delta`` and ``timestamp``.
|
|
22
|
+
|
|
23
|
+
Do not subclass ``Snapshot`` here. A subclass that exposes ``metrics``,
|
|
24
|
+
``vs_previous`` or ``vs_baseline`` as properties breaks every shared manager
|
|
25
|
+
method that reads those fields by attribute, and each one then needs a
|
|
26
|
+
hand-written copy. One such copy dropped ``snapshot_key``, ``subject`` and
|
|
27
|
+
``tool`` on the way to disk, which shipped in 0.25.0.
|
|
28
|
+
|
|
29
|
+
Usage
|
|
30
|
+
-----
|
|
31
|
+
>>> from pycode_kg.snapshots import SnapshotManager, metrics_from_dict
|
|
32
|
+
>>> mgr = SnapshotManager(".pycodekg/snapshots")
|
|
33
|
+
>>> snapshot = mgr.capture(version="0.5.1", key="v0.5.1", subject="repo:pycode-kg")
|
|
34
|
+
>>> mgr.save_snapshot(snapshot)
|
|
35
|
+
>>> metrics_from_dict(snapshot.metrics).docstring_coverage
|
|
36
|
+
0.0
|
|
37
|
+
|
|
38
|
+
Author: Eric G. Suchanek, PhD
|
|
39
|
+
|
|
40
|
+
License: Elastic 2.0
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import sqlite3
|
|
46
|
+
from dataclasses import dataclass, field
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
from typing import Any
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# Re-export shared base types (backwards-compat public API)
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
from kg_utils.snapshots import (
|
|
54
|
+
PruneResult, # noqa: F401 re-exported
|
|
55
|
+
Snapshot, # noqa: F401 re-exported
|
|
56
|
+
SnapshotManifest, # noqa: F401 re-exported
|
|
57
|
+
)
|
|
58
|
+
from kg_utils.snapshots import SnapshotManager as _BaseSnapshotManager
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
"SnapshotMetrics",
|
|
62
|
+
"SnapshotDelta",
|
|
63
|
+
"Snapshot",
|
|
64
|
+
"SnapshotManifest",
|
|
65
|
+
"SnapshotManager",
|
|
66
|
+
"PruneResult",
|
|
67
|
+
"metrics_to_dict",
|
|
68
|
+
"metrics_from_dict",
|
|
69
|
+
"delta_to_dict",
|
|
70
|
+
"delta_from_dict",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Domain-specific dataclasses (used by CLI and tests)
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class SnapshotMetrics:
|
|
81
|
+
"""Core metrics captured in a pycode-kg snapshot."""
|
|
82
|
+
|
|
83
|
+
total_nodes: int
|
|
84
|
+
total_edges: int
|
|
85
|
+
meaningful_nodes: int
|
|
86
|
+
docstring_coverage: float # 0.0 to 1.0
|
|
87
|
+
node_counts: dict[str, int]
|
|
88
|
+
edge_counts: dict[str, int]
|
|
89
|
+
critical_issues: int
|
|
90
|
+
complexity_median: float # median fan-in across functions
|
|
91
|
+
module_node_counts: dict[str, int] = field(default_factory=dict)
|
|
92
|
+
coverage_documented: int = 0 # nodes with a docstring
|
|
93
|
+
coverage_total: int = 0 # nodes eligible for docstring coverage
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class SnapshotDelta:
|
|
98
|
+
"""Deltas comparing this snapshot to a baseline or previous snapshot."""
|
|
99
|
+
|
|
100
|
+
nodes: int = 0
|
|
101
|
+
edges: int = 0
|
|
102
|
+
coverage_delta: float = 0.0
|
|
103
|
+
critical_issues_delta: int = 0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
# Conversion helpers
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def metrics_to_dict(m: SnapshotMetrics) -> dict[str, Any]:
|
|
112
|
+
"""Convert a ``SnapshotMetrics`` dataclass to a plain dict."""
|
|
113
|
+
return {
|
|
114
|
+
"total_nodes": m.total_nodes,
|
|
115
|
+
"total_edges": m.total_edges,
|
|
116
|
+
"meaningful_nodes": m.meaningful_nodes,
|
|
117
|
+
"docstring_coverage": m.docstring_coverage,
|
|
118
|
+
"node_counts": m.node_counts,
|
|
119
|
+
"edge_counts": m.edge_counts,
|
|
120
|
+
"critical_issues": m.critical_issues,
|
|
121
|
+
"complexity_median": m.complexity_median,
|
|
122
|
+
"module_node_counts": m.module_node_counts,
|
|
123
|
+
"coverage_documented": m.coverage_documented,
|
|
124
|
+
"coverage_total": m.coverage_total,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def metrics_from_dict(d: dict[str, Any]) -> SnapshotMetrics:
|
|
129
|
+
"""Reconstruct a ``SnapshotMetrics`` dataclass from a plain dict."""
|
|
130
|
+
return SnapshotMetrics(
|
|
131
|
+
total_nodes=int(d.get("total_nodes", 0)),
|
|
132
|
+
total_edges=int(d.get("total_edges", 0)),
|
|
133
|
+
meaningful_nodes=int(d.get("meaningful_nodes", 0)),
|
|
134
|
+
docstring_coverage=float(d.get("docstring_coverage", 0.0)),
|
|
135
|
+
node_counts=d.get("node_counts", {}),
|
|
136
|
+
edge_counts=d.get("edge_counts", {}),
|
|
137
|
+
critical_issues=int(d.get("critical_issues", 0)),
|
|
138
|
+
complexity_median=float(d.get("complexity_median", 0.0)),
|
|
139
|
+
module_node_counts=d.get("module_node_counts", {}),
|
|
140
|
+
coverage_documented=int(d.get("coverage_documented", 0)),
|
|
141
|
+
coverage_total=int(d.get("coverage_total", 0)),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def delta_to_dict(delta: SnapshotDelta | None) -> dict[str, Any] | None:
|
|
146
|
+
"""Convert a ``SnapshotDelta`` to a plain dict, or return None."""
|
|
147
|
+
if delta is None:
|
|
148
|
+
return None
|
|
149
|
+
return {
|
|
150
|
+
"nodes": delta.nodes,
|
|
151
|
+
"edges": delta.edges,
|
|
152
|
+
"coverage_delta": delta.coverage_delta,
|
|
153
|
+
"critical_issues_delta": delta.critical_issues_delta,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def delta_from_dict(d: dict[str, Any] | None) -> SnapshotDelta | None:
|
|
158
|
+
"""Reconstruct a ``SnapshotDelta`` from a plain dict, or return None."""
|
|
159
|
+
if d is None:
|
|
160
|
+
return None
|
|
161
|
+
return SnapshotDelta(
|
|
162
|
+
nodes=int(d.get("nodes", 0)),
|
|
163
|
+
edges=int(d.get("edges", 0)),
|
|
164
|
+
coverage_delta=float(d.get("coverage_delta", 0.0)),
|
|
165
|
+
critical_issues_delta=int(d.get("critical_issues_delta", 0)),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
# SnapshotManager — pycode-kg specialisation of the shared manager
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class SnapshotManager(_BaseSnapshotManager):
|
|
175
|
+
"""pycode-kg snapshot manager.
|
|
176
|
+
|
|
177
|
+
Subclasses the shared ``kg_utils.snapshots.SnapshotManager`` and adds:
|
|
178
|
+
|
|
179
|
+
* ``package_name="pycode-kg"`` default for version detection.
|
|
180
|
+
* A ``capture()`` naming the pycode-kg metric fields (``coverage``,
|
|
181
|
+
``critical_issues``, ``complexity_median`` and the coverage numerator
|
|
182
|
+
and denominator) and collecting per-module node counts.
|
|
183
|
+
* ``_compute_delta_from_metrics`` extended with ``coverage_delta`` and
|
|
184
|
+
``critical_issues_delta``.
|
|
185
|
+
* ``diff_snapshots`` extended with ``module_node_counts_delta``,
|
|
186
|
+
``issues_delta`` and ``timestamp``.
|
|
187
|
+
* ``_collect_module_node_counts()`` — SQLite per-module node counts.
|
|
188
|
+
|
|
189
|
+
Everything else -- saving, loading, listing, pruning, key handling -- is
|
|
190
|
+
inherited unchanged. Overriding those to convert between dicts and the
|
|
191
|
+
domain dataclasses is what this module used to do, and is what let the
|
|
192
|
+
0.25.0 snapshot key regression through.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
def __init__(
|
|
196
|
+
self,
|
|
197
|
+
snapshots_dir: Path | str,
|
|
198
|
+
*,
|
|
199
|
+
db_path: Path | str | None = None,
|
|
200
|
+
package_name: str = "pycode-kg",
|
|
201
|
+
) -> None:
|
|
202
|
+
"""Initialize the manager rooted at ``snapshots_dir``.
|
|
203
|
+
|
|
204
|
+
:param snapshots_dir: Directory where snapshot JSON files live; created
|
|
205
|
+
on first save if it does not exist.
|
|
206
|
+
:param db_path: PyCodeKG SQLite graph path; required for collecting
|
|
207
|
+
per-module node counts during ``capture()``. Optional otherwise.
|
|
208
|
+
:param package_name: Package name used for version detection in saved
|
|
209
|
+
snapshots. Defaults to ``"pycode-kg"``.
|
|
210
|
+
"""
|
|
211
|
+
super().__init__(snapshots_dir, package_name=package_name, db_path=db_path)
|
|
212
|
+
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
# capture — name the pycode-kg metric fields
|
|
215
|
+
# ------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
def capture( # ty: ignore[invalid-method-override]
|
|
218
|
+
self,
|
|
219
|
+
version: str | None = None,
|
|
220
|
+
branch: str | None = None,
|
|
221
|
+
graph_stats_dict: dict[str, Any] | None = None,
|
|
222
|
+
coverage: float = 0.0,
|
|
223
|
+
coverage_documented: int = 0,
|
|
224
|
+
coverage_total: int = 0,
|
|
225
|
+
critical_issues: int = 0,
|
|
226
|
+
complexity_median: float = 0.0,
|
|
227
|
+
hotspots: list[dict[str, Any]] | None = None,
|
|
228
|
+
issues: list[str] | None = None,
|
|
229
|
+
tree_hash: str = "",
|
|
230
|
+
key: str = "",
|
|
231
|
+
subject: str = "",
|
|
232
|
+
) -> Snapshot:
|
|
233
|
+
"""Capture a pycode-kg snapshot.
|
|
234
|
+
|
|
235
|
+
Names the pycode-kg metric fields explicitly rather than taking them
|
|
236
|
+
through ``**extra_metrics``, and adds per-module node counts, then
|
|
237
|
+
delegates to the shared implementation.
|
|
238
|
+
|
|
239
|
+
:param version: Version string (e.g., "0.5.1").
|
|
240
|
+
:param branch: Git branch name; auto-detected if None.
|
|
241
|
+
:param graph_stats_dict: Output from ``graph_stats()`` / ``store.stats()``.
|
|
242
|
+
:param coverage: Docstring coverage fraction (0.0-1.0).
|
|
243
|
+
:param coverage_documented: Nodes with a non-empty docstring — the
|
|
244
|
+
numerator behind ``coverage``. Shown beside the percentage in
|
|
245
|
+
Snapshot History so a coverage drop caused by adding undocumented
|
|
246
|
+
nodes reads differently from one caused by removing docstrings.
|
|
247
|
+
:param coverage_total: Nodes eligible for docstring coverage — the
|
|
248
|
+
denominator behind ``coverage``.
|
|
249
|
+
:param critical_issues: Number of critical issues detected.
|
|
250
|
+
:param complexity_median: Median fan-in across functions.
|
|
251
|
+
:param hotspots: Top hotspot entries.
|
|
252
|
+
:param issues: Issue description strings.
|
|
253
|
+
:param tree_hash: Git tree hash, recorded as provenance; auto-detected
|
|
254
|
+
if not provided. It is not the snapshot's key.
|
|
255
|
+
:param key: Snapshot identifier. Pass the release tag at release time;
|
|
256
|
+
omit it and the base assigns a UTC timestamp.
|
|
257
|
+
:param subject: What was measured, e.g. ``repo:pycode-kg``.
|
|
258
|
+
:return: New :class:`~kg_utils.snapshots.Snapshot` (not yet persisted).
|
|
259
|
+
"""
|
|
260
|
+
return super().capture(
|
|
261
|
+
version=version,
|
|
262
|
+
branch=branch,
|
|
263
|
+
graph_stats_dict=graph_stats_dict,
|
|
264
|
+
tree_hash=tree_hash,
|
|
265
|
+
key=key,
|
|
266
|
+
subject=subject,
|
|
267
|
+
hotspots=hotspots,
|
|
268
|
+
issues=issues,
|
|
269
|
+
docstring_coverage=coverage,
|
|
270
|
+
coverage_documented=coverage_documented,
|
|
271
|
+
coverage_total=coverage_total,
|
|
272
|
+
critical_issues=critical_issues,
|
|
273
|
+
complexity_median=complexity_median,
|
|
274
|
+
module_node_counts=self._collect_module_node_counts(),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# ------------------------------------------------------------------
|
|
278
|
+
# diff_snapshots — adds module_node_counts_delta, issues_delta, timestamp
|
|
279
|
+
# ------------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
def diff_snapshots(self, key_a: str, key_b: str) -> dict[str, Any]:
|
|
282
|
+
"""Compare two snapshots side-by-side.
|
|
283
|
+
|
|
284
|
+
Extends the shared diff with ``module_node_counts_delta``,
|
|
285
|
+
``issues_delta`` (introduced / resolved issue strings) and the
|
|
286
|
+
``timestamp`` of each side, which the CLI prints.
|
|
287
|
+
|
|
288
|
+
:param key_a: Earlier snapshot key.
|
|
289
|
+
:param key_b: Later snapshot key.
|
|
290
|
+
:return: The shared diff result with the pycode-kg additions.
|
|
291
|
+
"""
|
|
292
|
+
result = super().diff_snapshots(key_a, key_b)
|
|
293
|
+
if "error" in result:
|
|
294
|
+
return result
|
|
295
|
+
|
|
296
|
+
for side, key in (("a", key_a), ("b", key_b)):
|
|
297
|
+
snap = self.load_snapshot(key)
|
|
298
|
+
if snap is not None:
|
|
299
|
+
result[side]["timestamp"] = snap.timestamp
|
|
300
|
+
|
|
301
|
+
m_a: dict[str, Any] = result["a"]["metrics"]
|
|
302
|
+
m_b: dict[str, Any] = result["b"]["metrics"]
|
|
303
|
+
counts_a: dict[str, int] = m_a.get("module_node_counts", {})
|
|
304
|
+
counts_b: dict[str, int] = m_b.get("module_node_counts", {})
|
|
305
|
+
result["module_node_counts_delta"] = {
|
|
306
|
+
mod: counts_b.get(mod, 0) - counts_a.get(mod, 0)
|
|
307
|
+
for mod in set(counts_a) | set(counts_b)
|
|
308
|
+
if counts_b.get(mod, 0) != counts_a.get(mod, 0)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
issues_a = set(result["a"]["issues"])
|
|
312
|
+
issues_b = set(result["b"]["issues"])
|
|
313
|
+
result["issues_delta"] = {
|
|
314
|
+
"introduced": list(issues_b - issues_a),
|
|
315
|
+
"resolved": list(issues_a - issues_b),
|
|
316
|
+
}
|
|
317
|
+
return result
|
|
318
|
+
|
|
319
|
+
# ------------------------------------------------------------------
|
|
320
|
+
# Delta computation — adds coverage_delta and critical_issues_delta
|
|
321
|
+
# ------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
def _compute_delta_from_metrics(
|
|
324
|
+
self, new_m: dict[str, Any], old_m: dict[str, Any]
|
|
325
|
+
) -> dict[str, Any]:
|
|
326
|
+
"""Compute delta dict including pycode-kg specific fields."""
|
|
327
|
+
return {
|
|
328
|
+
"nodes": new_m.get("total_nodes", 0) - old_m.get("total_nodes", 0),
|
|
329
|
+
"edges": new_m.get("total_edges", 0) - old_m.get("total_edges", 0),
|
|
330
|
+
"coverage_delta": (
|
|
331
|
+
new_m.get("docstring_coverage", 0.0) - old_m.get("docstring_coverage", 0.0)
|
|
332
|
+
),
|
|
333
|
+
"critical_issues_delta": (
|
|
334
|
+
new_m.get("critical_issues", 0) - old_m.get("critical_issues", 0)
|
|
335
|
+
),
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
# ------------------------------------------------------------------
|
|
339
|
+
# SQLite per-module node counts
|
|
340
|
+
# ------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
def _collect_module_node_counts(self) -> dict[str, int]:
|
|
343
|
+
"""Query SQLite for per-module node counts.
|
|
344
|
+
|
|
345
|
+
:return: Dict mapping ``module_path`` to node count, or ``{}`` if the
|
|
346
|
+
database is unavailable or the query fails.
|
|
347
|
+
"""
|
|
348
|
+
if not self.db_path or not self.db_path.exists():
|
|
349
|
+
return {}
|
|
350
|
+
try:
|
|
351
|
+
with sqlite3.connect(self.db_path) as conn:
|
|
352
|
+
rows = conn.execute(
|
|
353
|
+
"SELECT module_path, COUNT(*) FROM nodes GROUP BY module_path"
|
|
354
|
+
).fetchall()
|
|
355
|
+
return {row[0]: row[1] for row in rows if row[0]}
|
|
356
|
+
except sqlite3.Error:
|
|
357
|
+
return {}
|
|
@@ -1,531 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
snapshots.py — Temporal Snapshots of PyCodeKG Metrics
|
|
3
|
-
|
|
4
|
-
Thin compatibility layer over the shared ``kg_utils.snapshots`` module.
|
|
5
|
-
|
|
6
|
-
The shared module provides canonical ``Snapshot``, ``SnapshotManifest``, and
|
|
7
|
-
``SnapshotManager`` backed by free-form dicts. This module re-exports those
|
|
8
|
-
types and adds:
|
|
9
|
-
|
|
10
|
-
- ``SnapshotMetrics`` — domain-specific dataclass (used by CLI and tests)
|
|
11
|
-
- ``SnapshotDelta`` — domain-specific dataclass (used by CLI and tests)
|
|
12
|
-
- ``SnapshotManager`` subclass that:
|
|
13
|
-
* sets ``package_name="pycode-kg"`` by default
|
|
14
|
-
* overrides ``capture()`` to accept the legacy per-field kwargs
|
|
15
|
-
(``coverage``, ``critical_issues``, ``complexity_median``) and
|
|
16
|
-
build the structured ``metrics`` dict
|
|
17
|
-
* overrides ``_compute_delta_from_metrics`` to include
|
|
18
|
-
``coverage_delta`` and ``critical_issues_delta``
|
|
19
|
-
* adds ``_collect_module_node_counts()`` — SQLite per-module counts
|
|
20
|
-
- ``metrics_to_dict`` / ``metrics_from_dict`` — helpers for converting
|
|
21
|
-
between ``SnapshotMetrics`` dataclass and the underlying dict
|
|
22
|
-
- ``delta_to_dict`` / ``delta_from_dict`` — same for ``SnapshotDelta``
|
|
23
|
-
|
|
24
|
-
``Snapshot`` is re-exported from ``kg_utils.snapshots``. For backwards
|
|
25
|
-
compatibility ``snapshot.metrics`` returns a ``SnapshotMetrics``-shaped
|
|
26
|
-
view object when the snapshot was constructed via this module's helpers.
|
|
27
|
-
|
|
28
|
-
Usage
|
|
29
|
-
-----
|
|
30
|
-
>>> from pycode_kg.snapshots import SnapshotManager
|
|
31
|
-
>>> mgr = SnapshotManager(".pycodekg/snapshots")
|
|
32
|
-
>>> snapshot = mgr.capture("v0.5.1", "develop", graph_stats_dict)
|
|
33
|
-
>>> mgr.save_snapshot(snapshot)
|
|
34
|
-
>>> manifest = mgr.load_manifest()
|
|
35
|
-
>>> prev = mgr.get_previous(tree_hash)
|
|
36
|
-
|
|
37
|
-
Author: Eric G. Suchanek, PhD
|
|
38
|
-
|
|
39
|
-
License: Elastic 2.0
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
from __future__ import annotations
|
|
43
|
-
|
|
44
|
-
import sqlite3
|
|
45
|
-
from dataclasses import dataclass, field
|
|
46
|
-
from pathlib import Path
|
|
47
|
-
from typing import Any
|
|
48
|
-
|
|
49
|
-
# ---------------------------------------------------------------------------
|
|
50
|
-
# Re-export shared base types (backwards-compat public API)
|
|
51
|
-
# ---------------------------------------------------------------------------
|
|
52
|
-
from kg_utils.snapshots import (
|
|
53
|
-
PruneResult, # noqa: F401 re-exported
|
|
54
|
-
SnapshotManifest, # noqa: F401 re-exported
|
|
55
|
-
)
|
|
56
|
-
from kg_utils.snapshots import Snapshot as _BaseSnapshot
|
|
57
|
-
from kg_utils.snapshots import SnapshotManager as _BaseSnapshotManager
|
|
58
|
-
|
|
59
|
-
__all__ = [
|
|
60
|
-
"SnapshotMetrics",
|
|
61
|
-
"SnapshotDelta",
|
|
62
|
-
"Snapshot",
|
|
63
|
-
"SnapshotManifest",
|
|
64
|
-
"SnapshotManager",
|
|
65
|
-
"PruneResult",
|
|
66
|
-
"metrics_to_dict",
|
|
67
|
-
"metrics_from_dict",
|
|
68
|
-
"delta_to_dict",
|
|
69
|
-
"delta_from_dict",
|
|
70
|
-
]
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
# ---------------------------------------------------------------------------
|
|
74
|
-
# Domain-specific dataclasses (used by CLI and tests)
|
|
75
|
-
# ---------------------------------------------------------------------------
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
@dataclass
|
|
79
|
-
class SnapshotMetrics:
|
|
80
|
-
"""Core metrics captured in a pycode-kg snapshot."""
|
|
81
|
-
|
|
82
|
-
total_nodes: int
|
|
83
|
-
total_edges: int
|
|
84
|
-
meaningful_nodes: int
|
|
85
|
-
docstring_coverage: float # 0.0 to 1.0
|
|
86
|
-
node_counts: dict[str, int]
|
|
87
|
-
edge_counts: dict[str, int]
|
|
88
|
-
critical_issues: int
|
|
89
|
-
complexity_median: float # median fan-in across functions
|
|
90
|
-
module_node_counts: dict[str, int] = field(default_factory=dict)
|
|
91
|
-
coverage_documented: int = 0 # nodes with a docstring
|
|
92
|
-
coverage_total: int = 0 # nodes eligible for docstring coverage
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
@dataclass
|
|
96
|
-
class SnapshotDelta:
|
|
97
|
-
"""Deltas comparing this snapshot to a baseline or previous snapshot."""
|
|
98
|
-
|
|
99
|
-
nodes: int = 0
|
|
100
|
-
edges: int = 0
|
|
101
|
-
coverage_delta: float = 0.0
|
|
102
|
-
critical_issues_delta: int = 0
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
# ---------------------------------------------------------------------------
|
|
106
|
-
# Conversion helpers
|
|
107
|
-
# ---------------------------------------------------------------------------
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def metrics_to_dict(m: SnapshotMetrics) -> dict[str, Any]:
|
|
111
|
-
"""Convert a ``SnapshotMetrics`` dataclass to a plain dict."""
|
|
112
|
-
return {
|
|
113
|
-
"total_nodes": m.total_nodes,
|
|
114
|
-
"total_edges": m.total_edges,
|
|
115
|
-
"meaningful_nodes": m.meaningful_nodes,
|
|
116
|
-
"docstring_coverage": m.docstring_coverage,
|
|
117
|
-
"node_counts": m.node_counts,
|
|
118
|
-
"edge_counts": m.edge_counts,
|
|
119
|
-
"critical_issues": m.critical_issues,
|
|
120
|
-
"complexity_median": m.complexity_median,
|
|
121
|
-
"module_node_counts": m.module_node_counts,
|
|
122
|
-
"coverage_documented": m.coverage_documented,
|
|
123
|
-
"coverage_total": m.coverage_total,
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def metrics_from_dict(d: dict[str, Any]) -> SnapshotMetrics:
|
|
128
|
-
"""Reconstruct a ``SnapshotMetrics`` dataclass from a plain dict."""
|
|
129
|
-
return SnapshotMetrics(
|
|
130
|
-
total_nodes=int(d.get("total_nodes", 0)),
|
|
131
|
-
total_edges=int(d.get("total_edges", 0)),
|
|
132
|
-
meaningful_nodes=int(d.get("meaningful_nodes", 0)),
|
|
133
|
-
docstring_coverage=float(d.get("docstring_coverage", 0.0)),
|
|
134
|
-
node_counts=d.get("node_counts", {}),
|
|
135
|
-
edge_counts=d.get("edge_counts", {}),
|
|
136
|
-
critical_issues=int(d.get("critical_issues", 0)),
|
|
137
|
-
complexity_median=float(d.get("complexity_median", 0.0)),
|
|
138
|
-
module_node_counts=d.get("module_node_counts", {}),
|
|
139
|
-
coverage_documented=int(d.get("coverage_documented", 0)),
|
|
140
|
-
coverage_total=int(d.get("coverage_total", 0)),
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def delta_to_dict(delta: SnapshotDelta | None) -> dict[str, Any] | None:
|
|
145
|
-
"""Convert a ``SnapshotDelta`` to a plain dict, or return None."""
|
|
146
|
-
if delta is None:
|
|
147
|
-
return None
|
|
148
|
-
return {
|
|
149
|
-
"nodes": delta.nodes,
|
|
150
|
-
"edges": delta.edges,
|
|
151
|
-
"coverage_delta": delta.coverage_delta,
|
|
152
|
-
"critical_issues_delta": delta.critical_issues_delta,
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def delta_from_dict(d: dict[str, Any] | None) -> SnapshotDelta | None:
|
|
157
|
-
"""Reconstruct a ``SnapshotDelta`` from a plain dict, or return None."""
|
|
158
|
-
if d is None:
|
|
159
|
-
return None
|
|
160
|
-
return SnapshotDelta(
|
|
161
|
-
nodes=int(d.get("nodes", 0)),
|
|
162
|
-
edges=int(d.get("edges", 0)),
|
|
163
|
-
coverage_delta=float(d.get("coverage_delta", 0.0)),
|
|
164
|
-
critical_issues_delta=int(d.get("critical_issues_delta", 0)),
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
# ---------------------------------------------------------------------------
|
|
169
|
-
# Snapshot — thin compatibility wrapper around the shared dict-based model
|
|
170
|
-
# ---------------------------------------------------------------------------
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
class Snapshot(_BaseSnapshot):
|
|
174
|
-
"""pycode-kg Snapshot with attribute-style access to metrics and deltas.
|
|
175
|
-
|
|
176
|
-
Extends the shared ``kg_utils.snapshots.Snapshot`` (which stores metrics
|
|
177
|
-
as a free-form dict) with ``@property`` accessors that return the typed
|
|
178
|
-
``SnapshotMetrics`` and ``SnapshotDelta`` objects that the CLI and tests
|
|
179
|
-
expect.
|
|
180
|
-
|
|
181
|
-
The underlying ``metrics``, ``vs_previous``, and ``vs_baseline`` fields
|
|
182
|
-
remain plain dicts on disk; the properties are view-only adapters.
|
|
183
|
-
|
|
184
|
-
``to_dict`` is **not** overridden. The base reads those three fields out of
|
|
185
|
-
``__dict__`` rather than through these properties (kgmodule-utils 0.19.0),
|
|
186
|
-
which is what the override used to exist for -- and the base is also what
|
|
187
|
-
supplies the current key scheme, so an override here would silently keep
|
|
188
|
-
writing tree-hash keys.
|
|
189
|
-
|
|
190
|
-
Implementation note
|
|
191
|
-
-------------------
|
|
192
|
-
Python dataclass fields are stored in ``__dict__`` under their field name.
|
|
193
|
-
The properties below always read and write the raw dict stored in
|
|
194
|
-
``self.__dict__`` directly so that the shared base-class infrastructure
|
|
195
|
-
(which expects plain dicts) continues to work without modification.
|
|
196
|
-
"""
|
|
197
|
-
|
|
198
|
-
@property # type: ignore[override]
|
|
199
|
-
def metrics(self) -> SnapshotMetrics: # type: ignore[override]
|
|
200
|
-
"""Return metrics as a ``SnapshotMetrics`` dataclass view."""
|
|
201
|
-
return metrics_from_dict(self.__dict__["metrics"])
|
|
202
|
-
|
|
203
|
-
@metrics.setter
|
|
204
|
-
def metrics(self, value: SnapshotMetrics | dict[str, Any]) -> None:
|
|
205
|
-
if isinstance(value, SnapshotMetrics):
|
|
206
|
-
self.__dict__["metrics"] = metrics_to_dict(value)
|
|
207
|
-
else:
|
|
208
|
-
self.__dict__["metrics"] = value
|
|
209
|
-
|
|
210
|
-
@property # type: ignore[override]
|
|
211
|
-
def vs_previous(self) -> SnapshotDelta | None: # type: ignore[override]
|
|
212
|
-
"""Return vs_previous as a ``SnapshotDelta`` dataclass view."""
|
|
213
|
-
return delta_from_dict(self.__dict__["vs_previous"])
|
|
214
|
-
|
|
215
|
-
@vs_previous.setter
|
|
216
|
-
def vs_previous(self, value: SnapshotDelta | dict[str, Any] | None) -> None:
|
|
217
|
-
if isinstance(value, SnapshotDelta):
|
|
218
|
-
self.__dict__["vs_previous"] = delta_to_dict(value)
|
|
219
|
-
else:
|
|
220
|
-
self.__dict__["vs_previous"] = value
|
|
221
|
-
|
|
222
|
-
@property # type: ignore[override]
|
|
223
|
-
def vs_baseline(self) -> SnapshotDelta | None: # type: ignore[override]
|
|
224
|
-
"""Return vs_baseline as a ``SnapshotDelta`` dataclass view."""
|
|
225
|
-
return delta_from_dict(self.__dict__["vs_baseline"])
|
|
226
|
-
|
|
227
|
-
@vs_baseline.setter
|
|
228
|
-
def vs_baseline(self, value: SnapshotDelta | dict[str, Any] | None) -> None:
|
|
229
|
-
if isinstance(value, SnapshotDelta):
|
|
230
|
-
self.__dict__["vs_baseline"] = delta_to_dict(value)
|
|
231
|
-
else:
|
|
232
|
-
self.__dict__["vs_baseline"] = value
|
|
233
|
-
|
|
234
|
-
@staticmethod
|
|
235
|
-
def from_dict(data: dict[str, Any]) -> Snapshot: # type: ignore[override]
|
|
236
|
-
"""Reconstruct a pycode-kg ``Snapshot`` from a dictionary."""
|
|
237
|
-
base = _BaseSnapshot.from_dict(data)
|
|
238
|
-
return _rewrap(base)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
# ---------------------------------------------------------------------------
|
|
242
|
-
# SnapshotManager — pycode-kg specialisation of the shared manager
|
|
243
|
-
# ---------------------------------------------------------------------------
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
class SnapshotManager(_BaseSnapshotManager):
|
|
247
|
-
"""pycode-kg snapshot manager.
|
|
248
|
-
|
|
249
|
-
Subclasses the shared ``kg_utils.snapshots.SnapshotManager`` and adds:
|
|
250
|
-
|
|
251
|
-
* ``package_name="pycode-kg"`` default for version detection.
|
|
252
|
-
* Legacy ``capture()`` kwargs: ``coverage``, ``critical_issues``,
|
|
253
|
-
``complexity_median`` — merged into the metrics dict.
|
|
254
|
-
* ``_compute_delta_from_metrics`` extended with ``coverage_delta`` and
|
|
255
|
-
``critical_issues_delta``.
|
|
256
|
-
* ``_collect_module_node_counts()`` — SQLite per-module node counts stored
|
|
257
|
-
in snapshot metrics under ``"module_node_counts"``.
|
|
258
|
-
* Returns ``pycode_kg.snapshots.Snapshot`` instances (with typed-accessor
|
|
259
|
-
properties) from all load/capture methods.
|
|
260
|
-
"""
|
|
261
|
-
|
|
262
|
-
def __init__(
|
|
263
|
-
self,
|
|
264
|
-
snapshots_dir: Path | str,
|
|
265
|
-
*,
|
|
266
|
-
db_path: Path | str | None = None,
|
|
267
|
-
package_name: str = "pycode-kg",
|
|
268
|
-
) -> None:
|
|
269
|
-
"""Initialize the manager rooted at ``snapshots_dir``.
|
|
270
|
-
|
|
271
|
-
:param snapshots_dir: Directory where snapshot JSON files live; created
|
|
272
|
-
on first save if it does not exist.
|
|
273
|
-
:param db_path: PyCodeKG SQLite graph path; required for collecting
|
|
274
|
-
per-module node counts during ``capture()``. Optional otherwise.
|
|
275
|
-
:param package_name: Package name used for version detection in saved
|
|
276
|
-
snapshots. Defaults to ``"pycode-kg"``.
|
|
277
|
-
"""
|
|
278
|
-
super().__init__(snapshots_dir, package_name=package_name, db_path=db_path)
|
|
279
|
-
|
|
280
|
-
# ------------------------------------------------------------------
|
|
281
|
-
# capture — backwards-compat wrapper
|
|
282
|
-
# ------------------------------------------------------------------
|
|
283
|
-
|
|
284
|
-
def capture( # ty: ignore[invalid-method-override]
|
|
285
|
-
self,
|
|
286
|
-
version: str | None = None,
|
|
287
|
-
branch: str | None = None,
|
|
288
|
-
graph_stats_dict: dict[str, Any] | None = None,
|
|
289
|
-
coverage: float = 0.0,
|
|
290
|
-
coverage_documented: int = 0,
|
|
291
|
-
coverage_total: int = 0,
|
|
292
|
-
critical_issues: int = 0,
|
|
293
|
-
complexity_median: float = 0.0,
|
|
294
|
-
hotspots: list[dict[str, Any]] | None = None,
|
|
295
|
-
issues: list[str] | None = None,
|
|
296
|
-
tree_hash: str = "",
|
|
297
|
-
key: str = "",
|
|
298
|
-
subject: str = "",
|
|
299
|
-
) -> Snapshot:
|
|
300
|
-
"""Capture a pycode-kg snapshot.
|
|
301
|
-
|
|
302
|
-
Accepts the legacy per-field kwargs (``coverage``, ``critical_issues``,
|
|
303
|
-
``complexity_median``) in addition to the dict-based
|
|
304
|
-
``graph_stats_dict`` from the base class, and builds the full metrics
|
|
305
|
-
dict expected by the shared infrastructure.
|
|
306
|
-
|
|
307
|
-
:param version: Version string (e.g., "0.5.1").
|
|
308
|
-
:param branch: Git branch name; auto-detected if None.
|
|
309
|
-
:param graph_stats_dict: Output from ``graph_stats()`` / ``store.stats()``.
|
|
310
|
-
:param coverage: Docstring coverage fraction (0.0–1.0).
|
|
311
|
-
:param coverage_documented: Nodes with a non-empty docstring — the
|
|
312
|
-
numerator behind ``coverage``. Shown beside the percentage in
|
|
313
|
-
Snapshot History so a coverage drop caused by adding undocumented
|
|
314
|
-
nodes reads differently from one caused by removing docstrings.
|
|
315
|
-
:param coverage_total: Nodes eligible for docstring coverage — the
|
|
316
|
-
denominator behind ``coverage``.
|
|
317
|
-
:param critical_issues: Number of critical issues detected.
|
|
318
|
-
:param complexity_median: Median fan-in across functions.
|
|
319
|
-
:param hotspots: Top hotspot entries.
|
|
320
|
-
:param issues: Issue description strings.
|
|
321
|
-
:param tree_hash: Git tree hash, recorded as provenance; auto-detected
|
|
322
|
-
if not provided. It is not the snapshot's key.
|
|
323
|
-
:param key: Snapshot identifier. Pass the release tag at release time;
|
|
324
|
-
omit it and the base assigns a UTC timestamp.
|
|
325
|
-
:param subject: What was measured, e.g. ``repo:pycode-kg``.
|
|
326
|
-
:return: New :class:`Snapshot` instance (not yet persisted).
|
|
327
|
-
"""
|
|
328
|
-
module_node_counts = self._collect_module_node_counts()
|
|
329
|
-
|
|
330
|
-
base_snap = super().capture(
|
|
331
|
-
version=version,
|
|
332
|
-
branch=branch,
|
|
333
|
-
graph_stats_dict=graph_stats_dict,
|
|
334
|
-
tree_hash=tree_hash,
|
|
335
|
-
key=key,
|
|
336
|
-
subject=subject,
|
|
337
|
-
hotspots=hotspots,
|
|
338
|
-
issues=issues,
|
|
339
|
-
docstring_coverage=coverage,
|
|
340
|
-
coverage_documented=coverage_documented,
|
|
341
|
-
coverage_total=coverage_total,
|
|
342
|
-
critical_issues=critical_issues,
|
|
343
|
-
complexity_median=complexity_median,
|
|
344
|
-
module_node_counts=module_node_counts,
|
|
345
|
-
)
|
|
346
|
-
|
|
347
|
-
return _rewrap(base_snap)
|
|
348
|
-
|
|
349
|
-
# ------------------------------------------------------------------
|
|
350
|
-
# diff_snapshots — adds module_node_counts_delta and issues_delta
|
|
351
|
-
# ------------------------------------------------------------------
|
|
352
|
-
|
|
353
|
-
def diff_snapshots(self, key_a: str, key_b: str) -> dict[str, Any]:
|
|
354
|
-
"""Compare two snapshots side-by-side.
|
|
355
|
-
|
|
356
|
-
Extends the base diff with ``module_node_counts_delta`` and
|
|
357
|
-
``issues_delta`` (introduced / resolved issue strings).
|
|
358
|
-
|
|
359
|
-
:param key_a: First snapshot key (tree hash).
|
|
360
|
-
:param key_b: Second snapshot key (tree hash).
|
|
361
|
-
:return: Dict with metrics from both, computed deltas.
|
|
362
|
-
"""
|
|
363
|
-
snap_a = _BaseSnapshotManager.load_snapshot(self, key_a)
|
|
364
|
-
snap_b = _BaseSnapshotManager.load_snapshot(self, key_b)
|
|
365
|
-
|
|
366
|
-
if not snap_a or not snap_b:
|
|
367
|
-
return {"error": "One or both snapshots not found"}
|
|
368
|
-
|
|
369
|
-
m_a = snap_a.metrics
|
|
370
|
-
m_b = snap_b.metrics
|
|
371
|
-
|
|
372
|
-
all_node_kinds = set(m_a.get("node_counts", {})) | set(m_b.get("node_counts", {}))
|
|
373
|
-
all_edge_rels = set(m_a.get("edge_counts", {})) | set(m_b.get("edge_counts", {}))
|
|
374
|
-
|
|
375
|
-
node_counts_delta = {
|
|
376
|
-
k: m_b.get("node_counts", {}).get(k, 0) - m_a.get("node_counts", {}).get(k, 0)
|
|
377
|
-
for k in all_node_kinds
|
|
378
|
-
}
|
|
379
|
-
edge_counts_delta = {
|
|
380
|
-
k: m_b.get("edge_counts", {}).get(k, 0) - m_a.get("edge_counts", {}).get(k, 0)
|
|
381
|
-
for k in all_edge_rels
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
all_modules = set(m_a.get("module_node_counts", {})) | set(
|
|
385
|
-
m_b.get("module_node_counts", {})
|
|
386
|
-
)
|
|
387
|
-
module_node_counts_delta = {
|
|
388
|
-
mod: m_b.get("module_node_counts", {}).get(mod, 0)
|
|
389
|
-
- m_a.get("module_node_counts", {}).get(mod, 0)
|
|
390
|
-
for mod in all_modules
|
|
391
|
-
if m_b.get("module_node_counts", {}).get(mod, 0)
|
|
392
|
-
!= m_a.get("module_node_counts", {}).get(mod, 0)
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
issues_a = set(snap_a.issues)
|
|
396
|
-
issues_b = set(snap_b.issues)
|
|
397
|
-
|
|
398
|
-
return {
|
|
399
|
-
"a": {
|
|
400
|
-
"key": snap_a.key,
|
|
401
|
-
"metrics": m_a,
|
|
402
|
-
"issues": snap_a.issues,
|
|
403
|
-
"timestamp": snap_a.timestamp,
|
|
404
|
-
},
|
|
405
|
-
"b": {
|
|
406
|
-
"key": snap_b.key,
|
|
407
|
-
"metrics": m_b,
|
|
408
|
-
"issues": snap_b.issues,
|
|
409
|
-
"timestamp": snap_b.timestamp,
|
|
410
|
-
},
|
|
411
|
-
"delta": self._compute_delta_from_metrics(m_b, m_a),
|
|
412
|
-
"node_counts_delta": node_counts_delta,
|
|
413
|
-
"edge_counts_delta": edge_counts_delta,
|
|
414
|
-
"module_node_counts_delta": module_node_counts_delta,
|
|
415
|
-
"issues_delta": {
|
|
416
|
-
"introduced": list(issues_b - issues_a),
|
|
417
|
-
"resolved": list(issues_a - issues_b),
|
|
418
|
-
},
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
# ------------------------------------------------------------------
|
|
422
|
-
# Delta computation — adds coverage_delta and critical_issues_delta
|
|
423
|
-
# ------------------------------------------------------------------
|
|
424
|
-
|
|
425
|
-
def _compute_delta(self, snap_new: _BaseSnapshot, snap_old: _BaseSnapshot) -> dict[str, Any]:
|
|
426
|
-
"""Compute delta, extracting raw metric dicts to avoid the typed-property layer."""
|
|
427
|
-
new_m = snap_new.__dict__.get("metrics", snap_new.metrics)
|
|
428
|
-
old_m = snap_old.__dict__.get("metrics", snap_old.metrics)
|
|
429
|
-
if isinstance(new_m, SnapshotMetrics):
|
|
430
|
-
new_m = metrics_to_dict(new_m)
|
|
431
|
-
if isinstance(old_m, SnapshotMetrics):
|
|
432
|
-
old_m = metrics_to_dict(old_m)
|
|
433
|
-
return self._compute_delta_from_metrics(new_m, old_m)
|
|
434
|
-
|
|
435
|
-
def _compute_delta_from_metrics(
|
|
436
|
-
self, new_m: dict[str, Any], old_m: dict[str, Any]
|
|
437
|
-
) -> dict[str, Any]:
|
|
438
|
-
"""Compute delta dict including pycode-kg specific fields."""
|
|
439
|
-
return {
|
|
440
|
-
"nodes": new_m.get("total_nodes", 0) - old_m.get("total_nodes", 0),
|
|
441
|
-
"edges": new_m.get("total_edges", 0) - old_m.get("total_edges", 0),
|
|
442
|
-
"coverage_delta": (
|
|
443
|
-
new_m.get("docstring_coverage", 0.0) - old_m.get("docstring_coverage", 0.0)
|
|
444
|
-
),
|
|
445
|
-
"critical_issues_delta": (
|
|
446
|
-
new_m.get("critical_issues", 0) - old_m.get("critical_issues", 0)
|
|
447
|
-
),
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
# ------------------------------------------------------------------
|
|
451
|
-
# save_snapshot — normalise typed properties back to raw dicts first
|
|
452
|
-
# ------------------------------------------------------------------
|
|
453
|
-
|
|
454
|
-
def save_snapshot(self, snapshot: _BaseSnapshot, *, force: bool = False) -> Any:
|
|
455
|
-
"""Persist snapshot, normalising any typed-property values to raw dicts.
|
|
456
|
-
|
|
457
|
-
The base ``save_snapshot`` inspects ``snapshot.metrics`` (expects a
|
|
458
|
-
dict) and ``snapshot.vs_previous`` / ``snapshot.vs_baseline`` (expects
|
|
459
|
-
dicts or None) directly. If ``snapshot`` is a pycode-kg ``Snapshot``
|
|
460
|
-
the properties return typed dataclasses instead; we substitute a plain
|
|
461
|
-
``_BaseSnapshot`` carrying the raw dicts so the base implementation
|
|
462
|
-
can serialise without modification.
|
|
463
|
-
"""
|
|
464
|
-
if isinstance(snapshot, Snapshot):
|
|
465
|
-
raw = _BaseSnapshot(
|
|
466
|
-
branch=snapshot.branch,
|
|
467
|
-
timestamp=snapshot.timestamp,
|
|
468
|
-
version=snapshot.version,
|
|
469
|
-
metrics=snapshot.__dict__["metrics"],
|
|
470
|
-
hotspots=snapshot.hotspots,
|
|
471
|
-
issues=snapshot.issues,
|
|
472
|
-
vs_previous=snapshot.__dict__["vs_previous"],
|
|
473
|
-
vs_baseline=snapshot.__dict__["vs_baseline"],
|
|
474
|
-
tree_hash=snapshot.tree_hash,
|
|
475
|
-
)
|
|
476
|
-
return super().save_snapshot(raw, force=force)
|
|
477
|
-
return super().save_snapshot(snapshot, force=force)
|
|
478
|
-
|
|
479
|
-
# ------------------------------------------------------------------
|
|
480
|
-
# Load helpers — re-wrap base Snapshot instances as pycode-kg Snapshots
|
|
481
|
-
# ------------------------------------------------------------------
|
|
482
|
-
|
|
483
|
-
def load_snapshot(self, key: str) -> Snapshot | None: # type: ignore[override]
|
|
484
|
-
"""Load a snapshot by key and re-wrap it as a pycode-kg ``Snapshot`` with typed-accessor properties. Returns ``None`` if no snapshot matches."""
|
|
485
|
-
snap = super().load_snapshot(key)
|
|
486
|
-
return _rewrap(snap) if snap is not None else None
|
|
487
|
-
|
|
488
|
-
def get_previous(self, key: str) -> Snapshot | None: # type: ignore[override]
|
|
489
|
-
"""Return the snapshot immediately preceding ``key`` in manifest chronology, re-wrapped as a pycode-kg ``Snapshot``. ``None`` when ``key`` is the earliest."""
|
|
490
|
-
snap = super().get_previous(key)
|
|
491
|
-
return _rewrap(snap) if snap is not None else None
|
|
492
|
-
|
|
493
|
-
def get_baseline(self) -> Snapshot | None: # type: ignore[override]
|
|
494
|
-
"""Return the earliest (baseline) snapshot in the manifest, re-wrapped as a pycode-kg ``Snapshot``. ``None`` if no snapshots exist."""
|
|
495
|
-
snap = super().get_baseline()
|
|
496
|
-
return _rewrap(snap) if snap is not None else None
|
|
497
|
-
|
|
498
|
-
# ------------------------------------------------------------------
|
|
499
|
-
# SQLite per-module node counts
|
|
500
|
-
# ------------------------------------------------------------------
|
|
501
|
-
|
|
502
|
-
def _collect_module_node_counts(self) -> dict[str, int]:
|
|
503
|
-
"""Query SQLite for per-module node counts.
|
|
504
|
-
|
|
505
|
-
:return: Dict mapping ``module_path`` to node count, or ``{}`` if the
|
|
506
|
-
database is unavailable or the query fails.
|
|
507
|
-
"""
|
|
508
|
-
if not self.db_path or not self.db_path.exists():
|
|
509
|
-
return {}
|
|
510
|
-
try:
|
|
511
|
-
with sqlite3.connect(self.db_path) as conn:
|
|
512
|
-
rows = conn.execute(
|
|
513
|
-
"SELECT module_path, COUNT(*) FROM nodes GROUP BY module_path"
|
|
514
|
-
).fetchall()
|
|
515
|
-
return {row[0]: row[1] for row in rows if row[0]}
|
|
516
|
-
except sqlite3.Error:
|
|
517
|
-
return {}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
# ---------------------------------------------------------------------------
|
|
521
|
-
# Internal helper
|
|
522
|
-
# ---------------------------------------------------------------------------
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
def _rewrap(base: _BaseSnapshot) -> Snapshot:
|
|
526
|
-
"""Re-wrap a base Snapshot as a pycode-kg Snapshot (no data copying)."""
|
|
527
|
-
if isinstance(base, Snapshot):
|
|
528
|
-
return base
|
|
529
|
-
snap = Snapshot.__new__(Snapshot)
|
|
530
|
-
snap.__dict__.update(base.__dict__)
|
|
531
|
-
return snap
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|