gedcom-x 0.5.1__py3-none-any.whl → 0.5.2__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,11 +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
12
 
13
+ from collections.abc import Sized
14
+
13
15
  class GenderType(Enum):
14
16
  Male = "http://gedcomx.org/Male"
15
17
  Female = "http://gedcomx.org/Female"
@@ -33,16 +35,67 @@ class Gender(Conclusion):
33
35
  def __init__(self,
34
36
  id: Optional[str] = None,
35
37
  lang: Optional[str] = 'en',
36
- sources: Optional[List[SourceReference]] = [],
37
- analysis: Optional[URI] = None,
38
- notes: Optional[List[Note]] = [],
38
+ sources: Optional[List[SourceReference]] = None,
39
+ analysis: Optional[Resource] = None,
40
+ notes: Optional[List[Note]] = None,
39
41
  confidence: Optional[ConfidenceLevel] = None,
40
42
  attribution: Optional[Attribution] = None,
41
- type: GenderType = None
43
+ type: Optional[GenderType] = None
42
44
  ) -> None:
43
- super().__init__(id, lang, sources, analysis, notes, confidence, attribution)
45
+ super().__init__(id=id, lang=lang, sources=sources, analysis=analysis, notes=notes, confidence=confidence, attribution=attribution)
44
46
  self.type = type
45
47
 
48
+ @property
49
+ 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
61
+
62
+ gender_fields = super()._as_dict_ # Start with base class fields
63
+ # Only add Relationship-specific fields
64
+ gender_fields.update({
65
+ 'type':self.type.value if self.type else None
66
+
67
+ })
68
+
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
81
+
46
82
  @classmethod
47
- def _from_json_(cls,json_text):
48
- return Gender()
83
+ 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
+
93
+
94
+ return Gender(id=id,
95
+ lang=lang,
96
+ sources=sources,
97
+ analysis=analysis,
98
+ notes=notes,
99
+ attribution=attribution,
100
+ type=type)
101
+
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
@@ -4,6 +4,7 @@ from enum import Enum
4
4
  from typing import List, Optional, Dict, Any
5
5
 
6
6
  from .Qualifier import Qualifier
7
+ from .Resource import Resource
7
8
  from .URI import URI
8
9
 
9
10
  class IdentifierType(Enum):
@@ -11,6 +12,8 @@ class IdentifierType(Enum):
11
12
  Authority = "http://gedcomx.org/Authority"
12
13
  Deprecated = "http://gedcomx.org/Deprecated"
13
14
  Persistant = "http://gedcomx.org/Persistent"
15
+ External = "https://gedcom.io/terms/v7/EXID"
16
+ Other = "user provided"
14
17
 
15
18
  @property
16
19
  def description(self):
@@ -35,10 +38,11 @@ class Identifier:
35
38
  identifier = 'http://gedcomx.org/v1/Identifier'
36
39
  version = 'http://gedcomx.org/conceptual-model/v1'
37
40
 
38
- def __init__(self, value: Optional[URI], type: Optional[IdentifierType] = IdentifierType.Primary) -> None:
39
- self.value = value
41
+ def __init__(self, value: Optional[List[URI]], type: Optional[IdentifierType] = IdentifierType.Primary) -> None:
42
+ if not isinstance(value,list):
43
+ value = [value]
40
44
  self.type = type
41
-
45
+ self.values = value if value else []
42
46
 
43
47
  @property
44
48
  def _as_dict_(self):
@@ -54,9 +58,8 @@ class Identifier:
54
58
  else:
55
59
  return str(value) # fallback for unknown objects
56
60
 
57
-
58
61
  identifier_fields = {
59
- 'value': self.value if self.value else None,
62
+ 'value': self.values if self.values else None,
60
63
  'type': self.type.value if self.type else None
61
64
 
62
65
  }
@@ -69,21 +72,101 @@ class Identifier:
69
72
  return identifier_fields
70
73
 
71
74
  @classmethod
