gedcom-x 0.5.1__py3-none-any.whl → 0.5.5__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.5.dist-info}/METADATA +1 -1
- gedcom_x-0.5.5.dist-info/RECORD +43 -0
- gedcomx/Address.py +42 -11
- gedcomx/Agent.py +136 -23
- gedcomx/Attribution.py +39 -91
- gedcomx/Conclusion.py +132 -53
- gedcomx/Coverage.py +10 -0
- gedcomx/Date.py +47 -11
- gedcomx/Document.py +43 -12
- gedcomx/Event.py +24 -5
- gedcomx/EvidenceReference.py +2 -2
- gedcomx/Exceptions.py +16 -0
- gedcomx/Fact.py +73 -50
- gedcomx/Gedcom.py +40 -333
- gedcomx/Gedcom5x.py +558 -0
- gedcomx/GedcomX.py +439 -194
- gedcomx/Gender.py +27 -8
- gedcomx/Group.py +3 -3
- gedcomx/Identifier.py +192 -55
- gedcomx/Logging.py +19 -0
- gedcomx/Mutations.py +228 -0
- gedcomx/Name.py +73 -38
- gedcomx/Note.py +5 -4
- gedcomx/OnlineAccount.py +2 -2
- gedcomx/Person.py +106 -92
- gedcomx/PlaceDescription.py +39 -16
- gedcomx/PlaceReference.py +14 -15
- gedcomx/Relationship.py +35 -56
- gedcomx/Resource.py +75 -0
- gedcomx/Serialization.py +394 -30
- gedcomx/SourceCitation.py +6 -1
- gedcomx/SourceDescription.py +89 -75
- gedcomx/SourceReference.py +33 -88
- gedcomx/Subject.py +12 -10
- gedcomx/TextValue.py +2 -1
- gedcomx/Translation.py +219 -0
- gedcomx/URI.py +96 -61
- gedcomx/Zip.py +1 -0
- gedcomx/__init__.py +11 -3
- 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.5.dist-info}/WHEEL +0 -0
- {gedcom_x-0.5.1.dist-info → gedcom_x-0.5.5.dist-info}/top_level.txt +0 -0
gedcomx/Conclusion.py
CHANGED
@@ -5,26 +5,113 @@ import warnings
|
|
5
5
|
from typing import List, Optional
|
6
6
|
|
7
7
|
from .Attribution import Attribution
|
8
|
+
#from .Document import Document
|
8
9
|
from .Note import Note
|
9
10
|
from .Qualifier import Qualifier
|
11
|
+
from .Serialization import Serialization
|
10
12
|
from .SourceReference import SourceReference
|
11
|
-
from .
|
13
|
+
from .Resource import Resource, URI
|
14
|
+
from .Extensions.rs10.rsLink import _rsLinkList, rsLink
|
15
|
+
|
16
|
+
from collections.abc import Sized
|
12
17
|
|
13
18
|
class ConfidenceLevel(Qualifier):
|
14
19
|
High = "http://gedcomx.org/High"
|
15
20
|
Medium = "http://gedcomx.org/Medium"
|
16
21
|
Low = "http://gedcomx.org/Low"
|
17
|
-
|
22
|
+
|
23
|
+
_NAME_TO_URI = {
|
24
|
+
"high": High,
|
25
|
+
"medium": Medium,
|
26
|
+
"low": Low,
|
27
|
+
}
|
28
|
+
|
29
|
+
@classmethod
|
30
|
+
def _from_json_(cls, data):
|
31
|
+
"""
|
32
|
+
Accepts:
|
33
|
+
- "High" | "Medium" | "Low"
|
34
|
+
- "http://gedcomx.org/High" | ".../Medium" | ".../Low"
|
35
|
+
- {"type": "..."} or {"value": "..."} or {"confidence": "..."} or {"level": "..."} or {"uri": "..."}
|
36
|
+
- existing ConfidenceLevel instance
|
37
|
+
Returns:
|
38
|
+
ConfidenceLevel instance with .value set to the canonical URI.
|
39
|
+
"""
|
40
|
+
if data is None:
|
41
|
+
return None
|
42
|
+
|
43
|
+
if isinstance(data, cls):
|
44
|
+
return data
|
45
|
+
|
46
|
+
# Extract token from dicts or use the raw scalar
|
47
|
+
if isinstance(data, dict):
|
48
|
+
token = (
|
49
|
+
data.get("confidence")
|
50
|
+
or data.get("type")
|
51
|
+
or data.get("value")
|
52
|
+
or data.get("level")
|
53
|
+
or data.get("uri")
|
54
|
+
)
|
55
|
+
else:
|
56
|
+
token = data
|
57
|
+
|
58
|
+
if token is None:
|
59
|
+
return None
|
60
|
+
|
61
|
+
token_str = str(token).strip()
|
62
|
+
|
63
|
+
# Normalize to canonical URI
|
64
|
+
if token_str.lower() in cls._NAME_TO_URI:
|
65
|
+
uri = cls._NAME_TO_URI[token_str.lower()]
|
66
|
+
elif token_str in (cls.High, cls.Medium, cls.Low):
|
67
|
+
uri = token_str
|
68
|
+
else:
|
69
|
+
raise ValueError(f"Unknown ConfidenceLevel: {token!r}")
|
70
|
+
|
71
|
+
# Create a ConfidenceLevel instance without invoking Qualifier.__init__
|
72
|
+
obj = cls.__new__(cls)
|
73
|
+
# store the canonical URI on the instance; used by description and (optionally) serialization
|
74
|
+
obj.value = uri
|
75
|
+
return obj
|
76
|
+
|
18
77
|
@property
|
19
78
|
def description(self):
|
20
79
|
descriptions = {
|
21
|
-
|
22
|
-
|
23
|
-
|
80
|
+
self.High: "The contributor has a high degree of confidence that the assertion is true.",
|
81
|
+
self.Medium: "The contributor has a medium degree of confidence that the assertion is true.",
|
82
|
+
self.Low: "The contributor has a low degree of confidence that the assertion is true."
|
24
83
|
}
|
25
|
-
|
84
|
+
# Works whether the instance holds .value or (edge-case) if `self` is compared directly
|
85
|
+
key = getattr(self, "value", self)
|
86
|
+
return descriptions.get(key, "No description available.")
|
87
|
+
|
26
88
|
|
27
89
|
class Conclusion:
|
90
|
+
"""
|
91
|
+
Represents a conclusion in the GEDCOM X conceptual model. A conclusion is a
|
92
|
+
genealogical assertion about a person, relationship, or event, derived from
|
93
|
+
one or more sources, with optional supporting metadata such as confidence,
|
94
|
+
attribution, and notes.
|
95
|
+
|
96
|
+
Args:
|
97
|
+
id (str, optional): A unique identifier for the conclusion. If not provided,
|
98
|
+
a UUID-based identifier will be automatically generated.
|
99
|
+
lang (str, optional): The language code of the conclusion. Defaults to 'en'.
|
100
|
+
sources (list[SourceReference], optional): A list of source references that
|
101
|
+
support the conclusion.
|
102
|
+
analysis (Document | Resource, optional): A reference to an analysis document
|
103
|
+
or resource that supports the conclusion.
|
104
|
+
notes (list[Note], optional): A list of notes providing additional context.
|
105
|
+
Defaults to an empty list.
|
106
|
+
confidence (ConfidenceLevel, optional): The contributor's confidence in the
|
107
|
+
conclusion (High, Medium, or Low).
|
108
|
+
attribution (Attribution, optional): Information about who contributed the
|
109
|
+
conclusion and when.
|
110
|
+
uri (Resource, optional): A URI reference for the conclusion. Defaults to a
|
111
|
+
URI with the fragment set to the `id`.
|
112
|
+
links (_LinkList, optional): A list of links associated with the conclusion.
|
113
|
+
Defaults to an empty `_LinkList`.
|
114
|
+
"""
|
28
115
|
identifier = 'http://gedcomx.org/v1/Conclusion'
|
29
116
|
version = 'http://gedcomx.org/conceptual-model/v1'
|
30
117
|
|
@@ -39,30 +126,32 @@ class Conclusion:
|
|
39
126
|
return short_uuid
|
40
127
|
|
41
128
|
def __init__(self,
|
42
|
-
id: Optional[str],
|
129
|
+
id: Optional[str] = None,
|
43
130
|
lang: Optional[str] = 'en',
|
44
|
-
sources: Optional[List[SourceReference]] =
|
45
|
-
analysis: Optional[
|
46
|
-
notes: Optional[List[Note]] =
|
131
|
+
sources: Optional[List[SourceReference]] = None,
|
132
|
+
analysis: Optional[object | Resource] = None,
|
133
|
+
notes: Optional[List[Note]] = None,
|
47
134
|
confidence: Optional[ConfidenceLevel] = None,
|
48
135
|
attribution: Optional[Attribution] = None,
|
49
|
-
uri: Optional[
|
50
|
-
|
136
|
+
uri: Optional[Resource] = None,
|
137
|
+
_max_note_count: int = 20,
|
138
|
+
links: Optional[_rsLinkList] = None) -> None:
|
51
139
|
|
52
140
|
self._id_generator = Conclusion.default_id_generator
|
53
141
|
|
54
|
-
self.id = id if id else
|
142
|
+
self.id = id if id else None
|
55
143
|
self.lang = lang
|
56
|
-
self.sources = sources
|
144
|
+
self.sources = sources if sources else []
|
57
145
|
self.analysis = analysis
|
58
|
-
self.notes = notes
|
146
|
+
self.notes = notes if notes else []
|
59
147
|
self.confidence = confidence
|
60
148
|
self.attribution = attribution
|
61
|
-
self.max_note_count =
|
62
|
-
self.
|
149
|
+
self.max_note_count = _max_note_count
|
150
|
+
self.uri = uri if uri else URI(fragment=id if id else self.id)
|
151
|
+
self.links = links if links else _rsLinkList() #NOTE This is not in specification, following FS format
|
63
152
|
|
64
153
|
def add_note(self,note_to_add: Note):
|
65
|
-
if len(self.notes) >= self.max_note_count:
|
154
|
+
if self.notes and len(self.notes) >= self.max_note_count:
|
66
155
|
warnings.warn(f"Max not count of {self.max_note_count} reached for id: {self.id}")
|
67
156
|
return False
|
68
157
|
if note_to_add and isinstance(note_to_add,Note):
|
@@ -79,48 +168,38 @@ class Conclusion:
|
|
79
168
|
self.sources.append(source_to_add)
|
80
169
|
else:
|
81
170
|
raise ValueError()
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
171
|
+
|
172
|
+
def add_link(self,link: rsLink) -> bool:
|
173
|
+
"""
|
174
|
+
Adds a link to the Conclusion link list.
|
175
|
+
|
176
|
+
Args:
|
177
|
+
link (rsLink): The link to be added.
|
178
|
+
|
179
|
+
Returns:
|
180
|
+
bool: The return value. True for success, False otherwise.
|
181
|
+
|
182
|
+
Note: Duplicate checking not impimented at this level
|
183
|
+
"""
|
184
|
+
if link and isinstance(link,rsLink):
|
185
|
+
self.links.add(link)
|
186
|
+
return True
|
187
|
+
return False
|
94
188
|
|
95
189
|
@property
|
96
190
|
def _as_dict_(self):
|
97
|
-
|
98
|
-
if isinstance(value, (str, int, float, bool, type(None))):
|
99
|
-
return value
|
100
|
-
elif isinstance(value, dict):
|
101
|
-
return {k: _serialize(v) for k, v in value.items()}
|
102
|
-
elif isinstance(value, (list, tuple, set)):
|
103
|
-
return [_serialize(v) for v in value]
|
104
|
-
elif hasattr(value, "_as_dict_"):
|
105
|
-
return value._as_dict_
|
106
|
-
else:
|
107
|
-
return str(value) # fallback for unknown objects
|
108
|
-
|
109
|
-
# Only add Relationship-specific fields
|
110
|
-
conclusion_fields = {
|
191
|
+
type_as_dict = {
|
111
192
|
'id':self.id,
|
112
193
|
'lang':self.lang,
|
113
|
-
'sources': [source for source in self.sources] if self.sources else None,
|
114
|
-
'analysis': self.analysis
|
194
|
+
'sources': [source._as_dict_ for source in self.sources] if self.sources else None,
|
195
|
+
'analysis': self.analysis if self.analysis else None,
|
115
196
|
'notes': [note for note in self.notes] if self.notes else None,
|
116
|
-
'confidence':self.confidence
|
197
|
+
'confidence':self.confidence,
|
198
|
+
'attribution':self.attribution,
|
199
|
+
'links':self.links._as_dict_ if self.links else None
|
117
200
|
}
|
118
|
-
|
119
|
-
|
120
|
-
for key, value in conclusion_fields.items():
|
121
|
-
if value is not None:
|
122
|
-
conclusion_fields[key] = _serialize(value)
|
123
|
-
return conclusion_fields
|
201
|
+
|
202
|
+
return Serialization.serialize_dict(type_as_dict)
|
124
203
|
|
125
204
|
def __eq__(self, other):
|
126
205
|
if not isinstance(other, self.__class__):
|
gedcomx/Coverage.py
CHANGED
@@ -2,6 +2,7 @@ from typing import Optional
|
|
2
2
|
|
3
3
|
from .Date import Date
|
4
4
|
from .PlaceReference import PlaceReference
|
5
|
+
from .Serialization import Serialization
|
5
6
|
|
6
7
|
class Coverage:
|
7
8
|
identifier = 'http://gedcomx.org/v1/Coverage'
|
@@ -13,6 +14,15 @@ class Coverage:
|
|
13
14
|
|
14
15
|
# ...existing code...
|
15
16
|
|
17
|
+
@property
|
18
|
+
def _as_dict_(self):
|
19
|
+
type_as_dict = {
|
20
|
+
'spatial': self.spatial if self.spatial else None,
|
21
|
+
'temporal': self. temporal if self.temporal else None
|
22
|
+
}
|
23
|
+
|
24
|
+
return Serialization.serialize_dict(type_as_dict)
|
25
|
+
|
16
26
|
@classmethod
|
17
27
|
def _from_json_(cls, data: dict):
|
18
28
|
"""
|
gedcomx/Date.py
CHANGED
@@ -1,29 +1,65 @@
|
|
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
|
+
@property
|
25
|
+
def _as_dict_(self):
|
18
26
|
return {'original': self.orginal,
|
19
27
|
'formal': self.formal}
|
20
28
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
29
|
+
@classmethod
|
30
|
+
def _from_json_(obj,data):
|
31
|
+
original = data.get('original',None)
|
32
|
+
formal = data.get('formal',None)
|
33
|
+
|
34
|
+
return Date(original=original,formal=formal)
|
35
|
+
|
36
|
+
|
37
|
+
|
38
|
+
def date_to_timestamp(date_str: str, assume_utc_if_naive: bool = True, print_definition: bool = True):
|
39
|
+
"""
|
40
|
+
Convert a date string of various formats into a Unix timestamp.
|
26
41
|
|
27
|
-
|
28
|
-
|
29
|
-
|
42
|
+
A "timestamp" refers to an instance of time, including values for year,
|
43
|
+
month, date, hour, minute, second, and timezone.
|
44
|
+
"""
|
45
|
+
# Handle year ranges like "1894-1912" → pick first year
|
46
|
+
if "-" in date_str and date_str.count("-") == 1 and all(part.isdigit() for part in date_str.split("-")):
|
47
|
+
date_str = date_str.split("-")[0].strip()
|
48
|
+
|
49
|
+
# Parse date
|
50
|
+
dt = parser.parse(date_str)
|
51
|
+
|
52
|
+
# Ensure timezone awareness
|
53
|
+
if dt.tzinfo is None:
|
54
|
+
dt = dt.replace(tzinfo=timezone.utc if assume_utc_if_naive else datetime.now().astimezone().tzinfo)
|
55
|
+
|
56
|
+
# Normalize to UTC and compute timestamp
|
57
|
+
dt_utc = dt.astimezone(timezone.utc)
|
58
|
+
epoch = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
59
|
+
ts = (dt_utc - epoch).total_seconds()
|
60
|
+
|
61
|
+
# Create ISO 8601 string with full date/time/timezone
|
62
|
+
full_timestamp_str = dt_utc.replace(microsecond=0).isoformat()
|
63
|
+
|
64
|
+
|
65
|
+
return ts, full_timestamp_str
|
gedcomx/Document.py
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
from enum import Enum
|
2
|
-
from typing import Optional
|
2
|
+
from typing import Optional, List
|
3
3
|
|
4
|
-
from
|
5
|
-
from
|
6
|
-
from
|
7
|
-
from
|
8
|
-
from gedcomx.URI import URI
|
4
|
+
from .Attribution import Attribution
|
5
|
+
from .Note import Note
|
6
|
+
from .SourceReference import SourceReference
|
7
|
+
from .Resource import Resource
|
9
8
|
|
10
9
|
from .Conclusion import Conclusion
|
10
|
+
from .Serialization import Serialization
|
11
11
|
|
12
12
|
class DocumentType(Enum):
|
13
13
|
Abstract = "http://gedcomx.org/Abstract"
|
@@ -33,10 +33,41 @@ 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
|
53
|
+
|
54
|
+
@property
|
55
|
+
def _as_dict(self):
|
56
|
+
type_as_dict = super()._as_dict_
|
57
|
+
if self.type:
|
58
|
+
type_as_dict['type'] = self.type.value
|
59
|
+
if self.extracted is not None:
|
60
|
+
type_as_dict['extracted'] = self.extracted
|
61
|
+
if self.textType:
|
62
|
+
type_as_dict['textType'] = self.textType.value
|
63
|
+
if self.text:
|
64
|
+
type_as_dict['text'] = self.text
|
65
|
+
return Serialization.serialize_dict(type_as_dict)
|
66
|
+
|
67
|
+
@classmethod
|
68
|
+
def _from_json_(cls, data: dict):
|
69
|
+
"""
|
70
|
+
Create a Person instance from a JSON-dict (already parsed).
|
71
|
+
"""
|
72
|
+
type_as_dict = Serialization.get_class_fields('Document')
|
73
|
+
return Serialization.deserialize(data, type_as_dict)
|
gedcomx/Event.py
CHANGED
@@ -9,9 +9,10 @@ from .Conclusion import Conclusion, ConfidenceLevel
|
|
9
9
|
from .Date import Date
|
10
10
|
from .Note import Note
|
11
11
|
from .PlaceReference import PlaceReference
|
12
|
+
from .Serialization import Serialization
|
12
13
|
from .SourceReference import SourceReference
|
13
14
|
from .Subject import Subject
|
14
|
-
from .
|
15
|
+
from .Resource import Resource
|
15
16
|
|
16
17
|
class EventRoleType(Enum):
|
17
18
|
Principal = "http://gedcomx.org/Principal"
|
@@ -37,11 +38,11 @@ class EventRole(Conclusion):
|
|
37
38
|
id: Optional[str] = None,
|
38
39
|
lang: Optional[str] = 'en',
|
39
40
|
sources: Optional[List[SourceReference]] = [],
|
40
|
-
analysis: Optional[
|
41
|
+
analysis: Optional[Resource] = None,
|
41
42
|
notes: Optional[List[Note]] = [],
|
42
43
|
confidence: Optional[ConfidenceLevel] = None,
|
43
44
|
attribution: Optional[Attribution] = None,
|
44
|
-
person:
|
45
|
+
person: Resource = None,
|
45
46
|
type: Optional[EventRoleType] = None,
|
46
47
|
details: Optional[str] = None) -> None:
|
47
48
|
super().__init__(id, lang, sources, analysis, notes, confidence, attribution)
|
@@ -84,6 +85,7 @@ class EventType(Enum):
|
|
84
85
|
Naturalization = "http://gedcomx.org/Naturalization"
|
85
86
|
Ordination = "http://gedcomx.org/Ordination"
|
86
87
|
Retirement = "http://gedcomx.org/Retirement"
|
88
|
+
MarriageSettlment = 'https://gedcom.io/terms/v7/MARS'
|
87
89
|
|
88
90
|
@property
|
89
91
|
def description(self):
|
@@ -180,7 +182,7 @@ class Event(Subject):
|
|
180
182
|
id: Optional[str] = None,
|
181
183
|
lang: Optional[str] = 'en',
|
182
184
|
sources: Optional[List[SourceReference]] = [],
|
183
|
-
analysis: Optional[
|
185
|
+
analysis: Optional[Resource] = None,
|
184
186
|
notes: Optional[List[Note]] = [],
|
185
187
|
confidence: Optional[ConfidenceLevel] = None,
|
186
188
|
attribution: Optional[Attribution] = None,
|
@@ -192,4 +194,21 @@ class Event(Subject):
|
|
192
194
|
date: Optional[Date] = None,
|
193
195
|
place: Optional[PlaceReference] = None,
|
194
196
|
roles: Optional[List[EventRole]] = []) -> None:
|
195
|
-
super().__init__(id, lang, sources, analysis, notes, confidence, attribution, extracted, evidence, media, identifiers)
|
197
|
+
super().__init__(id, lang, sources, analysis, notes, confidence, attribution, extracted, evidence, media, identifiers)
|
198
|
+
|
199
|
+
self.type = type if type and isinstance(type, EventType) else None
|
200
|
+
self.date = date if date and isinstance(date, Date) else None
|
201
|
+
self.place = place if place and isinstance(place, PlaceReference) else None
|
202
|
+
self.roles = roles if roles and isinstance(roles, list) else []
|
203
|
+
|
204
|
+
@property
|
205
|
+
def _as_dict_(self):
|
206
|
+
raise NotImplementedError("Not implemented yet")
|
207
|
+
|
208
|
+
@classmethod
|
209
|
+
def _from_json_(cls, data: dict):
|
210
|
+
"""
|
211
|
+
Create a Person instance from a JSON-dict (already parsed).
|
212
|
+
"""
|
213
|
+
type_as_dict = Serialization.get_class_fields('Event')
|
214
|
+
return Serialization.deserialize(data, type_as_dict)
|
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,16 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class GedcomXError(Exception):
|
4
|
+
"""Base for all app-specific errors."""
|
5
|
+
|
6
|
+
class GedcomClassAttributeError(GedcomXError):
|
7
|
+
def __init__(self, *args: object) -> None:
|
8
|
+
msg = f"This class need more information to be created: {args}"
|
9
|
+
super().__init__(msg)
|
10
|
+
|
11
|
+
|
12
|
+
class TagConversionError(GedcomXError):
|
13
|
+
def __init__(self, record,levelstack):
|
14
|
+
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__}"
|
15
|
+
super().__init__(msg)
|
16
|
+
|
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 .Extensions.rs10.rsLink import rsLink, _rsLinkList
|
26
|
+
|
21
27
|
|
22
28
|
class FactType(Enum):
|
23
29
|
# Person Fact Types
|
@@ -390,73 +396,90 @@ 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
|
-
confidence: ConfidenceLevel = None,
|
399
|
-
attribution: Attribution = None,
|
400
|
-
type: FactType = None,
|
404
|
+
confidence: Optional[ConfidenceLevel] = None,
|
405
|
+
attribution: Optional[Attribution] = None,
|
406
|
+
type: Optional[FactType] = None,
|
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[_rsLinkList] = 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
|
-
'date': self.date.
|
436
|
+
'date': self.date._as_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
|
-
}
|
440
|
+
})
|
441
|
+
|
442
|
+
return Serialization.serialize_dict(fact_dict)
|
443
|
+
|
444
|
+
@classmethod
|
445
|
+
def _from_json_(cls, data: Dict[str, Any]) -> 'Fact':
|
446
|
+
|
447
|
+
# Extract fields, no trailing commas!
|
448
|
+
id_ = data.get('id')
|
449
|
+
lang = data.get('lang', 'en')
|
450
|
+
sources = [SourceReference._from_json_(s) for s in data.get('sources',[])]
|
451
|
+
analysis = (Resource._from_json_(data['analysis'])
|
452
|
+
if data.get('analysis') else None)
|
453
|
+
notes = [Note._from_json_(n) for n in data.get('notes',[])]
|
454
|
+
confidence = (ConfidenceLevel._from_json_(data['confidence'])
|
455
|
+
if data.get('confidence') else None)
|
456
|
+
attribution = (Attribution._from_json_(data['attribution']) if data.get('attribution') else None)
|
457
|
+
fact_type = (FactType.from_value(data['type'])
|
458
|
+
if data.get('type') else None)
|
459
|
+
date = (Date._from_json_(data['date'])
|
460
|
+
if data.get('date') else None)
|
461
|
+
place = (PlaceReference._from_json_(data['place'])
|
462
|
+
if data.get('place') else None)
|
463
|
+
value = data.get('value')
|
464
|
+
qualifiers = [Qualifier._from_json_(q) for q in data.get('qualifiers', [])]
|
465
|
+
links = _rsLinkList._from_json_(data.get('links')) if data.get('links') else None
|
466
|
+
|
467
|
+
return cls(
|
468
|
+
id=id_,
|
469
|
+
lang=lang,
|
470
|
+
sources=sources,
|
471
|
+
analysis=analysis,
|
472
|
+
notes=notes,
|
473
|
+
confidence=confidence,
|
474
|
+
attribution=attribution,
|
475
|
+
type=fact_type,
|
476
|
+
date=date,
|
477
|
+
place=place,
|
478
|
+
value=value,
|
479
|
+
qualifiers=qualifiers,
|
480
|
+
links=links
|
481
|
+
)
|
434
482
|
|
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)
|
439
483
|
|
440
|
-
|
441
|
-
return fact_fields
|
442
484
|
|
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
485
|
|