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/Resource.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from typing import List, Optional
2
2
 
3
+
3
4
  from .URI import URI
4
5
 
5
6
  class Resource:
@@ -16,7 +17,7 @@ class Resource:
16
17
  """
17
18
  def __init__(self,uri: Optional[URI|str] = None, id:Optional[str] = None,top_lvl_object: Optional[object] = None,target= None) -> None:
18
19
 
19
- self.resource = URI.from_url(uri.value)
20
+ self.resource = URI.from_url(uri.value) if isinstance(uri,URI) else URI.from_url(uri) if isinstance(uri,str) else None
20
21
  self.Id = id
21
22
 
22
23
  self.type = None
@@ -31,9 +32,23 @@ class Resource:
31
32
 
32
33
  @property
33
34
  def _as_dict_(self):
34
- return {'resource':self.resource.value,
35
+ from .Serialization import Serialization
36
+ typ_as_dict = {'resource':self.resource.value if self.resource else None,
35
37
  'resourceId':self.Id}
38
+ return Serialization.serialize_dict(typ_as_dict)
39
+
40
+ @classmethod
41
+ def _from_json_(cls,data):
42
+ # TODO This is not used but taken care of in Serialization
43
+ r = Resource(uri=data.get('resource'),id=data.get('resourceId',None))
44
+ #return r
36
45
 
46
+ def __repr__(self) -> str:
47
+ return f"Resource(uri={self.resource}, id={self.Id}, target={self.target})"
48
+
49
+ def __str__(self) -> str:
50
+ return f"{self.resource}{f', id={self.Id}' if self.Id else ''}"
51
+
37
52
  def get_resource_as_dict(value):
38
53
  """
39
54
  If value is truthy:
@@ -58,4 +73,3 @@ def get_resource_as_dict(value):
58
73
  print((f"value: {value} as inproper attributes"))
59
74
  exit()
60
75
 
61
-
gedcomx/Serialization.py CHANGED
@@ -1,42 +1,25 @@
1
1
  from typing import Dict
2
2
 
3
+ from .Logging import get_logger
4
+
5
+ log = get_logger(__name__)
6
+ log.setLevel("DEBUG")
7
+ log.info("Logger initialized.")
8
+
3
9
  from collections.abc import Sized
10
+ from typing import Any, get_origin, get_args, List, Set, Tuple, Dict, Union, ForwardRef, Annotated
11
+ import types
12
+
13
+ import enum
14
+ from .Resource import Resource
15
+ from .Identifier import IdentifierList
16
+ from .URI import URI
4
17
 
18
+ _PRIMITIVES = (str, int, float, bool, type(None))
5
19
 
6
20
  def _has_parent_class(obj) -> bool:
7
21
  return hasattr(obj, '__class__') and hasattr(obj.__class__, '__bases__') and len(obj.__class__.__bases__) > 0
8
22
 
9
- def serialize_to_dict(obj,class_values:Dict,ignore_null=True):
10
- def _serialize(value):
11
- if isinstance(value, (str, int, float, bool, type(None))):
12
- return value
13
- elif isinstance(value, dict):
14
- return {k: _serialize(v) for k, v in value.items()}
15
- elif isinstance(value, (list, tuple, set)):
16
- return [_serialize(v) for v in value]
17
- elif hasattr(value, "_as_dict_"):
18
- return value._as_dict_
19
- else:
20
- return str(value) # fallback for unknown objects
21
-
22
- values_dict = {}
23
- if _has_parent_class(obj):
24
- values_dict.update(super(obj.__class__, obj)._as_dict_)
25
- if class_values:
26
- values_dict.update(class_values)
27
- # Serialize and exclude None values
28
-
29
- empty_fields = []
30
- for key, value in values_dict.items():
31
- if value is not None:
32
- values_dict[key] = _serialize(value)
33
- else:
34
- empty_fields.append(key)
35
-
36
- for key in empty_fields:
37
- del values_dict[key]
38
-
39
- return values_dict
40
23
 
41
24
  class Serialization:
42
25
 
