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/Relationship.py CHANGED
@@ -8,9 +8,9 @@ from .Fact import Fact
8
8
  from .Identifier import Identifier
9
9
  from .Note import Note
10
10
  from .Person import Person
11
- from .Serialization import serialize_to_dict
11
+ from .Serialization import Serialization
12
12
  from .SourceReference import SourceReference
13
- from .URI import URI
13
+ from .Resource import Resource
14
14
 
15
15
  from .Subject import Subject
16
16
 
@@ -27,14 +27,27 @@ class RelationshipType(Enum):
27
27
  return descriptions.get(self, "No description available.")
28
28
 
29
29
  class Relationship(Subject):
30
+ """Represents a relationship between two Person(s)
31
+
32
+ Args:
33
+ type (RelationshipType): Type of relationship
34
+ person1 (Person) = First Person in Relationship
35
+ person2 (Person): Second Person in Relationship
36
+
37
+ Raises:
38
+
39
+ """
30
40
  identifier = 'http://gedcomx.org/v1/Relationship'
31
41
  version = 'http://gedcomx.org/conceptual-model/v1'
32
42
 
33
- 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,
34
47
  id: Optional[str] = None,
35
48
  lang: Optional[str] = None,
36
49
  sources: Optional[List[SourceReference]] = None,
37
- analysis: Optional[URI] = None,
50
+ analysis: Optional[Resource] = None,
38
51
  notes: Optional[List[Note]] = None,
39
52
  confidence: Optional[ConfidenceLevel] = None,
40
53
  attribution: Optional[Attribution] = None,
@@ -43,74 +56,40 @@ class Relationship(Subject):
43
56
  media: Optional[List[SourceReference]] = None,
44
57
  identifiers: Optional[List[Identifier]] = None,
45
58
  type: Optional[RelationshipType] = None,
46
- person1: Optional[URI] = None,
47
- person2: Optional[URI] = None,
48
- facts: Optional[List[Fact]] = None) -> None:
59
+ ) -> None:
49
60
 
50
61
  # Call superclass initializer if required
51
62
  super().__init__(id, lang, sources, analysis, notes, confidence, attribution, extracted, evidence, media, identifiers)
52
63
 
53
- # Initialize optional parameters with default empty lists if None
54
- #self.sources = sources if sources is not None else []
55
- #self.notes = notes if notes is not None else []
56
- #self.evidence = evidence if evidence is not None else []
57
- #self.media = media if media is not None else []
58
- #self.identifiers = identifiers if identifiers is not None else []
59
- #self.facts = facts if facts is not None else []
60
-
61
- # Initialize other attributes
62
64
  self.type = type
63
65
  self.person1 = person1
64
66
  self.person2 = person2
65
- self.facts = facts if facts else None
67
+ self.facts = facts if facts else []
66
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
+
67
78
  @property
68
79
  def _as_dict_(self):
