fiscalisto 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.
@@ -0,0 +1,83 @@
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+ # Public CA roots the local runtime needs (not keys).
27
+ !certs/*.pem
28
+
29
+ # debug
30
+ npm-debug.log*
31
+ yarn-debug.log*
32
+ yarn-error.log*
33
+ .pnpm-debug.log*
34
+
35
+ # env files (can opt-in for committing if needed)
36
+ .env*
37
+
38
+ # vercel
39
+ .vercel
40
+
41
+ # typescript
42
+ *.tsbuildinfo
43
+ next-env.d.ts
44
+
45
+ # sqlite database
46
+ /data/
47
+
48
+ # auth schema (generated at Docker build time)
49
+ schema.auth.sql
50
+
51
+ # cloudflare / vite
52
+ .dev.vars
53
+ .wrangler/
54
+ dist/
55
+ src/routeTree.gen.ts
56
+ .dev-login.txt
57
+
58
+ # agent worktrees
59
+ .claude/worktrees/
60
+
61
+ # Agents' scratch files
62
+ /.scratch/
63
+
64
+ # local QA runs (screenshots, scripts)
65
+ .qa/
66
+
67
+ .qa/
68
+ .tanstack/tmp/
69
+ test-results/
70
+ playwright-report/
71
+ # Local marketing material (screenshots, store listings), not part of the app
72
+ marketing-screenshots/
73
+
74
+ # Fumadocs MDX compilation cache
75
+ .source/
76
+
77
+ # SDKs
78
+ __pycache__/
79
+ *.egg-info/
80
+ /sdk/python/dist/
81
+ /sdk/php/vendor/
82
+ /sdk/php/composer.lock
83
+ /sdk/dist-packages/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fiscalisto
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.
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.5
2
+ Name: fiscalisto
3
+ Version: 0.1.0
4
+ Summary: SDK de la API de facturación electrónica (CFDI 4.0) de Fiscalisto, para Python.
5
+ Project-URL: Homepage, https://fiscalisto.com/docs/api/sdk/python
6
+ Project-URL: Documentation, https://fiscalisto.com/docs/api
7
+ Project-URL: Source, https://github.com/fiscalisto/fiscalisto-python
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: cfdi,factura,facturacion,fiscalisto,mexico,sat,timbrado
11
+ Classifier: Natural Language :: Spanish
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+
18
+ # fiscalisto
19
+
20
+ El SDK de la [API de facturación de Fiscalisto](https://fiscalisto.com/docs/api): timbre CFDI 4.0 (facturas, pagos, notas de crédito, nómina, retenciones y lotes) desde Python.
21
+
22
+ - Tipos (`TypedDict`) para cada campo, con su descripción y su nombre en el XML del SAT; `mypy` y su editor los revisan.
23
+ - `Idempotency-Key` automática en cada `POST`: un reintento nunca timbra, crea ni manda nada dos veces.
24
+ - Reintentos solo de lo que es seguro repetir.
25
+ - Listas que se recorren solas con `for`.
26
+ - Webhooks verificados en una línea.
27
+ - Sin dependencias: Python 3.10 o más reciente.
28
+
29
+ ```bash
30
+ pip install fiscalisto
31
+ ```
32
+
33
+ ```python
34
+ from fiscalisto import Fiscalisto
35
+
36
+ fiscalisto = Fiscalisto() # la llave viene de FISCALISTO_API_KEY
37
+
38
+ invoice = fiscalisto.invoices.create(
39
+ {
40
+ "customer": {"tax_id": "ICV060329BY0", "legal_name": "INMOBILIARIA CVA", "tax_system": "601", "zip": "33826"},
41
+ "payment_form": "03",
42
+ "items": [{"description": "Mensualidad", "product_key": "86121700", "quantity": 2, "unit_price": 2500}],
43
+ },
44
+ idempotency_key="pedido-A-1042",
45
+ )
46
+
47
+ print(invoice["uuid"], invoice["total"])
48
+ ```
49
+
50
+ La guía completa está en [fiscalisto.com/docs/api/sdk](https://fiscalisto.com/docs/api/sdk) y cada página de la [referencia](https://fiscalisto.com/docs/api) trae su ejemplo con el SDK.
51
+
52
+ ## Desarrollo
53
+
54
+ `src/fiscalisto/types.py` y `src/fiscalisto/_resources.py` se generan desde la especificación OpenAPI con `bun run openapi` en la raíz del repositorio; no los edite. Lo demás se escribe a mano.
55
+
56
+ ```bash
57
+ PYTHONPATH=src python3 -m unittest discover -s tests
58
+ mypy --strict src && ruff check src tests
59
+ ```
@@ -0,0 +1,42 @@
1
+ # fiscalisto
2
+
3
+ El SDK de la [API de facturación de Fiscalisto](https://fiscalisto.com/docs/api): timbre CFDI 4.0 (facturas, pagos, notas de crédito, nómina, retenciones y lotes) desde Python.
4
+
5
+ - Tipos (`TypedDict`) para cada campo, con su descripción y su nombre en el XML del SAT; `mypy` y su editor los revisan.
6
+ - `Idempotency-Key` automática en cada `POST`: un reintento nunca timbra, crea ni manda nada dos veces.
7
+ - Reintentos solo de lo que es seguro repetir.
8
+ - Listas que se recorren solas con `for`.
9
+ - Webhooks verificados en una línea.
10
+ - Sin dependencias: Python 3.10 o más reciente.
11
+
12
+ ```bash
13
+ pip install fiscalisto
14
+ ```
15
+
16
+ ```python
17
+ from fiscalisto import Fiscalisto
18
+
19
+ fiscalisto = Fiscalisto() # la llave viene de FISCALISTO_API_KEY
20
+
21
+ invoice = fiscalisto.invoices.create(
22
+ {
23
+ "customer": {"tax_id": "ICV060329BY0", "legal_name": "INMOBILIARIA CVA", "tax_system": "601", "zip": "33826"},
24
+ "payment_form": "03",
25
+ "items": [{"description": "Mensualidad", "product_key": "86121700", "quantity": 2, "unit_price": 2500}],
26
+ },
27
+ idempotency_key="pedido-A-1042",
28
+ )
29
+
30
+ print(invoice["uuid"], invoice["total"])
31
+ ```
32
+
33
+ La guía completa está en [fiscalisto.com/docs/api/sdk](https://fiscalisto.com/docs/api/sdk) y cada página de la [referencia](https://fiscalisto.com/docs/api) trae su ejemplo con el SDK.
34
+
35
+ ## Desarrollo
36
+
37
+ `src/fiscalisto/types.py` y `src/fiscalisto/_resources.py` se generan desde la especificación OpenAPI con `bun run openapi` en la raíz del repositorio; no los edite. Lo demás se escribe a mano.
38
+
39
+ ```bash
40
+ PYTHONPATH=src python3 -m unittest discover -s tests
41
+ mypy --strict src && ruff check src tests
42
+ ```
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.24"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "fiscalisto"
7
+ version = "0.1.0"
8
+ description = "SDK de la API de facturación electrónica (CFDI 4.0) de Fiscalisto, para Python."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ dependencies = []
14
+ keywords = ["cfdi", "factura", "facturacion", "sat", "mexico", "timbrado", "fiscalisto"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Natural Language :: Spanish",
18
+ "Operating System :: OS Independent",
19
+ "Typing :: Typed",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://fiscalisto.com/docs/api/sdk/python"
24
+ Documentation = "https://fiscalisto.com/docs/api"
25
+ Source = "https://github.com/fiscalisto/fiscalisto-python"
26
+
27
+ [tool.hatch.build.targets.wheel]
28
+ packages = ["src/fiscalisto"]
29
+
30
+ [tool.ruff]
31
+ line-length = 100
32
+ target-version = "py310"
33
+ extend-exclude = ["src/fiscalisto/types.py", "src/fiscalisto/_resources.py"]
34
+
35
+ [tool.mypy]
36
+ strict = true
37
+ python_version = "3.10"
38
+ mypy_path = "src"
@@ -0,0 +1,61 @@
1
+ """El SDK de la API de facturación de Fiscalisto.
2
+
3
+ from fiscalisto import Fiscalisto
4
+
5
+ fiscalisto = Fiscalisto() # la llave viene de FISCALISTO_API_KEY
6
+ factura = fiscalisto.invoices.create({
7
+ "customer": {"tax_id": "ICV060329BY0", "legal_name": "INMOBILIARIA CVA", "tax_system": "601", "zip": "33826"},
8
+ "payment_form": "03",
9
+ "items": [{"description": "Consultoría", "product_key": "80101500", "unit_price": 1000}],
10
+ })
11
+
12
+ https://fiscalisto.com/docs/api
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from functools import cached_property
18
+
19
+ from ._core import (
20
+ VERSION,
21
+ ErrorType,
22
+ FiscalistoError,
23
+ HttpRequest,
24
+ HttpResponse,
25
+ ListObject,
26
+ Transport,
27
+ urllib_transport,
28
+ )
29
+ from ._resources import FiscalistoApi
30
+ from .error_codes import ErrorCode, ErrorCodeValue
31
+ from ._webhooks import FiscalistoWebhookError, Webhooks, verify_webhook
32
+
33
+ __version__ = VERSION
34
+
35
+
36
+ class Fiscalisto(FiscalistoApi):
37
+ """El cliente de la API: Fiscalisto("fk_test_…") o, sin llave, FISCALISTO_API_KEY.
38
+
39
+ Opciones: base_url, max_retries (2), timeout (80 s) y transport (otro cliente HTTP)."""
40
+
41
+ @cached_property
42
+ def webhooks(self) -> Webhooks:
43
+ """Verificar lo que llega a su endpoint."""
44
+ return Webhooks()
45
+
46
+
47
+ __all__ = [
48
+ "VERSION",
49
+ "ErrorCode",
50
+ "ErrorCodeValue",
51
+ "ErrorType",
52
+ "Fiscalisto",
53
+ "FiscalistoError",
54
+ "FiscalistoWebhookError",
55
+ "HttpRequest",
56
+ "HttpResponse",
57
+ "ListObject",
58
+ "Transport",
59
+ "urllib_transport",
60
+ "verify_webhook",
61
+ ]
@@ -0,0 +1,330 @@
1
+ """Lo que hace cada llamada del SDK: la llave, la Idempotency-Key, los reintentos seguros, los
2
+ errores y las listas que se recorren solas. Los recursos (_resources.py) solo dicen qué ruta
3
+ llamar."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import os
9
+ import random
10
+ import re
11
+ import time
12
+ import uuid
13
+ from collections.abc import Callable, Iterator, Mapping
14
+ from dataclasses import dataclass, field
15
+ from typing import Any, Generic, Literal, TypeVar
16
+ from urllib.error import HTTPError
17
+ from urllib.parse import urlencode, urljoin
18
+ from urllib.request import Request as _UrllibRequest
19
+ from urllib.request import urlopen
20
+
21
+ VERSION = "0.1.0"
22
+ DEFAULT_BASE_URL = "https://api.fiscalisto.com"
23
+
24
+ ErrorType = Literal[
25
+ "invalid_request_error",
26
+ "authentication_error",
27
+ "permission_error",
28
+ "not_found_error",
29
+ "idempotency_error",
30
+ "rate_limit_error",
31
+ "invoice_error",
32
+ "balance_error",
33
+ "api_error",
34
+ "connection_error",
35
+ ]
36
+
37
+
38
+ class FiscalistoError(Exception):
39
+ """Un error de la API (o de la conexión, con type connection_error). code es estable:
40
+ decida con él; el mensaje es para mostrarlo a una persona."""
41
+
42
+ type: ErrorType
43
+ code: str
44
+ message: str
45
+ #: El campo que hay que corregir (items.0.unit_price).
46
+ param: str | None
47
+ #: El código del SAT o del PAC cuando ellos lo rechazaron (CFDI40145).
48
+ sat_code: str | None
49
+ #: El comprobante rechazado (inv_…), cuando lo hay.
50
+ invoice: str | None
51
+ doc_url: str | None
52
+ #: Búsquelo en Desarrolladores › Solicitudes o mándelo a soporte.
53
+ request_id: str | None
54
+ #: El código HTTP (sin él, la solicitud no llegó).
55
+ status: int | None
56
+ #: El objeto error tal como llegó.
57
+ raw: dict[str, Any]
58
+
59
+ def __init__(
60
+ self, raw: Mapping[str, Any], status: int | None = None, request_id: str | None = None
61
+ ) -> None:
62
+ def text(k: str) -> str | None:
63
+ v = raw.get(k)
64
+ return v if isinstance(v, str) else None
65
+
66
+ self.raw = dict(raw)
67
+ self.message = text("message") or "Error de la API de Fiscalisto"
68
+ self.type = text("type") or "api_error" # type: ignore[assignment]
69
+ self.code = text("code") or "unknown"
70
+ self.param = text("param")
71
+ self.sat_code = text("sat_code")
72
+ self.invoice = text("invoice")
73
+ self.doc_url = text("doc_url")
74
+ self.request_id = text("request_id") or request_id
75
+ self.status = status
76
+ super().__init__(self.message)
77
+
78
+ def __repr__(self) -> str:
79
+ return (
80
+ f"FiscalistoError(code={self.code!r}, status={self.status!r}, message={self.message!r})"
81
+ )
82
+
83
+
84
+ @dataclass
85
+ class HttpRequest:
86
+ """Lo que el SDK manda (para un transport propio)."""
87
+
88
+ method: str
89
+ url: str
90
+ headers: dict[str, str]
91
+ body: bytes | None
92
+ timeout: float
93
+
94
+
95
+ @dataclass
96
+ class HttpResponse:
97
+ """Lo que el transport contesta: headers con nombres en minúsculas."""
98
+
99
+ status: int
100
+ headers: dict[str, str] = field(default_factory=dict)
101
+ body: bytes = b""
102
+
103
+
104
+ Transport = Callable[[HttpRequest], HttpResponse]
105
+ """Otro cliente HTTP (pruebas, proxies). Lanza OSError (o TimeoutError) si no hubo respuesta."""
106
+
107
+
108
+ def urllib_transport(request: HttpRequest) -> HttpResponse:
109
+ """El transport de siempre: urllib de la biblioteca estándar."""
110
+ req = _UrllibRequest(
111
+ request.url, data=request.body, headers=request.headers, method=request.method
112
+ )
113
+ try:
114
+ with urlopen(req, timeout=request.timeout) as res:
115
+ return HttpResponse(
116
+ res.status, {k.lower(): v for k, v in res.headers.items()}, res.read()
117
+ )
118
+ except HTTPError as error:
119
+ with error:
120
+ return HttpResponse(
121
+ error.code,
122
+ {k.lower(): v for k, v in (error.headers or {}).items()},
123
+ error.read(),
124
+ )
125
+
126
+
127
+ T = TypeVar("T")
128
+
129
+
130
+ class ListObject(Generic[T]):
131
+ """Una lista: data y has_more son la primera página; recorrerla con for da todos los
132
+ objetos, página por página."""
133
+
134
+ def __init__(self, page: Callable[[str | None], dict[str, Any]]) -> None:
135
+ self._page = page
136
+ self._first: dict[str, Any] | None = None
137
+
138
+ def first_page(self) -> dict[str, Any]:
139
+ """La primera página, como la contesta la API (object, data, has_more, url)."""
140
+ if self._first is None:
141
+ self._first = self._page(None)
142
+ return self._first
143
+
144
+ @property
145
+ def data(self) -> list[T]:
146
+ return self.first_page()["data"] # type: ignore[no-any-return]
147
+
148
+ @property
149
+ def has_more(self) -> bool:
150
+ return bool(self.first_page()["has_more"])
151
+
152
+ def __iter__(self) -> Iterator[T]:
153
+ page = self.first_page()
154
+ while True:
155
+ items = page["data"]
156
+ yield from items
157
+ if not page["has_more"] or not items:
158
+ return
159
+ page = self._page(items[-1]["id"])
160
+
161
+ def to_list(self, max: int = 10_000) -> list[T]:
162
+ """Todos los objetos en una lista, hasta max (por omisión 10,000)."""
163
+ out: list[T] = []
164
+ for item in self:
165
+ if len(out) >= max:
166
+ break
167
+ out.append(item)
168
+ return out
169
+
170
+
171
+ def _backoff(attempt: int) -> float:
172
+ """0.5 s, 1 s, 2 s… hasta 8 s, con algo de azar para no llegar todos juntos."""
173
+ return min(8.0, 0.5 * 2.0**attempt) * (0.75 + random.random() / 2)
174
+
175
+
176
+ def _query_value(value: Any) -> str:
177
+ if value is True:
178
+ return "true"
179
+ if value is False:
180
+ return "false"
181
+ return str(value)
182
+
183
+
184
+ _KEY = re.compile(r"^fk_(live|test)_")
185
+
186
+
187
+ class Core:
188
+ """El cliente: la llave, la dirección de la API, los reintentos y el transport."""
189
+
190
+ def __init__(
191
+ self,
192
+ api_key: str | None = None,
193
+ *,
194
+ base_url: str | None = None,
195
+ max_retries: int = 2,
196
+ timeout: float = 80.0,
197
+ transport: Transport | None = None,
198
+ sleep: Callable[[float], None] | None = None,
199
+ ) -> None:
200
+ """api_key: su llave fk_test_… o fk_live_…; sin ella, la variable FISCALISTO_API_KEY.
201
+ timeout: segundos para cada respuesta (80 por omisión: el PAC puede tardar)."""
202
+ key = api_key if api_key is not None else os.environ.get("FISCALISTO_API_KEY")
203
+ if not key:
204
+ raise ValueError(
205
+ 'Falta la llave de la API: Fiscalisto("fk_test_…") o la variable '
206
+ "FISCALISTO_API_KEY. Créela en Desarrolladores › Llaves."
207
+ )
208
+ if not _KEY.match(key):
209
+ raise ValueError(
210
+ "La llave de la API empieza con fk_live_ o fk_test_. (Un secreto whsec_ es "
211
+ "para verificar webhooks.)"
212
+ )
213
+ self._key = key
214
+ self._base_url = base_url or DEFAULT_BASE_URL
215
+ self._max_retries = max_retries
216
+ self._timeout = timeout
217
+ self._transport = transport or urllib_transport
218
+ self._sleep = sleep or time.sleep
219
+
220
+ @property
221
+ def livemode(self) -> bool:
222
+ """True con una llave fk_live_ (producción), False con fk_test_ (pruebas)."""
223
+ return self._key.startswith("fk_live_")
224
+
225
+ def request(
226
+ self,
227
+ method: str,
228
+ path: str,
229
+ *,
230
+ body: Any = None,
231
+ query: Mapping[str, Any] | None = None,
232
+ idempotent: bool = False,
233
+ result: Literal["json", "bytes", "text"] = "json",
234
+ idempotency_key: str | None = None,
235
+ timeout: float | None = None,
236
+ max_retries: int | None = None,
237
+ ) -> Any:
238
+ url = urljoin(self._base_url, path)
239
+ if query:
240
+ pairs = [(k, _query_value(v)) for k, v in query.items() if v is not None]
241
+ if pairs:
242
+ url = f"{url}?{urlencode(pairs)}"
243
+ headers = {
244
+ "authorization": f"Bearer {self._key}",
245
+ "user-agent": f"fiscalisto-python/{VERSION}",
246
+ "accept": "application/json",
247
+ }
248
+ key = (idempotency_key or str(uuid.uuid4())) if idempotent else None
249
+ if key:
250
+ headers["idempotency-key"] = key
251
+ data: bytes | None = None
252
+ if method == "POST":
253
+ headers["content-type"] = "application/json"
254
+ data = json.dumps(body if body is not None else {}, ensure_ascii=False).encode()
255
+ # Only what can't do something twice is repeated after a failure we can't see through:
256
+ # reads, deletes and requests with an Idempotency-Key. A 429 was never processed.
257
+ safe = method in ("GET", "DELETE") or key is not None
258
+ retries = self._max_retries if max_retries is None else max_retries
259
+ wait = self._timeout if timeout is None else timeout
260
+
261
+ attempt = 0
262
+ while True:
263
+ try:
264
+ res = self._transport(HttpRequest(method, url, dict(headers), data, wait))
265
+ except OSError as error:
266
+ if safe and attempt < retries:
267
+ self._sleep(_backoff(attempt))
268
+ attempt += 1
269
+ continue
270
+ timed_out = isinstance(error, TimeoutError) or "timed out" in str(error)
271
+ hint = (
272
+ f" Repita con la misma Idempotency-Key ({key}) para saber si se timbró."
273
+ if key
274
+ else ""
275
+ )
276
+ raise FiscalistoError(
277
+ {
278
+ "type": "connection_error",
279
+ "code": "timeout" if timed_out else "connection_failed",
280
+ "message": f"La API no contestó en {wait:g} s.{hint}"
281
+ if timed_out
282
+ else f"No se pudo conectar con la API: {error}",
283
+ "idempotency_key": key,
284
+ }
285
+ ) from error
286
+ request_id = res.headers.get("request-id")
287
+ if 200 <= res.status < 300:
288
+ if result == "bytes":
289
+ return res.body
290
+ if result == "text":
291
+ return res.body.decode("utf-8")
292
+ return json.loads(res.body)
293
+ try:
294
+ parsed = json.loads(res.body)
295
+ raw = parsed.get("error") if isinstance(parsed, dict) else None
296
+ except ValueError:
297
+ raw = None
298
+ raw = raw if isinstance(raw, dict) else {}
299
+ try:
300
+ retry_after = float(res.headers.get("retry-after", ""))
301
+ except ValueError:
302
+ retry_after = 0.0
303
+ retryable = (
304
+ res.status == 429
305
+ or raw.get("code") == "request_in_progress"
306
+ or (safe and res.status >= 500)
307
+ )
308
+ if retryable and attempt < retries and not retry_after > 60:
309
+ self._sleep(retry_after if retry_after > 0 else _backoff(attempt))
310
+ attempt += 1
311
+ continue
312
+ raise FiscalistoError(raw, res.status, request_id)
313
+
314
+ def list(
315
+ self,
316
+ path: str,
317
+ query: Mapping[str, Any] | None,
318
+ *,
319
+ timeout: float | None = None,
320
+ max_retries: int | None = None,
321
+ ) -> ListObject[Any]:
322
+ def page(starting_after: str | None) -> dict[str, Any]:
323
+ q = dict(query or {})
324
+ if starting_after:
325
+ q["starting_after"] = starting_after
326
+ return self.request( # type: ignore[no-any-return]
327
+ "GET", path, query=q, timeout=timeout, max_retries=max_retries
328
+ )
329
+
330
+ return ListObject(page)