72
- def _from_json_(cls, data: Dict[str, Any]) -> 'Identifier':
75
+ def _from_json_(cls, data: Dict[str, Any]) -> 'Identifier | None':
73
76
  """
74
77
  Construct an Identifier from a dict parsed from JSON.
75
78
  """
79
+ #for name, member in IdentifierType.__members__.items():
80
+ # print(name)
81
+
76
82
  # Parse value (URI dict or string)
77
-
83
+ print('--------------',data)
78
84
  for key in data.keys():
79
85
  type = key
80
86
  value = data[key]
81
- uri_obj: Optional[URI] = None
87
+ uri_obj: Optional[Resource] = None
82
88
  # TODO DO THIS BETTER
83
89
 
84
90
  # Parse type
85
91
  raw_type = data.get('type')
86
- id_type: Optional[IdentifierType] = IdentifierType(raw_type)
92
+ if raw_type is None:
93
+ return None
94
+ id_type: Optional[IdentifierType] = IdentifierType(raw_type) if raw_type else None
95
+ return cls(value=value, type=id_type)
96
+
97
+ class IdentifierList():
98
+ def __init__(self) -> None:
99
+ self.identifiers = {}
100
+
101
+ def make_hashable(self, obj):
102
+ """Convert any object into a hashable representation."""
103
+ if isinstance(obj, dict):
104
+ # Convert dict to sorted tuple of key/value pairs
105
+ return tuple(sorted((k, self.make_hashable(v)) for k, v in obj.items()))
106
+ elif isinstance(obj, (list, set, tuple)):
107
+ # Convert sequences/sets into tuples
108
+ 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
113
+ else:
114
+ return obj # Immutable stays as is
115
+
116
+ def unique_list(self, items):
117
+ """Return a list without duplicates, preserving order."""
118
+ seen = set()
119
+ result = []
120
+ for item in items:
121
+ h = self.make_hashable(item)
122
+ if h not in seen:
123
+ seen.add(h)
124
+ result.append(item)
125
+ return result
126
+
127
+ def append(self, identifier: Identifier):
128
+ if isinstance(identifier, Identifier):
129
+ self.add_identifer(identifier)
130
+ 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])
87
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
147
+
148
+ def __get_item__(self):
149
+ pass
150
+
151
+ @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
+
160
+ @property
161
+ 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]]
166
+
167
+ return identifiers_dict
168
+
169
+
170
+
171
+
88
172
 
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/Name.py CHANGED
@@ -5,8 +5,9 @@ from .Attribution import Attribution
5
5
  from .Conclusion import Conclusion, ConfidenceLevel
6
6
  from .Date import Date
7
7
  from .Note import Note
8
+ from .Serialization import Serialization
8
9
  from .SourceReference import SourceReference
9
- from .URI import URI
10
+ from .Resource import Resource
10
11
 
11
12
 
12
13
  class NameType(Enum):
@@ -99,10 +100,11 @@ class NamePart:
99
100
  self.value = value
100
101
  self.qualifiers = qualifiers
101
102
 
102
- def _prop_dict(self):
103
- return {'type': self.type.value if self.type else None,
104
- 'value': self.value if self.value else None,
105
- 'qualifiers': [qualifier.value for qualifier in self.qualifiers]}
103
+ def _as_dict_(self):
104
+ return Serialization.serialize_dict(
105
+ {'type': self.type.value if self.type else None,
106
+ 'value': self.value if self.value else None,
107
+ 'qualifiers': [qualifier.value for qualifier in self.qualifiers] if self.qualifiers else None})
106
108
 
107
109
  def __eq__(self, other):
108
110
  if not isinstance(other, NamePart):
@@ -120,10 +122,12 @@ class NameForm:
120
122
  self.fullText = fullText
121
123
  self.parts = parts
122
124
 
123
- def _prop_dict(self):
124
- return {'lang': self.lang,
125
- 'fullText': self.fullText,
126
- 'parts': [part._prop_dict() for part in self.parts]}
125
+ @property
126
+ def _as_dict_(self):
127
+ return Serialization.serialize_dict(
128
+ {'lang': self.lang,
129
+ 'fullText': self.fullText,
130
+ 'parts': [part._as_dict_() for part in self.parts] if self.parts else None})
127
131
 
