gedcom-x 0.5.8__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.
Files changed (45) hide show
  1. {gedcom_x-0.5.8.dist-info → gedcom_x-0.5.9.dist-info}/METADATA +1 -1
  2. gedcom_x-0.5.9.dist-info/RECORD +56 -0
  3. gedcomx/Extensions/rs10/rsLink.py +109 -59
  4. gedcomx/__init__.py +1 -1
  5. gedcomx/address.py +102 -16
  6. gedcomx/agent.py +81 -24
  7. gedcomx/attribution.py +52 -28
  8. gedcomx/conclusion.py +97 -45
  9. gedcomx/converter.py +209 -79
  10. gedcomx/coverage.py +10 -1
  11. gedcomx/date.py +42 -8
  12. gedcomx/document.py +37 -7
  13. gedcomx/event.py +77 -20
  14. gedcomx/evidence_reference.py +9 -0
  15. gedcomx/fact.py +53 -54
  16. gedcomx/gedcom.py +10 -0
  17. gedcomx/gedcom5x.py +30 -20
  18. gedcomx/gedcom7/__init__.py +1 -1
  19. gedcomx/gedcomx.py +95 -93
  20. gedcomx/gender.py +21 -9
  21. gedcomx/group.py +9 -0
  22. gedcomx/identifier.py +47 -20
  23. gedcomx/logging_hub.py +19 -0
  24. gedcomx/mutations.py +10 -5
  25. gedcomx/name.py +74 -33
  26. gedcomx/note.py +50 -18
  27. gedcomx/online_account.py +9 -0
  28. gedcomx/person.py +44 -26
  29. gedcomx/place_description.py +54 -8
  30. gedcomx/place_reference.py +30 -8
  31. gedcomx/qualifier.py +19 -3
  32. gedcomx/relationship.py +55 -14
  33. gedcomx/resource.py +45 -18
  34. gedcomx/serialization.py +400 -421
  35. gedcomx/source_citation.py +16 -4
  36. gedcomx/source_description.py +181 -94
  37. gedcomx/source_reference.py +51 -16
  38. gedcomx/subject.py +59 -14
  39. gedcomx/textvalue.py +66 -12
  40. gedcomx/translation.py +3 -3
  41. gedcomx/uri.py +155 -3
  42. gedcom_x-0.5.8.dist-info/RECORD +0 -56
  43. {gedcom_x-0.5.8.dist-info → gedcom_x-0.5.9.dist-info}/WHEEL +0 -0
  44. {gedcom_x-0.5.8.dist-info → gedcom_x-0.5.9.dist-info}/top_level.txt +0 -0
  45. /gedcomx/gedcom7/{Gedcom7.py → gedcom7.py} +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.9
4
4
  Summary: Python implimentation of gedcom-x standard
5
5
  Author-email: "David J. Cartwright" <davidcartwright@hotmail.com>
6
6
  License: MIT
