gedcom-x 0.5.8__py3-none-any.whl → 0.5.10__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 (50) hide show
  1. {gedcom_x-0.5.8.dist-info → gedcom_x-0.5.10.dist-info}/METADATA +1 -1
  2. gedcom_x-0.5.10.dist-info/RECORD +58 -0
  3. gedcomx/Extensions/rs10/rsLink.py +109 -59
  4. gedcomx/__init__.py +4 -1
  5. gedcomx/address.py +102 -16
  6. gedcomx/agent.py +81 -24
  7. gedcomx/attribution.py +52 -28
  8. gedcomx/conclusion.py +98 -46
  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/extensible.py +86 -0
  16. gedcomx/fact.py +53 -54
  17. gedcomx/gedcom.py +10 -0
  18. gedcomx/gedcom5x.py +30 -20
  19. gedcomx/gedcom7/GedcomStructure.py +1 -3
  20. gedcomx/gedcom7/__init__.py +2 -2
  21. gedcomx/gedcom7/{Gedcom7.py → gedcom7.py} +3 -3
  22. gedcomx/gedcom7/specification.py +4817 -0
  23. gedcomx/gedcomx.py +95 -93
  24. gedcomx/gender.py +21 -9
  25. gedcomx/group.py +9 -0
  26. gedcomx/identifier.py +47 -20
  27. gedcomx/logging_hub.py +19 -0
  28. gedcomx/mutations.py +10 -5
  29. gedcomx/name.py +74 -33
  30. gedcomx/note.py +50 -18
  31. gedcomx/online_account.py +9 -0
  32. gedcomx/person.py +46 -27
  33. gedcomx/place_description.py +54 -8
  34. gedcomx/place_reference.py +30 -8
  35. gedcomx/qualifier.py +19 -3
  36. gedcomx/relationship.py +55 -14
  37. gedcomx/resource.py +45 -18
  38. gedcomx/schemas.py +328 -0
  39. gedcomx/serialization.py +400 -421
  40. gedcomx/source_citation.py +16 -4
  41. gedcomx/source_description.py +181 -94
  42. gedcomx/source_reference.py +51 -16
  43. gedcomx/subject.py +59 -14
  44. gedcomx/textvalue.py +66 -12
  45. gedcomx/translation.py +3 -3
  46. gedcomx/uri.py +155 -3
  47. gedcom_x-0.5.8.dist-info/RECORD +0 -56
  48. gedcomx/gedcom7/Specification.py +0 -347
  49. {gedcom_x-0.5.8.dist-info → gedcom_x-0.5.10.dist-info}/WHEEL +0 -0
  50. {gedcom_x-0.5.8.dist-info → gedcom_x-0.5.10.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,13 @@
1
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
+ #=====================================================================
2
11
 
3
12
  class SourceCitation:
4
13
  identifier = 'http://gedcomx.org/v1/SourceCitation'
@@ -11,13 +20,16 @@ class SourceCitation:
11
20
  # ...existing code...
12
21
 
13
22
  @classmethod
14
- def _from_json_(cls, data: dict):
23
+ def _from_json_(cls, data: dict, context = None):
15
24
  """
16
25
  Create a SourceCitation instance from a JSON-dict (already parsed).
17
26
  """
18
- lang = data.get('lang', 'en')
19
- value = data.get('value')
20
- return cls(lang=lang, value=value)
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)
21
33
 
22
34
  @property
23
35
  def _as_dict_(self):
@@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional, TYPE_CHECKING
5
5
  if TYPE_CHECKING:
6
6
  from .document import Document
7
7
 