@@ -78,3 +61,341 @@ class Serialization:
78
61
  if v is not None and not (isinstance(v, Sized) and len(v) == 0)
79
62
  }
80
63
  return {}
64
+
65
+ @staticmethod
66
+ def _coerce_value(value: Any, typ: Any) -> Any:
67
+ """Coerce `value` to `typ`:
68
+ - primitives: pass through
69
+ - containers: recurse into elements
70
+ - objects: call typ._from_json_(dict) if available and value is dict
71
+ - already-instantiated objects of typ: pass through
72
+ - otherwise: return value unchanged
73
+ """
74
+ def is_enum_type(T) -> bool:
75
+ """Return True if T (possibly a typing construct) is or contains an Enum type."""
76
+ origin = get_origin(T)
77
+
78
+ # Unwrap Union/Optional/PEP 604 (A | B)
79
+ if origin in (Union, types.UnionType):
80
+ return any(is_enum_type(a) for a in get_args(T))
81
+
82
+ # Unwrap Annotated[T, ...]
83
+ if origin is Annotated:
84
+ return is_enum_type(get_args(T)[0])
85
+
86
+ # Resolve forward refs / strings if you use them
87
+ if isinstance(T, ForwardRef):
88
+ T = globals().get(T.__forward_arg__, T)
89
+ if isinstance(T, str):
90
+ T = globals().get(T, T)
91
+
92
+ # Finally check enum-ness
93
+ try:
94
+ return issubclass(T, enum.Enum)
95
+ except TypeError:
96
+ return False # not a class (e.g., typing.List[int], etc.)
97
+ log.debug(f"Coercing value '{value}' of type '{type(value).__name__}' to '{typ}'")
98
+
99
+ def _resolve(t):
100
+ # resolve ForwardRef('Resource') -> actual object if already in globals()
101
+ if isinstance(t, ForwardRef):
102
+ return globals().get(t.__forward_arg__, t)
103
+ return t
104
+
105
+ if is_enum_type(typ):
106
+ log.debug(f"Enum type detected: {typ}")
107
+ return typ(value) # cast to enum
108
+
109
+ origin = get_origin(typ)
110
+ if origin in (Union, types.UnionType):
111
+ args = tuple(_resolve(a) for a in get_args(typ))
112
+ else:
113
+ args = (_resolve(typ),)
114
+ log.debug(f"Origin: {origin}, args: {args}")
115
+
116
+ if Resource in args and isinstance(value, dict):
117
+ if Resource in args:
118
+ log.info(f"Deserializing Resource from value: {value}")
119
+ return Resource(uri=value.get('resource'), id=value.get('resourceId', None))
120
+
121
+ if isinstance(value, _PRIMITIVES):
122
+ if Resource in args:
123
+ log.info(f"Deserializing Resource from value: {value}")
124
+ return Resource(uri=value)
125
+ if URI in args:
126
+ log.info(f"Deserializing URI from value: {value}")
127
+ return URI.from_url(value)
128
+ return value
129
+
130
+ if IdentifierList in args:
131
+ log.error(f"Deserializing IdentifierList from value: {value}")
132
+ return IdentifierList._from_json_(value)
133
+
134
+ if origin in (list, List):
135
+ elem_args = get_args(typ) # NOT get_args(args)
136
+ elem_t = elem_args[0] if elem_args else Any
137
+ log.debug(f"List: {typ}, elem={elem_t}")
138
+ return [Serialization._coerce_value(v, elem_t) for v in (value or [])]
139
+
140
+ if origin in (set, Set):
141
+ (elem_t,) = args or (Any,)
142
+ return { Serialization._coerce_value(v, elem_t) for v in (value or []) }
143
+
144
+ if origin in (tuple, Tuple):
145
+ if not args:
146
+ return tuple(value)
147
+ if len(args) == 2 and args[1] is Ellipsis: # Tuple[T, ...]
148
+ elem_t = args[0]
149
+ return tuple(Serialization._coerce_value(v, elem_t) for v in (value or []))
150
+ return tuple(Serialization._coerce_value(v, t) for v, t in zip(value, args))
151
+
152
+ if origin in (dict, Dict):
153
+ k_t, v_t = args or (Any, Any)
154
+ return {
155
+ Serialization._coerce_value(k, k_t): Serialization._coerce_value(v, v_t)
156
+ for k, v in (value or {}).items()
157
+ }
158
+
159
+ # If `typ` has _from_json_ and value is a dict, use it
160
+ if hasattr(typ, "_from_json_") and isinstance(value, dict):
161
+ log.info(f"Deserializing {typ} from json method with value: {value}")
162
+ return typ._from_json_(value)
163
+
164
+ # If already the right type, keep it
165
+ try:
166
+ if isinstance(value, typ):
167
+ return value
168
+ except TypeError:
169
+ log.debug(f"Could not coerce value '{value}' to type '{typ}'")
170
+ pass # `typ` may be a typing construct not valid for isinstance
171
+
172
+ # Fallback: leave as-is
173
+ log.debug(f"Returning '{type(value)}' type")
174
+ return value
175
+
176
+ @staticmethod
177
+ def get_class_fields(cls_name) -> Dict:
178
+ from typing import List, Optional
179
+ from gedcomx.Attribution import Attribution
180
+ from gedcomx.Document import Document , DocumentType, TextType
181
+ from gedcomx.Note import Note
182
+ from gedcomx.Resource import Resource
183
+ from gedcomx.SourceReference import SourceReference
184
+ from gedcomx.extensions.rs10.rsLink import _rsLinkList
185
+ from gedcomx.Conclusion import ConfidenceLevel
186
+ from gedcomx.EvidenceReference import EvidenceReference
187
+ from gedcomx.Identifier import IdentifierList
188
+ from gedcomx.Gender import Gender, GenderType
189
+ from gedcomx.Fact import Fact
190
+ from gedcomx.Name import Name
191
+ from gedcomx.URI import URI
192
+ from gedcomx.Qualifier import Qualifier
193
+ from gedcomx.PlaceDescription import PlaceDescription
194
+ from gedcomx.PlaceReference import PlaceReference
195
+ from gedcomx.Person import Person
196
+ from gedcomx.Relationship import Relationship, RelationshipType
197
+ from gedcomx.Identifier import Identifier
198
+ from gedcomx.Date import Date
199
+ from gedcomx.TextValue import TextValue
200
+ from gedcomx.Address import Address
201
+ from gedcomx.OnlineAccount import OnlineAccount
202
+ from gedcomx.Event import Event, EventType, EventRole
203
+ from .SourceDescription import SourceDescription
204
+
205
+ fields = { 'Conclusion' : {
206
+ "id": str,
207
+ "lang": str,
208
+ "sources": List["SourceReference"],
209
+ "analysis": Document | Resource,
210
+ "notes": List[Note],
211
+ "confidence": ConfidenceLevel,
212
+ "attribution": Attribution,
213
+ "uri": "Resource",
214
+ "max_note_count": int,
215
+ "links": _rsLinkList
216
+ },
217
+ 'Subject' : {
218
+ "id": str,
219
+ "lang": str,
220
+ "sources": List["SourceReference"],
221
+ "analysis": Resource,
222
+ "notes": List["Note"],
223
+ "confidence": ConfidenceLevel,
224
+ "attribution": Attribution,
225
+ "extracted": bool,
226
+ "evidence": List[EvidenceReference],
227
+ "media": List[SourceReference],
228
+ "identifiers": IdentifierList,
229
+ "uri": Resource,
230
+ "links": _rsLinkList
231
+ },
232
+ 'Person' : {
233
+ "id": str,
234
+ "lang": str,
235
+ "sources": List[SourceReference],
236
+ "analysis": Resource,
237
+ "notes": List[Note],
238
+ "confidence": ConfidenceLevel,
239
+ "attribution": Attribution,
240
+ "extracted": bool,
241
+ "evidence": List[EvidenceReference],
242
+ "media": List[SourceReference],
243
+ "identifiers": IdentifierList,
244
+ "private": bool,
245
+ "gender": Gender,
246
+ "names": List[Name],
247
+ "facts": List[Fact],
248
+ "living": bool,
249
+ "links": _rsLinkList,
250
+ 'uri': Resource
251
+ },
252
+ 'SourceReference' : {
253
+ "description": SourceDescription | URI | Resource,
254
+ "descriptionId": str,
255
+ "attribution": Attribution,
256
+ "qualifiers": List[Qualifier],
257
+ },
258
+ 'Attribution' : {
259
+ "contributor": Resource | Attribution,
260
+ "modified": str,
261
+ "changeMessage": str,
262
+ "creator": Resource | Attribution,
263
+ "created": str
264
+ },
265
+ 'Gender' : {
266
+ "id": str,
267
+ "lang": str,
268
+ "sources": List[SourceReference],
269
+ "analysis": Resource,
270
+ "notes": List[Note],
271
+ "confidence": ConfidenceLevel,
272
+ "attribution": Attribution,
273
+ "type": GenderType,
274
+ },
275
+ 'PlaceReference' : {
276
+ "original": str,
277
+ "description": PlaceDescription | URI,
278
+ },
279
+ 'Relationship' : {
280
+ "id": str,
281
+ "lang": str,
282
+ "sources": List[SourceReference],
283
+ "analysis": Document | Resource,
284
+ "notes": List[Note],
285
+ "confidence": ConfidenceLevel,
286
+ "attribution": Attribution,
287
+ "extracted": bool,
288
+ "evidence": List[EvidenceReference],
289
+ "media": List[SourceReference],
290
+ "identifiers": IdentifierList,
291
+ "type": RelationshipType,
292
+ "person1": Person | Resource,
293
+ "person2": Person | Resource,
294
+ "facts": List[Fact],
295
+ },
296
+ 'Document' : {
297
+ "id": str,
298
+ "lang": str,
299
+ "sources": List[SourceReference],
300
+ "analysis": Resource,
301
+ "notes": List[Note],
302
+ "confidence": ConfidenceLevel,
303
+ "attribution": Attribution,
304
+ "type": DocumentType,
305
+ "extracted": bool,
306
+ "textType": TextType,
307
+ "text": str,
308
+ },
309
+ 'PlaceDescription' : {
310
+ "id": str,
311
+ "lang": str,
312
+ "sources": List[SourceReference],
313
+ "analysis": Resource,
314
+ "notes": List[Note],
315
+ "confidence": ConfidenceLevel,
316
+ "attribution": Attribution,
317
+ "extracted": bool,
318
+ "evidence": List[EvidenceReference],
319
+ "media": List[SourceReference],
320
+ "identifiers": List[IdentifierList],
321
+ "names": List[TextValue],
322
+ "type": str,
323
+ "place": URI,
324
+ "jurisdiction": Resource | PlaceDescription,
325
+ "latitude": float,
326
+ "longitude": float,
327
+ "temporalDescription": Date,
328
+ "spatialDescription": Resource,
329
+ },
330
+ "Agent" : {
331
+ "id": str,
332
+ "identifiers": IdentifierList,
333
+ "names": List[TextValue],
334
+ "homepage": URI,
335
+ "openid": URI,
336
+ "accounts": List[OnlineAccount],
337
+ "emails": List[URI],
338
+ "phones": List[URI],
339
+ "addresses": List[Address],
340
+ "person": object | Resource, # intended to be Person | Resource
341
+ # "xnotes": List[Note], # commented out in your __init__
342
+ "attribution": object, # for GEDCOM5/7 compatibility
343
+ "uri": URI | Resource,
344
+ },
345
+ 'Event' : {
346
+ "id": str,
347
+ "lang": str,
348
+ "sources": List[SourceReference],
349
+ "analysis": Resource,
350
+ "notes": List[Note],
351
+ "confidence": ConfidenceLevel,
352
+ "attribution": Attribution,
353
+ "extracted": bool,
354
+ "evidence": List[EvidenceReference],
355
+ "media": List[SourceReference],
356
+ "identifiers": List[Identifier],
357
+ "type": EventType,
358
+ "date": Date,
359
+ "place": PlaceReference,
360
+ "roles": List[EventRole],
361
+ }
362
+
363
+ }
364
+
365
+
366
+ return fields[cls_name] if cls_name in fields else {}
367
+
368
+ @staticmethod
369
+ def deserialize(data: dict[str, Any], class_type) -> Any:
370
+ """
371
+ Deserialize `data` according to `fields` (field -> type).
372
+ - Primitives are assigned directly.
373
+ - Objects use `type._from_json_(dict)` when present.
374
+ - Lists/Sets/Tuples/Dicts are recursively processed.
375
+ Returns (result, unknown_keys).
376
+ """
377
+ log.debug(f"Deserializing '{data}' into '{class_type.__name__}'")
378
+ class_fields = Serialization.get_class_fields(str(class_type.__name__))
379
+ if class_fields == {}:
380
+ log.warning(f"No class fields found for '{class_type.__name__}'")
381
+ log.debug(f"class fields: {class_fields}")
382
+ result: dict[str, Any] = {}
383
+ known = set(class_fields.keys())
384
+ log.debug(f"keys found in JSON: {data.keys()}")
385
+ #log.debug(f"known fields: {known}")
386
+ for name, typ in class_fields.items():
387
+ if name in data:
388
+ log.debug(f"Field '{name}' of {class_type.__name__} found in data")
389
+ result[name] = Serialization._coerce_value(data[name], typ)
390
+ #if type(result[name]) != class_fields[name]:# TODO Write better type checking
391
+ # log.error(f"Field '{name}' of {class_type.__name__} was expected to be of type '{class_fields[name]}', but got '{type(result[name])}' with value '{result[name]}'")
392
+ # raise TypeError(f"Field '{name}' expected type '{class_fields[name]}', got '{type(result[name])}'")
393
+ log.debug(f"Field '{name}' of '{class_type.__name__}' resulted in a '{type(result[name]).__name__}' with value '{result[name]}'")
394
+ else:
395
+ log.debug(f"Field '{name}' of '{class_type.__name__}' not found in JSON data")
396
+
397
+ unknown_keys = [k for k in data.keys() if k not in known]
398
+ log.info(f"Creating instance of {class_type.__name__} with fields: {result.keys()}")
399
+ new_cls = class_type(**result)
400
+ log.debug(f"Deserialized {class_type.__name__} with unknown keys: {unknown_keys}")
401
+ return new_cls # type: ignore, unknown_keys
@@ -78,7 +78,7 @@ class SourceDescription:
78
78
  notes: Optional[List[Note]] = None,