@@ -0,0 +1,56 @@
1
+ gedcomx/Logging.py,sha256=vBDOjawVXc4tCge1laYjy6_2Ves-fnGzG0m6NnLZejE,624
2
+ gedcomx/TopLevelTypeCollection.py,sha256=p99i-O5LXiXe3GlC6jWuz4nH1TAcKxOLbe0VRxWbSFY,1495
3
+ gedcomx/Zip.py,sha256=lBxcv-Vip45884EHj56wZJJ5I36Q38UuHUidDxQBoS8,14
4
+ gedcomx/__init__.py,sha256=8qEa_oYCC1nMyeMt0zNmsqXnocfA2q6VNUlp0cCnMLI,1723
5
+ gedcomx/address.py,sha256=7tGUPRItlSiUZ6-WaDNj4sbjvU8W_f2kw-I8s_nntTU,8049
6
+ gedcomx/agent.py,sha256=HxDRB8axhReUmTDtn0XFNemG6znjJQezi-qxO2JRfCg,12803
7
+ gedcomx/attribution.py,sha256=S648H7it_780WLA0c4dkBSrjeXIMz_YVMk1eJ6wZk6Q,4586
8
+ gedcomx/conclusion.py,sha256=BpLFZY0p02yGa3H9Fis2eZz8NYBi7YsiLbaX6N7ZICM,11167
9
+ gedcomx/converter.py,sha256=_VzdkjS52AVXrtKUAzgI6VGDaw7Wysa76B1Vt1XnNEs,57637
10
+ gedcomx/coverage.py,sha256=XmBiTGxuH9WoeXumrgu03EMWtccHe982hFJu2Coxceo,2206
11
+ gedcomx/date.py,sha256=xiAatY25DXTW8usmhbyx_6rmEae3FJnmR5FriGkCN80,3560
12
+ gedcomx/document.py,sha256=quaa1N1XcqhMdpTwCzpvnZfRgygK1mRhFPWyX09ZrWw,4555
13
+ gedcomx/event.py,sha256=7Iogi3eVmR4_-qg6seIo7orAmVS1ENvUQcJO3_tZGlc,15417
14
+ gedcomx/evidence_reference.py,sha256=vtP0rYpehfuu4Jugf1V9u5V_wgPu-2EPb7mqPLS_9cI,696
15
+ gedcomx/exceptions.py,sha256=0OdPM3euhBMgX8o61ZwPuKeN8zPuSuuDcSBFflVGFqk,587
16
+ gedcomx/extensible_enum.py,sha256=DftCZLMBNul3C9hwh-rf0GE3SVdvylvyd5mt7bX_l6o,6535
17
+ gedcomx/fact.py,sha256=bEAUKvLpn7ZAJjyOE57dcvOnvkdzZ63GqF3yb2nGhX0,24972
18
+ gedcomx/gedcom.py,sha256=A8GQ670E6_knfP37kOGHdVl_cC9RXQoc4Dk6D_qlnuQ,2059
19
+ gedcomx/gedcom5x.py,sha256=ajjLeTlncrSgw3JSBRo8Ip9aZik2NHnUt1iDhjc9-mE,23645
20
+ gedcomx/gedcomx.py,sha256=PunMwjHIE4fp7O7sjuNrI8p44_4Y3pI43FQSCh7pyqE,19329
21
+ gedcomx/gender.py,sha256=d2AEvTSa28sfQ1910X-Ysfi7MNDRMQ35EA7jcBYsLck,3178
22
+ gedcomx/group.py,sha256=I4vdBBsdMdqAMigkzGg4mRxrCAjvsJJ-bIa_aR6CDhA,2879
23
+ gedcomx/identifier.py,sha256=hCLfBuHVTVwa2VGD_2NFsiftFkqS3-7W8XMnyjmPZbM,9603
24
+ gedcomx/logging_hub.py,sha256=DbYGwMdVD_nEj0xpVzHjsMVTi_WjttLLRexiebezfzM,8307
25
+ gedcomx/mutations.py,sha256=ugQgfQh0o9r8FIB_dz5s6KqPyIDMHn2YSP7DvAHsDv0,7236
26
+ gedcomx/name.py,sha256=5UpNgFPBfTfzC5yLQDqmeSJ-Fg1T6Nio9vEa6f5kA2Y,19501
27
+ gedcomx/note.py,sha256=joDgKrDS9pbOEEve9LGopP2Gz48kFKqMeHYR72Z7bA0,3813
28
+ gedcomx/online_account.py,sha256=EYMW-CepfwppaJu5RiPQOK63YKxnjE8bVOPUNQqbrMM,639
29
+ gedcomx/person.py,sha256=QALVxY2NlZ93BU0QR9Q4Ykm4kZ-7j4j4QTVXo4hpO60,8993
30
+ gedcomx/place_description.py,sha256=vFCVW37tKBosYCGttkXppR7yDXA-L8YVLpixSN-8b4E,7564
31
+ gedcomx/place_reference.py,sha256=XE-tyRsvfXVK--vdiA_Pvj0ZrUecl72JldbV-c-k_PU,2939
32
+ gedcomx/qualifier.py,sha256=Lg5D_CRLKxaUHcCGLzXhKmpMkmdOeQso0V-XN3yH2n4,2352
33
+ gedcomx/relationship.py,sha256=_wA1NujEqQO0HzSfjlfLcl-yCFWBcOsRyxs3klr0HfE,5852
34
+ gedcomx/resource.py,sha256=ShK3sQOaNI4ZlrIUcepL7_uLd_QX4zMx-A1SU2In_m4,3608
35
+ gedcomx/serialization.py,sha256=Pg_tDO5wUXDgyMqVGPwAlm2WdBBNUA0L-TBaRSWhaNo,30308
36
+ gedcomx/source_citation.py,sha256=QSh1zHK9DsgjQu6oHPDTCReLVs2HP8YDR-NlLAG-pek,1237
37
+ gedcomx/source_description.py,sha256=A2R1T26HYqpk8Q4W8BYp7zlvHYqycHkUMX94uQRpwV8,19258
38
+ gedcomx/source_reference.py,sha256=iakI-vLMXbSMuZLDU2c4J5s-XDanOCUJvV2Mz8uRP_8,6895
39
+ gedcomx/subject.py,sha256=dgStcWiI3a3VX2QEgUIQNbo8HELD2sFUhYT-VI46ND8,5117
40
+ gedcomx/textvalue.py,sha256=Y-SDRpHR2gGpWIyaOvTYrM5EbfOUeX7Tnh_7iOITAbk,3135
41
+ gedcomx/translation.py,sha256=4REL68WaWqcC5f3XcBCZ1dn0gtaZftQJTCkvl1Sf-NA,61395
42
+ gedcomx/uri.py,sha256=ceCcbbrx99QekivhQ_xfMvkmaUKiW_IF21WKTOFBtJg,9693
43
+ gedcomx/Extensions/__init__.py,sha256=MQzi_whzlxiLiknUNh10hG8OVrNqJE38l6n-AwCssx8,24
44
+ gedcomx/Extensions/rs10/__init__.py,sha256=nSHoZiD8hsCAyE-KyRTuWSLqSJSFh12kSz7hqilAMps,26
45
+ gedcomx/Extensions/rs10/rsLink.py,sha256=IdmcG1pVicxX91ZMu9fUPwKYoXCl0Q60inGwiThDAVE,6004
46
+ gedcomx/gedcom7/Exceptions.py,sha256=xeKr4x8b7r8pOqJ9yMpsCVTyxPeOlREDGgKoM5rX4U0,149
47
+ gedcomx/gedcom7/GedcomStructure.py,sha256=ZKNoEcXc41KdrCLPx-A8ohOU9VYmXAkFc4xuZExsBPw,3435
48
+ gedcomx/gedcom7/Specification.py,sha256=qIBe9wzL1GB0l0NyetS1ncbhz5C44b9nMyjAxHuqMt8,9245
49
+ gedcomx/gedcom7/__init__.py,sha256=tPHWW9kchPFEZ5sfURWPPzK8ZBimsQn7SRDULAW1Yc4,679
50
+ gedcomx/gedcom7/g7interop.py,sha256=hSzwqeok2n7xziEvN2QiJY7bVCWrOnZIZWXubnkrv7w,9945
51
+ gedcomx/gedcom7/gedcom7.py,sha256=i_g9W0qsZQYLMoD2sBCA13ibKRYFnSf4uj9-ix_tE4Q,5614
52
+ gedcomx/gedcom7/logger.py,sha256=QM1SySyh91UEhs90d2DMhH-s9qGF8XS8I8gr1eOcmfw,617
53
+ gedcom_x-0.5.9.dist-info/METADATA,sha256=Wn8DcPMkZvmTZRrnNlo4GzUlwRYGSRbKJABN7oVYTaw,4332
54
+ gedcom_x-0.5.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
55
+ gedcom_x-0.5.9.dist-info/top_level.txt,sha256=smVBF4nxSU-mzCd6idtRYTbYjPICMMi8pTqewEmqF8Y,8
56
+ gedcom_x-0.5.9.dist-info/RECORD,,
@@ -1,19 +1,35 @@
1
- '''
2
- The "Link" Data Type
3
- The Link data type defines a representation of an available transition from one application state to another.
4
- The base definition of a link is provided by RFC 5988.
5
-
6
- Instances of Link can be reasonably expected as extension elements to any GEDCOM X data type,
7
- except data types that are defined by the GEDCOM X Conceptual Model to explicitly restrict extension properties.
8
- '''
9
-
10
- '''
11
- 8/25/25, 0.5.5, built serialization, type checking in init for href
12
- '''
13
-
14
- from typing import List, Optional
15
- from ...uri import URI
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
+ """
16
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
+ #=====================================================================
17
33
 
18
34
 
19
35
  class rsLink():
@@ -47,7 +63,7 @@ class rsLink():
47
63
  """
