gedcom-x 0.5.2__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/Mutations.py ADDED
@@ -0,0 +1,228 @@
1
+ from .Gedcom5x import GedcomRecord
2
+ from .Fact import Fact, FactType
3
+ from .Event import Event, EventType
4
+
5
+ fact_event_table = {
6
+ # Person Fact / Event Types
7
+ "ADOP": {
8
+ "Fact": FactType.AdoptiveParent,
9
+ "Event": EventType.Adoption,
10
+ },
11
+ "CHR": {
12
+ "Fact": FactType.AdultChristening,
13
+ "Event": EventType.AdultChristening,
14
+ },
15
+ "EVEN": {
16
+ "Fact": FactType.Amnesty,
17
+ # no Event
18
+ },
19
+ "BAPM": {
20
+ "Fact": FactType.Baptism,
21
+ "Event": EventType.Baptism,
22
+ },
23
+ "BARM": {
24
+ "Fact": FactType.BarMitzvah,
25
+ "Event": EventType.BarMitzvah,
26
+ },
27
+ "BASM": {
28
+ "Fact": FactType.BatMitzvah,
29
+ "Event": EventType.BatMitzvah,
30
+ },
31
+ "BIRT": {
32
+ "Fact": FactType.Birth,
33
+ "Event": EventType.Birth,
34
+ },
35
+ "BIRT, CHR": {
36
+ "Fact": FactType.Birth,
37
+ "Event": EventType.Birth,
38
+ },
39
+ "BLES": {
40
+ "Fact": FactType.Blessing,
41
+ "Event": EventType.Blessing,
42
+ },
43
+ "BURI": {
44
+ "Fact": FactType.Burial,
45
+ "Event": EventType.Burial,
46
+ },
47
+ "CAST": {
48
+ "Fact": FactType.Caste,
49
+ # no Event
50
+ },
51
+ "CENS": {
52
+ "Fact": FactType.Census,
53
+ "Event": EventType.Census,
54
+ },
55
+ "CIRC": {
56
+ "Fact": FactType.Circumcision,
57
+ "Event": EventType.Circumcision,
58
+ },
59
+ "CONF": {
60
+ "Fact": FactType.Confirmation,
61
+ "Event": EventType.Confirmation,
62
+ },
63
+ "CREM": {
64
+ "Fact": FactType.Cremation,
65
+ "Event": EventType.Cremation,
66
+ },
67
+ "DEAT": {
68
+ "Fact": FactType.Death,
69
+ "Event": EventType.Death,
70
+ },
71
+ "EDUC": {
72
+ "Fact": FactType.Education,
73
+ "Event": EventType.Education,
74
+ },
75
+ "EMIG": {
76
+ "Fact": FactType.Emigration,
77
+ "Event": EventType.Emigration,
78
+ },
79
+ "FCOM": {
80
+ "Fact": FactType.FirstCommunion,
81
+ "Event": EventType.FirstCommunion,
82
+ },
83
+ "GRAD": {
84
+ "Fact": FactType.Graduation,
85
+ # no Event
86
+ },
87
+ "IMMI": {
88
+ "Fact": FactType.Immigration,
89
+ "Event": EventType.Immigration,
90
+ },
91
+ "MIL": {
92
+ "Fact": FactType.MilitaryService,
93
+ # no Event
94
+ },
95
+ "NATI": {
96
+ "Fact": FactType.Nationality,
97
+ # no Event
98
+ },
99
+ "NATU": {
100
+ "Fact": FactType.Naturalization,
101
+ "Event": EventType.Naturalization,
102
+ },
103
+ "OCCU": {
104
+ "Fact": FactType.Occupation,
105
+ # no Event
106
+ },
107
+ "ORDN": {
108
+ "Fact": FactType.Ordination,
109
+ "Event": EventType.Ordination,
110
+ },
111
+ "DSCR": {
112
+ "Fact": FactType.PhysicalDescription,
113
+ # no Event
114
+ },
115
+ "PROB": {
116
+ "Fact": FactType.Probate,
117
+ # no Event
118
+ },
119
+ "PROP": {
120
+ "Fact": FactType.Property,
121
+ # no Event
122
+ },
123
+ "RELI": {
124
+ "Fact": FactType.Religion,
125
+ # no Event
126
+ },
127
+ "RESI": {
128
+ "Fact": FactType.Residence,
129
+ # no Event
130
+ },
131
+ "WILL": {
132
+ "Fact": FactType.Will,
133
+ # no Event
134
+ },
135
+
136
+ # Couple Relationship Fact / Event Types
137
+ "ANUL": {
138
+ "Fact": FactType.Annulment,
139
+ "Event": EventType.Annulment,
140
+ },
141
+ "DIV": {
142
+ "Fact": FactType.Divorce,
143
+ "Event": EventType.Divorce,
144
+ },
145
+ "DIVF": {
146
+ "Fact": FactType.DivorceFiling,
147
+ "Event": EventType.DivorceFiling,
148
+ },
149
+ "ENGA": {
150
+ "Fact": FactType.Engagement,
151
+ "Event": EventType.Engagement,
152
+ },
153
+ "MARR": {
154
+ "Fact": FactType.Marriage,
155
+ "Event": EventType.Marriage,
156
+ },
157
+ "MARB": {
158
+ "Fact": FactType.MarriageBanns,
159
+ # no Event
160
+ },
161
+ "MARC": {
162
+ "Fact": FactType.MarriageContract,
163
+ # no Event
164
+ },
165
+ "MARL": {
166
+ "Fact": FactType.MarriageLicense,
167
+ # no Event
168
+ },
169
+ "MARS":{
170
+ "Fact":EventType.MarriageSettlment
171
+
172
+ },
173
+ "SEPA": {
174
+ "Fact": FactType.Separation,
175
+ # no Event
176
+ },
177
+
178
+ }
179
+
180
+ class GedcomXObject:
181
+ def __init__(self,record: GedcomRecord | None = None) -> None:
182
+ self.created_with_tag: str = record.tag if record and isinstance(record, GedcomRecord) else None
183
+ self.created_at_level: int = record.level if record and isinstance(record, GedcomRecord) else None
184
+ self.created_at_line_number: int = record.line_number if record and isinstance(record, GedcomRecord) else None
185
+
186
+ class GedcomXSourceOrDocument(GedcomXObject):
187
+ def __init__(self,record: GedcomRecord | None = None) -> None:
188
+ super().__init__(record)
189
+ self.title: str = None
190
+ self.citation: str = None
191
+ self.page: str = None
192
+ self.contributor: str = None
193
+ self.publisher: str = None
194
+ self.rights: str = None
195
+ self.url: str = None
196
+ self.medium: str = None
197
+ self.type: str = None
198
+ self.format: str = None
199
+ self.created: str = None
200
+ self.modified: str = None
201
+ self.language: str = None
202
+ self.relation: str = None
203
+ self.identifier: str = None
204
+ self.description: str = None
205
+
206
+ class GedcomXEventOrFact(GedcomXObject):
207
+ def __new__(cls,record: GedcomRecord | None = None, object_stack: dict | None = None) -> object:
208
+ super().__init__(record)
209
+ if record.tag in fact_event_table.keys():
210
+
211
+ if 'Fact' in fact_event_table[record.tag].keys():
212
+ obj = Fact(type=fact_event_table[record.tag]['Fact'])
213
+ return obj
214
+ elif 'Event' in fact_event_table[record.tag].keys():
215
+ obj = Event(type=fact_event_table[record.tag]['Fact'])
216
+ else:
217
+ raise ValueError
218
+ else:
219
+ raise ValueError(f"{record.tag} not found in map")
220
+
221
+ class GedcomXRelationshipBuilder(GedcomXObject):
222
+ def __new__(cls,record: GedcomRecord | None = None, object_stack: dict | None = None) -> object:
223
+ last_relationship = object_stack.get('lastrelationship',None)
224
+ last_relationship_data = object_stack.get('lastrelationshipdata',None)
225
+ if not isinstance(last_relationship_data,dict):
226
+ last_relationship_data = None
227
+
228
+
gedcomx/Name.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from enum import Enum
2
2
  from typing import List,Optional
