linexcel 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.
- linexcel-0.1.0/.gitignore +14 -0
- linexcel-0.1.0/PKG-INFO +68 -0
- linexcel-0.1.0/README.md +39 -0
- linexcel-0.1.0/pyproject.toml +59 -0
- linexcel-0.1.0/src/linexcel/__init__.py +24 -0
- linexcel-0.1.0/src/linexcel/aidoc.py +158 -0
- linexcel-0.1.0/src/linexcel/analyzer.py +759 -0
- linexcel-0.1.0/src/linexcel/assets/cose-base.js +3214 -0
- linexcel-0.1.0/src/linexcel/assets/cytoscape-dagre.min.js +7 -0
- linexcel-0.1.0/src/linexcel/assets/cytoscape-fcose.js +1549 -0
- linexcel-0.1.0/src/linexcel/assets/cytoscape.min.js +31 -0
- linexcel-0.1.0/src/linexcel/assets/layout-base.js +5230 -0
- linexcel-0.1.0/src/linexcel/refs.py +295 -0
- linexcel-0.1.0/src/linexcel/result.py +206 -0
- linexcel-0.1.0/src/linexcel/rewrite.py +70 -0
- linexcel-0.1.0/src/linexcel/vba.py +198 -0
- linexcel-0.1.0/src/linexcel/viewer.py +552 -0
- linexcel-0.1.0/tests/conftest.py +41 -0
- linexcel-0.1.0/tests/test_lineage.py +242 -0
- linexcel-0.1.0/uv.lock +902 -0
linexcel-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: linexcel
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Data lineage analysis for Excel workbooks — formula extraction, dependency graph, step-by-step evaluation, AI documentation
|
|
5
|
+
Project-URL: Homepage, https://github.com/auspect/linexcel
|
|
6
|
+
Project-URL: Repository, https://github.com/auspect/linexcel
|
|
7
|
+
Author-email: Philippe Calvet <phcalvet@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: dependency-graph,excel,formula,lineage,openpyxl,vba
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: formualizer>=0.7.1
|
|
21
|
+
Requires-Dist: oletools>=0.60.2
|
|
22
|
+
Requires-Dist: openpyxl>=3.1
|
|
23
|
+
Provides-Extra: ai
|
|
24
|
+
Requires-Dist: google-genai>=2.10.0; extra == 'ai'
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.9; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# linexcel
|
|
31
|
+
|
|
32
|
+
Data lineage analysis for Excel workbooks.
|
|
33
|
+
|
|
34
|
+
Extracts every formula, groups stretched patterns (R1C1 canonicalization), builds a dependency graph (cells, ranges, defined names, VBA), decomposes composite functions with step-by-step evaluation, and optionally documents calculations via AI.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install linexcel
|
|
40
|
+
# AI documentation (optional):
|
|
41
|
+
pip install "linexcel[ai]"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from linexcel import analyze
|
|
48
|
+
|
|
49
|
+
result = analyze("workbook.xlsx")
|
|
50
|
+
result # interactive graph in marimo / Jupyter
|
|
51
|
+
result.save_html("out.html") # standalone offline HTML viewer
|
|
52
|
+
result.stats # {totalFormulas, totalNodes, ...}
|
|
53
|
+
result.warnings # list[str]
|
|
54
|
+
|
|
55
|
+
# AI documentation (optional, requires google-genai):
|
|
56
|
+
docs = result.document(api_key="...") # all calculation nodes
|
|
57
|
+
docs = result.document(node_ids=["A1"], api_key="...")
|
|
58
|
+
result.save_html("out.html", docs=docs) # docs embedded in HTML
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Features
|
|
62
|
+
|
|
63
|
+
- **Formula extraction** via [formualizer](https://pypi.org/project/formualizer/) (Rust engine)
|
|
64
|
+
- **Stretched pattern grouping** — 1000 identical formulas → 1 node
|
|
65
|
+
- **Dependency graph** — cells, ranges, defined names, VBA procedures
|
|
66
|
+
- **Step-by-step evaluation** — each operator/function evaluated individually
|
|
67
|
+
- **Standalone HTML viewer** — Cytoscape.js embedded, fully offline
|
|
68
|
+
- **AI documentation** — Gemini generates provable docs from deterministic lineage
|
linexcel-0.1.0/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# linexcel
|
|
2
|
+
|
|
3
|
+
Data lineage analysis for Excel workbooks.
|
|
4
|
+
|
|
5
|
+
Extracts every formula, groups stretched patterns (R1C1 canonicalization), builds a dependency graph (cells, ranges, defined names, VBA), decomposes composite functions with step-by-step evaluation, and optionally documents calculations via AI.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install linexcel
|
|
11
|
+
# AI documentation (optional):
|
|
12
|
+
pip install "linexcel[ai]"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from linexcel import analyze
|
|
19
|
+
|
|
20
|
+
result = analyze("workbook.xlsx")
|
|
21
|
+
result # interactive graph in marimo / Jupyter
|
|
22
|
+
result.save_html("out.html") # standalone offline HTML viewer
|
|
23
|
+
result.stats # {totalFormulas, totalNodes, ...}
|
|
24
|
+
result.warnings # list[str]
|
|
25
|
+
|
|
26
|
+
# AI documentation (optional, requires google-genai):
|
|
27
|
+
docs = result.document(api_key="...") # all calculation nodes
|
|
28
|
+
docs = result.document(node_ids=["A1"], api_key="...")
|
|
29
|
+
result.save_html("out.html", docs=docs) # docs embedded in HTML
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- **Formula extraction** via [formualizer](https://pypi.org/project/formualizer/) (Rust engine)
|
|
35
|
+
- **Stretched pattern grouping** — 1000 identical formulas → 1 node
|
|
36
|
+
- **Dependency graph** — cells, ranges, defined names, VBA procedures
|
|
37
|
+
- **Step-by-step evaluation** — each operator/function evaluated individually
|
|
38
|
+
- **Standalone HTML viewer** — Cytoscape.js embedded, fully offline
|
|
39
|
+
- **AI documentation** — Gemini generates provable docs from deterministic lineage
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "linexcel"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Data lineage analysis for Excel workbooks — formula extraction, dependency graph, step-by-step evaluation, AI documentation"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [{ name = "Philippe Calvet", email = "phcalvet@gmail.com" }]
|
|
9
|
+
keywords = ["excel", "lineage", "formula", "dependency-graph", "vba", "openpyxl"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Topic :: Office/Business :: Financial :: Spreadsheet",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"openpyxl>=3.1",
|
|
23
|
+
"formualizer>=0.7.1",
|
|
24
|
+
"oletools>=0.60.2",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
ai = ["google-genai>=2.10.0"]
|
|
29
|
+
dev = ["pytest>=8.0", "ruff>=0.9"]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/auspect/linexcel"
|
|
33
|
+
Repository = "https://github.com/auspect/linexcel"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["hatchling"]
|
|
37
|
+
build-backend = "hatchling.build"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/linexcel"]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.sdist]
|
|
43
|
+
artifacts = ["src/linexcel/assets/*.js"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
target-version = "py311"
|
|
47
|
+
line-length = 88
|
|
48
|
+
|
|
49
|
+
[tool.ruff.lint]
|
|
50
|
+
select = ["E", "F", "I", "N", "W", "UP"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint.per-file-ignores]
|
|
53
|
+
"src/linexcel/viewer.py" = ["E501"]
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
testpaths = ["tests"]
|
|
57
|
+
filterwarnings = [
|
|
58
|
+
"ignore:::oletools.olevba",
|
|
59
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Analyse de data lineage pour classeurs Excel.
|
|
2
|
+
|
|
3
|
+
Pipeline : extraction des formules (formualizer, moteur Rust) →
|
|
4
|
+
regroupement des formules étirées (canonicalisation R1C1) →
|
|
5
|
+
graphe de dépendances (cellules, plages, noms définis, VBA) →
|
|
6
|
+
décomposition des fonctions composées avec évaluation pas-à-pas →
|
|
7
|
+
documentation automatique par IA (google-genai) fondée sur le
|
|
8
|
+
lignage déterministe.
|
|
9
|
+
|
|
10
|
+
Utilisable comme bibliothèque autonome (marimo, Jupyter, script), sans
|
|
11
|
+
serveur FastAPI ni clé IA :
|
|
12
|
+
|
|
13
|
+
from linexcel import analyze
|
|
14
|
+
result = analyze("classeur.xlsx") # -> LineageResult
|
|
15
|
+
result # graphe interactif dans marimo
|
|
16
|
+
result.save_html("lineage.html") # visualiseur autonome
|
|
17
|
+
result.stats, result.warnings # métadonnées
|
|
18
|
+
result.document(api_key="…") # documentation IA (optionnelle)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from linexcel.analyzer import analyze_workbook
|
|
22
|
+
from linexcel.result import LineageResult, analyze
|
|
23
|
+
|
|
24
|
+
__all__ = ["analyze", "LineageResult", "analyze_workbook"]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Documentation automatique des calculs par IA (google-genai / Gemini).
|
|
2
|
+
|
|
3
|
+
Le modèle ne « devine » rien : chaque nœud lui est présenté avec son dossier
|
|
4
|
+
déterministe issu du graphe (formule exacte, décomposition évaluée étape par
|
|
5
|
+
étape, précédents et leurs valeurs, dépendants, étendue du groupe étiré,
|
|
6
|
+
liens VBA). La consigne impose de ne citer que ces faits, ce qui rend la
|
|
7
|
+
documentation « prouvable » : chaque affirmation renvoie à la formule ou à
|
|
8
|
+
une valeur du classeur.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
DEFAULT_MODEL = "gemini-2.5-flash"
|
|
18
|
+
MAX_DOSSIER_CHARS = 6_000
|
|
19
|
+
|
|
20
|
+
_SYSTEM = """Tu documentes des calculs Excel pour un lecteur métier francophone.
|
|
21
|
+
Pour le nœud fourni, rédige une fiche courte en Markdown :
|
|
22
|
+
1. **Rôle** — une phrase sur ce que calcule la formule ;
|
|
23
|
+
2. **Comment** — la logique, étape par étape, en t'appuyant STRICTEMENT sur la
|
|
24
|
+
décomposition fournie (cite les sous-expressions et leurs valeurs évaluées) ;
|
|
25
|
+
3. **Sources** — d'où viennent les données (précédents, plages, noms, VBA) ;
|
|
26
|
+
4. **Preuve** — la formule exacte et, si disponible, la valeur calculée.
|
|
27
|
+
Règles absolues : n'invente aucune donnée ; n'affirme rien qui ne soit pas dans
|
|
28
|
+
le dossier ; si une information manque, écris « non déterminé par le lignage ».
|
|
29
|
+
Réponds UNIQUEMENT avec la fiche Markdown, aucun JSON, aucun délimiteur."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AiDocError(RuntimeError):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _client(api_key: str | None = None):
|
|
37
|
+
try:
|
|
38
|
+
from google import genai
|
|
39
|
+
except ImportError as exc: # pragma: no cover
|
|
40
|
+
raise AiDocError(
|
|
41
|
+
"Le paquet google-genai n'est pas installé "
|
|
42
|
+
"(pip install 'backend[ai]' ou pip install google-genai)"
|
|
43
|
+
) from exc
|
|
44
|
+
api_key = api_key or os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")
|
|
45
|
+
if not api_key:
|
|
46
|
+
raise AiDocError(
|
|
47
|
+
"Aucune clé Gemini fournie : passer api_key=... ou définir "
|
|
48
|
+
"GOOGLE_API_KEY dans l'environnement"
|
|
49
|
+
)
|
|
50
|
+
return genai.Client(api_key=api_key)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def build_dossier(graph: dict[str, Any], node_id: str) -> dict[str, Any] | None:
|
|
54
|
+
"""Dossier déterministe d'un nœud : tout ce que l'IA a le droit d'utiliser."""
|
|
55
|
+
nodes = {n["id"]: n for n in graph["nodes"]}
|
|
56
|
+
node = nodes.get(node_id)
|
|
57
|
+
if node is None:
|
|
58
|
+
return None
|
|
59
|
+
precedents, dependents = [], []
|
|
60
|
+
for e in graph["edges"]:
|
|
61
|
+
if e["target"] == node_id:
|
|
62
|
+
src = nodes.get(e["source"], {})
|
|
63
|
+
precedents.append(_neighbor(src, e))
|
|
64
|
+
elif e["source"] == node_id:
|
|
65
|
+
dst = nodes.get(e["target"], {})
|
|
66
|
+
dependents.append(_neighbor(dst, e))
|
|
67
|
+
dossier = {
|
|
68
|
+
"node_id": node_id,
|
|
69
|
+
"kind": node.get("kind"),
|
|
70
|
+
"sheet": node.get("sheet"),
|
|
71
|
+
"adresse": node.get("addr"),
|
|
72
|
+
"formule": node.get("formula"),
|
|
73
|
+
"forme_r1c1": node.get("r1c1"),
|
|
74
|
+
"cellules_du_groupe": node.get("count"),
|
|
75
|
+
"etendue": node.get("bbox"),
|
|
76
|
+
"valeur_calculee": node.get("value"),
|
|
77
|
+
"exemples_valeurs": node.get("samples"),
|
|
78
|
+
"decomposition": _compact_steps(node.get("steps")),
|
|
79
|
+
"precedents": precedents[:30],
|
|
80
|
+
"dependants": dependents[:30],
|
|
81
|
+
}
|
|
82
|
+
if node.get("kind") == "vba":
|
|
83
|
+
dossier["vba"] = {
|
|
84
|
+
"module": node.get("module"),
|
|
85
|
+
"procedure": node.get("proc"),
|
|
86
|
+
"type": node.get("procKind"),
|
|
87
|
+
"code": (node.get("code") or "")[:2500],
|
|
88
|
+
}
|
|
89
|
+
return dossier
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _neighbor(other: dict, edge: dict) -> dict:
|
|
93
|
+
return {
|
|
94
|
+
"id": other.get("id"),
|
|
95
|
+
"kind": other.get("kind"),
|
|
96
|
+
"label": other.get("label"),
|
|
97
|
+
"lien": edge.get("kind"),
|
|
98
|
+
"valeur": other.get("value"),
|
|
99
|
+
"formule": other.get("formula"),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _compact_steps(step: dict | None) -> dict | None:
|
|
104
|
+
if step is None:
|
|
105
|
+
return None
|
|
106
|
+
out = {
|
|
107
|
+
"expression": step.get("expr"),
|
|
108
|
+
"operation": step.get("label"),
|
|
109
|
+
"valeur": step.get("value") if step.get("evaluated") else "non évaluée",
|
|
110
|
+
}
|
|
111
|
+
if step.get("inputs"):
|
|
112
|
+
out["entrees"] = step["inputs"]
|
|
113
|
+
children = [_compact_steps(c) for c in step.get("children", [])]
|
|
114
|
+
if children:
|
|
115
|
+
out["sous_etapes"] = children
|
|
116
|
+
return out
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def document_nodes(
|
|
120
|
+
graph: dict[str, Any],
|
|
121
|
+
node_ids: list[str],
|
|
122
|
+
model: str | None = None,
|
|
123
|
+
api_key: str | None = None,
|
|
124
|
+
) -> dict[str, str]:
|
|
125
|
+
"""Documente les nœuds demandés, par lots, et retourne {node_id: markdown}."""
|
|
126
|
+
client = _client(api_key)
|
|
127
|
+
model = model or os.getenv("GEMINI_MODEL", DEFAULT_MODEL)
|
|
128
|
+
docs: dict[str, str] = {}
|
|
129
|
+
dossiers = []
|
|
130
|
+
for nid in node_ids:
|
|
131
|
+
d = build_dossier(graph, nid)
|
|
132
|
+
if d is not None:
|
|
133
|
+
blob = json.dumps(d, ensure_ascii=False, default=str)
|
|
134
|
+
if len(blob) > MAX_DOSSIER_CHARS:
|
|
135
|
+
d["decomposition"] = "tronquée (formule très longue)"
|
|
136
|
+
blob = json.dumps(d, ensure_ascii=False, default=str)
|
|
137
|
+
dossiers.append((nid, blob))
|
|
138
|
+
|
|
139
|
+
for nid, blob in dossiers:
|
|
140
|
+
prompt = (
|
|
141
|
+
_SYSTEM
|
|
142
|
+
+ "\n\nDossier de lignage (déterministe, extrait du classeur) :\n"
|
|
143
|
+
+ blob
|
|
144
|
+
)
|
|
145
|
+
try:
|
|
146
|
+
response = client.models.generate_content(
|
|
147
|
+
model=model,
|
|
148
|
+
contents=prompt,
|
|
149
|
+
config={"temperature": 0.2},
|
|
150
|
+
)
|
|
151
|
+
text = (response.text or "").strip()
|
|
152
|
+
except AiDocError:
|
|
153
|
+
raise
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
raise AiDocError(f"Appel Gemini en échec : {exc}") from exc
|
|
156
|
+
if text:
|
|
157
|
+
docs[nid] = text
|
|
158
|
+
return docs
|