luranta 0.1.0__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 (45) hide show
  1. luranta-0.1.0/LICENSE +21 -0
  2. luranta-0.1.0/PKG-INFO +75 -0
  3. luranta-0.1.0/README.md +51 -0
  4. luranta-0.1.0/pyproject.toml +37 -0
  5. luranta-0.1.0/src/luranta/__init__.py +66 -0
  6. luranta-0.1.0/src/luranta/_client.py +279 -0
  7. luranta-0.1.0/src/luranta/_errors.py +36 -0
  8. luranta-0.1.0/src/luranta/_transport.py +302 -0
  9. luranta-0.1.0/src/luranta/py.typed +1 -0
  10. luranta-0.1.0/src/luranta_generated/__init__.py +1 -0
  11. luranta-0.1.0/src/luranta_generated/models/company.py +152 -0
  12. luranta-0.1.0/src/luranta_generated/models/company_list.py +126 -0
  13. luranta-0.1.0/src/luranta_generated/models/company_list_data_inner.py +152 -0
  14. luranta-0.1.0/src/luranta_generated/models/job.py +323 -0
  15. luranta-0.1.0/src/luranta_generated/models/job_company.py +97 -0
  16. luranta-0.1.0/src/luranta_generated/models/job_compensation.py +136 -0
  17. luranta-0.1.0/src/luranta_generated/models/job_compensation_packages_inner.py +199 -0
  18. luranta-0.1.0/src/luranta_generated/models/job_compensation_packages_inner_components_inner.py +163 -0
  19. luranta-0.1.0/src/luranta_generated/models/job_compensation_packages_inner_components_inner_one_of.py +110 -0
  20. luranta-0.1.0/src/luranta_generated/models/job_compensation_packages_inner_components_inner_one_of1.py +108 -0
  21. luranta-0.1.0/src/luranta_generated/models/job_compensation_packages_inner_components_inner_one_of2.py +110 -0
  22. luranta-0.1.0/src/luranta_generated/models/job_compensation_packages_inner_components_inner_one_of3.py +104 -0
  23. luranta-0.1.0/src/luranta_generated/models/job_compensation_packages_inner_scope.py +191 -0
  24. luranta-0.1.0/src/luranta_generated/models/job_compensation_packages_inner_scope_one_of.py +93 -0
  25. luranta-0.1.0/src/luranta_generated/models/job_compensation_packages_inner_scope_one_of1.py +95 -0
  26. luranta-0.1.0/src/luranta_generated/models/job_compensation_packages_inner_scope_one_of2.py +103 -0
  27. luranta-0.1.0/src/luranta_generated/models/job_compensation_packages_inner_scope_one_of3.py +103 -0
  28. luranta-0.1.0/src/luranta_generated/models/job_compensation_packages_inner_scope_one_of4.py +96 -0
  29. luranta-0.1.0/src/luranta_generated/models/job_compensation_packages_inner_scope_one_of5.py +93 -0
  30. luranta-0.1.0/src/luranta_generated/models/job_experience.py +101 -0
  31. luranta-0.1.0/src/luranta_generated/models/job_list.py +126 -0
  32. luranta-0.1.0/src/luranta_generated/models/job_list_data_inner.py +279 -0
  33. luranta-0.1.0/src/luranta_generated/models/job_location_disclosures_inner.py +105 -0
  34. luranta-0.1.0/src/luranta_generated/models/job_locations_inner.py +96 -0
  35. luranta-0.1.0/src/luranta_generated/models/job_primary_compensation_package.py +199 -0
  36. luranta-0.1.0/src/luranta_generated/models/location.py +109 -0
  37. luranta-0.1.0/src/luranta_generated/models/location_list.py +126 -0
  38. luranta-0.1.0/src/luranta_generated/models/location_list_data_inner.py +109 -0
  39. luranta-0.1.0/src/luranta_generated/models/problem_details.py +167 -0
  40. luranta-0.1.0/src/luranta_generated/models/problem_details_errors_inner.py +98 -0
  41. luranta-0.1.0/src/luranta_generated/models/taxonomy_snapshot.py +137 -0
  42. luranta-0.1.0/src/luranta_generated/models/taxonomy_snapshot_entries_inner.py +120 -0
  43. luranta-0.1.0/src/luranta_generated/models/tool.py +98 -0
  44. luranta-0.1.0/src/luranta_generated/models/tool_list.py +126 -0
  45. luranta-0.1.0/src/luranta_generated/models/tool_list_data_inner.py +98 -0
