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/gedcomx.py CHANGED
@@ -20,6 +20,7 @@ from typing import Any, Dict, List, Optional
20
20
  - 2025-08-31: _as_dict_ to only create entries in dict for fields that hold data,
21
21
  id_index functionality, will be used for resolution of Resources
22
22
  - 2025-09-03: _from_json_ refactor
23
+ - 2025-09-09: added schema_class
23
24
 
24
25
  ======================================================================
25
26
  """
@@ -40,6 +41,7 @@ from .person import Person
40
41
  from .place_description import PlaceDescription
41
42
  from .relationship import Relationship, RelationshipType
42
43
  from .resource import Resource
44
+ from .schemas import schema_class
43
45
  from .source_description import ResourceType, SourceDescription
44
46
  from .textvalue import TextValue
45
47
  from .uri import URI
@@ -228,6 +230,7 @@ def TypeCollection(item_type):
228
230
 
229
231
  return Collection()
230
232
 
233
+ @schema_class()
231
234
  class GedcomX:
232
235
  """
233
236
  Main GedcomX Object representing a Genealogy. Stores collections of Top Level Gedcom-X Types.
gedcomx/gender.py CHANGED
@@ -10,6 +10,7 @@ from typing import List, Optional
10
10
  Created: 2025-08-25
11
11
  Updated:
12
12
  - 2025-09-03: _from_json_ refactor
13
+ - 2025-09-09: added schema_class
13
14
 
14
15
  ======================================================================
15
16
  """
@@ -24,6 +25,7 @@ from .conclusion import ConfidenceLevel, Conclusion
24
25
  from .Extensions.rs10.rsLink import _rsLinks
25
26
  from .note import Note
26
27
  from .resource import Resource
28
+ from .schemas import schema_class
27
29
  from .source_reference import SourceReference
28
30
  from .logging_hub import hub, logging
29
31
  """
@@ -51,7 +53,8 @@ class GenderType(Enum):
51
53
  GenderType.Intersex: "Intersex (assignment at birth)."
52
54
  }
53
55
  return descriptions.get(self, "No description available.")
54
-
56
+
57
+ @schema_class()
55
58
  class Gender(Conclusion):
56
59
  identifier = 'http://gedcomx.org/v1/Gender'
57
60
  version = 'http://gedcomx.org/conceptual-model/v1'
@@ -69,6 +72,7 @@ class Gender(Conclusion):
69
72
  ) -> None:
70
73
  super().__init__(id=id, lang=lang, sources=sources, analysis=analysis, notes=notes, confidence=confidence, attribution=attribution, links=links)
71
74
  self.type = type
75
+ self.id = id if id else None # No need for id unless provided
72
76
 
73
77
  @property
74
78
  def _as_dict_(self):
gedcomx/group.py CHANGED
@@ -10,6 +10,7 @@ from typing import List, Optional
10
10
  Created: 2025-08-25
11
11
  Updated:
12
12
  - 2025-09-01: Updating basic structure, identify TODO s
13
+ - 2025-09-09: added schema_class
13
14
 
14
15
  ======================================================================
15
16
  """
@@ -29,8 +30,8 @@ from .note import Note
29
30
  from .place_reference import PlaceReference
30
31
  from .source_reference import SourceReference
31
32
  from .resource import Resource
32
-
33
33
  from .textvalue import TextValue
34
+ from .schemas import schema_class
34
35
  from .subject import Subject
35
36
  from .logging_hub import hub, logging
36
37
  """
@@ -53,6 +54,7 @@ class GroupRole: #TODO Impliment
53
54
  def __init__(self, person: Resource,type: Optional[Enum], date: Optional[Date],details: Optional[str]) -> None:
54
55
  pass
55
56
 
57
+ @schema_class(toplevel=True)
56
58
  class Group(Subject): #TODO Impliment
57
59
  identifier = 'http://gedcomx.org/v1/Group'
58
60
  version = 'http://gedcomx.org/conceptual-model/v1'
