Skip to content

Instantly share code, notes, and snippets.

@iand
Created July 10, 2026 07:53
Show Gist options
  • Select an option

  • Save iand/5039275fec0f8226d82b2780fc0a83ca to your computer and use it in GitHub Desktop.

Select an option

Save iand/5039275fec0f8226d82b2780fc0a83ca to your computer and use it in GitHub Desktop.
convert_dna_schema.py
#!/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