gedcom-x 0.5.1__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/Gender.py CHANGED
@@ -5,10 +5,13 @@ from gedcomx.Attribution import Attribution
5
5
  from gedcomx.Conclusion import ConfidenceLevel
6
6
  from gedcomx.Note import Note
7
7
  from gedcomx.SourceReference import SourceReference
8
- from gedcomx.URI import URI
8
+ from gedcomx.Resource import Resource
9
9
 
10
10
  from .Conclusion import Conclusion
11
11
  from .Qualifier import Qualifier
12
+ from .Serialization import Serialization
13
+
14
+ from collections.abc import Sized
12
15
 
13
16
  class GenderType(Enum):
14
17
  Male = "http://gedcomx.org/Male"
@@ -33,16 +36,32 @@ class Gender(Conclusion):
33
36
  def __init__(self,
34
37
  id: Optional[str] = None,
35
38
  lang: Optional[str] = 'en',
36
- sources: Optional[List[SourceReference]] = [],
37
- analysis: Optional[URI] = None,
38
- notes: Optional[List[Note]] = [],
39
+ sources: Optional[List[SourceReference]] = None,
40
+ analysis: Optional[Resource] = None,
41
+ notes: Optional[List[Note]] = None,
39
42
  confidence: Optional[ConfidenceLevel] = None,
40
43
  attribution: Optional[Attribution] = None,
41
- type: GenderType = None
44
+ type: Optional[GenderType] = None
42
45
  ) -> None:
43
- super().__init__(id, lang, sources, analysis, notes, confidence, attribution)
46
+ super().__init__(id=id, lang=lang, sources=sources, analysis=analysis, notes=notes, confidence=confidence, attribution=attribution)
44
47
  self.type = type
45
48
 
49
+ @property
50
+ def _as_dict_(self):
51
+
52
+
53
+ type_as_dict = super()._as_dict_ # Start with base class fields
54
+ # Only add Relationship-specific fields
55
+ type_as_dict.update({
56
+ 'type':self.type.value if self.type else None
57
+
58
+ })
59
+
60
+ return Serialization.serialize_dict(type_as_dict)
61
+
46
62
  @classmethod
47
- def _from_json_(cls,json_text):
48
- return Gender()
63
+ def _from_json_(cls,data):
64
+
65
+ return Serialization.deserialize(data, Gender)
66
+
67
+
gedcomx/Group.py CHANGED
@@ -9,7 +9,7 @@ from .Identifier import Identifier
9
9
  from .Note import Note
10
10
  from .PlaceReference import PlaceReference
11
11
  from .SourceReference import SourceReference
12
- from .URI import URI
12
+ from .Resource import Resource
13
13
 
14
14
  from .TextValue import TextValue
15
15
  from .Subject import Subject
@@ -22,14 +22,14 @@ class GroupRole:
22
22
  identifier = 'http://gedcomx.org/v1/GroupRole'
23
23
  version = 'http://gedcomx.org/conceptual-model/v1'
24
24
 
25
- def __init__(self, person: URI,type: Optional[Enum], date: Optional[Date],details: Optional[str]) -> None:
25
+ def __init__(self, person: Resource,type: Optional[Enum], date: Optional[Date],details: Optional[str]) -> None:
26
26
  pass
27
27
 
28
28
  class Group(Subject):
29
29
  identifier = 'http://gedcomx.org/v1/Group'
30
30
  version = 'http://gedcomx.org/conceptual-model/v1'
31
31
 