@@ -60,7 +62,14 @@ class Group(Subject): #TODO Impliment
60
62
  def __init__(self,
61
63
  id: str | None, lang: str | None,
62
64
  sources: List[SourceReference] | None,
63
- analysis: Document | Resource | None, notes: List[Note] | None, confidence: ConfidenceLevel | None, attribution: Attribution | None, extracted: bool | None, evidence: List[EvidenceReference] | None, media: List[SourceReference] | None, identifiers: List[Identifier] | None,
65
+ analysis: Document | Resource | None,
66
+ notes: List[Note] | None,
67
+ confidence: ConfidenceLevel | None,
68
+ attribution: Attribution | None,
69
+ extracted: bool | None,
70
+ evidence: List[EvidenceReference] | None,
71
+ media: List[SourceReference] | None,
72
+ identifiers: List[Identifier] | None,
64
73
  names: List[TextValue],
65
74
  date: Optional[Date],
66
75
  place: Optional[PlaceReference],
gedcomx/identifier.py CHANGED
@@ -16,6 +16,7 @@ import json
16
16
  Updated:
17
17
  - 2025-09-03: _from_json_ refactor
18
18
  - 2025-09-04: fixe identifier and identifieList json deserialization
19
+ - 2025-09-09: added schema_class
19
20
 
20
21
  ======================================================================
21
22
  """
@@ -27,6 +28,7 @@ GEDCOM Module Types
27
28
  """
28
29
  from .extensible_enum import _EnumItem
29
30
  from .resource import Resource
31
+ from .schemas import schema_class
30
32
  from .uri import URI
31
33
  from .extensible_enum import ExtensibleEnum
32
34
  from .logging_hub import hub, logging
@@ -65,7 +67,8 @@ IdentifierType.register("Persistent", "http://gedcomx.org/Persistent")
65
67
  IdentifierType.register("External", "https://gedcom.io/terms/v7/EXID")
66
68
  IdentifierType.register("Other", "user provided")
67
69
  IdentifierType.register("ChildAndParentsRelationship","http://familysearch.org/v1/ChildAndParentsRelationship")
68
-
70
+
71
+ @schema_class()
69
72
  class Identifier:
70
73
  identifier = 'http://gedcomx.org/v1/Identifier'
71
74
  version = 'http://gedcomx.org/conceptual-model/v1'
@@ -231,7 +234,7 @@ class IdentifierList:
231
234
  raise ValueError("Data must be a dict of identifiers.")
232
235
 
233
236
  @property
234
- def _as_dict_(self):
237
+ def _serializer(self):
235
238
  type_as_dict = {}
236
239
  for k in self.identifiers.keys():
237
240
  #print(k,self.identifiers[k],type(self.identifiers[k]))
gedcomx/logging_hub.py CHANGED
@@ -1,3 +1,17 @@
1
+
2
+ """
3
+ ======================================================================
4
+ Project: Gedcom-X
5
+ File: logging_hub.py
6
+ Author: David J. Cartwright
7
+ Purpose: provide module wide logging at context/channel level
8
+
9
+ Created: 2025-08-25
10
+ Updated:
11
+ - 2025-09-09: added global kill
12
+
13
+ ======================================================================
14
+ """
1
15
  # logging_hub.py
2
16
  from __future__ import annotations
3
17
  import logging
@@ -8,8 +22,12 @@ from dataclasses import dataclass
8
22
  from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
9
23
  from typing import Dict, Optional
10
24
 
11
- # Context key: which "channel" (log) is current?
12
- _current_channel: contextvars.ContextVar[str] = contextvars.ContextVar("current_log_channel", default="default")
25
+ # ──────────────────────────────────────────────────────────────────────────────
26
+ # Context: which "channel" (log) is current?
27
+ # ──────────────────────────────────────────────────────────────────────────────
28
+ _current_channel: contextvars.ContextVar[str] = contextvars.ContextVar(
29
+ "current_log_channel", default="default"
30
+ )
13
31
 
14
32
  def get_current_channel() -> str:
15
33
  return _current_channel.get()
@@ -17,16 +35,36 @@ def get_current_channel() -> str:
17
35
  def set_current_channel(name: str) -> None:
18
36
  _current_channel.set(name)
19
37
 
38
+ # ──────────────────────────────────────────────────────────────────────────────
39
+ # Filters and Handlers
40
+ # ──────────────────────────────────────────────────────────────────────────────
20
41
  class ChannelFilter(logging.Filter):
21
42
  """Injects the current channel into every LogRecord."""
22
43
  def filter(self, record: logging.LogRecord) -> bool:
23
44
  record.log_channel = get_current_channel()
24
45
  return True
25
46
 
47
+ class KillSwitchFilter(logging.Filter):
48
+ """Fast global on/off. Returning False drops the record early."""
49
+ def __init__(self) -> None:
50
+ super().__init__()
51
+ self._enabled: bool = True
52
+
53
+ @property
54
+ def enabled(self) -> bool:
55
+ return self._enabled
56
+
57
+ @enabled.setter
58
+ def enabled(self, value: bool) -> None:
59
+ self._enabled = bool(value)
60
+
61
+ def filter(self, record: logging.LogRecord) -> bool:
62
+ return self._enabled
63
+
26
64
  class DispatchingHandler(logging.Handler):
27
65
  """
28
- Routes records to a per-channel handler (file/stream), based on LogRecord.log_channel
29
- which is set by ChannelFilter.
66
+ Routes records to a per-channel handler (file/stream),
67
+ based on LogRecord.log_channel (set by ChannelFilter).
30
68
  """
31
69
  def __init__(self):
32
70
  super().__init__()
@@ -69,6 +107,9 @@ class DispatchingHandler(logging.Handler):
69
107
  return # channel muted
70
108
  handler.emit(record)
71
109
 
110
+ # ──────────────────────────────────────────────────────────────────────────────
111
+ # Configuration model
112
+ # ──────────────────────────────────────────────────────────────────────────────
72
113
  @dataclass
73
114
  class ChannelConfig:
74
115
  name: str
@@ -82,18 +123,21 @@ class ChannelConfig:
82
123
  # "size:10MB:3" -> RotatingFileHandler(maxBytes=10MB, backupCount=3)
83
124
  # "time:midnight:7" -> TimedRotatingFileHandler(when="midnight", backupCount=7)
84
125
 
126
+ # ──────────────────────────────────────────────────────────────────────────────
127
+ # Hub
128
+ # ──────────────────────────────────────────────────────────────────────────────
85
129
  class LoggingHub:
86
130
  """
87
- A centralized, context-aware logging hub.
88
- Usage:
131
+ Centralized, context-aware logging hub.
132
+
133
+ Example:
89
134
  hub = LoggingHub()
90
- hub.init_root() # do once at startup
91
- hub.start_channel(ChannelConfig(name="default", path="logs/default.log"))
92
- hub.set_current("default")
135
+ hub.init_root()
136
+ hub.start_channel(ChannelConfig(name="default", path="logs/app.log"), make_current=True)
93
137
 
94
- # In any module:
95
- log = logging.getLogger("gedcomx")
96
- log.info("hello") # goes to current channel
138
+ log = hub.get_logger("gedcomx")
139
+ if hub.loggingenable: # <— cheap guard to avoid formatting
140
+ log.info("hello %s", "world")
97
141
 
98
142
  with hub.use("import-job-42"):
99
143
  log.info("within job 42")
@@ -102,9 +146,10 @@ class LoggingHub:
102
146
  self.root_name = root_logger_name
103
147
  self._root = logging.getLogger(self.root_name)
104
148
  self._dispatch = DispatchingHandler()
105
- self._root.setLevel(logging.DEBUG) # Let handlers control final level
149
+ self._root.setLevel(logging.DEBUG) # Handlers/filters determine final behavior
106
150
 
107
151
  self._filter = ChannelFilter()
152
+ self._killswitch = KillSwitchFilter()
108
153
  self._initialized = False
109
154
 
110
155
  # -------- Initialization --------
@@ -114,10 +159,19 @@ class LoggingHub:
114
159
  # Clean existing handlers on the root logger (optional safety)
115
160
  for h in list(self._root.handlers):
116
161
  self._root.removeHandler(h)
162
+
163
+ # Order matters: kill-switch first for fastest early exit.
164
+ self._root.addFilter(self._killswitch)
117
165
  self._root.addFilter(self._filter)
118
166
  self._root.addHandler(self._dispatch)
119
167
  self._initialized = True
120
168
 
169
+ # Optional: env bootstrap
170
+ if os.getenv("GEDCOMX_LOG", "1").lower() in {"0", "false", "off"}:
171
+ self.disable_all()
172
+ if os.getenv("GEDCOMX_LOG_HARD", "0") == "1":
173
+ self.hard_disable()
174
+
121
175
  # -------- Channel Management --------
122
176
  def start_channel(self, cfg: ChannelConfig, make_current: bool = False, enabled: bool = True) -> None:
123
177
  """Create/replace a channel with a file/rotating handler."""
@@ -125,7 +179,6 @@ class LoggingHub:
125
179
  formatter = logging.Formatter(cfg.fmt, datefmt=cfg.datefmt)
126
180
 
127
181
  if cfg.path is None:
128
- # StreamHandler to stdout if no path provided
129
182
  handler = logging.StreamHandler()
130
183
  else:
131
184
  # Rotation options
@@ -133,7 +186,7 @@ class LoggingHub:
133
186
  # "size:10MB:3"
134
187
  _, size_str, backups_str = cfg.rotation.split(":")
135
188
  size_str = size_str.upper().replace("MB", "*1024*1024").replace("KB", "*1024")
136
- max_bytes = int(eval(size_str)) # safe for the limited substitutions we made
189
+ max_bytes = int(eval(size_str)) # simple controlled eval of KB/MB
137
190
  backup_count = int(backups_str)
138
191
  handler = RotatingFileHandler(cfg.path, maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8")
139
192
  elif cfg.rotation and cfg.rotation.startswith("time:"):
@@ -182,11 +235,66 @@ class LoggingHub:
182
235
  finally:
183
236
  _current_channel.reset(token)
184
237
 
185
- # -------- Utilities --------
238
+ # -------- Global kill switch (soft) --------
239
+ def enable_all(self) -> None:
240
+ self._killswitch.enabled = True
241
+
242
+ def disable_all(self) -> None:
243
+ self._killswitch.enabled = False
244
+
245
+ def is_enabled(self) -> bool:
246
+ return self._killswitch.enabled
247
+
248
+ @property
249
+ def logging_enabled(self) -> bool:
250
+ """Preferred property name."""
251
+ return self._killswitch.enabled
252
+
253
+ @logging_enabled.setter
254
+ def logging_enabled(self, value: bool) -> None:
255
+ self._killswitch.enabled = bool(value)
256
+
257
+ # Alias to match your requested spelling: hub.loggingenable
258
+ @property
259
+ def logEnabled(self) -> bool:
260
+ return self.logging_enabled
261
+
262
+ @logEnabled.setter
263
+ def logEnabled(self, value: bool) -> None:
264
+ self.logging_enabled = bool(value)
265
+
266
+ @contextmanager
267
+ def muted(self):
268
+ """Temporarily mute all logging in a with-block."""
269
+ prev = self._killswitch.enabled
270
+ try:
271
+ self._killswitch.enabled = False
272
+ yield
273
+ finally:
274
+ self._killswitch.enabled = prev
275
+
276
+ # -------- Hard nuke (affects 3rd-party libs too) --------
277
+ def hard_disable(self) -> None:
278
+ # Drop all messages of all loggers by using a level above CRITICAL
279
+ logging.disable(100)
280
+
281
+ def hard_enable(self) -> None:
282
+ logging.disable(0)
283
+
284
+ # -------- Convenience --------
186
285
  def set_default_channel(self, name: str) -> None:
187
286
  self._dispatch.set_default_channel(name)
188
287
 
189
- hub = LoggingHub("gedcomx") # this becomes your app logger root
288
+ def get_logger(self, name: Optional[str] = None) -> logging.Logger:
289
+ """Get a named logger under the hub root."""
290
+ if not name:
291
+ return self._root
292
+ return logging.getLogger(f"{self.root_name}.{name}")
293
+
294
+ # ──────────────────────────────────────────────────────────────────────────────
295
+ # Hub instance & sample bootstrap
296
+ # ──────────────────────────────────────────────────────────────────────────────
297
+ hub = LoggingHub("gedcomx") # app logger root
190
298
  hub.init_root() # do this ONCE at startup
191
299
 
192
300
  os.makedirs("logs", exist_ok=True)
@@ -211,16 +319,18 @@ hub.start_channel(
211
319
  name=serial_log,
212
320
  path=f"logs/{serial_log}.log",
213
321
  level=logging.DEBUG,
214
- rotation="size:10MB:3", # rotate by size, keep 3 backups
215
- ))
322
+ rotation="size:10MB:3",
323
+ )
324
+ )
216
325
 
217
326
  hub.start_channel(
218
327
  ChannelConfig(
219
328
  name=deserial_log,
220
329
  path=f"logs/{deserial_log}.log",
221
330
  level=logging.DEBUG,
222
- rotation="size:10MB:3", # rotate by size, keep 3 backups
223
- ))
331
+ rotation="size:10MB:3",
332
+ )
333
+ )
224
334
 
225
335
  # (optional) Also a console channel you can switch to
226
- hub.start_channel(ChannelConfig(name="console", path=None, level=logging.DEBUG))
336
+ hub.start_channel(ChannelConfig(name="console", path=None, level=logging.DEBUG))
gedcomx/name.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from enum import Enum
2
- from typing import List,Optional
2
+ from typing import List,Optional, Union
3
3
 
4
4
  """
5
5
  ======================================================================
@@ -12,6 +12,7 @@ from typing import List,Optional
12
12
  Updated:
13
13
  - 2025-08-31: _as_dict_ to only create entries in dict for fields that hold data
14
14
  - 2025-09-03: _from_json_ refactor
15
+ - 2025-09-09: added schema_class
15
16
 
16
17
  ======================================================================
17
18
  """
@@ -29,6 +30,7 @@ from .document import Document
29
30
  from .Extensions.rs10.rsLink import _rsLinks
30
31
  from .note import Note
31
32
  from .resource import Resource
33
+ from .schemas import schema_class
32
34
  from .source_reference import SourceReference
33
35
  from .logging_hub import hub, logging
34
36
  """
@@ -119,7 +121,8 @@ class NamePartType(Enum):
119
121
  NamePartType.Surname: "A surname."
120
122
  }
