gedcom-x 0.5.9__py3-none-any.whl → 0.5.11__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 (48) hide show
  1. {gedcom_x-0.5.9.dist-info → gedcom_x-0.5.11.dist-info}/METADATA +1 -1
  2. gedcom_x-0.5.11.dist-info/RECORD +57 -0
  3. gedcomx/Extensions/rs10/rsLink.py +2 -1
  4. gedcomx/__init__.py +8 -3
  5. gedcomx/address.py +3 -0
  6. gedcomx/agent.py +11 -6
  7. gedcomx/attribution.py +3 -1
  8. gedcomx/conclusion.py +10 -6
  9. gedcomx/converter.py +92 -27
  10. gedcomx/coverage.py +3 -1
  11. gedcomx/date.py +3 -0
  12. gedcomx/document.py +3 -1
  13. gedcomx/event.py +3 -0
  14. gedcomx/evidence_reference.py +30 -3
  15. gedcomx/extensible.py +86 -0
  16. gedcomx/fact.py +6 -2
  17. gedcomx/gedcom5x.py +21 -3
  18. gedcomx/gedcom7/GedcomStructure.py +1 -3
  19. gedcomx/gedcom7/__init__.py +1 -1
  20. gedcomx/gedcom7/gedcom7.py +3 -3
  21. gedcomx/gedcom7/specification.py +4817 -0
  22. gedcomx/gedcomx.py +3 -0
  23. gedcomx/gender.py +5 -1
  24. gedcomx/group.py +11 -2
  25. gedcomx/identifier.py +5 -2
  26. gedcomx/logging_hub.py +132 -22
  27. gedcomx/name.py +15 -6
  28. gedcomx/note.py +25 -10
  29. gedcomx/online_account.py +20 -0
  30. gedcomx/person.py +8 -6
  31. gedcomx/place_description.py +3 -1
  32. gedcomx/place_reference.py +5 -2
  33. gedcomx/qualifier.py +2 -0
  34. gedcomx/relationship.py +8 -5
  35. gedcomx/resource.py +20 -6
  36. gedcomx/schemas.py +530 -0
  37. gedcomx/serialization.py +36 -16
  38. gedcomx/source_citation.py +22 -0
  39. gedcomx/source_description.py +22 -18
  40. gedcomx/source_reference.py +25 -3
  41. gedcomx/subject.py +2 -3
  42. gedcomx/textvalue.py +19 -4
  43. gedcomx/uri.py +8 -6
  44. gedcom_x-0.5.9.dist-info/RECORD +0 -56
  45. gedcomx/Logging.py +0 -19
  46. gedcomx/gedcom7/Specification.py +0 -347
  47. {gedcom_x-0.5.9.dist-info → gedcom_x-0.5.11.dist-info}/WHEEL +0 -0
  48. {gedcom_x-0.5.9.dist-info → gedcom_x-0.5.11.dist-info}/top_level.txt +0 -0
