2026-08-07 22:54:22 +02:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
from pathlib import Path
|
2026-08-20 13:27:23 +02:00
|
|
|
from collections import OrderedDict
|
2026-08-07 22:54:22 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
DATA = Path("data")
|
|
|
|
|
REPORTS = Path("reports")
|
|
|
|
|
|
2026-08-20 13:27:23 +02:00
|
|
|
VERSION_DIRS = sorted(
|
|
|
|
|
[d for d in DATA.iterdir() if d.is_dir()],
|
|
|
|
|
key=lambda p: [int(x) for x in p.name.split(".")]
|
|
|
|
|
)
|
|
|
|
|
VERSIONS = [v.name for v in VERSION_DIRS]
|
2026-08-07 22:54:22 +02:00
|
|
|
|
|
|
|
|
def modules():
|
2026-08-20 13:27:23 +02:00
|
|
|
first = VERSION_DIRS[0]
|
2026-08-07 22:54:22 +02:00
|
|
|
return sorted(f.stem for f in first.glob("*.json"))
|
|
|
|
|
|
2026-08-20 13:27:23 +02:00
|
|
|
|
|
|
|
|
class Operation(object):
|
|
|
|
|
def __init__(self, data, source):
|
|
|
|
|
self.condition = None
|
|
|
|
|
self.via = None
|
|
|
|
|
self.unresolved = False
|
|
|
|
|
self.__dict__.update(data)
|
|
|
|
|
self.sources = [source,]
|
|
|
|
|
self.tables_unknown = not self.tables
|
|
|
|
|
if not self.tables:
|
|
|
|
|
self.tables = ('unknown',)
|
|
|
|
|
|
|
|
|
|
def toText(self):
|
|
|
|
|
marker = "unresolved " if self.unresolved else ""
|
|
|
|
|
text = [f"- **{self.phase}** {marker}`{self.operation}`"]
|
|
|
|
|
if self.condition:
|
|
|
|
|
text.append( f" - condition: `{self.condition}`")
|
|
|
|
|
if self.object:
|
|
|
|
|
text.append( f" - object: `{self.object}`" )
|
|
|
|
|
if self.via:
|
|
|
|
|
chain = " -> ".join(f"{v['method']} (Zeile {v['line']})" for v in self.via)
|
|
|
|
|
text.append( f" - via: {chain}" )
|
|
|
|
|
if self.tables_unknown:
|
|
|
|
|
text.append(" - Tabelle nicht erkannt!")
|
|
|
|
|
text.append( " - files:" )
|
|
|
|
|
for s in self.sources:
|
|
|
|
|
text.append( f" - `{s['file']}`" )
|
|
|
|
|
if self.args:
|
|
|
|
|
text.append( f" - args: `{', '.join(self.args)}`" )
|
|
|
|
|
if self.kwargs:
|
|
|
|
|
text.append(" - kwargs:")
|
|
|
|
|
for k, v in self.kwargs.items():
|
|
|
|
|
text.append( f" - {k}: `{v}`" )
|
|
|
|
|
if self.code:
|
|
|
|
|
text.append(" - code:")
|
|
|
|
|
text.append(f" '{self.code}'")
|
|
|
|
|
text.append('')
|
|
|
|
|
return '\n'.join(text)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def key(self):
|
|
|
|
|
# kwargs are intentionally ignored for now.
|
|
|
|
|
# The key is only used to collapse repeated migration blocks;
|
|
|
|
|
# it is not intended to establish semantic equality.
|
|
|
|
|
return ( self.phase, self.operation, self.object,
|
|
|
|
|
tuple(self.tables), tuple(self.args), self.code, )
|
|
|
|
|
|
|
|
|
|
class Table:
|
|
|
|
|
|
|
|
|
|
_allTables = {}
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def get(cls, name):
|
|
|
|
|
if name not in cls._allTables:
|
|
|
|
|
cls._allTables[name] = cls(name)
|
|
|
|
|
return cls._allTables[name]
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def addObject(cls, obj):
|
|
|
|
|
new = False
|
|
|
|
|
for t in obj.tables:
|
|
|
|
|
if cls.get(t).add(obj):
|
|
|
|
|
new = True
|
|
|
|
|
return new
|
|
|
|
|
|
|
|
|
|
def __init__(self, name):
|
|
|
|
|
self.versions = {key: {} for key in VERSIONS} # was angezeigt wird
|
|
|
|
|
|
|
|
|
|
# _current_presence: der TATSÄCHLICHE, vollständige Operationsbestand
|
|
|
|
|
# der Version, die wir gerade verarbeiten (unabhängig davon, ob wir
|
|
|
|
|
# sie anzeigen). _prev_presence: derselbe vollständige Bestand der
|
|
|
|
|
# UNMITTELBAR VORHERIGEN Version. Nur dieser Vergleich (nicht ein
|
|
|
|
|
# kumulatives "jemals gesehen") ist korrekt: taucht ein Block in
|
|
|
|
|
# einer Zwischenversion NICHT auf (weil er dort entfernt/ersetzt
|
|
|
|
|
# wurde) und kommt später identisch zurück, muss er wieder als neu
|
|
|
|
|
# gelten - auch wenn exakt derselbe Code schon vor langer Zeit
|
|
|
|
|
# existierte.
|
|
|
|
|
self._current_version = None
|
|
|
|
|
self._current_presence = {}
|
|
|
|
|
self._prev_presence = {}
|
|
|
|
|
|
|
|
|
|
def _advance_to(self, version):
|
|
|
|
|
if version == self._current_version:
|
|
|
|
|
return
|
|
|
|
|
if self._current_version is not None:
|
|
|
|
|
self._prev_presence = self._current_presence
|
|
|
|
|
self._current_version = version
|
|
|
|
|
self._current_presence = {}
|
|
|
|
|
|
|
|
|
|
def add(self, obj):
|
|
|
|
|
self._advance_to(obj.version)
|
|
|
|
|
key = obj.key
|
|
|
|
|
|
|
|
|
|
already_in_this_version = key in self._current_presence
|
|
|
|
|
self._current_presence[key] = obj # immer im tatsächlichen Bestand vermerken
|
|
|
|
|
|
|
|
|
|
if already_in_this_version:
|
|
|
|
|
return None # exakt dieselbe Operation kam mehrfach in DIESER Version vor
|
|
|
|
|
|
|
|
|
|
if key in self._prev_presence:
|
|
|
|
|
return None # unverändert gegenüber der unmittelbar vorherigen Version
|
|
|
|
|
|
|
|
|
|
self.versions[obj.version][key] = obj
|
|
|
|
|
return obj
|
|
|
|
|
|
2026-08-08 00:58:56 +02:00
|
|
|
class Module(object):
|
|
|
|
|
def __init__(self, module):
|
|
|
|
|
self.name = module
|
2026-08-20 13:27:23 +02:00
|
|
|
self.tables = {}
|
2026-08-08 00:58:56 +02:00
|
|
|
self.operations = []
|
|
|
|
|
|
|
|
|
|
def read_data(self):
|
2026-08-20 13:27:23 +02:00
|
|
|
for version in VERSION_DIRS:
|
2026-08-08 00:58:56 +02:00
|
|
|
|
2026-08-08 01:29:43 +02:00
|
|
|
vers = version.name
|
2026-08-07 22:54:22 +02:00
|
|
|
|
2026-08-08 00:58:56 +02:00
|
|
|
infile = version / f"{self.name}.json"
|
2026-08-07 22:54:22 +02:00
|
|
|
if not infile.exists():
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
data = json.loads(infile.read_text())
|
2026-08-08 00:58:56 +02:00
|
|
|
|
|
|
|
|
for f in data["files"]:
|
|
|
|
|
f['version'] = vers
|
2026-08-20 13:27:23 +02:00
|
|
|
|
2026-08-08 00:58:56 +02:00
|
|
|
for o in f['operations']:
|
2026-08-20 13:27:23 +02:00
|
|
|
source = {'module': f['module'],
|
|
|
|
|
'file': f['file'],
|
|
|
|
|
'class': f['class'],
|
|
|
|
|
'method': f['method'],
|
|
|
|
|
'line': f['line']}
|
|
|
|
|
op = Operation(o, source)
|
|
|
|
|
op.version = vers
|
|
|
|
|
if Table.addObject(op):
|
|
|
|
|
for t in op.tables:
|
|
|
|
|
if t not in self.tables:
|
|
|
|
|
self.tables[t] = Table.get(t)
|
2026-08-08 02:55:30 +02:00
|
|
|
|
2026-08-08 00:58:56 +02:00
|
|
|
def write_data(self):
|
|
|
|
|
|
|
|
|
|
outfile = REPORTS / f"{self.name}.md"
|
|
|
|
|
|
|
|
|
|
with outfile.open("w", encoding="utf8") as out:
|
|
|
|
|
|
2026-08-20 13:27:23 +02:00
|
|
|
out.write(f"# modul {self.name}\n\n")
|
2026-08-08 00:58:56 +02:00
|
|
|
|
2026-08-20 13:27:23 +02:00
|
|
|
# iterate all tables
|
|
|
|
|
for t in sorted(self.tables):
|
|
|
|
|
table = self.tables[t]
|
2026-08-08 00:58:56 +02:00
|
|
|
|
2026-08-20 13:27:23 +02:00
|
|
|
out.write(f"\n## Tabelle {t}\n")
|
2026-08-08 02:55:30 +02:00
|
|
|
|
|
|
|
|
# iterate all Versions
|
2026-08-20 13:27:23 +02:00
|
|
|
for tName, ops in sorted(table.versions.items()):
|
|
|
|
|
if not ops:
|
2026-08-08 00:58:56 +02:00
|
|
|
continue
|
|
|
|
|
|
2026-08-20 13:27:23 +02:00
|
|
|
new_version = tName
|
|
|
|
|
|
2026-08-08 02:55:30 +02:00
|
|
|
# iterate all files / occureces
|
2026-08-20 13:27:23 +02:00
|
|
|
for op in ops.values():
|
|
|
|
|
|
2026-08-08 22:27:12 +02:00
|
|
|
if new_version:
|
|
|
|
|
out.write("\n")
|
|
|
|
|
out.write(f"### {new_version}\n\n")
|
|
|
|
|
new_version = None
|
2026-08-20 13:27:23 +02:00
|
|
|
out.write(op.toText())
|
|
|
|
|
out.write("\n")
|
2026-08-08 00:58:56 +02:00
|
|
|
|
2026-08-07 22:54:22 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
|
|
|
|
|
REPORTS.mkdir(exist_ok=True)
|
|
|
|
|
|
|
|
|
|
for module in modules():
|
|
|
|
|
|
|
|
|
|
print(module)
|
2026-08-08 00:58:56 +02:00
|
|
|
|
|
|
|
|
mod = Module(module)
|
|
|
|
|
|
|
|
|
|
mod.read_data()
|
|
|
|
|
mod.write_data()
|
2026-08-07 22:54:22 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|