121
123
  return descriptions.get(self, "No description available.")
122
-
124
+
125
+ @schema_class()
123
126
  class NamePart:
124
127
  """Used to model a portion of a full name
125
128
  including the terms that make up that portion. Some name parts may have qualifiers
@@ -219,6 +222,7 @@ class NamePart:
219
222
  f"qualifiers={self.qualifiers!r})"
220
223
  )
221
224
 
225
+ @schema_class()
222
226
  class NameForm:
223
227
  """A representation of a name (a "name form")
224
228
  within a given cultural context, such as a given language and script.
@@ -285,6 +289,7 @@ class NameForm:
285
289
  def _fulltext_parts(self):
286
290
  pass
287
291
 
292
+ @schema_class()
288
293
  class Name(Conclusion):
289
294
  """**Defines a name of a person.**
290
295
 
@@ -334,11 +339,13 @@ class Name(Conclusion):
334
339
  #given = given.replace('/', '')
335
340
  #surname = surname.replace('/', '')
336
341
 
337
- given_name_part = NamePart(type = NamePartType.Given, value=given)
338
- surname_part = NamePart(type = NamePartType.Surname, value=surname)
342
+ parts =[]
343
+ if given: parts.append(NamePart(type = NamePartType.Given, value=given))
344
+ if surname: parts.append(NamePart(type = NamePartType.Surname, value=surname))
339
345
 
340
- name_form = NameForm(fullText=text,parts=[given_name_part,surname_part])
346
+ name_form = NameForm(fullText=text)
341
347
  name = Name(type=NameType.BirthName,nameForms=[name_form])
348
+
342
349
  else:
343
350
  name = Name()
344
351
  return name
@@ -346,7 +353,7 @@ class Name(Conclusion):
346
353
  def __init__(self, id: Optional[str] = None,
347
354
  lang: Optional[str] = None,
348
355
  sources: Optional[List[SourceReference]] = None,
349
- analysis: Optional[Document |Resource] = None,
356
+ analysis: Optional[Union[Resource,Document]] = None,
350
357
  notes: Optional[List[Note]] = None,
351
358
  confidence: Optional[ConfidenceLevel] = None,
352
359
  attribution: Optional[Attribution] = None,
@@ -358,6 +365,7 @@ class Name(Conclusion):
358
365
  self.type = type
359
366
  self.nameForms = nameForms if nameForms else []
360
367
  self.date = date
368
+ self.id = id if id else None # no need for id
361
369
 
362
370
  def _add_name_part(self, namepart: NamePart):
363
371
  if namepart and isinstance(namepart, NamePart):
@@ -418,6 +426,7 @@ class Name(Conclusion):
418
426
  f"date={self.date!r})"
419
427
  )
420
428
 
429
+
421
430
  class QuickName():
422
431
  def __new__(cls,name: str) -> Name:
423
432
  obj = Name(nameForms=[NameForm(fullText=name)])
gedcomx/note.py CHANGED
@@ -9,6 +9,7 @@ from typing import Any, Optional
9
9
  Created: 2025-08-25
10
10
  Updated:
11
11
  - 2025-09-03: _from_json_ refactor
12
+ - 2025-09-09: added schema_class
12
13
 
13
14
  ======================================================================
14
15
  """