79
79
  attribution: Optional[Attribution] = None,
80
80
  rights: Optional[List[Resource]] = [],
81
- coverage: Optional[Coverage] = None, # Coverage
81
+ coverage: Optional[List[Coverage]] = None, # Coverage
82
82
  descriptions: Optional[List[TextValue]] = None,
83
83
  identifiers: Optional[IdentifierList] = None,
84
84
  created: Optional[Date] = None,
@@ -180,18 +180,15 @@ class SourceDescription:
180
180
 
181
181
  @property
182
182
  def _as_dict_(self) -> Dict[str, Any]:
183
-
184
-
185
-
186
183
  type_as_dict = {
187
184
  'id': self.id,
188
185
  'about': self.about._as_dict_ if self.about else None,
189
186
  'resourceType': self.resourceType.value if self.resourceType else None,
190
187
  'citations': [c._as_dict_ for c in self.citations] or None,
191
188
  'mediaType': self.mediaType,
192
- 'mediator': get_resource_as_dict(self.mediator),
193
- 'publisher': get_resource_as_dict(self.publisher),
194
- 'authors': [get_resource_as_dict(a) for a in self.authors] or None,
189
+ 'mediator': self.mediator._as_dict_ if self.mediator else None,
190
+ 'publisher': self.publisher._as_dict_ if self.publisher else None,
191
+ 'authors': self.authors and [a._as_dict_ for a in self.authors] or None,
195
192
  'sources': [s._as_dict_ for s in self.sources] or None,
196
193
  'analysis': self.analysis._as_dict_ if self.analysis else None,
197
194
  'componentOf': self.componentOf._as_dict_ if self.componentOf else None,
@@ -199,13 +196,13 @@ class SourceDescription:
199
196
  'notes': [n._as_dict_ for n in self.notes] or None,
200
197
  'attribution': self.attribution._as_dict_ if self.attribution else None,
201
198
  'rights': [r._as_dict_ for r in self.rights] or None,
202
- 'coverage': self.coverage._as_dict_ if self.coverage else None,
199
+ 'coverage': [c._as_dict_ for c in self.coverage] or None,
203
200
  'descriptions': [d._as_dict_ for d in self.descriptions] or None,
204
201
  'identifiers': self.identifiers._as_dict_ if self.identifiers else None,
205
202
  'created': self.created if self.created else None,
206
203
  'modified': self.modified if self.modified else None,
207
204
  'published': self.published if self.published else None,
208
- 'repository': get_resource_as_dict(self.repository),
205
+ 'repository': self.repository._as_dict_ if self.repository else None,
209
206
  'uri': self.uri.value
210
207
  }