8
+
8
9
  """
9
10
  ======================================================================
10
11
  Project: Gedcom-X
@@ -31,12 +32,21 @@ from .attribution import Attribution
31
32
  from .coverage import Coverage
32
33
  from .date import Date
33
34
  from .identifier import Identifier, IdentifierList, make_uid
35
+ from .logging_hub import hub, logging
34
36
  from .note import Note
35
37
  from .resource import Resource
36
38
  from .source_citation import SourceCitation
37
39
  from .source_reference import SourceReference
38
40
  from .textvalue import TextValue
39
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"
40
50
  #=====================================================================
41
51
 
42
52
 
@@ -157,6 +167,8 @@ class SourceDescription:
157
167
  self.max_note_count = max_note_count
158
168
 
159
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
160
172
 
161
173
  @property
162
174
  def publisher(self) -> Resource | Agent | None:
@@ -195,7 +207,7 @@ class SourceDescription:
195
207
  return False
196
208
  self.notes.append(note_to_add)
197
209
 
198
- def add_source(self, source_to_add: SourceReference):
210
+ def add_source_reference(self, source_to_add: SourceReference):
199
211
  if source_to_add and isinstance(object,SourceReference):
200
212
  for current_source in self.sources:
201
213
  if current_source == source_to_add:
@@ -213,102 +225,177 @@ class SourceDescription:
213
225
  raise ValueError(f"Cannot add title of type {type(title_to_add)}")
214
226
 
215
227
  @property
216
- def _as_dict_(self) -> Dict[str, Any]:
228
+ def _as_dict_(self) -> Dict[str, Any] | None:
217
229
  from .serialization import Serialization
218
- type_as_dict = {}
219
-
220
- if self.id:
221
- type_as_dict['id'] = self.id
222
- if self.about:
223
- type_as_dict['about'] = self.about._as_dict_
224
- if self.resourceType:
225
- type_as_dict['resourceType'] = getattr(self.resourceType, 'value', self.resourceType)
226
- if self.citations:
227
- type_as_dict['citations'] = [c._as_dict_ for c in self.citations if c]
228
- if self.mediaType:
229
- type_as_dict['mediaType'] = self.mediaType
230
- if self.mediator:
231
- type_as_dict['mediator'] = self.mediator._as_dict_
232
- if self.publisher:
233
- type_as_dict['publisher'] = self.publisher._as_dict_ #TODO Resource this
234
- if self.authors:
235
- type_as_dict['authors'] = [a._as_dict_ for a in self.authors if a]
236
- if self.sources:
237
- type_as_dict['sources'] = [s._as_dict_ for s in self.sources if s]
238
- if self.analysis:
239
- type_as_dict['analysis'] = self.analysis._as_dict_
240
- if self.componentOf:
241
- type_as_dict['componentOf'] = self.componentOf._as_dict_
242
- if self.titles:
243
- type_as_dict['titles'] = [t._as_dict_ for t in self.titles if t]
244
- if self.notes:
245
- type_as_dict['notes'] = [n._as_dict_ for n in self.notes if n]
246
- if self.attribution:
247
- type_as_dict['attribution'] = self.attribution._as_dict_
248
- if self.rights:
249
- type_as_dict['rights'] = [r._as_dict_ for r in self.rights if r]
250
- if self.coverage:
251
- type_as_dict['coverage'] = [c._as_dict_ for c in self.coverage if c]
252
- if self.descriptions:
253
- type_as_dict['descriptions'] = [d for d in self.descriptions if d]
254
- if self.identifiers:
255
- type_as_dict['identifiers'] = self.identifiers._as_dict_
256
- if self.created is not None:
257
- type_as_dict['created'] = self.created
258
- if self.modified is not None:
259
- type_as_dict['modified'] = self.modified
260
- if self.published is not None:
261
- type_as_dict['published'] = self.published
262
- if self.repository:
263
- type_as_dict['repository'] = self.repository._as_dict_ #TODO Resource this
264
- if self.uri and self.uri.value:
265
- type_as_dict['uri'] = self.uri.value
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 = {}
266
234
 
267
-
268
- return Serialization.serialize_dict(type_as_dict)
269
-
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
270
249
 
271
- @classmethod
272
- def _from_json_(cls, data: Dict[str, Any]) -> 'SourceDescription':
273
- # TODO Hande Resource/URI
274
-
275
- # Basic fields
276
- id_ = data.get('id')
277
- rt = ResourceType(data['resourceType']) if data.get('resourceType') else None
278
-
279
- # Sub-objects
280
- citations = [SourceCitation._from_json_(c) for c in data.get('citations', [])]
281
- about = URI._from_json_(data['about']) if data.get('about') else None
282
- mediator = Resource._from_json_(data['mediator']) if data.get('mediator') else None
283
- publisher = Resource._from_json_(data['publisher']) if data.get('publisher') else None
284
- authors = [Resource._from_json_(a) for a in data.get('authors', [])] if data.get('authors') else None
285
- sources = [SourceReference._from_json_(s) for s in data.get('sources', [])]
286
- analysis = Resource._from_json_(data['analysis']) if data.get('analysis') else None
287
- component_of = SourceReference._from_json_(data['componentOf']) if data.get('componentOf') else None
288
- titles = [TextValue._from_json_(t) for t in data.get('titles', [])]
289
- notes = [Note._from_json_(n) for n in data.get('notes', [])]
290
- attribution = Attribution._from_json_(data['attribution']) if data.get('attribution') else None
291
- rights = [URI._from_json_(r) for r in data.get('rights', [])]
292
- coverage = [Coverage._from_json_(cvg) for cvg in data.get('coverage',[])]
293
- descriptions = [TextValue._from_json_(d) for d in data.get('descriptions', [])]
294
- identifiers = IdentifierList._from_json_(data.get('identifiers', []))
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
295
297
 
296
- created = Date._from_json_(data['created']) if data.get('created') else None
297
- modified = data.get('modified',None)
298
- published = Date._from_json_(data['published']) if data.get('published') else None
299
- repository = Agent._from_json_(data['repository']) if data.get('repository') else None
300
-
301
- return cls(
302
- id=id_, resourceType=rt, citations=citations,
303
- mediaType=data.get('mediaType'), about=about,
304
- mediator=mediator, publisher=publisher,
305
- authors=authors, sources=sources,
306
- analysis=analysis, componentOf=component_of,
307
- titles=titles, notes=notes, attribution=attribution,
308
- rights=rights, coverage=coverage,
309
- descriptions=descriptions, identifiers=identifiers,
310
- created=created, modified=modified,
311
- published=published, repository=repository
312
- )
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)
313
400
 
314
401
 
@@ -10,6 +10,15 @@ from .qualifier import Qualifier
10
10
  from .resource import Resource
11
11
 
12
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 CHANGED
@@ -9,7 +9,7 @@ from typing import List, Optional
9
9
 
10
10
  Created: 2025-08-25
11
11
  Updated:
12
- - 2025-08-31:
12
+ - 2025-09-03: _from_json_ refactor
13
13
 
14
14
  ======================================================================
15
15
  """