128
132
  def _fulltext_parts(self):
129
133
  pass
@@ -137,37 +141,40 @@ class Name(Conclusion):
137
141
  """
138
142
  Takes a string and returns a GedcomX Name Object
139
143
  """
140
- text = text.replace("/","")
141
- parts = text.rsplit(' ', 1)
142
-
143
- # Assign val1 and val2 based on the split
144
- given = parts[0] if len(parts) > 1 else ""
145
- surname = parts[1] if len(parts) > 1 else parts[0]
144
+ if text:
145
+ text = text.replace("/","")
146
+ parts = text.rsplit(' ', 1)
146
147
 
147
- # Remove any '/' characters from both val1 and val2
148
- #given = given.replace('/', '')
149
- #surname = surname.replace('/', '')
148
+ # Assign val1 and val2 based on the split
149
+ given = parts[0] if len(parts) > 1 else ""
150
+ surname = parts[1] if len(parts) > 1 else parts[0]
151
+
152
+ # Remove any '/' characters from both val1 and val2
153
+ #given = given.replace('/', '')
154
+ #surname = surname.replace('/', '')
150
155
 
151
- given_name_part = NamePart(type = NamePartType.Given, value=given)
152
- surname_part = NamePart(type = NamePartType.Surname, value=surname)
156
+ given_name_part = NamePart(type = NamePartType.Given, value=given)
157
+ surname_part = NamePart(type = NamePartType.Surname, value=surname)
153
158
 
154
- name_form = NameForm(fullText=text,parts=[given_name_part,surname_part])
155
- name = Name(type=NameType.BirthName,nameForms=[name_form])
159
+ name_form = NameForm(fullText=text,parts=[given_name_part,surname_part])
160
+ name = Name(type=NameType.BirthName,nameForms=[name_form])
161
+ else:
162
+ name = Name()
156
163
  return name
157
164
 
158
165
  def __init__(self, id: str = None,
159
166
  lang: str = 'en',
160
- sources: Optional[List[SourceReference]] = [],
161
- analysis: URI = None,
162
- notes: Optional[List[Note]] = [],
163
- confidence: ConfidenceLevel = None,
164
- attribution: Attribution = None,
167
+ sources: Optional[List[SourceReference]] = None,
168
+ analysis: Resource = None,
169
+ notes: Optional[List[Note]] = None,
170
+ confidence: Optional[ConfidenceLevel] = None,
171
+ attribution: Optional[Attribution] = None,
165
172
  type: Optional[NameType] = None,
166
- nameForms: NameForm = [],
173
+ nameForms: Optional[List[NameForm]]= None,
167
174
  date: Optional[Date] = None) -> None:
168
175
  super().__init__(id, lang, sources, analysis, notes, confidence, attribution)
169
176
  self.type = type
170
- self.nameForms = nameForms
177
+ self.nameForms = nameForms if nameForms else []
171
178
  self.date = date
172
179
 
173
180
  def _add_name_part(self, namepart_to_add: NamePart):
@@ -179,11 +186,38 @@ class Name(Conclusion):
179
186
 
180
187
  @property
181
188
  def _as_dict_(self):
189
+ def _serialize(value):
190
+ if isinstance(value, (str, int, float, bool, type(None))):
191
+ return value
192
+ elif isinstance(value, dict):
193
+ return {k: _serialize(v) for k, v in value.items()}
194
+ elif isinstance(value, (list, tuple, set)):
195
+ return [_serialize(v) for v in value]
196
+ elif hasattr(value, "_as_dict_"):
197
+ return value._as_dict_
198
+ else:
199
+ return str(value) # fallback for unknown objects
200
+
182
201
  name_as_dict = super()._as_dict_
