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: diff --git a/etc/development scripts/load_and_redact.py b/etc/development scripts/load_and_redact.py new file mode 100644 index 0000000..aeab3ee --- /dev/null +++ b/etc/development scripts/load_and_redact.py @@ -0,0 +1,85 @@ +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_redacted_output_path = base_dir / "Redacted Drawio File.drawio" +drawio_restored_output_path = base_dir / "Restored 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, +) + +# -------------------------------------------------- +# Write redacted draw.io file +# -------------------------------------------------- + +redacted_file = drawpyo.File() +redacted_file.file_path = str(base_dir) +redacted_file.file_name = drawio_redacted_output_path.name + +redacted_page = drawpyo.Page(file=redacted_file) + +# Add shapes +for shape in redacted_diagram.shapes: + shape.page = redacted_page + +# Add edges +for edge in redacted_diagram.edges: + edge.page = redacted_page + +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/__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/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/__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/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 2a9abf1..d7d408b 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, restore_values -__all__ = [logger, StandardColor, ColorScheme, PageSize] +__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 new file mode 100644 index 0000000..6dbd0f6 --- /dev/null +++ b/src/drawpyo/utils/redact.py @@ -0,0 +1,112 @@ +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: + """ + 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 "" + return "".join(ch if ch.isspace() else "X" for ch in text) + + +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: + 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, + ) + + logger.info(f"👤 Reaction map saved to: '{map_file_path}'") + return diagram + + +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}") + + 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 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] = {}