gedcom-x 0.5.6__py3-none-any.whl → 0.5.7__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.
@@ -1,24 +1,44 @@
1
- import base64
2
- import uuid
3
1
  import warnings
4
2
 
5
3
  from enum import Enum
6
4
  from typing import List, Optional, Dict, Any
7
5
 
6
+ from typing import Optional, TYPE_CHECKING
7
+ if TYPE_CHECKING:
8
+ from .Document import Document
9
+
10
+ """
11
+ ======================================================================
12
+ Project: Gedcom-X
13
+ File: SourceDescription.py
14
+ Author: David J. Cartwright
15
+ Purpose:
16
+
17
+ Created: 2025-07-25
18
+ Updated:
19
+ - 2025-08-31: _as_dict_ refactored to ignore empty fields, changed id creation to make_uid()
20
+
21
+
22
+ ======================================================================
23
+ """
24
+
25
+ """
26
+ ======================================================================
27
+ GEDCOM Module Types
28
+ ======================================================================
29
+ """
8
30
  from .Agent import Agent
9
31
  from .Attribution import Attribution
10
32
  from .Coverage import Coverage
11
33
  from .Date import Date
12
- from .Identifier import Identifier, IdentifierList
34
+ from .Identifier import Identifier, IdentifierList, make_uid
13
35
  from .Note import Note
36
+ from .Resource import Resource
14
37
  from .SourceCitation import SourceCitation
15
38
  from .SourceReference import SourceReference
16
39
  from .TextValue import TextValue
17
- from .Resource import Resource, get_resource_as_dict
18
- from .Serialization import Serialization
19
40
  from .URI import URI
20
-
21
- from collections.abc import Sized
41
+ #=====================================================================
22
42
 
23
43
  class ResourceType(Enum):
24
44
  Collection = "http://gedcomx.org/Collection"
@@ -38,41 +58,66 @@ class ResourceType(Enum):
38
58
  return descriptions.get(self, "No description available.")
39
59
 
40
60
  class SourceDescription:
41
- """
42
- The SourceDescription data type defines a description of a source of genealogical information.
43
- http://gedcomx.org/v1/SourceDescription
44
-
45
- Parameters
46
- ----------
47
- id : str
48
- Unique identifier for this SourceDescription.
49
- attribution : Attribution Object
50
- Attribution information for the Genealogy
51
- filepath : str
52
- Not Implimented.
53
- description : str
54
- Description of the Genealogy: ex. 'My Family Tree'
55
-
56
- Raises
57
- ------
58
- ValueError
59
- If `id` is not a valid UUID.
60
- """
61
- identifier = "http://gedcomx.org/v1/SourceDescription"
62
- """
63
- Gedcom-X Specification Identifier
61
+ """Description of a genealogical information source.
62
+
63
+ See: http://gedcomx.org/v1/SourceDescription
64
+
65
+ Args:
66
+ id (str | None): Unique identifier for this `SourceDescription`.
67
+ resourceType (ResourceType | None): Type/category of the resource being
68
+ described (e.g., digital artifact, physical artifact).
69
+ citations (list[SourceCitation] | None): Citations that reference or
70
+ justify this source description.
71
+ mediaType (str | None): IANA media (MIME) type of the resource
72
+ (e.g., ``"application/pdf"``).
73
+ about (URI | None): Canonical URI that the description is about.
74
+ mediator (Resource | None): The mediator resource (if any) involved in
75
+ providing access to the source.
76
+ publisher (Resource | Agent | None): Publisher of the resource.
77
+ authors (list[Resource] | None): Authors/creators of the resource.
78
+ sources (list[SourceReference] | None): Other sources this description
79
+ derives from or references.
80
+ analysis (Resource | None): Analysis document associated with the
81
+ resource (often a `Document`; kept generic to avoid circular imports).
82
+ componentOf (SourceReference | None): Reference to a parent/containing
83
+ source (this is a component/child of that source).
84
+ titles (list[TextValue] | None): One or more titles for the resource.
85
+ notes (list[Note] | None): Human-authored notes about the resource.
86
+ attribution (Attribution | None): Attribution metadata for who supplied
87
+ or curated this description.
88
+ rights (list[Resource] | None): Rights statements or licenses.
89
+ coverage (list[Coverage] | None): Spatial/temporal coverage of the
90
+ source’s content.
91
+ descriptions (list[TextValue] | None): Short textual summaries or
92
+ descriptions.
93
+ identifiers (IdentifierList | None): Alternative identifiers for the
94
+ resource (DOI, ARK, call numbers, etc.).
95
+ created (Date | None): Creation date of the resource.
96
+ modified (Date | None): Last modified date of the resource.
97
+ published (Date | None): Publication/release date of the resource.
98
+ repository (Agent | None): Repository or agency that holds the resource.
99
+ max_note_count (int): Maximum number of notes to retain/emit. Defaults to 20.
100
+
101
+ Raises:
102
+ ValueError: If `id` is not a valid UUID.
103
+
104
+ Attributes:
105
+ identifier (str): Gedcom-X specification identifier for this type.
64
106
  """
65
107
 
108
+ identifier = "http://gedcomx.org/v1/SourceDescription"
109
+ version = 'http://gedcomx.org/conceptual-model/v1'
110
+
66
111
  def __init__(self, id: Optional[str] = None,
67
112
  resourceType: Optional[ResourceType] = None,
68
113
  citations: Optional[List[SourceCitation]] = [],
69
114
  mediaType: Optional[str] = None,
70
115
  about: Optional[URI] = None,
71
- mediator: Optional[Resource] = None,
72
- publisher: Optional[Resource|Agent] = None,
116
+ mediator: Optional[Agent|Resource] = None,
117
+ publisher: Optional[Agent|Resource] = None,
73
118
  authors: Optional[List[Resource]] = None,
74
119
  sources: Optional[List[SourceReference]] = None, # SourceReference
75
- analysis: Optional[Resource] = None, # analysis should be of type 'Document', not specified to avoid circular import
120
+ analysis: Optional["Document|Resource"] = None, #TODO add type checker so its a document
76
121
  componentOf: Optional[SourceReference] = None, # SourceReference
77
122
  titles: Optional[List[TextValue]] = None,
78
123
  notes: Optional[List[Note]] = None,
@@ -87,7 +132,7 @@ class SourceDescription:
87
132
  repository: Optional[Agent] = None,
88
133
  max_note_count: int = 20):
89
134
 
90
- self.id = id if id else SourceDescription.default_id_generator()
135
+ self.id = id if id else make_uid()
91
136
  self.resourceType = resourceType
92
137
  self.citations = citations or []
93
138
  self.mediaType = mediaType
@@ -111,7 +156,7 @@ class SourceDescription:
111
156
  self.repository = repository
112
157
  self.max_note_count = max_note_count
113
158
 
114
- self.uri = URI(fragment=id)
159
+ self.uri = URI(fragment=id) if id else None #TODO Should i take care of this in the collections?
115
160
 
116
161
  @property
117
162
  def publisher(self) -> Resource | Agent | None:
@@ -150,8 +195,7 @@ class SourceDescription:
150
195
  return False
151
196
  self.notes.append(note_to_add)
152
197
 
153
- def add_source(self, source_to_add: object):
154
- #from .SourceReference import SourceReference
198
+ def add_source(self, source_to_add: SourceReference):
155
199
  if source_to_add and isinstance(object,SourceReference):
156
200
  for current_source in self.sources:
157
201
  if current_source == source_to_add:
@@ -167,44 +211,59 @@ class SourceDescription:
167
211
  self.titles.append(title_to_add)
168
212
  else:
169
213
  raise ValueError(f"Cannot add title of type {type(title_to_add)}")
170
-
171
- @staticmethod
172
- def default_id_generator():
173
- # Generate a standard UUID
174
- standard_uuid = uuid.uuid4()
175
- # Convert UUID to bytes
176
- uuid_bytes = standard_uuid.bytes
177
- # Encode bytes to a Base64 string
178
- short_uuid = base64.urlsafe_b64encode(uuid_bytes).rstrip(b'=').decode('utf-8')
179
- return 'SD-' + str(short_uuid)
180
-
214
+
181
215
  @property
182
216
  def _as_dict_(self) -> Dict[str, Any]:
183
- type_as_dict = {
184
- 'id': self.id,
185
- 'about': self.about._as_dict_ if self.about else None,
186
- 'resourceType': self.resourceType.value if self.resourceType else None,
187
- 'citations': [c._as_dict_ for c in self.citations] or None,
188
- 'mediaType': self.mediaType,
189
- 'mediator': self.mediator._as_dict_ if self.mediator else None,
190
- 'publisher': self.publisher._as_dict_ if self.publisher else None,
191
- 'authors': self.authors and [a._as_dict_ for a in self.authors] or None,
192
- 'sources': [s._as_dict_ for s in self.sources] or None,
193
- 'analysis': self.analysis._as_dict_ if self.analysis else None,
194
- 'componentOf': self.componentOf._as_dict_ if self.componentOf else None,
195
- 'titles': [t._as_dict_ for t in self.titles] or None,
196
- 'notes': [n._as_dict_ for n in self.notes] or None,
197
- 'attribution': self.attribution._as_dict_ if self.attribution else None,
198
- 'rights': [r._as_dict_ for r in self.rights] or None,
199
- 'coverage': [c._as_dict_ for c in self.coverage] or None,
200
- 'descriptions': [d._as_dict_ for d in self.descriptions] or None,
201
- 'identifiers': self.identifiers._as_dict_ if self.identifiers else None,
202
- 'created': self.created if self.created else None,
203
- 'modified': self.modified if self.modified else None,
204
- 'published': self.published if self.published else None,
205
- 'repository': self.repository._as_dict_ if self.repository else None,
206
- 'uri': self.uri.value
207
- }
217
+ from .Serialization import Serialization
218
+ type_as_dict = {}
219
+
220
+ if self.id:
221
+ type_as_dict['id'] = self.id
222
+ if self.about:
223
+ type_as_dict['about'] = self.about._as_dict_
224
+ if self.resourceType:
225
+ type_as_dict['resourceType'] = getattr(self.resourceType, 'value', self.resourceType)
226
+ if self.citations:
227
+ type_as_dict['citations'] = [c._as_dict_ for c in self.citations if c]
228
+ if self.mediaType:
229
+ type_as_dict['mediaType'] = self.mediaType
230
+ if self.mediator:
231
+ type_as_dict['mediator'] = self.mediator._as_dict_
232
+ if self.publisher:
233
+ type_as_dict['publisher'] = self.publisher._as_dict_ #TODO Resource this
234
+ if self.authors:
235
+ type_as_dict['authors'] = [a._as_dict_ for a in self.authors if a]
236
+ if self.sources:
237
+ type_as_dict['sources'] = [s._as_dict_ for s in self.sources if s]
238
+ if self.analysis:
239
+ type_as_dict['analysis'] = self.analysis._as_dict_
240
+ if self.componentOf:
241
+ type_as_dict['componentOf'] = self.componentOf._as_dict_
242
+ if self.titles:
243
+ type_as_dict['titles'] = [t._as_dict_ for t in self.titles if t]
244
+ if self.notes:
245
+ type_as_dict['notes'] = [n._as_dict_ for n in self.notes if n]
246
+ if self.attribution:
247
+ type_as_dict['attribution'] = self.attribution._as_dict_
248
+ if self.rights:
249
+ type_as_dict['rights'] = [r._as_dict_ for r in self.rights if r]
250
+ if self.coverage:
251
+ type_as_dict['coverage'] = [c._as_dict_ for c in self.coverage if c]
252
+ if self.descriptions:
253
+ type_as_dict['descriptions'] = [d for d in self.descriptions if d]
254
+ if self.identifiers:
255
+ type_as_dict['identifiers'] = self.identifiers._as_dict_
256
+ if self.created is not None:
257
+ type_as_dict['created'] = self.created
258
+ if self.modified is not None:
259
+ type_as_dict['modified'] = self.modified
260
+ if self.published is not None:
261
+ type_as_dict['published'] = self.published
262
+ if self.repository:
263
+ type_as_dict['repository'] = self.repository._as_dict_ #TODO Resource this
264
+ if self.uri and self.uri.value:
265
+ type_as_dict['uri'] = self.uri.value
266
+
208
267
 
209
268
  return Serialization.serialize_dict(type_as_dict)
210
269
 
@@ -222,7 +281,7 @@ class SourceDescription:
222
281
  about = URI._from_json_(data['about']) if data.get('about') else None
223
282
  mediator = Resource._from_json_(data['mediator']) if data.get('mediator') else None
224
283
  publisher = Resource._from_json_(data['publisher']) if data.get('publisher') else None
225
- authors = [Resource._from_json_(a) for a in data.get('authors', [])]
284
+ authors = [Resource._from_json_(a) for a in data.get('authors', [])] if data.get('authors') else None
226
285
  sources = [SourceReference._from_json_(s) for s in data.get('sources', [])]
227
286
  analysis = Resource._from_json_(data['analysis']) if data.get('analysis') else None
228
287
  component_of = SourceReference._from_json_(data['componentOf']) if data.get('componentOf') else None
@@ -1,10 +1,14 @@
1
- from typing import List, Optional
1
+ from __future__ import annotations
2
+ from typing import List, Optional, TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from .SourceDescription import SourceDescription
2
6
 
3
7
  from .Attribution import Attribution
4
8
  from .Qualifier import Qualifier
5
9
 
6
10
  from .Resource import Resource
7
- from .Serialization import Serialization
11
+
8
12
  from .URI import URI
9
13
 
10
14
  from collections.abc import Sized
@@ -48,7 +52,7 @@ class SourceReference:
48
52
  version = 'http://gedcomx.org/conceptual-model/v1'
49
53
 
50
54
  def __init__(self,
51
- description: URI | object | None = None,
55
+ description: URI | SourceDescription | None = None,
52
56
  descriptionId: Optional[str] = None,
53
57
  attribution: Optional[Attribution] = None,
54
58
  qualifiers: Optional[List[Qualifier]] = None
@@ -81,18 +85,15 @@ class SourceReference:
81
85
 
82
86
  @property
83
87
  def _as_dict_(self):
84
- # Only add Relationship-specific fields
88
+ from .Serialization import Serialization
85
89
  type_as_dict = {
86
90
  'description':self.description._as_dict_ if self.description else None,
87
91
  'descriptionId': self.descriptionId.replace("\n"," ").replace("\r"," ") if self.descriptionId else None,
88
92
  'attribution': self.attribution._as_dict_ if self.attribution else None,
89
- 'qualifiers':[qualifier.value for qualifier in self.qualifiers ] if self.qualifiers else None
93
+ 'qualifiers':[qualifier.__as_dict__ for qualifier in self.qualifiers] if (self.qualifiers and len(self.qualifiers) > 0) else None
90
94
  }
91
95
  return Serialization.serialize_dict(type_as_dict)
92
-
93
-
94
-
95
-
96
+
96
97
  @classmethod
97
98
  def _from_json_(cls, data: dict):
98
99
  """