@@ -22,11 +22,21 @@ GEDCOM Module Types
22
22
  from .attribution import Attribution
23
23
  from .conclusion import ConfidenceLevel, Conclusion
24
24
  from .evidence_reference import EvidenceReference
25
- from .Extensions.rs10.rsLink import _rsLinkList
25
+ from .Extensions.rs10.rsLink import _rsLinks
26
26
  from .identifier import Identifier, IdentifierList
27
+ from .logging_hub import hub, logging
27
28
  from .note import Note
28
29
  from .resource import Resource
29
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
+ #=====================================================================
30
40
 
31
41
 
32
42
  class Subject(Conclusion):
@@ -46,7 +56,7 @@ class Subject(Conclusion):
46
56
  media: Optional[List[SourceReference]] = [],
47
57
  identifiers: Optional[IdentifierList] = None,
48
58
  uri: Optional[Resource] = None,
49
- links: Optional[_rsLinkList] = None) -> None:
59
+ links: Optional[_rsLinks] = None) -> None:
50
60
  super().__init__(id, lang, sources, analysis, notes, confidence, attribution,links=links)
51
61
  self.extracted = extracted
52
62
  self.evidence = evidence
@@ -63,15 +73,50 @@ class Subject(Conclusion):
63
73
 
64
74
  @property
65
75
  def _as_dict_(self):
