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.
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 .URI import URI
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
- ConfidenceLevel.High: "The contributor has a high degree of confidence that the assertion is true.",
22
- ConfidenceLevel.Medium: "The contributor has a medium degree of confidence that the assertion is true.",
23
- ConfidenceLevel.Low: "The contributor has a low degree of confidence that the assertion is true."
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
- return descriptions.get(self, "No description available.")
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[URI] = None,
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[URI] = None,
50
- max_note_count: int = 20) -> None:
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 self._id_generator()
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 = max_note_count
62
- self._uri = uri if uri else URI(fragment=id)
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
- def _as_dict_(self):
85
- return {
86
- 'id':self.id,
87
- 'lang':self.lang,
88
- 'sources': [source._prop_dict() for source in self.sources] if self.sources else None,
89
- 'analysis': self.analysis._uri if self.analysis else None,
90
- 'notes':"Add notes here",
91
- 'confidence':self.confidence
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
- def _serialize(value):
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._uri if self.analysis else None,
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
- # Serialize and exclude None values
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
- def _prop_dict(self):
24
+ @property
25
+ def _as_dict_(self):
18
26
  return {'original': self.orginal,
19
27
  'formal': self.formal}
20
28
 
21
- # Date
22
- Date._from_json_ = classmethod(lambda cls, data: Date(
23
- original=data.get('original'),
24
- formal=data.get('formal')
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
- Date._to_dict_ = lambda self: {
28
- 'original': self.orginal,
29
- 'formal': self.formal}
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 gedcomx.Attribution import Attribution
5
- from gedcomx.Conclusion import ConfidenceLevel
6
- from gedcomx.Note import Note
7
- from gedcomx.SourceReference import SourceReference
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 | None, lang: str | None, sources: SourceReference | None, analysis: URI | None, notes: Note | None, confidence: ConfidenceLevel | None, attribution: Attribution | None,
37
- type: Optional[DocumentType],
38
- extracted: Optional[bool], # Default to False
39
- textType: Optional[TextType],
40
- text: str,
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 .URI import URI
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[URI] = None,
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: URI = None,
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[URI] = None,
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)
@@ -1,11 +1,11 @@
1
1
  from typing import Optional
2
2
 
3
3
  from .Attribution import Attribution
4
- from .URI import URI
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: URI, attribution: Optional[Attribution]) -> None:
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 .URI import URI
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: URI = None,
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 = []) -> None: #qualifiers: Optional[List[FactQualifier]] = []) -> None:
405
- super().__init__(id, lang, sources, analysis, notes, confidence, attribution)
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.qualifiers = qualifiers
417
+ self._qualifiers = qualifiers if qualifiers else []
411
418
 
419
+
412
420
  @property
413
- def _as_dict_(self):
414
- def _serialize(value):
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
- fact_fields = {
434
+ fact_dict.update( {
428
435
  'type': self.type.value if self.type else None,
429
- 'date': self.date._prop_dict() if self.date else None,
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