48
64
  identifier = "http://gedcomx.org/v1/Link"
49
65
 
50
- def __init__(self,rel: Optional[URI] = None,
66
+ def __init__(self,
51
67
  href: Optional[URI] = None,
52
68
  template: Optional[str] = None,
53
69
  type: Optional[str] = None,
@@ -56,7 +72,7 @@ class rsLink():
56
72
  hreflang: Optional[str] = None,
57
73
  title: Optional[str] = None) -> None:
58
74
 
59
- self.rel = rel
75
+
60
76
  self.href = href if isinstance(href,URI) else URI.from_url(href) if isinstance(href,str) else None
61
77
  self.template = template
62
78
  self.type = type
@@ -65,52 +81,86 @@ class rsLink():
65
81
  self.hreflang = hreflang
66
82
  self.title = title
67
83
 
68
- if self.href is None and self.template is None:
84
+ if self.href is None: # and self.template is None:
69
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
+
70
115
 
71
- @property
72
- def _as_dict_(self):
73
- from ...serialization import Serialization
74
- type_as_dict = {
75
- "rel": self.rel._as_dict_ if self.rel else None,
76
- "href": self.href._as_dict_ if self.href else None,
77
- "template": self.template, # RFC 6570 template if used
78
- "type": self.type, # media type (note: shadows built-in 'type')
79
- "accept": self.accept,
80
- "allow": self.allow,
81
- "hreflang": self.hreflang,
82
- "title": self.title,
83
- }
84
- return Serialization.serialize_dict(type_as_dict)
85
116
 
86
- @property
87
- def json(self):
88
- import json
89
- return json.dumps(self._as_dict_)
90
-
91
- class _rsLinkList():
92
- def __init__(self) -> None:
93
- self.links = {}
94
-
95
- def add(self,link: rsLink):
96
- if link and isinstance(link,rsLink):
97
- if link.rel in self.links.keys():
98
- self.links[link.rel].append(link.href)
99
- else:
100
- self.links[link.rel] = [link.href]
101
-
102
-
103
117
  @classmethod
104
- def _from_json_(cls,data: dict):
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
105
135
 
106
- link_list = _rsLinkList()
107
- for rel in data.keys():
108
- link_list.add(rsLink(rel,data[rel]))
109
- return link_list
110
-
111
- @property
112
- def _as_dict_(self) -> dict:
113
- return self.links
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
+
114
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
+ )
115
156
 