luranta-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Luranta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
luranta-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: luranta
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the Luranta API.
5
+ Project-URL: Documentation, https://docs.luranta.com
6
+ Project-URL: Source, https://github.com/luranta/sdk-python
7
+ Author-email: Luranta <support@luranta.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: <3.15,>=3.10
20
+ Requires-Dist: pydantic-core==2.46.4
21
+ Requires-Dist: pydantic==2.13.4
22
+ Requires-Dist: typing-extensions==4.16.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ # `luranta`
26
+
27
+ Official Python SDK for Luranta's preview API, published as `luranta`.
28
+
29
+ ```python
30
+ from luranta import Luranta
31
+
32
+ luranta = Luranta(api_key="lur_...")
33
+
34
+ for job in luranta.jobs.iterate({"roles": ["software-engineering"]}):
35
+ print(job.title)
36
+ ```
37
+
38
+ `jobs`, `companies`, `tools`, and `locations` expose `list`, `retrieve` where
39
+ applicable, cursor iterators, and `with_raw_response` methods. Versioned role
40
+ and industry taxonomies are available through `luranta.taxonomies.roles` and
41
+ `luranta.taxonomies.industries`; use their `current()` and `get(version)`
42
+ methods. Raw responses include request IDs and Luranta billing headers.
43
+
44
+ Taxonomy values, tools, and locations are evolving resources. Languages are
45
+ accepted as canonical BCP 47 strings (for example `en-GB` or `zh-Hans`) rather
46
+ than a generated enum.
47
+
48
+ Only HTTP `408`, `429`, `500`, `502`, `503`, and `504` responses carrying
49
+ valid RFC 9457 Problem Details with `retryable: true` and no billing evidence
50
+ are retried. `Retry-After` is honored. Network failures are ambiguous and
51
+ immediately raise `TransportError`. Cursor iterators stop on malformed or
52
+ repeated cursors rather than risk duplicate billable reads.
53
+
54
+ ## Generation
55
+
56
+ `src/luranta_generated` contains disposable models generated from the canonical
57
+ `../../apps/api/openapi/openapi.json` using OpenAPI Generator engine 7.24.0
58
+ through the exact 2.40.1 CLI wrapper. Generated HTTP code is intentionally
59
+ excluded so the safe runtime above is the only transport. A deterministic
60
+ response-only transform widens schema enums to strings while retaining their
61
+ known values as metadata, so a new server enum does not break an older SDK.
62
+
63
+ ```sh
64
+ bun run generate
65
+ bun run generate:check
66
+ ```
67
+
68
+ Luranta API keys belong only in trusted server-side environments. Never expose
69
+ one in browser or mobile application code.
70
+
71
+ The preview package supports the exact tested Python 3.10 through 3.14
72
+ maintenance lines.
73
+
74
+ The public source repository is [luranta/sdk-python](https://github.com/luranta/sdk-python).
75
+ Releases use PyPI Trusted Publishing with attestations.
@@ -0,0 +1,51 @@
1
+ # `luranta`
2
+
3
+ Official Python SDK for Luranta's preview API, published as `luranta`.
4
+
5
+ ```python
6
+ from luranta import Luranta
7
+
8
+ luranta = Luranta(api_key="lur_...")
9
+
10
+ for job in luranta.jobs.iterate({"roles": ["software-engineering"]}):
11
+ print(job.title)
12
+ ```
13
+
14
+ `jobs`, `companies`, `tools`, and `locations` expose `list`, `retrieve` where
15
+ applicable, cursor iterators, and `with_raw_response` methods. Versioned role
16
+ and industry taxonomies are available through `luranta.taxonomies.roles` and
17
+ `luranta.taxonomies.industries`; use their `current()` and `get(version)`
18
+ methods. Raw responses include request IDs and Luranta billing headers.
19
+
20
+ Taxonomy values, tools, and locations are evolving resources. Languages are
21
+ accepted as canonical BCP 47 strings (for example `en-GB` or `zh-Hans`) rather
22
+ than a generated enum.
23
+
24
+ Only HTTP `408`, `429`, `500`, `502`, `503`, and `504` responses carrying
25
+ valid RFC 9457 Problem Details with `retryable: true` and no billing evidence
26
+ are retried. `Retry-After` is honored. Network failures are ambiguous and
27
+ immediately raise `TransportError`. Cursor iterators stop on malformed or
28
+ repeated cursors rather than risk duplicate billable reads.
29
+
30
+ ## Generation
31
+
32
+ `src/luranta_generated` contains disposable models generated from the canonical
33
+ `../../apps/api/openapi/openapi.json` using OpenAPI Generator engine 7.24.0
34
+ through the exact 2.40.1 CLI wrapper. Generated HTTP code is intentionally
35
+ excluded so the safe runtime above is the only transport. A deterministic
36
+ response-only transform widens schema enums to strings while retaining their
37
+ known values as metadata, so a new server enum does not break an older SDK.
38
+
39
+ ```sh
40
+ bun run generate
41
+ bun run generate:check
42
+ ```
43
+
44
+ Luranta API keys belong only in trusted server-side environments. Never expose
45
+ one in browser or mobile application code.
46
+
47
+ The preview package supports the exact tested Python 3.10 through 3.14
48
+ maintenance lines.
49
+
50
+ The public source repository is [luranta/sdk-python](https://github.com/luranta/sdk-python).
51
+ Releases use PyPI Trusted Publishing with attestations.
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["hatchling==1.31.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "luranta"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the Luranta API."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10,<3.15"
11
+ license = "MIT"
12
+ authors = [{ name = "Luranta", email = "support@luranta.com" }]
13
+ dependencies = ["pydantic==2.13.4", "pydantic-core==2.46.4", "typing-extensions==4.16.0"]
14
+ classifiers = [
15
+ "Development Status :: 2 - Pre-Alpha",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ "Typing :: Typed",
24
+ ]
25
+
26
+ [project.urls]
27
+ Documentation = "https://docs.luranta.com"
28
+ Source = "https://github.com/luranta/sdk-python"
29
+
30
+ [tool.luranta]
31
+ release-enabled = true
32
+
33
+ [tool.hatch.build.targets.sdist]
34
+ include = ["/src", "/LICENSE", "/README.md"]
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/luranta", "src/luranta_generated"]
@@ -0,0 +1,66 @@
1
+ from luranta_generated.models.company import Company
2
+ from luranta_generated.models.company_list import CompanyList
3
+ from luranta_generated.models.company_list_data_inner import (
4
+ CompanyListDataInner as CompanySummary,
5
+ )
6
+ from luranta_generated.models.job import Job
7
+ from luranta_generated.models.job_list import JobList
8
+ from luranta_generated.models.job_list_data_inner import JobListDataInner as JobSummary
9
+ from luranta_generated.models.location import Location
10
+ from luranta_generated.models.location_list import LocationList
11
+ from luranta_generated.models.location_list_data_inner import (
12
+ LocationListDataInner as LocationSummary,
13
+ )
14
+ from luranta_generated.models.problem_details import ProblemDetails
15
+ from luranta_generated.models.taxonomy_snapshot import TaxonomySnapshot
16
+ from luranta_generated.models.taxonomy_snapshot_entries_inner import (
17
+ TaxonomySnapshotEntriesInner as TaxonomyEntry,
18
+ )
19
+ from luranta_generated.models.tool import Tool
20
+ from luranta_generated.models.tool_list import ToolList
21
+ from luranta_generated.models.tool_list_data_inner import ToolListDataInner as ToolSummary
22
+
23
+ from ._client import (
24
+ CatalogueListParams,
25
+ LocationsRawResource,
26
+ LocationsResource,
27
+ Luranta,
28
+ TaxonomiesResource,
29
+ TaxonomyResource,
30
+ ToolsRawResource,
31
+ ToolsResource,
32
+ )
33
+ from ._errors import LurantaProblemError, TransportError
34
+ from ._transport import HttpResponse, QueryParams, QueryValue, RawResponse
35
+
36
+ __all__ = [
37
+ "Company",
38
+ "CompanyList",
39
+ "CompanySummary",
40
+ "CatalogueListParams",
41
+ "HttpResponse",
42
+ "Job",
43
+ "JobList",
44
+ "JobSummary",
45
+ "Location",
46
+ "LocationList",
47
+ "LocationSummary",
48
+ "LocationsRawResource",
49
+ "LocationsResource",
50
+ "Luranta",
51
+ "LurantaProblemError",
52
+ "ProblemDetails",
53
+ "QueryParams",
54
+ "QueryValue",
55
+ "RawResponse",
56
+ "TaxonomiesResource",
57
+ "TaxonomyEntry",
58
+ "TaxonomyResource",
59
+ "TaxonomySnapshot",
60
+ "Tool",
61
+ "ToolList",
62
+ "ToolSummary",
63
+ "ToolsRawResource",
64
+ "ToolsResource",
65
+ "TransportError",
66
+ ]
@@ -0,0 +1,279 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+ from typing import Final
5
+ from urllib.parse import quote
6
+ from typing_extensions import NotRequired, Required, TypedDict
7
+
8
+ from luranta_generated.models.company import Company
9
+ from luranta_generated.models.company_list import CompanyList
10
+ from luranta_generated.models.company_list_data_inner import CompanyListDataInner
11
+ from luranta_generated.models.job import Job
12
+ from luranta_generated.models.job_list import JobList
13
+ from luranta_generated.models.job_list_data_inner import JobListDataInner
14
+ from luranta_generated.models.location import Location
15
+ from luranta_generated.models.location_list import LocationList
16
+ from luranta_generated.models.location_list_data_inner import LocationListDataInner
17
+ from luranta_generated.models.taxonomy_snapshot import TaxonomySnapshot
18
+ from luranta_generated.models.tool import Tool
19
+ from luranta_generated.models.tool_list import ToolList
20
+ from luranta_generated.models.tool_list_data_inner import ToolListDataInner
21
+
22
+ from ._transport import (
23
+ ClientTransport,
24
+ QueryParams,
25
+ RawResponse,
26
+ Transport,
27
+ )
28
+
29
+ DEFAULT_BASE_URL: Final = "https://api.luranta.com"
30
+
31
+
32
+ class CatalogueListParams(TypedDict):
33
+ query: Required[str]
34
+ cursor: NotRequired[str]
35
+ limit: NotRequired[int]
36
+
37
+
38
+ class Luranta:
39
+ def __init__(
40
+ self,
41
+ *,
42
+ api_key: str,
43
+ base_url: str = DEFAULT_BASE_URL,
44
+ max_retries: int = 2,
45
+ timeout: float = 30.0,
46
+ _transport: Transport | None = None,
47
+ ) -> None:
48
+ transport = ClientTransport(
49
+ api_key=api_key,
50
+ base_url=base_url,
51
+ max_retries=max_retries,
52
+ timeout=timeout,
53
+ transport=_transport,
54
+ )
55
+ self.jobs = JobsResource(transport)
56
+ self.companies = CompaniesResource(transport)
57
+ self.taxonomies = TaxonomiesResource(transport)
58
+ self.tools = ToolsResource(transport)
59
+ self.locations = LocationsResource(transport)
60
+
61
+
62
+ class JobsResource:
63
+ def __init__(self, transport: ClientTransport) -> None:
64
+ self._transport = transport
65
+ self.with_raw_response = JobsRawResource(transport)
66
+
67
+ def list(self, params: QueryParams | None = None) -> JobList:
68
+ return self._transport.request("/v0/jobs", JobList, params)
69
+
70
+ def retrieve(self, job_id: str) -> Job:
71
+ return self._transport.request(
72
+ f"/v0/jobs/{quote(job_id, safe='')}",
73
+ Job,
74
+ )
75
+
76
+ def iterate(self, params: QueryParams | None = None) -> Iterator[JobListDataInner]:
77
+ original = dict(params or {})
78
+ current = dict(original)
79
+ seen_cursors = _initial_cursors(original)
80
+ while True:
81
+ page = self.list(current)
82
+ yield from page.data
83
+ cursor = _advance_cursor(page.has_more, page.next_cursor, seen_cursors)
84
+ if cursor is None:
85
+ return
86
+ current = {**original, "cursor": cursor}
87
+
88
+
89
+ class JobsRawResource:
90
+ def __init__(self, transport: ClientTransport) -> None:
91
+ self._transport = transport
92
+
93
+ def list(self, params: QueryParams | None = None) -> RawResponse[JobList]:
94
+ return self._transport.request_raw("/v0/jobs", JobList, params)
95
+
96
+ def retrieve(self, job_id: str) -> RawResponse[Job]:
97
+ return self._transport.request_raw(
98
+ f"/v0/jobs/{quote(job_id, safe='')}",
99
+ Job,
100
+ )
101
+
102
+
103
+ class CompaniesResource:
104
+ def __init__(self, transport: ClientTransport) -> None:
105
+ self._transport = transport
106
+ self.with_raw_response = CompaniesRawResource(transport)
107
+
108
+ def list(self, params: QueryParams | None = None) -> CompanyList:
109
+ return self._transport.request("/v0/companies", CompanyList, params)
110
+
111
+ def retrieve(self, company_id: str) -> Company:
112
+ return self._transport.request(
113
+ f"/v0/companies/{quote(company_id, safe='')}",
114
+ Company,
115
+ )
116
+
117
+ def iterate(
118
+ self,
119
+ params: QueryParams | None = None,
120
+ ) -> Iterator[CompanyListDataInner]:
121
+ original = dict(params or {})
122
+ current = dict(original)
123
+ seen_cursors = _initial_cursors(original)
124
+ while True:
125
+ page = self.list(current)
126
+ yield from page.data
127
+ cursor = _advance_cursor(page.has_more, page.next_cursor, seen_cursors)
128
+ if cursor is None:
129
+ return
130
+ current = {**original, "cursor": cursor}
131
+
132
+
133
+ class CompaniesRawResource:
134
+ def __init__(self, transport: ClientTransport) -> None:
135
+ self._transport = transport
136
+
137
+ def list(self, params: QueryParams | None = None) -> RawResponse[CompanyList]:
138
+ return self._transport.request_raw("/v0/companies", CompanyList, params)
139
+
140
+ def retrieve(self, company_id: str) -> RawResponse[Company]:
141
+ return self._transport.request_raw(
142
+ f"/v0/companies/{quote(company_id, safe='')}",
143
+ Company,
144
+ )
145
+
146
+
147
+ class TaxonomyResource:
148
+ def __init__(self, transport: ClientTransport, name: str) -> None:
149
+ self._transport = transport
150
+ self._path = f"/v0/taxonomies/{name}"
151
+
152
+ def current(self) -> TaxonomySnapshot:
153
+ return self._transport.request(self._path, TaxonomySnapshot)
154
+
155
+ def get(self, version: int | str) -> TaxonomySnapshot:
156
+ return self._transport.request(
157
+ f"{self._path}/{quote(str(version), safe='')}",
158
+ TaxonomySnapshot,
159
+ )
160
+
161
+
162
+ class TaxonomiesResource:
163
+ def __init__(self, transport: ClientTransport) -> None:
164
+ self.roles = TaxonomyResource(transport, "roles")
165
+ self.industries = TaxonomyResource(transport, "industries")
166
+
167
+
168
+ class ToolsResource:
169
+ def __init__(self, transport: ClientTransport) -> None:
170
+ self._transport = transport
171
+ self.with_raw_response = ToolsRawResource(transport)
172
+
173
+ def list(self, params: CatalogueListParams) -> ToolList:
174
+ return self._transport.request("/v0/tools", ToolList, params)
175
+
176
+ def retrieve(self, code: str) -> Tool:
177
+ return self._transport.request(
178
+ f"/v0/tools/{quote(code, safe='')}",
179
+ Tool,
180
+ )
181
+
182
+ def iterate(self, params: CatalogueListParams) -> Iterator[ToolListDataInner]:
183
+ original = dict(params)
184
+ current = dict(original)
185
+ seen_cursors = _initial_cursors(original)
186
+ while True:
187
+ page = self.list(current)
188
+ yield from page.data
189
+ cursor = _advance_cursor(page.has_more, page.next_cursor, seen_cursors)
190
+ if cursor is None:
191
+ return
192
+ current = {**original, "cursor": cursor}
193
+
194
+
195
+ class ToolsRawResource:
196
+ def __init__(self, transport: ClientTransport) -> None:
197
+ self._transport = transport
198
+
199
+ def list(self, params: CatalogueListParams) -> RawResponse[ToolList]:
200
+ return self._transport.request_raw("/v0/tools", ToolList, params)
201
+
202
+ def retrieve(self, code: str) -> RawResponse[Tool]:
203
+ return self._transport.request_raw(
204
+ f"/v0/tools/{quote(code, safe='')}",
205
+ Tool,
206
+ )
207
+
208
+
209
+ class LocationsResource:
210
+ def __init__(self, transport: ClientTransport) -> None:
211
+ self._transport = transport
212
+ self.with_raw_response = LocationsRawResource(transport)
213
+
214
+ def list(self, params: CatalogueListParams) -> LocationList:
215
+ return self._transport.request("/v0/locations", LocationList, params)
216
+
217
+ def retrieve(self, location_id: str) -> Location:
218
+ return self._transport.request(
219
+ f"/v0/locations/{quote(location_id, safe='')}",
220
+ Location,
221
+ )
222
+
223
+ def iterate(
224
+ self,
225
+ params: CatalogueListParams,
226
+ ) -> Iterator[LocationListDataInner]:
227
+ original = dict(params or {})
228
+ current = dict(original)
229
+ seen_cursors = _initial_cursors(original)
230
+ while True:
231
+ page = self.list(current)
232
+ yield from page.data
233
+ cursor = _advance_cursor(page.has_more, page.next_cursor, seen_cursors)
234
+ if cursor is None:
235
+ return
236
+ current = {**original, "cursor": cursor}
237
+
238
+
239
+ class LocationsRawResource:
240
+ def __init__(self, transport: ClientTransport) -> None:
241
+ self._transport = transport
242
+
243
+ def list(self, params: CatalogueListParams) -> RawResponse[LocationList]:
244
+ return self._transport.request_raw("/v0/locations", LocationList, params)
245
+
246
+ def retrieve(self, location_id: str) -> RawResponse[Location]:
247
+ return self._transport.request_raw(
248
+ f"/v0/locations/{quote(location_id, safe='')}",
249
+ Location,
250
+ )
251
+
252
+
253
+ def _initial_cursors(params: QueryParams) -> set[str]:
254
+ cursor = params.get("cursor")
255
+ return {cursor} if isinstance(cursor, str) else set()
256
+
257
+
258
+ def _advance_cursor(
259
+ has_more: bool,
260
+ next_cursor: str | None,
261
+ seen_cursors: set[str],
262
+ ) -> str | None:
263
+ if not has_more:
264
+ return None
265
+
266
+ from ._errors import TransportError
267
+
268
+ if next_cursor is None:
269
+ raise TransportError(
270
+ "Luranta returned has_more without the next cursor required to "
271
+ "continue pagination."
272
+ )
273
+ if next_cursor in seen_cursors:
274
+ raise TransportError(
275
+ "Luranta returned a repeated cursor; pagination stopped to prevent "
276
+ "duplicate billable reads."
277
+ )
278
+ seen_cursors.add(next_cursor)
279
+ return next_cursor
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from luranta_generated.models.problem_details import ProblemDetails
4
+
5
+ from ._transport import HttpResponse
6
+
7
+
8
+ class LurantaProblemError(Exception):
9
+ """An HTTP error represented by Luranta RFC 9457 Problem Details."""
10
+
11
+ def __init__(
12
+ self,
13
+ problem: ProblemDetails | None,
14
+ response: HttpResponse,
15
+ ) -> None:
16
+ self.problem = problem
17
+ self.response = response
18
+ self.status = response.status_code
19
+ self.code = problem.code if problem is not None else "unexpected_error_response"
20
+ self.request_id = (
21
+ str(problem.request_id)
22
+ if problem is not None
23
+ else response.header("X-Request-Id")
24
+ )
25
+ self.retryable = problem.retryable if problem is not None else False
26
+ detail = (
27
+ problem.detail
28
+ if problem is not None
29
+ else f"Luranta returned HTTP {response.status_code} without valid Problem Details."
30
+ )
31
+ super().__init__(detail)
32
+
33
+
34
+ class TransportError(Exception):
35
+ """A request failure without a trustworthy, parseable HTTP response."""
36
+