32
- def __init__(self, id: str | None, lang: str | None, sources: SourceReference | None, analysis: URI | None, notes: Note | None, confidence: ConfidenceLevel | None, attribution: Attribution | None, extracted: bool | None, evidence: List[EvidenceReference] | None, media: List[SourceReference] | None, identifiers: List[Identifier] | None,
32
+ def __init__(self, id: str | None, lang: str | None, sources: SourceReference | None, analysis: Resource | None, notes: Note | None, confidence: ConfidenceLevel | None, attribution: Attribution | None, extracted: bool | None, evidence: List[EvidenceReference] | None, media: List[SourceReference] | None, identifiers: List[Identifier] | None,
33
33
  names: TextValue,
34
34
  date: Optional[Date],
35
35
  place: Optional[PlaceReference],
gedcomx/Identifier.py CHANGED
@@ -3,87 +3,224 @@ 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
8
+ from .Resource import Resource
7
9
  from .URI import URI
10
+ from .Extensible.extensibles import ExtensibleEnum
8
11
 
9
- class IdentifierType(Enum):
10
- Primary = "http://gedcomx.org/Primary"
11
- Authority = "http://gedcomx.org/Authority"
12
- Deprecated = "http://gedcomx.org/Deprecated"
13
- Persistant = "http://gedcomx.org/Persistent"
14
-
15
- @property
16
- def description(self):
17
- descriptions = {
18
- IdentifierType.Primary: (
19
- "The primary identifier for the resource. The value of the identifier MUST resolve to the instance of "
20
- "Subject to which the identifier applies."
21
- ),
22
- IdentifierType.Authority: (
23
- "An identifier for the resource in an external authority or other expert system. The value of the identifier "
24
- "MUST resolve to a public, authoritative source for information about the Subject to which the identifier applies."
25
- ),
26
- IdentifierType.Deprecated: (
27
- "An identifier that has been relegated, deprecated, or otherwise downgraded. This identifier is commonly used "
28
- "as the result of a merge when what was once a primary identifier for a resource is no longer the primary identifier. "
29
- "The value of the identifier MUST resolve to the instance of Subject to which the identifier applies."
30
- )
31
- }
32
- return descriptions.get(self, "No description available.")
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")
33
40
 
34
41
  class Identifier:
35
42
  identifier = 'http://gedcomx.org/v1/Identifier'
36
43
  version = 'http://gedcomx.org/conceptual-model/v1'
37
44
 
38
- def __init__(self, value: Optional[URI], type: Optional[IdentifierType] = IdentifierType.Primary) -> None:
39
- self.value = value
45
+ def __init__(self, value: Optional[List[URI]], type: Optional[IdentifierType] = IdentifierType.Primary) -> None:
46
+ if not isinstance(value,list):
47
+ value = [value] if value else []
40
48
  self.type = type
41
-
49
+ self.values = value if value else []
42
50
 
43
51
  @property
44
52
  def _as_dict_(self):
45
- def _serialize(value):
46
- if isinstance(value, (str, int, float, bool, type(None))):
47
- return value
48
- elif isinstance(value, dict):
49
- return {k: _serialize(v) for k, v in value.items()}
50
- elif isinstance(value, (list, tuple, set)):
51
- return [_serialize(v) for v in value]
52
- elif hasattr(value, "_as_dict_"):
53
- return value._as_dict_
54
- else:
55
- return str(value) # fallback for unknown objects
56
-
57
-
58
- identifier_fields = {
59
- 'value': self.value if self.value else None,
53
+ from .Serialization import Serialization
54
+ type_as_dict = {
55
+ 'value': [v for v in self.values] if self.values else None,
60
56
  'type': self.type.value if self.type else None
61
57
 
62
58
  }
63
59
 
64
- # Serialize and exclude None values
65
- for key, value in identifier_fields.items():
66
- if value is not None:
67
- identifier_fields[key] = _serialize(value)
68
-
69
- return identifier_fields
60
+ return Serialization.serialize_dict(type_as_dict)
70
61
 
71
62
  @classmethod
72
- def _from_json_(cls, data: Dict[str, Any]) -> 'Identifier':
63
+ def _from_json_(cls, data: Dict[str, Any]) -> 'Identifier | None':
73
64
  """
74
65
  Construct an Identifier from a dict parsed from JSON.
75
66
  """
76
- # Parse value (URI dict or string)
67
+ #for name, member in IdentifierType.__members__.items():
68
+ # print(name)
69
+
70
+
77
71
 
78
72
  for key in data.keys():
79
73
  type = key
80
74
  value = data[key]
81
- uri_obj: Optional[URI] = None
75
+ uri_obj: Optional[Resource] = None
82
76
  # TODO DO THIS BETTER
83
77
 
84
78
  # Parse type
85
79
  raw_type = data.get('type')
86
- id_type: Optional[IdentifierType] = IdentifierType(raw_type)
87
-
80
+ if raw_type is None:
81
+ return None
82
+ id_type: Optional[IdentifierType] = IdentifierType(raw_type) if raw_type else None
83
+ return cls(value=value, type=id_type)
84
+
85
+ class IdentifierList:
86
+ def __init__(self) -> None:
87
+ # maps identifier-type (e.g., str or IdentifierType.value) -> list of values
88
+ self.identifiers: dict[str, list] = {}
89
+
90
+ # -------------------- hashing/uniqueness helpers --------------------
91
+ def make_hashable(self, obj):
92
+ """Convert any object into a hashable representation."""
93
+ if isinstance(obj, dict):
94
+ return tuple(sorted((k, self.make_hashable(v)) for k, v in obj.items()))
95
+ elif isinstance(obj, (list, set, tuple)):
96
+ return tuple(self.make_hashable(i) for i in obj)
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()))
102
+ else:
103
+ return obj
104
+
105
+ def unique_list(self, items):
106
+ """Return a list without duplicates, preserving order."""
107
+ seen = set()
108
+ result = []
109
+ for item in items:
110
+ h = self.make_hashable(item)
111
+ if h not in seen:
112
+ seen.add(h)
113
+ result.append(item)
114
+ return result
115
+
116
+ # -------------------- public mutation API --------------------
117
+ def append(self, identifier: "Identifier"):
118
+ if isinstance(identifier, Identifier):
119
+ self.add_identifier(identifier)
120
+ else:
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)
197
+
198
+ @classmethod
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
+
212
+ @property
213
+ def _as_dict_(self):
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)
219
+
220
+ def __str__(self) -> str:
221
+ return json.dumps(self._as_dict_)
222
+
223
+
224
+
225
+
88
226
 
