Created
July 10, 2026 07:53
-
-
Save iand/5039275fec0f8226d82b2780fc0a83ca to your computer and use it in GitHub Desktop.
convert_dna_schema.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| # Copyright (C) 2026 Ian Davis | |
| # | |
| # Convert a Gramps XML file written before the DNA data-model revision to the | |
| # form the current importer expects. Both files carry schema version 1.8.0; | |
| # only the DNA elements changed structure. | |
| # | |
| # Structural changes handled: | |
| # 1. dna_segment: attribute "phase" renamed to "origin" (values 0-3 | |
| # unchanged). | |
| # 2. dnamatch: <predicted_relationship>TEXT</predicted_relationship> becomes | |
| # <predicted_relationship><description>TEXT</description></predicted_relationship>. | |
| # 3. dnamatch: <predicted_generations val="N"/> is removed; its value is | |
| # folded into the description text so the information is not lost. | |
| # | |
| # Additions in the new schema (weighted-cM fields, genome_build, the new | |
| # predicted_relationship attributes) are optional and absent from old files, so | |
| # they need no action. The removal of the GEDmatch provider is not rewritten: | |
| # <provider>GEDmatch</provider> loads as a custom provider label without loss. | |
| # | |
| # The transformation is text based so the DOCTYPE, namespace declaration and | |
| # every non-DNA element are preserved byte for byte. | |
| import argparse | |
| import re | |
| import sys | |
| # A self-closing <dna_segment .../> element. Attribute values are quoted and | |
| # contain no '>', so [^>]* is safe. | |
| DNA_SEGMENT_RE = re.compile(r"<dna_segment\b[^>]*/>") | |
| # An old text-form <predicted_relationship>, optionally followed on the next | |
| # line by its <predicted_generations>. The generations group swallows the | |
| # newline that separated the two lines so nothing is left dangling. | |
| PRED_REL_RE = re.compile( | |
| r"(?P<indent>[ \t]*)<predicted_relationship>(?P<text>.*?)</predicted_relationship>" | |
| r'(?P<gen>[ \t]*\n[ \t]*<predicted_generations val="(?P<genval>[^"]*)"\s*/>)?', | |
| re.DOTALL, | |
| ) | |
| # A <predicted_generations> with no preceding relationship on the line above. | |
| LONE_GEN_RE = re.compile( | |
| r'(?P<indent>[ \t]*)<predicted_generations val="(?P<genval>[^"]*)"\s*/>' | |
| ) | |
| def _rename_phase(match): | |
| """Rename the phase attribute to origin within a dna_segment element.""" | |
| return re.sub(r"\bphase=", "origin=", match.group(0)) | |
| def _generations_suffix(genval): | |
| """Return a description suffix for a generations value, or '' if it is empty/zero.""" | |
| if genval is None: | |
| return "" | |
| try: | |
| if float(genval) == 0: | |
| return "" | |
| except ValueError: | |
| pass | |
| return " [predicted generations: %s]" % genval | |
| def _build_predicted_relationship(indent, description): | |
| """Build the new element form for the given description text.""" | |
| return ( | |
| "%(i)s<predicted_relationship>\n" | |
| "%(i)s <description>%(d)s</description>\n" | |
| "%(i)s</predicted_relationship>" | |
| ) % {"i": indent, "d": description} | |
| def _convert_predicted_relationship(match): | |
| indent = match.group("indent") | |
| description = (match.group("text") or "").strip() | |
| description += _generations_suffix(match.group("genval")) | |
| return _build_predicted_relationship(indent, description.strip()) | |
| def _convert_lone_generations(match): | |
| indent = match.group("indent") | |
| description = _generations_suffix(match.group("genval")).strip() | |
| return _build_predicted_relationship(indent, description) | |
| def convert(text): | |
| """Return the converted XML text.""" | |
| text = DNA_SEGMENT_RE.sub(_rename_phase, text) | |
| text = PRED_REL_RE.sub(_convert_predicted_relationship, text) | |
| text = LONE_GEN_RE.sub(_convert_lone_generations, text) | |
| return text | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Convert a pre-revision Gramps DNA XML file to the current schema." | |
| ) | |
| parser.add_argument("input", help="path to the older .gramps/.xml file") | |
| parser.add_argument( | |
| "-o", | |
| "--output", | |
| help="write to this file instead of stdout", | |
| ) | |
| args = parser.parse_args() | |
| with open(args.input, "r", encoding="utf-8") as handle: | |
| text = handle.read() | |
| result = convert(text) | |
| if args.output: | |
| with open(args.output, "w", encoding="utf-8") as handle: | |
| handle.write(result) | |
| else: | |
| sys.stdout.write(result) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment