gedcom-x 0.5.8__tar.gz → 0.5.10__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 (75) hide show
  1. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/PKG-INFO +1 -1
  2. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcom_x.egg-info/PKG-INFO +1 -1
  3. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcom_x.egg-info/SOURCES.txt +5 -1
  4. gedcom_x-0.5.10/gedcomx/Extensions/rs10/rsLink.py +166 -0
  5. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/__init__.py +4 -1
  6. gedcom_x-0.5.10/gedcomx/address.py +217 -0
  7. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/agent.py +81 -24
  8. gedcom_x-0.5.10/gedcomx/attribution.py +115 -0
  9. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/conclusion.py +98 -46
  10. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/converter.py +209 -79
  11. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/coverage.py +10 -1
  12. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/date.py +42 -8
  13. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/document.py +37 -7
  14. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/event.py +77 -20
  15. gedcom_x-0.5.10/gedcomx/evidence_reference.py +20 -0
  16. gedcom_x-0.5.10/gedcomx/extensible.py +86 -0
  17. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/fact.py +53 -54
  18. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/gedcom.py +10 -0
  19. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/gedcom5x.py +30 -20
  20. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/gedcom7/GedcomStructure.py +1 -3
  21. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/gedcom7/__init__.py +2 -2
  22. gedcom_x-0.5.8/gedcomx/gedcom7/Gedcom7.py → gedcom_x-0.5.10/gedcomx/gedcom7/gedcom7.py +3 -3
  23. gedcom_x-0.5.10/gedcomx/gedcom7/specification.py +4817 -0
  24. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/gedcomx.py +95 -93
  25. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/gender.py +21 -9
  26. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/group.py +9 -0
  27. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/identifier.py +47 -20
  28. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/logging_hub.py +19 -0
  29. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/mutations.py +10 -5
  30. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/name.py +74 -33
  31. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/note.py +50 -18
  32. gedcom_x-0.5.10/gedcomx/online_account.py +19 -0
  33. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/person.py +46 -27
  34. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/place_description.py +54 -8
  35. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/place_reference.py +30 -8
  36. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/qualifier.py +19 -3
  37. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/relationship.py +55 -14
  38. gedcom_x-0.5.10/gedcomx/resource.py +112 -0
  39. gedcom_x-0.5.10/gedcomx/schemas.py +328 -0
  40. gedcom_x-0.5.10/gedcomx/serialization.py +794 -0
  41. gedcom_x-0.5.10/gedcomx/source_citation.py +37 -0
  42. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/source_description.py +181 -94
  43. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/source_reference.py +51 -16
  44. gedcom_x-0.5.10/gedcomx/subject.py +122 -0
  45. gedcom_x-0.5.10/gedcomx/textvalue.py +89 -0
  46. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/translation.py +3 -3
  47. gedcom_x-0.5.10/gedcomx/uri.py +273 -0
  48. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/pyproject.toml +1 -1
  49. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/setup.py +1 -1
  50. gedcom_x-0.5.8/gedcomx/Extensions/rs10/rsLink.py +0 -116
  51. gedcom_x-0.5.8/gedcomx/address.py +0 -131
  52. gedcom_x-0.5.8/gedcomx/attribution.py +0 -91
  53. gedcom_x-0.5.8/gedcomx/evidence_reference.py +0 -11
  54. gedcom_x-0.5.8/gedcomx/gedcom7/Specification.py +0 -347
  55. gedcom_x-0.5.8/gedcomx/online_account.py +0 -10
  56. gedcom_x-0.5.8/gedcomx/resource.py +0 -85
  57. gedcom_x-0.5.8/gedcomx/serialization.py +0 -815
  58. gedcom_x-0.5.8/gedcomx/source_citation.py +0 -25
  59. gedcom_x-0.5.8/gedcomx/subject.py +0 -77
  60. gedcom_x-0.5.8/gedcomx/textvalue.py +0 -35
  61. gedcom_x-0.5.8/gedcomx/uri.py +0 -121
  62. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/README.md +0 -0
  63. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcom_x.egg-info/dependency_links.txt +0 -0
  64. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcom_x.egg-info/top_level.txt +0 -0
  65. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/Extensions/__init__.py +0 -0
  66. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/Extensions/rs10/__init__.py +0 -0
  67. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/Logging.py +0 -0
  68. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/TopLevelTypeCollection.py +0 -0
  69. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/Zip.py +0 -0
  70. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/exceptions.py +0 -0
  71. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/extensible_enum.py +0 -0
  72. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/gedcom7/Exceptions.py +0 -0
  73. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/gedcom7/g7interop.py +0 -0
  74. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/gedcomx/gedcom7/logger.py +0 -0
  75. {gedcom_x-0.5.8 → gedcom_x-0.5.10}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gedcom-x