89
- return cls(value=value, type=id_type)
gedcomx/Logging.py ADDED
@@ -0,0 +1,19 @@
1
+ import logging
2
+
3
+ def get_logger(name='gedcomx.log'):
4
+ logger = logging.getLogger(name)
5
+ if not logger.handlers:
6
+ logger.setLevel(logging.DEBUG)
7
+
8
+ formatter = logging.Formatter('[%(asctime)s] %(levelname)s - %(name)s - %(message)s')
9
+
10
+ console_handler = logging.StreamHandler()
11
+ console_handler.setFormatter(formatter)
12
+ logger.addHandler(console_handler)
13
+
14
+ # Optional: file logging
15
+ file_handler = logging.FileHandler(f"{name}.log", encoding="utf-8")
16
+ file_handler.setFormatter(formatter)
17
+ logger.addHandler(file_handler)
18
+
19
+ return logger
gedcomx/Mutations.py ADDED
@@ -0,0 +1,228 @@
1
+ from .Gedcom5x import GedcomRecord
2
+ from .Fact import Fact, FactType
3
+ from .Event import Event, EventType
4
+
5
+ fact_event_table = {
6
+ # Person Fact / Event Types
7
+ "ADOP": {
8
+ "Fact": FactType.AdoptiveParent,
9
+ "Event": EventType.Adoption,
10
+ },
11
+ "CHR": {
12
+ "Fact": FactType.AdultChristening,
13
+ "Event": EventType.AdultChristening,
14
+ },
15
+ "EVEN": {
16
+ "Fact": FactType.Amnesty,
17
+ # no Event
18
+ },
19
+ "BAPM": {
20
+ "Fact": FactType.Baptism,
21
+ "Event": EventType.Baptism,
22
+ },
23
+ "BARM": {
24
+ "Fact": FactType.BarMitzvah,
25
+ "Event": EventType.BarMitzvah,
26
+ },
27
+ "BASM": {
28
+ "Fact": FactType.BatMitzvah,
29
+ "Event": EventType.BatMitzvah,
30
+ },
31
+ "BIRT": {
32
+ "Fact": FactType.Birth,
33
+ "Event": EventType.Birth,
34
+ },
35
+ "BIRT, CHR": {
36
+ "Fact": FactType.Birth,
37
+ "Event": EventType.Birth,
38
+ },
39
+ "BLES": {
40
+ "Fact": FactType.Blessing,
41
+ "Event": EventType.Blessing,
42
+ },
43
+ "BURI": {
44
+ "Fact": FactType.Burial,
45
+ "Event": EventType.Burial,
46
+ },
47
+ "CAST": {
48
+ "Fact": FactType.Caste,
49
+ # no Event
50
+ },
51
+ "CENS": {
52
+ "Fact": FactType.Census,
53
+ "Event": EventType.Census,
54
+ },
55
+ "CIRC": {
56
+ "Fact": FactType.Circumcision,
57
+ "Event": EventType.Circumcision,
58
+ },
59
+ "CONF": {
60
+ "Fact": FactType.Confirmation,
61
+ "Event": EventType.Confirmation,
62
+ },
63
+ "CREM": {
64
+ "Fact": FactType.Cremation,
65
+ "Event": EventType.Cremation,
66
+ },
67
+ "DEAT": {
68
+ "Fact": FactType.Death,
69
+ "Event": EventType.Death,
70
+ },
71
+ "EDUC": {
72
+ "Fact": FactType.Education,
73
+ "Event": EventType.Education,
74
+ },
75
+ "EMIG": {
76
+ "Fact": FactType.Emigration,
77
+ "Event": EventType.Emigration,
78
+ },
79
+ "FCOM": {
80
+ "Fact": FactType.FirstCommunion,
81
+ "Event": EventType.FirstCommunion,
82
+ },
83
+ "GRAD": {
84
+ "Fact": FactType.Graduation,
85
+ # no Event
86
+ },
87
+ "IMMI": {
88
+ "Fact": FactType.Immigration,
89
+ "Event": EventType.Immigration,
90
+ },
91
+ "MIL": {
92
+ "Fact": FactType.MilitaryService,
93
+ # no Event
94
+ },
95
+ "NATI": {
96
+ "Fact": FactType.Nationality,
97
+ # no Event
98
+ },
99
+ "NATU": {
100
+ "Fact": FactType.Naturalization,
101
+ "Event": EventType.Naturalization,
102
+ },
103
+ "OCCU": {
104
+ "Fact": FactType.Occupation,
105
+ # no Event
106
+ },
107
+ "ORDN": {
108
+ "Fact": FactType.Ordination,
109
+ "Event": EventType.Ordination,
110
+ },
111
+ "DSCR": {
112
+ "Fact": FactType.PhysicalDescription,
113
+ # no Event
114
+ },
115
+ "PROB": {
116
+ "Fact": FactType.Probate,
117
+ # no Event
118
+ },
119
+ "PROP": {
120
+ "Fact": FactType.Property,
121
+ # no Event
122
+ },
123
+ "RELI": {
124
+ "Fact": FactType.Religion,
125
+ # no Event
126
+ },
127
+ "RESI": {
128
+ "Fact": FactType.Residence,
129
+ # no Event
130
+ },
131
+ "WILL": {
132
+ "Fact": FactType.Will,
133
+ # no Event
134
+ },
135
+
136
+ # Couple Relationship Fact / Event Types
137
+ "ANUL": {
138
+ "Fact": FactType.Annulment,
139
+ "Event": EventType.Annulment,
140
+ },
141
+ "DIV": {
142
+ "Fact": FactType.Divorce,
143
+ "Event": EventType.Divorce,
144
+ },
145
+ "DIVF": {
146
+ "Fact": FactType.DivorceFiling,
147
+ "Event": EventType.DivorceFiling,
148
+ },
149
+ "ENGA": {
150
+ "Fact": FactType.Engagement,
151
+ "Event": EventType.Engagement,
152
+ },
153
+ "MARR": {
154
+ "Fact": FactType.Marriage,
155
+ "Event": EventType.Marriage,
156
+ },
157
+ "MARB": {
158
+ "Fact": FactType.MarriageBanns,
159
+ # no Event
160
+ },
161
+ "MARC": {
162
+ "Fact": FactType.MarriageContract,
163
+ # no Event
164
+ },
165
+ "MARL": {
166
+ "Fact": FactType.MarriageLicense,
167
+ # no Event
168
+ },
169
+ "MARS":{
170
+ "Fact":EventType.MarriageSettlment
171
+
172
+ },
173
+ "SEPA": {
174
+ "Fact": FactType.Separation,
175
+ # no Event
176
+ },
177
+
178
+ }
179
+
180
+ class GedcomXObject:
181
+ def __init__(self,record: GedcomRecord | None = None) -> None:
182
+ self.created_with_tag: str = record.tag if record and isinstance(record, GedcomRecord) else None
183
+ self.created_at_level: int = record.level if record and isinstance(record, GedcomRecord) else None
184
+ self.created_at_line_number: int = record.line_number if record and isinstance(record, GedcomRecord) else None
185
+
186
+ class GedcomXSourceOrDocument(GedcomXObject):
187
+ def __init__(self,record: GedcomRecord | None = None) -> None:
188
+ super().__init__(record)
189
+ self.title: str = None
190
+ self.citation: str = None
191
+ self.page: str = None
192
+ self.contributor: str = None
193
+ self.publisher: str = None
194
+ self.rights: str = None
195
+ self.url: str = None
196
+ self.medium: str = None
197
+ self.type: str = None
198
+ self.format: str = None
199
+ self.created: str = None
200
+ self.modified: str = None
201
+ self.language: str = None
202
+ self.relation: str = None
203
+ self.identifier: str = None
204
+ self.description: str = None
205
+
206
+ class GedcomXEventOrFact(GedcomXObject):
207
+ def __new__(cls,record: GedcomRecord | None = None, object_stack: dict | None = None) -> object:
208
+ super().__init__(record)
209
+ if record.tag in fact_event_table.keys():
210
+
211
+ if 'Fact' in fact_event_table[record.tag].keys():
212
+ obj = Fact(type=fact_event_table[record.tag]['Fact'])
213
+ return obj
214
+ elif 'Event' in fact_event_table[record.tag].keys():
215
+ obj = Event(type=fact_event_table[record.tag]['Fact'])
216
+ else:
217
+ raise ValueError
218
+ else:
219
+ raise ValueError(f"{record.tag} not found in map")
220
+
221
+ class GedcomXRelationshipBuilder(GedcomXObject):
222
+ def __new__(cls,record: GedcomRecord | None = None, object_stack: dict | None = None) -> object:
223
+ last_relationship = object_stack.get('lastrelationship',None)
224
+ last_relationship_data = object_stack.get('lastrelationshipdata',None)
225
+ if not isinstance(last_relationship_data,dict):
226
+ last_relationship_data = None
227
+
228
+