gedcom-x 0.5.1__py3-none-any.whl → 0.5.2__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.1.dist-info → gedcom_x-0.5.2.dist-info}/METADATA +1 -1
- gedcom_x-0.5.2.dist-info/RECORD +42 -0
- gedcomx/Address.py +40 -11
- gedcomx/Agent.py +129 -23
- gedcomx/Attribution.py +38 -54
- gedcomx/Conclusion.py +60 -45
- gedcomx/Date.py +49 -8
- gedcomx/Document.py +19 -9
- gedcomx/Event.py +4 -4
- gedcomx/EvidenceReference.py +2 -2
- gedcomx/Exceptions.py +10 -0
- gedcomx/Fact.py +70 -46
- gedcomx/Gedcom.py +110 -37
- gedcomx/GedcomX.py +405 -175
- gedcomx/Gender.py +61 -8
- gedcomx/Group.py +3 -3
- gedcomx/Identifier.py +93 -10
- gedcomx/Logging.py +19 -0
- gedcomx/Name.py +67 -38
- gedcomx/Note.py +5 -4
- gedcomx/OnlineAccount.py +2 -2
- gedcomx/Person.py +88 -33
- gedcomx/PlaceDescription.py +22 -8
- gedcomx/PlaceReference.py +7 -5
- gedcomx/Relationship.py +19 -9
- gedcomx/Resource.py +61 -0
- gedcomx/Serialization.py +44 -1
- gedcomx/SourceCitation.py +6 -1
- gedcomx/SourceDescription.py +89 -72
- gedcomx/SourceReference.py +25 -14
- gedcomx/Subject.py +10 -8
- gedcomx/TextValue.py +2 -1
- gedcomx/URI.py +95 -61
- gedcomx/Zip.py +1 -0
- gedcomx/_Links.py +37 -0
- gedcomx/__init__.py +4 -2
- gedcomx/g7interop.py +205 -0
- gedcom_x-0.5.1.dist-info/RECORD +0 -37
- gedcomx/_Resource.py +0 -11
- {gedcom_x-0.5.1.dist-info → gedcom_x-0.5.2.dist-info}/WHEEL +0 -0
- {gedcom_x-0.5.1.dist-info → gedcom_x-0.5.2.dist-info}/top_level.txt +0 -0
gedcomx/Date.py
CHANGED
@@ -1,29 +1,70 @@
|
|
1
1
|
from typing import Optional
|
2
|
+
from datetime import datetime, timezone
|
3
|
+
from dateutil import parser
|
4
|
+
import time
|
2
5
|
|
3
6
|
|
4
7
|
class DateFormat:
|
5
8
|
def __init__(self) -> None:
|
6
9
|
pass
|
7
|
-
|
10
|
+
|
11
|
+
class DateNormalization():
|
12
|
+
pass
|
8
13
|
|
9
14
|
class Date:
|
10
15
|
identifier = 'http://gedcomx.org/v1/Date'
|
11
16
|
version = 'http://gedcomx.org/conceptual-model/v1'
|
12
17
|
|
13
|
-
def __init__(self, original: Optional[str],formal: Optional[str | DateFormat] = None) -> None:
|
18
|
+
def __init__(self, original: Optional[str],normalized: Optional[DateNormalization] = None ,formal: Optional[str | DateFormat] = None) -> None:
|
14
19
|
self.orginal = original
|
15
20
|
self.formal = formal
|
21
|
+
|
22
|
+
self.normalized: DateNormalization | None = normalized if normalized else None
|
16
23
|
|
17
24
|
def _prop_dict(self):
|
18
25
|
return {'original': self.orginal,
|
19
26
|
'formal': self.formal}
|
20
27
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
28
|
+
@classmethod
|
29
|
+
def _from_json_(obj,data):
|
30
|
+
original = data.get('original',None)
|
31
|
+
formal = data.get('formal',None)
|
32
|
+
|
33
|
+
return Date(original=original,formal=formal)
|
34
|
+
|
26
35
|
|
27
36
|
Date._to_dict_ = lambda self: {
|
28
37
|
'original': self.orginal,
|
29
|
-
'formal': self.formal}
|
38
|
+
'formal': self.formal}
|
39
|
+
|
40
|
+
|
41
|
+
|
42
|
+
|
43
|
+
def date_to_timestamp(date_str: str, assume_utc_if_naive: bool = True, print_definition: bool = True):
|
44
|
+
"""
|
45
|
+
Convert a date string of various formats into a Unix timestamp.
|
46
|
+
|
47
|
+
A "timestamp" refers to an instance of time, including values for year,
|
48
|
+
month, date, hour, minute, second, and timezone.
|
49
|
+
"""
|
50
|
+
# Handle year ranges like "1894-1912" → pick first year
|
51
|
+
if "-" in date_str and date_str.count("-") == 1 and all(part.isdigit() for part in date_str.split("-")):
|
52
|
+
date_str = date_str.split("-")[0].strip()
|
53
|
+
|
54
|
+
# Parse date
|
55
|
+
dt = parser.parse(date_str)
|
56
|
+
|
57
|
+
# Ensure timezone awareness
|
58
|
+
if dt.tzinfo is None:
|
59
|
+
dt = dt.replace(tzinfo=timezone.utc if assume_utc_if_naive else datetime.now().astimezone().tzinfo)
|
60
|
+
|
61
|
+
# Normalize to UTC and compute timestamp
|
62
|
+
dt_utc = dt.astimezone(timezone.utc)
|
63
|
+
epoch = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
64
|
+
ts = (dt_utc - epoch).total_seconds()
|
65
|
+
|
66
|
+
# Create ISO 8601 string with full date/time/timezone
|
67
|
+
full_timestamp_str = dt_utc.replace(microsecond=0).isoformat()
|
68
|
+
|
69
|
+
|
70
|
+
return ts, full_timestamp_str
|
gedcomx/Document.py
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
from enum import Enum
|
2
|
-
from typing import Optional
|
2
|
+
from typing import Optional, List
|
3
3
|
|
4
4
|
from gedcomx.Attribution import Attribution
|
5
|
-
from gedcomx.Conclusion import ConfidenceLevel
|
5
|
+
#from gedcomx.Conclusion import ConfidenceLevel
|
6
6
|
from gedcomx.Note import Note
|
7
7
|
from gedcomx.SourceReference import SourceReference
|
8
|
-
from gedcomx.
|
8
|
+
from gedcomx.Resource import Resource
|
9
9
|
|
10
10
|
from .Conclusion import Conclusion
|
11
11
|
|
@@ -33,10 +33,20 @@ class Document(Conclusion):
|
|
33
33
|
identifier = 'http://gedcomx.org/v1/Document'
|
34
34
|
version = 'http://gedcomx.org/conceptual-model/v1'
|
35
35
|
|
36
|
-
def __init__(self, id: str
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
36
|
+
def __init__(self, id: Optional[str] = None,
|
37
|
+
lang: Optional[str] = None,
|
38
|
+
sources: Optional[List[SourceReference]] = None,
|
39
|
+
analysis: Optional[Resource] = None,
|
40
|
+
notes: Optional[List[Note]] = None,
|
41
|
+
confidence: Optional[object] = None, # ConfidenceLevel
|
42
|
+
attribution: Optional[Attribution] = None,
|
43
|
+
type: Optional[DocumentType] = None,
|
44
|
+
extracted: Optional[bool] = None, # Default to False
|
45
|
+
textType: Optional[TextType] = None,
|
46
|
+
text: Optional[str] = None,
|
41
47
|
) -> None:
|
42
|
-
super().__init__(id, lang, sources, analysis, notes, confidence, attribution)
|
48
|
+
super().__init__(id, lang, sources, analysis, notes, confidence, attribution)
|
49
|
+
self.type = type
|
50
|
+
self.extracted = extracted
|
51
|
+
self.textType = textType
|
52
|
+
self.text = text
|
gedcomx/Event.py
CHANGED
@@ -11,7 +11,7 @@ from .Note import Note
|
|
11
11
|
from .PlaceReference import PlaceReference
|
12
12
|
from .SourceReference import SourceReference
|
13
13
|
from .Subject import Subject
|
14
|
-
from .
|
14
|
+
from .Resource import Resource
|
15
15
|
|
16
16
|
class EventRoleType(Enum):
|
17
17
|
Principal = "http://gedcomx.org/Principal"
|
@@ -37,11 +37,11 @@ class EventRole(Conclusion):
|
|
37
37
|
id: Optional[str] = None,
|
38
38
|
lang: Optional[str] = 'en',
|
39
39
|
sources: Optional[List[SourceReference]] = [],
|
40
|
-
analysis: Optional[
|
40
|
+
analysis: Optional[Resource] = None,
|
41
41
|
notes: Optional[List[Note]] = [],
|
42
42
|
confidence: Optional[ConfidenceLevel] = None,
|
43
43
|
attribution: Optional[Attribution] = None,
|
44
|
-
person:
|
44
|
+
person: Resource = None,
|
45
45
|
type: Optional[EventRoleType] = None,
|
46
46
|
details: Optional[str] = None) -> None:
|
47
47
|
super().__init__(id, lang, sources, analysis, notes, confidence, attribution)
|
@@ -180,7 +180,7 @@ class Event(Subject):
|
|
180
180
|
id: Optional[str] = None,
|
181
181
|
lang: Optional[str] = 'en',
|
182
182
|
sources: Optional[List[SourceReference]] = [],
|
183
|
-
analysis: Optional[
|
183
|
+
analysis: Optional[Resource] = None,
|
184
184
|
notes: Optional[List[Note]] = [],
|
185
185
|
confidence: Optional[ConfidenceLevel] = None,
|
186
186
|
attribution: Optional[Attribution] = None,
|
gedcomx/EvidenceReference.py
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
from typing import Optional
|
2
2
|
|
3
3
|
from .Attribution import Attribution
|
4
|
-
from .
|
4
|
+
from .Resource import Resource
|
5
5
|
|
6
6
|
class EvidenceReference:
|
7
7
|
identifier = 'http://gedcomx.org/v1/EvidenceReference'
|
8
8
|
version = 'http://gedcomx.org/conceptual-model/v1'
|
9
9
|
|
10
|
-
def __init__(self, resource:
|
10
|
+
def __init__(self, resource: Resource, attribution: Optional[Attribution]) -> None:
|
11
11
|
pass
|
gedcomx/Exceptions.py
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class GedcomXError(Exception):
|
4
|
+
"""Base for all app-specific errors."""
|
5
|
+
|
6
|
+
class TagConversionError(GedcomXError):
|
7
|
+
def __init__(self, record,levelstack):
|
8
|
+
msg = f"Cannot convert: #{record.line} TAG: {record.tag} {record.xref if record.xref else ''} Value:{record.value} STACK: {type(levelstack[record.level-1]).__name__}"
|
9
|
+
super().__init__(msg)
|
10
|
+
|
gedcomx/Fact.py
CHANGED
@@ -3,21 +3,27 @@ import re
|
|
3
3
|
|
4
4
|
from datetime import datetime
|
5
5
|
from enum import Enum
|
6
|
-
from typing import List, Optional
|
6
|
+
from typing import List, Optional, Dict, Any
|
7
7
|
|
8
8
|
from .Attribution import Attribution
|
9
9
|
from .Conclusion import ConfidenceLevel
|
10
|
+
from .Document import Document
|
10
11
|
from .Date import Date
|
11
12
|
from .Note import Note
|
12
13
|
from .PlaceReference import PlaceReference
|
13
14
|
from .SourceReference import SourceReference
|
14
|
-
from .
|
15
|
+
from .Serialization import Serialization
|
16
|
+
from .Resource import Resource
|
15
17
|
|
16
18
|
from .Conclusion import Conclusion
|
17
19
|
from .Qualifier import Qualifier
|
18
20
|
|
19
21
|
from enum import Enum
|
20
22
|
|
23
|
+
from collections.abc import Sized
|
24
|
+
|
25
|
+
from ._Links import _Link, _LinkList
|
26
|
+
|
21
27
|
|
22
28
|
class FactType(Enum):
|
23
29
|
# Person Fact Types
|
@@ -390,10 +396,10 @@ class Fact(Conclusion):
|
|
390
396
|
version = 'http://gedcomx.org/conceptual-model/v1'
|
391
397
|
|
392
398
|
def __init__(self,
|
393
|
-
id: str = None,
|
399
|
+
id: Optional[str] = None,
|
394
400
|
lang: str = 'en',
|
395
401
|
sources: Optional[List[SourceReference]] = [],
|
396
|
-
analysis:
|
402
|
+
analysis: Optional[Resource | Document] = None,
|
397
403
|
notes: Optional[List[Note]] = [],
|
398
404
|
confidence: ConfidenceLevel = None,
|
399
405
|
attribution: Attribution = None,
|
@@ -401,62 +407,80 @@ class Fact(Conclusion):
|
|
401
407
|
date: Optional[Date] = None,
|
402
408
|
place: Optional[PlaceReference] = None,
|
403
409
|
value: Optional[str] = None,
|
404
|
-
qualifiers
|
405
|
-
|
410
|
+
qualifiers: Optional[List[FactQualifier]] = None,
|
411
|
+
links: Optional[_LinkList] = None):
|
412
|
+
super().__init__(id, lang, sources, analysis, notes, confidence, attribution, links=links)
|
406
413
|
self.type = type
|
407
414
|
self.date = date
|
408
415
|
self.place = place
|
409
416
|
self.value = value
|
410
|
-
self.
|
417
|
+
self._qualifiers = qualifiers if qualifiers else []
|
411
418
|
|
419
|
+
|
412
420
|
@property
|
413
|
-
def
|
414
|
-
|
415
|
-
if isinstance(value, (str, int, float, bool, type(None))):
|
416
|
-
return value
|
417
|
-
elif isinstance(value, dict):
|
418
|
-
return {k: _serialize(v) for k, v in value.items()}
|
419
|
-
elif isinstance(value, (list, tuple, set)):
|
420
|
-
return [_serialize(v) for v in value]
|
421
|
-
elif hasattr(value, "_as_dict_"):
|
422
|
-
return value._as_dict_
|
423
|
-
else:
|
424
|
-
return str("UKN " + value) # fallback for unknown objects
|
421
|
+
def qualifiers(self) -> List[FactQualifier]:
|
422
|
+
return self._qualifiers # type: ignore
|
425
423
|
|
424
|
+
@qualifiers.setter
|
425
|
+
def qualifiers(self, value: List[FactQualifier]):
|
426
|
+
if (not isinstance(value, list)) or (not all(isinstance(item, FactQualifier) for item in value)):
|
427
|
+
raise ValueError("sources must be a list of GedcomRecord objects.")
|
428
|
+
self._qualifiers.extend(value)
|
429
|
+
|
430
|
+
@property
|
431
|
+
def _as_dict_(self):
|
432
|
+
fact_dict = super()._as_dict_
|
426
433
|
# Only add Relationship-specific fields
|
427
|
-
|
434
|
+
fact_dict.update( {
|
428
435
|
'type': self.type.value if self.type else None,
|
429
436
|
'date': self.date._prop_dict() if self.date else None,
|
430
437
|
'place': self.place._as_dict_ if self.place else None,
|
431
438
|
'value': self.value,
|
432
439
|
'qualifiers': [q.value for q in self.qualifiers] if self.qualifiers else []
|
433
|
-
}
|
434
|
-
|
435
|
-
# Serialize and exclude None values
|
436
|
-
for key, value in fact_fields.items():
|
437
|
-
if value is not None:
|
438
|
-
fact_fields[key] = _serialize(value)
|
440
|
+
})
|
439
441
|
|
440
442
|
|
441
|
-
return
|
443
|
+
return Serialization.serialize_dict(fact_dict)
|
444
|
+
|
445
|
+
@classmethod
|
446
|
+
def _from_json_(cls, data: Dict[str, Any]) -> 'Fact':
|
447
|
+
|
448
|
+
# Extract fields, no trailing commas!
|
449
|
+
id_ = data.get('id')
|
450
|
+
lang = data.get('lang', 'en')
|
451
|
+
sources = [SourceReference._from_json_(s) for s in data.get('sources',[])]
|
452
|
+
analysis = (Resource._from_json_(data['analysis'])
|
453
|
+
if data.get('analysis') else None)
|
454
|
+
notes = [Note._from_json_(n) for n in data.get('notes',[])]
|
455
|
+
confidence = (ConfidenceLevel._from_json_(data['confidence'])
|
456
|
+
if data.get('confidence') else None)
|
457
|
+
attribution = (Attribution._from_json_(data['attribution']) if data.get('attribution') else None)
|
458
|
+
fact_type = (FactType.from_value(data['type'])
|
459
|
+
if data.get('type') else None)
|
460
|
+
date = (Date._from_json_(data['date'])
|
461
|
+
if data.get('date') else None)
|
462
|
+
place = (PlaceReference._from_json_(data['place'])
|
463
|
+
if data.get('place') else None)
|
464
|
+
value = data.get('value')
|
465
|
+
qualifiers = [Qualifier._from_json_(q) for q in data.get('qualifiers', [])]
|
466
|
+
links = _LinkList._from_json_(data.get('links')) if data.get('links') else None
|
467
|
+
|
468
|
+
return cls(
|
469
|
+
id=id_,
|
470
|
+
lang=lang,
|
471
|
+
sources=sources,
|
472
|
+
analysis=analysis,
|
473
|
+
notes=notes,
|
474
|
+
confidence=confidence,
|
475
|
+
attribution=attribution,
|
476
|
+
type=fact_type,
|
477
|
+
date=date,
|
478
|
+
place=place,
|
479
|
+
value=value,
|
480
|
+
qualifiers=qualifiers,
|
481
|
+
links=links
|
482
|
+
)
|
483
|
+
|
484
|
+
|
442
485
|
|
443
|
-
def ensure_list(val):
|
444
|
-
if val is None:
|
445
|
-
return []
|
446
|
-
return val if isinstance(val, list) else [val]
|
447
|
-
# Fact
|
448
|
-
Fact._from_json_ = classmethod(lambda cls, data: cls(
|
449
|
-
id=data.get('id'),
|
450
|
-
lang=data.get('lang', 'en'),
|
451
|
-
sources=[SourceReference._from_json_(s) for s in ensure_list(data.get('sources'))],
|
452
|
-
analysis=URI._from_json_(data['analysis']) if data.get('analysis') else None,
|
453
|
-
notes=[Note._from_json_(n) for n in ensure_list(data.get('notes'))],
|
454
|
-
confidence=ConfidenceLevel._from_json_(data['confidence']) if data.get('confidence') else None,
|
455
|
-
attribution=Attribution._from_json_(data['attribution']) if data.get('attribution') else None,
|
456
|
-
type=FactType.from_value(data['type']) if data.get('type') else None,
|
457
|
-
date=Date._from_json_(data['date']) if data.get('date') else None,
|
458
|
-
place=PlaceReference._from_json_(data['place']) if data.get('place') else None,
|
459
|
-
value=data.get('value'),
|
460
|
-
qualifiers=[Qualifier._from_json_(q) for q in ensure_list(data.get('qualifiers'))]
|
461
|
-
))
|
462
486
|
|
gedcomx/Gedcom.py
CHANGED
@@ -3,10 +3,23 @@
|
|
3
3
|
|
4
4
|
import html
|
5
5
|
import os
|
6
|
-
from typing import List, Optional
|
6
|
+
from typing import List, Optional, Tuple
|
7
|
+
import re
|
7
8
|
|
8
9
|
BOM = '\ufeff'
|
9
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
|
+
|
10
23
|
# Add hash table for XREF of Zero Recrods?
|
11
24
|
|
12
25
|
nonzero = '[1-9]'
|
@@ -30,8 +43,9 @@ eol = '(\\\r(\\\n)?|\\\n)'
|
|
30
43
|
line = f'{level}{d}((?P<xref>{xref}){d})?(?P<tag>{tag})({d}{lineval})?{eol}'
|
31
44
|
|
32
45
|
class GedcomRecord():
|
33
|
-
|
34
|
-
|
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
|
35
49
|
self._subRecords = []
|
36
50
|
self.level = int(level)
|
37
51
|
self.xref = xref
|
@@ -42,10 +56,10 @@ class GedcomRecord():
|
|
42
56
|
self.parent = None
|
43
57
|
self.root = None
|
44
58
|
|
45
|
-
if self.value.endswith('@') and self.value.startswith('@'):
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
49
63
|
|
50
64
|
@property
|
51
65
|
def _as_dict_(self):
|
@@ -67,7 +81,7 @@ class GedcomRecord():
|
|
67
81
|
raise ValueError(f"SubRecord must be next level from this record (level:{self.level}, subRecord has level {record.level})")
|
68
82
|
|
69
83
|
def recordOnly(self):
|
70
|
-
return GedcomRecord(line_num=self.
|
84
|
+
return GedcomRecord(line_num=self.line,level=self.level,tag=self.tag,value=self.value)
|
71
85
|
|
72
86
|
def dump(self):
|
73
87
|
record_dump = f"Level: {self.level}, tag: {self.tag}, value: {self.value}, subRecords: {len(self._subRecords)}\n"
|
@@ -77,7 +91,7 @@ class GedcomRecord():
|
|
77
91
|
|
78
92
|
def describe(self,subRecords: bool = False):
|
79
93
|
level_str = '\t'* self.level
|
80
|
-
description = f"Line {self.
|
94
|
+
description = f"Line {self.line}: {level_str} Level: {self.level}, tag: '{self.tag}', xref={self.xref} value: '{self.value}', subRecords: {len(self._subRecords)}"
|
81
95
|
if subRecords:
|
82
96
|
for subRecord in self.subRecords():
|
83
97
|
description = description + '\n' + subRecord.describe(subRecords=True)
|
@@ -128,13 +142,25 @@ class GedcomRecord():
|
|
128
142
|
yield from self._flatten_subrecords(subrecord)
|
129
143
|
|
130
144
|
class Gedcom():
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
145
|
+
"""
|
146
|
+
Object representing a Genealogy in legacy GEDCOM 5.x / 7 format.
|
147
|
+
|
148
|
+
Parameters
|
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.
|
136
154
|
|
137
|
-
|
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
|
+
|
138
164
|
def __init__(self, records: Optional[List[GedcomRecord]] = None,filepath: str = None) -> None:
|
139
165
|
if filepath:
|
140
166
|
self.records = self._records_from_file(filepath)
|
@@ -142,34 +168,42 @@ class Gedcom():
|
|
142
168
|
self.records: List[GedcomRecord] = records if records else []
|
143
169
|
|
144
170
|
|
171
|
+
|
145
172
|
self._sources = []
|
146
173
|
self._repositories = []
|
147
174
|
self._individuals = []
|
148
175
|
self._families = []
|
149
176
|
self._objects = []
|
177
|
+
self._snotes = []
|
150
178
|
|
151
179
|
if self.records:
|
152
180
|
for record in self.records:
|
153
181
|
if record.tag == 'INDI':
|
154
|
-
|
182
|
+
|
155
183
|
self._individuals.append(record)
|
156
184
|
if record.tag == 'SOUR' and record.level == 0:
|
157
|
-
|
185
|
+
|
158
186
|
self._sources.append(record)
|
159
187
|
if record.tag == 'REPO' and record.level == 0:
|
160
|
-
record.
|
188
|
+
print(record.describe())
|
189
|
+
|
161
190
|
self._repositories.append(record)
|
162
191
|
if record.tag == 'FAM' and record.level == 0:
|
163
|
-
|
192
|
+
|
164
193
|
self._families.append(record)
|
165
194
|
if record.tag == 'OBJE' and record.level == 0:
|
166
|
-
|
195
|
+
|
167
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)
|
168
201
|
|
202
|
+
|
169
203
|
# =========================================================
|
170
204
|
# 2. PROPERTY ACCESSORS (GETTERS & SETTERS)
|
171
205
|
# =========================================================
|
172
|
-
|
206
|
+
|
173
207
|
@property
|
174
208
|
def json(self):
|
175
209
|
import json
|
@@ -216,6 +250,9 @@ class Gedcom():
|
|
216
250
|
|
217
251
|
@property
|
218
252
|
def repositories(self) -> List[GedcomRecord]:
|
253
|
+
"""
|
254
|
+
List of **REPO** records found in the Genealogy
|
255
|
+
"""
|
219
256
|
return self._repositories
|
220
257
|
|
221
258
|
@repositories.setter
|
@@ -254,18 +291,40 @@ class Gedcom():
|
|
254
291
|
raise ValueError("objects must be a list of GedcomRecord objects.")
|
255
292
|
self._objects = value
|
256
293
|
|
257
|
-
|
258
|
-
# 3. METHODS
|
259
|
-
# =========================================================
|
294
|
+
|
260
295
|
|
261
|
-
def write(self):
|
296
|
+
def write(self) -> bool:
|
262
297
|
"""
|
263
298
|
Method placeholder for writing GEDCOM files.
|
299
|
+
|
300
|
+
Raises
|
301
|
+
------
|
302
|
+
NotImplementedError
|
303
|
+
writing to legacy GEDCOM file is not currently implimented.
|
264
304
|
"""
|
265
305
|
raise NotImplementedError("Writing of GEDCOM files is not implemented.")
|
266
306
|
|
267
307
|
@staticmethod
|
268
308
|
def _records_from_file(filepath: str) -> List[GedcomRecord]:
|
309
|
+
def parse_gedcom7_line(line: str) -> Optional[Tuple[int, Optional[str], str, Optional[str], Optional[str]]]:
|
310
|
+
"""
|
311
|
+
Parse a GEDCOM 7 line into: level, xref_id (record), tag, value, xref_value (if value is an @X@)
|
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
|
269
328
|
extension = '.ged'
|
270
329
|
|
271
330
|
if not os.path.exists(filepath):
|
@@ -281,6 +340,7 @@ class Gedcom():
|
|
281
340
|
|
282
341
|
records = []
|
283
342
|
record_map = {0: None, 1: None, 2: None, 3: None, 4: None, 5: None}
|
343
|
+
|
284
344
|
for l, line in enumerate(lines):
|
285
345
|
if line.startswith(BOM):
|
286
346
|
line = line.lstrip(BOM)
|
@@ -296,18 +356,27 @@ class Gedcom():
|
|
296
356
|
if len(parts) == 3:
|
297
357
|
level, col2, col3 = parts
|
298
358
|
|
299
|
-
if col3 in Gedcom.
|
359
|
+
if col3 in Gedcom._top_level_tags:
|
300
360
|
tag = col3
|
301
361
|
value = col2
|
302
362
|
else:
|
303
363
|
tag = col2
|
304
364
|
value = col3
|
365
|
+
|
305
366
|
else:
|
306
367
|
level, tag = parts
|
307
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
|
+
|
308
375
|
level = int(level)
|
309
376
|
|
310
|
-
new_record = GedcomRecord(line_num=l + 1, level=level, tag=tag, value=value)
|
377
|
+
new_record = GedcomRecord(line_num=l + 1, level=level, tag=tag, xref=xref,value=value)
|
378
|
+
|
379
|
+
|
311
380
|
if level == 0:
|
312
381
|
records.append(new_record)
|
313
382
|
else:
|
@@ -316,6 +385,7 @@ class Gedcom():
|
|
316
385
|
record_map[int(level) - 1].addSubRecord(new_record)
|
317
386
|
record_map[int(level)] = new_record
|
318
387
|
|
388
|
+
|
319
389
|
return records if records else None
|
320
390
|
|
321
391
|
@staticmethod
|
@@ -330,17 +400,20 @@ class Gedcom():
|
|
330
400
|
Gedcom: An instance of the Gedcom class.
|
331
401
|
"""
|
332
402
|
records = Gedcom._records_from_file(filepath)
|
403
|
+
|
333
404
|
gedcom = Gedcom(records=records)
|
334
405
|
|
335
406
|
return gedcom
|
407
|
+
|
408
|
+
def merge_with_file(self, file_path: str) -> bool:
|
409
|
+
"""
|
410
|
+
Adds records from a valid (``*``.ged) file to the current Genealogy
|
411
|
+
|
412
|
+
Args:
|
413
|
+
filepath (str): The path to the GEDCOM file.
|
414
|
+
|
415
|
+
Returns:
|
416
|
+
bool: Indicates if merge was successful.
|
417
|
+
"""
|
418
|
+
return True
|
336
419
|
|
337
|
-
#
|
338
|
-
#import re
|
339
|
-
#filepath = r"C:\Users\User\Documents\PythonProjects\gedcomx\.ged_files\_DJC_ Nunda Cartwright Family.ged"
|
340
|
-
#with open(filepath, 'r', encoding='utf-8') as file:
|
341
|
-
# string = file.read()
|
342
|
-
#
|
343
|
-
#for match in re.finditer(line, string):
|
344
|
-
# data = match.groupdict()
|
345
|
-
# print(data)
|
346
|
-
#'''
|