3
- Version: 0.5.8
3
+ Version: 0.5.10
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.8
3
+ Version: 0.5.10
4
4
  Summary: Python implimentation of gedcom-x standard
5
5
  Author-email: "David J. Cartwright" <davidcartwright@hotmail.com>
6
6
  License: MIT
@@ -49,6 +49,7 @@ gedcomx/document.py
49
49
  gedcomx/event.py
50
50
  gedcomx/evidence_reference.py
51
51
  gedcomx/exceptions.py
52
+ gedcomx/extensible.py
52
53
  gedcomx/extensible_enum.py
53
54
  gedcomx/fact.py
54
55
  gedcomx/gedcom.py
@@ -68,6 +69,7 @@ gedcomx/place_reference.py
68
69
  gedcomx/qualifier.py
69
70
  gedcomx/relationship.py
70
71
  gedcomx/resource.py
72
+ gedcomx/schemas.py
71
73
  gedcomx/serialization.py
72
74
  gedcomx/source_citation.py
73
75
  gedcomx/source_description.py
@@ -85,4 +87,6 @@ gedcomx/gedcom7/GedcomStructure.py
85
87
  gedcomx/gedcom7/Specification.py
86
88
  gedcomx/gedcom7/__init__.py
87
89
  gedcomx/gedcom7/g7interop.py
