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.
- gedcom_x-0.5.7.dist-info/METADATA +144 -0
- gedcom_x-0.5.7.dist-info/RECORD +49 -0
- gedcomx/Address.py +13 -13
- gedcomx/Agent.py +28 -16
- gedcomx/Attribution.py +34 -7
- gedcomx/Conclusion.py +24 -13
- gedcomx/Converter.py +1034 -0
- gedcomx/Coverage.py +7 -6
- gedcomx/Date.py +11 -4
- gedcomx/Document.py +2 -1
- gedcomx/Event.py +95 -20
- gedcomx/ExtensibleEnum.py +183 -0
- gedcomx/Extensions/__init__.py +1 -0
- gedcomx/Extensions/rs10/__init__.py +1 -0
- gedcomx/Extensions/rs10/rsLink.py +116 -0
- gedcomx/Fact.py +16 -13
- gedcomx/Gedcom5x.py +115 -77
- gedcomx/GedcomX.py +184 -1034
- gedcomx/Gender.py +7 -9
- gedcomx/Identifier.py +10 -13
- gedcomx/LoggingHub.py +207 -0
- gedcomx/Mutations.py +8 -8
- gedcomx/Name.py +207 -87
- gedcomx/Note.py +16 -9
- gedcomx/Person.py +39 -18
- gedcomx/PlaceDescription.py +70 -19
- gedcomx/PlaceReference.py +40 -8
- gedcomx/Qualifier.py +39 -12
- gedcomx/Relationship.py +5 -3
- gedcomx/Resource.py +38 -28
- gedcomx/Serialization.py +773 -358
- gedcomx/SourceDescription.py +133 -74
- gedcomx/SourceReference.py +10 -9
- gedcomx/Subject.py +5 -21
- gedcomx/Translation.py +976 -1
- gedcomx/URI.py +1 -1
- gedcomx/__init__.py +4 -2
- gedcom_x-0.5.5.dist-info/METADATA +0 -17
- gedcom_x-0.5.5.dist-info/RECORD +0 -43
- {gedcom_x-0.5.5.dist-info → gedcom_x-0.5.7.dist-info}/WHEEL +0 -0
- {gedcom_x-0.5.5.dist-info → gedcom_x-0.5.7.dist-info}/top_level.txt +0 -0
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
|
-
|
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] =
|
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 .
|
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
|
-
|
56
|
-
|
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
|
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:
|
182
|
-
self.created_with_tag: str = record.tag if record and isinstance(record,
|
183
|
-
self.created_at_level: int = record.level if record and isinstance(record,
|
184
|
-
self.created_at_line_number: int = record.line_number if record and isinstance(record,
|
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:
|
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:
|
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:
|
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):
|