3
+ from typing_extensions import Self
3
4
 
4
5
  from .Attribution import Attribution
5
6
  from .Conclusion import Conclusion, ConfidenceLevel
@@ -220,6 +221,11 @@ class Name(Conclusion):
220
221
 
221
222
  return name_as_dict
222
223
 
224
+ class QuickName():
225
+ def __new__(cls,name: str) -> Name:
226
+ obj = Name(nameForms=[NameForm(fullText=name)])
227
+ return obj
228
+
223
229
  def ensure_list(val):
224
230
  if val is None:
225
231
  return []
gedcomx/Person.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from enum import Enum
2
2
  from typing import List, Optional
3
+ from urllib.parse import urljoin
3
4
 
4
5
  from .Attribution import Attribution
5
6
  from .Conclusion import ConfidenceLevel
@@ -8,13 +9,14 @@ from .EvidenceReference import EvidenceReference
8
9
  from .Fact import Fact, FactType
9
10
  from .Gender import Gender, GenderType
10
11
  from .Identifier import IdentifierList
11
- from .Name import Name
12
+ from .Name import Name, QuickName
12
13
  from .Note import Note
13
14
  from .SourceReference import SourceReference
15
+ from .Serialization import Serialization
14
16
  from .Subject import Subject
15
17
  from .Resource import Resource