88
- gedcomx/gedcom7/logger.py
90
+ gedcomx/gedcom7/gedcom7.py
91
+ gedcomx/gedcom7/logger.py
92
+ gedcomx/gedcom7/specification.py
@@ -0,0 +1,166 @@
1
+ from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple, Union
2
+ """
3
+ ======================================================================
4
+ Project: Gedcom-X
5
+ File: rsLink.py
6
+ Author: David J. Cartwright
7
+ Purpose: Link type of GedcomX RS (Extension)
8
+
9
+ Created: 2025-08-25
10
+ Updated:
11
+ -
12
+
13
+ ======================================================================
14
+ """
15
+
16
+ """
17
+ ======================================================================
18
+ GEDCOM Module Types
19
+ ======================================================================
20
+ """
21
+ from ...exceptions import GedcomClassAttributeError
22
+ from ...logging_hub import hub, logging
23
+ from ...uri import URI
24
+ """
25
+ ======================================================================
26
+ Logging
27
+ ======================================================================
28
+ """
29
+ log = logging.getLogger("gedcomx")
30
+ serial_log = "gedcomx.serialization"
31
+ deserial_log = "gedcomx.deserialization"
32
+ #=====================================================================
33
+
34
+
35
+ class rsLink():
36
+ """A link description object. RS Extension to GedcomX by FamilySearch.
37
+
38
+ Args:
39
+ rel (str): Link relation identifier. Required.
40
+ href (str, optional): Link target URI. If omitted, provide `template`.
41
+ template (str, optional): URI Template (see RFC 6570). If omitted, provide `href`.
42
+ type (str, optional): Media type(s) of the linked resource (RFC 2616 §3.7).
43
+ accept (str, optional): Acceptable media type(s) for updating the linked resource (RFC 2616 §3.7).
44
+ allow (str, optional): Allowable HTTP methods to transition to the linked resource (RFC 2616 §14.7).
45
+ hreflang (str, optional): Language of the linked resource (e.g., BCP-47 tag).
46
+ title (str, optional): Human-readable label for the link.
47
+
48
+ Raises:
49
+ ValueError: If neither `href` nor `template` is provided.
50
+ """
51
+
52
+ """Attribution Information for a Genealogy, Conclusion, Subject and child classes
53
+
54
+ Args:
55
+ contributor (Agent, optional): Contributor to object being attributed.
56
+ modified (timestamp, optional): timestamp for when this record was modified.
57
+ changeMessage (str, optional): Birth date (YYYY-MM-DD).
58
+ creator (Agent, optional): Creator of object being attributed.
59
+ created (timestamp, optional): timestamp for when this record was created
60
+
61
+ Raises:
62
+
63
+ """
64
+ identifier = "http://gedcomx.org/v1/Link"
65
+
66
+ def __init__(self,
67
+ href: Optional[URI] = None,
68
+ template: Optional[str] = None,
69
+ type: Optional[str] = None,
70
+ accept: Optional[str] = None,
71
+ allow: Optional[str] = None,
72
+ hreflang: Optional[str] = None,
73
+ title: Optional[str] = None) -> None:
74
+
75
+
76
+ self.href = href if isinstance(href,URI) else URI.from_url(href) if isinstance(href,str) else None
77
+ self.template = template
78
+ self.type = type
79
+ self.accept = accept
80
+ self.allow = allow
81
+ self.hreflang = hreflang
82
+ self.title = title
83
+
84
+ if self.href is None: # and self.template is None:
85
+ raise GedcomClassAttributeError("href or template are required")
86
+
87
+ def __str__(self) -> str:
88
+ def to_text(v):
89
+ if v is None:
90
+ return None
91
+ # unwrap URI-like objects
92
+ if isinstance(v, URI):
93
+ return getattr(v, "value", None) or str(v)
94
+ # normalize strings (skip empty/whitespace-only)
95
+ if isinstance(v, str):
96
+ s = v.strip()
97
+ return s or None
98
+ return str(v)
99
+
100
+ parts = []
101
+
102
+ # show href as the primary bit if present
103
+ href_s = to_text(self.href)
104
+ if href_s:
105
+ parts.append(href_s)
106
+
107
+ # show other fields as key=value
108
+ for name in ("template", "type", "accept", "allow", "hreflang", "title"):
109
+ val = to_text(getattr(self, name, None))
110
+ if val:
111
+ parts.append(f"{name}={val}")
112
+
113
+ return " | ".join(parts) if parts else self.__class__.__name__
114
+
115
+
116
+
117
+ @classmethod
118
+ def _from_json_(cls, data: Any, context: Any = None) -> "rsLink":
119
+ """
120
+ Build an rsLink from JSON.
121
+
122
+ Accepted shapes:
123
+ - {"rel": "self", "href": "https://..."}
124
+ - {"rel": {...}, "href": {...}} # URI objects as dicts
125
+ - {"href": "https://...", "type": "...", ...} # rel optional
126
+ - {"uri": "https://..."} or {"url": "..."} # href aliases
127
+ - "https://example.com" # shorthand -> href only
128
+
129
+ Note:
130
+ - `rel` is coerced to a URI if possible.
131
+ - `href` is coerced to a URI (string/dict supported).
132
+ - If both `href` and `template` are missing, __init__ will raise.
133
+ """
134
+ # Shorthand: bare string is an href
135
+
136
+ if not isinstance(data, dict):
137
+ raise TypeError(f"{cls.__name__}._from_json_ expected dict or str, got {type(data)}")
138
+
139
+ print("LINK DATA:",data)
140
+ # Extract with common aliases
141
+ rel = data.get("rel")
142
+ href = data.get("href")
143
+
144
+
145
+
146
+ return cls(
147
+ rel=rel,
148
+ href=href,
149
+ template=data.get("template"),
150
+ type=data.get("type"),
151
+ accept=data.get("accept"),
152
+ allow=data.get("allow"),
153
+ hreflang=data.get("hreflang"),
154
+ title=data.get("title"),
155
+ )
156
+
157
+
158
+ class _rsLinks():
159
+
160
+ def __init__(self,
161
+ person: rsLink | None = None,
162
+ portrait: rsLink | None= None
163
+ ) -> None:
164
+
165
+ self.person = person
166
+ self.portrait = portrait
@@ -1,3 +1,4 @@
1
+ from .schemas import SCHEMA
1
2
  from .agent import Agent