69
- return serialize_to_dict(self, {
80
+ type_as_dict = super()._as_dict_
81
+ type_as_dict.update({
70
82
  "type": self.type.value if isinstance(self.type, RelationshipType) else self.type,
71
- "person1": self.person1._uri,
72
- "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,
73
85
  "facts": [fact for fact in self.facts] if self.facts else None
74
86
  })
87
+ return Serialization.serialize_dict(type_as_dict)
75
88
 
76
89
  @classmethod
77
90
  def _from_json_(cls, data: dict):
78
91
  """
79
- Create a Relationship instance from a JSON-dict (already parsed).
92
+ Create a Person instance from a JSON-dict (already parsed).
80
93
  """
81
- def ensure_list(value):
82
- if value is None:
83
- return []
84
- if isinstance(value, list):
85
- return value
86
- return [value] # wrap single item in list
87
-
88
- # Basic scalar fields (adjust as needed)
89
- id_ = data.get('id')
90
- type_ = data.get('type')
91
- extracted = data.get('extracted', None)
92
- private = data.get('private', None)
93
-
94
- # Complex singletons (adjust as needed)
95
- person1 = URI.from_url(data.get('person1')['resource']) if data.get('person1') else None
96
- person2 = URI.from_url(data.get('person2')['resource']) if data.get('person2') else None
97
- facts = [Fact._from_json_(o) for o in ensure_list(data.get('facts'))]
98
- sources = [SourceReference._from_json_(o) for o in ensure_list(data.get('sources'))]
99
- notes = [Note._from_json_(o) for o in ensure_list(data.get('notes'))]
100
-
101
- # Build the instance
102
- inst = cls(
103
- id = id_,
104
- type = type_,
105
- extracted = extracted,
106
- #private = private, #TODO Has this been added?
107
- person1 = person1,
108
- person2 = person2,
109
- facts = facts,
110
- sources = sources,
111
- notes = notes
112
- )
113
-
114
- return inst
115
-
94
+ return Serialization.deserialize(data, Relationship)
116
95
 
gedcomx/Resource.py ADDED
@@ -0,0 +1,75 @@
1
+ from typing import List, Optional
2
+
3
+
4
+ from .URI import URI
5
+
6
+ class Resource:
7
+ """
8
+ Class used to track and resolve URIs and references between datastores.
9
+
10
+ Parameters
11
+ ----------
12
+
13
+ Raises
14
+ ------
15
+ ValueError
16
+ If `id` is not a valid UUID.
17
+ """
18
+ def __init__(self,uri: Optional[URI|str] = None, id:Optional[str] = None,top_lvl_object: Optional[object] = None,target= None) -> None:
19
+
20
+ self.resource = URI.from_url(uri.value) if isinstance(uri,URI) else URI.from_url(uri) if isinstance(uri,str) else None
21
+ self.Id = id
22
+
23
+ self.type = None
24
+ self.resolved = False
25
+ self.target: object = target
26
+ self.remote: bool | None = None # is the resource pointed to persitent on a remote datastore?
27
+
28
+ if target:
29
+ self.resource = target._uri
30
+ self.Id = target.id
31
+ self.type = type(target)
32
+
33
+ @property
34
+ def _as_dict_(self):
35
+ from .Serialization import Serialization
36
+ typ_as_dict = {'resource':self.resource.value if self.resource else None,
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
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
+
52
+ def get_resource_as_dict(value):
53
+ """
54
+ If value is truthy:
55
+ - If it's already a Resource, return it.
56
+ - Otherwise, wrap it in Resource using (value._uri, value.id).
57
+ Returns None if value is falsy.
58
+ """
59
+
60
+ if not value:
61
+ return None
62
+
63
+ if isinstance(value, Resource):
64
+ return value._as_dict_
65
+
66
+ try:
67
+ return Resource(
68
+ getattr(value, "uri", None),
69
+ getattr(value, "id", None)
70
+ )._as_dict_
71
+ except AttributeError:
72
+ print('get_resource_as_dict',type(value),value)
73
+ print((f"value: {value} as inproper attributes"))
74
+ exit()
75
+
gedcomx/Serialization.py CHANGED
@@ -1,37 +1,401 @@
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
+
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
17
+
18
+ _PRIMITIVES = (str, int, float, bool, type(None))
3
19
 
4
20
  def _has_parent_class(obj) -> bool:
5
21
  return hasattr(obj, '__class__') and hasattr(obj.__class__, '__bases__') and len(obj.__class__.__bases__) > 0
6
22
 
7
- def serialize_to_dict(obj,class_values:Dict,ignore_null=True):
8
- def _serialize(value):
9
- if isinstance(value, (str, int, float, bool, type(None))):
10
- return value
11
- elif isinstance(value, dict):
12
- return {k: _serialize(v) for k, v in value.items()}
13
- elif isinstance(value, (list, tuple, set)):
14
- return [_serialize(v) for v in value]
15
- elif hasattr(value, "_as_dict_"):
16
- return value._as_dict_
17
- else:
18
- return str(value) # fallback for unknown objects
19
-
20
- values_dict = {}
21
- if _has_parent_class(obj):
22
- values_dict.update(super(obj.__class__, obj)._as_dict_)
23
- if class_values:
24
- values_dict.update(class_values)
25
- # Serialize and exclude None values
26
-
27
- empty_fields = []
28
- for key, value in values_dict.items():
29
- if value is not None:
30
- values_dict[key] = _serialize(value)
23
+
24
+ class Serialization:
25
+
26
+ @staticmethod
27
+ def serialize_dict(dict_to_serialize: dict) -> dict:
28
+ """
29
+ Iterates through the dict, serilaizing all Gedcom Types into a json compatible value
30
+
31
+ Parameters
32
+ ----------
33
+ dict_to_serialize: dict
34
+ dict that has been created from any Gedcom Type Object's _as_dict_ property
35
+
36
+ Raises
37
+ ------
38
+ ValueError
39
+ If `id` is not a valid UUID.
40
+ """
41
+ def _serialize(value):
42
+ if isinstance(value, (str, int, float, bool, type(None))):
43
+ return value
44
+ elif isinstance(value, dict):
45
+ return {k: _serialize(v) for k, v in value.items()}
46
+ elif isinstance(value, (list, tuple, set)):
47
+ return [_serialize(v) for v in value]
48
+ elif hasattr(value, "_as_dict_"):
49
+ return value._as_dict_
50
+ else:
51
+ return str(value) # fallback for unknown objects
52
+
53
+ if dict_to_serialize and isinstance(dict_to_serialize,dict):
54
+ for key, value in dict_to_serialize.items():
55
+ if value is not None:
56
+ dict_to_serialize[key] = _serialize(value)
57
+
58
+ return {
59
+ k: v
60
+ for k, v in dict_to_serialize.items()
61
+ if v is not None and not (isinstance(v, Sized) and len(v) == 0)
62
+ }
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))
31
112
  else:
32
- empty_fields.append(key)
33
-
34
- for key in empty_fields:
35
- del values_dict[key]
36
-
37
- return values_dict
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
gedcomx/SourceCitation.py CHANGED
@@ -17,4 +17,9 @@ class SourceCitation:
17
17
  """
18
18
  lang = data.get('lang', 'en')
19
19
  value = data.get('value')
20
- return cls(lang=lang, value=value)
20
+ return cls(lang=lang, value=value)
21
+
22
+ @property
23
+ def _as_dict_(self):
24
+ return {'lang':self.lang,
25
+ 'value': self.value}