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/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 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 = {}
|
234
|
+
|
235
|
+
self.default_id_generator = make_uid
|
228
236
|
|
229
|
-
def
|
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):
|
@@ -362,7 +370,12 @@ class GedcomX:
|
|
362
370
|
"""
|
363
371
|
gedcomx_json = {
|
364
372
|
'persons': [person._as_dict_ for person in self.persons],
|
365
|
-
'sourceDescriptions' : [sourceDescription._as_dict_ for sourceDescription in self.source_descriptions]
|
373
|
+
'sourceDescriptions' : [sourceDescription._as_dict_ for sourceDescription in self.source_descriptions],
|
374
|
+
'relationships': [relationship._as_dict_ for relationship in self.relationships],
|
375
|
+
'agents': [agent._as_dict_ for agent in self.agents],
|
376
|
+
'events': [event._as_dict_ for event in self.events],
|
377
|
+
'places': [place._as_dict_ for place in self.places],
|
378
|
+
'documents': [document._as_dict_ for document in self.documents],
|
366
379
|
}
|
367
380
|
return json.dumps(gedcomx_json, indent=4)
|
368
381
|
|
@@ -852,6 +865,8 @@ class Translater():
|
|
852
865
|
self.object_stack.append(gxobject)
|
853
866
|
self.object_map[record.level] = gxobject
|
854
867
|
else:
|
868
|
+
convert_log.warning(f"Could not convert EVEN '{value}' for object of type {type(self.object_map[record.level-1])} in record {record.describe()}")
|
869
|
+
return
|
855
870
|
raise TagConversionError(record=record,levelstack=self.object_map)
|
856
871
|
assert False
|
857
872
|
# TODO: Fix, this. making an event to cacth subtags, why are these fact tied to a source? GEDCOM is horrible
|
@@ -1064,7 +1079,7 @@ class Translater():
|
|
1064
1079
|
def handle_page(self, record: GedcomRecord):
|
1065
1080
|
if isinstance(self.object_map[record.level-1], SourceReference):
|
1066
1081
|
self.object_map[record.level-1].descriptionId = record.value
|
1067
|
-
self.object_map[record.level-1].add_qualifier(KnownSourceReference.Page)
|
1082
|
+
self.object_map[record.level-1].add_qualifier(KnownSourceReference(name=str(KnownSourceReference.Page),value=record.value))
|
1068
1083
|
|
1069
1084
|
#self.object_stack.append(gxobject)
|
1070
1085
|
#self.object_map[record.level] = gxobject
|
@@ -1081,21 +1096,21 @@ class Translater():
|
|
1081
1096
|
self.object_map[record.level] = gxobject
|
1082
1097
|
elif isinstance(self.object_map[record.level-1], Event):
|
1083
1098
|
if self.gedcomx.places.byName(record.value):
|
1084
|
-
self.object_map[record.level-1].place = PlaceReference(original=record.value,
|
1099
|
+
self.object_map[record.level-1].place = PlaceReference(original=record.value, description=self.gedcomx.places.byName(record.value)[0])
|
1085
1100
|
else:
|
1086
1101
|
place_des = PlaceDescription(names=[TextValue(value=record.value)])
|
1087
1102
|
self.gedcomx.add_place_description(place_des)
|
1088
|
-
self.object_map[record.level-1].place = PlaceReference(original=record.value,
|
1103
|
+
self.object_map[record.level-1].place = PlaceReference(original=record.value, description=place_des)
|
1089
1104
|
if len(record.subRecords()) > 0:
|
1090
1105
|
self.object_map[record.level]= place_des
|
1091
1106
|
|
1092
1107
|
elif isinstance(self.object_map[record.level-1], Fact):
|
1093
1108
|
if self.gedcomx.places.byName(record.value):
|
1094
|
-
self.object_map[record.level-1].place = PlaceReference(original=record.value,
|
1109
|
+
self.object_map[record.level-1].place = PlaceReference(original=record.value, description=self.gedcomx.places.byName(record.value)[0])
|
1095
1110
|
else:
|
1096
1111
|
place_des = PlaceDescription(names=[TextValue(value=record.value)])
|
1097
1112
|
self.gedcomx.add_place_description(place_des)
|
1098
|
-
self.object_map[record.level-1].place = PlaceReference(original=record.value,
|
1113
|
+
self.object_map[record.level-1].place = PlaceReference(original=record.value, description=place_des)
|
1099
1114
|
elif isinstance(self.object_map[record.level-1], SourceDescription):
|
1100
1115
|
gxobject = Note(text='Place: ' + record.value)
|
1101
1116
|
self.object_map[record.level-1].add_note(gxobject)
|
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 .Extensible.extensibles 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
|
|