2
3
  from .address import Address
3
4
  from .attribution import Attribution
@@ -12,6 +13,8 @@ from .extensible_enum import ExtensibleEnum
12
13
  from .event import Event
13
14
  from .event import EventType
14
15
  from .event import EventRole
16
+ from .extensible import Extensible
17
+ from. extensible import _ExtraField
15
18
  from .fact import Fact
16
19
  from .fact import FactQualifier
17
20
  from .fact import FactType
@@ -43,7 +46,7 @@ from .uri import URI
43
46
 
44
47
  from .Extensions.rs10.rsLink import rsLink
45
48
 
46
- from .gedcom7.Gedcom7 import Gedcom7, GedcomStructure
49
+ from .gedcom7.gedcom7 import Gedcom7, GedcomStructure
47
50
  from .translation import g7toXtable
48
51
 
49
52
 
@@ -0,0 +1,217 @@
1
+ from typing import Any, Dict, Optional, List
2
+
3
+ """
4
+ ======================================================================
5
+ Project: Gedcom-X
6
+ File: address.py
7
+ Author: David J. Cartwright
8
+ Purpose:
9
+
10
+ Created: 2025-08-25
11
+ Updated:
12
+ - 2025-09-03: _from_json_ refactoring
13
+
14
+ ======================================================================
15
+ """
16
+
17
+ """
18
+ ======================================================================
19
+ GEDCOM Module Types
20
+ ======================================================================
21
+ """
22
+ from .logging_hub import hub, logging
23
+ """
24
+ ======================================================================
25
+ Logging
26
+ ======================================================================
27
+ """
28
+ log = logging.getLogger("gedcomx")
29
+ serial_log = "gedcomx.serialization"
30
+ #=====================================================================
31
+
32
+ class Address:
33
+ """A GedcomX Address Data Type
34
+ A GedcomX Address Data Type.
35
+
36
+ Represents a postal address according to the GedcomX conceptual model.
37
+
38
+ Args:
39
+ value (str, optional): A complete address as a single string.
40
+ city (str, optional): Name of the city or town.
41
+ country (str, optional): Name of the country.
42
+ postalCode (str, optional): Postal or ZIP code.
43
+ stateOrProvince (str, optional): Name of the state, province, or region.
44
+ street (str, optional): First street address line.
45
+ street2 (str, optional): Second street address line.
46
+ street3 (str, optional): Third street address line.
47
+ street4 (str, optional): Fourth street address line.
48
+ street5 (str, optional): Fifth street address line.
49
+ street6 (str, optional): Sixth street address line.
50
+ """
51
+
52
+ identifier = "http://gedcomx.org/v1/Address"
53
+ version = 'http://gedcomx.org/conceptual-model/v1'
54
+
55
+ def __init__(self, value: Optional[str] = None,
56
+ city: Optional[str] = None,
57
+ country: Optional[str] = None,
58
+ postalCode: Optional[str] = None,
59
+ stateOrProvince: Optional[str] = None,
60
+ street: Optional[str] = None,
61
+ street2: Optional[str] = None,
62
+ street3: Optional[str] = None,
63
+ street4: Optional[str] = None,
64
+ street5: Optional[str] = None,
65
+ street6: Optional[str] = None):
66
+
67
+ self._value = value #TODO impliment a parser for date strings.
68
+ self.city = city
69
+ self.country = country
70
+ self.postalCode = postalCode
71
+ self.stateOrProvince = stateOrProvince
72
+ self.street = street
73
+ self.street2 = street2
74
+ self.street3 = street3
75
+ self.street4 = street4
76
+ self.street5 = street5
77
+ self.street6 = street6
78
+
79
+ @property
80
+ def value(self) -> str:
81
+ return ', '.join(filter(None, [
82
+ self.street, self.street2, self.street3,
83
+ self.street4, self.street5, self.street6,
84
+ self.city, self.stateOrProvince,
85
+ self.postalCode, self.country
86
+ ]))
87
+
88
+ @value.setter
89
+ def value(self,value: str):
90
+ self._value = value
91
+ return
92
+ raise NotImplementedError("Parsing of a full address is not implimented.")
93
+
94
+ def _append(self,value):
95
+ if self._value:
96
+ self._value = self._value + ' ' + value
97
+ else:
98
+ self._value = value
99
+
100
+ def __eq__(self, other):
101
+ if not isinstance(other, self.__class__):
102
+ return False
103
+
104
+ return (
105
+ self.value == other.value and
106
+ self.city == other.city and
107
+ self.country == other.country and
108
+ self.postalCode == other.postalCode and
109
+ self.stateOrProvince == other.stateOrProvince and
110
+ self.street == other.street and
111
+ self.street2 == other.street2 and
112
+ self.street3 == other.street3 and
113
+ self.street4 == other.street4 and
114
+ self.street5 == other.street5 and
115
+ self.street6 == other.street6
116
+ )
117
+
118
+ def __str__(self) -> str:
119
+ # Combine non-empty address components into a formatted string
120
+ parts = [
121
+ self._value,
122
+ self.street,
123
+ self.street2,
124
+ self.street3,
125
+ self.street4,
126
+ self.street5,
127
+ self.street6,
128
+ self.city,
129
+ self.stateOrProvince,
130
+ self.postalCode,
131
+ self.country
132
+ ]
133
+
134
+ # Filter out any parts that are None or empty strings
135
+ filtered_parts = [str(part) for part in parts if part]
136
+
137
+ # Join the remaining parts with a comma and space
138
+ return ', '.join(filtered_parts)
139
+
140
+ @property
141
+ def _as_dict_(self):
142
+ with hub.use(serial_log):
143
+ log.debug(f"Serializing 'Address' with value: '{self.value}'")
144
+ type_as_dict = {}
145
+ if self.city: type_as_dict["city"] = self.city
146
+ if self.country: type_as_dict["country"] = self.country
147
+ if self.postalCode: type_as_dict["postalCode"] = self.postalCode
148
+ if self.stateOrProvince: type_as_dict["stateOrProvince"] = self.stateOrProvince
149
+ if self.street: type_as_dict["street"] = self.street
150
+ if self.street2: type_as_dict["street2"] = self.street2
151
+ if self.street3: type_as_dict["street3"] = self.street3
152
+ if self.street4: type_as_dict["street4"] = self.street4
153
+ if self.street5: type_as_dict["street5"] = self.street5
154
+ if self.street6: type_as_dict["street6"] = self.street6
155
+ log.debug(f"'Address' serialized with fields: '{type_as_dict.keys()}'")
156
+ if type_as_dict == {} or len(type_as_dict.keys()) == 0: log.warning("serializing and empty 'Address' Object")
157
+
158
+ return type_as_dict if type_as_dict != {} else None
159
+
160
+
161
+ @classmethod
162
+ def _from_json_(cls, data: Any, context: Any = None) -> "Address":
163
+ """
164
+ Build an Address from JSON.
165
+ Supports:
166
+ - Shorthand string -> value
167
+ - Aliases: postal_code/postal -> postalCode; state/province -> stateOrProvince
168
+ - Line aliases: line1..line6 / address1..address6 / addr1..addr6 -> street..street6
169
+ - 'lines': [..] list -> street..street6
170
+ """
171
+ if data is None: return None
172
+
173
+ if not isinstance(data, dict):
174
+ raise TypeError(f"{cls.__name__}._from_json_ expected dict or str, got {type(data)}")
175
+
176
+ address_data: Dict[str, Any] = {}
177
+
178
+ # Freeform value (accept a few aliases)
179
+ if (v := data.get("value")) is None:
180
+ address_data["value"] = str(v)
181
+
182
+ # Simple scalars
183
+ if (city := data.get("city")) is not None:
184
+ address_data["city"] = city
185
+ if (country := data.get("country")) is not None:
186
+ address_data["country"] = country
187
+
188
+ # Postal code (aliases)
189
+ if (postal := data.get("postalCode")) is not None:
190
+ address_data["postalCode"] = postal
191
+
192
+ # State / Province (aliases)
193
+ if (stateprov := data.get("stateOrProvince")) is not None:
194
+ address_data["stateOrProvince"] = stateprov
195
+
196
+ if data.get("street") is not None:
197
+ address_data["street"] = data["street"]
198
+
199
+ if data.get("street2") is not None:
200
+ address_data["street2"] = data["street2"]
201
+
202
+ if data.get("street3") is not None:
203
+ address_data["street3"] = data["street3"]
204
+
205
+ if data.get("street4") is not None:
206
+ address_data["street4"] = data["street4"]
207
+
208
+ if data.get("street5") is not None:
209
+ address_data["street5"] = data["street5"]
210
+
211
+ if data.get("street6") is not None:
212
+ address_data["street6"] = data["street6"]
213
+
214
+
215
+ return cls(**address_data)
216
+
217
+
@@ -1,7 +1,7 @@
1
1
  import base64