@@ -19,6 +20,7 @@ GEDCOM Module Types
19
20
  ======================================================================
20
21
  """
21
22
  from .attribution import Attribution
23
+ from .schemas import schema_class
22
24
  from .logging_hub import hub, logging
23
25
  """
24
26
  ======================================================================
@@ -29,6 +31,7 @@ log = logging.getLogger("gedcomx")
29
31
  serial_log = "gedcomx.serialization"
30
32
  #=====================================================================
31
33
 
34
+ @schema_class()
32
35
  class Note:
33
36
  identifier = 'http://gedcomx.org/v1/Note'
34
37
  version = 'http://gedcomx.org/conceptual-model/v1'
@@ -65,19 +68,31 @@ class Note:
65
68
  return type_as_dict if type_as_dict != {} else None
66
69
  return Serialization.serialize_dict(type_as_dict)
67
70
 
68
- def __eq__(self, other):
71
+ # ---- hashing & equality ----
72
+ @staticmethod
73
+ def _norm(s: str | None) -> str:
74
+ # normalize None -> "", strip outer whitespace
75
+ return (s or "").strip()
76
+
77
+ def _key(self) -> tuple:
78
+ # Base identity: language (case-insensitive), subject, text
79
+ base = (
80
+ self._norm(self.lang).casefold(),
81
+ self._norm(self.subject),
82
+ self._norm(self.text),
83
+ )
84
+ # If you want attribution to affect identity AND it has a stable id,
85
+ # uncomment the next 3 lines:
86
+ # a = self.attribution
87
+ # a_id = getattr(a, "id", None) if a is not None else None
88
+ # return base + (a_id,)
89
+ return base
90
+
91
+ def __eq__(self, other: object) -> bool:
69
92
  if not isinstance(other, Note):