211
208
 
@@ -4,6 +4,7 @@ from .Attribution import Attribution
4
4
  from .Qualifier import Qualifier
5
5
 
6
6
  from .Resource import Resource
7
+ from .Serialization import Serialization
7
8
  from .URI import URI
8
9
 
9
10
  from collections.abc import Sized
@@ -53,34 +54,21 @@ class SourceReference:
53
54
  qualifiers: Optional[List[Qualifier]] = None
54
55
  ) -> None:
55
56
 
56
- #if not isinstance(description,URI): raise ValueError(f"description is of type {type(description)}")
57
- '''from .SourceDescription import SourceDescription
58
- self._description_object = None
59
- if isinstance(description,URI):
60
- #TODO See if Local, If not try to resolve,
61
- self._description_object = description
62
-
63
- elif isinstance(description,SourceDescription):
64
- self._description_object = description
65
- if hasattr(description,'_uri'):
66
- self.description = description._uri
67
- else:
68
- assert False
69
- self.description = Resource(object=description)
70
- description._uri = self.description
71
- description._object = description
72
- else:
73
- raise ValueError(f"'description' must be of type 'SourceDescription' or 'URI', type: {type(description)} was provided")'''
74
-
75
-
76
57
  self.description = description
77
58
  self.descriptionId = descriptionId