2
2
  import uuid
3
3
 
4
- from typing import List, Optional
4
+ from typing import Any, Dict, List, Optional
5
5
  """
6
6
  ======================================================================
7
7
  Project: Gedcom-X
@@ -11,7 +11,7 @@ from typing import List, Optional
11
11
 
12
12
  Created: 2025-08-25
13
13
  Updated:
14
- -
14
+ - 2025-09-03: _from_json_ refactor
15
15
 
16
16
  ======================================================================
17
17
  """
@@ -22,11 +22,20 @@ GEDCOM Module Type Imports
22
22
  ======================================================================
23
23
  """
24
24
  from .address import Address
25
- from .identifier import Identifier, IdentifierList
25
+ from .identifier import Identifier, IdentifierList, make_uid
26
26
  from .online_account import OnlineAccount
27
27
  from .resource import Resource
28
28
  from .textvalue import TextValue
29
29
  from .uri import URI
30
+ from .logging_hub import hub, logging
31
+ """
32
+ ======================================================================
33
+ Logging
34
+ ======================================================================
35
+ """
36
+ log = logging.getLogger("gedcomx")
37
+ serial_log = "gedcomx.serialization"
38
+ #=====================================================================
30
39
 
31
40
 
32
41
  class Agent:
@@ -61,17 +70,7 @@ class Agent:
61
70
  attribution (Attribution, optional): Attribution information related to the agent.
62
71
  uri (Resource, optional): A URI reference for this agent.
63
72
  """