gedcomx/Subject.py CHANGED
@@ -6,7 +6,7 @@ from .Conclusion import Conclusion, ConfidenceLevel
6
6
  from .EvidenceReference import EvidenceReference
7
7
  from .Identifier import Identifier, IdentifierList
8
8
  from .Note import Note
9
- from .Serialization import Serialization
9
+
10
10
  from .SourceReference import SourceReference
11
11
  from .Resource import Resource
12
12
  from .Extensions.rs10.rsLink import _rsLinkList
@@ -45,31 +45,15 @@ class Subject(Conclusion):
45
45
 
46
46
  @property
47
47
  def _as_dict_(self):
48
- def _serialize(value):
49
- if isinstance(value, (str, int, float, bool, type(None))):
50
- return value
51
- elif isinstance(value, dict):
52
- return {k: _serialize(v) for k, v in value.items()}
53
- elif isinstance(value, (list, tuple, set)):
54
- return [_serialize(v) for v in value]
55
- elif hasattr(value, "_as_dict_"):
56
- return value._as_dict_
57
- else:
58
- return str(value) # fallback for unknown objects
48
+ from .Serialization import Serialization
59
49
 
60
- subject_fields = super()._as_dict_ # Start with base class fields
50
+ type_as_dict = super()._as_dict_ # Start with base class fields
61
51
  # Only add Relationship-specific fields
62
- subject_fields.update({
52
+ type_as_dict.update({
63
53
  "extracted": self.extracted,
64
54
  "evidence": [evidence_ref for evidence_ref in self.evidence] if self.evidence else None,
65
55
  "media": [media for media in self.media] if self.media else None,
66
56
  "identifiers": self.identifiers._as_dict_ if self.identifiers else None
67
57
 
68
58
  })
69
-
70
- # Serialize and exclude None values
71
- for key, value in subject_fields.items():
72
- if value is not None:
73
- subject_fields[key] = _serialize(value)
74
-
75
- return subject_fields
59
+ return Serialization.serialize_dict(type_as_dict)