78
59
  self.attribution = attribution
79
- self.qualifiers = qualifiers
60
+ self.qualifiers = qualifiers if qualifiers and isinstance(qualifiers, list) else []
80
61
 
81
62
  def add_qualifier(self, qualifier: Qualifier):
82
- if isinstance(qualifier, Qualifier):
63
+ if isinstance(qualifier, (Qualifier,KnownSourceReference)):
64
+ if self.qualifiers:
65
+ #TODO Prevent Duplicates
66
+ for current_qualifier in self.qualifiers:
67
+ if qualifier == current_qualifier:
68
+ return
83
69
  self.qualifiers.append(qualifier)
70
+ return
71
+ raise ValueError("The 'qualifier' must be type 'Qualifier' or 'KnownSourceReference', not " + str(type(qualifier)))
84
72
 
85
73
  def append(self, text_to_add: str):
86
74
  if text_to_add and isinstance(text_to_add, str):
@@ -93,82 +81,28 @@ class SourceReference:
93
81
 
94
82
  @property
95
83
  def _as_dict_(self):
96
-
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
84
  # Only add Relationship-specific fields
110
- sourcereference_fields = {
111
- 'description':self.description.uri if self.description else None,
85
+ type_as_dict = {
86
+ 'description':self.description._as_dict_ if self.description else None,
112
87
  'descriptionId': self.descriptionId.replace("\n"," ").replace("\r"," ") if self.descriptionId else None,
113
88
  'attribution': self.attribution._as_dict_ if self.attribution else None,
114
89
  'qualifiers':[qualifier.value for qualifier in self.qualifiers ] if self.qualifiers else None
115
90
  }
91
+ return Serialization.serialize_dict(type_as_dict)
116
92
 
117
- # Serialize and exclude None values
118
- for key, value in sourcereference_fields.items():
119
- if value is not None:
120
- sourcereference_fields[key] = _serialize(value)
121
-
122
- return {
123
- k: v
124
- for k, v in sourcereference_fields.items()
125
- if v is not None and not (isinstance(v, Sized) and len(v) == 0)
126
- }
93
+
127
94
 
128
95
 
129
96
  @classmethod
130
- def _from_json_(cls, data: dict):
131
-
97
+ def _from_json_(cls, data: dict):
132
98
  """
133
- Rehydrate a SourceReference from the dict form produced by _as_dict_.
99
+ Rehydrate a SourceReference from the dict passed down from JSON deserialization.
100
+ NOTE: This does not resolve references to SourceDescription objects.
134
101
  """
135
- from .SourceDescription import SourceDescription
136
-
137
- # 1) Reconstruct the SourceDescription object
138
- desc_json = data.get('description')
139
- #if not desc_json:
140
- # raise ValueError("SourceReference JSON missing 'description'")
141
- #desc_obj = SourceDescription._from_json_(desc_json)
142
- desc_obj = URI.from_url_(data.get('description')) #TODO <--- URI Reference
143
-
144
-
145
- # 2) Simple fields
146
- description_id = data.get('descriptionId')
147
-
148
- # 3) Attribution (if present)
149
- attrib = None
150
- if data.get('attribution') is not None:
151
- attrib = Attribution._from_json_(data['attribution'])
102
+ from .Serialization import Serialization
103
+ return Serialization.deserialize(data, SourceReference)
104
+
152
105
 
153
- # 4) Qualifiers list
154
- raw_quals = data.get('qualifiers', [])
155
- qualifiers: List[Qualifier] = []
156
- for q in raw_quals:
157
- try:
158
- # Try the known‐source enum first
159
- qualifiers.append(KnownSourceReference(q))
160
- except ValueError:
161
- # Fallback to generic Qualifier
162
- qualifiers.append(Qualifier(q))
163
-
164
- # 5) Instantiate via your existing __init__
165
- inst = cls(
166
- description=desc_obj,
167
- descriptionId=description_id,
168
- attribution=attrib,
169
- qualifiers=qualifiers
170
- )
171
- return inst
172
106
 
173
107
  def __eq__(self, other):
174
108
  if not isinstance(other, self.__class__):
gedcomx/Subject.py CHANGED
@@ -9,7 +9,7 @@ from .Note import Note
9
9
  from .Serialization import Serialization
10
10
  from .SourceReference import SourceReference
11
11
  from .Resource import Resource
12
- from ._Links import _LinkList
12
+ from .Extensions.rs10.rsLink import _rsLinkList
13
13
 
14
14
  class Subject(Conclusion):
15
15
  identifier = 'http://gedcomx.org/v1/Subject'
@@ -28,14 +28,14 @@ class Subject(Conclusion):
28
28
  media: Optional[List[SourceReference]] = [],
29
29
  identifiers: Optional[IdentifierList] = None,
30
30
  uri: Optional[Resource] = None,
31
- links: Optional[_LinkList] = None) -> None:
31
+ links: Optional[_rsLinkList] = None) -> None:
32
32
  super().__init__(id, lang, sources, analysis, notes, confidence, attribution,links=links)
33
33
  self.extracted = extracted
34
34
  self.evidence = evidence
35
35
  self.media = media
36
- self.identifiers = identifiers
36
+ self.identifiers = identifiers if identifiers else IdentifierList()
37
+ self.uri = uri
37
38
 
38
-
39
39
  def add_identifier(self, identifier_to_add: Identifier):
40
40
  if identifier_to_add and isinstance(identifier_to_add,Identifier):
41
41
  for current_identifier in self.identifiers: