gedcom-x 0.5__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.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 +111 -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.dist-info/RECORD +0 -37
- gedcomx/_Resource.py +0 -11
- {gedcom_x-0.5.dist-info → gedcom_x-0.5.2.dist-info}/WHEEL +0 -0
- {gedcom_x-0.5.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"
|
@@ -76,7 +90,8 @@ class GedcomRecord():
|
|
76
90
|
return record_dump
|
77
91
|
|
78
92
|
def describe(self,subRecords: bool = False):
|
79
|
-
|
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)}"
|
80
95
|
if subRecords:
|
81
96
|
for subRecord in self.subRecords():
|
82
97
|
description = description + '\n' + subRecord.describe(subRecords=True)
|
@@ -127,13 +142,25 @@ class GedcomRecord():
|
|
127
142
|
yield from self._flatten_subrecords(subrecord)
|
128
143
|
|
129
144
|
class Gedcom():
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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.
|
135
154
|
|
136
|
-
|
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
|
+
|
137
164
|
def __init__(self, records: Optional[List[GedcomRecord]] = None,filepath: str = None) -> None:
|
138
165
|
if filepath:
|
139
166
|
self.records = self._records_from_file(filepath)
|
@@ -141,34 +168,42 @@ class Gedcom():
|
|
141
168
|
self.records: List[GedcomRecord] = records if records else []
|
142
169
|
|
143
170
|
|
171
|
+
|
144
172
|
self._sources = []
|
145
173
|
self._repositories = []
|
146
174
|
self._individuals = []
|
147
175
|
self._families = []
|
148
176
|
self._objects = []
|
177
|
+
self._snotes = []
|
149
178
|
|
150
179
|
if self.records:
|
151
180
|
for record in self.records:
|
152
181
|
if record.tag == 'INDI':
|
153
|
-
|
182
|
+
|
154
183
|
self._individuals.append(record)
|
155
184
|
if record.tag == 'SOUR' and record.level == 0:
|
156
|
-
|
185
|
+
|
157
186
|
self._sources.append(record)
|
158
187
|
if record.tag == 'REPO' and record.level == 0:
|
159
|
-
record.
|
188
|
+
print(record.describe())
|
189
|
+
|
160
190
|
self._repositories.append(record)
|
161
191
|
if record.tag == 'FAM' and record.level == 0:
|
162
|
-
|
192
|
+
|
163
193
|
self._families.append(record)
|
164
194
|
if record.tag == 'OBJE' and record.level == 0:
|
165
|
-
|
195
|
+
|
166
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)
|
167
201
|
|
202
|
+
|
168
203
|
# =========================================================
|
169
204
|
# 2. PROPERTY ACCESSORS (GETTERS & SETTERS)
|
170
205
|
# =========================================================
|
171
|
-
|
206
|
+
|
172
207
|
@property
|
173
208
|
def json(self):
|
174
209
|
import json
|
@@ -215,6 +250,9 @@ class Gedcom():
|
|
215
250
|
|
216
251
|
@property
|
217
252
|
def repositories(self) -> List[GedcomRecord]:
|
253
|
+
"""
|
254
|
+
List of **REPO** records found in the Genealogy
|
255
|
+
"""
|
218
256
|
return self._repositories
|
219
257
|
|
220
258
|
@repositories.setter
|
@@ -253,18 +291,40 @@ class Gedcom():
|
|
253
291
|
raise ValueError("objects must be a list of GedcomRecord objects.")
|
254
292
|
self._objects = value
|
255
293
|
|
256
|
-
|
257
|
-
# 3. METHODS
|
258
|
-
# =========================================================
|
294
|
+
|
259
295
|
|
260
|
-
def write(self):
|
296
|
+
def write(self) -> bool:
|
261
297
|
"""
|
262
298
|
Method placeholder for writing GEDCOM files.
|
299
|
+
|
300
|
+
Raises
|
301
|
+
------
|
302
|
+
NotImplementedError
|
303
|
+
writing to legacy GEDCOM file is not currently implimented.
|
263
304
|
"""
|
264
305
|
raise NotImplementedError("Writing of GEDCOM files is not implemented.")
|
265
306
|
|
266
307
|
@staticmethod
|
267
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
|
268
328
|
extension = '.ged'
|
269
329
|
|
270
330
|
if not os.path.exists(filepath):
|
@@ -280,6 +340,7 @@ class Gedcom():
|
|
280
340
|
|
281
341
|
records = []
|
282
342
|
record_map = {0: None, 1: None, 2: None, 3: None, 4: None, 5: None}
|
343
|
+
|
283
344
|
for l, line in enumerate(lines):
|
284
345
|
if line.startswith(BOM):
|
285
346
|
line = line.lstrip(BOM)
|
@@ -295,18 +356,27 @@ class Gedcom():
|
|
295
356
|
if len(parts) == 3:
|
296
357
|
level, col2, col3 = parts
|
297
358
|
|
298
|
-
if col3 in Gedcom.
|
359
|
+
if col3 in Gedcom._top_level_tags:
|
299
360
|
tag = col3
|
300
361
|
value = col2
|
301
362
|
else:
|
302
363
|
tag = col2
|
303
364
|
value = col3
|
365
|
+
|
304
366
|
else:
|
305
367
|
level, tag = parts
|
306
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
|
+
|
307
375
|
level = int(level)
|
308
376
|
|
309
|
-
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
|
+
|
310
380
|
if level == 0:
|
311
381
|
records.append(new_record)
|
312
382
|
else:
|
@@ -315,6 +385,7 @@ class Gedcom():
|
|
315
385
|
record_map[int(level) - 1].addSubRecord(new_record)
|
316
386
|
record_map[int(level)] = new_record
|
317
387
|
|
388
|
+
|
318
389
|
return records if records else None
|
319
390
|
|
320
391
|
@staticmethod
|
@@ -329,17 +400,20 @@ class Gedcom():
|
|
329
400
|
Gedcom: An instance of the Gedcom class.
|
330
401
|
"""
|
331
402
|
records = Gedcom._records_from_file(filepath)
|
403
|
+
|
332
404
|
gedcom = Gedcom(records=records)
|
333
405
|
|
334
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
|
335
419
|
|
336
|
-
#
|
337
|
-
#import re
|
338
|
-
#filepath = r"C:\Users\User\Documents\PythonProjects\gedcomx\.ged_files\_DJC_ Nunda Cartwright Family.ged"
|
339
|
-
#with open(filepath, 'r', encoding='utf-8') as file:
|
340
|
-
# string = file.read()
|
341
|
-
#
|
342
|
-
#for match in re.finditer(line, string):
|
343
|
-
# data = match.groupdict()
|
344
|
-
# print(data)
|
345
|
-
#'''
|