From da734809746a8f851ac0842c9faa58f1a4228f4e Mon Sep 17 00:00:00 2001 From: Pascal Rothe <132349052+ibirothe@users.noreply.github.com> Date: Thu, 1 Jan 2026 08:43:18 +0100 Subject: [PATCH 1/4] Update CONTRIBUTING.md --- CONTRIBUTING.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f8d73a5..7fdd3b6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,6 +32,16 @@ To run the test suite enter in your terminal: uv run tox ``` +#### Pull Requests + +Ensure there is an existing issue describing the bug, feature, or improvement. If not, create one and discuss the approach before starting work. Fork the repository and create a feature branch from the `dev` branch. + +**PR Requirements:** +- Code builds successfully and all tests pass. +- New functionality includes appropriate tests. +- Documentation is updated if behavior or usage has changed. +- No unnecessary formatting or unrelated refactoring is included. + #### Building Building can also be done with `uv`! The build system (flit) is specified in the `pyproject.toml` file and should have been installed automatically by `uv`. To build the wheels enter in your terminal: From 30a454a7f894bf6e57d05cee36ab279b30b72dc3 Mon Sep 17 00:00:00 2001 From: Pascal Rothe Date: Fri, 9 Jan 2026 11:26:13 +0100 Subject: [PATCH 2/4] Redaction of ParsedDiagram and map file --- etc/development scripts/load_and_redact.py | 56 ++++++++++++++++++++++ src/drawpyo/__init__.py | 3 +- src/drawpyo/drawio_import/__init__.py | 4 +- src/drawpyo/utils/__init__.py | 3 +- src/drawpyo/utils/redact.py | 36 ++++++++++++++ 5 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 etc/development scripts/load_and_redact.py create mode 100644 src/drawpyo/utils/redact.py diff --git a/etc/development scripts/load_and_redact.py b/etc/development scripts/load_and_redact.py new file mode 100644 index 0000000..4b2f786 --- /dev/null +++ b/etc/development scripts/load_and_redact.py @@ -0,0 +1,56 @@ +from pathlib import Path +import drawpyo +from drawpyo import load_diagram + +# -------------------------------------------------- +# Paths +# -------------------------------------------------- + +base_dir = Path.home() / "Test Drawpyo Charts" +base_dir.mkdir(parents=True, exist_ok=True) + +drawio_input_path = ( + Path(__file__).parent + / ".." + / "reference drawio charts" + / "Pourover Flowchart.drawio" +).resolve() + +redaction_map_path = base_dir / "redaction_map.json" +drawio_output_path = base_dir / "Redacted Drawio File.drawio" + +# -------------------------------------------------- +# Load diagram +# -------------------------------------------------- + +diagram = load_diagram(drawio_input_path) + +# -------------------------------------------------- +# Redact diagram + write map +# -------------------------------------------------- + +redacted_diagram = drawpyo.utils.redact_values( + diagram, + map_file_path=redaction_map_path, +) + +# -------------------------------------------------- +# Create output file +# -------------------------------------------------- + +file = drawpyo.File() +file.file_path = str(base_dir) +file.file_name = drawio_output_path.name + +page = drawpyo.Page(file=file) + +# Add shapes +for shape in redacted_diagram.shapes: + shape.page = page + +# Add edges +for edge in redacted_diagram.edges: + edge.page = page + +# Write draw.io file +file.write() diff --git a/src/drawpyo/__init__.py b/src/drawpyo/__init__.py index 718a3c1..e25012c 100644 --- a/src/drawpyo/__init__.py +++ b/src/drawpyo/__init__.py @@ -7,7 +7,7 @@ from .utils.logger import logger from .utils.page_sizes import PageSize -from .drawio_import import load_diagram +from .drawio_import import load_diagram, ParsedDiagram from . import utils from . import diagram @@ -27,6 +27,7 @@ diagram_types, drawio_import, load_diagram, + ParsedDiagram, ] __version__ = "0.2.4" diff --git a/src/drawpyo/drawio_import/__init__.py b/src/drawpyo/drawio_import/__init__.py index fd0a301..ffa184a 100644 --- a/src/drawpyo/drawio_import/__init__.py +++ b/src/drawpyo/drawio_import/__init__.py @@ -1,2 +1,4 @@ from .raw import RawMxCell, RawGeometry -from .drawio_parser import load_diagram +from .drawio_parser import load_diagram, ParsedDiagram + +__all__ = [ParsedDiagram, RawGeometry, RawMxCell] diff --git a/src/drawpyo/utils/__init__.py b/src/drawpyo/utils/__init__.py index 2a9abf1..75f1b9e 100644 --- a/src/drawpyo/utils/__init__.py +++ b/src/drawpyo/utils/__init__.py @@ -2,5 +2,6 @@ from .standard_colors import StandardColor from .color_scheme import ColorScheme from .page_sizes import PageSize +from .redact import redact_values -__all__ = [logger, StandardColor, ColorScheme, PageSize] +__all__ = [logger, StandardColor, ColorScheme, PageSize, redact_values] diff --git a/src/drawpyo/utils/redact.py b/src/drawpyo/utils/redact.py new file mode 100644 index 0000000..4b6b4c8 --- /dev/null +++ b/src/drawpyo/utils/redact.py @@ -0,0 +1,36 @@ +from drawpyo.diagram import Object, Edge +import json + + +def redact_text(text: str | None) -> str | None: + if text is None: + return None + return "".join(ch if ch.isspace() else "X" for ch in text) + + +def redact_values(diagram, map_file_path): + id_value_map: dict[str, str] = {} + + for node in diagram.shapes + diagram.edges: + if isinstance(node, Object): + if node.value is None: + continue + id_value_map[node.id] = node.value + node.value = redact_text(node.value) + + elif isinstance(node, Edge): + if node.label is None: + continue + id_value_map[node.id] = node.label + node.label = redact_text(node.label) + + with open(map_file_path, "w", encoding="utf-8") as f: + json.dump( + id_value_map, + f, + indent=2, + ensure_ascii=False, + sort_keys=True, + ) + + return diagram From 239caa1b1212f0c10c68a8f73c3897358f71d2a4 Mon Sep 17 00:00:00 2001 From: Pascal Rothe Date: Fri, 9 Jan 2026 13:18:52 +0100 Subject: [PATCH 3/4] restore function --- etc/development scripts/load_and_redact.py | 49 +++++++++++++++++----- src/drawpyo/utils/redact.py | 39 +++++++++++++++++ 2 files changed, 78 insertions(+), 10 deletions(-) diff --git a/etc/development scripts/load_and_redact.py b/etc/development scripts/load_and_redact.py index 4b2f786..aeab3ee 100644 --- a/etc/development scripts/load_and_redact.py +++ b/etc/development scripts/load_and_redact.py @@ -17,7 +17,8 @@ ).resolve() redaction_map_path = base_dir / "redaction_map.json" -drawio_output_path = base_dir / "Redacted Drawio File.drawio" +drawio_redacted_output_path = base_dir / "Redacted Drawio File.drawio" +drawio_restored_output_path = base_dir / "Restored Drawio File.drawio" # -------------------------------------------------- # Load diagram @@ -35,22 +36,50 @@ ) # -------------------------------------------------- -# Create output file +# Write redacted draw.io file # -------------------------------------------------- -file = drawpyo.File() -file.file_path = str(base_dir) -file.file_name = drawio_output_path.name +redacted_file = drawpyo.File() +redacted_file.file_path = str(base_dir) +redacted_file.file_name = drawio_redacted_output_path.name -page = drawpyo.Page(file=file) +redacted_page = drawpyo.Page(file=redacted_file) # Add shapes for shape in redacted_diagram.shapes: - shape.page = page + shape.page = redacted_page # Add edges for edge in redacted_diagram.edges: - edge.page = page + edge.page = redacted_page -# Write draw.io file -file.write() +redacted_file.write() + +# -------------------------------------------------- +# Restore diagram from map +# -------------------------------------------------- + +restored_diagram = drawpyo.utils.restore_values( + redacted_diagram, + map_file_path=redaction_map_path, +) + +# -------------------------------------------------- +# Write restored (unredacted) draw.io file +# -------------------------------------------------- + +restored_file = drawpyo.File() +restored_file.file_path = str(base_dir) +restored_file.file_name = drawio_restored_output_path.name + +restored_page = drawpyo.Page(file=restored_file) + +# Add shapes +for shape in restored_diagram.shapes: + shape.page = restored_page + +# Add edges +for edge in restored_diagram.edges: + edge.page = restored_page + +restored_file.write() diff --git a/src/drawpyo/utils/redact.py b/src/drawpyo/utils/redact.py index 4b6b4c8..ae2397a 100644 --- a/src/drawpyo/utils/redact.py +++ b/src/drawpyo/utils/redact.py @@ -1,4 +1,6 @@ from drawpyo.diagram import Object, Edge +from drawpyo.utils import logger +from pathlib import Path import json @@ -33,4 +35,41 @@ def redact_values(diagram, map_file_path): sort_keys=True, ) + logger.info(f"👤 Reaction map saved to: '{map_file_path}'") + return diagram + + +def restore_values(diagram, map_file_path: Path): + if not map_file_path.exists(): + raise FileNotFoundError(f"Restore map not found: {map_file_path}") + + with map_file_path.open("r", encoding="utf-8") as f: + id_value_map: dict[str, str] = json.load(f) + + restored = 0 + missing = 0 + + for cell_id, original_value in id_value_map.items(): + matched_element = None + + for element in diagram.shapes + diagram.edges: + if str(element.id) == str(cell_id): + matched_element = element + break + + if matched_element is None: + missing += 1 + logger.warning(f" No matching object/edge found: '{cell_id}'") + continue + + if isinstance(matched_element, Object): + matched_element.value = original_value + restored += 1 + + elif isinstance(matched_element, Edge): + matched_element.label = original_value + restored += 1 + + logger.info(f"🔄 Restore complete: restored={restored}, missing={missing}") + return diagram From 5ef6268a0188a66a7aa5ba88c131f95d77d51b6a Mon Sep 17 00:00:00 2001 From: Pascal Rothe Date: Fri, 9 Jan 2026 14:27:16 +0100 Subject: [PATCH 4/4] Added future annotations everywhere, fixed circular import, docstrings, type hints --- src/drawpyo/diagram/edges.py | 2 + src/drawpyo/diagram/extended_objects.py | 2 + src/drawpyo/diagram/objects.py | 2 + src/drawpyo/diagram/text_format.py | 2 + src/drawpyo/diagram_types/bar_chart.py | 2 + src/drawpyo/diagram_types/class_diagram.py | 2 + src/drawpyo/diagram_types/legend.py | 2 + src/drawpyo/diagram_types/pie_chart.py | 2 + src/drawpyo/drawio_import/drawio_parser.py | 4 +- src/drawpyo/drawio_import/raw.py | 2 + src/drawpyo/file.py | 2 + src/drawpyo/page.py | 2 + src/drawpyo/utils/__init__.py | 4 +- src/drawpyo/utils/color_scheme.py | 1 + src/drawpyo/utils/logger.py | 2 + src/drawpyo/utils/page_sizes.py | 2 + src/drawpyo/utils/redact.py | 49 +++++++++++++++++++--- src/drawpyo/utils/standard_colors.py | 2 + src/drawpyo/xml_base.py | 2 + 19 files changed, 79 insertions(+), 9 deletions(-) diff --git a/src/drawpyo/diagram/edges.py b/src/drawpyo/diagram/edges.py index ca5d8a1..7800480 100644 --- a/src/drawpyo/diagram/edges.py +++ b/src/drawpyo/diagram/edges.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from os import path from typing import Optional, Dict, Any, List, Union, Tuple from ..utils.logger import logger diff --git a/src/drawpyo/diagram/extended_objects.py b/src/drawpyo/diagram/extended_objects.py index ff5bad8..b2f1a31 100644 --- a/src/drawpyo/diagram/extended_objects.py +++ b/src/drawpyo/diagram/extended_objects.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import List as ListType, Optional, Any, Union from .objects import Object, object_from_library diff --git a/src/drawpyo/diagram/objects.py b/src/drawpyo/diagram/objects.py index 8ed2e55..a8dc8e6 100644 --- a/src/drawpyo/diagram/objects.py +++ b/src/drawpyo/diagram/objects.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from os import path from typing import Optional, Dict, Any, List, Union, Tuple from ..utils.logger import logger diff --git a/src/drawpyo/diagram/text_format.py b/src/drawpyo/diagram/text_format.py index 9d89c47..4fe170f 100644 --- a/src/drawpyo/diagram/text_format.py +++ b/src/drawpyo/diagram/text_format.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Optional, Dict, Any, Union from ..utils.logger import logger from .base_diagram import DiagramBase diff --git a/src/drawpyo/diagram_types/bar_chart.py b/src/drawpyo/diagram_types/bar_chart.py index 878b194..7143bd3 100644 --- a/src/drawpyo/diagram_types/bar_chart.py +++ b/src/drawpyo/diagram_types/bar_chart.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Callable, Union, Optional from copy import deepcopy from ..diagram.objects import Object, Group diff --git a/src/drawpyo/diagram_types/class_diagram.py b/src/drawpyo/diagram_types/class_diagram.py index de6d72d..9dfefec 100644 --- a/src/drawpyo/diagram_types/class_diagram.py +++ b/src/drawpyo/diagram_types/class_diagram.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Optional, Any from .tree import NodeObject, TreeGroup, TreeDiagram diff --git a/src/drawpyo/diagram_types/legend.py b/src/drawpyo/diagram_types/legend.py index aa4f877..98a0ac9 100644 --- a/src/drawpyo/diagram_types/legend.py +++ b/src/drawpyo/diagram_types/legend.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Union, Optional from copy import deepcopy from ..diagram.objects import Object, Group diff --git a/src/drawpyo/diagram_types/pie_chart.py b/src/drawpyo/diagram_types/pie_chart.py index acdb3e8..ca4b3c1 100644 --- a/src/drawpyo/diagram_types/pie_chart.py +++ b/src/drawpyo/diagram_types/pie_chart.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Callable, Union, Optional from copy import deepcopy from ..diagram.objects import Object, Group diff --git a/src/drawpyo/drawio_import/drawio_parser.py b/src/drawpyo/drawio_import/drawio_parser.py index 3a1b625..e4aaf79 100644 --- a/src/drawpyo/drawio_import/drawio_parser.py +++ b/src/drawpyo/drawio_import/drawio_parser.py @@ -1,10 +1,12 @@ +from __future__ import annotations + import xml.etree.ElementTree as ET from pathlib import Path from typing import Dict, List, Optional from dataclasses import dataclass, field from .raw import RawMxCell, RawGeometry -from drawpyo import logger +from drawpyo.utils import logger from drawpyo.diagram import Object, Edge, DiagramBase diff --git a/src/drawpyo/drawio_import/raw.py b/src/drawpyo/drawio_import/raw.py index 0cbc843..b0deb4f 100644 --- a/src/drawpyo/drawio_import/raw.py +++ b/src/drawpyo/drawio_import/raw.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dataclasses import dataclass, field from typing import Optional, List, Tuple diff --git a/src/drawpyo/file.py b/src/drawpyo/file.py index 00a5fc3..8df5ce4 100644 --- a/src/drawpyo/file.py +++ b/src/drawpyo/file.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import List, Optional, Any, Union, Dict from .xml_base import XMLBase from datetime import datetime diff --git a/src/drawpyo/page.py b/src/drawpyo/page.py index 655d752..c566621 100644 --- a/src/drawpyo/page.py +++ b/src/drawpyo/page.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import List, Optional, Any, Union, Dict from .xml_base import XMLBase from .utils.logger import logger diff --git a/src/drawpyo/utils/__init__.py b/src/drawpyo/utils/__init__.py index 75f1b9e..d7d408b 100644 --- a/src/drawpyo/utils/__init__.py +++ b/src/drawpyo/utils/__init__.py @@ -2,6 +2,6 @@ from .standard_colors import StandardColor from .color_scheme import ColorScheme from .page_sizes import PageSize -from .redact import redact_values +from .redact import redact_values, restore_values -__all__ = [logger, StandardColor, ColorScheme, PageSize, redact_values] +__all__ = [logger, StandardColor, ColorScheme, PageSize, redact_values, restore_values] diff --git a/src/drawpyo/utils/color_scheme.py b/src/drawpyo/utils/color_scheme.py index eecca0e..3eab0a3 100644 --- a/src/drawpyo/utils/color_scheme.py +++ b/src/drawpyo/utils/color_scheme.py @@ -1,4 +1,5 @@ from __future__ import annotations + from typing import Union import re from .logger import logger diff --git a/src/drawpyo/utils/logger.py b/src/drawpyo/utils/logger.py index 4f4a839..abd91c1 100644 --- a/src/drawpyo/utils/logger.py +++ b/src/drawpyo/utils/logger.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging """Set the logging level. diff --git a/src/drawpyo/utils/page_sizes.py b/src/drawpyo/utils/page_sizes.py index 86124ce..33f937c 100644 --- a/src/drawpyo/utils/page_sizes.py +++ b/src/drawpyo/utils/page_sizes.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from enum import Enum diff --git a/src/drawpyo/utils/redact.py b/src/drawpyo/utils/redact.py index ae2397a..6dbd0f6 100644 --- a/src/drawpyo/utils/redact.py +++ b/src/drawpyo/utils/redact.py @@ -1,16 +1,43 @@ +from __future__ import annotations + from drawpyo.diagram import Object, Edge +from drawpyo.drawio_import import ParsedDiagram from drawpyo.utils import logger from pathlib import Path import json -def redact_text(text: str | None) -> str | None: +def _redact_text(text: str | None) -> str: + """ + Redact a text string by replacing all non-whitespace characters with 'X'. + + Args: + text: The input text to redact, or None. + + Returns: + The redacted text with non-whitespace characters replaced by 'X', + or None if the input was None. + """ if text is None: - return None + return "" return "".join(ch if ch.isspace() else "X" for ch in text) -def redact_values(diagram, map_file_path): +def redact_values(diagram: ParsedDiagram, map_file_path: Path) -> ParsedDiagram: + """ + Redact all textual values in a diagram. + + Original values are stored in a JSON file, keyed by element ID, so they + can later be restored. + + Args: + diagram: A diagram instance containing shapes and edges. + map_file_path: File path where the ID-to-original-value mapping + will be written as JSON. + + Returns: + The modified diagram with all applicable text fields redacted. + """ id_value_map: dict[str, str] = {} for node in diagram.shapes + diagram.edges: @@ -18,13 +45,13 @@ def redact_values(diagram, map_file_path): if node.value is None: continue id_value_map[node.id] = node.value - node.value = redact_text(node.value) + node.value = _redact_text(node.value) elif isinstance(node, Edge): if node.label is None: continue id_value_map[node.id] = node.label - node.label = redact_text(node.label) + node.label = _redact_text(node.label) with open(map_file_path, "w", encoding="utf-8") as f: json.dump( @@ -39,7 +66,17 @@ def redact_values(diagram, map_file_path): return diagram -def restore_values(diagram, map_file_path: Path): +def restore_values(diagram: ParsedDiagram, map_file_path: Path) -> ParsedDiagram: + """ + Restore redacted diagram values from a JSON mapping file. + + Args: + diagram: A diagram instance containing shapes and edges. + map_file_path: Path to the JSON file created by `redact_values`. + + Returns: + The modified diagram with restored text values. + """ if not map_file_path.exists(): raise FileNotFoundError(f"Restore map not found: {map_file_path}") diff --git a/src/drawpyo/utils/standard_colors.py b/src/drawpyo/utils/standard_colors.py index ac27867..8336ed2 100644 --- a/src/drawpyo/utils/standard_colors.py +++ b/src/drawpyo/utils/standard_colors.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from enum import Enum diff --git a/src/drawpyo/xml_base.py b/src/drawpyo/xml_base.py index aa042c8..d3f270d 100644 --- a/src/drawpyo/xml_base.py +++ b/src/drawpyo/xml_base.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Dict, Optional, Any, Union xmlize: Dict[str, str] = {}