70
93
  return NotImplemented
94
+ return self._key() == other._key()
71
95
 
72
- def safe_str(val):
73
- return val.strip() if isinstance(val, str) else ''
74
-
75
- return (
76
- #safe_str(self.lang) == safe_str(other.lang) and
77
- #safe_str(self.subject) == safe_str(other.subject) and
78
- safe_str(self.text) == safe_str(other.text) #and
79
- # self.attribution == other.attribution # Assumes Attribution defines __eq__
80
- )
81
96
 
82
97
  @classmethod
83
98
  def _from_json_(cls, data: Any, context=None) -> "Note":
gedcomx/online_account.py CHANGED
@@ -1,6 +1,25 @@
1
1
  from typing import Optional
2
+ """
3
+ ======================================================================
4
+ Project: Gedcom-X
5
+ File: online_account.py
6
+ Author: David J. Cartwright
7
+ Purpose:
8
+
9
+ Created: 2025-08-25
10
+ Updated:
11
+ - 2025-09-09: added schema_class
12
+
13
+ ======================================================================
14
+ """
2
15
 
16
+ """
17
+ ======================================================================
18
+ GEDCOM Module Types
19
+ ======================================================================
20
+ """
3
21
  from .resource import Resource
22
+ from .schemas import schema_class
4
23
  from .logging_hub import hub, logging
5
24
  """
6
25
  ======================================================================
@@ -11,6 +30,7 @@ log = logging.getLogger("gedcomx")
11
30
  serial_log = "gedcomx.serialization"
12
31
  #=====================================================================
13
32
 
33
+ @schema_class()
14
34
  class OnlineAccount:
15
35
  identifier = 'http://gedcomx.org/v1/OnlineAccount'
16
36
  version = 'http://gedcomx.org/conceptual-model/v1'
gedcomx/person.py CHANGED
@@ -12,6 +12,7 @@ from urllib.parse import urljoin
12
12
  Updated:
13
13
  - 2025-08-31: _as_dict_ to only create entries in dict for fields that hold data
14
14
  - 2025-09-03: _from_json_ refactor
15
+ - 2025-09-09: added schema_class
15
16
 
16
17
  ======================================================================
17
18
  """
@@ -26,6 +27,7 @@ from .conclusion import ConfidenceLevel
26
27
  from .date import Date
27
28
  from .evidence_reference import EvidenceReference
28
29
  from .Extensions.rs10.rsLink import _rsLinks
30
+ from .extensible import Extensible
29
31
  from .fact import Fact, FactType
30
32
  from .gender import Gender, GenderType
31
33
  from .identifier import IdentifierList
@@ -33,6 +35,7 @@ from .logging_hub import hub, logging
33
35
  from .name import Name, QuickName
34
36
  from .note import Note
35
37
  from .resource import Resource
38
+ from .schemas import schema_class
36
39
  from .source_reference import SourceReference
37
40
  from .subject import Subject
38
41
  from .logging_hub import hub, logging
@@ -47,8 +50,8 @@ deserial_log = "degedcomx.serialization"
47
50
  #=====================================================================
48
51
 
49
52
 
50
-
51
- class Person(Subject):
53
+ @schema_class(toplevel=True)
54
+ class Person(Extensible,Subject):
52
55
  """A person in the system.
53
56
 
54
57
  Args:
@@ -63,7 +66,7 @@ class Person(Subject):
63
66
  identifier = 'http://gedcomx.org/v1/Person'
64
67
  version = 'http://gedcomx.org/conceptual-model/v1'
65
68
 
66
- def __init__(self, id: str | None = None,
69
+ def __init__(self, id: Optional[str] = None,
67
70
  lang: str = 'en',
68
71
  sources: Optional[List[SourceReference]] = None,
69
72
  analysis: Optional[Resource] = None,
@@ -79,10 +82,9 @@ class Person(Subject):
79
82
  names: Optional[List[Name]] = None,
80
83
  facts: Optional[List[Fact]] = None,
81
84
  living: Optional[bool] = None,
82
- links: Optional[_rsLinks] = None,
83
- uri: Optional[Resource] = None) -> None:
85
+ links: Optional[_rsLinks] = None,) -> None:
84
86
  # Call superclass initializer if needed
85
- super().__init__(id, lang, sources, analysis, notes, confidence, attribution, extracted, evidence, media, identifiers,links=links,uri=uri)
87
+ super().__init__(id, lang, sources, analysis, notes, confidence, attribution, extracted, evidence, media, identifiers,links=links)
86
88
 
87
89
  # Initialize mutable attributes to empty lists if None
88
90
  self.sources = sources if sources is not None else []
@@ -11,6 +11,7 @@ from typing import Any, Dict, List, Optional
11
11
  Updated:
12
12
  - 2025-09-01: filename PEP8 standard
13
13
  - 2025-09-03: _from_json_ refactored
14
+ - 2025-09-09: added schema_class
14
15
 
15
16
  ======================================================================
16
17
  """
@@ -29,6 +30,7 @@ from .identifier import IdentifierList
29
30
  from .note import Note
30
31
  from .resource import Resource
31
32
  from .source_reference import SourceReference
33
+ from .schemas import schema_class
32
34
  from .subject import Subject
33
35
  from .textvalue import TextValue
34
36
  from .uri import URI
@@ -42,7 +44,7 @@ log = logging.getLogger("gedcomx")
42
44
  serial_log = "gedcomx.serialization"
43
45
  #=====================================================================
44
46
 
45
-
47
+ @schema_class(toplevel=True)
46
48
  class PlaceDescription(Subject):
47
49
  """PlaceDescription describes the details of a place in terms of
48
50
  its name and possibly its type, time period, and/or a geospatial description