ecuafact 1.0.0b1__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.

Potentially problematic release.


This version of ecuafact might be problematic. Click here for more details.

ecuafact/__init__.py ADDED
@@ -0,0 +1,97 @@
1
+ """Cliente oficial del API publico Ecuafact (contrato v1)."""
2
+
3
+ from .client import EcuafactClient
4
+ from .contribuyente import EcuafactContribuyente
5
+ from .errors import EcuafactApiException, EcuafactSdkException
6
+ from .models import (
7
+ Admission,
8
+ ApiError,
9
+ BusquedaContribuyente,
10
+ CampoAdicional,
11
+ ClaveAccesoDecodificada,
12
+ Comprobante,
13
+ ComprobanteRequest,
14
+ Compensacion,
15
+ Contexto,
16
+ Contribuyente,
17
+ ContribuyenteConsulta,
18
+ Detalle,
19
+ Destinatario,
20
+ DestinoSustitutiva,
21
+ DocsSustento,
22
+ EmisionResultado,
23
+ EstadoTributario,
24
+ EstablecimientoConsulta,
25
+ EstablecimientosContribuyente,
26
+ Impuesto,
27
+ ImpuestoDocSustento,
28
+ InfoDocumento,
29
+ InfoSustitutivaGuia,
30
+ InfoTributaria,
31
+ ListadoRequest,
32
+ MaquinaFiscal,
33
+ Motivo,
34
+ Operation,
35
+ Pago,
36
+ PaginaComprobantes,
37
+ QuotaBucket,
38
+ RepresentanteLegalConsulta,
39
+ RetencionFacturaLinea,
40
+ RetencionLinea,
41
+ ReembolsoDetalle,
42
+ RubroTercero,
43
+ TipoNegociable,
44
+ ValidacionIdentificacion,
45
+ )
46
+ from .options import EcuafactClientOptions, ResolvedOptions, resolve_options
47
+ from .webhooks import sign, verify
48
+
49
+ __all__ = [
50
+ "EcuafactClient",
51
+ "EcuafactContribuyente",
52
+ "EcuafactApiException",
53
+ "EcuafactSdkException",
54
+ "EcuafactClientOptions",
55
+ "ResolvedOptions",
56
+ "resolve_options",
57
+ "sign",
58
+ "verify",
59
+ "Admission",
60
+ "ApiError",
61
+ "BusquedaContribuyente",
62
+ "CampoAdicional",
63
+ "ClaveAccesoDecodificada",
64
+ "Comprobante",
65
+ "ComprobanteRequest",
66
+ "Compensacion",
67
+ "Contexto",
68
+ "Contribuyente",
69
+ "ContribuyenteConsulta",
70
+ "Detalle",
71
+ "Destinatario",
72
+ "DestinoSustitutiva",
73
+ "DocsSustento",
74
+ "EmisionResultado",
75
+ "EstadoTributario",
76
+ "EstablecimientoConsulta",
77
+ "EstablecimientosContribuyente",
78
+ "Impuesto",
79
+ "ImpuestoDocSustento",
80
+ "InfoDocumento",
81
+ "InfoSustitutivaGuia",
82
+ "InfoTributaria",
83
+ "ListadoRequest",
84
+ "MaquinaFiscal",
85
+ "Motivo",
86
+ "Operation",
87
+ "Pago",
88
+ "PaginaComprobantes",
89
+ "QuotaBucket",
90
+ "RepresentanteLegalConsulta",
91
+ "RetencionFacturaLinea",
92
+ "RetencionLinea",
93
+ "ReembolsoDetalle",
94
+ "RubroTercero",
95
+ "TipoNegociable",
96
+ "ValidacionIdentificacion",
97
+ ]
ecuafact/client.py ADDED
@@ -0,0 +1,436 @@
1
+ """Cliente HTTP del API publico Ecuafact para una credencial de integracion."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime as _datetime
6
+ import json
7
+ import random
8
+ import time
9
+ import uuid
10
+ from email.utils import parsedate_to_datetime
11
+ from typing import TypeVar
12
+ from urllib.parse import quote
13
+
14
+ import httpx
15
+ from pydantic import TypeAdapter, ValidationError
16
+
17
+ from .errors import EcuafactApiException, EcuafactSdkException
18
+ from .models import (
19
+ Admission,
20
+ ApiError,
21
+ BusquedaContribuyente,
22
+ ClaveAccesoDecodificada,
23
+ ComprobanteRequest,
24
+ Contexto,
25
+ ContribuyenteConsulta,
26
+ EmisionResultado,
27
+ EstablecimientosContribuyente,
28
+ ListadoRequest,
29
+ Operation,
30
+ PaginaComprobantes,
31
+ QuotaBucket,
32
+ ValidacionIdentificacion,
33
+ ArchivoComprobante,
34
+ PerfilEmisor,
35
+ PerfilEmisorUpdate,
36
+ CorreoEnviado,
37
+ CatalogoItem,
38
+ )
39
+ from .options import EcuafactClientOptions, ResolvedOptions, resolve_options
40
+
41
+ TRANSIENT_STATUS = {408, 425, 429, 500, 502, 503, 504}
42
+ T = TypeVar("T")
43
+
44
+
45
+ class EcuafactClient:
46
+ """Cliente del API publico Ecuafact para una credencial de integracion."""
47
+
48
+ def __init__(self, options: EcuafactClientOptions) -> None:
49
+ self.options: ResolvedOptions = resolve_options(options)
50
+ self._http = httpx.Client(
51
+ base_url=self.options.base_address,
52
+ timeout=self.options.timeout,
53
+ headers={
54
+ "X-Api-Key": self.options.api_key,
55
+ "Accept": "application/json",
56
+ "User-Agent": self.options.user_agent,
57
+ },
58
+ transport=self.options.transport,
59
+ )
60
+
61
+ # -- ciclo de vida ------------------------------------------------------
62
+
63
+ def close(self) -> None:
64
+ self._http.close()
65
+
66
+ def __enter__(self) -> "EcuafactClient":
67
+ return self
68
+
69
+ def __exit__(self, *_exc: object) -> None:
70
+ self.close()
71
+
72
+ # -- API ----------------------------------------------------------------
73
+
74
+ def para(self, identificacion: str) -> "EcuafactContribuyente":
75
+ from .contribuyente import EcuafactContribuyente
76
+
77
+ return EcuafactContribuyente(self, identificacion)
78
+
79
+ def emitir(
80
+ self, comprobante: ComprobanteRequest, idempotency_key: str | None = None
81
+ ) -> EmisionResultado:
82
+ return self.emitir_en(self._require_identificacion(), comprobante, idempotency_key)
83
+
84
+ def emitir_en(
85
+ self,
86
+ identificacion: str,
87
+ comprobante: ComprobanteRequest,
88
+ idempotency_key: str | None = None,
89
+ ) -> EmisionResultado:
90
+ key = idempotency_key if idempotency_key else uuid.uuid4().hex
91
+ _validate_idempotency_key(key)
92
+ path = f"v1/contribuyentes/{_q(identificacion)}/comprobantes"
93
+ admission, correlation = self._send_meta("POST", path, comprobante, key, True, Admission)
94
+ return EmisionResultado(admission, key, correlation)
95
+
96
+ def get_operacion(self, id_operacion: str) -> Operation:
97
+ return self._send("GET", f"v1/operaciones/{id_operacion}", None, None, False, Operation)
98
+
99
+ def consultar_operacion(self, id_operacion: str) -> Operation:
100
+ """Igual que get_operacion. El API no expone un POST de consulta."""
101
+ return self.get_operacion(id_operacion)
102
+
103
+ def get_contexto(self) -> Contexto:
104
+ return self._send("GET", "v1/contexto", None, None, False, Contexto)
105
+
106
+ def descargar_ride(self, identificacion: str, clave_acceso: str) -> "ArchivoComprobante":
107
+ """Descarga el RIDE (PDF) de un comprobante emitido."""
108
+ return self._descargar(identificacion, clave_acceso, "ride")
109
+
110
+ def descargar_xml(self, identificacion: str, clave_acceso: str) -> "ArchivoComprobante":
111
+ """Descarga el XML. Revise provisional."""
112
+ return self._descargar(identificacion, clave_acceso, "xml")
113
+
114
+ def get_perfil(self, identificacion: str) -> "PerfilEmisor":
115
+ """Lee el perfil del emisor."""
116
+ return self._send("GET", self._ruta_perfil(identificacion), None, None, False, PerfilEmisor)
117
+
118
+ def enviar_correo(self, identificacion: str, clave_acceso: str, destinatario: str) -> CorreoEnviado:
119
+ """Reenvia el correo de un comprobante autorizado."""
120
+ path = f"v1/contribuyentes/{_q(identificacion)}/comprobantes/{_q(clave_acceso)}/correo"
121
+ response = self._http.request("POST", path, json={"destinatario": destinatario})
122
+ if response.status_code >= 400:
123
+ raise _build_api_exception(response.status_code, response.text, None, None)
124
+ datos = response.json().get("datos")
125
+ return CorreoEnviado.model_validate(datos)
126
+
127
+ def listar_catalogo(self, nombre: str) -> list[CatalogoItem]:
128
+ """Lee un catalogo publicado."""
129
+ return self._send("GET", f"v1/catalogos/{_q(nombre)}", None, None, False, CatalogoItem, list_result=True)
130
+
131
+ def actualizar_perfil(self, identificacion: str, cambios: PerfilEmisorUpdate) -> PerfilEmisor:
132
+ """Actualiza los campos editables del perfil."""
133
+ response = self._http.request(
134
+ "PUT",
135
+ self._ruta_perfil(identificacion),
136
+ json=cambios.model_dump(by_alias=True, exclude_none=True),
137
+ )
138
+ if response.status_code >= 400:
139
+ raise _build_api_exception(response.status_code, response.text, None, None)
140
+ datos = response.json().get("datos")
141
+ return PerfilEmisor.model_validate(datos)
142
+
143
+ def _ruta_perfil(self, identificacion: str) -> str:
144
+ return f"v1/contribuyentes/{_q(identificacion)}/perfil"
145
+
146
+ def _descargar(self, identificacion: str, clave_acceso: str, formato: str) -> ArchivoComprobante:
147
+ path = (
148
+ f"v1/contribuyentes/{_q(identificacion)}"
149
+ f"/comprobantes/{_q(clave_acceso)}/{formato}"
150
+ )
151
+ max_attempts = max(1, self.options.max_attempts) if self.options.retry_transient_failures else 1
152
+ response = None
153
+ for attempt in range(1, max_attempts + 1):
154
+ try:
155
+ response = self._http.request("GET", path)
156
+ except httpx.HTTPError as exc:
157
+ if attempt < max_attempts:
158
+ _delay(self._compute_delay_ms(attempt, None))
159
+ continue
160
+ raise EcuafactSdkException("No hay conexion con el API.") from exc
161
+ if (
162
+ response.status_code >= 400
163
+ and response.status_code in TRANSIENT_STATUS
164
+ and attempt < max_attempts
165
+ ):
166
+ _delay(self._compute_delay_ms(attempt, response.headers.get("Retry-After")))
167
+ continue
168
+ break
169
+ if response is None:
170
+ raise EcuafactSdkException("No fue posible completar la solicitud con el API.")
171
+ if response.status_code >= 400:
172
+ raise _build_api_exception(
173
+ response.status_code,
174
+ response.text,
175
+ response.headers.get("X-Correlation-Id"),
176
+ None,
177
+ )
178
+ tipo = response.headers.get("Content-Type", "application/octet-stream").split(";")[0]
179
+ marca = response.headers.get("X-Contenido", "")
180
+ return ArchivoComprobante(response.content, tipo, marca.lower() == "provisional")
181
+
182
+ def get_consumo(self) -> QuotaBucket:
183
+ return self._send("GET", "v1/consumo", None, None, True, QuotaBucket)
184
+
185
+ def listar_emitidos(self, filtros: ListadoRequest | None = None) -> PaginaComprobantes:
186
+ return self._listar(self._require_identificacion(), False, filtros)
187
+
188
+ def listar_recibidos(self, filtros: ListadoRequest | None = None) -> PaginaComprobantes:
189
+ return self._listar(self._require_identificacion(), True, filtros)
190
+
191
+ def listar_emitidos_en(
192
+ self, identificacion: str, filtros: ListadoRequest | None = None
193
+ ) -> PaginaComprobantes:
194
+ return self._listar(identificacion, False, filtros)
195
+
196
+ def listar_recibidos_en(
197
+ self, identificacion: str, filtros: ListadoRequest | None = None
198
+ ) -> PaginaComprobantes:
199
+ return self._listar(identificacion, True, filtros)
200
+
201
+ def consultar_contribuyente(
202
+ self, identificacion: str, fusionar: bool = True, fuente: str | None = None
203
+ ) -> ContribuyenteConsulta:
204
+ if not identificacion or not identificacion.strip():
205
+ raise EcuafactSdkException("La identificacion es obligatoria.")
206
+ flag = "true" if fusionar else "false"
207
+ path = f"v1/consultas/contribuyentes/{_q(identificacion)}?fusionar={flag}"
208
+ if fuente and fuente.strip():
209
+ path += f"&fuente={_q(fuente)}"
210
+ return self._send("GET", path, None, None, True, ContribuyenteConsulta)
211
+
212
+ def buscar_contribuyentes(
213
+ self,
214
+ apellidos: str,
215
+ nombres: str | None = None,
216
+ clase: str | None = None,
217
+ max: int | None = None,
218
+ ) -> list[BusquedaContribuyente]:
219
+ if not apellidos or not apellidos.strip():
220
+ raise EcuafactSdkException("apellidos es obligatorio.")
221
+ parts = [f"apellidos={_q(apellidos)}"]
222
+ if nombres and nombres.strip():
223
+ parts.append(f"nombres={_q(nombres)}")
224
+ if clase and clase.strip():
225
+ parts.append(f"clase={_q(clase)}")
226
+ if max is not None:
227
+ parts.append(f"max={max}")
228
+ path = "v1/consultas/contribuyentes?" + "&".join(parts)
229
+ return self._send_list("GET", path, BusquedaContribuyente)
230
+
231
+ def listar_establecimientos(self, ruc: str, filtro: str | None = None) -> EstablecimientosContribuyente:
232
+ if not ruc or not ruc.strip():
233
+ raise EcuafactSdkException("El RUC es obligatorio.")
234
+ path = f"v1/consultas/establecimientos/{_q(ruc)}"
235
+ if filtro and filtro.strip():
236
+ path += f"?filtro={_q(filtro)}"
237
+ return self._send("GET", path, None, None, True, EstablecimientosContribuyente)
238
+
239
+ def validar_identificacion(self, numero: str) -> ValidacionIdentificacion:
240
+ if not numero or not numero.strip():
241
+ raise EcuafactSdkException("El numero a validar es obligatorio.")
242
+ path = f"v1/consultas/identificacion/validar/{_q(numero)}"
243
+ return self._send("GET", path, None, None, True, ValidacionIdentificacion)
244
+
245
+ def decodificar_clave_acceso(self, clave_acceso: str) -> ClaveAccesoDecodificada:
246
+ if not clave_acceso or not clave_acceso.strip():
247
+ raise EcuafactSdkException("La clave de acceso es obligatoria.")
248
+ path = f"v1/consultas/comprobantes/decodificar/{_q(clave_acceso)}"
249
+ return self._send("GET", path, None, None, True, ClaveAccesoDecodificada)
250
+
251
+ # -- interno ------------------------------------------------------------
252
+
253
+ def _listar(
254
+ self,
255
+ identificacion: str,
256
+ recibidos: bool,
257
+ filtros: ListadoRequest | None,
258
+ ) -> PaginaComprobantes:
259
+ direccion = "recibidos" if recibidos else "emitidos"
260
+ path = (
261
+ f"v1/contribuyentes/{_q(identificacion)}"
262
+ f"/comprobantes/{direccion}{_build_query(filtros)}"
263
+ )
264
+ return self._send("GET", path, None, None, False, PaginaComprobantes)
265
+
266
+ def _send_list(self, method: str, path: str, model: type[T]) -> list[T]:
267
+ return self._send(method, path, None, None, True, model, list_result=True)
268
+
269
+ def _send(
270
+ self,
271
+ method: str,
272
+ path: str,
273
+ body: ComprobanteRequest | None,
274
+ idempotency_key: str | None,
275
+ flat: bool,
276
+ model: type[T],
277
+ list_result: bool = False,
278
+ ) -> T:
279
+ data, _ = self._send_meta(method, path, body, idempotency_key, flat, model, list_result)
280
+ return data
281
+
282
+ def _send_meta(
283
+ self,
284
+ method: str,
285
+ path: str,
286
+ body: ComprobanteRequest | None,
287
+ idempotency_key: str | None,
288
+ flat: bool,
289
+ model: type[T],
290
+ list_result: bool = False,
291
+ ) -> tuple[T, str | None]:
292
+ # Solo se reintentan solicitudes idempotentes: GET, o POST/PUT con Idempotency-Key.
293
+ retryable = method == "GET" or (
294
+ idempotency_key is not None and method in ("POST", "PUT")
295
+ )
296
+ max_attempts = (
297
+ max(1, self.options.max_attempts)
298
+ if self.options.retry_transient_failures and retryable
299
+ else 1
300
+ )
301
+ headers: dict[str, str] = {}
302
+ if idempotency_key:
303
+ headers["Idempotency-Key"] = idempotency_key
304
+ payload = body.model_dump(by_alias=True, exclude_none=True) if body is not None else None
305
+
306
+ for attempt in range(1, max_attempts + 1):
307
+ try:
308
+ response = self._http.request(method, path, json=payload, headers=headers)
309
+ except httpx.HTTPError as exc:
310
+ if attempt < max_attempts:
311
+ _delay(self._compute_delay_ms(attempt, None))
312
+ continue
313
+ raise EcuafactSdkException(
314
+ "No hay conexion con el API. Si estaba emitiendo, conserve la misma Idempotency-Key y reintente."
315
+ ) from exc
316
+
317
+ correlation = response.headers.get("X-Correlation-Id")
318
+ if response.status_code >= 400:
319
+ if response.status_code in TRANSIENT_STATUS and attempt < max_attempts:
320
+ _delay(self._compute_delay_ms(attempt, response.headers.get("Retry-After")))
321
+ continue
322
+ raise _build_api_exception(response.status_code, response.text, correlation, idempotency_key)
323
+
324
+ return _parse(response.text, flat, model, list_result), correlation
325
+
326
+ raise EcuafactSdkException("No fue posible completar la solicitud con el API.")
327
+
328
+ def _compute_delay_ms(self, attempt: int, retry_after: str | None) -> float:
329
+ millis = (200.0 * attempt) + random.uniform(0, 50.0 * attempt)
330
+ if retry_after and self.options.respect_retry_after:
331
+ value = _parse_retry_after_ms(retry_after)
332
+ if value is not None and value > millis:
333
+ millis = value
334
+ cap = self.options.max_retry_delay_ms
335
+ if cap and cap > 0 and millis > cap:
336
+ millis = cap
337
+ return millis
338
+
339
+ def _require_identificacion(self) -> str:
340
+ value = self.options.identificacion
341
+ if not value or not value.strip():
342
+ raise EcuafactSdkException(
343
+ "Falta el RUC por defecto (identificacion). Configurelo para integraciones de un solo RUC o use "
344
+ "emitir_en(ruc, ...) o client.para(ruc)."
345
+ )
346
+ return value
347
+
348
+
349
+ def _q(value: str) -> str:
350
+ return quote(value, safe="")
351
+
352
+
353
+ def _build_query(filtros: ListadoRequest | None) -> str:
354
+ if filtros is None:
355
+ return ""
356
+ parts: list[str] = []
357
+ for name, value in (
358
+ ("desde", filtros.desde),
359
+ ("hasta", filtros.hasta),
360
+ ("codDoc", filtros.cod_doc),
361
+ ("buscar", filtros.buscar),
362
+ ("pagina", filtros.pagina),
363
+ ("tamanoPagina", filtros.tamano_pagina),
364
+ ):
365
+ if value is None or value == "":
366
+ continue
367
+ parts.append(f"{name}={_q(str(value))}")
368
+ return "?" + "&".join(parts) if parts else ""
369
+
370
+
371
+ def _parse(text: str, flat: bool, model: type[T], list_result: bool = False) -> T:
372
+ if text is None or text == "":
373
+ raise EcuafactSdkException("El API no devolvio datos.")
374
+ try:
375
+ data = json.loads(text)
376
+ except ValueError as exc:
377
+ raise EcuafactSdkException("Respuesta del API no reconocida.") from exc
378
+ if not flat:
379
+ data = data.get("datos") if isinstance(data, dict) else None
380
+ if data is None:
381
+ raise EcuafactSdkException("El API no devolvio datos.")
382
+ try:
383
+ if list_result:
384
+ return TypeAdapter(list[model]).validate_python(data)
385
+ return model.model_validate(data)
386
+ except ValidationError as exc:
387
+ raise EcuafactSdkException("Respuesta del API no reconocida.") from exc
388
+
389
+
390
+ def _build_api_exception(
391
+ status: int, text: str, correlation: str | None, idempotency_key: str | None
392
+ ) -> EcuafactApiException:
393
+ error: ApiError | None = None
394
+ if text and text.strip():
395
+ try:
396
+ error = ApiError.model_validate_json(text)
397
+ except ValidationError:
398
+ error = None
399
+ return EcuafactApiException(
400
+ error.codigo if error else None,
401
+ error.mensaje if error else None,
402
+ status,
403
+ correlation,
404
+ (error.errores or []) if error else [],
405
+ idempotency_key,
406
+ )
407
+
408
+
409
+ def _validate_idempotency_key(key: str) -> None:
410
+ if len(key) > 128:
411
+ raise EcuafactSdkException("Idempotency-Key no puede exceder 128 caracteres.")
412
+ if any(ord(character) > 127 for character in key):
413
+ raise EcuafactSdkException("Idempotency-Key debe ser ASCII.")
414
+
415
+
416
+ def _delay(millis: float) -> None:
417
+ if millis > 0:
418
+ time.sleep(millis / 1000.0)
419
+
420
+
421
+ def _parse_retry_after_ms(value: str) -> float | None:
422
+ text = value.strip()
423
+ if not text:
424
+ return None
425
+ if text.isdigit():
426
+ return float(text) * 1000.0
427
+ try:
428
+ when = parsedate_to_datetime(text)
429
+ except (TypeError, ValueError):
430
+ return None
431
+ if when is None:
432
+ return None
433
+ if when.tzinfo is None:
434
+ when = when.replace(tzinfo=_datetime.timezone.utc)
435
+ delta = (when - _datetime.datetime.now(_datetime.timezone.utc)).total_seconds() * 1000.0
436
+ return delta if delta > 0 else None
@@ -0,0 +1,31 @@
1
+ """Vista de emision y consulta fijada a un RUC (multi-RUC)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from .errors import EcuafactSdkException
8
+ from .models import EmisionResultado, ListadoRequest, PaginaComprobantes, QuotaBucket
9
+
10
+ if TYPE_CHECKING:
11
+ from .client import EcuafactClient
12
+
13
+
14
+ class EcuafactContribuyente:
15
+ def __init__(self, client: "EcuafactClient", identificacion: str) -> None:
16
+ if not identificacion or not identificacion.strip():
17
+ raise EcuafactSdkException("El RUC es obligatorio.")
18
+ self._client = client
19
+ self.identificacion = identificacion
20
+
21
+ def emitir(self, comprobante, idempotency_key: str | None = None) -> EmisionResultado:
22
+ return self._client.emitir_en(self.identificacion, comprobante, idempotency_key)
23
+
24
+ def listar_emitidos(self, filtros: ListadoRequest | None = None) -> PaginaComprobantes:
25
+ return self._client.listar_emitidos_en(self.identificacion, filtros)
26
+
27
+ def listar_recibidos(self, filtros: ListadoRequest | None = None) -> PaginaComprobantes:
28
+ return self._client.listar_recibidos_en(self.identificacion, filtros)
29
+
30
+ def get_consumo(self) -> QuotaBucket:
31
+ return self._client.get_consumo()
ecuafact/errors.py ADDED
@@ -0,0 +1,26 @@
1
+ """Error local del SDK (configuracion, transporte o respuesta no reconocida)."""
2
+
3
+
4
+ class EcuafactSdkException(Exception):
5
+ """Error local del SDK."""
6
+
7
+
8
+ class EcuafactApiException(EcuafactSdkException):
9
+ """Error devuelto por el API con el contrato publico (plano)."""
10
+
11
+ def __init__(
12
+ self,
13
+ codigo: str | None,
14
+ mensaje: str | None,
15
+ estado_http: int,
16
+ id_seguimiento: str | None,
17
+ errores: list[str],
18
+ idempotency_key: str | None,
19
+ ) -> None:
20
+ super().__init__(mensaje or f"El API devolvio el estado HTTP {estado_http}.")
21
+ self.codigo = codigo
22
+ self.mensaje = mensaje
23
+ self.estado_http = estado_http
24
+ self.id_seguimiento = id_seguimiento
25
+ self.errores = errores
26
+ self.idempotency_key = idempotency_key