116
-
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
gedcomx/__init__.py CHANGED
@@ -43,7 +43,7 @@ from .uri import URI
43
43
 
44
44
  from .Extensions.rs10.rsLink import rsLink
45
45
 
46
- from .gedcom7.Gedcom7 import Gedcom7, GedcomStructure
46
+ from .gedcom7.gedcom7 import Gedcom7, GedcomStructure
47
47
  from .translation import g7toXtable
48
48
 
49
49
 
gedcomx/address.py CHANGED
@@ -1,7 +1,33 @@
1
- from typing import Optional
2
- import json
1
+ from typing import Any, Dict, Optional, List
3
2
 
3
+ """
4
+ ======================================================================
5
+ Project: Gedcom-X
6
+ File: address.py
7
+ Author: David J. Cartwright
8
+ Purpose:
4
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
+ #=====================================================================
5
31
 
6
32
  class Address:
7
33
  """A GedcomX Address Data Type
@@ -113,19 +139,79 @@ class Address:
113
139
 
114
140
  @property
115
141
  def _as_dict_(self):
116
- from .serialization import Serialization
117
- type_as_dict = {}
118
- if self.city: type_as_dict["city"] = self.city
119
- if self.country: type_as_dict["country"] = self.country
120
- if self.postalCode: type_as_dict["postalCode"] = self.postalCode
121
- if self.stateOrProvince: type_as_dict["stateOrProvince"] = self.stateOrProvince
122
- if self.street: type_as_dict["street"] = self.street
123
- if self.street2: type_as_dict["street2"] = self.street2
124
- if self.street3: type_as_dict["street3"] = self.street3
125
- if self.street4: type_as_dict["street4"] = self.street4
126
- if self.street5: type_as_dict["street5"] = self.street5
127
- if self.street6: type_as_dict["street6"] = self.street6
128
-
129
- return Serialization.serialize_dict(type_as_dict)
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)
130
216
 
131
217
 
gedcomx/agent.py CHANGED
@@ -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
  """
gedcomx/attribution.py CHANGED
@@ -12,6 +12,8 @@ from typing import Optional, Dict, Any
12
12
  Created: 2025-08-25
13
13
  Updated:
14
14
  - 2025-08-31: fixed _as_dict_ to deal with Resources and ignore empty fields
15
+ - 2025-09-03: _from_json_ refactor
16
+
15
17
 
16
18
  ======================================================================
17
19
  """
