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/GedcomX.py
CHANGED
@@ -20,10 +20,10 @@ from .Exceptions import TagConversionError
|
|
20
20
|
from .Event import Event,EventType,EventRole,EventRoleType
|
21
21
|
from .Fact import Fact, FactType, FactQualifier
|
22
22
|
from .Gedcom import Gedcom
|
23
|
-
from .
|
23
|
+
from .Gedcom5x import Gedcom5x, GedcomRecord
|
24
24
|
from .Gender import Gender, GenderType
|
25
25
|
from .Group import Group
|
26
|
-
from .Identifier import Identifier, IdentifierType
|
26
|
+
from .Identifier import Identifier, IdentifierType, make_uid, IdentifierList
|
27
27
|
from .Logging import get_logger
|
28
28
|
from .Name import Name, NameType, NameForm, NamePart, NamePartType, NamePartQualifier
|
29
29
|
from .Note import Note
|
@@ -129,7 +129,10 @@ def TypeCollection(item_type):
|
|
129
129
|
def append(self, item):
|
130
130
|
if not isinstance(item, item_type):
|
131
131
|
raise TypeError(f"Expected item of type {item_type.__name__}, got {type(item).__name__}")
|
132
|
-
|
132
|
+
if item.uri:
|
133
|
+
item.uri.path = f'{str(item_type.__name__)}s' if (item.uri.path is None or item.uri.path == "") else item.uri.path
|
134
|
+
else:
|
135
|
+
item.uri = URI(path=f'/{item_type.__name__}s/')
|
133
136
|
|
134
137
|
|
135
138
|
#if isinstance(item,Agent):
|
@@ -208,10 +211,13 @@ class GedcomX:
|
|
208
211
|
"""
|
209
212
|
version = 'http://gedcomx.org/conceptual-model/v1'
|
210
213
|
|
211
|
-
def __init__(self, id: str = None,
|
214
|
+
def __init__(self, id: Optional[str] = None,
|
215
|
+
attribution: Optional[Attribution] = None,
|
216
|
+
filepath: Optional[str] = None,
|
217
|
+
description: Optional[str] = None) -> None:
|
218
|
+
|
212
219
|
self.id = id
|
213
220
|
self.attribution = attribution
|
214
|
-
|
215
221
|
self._filepath = None
|
216
222
|
|
217
223
|
self.description = description
|
@@ -224,9 +230,11 @@ class GedcomX:
|
|
224
230
|
self.places = TypeCollection(PlaceDescription)
|
225
231
|
self.groups = TypeCollection(Group)
|
226
232
|
|
227
|
-
self.
|
233
|
+
self.relationship_table = {}
|
228
234
|
|
229
|
-
|
235
|
+
self.default_id_generator = make_uid
|
236
|
+
|
237
|
+
def add(self,gedcomx_type_object):
|
230
238
|
if gedcomx_type_object:
|
231
239
|
if isinstance(gedcomx_type_object,Person):
|
232
240
|
self.add_person(gedcomx_type_object)
|
@@ -285,24 +293,24 @@ class GedcomX:
|
|
285
293
|
relationship.person1.id = self.personURIgenerator()
|
286
294
|
if not self.persons.byId(relationship.person1.id):
|
287
295
|
self.persons.append(relationship.person1)
|
288
|
-
if relationship.person1.id not in self.
|
289
|
-
self.
|
290
|
-
self.
|
296
|
+
if relationship.person1.id not in self.relationship_table:
|
297
|
+
self.relationship_table[relationship.person1.id] = []
|
298
|
+
self.relationship_table[relationship.person1.id].append(relationship)
|
291
299
|
relationship.person1._add_relationship(relationship)
|
292
300
|
else:
|
293
|
-
|
301
|
+
pass
|
294
302
|
|
295
303
|
if relationship.person2:
|
296
304
|
if relationship.person2.id is None:
|
297
305
|
relationship.person2.id = self.personURIgenerator() #TODO
|
298
306
|
if not self.persons.byId(relationship.person2.id):
|
299
307
|
self.persons.append(relationship.person2)
|
300
|
-
if relationship.person2.id not in self.
|
301
|
-
self.
|
302
|
-
self.
|
308
|
+
if relationship.person2.id not in self.relationship_table:
|
309
|
+
self.relationship_table[relationship.person2.id] = []
|
310
|
+
self.relationship_table[relationship.person2.id].append(relationship)
|
303
311
|
relationship.person2._add_relationship(relationship)
|
304
312
|
else:
|
305
|
-
|
313
|
+
pass
|
306
314
|
|
307
315
|
self.relationships.append(relationship)
|
308
316
|
else:
|
@@ -311,7 +319,7 @@ class GedcomX:
|
|
311
319
|
def add_place_description(self,placeDescription: PlaceDescription):
|
312
320
|
if placeDescription and isinstance(placeDescription,PlaceDescription):
|
313
321
|
if placeDescription.id is None:
|
314
|
-
|
322
|
+
Warning("PlaceDescription has no id")
|
315
323
|
self.places.append(placeDescription)
|
316
324
|
|
317
325
|
def add_agent(self,agent: Agent):
|
@@ -332,7 +340,8 @@ class GedcomX:
|
|
332
340
|
if agent.id is None:
|
333
341
|
agent.id = Agent.default_id_generator()
|
334
342
|
if self.agents.byId(agent.id):
|
335
|
-
|
343
|
+
pass #TODO Deal with duplicates
|
344
|
+
#raise ValueError
|
336
345
|
print(f'Added Agent with id: {agent.id}')
|
337
346
|
self.agents.append(agent)
|
338
347
|
|
@@ -362,12 +371,17 @@ class GedcomX:
|
|
362
371
|
"""
|
363
372
|
gedcomx_json = {
|
364
373
|
'persons': [person._as_dict_ for person in self.persons],
|
365
|
-
'sourceDescriptions' : [sourceDescription._as_dict_ for sourceDescription in self.source_descriptions]
|
374
|
+
'sourceDescriptions' : [sourceDescription._as_dict_ for sourceDescription in self.source_descriptions],
|
375
|
+
'relationships': [relationship._as_dict_ for relationship in self.relationships],
|
376
|
+
'agents': [agent._as_dict_ for agent in self.agents],
|
377
|
+
'events': [event._as_dict_ for event in self.events],
|
378
|
+
'places': [place._as_dict_ for place in self.places],
|
379
|
+
'documents': [document._as_dict_ for document in self.documents],
|
366
380
|
}
|
367
381
|
return json.dumps(gedcomx_json, indent=4)
|
368
382
|
|
369
383
|
class Translater():
|
370
|
-
def __init__(self,gedcom:
|
384
|
+
def __init__(self,gedcom: Gedcom5x) -> None:
|
371
385
|
self.handlers = {}
|
372
386
|
self.gedcom: Gedcom = gedcom
|
373
387
|
self.gedcomx = GedcomX()
|
@@ -852,6 +866,8 @@ class Translater():
|
|
852
866
|
self.object_stack.append(gxobject)
|
853
867
|
self.object_map[record.level] = gxobject
|
854
868
|
else:
|
869
|
+
convert_log.warning(f"Could not convert EVEN '{value}' for object of type {type(self.object_map[record.level-1])} in record {record.describe()}")
|
870
|
+
return
|
855
871
|
raise TagConversionError(record=record,levelstack=self.object_map)
|
856
872
|
assert False
|
857
873
|
# TODO: Fix, this. making an event to cacth subtags, why are these fact tied to a source? GEDCOM is horrible
|
@@ -957,7 +973,7 @@ class Translater():
|
|
957
973
|
elif record.parent.tag == 'TRAN':
|
958
974
|
pass #TODO
|
959
975
|
else:
|
960
|
-
raise TagConversionError(record=record,levelstack=self.object_map)
|
976
|
+
convert_log.error(f"raise TagConversionError(record=record,levelstack=self.object_map")
|
961
977
|
|
962
978
|
def handle_givn(self, record: GedcomRecord):
|
963
979
|
if isinstance(self.object_map[record.level-1], Name):
|
@@ -1036,6 +1052,12 @@ class Translater():
|
|
1036
1052
|
self.object_map[record.level-1].changeMessage = record.value
|
1037
1053
|
else:
|
1038
1054
|
self.object_map[record.level-1].changeMessage = self.object_map[record.level-1].changeMessage + '' + record.value
|
1055
|
+
elif isinstance(self.object_map[record.level-1], Note):
|
1056
|
+
gxobject = Note(text=Translater.clean_str(record.value))
|
1057
|
+
self.object_map[record.level-2].add_note(gxobject)
|
1058
|
+
|
1059
|
+
self.object_stack.append(gxobject)
|
1060
|
+
self.object_map[record.level] = gxobject
|
1039
1061
|
|
1040
1062
|
else:
|
1041
1063
|
raise ValueError(f"Could not handle 'NOTE' tag in record {record.describe()}, last stack object {type(self.object_map[record.level-1])}")
|
@@ -1064,7 +1086,7 @@ class Translater():
|
|
1064
1086
|
def handle_page(self, record: GedcomRecord):
|
1065
1087
|
if isinstance(self.object_map[record.level-1], SourceReference):
|
1066
1088
|
self.object_map[record.level-1].descriptionId = record.value
|
1067
|
-
self.object_map[record.level-1].add_qualifier(KnownSourceReference.Page)
|
1089
|
+
self.object_map[record.level-1].add_qualifier(KnownSourceReference(name=str(KnownSourceReference.Page),value=record.value))
|
1068
1090
|
|
1069
1091
|
#self.object_stack.append(gxobject)
|
1070
1092
|
#self.object_map[record.level] = gxobject
|
@@ -1081,21 +1103,21 @@ class Translater():
|
|
1081
1103
|
self.object_map[record.level] = gxobject
|
1082
1104
|
elif isinstance(self.object_map[record.level-1], Event):
|
1083
1105
|
if self.gedcomx.places.byName(record.value):
|
1084
|
-
self.object_map[record.level-1].place = PlaceReference(original=record.value,
|
1106
|
+
self.object_map[record.level-1].place = PlaceReference(original=record.value, description=self.gedcomx.places.byName(record.value)[0])
|
1085
1107
|
else:
|
1086
1108
|
place_des = PlaceDescription(names=[TextValue(value=record.value)])
|
1087
1109
|
self.gedcomx.add_place_description(place_des)
|
1088
|
-
self.object_map[record.level-1].place = PlaceReference(original=record.value,
|
1110
|
+
self.object_map[record.level-1].place = PlaceReference(original=record.value, description=place_des)
|
1089
1111
|
if len(record.subRecords()) > 0:
|
1090
1112
|
self.object_map[record.level]= place_des
|
1091
1113
|
|
1092
1114
|
elif isinstance(self.object_map[record.level-1], Fact):
|
1093
1115
|
if self.gedcomx.places.byName(record.value):
|
1094
|
-
self.object_map[record.level-1].place = PlaceReference(original=record.value,
|
1116
|
+
self.object_map[record.level-1].place = PlaceReference(original=record.value, description=self.gedcomx.places.byName(record.value)[0])
|
1095
1117
|
else:
|
1096
1118
|
place_des = PlaceDescription(names=[TextValue(value=record.value)])
|
1097
1119
|
self.gedcomx.add_place_description(place_des)
|
1098
|
-
self.object_map[record.level-1].place = PlaceReference(original=record.value,
|
1120
|
+
self.object_map[record.level-1].place = PlaceReference(original=record.value, description=place_des)
|
1099
1121
|
elif isinstance(self.object_map[record.level-1], SourceDescription):
|
1100
1122
|
gxobject = Note(text='Place: ' + record.value)
|
1101
1123
|
self.object_map[record.level-1].add_note(gxobject)
|
@@ -1291,7 +1313,7 @@ class Translater():
|
|
1291
1313
|
|
1292
1314
|
self.object_map[record.level]._add_name_part(gxobject)
|
1293
1315
|
else:
|
1294
|
-
raise TagConversionError(record=record,levelstack=self.object_map)
|
1316
|
+
convert_log.error(f"raise TagConversionError(record=record,levelstack=self.object_map)")
|
1295
1317
|
|
1296
1318
|
def handle_tran(self, record: GedcomRecord):
|
1297
1319
|
pass
|
gedcomx/Gender.py
CHANGED
@@ -9,6 +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
|
from collections.abc import Sized
|
14
15
|
|
@@ -47,55 +48,20 @@ class Gender(Conclusion):
|
|
47
48
|
|
48
49
|
@property
|
49
50
|
def _as_dict_(self):
|
50
|
-
|
51
|
-
if isinstance(value, (str, int, float, bool, type(None))):
|
52
|
-
return value
|
53
|
-
elif isinstance(value, dict):
|
54
|
-
return {k: _serialize(v) for k, v in value.items()}
|
55
|
-
elif isinstance(value, (list, tuple, set)):
|
56
|
-
return [_serialize(v) for v in value]
|
57
|
-
elif hasattr(value, "_as_dict_"):
|
58
|
-
return value._as_dict_
|
59
|
-
else:
|
60
|
-
return str(value) # fallback for unknown objects
|
51
|
+
|
61
52
|
|
62
|
-
|
53
|
+
type_as_dict = super()._as_dict_ # Start with base class fields
|
63
54
|
# Only add Relationship-specific fields
|
64
|
-
|
55
|
+
type_as_dict.update({
|
65
56
|
'type':self.type.value if self.type else None
|
66
57
|
|
67
58
|
})
|
68
59
|
|
69
|
-
|
70
|
-
for key, value in gender_fields.items():
|
71
|
-
if value is not None:
|
72
|
-
gender_fields[key] = _serialize(value)
|
73
|
-
|
74
|
-
return {
|
75
|
-
k: v
|
76
|
-
for k, v in gender_fields.items()
|
77
|
-
if v is not None and not (isinstance(v, Sized) and len(v) == 0)
|
78
|
-
}
|
79
|
-
|
80
|
-
return gender_fields
|
60
|
+
return Serialization.serialize_dict(type_as_dict)
|
81
61
|
|
82
62
|
@classmethod
|
83
63
|
def _from_json_(cls,data):
|
84
|
-
id = data.get('id') if data.get('id') else None
|
85
|
-
lang = data.get('lang',None)
|
86
|
-
sources = [SourceReference._from_json_(o) for o in data.get('sources')] if data.get('sources') else None
|
87
|
-
analysis = None #URI.from_url(data.get('analysis')) if data.get('analysis',None) else None,
|
88
|
-
notes = [Note._from_json_(o) for o in data.get('notes',[])]
|
89
|
-
#TODO confidence = ConfidenceLevel(data.get('confidence')),
|
90
|
-
attribution = Attribution._from_json_(data.get('attribution'))
|
91
|
-
type = GenderType(data.get('type'))
|
92
64
|
|
65
|
+
return Serialization.deserialize(data, Gender)
|
93
66
|
|
94
|
-
return Gender(id=id,
|
95
|
-
lang=lang,
|
96
|
-
sources=sources,
|
97
|
-
analysis=analysis,
|
98
|
-
notes=notes,
|
99
|
-
attribution=attribution,
|
100
|
-
type=type)
|
101
67
|
|
gedcomx/Identifier.py
CHANGED
@@ -3,36 +3,40 @@ from enum import Enum
|
|
3
3
|
|
4
4
|
from typing import List, Optional, Dict, Any
|
5
5
|
|
6
|
-
from .
|
6
|
+
from collections.abc import Iterator
|
7
|
+
import json
|
7
8
|
from .Resource import Resource
|
8
9
|
from .URI import URI
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
10
|
+
from .ExtensibleEnum import ExtensibleEnum
|
11
|
+
|
12
|
+
import secrets
|
13
|
+
import string
|
14
|
+
import json
|
15
|
+
|
16
|
+
def make_uid(length: int = 10, alphabet: str = string.ascii_letters + string.digits) -> str:
|
17
|
+
"""
|
18
|
+
Generate a cryptographically secure alphanumeric UID.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
length: Number of characters to generate (must be > 0).
|
22
|
+
alphabet: Characters to choose from (default: A-Za-z0-9).
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
A random string of `length` characters from `alphabet`.
|
26
|
+
"""
|
27
|
+
if length <= 0:
|
28
|
+
raise ValueError("length must be > 0")
|
29
|
+
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
30
|
+
|
31
|
+
class IdentifierType(ExtensibleEnum):
|
32
|
+
"""Enumeration of identifier types."""
|
33
|
+
pass
|
34
|
+
IdentifierType.register("Primary", "http://gedcomx.org/Primary")
|
35
|
+
IdentifierType.register("Authority", "http://gedcomx.org/Authority")
|
36
|
+
IdentifierType.register("Deprecated", "http://gedcomx.org/Deprecated")
|
37
|
+
IdentifierType.register("Persistent", "http://gedcomx.org/Persistent")
|
38
|
+
IdentifierType.register("External", "https://gedcom.io/terms/v7/EXID")
|
39
|
+
IdentifierType.register("Other", "user provided")
|
36
40
|
|
37
41
|
class Identifier:
|
38
42
|
identifier = 'http://gedcomx.org/v1/Identifier'
|
@@ -40,36 +44,20 @@ class Identifier:
|
|
40
44
|
|
41
45
|
def __init__(self, value: Optional[List[URI]], type: Optional[IdentifierType] = IdentifierType.Primary) -> None:
|
42
46
|
if not isinstance(value,list):
|
43
|
-
value = [value]
|
47
|
+
value = [value] if value else []
|
44
48
|
self.type = type
|
45
49
|
self.values = value if value else []
|
46
50
|
|
47
51
|
@property
|
48
52
|
def _as_dict_(self):
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
elif isinstance(value, dict):
|
53
|
-
return {k: _serialize(v) for k, v in value.items()}
|
54
|
-
elif isinstance(value, (list, tuple, set)):
|
55
|
-
return [_serialize(v) for v in value]
|
56
|
-
elif hasattr(value, "_as_dict_"):
|
57
|
-
return value._as_dict_
|
58
|
-
else:
|
59
|
-
return str(value) # fallback for unknown objects
|
60
|
-
|
61
|
-
identifier_fields = {
|
62
|
-
'value': self.values if self.values else None,
|
53
|
+
from .Serialization import Serialization
|
54
|
+
type_as_dict = {
|
55
|
+
'value': [v for v in self.values] if self.values else None,
|
63
56
|
'type': self.type.value if self.type else None
|
64
57
|
|
65
58
|
}
|
66
59
|
|
67
|
-
|
68
|
-
for key, value in identifier_fields.items():
|
69
|
-
if value is not None:
|
70
|
-
identifier_fields[key] = _serialize(value)
|
71
|
-
|
72
|
-
return identifier_fields
|
60
|
+
return Serialization.serialize_dict(type_as_dict)
|
73
61
|
|
74
62
|
@classmethod
|
75
63
|
def _from_json_(cls, data: Dict[str, Any]) -> 'Identifier | None':
|
@@ -79,8 +67,8 @@ class Identifier:
|
|
79
67
|
#for name, member in IdentifierType.__members__.items():
|
80
68
|
# print(name)
|
81
69
|
|
82
|
-
|
83
|
-
|
70
|
+
|
71
|
+
|
84
72
|
for key in data.keys():
|
85
73
|
type = key
|
86
74
|
value = data[key]
|
@@ -94,24 +82,25 @@ class Identifier:
|
|
94
82
|
id_type: Optional[IdentifierType] = IdentifierType(raw_type) if raw_type else None
|
95
83
|
return cls(value=value, type=id_type)
|
96
84
|
|
97
|
-
class IdentifierList
|
85
|
+
class IdentifierList:
|
98
86
|
def __init__(self) -> None:
|
99
|
-
|
100
|
-
|
87
|
+
# maps identifier-type (e.g., str or IdentifierType.value) -> list of values
|
88
|
+
self.identifiers: dict[str, list] = {}
|
89
|
+
|
90
|
+
# -------------------- hashing/uniqueness helpers --------------------
|
101
91
|
def make_hashable(self, obj):
|
102
92
|
"""Convert any object into a hashable representation."""
|
103
93
|
if isinstance(obj, dict):
|
104
|
-
# Convert dict to sorted tuple of key/value pairs
|
105
94
|
return tuple(sorted((k, self.make_hashable(v)) for k, v in obj.items()))
|
106
95
|
elif isinstance(obj, (list, set, tuple)):
|
107
|
-
# Convert sequences/sets into tuples
|
108
96
|
return tuple(self.make_hashable(i) for i in obj)
|
109
|
-
elif
|
110
|
-
|
111
|
-
|
112
|
-
|
97
|
+
elif isinstance(obj, URI):
|
98
|
+
return obj._as_dict_
|
99
|
+
elif hasattr(obj, "_as_dict_"):
|
100
|
+
d = getattr(obj, "_as_dict_")
|
101
|
+
return tuple(sorted((k, self.make_hashable(v)) for k, v in d.items()))
|
113
102
|
else:
|
114
|
-
return obj
|
103
|
+
return obj
|
115
104
|
|
116
105
|
def unique_list(self, items):
|
117
106
|
"""Return a list without duplicates, preserving order."""
|
@@ -123,48 +112,113 @@ class IdentifierList():
|
|
123
112
|
seen.add(h)
|
124
113
|
result.append(item)
|
125
114
|
return result
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
115
|
+
|
116
|
+
# -------------------- public mutation API --------------------
|
117
|
+
def append(self, identifier: "Identifier"):
|
118
|
+
if isinstance(identifier, Identifier):
|
119
|
+
self.add_identifier(identifier)
|
130
120
|
else:
|
131
|
-
raise ValueError()
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
121
|
+
raise ValueError("append expects an Identifier instance")
|
122
|
+
|
123
|
+
# keep the old name working; point it at the corrected spelling
|
124
|
+
def add_identifer(self, identifier: "Identifier"): # backward-compat alias
|
125
|
+
return self.add_identifier(identifier)
|
126
|
+
|
127
|
+
def add_identifier(self, identifier: "Identifier"):
|
128
|
+
"""Add/merge an Identifier (which may contain multiple values)."""
|
129
|
+
if not (identifier and isinstance(identifier, Identifier) and identifier.type):
|
130
|
+
raise ValueError("The 'identifier' must be a valid Identifier instance with a type.")
|
131
|
+
|
132
|
+
key = identifier.type.value if hasattr(identifier.type, "value") else str(identifier.type)
|
133
|
+
existing = self.identifiers.get(key, [])
|
134
|
+
merged = self.unique_list(list(existing) + list(identifier.values))
|
135
|
+
self.identifiers[key] = merged
|
136
|
+
|
137
|
+
# -------------------- queries --------------------
|
138
|
+
def contains(self, identifier: "Identifier") -> bool:
|
139
|
+
"""Return True if any of the identifier's values are present under that type."""
|
140
|
+
if not (identifier and isinstance(identifier, Identifier) and identifier.type):
|
141
|
+
return False
|
142
|
+
key = identifier.type.value if hasattr(identifier.type, "value") else str(identifier.type)
|
143
|
+
if key not in self.identifiers:
|
144
|
+
return False
|
145
|
+
pool = self.identifiers[key]
|
146
|
+
# treat values as a list on the incoming Identifier
|
147
|
+
for v in getattr(identifier, "values", []):
|
148
|
+
if any(self.make_hashable(v) == self.make_hashable(p) for p in pool):
|
149
|
+
return True
|
150
|
+
return False
|
151
|
+
|
152
|
+
# -------------------- mapping-like dunder methods --------------------
|
153
|
+
def __iter__(self) -> Iterator[str]:
|
154
|
+
"""Iterate over identifier *types* (keys)."""
|
155
|
+
return iter(self.identifiers)
|
156
|
+
|
157
|
+
def __len__(self) -> int:
|
158
|
+
"""Number of identifier types (keys)."""
|
159
|
+
return len(self.identifiers)
|
160
|
+
|
161
|
+
def __contains__(self, key) -> bool:
|
162
|
+
"""Check if a type key exists (accepts str or enum with .value)."""
|
163
|
+
k = key.value if hasattr(key, "value") else str(key)
|
164
|
+
return k in self.identifiers
|
165
|
+
|
166
|
+
def __getitem__(self, key):
|
167
|
+
"""Lookup values by type key (accepts str or enum with .value)."""
|
168
|
+
k = key.value if hasattr(key, "value") else str(key)
|
169
|
+
return self.identifiers[k]
|
170
|
+
|
171
|
+
# (optional) enable assignment via mapping syntax
|
172
|
+
def __setitem__(self, key, values):
|
173
|
+
"""Set/replace the list of values for a type key."""
|
174
|
+
k = key.value if hasattr(key, "value") else str(key)
|
175
|
+
vals = values if isinstance(values, list) else [values]
|
176
|
+
self.identifiers[k] = self.unique_list(vals)
|
177
|
+
|
178
|
+
def __delitem__(self, key):
|
179
|
+
k = key.value if hasattr(key, "value") else str(key)
|
180
|
+
del self.identifiers[k]
|
181
|
+
|
182
|
+
# -------------------- dict-style convenience --------------------
|
183
|
+
def keys(self):
|
184
|
+
return self.identifiers.keys()
|
185
|
+
|
186
|
+
def values(self):
|
187
|
+
return self.identifiers.values()
|
188
|
+
|
189
|
+
def items(self):
|
190
|
+
return self.identifiers.items()
|
191
|
+
|
192
|
+
def iter_pairs(self) -> Iterator[tuple[str, object]]:
|
193
|
+
"""Flattened iterator over (type_key, value) pairs."""
|
194
|
+
for k, vals in self.identifiers.items():
|
195
|
+
for v in vals:
|
196
|
+
yield (k, v)
|
147
197
|
|
148
|
-
def __get_item__(self):
|
149
|
-
pass
|
150
|
-
|
151
198
|
@classmethod
|
152
|
-
def _from_json_(cls,data):
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
199
|
+
def _from_json_(cls, data):
|
200
|
+
if isinstance(data, dict):
|
201
|
+
identifier_list = IdentifierList()
|
202
|
+
for key, vals in data.items():
|
203
|
+
# Accept both single value and list in JSON
|
204
|
+
vals = vals if isinstance(vals, list) else [vals]
|
205
|
+
identifier_list.add_identifier(
|
206
|
+
Identifier(value=vals, type=IdentifierType(key))
|
207
|
+
)
|
208
|
+
return identifier_list
|
209
|
+
else:
|
210
|
+
raise ValueError("Data must be a dict of identifiers.")
|
211
|
+
|
160
212
|
@property
|
161
213
|
def _as_dict_(self):
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
214
|
+
# If you want a *copy*, return `dict(self.identifiers)`
|
215
|
+
return self.identifiers
|
216
|
+
|
217
|
+
def __repr__(self) -> str:
|
218
|
+
return json.dumps(self._as_dict_, indent=4)
|
166
219
|
|
167
|
-
|
220
|
+
def __str__(self) -> str:
|
221
|
+
return json.dumps(self._as_dict_)
|
168
222
|
|
169
223
|
|
170
224
|
|