gedcom-x 0.5.8__py3-none-any.whl → 0.5.9__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.
Files changed (45) hide show
  1. {gedcom_x-0.5.8.dist-info → gedcom_x-0.5.9.dist-info}/METADATA +1 -1
  2. gedcom_x-0.5.9.dist-info/RECORD +56 -0
  3. gedcomx/Extensions/rs10/rsLink.py +109 -59
  4. gedcomx/__init__.py +1 -1
  5. gedcomx/address.py +102 -16
  6. gedcomx/agent.py +81 -24
  7. gedcomx/attribution.py +52 -28
  8. gedcomx/conclusion.py +97 -45
  9. gedcomx/converter.py +209 -79
  10. gedcomx/coverage.py +10 -1
  11. gedcomx/date.py +42 -8
  12. gedcomx/document.py +37 -7
  13. gedcomx/event.py +77 -20
  14. gedcomx/evidence_reference.py +9 -0
  15. gedcomx/fact.py +53 -54
  16. gedcomx/gedcom.py +10 -0
  17. gedcomx/gedcom5x.py +30 -20
  18. gedcomx/gedcom7/__init__.py +1 -1
  19. gedcomx/gedcomx.py +95 -93
  20. gedcomx/gender.py +21 -9
  21. gedcomx/group.py +9 -0
  22. gedcomx/identifier.py +47 -20
  23. gedcomx/logging_hub.py +19 -0
  24. gedcomx/mutations.py +10 -5
  25. gedcomx/name.py +74 -33
  26. gedcomx/note.py +50 -18
  27. gedcomx/online_account.py +9 -0
  28. gedcomx/person.py +44 -26
  29. gedcomx/place_description.py +54 -8
  30. gedcomx/place_reference.py +30 -8
  31. gedcomx/qualifier.py +19 -3
  32. gedcomx/relationship.py +55 -14
  33. gedcomx/resource.py +45 -18
  34. gedcomx/serialization.py +400 -421
  35. gedcomx/source_citation.py +16 -4
  36. gedcomx/source_description.py +181 -94
  37. gedcomx/source_reference.py +51 -16
  38. gedcomx/subject.py +59 -14
  39. gedcomx/textvalue.py +66 -12
  40. gedcomx/translation.py +3 -3
  41. gedcomx/uri.py +155 -3
  42. gedcom_x-0.5.8.dist-info/RECORD +0 -56
  43. {gedcom_x-0.5.8.dist-info → gedcom_x-0.5.9.dist-info}/WHEEL +0 -0
  44. {gedcom_x-0.5.8.dist-info → gedcom_x-0.5.9.dist-info}/top_level.txt +0 -0
  45. /gedcomx/gedcom7/{Gedcom7.py → gedcom7.py} +0 -0
gedcomx/serialization.py CHANGED
@@ -1,11 +1,14 @@
1
1
  from __future__ import annotations
2
- from collections.abc import Sized
3
2
  from functools import lru_cache
4
- from typing import Any, Callable, Dict, ForwardRef, List, Set, Tuple, Union, get_args, get_origin
5
3
 
6
4
  import enum
7
5
  import logging
8
6
  import types
7
+ from collections.abc import Sized
8
+ from typing import Any, Dict, List, Set, Tuple, Union, Annotated, ForwardRef, get_args, get_origin
9
+ from typing import Any, Callable, Mapping, List, Dict, Tuple, Set
10
+ from typing import List, Optional
11
+ from time import perf_counter
9
12
 
10
13
  """
11
14
  ======================================================================
@@ -17,7 +20,6 @@ import types
17
20
  Created: 2025-08-25
18
21
  Updated:
19
22
  - 2025-08-31: cleaned up imports and documentation
20
- - 2025-09-01: filename PEP8 standard, imports changed accordingly
21
23
 
22
24
  ======================================================================
23
25
  """
@@ -35,7 +37,7 @@ from .date import Date
35
37
  from .document import Document, DocumentType, TextType
36
38
  from .evidence_reference import EvidenceReference
37
39
  from .event import Event, EventType, EventRole, EventRoleType
38
- from .Extensions.rs10.rsLink import _rsLinkList
40
+ from .Extensions.rs10.rsLink import _rsLinks, rsLink
39
41
  from .fact import Fact, FactType, FactQualifier
40
42
  from .gender import Gender, GenderType
41
43
  from .identifier import IdentifierList, Identifier
@@ -58,14 +60,8 @@ from .uri import URI
58
60
  log = logging.getLogger("gedcomx")
59
61
  deserialization = "gedcomx.deserialization"
60
62
 
61
- hub.start_channel(
62
- ChannelConfig(
63
- name=deserialization,
64
- path=f"logs/{deserialization}.log",
65
- level=logging.DEBUG,
66
- rotation="size:10MB:3", # rotate by size, keep 3 backups
67
- )
68
- )
63
+ serial_log = "gedcomx.serialization"
64
+ deserial_log = "gedcomx.deserialization"
69
65
 
70
66
  _PRIMITIVES = (str, int, float, bool, type(None))
71
67
 
@@ -75,7 +71,58 @@ def _has_parent_class(obj) -> bool:
75
71
  class Serialization:
76
72
 
77
73
  @staticmethod
78
- def serialize_dict(dict_to_serialize: dict) -> dict:
74
+ def serialize(obj: object):
75
+ if obj is not None:
76
+ with hub.use(serial_log):
77
+
78
+ if isinstance(obj, (str, int, float, bool, type(None))):
79
+ return obj
80
+ if isinstance(obj, dict):
81
+ return {k: Serialization.serialize(v) for k, v in obj.items()}
82
+ if isinstance(obj, URI):
83
+ return obj.value
84
+ if isinstance(obj, (list, tuple, set)):
85
+ l = [Serialization.serialize(v) for v in obj]
86
+ if len(l) == 0: return None
87
+ return l
88
+ if type(obj).__name__ == 'Collection':
89
+ l= [Serialization.serialize(v) for v in obj]
90
+ if len(l) == 0: return None
91
+ return l
92
+ if isinstance(obj, enum.Enum):
93
+ return Serialization.serialize(obj.value)
94
+
95
+ log.debug(f"Serializing a '{type(obj).__name__}'")
96
+ type_as_dict = {}
97
+ fields = Serialization.get_class_fields(type(obj).__name__)
98
+ if fields:
99
+ for field_name, type_ in fields.items():
100
+ if hasattr(obj,field_name):
101
+ if (v := getattr(obj,field_name)) is not None:
102
+ if type_ == Resource:
103
+ log.error(f"Refering to a {type(obj).__name__} with a '{type_}'")
104
+ res = Resource(target=v)
105
+ sv = Serialization.serialize(res)
106
+ type_as_dict[field_name] = sv
107
+ elif type_ == URI:
108
+ log.error(f"Refering to a {type(obj).__name__} with a '{type_}'")
109
+ uri = URI(target=v)
110
+ sv = Serialization.serialize(uri)
111
+ type_as_dict[field_name] = sv
112
+ elif (sv := Serialization.serialize(v)) is not None:
113
+ type_as_dict[field_name] = sv
114
+ else:
115
+ log.error(f"{type(obj).__name__} did not have field '{field_name}'")
116
+ if type_as_dict == {}: log.error(f"Serialized a '{type(obj).__name__}' with empty fields: '{fields}'")
117
+ else: log.debug(f"Serialized a '{type(obj).__name__}' with fields '{type_as_dict})'")
118
+ #return Serialization._serialize_dict(type_as_dict)
119
+ return type_as_dict if type_as_dict != {} else None
120
+ else:
121
+ log.error(f"Could not find fields for {type(obj).__name__}")
122
+ return None
123
+
124
+ @staticmethod
125
+ def _serialize_dict(dict_to_serialize: dict) -> dict:
79
126
  """
80
127
  Walk a dict and serialize nested GedcomX objects to JSON-compatible values.
81
128
  - Uses `_as_dict_` on your objects when present
@@ -85,9 +132,9 @@ class Serialization:
85
132
  def _serialize(value):
86
133
  if isinstance(value, (str, int, float, bool, type(None))):
87
134
  return value
88
- if hasattr(value, "_as_dict_"):
135
+ if (fields := Serialization.get_class_fields(type(value).__name__)) is not None:
89
136
  # Expect your objects expose a snapshot via _as_dict_
90
- return value._as_dict_
137
+ return Serialization.serialize(value)
91
138
  if isinstance(value, dict):
92
139
  return {k: _serialize(v) for k, v in value.items()}
93
140
  if isinstance(value, (list, tuple, set)):
@@ -155,302 +202,356 @@ class Serialization:
155
202
  inst._resource_setters = []
156
203
 
157
204
  # --- your deserialize with setters --------------------------------------
205
+
158
206
  @classmethod
159
207
  def deserialize(
160
208
  cls,
161
- data: Dict[str, Any],
209
+ data: dict[str, Any],
162
210
  class_type: type,
163
211
  *,
164
- resolver: Callable[[Any], Any] | None = None, # pass a function to resolve Resources now
165
- queue_setters: bool = True # also stash setters on instance for later
212
+ resolver: Callable[[Any], Any] | None = None,
213
+ queue_setters: bool = True,
166
214
  ) -> Any:
167
- class_fields = cls.get_class_fields(class_type.__name__)
168
- result: Dict[str, Any] = {}
169
- # collect setters that know how to assign back to attributes
170
- pending_setters: List[Callable[[Any, Callable[[Any], Any]], None]] = []
171
-
172
- for name, typ in class_fields.items():
173
- if name not in data:
174
- continue
175
- coerced = cls._coerce_value(data[name], typ)
176
- result[name] = coerced
177
-
178
- # if this attribute (or inside it) has Resource(s), prepare a setter
179
- if cls._has_resource_value(coerced):
180
- def make_setter(attr_name: str, raw_value: Any):
181
- # capture references to the *exact* object we just built for this attribute
182
- def _setter(instance: Any, _resolver: Callable[[Any], Any]) -> None:
183
- resolved = cls._resolve_structure(raw_value, _resolver)
184
- setattr(instance, attr_name, resolved)
185
- return _setter
186
- pending_setters.append(make_setter(name, coerced))
187
-
188
- # build the instance
189
- inst = class_type(**result)
190
-
191
- # apply now, if resolver provided
192
- if resolver is not None and pending_setters:
193
- for set_fn in pending_setters:
194
- set_fn(inst, resolver)
195
-
196
- # optionally store for later (gives you a real attribute assignment later)
197
- if queue_setters:
198
- # merge if already present
199
- existing = getattr(inst, "_resource_setters", [])
200
- inst._resource_setters = [*existing, *pending_setters]
215
+
216
+ with hub.use(deserial_log):
217
+ t0 = perf_counter()
218
+ class_fields = cls.get_class_fields(class_type.__name__)
219
+
220
+ result: dict[str, Any] = {}
221
+ pending: list[tuple[str, Any]] = []
222
+
223
+ # bind hot callables
224
+ _coerce = cls._coerce_value
225
+ _hasres = cls._has_resource_value
226
+
227
+ log.debug("deserialize[%s]: keys=%s", class_type.__name__, list(data.keys()))
201
228
 
229
+ for name, typ in class_fields.items():
230
+ raw = data.get(name, None)
231
+ if raw is None:
232
+ continue
233
+ try:
234
+ val = _coerce(raw, typ)
235
+ except Exception:
236
+ log.exception("deserialize[%s]: coercion failed for field '%s' raw=%r",
237
+ class_type.__name__, name, raw)
238
+ raise
239
+ result[name] = val
240
+ if _hasres(val):
241
+ pending.append((name, val))
242
+
243
+ # instantiate
244
+ try:
245
+ inst = class_type(**result)
246
+ except TypeError:
247
+ log.exception("deserialize[%s]: __init__ failed with kwargs=%s",
248
+ class_type.__name__, list(result.keys()))
249
+ raise
250
+
251
+ # resolve now (optional)
252
+ if resolver and pending:
253
+ for attr, raw in pending:
254
+ try:
255
+ resolved = cls._resolve_structure(raw, resolver)
256
+ setattr(inst, attr, resolved)
257
+ except Exception:
258
+ log.exception("deserialize[%s]: resolver failed for '%s'", class_type.__name__, attr)
259
+ raise
260
+
261
+ # queue setters (store (attr, raw) tuples) — preserves your later-resolution behavior
262
+ if queue_setters and pending:
263
+ existing = getattr(inst, "_resource_setters", [])
264
+ inst._resource_setters = [*existing, *pending]
265
+
266
+
267
+ log.debug("deserialize[%s]: done in %.3f ms (resolved=%d, queued=%d)",
268
+ class_type.__name__, (perf_counter() - t0) * 1000,
269
+ int(bool(resolver)) * len(pending), len(pending))
202
270
  return inst
203
271
 
204
272
  @staticmethod
273
+ @lru_cache(maxsize=None)
205
274
  def get_class_fields(cls_name) -> Dict:
206
275
  # NOTE: keep imports local to avoid circulars
207
276
 
208
277
 
209
278
  fields = {
210
- "GedcomX": {"persons": List[Person]},
211
- "Conclusion": {
212
- "id": str,
213
- "lang": str,
214
- "sources": List["SourceReference"],
215
- "analysis": Document | Resource,
216
- "notes": List[Note],
217
- "confidence": ConfidenceLevel,
218
- "attribution": Attribution,
219
- "uri": "Resource",
220
- "max_note_count": int,
221
- "links": _rsLinkList,
222
- },
223
- "Subject": {
224
- "id": str,
225
- "lang": str,
226
- "sources": List["SourceReference"],
227
- "analysis": Resource,
228
- "notes": List["Note"],
229
- "confidence": ConfidenceLevel,
230
- "attribution": Attribution,
231
- "extracted": bool,
232
- "evidence": List[EvidenceReference],
233
- "media": List[SourceReference],
234
- "identifiers": IdentifierList,
235
- "uri": Resource,
236
- "links": _rsLinkList,
237
- },
238
- "Person": {
239
- "id": str,
240
- "lang": str,
241
- "sources": List[SourceReference],
242
- "analysis": Resource,
243
- "notes": List[Note],
244
- "confidence": ConfidenceLevel,
245
- "attribution": Attribution,
246
- "extracted": bool,
247
- "evidence": List[EvidenceReference],
248
- "media": List[SourceReference],
249
- "identifiers": IdentifierList,
250
- "private": bool,
251
- "gender": Gender,
252
- "names": List[Name],
253
- "facts": List[Fact],
254
- "living": bool,
255
- "links": _rsLinkList,
256
- #"uri": URI,
257
- },
258
- "SourceReference": {
259
- "description": Resource,
260
- "descriptionId": str,
261
- "attribution": Attribution,
262
- "qualifiers": List[Qualifier],
263
- },
264
- "Attribution": {
265
- "contributor": Resource,
266
- "modified": str,
267
- "changeMessage": str,
268
- "creator": Resource,
269
- "created": str,
270
- },
271
- "SourceDescription": {
272
- "id": str,
273
- "resourceType": ResourceType,
274
- "citations": List[SourceCitation],
275
- "mediaType": str,
276
- "about": URI,
277
- "mediator": Resource,
278
- "publisher": Resource, # forward-ref to avoid circular import
279
- "authors": List[Resource],
280
- "sources": List[SourceReference], # SourceReference
281
- "analysis": Resource, # analysis is typically a Document (kept union to avoid cycle)
282
- "componentOf": SourceReference, # SourceReference
283
- "titles": List[TextValue],
284
- "notes": List[Note],
285
- "attribution": Attribution,
286
- "rights": List[Resource],
287
- "coverage": List[Coverage], # Coverage
288
- "descriptions": List[TextValue],
289
- "identifiers": IdentifierList,
290
- "created": Date,
291
- "modified": Date,
292
- "published": Date,
293
- "repository": Agent, # forward-ref
294
- "max_note_count": int,
295
- },
296
- "Gender": {
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": GenderType,
305
- },
306
- "PlaceReference": {
307
- "original": str,
308
- "description": URI,
309
- },
310
- "Relationship": {
311
- "id": str,
312
- "lang": str,
313
- "sources": List[SourceReference],
314
- "analysis": Resource,
315
- "notes": List[Note],
316
- "confidence": ConfidenceLevel,
317
- "attribution": Attribution,
318
- "extracted": bool,
319
- "evidence": List[EvidenceReference],
320
- "media": List[SourceReference],
321
- "identifiers": IdentifierList,
322
- "type": RelationshipType,
323
- "person1": Resource,
324
- "person2": Resource,
325
- "facts": List[Fact],
326
- },
327
- "Document": {
328
- "id": str,
329
- "lang": str,
330
- "sources": List[SourceReference],
331
- "analysis": Resource,
332
- "notes": List[Note],
333
- "confidence": ConfidenceLevel,
334
- "attribution": Attribution,
335
- "type": DocumentType,
336
- "extracted": bool,
337
- "textType": TextType,
338
- "text": str,
339
- },
340
- "PlaceDescription": {
341
- "id": str,
342
- "lang": str,
343
- "sources": List[SourceReference],
344
- "analysis": Resource,
345
- "notes": List[Note],
346
- "confidence": ConfidenceLevel,
347
- "attribution": Attribution,
348
- "extracted": bool,
349
- "evidence": List[EvidenceReference],
350
- "media": List[SourceReference],
351
- "identifiers": List[IdentifierList],
352
- "names": List[TextValue],
353
- "type": str,
354
- "place": URI,
355
- "jurisdiction": Resource,
356
- "latitude": float,
357
- "longitude": float,
358
- "temporalDescription": Date,
359
- "spatialDescription": Resource,
360
- },
361
- "Agent": {
362
- "id": str,
363
- "identifiers": IdentifierList,
364
- "names": List[TextValue],
365
- "homepage": URI,
366
- "openid": URI,
367
- "accounts": List[OnlineAccount],
368
- "emails": List[URI],
369
- "phones": List[URI],
370
- "addresses": List[Address],
371
- "person": object | Resource, # intended Person | Resource
372
- "attribution": object, # GEDCOM5/7 compatibility
373
- "uri": URI | Resource,
374
- },
375
- "Event": {
376
- "id": str,
377
- "lang": str,
378
- "sources": List[SourceReference],
379
- "analysis": Resource,
380
- "notes": List[Note],
381
- "confidence": ConfidenceLevel,
382
- "attribution": Attribution,
383
- "extracted": bool,
384
- "evidence": List[EvidenceReference],
385
- "media": List[SourceReference],
386
- "identifiers": List[Identifier],
387
- "type": EventType,
388
- "date": Date,
389
- "place": PlaceReference,
390
- "roles": List[EventRole],
391
- },
392
- "EventRole": {
393
- "id:": str,
394
- "lang": str,
395
- "sources": List[SourceReference],
396
- "analysis": Resource,
397
- "notes": List[Note],
398
- "confidence": ConfidenceLevel,
399
- "attribution": Attribution,
400
- "person": Resource,
401
- "type": EventRoleType,
402
- "details": str,
403
- },
404
- "Resource":{
405
- "resource":URI,
406
- "id":str
407
- },
408
- "Qualifier":{
409
- "name":str,
410
- "value":str
411
- },
412
- "KnownSourceReference":{
413
- "name":str,
414
- "value":str
415
- },
416
- "Name": {
417
- "id": str,
418
- "lang": str,
419
- "sources": List[SourceReference],
420
- "analysis": Resource,
421
- "notes": List[Note],
422
- "confidence": ConfidenceLevel,
423
- "attribution": Attribution,
424
- "type": NameType,
425
- "nameForms": List[NameForm], # use string to avoid circulars if needed
426
- "date": Date,
427
- },
428
- "NameForm": {
429
- "lang": str,
430
- "fullText": str,
431
- "parts": List[NamePart], # use "NamePart" as a forward-ref to avoid circulars
432
- },
433
- "NamePart": {
434
- "type": NamePartType,
435
- "value": str,
436
- "qualifiers": List["NamePartQualifier"], # quote if you want to avoid circulars
437
- },
438
- "Fact":{
439
- "id": str,
440
- "lang": str,
441
- "sources": List[SourceReference],
442
- "analysis": Resource | Document,
443
- "notes": List[Note],
444
- "confidence": ConfidenceLevel,
445
- "attribution": Attribution,
446
- "type": FactType,
447
- "date": Date,
448
- "place": PlaceReference,
449
- "value": str,
450
- "qualifiers": List[FactQualifier],
451
- "links": _rsLinkList,
452
- }
453
- }
279
+ "Agent": {
280
+ "id": str,
281
+ "identifiers": IdentifierList,
282
+ "names": List[TextValue],
283
+ "homepage": URI,
284
+ "openid": URI,
285
+ "accounts": List[OnlineAccount],
286
+ "emails": List[URI],
287
+ "phones": List[URI],
288
+ "addresses": List[Address],
289
+ "person": object | Resource, # intended Person | Resource
290
+ "attribution": object, # GEDCOM5/7 compatibility
291
+ "uri": URI | Resource,
292
+ },
293
+ "Attribution": {
294
+ "contributor": Resource,
295
+ "modified": str,
296
+ "changeMessage": str,
297
+ "creator": Resource,
298
+ "created": str,
299
+ },
300
+ "Conclusion": {
301
+ "id": str,
302
+ "lang": str,
303
+ "sources": List["SourceReference"],
304
+ "analysis": Document | Resource,
305
+ "notes": List[Note],
306
+ "confidence": ConfidenceLevel,
307
+ "attribution": Attribution,
308
+ "uri": "Resource",
309
+ "max_note_count": int,
310
+ "links": _rsLinks,
311
+ },
312
+ "Date": {
313
+ "original": str,
314
+ "formal": str,
315
+ "normalized": str,
316
+ },
317
+ "Document": {
318
+ "id": str,
319
+ "lang": str,
320
+ "sources": List[SourceReference],
321
+ "analysis": Resource,
322
+ "notes": List[Note],
323
+ "confidence": ConfidenceLevel,
324
+ "attribution": Attribution,
325
+ "type": DocumentType,
326
+ "extracted": bool,
327
+ "textType": TextType,
328
+ "text": str,
329
+ },
330
+ "Event": {
331
+ "id": str,
332
+ "lang": str,
333
+ "sources": List[SourceReference],
334
+ "analysis": Resource,
335
+ "notes": List[Note],
336
+ "confidence": ConfidenceLevel,
337
+ "attribution": Attribution,
338
+ "extracted": bool,
339
+ "evidence": List[EvidenceReference],
340
+ "media": List[SourceReference],
341
+ "identifiers": List[Identifier],
342
+ "type": EventType,
343
+ "date": Date,
344
+ "place": PlaceReference,
345
+ "roles": List[EventRole],
346
+ },
347
+ "EventRole": {
348
+ "id:": str,
349
+ "lang": str,
350
+ "sources": List[SourceReference],
351
+ "analysis": Resource,
352
+ "notes": List[Note],
353
+ "confidence": ConfidenceLevel,
354
+ "attribution": Attribution,
355
+ "person": Resource,
356
+ "type": EventRoleType,
357
+ "details": str,
358
+ },
359
+ "Fact": {
360
+ "id": str,
361
+ "lang": str,
362
+ "sources": List[SourceReference],
363
+ "analysis": Resource | Document,
364
+ "notes": List[Note],
365
+ "confidence": ConfidenceLevel,
366
+ "attribution": Attribution,
367
+ "type": FactType,
368
+ "date": Date,
369
+ "place": PlaceReference,
370
+ "value": str,
371
+ "qualifiers": List[FactQualifier],
372
+ "links": _rsLinks,
373
+ },
374
+ "GedcomX": {
375
+ "persons": List[Person],
376
+ "relationships": List[Relationship],
377
+ "sourceDescriptions": List[SourceDescription],
378
+ "agents": List[Agent],
379
+ "places": List[PlaceDescription]
380
+ },
381
+ "Gender": {
382
+ "id": str,
383
+ "lang": str,
384
+ "sources": List[SourceReference],
385
+ "analysis": Resource,
386
+ "notes": List[Note],
387
+ "confidence": ConfidenceLevel,
388
+ "attribution": Attribution,
389
+ "type": GenderType,
390
+ },
391
+ "KnownSourceReference": {
392
+ "name": str,
393
+ "value": str,
394
+ },
395
+ "Name": {
396
+ "id": str,
397
+ "lang": str,
398
+ "sources": List[SourceReference],
399
+ "analysis": Resource,
400
+ "notes": List[Note],
401
+ "confidence": ConfidenceLevel,
402
+ "attribution": Attribution,
403
+ "type": NameType,
404
+ "nameForms": List[NameForm], # use string to avoid circulars if needed
405
+ "date": Date,
406
+ },
407
+ "NameForm": {
408
+ "lang": str,
409
+ "fullText": str,
410
+ "parts": List[NamePart], # use "NamePart" as a forward-ref to avoid circulars
411
+ },
412
+ "NamePart": {
413
+ "type": NamePartType,
414
+ "value": str,
415
+ "qualifiers": List["NamePartQualifier"], # quote if you want to avoid circulars
416
+ },
417
+ "Note":{"lang":str,
418
+ "subject":str,
419
+ "text":str,
420
+ "attribution": Attribution},
421
+ "Person": {
422
+ "id": str,
423
+ "lang": str,
424
+ "sources": List[SourceReference],
425
+ "analysis": Resource,
426
+ "notes": List[Note],
427
+ "confidence": ConfidenceLevel,
428
+ "attribution": Attribution,
429
+ "extracted": bool,
430
+ "evidence": List[EvidenceReference],
431
+ "media": List[SourceReference],
432
+ "identifiers": IdentifierList,
433
+ "private": bool,
434
+ "gender": Gender,
435
+ "names": List[Name],
436
+ "facts": List[Fact],
437
+ "living": bool,
438
+ "links": _rsLinks,
439
+ },
440
+ "PlaceDescription": {
441
+ "id": str,
442
+ "lang": str,
443
+ "sources": List[SourceReference],
444
+ "analysis": Resource,
445
+ "notes": List[Note],
446
+ "confidence": ConfidenceLevel,
447
+ "attribution": Attribution,
448
+ "extracted": bool,
449
+ "evidence": List[EvidenceReference],
450
+ "media": List[SourceReference],
451
+ "identifiers": List[IdentifierList],
452
+ "names": List[TextValue],
453
+ "type": str,
454
+ "place": URI,
455
+ "jurisdiction": Resource,
456
+ "latitude": float,
457
+ "longitude": float,
458
+ "temporalDescription": Date,
459
+ "spatialDescription": Resource,
460
+ },
461
+ "PlaceReference": {
462
+ "original": str,
463
+ "description": URI,
464
+ },
465
+ "Qualifier": {
466
+ "name": str,
467
+ "value": str,
468
+ },
469
+ "_rsLinks": {
470
+ "person":rsLink,
471
+ "portrait":rsLink},
472
+ "rsLink": {
473
+ "href": URI,
474
+ "template": str,
475
+ "type": str,
476
+ "accept": str,
477
+ "allow": str,
478
+ "hreflang": str,
479
+ "title": str,
480
+ },
481
+ "Relationship": {
482
+ "id": str,
483
+ "lang": str,
484
+ "sources": List[SourceReference],
485
+ "analysis": Resource,
486
+ "notes": List[Note],
487
+ "confidence": ConfidenceLevel,
488
+ "attribution": Attribution,
489
+ "extracted": bool,
490
+ "evidence": List[EvidenceReference],
491
+ "media": List[SourceReference],
492
+ "identifiers": IdentifierList,
493
+ "type": RelationshipType,
494
+ "person1": Resource,
495
+ "person2": Resource,
496
+ "facts": List[Fact],
497
+ },
498
+ "Resource": {
499
+ "resource": str,
500
+ "resourceId": str,
501
+ },
502
+ "SourceDescription": {
503
+ "id": str,
504
+ "resourceType": ResourceType,
505
+ "citations": List[SourceCitation],
506
+ "mediaType": str,
507
+ "about": URI,
508
+ "mediator": Resource,
509
+ "publisher": Resource, # forward-ref to avoid circular import
510
+ "authors": List[Resource],
511
+ "sources": List[SourceReference], # SourceReference
512
+ "analysis": Resource, # analysis is typically a Document (kept union to avoid cycle)
513
+ "componentOf": SourceReference, # SourceReference
514
+ "titles": List[TextValue],
515
+ "notes": List[Note],
516
+ "attribution": Attribution,
517
+ "rights": List[Resource],
518
+ "coverage": List[Coverage], # Coverage
519
+ "descriptions": List[TextValue],
520
+ "identifiers": IdentifierList,
521
+ "created": Date,
522
+ "modified": Date,
523
+ "published": Date,
524
+ "repository": Agent, # forward-ref
525
+ "max_note_count": int,
526
+ },
527
+ "SourceReference": {
528
+ "description": Resource,
529
+ "descriptionId": str,
530
+ "attribution": Attribution,
531
+ "qualifiers": List[Qualifier],
532
+ },
533
+ "Subject": {
534
+ "id": str,
535
+ "lang": str,
536
+ "sources": List["SourceReference"],
537
+ "analysis": Resource,
538
+ "notes": List["Note"],
539
+ "confidence": ConfidenceLevel,
540
+ "attribution": Attribution,
541
+ "extracted": bool,
542
+ "evidence": List[EvidenceReference],
543
+ "media": List[SourceReference],
544
+ "identifiers": IdentifierList,
545
+ "uri": Resource,
546
+ "links": _rsLinks,
547
+ },
548
+ "TextValue":{"lang":str,"value":str},
549
+ "URI": {
550
+ "value": str,
551
+ },
552
+
553
+ }
554
+
454
555
  return fields.get(cls_name, {})
455
556
 
456
557
 
@@ -495,7 +596,7 @@ class Serialization:
495
596
  if T is Resource:
496
597
  log.debug("COERCE str->Resource: %r", value)
497
598
  try:
498
- ret = Resource(uri=value)
599
+ ret = Resource(resourceId=value)
499
600
  log.debug("COERCE str->Resource: built %r", ret)
500
601
  return ret
501
602
  except Exception:
@@ -517,7 +618,7 @@ class Serialization:
517
618
  if T is Resource and isinstance(value, dict):
518
619
  log.debug("COERCE dict->Resource: %r", value)
519
620
  try:
520
- ret = Resource(uri=value.get("resource"), id=value.get("resourceId"))
621
+ ret = Resource(resource=value.get("resource"), resourceId=value.get("resourceId"))
521
622
  log.debug("COERCE dict->Resource: built %r", ret)
522
623
  return ret
523
624
  except Exception:
@@ -637,129 +738,7 @@ class Serialization:
637
738
  return value
638
739
 
639
740
 
640
- # Dict to Resource
641
- if T is Resource and isinstance(value, dict):
642
- log.debug("COERCE dict->Resource: %r", value)
643
- try:
644
- ret = Resource(uri=value.get("resource"), id=value.get("resourceId"))
645
- log.debug("COERCE dict->Resource: built %r", ret)
646
- return ret
647
- except Exception:
648
- log.exception("COERCE dict->Resource: failed for %r", value)
649
- return value
650
-
651
- # IdentifierList special
652
- if T is IdentifierList:
653
- log.debug("COERCE IdentifierList: %r", value)
654
- try:
655
- ret = IdentifierList._from_json_(value)
656
- log.debug("COERCE IdentifierList: built %r", ret)
657
- return ret
658
- except Exception:
659
- log.exception("COERCE IdentifierList: _from_json_ failed for %r", value)
660
- return value
661
-
662
- # Containers
663
- if self._is_list_like(T):
664
- elem_t = args[0] if args else Any
665
- log.debug("COERCE list-like: len=%s, elem_t=%r", len(value or []), elem_t)
666
- try:
667
- ret = [self._coerce_value(v, elem_t) for v in (value or [])]
668
- log.debug("COERCE list-like: result sample=%r", ret[:3] if isinstance(ret, list) else ret)
669
- return ret
670
- except Exception:
671
- log.exception("COERCE list-like: failed for value=%r elem_t=%r", value, elem_t)
672
- return value
673
-
674
- if self._is_set_like(T):
675
- elem_t = args[0] if args else Any
676
- log.debug("COERCE set-like: len=%s, elem_t=%r", len(value or []), elem_t)
677
- try:
678
- ret = {self._coerce_value(v, elem_t) for v in (value or [])}
679
- log.debug("COERCE set-like: result size=%d", len(ret))
680
- return ret
681
- except Exception:
682
- log.exception("COERCE set-like: failed for value=%r elem_t=%r", value, elem_t)
683
- return value
684
-
685
- if self._is_tuple_like(T):
686
- log.debug("COERCE tuple-like: value=%r, args=%r", value, args)
687
- try:
688
- if not value:
689
- log.debug("COERCE tuple-like: empty/None -> ()")
690
- return tuple(value or ())
691
- if len(args) == 2 and args[1] is Ellipsis:
692
- elem_t = args[0]
693
- ret = tuple(self._coerce_value(v, elem_t) for v in (value or ()))
694
- log.debug("COERCE tuple-like variadic: size=%d", len(ret))
695
- return ret
696
- ret = tuple(self._coerce_value(v, t) for v, t in zip(value, args))
697
- log.debug("COERCE tuple-like fixed: size=%d", len(ret))
698
- return ret
699
- except Exception:
700
- log.exception("COERCE tuple-like: failed for value=%r args=%r", value, args)
701
- return value
702
-
703
- if self._is_dict_like(T):
704
- k_t = args[0] if len(args) >= 1 else Any
705
- v_t = args[1] if len(args) >= 2 else Any
706
- log.debug("COERCE dict-like: keys=%s, k_t=%r, v_t=%r", len((value or {}).keys()), k_t, v_t)
707
- try:
708
- ret = {
709
- self._coerce_value(k, k_t): self._coerce_value(v, v_t)
710
- for k, v in (value or {}).items()
711
- }
712
- log.debug("COERCE dict-like: result size=%d", len(ret))
713
- return ret
714
- except Exception:
715
- log.exception("COERCE dict-like: failed for value=%r k_t=%r v_t=%r", value, k_t, v_t)
716
- return value
717
-
718
- # Objects via registry
719
- if isinstance(T, type) and isinstance(value, dict):
720
- fields = self.get_class_fields(T.__name__) or {}
721
- log.debug(
722
- "COERCE object: class=%s, input_keys=%s, registered_fields=%s",
723
- T.__name__, list(value.keys()), list(fields.keys())
724
- )
725
- if fields:
726
- kwargs = {}
727
- present = []
728
- for fname, ftype in fields.items():
729
- if fname in value:
730
- resolved = self._resolve_forward(self._unwrap(ftype))
731
- log.debug("COERCE object.field: %s.%s -> %r, raw=%r", T.__name__, fname, resolved, value[fname])
732
- try:
733
- coerced = self._coerce_value(value[fname], resolved)
734
- kwargs[fname] = coerced
735
- present.append(fname)
736
- log.debug("COERCE object.field: %s.%s coerced -> %r", T.__name__, fname, coerced)
737
- except Exception:
738
- log.exception("COERCE object.field: %s.%s failed", T.__name__, fname)
739
- unknown = [k for k in value.keys() if k not in fields]
740
- if unknown:
741
- log.debug("COERCE object: %s unknown keys ignored: %s", T.__name__, unknown)
742
- try:
743
- log.debug("COERCE object: instantiate %s(**%s)", T.__name__, present)
744
- ret = T(**kwargs)
745
- log.debug("COERCE object: success -> %r", ret)
746
- return ret
747
- except TypeError as e:
748
- log.warning("COERCE object: instantiate %s failed with kwargs=%s: %s", T.__name__, list(kwargs.keys()), e)
749
- log.debug("COERCE object: returning partially coerced dict")
750
- return kwargs
751
-
752
- # Already correct type?
753
- try:
754
- if isinstance(value, T):
755
- log.debug("COERCE passthrough: value already instance of %r", T)
756
- return value
757
- except TypeError:
758
- log.debug("COERCE isinstance not applicable: T=%r", T)
759
-
760
- log.debug("COERCE fallback: returning original value=%r (type=%s)", value, type(value).__name__)
761
- return value
762
-
741
+
763
742
  # -------------------------- TYPE HELPERS --------------------------
764
743
 
765
744
  @staticmethod