@@ -21,9 +23,16 @@ from typing import Optional, Dict, Any
21
23
  GEDCOM Module Types
22
24
  ======================================================================
23
25
  """
24
-
25
26
  from .agent import Agent
26
27
  from .resource import Resource
28
+ from .logging_hub import hub, logging
29
+ """
30
+ ======================================================================
31
+ Logging
32
+ ======================================================================
33
+ """
34
+ log = logging.getLogger("gedcomx")
35
+ serial_log = "gedcomx.serialization"
27
36
  #=====================================================================
28
37
 
29
38
 
@@ -56,36 +65,51 @@ class Attribution:
56
65
  self.created = created
57
66
 
58
67
  @property
59
- def _as_dict_(self) -> Dict[str, Any]:
68
+ def _as_dict_(self) -> Dict[str, Any] | None:
60
69
  """
61
70
  Serialize Attribution to a JSON-ready dict, skipping None values.
62
71
  """
63
- from Serialization import Serialization
64
- type_as_dict: Dict[str, Any] = {}
65
- if self.contributor:
66
- type_as_dict['contributor'] = Resource(target=self.contributor)._as_dict_
67
- if self.modified:
68
- type_as_dict['modified'] = self.modified if self.modified else None
69
- if self.changeMessage:
70
- type_as_dict['changeMessage'] = self.changeMessage if self.changeMessage else None
71
- if self.creator:
72
- type_as_dict['creator'] = Resource(target=self.creator)._as_dict_
73
- if self.created:
74
- type_as_dict['created'] = self.created if self.created else None
72
+ with hub.use(serial_log):
73
+ log.debug(f"Serializing 'Attribution'")
74
+ type_as_dict: Dict[str, Any] = {}
75
+ if self.contributor:
76
+ type_as_dict['contributor'] = Resource(target=self.contributor)._as_dict_
77
+ if self.modified:
78
+ type_as_dict['modified'] = self.modified if self.modified else None
79
+ if self.changeMessage:
80
+ type_as_dict['changeMessage'] = self.changeMessage if self.changeMessage else None
81
+ if self.creator:
82
+ type_as_dict['creator'] = Resource(target=self.creator)._as_dict_
83
+ if self.created:
84
+ type_as_dict['created'] = self.created if self.created else None
75
85
 
76
- return Serialization.serialize_dict(type_as_dict)
86
+ log.debug(f"'Attribution' serialized with fields: {type_as_dict.keys()}")
87
+ if type_as_dict == {}: log.warning("serializing and empty 'Attribution'")
88
+ return type_as_dict if type_as_dict != {} else None
77
89
 
78
90
  @classmethod
79
- def _from_json_(cls, data: Dict[str, Any]) -> 'Attribution':
80
- """
81
- Construct Attribution from a dict (as parsed from JSON).
82
- Handles 'created' and 'modified' as ISO strings or epoch ms ints.
83
- """
84
- # contributor
85
-
86
- """
87
- Create a Person instance from a JSON-dict (already parsed).
88
- """
89
- from .serialization import Serialization
90
-
91
- return Serialization.deserialize(data, Attribution)
91
+ def _from_json_(cls, data: Dict[str, Any],context) -> 'Attribution':
92
+ if not isinstance(data, dict):
93
+ raise TypeError(f"{cls.__name__}._from_json_ expected dict or str, got {type(data)}")
94
+
95
+ attribution_data: Dict[str, Any] = {}
96
+
97
+ # contributor: Agent | Resource | URI string
98
+ if (contrib := data.get("contributor")) is not None:
99
+ attribution_data["contributor"] = Resource._from_json_(contrib, context)
100
+
101
+ # creator: Agent | Resource | URI string
102
+ if (creator := data.get("creator")) is not None:
103
+ attribution_data["creator"] = Resource._from_json_(creator, context)
104
+
105
+ # changeMessage: str
106
+ if (cm := data.get("changeMessage")) is not None:
107
+ attribution_data["changeMessage"] = cm
108
+
109
+ # created/modified: datetime
110
+ if (created := data.get("created")) is not None:
111
+ attribution_data["created"] = created
112
+ if (modified := data.get("modified")) is not None:
113
+ attribution_data["modified"] = modified
114
+
115
+ return cls(**attribution_data)