gedcom-x 0.5.7__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 (69) hide show
  1. {gedcom_x-0.5.7.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 +110 -60
  4. gedcomx/TopLevelTypeCollection.py +1 -1
  5. gedcomx/__init__.py +43 -42
  6. gedcomx/address.py +217 -0
  7. gedcomx/{Agent.py → agent.py} +107 -34
  8. gedcomx/attribution.py +115 -0
  9. gedcomx/{Conclusion.py → conclusion.py} +120 -51
  10. gedcomx/{Converter.py → converter.py} +261 -116
  11. gedcomx/coverage.py +64 -0
  12. gedcomx/{Date.py → date.py} +43 -9
  13. gedcomx/{Document.py → document.py} +60 -12
  14. gedcomx/{Event.py → event.py} +88 -31
  15. gedcomx/evidence_reference.py +20 -0
  16. gedcomx/{Fact.py → fact.py} +81 -74
  17. gedcomx/{Gedcom.py → gedcom.py} +10 -0
  18. gedcomx/{Gedcom5x.py → gedcom5x.py} +31 -21
  19. gedcomx/gedcom7/Exceptions.py +9 -0
  20. gedcomx/gedcom7/GedcomStructure.py +94 -0
  21. gedcomx/gedcom7/Specification.py +347 -0
  22. gedcomx/gedcom7/__init__.py +26 -0
  23. gedcomx/gedcom7/g7interop.py +205 -0
  24. gedcomx/gedcom7/gedcom7.py +160 -0
  25. gedcomx/gedcom7/logger.py +19 -0
  26. gedcomx/{GedcomX.py → gedcomx.py} +109 -106
  27. gedcomx/gender.py +91 -0
  28. gedcomx/group.py +72 -0
  29. gedcomx/{Identifier.py → identifier.py} +48 -21
  30. gedcomx/{LoggingHub.py → logging_hub.py} +19 -0
  31. gedcomx/{Mutations.py → mutations.py} +59 -30
  32. gedcomx/{Name.py → name.py} +88 -47
  33. gedcomx/note.py +105 -0
  34. gedcomx/online_account.py +19 -0
  35. gedcomx/{Person.py → person.py} +61 -41
  36. gedcomx/{PlaceDescription.py → place_description.py} +71 -23
  37. gedcomx/{PlaceReference.py → place_reference.py} +32 -10
  38. gedcomx/{Qualifier.py → qualifier.py} +20 -4
  39. gedcomx/relationship.py +156 -0
  40. gedcomx/resource.py +112 -0
  41. gedcomx/serialization.py +794 -0
  42. gedcomx/source_citation.py +37 -0
  43. gedcomx/source_description.py +401 -0
  44. gedcomx/{SourceReference.py → source_reference.py} +56 -21
  45. gedcomx/subject.py +122 -0
  46. gedcomx/textvalue.py +89 -0
  47. gedcomx/{Translation.py → translation.py} +4 -4
  48. gedcomx/uri.py +273 -0
  49. gedcom_x-0.5.7.dist-info/RECORD +0 -49
  50. gedcomx/Address.py +0 -131
  51. gedcomx/Attribution.py +0 -91
  52. gedcomx/Coverage.py +0 -37
  53. gedcomx/EvidenceReference.py +0 -11
  54. gedcomx/Gender.py +0 -65
  55. gedcomx/Group.py +0 -37
  56. gedcomx/Note.py +0 -73
  57. gedcomx/OnlineAccount.py +0 -10
  58. gedcomx/Relationship.py +0 -97
  59. gedcomx/Resource.py +0 -85
  60. gedcomx/Serialization.py +0 -816
  61. gedcomx/SourceCitation.py +0 -25
  62. gedcomx/SourceDescription.py +0 -314
  63. gedcomx/Subject.py +0 -59
  64. gedcomx/TextValue.py +0 -35
  65. gedcomx/URI.py +0 -105
  66. {gedcom_x-0.5.7.dist-info → gedcom_x-0.5.9.dist-info}/WHEEL +0 -0
  67. {gedcom_x-0.5.7.dist-info → gedcom_x-0.5.9.dist-info}/top_level.txt +0 -0
  68. /gedcomx/{Exceptions.py → exceptions.py} +0 -0
  69. /gedcomx/{ExtensibleEnum.py → extensible_enum.py} +0 -0
@@ -0,0 +1,37 @@
1
+ from typing import Optional
2
+ from .logging_hub import hub, logging
3
+ """
4
+ ======================================================================
5
+ Logging
6
+ ======================================================================
7
+ """
8
+ log = logging.getLogger("gedcomx")
9
+ serial_log = "gedcomx.serialization"
10
+ #=====================================================================
11
+
12
+ class SourceCitation:
13
+ identifier = 'http://gedcomx.org/v1/SourceCitation'
14
+ version = 'http://gedcomx.org/conceptual-model/v1'
15
+
16
+ def __init__(self, lang: Optional[str], value: str) -> None:
17
+ self.lang = lang if lang else 'en'
18
+ self.value = value
19
+
20
+ # ...existing code...
21
+
22
+ @classmethod
23
+ def _from_json_(cls, data: dict, context = None):
24
+ """
25
+ Create a SourceCitation instance from a JSON-dict (already parsed).
26
+ """
27
+ object_data = {}
28
+ if (lang := data.get('lang')) is not None:
29
+ object_data['lang'] = lang
30
+ if (value := data.get('value')) is not None:
31
+ object_data['value'] = value
32
+ return cls(**object_data)
33
+
34
+ @property
35
+ def _as_dict_(self):
36
+ return {'lang':self.lang,
37
+ 'value': self.value}
@@ -0,0 +1,401 @@
1
+ import warnings
2
+
3
+ from enum import Enum
4
+ from typing import Any, Dict, List, Optional, TYPE_CHECKING
5
+ if TYPE_CHECKING:
6
+ from .document import Document
7
+
8
+
9
+ """
10
+ ======================================================================
11
+ Project: Gedcom-X
12
+ File: SourceDescription.py
13
+ Author: David J. Cartwright
14
+ Purpose:
15
+
16
+ Created: 2025-07-25
17
+ Updated:
18
+ - 2025-08-31: _as_dict_ refactored to ignore empty fields, changed id creation to make_uid()
19
+ - 2025-09-01: filename PEP8 standard, imports changed accordingly
20
+
21
+
22
+ ======================================================================
23
+ """
24
+
25
+ """
26
+ ======================================================================
27
+ GEDCOM Module Types
28
+ ======================================================================
29
+ """
30
+ from .agent import Agent
31
+ from .attribution import Attribution
32
+ from .coverage import Coverage
33
+ from .date import Date
34
+ from .identifier import Identifier, IdentifierList, make_uid
35
+ from .logging_hub import hub, logging
36
+ from .note import Note
37
+ from .resource import Resource
38
+ from .source_citation import SourceCitation
39
+ from .source_reference import SourceReference
40
+ from .textvalue import TextValue
41
+ from .uri import URI
42
+ """
43
+ ======================================================================
44
+ Logging
45
+ ======================================================================
46
+ """
47
+ log = logging.getLogger("gedcomx")
48
+ serial_log = "gedcomx.serialization"
49
+ deserial_log = "gedcomx.deserialization"
50
+ #=====================================================================
51
+
52
+
53
+ class ResourceType(Enum):
54
+ Collection = "http://gedcomx.org/Collection"
55
+ PhysicalArtifact = "http://gedcomx.org/PhysicalArtifact"
56
+ DigitalArtifact = "http://gedcomx.org/DigitalArtifact"
57
+ Record = "http://gedcomx.org/Record"
58
+ Person = "http://gedcomx.org/Person"
59
+
60
+ @property
61
+ def description(self):
62
+ descriptions = {
63
+ ResourceType.Collection: "A collection of genealogical resources. A collection may contain physical artifacts (such as a collection of books in a library), records (such as the 1940 U.S. Census), or digital artifacts (such as an online genealogical application).",
64
+ ResourceType.PhysicalArtifact: "A physical artifact, such as a book.",
65
+ ResourceType.DigitalArtifact: "A digital artifact, such as a digital image of a birth certificate or other record.",
66
+ ResourceType.Record: "A historical record, such as a census record or a vital record."
67
+ }
68
+ return descriptions.get(self, "No description available.")
69
+
70
+ class SourceDescription:
71
+ """Description of a genealogical information source.
72
+
73
+ See: http://gedcomx.org/v1/SourceDescription
74
+
75
+ Args:
76
+ id (str | None): Unique identifier for this `SourceDescription`.
77
+ resourceType (ResourceType | None): Type/category of the resource being
78
+ described (e.g., digital artifact, physical artifact).
79
+ citations (list[SourceCitation] | None): Citations that reference or
80
+ justify this source description.
81
+ mediaType (str | None): IANA media (MIME) type of the resource
82
+ (e.g., ``"application/pdf"``).
83
+ about (URI | None): Canonical URI that the description is about.
84
+ mediator (Resource | None): The mediator resource (if any) involved in
85
+ providing access to the source.
86
+ publisher (Resource | Agent | None): Publisher of the resource.
87
+ authors (list[Resource] | None): Authors/creators of the resource.
88
+ sources (list[SourceReference] | None): Other sources this description
89
+ derives from or references.
90
+ analysis (Resource | None): Analysis document associated with the
91
+ resource (often a `Document`; kept generic to avoid circular imports).
92
+ componentOf (SourceReference | None): Reference to a parent/containing
93
+ source (this is a component/child of that source).
94
+ titles (list[TextValue] | None): One or more titles for the resource.
95
+ notes (list[Note] | None): Human-authored notes about the resource.
96
+ attribution (Attribution | None): Attribution metadata for who supplied
97
+ or curated this description.
98
+ rights (list[Resource] | None): Rights statements or licenses.
99
+ coverage (list[Coverage] | None): Spatial/temporal coverage of the
100
+ source’s content.
101
+ descriptions (list[TextValue] | None): Short textual summaries or
102
+ descriptions.
103
+ identifiers (IdentifierList | None): Alternative identifiers for the
104
+ resource (DOI, ARK, call numbers, etc.).
105
+ created (Date | None): Creation date of the resource.
106
+ modified (Date | None): Last modified date of the resource.
107
+ published (Date | None): Publication/release date of the resource.
108
+ repository (Agent | None): Repository or agency that holds the resource.
109
+ max_note_count (int): Maximum number of notes to retain/emit. Defaults to 20.
110
+
111
+ Raises:
112
+ ValueError: If `id` is not a valid UUID.
113
+
114
+ Attributes:
115
+ identifier (str): Gedcom-X specification identifier for this type.
116
+ """
117
+
118
+ identifier = "http://gedcomx.org/v1/SourceDescription"
119
+ version = 'http://gedcomx.org/conceptual-model/v1'
120
+
121
+ def __init__(self, id: Optional[str] = None,
122
+ resourceType: Optional[ResourceType] = None,
123
+ citations: Optional[List[SourceCitation]] = [],
124
+ mediaType: Optional[str] = None,
125
+ about: Optional[URI] = None,
126
+ mediator: Optional[Agent|Resource] = None,
127
+ publisher: Optional[Agent|Resource] = None,
128
+ authors: Optional[List[Resource]] = None,
129
+ sources: Optional[List[SourceReference]] = None, # SourceReference
130
+ analysis: Optional["Document|Resource"] = None, #TODO add type checker so its a document
131
+ componentOf: Optional[SourceReference] = None, # SourceReference
132
+ titles: Optional[List[TextValue]] = None,
133
+ notes: Optional[List[Note]] = None,
134
+ attribution: Optional[Attribution] = None,
135
+ rights: Optional[List[Resource]] = [],
136
+ coverage: Optional[List[Coverage]] = None, # Coverage
137
+ descriptions: Optional[List[TextValue]] = None,
138
+ identifiers: Optional[IdentifierList] = None,
139
+ created: Optional[Date] = None,
140
+ modified: Optional[Date] = None,
141
+ published: Optional[Date] = None,
142
+ repository: Optional[Agent] = None,
143
+ max_note_count: int = 20):
144
+
145
+ self.id = id if id else make_uid()
146
+ self.resourceType = resourceType
147
+ self.citations = citations or []
148
+ self.mediaType = mediaType
149
+ self.about = about
150
+ self.mediator = mediator
151
+ self._publisher = publisher
152
+ self.authors = authors or []
153
+ self.sources = sources or []
154
+ self.analysis = analysis
155
+ self.componentOf = componentOf
156
+ self.titles = titles or []
157
+ self.notes = notes or []
158
+ self.attribution = attribution
159
+ self.rights = rights or []
160
+ self.coverage = coverage or []
161
+ self.descriptions = descriptions or []
162
+ self.identifiers = identifiers or IdentifierList()
163
+ self.created = created
164
+ self.modified = modified
165
+ self.published = published
166
+ self.repository = repository
167
+ self.max_note_count = max_note_count
168
+
169
+ self.uri = URI(fragment=id) if id else None #TODO Should i take care of this in the collections?
170
+
171
+ self._place_holder = False
172
+
173
+ @property
174
+ def publisher(self) -> Resource | Agent | None:
175
+ return self._publisher
176
+
177
+
178
+ @publisher.setter
179
+ def publisher(self, value: Resource | Agent):
180
+ if value is None:
181
+ self._publisher = None
182
+ elif isinstance(value,Resource):
183
+ self._publisher = value
184
+ elif isinstance(value,Agent):
185
+ self._publisher = value
186
+ else:
187
+ raise ValueError(f"'publisher' must be of type 'URI' or 'Agent', type: {type(value)} was provided")
188
+
189
+ def add_description(self, desccription_to_add: TextValue):
190
+ if desccription_to_add and isinstance(desccription_to_add,TextValue):
191
+ for current_description in self.descriptions:
192
+ if desccription_to_add == current_description:
193
+ return
194
+ self.descriptions.append(desccription_to_add)
195
+
196
+ def add_identifier(self, identifier_to_add: Identifier):
197
+ if identifier_to_add and isinstance(identifier_to_add,Identifier):
198
+ self.identifiers.append(identifier_to_add)
199
+
200
+ def add_note(self,note_to_add: Note):
201
+ if len(self.notes) >= self.max_note_count:
202
+ warnings.warn(f"Max not count of {self.max_note_count} reached for id: {self.id}")
203
+ return False
204
+ if note_to_add and isinstance(note_to_add,Note):
205
+ for existing in self.notes:
206
+ if note_to_add == existing:
207
+ return False
208
+ self.notes.append(note_to_add)
209
+
210
+ def add_source_reference(self, source_to_add: SourceReference):
211
+ if source_to_add and isinstance(object,SourceReference):
212
+ for current_source in self.sources:
213
+ if current_source == source_to_add:
214
+ return
215
+ self.sources.append(source_to_add)
216
+
217
+ def add_title(self, title_to_add: TextValue):
218
+ if isinstance(title_to_add,str): title_to_add = TextValue(value=title_to_add)
219
+ if title_to_add and isinstance(title_to_add, TextValue):
220
+ for current_title in self.titles:
221
+ if title_to_add == current_title:
222
+ return False
223
+ self.titles.append(title_to_add)
224
+ else:
225
+ raise ValueError(f"Cannot add title of type {type(title_to_add)}")
226
+
227
+ @property
228
+ def _as_dict_(self) -> Dict[str, Any] | None:
229
+ from .serialization import Serialization
230
+ return Serialization.serialize(self)
231
+ with hub.use(serial_log):
232
+ log.debug(f"Serializing 'SourceDescription' with id: {self.id}")
233
+ type_as_dict = {}
234
+
235
+ if self.id:
236
+ type_as_dict['id'] = self.id
237
+ if self.about:
238
+ type_as_dict['about'] = self.about._as_dict_
239
+ if self.resourceType:
240
+ if isinstance(self.resourceType,str):
241
+ log.warning(f"'SourceDescription.resourceType' should not be a string {self.resourceType}")
242
+ type_as_dict['resourceType'] = self.resourceType
243
+ else:
244
+ type_as_dict['resourceType'] = self.resourceType.value
245
+ if self.citations:
246
+ type_as_dict['citations'] = [c._as_dict_ for c in self.citations if c]
247
+ if self.mediaType:
248
+ type_as_dict['mediaType'] = self.mediaType
249
+
250
+ if self.mediator:
251
+ type_as_dict['mediator'] = self.mediator._as_dict_
252
+ if self.publisher:
253
+ type_as_dict['publisher'] = self.publisher._as_dict_ #TODO Resource this
254
+ if self.authors:
255
+ type_as_dict['authors'] = [a._as_dict_ for a in self.authors if a]
256
+ if self.sources:
257
+ type_as_dict['sources'] = [s._as_dict_ for s in self.sources if s]
258
+
259
+
260
+ if self.analysis:
261
+ type_as_dict['analysis'] = self.analysis._as_dict_
262
+ if self.componentOf:
263
+ type_as_dict['componentOf'] = self.componentOf._as_dict_
264
+ if self.titles:
265
+ type_as_dict['titles'] = [t._as_dict_ for t in self.titles if t]
266
+ if self.notes:
267
+ type_as_dict['notes'] = [n._as_dict_ for n in self.notes if n]
268
+ if self.attribution:
269
+ type_as_dict['attribution'] = self.attribution._as_dict_
270
+ if self.rights:
271
+ type_as_dict['rights'] = [r._as_dict_ for r in self.rights if r]
272
+
273
+ if self.coverage:
274
+ type_as_dict['coverage'] = [c._as_dict_ for c in self.coverage if c]
275
+
276
+ if self.descriptions:
277
+ if not (isinstance(self.descriptions, list) and all(isinstance(x, TextValue) for x in self.descriptions)):
278
+ assert False
279
+ type_as_dict['descriptions'] = [d._as_dict_ for d in self.descriptions if d]
280
+
281
+ if self.identifiers:
282
+ type_as_dict['identifiers'] = self.identifiers._as_dict_
283
+
284
+ if self.created is not None:
285
+ type_as_dict['created'] = self.created
286
+ if self.modified is not None:
287
+ type_as_dict['modified'] = self.modified
288
+ if self.published is not None:
289
+ type_as_dict['published'] = self.published
290
+
291
+ if self.repository:
292
+ type_as_dict['repository'] = self.repository._as_dict_ #TODO Resource this
293
+
294
+ log.debug(f"'SourceDescription' serialized with fields: '{type_as_dict.keys()}'")
295
+ if type_as_dict == {}: log.warning("serializing and empty 'SourceDescription'")
296
+ return type_as_dict if type_as_dict != {} else None
297
+
298
+
299
+ @classmethod
300
+ def _from_json_(cls, data: Dict[str, Any], context: Any = None) -> "SourceDescription":
301
+ if not isinstance(data, dict):
302
+ raise TypeError(f"{cls.__name__}._from_json_ expected dict, got {type(data)}")
303
+
304
+ source_description_data: Dict[str, Any] = {}
305
+
306
+ # ── Scalars ──────────────────────────────────────────────────────────────
307
+ if (id_ := data.get("id")) is not None:
308
+ source_description_data["id"] = id_
309
+ if (media_type := data.get("mediaType")) is not None:
310
+ source_description_data["mediaType"] = media_type
311
+ if (mnc := data.get("max_note_count")) is not None:
312
+ source_description_data["max_note_count"] = int(mnc)
313
+
314
+ # resourceType (enum or similar)
315
+ if (rt := data.get("resourceType")) is not None:
316
+ source_description_data["resourceType"] = ResourceType(rt)
317
+
318
+ # about: URI (accept string or dict)
319
+ if (about := data.get("about")) is not None:
320
+ source_description_data["about"] = URI.from_url(about) if isinstance(about, str) else URI._from_json_(about, context)
321
+
322
+ # mediator / publisher can be Agent or Resource
323
+ if (mediator := data.get("mediator")) is not None:
324
+ if isinstance(mediator, dict) and any(k in mediator for k in ("names", "emails", "accounts", "addresses", "person")):
325
+ source_description_data["mediator"] = Agent._from_json_(mediator, context)
326
+ else:
327
+ source_description_data["mediator"] = Resource._from_json_(mediator, context)
328
+
329
+ if (publisher := data.get("publisher")) is not None:
330
+ if isinstance(publisher, dict) and any(k in publisher for k in ("names", "emails", "accounts", "addresses", "person")):
331
+ source_description_data["publisher"] = Agent._from_json_(publisher, context)
332
+ else:
333
+ source_description_data["publisher"] = Resource._from_json_(publisher, context)
334
+
335
+ # repository (Agent)
336
+ if (repo := data.get("repository")) is not None:
337
+ source_description_data["repository"] = Agent._from_json_(repo, context)
338
+
339
+ # analysis: Document | Resource (prefer Document when possible)
340
+ if (analysis := data.get("analysis")) is not None:
341
+ try:
342
+ source_description_data["analysis"] = Document._from_json_(analysis, context)
343
+ except Exception:
344
+ source_description_data["analysis"] = Resource._from_json_(analysis, context)
345
+
346
+ # componentOf: SourceReference
347
+ if (component := data.get("componentOf")) is not None:
348
+ source_description_data["componentOf"] = SourceReference._from_json_(component, context)
349
+
350
+ # identifiers
351
+ if (identifiers := data.get("identifiers")) is not None:
352
+ source_description_data["identifiers"] = IdentifierList._from_json_(identifiers, context)
353
+
354
+ # timestamps
355
+ if (created := data.get("created")) is not None:
356
+ source_description_data["created"] = created
357
+ if (modified := data.get("modified")) is not None:
358
+ source_description_data["modified"] = modified
359
+ if (published := data.get("published")) is not None:
360
+ source_description_data["published"] = published
361
+
362
+ # ── Lists ───────────────────────────────────────────────────────────────
363
+ if (citations := data.get("citations")) is not None:
364
+ source_description_data["citations"] = [SourceCitation._from_json_(c, context) for c in citations]
365
+
366
+ if (authors := data.get("authors")) is not None:
367
+ source_description_data["authors"] = [Resource._from_json_(a, context) for a in authors]
368
+
369
+ if (sources := data.get("sources")) is not None:
370
+ source_description_data["sources"] = [SourceReference._from_json_(s, context) for s in sources]
371
+
372
+ if (titles := data.get("titles")) is not None:
373
+ source_description_data["titles"] = [
374
+ (TextValue._from_json_(t, context) if isinstance(t, dict) else TextValue(t))
375
+ for t in titles
376
+ ]
377
+
378
+ if (notes := data.get("notes")) is not None:
379
+ source_description_data["notes"] = [Note._from_json_(n, context) for n in notes]
380
+
381
+ if (rights := data.get("rights")) is not None:
382
+ source_description_data["rights"] = [Resource._from_json_(r, context) for r in rights]
383
+
384
+ if (coverage := data.get("coverage")) is not None:
385
+ source_description_data["coverage"] = [Coverage._from_json_(c, context) for c in coverage]
386
+
387
+ if (descriptions := data.get("descriptions")) is not None:
388
+ for d in descriptions:
389
+ if not isinstance(d, dict): assert False
390
+ source_description_data["descriptions"] = [
391
+ (TextValue._from_json_(d, context) if isinstance(d, dict) else TextValue(value="somethings fucked up"))
392
+ for d in descriptions
393
+ ]
394
+
395
+ # attribution
396
+ if (attr := data.get("attribution")) is not None:
397
+ source_description_data["attribution"] = Attribution._from_json_(attr, context) if isinstance(attr, dict) else attr
398
+
399
+ return cls(**source_description_data)
400
+
401
+
@@ -2,14 +2,23 @@ from __future__ import annotations
2
2
  from typing import List, Optional, TYPE_CHECKING
3
3
 
4
4
  if TYPE_CHECKING:
5
- from .SourceDescription import SourceDescription
5
+ from .source_description import SourceDescription
6
6
 
7
- from .Attribution import Attribution
8
- from .Qualifier import Qualifier
7
+ from .attribution import Attribution
8
+ from .qualifier import Qualifier
9
9
 
10
- from .Resource import Resource
10
+ from .resource import Resource
11
11
 
12
- from .URI import URI
12
+ from .uri import URI
13
+ from .logging_hub import hub, logging
14
+ """
15
+ ======================================================================
16
+ Logging
17
+ ======================================================================
18
+ """
19
+ log = logging.getLogger("gedcomx")
20
+ serial_log = "gedcomx.serialization"
21
+ #=====================================================================
13
22
 
14
23
  from collections.abc import Sized
15
24
 
@@ -52,7 +61,7 @@ class SourceReference:
52
61
  version = 'http://gedcomx.org/conceptual-model/v1'
53
62
 
54
63
  def __init__(self,
55
- description: URI | SourceDescription | None = None,
64
+ description: SourceDescription | URI | None = None,
56
65
  descriptionId: Optional[str] = None,
57
66
  attribution: Optional[Attribution] = None,
58
67
  qualifiers: Optional[List[Qualifier]] = None
@@ -85,23 +94,49 @@ class SourceReference:
85
94
 
86
95
  @property
87
96
  def _as_dict_(self):
88
- from .Serialization import Serialization
89
- type_as_dict = {
90
- 'description':self.description._as_dict_ if self.description else None,
91
- 'descriptionId': self.descriptionId.replace("\n"," ").replace("\r"," ") if self.descriptionId else None,
92
- 'attribution': self.attribution._as_dict_ if self.attribution else None,
93
- 'qualifiers':[qualifier.__as_dict__ for qualifier in self.qualifiers] if (self.qualifiers and len(self.qualifiers) > 0) else None
94
- }
95
- return Serialization.serialize_dict(type_as_dict)
97
+ with hub.use(serial_log):
98
+ log.debug(f"Serializing 'SourceReference' with 'descriptionId': '{self.descriptionId}'")
99
+ type_as_dict = {}
100
+ if self.description is not None:
101
+ type_as_dict['description']= URI(target=self.description)._as_dict_ if self.description is not None else None
102
+ if self.descriptionId is not None:
103
+ type_as_dict['descriptionId']= self.descriptionId.replace("\n"," ").replace("\r"," ") if self.descriptionId else None
104
+ if self.attribution is not None:
105
+ type_as_dict['attribution'] = self.attribution._as_dict_ if self.attribution else None
106
+ if self.qualifiers is not None and self.qualifiers != []:
107
+ type_as_dict['qualifiers'] = [qualifier.__as_dict__ for qualifier in self.qualifiers] if (self.qualifiers and len(self.qualifiers) > 0) else None
108
+ log.debug(f"'SourceReference' serialized with fields: {type_as_dict.keys()}")
109
+ if type_as_dict == {}: log.warning("serializing and empty 'SourceReference'")
110
+ return type_as_dict if type_as_dict != {} else None
96
111
 
97
112
  @classmethod
98
- def _from_json_(cls, data: dict):
99
- """
100
- Rehydrate a SourceReference from the dict passed down from JSON deserialization.
101
- NOTE: This does not resolve references to SourceDescription objects.
102
- """
103
- from .Serialization import Serialization
104
- return Serialization.deserialize(data, SourceReference)
113
+ def _from_json_(cls, data: dict, context=None) -> "SourceReference":
114
+ ref = {}
115
+
116
+ # Scalars
117
+ if (descriptionId := data.get("descriptionId")) is not None:
118
+ ref["descriptionId"] = descriptionId
119
+
120
+ # Objects (description could be URI or SourceDescription)
121
+ if (description := data.get("description")) is not None:
122
+ # if description is just a string, assume URI
123
+ if isinstance(description, str):
124
+ ref["description"] = URI(description)
125
+ elif isinstance(description, dict):
126
+ print(">>",description)
127
+ ref["description"] = Resource._from_json_(description, context)
128
+ assert False
129
+ else:
130
+ pass #TODO
131
+ #print(ref["descriptionId"])
132
+
133
+ if (attribution := data.get("attribution")) is not None:
134
+ ref["attribution"] = Attribution._from_json_(attribution, context)
135
+
136
+ if (qualifiers := data.get("qualifiers")) is not None:
137
+ ref["qualifiers"] = [Qualifier._from_json_(q, context) for q in qualifiers]
138
+
139
+ return cls(**ref)
105
140
 
106
141
 
107
142
 
gedcomx/subject.py ADDED
@@ -0,0 +1,122 @@
1
+ import warnings
2
+ from typing import List, Optional
3
+ """
4
+ ======================================================================
5
+ Project: Gedcom-X
6
+ File: subject.py
7
+ Author: David J. Cartwright
8
+ Purpose:
9
+
10
+ Created: 2025-08-25
11
+ Updated:
12
+ - 2025-09-03: _from_json_ refactor
13
+
14
+ ======================================================================
15
+ """
16
+
17
+ """
18
+ ======================================================================
19
+ GEDCOM Module Types
20
+ ======================================================================
21
+ """
22
+ from .attribution import Attribution
23
+ from .conclusion import ConfidenceLevel, Conclusion
24
+ from .evidence_reference import EvidenceReference
25
+ from .Extensions.rs10.rsLink import _rsLinks
26
+ from .identifier import Identifier, IdentifierList
27
+ from .logging_hub import hub, logging
28
+ from .note import Note
29
+ from .resource import Resource
30
+ from .source_reference import SourceReference
31
+ from. uri import URI
32
+ """
33
+ ======================================================================
34
+ Logging
35
+ ======================================================================
36
+ """
37
+ log = logging.getLogger("gedcomx")
38
+ serial_log = "gedcomx.serialization"
39
+ #=====================================================================
40
+
41
+
42
+ class Subject(Conclusion):
43
+ identifier = 'http://gedcomx.org/v1/Subject'
44
+ version = 'http://gedcomx.org/conceptual-model/v1'
45
+
46
+ def __init__(self,
47
+ id: Optional[str],
48
+ lang: Optional[str] = 'en',
49
+ sources: Optional[List[SourceReference]] = [],
50
+ analysis: Optional[Resource] = None,
51
+ notes: Optional[List[Note]] = [],
52
+ confidence: Optional[ConfidenceLevel] = None,
53
+ attribution: Optional[Attribution] = None,
54
+ extracted: Optional[bool] = None,
55
+ evidence: Optional[List[EvidenceReference]] = [],
56
+ media: Optional[List[SourceReference]] = [],
57
+ identifiers: Optional[IdentifierList] = None,
58
+ uri: Optional[Resource] = None,
59
+ links: Optional[_rsLinks] = None) -> None:
60
+ super().__init__(id, lang, sources, analysis, notes, confidence, attribution,links=links)
61
+ self.extracted = extracted
62
+ self.evidence = evidence
63
+ self.media = media
64
+ self.identifiers = identifiers if identifiers else IdentifierList()
65
+ self.uri = uri
66
+
67
+ def add_identifier(self, identifier_to_add: Identifier):
68
+ if identifier_to_add and isinstance(identifier_to_add,Identifier):
69
+ for current_identifier in self.identifiers:
70
+ if identifier_to_add == current_identifier:
71
+ return
72
+ self.identifiers.append(identifier_to_add)
73
+
74
+ @property
75
+ def _as_dict_(self):
76
+ with hub.use(serial_log):
77
+ log.debug(f"Serializing 'Subject' with id: '{self.id}'")
78
+ type_as_dict = super()._as_dict_ # Start with base class fields
79
+ if type_as_dict is None: type_as_dict = {}
80
+ if self.extracted:
81
+ type_as_dict["extracted"] = self.extracted
82
+ if self.evidence:
83
+ type_as_dict["evidence"] = [evidence_ref for evidence_ref in self.evidence] if self.evidence else None
84
+ if self.media:
85
+ type_as_dict["media"] = [media for media in self.media] if self.media else None
86
+ if self.identifiers:
87
+ type_as_dict["identifiers"] = self.identifiers._as_dict_ if self.identifiers else None
88
+ log.debug(f"'Subject' serialized with fields: '{type_as_dict.keys()}'")
89
+ if type_as_dict == {} or len(type_as_dict.keys()) == 0: log.warning("serializing and empty 'Subject' Object")
90
+
91
+ return type_as_dict if type_as_dict != {} else None
92
+
93
+
94
+ @classmethod
95
+ def _dict_from_json_(cls, data: dict, context = None) -> dict:
96
+ subject_data = Conclusion._dict_from_json_(data,context)
97
+
98
+ # Bool
99
+ if (extracted := data.get("extracted")) is not None:
100
+ # cast to bool in case JSON gives "true"/"false" as string
101
+ if isinstance(extracted, str):
102
+ subject_data["extracted"] = extracted.lower() == "true"
103
+ else:
104
+ subject_data["extracted"] = bool(extracted)
105
+
106
+ # Lists
107
+ if (evidence := data.get("evidence")) is not None:
108
+ subject_data["evidence"] = [EvidenceReference._from_json_(e, context) for e in evidence]
109
+
110
+ if (media := data.get("media")) is not None:
111
+ subject_data["media"] = [SourceReference._from_json_(m, context) for m in media]
112
+
113
+ # Identifiers
114
+ if (identifiers := data.get("identifiers")) is not None:
115
+ subject_data["identifiers"] = IdentifierList._from_json_(identifiers, context)
116
+
117
+ # URI
118
+ if (uri := data.get("uri")) is not None:
119
+ subject_data["uri"] = URI(uri)
120
+
121
+ #return cls(**conclusion)
122
+ return subject_data