16
18
  from collections.abc import Sized
17
- from ._Links import _LinkList
19
+ from .Extensions.rs10.rsLink import _rsLinkList
18
20
 
19
21
  class Person(Subject):
20
22
  """A person in the system.
@@ -47,9 +49,10 @@ class Person(Subject):
47
49
  names: Optional[List[Name]] = None,
48
50
  facts: Optional[List[Fact]] = None,
49
51
  living: Optional[bool] = False,
50
- links: Optional[_LinkList] = None) -> None:
52
+ links: Optional[_rsLinkList] = None,
53
+ uri: Optional[Resource] = None) -> None:
51
54
  # Call superclass initializer if needed
52
- super().__init__(id, lang, sources, analysis, notes, confidence, attribution, extracted, evidence, media, identifiers,links=links)
55
+ super().__init__(id, lang, sources, analysis, notes, confidence, attribution, extracted, evidence, media, identifiers,links=links,uri=uri)
53
56
 
54
57
  # Initialize mutable attributes to empty lists if None
55
58
  self.sources = sources if sources is not None else []
@@ -108,103 +111,59 @@ class Person(Subject):
108
111
 
109
112
  @property
110
113
  def _as_dict_(self):
111
- def _serialize(value):
112
- if isinstance(value, (str, int, float, bool, type(None))):
113
- return value
114
- elif isinstance(value, dict):
115
- return {k: _serialize(v) for k, v in value.items()}
116
- elif isinstance(value, (list, tuple, set)):
117
- return [_serialize(v) for v in value]
118
- elif hasattr(value, "_as_dict_"):
119
- return value._as_dict_
120
- else:
121
- return str(value) # fallback for unknown objects
122
-
123
- subject_fields = super()._as_dict_ # Start with base class fields
114
+ type_as_dict = super()._as_dict_ # Start with base class fields
124
115
  # Only add Relationship-specific fields
125
- subject_fields.update({
116
+ type_as_dict.update({
126
117
  'private': self.private,
127
118
  'living': self.living,
128
119
  'gender': self.gender._as_dict_ if self.gender else None,
129
120
  'names': [name._as_dict_ for name in self.names],
130
- 'facts': [fact for fact in self.facts],
131
- 'uri': 'uri: ' + self.uri.value
121
+ 'facts': [fact._as_dict_ for fact in self.facts],
122
+ 'uri': self.uri._as_dict_ if self.uri else None
132
123
 
133
124
  })
134
125
 
135
- # Serialize and exclude None values
136
- for key, value in subject_fields.items():
137
- if value is not None:
138
- subject_fields[key] = _serialize(value)
139
-
140
- # 3) merge and filter out None *at the top level*
141
- return {
142
- k: v
143
- for k, v in subject_fields.items()
144
- if v is not None and not (isinstance(v, Sized) and len(v) == 0)
145
- }
146
-
147
-
126
+ return Serialization.serialize_dict(type_as_dict)
127
+
148
128
  @classmethod
149
129
  def _from_json_(cls, data: dict):
150
130
  """
