gedcom-x 0.5.1__tar.gz → 0.5.5__tar.gz

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.
Files changed (62) hide show
  1. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/PKG-INFO +1 -1
  2. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcom_x.egg-info/PKG-INFO +1 -1
  3. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcom_x.egg-info/SOURCES.txt +7 -1
  4. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcomx/Address.py +42 -11
  5. gedcom_x-0.5.5/gedcomx/Agent.py +196 -0
  6. gedcom_x-0.5.5/gedcomx/Attribution.py +64 -0
  7. gedcom_x-0.5.5/gedcomx/Conclusion.py +216 -0
  8. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcomx/Coverage.py +10 -0
  9. gedcom_x-0.5.5/gedcomx/Date.py +65 -0
  10. gedcom_x-0.5.5/gedcomx/Document.py +73 -0
  11. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcomx/Event.py +24 -5
  12. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcomx/EvidenceReference.py +2 -2
  13. gedcom_x-0.5.5/gedcomx/Exceptions.py +16 -0
  14. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcomx/Fact.py +73 -50
  15. gedcom_x-0.5.5/gedcomx/Gedcom.py +53 -0
  16. gedcom_x-0.5.5/gedcomx/Gedcom5x.py +558 -0
  17. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcomx/GedcomX.py +439 -194
  18. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcomx/Gender.py +27 -8
  19. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcomx/Group.py +3 -3
  20. gedcom_x-0.5.5/gedcomx/Identifier.py +226 -0
  21. gedcom_x-0.5.5/gedcomx/Logging.py +19 -0
  22. gedcom_x-0.5.5/gedcomx/Mutations.py +228 -0
  23. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcomx/Name.py +73 -38
  24. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcomx/Note.py +5 -4
  25. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcomx/OnlineAccount.py +2 -2
  26. gedcom_x-0.5.5/gedcomx/Person.py +192 -0
  27. gedcom_x-0.5.5/gedcomx/PlaceDescription.py +70 -0
  28. gedcom_x-0.5.5/gedcomx/PlaceReference.py +30 -0
  29. gedcom_x-0.5.5/gedcomx/Relationship.py +95 -0
  30. gedcom_x-0.5.5/gedcomx/Resource.py +75 -0
  31. gedcom_x-0.5.5/gedcomx/Serialization.py +401 -0
  32. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcomx/SourceCitation.py +6 -1
  33. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcomx/SourceDescription.py +89 -75
  34. gedcom_x-0.5.5/gedcomx/SourceReference.py +113 -0
  35. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcomx/Subject.py +12 -10
  36. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcomx/TextValue.py +2 -1
  37. gedcom_x-0.5.5/gedcomx/Translation.py +219 -0
  38. gedcom_x-0.5.5/gedcomx/URI.py +105 -0
  39. gedcom_x-0.5.5/gedcomx/Zip.py +1 -0
  40. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcomx/__init__.py +11 -3
  41. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/pyproject.toml +1 -1
  42. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/setup.py +1 -1
  43. gedcom_x-0.5.1/gedcomx/Agent.py +0 -83
  44. gedcom_x-0.5.1/gedcomx/Attribution.py +0 -116
  45. gedcom_x-0.5.1/gedcomx/Conclusion.py +0 -137
  46. gedcom_x-0.5.1/gedcomx/Date.py +0 -29
  47. gedcom_x-0.5.1/gedcomx/Document.py +0 -42
  48. gedcom_x-0.5.1/gedcomx/Gedcom.py +0 -346
  49. gedcom_x-0.5.1/gedcomx/Identifier.py +0 -89
  50. gedcom_x-0.5.1/gedcomx/Person.py +0 -178
  51. gedcom_x-0.5.1/gedcomx/PlaceDescription.py +0 -47
  52. gedcom_x-0.5.1/gedcomx/PlaceReference.py +0 -31
  53. gedcom_x-0.5.1/gedcomx/Relationship.py +0 -116
  54. gedcom_x-0.5.1/gedcomx/Serialization.py +0 -37
  55. gedcom_x-0.5.1/gedcomx/SourceReference.py +0 -168
  56. gedcom_x-0.5.1/gedcomx/URI.py +0 -70
  57. gedcom_x-0.5.1/gedcomx/_Resource.py +0 -11
  58. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcom_x.egg-info/dependency_links.txt +0 -0
  59. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcom_x.egg-info/top_level.txt +0 -0
  60. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcomx/Qualifier.py +0 -0
  61. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/gedcomx/TopLevelTypeCollection.py +0 -0
  62. {gedcom_x-0.5.1 → gedcom_x-0.5.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gedcom-x
3
- Version: 0.5.1
3
+ Version: 0.5.5
4
4
  Summary: Python implimentation of gedcom-x standard
5
5
  Author-email: "David J. Cartwright" <davidcartwright@hotmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gedcom-x
3
- Version: 0.5.1
3
+ Version: 0.5.5
4
4
  Summary: Python implimentation of gedcom-x standard
5
5
  Author-email: "David J. Cartwright" <davidcartwright@hotmail.com>
6
6
  License: MIT
@@ -13,12 +13,16 @@ gedcomx/Date.py
13
13
  gedcomx/Document.py
14
14
  gedcomx/Event.py
15
15
  gedcomx/EvidenceReference.py
16
+ gedcomx/Exceptions.py
16
17
  gedcomx/Fact.py
17
18
  gedcomx/Gedcom.py
19
+ gedcomx/Gedcom5x.py
18
20
  gedcomx/GedcomX.py
19
21
  gedcomx/Gender.py
20
22
  gedcomx/Group.py
21
23
  gedcomx/Identifier.py
24
+ gedcomx/Logging.py
25
+ gedcomx/Mutations.py
22
26
  gedcomx/Name.py
23
27
  gedcomx/Note.py
24
28
  gedcomx/OnlineAccount.py
@@ -27,6 +31,7 @@ gedcomx/PlaceDescription.py
27
31
  gedcomx/PlaceReference.py
28
32
  gedcomx/Qualifier.py
29
33
  gedcomx/Relationship.py
34
+ gedcomx/Resource.py
30
35
  gedcomx/Serialization.py
31
36
  gedcomx/SourceCitation.py
32
37
  gedcomx/SourceDescription.py
@@ -34,6 +39,7 @@ gedcomx/SourceReference.py
34
39
  gedcomx/Subject.py
35
40
  gedcomx/TextValue.py
36
41
  gedcomx/TopLevelTypeCollection.py
42
+ gedcomx/Translation.py
37
43
  gedcomx/URI.py
38
- gedcomx/_Resource.py
44
+ gedcomx/Zip.py
39
45
  gedcomx/__init__.py
@@ -1,7 +1,28 @@
1
1
  from typing import Optional
2
2
  import json
3
3
 
4
+ from .Serialization import Serialization
5
+
4
6
  class Address:
7
+ """A GedcomX Address Data Type
8
+ A GedcomX Address Data Type.
9
+
10
+ Represents a postal address according to the GedcomX conceptual model.
11
+
12
+ Args:
13
+ value (str, optional): A complete address as a single string.
14
+ city (str, optional): Name of the city or town.
15
+ country (str, optional): Name of the country.
16
+ postalCode (str, optional): Postal or ZIP code.
17
+ stateOrProvince (str, optional): Name of the state, province, or region.
18
+ street (str, optional): First street address line.
19
+ street2 (str, optional): Second street address line.
20
+ street3 (str, optional): Third street address line.
21
+ street4 (str, optional): Fourth street address line.
22
+ street5 (str, optional): Fifth street address line.
23
+ street6 (str, optional): Sixth street address line.
24
+ """
25
+
5
26
  identifier = "http://gedcomx.org/v1/Address"
6
27
  version = 'http://gedcomx.org/conceptual-model/v1'
7
28
 
@@ -30,17 +51,26 @@ class Address:
30
51
  self.street6 = street6
31
52
 
32
53
  @property
33
- def value(self) -> Optional[str]:
34
- if self._value:
35
- return self._value
54
+ def value(self) -> str:
36
55
  return ', '.join(filter(None, [
37
- self._street, self._street2, self._street3,
38
- self._street4, self._street5, self._street6,
39
- self._city, self._stateOrProvince,
40
- self._postalCode, self._country
56
+ self.street, self.street2, self.street3,
57
+ self.street4, self.street5, self.street6,
58
+ self.city, self.stateOrProvince,
59
+ self.postalCode, self.country
41
60
  ]))
42
61
 
43
-
62
+ @value.setter
63
+ def value(self,value: str):
64
+ self._value = value
65
+ return
66
+ raise NotImplementedError("Parsing of a full address is not implimented.")
67
+
68
+ def _append(self,value):
69
+ if self._value:
70
+ self._value = self._value + ' ' + value
71
+ else:
72
+ self._value = value
73
+
44
74
  def __eq__(self, other):
45
75
  if not isinstance(other, self.__class__):
46
76
  return False
@@ -82,9 +112,8 @@ class Address:
82
112
  return ', '.join(filtered_parts)
83
113
 
84
114
  @property
85
- def __as_dict__(self):
86
- return {
87
- #"value": self._value if self._value else None,
115
+ def _as_dict_(self):
116
+ type_as_dict = {
88
117
  "city": self.city if self.city else None,
89
118
  "country": self.country if self.country else None,
90
119
  "postalCode": self.postalCode if self.postalCode else None,
@@ -97,4 +126,6 @@ class Address:
97
126
  "street6": self.street6 if self.street6 else None
98
127
  }
99
128
 
129
+ return Serialization.serialize_dict(type_as_dict)
130
+
100
131
 
@@ -0,0 +1,196 @@
1
+ import base64
2
+ import uuid
3
+
4
+ from typing import List, Optional
5
+
6
+ from .Address import Address
7
+ #from .Attribution import Attribution
8
+ from .Identifier import Identifier, IdentifierList
9
+
10
+ from .OnlineAccount import OnlineAccount
11
+ from .TextValue import TextValue
12
+ from .Resource import Resource
13
+ from .URI import URI
14
+ from .Serialization import Serialization
15
+
16
+
17
+ class Agent:
18
+ """A GedcomX Agent Data Type.
19
+
20
+ Represents an agent entity such as a person, organization, or software
21
+ responsible for creating or modifying genealogical data, as defined in
22
+ the GedcomX conceptual model.
23
+
24
+ Static Methods:
25
+ default_id_generator(): Generates a short, URL-safe Base64-encoded UUID
26
+ for use as a default agent identifier.
27
+
28
+ Args:
29
+ id (str, optional): A unique identifier for this agent. If not provided,
30
+ one may be generated automatically using `default_id_generator()`.
31
+ identifiers (IdentifierList, optional): A list of alternate identifiers for this agent.
32
+ names (List[TextValue], optional): Names associated with the agent. Defaults to an empty list.
33
+ homepage (URI, optional): A link to the agent's homepage or primary website.
34
+ openid (URI, optional): The OpenID identifier for the agent.
35
+ accounts (List[OnlineAccount], optional): Online accounts associated with the agent.
36
+ Defaults to an empty list.
37
+ emails (List[URI], optional): Email addresses associated with the agent.
38
+ Defaults to an empty list.
39
+ phones (List[Resource], optional): Phone numbers associated with the agent.
40
+ Defaults to an empty list.
41
+ addresses (List[Address], optional): Postal addresses associated with the agent.
42
+ Defaults to an empty list.
43
+ person (Person, optional): A reference to the person represented
44
+ by the agent. Accepts a `Person` object or a `Resource` reference.
45
+ Declared as `object` to avoid circular imports.
46
+ attribution (Attribution, optional): Attribution information related to the agent.
47
+ uri (Resource, optional): A URI reference for this agent.
48
+ """
49
+
50
+ @staticmethod
51
+ def default_id_generator():
52
+ # Generate a standard UUID
53
+ standard_uuid = uuid.uuid4()
54
+ # Convert UUID to bytes
55
+ uuid_bytes = standard_uuid.bytes
56
+ # Encode bytes to a Base64 string
57
+ short_uuid = base64.urlsafe_b64encode(uuid_bytes).rstrip(b'=').decode('utf-8')
58
+ return short_uuid
59
+
60
+ def __init__(self, id: Optional[str] = None,
61
+ identifiers: Optional[IdentifierList] = None,
62
+ names: Optional[List[TextValue]] = [],
63
+ homepage: Optional[URI] = None,
64
+ openid: Optional[URI] = None,
65
+ accounts: Optional[List[OnlineAccount]] = [],
66
+ emails: Optional[List[URI]] = [],
67
+ phones: Optional[List[URI]] = [],
68
+ addresses: Optional[List[Address]] = [],
69
+ person: Optional[object] | Optional[Resource] = None, # should be of Type 'Person', 'object' to avoid circular imports
70
+ #xnotes: Optional[List[Note]] = None,
71
+ attribution: Optional[object] = None, # Added for compatibility with GEDCOM5/7 Imports
72
+ uri: Optional[URI | Resource] = None):
73
+
74
+ self._id_generator = Agent.default_id_generator
75
+
76
+ self.id = id if id else None #TODO self._id_generator()
77
+ self.identifiers = identifiers or IdentifierList()
78
+ self.names = names if names else []
79
+ self.homepage = homepage or None
80
+ self.openid = openid or None
81
+ self.accounts = accounts or []
82
+ self.emails = emails or []
83
+ self.phones = phones or []
84
+ self.addresses = addresses if addresses else []
85
+ self.xnotes = []
86
+ self.attribution = attribution or None
87
+ self.uri = URI(fragment=self.id)
88
+
89
+ def _append_to_name(self, text_to_append: str):
90
+ if self.names and self.names[0] and self.names[0].value:
91
+ self.names[0].value = self.names[0].value + text_to_append
92
+ elif self.names and self.names[0]:
93
+ self.names[0].value = text_to_append
94
+ else:
95
+ raise ValueError() #TODO
96
+
97
+ def add_address(self, address_to_add: Address):
98
+ if address_to_add and isinstance(address_to_add, Address):
99
+ for current_address in self.addresses:
100
+ if address_to_add == current_address:
101
+ return False
102
+ self.addresses.append(address_to_add)
103
+ else:
104
+ raise ValueError(f"address must be of type Address, not {type(address_to_add)}")
105
+
106
+ def add_name(self, name_to_add: TextValue):
107
+ if isinstance(name_to_add,str): name_to_add = TextValue(value=name_to_add)
108
+ if name_to_add and isinstance(name_to_add,TextValue):
109
+ for current_name in self.names:
110
+ if name_to_add == current_name:
111
+ return
112
+ self.names.append(name_to_add)
113
+ else:
114
+ raise ValueError(f'name must be of type str or TextValue, recived {type(name_to_add)}')
115
+
116
+ def add_note(self, note_to_add: 'Note'):
117
+ from .Note import Note
118
+ if note_to_add and isinstance(note_to_add,Note):
119
+ self.xnotes.append(note_to_add)
120
+ else:
121
+ raise ValueError(f'note must be of type Note, recived {type(note_to_add)}')
122
+
123
+ def add_identifier(self, identifier_to_add: Identifier):
124
+ self.identifiers.append(identifier_to_add)
125
+
126
+ @property
127
+ def _as_dict_(self):
128
+ type_as_dict = {
129
+ "id": self.id if self.id else None,
130
+ "identifiers": self.identifiers._as_dict_ if self.identifiers else None,
131
+ "names": [name._as_dict_ for name in self.names],
132
+ "homepage": self.homepage if self.homepage else None,
133
+ "openid": self.openid if self.openid else None,
134
+ "accounts": self.accounts if self.accounts else None,
135
+ "emails": self.emails if self.emails else None,
136
+ "phones": self.phones if self.phones else None,
137
+ "addresses": [address._as_dict_ for address in self.addresses],
138
+ "notes": [note._as_dict_ for note in self.xnotes]
139
+ }
140
+ return Serialization.serialize_dict(type_as_dict)
141
+
142
+ @classmethod
143
+ def _from_json_(cls, data: dict):
144
+ """
145
+ Create a Person instance from a JSON-dict (already parsed).
146
+ """
147
+ type_as_dict = Serialization.get_class_fields('Agent')
148
+ return Serialization.deserialize(data, type_as_dict)
149
+
150
+ def __str__(self):
151
+ """
152
+ Return a human-readable string representation of the Agent.
153
+
154
+ Returns:
155
+ str: A concise description including ID, primary name (if any), and type of agent.
156
+ """
157
+ primary_name = self.names[0].value if self.names else "Unnamed Agent"
158
+ homepage_str = f", homepage={self.homepage}" if self.homepage else ""
159
+ return f"Agent(id={self.id}, name='{primary_name}'{homepage_str})"
160
+
161
+ def __eq__(self, other):
162
+ """
163
+ Determine equality between two Agent instances.
164
+
165
+ Args:
166
+ other (Agent): The other object to compare against.
167
+
168
+ Returns:
169
+ bool: True if both objects represent the same agent, False otherwise.
170
+ """
171
+ '''
172
+ if not isinstance(other, Agent):
173
+ return NotImplemented
174
+
175
+ return (
176
+ self.id == other.id and
177
+ self.identifiers == other.identifiers and
178
+ self.names == other.names and
179
+ self.homepage == other.homepage and
180
+ self.openid == other.openid and
181
+ self.accounts == other.accounts and
182
+ self.emails == other.emails and
183
+ self.phones == other.phones and
184
+ self.addresses == other.addresses and
185
+ self.person == other.person and
186
+ self.attribution == other.attribution and
187
+ self.uri == other.uri
188
+ )
189
+ '''
190
+
191
+ self_names = {n.value for n in self.names if hasattr(n, "value")}
192
+ other_names = {n.value for n in other.names if hasattr(n, "value")}
193
+ if self_names & other_names: # intersection not empty
194
+ return True
195
+
196
+ return False
@@ -0,0 +1,64 @@
1
+
2
+ from datetime import datetime
3
+ from typing import Optional, Dict, Any
4
+
5
+ from .Agent import Agent
6
+ from .Resource import Resource, get_resource_as_dict
7
+ from .Serialization import Serialization
8
+
9
+ class Attribution:
10
+ """Attribution Information for a Genealogy, Conclusion, Subject and child classes
11
+
12
+ Args:
13
+ contributor (Agent, optional): Contributor to object being attributed.
14
+ modified (timestamp, optional): timestamp for when this record was modified.
15
+ changeMessage (str, optional): Birth date (YYYY-MM-DD).
16
+ creator (Agent, optional): Creator of object being attributed.
17
+ created (timestamp, optional): timestamp for when this record was created
18
+
19
+ Raises:
20
+
21
+ """
22
+ identifier = 'http://gedcomx.org/v1/Attribution'
23
+ version = 'http://gedcomx.org/conceptual-model/v1'
24
+
25
+ def __init__(self,contributor: Optional[Agent | Resource] = None,
26
+ modified: Optional[datetime] = None,
27
+ changeMessage: Optional[str] = None,
28
+ creator: Optional[Agent | Resource] = None,
29
+ created: Optional[datetime] = None) -> None:
30
+
31
+ self.contributor = contributor
32
+ self.modified = modified
33
+ self.changeMessage = changeMessage
34
+ self.creator = creator
35
+ self.created = created
36
+
37
+ @property
38
+ def _as_dict_(self) -> Dict[str, Any]:
39
+ """
40
+ Serialize Attribution to a JSON-ready dict, skipping None values.
41
+ """
42
+ type_as_dict: Dict[str, Any] = {}
43
+ type_as_dict['contributor'] = get_resource_as_dict(self.contributor)
44
+ type_as_dict['modified'] = self.modified if self.modified else None
45
+ type_as_dict['changeMessage'] = self.changeMessage if self.changeMessage else None
46
+ type_as_dict['creator'] = get_resource_as_dict(self.creator)
47
+ type_as_dict['created'] = self.created if self.created else None
48
+
49
+ return Serialization.serialize_dict(type_as_dict)
50
+
51
+ @classmethod
52
+ def _from_json_(cls, data: Dict[str, Any]) -> 'Attribution':
53
+ """
54
+ Construct Attribution from a dict (as parsed from JSON).
55
+ Handles 'created' and 'modified' as ISO strings or epoch ms ints.
56
+ """
57
+ # contributor
58
+
59
+ """
60
+ Create a Person instance from a JSON-dict (already parsed).
61
+ """
62
+ from .Serialization import Serialization
63
+
64
+ return Serialization.deserialize(data, Attribution)
@@ -0,0 +1,216 @@
1
+ import base64
2
+ import uuid
3
+ import warnings
4
+
5
+ from typing import List, Optional
6
+
7
+ from .Attribution import Attribution
8
+ #from .Document import Document
9
+ from .Note import Note
10
+ from .Qualifier import Qualifier
11
+ from .Serialization import Serialization
12
+ from .SourceReference import SourceReference
13
+ from .Resource import Resource, URI
14
+ from .Extensions.rs10.rsLink import _rsLinkList, rsLink
15
+
16
+ from collections.abc import Sized
17
+
18
+ class ConfidenceLevel(Qualifier):
19
+ High = "http://gedcomx.org/High"
20
+ Medium = "http://gedcomx.org/Medium"
21
+ Low = "http://gedcomx.org/Low"
22
+
23
+ _NAME_TO_URI = {
24
+ "high": High,
25
+ "medium": Medium,
26
+ "low": Low,
27
+ }
28
+
29
+ @classmethod
30
+ def _from_json_(cls, data):
31
+ """
32
+ Accepts:
33
+ - "High" | "Medium" | "Low"
34
+ - "http://gedcomx.org/High" | ".../Medium" | ".../Low"
35
+ - {"type": "..."} or {"value": "..."} or {"confidence": "..."} or {"level": "..."} or {"uri": "..."}
36
+ - existing ConfidenceLevel instance
37
+ Returns:
38
+ ConfidenceLevel instance with .value set to the canonical URI.
39
+ """
40
+ if data is None:
41
+ return None
42
+
43
+ if isinstance(data, cls):
44
+ return data
45
+
46
+ # Extract token from dicts or use the raw scalar
47
+ if isinstance(data, dict):
48
+ token = (
49
+ data.get("confidence")
50
+ or data.get("type")
51
+ or data.get("value")
52
+ or data.get("level")
53
+ or data.get("uri")
54
+ )
55
+ else:
56
+ token = data
57
+
58
+ if token is None:
59
+ return None
60
+
61
+ token_str = str(token).strip()
62
+
63
+ # Normalize to canonical URI
64
+ if token_str.lower() in cls._NAME_TO_URI:
65
+ uri = cls._NAME_TO_URI[token_str.lower()]
66
+ elif token_str in (cls.High, cls.Medium, cls.Low):
67
+ uri = token_str
68
+ else:
69
+ raise ValueError(f"Unknown ConfidenceLevel: {token!r}")
70
+
71
+ # Create a ConfidenceLevel instance without invoking Qualifier.__init__
72
+ obj = cls.__new__(cls)
73
+ # store the canonical URI on the instance; used by description and (optionally) serialization
74
+ obj.value = uri
75
+ return obj
76
+
77
+ @property
78
+ def description(self):
79
+ descriptions = {
80
+ self.High: "The contributor has a high degree of confidence that the assertion is true.",
81
+ self.Medium: "The contributor has a medium degree of confidence that the assertion is true.",
82
+ self.Low: "The contributor has a low degree of confidence that the assertion is true."
83
+ }
84
+ # Works whether the instance holds .value or (edge-case) if `self` is compared directly
85
+ key = getattr(self, "value", self)
86
+ return descriptions.get(key, "No description available.")
87
+
88
+
89
+ class Conclusion:
90
+ """
91
+ Represents a conclusion in the GEDCOM X conceptual model. A conclusion is a
92
+ genealogical assertion about a person, relationship, or event, derived from
93
+ one or more sources, with optional supporting metadata such as confidence,
94
+ attribution, and notes.
95
+
96
+ Args:
97
+ id (str, optional): A unique identifier for the conclusion. If not provided,
98
+ a UUID-based identifier will be automatically generated.
99
+ lang (str, optional): The language code of the conclusion. Defaults to 'en'.
100
+ sources (list[SourceReference], optional): A list of source references that
101
+ support the conclusion.
102
+ analysis (Document | Resource, optional): A reference to an analysis document
103
+ or resource that supports the conclusion.
104
+ notes (list[Note], optional): A list of notes providing additional context.
105
+ Defaults to an empty list.
106
+ confidence (ConfidenceLevel, optional): The contributor's confidence in the
107
+ conclusion (High, Medium, or Low).
108
+ attribution (Attribution, optional): Information about who contributed the
109
+ conclusion and when.
110
+ uri (Resource, optional): A URI reference for the conclusion. Defaults to a
111
+ URI with the fragment set to the `id`.
112
+ links (_LinkList, optional): A list of links associated with the conclusion.
113
+ Defaults to an empty `_LinkList`.
114
+ """
115
+ identifier = 'http://gedcomx.org/v1/Conclusion'
116
+ version = 'http://gedcomx.org/conceptual-model/v1'
117
+
118
+ @staticmethod
119
+ def default_id_generator():
120
+ # Generate a standard UUID
121
+ standard_uuid = uuid.uuid4()
122
+ # Convert UUID to bytes
123
+ uuid_bytes = standard_uuid.bytes
124
+ # Encode bytes to a Base64 string
125
+ short_uuid = base64.urlsafe_b64encode(uuid_bytes).rstrip(b'=').decode('utf-8')
126
+ return short_uuid
127
+
128
+ def __init__(self,
129
+ id: Optional[str] = None,
130
+ lang: Optional[str] = 'en',
131
+ sources: Optional[List[SourceReference]] = None,
132
+ analysis: Optional[object | Resource] = None,
133
+ notes: Optional[List[Note]] = None,
134
+ confidence: Optional[ConfidenceLevel] = None,
135
+ attribution: Optional[Attribution] = None,
136
+ uri: Optional[Resource] = None,
137
+ _max_note_count: int = 20,
138
+ links: Optional[_rsLinkList] = None) -> None:
139
+
140
+ self._id_generator = Conclusion.default_id_generator
141
+
142
+ self.id = id if id else None
143
+ self.lang = lang
144
+ self.sources = sources if sources else []
145
+ self.analysis = analysis
146
+ self.notes = notes if notes else []
147
+ self.confidence = confidence
148
+ self.attribution = attribution
149
+ self.max_note_count = _max_note_count
150
+ self.uri = uri if uri else URI(fragment=id if id else self.id)
151
+ self.links = links if links else _rsLinkList() #NOTE This is not in specification, following FS format
152
+
153
+ def add_note(self,note_to_add: Note):
154
+ if self.notes and len(self.notes) >= self.max_note_count:
155
+ warnings.warn(f"Max not count of {self.max_note_count} reached for id: {self.id}")
156
+ return False
157
+ if note_to_add and isinstance(note_to_add,Note):
158
+ for existing in self.notes:
159
+ if note_to_add == existing:
160
+ return False
161
+ self.notes.append(note_to_add)
162
+
163
+ def add_source(self, source_to_add: SourceReference):
164
+ if source_to_add and isinstance(source_to_add,SourceReference):
165
+ for current_source in self.sources:
166
+ if source_to_add == current_source:
167
+ return
168
+ self.sources.append(source_to_add)
169
+ else:
170
+ raise ValueError()
171
+
172
+ def add_link(self,link: rsLink) -> bool:
173
+ """
174
+ Adds a link to the Conclusion link list.
175
+
176
+ Args:
177
+ link (rsLink): The link to be added.
178
+
179
+ Returns:
180
+ bool: The return value. True for success, False otherwise.
181
+
182
+ Note: Duplicate checking not impimented at this level
183
+ """
184
+ if link and isinstance(link,rsLink):
185
+ self.links.add(link)
186
+ return True
187
+ return False
188
+
189
+ @property
190
+ def _as_dict_(self):
191
+ type_as_dict = {
192
+ 'id':self.id,
193
+ 'lang':self.lang,
194
+ 'sources': [source._as_dict_ for source in self.sources] if self.sources else None,
195
+ 'analysis': self.analysis if self.analysis else None,
196
+ 'notes': [note for note in self.notes] if self.notes else None,
197
+ 'confidence':self.confidence,
198
+ 'attribution':self.attribution,
199
+ 'links':self.links._as_dict_ if self.links else None
200
+ }
201
+
202
+ return Serialization.serialize_dict(type_as_dict)
203
+
204
+ def __eq__(self, other):
205
+ if not isinstance(other, self.__class__):
206
+ return False
207
+
208
+ return (
209
+ self.id == other.id and
210
+ self.lang == other.lang and
211
+ self.sources == other.sources and
212
+ self.analysis == other.analysis and
213
+ self.notes == other.notes and
214
+ self.confidence == other.confidence and
215
+ self.attribution == other.attribution
216
+ )
@@ -2,6 +2,7 @@ from typing import Optional
2
2
 
3
3
  from .Date import Date
4
4
  from .PlaceReference import PlaceReference
5
+ from .Serialization import Serialization
5
6
 
6
7
  class Coverage:
7
8
  identifier = 'http://gedcomx.org/v1/Coverage'
@@ -13,6 +14,15 @@ class Coverage:
13
14
 
14
15
  # ...existing code...
15
16
 
17
+ @property
18
+ def _as_dict_(self):
19
+ type_as_dict = {
20
+ 'spatial': self.spatial if self.spatial else None,
21
+ 'temporal': self. temporal if self.temporal else None
22
+ }
23
+
24
+ return Serialization.serialize_dict(type_as_dict)
25
+
16
26
  @classmethod
17
27
  def _from_json_(cls, data: dict):
18
28
  """