gedcom-x 0.5.2__py3-none-any.whl → 0.5.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gedcom_x-0.5.6.dist-info/METADATA +144 -0
- gedcom_x-0.5.6.dist-info/RECORD +45 -0
- gedcomx/Address.py +2 -0
- gedcomx/Agent.py +9 -2
- gedcomx/Attribution.py +10 -46
- gedcomx/Conclusion.py +85 -21
- gedcomx/Coverage.py +10 -0
- gedcomx/Date.py +2 -7
- gedcomx/Document.py +27 -6
- gedcomx/Event.py +20 -1
- gedcomx/Exceptions.py +6 -0
- gedcomx/ExtensibleEnum.py +183 -0
- gedcomx/Fact.py +7 -8
- gedcomx/Gedcom.py +38 -404
- gedcomx/Gedcom5x.py +579 -0
- gedcomx/GedcomX.py +48 -26
- gedcomx/Gender.py +6 -40
- gedcomx/Identifier.py +151 -97
- gedcomx/LoggingHub.py +186 -0
- gedcomx/Mutations.py +228 -0
- gedcomx/Name.py +6 -0
- gedcomx/Person.py +49 -90
- gedcomx/PlaceDescription.py +23 -14
- gedcomx/PlaceReference.py +12 -15
- gedcomx/Relationship.py +23 -54
- gedcomx/Resource.py +17 -3
- gedcomx/Serialization.py +352 -31
- gedcomx/SourceDescription.py +6 -9
- gedcomx/SourceReference.py +20 -86
- gedcomx/Subject.py +4 -4
- gedcomx/Translation.py +219 -0
- gedcomx/URI.py +1 -0
- gedcomx/__init__.py +8 -1
- gedcom_x-0.5.2.dist-info/METADATA +0 -17
- gedcom_x-0.5.2.dist-info/RECORD +0 -42
- gedcomx/_Links.py +0 -37
- gedcomx/g7interop.py +0 -205
- {gedcom_x-0.5.2.dist-info → gedcom_x-0.5.6.dist-info}/WHEEL +0 -0
- {gedcom_x-0.5.2.dist-info → gedcom_x-0.5.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,183 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from typing import Any, Dict, Iterator, Literal
|
4
|
+
|
5
|
+
"""
|
6
|
+
======================================================================
|
7
|
+
Project: Gedcom-X
|
8
|
+
File: ExtensibleEnum.py
|
9
|
+
Author: David J. Cartwright
|
10
|
+
Purpose: Create a class that can act like an enum but be extended by the user at runtime.
|
11
|
+
|
12
|
+
Created: 2025-08-25
|
13
|
+
Updated:
|
14
|
+
- YYYY-MM-DD: <change description>
|
15
|
+
|
16
|
+
======================================================================
|
17
|
+
"""
|
18
|
+
|
19
|
+
@dataclass(frozen=True, slots=True)
|
20
|
+
class _EnumItem:
|
21
|
+
"""
|
22
|
+
A single registered member of an :class:`ExtensibleEnum`.
|
23
|
+
|
24
|
+
Each `_EnumItem` represents one (name, value) pair that belongs
|
25
|
+
to a particular `ExtensibleEnum` subclass. Items are immutable
|
26
|
+
once created.
|
27
|
+
|
28
|
+
Attributes
|
29
|
+
----------
|
30
|
+
owner : type
|
31
|
+
The subclass of :class:`ExtensibleEnum` that owns this member
|
32
|
+
(e.g., `Color`).
|
33
|
+
name : str
|
34
|
+
The symbolic name of the member (e.g., `"RED"`).
|
35
|
+
value : Any
|
36
|
+
The underlying value associated with the member (e.g., `"r"`).
|
37
|
+
|
38
|
+
Notes
|
39
|
+
-----
|
40
|
+
- Equality is determined by object identity (not overridden).
|
41
|
+
- Instances are hashable by default since the dataclass is frozen.
|
42
|
+
- The `__repr__` and `__str__` provide user-friendly string forms.
|
43
|
+
|
44
|
+
Examples
|
45
|
+
--------
|
46
|
+
>>> class Color(ExtensibleEnum): ...
|
47
|
+
>>> red = Color.register("RED", "r")
|
48
|
+
>>> repr(red)
|
49
|
+
'Color.RED'
|
50
|
+
>>> str(red)
|
51
|
+
'RED'
|
52
|
+
>>> red.value
|
53
|
+
'r'
|
54
|
+
"""
|
55
|
+
owner: type
|
56
|
+
name: str
|
57
|
+
value: Any
|
58
|
+
def __repr__(self) -> str: # print(...) shows "Color.RED"
|
59
|
+
return f"{self.owner.__name__}.{self.name}"
|
60
|
+
def __str__(self) -> str:
|
61
|
+
return self.name
|
62
|
+
|
63
|
+
class _ExtEnumMeta(type):
|
64
|
+
def __iter__(cls) -> Iterator[_EnumItem]:
|
65
|
+
return iter(cls._members.values())
|
66
|
+
def __contains__(cls, item: object) -> bool:
|
67
|
+
return item in cls._members.values()
|
68
|
+
# Support Color('RED') / Color(2)
|
69
|
+
def __call__(cls, arg: Any, /, *, by: Literal["auto","name","value"]="auto") -> _EnumItem:
|
70
|
+
if isinstance(arg, _EnumItem):
|
71
|
+
if arg.owner is cls:
|
72
|
+
return arg
|
73
|
+
raise TypeError(f"{arg!r} is not a member of {cls.__name__}")
|
74
|
+
if by == "name":
|
75
|
+
return cls.get(str(arg))
|
76
|
+
if by == "value":
|
77
|
+
return cls.from_value(arg)
|
78
|
+
if isinstance(arg, str) and arg in cls._members:
|
79
|
+
return cls.get(arg)
|
80
|
+
return cls.from_value(arg)
|
81
|
+
|
82
|
+
class ExtensibleEnum(metaclass=_ExtEnumMeta):
|
83
|
+
"""
|
84
|
+
A lightweight, **runtime-extensible**, enum-like base class.
|
85
|
+
|
86
|
+
Subclass this to create an enum whose members can be registered at runtime.
|
87
|
+
Registered members are exposed as class attributes (e.g., `Color.RED`) and
|
88
|
+
can be retrieved by name (`Color.get("RED")`) or by value
|
89
|
+
(`Color.from_value("r")`). Square-bracket lookup (`Color["RED"]`) is also
|
90
|
+
supported via ``__class_getitem__``.
|
91
|
+
|
92
|
+
This is useful when:
|
93
|
+
- The full set of enum values is not known until runtime (plugins, config).
|
94
|
+
- You need attribute-style access (`Color.RED`) but want to add members
|
95
|
+
dynamically and/or validate uniqueness of names/values.
|
96
|
+
|
97
|
+
Notes
|
98
|
+
-----
|
99
|
+
- **Uniqueness:** Names and values are unique within a subclass.
|
100
|
+
- **Per-subclass registry:** Each subclass has its own member registry.
|
101
|
+
- **Thread safety:** Registration is **not** thread-safe. If multiple threads
|
102
|
+
may register members, wrap `register()` calls in your own lock.
|
103
|
+
- **Immutability:** Once registered, a member’s `name` and `value` are fixed.
|
104
|
+
Re-registering the same `name` with the *same* `value` returns the existing
|
105
|
+
item; a different value raises an error.
|
106
|
+
|
107
|
+
Examples
|
108
|
+
--------
|
109
|
+
Define an extensible enum and register members:
|
110
|
+
|
111
|
+
>>> class Color(ExtensibleEnum):
|
112
|
+
... pass
|
113
|
+
...
|
114
|
+
>>> Color.register("RED", "r")
|
115
|
+
_EnumItem(owner=Color, name='RED', value='r')
|
116
|
+
>>> Color.register("GREEN", "g")
|
117
|
+
_EnumItem(owner=Color, name='GREEN', value='g')
|
118
|
+
|
119
|
+
Access members:
|
120
|
+
|
121
|
+
>>> Color.RED is Color.get("RED")
|
122
|
+
True
|
123
|
+
>>> Color["GREEN"] is Color.get("GREEN")
|
124
|
+
True
|
125
|
+
>>> Color.from_value("g") is Color.GREEN
|
126
|
+
True
|
127
|
+
>>> Color.names()
|
128
|
+
['RED', 'GREEN']
|
129
|
+
|
130
|
+
Error cases:
|
131
|
+
|
132
|
+
>>> Color.register("RED", "different") # doctest: +IGNORE_EXCEPTION_DETAIL
|
133
|
+
ValueError: name 'RED' already used with different value 'r'
|
134
|
+
>>> Color.get("BLUE") # doctest: +IGNORE_EXCEPTION_DETAIL
|
135
|
+
KeyError: Color has no member named 'BLUE'
|
136
|
+
>>> Color.from_value("b") # doctest: +IGNORE_EXCEPTION_DETAIL
|
137
|
+
KeyError: Color has no member with value 'b'
|
138
|
+
"""
|
139
|
+
"""Runtime-extensible enum-like base."""
|
140
|
+
_members: Dict[str, _EnumItem] = {}
|
141
|
+
|
142
|
+
def __init_subclass__(cls, **kw):
|
143
|
+
super().__init_subclass__(**kw)
|
144
|
+
cls._members = {} # fresh registry per subclass
|
145
|
+
|
146
|
+
@classmethod
|
147
|
+
def __class_getitem__(cls, key: str) -> _EnumItem: # Color['RED']
|
148
|
+
return cls.get(key)
|
149
|
+
|
150
|
+
@classmethod
|
151
|
+
def register(cls, name: str, value: Any) -> _EnumItem:
|
152
|
+
if not isinstance(name, str) or not name.isidentifier():
|
153
|
+
raise ValueError("name must be a valid identifier")
|
154
|
+
if name in cls._members:
|
155
|
+
item = cls._members[name]
|
156
|
+
if item.value != value:
|
157
|
+
raise ValueError(f"name {name!r} already used with different value {item.value!r}")
|
158
|
+
return item
|
159
|
+
if any(m.value == value for m in cls._members.values()):
|
160
|
+
raise ValueError(f"value {value!r} already used")
|
161
|
+
item = _EnumItem(owner=cls, name=name, value=value)
|
162
|
+
cls._members[name] = item
|
163
|
+
setattr(cls, name, item) # enables Color.RED attribute
|
164
|
+
return item
|
165
|
+
|
166
|
+
@classmethod
|
167
|
+
def names(cls) -> list[str]:
|
168
|
+
return list(cls._members.keys())
|
169
|
+
|
170
|
+
@classmethod
|
171
|
+
def get(cls, name: str) -> _EnumItem:
|
172
|
+
try:
|
173
|
+
return cls._members[name]
|
174
|
+
except KeyError as e:
|
175
|
+
raise KeyError(f"{cls.__name__} has no member named {name!r}") from e
|
176
|
+
|
177
|
+
@classmethod
|
178
|
+
def from_value(cls, value: Any) -> _EnumItem:
|
179
|
+
for m in cls._members.values():
|
180
|
+
if m.value == value:
|
181
|
+
return m
|
182
|
+
raise KeyError(f"{cls.__name__} has no member with value {value!r}")
|
183
|
+
|
gedcomx/Fact.py
CHANGED
@@ -22,7 +22,7 @@ from enum import Enum
|
|
22
22
|
|
23
23
|
from collections.abc import Sized
|
24
24
|
|
25
|
-
from .
|
25
|
+
from .Extensions.rs10.rsLink import rsLink, _rsLinkList
|
26
26
|
|
27
27
|
|
28
28
|
class FactType(Enum):
|
@@ -401,14 +401,14 @@ class Fact(Conclusion):
|
|
401
401
|
sources: Optional[List[SourceReference]] = [],
|
402
402
|
analysis: Optional[Resource | Document] = None,
|
403
403
|
notes: Optional[List[Note]] = [],
|
404
|
-
confidence: ConfidenceLevel = None,
|
405
|
-
attribution: Attribution = None,
|
406
|
-
type: FactType = None,
|
404
|
+
confidence: Optional[ConfidenceLevel] = None,
|
405
|
+
attribution: Optional[Attribution] = None,
|
406
|
+
type: Optional[FactType] = None,
|
407
407
|
date: Optional[Date] = None,
|
408
408
|
place: Optional[PlaceReference] = None,
|
409
409
|
value: Optional[str] = None,
|
410
410
|
qualifiers: Optional[List[FactQualifier]] = None,
|
411
|
-
links: Optional[
|
411
|
+
links: Optional[_rsLinkList] = None):
|
412
412
|
super().__init__(id, lang, sources, analysis, notes, confidence, attribution, links=links)
|
413
413
|
self.type = type
|
414
414
|
self.date = date
|
@@ -433,12 +433,11 @@ class Fact(Conclusion):
|
|
433
433
|
# Only add Relationship-specific fields
|
434
434
|
fact_dict.update( {
|
435
435
|
'type': self.type.value if self.type else None,
|
436
|
-
'date': self.date.
|
436
|
+
'date': self.date._as_dict_ if self.date else None,
|
437
437
|
'place': self.place._as_dict_ if self.place else None,
|
438
438
|
'value': self.value,
|
439
439
|
'qualifiers': [q.value for q in self.qualifiers] if self.qualifiers else []
|
440
440
|
})
|
441
|
-
|
442
441
|
|
443
442
|
return Serialization.serialize_dict(fact_dict)
|
444
443
|
|
@@ -463,7 +462,7 @@ class Fact(Conclusion):
|
|
463
462
|
if data.get('place') else None)
|
464
463
|
value = data.get('value')
|
465
464
|
qualifiers = [Qualifier._from_json_(q) for q in data.get('qualifiers', [])]
|
466
|
-
links =
|
465
|
+
links = _rsLinkList._from_json_(data.get('links')) if data.get('links') else None
|
467
466
|
|
468
467
|
return cls(
|
469
468
|
id=id_,
|
gedcomx/Gedcom.py
CHANGED
@@ -1,419 +1,53 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
|
-
|
4
|
-
import html
|
5
|
-
import os
|
6
|
-
from typing import List, Optional, Tuple
|
7
1
|
import re
|
8
2
|
|
9
|
-
BOM = '\ufeff'
|
10
|
-
|
11
|
-
GEDCOM7_LINE_RE = re.compile(
|
12
|
-
r"""^
|
13
|
-
(?P<level>\d+) # Level
|
14
|
-
(?:\s+@(?P<xref>[^@]+)@)? # Optional record identifier
|
15
|
-
\s+(?P<tag>[A-Z0-9_-]+) # Tag
|
16
|
-
(?:\s+(?P<value>.+))? # Optional value (may be XREF)
|
17
|
-
$""",
|
18
|
-
re.VERBOSE
|
19
|
-
)
|
20
|
-
|
21
|
-
XREF_RE = re.compile(r'^@[^@]+@$')
|
22
|
-
|
23
|
-
# Add hash table for XREF of Zero Recrods?
|
24
|
-
|
25
|
-
nonzero = '[1-9]'
|
26
|
-
level = f'(?P<level>0|{nonzero}[0-9]*)'
|
27
|
-
atsign = '@'
|
28
|
-
underscore = '_'
|
29
|
-
ucletter = '[A-Z]'
|
30
|
-
tagchar = f'({ucletter}|[0-9]|{underscore})'
|
31
|
-
xref = f'{atsign}({tagchar})+{atsign}'
|
32
|
-
d = '\\ '
|
33
|
-
stdtag = f'{ucletter}({tagchar})*'
|
34
|
-
exttag = f'{underscore}({tagchar})+'
|
35
|
-
tag = f'({stdtag}|{exttag})'
|
36
|
-
voidptr = '@VOID@'
|
37
|
-
pointer = f'(?P<pointer>{voidptr}|{xref})'
|
38
|
-
nonat = '[\t -?A-\\U0010ffff]'
|
39
|
-
noneol = '[\t -\\U0010ffff]'
|
40
|
-
linestr = f'(?P<linestr>({nonat}|{atsign}{atsign})({noneol})*)'
|
41
|
-
lineval = f'({pointer}|{linestr})'
|
42
|
-
eol = '(\\\r(\\\n)?|\\\n)'
|
43
|
-
line = f'{level}{d}((?P<xref>{xref}){d})?(?P<tag>{tag})({d}{lineval})?{eol}'
|
44
|
-
|
45
|
-
class GedcomRecord():
|
46
|
-
|
47
|
-
def __init__(self,line_num: Optional[int] =None,level: int =-1, tag='NONR', xref: Optional[str] = None, value: Optional[str] = None) -> None:
|
48
|
-
self.line = line_num
|
49
|
-
self._subRecords = []
|
50
|
-
self.level = int(level)
|
51
|
-
self.xref = xref
|
52
|
-
self.pointer: bool = False
|
53
|
-
self.tag = str(tag).strip()
|
54
|
-
self.value = value
|
55
|
-
|
56
|
-
self.parent = None
|
57
|
-
self.root = None
|
58
|
-
|
59
|
-
#if self.value and (self.value.endswith('@') and self.value.startswith('@')):
|
60
|
-
# self.xref = self.value.replace('@','')
|
61
|
-
# if level > 0:
|
62
|
-
# self.pointer = True
|
63
|
-
|
64
|
-
@property
|
65
|
-
def _as_dict_(self):
|
66
|
-
record_dict = {
|
67
|
-
'level':self.level,
|
68
|
-
'xref':self.xref,
|
69
|
-
'tag': self.tag,
|
70
|
-
'pointer': self.pointer,
|
71
|
-
'value': self.value,
|
72
|
-
'subrecords': [subrecord._as_dict_ for subrecord in self._subRecords]
|
73
|
-
}
|
74
|
-
return record_dict
|
75
|
-
|
76
|
-
def addSubRecord(self, record):
|
77
|
-
if record and record.level == self.level+1:
|
78
|
-
record.parent = self
|
79
|
-
self._subRecords.append(record)
|
80
|
-
else:
|
81
|
-
raise ValueError(f"SubRecord must be next level from this record (level:{self.level}, subRecord has level {record.level})")
|
82
|
-
|
83
|
-
def recordOnly(self):
|
84
|
-
return GedcomRecord(line_num=self.line,level=self.level,tag=self.tag,value=self.value)
|
85
|
-
|
86
|
-
def dump(self):
|
87
|
-
record_dump = f"Level: {self.level}, tag: {self.tag}, value: {self.value}, subRecords: {len(self._subRecords)}\n"
|
88
|
-
for record in self._subRecords:
|
89
|
-
record_dump += "\t" + record.dump() # Recursively call dump on sub_records and concatenate
|
90
|
-
return record_dump
|
91
|
-
|
92
|
-
def describe(self,subRecords: bool = False):
|
93
|
-
level_str = '\t'* self.level
|
94
|
-
description = f"Line {self.line}: {level_str} Level: {self.level}, tag: '{self.tag}', xref={self.xref} value: '{self.value}', subRecords: {len(self._subRecords)}"
|
95
|
-
if subRecords:
|
96
|
-
for subRecord in self.subRecords():
|
97
|
-
description = description + '\n' + subRecord.describe(subRecords=True)
|
98
|
-
return description
|
99
|
-
|
100
|
-
|
101
|
-
def subRecord(self, tag):
|
102
|
-
result = [record for record in self._subRecords if record.tag == tag]
|
103
|
-
if len(result) == 0: return None
|
104
|
-
return result
|
105
|
-
|
106
|
-
def subRecords(self, tag: str = None):
|
107
|
-
if not tag:
|
108
|
-
return self._subRecords
|
109
|
-
else:
|
110
|
-
tags = tag.split("/", 1) # Split into first tag and the rest
|
111
|
-
|
112
|
-
# Collect all records matching the first tag
|
113
|
-
matching_records = [record for record in self._subRecords if record.tag == tags[0]]
|
114
|
-
|
115
|
-
if not matching_records:
|
116
|
-
return None # No matching records found for the first tag
|
117
|
-
|
118
|
-
if len(tags) == 1:
|
119
|
-
return matching_records # Return all matching records for the final tag
|
120
|
-
|
121
|
-
# Recurse into each matching record's subRecords and collect results
|
122
|
-
results = []
|
123
|
-
for record in matching_records:
|
124
|
-
sub_result = record.subRecords(tags[1])
|
125
|
-
if sub_result:
|
126
|
-
if isinstance(sub_result, list):
|
127
|
-
results.extend(sub_result)
|
128
|
-
else:
|
129
|
-
results.append(sub_result)
|
130
|
-
|
131
|
-
return results if results else None
|
132
|
-
|
133
|
-
def __call__(self) -> None:
|
134
|
-
return self.describe()
|
135
|
-
|
136
|
-
def __iter__(self):
|
137
|
-
return self._flatten_subrecords(self)
|
138
|
-
|
139
|
-
def _flatten_subrecords(self, record):
|
140
|
-
yield record
|
141
|
-
for subrecord in record._subRecords:
|
142
|
-
yield from self._flatten_subrecords(subrecord)
|
143
|
-
|
144
3
|
class Gedcom():
|
145
|
-
|
146
|
-
|
4
|
+
def __init__(self) -> None:
|
5
|
+
pass
|
147
6
|
|
148
|
-
|
149
|
-
|
150
|
-
records : List[GedcomReord]
|
151
|
-
List of GedcomRecords to initialize the genealogy with
|
152
|
-
filepath : str
|
153
|
-
path to a GEDCOM (``*``.ged), if provided object will read, parse and initialize with records in the file.
|
154
|
-
|
155
|
-
Note
|
156
|
-
----
|
157
|
-
**file_path** takes precidence over **records**.
|
158
|
-
If no arguments are provided, Gedcom Object will initialize with no records.
|
159
|
-
|
160
|
-
|
161
|
-
"""
|
162
|
-
_top_level_tags = ['INDI', 'FAM', 'OBJE', 'SOUR', 'REPO', 'NOTE', 'HEAD','SNOTE']
|
163
|
-
|
164
|
-
def __init__(self, records: Optional[List[GedcomRecord]] = None,filepath: str = None) -> None:
|
165
|
-
if filepath:
|
166
|
-
self.records = self._records_from_file(filepath)
|
167
|
-
elif records:
|
168
|
-
self.records: List[GedcomRecord] = records if records else []
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
self._sources = []
|
173
|
-
self._repositories = []
|
174
|
-
self._individuals = []
|
175
|
-
self._families = []
|
176
|
-
self._objects = []
|
177
|
-
self._snotes = []
|
178
|
-
|
179
|
-
if self.records:
|
180
|
-
for record in self.records:
|
181
|
-
if record.tag == 'INDI':
|
182
|
-
|
183
|
-
self._individuals.append(record)
|
184
|
-
if record.tag == 'SOUR' and record.level == 0:
|
185
|
-
|
186
|
-
self._sources.append(record)
|
187
|
-
if record.tag == 'REPO' and record.level == 0:
|
188
|
-
print(record.describe())
|
189
|
-
|
190
|
-
self._repositories.append(record)
|
191
|
-
if record.tag == 'FAM' and record.level == 0:
|
192
|
-
|
193
|
-
self._families.append(record)
|
194
|
-
if record.tag == 'OBJE' and record.level == 0:
|
195
|
-
|
196
|
-
self._objects.append(record)
|
197
|
-
if record.tag == 'SNOTE' and record.level == 0:
|
198
|
-
|
199
|
-
record.xref = record.value
|
200
|
-
self._snotes.append(record)
|
201
|
-
|
202
|
-
|
203
|
-
# =========================================================
|
204
|
-
# 2. PROPERTY ACCESSORS (GETTERS & SETTERS)
|
205
|
-
# =========================================================
|
206
|
-
|
207
|
-
@property
|
208
|
-
def json(self):
|
209
|
-
import json
|
210
|
-
return json.dumps({'Individuals': [indi._as_dict_ for indi in self._individuals]},indent=4)
|
211
|
-
|
212
|
-
def stats(self):
|
213
|
-
def print_table(pairs):
|
214
|
-
|
215
|
-
# Calculate the width of the columns
|
216
|
-
name_width = max(len(name) for name, _ in pairs)
|
217
|
-
value_width = max(len(str(value)) for _, value in pairs)
|
218
|
-
|
219
|
-
# Print the header
|
220
|
-
print('GEDCOM Import Results')
|
221
|
-
header = f"{'Type'.ljust(name_width)} | {'Count'.ljust(value_width)}"
|
222
|
-
print('-' * len(header))
|
223
|
-
print(header)
|
224
|
-
print('-' * len(header))
|
225
|
-
|
226
|
-
# Print each pair in the table
|
227
|
-
for name, value in pairs:
|
228
|
-
print(f"{name.ljust(name_width)} | {str(value).ljust(value_width)}")
|
229
|
-
|
230
|
-
imports_stats = [
|
231
|
-
('Top Level Records', len(self.records)),
|
232
|
-
('Individuals', len(self.individuals)),
|
233
|
-
('Family Group Records', len(self.families)),
|
234
|
-
('Repositories', len(self.repositories)),
|
235
|
-
('Sources', len(self.sources)),
|
236
|
-
('Objects', len(self.objects))
|
237
|
-
]
|
238
|
-
|
239
|
-
print_table(imports_stats)
|
240
|
-
|
241
|
-
@property
|
242
|
-
def sources(self) -> List[GedcomRecord]:
|
243
|
-
return self._sources
|
244
|
-
|
245
|
-
@sources.setter
|
246
|
-
def sources(self, value: List[GedcomRecord]):
|
247
|
-
if not isinstance(value, list) or not all(isinstance(item, GedcomRecord) for item in value):
|
248
|
-
raise ValueError("sources must be a list of GedcomRecord objects.")
|
249
|
-
self._sources = value
|
250
|
-
|
251
|
-
@property
|
252
|
-
def repositories(self) -> List[GedcomRecord]:
|
253
|
-
"""
|
254
|
-
List of **REPO** records found in the Genealogy
|
255
|
-
"""
|
256
|
-
return self._repositories
|
257
|
-
|
258
|
-
@repositories.setter
|
259
|
-
def repositories(self, value: List[GedcomRecord]):
|
260
|
-
if not isinstance(value, list) or not all(isinstance(item, GedcomRecord) for item in value):
|
261
|
-
raise ValueError("repositories must be a list of GedcomRecord objects.")
|
262
|
-
self._repositories = value
|
263
|
-
|
264
|
-
@property
|
265
|
-
def individuals(self) -> List[GedcomRecord]:
|
266
|
-
return self._individuals
|
267
|
-
|
268
|
-
@individuals.setter
|
269
|
-
def individuals(self, value: List[GedcomRecord]):
|
270
|
-
if not isinstance(value, list) or not all(isinstance(item, GedcomRecord) for item in value):
|
271
|
-
raise ValueError("individuals must be a list of GedcomRecord objects.")
|
272
|
-
self._individuals = value
|
273
|
-
|
274
|
-
@property
|
275
|
-
def families(self) -> List[GedcomRecord]:
|
276
|
-
return self._families
|
277
|
-
|
278
|
-
@families.setter
|
279
|
-
def families(self, value: List[GedcomRecord]):
|
280
|
-
if not isinstance(value, list) or not all(isinstance(item, GedcomRecord) for item in value):
|
281
|
-
raise ValueError("families must be a list of GedcomRecord objects.")
|
282
|
-
self._families = value
|
283
|
-
|
284
|
-
@property
|
285
|
-
def objects(self) -> List[GedcomRecord]:
|
286
|
-
return self._objects
|
287
|
-
|
288
|
-
@objects.setter
|
289
|
-
def objects(self, value: List[GedcomRecord]):
|
290
|
-
if not isinstance(value, list) or not all(isinstance(item, GedcomRecord) for item in value):
|
291
|
-
raise ValueError("objects must be a list of GedcomRecord objects.")
|
292
|
-
self._objects = value
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
def write(self) -> bool:
|
7
|
+
@staticmethod
|
8
|
+
def read_gedcom_version(filepath: str) -> str | None:
|
297
9
|
"""
|
298
|
-
|
10
|
+
Reads only the HEAD section of a GEDCOM file and returns the GEDCOM standard version.
|
11
|
+
Looks specifically for HEAD → GEDC → VERS.
|
299
12
|
|
300
|
-
|
301
|
-
|
302
|
-
NotImplementedError
|
303
|
-
writing to legacy GEDCOM file is not currently implimented.
|
13
|
+
Returns:
|
14
|
+
str: GEDCOM version (e.g., "5.5.1" or "7.0.0"), or None if not found.
|
304
15
|
"""
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
Returns:
|
314
|
-
(level, xref_id, tag, value, xref_value)
|
315
|
-
"""
|
316
|
-
match = GEDCOM7_LINE_RE.match(line.strip())
|
317
|
-
if not match:
|
318
|
-
return None
|
319
|
-
|
320
|
-
level = int(match.group("level"))
|
321
|
-
xref_id = match.group("xref")
|
322
|
-
tag = match.group("tag")
|
323
|
-
value = match.group("value")
|
324
|
-
if value == 'None': value = None
|
325
|
-
xref_value = value.strip("@") if value and XREF_RE.match(value.strip()) else None
|
326
|
-
|
327
|
-
return level, xref_id, tag, value, xref_value
|
328
|
-
extension = '.ged'
|
329
|
-
|
330
|
-
if not os.path.exists(filepath):
|
331
|
-
print(f"File does not exist: {filepath}")
|
332
|
-
raise FileNotFoundError
|
333
|
-
elif not filepath.lower().endswith(extension.lower()):
|
334
|
-
print(f"File does not have the correct extension: {filepath}")
|
335
|
-
raise Exception("File does not appear to be a GEDCOM")
|
336
|
-
|
337
|
-
print("Reading from GEDCOM file")
|
338
|
-
with open(filepath, 'r', encoding='utf-8') as file:
|
339
|
-
lines = [line.strip() for line in file]
|
340
|
-
|
341
|
-
records = []
|
342
|
-
record_map = {0: None, 1: None, 2: None, 3: None, 4: None, 5: None}
|
343
|
-
|
344
|
-
for l, line in enumerate(lines):
|
345
|
-
if line.startswith(BOM):
|
346
|
-
line = line.lstrip(BOM)
|
347
|
-
line = html.unescape(line).replace('"', '')
|
348
|
-
|
349
|
-
if line.strip() == '':
|
16
|
+
version = None
|
17
|
+
inside_head = False
|
18
|
+
inside_gedc = False
|
19
|
+
|
20
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
21
|
+
for line in f:
|
22
|
+
parts = line.strip().split(maxsplit=2)
|
23
|
+
if not parts:
|
350
24
|
continue
|
351
25
|
|
352
|
-
level
|
353
|
-
|
354
|
-
|
355
|
-
parts = line.split(maxsplit=2)
|
356
|
-
if len(parts) == 3:
|
357
|
-
level, col2, col3 = parts
|
358
|
-
|
359
|
-
if col3 in Gedcom._top_level_tags:
|
360
|
-
tag = col3
|
361
|
-
value = col2
|
362
|
-
else:
|
363
|
-
tag = col2
|
364
|
-
value = col3
|
365
|
-
|
366
|
-
else:
|
367
|
-
level, tag = parts
|
368
|
-
|
369
|
-
level, xref, tag, value, xref_value = parse_gedcom7_line(line)
|
370
|
-
|
371
|
-
if xref is None and xref_value is not None:
|
372
|
-
xref = xref_value
|
373
|
-
# print(l, level, xref, tag, value, xref_value)
|
374
|
-
|
375
|
-
level = int(level)
|
376
|
-
|
377
|
-
new_record = GedcomRecord(line_num=l + 1, level=level, tag=tag, xref=xref,value=value)
|
378
|
-
|
379
|
-
|
380
|
-
if level == 0:
|
381
|
-
records.append(new_record)
|
382
|
-
else:
|
383
|
-
new_record.root = record_map[0]
|
384
|
-
new_record.parent = record_map[int(level) - 1]
|
385
|
-
record_map[int(level) - 1].addSubRecord(new_record)
|
386
|
-
record_map[int(level)] = new_record
|
387
|
-
|
388
|
-
|
389
|
-
return records if records else None
|
390
|
-
|
391
|
-
@staticmethod
|
392
|
-
def fromFile(filepath: str) -> 'Gedcom':
|
393
|
-
"""
|
394
|
-
Static method to create a Gedcom object from a GEDCOM file.
|
26
|
+
level = int(parts[0])
|
27
|
+
tag = parts[1] if len(parts) > 1 else ""
|
28
|
+
value = parts[2] if len(parts) > 2 else None
|
395
29
|
|
396
|
-
|
397
|
-
|
30
|
+
# Enter HEAD
|
31
|
+
if level == 0 and tag == "HEAD":
|
32
|
+
inside_head = True
|
33
|
+
continue
|
398
34
|
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
records = Gedcom._records_from_file(filepath)
|
403
|
-
|
404
|
-
gedcom = Gedcom(records=records)
|
35
|
+
# Leave HEAD block
|
36
|
+
if inside_head and level == 0:
|
37
|
+
break
|
405
38
|
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
Adds records from a valid (``*``.ged) file to the current Genealogy
|
39
|
+
# Inside HEAD, look for GEDC
|
40
|
+
if inside_head and level == 1 and tag == "GEDC":
|
41
|
+
inside_gedc = True
|
42
|
+
continue
|
411
43
|
|
412
|
-
|
413
|
-
|
44
|
+
# If we drop back to level 1 (but not GEDC), stop looking inside GEDC
|
45
|
+
if inside_gedc and level == 1:
|
46
|
+
inside_gedc = False
|
414
47
|
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
48
|
+
# Inside GEDC, look for VERS
|
49
|
+
if inside_gedc and tag == "VERS":
|
50
|
+
version = value
|
51
|
+
break
|
419
52
|
|
53
|
+
return version
|