151
131
  Create a Person instance from a JSON-dict (already parsed).
152
132
  """
153
-
154
-
155
- # Basic scalar fields
156
- id_ = data.get('id')
157
- lang = data.get('lang', 'en')
158
- private = data.get('private', False)
159
- extracted = data.get('extracted', False)
160
-
161
- living = data.get('extracted', False)
162
-
163
- # Complex singletons
164
- analysis = Resource._from_json_(data['analysis']) if data.get('analysis') else None
165
- attribution = Attribution._from_json_(data['attribution']) if data.get('attribution') else None
166
- confidence = ConfidenceLevel_from_json_(data['confidence']) if data.get('confidence') else None
167
-
168
- # Gender (string or dict depending on your JSON)
169
- gender_json = data.get('gender')
170
- if isinstance(gender_json, dict):
171
- gender = Gender._from_json_(gender_json)
172
- else:
173
- # if it's just the enum value
174
- gender = Gender(type=GenderType(gender_json)) if gender_json else Gender(type=GenderType.Unknown)
175
-
176
-
177
- sources = [SourceReference._from_json_(o) for o in data.get('sources')] if data.get('sources') else None
178
- notes = [Note._from_json_(o) for o in data.get('notes')] if data.get('notes') else None
179
- evidence = [EvidenceReference._from_json_(o) for o in data.get('evidence')] if data.get('evidence') else None
180
- media = [SourceReference._from_json_(o) for o in data.get('media')] if data.get('media') else None
181
- identifiers = IdentifierList._from_json_(data.get('identifiers')) if data.get('identifiers') else None
182
- names = [Name._from_json_(o) for o in data.get('names')] if data.get('names') else None
183
- facts = [Fact._from_json_(o) for o in data.get('facts')] if data.get('facts') else None
184
- links = _LinkList._from_json_(data.get('links')) if data.get('links') else None
185
-
186
- # Build the instance
187
- inst = cls(
188
- id = id_,
189
- lang = lang,
190
- sources = sources,
191
- analysis = analysis,
192
- notes = notes,
193
- confidence = confidence,
194
- attribution = attribution,
195
- extracted = extracted,
196
- evidence = evidence,
197
- media = media,
198
- identifiers = identifiers,
199
- private = private,
200
- gender = gender,
201
- names = names,
202
- facts = facts,
203
- living = living,
204
- links = links
205
- )
206
-
207
- return inst
133
+ return Serialization.deserialize(data, Person)
134
+
135
+ @classmethod
136
+ def from_familysearch(cls, pid: str, token: str, *, base_url: Optional[str] = None):
137
+ """
138
+ Fetch a single person by PID from FamilySearch and return a Person.
139
+ - pid: e.g. "KPHP-4B4"
140
+ - token: OAuth2 access token (Bearer)
141
+ - base_url: override API base (defaults to settings.FS_API_BASE or prod)
142
+ """
143
+ import requests
144
+ default_base = "https://apibeta.familysearch.org/platform/"
145
+
146
+ base = (base_url or default_base).rstrip("/") + "/"
147
+ url = urljoin(base, f"tree/persons/{pid}")
148
+
149
+ headers = {
150
+ "Accept": "application/json",
151
+ "Authorization": f"Bearer {token}",
152
+ }
153
+
154
+ resp = requests.get(url, headers=headers, timeout=(5, 30))
155
+ resp.raise_for_status()
156
+
157
+ payload = resp.json()
158
+ persons = payload.get("persons") or []
159
+
160
+ # Prefer exact match on PID, else first item if present
161
+ person_json = next((p for p in persons if (p.get("id") == pid)), None) or (persons[0] if persons else None)
162
+ if not person_json:
163
+ raise ValueError(f"FamilySearch returned no person for PID {pid}")
164
+
165
+ # Keep your existing deserialization helper
166
+ return Serialization.deserialize(person_json, Person)
208
167
 
209
168
  class QuickPerson:
210
169
  """A GedcomX Person Data Type created with basic information.