gedcomx/schemas.py ADDED
@@ -0,0 +1,530 @@
1
+
2
+ from __future__ import annotations
3
+
4
+ import inspect
5
+ import sys
6
+ import types
7
+ from typing import Any, Dict, Union, get_args, get_origin, get_type_hints
8
+
9
+ try:
10
+ # typing.Annotated may not exist in older 3.9 without typing_extensions
11
+ from typing import Annotated # type: ignore
12
+ except Exception: # pragma: no cover
13
+ Annotated = None # type: ignore
14
+
15
+ _UNION_ORIGINS = tuple(
16
+ x for x in (Union, getattr(types, "UnionType", None)) if x is not None
17
+ )
18
+
19
+ """
20
+ ======================================================================
21
+ Project: Gedcom-X
22
+ File: schema.py
23
+ Author: David J. Cartwright
24
+ Purpose: provide schema for serializatin and extensibility
25
+
26
+ Created: 2025-08-25
27
+ Updated:
28
+ - 2025-09-03: _from_json_ refactor
29
+ - 2025-09-09: added schema_class
30
+
31
+ ======================================================================
32
+ """
33
+
34
+
35
+
36
+
37
+
38
+ class Schema:
39
+ """
40
+ Central registry of fields for classes.
41
+
42
+ - field_type_table: {"ClassName": {"field": <type or type-string>}}
43
+ - URI/Resource preference in unions: URI > Resource > first-declared
44
+ - Optional/None is stripped
45
+ - Containers are preserved; their inner args are normalized recursively
46
+ """
47
+
48
+ def __init__(self) -> None:
49
+ self.field_type_table: Dict[str, Dict[str, Any]] = {}
50
+ self._extras: Dict[str, Dict[str, Any]] = {}
51
+ self._toplevel: Dict[str, Dict[str, Any]] = {}
52
+
53
+ # Optional binding to concrete classes to avoid name-only matching.
54
+ self._uri_cls: type | None = None
55
+ self._resource_cls: type | None = None
56
+
57
+ # ──────────────────────────────
58
+ # Bind concrete classes (optional)
59
+ # ──────────────────────────────
60
+ def set_uri_class(self, cls: type | None) -> None:
61
+ self._uri_cls = cls
62
+
63
+ def set_resource_class(self, cls: type | None) -> None:
64
+ self._resource_cls = cls
65
+
66
+ # ──────────────────────────────
67
+ # Public API
68
+ # ──────────────────────────────
69
+ def register_class(
70
+ self,
71
+ cls: type,
72
+ *,
73
+ mapping: Dict[str, Any] | None = None,
74
+ include_bases: bool = True,
75
+ use_annotations: bool = True,
76
+ use_init: bool = True,
77
+ overwrite: bool = False,
78
+ ignore: set[str] | None = None,
79
+ toplevel: bool = False,
80
+ toplevel_meta: Dict[str, Any] | None = None,
81
+ ) -> None:
82
+ """
83
+ Introspect and register fields for a class.
84
+
85
+ - reads class __annotations__ (preferred) or __init__ annotations
86
+ - merges base classes (MRO) if include_bases=True
87
+ - applies `mapping` overrides last
88
+ - normalizes each type:
89
+ strip Optional → prefer URI/Resource → collapse union to single
90
+ """
91
+ cname = cls.__name__
92
+ ignore = ignore or set()
93
+
94
+ def collect(c: type) -> Dict[str, Any]:
95
+ d: Dict[str, Any] = {}
96
+ if use_annotations:
97
+ d.update(self._get_hints_from_class(c))
98
+ if use_init and not d:
99
+ d.update(self._get_hints_from_init(c))
100
+ # filter private / ignored
101
+ for k in list(d.keys()):
102
+ if k in ignore or k.startswith("_"):
103
+ d.pop(k, None)
104
+ # normalize each
105
+ for k, v in list(d.items()):
106
+ d[k] = self._normalize_field_type(v)
107
+ return d
108
+
109
+ fields: Dict[str, Any] = {}
110
+ classes = list(reversed(cls.mro())) if include_bases else [cls]
111
+ for c in classes:
112
+ if c is object:
113
+ continue
114
+ fields.update(collect(c))
115
+
116
+ if mapping:
117
+ for k, v in mapping.items():
118
+ fields[k] = self._normalize_field_type(v)
119
+
120
+ if not overwrite and cname in self.field_type_table:
121
+ self.field_type_table[cname].update(fields)
122
+ else:
123
+ self.field_type_table[cname] = fields
124
+
125
+ if toplevel:
126
+ self._toplevel[cname] = dict(toplevel_meta or {})
127
+
128
+ def register_extra(
129
+ self,
130
+ cls: type,
131
+ name: str,
132
+ typ: Any,
133
+ *,
134
+ overwrite: bool = False,
135
+ ) -> None:
136
+ """Register a single extra field (normalized)."""
137
+ cname = cls.__name__
138
+ self.field_type_table.setdefault(cname, {})
139
+ if not overwrite and name in self.field_type_table[cname]:
140
+ return
141
+ nt = self._normalize_field_type(typ)
142
+ self.field_type_table[cname][name] = nt
143
+ self._extras.setdefault(cname, {})[name] = nt
144
+
145
+ def normalize_all(self) -> None:
146
+ """Re-run normalization across all registered fields."""
147
+ for _, fields in self.field_type_table.items():
148
+ for k, v in list(fields.items()):
149
+ fields[k] = self._normalize_field_type(v)
150
+
151
+ # lookups
152
+ def get_class_fields(self, type_name: str) -> Dict[str, Any] | None:
153
+ return self.field_type_table.get(type_name)
154
+
155
+ def set_toplevel(self, cls: type, *, meta: Dict[str, Any] | None = None) -> None:
156
+ self._toplevel[cls.__name__] = dict(meta or {})
157
+
158
+ def is_toplevel(self, cls_or_name: type | str) -> bool:
159
+ name = cls_or_name if isinstance(cls_or_name, str) else cls_or_name.__name__
160
+ return name in self._toplevel
161
+
162
+ def get_toplevel(self) -> Dict[str, Dict[str, Any]]:
163
+ return dict(self._toplevel)
164
+
165
+ def get_extras(self, cls_or_name: type | str) -> Dict[str, Any]:
166
+ name = cls_or_name if isinstance(cls_or_name, str) else cls_or_name.__name__
167
+ return dict(self._extras.get(name, {}))
168
+
169
+ @property
170
+ def json(self) -> dict[str, dict[str, str]]:
171
+ return schema_to_jsonable(self)
172
+
173
+ # ──────────────────────────────
174
+ # Introspection helpers
175
+ # ──────────────────────────────
176
+ def _get_hints_from_class(self, cls: type) -> Dict[str, Any]:
177
+ """Resolved type hints from class annotations; fallback to raw __annotations__."""
178
+ module = sys.modules.get(cls.__module__)
179
+ gns = module.__dict__ if module else {}
180
+ lns = dict(vars(cls))
181
+ try:
182
+ return get_type_hints(cls, include_extras=True, globalns=gns, localns=lns)
183
+ except Exception:
184
+ return dict(getattr(cls, "__annotations__", {}) or {})
185
+
186
+ def _get_hints_from_init(self, cls: type) -> Dict[str, Any]:
187
+ """Parameter annotations from __init__ (excluding self/return/*args/**kwargs)."""
188
+ fn = cls.__dict__.get("__init__", getattr(cls, "__init__", None))
189
+ if not callable(fn):
190
+ return {}
191
+ module = sys.modules.get(cls.__module__)
192
+ gns = module.__dict__ if module else {}
193
+ lns = dict(vars(cls))
194
+ try:
195
+ hints = get_type_hints(fn, include_extras=True, globalns=gns, localns=lns)
196
+ except Exception:
197
+ hints = dict(getattr(fn, "__annotations__", {}) or {})
198
+ hints.pop("return", None)
199
+ hints.pop("self", None)
200
+ # drop *args/**kwargs
201
+ sig = inspect.signature(fn)
202
+ for pname, p in list(sig.parameters.items()):
203
+ if p.kind in (p.VAR_POSITIONAL, p.VAR_KEYWORD):
204
+ hints.pop(pname, None)
205
+ return hints
206
+
207
+ # ──────────────────────────────
208
+ # Normalization pipeline
209
+ # strip Optional -> prefer URI/Resource -> collapse unions
210
+ # (recurse into containers)
211
+ # ──────────────────────────────
212
+ def _normalize_field_type(self, tp: Any) -> Any:
213
+ tp = self._strip_optional(tp)
214
+ tp = self._prefer_uri_or_resource_or_first(tp)
215
+ tp = self._collapse_unions(tp)
216
+ return tp
217
+
218
+ # 1) Remove None from unions and strip Optional[...] wrappers
219
+ def _strip_optional(self, tp: Any) -> Any:
220
+ if isinstance(tp, str):
221
+ return self._strip_optional_str(tp)
222
+
223
+ origin = get_origin(tp)
224
+ args = get_args(tp)
225
+
226
+ if Annotated is not None and origin is Annotated:
227
+ return self._strip_optional(args[0])
228
+
229
+ if origin in _UNION_ORIGINS:
230
+ kept = tuple(a for a in args if a is not type(None)) # noqa: E721
231
+ if not kept:
232
+ return Any
233
+ if len(kept) == 1:
234
+ return self._strip_optional(kept[0])
235
+ # rebuild union (still a union; later steps will collapse)
236
+ return self._rebuild_union_tuple(kept)
237
+
238
+ if origin in (list, set, tuple, dict):
239
+ sub = tuple(self._strip_optional(a) for a in args)
240
+ return self._rebuild_param(origin, sub, fallback=tp)
241
+
242
+ return tp
243
+
244
+ # 2) In any union, prefer URI (if present), else Resource (if present), else first declared
245
+ def _prefer_uri_or_resource_or_first(self, tp: Any) -> Any:
246
+ if isinstance(tp, str):
247
+ return self._prefer_str(tp)
248
+
249
+ origin = get_origin(tp)
250
+ args = get_args(tp)
251
+
252
+ if Annotated is not None and origin is Annotated:
253
+ return self._prefer_uri_or_resource_or_first(args[0])
254
+
255
+ if origin in _UNION_ORIGINS:
256
+ # order is as-declared in typing args
257
+ names = [self._name_of(a) for a in args]
258
+ if "URI" in names:
259
+ i = names.index("URI")
260
+ return args[i] if not isinstance(args[i], str) else "URI"
261
+ if "Resource" in names:
262
+ i = names.index("Resource")
263
+ return args[i] if not isinstance(args[i], str) else "Resource"
264
+ # no preferred present → pick first declared
265
+ return self._prefer_uri_or_resource_or_first(args[0])
266
+
267
+ if origin in (list, set, tuple, dict):
268
+ sub = tuple(self._prefer_uri_or_resource_or_first(a) for a in args)
269
+ return self._rebuild_param(origin, sub, fallback=tp)
270
+
271
+ return tp
272
+
273
+ # 3) If any union still remains (it shouldn't after step 2), collapse to first
274
+ def _collapse_unions(self, tp: Any) -> Any:
275
+ if isinstance(tp, str):
276
+ return self._collapse_unions_str(tp)
277
+
278
+ origin = get_origin(tp)
279
+ args = get_args(tp)
280
+
281
+ if Annotated is not None and origin is Annotated:
282
+ return self._collapse_unions(args[0])
283
+
284
+ if origin in _UNION_ORIGINS:
285
+ return self._collapse_unions(args[0]) # first only
286
+
287
+ if origin in (list, set, tuple, dict):
288
+ sub = tuple(self._collapse_unions(a) for a in args)
289
+ return self._rebuild_param(origin, sub, fallback=tp)
290
+
291
+ return tp
292
+
293
+ # ──────────────────────────────
294
+ # Helpers: typing objects
295
+ # ──────────────────────────────
296
+ def _name_of(self, a: Any) -> str:
297
+ if isinstance(a, str):
298
+ return a.strip()
299
+ if isinstance(a, type):
300
+ return getattr(a, "__name__", str(a))
301
+ # typing objects: str(...) fallback
302
+ return str(a).replace("typing.", "")
303
+
304
+ def _rebuild_union_tuple(self, args: tuple[Any, ...]) -> Any:
305
+ # Rebuild a typing-style union from args (for 3.10+ this becomes A|B)
306
+ out = args[0]
307
+ for a in args[1:]:
308
+ try:
309
+ out = out | a # type: ignore[operator]
310
+ except TypeError:
311
+ out = Union[(out, a)] # type: ignore[index]
312
+ return out
313
+
314
+ def _rebuild_param(self, origin: Any, sub: tuple[Any, ...], *, fallback: Any) -> Any:
315
+ try:
316
+ return origin[sub] if sub else origin
317
+ except TypeError:
318
+ return fallback
319
+
320
+ # ──────────────────────────────
321
+ # Helpers: strings
322
+ # ──────────────────────────────
323
+ def _strip_optional_str(self, s: str) -> str:
324
+ s = s.strip().replace("typing.", "")
325
+ # peel Optional[...] wrappers
326
+ while s.startswith("Optional[") and s.endswith("]"):
327
+ s = s[len("Optional["):-1].strip()
328
+
329
+ # Union[A, B, None]
330
+ if s.startswith("Union[") and s.endswith("]"):
331
+ inner = s[len("Union["):-1].strip()
332
+ parts = [p for p in self._split_top_level(inner, ",") if p.strip() not in ("None", "NoneType", "")]
333
+ return f"Union[{', '.join(parts)}]" if len(parts) > 1 else (parts[0].strip() if parts else "Any")
334
+
335
+ # A | B | None
336
+ if "|" in s:
337
+ parts = [p.strip() for p in self._split_top_level(s, "|")]
338
+ parts = [p for p in parts if p not in ("None", "NoneType", "")]
339
+ return " | ".join(parts) if len(parts) > 1 else (parts[0] if parts else "Any")
340
+
341
+ # containers: normalize inside
342
+ for head in ("List", "Set", "Tuple", "Dict", "Annotated"):
343
+ if s.startswith(head + "[") and s.endswith("]"):
344
+ inner = s[len(head) + 1:-1]
345
+ if head == "Annotated":
346
+ # Annotated[T, ...] -> T
347
+ items = self._split_top_level(inner, ",")
348
+ return self._strip_optional_str(items[0].strip()) if items else "Any"
349
+ if head in ("List", "Set"):
350
+ elem = self._strip_optional_str(inner)
351
+ return f"{head}[{elem}]"
352
+ if head == "Tuple":
353
+ elems = [self._strip_optional_str(p.strip()) for p in self._split_top_level(inner, ",")]
354
+ return f"Tuple[{', '.join(elems)}]"
355
+ if head == "Dict":
356
+ kv = self._split_top_level(inner, ",")
357
+ k = self._strip_optional_str(kv[0].strip()) if kv else "Any"
358
+ v = self._strip_optional_str(kv[1].strip()) if len(kv) > 1 else "Any"
359
+ return f"Dict[{k}, {v}]"
360
+
361
+ return s
362
+
363
+ def _prefer_str(self, s: str) -> str:
364
+ s = s.strip().replace("typing.", "")
365
+ # Union[...] form
366
+ if s.startswith("Union[") and s.endswith("]"):
367
+ inner = s[len("Union["):-1]
368
+ parts = [p.strip() for p in self._split_top_level(inner, ",")]
369
+ if "URI" in parts:
370
+ return "URI"
371
+ if "Resource" in parts:
372
+ return "Resource"
373
+ return parts[0] if parts else "Any"
374
+
375
+ # PEP 604 bars
376
+ if "|" in s:
377
+ parts = [p.strip() for p in self._split_top_level(s, "|")]
378
+ if "URI" in parts:
379
+ return "URI"
380
+ if "Resource" in parts:
381
+ return "Resource"
382
+ return parts[0] if parts else "Any"
383
+
384
+ # containers
385
+ for head in ("List", "Set", "Tuple", "Dict", "Annotated"):
386
+ if s.startswith(head + "[") and s.endswith("]"):
387
+ inner = s[len(head) + 1:-1]
388
+ if head == "Annotated":
389
+ items = self._split_top_level(inner, ",")
390
+ return self._prefer_str(items[0].strip()) if items else "Any"
391
+ if head in ("List", "Set"):
392
+ elem = self._prefer_str(inner)
393
+ return f"{head}[{elem}]"
394
+ if head == "Tuple":
395
+ elems = [self._prefer_str(p.strip()) for p in self._split_top_level(inner, ",")]
396
+ return f"Tuple[{', '.join(elems)}]"
397
+ if head == "Dict":
398
+ kv = self._split_top_level(inner, ",")
399
+ k = self._prefer_str(kv[0].strip()) if kv else "Any"
400
+ v = self._prefer_str(kv[1].strip()) if len(kv) > 1 else "Any"
401
+ return f"Dict[{k}, {v}]"
402
+
403
+ return s
404
+
405
+ def _collapse_unions_str(self, s: str) -> str:
406
+ s = s.strip().replace("typing.", "")
407
+ if s.startswith("Union[") and s.endswith("]"):
408
+ inner = s[len("Union["):-1]
409
+ parts = [p.strip() for p in self._split_top_level(inner, ",")]
410
+ return parts[0] if parts else "Any"
411
+ if "|" in s:
412
+ parts = [p.strip() for p in self._split_top_level(s, "|")]
413
+ return parts[0] if parts else "Any"
414
+
415
+ # containers
416
+ for head in ("List", "Set", "Tuple", "Dict", "Annotated"):
417
+ if s.startswith(head + "[") and s.endswith("]"):
418
+ inner = s[len(head) + 1:-1]
419
+ if head == "Annotated":
420
+ items = self._split_top_level(inner, ",")
421
+ return self._collapse_unions_str(items[0].strip()) if items else "Any"
422
+ if head in ("List", "Set"):
423
+ elem = self._collapse_unions_str(inner)
424
+ return f"{head}[{elem}]"
425
+ if head == "Tuple":
426
+ elems = [self._collapse_unions_str(p.strip()) for p in self._split_top_level(inner, ",")]
427
+ return f"Tuple[{', '.join(elems)}]"
428
+ if head == "Dict":
429
+ kv = self._split_top_level(inner, ",")
430
+ k = self._collapse_unions_str(kv[0].strip()) if kv else "Any"
431
+ v = self._collapse_unions_str(kv[1].strip()) if len(kv) > 1 else "Any"
432
+ return f"Dict[{k}, {v}]"
433
+ return s
434
+
435
+ def _split_top_level(self, s: str, sep: str) -> list[str]:
436
+ """
437
+ Split `s` by single-char separator (',' or '|') at top level (not inside brackets).
438
+ """
439
+ out: list[str] = []
440
+ buf: list[str] = []
441
+ depth = 0
442
+ for ch in s:
443
+ if ch == "[":
444
+ depth += 1
445
+ elif ch == "]":
446
+ depth -= 1
447
+ if ch == sep and depth == 0:
448
+ out.append("".join(buf).strip())
449
+ buf = []
450
+ else:
451
+ buf.append(ch)
452
+ out.append("".join(buf).strip())
453
+ return [p for p in out if p != ""]
454
+
455
+ # ──────────────────────────────
456
+ # Stringification helpers (JSON dump)
457
+ # ──────────────────────────────
458
+ def type_repr(tp: Any) -> str:
459
+ if isinstance(tp, str):
460
+ return tp
461
+
462
+ origin = get_origin(tp)
463
+ args = get_args(tp)
464
+
465
+ if origin is None:
466
+ if isinstance(tp, type):
467
+ return tp.__name__
468
+ return str(tp).replace("typing.", "")
469
+
470
+ if origin in _UNION_ORIGINS:
471
+ return " | ".join(type_repr(a) for a in args)
472
+
473
+ if origin is list:
474
+ return f"List[{type_repr(args[0])}]" if args else "List[Any]"
475
+ if origin is set:
476
+ return f"Set[{type_repr(args[0])}]" if args else "Set[Any]"
477
+ if origin is tuple:
478
+ return "Tuple[" + ", ".join(type_repr(a) for a in args) + "]" if args else "Tuple"
479
+ if origin is dict:
480
+ k, v = args or (Any, Any)
481
+ return f"Dict[{type_repr(k)}, {type_repr(v)}]"
482
+
483
+ return str(tp).replace("typing.", "")
484
+
485
+
486
+ def schema_to_jsonable(schema: Schema) -> dict[str, dict[str, str]]:
487
+ out: dict[str, dict[str, str]] = {}
488
+ for cls_name, fields in schema.field_type_table.items():
489
+ out[cls_name] = {fname: type_repr(ftype) for fname, ftype in fields.items()}
490
+ return out
491
+
492
+
493
+
494
+
495
+
496
+ def schema_class(
497
+ mapping: Dict[str, Any] | None = None,
498
+ *,
499
+ include_bases: bool = True,
500
+ use_annotations: bool = True,
501
+ use_init: bool = True,
502
+ overwrite: bool = False,
503
+ ignore: set[str] | None = None,
504
+ toplevel: bool = False,
505
+ toplevel_meta: Dict[str, Any] | None = None,
506
+ ):
507
+ """Decorator to register a class with SCHEMA at import time."""
508
+ def deco(cls: type):
509
+ SCHEMA.register_class(
510
+ cls,
511
+ mapping=mapping,
512
+ include_bases=include_bases,
513
+ use_annotations=use_annotations,
514
+ use_init=use_init,
515
+ overwrite=overwrite,
516
+ ignore=ignore,
517
+ toplevel=toplevel,
518
+ toplevel_meta=toplevel_meta,
519
+ )
520
+ return cls
521
+ return deco
522
+
523
+
524
+ # Singleton instance
525
+ SCHEMA = Schema()
526
+
527
+
528
+
529
+
530
+
gedcomx/serialization.py CHANGED
@@ -51,6 +51,7 @@ from .place_reference import PlaceReference
51
51
  from .qualifier import Qualifier
