6c8c235287
- Marker tragen KEY (ID der ID-Datei) und SLOTS (Bauteil je Slot), die Zeichnung die Bauteiltabelle SPS_SKEL_PARTS (XRECORD) fuer "Im Plan waehlen" im Dialog TRO_EDIT (dxfmakros) - --read-tros schreibt geaenderte, neue und geloeschte Marker als Abschnitte edit/edit_add nach cfg/tro_overrides.ini; tro_flow.apply_edits wendet nur diese Arten an (Bauteile mit Lage, exportfest) - --emit-lisp schreibt dazu *TRO-SLOTS* und *TRO-SLOT-KINDS* - model: Lagerlinien zaehlen je TRO (Katalog nur Vorgabe), Mubea-Speicher jetzt 20 Linien (Golden-Datei) - Staubereich als stehendes graues Rechteck direkt vor seinem TRO-Marker (jam_abstand statt jam_versatz); Marker 150 mm, Text 120 mm Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
532 lines
20 KiB
Python
532 lines
20 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
tro_overrides.py - manuelle Korrekturen an der abgeleiteten TRO-Liste.
|
|
|
|
`tro_flow.py` leitet die TROs allein aus dem mechanischen Layout (CSV-Export)
|
|
ab. Manches steht dort aber nicht drin: eine Weiche, die im Export fehlt, ein
|
|
Scanner, der den Typ auf `1Sep_SSCC` hebt, oder eine Stelle, die vor Ort als
|
|
eigener TRO erkannt wurde. Solche Erkenntnisse kommen aus der Begehung bzw.
|
|
aus dem Review der erzeugten Diagramme.
|
|
|
|
Damit sie den naechsten Lauf ueberleben, stehen sie in einer INI-Datei
|
|
(`%SKEL_CFG%/tro_overrides.ini`) statt in den erzeugten Dateien.
|
|
|
|
Seit 2026-10-05 nur noch von `material_flow.py --connect` gelesen (Abschnitt
|
|
`[<Anlage>.manual]` der connect.ini). `tro_flow.py` und `tro_annotate.py`
|
|
wenden keine Korrekturen mehr an: die Abschnitte hingen an laufenden
|
|
TRO-Nummern und veralteten mit jedem Export - Fehler der Ableitung werden
|
|
seitdem im Code behoben (lib/tro_flow.py), nicht in der INI.
|
|
|
|
Aufbau der INI (Sektionsnamen mit dem CSV-Dateinamen als Praefix, damit
|
|
mehrere Anlagen in einer Datei stehen koennen):
|
|
|
|
[<csv>.tro.<TRO-ID>] Korrektur an einem abgeleiteten TRO
|
|
type = 1Sep1Swi neuer Katalogtyp
|
|
reason = ... Begruendung (kommt in die Doku)
|
|
confidence = bestaetigt Vertrauensangabe (Default: bestaetigt)
|
|
|
|
[<csv>.openpoint.<Name>] offener Punkt - noch KEIN TRO (model.TroCandidate)
|
|
x = 529.95 Anlagenkoordinate in mm
|
|
y = -3895.27
|
|
z = 1468
|
|
expect = 1Sep1Swi hier erwarteter TRO-Typ (-> type_name)
|
|
belongs_to = TRO10 TRO, der das Bauteil heute enthaelt
|
|
note = ... Erlaeuterung
|
|
|
|
Offene Punkte aendern die TRO-Struktur ausdruecklich **nicht**.
|
|
|
|
Handkorrekturen aus BricsCAD (seit 2026-10-26)
|
|
-----------------------------------------------
|
|
Die Abschnitte oben wendet tro_flow nicht mehr an (siehe unten). Die Arten
|
|
`edit` und `edit_add` dagegen schon: sie schreibt `tro_annotate --read-tros`
|
|
aus den im Dialog TRO_EDIT/TRO_INSERT (dxfmakros) geaenderten Markern, und
|
|
`tro_flow.apply_edits` wendet sie nach der Ableitung an. Sie haengen an der
|
|
ID aus `<csv>_tro_ids.ini`, die ueber die Lage des Separators stabil bleibt -
|
|
anders als die laufenden Nummern, an denen die alten Abschnitte veraltet sind.
|
|
|
|
[<csv>.edit.<ID>] Korrektur an einem abgeleiteten TRO
|
|
type = 1Sep1Swi neuer Katalogtyp (fehlt = wie abgeleitet)
|
|
name = Einschleus_1 neue Kennung (fehlt = <ID> bleibt)
|
|
delete = true TRO entfaellt
|
|
separator1 = 0010 @ 1234.5 -567.8 Bauteil je Slot: TeileId @ x y (mm,
|
|
switch1 = - CSV); "-" = Slot bewusst leer
|
|
lines = 0066 @ 958 12300, 0069 @ 958 12600 Lagerlinien in Reihenfolge
|
|
reason = ... Begruendung (bleibt beim Neuschreiben erhalten)
|
|
|
|
[<csv>.edit_add.<ID>] neuer TRO (TRO_INSERT), Schluessel wie oben
|
|
|
|
Stehen Slots im Abschnitt, gelten sie vollstaendig (fehlende Slots des Typs
|
|
bleiben leer); stehen keine, bleiben die Bauteile wie abgeleitet. Die Lage
|
|
hinter der TeileId macht den Eintrag exportfest: passt die TeileId im neuen
|
|
Export nicht mehr zur Lage, gilt das naechste Bauteil derselben Art dort.
|
|
|
|
Die Felder heissen wie in lib/model.py (`type_name`, `separators`, `hosts`,
|
|
`reason`, `confidence`); Verbindungen und offene Punkte sind direkt
|
|
`model.Connection` bzw. `model.TroCandidate`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import configparser
|
|
import os
|
|
import re
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
from model import Connection, TroCandidate
|
|
from tro_catalog import get_tro
|
|
|
|
OVERRIDE_FILE = "tro_overrides.ini"
|
|
|
|
# Vertrauensangabe fuer manuell gesetzte Typen. Bewusst ein eigener Wert - so
|
|
# ist in der Doku sichtbar, dass der Typ nicht aus dem Layout stammt.
|
|
DEFAULT_CONFIDENCE = "bestaetigt"
|
|
|
|
|
|
@dataclass
|
|
class TroOverride:
|
|
"""Korrektur an einem abgeleiteten TRO.
|
|
|
|
Adressiert entweder ueber die TRO-ID (`TRO03`) oder - stabiler - ueber die
|
|
Nummer eines seiner Separatoren (`SEP0011`). Die TRO-IDs sind laufende
|
|
Nummern eines Laufs und verschieben sich, sobald TROs dazukommen oder
|
|
wegfallen; die Separator-Nummer aus dem CSV-Export tut das nicht.
|
|
"""
|
|
|
|
# genau eins von beiden ist gesetzt
|
|
tro_id: str = ""
|
|
separator: str = ""
|
|
type_name: str = ""
|
|
reason: str = ""
|
|
confidence: str = ""
|
|
|
|
def matches(self, tro) -> bool:
|
|
if self.separator:
|
|
return self.separator in tro.separators
|
|
return self.tro_id == tro.tro_id
|
|
|
|
|
|
@dataclass
|
|
class SplitSpec:
|
|
"""Ein neuer TRO, herausgeloest aus einem bereits abgeleiteten.
|
|
|
|
Fuer Uebergabestellen, die die Ableitung nicht als eigenstaendig erkennt,
|
|
weil die "Zuordnung" des Separators auf ein Transportobjekt zeigt (Strecke,
|
|
Gefaellestrecke) statt auf die Kreisel-Bahn, an der er tatsaechlich
|
|
schaltet. Die genannten Separatoren/Scanner werden ihrem bisherigen TRO
|
|
entnommen und bilden einen neuen.
|
|
"""
|
|
|
|
name: str
|
|
type_name: str
|
|
separators: list[str] = field(default_factory=list)
|
|
scanners: list[str] = field(default_factory=list)
|
|
hosts: list[str] = field(default_factory=list)
|
|
label: str = ""
|
|
reason: str = ""
|
|
confidence: str = DEFAULT_CONFIDENCE
|
|
|
|
|
|
@dataclass
|
|
class MergeSpec:
|
|
"""Mehrere abgeleitete TROs zu einem Steuerobjekt zusammenfassen.
|
|
|
|
Fuer Stellen, die mechanisch aus mehreren Bauteilgruppen bestehen,
|
|
steuerungstechnisch aber eine einzige Uebergabestelle sind - etwa zwei
|
|
Sperren beiderseits eines Kreisel-Beruehrpunkts. Adressiert wird ueber die
|
|
Separatoren, weil deren Nummern ueber Exporte hinweg stabiler sind als die
|
|
laufenden TRO-Nummern.
|
|
"""
|
|
|
|
name: str
|
|
type_name: str
|
|
separators: list[str] = field(default_factory=list)
|
|
hosts: list[str] = field(default_factory=list)
|
|
label: str = ""
|
|
reason: str = ""
|
|
confidence: str = DEFAULT_CONFIDENCE
|
|
|
|
|
|
@dataclass
|
|
class PartRef:
|
|
"""Bauteil in einem Slot: TeileId plus Lage (mm, CSV) als Rueckfallebene.
|
|
|
|
Die TeileId wechselt zwischen zwei Exporten derselben Anlage, die Lage
|
|
nicht (wie bei der ID-Datei, lib/tro_ids.py). ``teile_id == ""`` ist ein
|
|
bewusst leerer Slot.
|
|
"""
|
|
|
|
teile_id: str = ""
|
|
x: float | None = None
|
|
y: float | None = None
|
|
|
|
def render(self) -> str:
|
|
"""'0010 @ 1234.5 -567.8', '0010' ohne Lage, '-' fuer leer."""
|
|
if not self.teile_id:
|
|
return "-"
|
|
if self.x is None or self.y is None:
|
|
return self.teile_id
|
|
return f"{self.teile_id} @ {self.x:.1f} {self.y:.1f}"
|
|
|
|
@classmethod
|
|
def parse(cls, raw: str) -> PartRef:
|
|
"""Gegenstueck zu :meth:`render` (ValueError bei falscher Form)."""
|
|
text = raw.strip()
|
|
if text in ("", "-"):
|
|
return cls()
|
|
teile_id, at, pos = (t.strip() for t in text.partition("@"))
|
|
if not teile_id or " " in teile_id:
|
|
raise ValueError(f"'{raw}' ist kein Bauteil '<TeileId> @ x y'")
|
|
if not at:
|
|
return cls(teile_id=teile_id)
|
|
werte = pos.replace(",", " ").split()
|
|
if len(werte) != 2:
|
|
raise ValueError(f"'{raw}': Lage hinter @ braucht x und y")
|
|
return cls(teile_id=teile_id, x=float(werte[0]), y=float(werte[1]))
|
|
|
|
|
|
# Slot-Schluessel in einem edit-Abschnitt (lines steht fuer alle Lagerlinien)
|
|
_SLOT_KEY = re.compile(r"^(separator|switch|scanner)\d+$|^drive$")
|
|
_EDIT_KEYS = ("type", "name", "delete", "reason", "lines")
|
|
|
|
|
|
@dataclass
|
|
class TroEdit:
|
|
"""Handkorrektur aus BricsCAD (Abschnitt edit bzw. edit_add).
|
|
|
|
``key`` ist die ID aus ``<csv>_tro_ids.ini`` (edit) bzw. die Kennung des
|
|
neuen TRO (edit_add). ``slots``/``lines`` sind None, wenn die Bauteile wie
|
|
abgeleitet bleiben.
|
|
"""
|
|
|
|
key: str
|
|
add: bool = False
|
|
delete: bool = False
|
|
name: str = ""
|
|
type_name: str = ""
|
|
slots: dict[str, PartRef] | None = None
|
|
lines: list[PartRef] | None = None
|
|
reason: str = ""
|
|
|
|
@property
|
|
def section_kind(self) -> str:
|
|
return "edit_add" if self.add else "edit"
|
|
|
|
def is_empty(self) -> bool:
|
|
"""Nichts zu tun (ein edit-Abschnitt ohne Aenderung entfaellt)."""
|
|
return not (self.add or self.delete or self.name or self.type_name
|
|
or self.slots is not None or self.lines is not None)
|
|
|
|
|
|
@dataclass
|
|
class Overrides:
|
|
"""Alle Korrekturen zu einer CSV-Datei.
|
|
|
|
``edits`` (Handkorrekturen aus BricsCAD) zaehlen fuer ``bool()`` nicht mit:
|
|
material_flow --connect liest nur die uebrigen Arten.
|
|
"""
|
|
|
|
source: Path | None = None
|
|
tros: dict[str, TroOverride] = field(default_factory=dict)
|
|
splits: list[SplitSpec] = field(default_factory=list)
|
|
merges: list[MergeSpec] = field(default_factory=list)
|
|
# Gesetzte Topologie: ersetzt die abgeleiteten Kanten vollstaendig
|
|
connections: list[Connection] = field(default_factory=list)
|
|
candidates: list[TroCandidate] = field(default_factory=list)
|
|
# Handkorrekturen aus BricsCAD in Dateireihenfolge (tro_flow.apply_edits)
|
|
edits: list[TroEdit] = field(default_factory=list)
|
|
# Protokoll der tatsaechlich angewandten Korrekturen (fuer die Doku)
|
|
applied: list[str] = field(default_factory=list)
|
|
warnings: list[str] = field(default_factory=list)
|
|
|
|
def __bool__(self) -> bool:
|
|
return bool(self.tros or self.splits or self.merges or self.connections
|
|
or self.candidates)
|
|
|
|
|
|
def override_path(cfg_dir: Path | None = None) -> Path:
|
|
"""Pfad zur Override-Datei: %SKEL_CFG%, sonst <projekt>/cfg."""
|
|
if cfg_dir is not None:
|
|
return cfg_dir / OVERRIDE_FILE
|
|
env = os.environ.get("SKEL_CFG")
|
|
if env:
|
|
return Path(env) / OVERRIDE_FILE
|
|
return Path(__file__).resolve().parent.parent / "cfg" / OVERRIDE_FILE
|
|
|
|
|
|
def _as_float(raw: str, where: str, warnings: list[str]) -> float | None:
|
|
try:
|
|
return float(str(raw).strip())
|
|
except (TypeError, ValueError):
|
|
warnings.append(f"{where}: '{raw}' ist keine Zahl - Eintrag ignoriert")
|
|
return None
|
|
|
|
|
|
def _as_list(raw: str) -> list[str]:
|
|
return [s.strip() for s in raw.split(",") if s.strip()]
|
|
|
|
|
|
def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
|
|
"""Korrekturen zu `csv_name` laden (leeres Ergebnis, wenn es keine gibt)."""
|
|
path = override_path(cfg_dir)
|
|
result = Overrides()
|
|
if not path.is_file():
|
|
return result
|
|
result.source = path
|
|
|
|
parser = configparser.ConfigParser(inline_comment_prefixes=(";",))
|
|
# Schluessel sind Kleinschreibung, Sektionsnamen bleiben wie geschrieben.
|
|
parser.read(path, encoding="utf-8")
|
|
|
|
prefix = csv_name.lower()
|
|
for section in parser.sections():
|
|
# [<csv>.connections] - gesetzte Topologie, eigene Form ohne <name>
|
|
if section.lower() == f"{prefix}.connections":
|
|
for key, raw in parser[section].items():
|
|
pfeil, _, rest = str(raw).partition("|")
|
|
src, sep, dst = (t.strip() for t in pfeil.partition("->"))
|
|
if not sep or not src or not dst:
|
|
result.warnings.append(
|
|
f"[{section}] {key}: '{raw}' ist keine Verbindung "
|
|
f"'<von> -> <nach>'"
|
|
)
|
|
continue
|
|
result.connections.append(
|
|
Connection(src=src, dst=dst, kind=rest.strip())
|
|
)
|
|
continue
|
|
|
|
parts = section.split(".")
|
|
# <csv>.<art>.<name> - der CSV-Name kann selbst einen Punkt enthalten
|
|
if len(parts) < 3:
|
|
continue
|
|
kind, name = parts[-2].lower(), parts[-1]
|
|
owner = ".".join(parts[:-2]).lower()
|
|
if owner != prefix:
|
|
continue
|
|
values = parser[section]
|
|
|
|
if kind == "tro":
|
|
type_name = values.get("type", "").strip()
|
|
if type_name and get_tro(type_name) is None:
|
|
result.warnings.append(
|
|
f"[{section}]: Typ '{type_name}' steht nicht im TRO-Katalog "
|
|
f"- Korrektur ignoriert"
|
|
)
|
|
continue
|
|
key = name.upper()
|
|
by_sep = key.startswith("SEP")
|
|
result.tros[key] = TroOverride(
|
|
tro_id="" if by_sep else key,
|
|
separator=key[3:] if by_sep else "",
|
|
type_name=type_name,
|
|
reason=values.get("reason", "").strip(),
|
|
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
|
|
)
|
|
elif kind == "split":
|
|
type_name = values.get("type", "").strip()
|
|
if get_tro(type_name) is None:
|
|
result.warnings.append(
|
|
f"[{section}]: Typ '{type_name}' steht nicht im TRO-Katalog "
|
|
f"- neuer TRO nicht angelegt"
|
|
)
|
|
continue
|
|
seps = _as_list(values.get("separators", ""))
|
|
if not seps:
|
|
result.warnings.append(
|
|
f"[{section}]: 'separators' fehlt - ohne Separator kein TRO"
|
|
)
|
|
continue
|
|
result.splits.append(
|
|
SplitSpec(
|
|
name=name,
|
|
type_name=type_name,
|
|
separators=seps,
|
|
scanners=_as_list(values.get("scanners", "")),
|
|
hosts=_as_list(values.get("hosts", values.get("host", ""))),
|
|
label=values.get("label", "").strip(),
|
|
reason=values.get("reason", "").strip(),
|
|
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
|
|
)
|
|
)
|
|
elif kind == "merge":
|
|
type_name = values.get("type", "").strip()
|
|
if get_tro(type_name) is None:
|
|
result.warnings.append(
|
|
f"[{section}]: Typ '{type_name}' steht nicht im TRO-Katalog "
|
|
f"- Zusammenfassung nicht durchgefuehrt"
|
|
)
|
|
continue
|
|
seps = _as_list(values.get("separators", ""))
|
|
if len(seps) < 2:
|
|
result.warnings.append(
|
|
f"[{section}]: 'separators' braucht mindestens zwei Nummern"
|
|
)
|
|
continue
|
|
result.merges.append(
|
|
MergeSpec(
|
|
name=name,
|
|
type_name=type_name,
|
|
separators=seps,
|
|
hosts=_as_list(values.get("hosts", values.get("host", ""))),
|
|
label=values.get("label", "").strip(),
|
|
reason=values.get("reason", "").strip(),
|
|
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
|
|
)
|
|
)
|
|
elif kind == "openpoint":
|
|
x = _as_float(values.get("x", ""), f"[{section}] x", result.warnings)
|
|
y = _as_float(values.get("y", ""), f"[{section}] y", result.warnings)
|
|
if x is None or y is None:
|
|
continue
|
|
z_raw = values.get("z", "").strip()
|
|
z = _as_float(z_raw, f"[{section}] z", result.warnings) if z_raw else None
|
|
type_name = values.get("expect", "").strip()
|
|
if type_name and get_tro(type_name) is None:
|
|
result.warnings.append(
|
|
f"[{section}]: erwarteter Typ '{type_name}' steht nicht im "
|
|
f"TRO-Katalog"
|
|
)
|
|
result.candidates.append(
|
|
TroCandidate(
|
|
name=name,
|
|
type_name=type_name,
|
|
position=(x, y, z if z is not None else 0.0),
|
|
belongs_to=values.get("belongs_to", "").strip(),
|
|
note=values.get("note", "").strip(),
|
|
)
|
|
)
|
|
elif kind in ("edit", "edit_add"):
|
|
edit = _parse_edit(section, name, kind == "edit_add", values,
|
|
result.warnings)
|
|
if edit is not None:
|
|
result.edits.append(edit)
|
|
else:
|
|
result.warnings.append(
|
|
f"[{section}]: unbekannte Art '{kind}' - erwartet 'tro', 'split', "
|
|
f"'merge', 'openpoint', 'edit' oder 'edit_add'"
|
|
)
|
|
|
|
result.candidates.sort(key=lambda p: p.name)
|
|
return result
|
|
|
|
|
|
def _parse_edit(section: str, name: str, add: bool, values,
|
|
warnings: list[str]) -> TroEdit | None:
|
|
"""Einen edit-/edit_add-Abschnitt lesen (None bei einem Fehler)."""
|
|
type_name = values.get("type", "").strip()
|
|
if type_name and get_tro(type_name) is None:
|
|
warnings.append(f"[{section}]: Typ '{type_name}' steht nicht im TRO-Katalog "
|
|
f"- Korrektur ignoriert")
|
|
return None
|
|
if add and not type_name:
|
|
warnings.append(f"[{section}]: neuer TRO ohne 'type' - nicht angelegt")
|
|
return None
|
|
edit = TroEdit(
|
|
key=name,
|
|
add=add,
|
|
delete=values.get("delete", "").strip().lower() in ("true", "ja", "1", "yes"),
|
|
name=values.get("name", "").strip(),
|
|
type_name=type_name,
|
|
reason=values.get("reason", "").strip(),
|
|
)
|
|
try:
|
|
slots = {key: PartRef.parse(raw) for key, raw in values.items()
|
|
if key not in _EDIT_KEYS}
|
|
if "lines" in values:
|
|
edit.lines = [PartRef.parse(t) for t in values["lines"].split(",") if t.strip()]
|
|
except ValueError as exc:
|
|
warnings.append(f"[{section}]: {exc} - Korrektur ignoriert")
|
|
return None
|
|
falsch = sorted(k for k in slots if not _SLOT_KEY.match(k))
|
|
if falsch:
|
|
warnings.append(f"[{section}]: unbekannte Schluessel {', '.join(falsch)} - "
|
|
f"erwartet type, name, delete, reason, lines oder Slots "
|
|
f"(separator1, switch1, scanner1, drive)")
|
|
return None
|
|
if slots:
|
|
edit.slots = slots
|
|
return edit
|
|
|
|
|
|
# Kopfzeile des von --read-tros geschriebenen Blocks; beim Neuschreiben
|
|
# wird sie samt aller edit-/edit_add-Abschnitte der CSV ersetzt.
|
|
_EDIT_HEAD = "; --- Handkorrekturen aus BricsCAD zu {csv}"
|
|
_HEADER = re.compile(r"^\s*\[(.+)\]\s*$")
|
|
|
|
|
|
def render_edit(csv_name: str, edit: TroEdit) -> str:
|
|
"""Einen Abschnitt als INI-Text."""
|
|
lines = [f"[{csv_name}.{edit.section_kind}.{edit.key}]"]
|
|
if edit.delete:
|
|
lines.append("delete = true")
|
|
if edit.type_name:
|
|
lines.append(f"type = {edit.type_name}")
|
|
if edit.name:
|
|
lines.append(f"name = {edit.name}")
|
|
for slot, ref in (edit.slots or {}).items():
|
|
lines.append(f"{slot} = {ref.render()}")
|
|
if edit.lines is not None:
|
|
lines.append(f"lines = {', '.join(r.render() for r in edit.lines)}")
|
|
if edit.reason:
|
|
lines.append(f"reason = {edit.reason}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def save_edits(csv_name: str, edits: list[TroEdit], cfg_dir: Path | None = None) -> Path:
|
|
"""Alle edit-/edit_add-Abschnitte zu `csv_name` durch `edits` ersetzen.
|
|
|
|
Der Rest der Datei (von Hand gepflegt, mit Kommentaren) bleibt Zeile fuer
|
|
Zeile erhalten; der neue Block steht am Ende. Kommentarzeilen direkt vor
|
|
dem naechsten Abschnitt gehoeren zu diesem und bleiben stehen.
|
|
"""
|
|
path = override_path(cfg_dir)
|
|
text = path.read_text(encoding="utf-8") if path.is_file() else ""
|
|
prefix = csv_name.lower()
|
|
head = _EDIT_HEAD.format(csv=csv_name).lower()
|
|
|
|
def ist_edit(section: str) -> bool:
|
|
parts = section.split(".")
|
|
return (len(parts) >= 3 and parts[-2].lower() in ("edit", "edit_add")
|
|
and ".".join(parts[:-2]).lower() == prefix)
|
|
|
|
def ist_rand(line: str) -> bool:
|
|
return not line.strip() or line.lstrip().startswith(";")
|
|
|
|
keep: list[str] = []
|
|
skipped: list[str] = []
|
|
skipping = False
|
|
for line in text.splitlines():
|
|
if line.strip().lower().startswith(head):
|
|
continue
|
|
if m := _HEADER.match(line):
|
|
if skipping:
|
|
# Kommentare/Leerzeilen am Ende gehoeren zum naechsten Abschnitt
|
|
tail: list[str] = []
|
|
while skipped and ist_rand(skipped[-1]):
|
|
tail.insert(0, skipped.pop())
|
|
keep.extend(tail)
|
|
skipping = ist_edit(m.group(1).strip())
|
|
skipped = []
|
|
if skipping:
|
|
continue
|
|
if skipping:
|
|
skipped.append(line)
|
|
else:
|
|
keep.append(line)
|
|
|
|
while keep and not keep[-1].strip():
|
|
keep.pop()
|
|
if edits:
|
|
if keep:
|
|
keep += ["", ""]
|
|
keep.append(_EDIT_HEAD.format(csv=csv_name)
|
|
+ f" - tro_annotate --read-tros, {datetime.now():%Y-%m-%d}")
|
|
for edit in edits:
|
|
keep += [render_edit(csv_name, edit), ""]
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text("\n".join(keep).rstrip("\n") + "\n", encoding="utf-8")
|
|
return path
|