@@ -221,7 +180,7 @@ class QuickPerson:
221
180
  Raises:
222
181
  ValueError: If `id` is not a valid UUID.
223
182
  """
224
- def __new__(cls, name: Optional[str], dob: Optional[str], dod: Optional[str]):
183
+ def __new__(cls, name: str, dob: Optional[str] = None, dod: Optional[str] = None):
225
184
  # Build facts from args
226
185
  facts = []
227
186
  if dob:
@@ -230,4 +189,4 @@ class QuickPerson:
230
189
  facts.append(Fact(type=FactType.Death, date=Date(original=dod)))
231
190
 
232
191
  # Return the different class instance
233
- return Person(facts=facts, names=[name] if name else None)
192
+ return Person(facts=facts, names=[QuickName(name=name)] if name else None)
@@ -17,17 +17,18 @@ class PlaceDescription(Subject):
17
17
  identifier = "http://gedcomx.org/v1/PlaceDescription"
18
18
  version = 'http://gedcomx.org/conceptual-model/v1'
19
19
 
20
- def __init__(self, id: str =None,
20
+ def __init__(self, id: Optional[str] =None,
21
21
  lang: str = 'en',
22
- sources: Optional[List[SourceReference]] = [],
23
- analysis: Resource = None, notes: Optional[List[Note]] =[],
24
- confidence: ConfidenceLevel = None,
25
- attribution: Attribution = None,
26
- extracted: bool = None,
27
- evidence: List[EvidenceReference] = None,
28
- media: List[SourceReference] = [],
29
- identifiers: List[IdentifierList] = [],
30
- names: List[TextValue] = [],
22
+ sources: Optional[List[SourceReference]] = None,
23
+ analysis: Optional[Resource] = None,
24
+ notes: Optional[List[Note]] =None,
25
+ confidence: Optional[ConfidenceLevel] = None,
26
+ attribution: Optional[Attribution] = None,
27
+ extracted: Optional[bool] = None,
28
+ evidence: Optional[List[EvidenceReference]] = None,
29
+ media: Optional[List[SourceReference]] = None,
30
+ identifiers: Optional[IdentifierList] = None,
31
+ names: Optional[List[TextValue]] = None,
31
32
  type: Optional[str] = None,
32
33
  place: Optional[URI] = None,
33
34
  jurisdiction: Optional["Resource | PlaceDescription"] = None, # PlaceDescription
@@ -35,6 +36,7 @@ class PlaceDescription(Subject):
35
36
  longitude: Optional[float] = None,
36
37
  temporalDescription: Optional[Date] = None,
37
38
  spatialDescription: Optional[Resource] = None,) -> None:
39
+
38
40
  super().__init__(id, lang, sources, analysis, notes, confidence, attribution, extracted, evidence, media, identifiers)
39
41
  self.names = names
40
42
  self.type = type
@@ -47,8 +49,8 @@ class PlaceDescription(Subject):
47
49
 
48
50
  @property
49
51
  def _as_dict_(self):
50
- place_description_dict = super()._as_dict_
51
- place_description_dict.update({
52
+ type_as_dict = super()._as_dict_
53
+ type_as_dict.update({
52
54
  "names": [n for n in self.names] if self.names else None,
53
55
  "type": self.type if self.type else None,
54
56
  "place": self.place._as_dict_ if self.place else None,
@@ -56,6 +58,13 @@ class PlaceDescription(Subject):
56
58
  "latitude": float(self.latitide) if self.latitide else None,
57
59
  "longitude": float(self.longitute) if self.longitute else None,
58
60
  "temporalDescription": self.temporalDescription if self.temporalDescription else None,
59
- "spatialDescription": self.spacialDescription._as_dict_ if self.temporalDescription else None
61
+ "spatialDescription": self.spacialDescription._as_dict_ if self.spacialDescription else None
60
62
  })
61
- return Serialization.serialize_dict(place_description_dict)
63
+ return Serialization.serialize_dict(type_as_dict)
64
+
65
+ @classmethod
66
+ def _from_json_(cls, data: dict):
67
+ """
68
+ Create a PlaceDescription instance from a JSON-dict (already parsed).
69
+ """
70
+ return Serialization.deserialize(data, PlaceDescription)
gedcomx/PlaceReference.py CHANGED
@@ -7,27 +7,24 @@ class PlaceReference:
7
7
  identifier = 'http://gedcomx.org/v1/PlaceReference'
8
8
  version = 'http://gedcomx.org/conceptual-model/v1'
9
9
 
10
- def __init__(self, original: Optional[str], descriptionRef: Optional[Resource]) -> None:
10
+ def __init__(self,
11
+ original: Optional[str] = None,
12
+ description: Optional[Resource] = None) -> None:
11
13
  self.original = original
12
- self.descriptionRef = descriptionRef
14
+ self.description = description
13
15
 
14
16
  @property
15
17
  def _as_dict_(self):
16
- place_reference_dict = {
18
+ type_as_dict = {
17
19
  'original': self.original,
18
- 'descriptionRef': self.descriptionRef._as_dict_ if isinstance(self.descriptionRef,Resource) else Resource(target=self.descriptionRef)._as_dict_
20
+ 'description': self.description._as_dict_ if self.description else None
19
21
  }
20
- return Serialization.serialize_dict(place_reference_dict)
22
+ return Serialization.serialize_dict(type_as_dict)
23
+
24
+ @classmethod
25
+ def _from_json_(cls, data):
26
+
27
+ return Serialization.deserialize(data, PlaceReference)
21
28
 
22
- def ensure_list(val):
23
- if val is None:
24
- return []
25
- return val if isinstance(val, list) else [val]
26
29
 
27
- # PlaceReference
28
- PlaceReference._from_json_ = classmethod(lambda cls, data: PlaceReference(
29
- original=data.get('original'),
30
- descriptionRef=Resource._from_json_(data['description']) if data.get('description') else None
31
- ))
32
30
 
33
-
gedcomx/Relationship.py CHANGED
@@ -35,12 +35,15 @@ class Relationship(Subject):
35
35
  person2 (Person): Second Person in Relationship
36
36
 
37
37
  Raises:
38
- ValueError: If `id` is not a valid UUID.
38
+
39
39
  """
