gedcom-x 0.5.5__py3-none-any.whl → 0.5.7__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.
gedcomx/Gender.py CHANGED
@@ -9,7 +9,7 @@ from gedcomx.Resource import Resource
9
9
 
10
10
  from .Conclusion import Conclusion
11
11
  from .Qualifier import Qualifier
12
- from .Serialization import Serialization
12
+
13
13
 
14
14
  from collections.abc import Sized
15
15
 
@@ -35,7 +35,7 @@ class Gender(Conclusion):
35
35
 
36
36
  def __init__(self,
37
37
  id: Optional[str] = None,
38
- lang: Optional[str] = 'en',
38
+ lang: Optional[str] = None,
39
39
  sources: Optional[List[SourceReference]] = None,
40
40
  analysis: Optional[Resource] = None,
41
41
  notes: Optional[List[Note]] = None,
@@ -48,19 +48,17 @@ class Gender(Conclusion):
48
48
 
49
49
  @property
50
50
  def _as_dict_(self):
51
+ from .Serialization import Serialization
52
+ type_as_dict = super()._as_dict_
53
+ if self.type:
54
+ type_as_dict['type'] = self.type.value if self.type else None
51
55
 
52
-
53
- type_as_dict = super()._as_dict_ # Start with base class fields
54
- # Only add Relationship-specific fields
55
- type_as_dict.update({
56
- 'type':self.type.value if self.type else None
57
-
58
- })
59
56
 
60
57
  return Serialization.serialize_dict(type_as_dict)
61
58
 
62
59
  @classmethod
63
60
  def _from_json_(cls,data):
61
+ from .Serialization import Serialization
64
62
 
65
63
  return Serialization.deserialize(data, Gender)
66
64
 
gedcomx/Identifier.py CHANGED
@@ -7,7 +7,7 @@ from collections.abc import Iterator
7
7
  import json
8
8
  from .Resource import Resource
9
9
  from .URI import URI
10
- from .Extensible.extensibles import ExtensibleEnum
10
+ from .ExtensibleEnum import ExtensibleEnum
11
11
 
12
12
  import secrets
13
13
  import string
@@ -26,11 +26,12 @@ def make_uid(length: int = 10, alphabet: str = string.ascii_letters + string.dig
26
26
  """
27
27
  if length <= 0:
28
28
  raise ValueError("length must be > 0")
29
- return ''.join(secrets.choice(alphabet) for _ in range(length))
29
+ return ''.join(secrets.choice(alphabet) for _ in range(length)).upper()
30
30
 
31
31
  class IdentifierType(ExtensibleEnum):
32
- """Enumeration of identifier types."""
33
32
  pass
33
+
34
+ """Enumeration of identifier types."""
34
35
  IdentifierType.register("Primary", "http://gedcomx.org/Primary")
35
36
  IdentifierType.register("Authority", "http://gedcomx.org/Authority")
36
37
  IdentifierType.register("Deprecated", "http://gedcomx.org/Deprecated")
@@ -42,7 +43,7 @@ class Identifier:
42
43
  identifier = 'http://gedcomx.org/v1/Identifier'
43
44
  version = 'http://gedcomx.org/conceptual-model/v1'
44
45
 
45
- def __init__(self, value: Optional[List[URI]], type: Optional[IdentifierType] = IdentifierType.Primary) -> None:
46
+ def __init__(self, value: Optional[List[URI]], type: Optional[IdentifierType] = IdentifierType.Primary) -> None: # type: ignore
46
47
  if not isinstance(value,list):
47
48
  value = [value] if value else []
48
49
  self.type = type
@@ -51,11 +52,11 @@ class Identifier:
51
52
  @property
52
53
  def _as_dict_(self):
53
54
  from .Serialization import Serialization
54
- type_as_dict = {
55
- 'value': [v for v in self.values] if self.values else None,
56
- 'type': self.type.value if self.type else None
57
-
58
- }
55
+ type_as_dict = {}
56
+ if self.values:
57
+ type_as_dict["value"] = list(self.values) # or [v for v in self.values]
58
+ if self.type:
59
+ type_as_dict["type"] = getattr(self.type, "value", self.type) # type: ignore[attr-defined]
59
60
 
60
61
  return Serialization.serialize_dict(type_as_dict)
61
62
 
@@ -64,10 +65,6 @@ class Identifier:
64
65
  """
65
66
  Construct an Identifier from a dict parsed from JSON.
66
67
  """
67
- #for name, member in IdentifierType.__members__.items():
68
- # print(name)
69
-
70
-
71
68
 
72
69
  for key in data.keys():
73
70
  type = key
gedcomx/LoggingHub.py ADDED
@@ -0,0 +1,207 @@
1
+ # logging_hub.py
2
+ from __future__ import annotations
3
+ import logging
4
+ import contextvars
5
+ import os
6
+ from contextlib import contextmanager
7
+ from dataclasses import dataclass
8
+ from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
9
+ from typing import Dict, Optional
10
+
11
+ # Context key: which "channel" (log) is current?
12
+ _current_channel: contextvars.ContextVar[str] = contextvars.ContextVar("current_log_channel", default="default")
13
+
14
+ def get_current_channel() -> str:
15
+ return _current_channel.get()
16
+
17
+ def set_current_channel(name: str) -> None:
18
+ _current_channel.set(name)
19
+
20
+ class ChannelFilter(logging.Filter):
21
+ """Injects the current channel into every LogRecord."""
22
+ def filter(self, record: logging.LogRecord) -> bool:
23
+ record.log_channel = get_current_channel()
24
+ return True
25
+
26
+ class DispatchingHandler(logging.Handler):
27
+ """
28
+ Routes records to a per-channel handler (file/stream), based on LogRecord.log_channel
29
+ which is set by ChannelFilter.
30
+ """
31
+ def __init__(self):
32
+ super().__init__()
33
+ self._channel_handlers: Dict[str, logging.Handler] = {}
34
+ self._enabled: Dict[str, bool] = {}
35
+ self._default_channel = "default"
36
+
37
+ def set_default_channel(self, name: str) -> None:
38
+ self._default_channel = name
39
+
40
+ def add_channel(self, name: str, handler: logging.Handler, enabled: bool = True) -> None:
41
+ self._channel_handlers[name] = handler
42
+ self._enabled[name] = enabled
43
+
44
+ def enable(self, name: str) -> None:
45
+ self._enabled[name] = True
46
+
47
+ def disable(self, name: str) -> None:
48
+ self._enabled[name] = False
49
+
50
+ def remove_channel(self, name: str) -> None:
51
+ h = self._channel_handlers.pop(name, None)
52
+ self._enabled.pop(name, None)
53
+ if h:
54
+ try:
55
+ h.flush()
56
+ h.close()
57
+ except Exception:
58
+ pass
59
+
60
+ def has_channel(self, name: str) -> bool:
61
+ return name in self._channel_handlers
62
+
63
+ def emit(self, record: logging.LogRecord) -> None:
64
+ channel = getattr(record, "log_channel", None) or self._default_channel
65
+ handler = self._channel_handlers.get(channel) or self._channel_handlers.get(self._default_channel)
66
+ if not handler:
67
+ return # nothing to write to
68
+ if not self._enabled.get(channel, True):
69
+ return # channel muted
70
+ handler.emit(record)
71
+
72
+ @dataclass
73
+ class ChannelConfig:
74
+ name: str
75
+ path: Optional[str] = None
76
+ level: int = logging.INFO
77
+ fmt: str = "[%(asctime)s] %(levelname)s %(log_channel)s %(name)s: %(message)s"
78
+ datefmt: str = "%Y-%m-%d %H:%M:%S"
79
+ rotation: Optional[str] = None
80
+ # rotation options:
81
+ # None -> plain FileHandler
82
+ # "size:10MB:3" -> RotatingFileHandler(maxBytes=10MB, backupCount=3)
83
+ # "time:midnight:7" -> TimedRotatingFileHandler(when="midnight", backupCount=7)
84
+
85
+ class LoggingHub:
86
+ """
87
+ A centralized, context-aware logging hub.
88
+ Usage:
89
+ 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")
93
+
94
+ # In any module:
95
+ log = logging.getLogger("gedcomx")
96
+ log.info("hello") # goes to current channel
97
+
98
+ with hub.use("import-job-42"):
99
+ log.info("within job 42")
100
+ """
101
+ def __init__(self, root_logger_name: str = "gedcomx"):
102
+ self.root_name = root_logger_name
103
+ self._root = logging.getLogger(self.root_name)
104
+ self._dispatch = DispatchingHandler()
105
+ self._root.setLevel(logging.DEBUG) # Let handlers control final level
106
+
107
+ self._filter = ChannelFilter()
108
+ self._initialized = False
109
+
110
+ # -------- Initialization --------
111
+ def init_root(self) -> None:
112
+ if self._initialized:
113
+ return
114
+ # Clean existing handlers on the root logger (optional safety)
115
+ for h in list(self._root.handlers):
116
+ self._root.removeHandler(h)
117
+ self._root.addFilter(self._filter)
118
+ self._root.addHandler(self._dispatch)
119
+ self._initialized = True
120
+
121
+ # -------- Channel Management --------
122
+ def start_channel(self, cfg: ChannelConfig, make_current: bool = False, enabled: bool = True) -> None:
123
+ """Create/replace a channel with a file/rotating handler."""
124
+ handler: logging.Handler
125
+ formatter = logging.Formatter(cfg.fmt, datefmt=cfg.datefmt)
126
+
127
+ if cfg.path is None:
128
+ # StreamHandler to stdout if no path provided
129
+ handler = logging.StreamHandler()
130
+ else:
131
+ # Rotation options
132
+ if cfg.rotation and cfg.rotation.startswith("size:"):
133
+ # "size:10MB:3"
134
+ _, size_str, backups_str = cfg.rotation.split(":")
135
+ 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
137
+ backup_count = int(backups_str)
138
+ handler = RotatingFileHandler(cfg.path, maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8")
139
+ elif cfg.rotation and cfg.rotation.startswith("time:"):
140
+ # "time:midnight:7" or "time:H:24"
141
+ parts = cfg.rotation.split(":")
142
+ when = parts[1]
143
+ backup_count = int(parts[2]) if len(parts) > 2 else 7
144
+ handler = TimedRotatingFileHandler(cfg.path, when=when, backupCount=backup_count, encoding="utf-8", utc=False)
145
+ else:
146
+ handler = logging.FileHandler(cfg.path, encoding="utf-8")
147
+
148
+ handler.setLevel(cfg.level)
149
+ handler.setFormatter(formatter)
150
+
151
+ # Replace if exists
152
+ if self._dispatch.has_channel(cfg.name):
153
+ self._dispatch.remove_channel(cfg.name)
154
+ self._dispatch.add_channel(cfg.name, handler, enabled=enabled)
155
+
156
+ if make_current:
157
+ self.set_current(cfg.name)
158
+
159
+ def stop_channel(self, name: str) -> None:
160
+ self._dispatch.remove_channel(name)
161
+
162
+ def enable(self, name: str) -> None:
163
+ self._dispatch.enable(name)
164
+
165
+ def disable(self, name: str) -> None:
166
+ self._dispatch.disable(name)
167
+
168
+ def list_channels(self) -> Dict[str, bool]:
169
+ """Return dict of channel -> enabled?"""
170
+ return {name: enabled for name, enabled in self._dispatch._enabled.items()}
171
+
172
+ # -------- Current Channel --------
173
+ def set_current(self, name: str) -> None:
174
+ set_current_channel(name)
175
+
176
+ @contextmanager
177
+ def use(self, name: str):
178
+ """Temporarily switch to a channel within a with-block."""
179
+ token = _current_channel.set(name)
180
+ try:
181
+ yield
182
+ finally:
183
+ _current_channel.reset(token)
184
+
185
+ # -------- Utilities --------
186
+ def set_default_channel(self, name: str) -> None:
187
+ self._dispatch.set_default_channel(name)
188
+
189
+ hub = LoggingHub("gedcomx") # this becomes your app logger root
190
+ hub.init_root() # do this ONCE at startup
191
+
192
+ os.makedirs("logs", exist_ok=True)
193
+
194
+ # 1) Start a default channel (file w/ daily rotation)
195
+ hub.start_channel(
196
+ ChannelConfig(
197
+ name="default",
198
+ path="logs/app.log",
199
+ level=logging.INFO,
200
+ fmt="[%(asctime)s] %(levelname)s %(log_channel)s %(name)s: %(message)s",
201
+ rotation="time:midnight:7",
202
+ ),
203
+ make_current=True
204
+ )
205
+
206
+ # (optional) Also a console channel you can switch to
207
+ hub.start_channel(ChannelConfig(name="console", path=None, level=logging.DEBUG))
gedcomx/Mutations.py CHANGED
@@ -1,4 +1,4 @@
1
- from .Gedcom5x import GedcomRecord
1
+ from .Gedcom5x import Gedcom5xRecord
2
2
  from .Fact import Fact, FactType
3
3
  from .Event import Event, EventType
4
4
 
@@ -178,13 +178,13 @@ fact_event_table = {
178
178
  }
179
179
 
180
180
  class GedcomXObject:
181
- def __init__(self,record: GedcomRecord | None = None) -> None:
182
- self.created_with_tag: str = record.tag if record and isinstance(record, GedcomRecord) else None
183
- self.created_at_level: int = record.level if record and isinstance(record, GedcomRecord) else None
184
- self.created_at_line_number: int = record.line_number if record and isinstance(record, GedcomRecord) else None
181
+ def __init__(self,record: Gedcom5xRecord | None = None) -> None:
182
+ self.created_with_tag: str = record.tag if record and isinstance(record, Gedcom5xRecord) else None
183
+ self.created_at_level: int = record.level if record and isinstance(record, Gedcom5xRecord) else None
184
+ self.created_at_line_number: int = record.line_number if record and isinstance(record, Gedcom5xRecord) else None
185
185
 
186
186
  class GedcomXSourceOrDocument(GedcomXObject):
187
- def __init__(self,record: GedcomRecord | None = None) -> None:
187
+ def __init__(self,record: Gedcom5xRecord | None = None) -> None:
188
188
  super().__init__(record)
189
189
  self.title: str = None
190
190
  self.citation: str = None
@@ -204,7 +204,7 @@ class GedcomXSourceOrDocument(GedcomXObject):
204
204
  self.description: str = None
205
205
 
206
206
  class GedcomXEventOrFact(GedcomXObject):
207
- def __new__(cls,record: GedcomRecord | None = None, object_stack: dict | None = None) -> object:
207
+ def __new__(cls,record: Gedcom5xRecord | None = None, object_stack: dict | None = None) -> object:
208
208
  super().__init__(record)
209
209
  if record.tag in fact_event_table.keys():
210
210
 
@@ -219,7 +219,7 @@ class GedcomXEventOrFact(GedcomXObject):
219
219
  raise ValueError(f"{record.tag} not found in map")
220
220
 
221
221
  class GedcomXRelationshipBuilder(GedcomXObject):
222
- def __new__(cls,record: GedcomRecord | None = None, object_stack: dict | None = None) -> object:
222
+ def __new__(cls,record: Gedcom5xRecord | None = None, object_stack: dict | None = None) -> object:
223
223
  last_relationship = object_stack.get('lastrelationship',None)
224
224
  last_relationship_data = object_stack.get('lastrelationshipdata',None)
225
225
  if not isinstance(last_relationship_data,dict):