202
+
183
203
  name_as_dict.update( {
184
204
  'type':self.type.value if self.type else None,
185
- 'nameForms': [nameForm._prop_dict() for nameForm in self.nameForms],
186
- 'date': self.date._prop_date() if self.date else None})
205
+ 'nameForms': [nameForm._as_dict_ for nameForm in self.nameForms],
206
+ 'date': self.date._as_dict_ if self.date else None})
207
+
208
+ #del name_as_dict['id']
209
+ # Serialize and exclude None values
210
+ for key, value in name_as_dict.items():
211
+ if value is not None:
212
+ name_as_dict[key] = _serialize(value)
213
+
214
+ # 3) merge and filter out None *at the top level*
215
+ return {
216
+ k: v
217
+ for k, v in name_as_dict.items()
218
+ if v is not None
219
+ }
220
+
187
221
  return name_as_dict
188
222
 
189
223
  def ensure_list(val):
@@ -223,7 +257,7 @@ Name._from_json_ = classmethod(lambda cls, data: cls(
223
257
  id=data.get('id'),
224
258
  lang=data.get('lang', 'en'),
225
259
  sources=[SourceReference._from_json_(s) for s in ensure_list(data.get('sources'))],
226
- analysis=URI._from_json_(data['analysis']) if data.get('analysis') else None,
260
+ analysis=Resource._from_json_(data['analysis']) if data.get('analysis') else None,
227
261
  notes=[Note._from_json_(n) for n in ensure_list(data.get('notes'))],
228
262
  confidence=ConfidenceLevel._from_json_(data['confidence']) if data.get('confidence') else None,
229
263
  attribution=Attribution._from_json_(data['attribution']) if data.get('attribution') else None,
@@ -232,10 +266,5 @@ Name._from_json_ = classmethod(lambda cls, data: cls(
232
266
  date=Date._from_json_(data['date']) if data.get('date') else None
233
267
  ))
234
268
 
235
- Name._to_dict_ = lambda self: {
236
- **self._as_dict_,
237
- 'type': self.type.value if self.type else None,
238
- 'nameForms': [nf._to_dict_() for nf in self.nameForms] if self.nameForms else [],
239
- 'date': self.date._prop_date() if self.date else None
240
- }
269
+
241
270
 
gedcomx/Note.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from typing import Optional
2
2
 
3
3
  from .Attribution import Attribution
4
+ from .Serialization import Serialization
4
5
 
5
6
  class Note:
6
7
  identifier = 'http://gedcomx.org/v1/Note'
@@ -10,24 +11,24 @@ class Note:
10
11
  self.lang = lang
11
12
  self.subject = subject
12
13
  self.text = text
13
- self.attribution = attribution
14
-
15
-
14
+ self.attribution = attribution
16
15
 
17
16
  def append(self, text_to_add: str):
18
17
  if text_to_add and isinstance(text_to_add, str):
19
18
  self.text = self.text + text_to_add
20
19
  else:
20
+ return #TODO
21
21
  raise ValueError("The text to add must be a non-empty string.")
22
22
 
23
23
  @property
24
24
  def _as_dict_(self):
25
- return {
25
+ note_dict = {
26
26
  "lang":self.lang if self.lang else None,
27
27
  "subject":self.subject if self.subject else None,
28
28
  "text":self.text if self.text else None,
29
29
  "attribution": self.attribution if self.attribution else None
30
30
  }
31
+ return Serialization.serialize_dict(note_dict)
31
32
 
32
33
  def __eq__(self, other):
33
34
  if not isinstance(other, Note):
gedcomx/OnlineAccount.py CHANGED
@@ -1,10 +1,10 @@
1
1
  from typing import Optional
2
2
 
3
- from .URI import URI
3
+ from .Resource import Resource
4
4
 
5
5
  class OnlineAccount:
6
6
  identifier = 'http://gedcomx.org/v1/OnlineAccount'
7
7
  version = 'http://gedcomx.org/conceptual-model/v1'
8
8
 
9
- def __init__(self, serviceHomepage: URI, accountName: str) -> None:
9
+ def __init__(self, serviceHomepage: Resource, accountName: str) -> None:
10
10
  pass