40
40
  identifier = 'http://gedcomx.org/v1/Relationship'
41
41
  version = 'http://gedcomx.org/conceptual-model/v1'
42
42
 
43
- def __init__(self,
43
+ def __init__(self,
44
+ person1: Optional[Person | Resource] = None,
45
+ person2: Optional[Person | Resource] = None,
46
+ facts: Optional[List[Fact]] = None,
44
47
  id: Optional[str] = None,
45
48
  lang: Optional[str] = None,
46
49
  sources: Optional[List[SourceReference]] = None,
@@ -53,74 +56,40 @@ class Relationship(Subject):
53
56
  media: Optional[List[SourceReference]] = None,
54
57
  identifiers: Optional[List[Identifier]] = None,
55
58
  type: Optional[RelationshipType] = None,
56
- person1: Optional[Person | Resource] = None,
57
- person2: Optional[Person | Resource] = None,
58
- facts: Optional[List[Fact]] = None) -> None:
59
+ ) -> None:
59
60
 
60
61
  # Call superclass initializer if required
61
62
  super().__init__(id, lang, sources, analysis, notes, confidence, attribution, extracted, evidence, media, identifiers)
62
63
 
63
- # Initialize optional parameters with default empty lists if None
64
- #self.sources = sources if sources is not None else []
65
- #self.notes = notes if notes is not None else []
66
- #self.evidence = evidence if evidence is not None else []
67
- #self.media = media if media is not None else []
68
- #self.identifiers = identifiers if identifiers is not None else []
69
- #self.facts = facts if facts is not None else []
70
-
71
- # Initialize other attributes
72
64
  self.type = type