52
52
  from .relationship import Relationship, RelationshipType
53
53
  from .resource import Resource
54
+ from .schemas import SCHEMA
54
55
  from .source_description import SourceDescription, ResourceType, SourceCitation, Coverage
55
56
  from .source_reference import SourceReference
56
57
  from .textvalue import TextValue
@@ -74,6 +75,10 @@ class Serialization:
74
75
  def serialize(obj: object):
75
76
  if obj is not None:
76
77
  with hub.use(serial_log):
78
+ if SCHEMA.is_toplevel(type(obj)):
79
+ if hub.logEnabled: log.debug("-" * 20)
80
+ if hub.logEnabled: log.debug(f"Serializing TOP LEVEL TYPE '{type(obj).__name__}'")
81
+ #if hub.logEnabled: log.debug(f"Serializing a '{type(obj).__name__}'")
77
82
 
78
83
  if isinstance(obj, (str, int, float, bool, type(None))):
79
84
  return obj
@@ -82,43 +87,58 @@ class Serialization:
82
87
  if isinstance(obj, URI):
83
88
  return obj.value
84
89
  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
90
+ if hub.logEnabled: log.debug(f"'{type(obj).__name__}' is an (list, tuple, set)")
91
+ if len(obj) == 0:
92
+ if hub.logEnabled: log.debug(f"'{type(obj).__name__}' is an empty (list, tuple, set)")
93
+ return None
94
+ #l = [Serialization.serialize(v) for v in obj]
95
+ r_l = []
96
+ for i, item in enumerate(obj):
97
+ if hub.logEnabled: log.debug(f"Serializing item {i} of '{type(obj).__name__}'")
98
+ r_l.append(Serialization.serialize(item) )
99
+ return r_l
100
+
88
101
  if type(obj).__name__ == 'Collection':
