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.
Files changed (69) hide show
  1. {gedcom_x-0.5.7.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 +110 -60
  4. gedcomx/TopLevelTypeCollection.py +1 -1
  5. gedcomx/__init__.py +43 -42
  6. gedcomx/address.py +217 -0
  7. gedcomx/{Agent.py → agent.py} +107 -34
  8. gedcomx/attribution.py +115 -0
  9. gedcomx/{Conclusion.py → conclusion.py} +120 -51
  10. gedcomx/{Converter.py → converter.py} +261 -116
  11. gedcomx/coverage.py +64 -0
  12. gedcomx/{Date.py → date.py} +43 -9
  13. gedcomx/{Document.py → document.py} +60 -12
  14. gedcomx/{Event.py → event.py} +88 -31
  15. gedcomx/evidence_reference.py +20 -0
  16. gedcomx/{Fact.py → fact.py} +81 -74
  17. gedcomx/{Gedcom.py → gedcom.py} +10 -0
  18. gedcomx/{Gedcom5x.py → gedcom5x.py} +31 -21
  19. gedcomx/gedcom7/Exceptions.py +9 -0
  20. gedcomx/gedcom7/GedcomStructure.py +94 -0
  21. gedcomx/gedcom7/Specification.py +347 -0
  22. gedcomx/gedcom7/__init__.py +26 -0
  23. gedcomx/gedcom7/g7interop.py +205 -0
  24. gedcomx/gedcom7/gedcom7.py +160 -0
  25. gedcomx/gedcom7/logger.py +19 -0
  26. gedcomx/{GedcomX.py → gedcomx.py} +109 -106
  27. gedcomx/gender.py +91 -0
  28. gedcomx/group.py +72 -0
  29. gedcomx/{Identifier.py → identifier.py} +48 -21
  30. gedcomx/{LoggingHub.py → logging_hub.py} +19 -0
  31. gedcomx/{Mutations.py → mutations.py} +59 -30
  32. gedcomx/{Name.py → name.py} +88 -47
  33. gedcomx/note.py +105 -0
  34. gedcomx/online_account.py +19 -0
  35. gedcomx/{Person.py → person.py} +61 -41
  36. gedcomx/{PlaceDescription.py → place_description.py} +71 -23
  37. gedcomx/{PlaceReference.py → place_reference.py} +32 -10
  38. gedcomx/{Qualifier.py → qualifier.py} +20 -4
  39. gedcomx/relationship.py +156 -0
  40. gedcomx/resource.py +112 -0
  41. gedcomx/serialization.py +794 -0
  42. gedcomx/source_citation.py +37 -0
  43. gedcomx/source_description.py +401 -0
  44. gedcomx/{SourceReference.py → source_reference.py} +56 -21
  45. gedcomx/subject.py +122 -0
  46. gedcomx/textvalue.py +89 -0
  47. gedcomx/{Translation.py → translation.py} +4 -4
  48. gedcomx/uri.py +273 -0
  49. gedcom_x-0.5.7.dist-info/RECORD +0 -49
  50. gedcomx/Address.py +0 -131
  51. gedcomx/Attribution.py +0 -91
  52. gedcomx/Coverage.py +0 -37
  53. gedcomx/EvidenceReference.py +0 -11
  54. gedcomx/Gender.py +0 -65
  55. gedcomx/Group.py +0 -37
  56. gedcomx/Note.py +0 -73
  57. gedcomx/OnlineAccount.py +0 -10
  58. gedcomx/Relationship.py +0 -97
  59. gedcomx/Resource.py +0 -85
  60. gedcomx/Serialization.py +0 -816
  61. gedcomx/SourceCitation.py +0 -25
  62. gedcomx/SourceDescription.py +0 -314
  63. gedcomx/Subject.py +0 -59
  64. gedcomx/TextValue.py +0 -35
  65. gedcomx/URI.py +0 -105
  66. {gedcom_x-0.5.7.dist-info → gedcom_x-0.5.9.dist-info}/WHEEL +0 -0
  67. {gedcom_x-0.5.7.dist-info → gedcom_x-0.5.9.dist-info}/top_level.txt +0 -0
  68. /gedcomx/{Exceptions.py → exceptions.py} +0 -0
  69. /gedcomx/{ExtensibleEnum.py → extensible_enum.py} +0 -0
gedcomx/SourceCitation.py DELETED
@@ -1,25 +0,0 @@
1
- from typing import Optional
2
-
3
- class SourceCitation:
4
- identifier = 'http://gedcomx.org/v1/SourceCitation'
5
- version = 'http://gedcomx.org/conceptual-model/v1'
6
-
7
- def __init__(self, lang: Optional[str], value: str) -> None:
8
- self.lang = lang if lang else 'en'
9
- self.value = value
10
-
11
- # ...existing code...
12
-
13
- @classmethod
14
- def _from_json_(cls, data: dict):
15
- """
16
- Create a SourceCitation instance from a JSON-dict (already parsed).
17
- """
18
- lang = data.get('lang', 'en')
19
- value = data.get('value')
20
- return cls(lang=lang, value=value)
21
-
22
- @property
23
- def _as_dict_(self):
24
- return {'lang':self.lang,
25
- 'value': self.value}
@@ -1,314 +0,0 @@
1
- import warnings
2
-
3
- from enum import Enum
4
- from typing import List, Optional, Dict, Any
5
-
6
- from typing import Optional, TYPE_CHECKING
7
- if TYPE_CHECKING:
8
- from .Document import Document
9
-
10
- """
11
- ======================================================================
12
- Project: Gedcom-X
13
- File: SourceDescription.py
14
- Author: David J. Cartwright
15
- Purpose:
16
-
17
- Created: 2025-07-25
18
- Updated:
19
- - 2025-08-31: _as_dict_ refactored to ignore empty fields, changed id creation to make_uid()
20
-
21
-
22
- ======================================================================
23
- """
24
-
25
- """
26
- ======================================================================
27
- GEDCOM Module Types
28
- ======================================================================
29
- """
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 .Note import Note
36
- from .Resource import Resource
37
- from .SourceCitation import SourceCitation
38
- from .SourceReference import SourceReference
39
- from .TextValue import TextValue
40
- from .URI import URI
41
- #=====================================================================
42
-
43
- class ResourceType(Enum):
44
- Collection = "http://gedcomx.org/Collection"
45
- PhysicalArtifact = "http://gedcomx.org/PhysicalArtifact"
46
- DigitalArtifact = "http://gedcomx.org/DigitalArtifact"
47
- Record = "http://gedcomx.org/Record"
48
- Person = "http://gedcomx.org/Person"
49
-
50
- @property
51
- def description(self):
52
- descriptions = {
53
- 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).",
54
- ResourceType.PhysicalArtifact: "A physical artifact, such as a book.",
55
- ResourceType.DigitalArtifact: "A digital artifact, such as a digital image of a birth certificate or other record.",
56
- ResourceType.Record: "A historical record, such as a census record or a vital record."
57
- }
58
- return descriptions.get(self, "No description available.")
59
-
60
- class SourceDescription:
61
- """Description of a genealogical information source.
62
-
63
- See: http://gedcomx.org/v1/SourceDescription
64
-
65
- Args:
66
- id (str | None): Unique identifier for this `SourceDescription`.
67
- resourceType (ResourceType | None): Type/category of the resource being
68
- described (e.g., digital artifact, physical artifact).
69
- citations (list[SourceCitation] | None): Citations that reference or
70
- justify this source description.
71
- mediaType (str | None): IANA media (MIME) type of the resource
72
- (e.g., ``"application/pdf"``).
73
- about (URI | None): Canonical URI that the description is about.
74
- mediator (Resource | None): The mediator resource (if any) involved in
75
- providing access to the source.
76
- publisher (Resource | Agent | None): Publisher of the resource.
77
- authors (list[Resource] | None): Authors/creators of the resource.
78
- sources (list[SourceReference] | None): Other sources this description
79
- derives from or references.
80
- analysis (Resource | None): Analysis document associated with the
81
- resource (often a `Document`; kept generic to avoid circular imports).
82
- componentOf (SourceReference | None): Reference to a parent/containing
83
- source (this is a component/child of that source).
84
- titles (list[TextValue] | None): One or more titles for the resource.
85
- notes (list[Note] | None): Human-authored notes about the resource.
86
- attribution (Attribution | None): Attribution metadata for who supplied
87
- or curated this description.
88
- rights (list[Resource] | None): Rights statements or licenses.
89
- coverage (list[Coverage] | None): Spatial/temporal coverage of the
90
- source’s content.
91
- descriptions (list[TextValue] | None): Short textual summaries or
92
- descriptions.
93
- identifiers (IdentifierList | None): Alternative identifiers for the
94
- resource (DOI, ARK, call numbers, etc.).
95
- created (Date | None): Creation date of the resource.
96
- modified (Date | None): Last modified date of the resource.
97
- published (Date | None): Publication/release date of the resource.
98
- repository (Agent | None): Repository or agency that holds the resource.
99
- max_note_count (int): Maximum number of notes to retain/emit. Defaults to 20.
100
-
101
- Raises:
102
- ValueError: If `id` is not a valid UUID.
103
-
104
- Attributes:
105
- identifier (str): Gedcom-X specification identifier for this type.
106
- """
107
-
108
- identifier = "http://gedcomx.org/v1/SourceDescription"
109
- version = 'http://gedcomx.org/conceptual-model/v1'
110
-
111
- def __init__(self, id: Optional[str] = None,
112
- resourceType: Optional[ResourceType] = None,
113
- citations: Optional[List[SourceCitation]] = [],
114
- mediaType: Optional[str] = None,
115
- about: Optional[URI] = None,
116
- mediator: Optional[Agent|Resource] = None,
117
- publisher: Optional[Agent|Resource] = None,
118
- authors: Optional[List[Resource]] = None,
119
- sources: Optional[List[SourceReference]] = None, # SourceReference
120
- analysis: Optional["Document|Resource"] = None, #TODO add type checker so its a document
121
- componentOf: Optional[SourceReference] = None, # SourceReference
122
- titles: Optional[List[TextValue]] = None,
123
- notes: Optional[List[Note]] = None,
124
- attribution: Optional[Attribution] = None,
125
- rights: Optional[List[Resource]] = [],
126
- coverage: Optional[List[Coverage]] = None, # Coverage
127
- descriptions: Optional[List[TextValue]] = None,
128
- identifiers: Optional[IdentifierList] = None,
129
- created: Optional[Date] = None,
130
- modified: Optional[Date] = None,
131
- published: Optional[Date] = None,
132
- repository: Optional[Agent] = None,
133
- max_note_count: int = 20):
134
-
135
- self.id = id if id else make_uid()
136
- self.resourceType = resourceType
137
- self.citations = citations or []
138
- self.mediaType = mediaType
139
- self.about = about
140
- self.mediator = mediator
141
- self._publisher = publisher
142
- self.authors = authors or []
143
- self.sources = sources or []
144
- self.analysis = analysis
145
- self.componentOf = componentOf
146
- self.titles = titles or []
147
- self.notes = notes or []
148
- self.attribution = attribution
149
- self.rights = rights or []
150
- self.coverage = coverage or []
151
- self.descriptions = descriptions or []
152
- self.identifiers = identifiers or IdentifierList()
153
- self.created = created
154
- self.modified = modified
155
- self.published = published
156
- self.repository = repository
157
- self.max_note_count = max_note_count
158
-
159
- self.uri = URI(fragment=id) if id else None #TODO Should i take care of this in the collections?
160
-
161
- @property
162
- def publisher(self) -> Resource | Agent | None:
163
- return self._publisher
164
-
165
-
166
- @publisher.setter
167
- def publisher(self, value: Resource | Agent):
168
- if value is None:
169
- self._publisher = None
170
- elif isinstance(value,Resource):
171
- self._publisher = value
172
- elif isinstance(value,Agent):
173
- self._publisher = value
174
- else:
175
- raise ValueError(f"'publisher' must be of type 'URI' or 'Agent', type: {type(value)} was provided")
176
-
177
- def add_description(self, desccription_to_add: TextValue):
178
- if desccription_to_add and isinstance(desccription_to_add,TextValue):
179
- for current_description in self.descriptions:
180
- if desccription_to_add == current_description:
181
- return
182
- self.descriptions.append(desccription_to_add)
183
-
184
- def add_identifier(self, identifier_to_add: Identifier):
185
- if identifier_to_add and isinstance(identifier_to_add,Identifier):
186
- self.identifiers.append(identifier_to_add)
187
-
188
- def add_note(self,note_to_add: Note):
189
- if len(self.notes) >= self.max_note_count:
190
- warnings.warn(f"Max not count of {self.max_note_count} reached for id: {self.id}")
191
- return False
192
- if note_to_add and isinstance(note_to_add,Note):
193
- for existing in self.notes:
194
- if note_to_add == existing:
195
- return False
196
- self.notes.append(note_to_add)
197
-
198
- def add_source(self, source_to_add: SourceReference):
199
- if source_to_add and isinstance(object,SourceReference):
200
- for current_source in self.sources:
201
- if current_source == source_to_add:
202
- return
203
- self.sources.append(source_to_add)
204
-
205
- def add_title(self, title_to_add: TextValue):
206
- if isinstance(title_to_add,str): title_to_add = TextValue(value=title_to_add)
207
- if title_to_add and isinstance(title_to_add, TextValue):
208
- for current_title in self.titles:
209
- if title_to_add == current_title:
210
- return False
211
- self.titles.append(title_to_add)
212
- else:
213
- raise ValueError(f"Cannot add title of type {type(title_to_add)}")
214
-
215
- @property
216
- def _as_dict_(self) -> Dict[str, Any]:
217
- from .Serialization import Serialization
218
- type_as_dict = {}
219
-
220
- if self.id:
221
- type_as_dict['id'] = self.id
222
- if self.about:
223
- type_as_dict['about'] = self.about._as_dict_
224
- if self.resourceType:
225
- type_as_dict['resourceType'] = getattr(self.resourceType, 'value', self.resourceType)
226
- if self.citations:
227
- type_as_dict['citations'] = [c._as_dict_ for c in self.citations if c]
228
- if self.mediaType:
229
- type_as_dict['mediaType'] = self.mediaType
230
- if self.mediator:
231
- type_as_dict['mediator'] = self.mediator._as_dict_
232
- if self.publisher:
233
- type_as_dict['publisher'] = self.publisher._as_dict_ #TODO Resource this
234
- if self.authors:
235
- type_as_dict['authors'] = [a._as_dict_ for a in self.authors if a]
236
- if self.sources:
237
- type_as_dict['sources'] = [s._as_dict_ for s in self.sources if s]
238
- if self.analysis:
239
- type_as_dict['analysis'] = self.analysis._as_dict_
240
- if self.componentOf:
241
- type_as_dict['componentOf'] = self.componentOf._as_dict_
242
- if self.titles:
243
- type_as_dict['titles'] = [t._as_dict_ for t in self.titles if t]
244
- if self.notes:
245
- type_as_dict['notes'] = [n._as_dict_ for n in self.notes if n]
246
- if self.attribution:
247
- type_as_dict['attribution'] = self.attribution._as_dict_
248
- if self.rights:
249
- type_as_dict['rights'] = [r._as_dict_ for r in self.rights if r]
250
- if self.coverage:
251
- type_as_dict['coverage'] = [c._as_dict_ for c in self.coverage if c]
252
- if self.descriptions:
253
- type_as_dict['descriptions'] = [d for d in self.descriptions if d]
254
- if self.identifiers:
255
- type_as_dict['identifiers'] = self.identifiers._as_dict_
256
- if self.created is not None:
257
- type_as_dict['created'] = self.created
258
- if self.modified is not None:
259
- type_as_dict['modified'] = self.modified
260
- if self.published is not None:
261
- type_as_dict['published'] = self.published
262
- if self.repository:
263
- type_as_dict['repository'] = self.repository._as_dict_ #TODO Resource this
264
- if self.uri and self.uri.value:
265
- type_as_dict['uri'] = self.uri.value
266
-
267
-
268
- return Serialization.serialize_dict(type_as_dict)
269
-
270
-
271
- @classmethod
272
- def _from_json_(cls, data: Dict[str, Any]) -> 'SourceDescription':
273
- # TODO Hande Resource/URI
274
-
275
- # Basic fields
276
- id_ = data.get('id')
277
- rt = ResourceType(data['resourceType']) if data.get('resourceType') else None
278
-
279
- # Sub-objects
280
- citations = [SourceCitation._from_json_(c) for c in data.get('citations', [])]
281
- about = URI._from_json_(data['about']) if data.get('about') else None
282
- mediator = Resource._from_json_(data['mediator']) if data.get('mediator') else None
283
- publisher = Resource._from_json_(data['publisher']) if data.get('publisher') else None
284
- authors = [Resource._from_json_(a) for a in data.get('authors', [])] if data.get('authors') else None
285
- sources = [SourceReference._from_json_(s) for s in data.get('sources', [])]
286
- analysis = Resource._from_json_(data['analysis']) if data.get('analysis') else None
287
- component_of = SourceReference._from_json_(data['componentOf']) if data.get('componentOf') else None
288
- titles = [TextValue._from_json_(t) for t in data.get('titles', [])]
289
- notes = [Note._from_json_(n) for n in data.get('notes', [])]
290
- attribution = Attribution._from_json_(data['attribution']) if data.get('attribution') else None
291
- rights = [URI._from_json_(r) for r in data.get('rights', [])]
292
- coverage = [Coverage._from_json_(cvg) for cvg in data.get('coverage',[])]
293
- descriptions = [TextValue._from_json_(d) for d in data.get('descriptions', [])]
294
- identifiers = IdentifierList._from_json_(data.get('identifiers', []))
295
-
296
- created = Date._from_json_(data['created']) if data.get('created') else None
297
- modified = data.get('modified',None)
298
- published = Date._from_json_(data['published']) if data.get('published') else None
299
- repository = Agent._from_json_(data['repository']) if data.get('repository') else None
300
-
301
- return cls(
302
- id=id_, resourceType=rt, citations=citations,
303
- mediaType=data.get('mediaType'), about=about,
304
- mediator=mediator, publisher=publisher,
305
- authors=authors, sources=sources,
306
- analysis=analysis, componentOf=component_of,
307
- titles=titles, notes=notes, attribution=attribution,
308
- rights=rights, coverage=coverage,
309
- descriptions=descriptions, identifiers=identifiers,
310
- created=created, modified=modified,
311
- published=published, repository=repository
312
- )
313
-
314
-
gedcomx/Subject.py DELETED
@@ -1,59 +0,0 @@
1
- import warnings
2
- from typing import List, Optional
3
-
4
- from .Attribution import Attribution
5
- from .Conclusion import Conclusion, ConfidenceLevel
6
- from .EvidenceReference import EvidenceReference
7
- from .Identifier import Identifier, IdentifierList
8
- from .Note import Note
9
-
10
- from .SourceReference import SourceReference
11
- from .Resource import Resource
12
- from .Extensions.rs10.rsLink import _rsLinkList
13
-
14
- class Subject(Conclusion):
15
- identifier = 'http://gedcomx.org/v1/Subject'
16
- version = 'http://gedcomx.org/conceptual-model/v1'
17
-
18
- def __init__(self,
19
- id: Optional[str],
20
- lang: Optional[str] = 'en',
21
- sources: Optional[List[SourceReference]] = [],
22
- analysis: Optional[Resource] = None,
23
- notes: Optional[List[Note]] = [],
24
- confidence: Optional[ConfidenceLevel] = None,
25
- attribution: Optional[Attribution] = None,
26
- extracted: Optional[bool] = None,
27
- evidence: Optional[List[EvidenceReference]] = [],
28
- media: Optional[List[SourceReference]] = [],
29
- identifiers: Optional[IdentifierList] = None,
30
- uri: Optional[Resource] = None,
31
- links: Optional[_rsLinkList] = None) -> None:
32
- super().__init__(id, lang, sources, analysis, notes, confidence, attribution,links=links)
33
- self.extracted = extracted
34
- self.evidence = evidence
35
- self.media = media
36
- self.identifiers = identifiers if identifiers else IdentifierList()
37
- self.uri = uri
38
-
39
- def add_identifier(self, identifier_to_add: Identifier):
40
- if identifier_to_add and isinstance(identifier_to_add,Identifier):
41
- for current_identifier in self.identifiers:
42
- if identifier_to_add == current_identifier:
43
- return
44
- self.identifiers.append(identifier_to_add)
45
-
46
- @property
47
- def _as_dict_(self):
48
- from .Serialization import Serialization
49
-
50
- type_as_dict = super()._as_dict_ # Start with base class fields
51
- # Only add Relationship-specific fields
52
- type_as_dict.update({
53
- "extracted": self.extracted,
54
- "evidence": [evidence_ref for evidence_ref in self.evidence] if self.evidence else None,
55
- "media": [media for media in self.media] if self.media else None,
56
- "identifiers": self.identifiers._as_dict_ if self.identifiers else None
57
-
58
- })
59
- return Serialization.serialize_dict(type_as_dict)
gedcomx/TextValue.py DELETED
@@ -1,35 +0,0 @@
1
- from typing import Optional
2
-
3
- class TextValue:
4
- identifier = 'http://gedcomx.org/v1/TextValue'
5
- version = 'http://gedcomx.org/conceptual-model/v1'
6
-
7
- def __init__(self, value: Optional[str] = None, lang: Optional[str] = 'en') -> None:
8
- self.lang = lang
9
- self.value = value
10
-
11
- def _append_to_value(self, value_to_append):
12
- if not isinstance(value_to_append, str):
13
- raise ValueError(f"Cannot append object of type {type(value_to_append)}.")
14
- self.value += ' ' + value_to_append
15
-
16
- @property
17
- def _as_dict_(self):
18
- return {
19
- "lang":self.lang if self.lang else None,
20
- "value":self.value if self.value else None
21
- }
22
-
23
- def __str__(self):
24
- return f"{self.value} ({self.lang})"
25
-
26
- # ...existing code...
27
-
28
- @classmethod
29
- def _from_json_(cls, data: dict):
30
- """
31
- Create a TextValue instance from a JSON-dict (already parsed).
32
- """
33
- value = data.get('value')
34
- lang = data.get('lang', 'en')
35
- return cls(value=value, lang=lang)
gedcomx/URI.py DELETED
@@ -1,105 +0,0 @@
1
- from __future__ import annotations
2
- from dataclasses import dataclass, field
3
- from typing import Mapping, Optional, Union, Iterable, Tuple
4
- from urllib.parse import urlsplit, urlunsplit, urlencode, parse_qsl, SplitResult
5
-
6
- _DEFAULT_SCHEME = "gedcomx"
7
-
8
- @dataclass(frozen=False, slots=True)
9
- class URI:
10
- scheme: str = field(default=_DEFAULT_SCHEME)
11
- authority: str = field(default="")
12
- path: str = field(default="")
13
- query: str = field(default="")
14
- fragment: str = field(default="")
15
-
16
- # ---------- constructors ----------
17
- @classmethod
18
- def from_url(cls, url: str, *, default_scheme: str = _DEFAULT_SCHEME) -> "URI":
19
- s = urlsplit(url)
20
- scheme = s.scheme or default_scheme
21
- return cls(scheme=scheme, authority=s.netloc, path=s.path, query=s.query, fragment=s.fragment)
22
-
23
- @classmethod
24
- def parse(cls, value: str) -> "URI":
25
- return cls.from_url(value)
26
-
27
- @classmethod
28
- def from_parts(
29
- cls,
30
- *,
31
- scheme: Optional[str] = None,
32
- authority: str = "",
33
- path: str = "",
34
- query: Union[str, Mapping[str, str], Iterable[Tuple[str, str]]] = "",
35
- fragment: str = ""
36
- ) -> "URI":
37
- q = query if isinstance(query, str) else urlencode(query, doseq=True)
38
- return cls(scheme=scheme or _DEFAULT_SCHEME, authority=authority, path=path, query=q, fragment=fragment)
39
-
40
- # ---------- views ----------
41
- @property
42
- def value(self) -> str:
43
- return str(self)
44
-
45
- def split(self) -> SplitResult:
46
- return SplitResult(self.scheme, self.authority, self.path, self.query, self.fragment)
47
-
48
- def __str__(self) -> str:
49
- return urlunsplit(self.split())
50
-
51
- @property
52
- def _as_dict_(self) -> str:
53
- # Keeps a simple, explicit structure
54
- return urlunsplit(self.split())
55
- return {
56
- "scheme": self.scheme,
57
- "authority": self.authority,
58
- "path": self.path,
59
- "query": self.query,
60
- "fragment": self.fragment,
61
- "value": str(self),
62
- }
63
-
64
- # Accepts {'resource': '...'} or a plain string, mirroring your original
65
- @classmethod
66
- def from_jsonish(cls, data: Union[str, Mapping[str, str]]) -> "URI":
67
- if isinstance(data, str):
68
- return cls.from_url(data)
69
- if isinstance(data, Mapping):
70
- raw = data.get("resource") or data.get("value") or ""
71
- if raw:
72
- return cls.from_url(raw)
73
- raise ValueError(f"Cannot build URI from: {data!r}")
74
-
75
- # ---------- functional updaters ----------
76
- def with_scheme(self, scheme: str) -> "URI": return self.replace(scheme=scheme)
77
- def with_authority(self, authority: str) -> "URI": return self.replace(authority=authority)
78
- def with_path(self, path: str, *, join: bool = False) -> "URI":
79
- new_path = (self.path.rstrip("/") + "/" + path.lstrip("/")) if join else path
80
- return self.replace(path=new_path)
81
- def with_fragment(self, fragment: str | None) -> "URI":
82
- return self.replace(fragment=(fragment or ""))
83
- def without_fragment(self) -> "URI": return self.replace(fragment="")
84
- def with_query(self, query: Union[str, Mapping[str, str], Iterable[Tuple[str, str]]]) -> "URI":
85
- q = query if isinstance(query, str) else urlencode(query, doseq=True)
86
- return self.replace(query=q)
87
- def add_query_params(self, params: Mapping[str, Union[str, Iterable[str]]]) -> "URI":
88
- existing = parse_qsl(self.query, keep_blank_values=True)
89
- for k, v in params.items():
90
- if isinstance(v, str):
91
- existing.append((k, v))
92
- else:
93
- for vv in v:
94
- existing.append((k, vv))
95
- return self.replace(query=urlencode(existing, doseq=True))
96
-
97
- # ---------- helpers ----------
98
- def replace(self, **kwargs) -> "URI":
99
- return URI(
100
- scheme=kwargs.get("scheme", self.scheme or _DEFAULT_SCHEME),
101
- authority=kwargs.get("authority", self.authority),
102
- path=kwargs.get("path", self.path),
103
- query=kwargs.get("query", self.query),
104
- fragment=kwargs.get("fragment", self.fragment),
105
- )
File without changes
File without changes