73
65
  self.person1 = person1
74
66
  self.person2 = person2
75
- self.facts = facts if facts else None
67
+ self.facts = facts if facts else []
76
68
 
69
+ def add_fact(self,fact: Fact):
70
+ if fact is not None and isinstance(fact,Fact):
71
+ for existing_fact in self.facts:
72
+ if fact == existing_fact:
73
+ return
74
+ self.facts.append(fact)
75
+ else:
76
+ raise TypeError(f"Expected type 'Fact' recieved type {type(fact)}")
77
+
77
78
  @property
78
79
  def _as_dict_(self):
79
- return serialize_to_dict(self, {
80
+ type_as_dict = super()._as_dict_
81
+ type_as_dict.update({
80
82
  "type": self.type.value if isinstance(self.type, RelationshipType) else self.type,
81
- "person1": self.person1.uri,
82
- "person2": self.person2.uri,
83
+ "person1": self.person1._as_dict_ if self.person1 else None,
84
+ "person2": self.person2._as_dict_ if self.person2 else None,
83
85
  "facts": [fact for fact in self.facts] if self.facts else None
84
86
  })
87
+ return Serialization.serialize_dict(type_as_dict)
85
88
 
86
89
  @classmethod
87
90
  def _from_json_(cls, data: dict):
88
91
  """
89
- Create a Relationship instance from a JSON-dict (already parsed).
92
+ Create a Person instance from a JSON-dict (already parsed).
90
93
  """
91
- def ensure_list(value):
92
- if value is None:
93
- return []
94
- if isinstance(value, list):
95
- return value
96
- return [value] # wrap single item in list
97
-
98
- # Basic scalar fields (adjust as needed)
99
- id_ = data.get('id')
100
- type_ = data.get('type')
101
- extracted = data.get('extracted', None)
102
- private = data.get('private', None)
103
-
104
- # Complex singletons (adjust as needed)
105
- person1 = Resource.from_url(data.get('person1')['resource']) if data.get('person1') else None
106
- person2 = Resource.from_url(data.get('person2')['resource']) if data.get('person2') else None
107
- facts = [Fact._from_json_(o) for o in ensure_list(data.get('facts'))]
108
- sources = [SourceReference._from_json_(o) for o in ensure_list(data.get('sources'))]
109
- notes = [Note._from_json_(o) for o in ensure_list(data.get('notes'))]
110
-
111
- # Build the instance
112
- inst = cls(
113
- id = id_,
114
- type = type_,
115
- extracted = extracted,
116
- #private = private, #TODO Has this been added?
117
- person1 = person1,
118
- person2 = person2,
119
- facts = facts,
120
- sources = sources,
121
- notes = notes
122
- )
123
-
124
- return inst
125
-
94
+ return Serialization.deserialize(data, Relationship)
126
95