89
102
  l= [Serialization.serialize(v) for v in obj]
90
103
  if len(l) == 0: return None
91
104
  return l
92
105
  if isinstance(obj, enum.Enum):
106
+ if hub.logEnabled: log.debug(f"'{type(obj).__name__}' is an eNum")
93
107
  return Serialization.serialize(obj.value)
94
108
 
95
- log.debug(f"Serializing a '{type(obj).__name__}'")
109
+ #if hub.logEnabled: log.debug(f"Serializing a '{type(obj).__name__}'")
96
110
  type_as_dict = {}
97
- fields = Serialization.get_class_fields(type(obj).__name__)
111
+ fields = SCHEMA.get_class_fields(type(obj).__name__)
98
112
  if fields:
99
113
  for field_name, type_ in fields.items():
100
114
  if hasattr(obj,field_name):
115
+
101
116
  if (v := getattr(obj,field_name)) is not None:
117
+ if hub.logEnabled: log.debug(f"Found {type(obj).__name__}.{field_name} with a '{type_}'")
102
118
  if type_ == Resource:
103
- log.error(f"Refering to a {type(obj).__name__} with a '{type_}'")
119
+ log.debug(f"Refering to a {type(obj).__name__}.{field_name} with a '{type_}'")
104
120
  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_}'")