64
-
65
- @staticmethod
66
- def default_id_generator():
67
- # Generate a standard UUID
68
- standard_uuid = uuid.uuid4()
69
- # Convert UUID to bytes
70
- uuid_bytes = standard_uuid.bytes
71
- # Encode bytes to a Base64 string
72
- short_uuid = base64.urlsafe_b64encode(uuid_bytes).rstrip(b'=').decode('utf-8')
73
- return short_uuid
74
-
73
+
75
74
  def __init__(self, id: Optional[str] = None,
76
75
  identifiers: Optional[IdentifierList] = None,
77
76
  names: Optional[List[TextValue]] = [],
@@ -86,9 +85,8 @@ class Agent:
86
85
  attribution: Optional[object] = None, # Added for compatibility with GEDCOM5/7 Imports
87
86
  uri: Optional[URI | Resource] = None):
88
87
 
89
- self._id_generator = Agent.default_id_generator
90
-
91
- self.id = id if id else None #TODO self._id_generator()
88
+
89
+ self.id = id if id else make_uid()
92
90
  self.identifiers = identifiers or IdentifierList()
93
91
  self.names = names if names else []
94
92
  self.homepage = homepage or None
@@ -164,16 +162,75 @@ class Agent:
164
162
  type_as_dict["addresses"] = [address._as_dict_ for address in self.addresses if address]
