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.
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 .Gedcom import GedcomRecord
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
- item.uri.path = f'{str(item_type.__name__)}s' if (item.uri.path is None or item.uri.path == "") else item.uri.path
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, attribution: Attribution = None, filepath: str = None, description: str = None) -> 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.person_relationships_lookup = {}
233
+ self.relationship_table = {}
234
+
235
+ self.default_id_generator = make_uid
228
236
 
229
- def _add(self,gedcomx_type_object):
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.person_relationships_lookup:
289
- self.person_relationships_lookup[relationship.person1.id] = []
290
- self.person_relationships_lookup[relationship.person1.id].append(relationship)
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
- raise ValueError
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.person_relationships_lookup:
301
- self.person_relationships_lookup[relationship.person2.id] = []
302
- self.person_relationships_lookup[relationship.person2.id].append(relationship)
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
- raise ValueError
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
- placeDescription.id = self.PlaceDescriptionURIGenerator()
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, descriptionRef=self.gedcomx.places.byName(record.value)[0])
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, descriptionRef=place_des)
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, descriptionRef=self.gedcomx.places.byName(record.value)[0])
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, descriptionRef=place_des)
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
- def _serialize(value):
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
- gender_fields = super()._as_dict_ # Start with base class fields
53
+ type_as_dict = super()._as_dict_ # Start with base class fields
63
54
  # Only add Relationship-specific fields
64
- gender_fields.update({
55
+ type_as_dict.update({
65
56
  'type':self.type.value if self.type else None
66
57
 
67
58
  })
68
59
 
69
- # Serialize and exclude None values
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 .Qualifier import Qualifier
6
+ from collections.abc import Iterator
7
+ import json
7
8
  from .Resource import Resource
8
9
  from .URI import URI
9
-
10
- class IdentifierType(Enum):
11
- Primary = "http://gedcomx.org/Primary"
12
- Authority = "http://gedcomx.org/Authority"
13
- Deprecated = "http://gedcomx.org/Deprecated"
14
- Persistant = "http://gedcomx.org/Persistent"
15
- External = "https://gedcom.io/terms/v7/EXID"
16
- Other = "user provided"
17
-
18
- @property
19
- def description(self):
20
- descriptions = {
21
- IdentifierType.Primary: (
22
- "The primary identifier for the resource. The value of the identifier MUST resolve to the instance of "
23
- "Subject to which the identifier applies."
24
- ),
25
- IdentifierType.Authority: (
26
- "An identifier for the resource in an external authority or other expert system. The value of the identifier "
27
- "MUST resolve to a public, authoritative source for information about the Subject to which the identifier applies."
28
- ),
29
- IdentifierType.Deprecated: (
30
- "An identifier that has been relegated, deprecated, or otherwise downgraded. This identifier is commonly used "
31
- "as the result of a merge when what was once a primary identifier for a resource is no longer the primary identifier. "
32
- "The value of the identifier MUST resolve to the instance of Subject to which the identifier applies."
33
- )
34
- }
35
- return descriptions.get(self, "No description available.")
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
- def _serialize(value):
50
- if isinstance(value, (str, int, float, bool, type(None))):
51
- return value
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
- # Serialize and exclude None values
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
- # Parse value (URI dict or string)
83
- print('--------------',data)
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
- self.identifiers = {}
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 hasattr(obj,'_as_dict_'):
110
- as_dict = obj._as_dict_
111
- t = tuple(sorted((k, self.make_hashable(v)) for k, v in as_dict.items()))
112
- return t
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 # Immutable stays as is
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
- def append(self, identifier: Identifier):
128
- if isinstance(identifier, Identifier):
129
- self.add_identifer(identifier)
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
- def add_identifer(self, identifier: Identifier):
134
- if identifier and isinstance(identifier,Identifier):
135
- if identifier.type.value in self.identifiers.keys():
136
- self.identifiers[identifier.type.value].extend(identifier.values)
137
- else:
138
- self.identifiers[identifier.type.value] = identifier.values
139
- print(self.identifiers[identifier.type.value])
140
- self.identifiers[identifier.type.value] = self.unique_list(self.identifiers[identifier.type.value])
141
-
142
- # TODO Merge Identifiers
143
- def contains(self,identifier: Identifier):
144
- if identifier and isinstance(identifier,Identifier):
145
- if identifier.type.value in self.identifiers.keys():
146
- pass
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
- identifier_list = IdentifierList()
154
- for name, member in IdentifierType.__members__.items():
155
- values = data.get(member.value,None)
156
- if values:
157
- identifier_list.add_identifer(Identifier(values,IdentifierType(member.value)))
158
- return identifier_list
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
- identifiers_dict = {}
163
- for key in self.identifiers.keys():
164
- # Should always be flat due to unique_list in add method.
165
- identifiers_dict[key] = [u.value for u in self.identifiers[key]]
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
- return identifiers_dict
220
+ def __str__(self) -> str:
221
+ return json.dumps(self._as_dict_)
168
222
 
169
223
 
170
224