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.
@@ -0,0 +1,14 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+
9
+ # IDE
10
+ .vscode/
11
+ .idea/
12
+
13
+ # OS
14
+ .DS_Store
@@ -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
@@ -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