121
+ type_as_dict[field_name] = Serialization.serialize(res.value)
122
+ elif type_ == URI or type_ == 'URI':
123
+ log.debug(f"Refering to a {type(obj).__name__}.{field_name} with a '{type_}'")
109
124
  uri = URI(target=v)
110
- sv = Serialization.serialize(uri)
111
- type_as_dict[field_name] = sv
125
+ type_as_dict[field_name] = uri.value
112
126
  elif (sv := Serialization.serialize(v)) is not None:
127
+ if hub.logEnabled: log.debug(f"Fall through, {type(obj).__name__}.{field_name}'")
113
128
  type_as_dict[field_name] = sv
114
129
  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})'")
130
+ if hub.logEnabled: log.warning(f"{type(obj).__name__} did not have field '{field_name}'")
131
+ #if type_as_dict == {}: log.error(f"Serialized a '{type(obj).__name__}' with empty fields: '{fields}'")
132
+ #else:
133
+ #if hub.logEnabled: log.debug(f"Serialized a '{type(obj).__name__}' with fields '{type_as_dict})'")
134
+ if hub.logEnabled: log.debug(f"<- Serialized a '%s'",type(obj).__name__)
118
135
  #return Serialization._serialize_dict(type_as_dict)
119
136
  return type_as_dict if type_as_dict != {} else None