165
163
  if self.xnotes:
166
164
  type_as_dict["notes"] = [note._as_dict_() for note in self.xnotes if note]
165
+ return type_as_dict if type_as_dict != {} else None
167
166
  return Serialization.serialize_dict(type_as_dict)
168
167
 
169
168
  @classmethod
170
- def _from_json_(cls, data: dict):
171
- """
172
- Create a Person instance from a JSON-dict (already parsed).
173
- """
174
- from .serialization import Serialization
175
- type_as_dict = Serialization.get_class_fields('Agent')
176
- return Serialization.deserialize(type_as_dict,Agent)
169
+ def _from_json_(cls, data: Any, context: Any = None) -> "Agent":
170
+
171
+ if not isinstance(data, dict):
172
+ raise TypeError(f"{cls.__name__}._from_json_ expected dict or str, got {type(data)}")
173
+
174
+ agent_data: Dict[str, Any] = {}
175
+
176
+ # ── Scalars ──────────────────────────────────────────────────────────────
177
+ if (id_ := data.get("id")) is not None:
178
+ agent_data["id"] = id_
179
+
180
+ # ── Objects ─────────────────────────────────────────────────────────────
181
+ if (identifiers := data.get("identifiers")) is not None:
182
+ agent_data["identifiers"] = IdentifierList._from_json_(identifiers, context)
183
+
184
+ # homepage / openid / uri: accept string or dict
185
+ if (homepage := data.get("homepage")) is not None:
186
+ agent_data["homepage"] = URI.from_url_(homepage) if isinstance(homepage, str) else URI._from_json_(homepage, context)
187
+
188
+ if (openid := data.get("openid")) is not None:
189
+ agent_data["openid"] = URI.from_url_(openid) if isinstance(openid, str) else URI._from_json_(openid, context)
190
+
191
+ if (uri := data.get("uri")) is not None:
192
+ if isinstance(uri, str):
193
+ agent_data["uri"] = URI.from_url_(uri)
194
+ else: raise ValueError()
195
+
196
+ # person can be a full Person object or a Resource/URI reference
197
+ if (person := data.get("person")) is not None:
198
+ if isinstance(person, dict):
199
+ agent_data["person"] = Resource._from_json_(person, context)
200
+ else:
201
+ raise ValueError()
202
+
203
+ # attribution (GEDCOM5/7 compatibility): try Attribution if shape matches; otherwise store as-is
204
+ if (attr := data.get("attribution")) is not None:
205
+ """
206
+ ======================================================================
207
+ GEDCOM Module Type Imports
208
+ ======================================================================
209
+ """
210
+ from .attribution import Attribution
211
+ #======================================================================
212
+ if isinstance(attr, dict) and any(k in attr for k in ("contributor", "created", "modified")):
213
+ agent_data["attribution"] = Attribution._from_json_(attr, context)
214
+ else:
215
+ raise ValueError()
216
+
217
+ # ── Lists ───────────────────────────────────────────────────────────────
218
+ if (names := data.get("names")) is not None:
219
+ agent_data["names"] = [TextValue._from_json_(n, context) if isinstance(n, (dict,)) else TextValue(n) for n in names]
220
+
221
+ if (accounts := data.get("accounts")) is not None:
222
+ agent_data["accounts"] = [OnlineAccount._from_json_(a, context) for a in accounts]
223
+
224
+ if (emails := data.get("emails")) is not None:
225
+ agent_data["emails"] = [URI.from_url_(e) if isinstance(e, str) else URI._from_json_(e, context) for e in emails]
226
+
227
+ if (phones := data.get("phones")) is not None:
228
+ agent_data["phones"] = [URI.from_url_(p) if isinstance(p, str) else URI._from_json_(p, context) for p in phones]
229
+
230
+ if (addresses := data.get("addresses")) is not None:
231
+ agent_data["addresses"] = [Address._from_json_(a, context) for a in addresses]
232
+
233
+ return cls(**agent_data)
177
234
 
178
235
  def __str__(self):
179
236
  """