gedcom-x 0.5.7__py3-none-any.whl → 0.5.9__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.7.dist-info → gedcom_x-0.5.9.dist-info}/METADATA +1 -1
- gedcom_x-0.5.9.dist-info/RECORD +56 -0
- gedcomx/Extensions/rs10/rsLink.py +110 -60
- gedcomx/TopLevelTypeCollection.py +1 -1
- gedcomx/__init__.py +43 -42
- gedcomx/address.py +217 -0
- gedcomx/{Agent.py → agent.py} +107 -34
- gedcomx/attribution.py +115 -0
- gedcomx/{Conclusion.py → conclusion.py} +120 -51
- gedcomx/{Converter.py → converter.py} +261 -116
- gedcomx/coverage.py +64 -0
- gedcomx/{Date.py → date.py} +43 -9
- gedcomx/{Document.py → document.py} +60 -12
- gedcomx/{Event.py → event.py} +88 -31
- gedcomx/evidence_reference.py +20 -0
- gedcomx/{Fact.py → fact.py} +81 -74
- gedcomx/{Gedcom.py → gedcom.py} +10 -0
- gedcomx/{Gedcom5x.py → gedcom5x.py} +31 -21
- gedcomx/gedcom7/Exceptions.py +9 -0
- gedcomx/gedcom7/GedcomStructure.py +94 -0
- gedcomx/gedcom7/Specification.py +347 -0
- gedcomx/gedcom7/__init__.py +26 -0
- gedcomx/gedcom7/g7interop.py +205 -0
- gedcomx/gedcom7/gedcom7.py +160 -0
- gedcomx/gedcom7/logger.py +19 -0
- gedcomx/{GedcomX.py → gedcomx.py} +109 -106
- gedcomx/gender.py +91 -0
- gedcomx/group.py +72 -0
- gedcomx/{Identifier.py → identifier.py} +48 -21
- gedcomx/{LoggingHub.py → logging_hub.py} +19 -0
- gedcomx/{Mutations.py → mutations.py} +59 -30
- gedcomx/{Name.py → name.py} +88 -47
- gedcomx/note.py +105 -0
- gedcomx/online_account.py +19 -0
- gedcomx/{Person.py → person.py} +61 -41
- gedcomx/{PlaceDescription.py → place_description.py} +71 -23
- gedcomx/{PlaceReference.py → place_reference.py} +32 -10
- gedcomx/{Qualifier.py → qualifier.py} +20 -4
- gedcomx/relationship.py +156 -0
- gedcomx/resource.py +112 -0
- gedcomx/serialization.py +794 -0
- gedcomx/source_citation.py +37 -0
- gedcomx/source_description.py +401 -0
- gedcomx/{SourceReference.py → source_reference.py} +56 -21
- gedcomx/subject.py +122 -0
- gedcomx/textvalue.py +89 -0
- gedcomx/{Translation.py → translation.py} +4 -4
- gedcomx/uri.py +273 -0
- gedcom_x-0.5.7.dist-info/RECORD +0 -49
- gedcomx/Address.py +0 -131
- gedcomx/Attribution.py +0 -91
- gedcomx/Coverage.py +0 -37
- gedcomx/EvidenceReference.py +0 -11
- gedcomx/Gender.py +0 -65
- gedcomx/Group.py +0 -37
- gedcomx/Note.py +0 -73
- gedcomx/OnlineAccount.py +0 -10
- gedcomx/Relationship.py +0 -97
- gedcomx/Resource.py +0 -85
- gedcomx/Serialization.py +0 -816
- gedcomx/SourceCitation.py +0 -25
- gedcomx/SourceDescription.py +0 -314
- gedcomx/Subject.py +0 -59
- gedcomx/TextValue.py +0 -35
- gedcomx/URI.py +0 -105
- {gedcom_x-0.5.7.dist-info → gedcom_x-0.5.9.dist-info}/WHEEL +0 -0
- {gedcom_x-0.5.7.dist-info → gedcom_x-0.5.9.dist-info}/top_level.txt +0 -0
- /gedcomx/{Exceptions.py → exceptions.py} +0 -0
- /gedcomx/{ExtensibleEnum.py → extensible_enum.py} +0 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
from .logging_hub import hub, logging
|
3
|
+
"""
|
4
|
+
======================================================================
|
5
|
+
Logging
|
6
|
+
======================================================================
|
7
|
+
"""
|
8
|
+
log = logging.getLogger("gedcomx")
|
9
|
+
serial_log = "gedcomx.serialization"
|
10
|
+
#=====================================================================
|
11
|
+
|
12
|
+
class SourceCitation:
|
13
|
+
identifier = 'http://gedcomx.org/v1/SourceCitation'
|
14
|
+
version = 'http://gedcomx.org/conceptual-model/v1'
|
15
|
+
|
16
|
+
def __init__(self, lang: Optional[str], value: str) -> None:
|
17
|
+
self.lang = lang if lang else 'en'
|
18
|
+
self.value = value
|
19
|
+
|
20
|
+
# ...existing code...
|
21
|
+
|
22
|
+
@classmethod
|
23
|
+
def _from_json_(cls, data: dict, context = None):
|
24
|
+
"""
|
25
|
+
Create a SourceCitation instance from a JSON-dict (already parsed).
|
26
|
+
"""
|
27
|
+
object_data = {}
|
28
|
+
if (lang := data.get('lang')) is not None:
|
29
|
+
object_data['lang'] = lang
|
30
|
+
if (value := data.get('value')) is not None:
|
31
|
+
object_data['value'] = value
|
32
|
+
return cls(**object_data)
|
33
|
+
|
34
|
+
@property
|
35
|
+
def _as_dict_(self):
|
36
|
+
return {'lang':self.lang,
|
37
|
+
'value': self.value}
|
@@ -0,0 +1,401 @@
|
|
1
|
+
import warnings
|
2
|
+
|
3
|
+
from enum import Enum
|
4
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
5
|
+
if TYPE_CHECKING:
|
6
|
+
from .document import Document
|
7
|
+
|
8
|
+
|
9
|
+
"""
|
10
|
+
======================================================================
|
11
|
+
Project: Gedcom-X
|
12
|
+
File: SourceDescription.py
|
13
|
+
Author: David J. Cartwright
|
14
|
+
Purpose:
|
15
|
+
|
16
|
+
Created: 2025-07-25
|
17
|
+
Updated:
|
18
|
+
- 2025-08-31: _as_dict_ refactored to ignore empty fields, changed id creation to make_uid()
|
19
|
+
- 2025-09-01: filename PEP8 standard, imports changed accordingly
|
20
|
+
|
21
|
+
|
22
|
+
======================================================================
|
23
|
+
"""
|
24
|
+
|
25
|
+
"""
|
26
|
+
======================================================================
|
27
|
+
GEDCOM Module Types
|
28
|
+
======================================================================
|
29
|
+
"""
|
30
|
+
from .agent import Agent
|
31
|
+
from .attribution import Attribution
|
32
|
+
from .coverage import Coverage
|
33
|
+
from .date import Date
|
34
|
+
from .identifier import Identifier, IdentifierList, make_uid
|
35
|
+
from .logging_hub import hub, logging
|
36
|
+
from .note import Note
|
37
|
+
from .resource import Resource
|
38
|
+
from .source_citation import SourceCitation
|
39
|
+
from .source_reference import SourceReference
|
40
|
+
from .textvalue import TextValue
|
41
|
+
from .uri import URI
|
42
|
+
"""
|
43
|
+
======================================================================
|
44
|
+
Logging
|
45
|
+
======================================================================
|
46
|
+
"""
|
47
|
+
log = logging.getLogger("gedcomx")
|
48
|
+
serial_log = "gedcomx.serialization"
|
49
|
+
deserial_log = "gedcomx.deserialization"
|
50
|
+
#=====================================================================
|
51
|
+
|
52
|
+
|
53
|
+
class ResourceType(Enum):
|
54
|
+
Collection = "http://gedcomx.org/Collection"
|
55
|
+
PhysicalArtifact = "http://gedcomx.org/PhysicalArtifact"
|
56
|
+
DigitalArtifact = "http://gedcomx.org/DigitalArtifact"
|
57
|
+
Record = "http://gedcomx.org/Record"
|
58
|
+
Person = "http://gedcomx.org/Person"
|
59
|
+
|
60
|
+
@property
|
61
|
+
def description(self):
|
62
|
+
descriptions = {
|
63
|
+
ResourceType.Collection: "A collection of genealogical resources. A collection may contain physical artifacts (such as a collection of books in a library), records (such as the 1940 U.S. Census), or digital artifacts (such as an online genealogical application).",
|
64
|
+
ResourceType.PhysicalArtifact: "A physical artifact, such as a book.",
|
65
|
+
ResourceType.DigitalArtifact: "A digital artifact, such as a digital image of a birth certificate or other record.",
|
66
|
+
ResourceType.Record: "A historical record, such as a census record or a vital record."
|
67
|
+
}
|
68
|
+
return descriptions.get(self, "No description available.")
|
69
|
+
|
70
|
+
class SourceDescription:
|
71
|
+
"""Description of a genealogical information source.
|
72
|
+
|
73
|
+
See: http://gedcomx.org/v1/SourceDescription
|
74
|
+
|
75
|
+
Args:
|
76
|
+
id (str | None): Unique identifier for this `SourceDescription`.
|
77
|
+
resourceType (ResourceType | None): Type/category of the resource being
|
78
|
+
described (e.g., digital artifact, physical artifact).
|
79
|
+
citations (list[SourceCitation] | None): Citations that reference or
|
80
|
+
justify this source description.
|
81
|
+
mediaType (str | None): IANA media (MIME) type of the resource
|
82
|
+
(e.g., ``"application/pdf"``).
|
83
|
+
about (URI | None): Canonical URI that the description is about.
|
84
|
+
mediator (Resource | None): The mediator resource (if any) involved in
|
85
|
+
providing access to the source.
|
86
|
+
publisher (Resource | Agent | None): Publisher of the resource.
|
87
|
+
authors (list[Resource] | None): Authors/creators of the resource.
|
88
|
+
sources (list[SourceReference] | None): Other sources this description
|
89
|
+
derives from or references.
|
90
|
+
analysis (Resource | None): Analysis document associated with the
|
91
|
+
resource (often a `Document`; kept generic to avoid circular imports).
|
92
|
+
componentOf (SourceReference | None): Reference to a parent/containing
|
93
|
+
source (this is a component/child of that source).
|
94
|
+
titles (list[TextValue] | None): One or more titles for the resource.
|
95
|
+
notes (list[Note] | None): Human-authored notes about the resource.
|
96
|
+
attribution (Attribution | None): Attribution metadata for who supplied
|
97
|
+
or curated this description.
|
98
|
+
rights (list[Resource] | None): Rights statements or licenses.
|
99
|
+
coverage (list[Coverage] | None): Spatial/temporal coverage of the
|
100
|
+
source’s content.
|
101
|
+
descriptions (list[TextValue] | None): Short textual summaries or
|
102
|
+
descriptions.
|
103
|
+
identifiers (IdentifierList | None): Alternative identifiers for the
|
104
|
+
resource (DOI, ARK, call numbers, etc.).
|
105
|
+
created (Date | None): Creation date of the resource.
|
106
|
+
modified (Date | None): Last modified date of the resource.
|
107
|
+
published (Date | None): Publication/release date of the resource.
|
108
|
+
repository (Agent | None): Repository or agency that holds the resource.
|
109
|
+
max_note_count (int): Maximum number of notes to retain/emit. Defaults to 20.
|
110
|
+
|
111
|
+
Raises:
|
112
|
+
ValueError: If `id` is not a valid UUID.
|
113
|
+
|
114
|
+
Attributes:
|
115
|
+
identifier (str): Gedcom-X specification identifier for this type.
|
116
|
+
"""
|
117
|
+
|
118
|
+
identifier = "http://gedcomx.org/v1/SourceDescription"
|
119
|
+
version = 'http://gedcomx.org/conceptual-model/v1'
|
120
|
+
|
121
|
+
def __init__(self, id: Optional[str] = None,
|
122
|
+
resourceType: Optional[ResourceType] = None,
|
123
|
+
citations: Optional[List[SourceCitation]] = [],
|
124
|
+
mediaType: Optional[str] = None,
|
125
|
+
about: Optional[URI] = None,
|
126
|
+
mediator: Optional[Agent|Resource] = None,
|
127
|
+
publisher: Optional[Agent|Resource] = None,
|
128
|
+
authors: Optional[List[Resource]] = None,
|
129
|
+
sources: Optional[List[SourceReference]] = None, # SourceReference
|
130
|
+
analysis: Optional["Document|Resource"] = None, #TODO add type checker so its a document
|
131
|
+
componentOf: Optional[SourceReference] = None, # SourceReference
|
132
|
+
titles: Optional[List[TextValue]] = None,
|
133
|
+
notes: Optional[List[Note]] = None,
|
134
|
+
attribution: Optional[Attribution] = None,
|
135
|
+
rights: Optional[List[Resource]] = [],
|
136
|
+
coverage: Optional[List[Coverage]] = None, # Coverage
|
137
|
+
descriptions: Optional[List[TextValue]] = None,
|
138
|
+
identifiers: Optional[IdentifierList] = None,
|
139
|
+
created: Optional[Date] = None,
|
140
|
+
modified: Optional[Date] = None,
|
141
|
+
published: Optional[Date] = None,
|
142
|
+
repository: Optional[Agent] = None,
|
143
|
+
max_note_count: int = 20):
|
144
|
+
|
145
|
+
self.id = id if id else make_uid()
|
146
|
+
self.resourceType = resourceType
|
147
|
+
self.citations = citations or []
|
148
|
+
self.mediaType = mediaType
|
149
|
+
self.about = about
|
150
|
+
self.mediator = mediator
|
151
|
+
self._publisher = publisher
|
152
|
+
self.authors = authors or []
|
153
|
+
self.sources = sources or []
|
154
|
+
self.analysis = analysis
|
155
|
+
self.componentOf = componentOf
|
156
|
+
self.titles = titles or []
|
157
|
+
self.notes = notes or []
|
158
|
+
self.attribution = attribution
|
159
|
+
self.rights = rights or []
|
160
|
+
self.coverage = coverage or []
|
161
|
+
self.descriptions = descriptions or []
|
162
|
+
self.identifiers = identifiers or IdentifierList()
|
163
|
+
self.created = created
|
164
|
+
self.modified = modified
|
165
|
+
self.published = published
|
166
|
+
self.repository = repository
|
167
|
+
self.max_note_count = max_note_count
|
168
|
+
|
169
|
+
self.uri = URI(fragment=id) if id else None #TODO Should i take care of this in the collections?
|
170
|
+
|
171
|
+
self._place_holder = False
|
172
|
+
|
173
|
+
@property
|
174
|
+
def publisher(self) -> Resource | Agent | None:
|
175
|
+
return self._publisher
|
176
|
+
|
177
|
+
|
178
|
+
@publisher.setter
|
179
|
+
def publisher(self, value: Resource | Agent):
|
180
|
+
if value is None:
|
181
|
+
self._publisher = None
|
182
|
+
elif isinstance(value,Resource):
|
183
|
+
self._publisher = value
|
184
|
+
elif isinstance(value,Agent):
|
185
|
+
self._publisher = value
|
186
|
+
else:
|
187
|
+
raise ValueError(f"'publisher' must be of type 'URI' or 'Agent', type: {type(value)} was provided")
|
188
|
+
|
189
|
+
def add_description(self, desccription_to_add: TextValue):
|
190
|
+
if desccription_to_add and isinstance(desccription_to_add,TextValue):
|
191
|
+
for current_description in self.descriptions:
|
192
|
+
if desccription_to_add == current_description:
|
193
|
+
return
|
194
|
+
self.descriptions.append(desccription_to_add)
|
195
|
+
|
196
|
+
def add_identifier(self, identifier_to_add: Identifier):
|
197
|
+
if identifier_to_add and isinstance(identifier_to_add,Identifier):
|
198
|
+
self.identifiers.append(identifier_to_add)
|
199
|
+
|
200
|
+
def add_note(self,note_to_add: Note):
|
201
|
+
if len(self.notes) >= self.max_note_count:
|
202
|
+
warnings.warn(f"Max not count of {self.max_note_count} reached for id: {self.id}")
|
203
|
+
return False
|
204
|
+
if note_to_add and isinstance(note_to_add,Note):
|
205
|
+
for existing in self.notes:
|
206
|
+
if note_to_add == existing:
|
207
|
+
return False
|
208
|
+
self.notes.append(note_to_add)
|
209
|
+
|
210
|
+
def add_source_reference(self, source_to_add: SourceReference):
|
211
|
+
if source_to_add and isinstance(object,SourceReference):
|
212
|
+
for current_source in self.sources:
|
213
|
+
if current_source == source_to_add:
|
214
|
+
return
|
215
|
+
self.sources.append(source_to_add)
|
216
|
+
|
217
|
+
def add_title(self, title_to_add: TextValue):
|
218
|
+
if isinstance(title_to_add,str): title_to_add = TextValue(value=title_to_add)
|
219
|
+
if title_to_add and isinstance(title_to_add, TextValue):
|
220
|
+
for current_title in self.titles:
|
221
|
+
if title_to_add == current_title:
|
222
|
+
return False
|
223
|
+
self.titles.append(title_to_add)
|
224
|
+
else:
|
225
|
+
raise ValueError(f"Cannot add title of type {type(title_to_add)}")
|
226
|
+
|
227
|
+
@property
|
228
|
+
def _as_dict_(self) -> Dict[str, Any] | None:
|
229
|
+
from .serialization import Serialization
|
230
|
+
return Serialization.serialize(self)
|
231
|
+
with hub.use(serial_log):
|
232
|
+
log.debug(f"Serializing 'SourceDescription' with id: {self.id}")
|
233
|
+
type_as_dict = {}
|
234
|
+
|
235
|
+
if self.id:
|
236
|
+
type_as_dict['id'] = self.id
|
237
|
+
if self.about:
|
238
|
+
type_as_dict['about'] = self.about._as_dict_
|
239
|
+
if self.resourceType:
|
240
|
+
if isinstance(self.resourceType,str):
|
241
|
+
log.warning(f"'SourceDescription.resourceType' should not be a string {self.resourceType}")
|
242
|
+
type_as_dict['resourceType'] = self.resourceType
|
243
|
+
else:
|
244
|
+
type_as_dict['resourceType'] = self.resourceType.value
|
245
|
+
if self.citations:
|
246
|
+
type_as_dict['citations'] = [c._as_dict_ for c in self.citations if c]
|
247
|
+
if self.mediaType:
|
248
|
+
type_as_dict['mediaType'] = self.mediaType
|
249
|
+
|
250
|
+
if self.mediator:
|
251
|
+
type_as_dict['mediator'] = self.mediator._as_dict_
|
252
|
+
if self.publisher:
|
253
|
+
type_as_dict['publisher'] = self.publisher._as_dict_ #TODO Resource this
|
254
|
+
if self.authors:
|
255
|
+
type_as_dict['authors'] = [a._as_dict_ for a in self.authors if a]
|
256
|
+
if self.sources:
|
257
|
+
type_as_dict['sources'] = [s._as_dict_ for s in self.sources if s]
|
258
|
+
|
259
|
+
|
260
|
+
if self.analysis:
|
261
|
+
type_as_dict['analysis'] = self.analysis._as_dict_
|
262
|
+
if self.componentOf:
|
263
|
+
type_as_dict['componentOf'] = self.componentOf._as_dict_
|
264
|
+
if self.titles:
|
265
|
+
type_as_dict['titles'] = [t._as_dict_ for t in self.titles if t]
|
266
|
+
if self.notes:
|
267
|
+
type_as_dict['notes'] = [n._as_dict_ for n in self.notes if n]
|
268
|
+
if self.attribution:
|
269
|
+
type_as_dict['attribution'] = self.attribution._as_dict_
|
270
|
+
if self.rights:
|
271
|
+
type_as_dict['rights'] = [r._as_dict_ for r in self.rights if r]
|
272
|
+
|
273
|
+
if self.coverage:
|
274
|
+
type_as_dict['coverage'] = [c._as_dict_ for c in self.coverage if c]
|
275
|
+
|
276
|
+
if self.descriptions:
|
277
|
+
if not (isinstance(self.descriptions, list) and all(isinstance(x, TextValue) for x in self.descriptions)):
|
278
|
+
assert False
|
279
|
+
type_as_dict['descriptions'] = [d._as_dict_ for d in self.descriptions if d]
|
280
|
+
|
281
|
+
if self.identifiers:
|
282
|
+
type_as_dict['identifiers'] = self.identifiers._as_dict_
|
283
|
+
|
284
|
+
if self.created is not None:
|
285
|
+
type_as_dict['created'] = self.created
|
286
|
+
if self.modified is not None:
|
287
|
+
type_as_dict['modified'] = self.modified
|
288
|
+
if self.published is not None:
|
289
|
+
type_as_dict['published'] = self.published
|
290
|
+
|
291
|
+
if self.repository:
|
292
|
+
type_as_dict['repository'] = self.repository._as_dict_ #TODO Resource this
|
293
|
+
|
294
|
+
log.debug(f"'SourceDescription' serialized with fields: '{type_as_dict.keys()}'")
|
295
|
+
if type_as_dict == {}: log.warning("serializing and empty 'SourceDescription'")
|
296
|
+
return type_as_dict if type_as_dict != {} else None
|
297
|
+
|
298
|
+
|
299
|
+
@classmethod
|
300
|
+
def _from_json_(cls, data: Dict[str, Any], context: Any = None) -> "SourceDescription":
|
301
|
+
if not isinstance(data, dict):
|
302
|
+
raise TypeError(f"{cls.__name__}._from_json_ expected dict, got {type(data)}")
|
303
|
+
|
304
|
+
source_description_data: Dict[str, Any] = {}
|
305
|
+
|
306
|
+
# ── Scalars ──────────────────────────────────────────────────────────────
|
307
|
+
if (id_ := data.get("id")) is not None:
|
308
|
+
source_description_data["id"] = id_
|
309
|
+
if (media_type := data.get("mediaType")) is not None:
|
310
|
+
source_description_data["mediaType"] = media_type
|
311
|
+
if (mnc := data.get("max_note_count")) is not None:
|
312
|
+
source_description_data["max_note_count"] = int(mnc)
|
313
|
+
|
314
|
+
# resourceType (enum or similar)
|
315
|
+
if (rt := data.get("resourceType")) is not None:
|
316
|
+
source_description_data["resourceType"] = ResourceType(rt)
|
317
|
+
|
318
|
+
# about: URI (accept string or dict)
|
319
|
+
if (about := data.get("about")) is not None:
|
320
|
+
source_description_data["about"] = URI.from_url(about) if isinstance(about, str) else URI._from_json_(about, context)
|
321
|
+
|
322
|
+
# mediator / publisher can be Agent or Resource
|
323
|
+
if (mediator := data.get("mediator")) is not None:
|
324
|
+
if isinstance(mediator, dict) and any(k in mediator for k in ("names", "emails", "accounts", "addresses", "person")):
|
325
|
+
source_description_data["mediator"] = Agent._from_json_(mediator, context)
|
326
|
+
else:
|
327
|
+
source_description_data["mediator"] = Resource._from_json_(mediator, context)
|
328
|
+
|
329
|
+
if (publisher := data.get("publisher")) is not None:
|
330
|
+
if isinstance(publisher, dict) and any(k in publisher for k in ("names", "emails", "accounts", "addresses", "person")):
|
331
|
+
source_description_data["publisher"] = Agent._from_json_(publisher, context)
|
332
|
+
else:
|
333
|
+
source_description_data["publisher"] = Resource._from_json_(publisher, context)
|
334
|
+
|
335
|
+
# repository (Agent)
|
336
|
+
if (repo := data.get("repository")) is not None:
|
337
|
+
source_description_data["repository"] = Agent._from_json_(repo, context)
|
338
|
+
|
339
|
+
# analysis: Document | Resource (prefer Document when possible)
|
340
|
+
if (analysis := data.get("analysis")) is not None:
|
341
|
+
try:
|
342
|
+
source_description_data["analysis"] = Document._from_json_(analysis, context)
|
343
|
+
except Exception:
|
344
|
+
source_description_data["analysis"] = Resource._from_json_(analysis, context)
|
345
|
+
|
346
|
+
# componentOf: SourceReference
|
347
|
+
if (component := data.get("componentOf")) is not None:
|
348
|
+
source_description_data["componentOf"] = SourceReference._from_json_(component, context)
|
349
|
+
|
350
|
+
# identifiers
|
351
|
+
if (identifiers := data.get("identifiers")) is not None:
|
352
|
+
source_description_data["identifiers"] = IdentifierList._from_json_(identifiers, context)
|
353
|
+
|
354
|
+
# timestamps
|
355
|
+
if (created := data.get("created")) is not None:
|
356
|
+
source_description_data["created"] = created
|
357
|
+
if (modified := data.get("modified")) is not None:
|
358
|
+
source_description_data["modified"] = modified
|
359
|
+
if (published := data.get("published")) is not None:
|
360
|
+
source_description_data["published"] = published
|
361
|
+
|
362
|
+
# ── Lists ───────────────────────────────────────────────────────────────
|
363
|
+
if (citations := data.get("citations")) is not None:
|
364
|
+
source_description_data["citations"] = [SourceCitation._from_json_(c, context) for c in citations]
|
365
|
+
|
366
|
+
if (authors := data.get("authors")) is not None:
|
367
|
+
source_description_data["authors"] = [Resource._from_json_(a, context) for a in authors]
|
368
|
+
|
369
|
+
if (sources := data.get("sources")) is not None:
|
370
|
+
source_description_data["sources"] = [SourceReference._from_json_(s, context) for s in sources]
|
371
|
+
|
372
|
+
if (titles := data.get("titles")) is not None:
|
373
|
+
source_description_data["titles"] = [
|
374
|
+
(TextValue._from_json_(t, context) if isinstance(t, dict) else TextValue(t))
|
375
|
+
for t in titles
|
376
|
+
]
|
377
|
+
|
378
|
+
if (notes := data.get("notes")) is not None:
|
379
|
+
source_description_data["notes"] = [Note._from_json_(n, context) for n in notes]
|
380
|
+
|
381
|
+
if (rights := data.get("rights")) is not None:
|
382
|
+
source_description_data["rights"] = [Resource._from_json_(r, context) for r in rights]
|
383
|
+
|
384
|
+
if (coverage := data.get("coverage")) is not None:
|
385
|
+
source_description_data["coverage"] = [Coverage._from_json_(c, context) for c in coverage]
|
386
|
+
|
387
|
+
if (descriptions := data.get("descriptions")) is not None:
|
388
|
+
for d in descriptions:
|
389
|
+
if not isinstance(d, dict): assert False
|
390
|
+
source_description_data["descriptions"] = [
|
391
|
+
(TextValue._from_json_(d, context) if isinstance(d, dict) else TextValue(value="somethings fucked up"))
|
392
|
+
for d in descriptions
|
393
|
+
]
|
394
|
+
|
395
|
+
# attribution
|
396
|
+
if (attr := data.get("attribution")) is not None:
|
397
|
+
source_description_data["attribution"] = Attribution._from_json_(attr, context) if isinstance(attr, dict) else attr
|
398
|
+
|
399
|
+
return cls(**source_description_data)
|
400
|
+
|
401
|
+
|
@@ -2,14 +2,23 @@ from __future__ import annotations
|
|
2
2
|
from typing import List, Optional, TYPE_CHECKING
|
3
3
|
|
4
4
|
if TYPE_CHECKING:
|
5
|
-
from .
|
5
|
+
from .source_description import SourceDescription
|
6
6
|
|
7
|
-
from .
|
8
|
-
from .
|
7
|
+
from .attribution import Attribution
|
8
|
+
from .qualifier import Qualifier
|
9
9
|
|
10
|
-
from .
|
10
|
+
from .resource import Resource
|
11
11
|
|
12
|
-
from .
|
12
|
+
from .uri import URI
|
13
|
+
from .logging_hub import hub, logging
|
14
|
+
"""
|
15
|
+
======================================================================
|
16
|
+
Logging
|
17
|
+
======================================================================
|
18
|
+
"""
|
19
|
+
log = logging.getLogger("gedcomx")
|
20
|
+
serial_log = "gedcomx.serialization"
|
21
|
+
#=====================================================================
|
13
22
|
|
14
23
|
from collections.abc import Sized
|
15
24
|
|
@@ -52,7 +61,7 @@ class SourceReference:
|
|
52
61
|
version = 'http://gedcomx.org/conceptual-model/v1'
|
53
62
|
|
54
63
|
def __init__(self,
|
55
|
-
description:
|
64
|
+
description: SourceDescription | URI | None = None,
|
56
65
|
descriptionId: Optional[str] = None,
|
57
66
|
attribution: Optional[Attribution] = None,
|
58
67
|
qualifiers: Optional[List[Qualifier]] = None
|
@@ -85,23 +94,49 @@ class SourceReference:
|
|
85
94
|
|
86
95
|
@property
|
87
96
|
def _as_dict_(self):
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
97
|
+
with hub.use(serial_log):
|
98
|
+
log.debug(f"Serializing 'SourceReference' with 'descriptionId': '{self.descriptionId}'")
|
99
|
+
type_as_dict = {}
|
100
|
+
if self.description is not None:
|
101
|
+
type_as_dict['description']= URI(target=self.description)._as_dict_ if self.description is not None else None
|
102
|
+
if self.descriptionId is not None:
|
103
|
+
type_as_dict['descriptionId']= self.descriptionId.replace("\n"," ").replace("\r"," ") if self.descriptionId else None
|
104
|
+
if self.attribution is not None:
|
105
|
+
type_as_dict['attribution'] = self.attribution._as_dict_ if self.attribution else None
|
106
|
+
if self.qualifiers is not None and self.qualifiers != []:
|
107
|
+
type_as_dict['qualifiers'] = [qualifier.__as_dict__ for qualifier in self.qualifiers] if (self.qualifiers and len(self.qualifiers) > 0) else None
|
108
|
+
log.debug(f"'SourceReference' serialized with fields: {type_as_dict.keys()}")
|
109
|
+
if type_as_dict == {}: log.warning("serializing and empty 'SourceReference'")
|
110
|
+
return type_as_dict if type_as_dict != {} else None
|
96
111
|
|
97
112
|
@classmethod
|
98
|
-
def _from_json_(cls, data: dict):
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
""
|
103
|
-
|
104
|
-
|
113
|
+
def _from_json_(cls, data: dict, context=None) -> "SourceReference":
|
114
|
+
ref = {}
|
115
|
+
|
116
|
+
# Scalars
|
117
|
+
if (descriptionId := data.get("descriptionId")) is not None:
|
118
|
+
ref["descriptionId"] = descriptionId
|
119
|
+
|
120
|
+
# Objects (description could be URI or SourceDescription)
|
121
|
+
if (description := data.get("description")) is not None:
|
122
|
+
# if description is just a string, assume URI
|
123
|
+
if isinstance(description, str):
|
124
|
+
ref["description"] = URI(description)
|
125
|
+
elif isinstance(description, dict):
|
126
|
+
print(">>",description)
|
127
|
+
ref["description"] = Resource._from_json_(description, context)
|
128
|
+
assert False
|
129
|
+
else:
|
130
|
+
pass #TODO
|
131
|
+
#print(ref["descriptionId"])
|
132
|
+
|
133
|
+
if (attribution := data.get("attribution")) is not None:
|
134
|
+
ref["attribution"] = Attribution._from_json_(attribution, context)
|
135
|
+
|
136
|
+
if (qualifiers := data.get("qualifiers")) is not None:
|
137
|
+
ref["qualifiers"] = [Qualifier._from_json_(q, context) for q in qualifiers]
|
138
|
+
|
139
|
+
return cls(**ref)
|
105
140
|
|
106
141
|
|
107
142
|
|
gedcomx/subject.py
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
import warnings
|
2
|
+
from typing import List, Optional
|
3
|
+
"""
|
4
|
+
======================================================================
|
5
|
+
Project: Gedcom-X
|
6
|
+
File: subject.py
|
7
|
+
Author: David J. Cartwright
|
8
|
+
Purpose:
|
9
|
+
|
10
|
+
Created: 2025-08-25
|
11
|
+
Updated:
|
12
|
+
- 2025-09-03: _from_json_ refactor
|
13
|
+
|
14
|
+
======================================================================
|
15
|
+
"""
|
16
|
+
|
17
|
+
"""
|
18
|
+
======================================================================
|
19
|
+
GEDCOM Module Types
|
20
|
+
======================================================================
|
21
|
+
"""
|
22
|
+
from .attribution import Attribution
|
23
|
+
from .conclusion import ConfidenceLevel, Conclusion
|
24
|
+
from .evidence_reference import EvidenceReference
|
25
|
+
from .Extensions.rs10.rsLink import _rsLinks
|
26
|
+
from .identifier import Identifier, IdentifierList
|
27
|
+
from .logging_hub import hub, logging
|
28
|
+
from .note import Note
|
29
|
+
from .resource import Resource
|
30
|
+
from .source_reference import SourceReference
|
31
|
+
from. uri import URI
|
32
|
+
"""
|
33
|
+
======================================================================
|
34
|
+
Logging
|
35
|
+
======================================================================
|
36
|
+
"""
|
37
|
+
log = logging.getLogger("gedcomx")
|
38
|
+
serial_log = "gedcomx.serialization"
|
39
|
+
#=====================================================================
|
40
|
+
|
41
|
+
|
42
|
+
class Subject(Conclusion):
|
43
|
+
identifier = 'http://gedcomx.org/v1/Subject'
|
44
|
+
version = 'http://gedcomx.org/conceptual-model/v1'
|
45
|
+
|
46
|
+
def __init__(self,
|
47
|
+
id: Optional[str],
|
48
|
+
lang: Optional[str] = 'en',
|
49
|
+
sources: Optional[List[SourceReference]] = [],
|
50
|
+
analysis: Optional[Resource] = None,
|
51
|
+
notes: Optional[List[Note]] = [],
|
52
|
+
confidence: Optional[ConfidenceLevel] = None,
|
53
|
+
attribution: Optional[Attribution] = None,
|
54
|
+
extracted: Optional[bool] = None,
|
55
|
+
evidence: Optional[List[EvidenceReference]] = [],
|
56
|
+
media: Optional[List[SourceReference]] = [],
|
57
|
+
identifiers: Optional[IdentifierList] = None,
|
58
|
+
uri: Optional[Resource] = None,
|
59
|
+
links: Optional[_rsLinks] = None) -> None:
|
60
|
+
super().__init__(id, lang, sources, analysis, notes, confidence, attribution,links=links)
|
61
|
+
self.extracted = extracted
|
62
|
+
self.evidence = evidence
|
63
|
+
self.media = media
|
64
|
+
self.identifiers = identifiers if identifiers else IdentifierList()
|
65
|
+
self.uri = uri
|
66
|
+
|
67
|
+
def add_identifier(self, identifier_to_add: Identifier):
|
68
|
+
if identifier_to_add and isinstance(identifier_to_add,Identifier):
|
69
|
+
for current_identifier in self.identifiers:
|
70
|
+
if identifier_to_add == current_identifier:
|
71
|
+
return
|
72
|
+
self.identifiers.append(identifier_to_add)
|
73
|
+
|
74
|
+
@property
|
75
|
+
def _as_dict_(self):
|
76
|
+
with hub.use(serial_log):
|
77
|
+
log.debug(f"Serializing 'Subject' with id: '{self.id}'")
|
78
|
+
type_as_dict = super()._as_dict_ # Start with base class fields
|
79
|
+
if type_as_dict is None: type_as_dict = {}
|
80
|
+
if self.extracted:
|
81
|
+
type_as_dict["extracted"] = self.extracted
|
82
|
+
if self.evidence:
|
83
|
+
type_as_dict["evidence"] = [evidence_ref for evidence_ref in self.evidence] if self.evidence else None
|
84
|
+
if self.media:
|
85
|
+
type_as_dict["media"] = [media for media in self.media] if self.media else None
|
86
|
+
if self.identifiers:
|
87
|
+
type_as_dict["identifiers"] = self.identifiers._as_dict_ if self.identifiers else None
|
88
|
+
log.debug(f"'Subject' serialized with fields: '{type_as_dict.keys()}'")
|
89
|
+
if type_as_dict == {} or len(type_as_dict.keys()) == 0: log.warning("serializing and empty 'Subject' Object")
|
90
|
+
|
91
|
+
return type_as_dict if type_as_dict != {} else None
|
92
|
+
|
93
|
+
|
94
|
+
@classmethod
|
95
|
+
def _dict_from_json_(cls, data: dict, context = None) -> dict:
|
96
|
+
subject_data = Conclusion._dict_from_json_(data,context)
|
97
|
+
|
98
|
+
# Bool
|
99
|
+
if (extracted := data.get("extracted")) is not None:
|
100
|
+
# cast to bool in case JSON gives "true"/"false" as string
|
101
|
+
if isinstance(extracted, str):
|
102
|
+
subject_data["extracted"] = extracted.lower() == "true"
|
103
|
+
else:
|
104
|
+
subject_data["extracted"] = bool(extracted)
|
105
|
+
|
106
|
+
# Lists
|
107
|
+
if (evidence := data.get("evidence")) is not None:
|
108
|
+
subject_data["evidence"] = [EvidenceReference._from_json_(e, context) for e in evidence]
|
109
|
+
|
110
|
+
if (media := data.get("media")) is not None:
|
111
|
+
subject_data["media"] = [SourceReference._from_json_(m, context) for m in media]
|
112
|
+
|
113
|
+
# Identifiers
|
114
|
+
if (identifiers := data.get("identifiers")) is not None:
|
115
|
+
subject_data["identifiers"] = IdentifierList._from_json_(identifiers, context)
|
116
|
+
|
117
|
+
# URI
|
118
|
+
if (uri := data.get("uri")) is not None:
|
119
|
+
subject_data["uri"] = URI(uri)
|
120
|
+
|
121
|
+
#return cls(**conclusion)
|
122
|
+
return subject_data
|