137
+ elif hasattr(obj,'_serializer'):
138
+ if hub.logEnabled: log.debug(f"'%s' has a serializer, using it.",type(obj).__name__)
139
+ return getattr(obj,'_serializer')
120
140
  else:
121
- log.error(f"Could not find fields for {type(obj).__name__}")
141
+ if hub.logEnabled: log.error(f"Could not find fields for {type(obj).__name__}")
122
142
  return None
123
143
 
124
144
  @staticmethod
@@ -1,3 +1,24 @@
1
+ """
2
+ ======================================================================
3
+ Project: Gedcom-X
4
+ File: source_citation.py
5
+ Author: David J. Cartwright
6
+ Purpose:
7
+
8
+ Created: 2025-07-25
9
+ Updated:
10
+ - 2025-09-09 added schema_class
11
+
12
+
13
+ ======================================================================
14
+ """
15
+
16
+ """
17
+ ======================================================================
18
+ GEDCOM Module Types
19
+ ======================================================================
20
+ """
21
+ from .schemas import schema_class
1
22
  from typing import Optional
2
23
  from .logging_hub import hub, logging
3
24
  """
@@ -9,6 +30,7 @@ log = logging.getLogger("gedcomx")
9
30
  serial_log = "gedcomx.serialization"
10
31
  #=====================================================================
11
32
 
33
+ @schema_class()
12
34
  class SourceCitation:
13
35
  identifier = 'http://gedcomx.org/v1/SourceCitation'
14
36
  version = 'http://gedcomx.org/conceptual-model/v1'