66
- from .serialization import Serialization
67
-
68
- type_as_dict = super()._as_dict_ # Start with base class fields
69
- # Only add Relationship-specific fields
70
- type_as_dict.update({
71
- "extracted": self.extracted,
72
- "evidence": [evidence_ref for evidence_ref in self.evidence] if self.evidence else None,
73
- "media": [media for media in self.media] if self.media else None,
74
- "identifiers": self.identifiers._as_dict_ if self.identifiers else None
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")
75
90
 
76
- })
77
- return Serialization.serialize_dict(type_as_dict)
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
gedcomx/textvalue.py CHANGED
@@ -1,35 +1,89 @@
1
- from typing import Optional
1
+ from typing import Any, Optional, Dict
2
+
3
+ """
4
+ ======================================================================
5
+ Project: Gedcom-X
6
+ File: textvalue.py
7
+ Author: David J. Cartwright
8
+ Purpose:
9
+
10
+ Created: 2025-08-25
11
+ Updated:
12
+ - 2025-09-03 _from_json_ refactor
13
+ - 2025-09-04 added _str_ and _repr_ dunders
14
+
15
+ ======================================================================
16
+ """
17
+
18
+ """
19
+ ======================================================================
20
+ GEDCOM Module Types
21
+ ======================================================================
22
+ """
23
+ from .logging_hub import hub, logging
24
+ """
25
+ ======================================================================
26
+ Logging
27
+ ======================================================================
28
+ """
29
+ log = logging.getLogger("gedcomx")
30
+ serial_log = "gedcomx.serialization"
31
+ #=====================================================================
2
32
 
3
33
  class TextValue:
4
34
  identifier = 'http://gedcomx.org/v1/TextValue'
5
35
  version = 'http://gedcomx.org/conceptual-model/v1'
6
36
 
7
- def __init__(self, value: Optional[str] = None, lang: Optional[str] = 'en') -> None:
37
+ def __init__(self, value: Optional[str] = None, lang: Optional[str] = None) -> None:
8
38
  self.lang = lang
9
39
  self.value = value
10
40
 
11
41
  def _append_to_value(self, value_to_append):
12
42
  if not isinstance(value_to_append, str):
13
43
  raise ValueError(f"Cannot append object of type {type(value_to_append)}.")
14
- self.value += ' ' + value_to_append
44
+ if self.value is None: self.value = value_to_append
45
+ else: self.value += ' ' + value_to_append
15
46
 
16
47
  @property
17
48
  def _as_dict_(self):
18
- return {
19
- "lang":self.lang if self.lang else None,
20
- "value":self.value if self.value else None
21
- }
49
+ from .serialization import Serialization
50
+ return Serialization.serialize(self)
51
+ type_as_dict = {}
52
+ if self.lang is not None: type_as_dict['lang'] = self.lang
53
+ if self.value is not None: type_as_dict['value'] = self.value
54
+ return type_as_dict if type_as_dict != {} else None
22
55
 
23
56
  def __str__(self):
24
57
  return f"{self.value} ({self.lang})"
25
58
 
59
+ def __eq__(self, other: object) -> bool:
60
+ if isinstance(other,TextValue):
61
+ if self.value.strip() == other.value.strip():
62
+ return True
63
+ return False
64
+
26
65
  # ...existing code...
27
66
 
28
67
  @classmethod
29
- def _from_json_(cls, data: dict):
68
+ def _from_json_(cls, data: Any, context: Any = None) -> "TextValue":
30
69
  """
31
- Create a TextValue instance from a JSON-dict (already parsed).
70
+ Create a TextValue from JSON.
71
+ - Accepts a dict like {"value": "...", "lang": "en"} or a plain string shorthand.
72
+ - Defaults lang to "en" if not provided.
32
73
  """
33
- value = data.get('value')
34
- lang = data.get('lang', 'en')
35
- return cls(value=value, lang=lang)
74
+ if not isinstance(data, dict):
75
+ raise TypeError(f"{cls.__name__}._from_json_ expected dict or str, got {type(data)}")
76
+
77
+ text_value_data: Dict[str, Any] = {}
78
+
79
+ if (v := data.get("value")) is None:
80
+ text_value_data["value"] = v
81
+ if (l := data.get("lang")) is None:
82
+ text_value_data["lang"] = l
83
+
84
+ return cls(**text_value_data)
85
+
86
+ def __repr__(self) -> str:
87
+ # Debug-friendly: unambiguous constructor-style representation
88
+ cls = self.__class__.__name__
89
+ return f"{cls}(value={self.value!r}, lang={self.lang!r})"