gedcom-x 0.5.2__py3-none-any.whl → 0.5.5__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.2.dist-info → gedcom_x-0.5.5.dist-info}/METADATA +1 -1
- gedcom_x-0.5.5.dist-info/RECORD +43 -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/Fact.py +7 -8
- gedcomx/Gedcom.py +38 -404
- gedcomx/Gedcom5x.py +558 -0
- gedcomx/GedcomX.py +37 -22
- gedcomx/Gender.py +6 -40
- gedcomx/Identifier.py +151 -97
- 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 +7 -1
- 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.5.dist-info}/WHEEL +0 -0
- {gedcom_x-0.5.2.dist-info → gedcom_x-0.5.5.dist-info}/top_level.txt +0 -0
gedcomx/Resource.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from typing import List, Optional
|
2
2
|
|
3
|
+
|
3
4
|
from .URI import URI
|
4
5
|
|
5
6
|
class Resource:
|
@@ -16,7 +17,7 @@ class Resource:
|
|
16
17
|
"""
|
17
18
|
def __init__(self,uri: Optional[URI|str] = None, id:Optional[str] = None,top_lvl_object: Optional[object] = None,target= None) -> None:
|
18
19
|
|
19
|
-
self.resource = URI.from_url(uri.value)
|
20
|
+
self.resource = URI.from_url(uri.value) if isinstance(uri,URI) else URI.from_url(uri) if isinstance(uri,str) else None
|
20
21
|
self.Id = id
|
21
22
|
|
22
23
|
self.type = None
|
@@ -31,9 +32,23 @@ class Resource:
|
|
31
32
|
|
32
33
|
@property
|
33
34
|
def _as_dict_(self):
|
34
|
-
|
35
|
+
from .Serialization import Serialization
|
36
|
+
typ_as_dict = {'resource':self.resource.value if self.resource else None,
|
35
37
|
'resourceId':self.Id}
|
38
|
+
return Serialization.serialize_dict(typ_as_dict)
|
39
|
+
|
40
|
+
@classmethod
|
41
|
+
def _from_json_(cls,data):
|
42
|
+
# TODO This is not used but taken care of in Serialization
|
43
|
+
r = Resource(uri=data.get('resource'),id=data.get('resourceId',None))
|
44
|
+
#return r
|
36
45
|
|
46
|
+
def __repr__(self) -> str:
|
47
|
+
return f"Resource(uri={self.resource}, id={self.Id}, target={self.target})"
|
48
|
+
|
49
|
+
def __str__(self) -> str:
|
50
|
+
return f"{self.resource}{f', id={self.Id}' if self.Id else ''}"
|
51
|
+
|
37
52
|
def get_resource_as_dict(value):
|
38
53
|
"""
|
39
54
|
If value is truthy:
|
@@ -58,4 +73,3 @@ def get_resource_as_dict(value):
|
|
58
73
|
print((f"value: {value} as inproper attributes"))
|
59
74
|
exit()
|
60
75
|
|
61
|
-
|
gedcomx/Serialization.py
CHANGED
@@ -1,42 +1,25 @@
|
|
1
1
|
from typing import Dict
|
2
2
|
|
3
|
+
from .Logging import get_logger
|
4
|
+
|
5
|
+
log = get_logger(__name__)
|
6
|
+
log.setLevel("DEBUG")
|
7
|
+
log.info("Logger initialized.")
|
8
|
+
|
3
9
|
from collections.abc import Sized
|
10
|
+
from typing import Any, get_origin, get_args, List, Set, Tuple, Dict, Union, ForwardRef, Annotated
|
11
|
+
import types
|
12
|
+
|
13
|
+
import enum
|
14
|
+
from .Resource import Resource
|
15
|
+
from .Identifier import IdentifierList
|
16
|
+
from .URI import URI
|
4
17
|
|
18
|
+
_PRIMITIVES = (str, int, float, bool, type(None))
|
5
19
|
|
6
20
|
def _has_parent_class(obj) -> bool:
|
7
21
|
return hasattr(obj, '__class__') and hasattr(obj.__class__, '__bases__') and len(obj.__class__.__bases__) > 0
|
8
22
|
|
9
|
-
def serialize_to_dict(obj,class_values:Dict,ignore_null=True):
|
10
|
-
def _serialize(value):
|
11
|
-
if isinstance(value, (str, int, float, bool, type(None))):
|
12
|
-
return value
|
13
|
-
elif isinstance(value, dict):
|
14
|
-
return {k: _serialize(v) for k, v in value.items()}
|
15
|
-
elif isinstance(value, (list, tuple, set)):
|
16
|
-
return [_serialize(v) for v in value]
|
17
|
-
elif hasattr(value, "_as_dict_"):
|
18
|
-
return value._as_dict_
|
19
|
-
else:
|
20
|
-
return str(value) # fallback for unknown objects
|
21
|
-
|
22
|
-
values_dict = {}
|
23
|
-
if _has_parent_class(obj):
|
24
|
-
values_dict.update(super(obj.__class__, obj)._as_dict_)
|
25
|
-
if class_values:
|
26
|
-
values_dict.update(class_values)
|
27
|
-
# Serialize and exclude None values
|
28
|
-
|
29
|
-
empty_fields = []
|
30
|
-
for key, value in values_dict.items():
|
31
|
-
if value is not None:
|
32
|
-
values_dict[key] = _serialize(value)
|
33
|
-
else:
|
34
|
-
empty_fields.append(key)
|
35
|
-
|
36
|
-
for key in empty_fields:
|
37
|
-
del values_dict[key]
|
38
|
-
|
39
|
-
return values_dict
|
40
23
|
|
41
24
|
class Serialization:
|
42
25
|
|
@@ -78,3 +61,341 @@ class Serialization:
|
|
78
61
|
if v is not None and not (isinstance(v, Sized) and len(v) == 0)
|
79
62
|
}
|
80
63
|
return {}
|
64
|
+
|
65
|
+
@staticmethod
|
66
|
+
def _coerce_value(value: Any, typ: Any) -> Any:
|
67
|
+
"""Coerce `value` to `typ`:
|
68
|
+
- primitives: pass through
|
69
|
+
- containers: recurse into elements
|
70
|
+
- objects: call typ._from_json_(dict) if available and value is dict
|
71
|
+
- already-instantiated objects of typ: pass through
|
72
|
+
- otherwise: return value unchanged
|
73
|
+
"""
|
74
|
+
def is_enum_type(T) -> bool:
|
75
|
+
"""Return True if T (possibly a typing construct) is or contains an Enum type."""
|
76
|
+
origin = get_origin(T)
|
77
|
+
|
78
|
+
# Unwrap Union/Optional/PEP 604 (A | B)
|
79
|
+
if origin in (Union, types.UnionType):
|
80
|
+
return any(is_enum_type(a) for a in get_args(T))
|
81
|
+
|
82
|
+
# Unwrap Annotated[T, ...]
|
83
|
+
if origin is Annotated:
|
84
|
+
return is_enum_type(get_args(T)[0])
|
85
|
+
|
86
|
+
# Resolve forward refs / strings if you use them
|
87
|
+
if isinstance(T, ForwardRef):
|
88
|
+
T = globals().get(T.__forward_arg__, T)
|
89
|
+
if isinstance(T, str):
|
90
|
+
T = globals().get(T, T)
|
91
|
+
|
92
|
+
# Finally check enum-ness
|
93
|
+
try:
|
94
|
+
return issubclass(T, enum.Enum)
|
95
|
+
except TypeError:
|
96
|
+
return False # not a class (e.g., typing.List[int], etc.)
|
97
|
+
log.debug(f"Coercing value '{value}' of type '{type(value).__name__}' to '{typ}'")
|
98
|
+
|
99
|
+
def _resolve(t):
|
100
|
+
# resolve ForwardRef('Resource') -> actual object if already in globals()
|
101
|
+
if isinstance(t, ForwardRef):
|
102
|
+
return globals().get(t.__forward_arg__, t)
|
103
|
+
return t
|
104
|
+
|
105
|
+
if is_enum_type(typ):
|
106
|
+
log.debug(f"Enum type detected: {typ}")
|
107
|
+
return typ(value) # cast to enum
|
108
|
+
|
109
|
+
origin = get_origin(typ)
|
110
|
+
if origin in (Union, types.UnionType):
|
111
|
+
args = tuple(_resolve(a) for a in get_args(typ))
|
112
|
+
else:
|
113
|
+
args = (_resolve(typ),)
|
114
|
+
log.debug(f"Origin: {origin}, args: {args}")
|
115
|
+
|
116
|
+
if Resource in args and isinstance(value, dict):
|
117
|
+
if Resource in args:
|
118
|
+
log.info(f"Deserializing Resource from value: {value}")
|
119
|
+
return Resource(uri=value.get('resource'), id=value.get('resourceId', None))
|
120
|
+
|
121
|
+
if isinstance(value, _PRIMITIVES):
|
122
|
+
if Resource in args:
|
123
|
+
log.info(f"Deserializing Resource from value: {value}")
|
124
|
+
return Resource(uri=value)
|
125
|
+
if URI in args:
|
126
|
+
log.info(f"Deserializing URI from value: {value}")
|
127
|
+
return URI.from_url(value)
|
128
|
+
return value
|
129
|
+
|
130
|
+
if IdentifierList in args:
|
131
|
+
log.error(f"Deserializing IdentifierList from value: {value}")
|
132
|
+
return IdentifierList._from_json_(value)
|
133
|
+
|
134
|
+
if origin in (list, List):
|
135
|
+
elem_args = get_args(typ) # NOT get_args(args)
|
136
|
+
elem_t = elem_args[0] if elem_args else Any
|
137
|
+
log.debug(f"List: {typ}, elem={elem_t}")
|
138
|
+
return [Serialization._coerce_value(v, elem_t) for v in (value or [])]
|
139
|
+
|
140
|
+
if origin in (set, Set):
|
141
|
+
(elem_t,) = args or (Any,)
|
142
|
+
return { Serialization._coerce_value(v, elem_t) for v in (value or []) }
|
143
|
+
|
144
|
+
if origin in (tuple, Tuple):
|
145
|
+
if not args:
|
146
|
+
return tuple(value)
|
147
|
+
if len(args) == 2 and args[1] is Ellipsis: # Tuple[T, ...]
|
148
|
+
elem_t = args[0]
|
149
|
+
return tuple(Serialization._coerce_value(v, elem_t) for v in (value or []))
|
150
|
+
return tuple(Serialization._coerce_value(v, t) for v, t in zip(value, args))
|
151
|
+
|
152
|
+
if origin in (dict, Dict):
|
153
|
+
k_t, v_t = args or (Any, Any)
|
154
|
+
return {
|
155
|
+
Serialization._coerce_value(k, k_t): Serialization._coerce_value(v, v_t)
|
156
|
+
for k, v in (value or {}).items()
|
157
|
+
}
|
158
|
+
|
159
|
+
# If `typ` has _from_json_ and value is a dict, use it
|
160
|
+
if hasattr(typ, "_from_json_") and isinstance(value, dict):
|
161
|
+
log.info(f"Deserializing {typ} from json method with value: {value}")
|
162
|
+
return typ._from_json_(value)
|
163
|
+
|
164
|
+
# If already the right type, keep it
|
165
|
+
try:
|
166
|
+
if isinstance(value, typ):
|
167
|
+
return value
|
168
|
+
except TypeError:
|
169
|
+
log.debug(f"Could not coerce value '{value}' to type '{typ}'")
|
170
|
+
pass # `typ` may be a typing construct not valid for isinstance
|
171
|
+
|
172
|
+
# Fallback: leave as-is
|
173
|
+
log.debug(f"Returning '{type(value)}' type")
|
174
|
+
return value
|
175
|
+
|
176
|
+
@staticmethod
|
177
|
+
def get_class_fields(cls_name) -> Dict:
|
178
|
+
from typing import List, Optional
|
179
|
+
from gedcomx.Attribution import Attribution
|
180
|
+
from gedcomx.Document import Document , DocumentType, TextType
|
181
|
+
from gedcomx.Note import Note
|
182
|
+
from gedcomx.Resource import Resource
|
183
|
+
from gedcomx.SourceReference import SourceReference
|
184
|
+
from gedcomx.extensions.rs10.rsLink import _rsLinkList
|
185
|
+
from gedcomx.Conclusion import ConfidenceLevel
|
186
|
+
from gedcomx.EvidenceReference import EvidenceReference
|
187
|
+
from gedcomx.Identifier import IdentifierList
|
188
|
+
from gedcomx.Gender import Gender, GenderType
|
189
|
+
from gedcomx.Fact import Fact
|
190
|
+
from gedcomx.Name import Name
|
191
|
+
from gedcomx.URI import URI
|
192
|
+
from gedcomx.Qualifier import Qualifier
|
193
|
+
from gedcomx.PlaceDescription import PlaceDescription
|
194
|
+
from gedcomx.PlaceReference import PlaceReference
|
195
|
+
from gedcomx.Person import Person
|
196
|
+
from gedcomx.Relationship import Relationship, RelationshipType
|
197
|
+
from gedcomx.Identifier import Identifier
|
198
|
+
from gedcomx.Date import Date
|
199
|
+
from gedcomx.TextValue import TextValue
|
200
|
+
from gedcomx.Address import Address
|
201
|
+
from gedcomx.OnlineAccount import OnlineAccount
|
202
|
+
from gedcomx.Event import Event, EventType, EventRole
|
203
|
+
from .SourceDescription import SourceDescription
|
204
|
+
|
205
|
+
fields = { 'Conclusion' : {
|
206
|
+
"id": str,
|
207
|
+
"lang": str,
|
208
|
+
"sources": List["SourceReference"],
|
209
|
+
"analysis": Document | Resource,
|
210
|
+
"notes": List[Note],
|
211
|
+
"confidence": ConfidenceLevel,
|
212
|
+
"attribution": Attribution,
|
213
|
+
"uri": "Resource",
|
214
|
+
"max_note_count": int,
|
215
|
+
"links": _rsLinkList
|
216
|
+
},
|
217
|
+
'Subject' : {
|
218
|
+
"id": str,
|
219
|
+
"lang": str,
|
220
|
+
"sources": List["SourceReference"],
|
221
|
+
"analysis": Resource,
|
222
|
+
"notes": List["Note"],
|
223
|
+
"confidence": ConfidenceLevel,
|
224
|
+
"attribution": Attribution,
|
225
|
+
"extracted": bool,
|
226
|
+
"evidence": List[EvidenceReference],
|
227
|
+
"media": List[SourceReference],
|
228
|
+
"identifiers": IdentifierList,
|
229
|
+
"uri": Resource,
|
230
|
+
"links": _rsLinkList
|
231
|
+
},
|
232
|
+
'Person' : {
|
233
|
+
"id": str,
|
234
|
+
"lang": str,
|
235
|
+
"sources": List[SourceReference],
|
236
|
+
"analysis": Resource,
|
237
|
+
"notes": List[Note],
|
238
|
+
"confidence": ConfidenceLevel,
|
239
|
+
"attribution": Attribution,
|
240
|
+
"extracted": bool,
|
241
|
+
"evidence": List[EvidenceReference],
|
242
|
+
"media": List[SourceReference],
|
243
|
+
"identifiers": IdentifierList,
|
244
|
+
"private": bool,
|
245
|
+
"gender": Gender,
|
246
|
+
"names": List[Name],
|
247
|
+
"facts": List[Fact],
|
248
|
+
"living": bool,
|
249
|
+
"links": _rsLinkList,
|
250
|
+
'uri': Resource
|
251
|
+
},
|
252
|
+
'SourceReference' : {
|
253
|
+
"description": SourceDescription | URI | Resource,
|
254
|
+
"descriptionId": str,
|
255
|
+
"attribution": Attribution,
|
256
|
+
"qualifiers": List[Qualifier],
|
257
|
+
},
|
258
|
+
'Attribution' : {
|
259
|
+
"contributor": Resource | Attribution,
|
260
|
+
"modified": str,
|
261
|
+
"changeMessage": str,
|
262
|
+
"creator": Resource | Attribution,
|
263
|
+
"created": str
|
264
|
+
},
|
265
|
+
'Gender' : {
|
266
|
+
"id": str,
|
267
|
+
"lang": str,
|
268
|
+
"sources": List[SourceReference],
|
269
|
+
"analysis": Resource,
|
270
|
+
"notes": List[Note],
|
271
|
+
"confidence": ConfidenceLevel,
|
272
|
+
"attribution": Attribution,
|
273
|
+
"type": GenderType,
|
274
|
+
},
|
275
|
+
'PlaceReference' : {
|
276
|
+
"original": str,
|
277
|
+
"description": PlaceDescription | URI,
|
278
|
+
},
|
279
|
+
'Relationship' : {
|
280
|
+
"id": str,
|
281
|
+
"lang": str,
|
282
|
+
"sources": List[SourceReference],
|
283
|
+
"analysis": Document | Resource,
|
284
|
+
"notes": List[Note],
|
285
|
+
"confidence": ConfidenceLevel,
|
286
|
+
"attribution": Attribution,
|
287
|
+
"extracted": bool,
|
288
|
+
"evidence": List[EvidenceReference],
|
289
|
+
"media": List[SourceReference],
|
290
|
+
"identifiers": IdentifierList,
|
291
|
+
"type": RelationshipType,
|
292
|
+
"person1": Person | Resource,
|
293
|
+
"person2": Person | Resource,
|
294
|
+
"facts": List[Fact],
|
295
|
+
},
|
296
|
+
'Document' : {
|
297
|
+
"id": str,
|
298
|
+
"lang": str,
|
299
|
+
"sources": List[SourceReference],
|
300
|
+
"analysis": Resource,
|
301
|
+
"notes": List[Note],
|
302
|
+
"confidence": ConfidenceLevel,
|
303
|
+
"attribution": Attribution,
|
304
|
+
"type": DocumentType,
|
305
|
+
"extracted": bool,
|
306
|
+
"textType": TextType,
|
307
|
+
"text": str,
|
308
|
+
},
|
309
|
+
'PlaceDescription' : {
|
310
|
+
"id": str,
|
311
|
+
"lang": str,
|
312
|
+
"sources": List[SourceReference],
|
313
|
+
"analysis": Resource,
|
314
|
+
"notes": List[Note],
|
315
|
+
"confidence": ConfidenceLevel,
|
316
|
+
"attribution": Attribution,
|
317
|
+
"extracted": bool,
|
318
|
+
"evidence": List[EvidenceReference],
|
319
|
+
"media": List[SourceReference],
|
320
|
+
"identifiers": List[IdentifierList],
|
321
|
+
"names": List[TextValue],
|
322
|
+
"type": str,
|
323
|
+
"place": URI,
|
324
|
+
"jurisdiction": Resource | PlaceDescription,
|
325
|
+
"latitude": float,
|
326
|
+
"longitude": float,
|
327
|
+
"temporalDescription": Date,
|
328
|
+
"spatialDescription": Resource,
|
329
|
+
},
|
330
|
+
"Agent" : {
|
331
|
+
"id": str,
|
332
|
+
"identifiers": IdentifierList,
|
333
|
+
"names": List[TextValue],
|
334
|
+
"homepage": URI,
|
335
|
+
"openid": URI,
|
336
|
+
"accounts": List[OnlineAccount],
|
337
|
+
"emails": List[URI],
|
338
|
+
"phones": List[URI],
|
339
|
+
"addresses": List[Address],
|
340
|
+
"person": object | Resource, # intended to be Person | Resource
|
341
|
+
# "xnotes": List[Note], # commented out in your __init__
|
342
|
+
"attribution": object, # for GEDCOM5/7 compatibility
|
343
|
+
"uri": URI | Resource,
|
344
|
+
},
|
345
|
+
'Event' : {
|
346
|
+
"id": str,
|
347
|
+
"lang": str,
|
348
|
+
"sources": List[SourceReference],
|
349
|
+
"analysis": Resource,
|
350
|
+
"notes": List[Note],
|
351
|
+
"confidence": ConfidenceLevel,
|
352
|
+
"attribution": Attribution,
|
353
|
+
"extracted": bool,
|
354
|
+
"evidence": List[EvidenceReference],
|
355
|
+
"media": List[SourceReference],
|
356
|
+
"identifiers": List[Identifier],
|
357
|
+
"type": EventType,
|
358
|
+
"date": Date,
|
359
|
+
"place": PlaceReference,
|
360
|
+
"roles": List[EventRole],
|
361
|
+
}
|
362
|
+
|
363
|
+
}
|
364
|
+
|
365
|
+
|
366
|
+
return fields[cls_name] if cls_name in fields else {}
|
367
|
+
|
368
|
+
@staticmethod
|
369
|
+
def deserialize(data: dict[str, Any], class_type) -> Any:
|
370
|
+
"""
|
371
|
+
Deserialize `data` according to `fields` (field -> type).
|
372
|
+
- Primitives are assigned directly.
|
373
|
+
- Objects use `type._from_json_(dict)` when present.
|
374
|
+
- Lists/Sets/Tuples/Dicts are recursively processed.
|
375
|
+
Returns (result, unknown_keys).
|
376
|
+
"""
|
377
|
+
log.debug(f"Deserializing '{data}' into '{class_type.__name__}'")
|
378
|
+
class_fields = Serialization.get_class_fields(str(class_type.__name__))
|
379
|
+
if class_fields == {}:
|
380
|
+
log.warning(f"No class fields found for '{class_type.__name__}'")
|
381
|
+
log.debug(f"class fields: {class_fields}")
|
382
|
+
result: dict[str, Any] = {}
|
383
|
+
known = set(class_fields.keys())
|
384
|
+
log.debug(f"keys found in JSON: {data.keys()}")
|
385
|
+
#log.debug(f"known fields: {known}")
|
386
|
+
for name, typ in class_fields.items():
|
387
|
+
if name in data:
|
388
|
+
log.debug(f"Field '{name}' of {class_type.__name__} found in data")
|
389
|
+
result[name] = Serialization._coerce_value(data[name], typ)
|
390
|
+
#if type(result[name]) != class_fields[name]:# TODO Write better type checking
|
391
|
+
# log.error(f"Field '{name}' of {class_type.__name__} was expected to be of type '{class_fields[name]}', but got '{type(result[name])}' with value '{result[name]}'")
|
392
|
+
# raise TypeError(f"Field '{name}' expected type '{class_fields[name]}', got '{type(result[name])}'")
|
393
|
+
log.debug(f"Field '{name}' of '{class_type.__name__}' resulted in a '{type(result[name]).__name__}' with value '{result[name]}'")
|
394
|
+
else:
|
395
|
+
log.debug(f"Field '{name}' of '{class_type.__name__}' not found in JSON data")
|
396
|
+
|
397
|
+
unknown_keys = [k for k in data.keys() if k not in known]
|
398
|
+
log.info(f"Creating instance of {class_type.__name__} with fields: {result.keys()}")
|
399
|
+
new_cls = class_type(**result)
|
400
|
+
log.debug(f"Deserialized {class_type.__name__} with unknown keys: {unknown_keys}")
|
401
|
+
return new_cls # type: ignore, unknown_keys
|
gedcomx/SourceDescription.py
CHANGED
@@ -78,7 +78,7 @@ class SourceDescription:
|
|
78
78
|
notes: Optional[List[Note]] = None,
|
79
79
|
attribution: Optional[Attribution] = None,
|
80
80
|
rights: Optional[List[Resource]] = [],
|
81
|
-
coverage: Optional[Coverage] = None, # Coverage
|
81
|
+
coverage: Optional[List[Coverage]] = None, # Coverage
|
82
82
|
descriptions: Optional[List[TextValue]] = None,
|
83
83
|
identifiers: Optional[IdentifierList] = None,
|
84
84
|
created: Optional[Date] = None,
|
@@ -180,18 +180,15 @@ class SourceDescription:
|
|
180
180
|
|
181
181
|
@property
|
182
182
|
def _as_dict_(self) -> Dict[str, Any]:
|
183
|
-
|
184
|
-
|
185
|
-
|
186
183
|
type_as_dict = {
|
187
184
|
'id': self.id,
|
188
185
|
'about': self.about._as_dict_ if self.about else None,
|
189
186
|
'resourceType': self.resourceType.value if self.resourceType else None,
|
190
187
|
'citations': [c._as_dict_ for c in self.citations] or None,
|
191
188
|
'mediaType': self.mediaType,
|
192
|
-
'mediator':
|
193
|
-
'publisher':
|
194
|
-
'authors': [
|
189
|
+
'mediator': self.mediator._as_dict_ if self.mediator else None,
|
190
|
+
'publisher': self.publisher._as_dict_ if self.publisher else None,
|
191
|
+
'authors': self.authors and [a._as_dict_ for a in self.authors] or None,
|
195
192
|
'sources': [s._as_dict_ for s in self.sources] or None,
|
196
193
|
'analysis': self.analysis._as_dict_ if self.analysis else None,
|
197
194
|
'componentOf': self.componentOf._as_dict_ if self.componentOf else None,
|
@@ -199,13 +196,13 @@ class SourceDescription:
|
|
199
196
|
'notes': [n._as_dict_ for n in self.notes] or None,
|
200
197
|
'attribution': self.attribution._as_dict_ if self.attribution else None,
|
201
198
|
'rights': [r._as_dict_ for r in self.rights] or None,
|
202
|
-
'coverage':
|
199
|
+
'coverage': [c._as_dict_ for c in self.coverage] or None,
|
203
200
|
'descriptions': [d._as_dict_ for d in self.descriptions] or None,
|
204
201
|
'identifiers': self.identifiers._as_dict_ if self.identifiers else None,
|
205
202
|
'created': self.created if self.created else None,
|
206
203
|
'modified': self.modified if self.modified else None,
|
207
204
|
'published': self.published if self.published else None,
|
208
|
-
'repository':
|
205
|
+
'repository': self.repository._as_dict_ if self.repository else None,
|
209
206
|
'uri': self.uri.value
|
210
207
|
}
|
211
208
|
|
gedcomx/SourceReference.py
CHANGED
@@ -4,6 +4,7 @@ from .Attribution import Attribution
|
|
4
4
|
from .Qualifier import Qualifier
|
5
5
|
|
6
6
|
from .Resource import Resource
|
7
|
+
from .Serialization import Serialization
|
7
8
|
from .URI import URI
|
8
9
|
|
9
10
|
from collections.abc import Sized
|
@@ -53,34 +54,21 @@ class SourceReference:
|
|
53
54
|
qualifiers: Optional[List[Qualifier]] = None
|
54
55
|
) -> None:
|
55
56
|
|
56
|
-
#if not isinstance(description,URI): raise ValueError(f"description is of type {type(description)}")
|
57
|
-
'''from .SourceDescription import SourceDescription
|
58
|
-
self._description_object = None
|
59
|
-
if isinstance(description,URI):
|
60
|
-
#TODO See if Local, If not try to resolve,
|
61
|
-
self._description_object = description
|
62
|
-
|
63
|
-
elif isinstance(description,SourceDescription):
|
64
|
-
self._description_object = description
|
65
|
-
if hasattr(description,'_uri'):
|
66
|
-
self.description = description._uri
|
67
|
-
else:
|
68
|
-
assert False
|
69
|
-
self.description = Resource(object=description)
|
70
|
-
description._uri = self.description
|
71
|
-
description._object = description
|
72
|
-
else:
|
73
|
-
raise ValueError(f"'description' must be of type 'SourceDescription' or 'URI', type: {type(description)} was provided")'''
|
74
|
-
|
75
|
-
|
76
57
|
self.description = description
|
77
58
|
self.descriptionId = descriptionId
|
78
59
|
self.attribution = attribution
|
79
|
-
self.qualifiers = qualifiers
|
60
|
+
self.qualifiers = qualifiers if qualifiers and isinstance(qualifiers, list) else []
|
80
61
|
|
81
62
|
def add_qualifier(self, qualifier: Qualifier):
|
82
|
-
if isinstance(qualifier, Qualifier):
|
63
|
+
if isinstance(qualifier, (Qualifier,KnownSourceReference)):
|
64
|
+
if self.qualifiers:
|
65
|
+
#TODO Prevent Duplicates
|
66
|
+
for current_qualifier in self.qualifiers:
|
67
|
+
if qualifier == current_qualifier:
|
68
|
+
return
|
83
69
|
self.qualifiers.append(qualifier)
|
70
|
+
return
|
71
|
+
raise ValueError("The 'qualifier' must be type 'Qualifier' or 'KnownSourceReference', not " + str(type(qualifier)))
|
84
72
|
|
85
73
|
def append(self, text_to_add: str):
|
86
74
|
if text_to_add and isinstance(text_to_add, str):
|
@@ -93,82 +81,28 @@ class SourceReference:
|
|
93
81
|
|
94
82
|
@property
|
95
83
|
def _as_dict_(self):
|
96
|
-
|
97
|
-
def _serialize(value):
|
98
|
-
if isinstance(value, (str, int, float, bool, type(None))):
|
99
|
-
return value
|
100
|
-
elif isinstance(value, dict):
|
101
|
-
return {k: _serialize(v) for k, v in value.items()}
|
102
|
-
elif isinstance(value, (list, tuple, set)):
|
103
|
-
return [_serialize(v) for v in value]
|
104
|
-
elif hasattr(value, "_as_dict_"):
|
105
|
-
return value._as_dict_
|
106
|
-
else:
|
107
|
-
return str(value) # fallback for unknown objects
|
108
|
-
|
109
84
|
# Only add Relationship-specific fields
|
110
|
-
|
111
|
-
'description':self.description.
|
85
|
+
type_as_dict = {
|
86
|
+
'description':self.description._as_dict_ if self.description else None,
|
112
87
|
'descriptionId': self.descriptionId.replace("\n"," ").replace("\r"," ") if self.descriptionId else None,
|
113
88
|
'attribution': self.attribution._as_dict_ if self.attribution else None,
|
114
89
|
'qualifiers':[qualifier.value for qualifier in self.qualifiers ] if self.qualifiers else None
|
115
90
|
}
|
91
|
+
return Serialization.serialize_dict(type_as_dict)
|
116
92
|
|
117
|
-
|
118
|
-
for key, value in sourcereference_fields.items():
|
119
|
-
if value is not None:
|
120
|
-
sourcereference_fields[key] = _serialize(value)
|
121
|
-
|
122
|
-
return {
|
123
|
-
k: v
|
124
|
-
for k, v in sourcereference_fields.items()
|
125
|
-
if v is not None and not (isinstance(v, Sized) and len(v) == 0)
|
126
|
-
}
|
93
|
+
|
127
94
|
|
128
95
|
|
129
96
|
@classmethod
|
130
|
-
def _from_json_(cls, data: dict):
|
131
|
-
|
97
|
+
def _from_json_(cls, data: dict):
|
132
98
|
"""
|
133
|
-
Rehydrate a SourceReference from the dict
|
99
|
+
Rehydrate a SourceReference from the dict passed down from JSON deserialization.
|
100
|
+
NOTE: This does not resolve references to SourceDescription objects.
|
134
101
|
"""
|
135
|
-
from .
|
136
|
-
|
137
|
-
|
138
|
-
desc_json = data.get('description')
|
139
|
-
#if not desc_json:
|
140
|
-
# raise ValueError("SourceReference JSON missing 'description'")
|
141
|
-
#desc_obj = SourceDescription._from_json_(desc_json)
|
142
|
-
desc_obj = URI.from_url_(data.get('description')) #TODO <--- URI Reference
|
143
|
-
|
144
|
-
|
145
|
-
# 2) Simple fields
|
146
|
-
description_id = data.get('descriptionId')
|
147
|
-
|
148
|
-
# 3) Attribution (if present)
|
149
|
-
attrib = None
|
150
|
-
if data.get('attribution') is not None:
|
151
|
-
attrib = Attribution._from_json_(data['attribution'])
|
102
|
+
from .Serialization import Serialization
|
103
|
+
return Serialization.deserialize(data, SourceReference)
|
104
|
+
|
152
105
|
|
153
|
-
# 4) Qualifiers list
|
154
|
-
raw_quals = data.get('qualifiers', [])
|
155
|
-
qualifiers: List[Qualifier] = []
|
156
|
-
for q in raw_quals:
|
157
|
-
try:
|
158
|
-
# Try the known‐source enum first
|
159
|
-
qualifiers.append(KnownSourceReference(q))
|
160
|
-
except ValueError:
|
161
|
-
# Fallback to generic Qualifier
|
162
|
-
qualifiers.append(Qualifier(q))
|
163
|
-
|
164
|
-
# 5) Instantiate via your existing __init__
|
165
|
-
inst = cls(
|
166
|
-
description=desc_obj,
|
167
|
-
descriptionId=description_id,
|
168
|
-
attribution=attrib,
|
169
|
-
qualifiers=qualifiers
|
170
|
-
)
|
171
|
-
return inst
|
172
106
|
|
173
107
|
def __eq__(self, other):
|
174
108
|
if not isinstance(other, self.__class__):
|
gedcomx/Subject.py
CHANGED
@@ -9,7 +9,7 @@ from .Note import Note
|
|
9
9
|
from .Serialization import Serialization
|
10
10
|
from .SourceReference import SourceReference
|
11
11
|
from .Resource import Resource
|
12
|
-
from .
|
12
|
+
from .Extensions.rs10.rsLink import _rsLinkList
|
13
13
|
|
14
14
|
class Subject(Conclusion):
|
15
15
|
identifier = 'http://gedcomx.org/v1/Subject'
|
@@ -28,14 +28,14 @@ class Subject(Conclusion):
|
|
28
28
|
media: Optional[List[SourceReference]] = [],
|
29
29
|
identifiers: Optional[IdentifierList] = None,
|
30
30
|
uri: Optional[Resource] = None,
|
31
|
-
links: Optional[
|
31
|
+
links: Optional[_rsLinkList] = None) -> None:
|
32
32
|
super().__init__(id, lang, sources, analysis, notes, confidence, attribution,links=links)
|
33
33
|
self.extracted = extracted
|
34
34
|
self.evidence = evidence
|
35
35
|
self.media = media
|
36
|
-
self.identifiers = identifiers
|
36
|
+
self.identifiers = identifiers if identifiers else IdentifierList()
|
37
|
+
self.uri = uri
|
37
38
|
|
38
|
-
|
39
39
|
def add_identifier(self, identifier_to_add: Identifier):
|
40
40
|
if identifier_to_add and isinstance(identifier_to_add,Identifier):
|
41
41
|
for current_identifier in self.identifiers:
|