|
| 1 | +# Metadata Change Detection for Document Versions |
| 2 | + |
| 3 | +<div align="center"> |
| 4 | + |
| 5 | +[](https://docs.groupdocs.com/metadata/python-net/) |
| 6 | +[](https://blog.groupdocs.com/categories/groupdocs.metadata-product-family/) |
| 7 | +[](https://forum.groupdocs.com/c/metadata/) |
| 8 | + |
| 9 | +</div> |
| 10 | + |
| 11 | +## 🚀 Quick Start |
| 12 | + |
| 13 | +document-version-metadata-diff-python is a runnable Python demo that answers one question fast: what changed in a document's metadata between two revisions? Install `groupdocs-metadata-net==26.5`, run `python main.py`, and the seeded pair of DOCX revisions produces a classified property diff, two forensic reports, and JSON plus CSV exports, each step asserted with a PASS line. |
| 14 | + |
| 15 | +## ✨ What You'll Learn |
| 16 | + |
| 17 | +- Flatten a file's complete property tree into one Python dict with a single call |
| 18 | +- Classify metadata differences into added, removed, and changed with both values kept |
| 19 | +- Isolate ownership signals (Creator, Editor, Manager, Company) using tag predicates |
| 20 | +- Isolate the editing timeline: revision counters, editing time, created/modified/printed stamps |
| 21 | +- Ship findings as a stable JSON schema and a four-column CSV |
| 22 | + |
| 23 | +## 📋 Table of Contents |
| 24 | + |
| 25 | +- [About This Repository](#-about-this-repository) |
| 26 | +- [Key Features](#-key-features) |
| 27 | +- [Prerequisites](#-prerequisites) |
| 28 | +- [Repository Structure](#-repository-structure) |
| 29 | +- [Implementation Examples](#-implementation-examples) |
| 30 | +- [Related Resources](#-related-resources) |
| 31 | +- [Keywords](#-keywords) |
| 32 | + |
| 33 | +## 📖 About This Repository |
| 34 | + |
| 35 | +This repository shows metadata version comparison end to end using GroupDocs.Metadata for Python via .NET. The library reads built-in fields, custom properties, and XMP through one `find_properties` call across 170+ formats, per the [product documentation](https://docs.groupdocs.com/metadata/python-net/), and its tag system classifies identity and time properties without format-specific names. The examples target engineers who need change detection inside document workflows, from automated intake to dispute support. Everything downstream of the API stays in plain dicts, so the audit logic reads like ordinary Python. I keep this project around as my smoke test after library upgrades; if the six asserts pass, the metadata layer behaves. |
| 36 | + |
| 37 | +## 🔑 Key Features |
| 38 | + |
| 39 | +### GroupDocs.Metadata Capabilities |
| 40 | + |
| 41 | +| Feature | Description | |
| 42 | +|---------|-------------| |
| 43 | +| **Whole-tree search** | `find_properties` walks every metadata package behind one predicate | |
| 44 | +| **Tag classification** | `Tags.person`, `Tags.corporate`, and `Tags.time` mark properties by meaning | |
| 45 | +| **Interpreted values** | dates and enumerations arrive human-readable, not as raw serials | |
| 46 | +| **Format breadth** | the same calls serve DOCX, PDF, XLSX, images, and audio | |
| 47 | +| **Write support** | properties can be updated or removed with the same API family | |
| 48 | + |
| 49 | +### What This Repository Demonstrates |
| 50 | + |
| 51 | +A complete two-revision diff pipeline in six small functions. Tag-driven detectors handle the ownership and revision questions, audit-grade exports feed dashboards (JSON) and spreadsheets or SIEMs (CSV), and a self-asserting `main.py` verifies every step on the seeded sample pair. |
| 52 | + |
| 53 | +## ⚙️ Prerequisites |
| 54 | + |
| 55 | +You need Python 3 (any actively supported CPython with pip) and the package: `pip install groupdocs-metadata-net==26.5`. A license is optional. Evaluation mode runs everything here, and setting `LICENSE_PATH` in `main.py` lifts the evaluation limits. |
| 56 | + |
| 57 | +## 📁 Repository Structure |
| 58 | + |
| 59 | +``` |
| 60 | +document-version-metadata-diff-python/ |
| 61 | +│ |
| 62 | +├── main.py |
| 63 | +├── requirements.txt |
| 64 | +├── methods/ |
| 65 | +│ ├── __init__.py |
| 66 | +│ ├── compare_metadata_sets.py |
| 67 | +│ ├── detect_ownership_changes.py |
| 68 | +│ ├── detect_revision_history.py |
| 69 | +│ ├── export_diff_to_csv.py |
| 70 | +│ ├── export_diff_to_json.py |
| 71 | +│ └── extract_all_metadata.py |
| 72 | +└── resources/ |
| 73 | + ├── document-v1.docx |
| 74 | + └── document-v2.docx |
| 75 | +``` |
| 76 | + |
| 77 | +### File Overview |
| 78 | + |
| 79 | +- **main.py** – drives all six functions against the sample pair and asserts each result |
| 80 | +- **requirements.txt** – pins `groupdocs-metadata-net==26.5` |
| 81 | +- **methods/extract_all_metadata.py** – property tree to dict |
| 82 | +- **methods/compare_metadata_sets.py** – the `MetadataDiff` builder |
| 83 | +- **methods/detect_ownership_changes.py** – identity-change detector |
| 84 | +- **methods/detect_revision_history.py** – editing-timeline detector |
| 85 | +- **methods/export_diff_to_json.py** / **export_diff_to_csv.py** – report writers |
| 86 | +- **resources/** – seeded `document-v1.docx` and `document-v2.docx` |
| 87 | + |
| 88 | +## 💻 Implementation Examples |
| 89 | + |
| 90 | +### Example 1: Extracts every accessible metadata property |
| 91 | + |
| 92 | +The foundation call. One pass collects built-in, custom, and XMP properties keyed by qualified name, preferring `interpreted_value` so the dict is readable as-is. |
| 93 | + |
| 94 | +```python |
| 95 | +result = {} |
| 96 | +with Metadata(document_path) as metadata: |
| 97 | + for prop in metadata.find_properties(lambda p: p.name is not None): |
| 98 | + key = prop.name |
| 99 | + value = (str(prop.interpreted_value) if prop.interpreted_value is not None |
| 100 | + else (str(prop.value) if prop.value is not None else "")) |
| 101 | + result[key] = value |
| 102 | +return result |
| 103 | +``` |
| 104 | + |
| 105 | +What this example shows: |
| 106 | + |
| 107 | +`find_properties` with a name predicate is the whole extraction story; there is no per-layer code. The returned dict is what every later function consumes, and its size on the sample files is asserted to be non-zero by `main.py`. |
| 108 | + |
| 109 | +--- |
| 110 | + |
| 111 | +### Example 2: Compares metadata between two document versions |
| 112 | + |
| 113 | +The core diff. Both revisions go through the extractor, then set logic fills a `MetadataDiff` value object with three maps. |
| 114 | + |
| 115 | +```python |
| 116 | +v1 = extract_all_metadata(path_v1) |
| 117 | +v2 = extract_all_metadata(path_v2) |
| 118 | +diff = MetadataDiff() |
| 119 | + |
| 120 | +for k, v in v2.items(): |
| 121 | + if k not in v1: |
| 122 | + diff.added[k] = v |
| 123 | + elif v1[k] != v: |
| 124 | + diff.changed[k] = (v1[k], v) |
| 125 | + |
| 126 | +for k, v in v1.items(): |
| 127 | + if k not in v2: |
| 128 | + diff.removed[k] = v |
| 129 | + |
| 130 | +return diff |
| 131 | +``` |
| 132 | + |
| 133 | +What this example shows: |
| 134 | + |
| 135 | +Changed entries keep old and new values as a pair, so the result is a finding rather than a hint. `total_changes` on the object sums all three maps for quick thresholds. |
| 136 | + |
| 137 | +--- |
| 138 | + |
| 139 | +### Example 3: Detects ownership and authorship changes |
| 140 | + |
| 141 | +The identity question, answered directly. The module-level `_read_ownership` helper collects only properties tagged as person or company, and the delta loop reports differences with `<missing>` marking one-sided fields. |
| 142 | + |
| 143 | +```python |
| 144 | +v1 = _read_ownership(path_v1) |
| 145 | +v2 = _read_ownership(path_v2) |
| 146 | +all_keys = set(v1.keys()) | set(v2.keys()) |
| 147 | +changes = {} |
| 148 | +for k in all_keys: |
| 149 | + old_v = v1.get(k, "<missing>") |
| 150 | + new_v = v2.get(k, "<missing>") |
| 151 | + if old_v != new_v: |
| 152 | + changes[k] = (old_v, new_v) |
| 153 | +return changes |
| 154 | +``` |
| 155 | + |
| 156 | +What this example shows: |
| 157 | + |
| 158 | +Tag predicates make the detector format-independent: no property is named, yet Creator, LastSavedBy, Manager, and Company are all covered. A disappeared identity field surfaces just as loudly as a changed one. |
| 159 | + |
| 160 | +--- |
| 161 | + |
| 162 | +### Example 4: Detects revision-history and editing-time changes |
| 163 | + |
| 164 | +The timing question. `_read_revision` mixes `Tags.time` predicates with name rules for `Revision` and `EditTime` counters, catching classified timestamps and counter fields together. |
| 165 | + |
| 166 | +```python |
| 167 | +v1 = _read_revision(path_v1) |
| 168 | +v2 = _read_revision(path_v2) |
| 169 | +all_keys = set(v1.keys()) | set(v2.keys()) |
| 170 | +changes = {} |
| 171 | +for k in all_keys: |
| 172 | + old_v = v1.get(k, "<missing>") |
| 173 | + new_v = v2.get(k, "<missing>") |
| 174 | + if old_v != new_v: |
| 175 | + changes[k] = (old_v, new_v) |
| 176 | +return changes |
| 177 | +``` |
| 178 | + |
| 179 | +What this example shows: |
| 180 | + |
| 181 | +RevisionNumber, TotalEditingTime, and LastPrinted move with every editing session even when the visible text is untouched. This detector surfaces that invisible activity as from/to pairs. |
| 182 | + |
| 183 | +--- |
| 184 | + |
| 185 | +### Example 5: Exports the diff as a JSON audit report |
| 186 | + |
| 187 | +Machine-facing output. The three maps serialize into a stable schema, with changed entries expanded into from/to objects. |
| 188 | + |
| 189 | +```python |
| 190 | +payload = { |
| 191 | + "added": diff.added, |
| 192 | + "removed": diff.removed, |
| 193 | + "changed": {k: {"from": v[0], "to": v[1]} for k, v in diff.changed.items()}, |
| 194 | +} |
| 195 | +with open(output_path, "w", encoding="utf-8") as f: |
| 196 | + json.dump(payload, f, indent=2, ensure_ascii=False) |
| 197 | +``` |
| 198 | + |
| 199 | +What this example shows: |
| 200 | + |
| 201 | +A dashboard or case-management API ingests `diff.json` without transformation, and because the schema never varies, reports from different runs line up into a timeline. |
| 202 | + |
| 203 | +--- |
| 204 | + |
| 205 | +### Example 6: Exports the diff as a CSV audit report |
| 206 | + |
| 207 | +People-facing output. Four columns, one row per change, ready for Excel or a SIEM. |
| 208 | + |
| 209 | +```python |
| 210 | +with open(output_path, "w", encoding="utf-8", newline="") as f: |
| 211 | + writer = csv.writer(f) |
| 212 | + writer.writerow(["change_type", "property", "old_value", "new_value"]) |
| 213 | + for k, v in diff.added.items(): |
| 214 | + writer.writerow(["added", k, "", v]) |
| 215 | + for k, v in diff.removed.items(): |
| 216 | + writer.writerow(["removed", k, v, ""]) |
| 217 | + for k, (old_v, new_v) in diff.changed.items(): |
| 218 | + writer.writerow(["changed", k, old_v, new_v]) |
| 219 | +``` |
| 220 | + |
| 221 | +What this example shows: |
| 222 | + |
| 223 | +The flattening rule is fixed: added rows leave old_value empty, removed rows leave new_value empty, changed rows carry both. No parsing code needed on the receiving end. |
| 224 | + |
| 225 | +### Which properties count as ownership signals? |
| 226 | + |
| 227 | +Four tag groups: Tags.person.creator covers Author and LastSavedBy, Tags.person.editor covers the last editor, Tags.person.manager maps to Manager, and Tags.corporate.company maps to Company. The detector reports any of them whose value differs between versions, including fields present on only one side. Everything else, timestamps included, belongs to the revision detector instead. That split keeps each report readable. |
| 228 | + |
| 229 | +## 📚 Related Resources |
| 230 | + |
| 231 | +Explore these additional resources to deepen your understanding of metadata version comparison: |
| 232 | + |
| 233 | +* **Step-by-step use case guide in the documentation** – the same pipeline as a tutorial series: [Read the guide →](https://docs.groupdocs.com/metadata/python-net/use-cases/compare-metadata-between-document-versions/) |
| 234 | + |
| 235 | +* **In-depth blog article about this project** – the quick-start walkthrough built on this repo: [Read the article →](https://blog.groupdocs.com/metadata/compare-metadata-between-document-versions-python-net/) |
| 236 | + |
| 237 | +* **How to Compare Document Metadata Between Versions in Java** – the same audit approach on the Java platform: [Read the article →](https://blog.groupdocs.com/metadata/compare-metadata-versions-java/) |
| 238 | + |
| 239 | +* **Edit Metadata in Python Applications** – the wider read/update/remove API surface: [Read the article →](https://blog.groupdocs.com/metadata/edit-metadata-in-python/) |
| 240 | + |
| 241 | +* **Best Practices in Metadata Management** – the policy context around property hygiene: [Read the article →](https://blog.groupdocs.com/metadata/best-practices-in-metadata-management/) |
| 242 | + |
| 243 | +## 🏷️ Keywords |
| 244 | + |
| 245 | +`document metadata diff`, `compare metadata versions`, `python metadata comparison`, `detect authorship changes`, `revision number tracking`, `TotalEditingTime`, `LastPrinted`, `document forensics python`, `e-discovery metadata`, `metadata audit report`, `csv audit export`, `json diff schema`, `groupdocs metadata python`, `python via .net`, `find_properties`, `metadata tags`, `docx properties`, `document version control`, `compliance snapshot`, `metadata tampering detection` |
| 246 | + |
| 247 | +--- |
| 248 | + |
| 249 | +<div align="center"> |
| 250 | + |
| 251 | +**Need help?** [Get Free Support](https://forum.groupdocs.com/c/metadata/) | [Read the Docs](https://docs.groupdocs.com/metadata/python-net/) |
| 252 | + |
| 253 | +</div> |
0 commit comments