gedcom-x 0.5.2__py3-none-any.whl → 0.5.6__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.6.dist-info/METADATA +144 -0
- gedcom_x-0.5.6.dist-info/RECORD +45 -0
- gedcomx/Address.py +2 -0
- gedcomx/Agent.py +9 -2
- gedcomx/Attribution.py +10 -46
- gedcomx/Conclusion.py +85 -21
- gedcomx/Coverage.py +10 -0
- gedcomx/Date.py +2 -7
- gedcomx/Document.py +27 -6
- gedcomx/Event.py +20 -1
- gedcomx/Exceptions.py +6 -0
- gedcomx/ExtensibleEnum.py +183 -0
- gedcomx/Fact.py +7 -8
- gedcomx/Gedcom.py +38 -404
- gedcomx/Gedcom5x.py +579 -0
- gedcomx/GedcomX.py +48 -26
- gedcomx/Gender.py +6 -40
- gedcomx/Identifier.py +151 -97
- gedcomx/LoggingHub.py +186 -0
- gedcomx/Mutations.py +228 -0
- gedcomx/Name.py +6 -0
- gedcomx/Person.py +49 -90
- gedcomx/PlaceDescription.py +23 -14
- gedcomx/PlaceReference.py +12 -15
- gedcomx/Relationship.py +23 -54
- gedcomx/Resource.py +17 -3
- gedcomx/Serialization.py +352 -31
- gedcomx/SourceDescription.py +6 -9
- gedcomx/SourceReference.py +20 -86
- gedcomx/Subject.py +4 -4
- gedcomx/Translation.py +219 -0
- gedcomx/URI.py +1 -0
- gedcomx/__init__.py +8 -1
- gedcom_x-0.5.2.dist-info/METADATA +0 -17
- gedcom_x-0.5.2.dist-info/RECORD +0 -42
- gedcomx/_Links.py +0 -37
- gedcomx/g7interop.py +0 -205
- {gedcom_x-0.5.2.dist-info → gedcom_x-0.5.6.dist-info}/WHEEL +0 -0
- {gedcom_x-0.5.2.dist-info → gedcom_x-0.5.6.dist-info}/top_level.txt +0 -0
gedcomx/LoggingHub.py
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
# logging_hub.py
|
2
|
+
from __future__ import annotations
|
3
|
+
import logging
|
4
|
+
import contextvars
|
5
|
+
from contextlib import contextmanager
|
6
|
+
from dataclasses import dataclass
|
7
|
+
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
|
8
|
+
from typing import Dict, Optional
|
9
|
+
|
10
|
+
# Context key: which "channel" (log) is current?
|
11
|
+
_current_channel: contextvars.ContextVar[str] = contextvars.ContextVar("current_log_channel", default="default")
|
12
|
+
|
13
|
+
def get_current_channel() -> str:
|
14
|
+
return _current_channel.get()
|
15
|
+
|
16
|
+
def set_current_channel(name: str) -> None:
|
17
|
+
_current_channel.set(name)
|
18
|
+
|
19
|
+
class ChannelFilter(logging.Filter):
|
20
|
+
"""Injects the current channel into every LogRecord."""
|
21
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
22
|
+
record.log_channel = get_current_channel()
|
23
|
+
return True
|
24
|
+
|
25
|
+
class DispatchingHandler(logging.Handler):
|
26
|
+
"""
|
27
|
+
Routes records to a per-channel handler (file/stream), based on LogRecord.log_channel
|
28
|
+
which is set by ChannelFilter.
|
29
|
+
"""
|
30
|
+
def __init__(self):
|
31
|
+
super().__init__()
|
32
|
+
self._channel_handlers: Dict[str, logging.Handler] = {}
|
33
|
+
self._enabled: Dict[str, bool] = {}
|
34
|
+
self._default_channel = "default"
|
35
|
+
|
36
|
+
def set_default_channel(self, name: str) -> None:
|
37
|
+
self._default_channel = name
|
38
|
+
|
39
|
+
def add_channel(self, name: str, handler: logging.Handler, enabled: bool = True) -> None:
|
40
|
+
self._channel_handlers[name] = handler
|
41
|
+
self._enabled[name] = enabled
|
42
|
+
|
43
|
+
def enable(self, name: str) -> None:
|
44
|
+
self._enabled[name] = True
|
45
|
+
|
46
|
+
def disable(self, name: str) -> None:
|
47
|
+
self._enabled[name] = False
|
48
|
+
|
49
|
+
def remove_channel(self, name: str) -> None:
|
50
|
+
h = self._channel_handlers.pop(name, None)
|
51
|
+
self._enabled.pop(name, None)
|
52
|
+
if h:
|
53
|
+
try:
|
54
|
+
h.flush()
|
55
|
+
h.close()
|
56
|
+
except Exception:
|
57
|
+
pass
|
58
|
+
|
59
|
+
def has_channel(self, name: str) -> bool:
|
60
|
+
return name in self._channel_handlers
|
61
|
+
|
62
|
+
def emit(self, record: logging.LogRecord) -> None:
|
63
|
+
channel = getattr(record, "log_channel", None) or self._default_channel
|
64
|
+
handler = self._channel_handlers.get(channel) or self._channel_handlers.get(self._default_channel)
|
65
|
+
if not handler:
|
66
|
+
return # nothing to write to
|
67
|
+
if not self._enabled.get(channel, True):
|
68
|
+
return # channel muted
|
69
|
+
handler.emit(record)
|
70
|
+
|
71
|
+
@dataclass
|
72
|
+
class ChannelConfig:
|
73
|
+
name: str
|
74
|
+
path: Optional[str] = None
|
75
|
+
level: int = logging.INFO
|
76
|
+
fmt: str = "[%(asctime)s] %(levelname)s %(log_channel)s %(name)s: %(message)s"
|
77
|
+
datefmt: str = "%Y-%m-%d %H:%M:%S"
|
78
|
+
rotation: Optional[str] = None
|
79
|
+
# rotation options:
|
80
|
+
# None -> plain FileHandler
|
81
|
+
# "size:10MB:3" -> RotatingFileHandler(maxBytes=10MB, backupCount=3)
|
82
|
+
# "time:midnight:7" -> TimedRotatingFileHandler(when="midnight", backupCount=7)
|
83
|
+
|
84
|
+
class LoggingHub:
|
85
|
+
"""
|
86
|
+
A centralized, context-aware logging hub.
|
87
|
+
Usage:
|
88
|
+
hub = LoggingHub()
|
89
|
+
hub.init_root() # do once at startup
|
90
|
+
hub.start_channel(ChannelConfig(name="default", path="logs/default.log"))
|
91
|
+
hub.set_current("default")
|
92
|
+
|
93
|
+
# In any module:
|
94
|
+
log = logging.getLogger("gedcomx")
|
95
|
+
log.info("hello") # goes to current channel
|
96
|
+
|
97
|
+
with hub.use("import-job-42"):
|
98
|
+
log.info("within job 42")
|
99
|
+
"""
|
100
|
+
def __init__(self, root_logger_name: str = "gedcomx"):
|
101
|
+
self.root_name = root_logger_name
|
102
|
+
self._root = logging.getLogger(self.root_name)
|
103
|
+
self._dispatch = DispatchingHandler()
|
104
|
+
self._root.setLevel(logging.DEBUG) # Let handlers control final level
|
105
|
+
|
106
|
+
self._filter = ChannelFilter()
|
107
|
+
self._initialized = False
|
108
|
+
|
109
|
+
# -------- Initialization --------
|
110
|
+
def init_root(self) -> None:
|
111
|
+
if self._initialized:
|
112
|
+
return
|
113
|
+
# Clean existing handlers on the root logger (optional safety)
|
114
|
+
for h in list(self._root.handlers):
|
115
|
+
self._root.removeHandler(h)
|
116
|
+
self._root.addFilter(self._filter)
|
117
|
+
self._root.addHandler(self._dispatch)
|
118
|
+
self._initialized = True
|
119
|
+
|
120
|
+
# -------- Channel Management --------
|
121
|
+
def start_channel(self, cfg: ChannelConfig, make_current: bool = False, enabled: bool = True) -> None:
|
122
|
+
"""Create/replace a channel with a file/rotating handler."""
|
123
|
+
handler: logging.Handler
|
124
|
+
formatter = logging.Formatter(cfg.fmt, datefmt=cfg.datefmt)
|
125
|
+
|
126
|
+
if cfg.path is None:
|
127
|
+
# StreamHandler to stdout if no path provided
|
128
|
+
handler = logging.StreamHandler()
|
129
|
+
else:
|
130
|
+
# Rotation options
|
131
|
+
if cfg.rotation and cfg.rotation.startswith("size:"):
|
132
|
+
# "size:10MB:3"
|
133
|
+
_, size_str, backups_str = cfg.rotation.split(":")
|
134
|
+
size_str = size_str.upper().replace("MB", "*1024*1024").replace("KB", "*1024")
|
135
|
+
max_bytes = int(eval(size_str)) # safe for the limited substitutions we made
|
136
|
+
backup_count = int(backups_str)
|
137
|
+
handler = RotatingFileHandler(cfg.path, maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8")
|
138
|
+
elif cfg.rotation and cfg.rotation.startswith("time:"):
|
139
|
+
# "time:midnight:7" or "time:H:24"
|
140
|
+
parts = cfg.rotation.split(":")
|
141
|
+
when = parts[1]
|
142
|
+
backup_count = int(parts[2]) if len(parts) > 2 else 7
|
143
|
+
handler = TimedRotatingFileHandler(cfg.path, when=when, backupCount=backup_count, encoding="utf-8", utc=False)
|
144
|
+
else:
|
145
|
+
handler = logging.FileHandler(cfg.path, encoding="utf-8")
|
146
|
+
|
147
|
+
handler.setLevel(cfg.level)
|
148
|
+
handler.setFormatter(formatter)
|
149
|
+
|
150
|
+
# Replace if exists
|
151
|
+
if self._dispatch.has_channel(cfg.name):
|
152
|
+
self._dispatch.remove_channel(cfg.name)
|
153
|
+
self._dispatch.add_channel(cfg.name, handler, enabled=enabled)
|
154
|
+
|
155
|
+
if make_current:
|
156
|
+
self.set_current(cfg.name)
|
157
|
+
|
158
|
+
def stop_channel(self, name: str) -> None:
|
159
|
+
self._dispatch.remove_channel(name)
|
160
|
+
|
161
|
+
def enable(self, name: str) -> None:
|
162
|
+
self._dispatch.enable(name)
|
163
|
+
|
164
|
+
def disable(self, name: str) -> None:
|
165
|
+
self._dispatch.disable(name)
|
166
|
+
|
167
|
+
def list_channels(self) -> Dict[str, bool]:
|
168
|
+
"""Return dict of channel -> enabled?"""
|
169
|
+
return {name: enabled for name, enabled in self._dispatch._enabled.items()}
|
170
|
+
|
171
|
+
# -------- Current Channel --------
|
172
|
+
def set_current(self, name: str) -> None:
|
173
|
+
set_current_channel(name)
|
174
|
+
|
175
|
+
@contextmanager
|
176
|
+
def use(self, name: str):
|
177
|
+
"""Temporarily switch to a channel within a with-block."""
|
178
|
+
token = _current_channel.set(name)
|
179
|
+
try:
|
180
|
+
yield
|
181
|
+
finally:
|
182
|
+
_current_channel.reset(token)
|
183
|
+
|
184
|
+
# -------- Utilities --------
|
185
|
+
def set_default_channel(self, name: str) -> None:
|
186
|
+
self._dispatch.set_default_channel(name)
|
gedcomx/Mutations.py
ADDED
@@ -0,0 +1,228 @@
|
|
1
|
+
from .Gedcom5x import GedcomRecord
|
2
|
+
from .Fact import Fact, FactType
|
3
|
+
from .Event import Event, EventType
|
4
|
+
|
5
|
+
fact_event_table = {
|
6
|
+
# Person Fact / Event Types
|
7
|
+
"ADOP": {
|
8
|
+
"Fact": FactType.AdoptiveParent,
|
9
|
+
"Event": EventType.Adoption,
|
10
|
+
},
|
11
|
+
"CHR": {
|
12
|
+
"Fact": FactType.AdultChristening,
|
13
|
+
"Event": EventType.AdultChristening,
|
14
|
+
},
|
15
|
+
"EVEN": {
|
16
|
+
"Fact": FactType.Amnesty,
|
17
|
+
# no Event
|
18
|
+
},
|
19
|
+
"BAPM": {
|
20
|
+
"Fact": FactType.Baptism,
|
21
|
+
"Event": EventType.Baptism,
|
22
|
+
},
|
23
|
+
"BARM": {
|
24
|
+
"Fact": FactType.BarMitzvah,
|
25
|
+
"Event": EventType.BarMitzvah,
|
26
|
+
},
|
27
|
+
"BASM": {
|
28
|
+
"Fact": FactType.BatMitzvah,
|
29
|
+
"Event": EventType.BatMitzvah,
|
30
|
+
},
|
31
|
+
"BIRT": {
|
32
|
+
"Fact": FactType.Birth,
|
33
|
+
"Event": EventType.Birth,
|
34
|
+
},
|
35
|
+
"BIRT, CHR": {
|
36
|
+
"Fact": FactType.Birth,
|
37
|
+
"Event": EventType.Birth,
|
38
|
+
},
|
39
|
+
"BLES": {
|
40
|
+
"Fact": FactType.Blessing,
|
41
|
+
"Event": EventType.Blessing,
|
42
|
+
},
|
43
|
+
"BURI": {
|
44
|
+
"Fact": FactType.Burial,
|
45
|
+
"Event": EventType.Burial,
|
46
|
+
},
|
47
|
+
"CAST": {
|
48
|
+
"Fact": FactType.Caste,
|
49
|
+
# no Event
|
50
|
+
},
|
51
|
+
"CENS": {
|
52
|
+
"Fact": FactType.Census,
|
53
|
+
"Event": EventType.Census,
|
54
|
+
},
|
55
|
+
"CIRC": {
|
56
|
+
"Fact": FactType.Circumcision,
|
57
|
+
"Event": EventType.Circumcision,
|
58
|
+
},
|
59
|
+
"CONF": {
|
60
|
+
"Fact": FactType.Confirmation,
|
61
|
+
"Event": EventType.Confirmation,
|
62
|
+
},
|
63
|
+
"CREM": {
|
64
|
+
"Fact": FactType.Cremation,
|
65
|
+
"Event": EventType.Cremation,
|
66
|
+
},
|
67
|
+
"DEAT": {
|
68
|
+
"Fact": FactType.Death,
|
69
|
+
"Event": EventType.Death,
|
70
|
+
},
|
71
|
+
"EDUC": {
|
72
|
+
"Fact": FactType.Education,
|
73
|
+
"Event": EventType.Education,
|
74
|
+
},
|
75
|
+
"EMIG": {
|
76
|
+
"Fact": FactType.Emigration,
|
77
|
+
"Event": EventType.Emigration,
|
78
|
+
},
|
79
|
+
"FCOM": {
|
80
|
+
"Fact": FactType.FirstCommunion,
|
81
|
+
"Event": EventType.FirstCommunion,
|
82
|
+
},
|
83
|
+
"GRAD": {
|
84
|
+
"Fact": FactType.Graduation,
|
85
|
+
# no Event
|
86
|
+
},
|
87
|
+
"IMMI": {
|
88
|
+
"Fact": FactType.Immigration,
|
89
|
+
"Event": EventType.Immigration,
|
90
|
+
},
|
91
|
+
"MIL": {
|
92
|
+
"Fact": FactType.MilitaryService,
|
93
|
+
# no Event
|
94
|
+
},
|
95
|
+
"NATI": {
|
96
|
+
"Fact": FactType.Nationality,
|
97
|
+
# no Event
|
98
|
+
},
|
99
|
+
"NATU": {
|
100
|
+
"Fact": FactType.Naturalization,
|
101
|
+
"Event": EventType.Naturalization,
|
102
|
+
},
|
103
|
+
"OCCU": {
|
104
|
+
"Fact": FactType.Occupation,
|
105
|
+
# no Event
|
106
|
+
},
|
107
|
+
"ORDN": {
|
108
|
+
"Fact": FactType.Ordination,
|
109
|
+
"Event": EventType.Ordination,
|
110
|
+
},
|
111
|
+
"DSCR": {
|
112
|
+
"Fact": FactType.PhysicalDescription,
|
113
|
+
# no Event
|
114
|
+
},
|
115
|
+
"PROB": {
|
116
|
+
"Fact": FactType.Probate,
|
117
|
+
# no Event
|
118
|
+
},
|
119
|
+
"PROP": {
|
120
|
+
"Fact": FactType.Property,
|
121
|
+
# no Event
|
122
|
+
},
|
123
|
+
"RELI": {
|
124
|
+
"Fact": FactType.Religion,
|
125
|
+
# no Event
|
126
|
+
},
|
127
|
+
"RESI": {
|
128
|
+
"Fact": FactType.Residence,
|
129
|
+
# no Event
|
130
|
+
},
|
131
|
+
"WILL": {
|
132
|
+
"Fact": FactType.Will,
|
133
|
+
# no Event
|
134
|
+
},
|
135
|
+
|
136
|
+
# Couple Relationship Fact / Event Types
|
137
|
+
"ANUL": {
|
138
|
+
"Fact": FactType.Annulment,
|
139
|
+
"Event": EventType.Annulment,
|
140
|
+
},
|
141
|
+
"DIV": {
|
142
|
+
"Fact": FactType.Divorce,
|
143
|
+
"Event": EventType.Divorce,
|
144
|
+
},
|
145
|
+
"DIVF": {
|
146
|
+
"Fact": FactType.DivorceFiling,
|
147
|
+
"Event": EventType.DivorceFiling,
|
148
|
+
},
|
149
|
+
"ENGA": {
|
150
|
+
"Fact": FactType.Engagement,
|
151
|
+
"Event": EventType.Engagement,
|
152
|
+
},
|
153
|
+
"MARR": {
|
154
|
+
"Fact": FactType.Marriage,
|
155
|
+
"Event": EventType.Marriage,
|
156
|
+
},
|
157
|
+
"MARB": {
|
158
|
+
"Fact": FactType.MarriageBanns,
|
159
|
+
# no Event
|
160
|
+
},
|
161
|
+
"MARC": {
|
162
|
+
"Fact": FactType.MarriageContract,
|
163
|
+
# no Event
|
164
|
+
},
|
165
|
+
"MARL": {
|
166
|
+
"Fact": FactType.MarriageLicense,
|
167
|
+
# no Event
|
168
|
+
},
|
169
|
+
"MARS":{
|
170
|
+
"Fact":EventType.MarriageSettlment
|
171
|
+
|
172
|
+
},
|
173
|
+
"SEPA": {
|
174
|
+
"Fact": FactType.Separation,
|
175
|
+
# no Event
|
176
|
+
},
|
177
|
+
|
178
|
+
}
|
179
|
+
|
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
|
185
|
+
|
186
|
+
class GedcomXSourceOrDocument(GedcomXObject):
|
187
|
+
def __init__(self,record: GedcomRecord | None = None) -> None:
|
188
|
+
super().__init__(record)
|
189
|
+
self.title: str = None
|
190
|
+
self.citation: str = None
|
191
|
+
self.page: str = None
|
192
|
+
self.contributor: str = None
|
193
|
+
self.publisher: str = None
|
194
|
+
self.rights: str = None
|
195
|
+
self.url: str = None
|
196
|
+
self.medium: str = None
|
197
|
+
self.type: str = None
|
198
|
+
self.format: str = None
|
199
|
+
self.created: str = None
|
200
|
+
self.modified: str = None
|
201
|
+
self.language: str = None
|
202
|
+
self.relation: str = None
|
203
|
+
self.identifier: str = None
|
204
|
+
self.description: str = None
|
205
|
+
|
206
|
+
class GedcomXEventOrFact(GedcomXObject):
|
207
|
+
def __new__(cls,record: GedcomRecord | None = None, object_stack: dict | None = None) -> object:
|
208
|
+
super().__init__(record)
|
209
|
+
if record.tag in fact_event_table.keys():
|
210
|
+
|
211
|
+
if 'Fact' in fact_event_table[record.tag].keys():
|
212
|
+
obj = Fact(type=fact_event_table[record.tag]['Fact'])
|
213
|
+
return obj
|
214
|
+
elif 'Event' in fact_event_table[record.tag].keys():
|
215
|
+
obj = Event(type=fact_event_table[record.tag]['Fact'])
|
216
|
+
else:
|
217
|
+
raise ValueError
|
218
|
+
else:
|
219
|
+
raise ValueError(f"{record.tag} not found in map")
|
220
|
+
|
221
|
+
class GedcomXRelationshipBuilder(GedcomXObject):
|
222
|
+
def __new__(cls,record: GedcomRecord | None = None, object_stack: dict | None = None) -> object:
|
223
|
+
last_relationship = object_stack.get('lastrelationship',None)
|
224
|
+
last_relationship_data = object_stack.get('lastrelationshipdata',None)
|
225
|
+
if not isinstance(last_relationship_data,dict):
|
226
|
+
last_relationship_data = None
|
227
|
+
|
228
|
+
|
gedcomx/Name.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from enum import Enum
|
2
2
|
from typing import List,Optional
|
3
|
+
from typing_extensions import Self
|
3
4
|
|
4
5
|
from .Attribution import Attribution
|
5
6
|
from .Conclusion import Conclusion, ConfidenceLevel
|
@@ -220,6 +221,11 @@ class Name(Conclusion):
|
|
220
221
|
|
221
222
|
return name_as_dict
|
222
223
|
|
224
|
+
class QuickName():
|
225
|
+
def __new__(cls,name: str) -> Name:
|
226
|
+
obj = Name(nameForms=[NameForm(fullText=name)])
|
227
|
+
return obj
|
228
|
+
|
223
229
|
def ensure_list(val):
|
224
230
|
if val is None:
|
225
231
|
return []
|
gedcomx/Person.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from enum import Enum
|
2
2
|
from typing import List, Optional
|
3
|
+
from urllib.parse import urljoin
|
3
4
|
|
4
5
|
from .Attribution import Attribution
|
5
6
|
from .Conclusion import ConfidenceLevel
|
@@ -8,13 +9,14 @@ from .EvidenceReference import EvidenceReference
|
|
8
9
|
from .Fact import Fact, FactType
|
9
10
|
from .Gender import Gender, GenderType
|
10
11
|
from .Identifier import IdentifierList
|
11
|
-
from .Name import Name
|
12
|
+
from .Name import Name, QuickName
|
12
13
|
from .Note import Note
|
13
14
|
from .SourceReference import SourceReference
|
15
|
+
from .Serialization import Serialization
|
14
16
|
from .Subject import Subject
|
15
17
|
from .Resource import Resource
|
16
18
|
from collections.abc import Sized
|
17
|
-
from .
|
19
|
+
from .Extensions.rs10.rsLink import _rsLinkList
|
18
20
|
|
19
21
|
class Person(Subject):
|
20
22
|
"""A person in the system.
|
@@ -47,9 +49,10 @@ class Person(Subject):
|
|
47
49
|
names: Optional[List[Name]] = None,
|
48
50
|
facts: Optional[List[Fact]] = None,
|
49
51
|
living: Optional[bool] = False,
|
50
|
-
links: Optional[
|
52
|
+
links: Optional[_rsLinkList] = None,
|
53
|
+
uri: Optional[Resource] = None) -> None:
|
51
54
|
# Call superclass initializer if needed
|
52
|
-
super().__init__(id, lang, sources, analysis, notes, confidence, attribution, extracted, evidence, media, identifiers,links=links)
|
55
|
+
super().__init__(id, lang, sources, analysis, notes, confidence, attribution, extracted, evidence, media, identifiers,links=links,uri=uri)
|
53
56
|
|
54
57
|
# Initialize mutable attributes to empty lists if None
|
55
58
|
self.sources = sources if sources is not None else []
|
@@ -108,103 +111,59 @@ class Person(Subject):
|
|
108
111
|
|
109
112
|
@property
|
110
113
|
def _as_dict_(self):
|
111
|
-
|
112
|
-
if isinstance(value, (str, int, float, bool, type(None))):
|
113
|
-
return value
|
114
|
-
elif isinstance(value, dict):
|
115
|
-
return {k: _serialize(v) for k, v in value.items()}
|
116
|
-
elif isinstance(value, (list, tuple, set)):
|
117
|
-
return [_serialize(v) for v in value]
|
118
|
-
elif hasattr(value, "_as_dict_"):
|
119
|
-
return value._as_dict_
|
120
|
-
else:
|
121
|
-
return str(value) # fallback for unknown objects
|
122
|
-
|
123
|
-
subject_fields = super()._as_dict_ # Start with base class fields
|
114
|
+
type_as_dict = super()._as_dict_ # Start with base class fields
|
124
115
|
# Only add Relationship-specific fields
|
125
|
-
|
116
|
+
type_as_dict.update({
|
126
117
|
'private': self.private,
|
127
118
|
'living': self.living,
|
128
119
|
'gender': self.gender._as_dict_ if self.gender else None,
|
129
120
|
'names': [name._as_dict_ for name in self.names],
|
130
|
-
'facts': [fact for fact in self.facts],
|
131
|
-
'uri':
|
121
|
+
'facts': [fact._as_dict_ for fact in self.facts],
|
122
|
+
'uri': self.uri._as_dict_ if self.uri else None
|
132
123
|
|
133
124
|
})
|
134
125
|
|
135
|
-
|
136
|
-
|
137
|
-
if value is not None:
|
138
|
-
subject_fields[key] = _serialize(value)
|
139
|
-
|
140
|
-
# 3) merge and filter out None *at the top level*
|
141
|
-
return {
|
142
|
-
k: v
|
143
|
-
for k, v in subject_fields.items()
|
144
|
-
if v is not None and not (isinstance(v, Sized) and len(v) == 0)
|
145
|
-
}
|
146
|
-
|
147
|
-
|
126
|
+
return Serialization.serialize_dict(type_as_dict)
|
127
|
+
|
148
128
|
@classmethod
|
149
129
|
def _from_json_(cls, data: dict):
|
150
130
|
"""
|
151
131
|
Create a Person instance from a JSON-dict (already parsed).
|
152
132
|
"""
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
inst = cls(
|
188
|
-
id = id_,
|
189
|
-
lang = lang,
|
190
|
-
sources = sources,
|
191
|
-
analysis = analysis,
|
192
|
-
notes = notes,
|
193
|
-
confidence = confidence,
|
194
|
-
attribution = attribution,
|
195
|
-
extracted = extracted,
|
196
|
-
evidence = evidence,
|
197
|
-
media = media,
|
198
|
-
identifiers = identifiers,
|
199
|
-
private = private,
|
200
|
-
gender = gender,
|
201
|
-
names = names,
|
202
|
-
facts = facts,
|
203
|
-
living = living,
|
204
|
-
links = links
|
205
|
-
)
|
206
|
-
|
207
|
-
return inst
|
133
|
+
return Serialization.deserialize(data, Person)
|
134
|
+
|
135
|
+
@classmethod
|
136
|
+
def from_familysearch(cls, pid: str, token: str, *, base_url: Optional[str] = None):
|
137
|
+
"""
|
138
|
+
Fetch a single person by PID from FamilySearch and return a Person.
|
139
|
+
- pid: e.g. "KPHP-4B4"
|
140
|
+
- token: OAuth2 access token (Bearer)
|
141
|
+
- base_url: override API base (defaults to settings.FS_API_BASE or prod)
|
142
|
+
"""
|
143
|
+
import requests
|
144
|
+
default_base = "https://apibeta.familysearch.org/platform/"
|
145
|
+
|
146
|
+
base = (base_url or default_base).rstrip("/") + "/"
|
147
|
+
url = urljoin(base, f"tree/persons/{pid}")
|
148
|
+
|
149
|
+
headers = {
|
150
|
+
"Accept": "application/json",
|
151
|
+
"Authorization": f"Bearer {token}",
|
152
|
+
}
|
153
|
+
|
154
|
+
resp = requests.get(url, headers=headers, timeout=(5, 30))
|
155
|
+
resp.raise_for_status()
|
156
|
+
|
157
|
+
payload = resp.json()
|
158
|
+
persons = payload.get("persons") or []
|
159
|
+
|
160
|
+
# Prefer exact match on PID, else first item if present
|
161
|
+
person_json = next((p for p in persons if (p.get("id") == pid)), None) or (persons[0] if persons else None)
|
162
|
+
if not person_json:
|
163
|
+
raise ValueError(f"FamilySearch returned no person for PID {pid}")
|
164
|
+
|
165
|
+
# Keep your existing deserialization helper
|
166
|
+
return Serialization.deserialize(person_json, Person)
|
208
167
|
|
209
168
|
class QuickPerson:
|
210
169
|
"""A GedcomX Person Data Type created with basic information.
|
@@ -221,7 +180,7 @@ class QuickPerson:
|
|
221
180
|
Raises:
|
222
181
|
ValueError: If `id` is not a valid UUID.
|
223
182
|
"""
|
224
|
-
def __new__(cls, name:
|
183
|
+
def __new__(cls, name: str, dob: Optional[str] = None, dod: Optional[str] = None):
|
225
184
|
# Build facts from args
|
226
185
|
facts = []
|
227
186
|
if dob:
|
@@ -230,4 +189,4 @@ class QuickPerson:
|
|
230
189
|
facts.append(Fact(type=FactType.Death, date=Date(original=dod)))
|
231
190
|
|
232
191
|
# Return the different class instance
|
233
|
-
return Person(facts=facts, names=[name] if name else None)
|
192
|
+
return Person(facts=facts, names=[QuickName(name=name)] if name else None)
|