libredte-lib-sdk 0.5.2__tar.gz → 0.6.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 (94) hide show
  1. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/PKG-INFO +15 -3
  2. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/README.rst +14 -2
  3. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/book/builder.py +3 -2
  4. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/book/loader.py +3 -2
  5. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/book/models.py +5 -0
  6. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/common.py +30 -0
  7. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/document/__init__.py +8 -0
  8. libredte_lib_sdk-0.6.0/libredte_lib_sdk/billing/document/batch_processor.py +72 -0
  9. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/document/builder.py +15 -10
  10. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/document/dispatcher.py +9 -6
  11. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/document/loader.py +5 -2
  12. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/document/models.py +54 -6
  13. libredte_lib_sdk-0.6.0/libredte_lib_sdk/billing/document/parser.py +58 -0
  14. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/document/renderer.py +3 -2
  15. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/exchange/document_response.py +5 -4
  16. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/exchange/models.py +7 -0
  17. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/identifier/caf_faker.py +3 -2
  18. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/identifier/caf_loader.py +5 -2
  19. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/identifier/caf_validator.py +5 -2
  20. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/identifier/models.py +4 -0
  21. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/integration/models.py +32 -0
  22. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/integration/sii_dte.py +13 -10
  23. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/integration/sii_rcv.py +9 -8
  24. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/integration/sii_rtc.py +3 -2
  25. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/ownership_transfer/aec.py +3 -2
  26. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/ownership_transfer/models.py +4 -0
  27. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/trading_parties/mandatario_manager.py +5 -4
  28. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/trading_parties/models.py +8 -0
  29. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/client.py +52 -4
  30. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/exceptions.py +10 -0
  31. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/human_resources/payroll/renderer.py +3 -2
  32. libredte_lib_sdk-0.6.0/libredte_lib_sdk/response_registry.py +81 -0
  33. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/sdk.py +7 -0
  34. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/system/certificate/loader.py +3 -2
  35. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/pyproject.toml +1 -1
  36. libredte_lib_sdk-0.6.0/tests/fixtures/certificate_112223339.p12 +0 -0
  37. libredte_lib_sdk-0.6.0/tests/integration/test_batch_processor.py +96 -0
  38. libredte_lib_sdk-0.6.0/tests/integration/test_certificate_loader.py +55 -0
  39. libredte_lib_sdk-0.6.0/tests/integration/test_document_parser.py +44 -0
  40. libredte_lib_sdk-0.6.0/tests/integration/test_operation_not_found.py +26 -0
  41. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/tests/test_client.py +15 -0
  42. libredte_lib_sdk-0.6.0/tests/test_response_registry.py +170 -0
  43. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/tests/test_services.py +413 -33
  44. libredte_lib_sdk-0.5.2/.claude/scheduled_tasks.lock +0 -1
  45. libredte_lib_sdk-0.5.2/tests/integration/test_certificate_loader.py +0 -74
  46. libredte_lib_sdk-0.5.2/tests/integration/test_operation_not_found.py +0 -34
  47. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/.github/workflows/ci.yml +0 -0
  48. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/.github/workflows/docs.yml +0 -0
  49. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/.gitignore +0 -0
  50. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/.readthedocs.yaml +0 -0
  51. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/CLAUDE.md +0 -0
  52. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/LICENSE +0 -0
  53. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/Makefile +0 -0
  54. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/docs/Makefile +0 -0
  55. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/docs/conf.py +0 -0
  56. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/docs/index.rst +0 -0
  57. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/__init__.py +0 -0
  58. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/__init__.py +0 -0
  59. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/book/__init__.py +0 -0
  60. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/book/validator.py +0 -0
  61. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/document/examples.py +0 -0
  62. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/document/validator.py +0 -0
  63. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/enums.py +0 -0
  64. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/exchange/__init__.py +0 -0
  65. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/identifier/__init__.py +0 -0
  66. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/integration/__init__.py +0 -0
  67. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/ownership_transfer/__init__.py +0 -0
  68. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/billing/trading_parties/__init__.py +0 -0
  69. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/human_resources/__init__.py +0 -0
  70. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/human_resources/integration/__init__.py +0 -0
  71. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/human_resources/integration/previred_provider.py +0 -0
  72. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/human_resources/payroll/__init__.py +0 -0
  73. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/human_resources/payroll/calculator.py +0 -0
  74. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/py.typed +0 -0
  75. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/system/__init__.py +0 -0
  76. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/system/certificate/__init__.py +0 -0
  77. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/system/repository/__init__.py +0 -0
  78. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/libredte_lib_sdk/system/repository/catalog.py +0 -0
  79. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/tests/__init__.py +0 -0
  80. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/tests/conftest.py +0 -0
  81. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/tests/integration/__init__.py +0 -0
  82. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/tests/integration/conftest.py +0 -0
  83. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/tests/integration/test_book.py +0 -0
  84. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/tests/integration/test_caf.py +0 -0
  85. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/tests/integration/test_document_loader.py +0 -0
  86. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/tests/integration/test_document_rendering.py +0 -0
  87. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/tests/integration/test_document_validation.py +0 -0
  88. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/tests/integration/test_examples.py +0 -0
  89. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/tests/integration/test_exchange.py +0 -0
  90. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/tests/integration/test_human_resources.py +0 -0
  91. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/tests/integration/test_ownership_transfer.py +0 -0
  92. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/tests/integration/test_repository.py +0 -0
  93. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/tests/test_exceptions.py +0 -0
  94. {libredte_lib_sdk-0.5.2 → libredte_lib_sdk-0.6.0}/tests/test_models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: libredte-lib-sdk
3
- Version: 0.5.2
3
+ Version: 0.6.0
4
4
  Summary: SDK Python para la API de LibreDTE Lib (facturacion electronica de Chile).
5
5
  Project-URL: Homepage, https://www.libredte.cl
6
6
  Project-URL: Source, https://github.com/libredte/libredte-lib-sdk-python
@@ -187,14 +187,26 @@ entorno ``LIBREDTE_LIB_SDK_BASE_URL``:
187
187
  ``core.libredte.cl`` y ``pro.libredte.cl`` exponen el mismo contrato con
188
188
  límites de solicitudes distintos; ambos aceptan la variante ``LibreDTE
189
189
  (base_url=..., ...)`` con el resto de los parámetros de configuración
190
- (``api_token``, ``timeout``, ``http_client``).
190
+ (``api_token``, ``auth_scheme``, ``timeout``, ``http_client``).
191
191
 
192
192
  Autenticación
193
193
  ~~~~~~~~~~~~~
194
194
 
195
195
  ``LibreDTE(api_token=...)`` o la variable de entorno
196
196
  ``LIBREDTE_LIB_SDK_TOKEN`` — si está presente, se manda como header
197
- ``Authorization: Bearer <token>``.
197
+ ``Authorization: <esquema> <token>``. El esquema es ``Bearer`` por
198
+ defecto (lo que espera ``core.libredte.cl``/``pro.libredte.cl``);
199
+ configurable con ``LibreDTE(auth_scheme=...)`` o la variable de entorno
200
+ ``LIBREDTE_LIB_SDK_AUTH_SCHEME`` para hablar con otra API que use un
201
+ esquema distinto (ej. ``'Token'``):
202
+
203
+ .. code-block:: python
204
+
205
+ sdk = LibreDTE(
206
+ base_url='https://app.apigateway.cl/api/v2/libredte',
207
+ api_token='...',
208
+ auth_scheme='Token',
209
+ )
198
210
 
199
211
  Recursos de ``system``
200
212
  -----------------------
@@ -143,14 +143,26 @@ entorno ``LIBREDTE_LIB_SDK_BASE_URL``:
143
143
  ``core.libredte.cl`` y ``pro.libredte.cl`` exponen el mismo contrato con
144
144
  límites de solicitudes distintos; ambos aceptan la variante ``LibreDTE
145
145
  (base_url=..., ...)`` con el resto de los parámetros de configuración
146
- (``api_token``, ``timeout``, ``http_client``).
146
+ (``api_token``, ``auth_scheme``, ``timeout``, ``http_client``).
147
147
 
148
148
  Autenticación
149
149
  ~~~~~~~~~~~~~
150
150
 
151
151
  ``LibreDTE(api_token=...)`` o la variable de entorno
152
152
  ``LIBREDTE_LIB_SDK_TOKEN`` — si está presente, se manda como header
153
- ``Authorization: Bearer <token>``.
153
+ ``Authorization: <esquema> <token>``. El esquema es ``Bearer`` por
154
+ defecto (lo que espera ``core.libredte.cl``/``pro.libredte.cl``);
155
+ configurable con ``LibreDTE(auth_scheme=...)`` o la variable de entorno
156
+ ``LIBREDTE_LIB_SDK_AUTH_SCHEME`` para hablar con otra API que use un
157
+ esquema distinto (ej. ``'Token'``):
158
+
159
+ .. code-block:: python
160
+
161
+ sdk = LibreDTE(
162
+ base_url='https://app.apigateway.cl/api/v2/libredte',
163
+ api_token='...',
164
+ auth_scheme='Token',
165
+ )
154
166
 
155
167
  Recursos de ``system``
156
168
  -----------------------
@@ -8,6 +8,7 @@ from __future__ import annotations
8
8
  from typing import Any
9
9
 
10
10
  from ...client import ApiClient
11
+ from ...response_registry import build_response
11
12
  from ..trading_parties.models import Certificate
12
13
  from .models import BookBag
13
14
 
@@ -37,8 +38,8 @@ class BookBuilderService:
37
38
  .load()`, acá `BookBag.book` viene poblado (el libro construido
38
39
  y firmado, con su `xml`).
39
40
  """
40
- data = self._client.call(
41
+ response = self._client.call_response(
41
42
  self._BUILD_OPERATION,
42
43
  bag={**bag, 'certificate': certificate.to_payload()},
43
44
  )
44
- return BookBag.from_api(data)
45
+ return build_response(BookBag, response)
@@ -8,6 +8,7 @@ from __future__ import annotations
8
8
  from typing import Any
9
9
 
10
10
  from ...client import ApiClient
11
+ from ...response_registry import build_response
11
12
  from ..trading_parties.models import Certificate
12
13
  from .models import BookBag
13
14
 
@@ -35,8 +36,8 @@ class BookLoaderService:
35
36
  el libro); `BookBag.book_auth` viene poblado solo si
36
37
  `bag['emisor']` incluye `autorizacion_dte`.
37
38
  """
38
- data = self._client.call(
39
+ response = self._client.call_response(
39
40
  self._LOAD_OPERATION,
40
41
  bag={**bag, 'certificate': certificate.to_payload()},
41
42
  )
42
- return BookBag.from_api(data)
43
+ return build_response(BookBag, response)
@@ -9,7 +9,12 @@ import base64
9
9
  from dataclasses import dataclass
10
10
  from typing import Any, cast
11
11
 
12
+ from ...response_registry import api_response
12
13
 
14
+
15
+ @api_response(
16
+ 'libredte\\lib\\Core\\Package\\Billing\\Component\\Book\\Support\\BookBag',
17
+ )
13
18
  @dataclass(frozen=True, slots=True)
14
19
  class BookBag:
15
20
  """
@@ -6,6 +6,36 @@
6
6
  from __future__ import annotations
7
7
 
8
8
  import base64
9
+ from typing import Any
10
+
11
+
12
+ def encode_content(content: str | bytes) -> str:
13
+ """
14
+ Codifica en base64 un contenido para enviarlo a la API.
15
+
16
+ Un `bytes` se envía tal cual, sin alterar su codificación (lo correcto
17
+ para el contenido de un archivo). Un `str` ya es texto decodificado, así
18
+ que se codifica en UTF-8.
19
+ """
20
+ raw = content if isinstance(content, bytes) else content.encode('utf-8')
21
+
22
+ return base64.b64encode(raw).decode()
23
+
24
+
25
+ def encode_input_data(
26
+ input_data: str | bytes | dict[str, Any],
27
+ ) -> str | dict[str, Any]:
28
+ """
29
+ Prepara el `inputData` de una bolsa para enviarlo a la API.
30
+
31
+ Un `dict` son los datos en JSON y viaja tal cual. Todo lo demás es el
32
+ contenido original en otro formato (XML, YAML, texto, etc.) y viaja en
33
+ base64 (ver `encode_content()`).
34
+ """
35
+ if isinstance(input_data, dict):
36
+ return input_data
37
+
38
+ return encode_content(input_data)
9
39
 
10
40
 
11
41
  class XmlPayloadMixin:
@@ -6,29 +6,35 @@
6
6
  from __future__ import annotations
7
7
 
8
8
  from ...client import ApiClient
9
+ from .batch_processor import DocumentBatchProcessorService
9
10
  from .builder import DocumentBuilderService
10
11
  from .dispatcher import DocumentDispatcherService
11
12
  from .examples import DocumentExamplesService
12
13
  from .loader import DocumentLoaderService
13
14
  from .models import (
14
15
  DocumentBag,
16
+ DocumentBatch,
15
17
  DocumentEnvelope,
16
18
  Example,
17
19
  ExampleSummary,
18
20
  RenderedDocument,
19
21
  RenderResult,
20
22
  )
23
+ from .parser import DocumentParserService
21
24
  from .renderer import DocumentRendererService
22
25
  from .validator import DocumentValidatorService
23
26
 
24
27
  __all__ = [
25
28
  'DocumentBag',
29
+ 'DocumentBatch',
30
+ 'DocumentBatchProcessorService',
26
31
  'DocumentBuilderService',
27
32
  'DocumentComponent',
28
33
  'DocumentDispatcherService',
29
34
  'DocumentEnvelope',
30
35
  'DocumentExamplesService',
31
36
  'DocumentLoaderService',
37
+ 'DocumentParserService',
32
38
  'DocumentRendererService',
33
39
  'DocumentValidatorService',
34
40
  'Example',
@@ -49,3 +55,5 @@ class DocumentComponent:
49
55
  self.examples = DocumentExamplesService(client)
50
56
  self.validator = DocumentValidatorService(client)
51
57
  self.loader = DocumentLoaderService(client)
58
+ self.parser = DocumentParserService(client)
59
+ self.batch_processor = DocumentBatchProcessorService(client)
@@ -0,0 +1,72 @@
1
+ # Copyright (C) 2026 LibreDTE <https://www.libredte.cl>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """Servicio para `billing.document.batch_processor`."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from ...client import ApiClient
11
+ from ...response_registry import build_response
12
+ from ..common import encode_content
13
+ from .models import DocumentBatch
14
+
15
+
16
+ class DocumentBatchProcessorService:
17
+ """
18
+ Parsea un lote de documentos de una emisión masiva.
19
+
20
+ Solo cubre `parse`: entrega los documentos parseados, sin construirlos
21
+ (sin folios, CAF ni certificado).
22
+ """
23
+
24
+ _PARSE_OPERATION = 'billing.document.batch_processor::parse'
25
+
26
+ def __init__(self, client: ApiClient) -> None:
27
+ """Guarda el `ApiClient` compartido usado para llamar a la API."""
28
+ self._client = client
29
+
30
+ def parse(
31
+ self,
32
+ input_data: str | bytes,
33
+ *,
34
+ emisor: dict[str, Any] | None = None,
35
+ strategy: str | None = None,
36
+ complete: bool | None = None,
37
+ ) -> DocumentBatch:
38
+ """
39
+ Parsea el archivo de un lote y entrega las bolsas de sus documentos.
40
+
41
+ `input_data` es el contenido del archivo (CSV, XLSX, etc.). Se debe
42
+ pasar como `bytes` (leído en modo `rb`): se envía sin alterar. Un
43
+ `str` ya es texto decodificado y se envía en UTF-8.
44
+
45
+ `emisor` es un `dict` con `rut`/`razon_social` (y demás datos del
46
+ emisor): con `complete` activo (por defecto) se usan para completar
47
+ los datos del emisor de cada documento.
48
+
49
+ `strategy` es la estrategia del archivo (`'spreadsheet.csv'` por
50
+ defecto; `'spreadsheet.xlsx'` solo existe en Lib Pro). `complete`
51
+ indica si se completan los datos del documento con los del emisor
52
+ (por defecto la API lo hace).
53
+
54
+ Un error en el archivo lanza `LibreDteApiError` indicando la fila
55
+ (ej. ``Fila 3: ...``).
56
+ """
57
+ batch: dict[str, Any] = {'inputData': encode_content(input_data)}
58
+ if emisor is not None:
59
+ batch['emisor'] = emisor
60
+
61
+ options: dict[str, Any] = {}
62
+ if strategy is not None:
63
+ options['strategy'] = strategy
64
+ if complete is not None:
65
+ options['complete'] = complete
66
+ if options:
67
+ batch['options'] = {'batch_processor': options}
68
+
69
+ return build_response(
70
+ DocumentBatch,
71
+ self._client.call_response(self._PARSE_OPERATION, batch=batch),
72
+ )
@@ -8,6 +8,8 @@ from __future__ import annotations
8
8
  from typing import Any
9
9
 
10
10
  from ...client import ApiClient
11
+ from ...response_registry import build_response
12
+ from ..common import encode_input_data
11
13
  from ..trading_parties.models import Certificate
12
14
  from .models import DocumentBag
13
15
 
@@ -30,7 +32,7 @@ class DocumentBuilderService:
30
32
 
31
33
  def build_draft(
32
34
  self,
33
- input_data: str | dict[str, Any],
35
+ input_data: str | bytes | dict[str, Any],
34
36
  *,
35
37
  options: dict[str, Any] | None = None,
36
38
  ) -> DocumentBag:
@@ -39,25 +41,28 @@ class DocumentBuilderService:
39
41
 
40
42
  `input_data` es el `Encabezado`/`Detalle` (y demás nodos) del
41
43
  formato DTE del SII como `dict` — o datos en otro formato
42
- (XML/YAML, un formulario, etc.) junto con
44
+ (XML/YAML, un formulario, etc.), como `bytes` o `str`, junto con
43
45
  `options={'parser': {'strategy': '<estrategia>'}}` para indicar
44
- cómo parsearlos. Sin `options`, la API asume que `input_data` ya
46
+ cómo parsearlos. El contenido de un archivo se debe pasar como
47
+ `bytes` (leído en modo `rb`): se envía sin alterar su
48
+ codificación. Un `str` ya es texto decodificado y se envía en
49
+ UTF-8. Sin `options`, la API asume que `input_data` ya
45
50
  viene en el formato DTE del SII (estrategia `default.json`).
46
51
  `Encabezado.IdDoc.Folio` es opcional acá — un borrador sin folio
47
52
  no falla (queda con folio `0` en `DocumentBag.document_id`, sin
48
53
  nodo `Folio` en el XML). Sin CAF ni certificado, el resultado no
49
54
  queda timbrado (`DocumentBag.is_timbrado` es `False`).
50
55
  """
51
- bag: dict[str, Any] = {'inputData': input_data}
56
+ bag: dict[str, Any] = {'inputData': encode_input_data(input_data)}
52
57
  if options is not None:
53
58
  bag['options'] = options
54
59
 
55
- data = self._client.call(self._BUILD_OPERATION, bag=bag)
56
- return DocumentBag.from_api(data)
60
+ response = self._client.call_response(self._BUILD_OPERATION, bag=bag)
61
+ return build_response(DocumentBag, response)
57
62
 
58
63
  def build_signed(
59
64
  self,
60
- input_data: str | dict[str, Any],
65
+ input_data: str | bytes | dict[str, Any],
61
66
  *,
62
67
  options: dict[str, Any] | None = None,
63
68
  caf_xml: str,
@@ -75,11 +80,11 @@ class DocumentBuilderService:
75
80
  `IdentifierComponent.caf_faker` y
76
81
  `TradingPartiesComponent.mandatario_manager`.
77
82
  """
78
- bag: dict[str, Any] = {'inputData': input_data}
83
+ bag: dict[str, Any] = {'inputData': encode_input_data(input_data)}
79
84
  if options is not None:
80
85
  bag['options'] = options
81
86
  bag['caf'] = caf_xml
82
87
  bag['certificate'] = certificate.to_payload()
83
88
 
84
- data = self._client.call(self._BUILD_OPERATION, bag=bag)
85
- return DocumentBag.from_api(data)
89
+ response = self._client.call_response(self._BUILD_OPERATION, bag=bag)
90
+ return build_response(DocumentBag, response)
@@ -9,6 +9,7 @@ from collections.abc import Sequence
9
9
  from typing import Any, cast
10
10
 
11
11
  from ...client import ApiClient
12
+ from ...response_registry import build_response
12
13
  from .models import DocumentBag, DocumentEnvelope
13
14
 
14
15
 
@@ -37,11 +38,11 @@ class DocumentDispatcherService:
37
38
  `emisor` incluidos, no hace falta pasarlos aparte. El sobre
38
39
  resultante es lo que se envía al SII (`SiiDteService.send`).
39
40
  """
40
- data = self._client.call(
41
+ response = self._client.call_response(
41
42
  self._CREATE_OPERATION,
42
43
  bag=self._bag_payload(bag),
43
44
  )
44
- return DocumentEnvelope.from_api(data)
45
+ return build_response(DocumentEnvelope, response)
45
46
 
46
47
  def create_many(self, bags: Sequence[DocumentBag]) -> DocumentEnvelope:
47
48
  """
@@ -53,11 +54,11 @@ class DocumentDispatcherService:
53
54
  del PRIMER `DocumentBag` (el sobre en sí también se firma, no
54
55
  solo cada documento).
55
56
  """
56
- data = self._client.call(
57
+ response = self._client.call_response(
57
58
  self._CREATE_MANY_OPERATION,
58
59
  bags=[self._bag_payload(bag) for bag in bags],
59
60
  )
60
- return DocumentEnvelope.from_api(data)
61
+ return build_response(DocumentEnvelope, response)
61
62
 
62
63
  @staticmethod
63
64
  def _bag_payload(bag: DocumentBag) -> dict[str, Any]:
@@ -87,8 +88,10 @@ class DocumentDispatcherService:
87
88
  más — y en `caratula` los datos de la carátula ya presente en
88
89
  ese XML.
89
90
  """
90
- data = self._client.call(self._LOAD_XML_OPERATION, xml=xml_base64)
91
- return DocumentEnvelope.from_api(data)
91
+ response = self._client.call_response(
92
+ self._LOAD_XML_OPERATION, xml=xml_base64
93
+ )
94
+ return build_response(DocumentEnvelope, response)
92
95
 
93
96
  def validate(self, source: str) -> dict[str, Any]:
94
97
  """
@@ -6,6 +6,7 @@
6
6
  from __future__ import annotations
7
7
 
8
8
  from ...client import ApiClient
9
+ from ...response_registry import build_response
9
10
  from .models import DocumentBag
10
11
 
11
12
 
@@ -34,5 +35,7 @@ class DocumentLoaderService:
34
35
  acá, `document`/`document_type`/`document_stamp_base64`/etc.
35
36
  son el valor agregado real: los datos ya normalizados.
36
37
  """
37
- data = self._client.call(self._LOAD_XML_OPERATION, xml=xml_base64)
38
- return DocumentBag.from_api(data)
38
+ response = self._client.call_response(
39
+ self._LOAD_XML_OPERATION, xml=xml_base64
40
+ )
41
+ return build_response(DocumentBag, response)
@@ -15,18 +15,25 @@ import base64
15
15
  from dataclasses import dataclass
16
16
  from typing import Any
17
17
 
18
+ from ...response_registry import api_response
18
19
  from ..common import XmlPayloadMixin
19
20
  from ..identifier.models import Caf
20
21
  from ..trading_parties.models import Certificate
21
22
 
23
+ _PHP_SUPPORT = (
24
+ 'libredte\\lib\\Core\\Package\\Billing\\Component\\Document\\Support'
25
+ )
22
26
 
27
+
28
+ @api_response(f'{_PHP_SUPPORT}\\DocumentBag')
23
29
  @dataclass(frozen=True, slots=True)
24
30
  class DocumentBag:
25
31
  """
26
32
  Bolsa de un documento tributario.
27
33
 
28
- Devuelta por `document.builder::build`/`document.loader::loadXml`
29
- — ambas en esta misma forma. `document`
34
+ Devuelta por `document.builder::build`/`document.loader::loadXml`/
35
+ `document.parser::parse` — todas en esta misma forma, con los campos
36
+ que correspondan a cuánto se hizo con el documento. `document`
30
37
  (`Encabezado`/`Detalle`, con el merge de `libredte_data.extra.dte`
31
38
  si se pasó al renderizar) es la vista "de datos" limpia del
32
39
  documento — SIN los campos opcionales no usados (se omiten, no
@@ -66,13 +73,20 @@ class DocumentBag:
66
73
  (`document_stamp_base64` es el mismo dato, pero como XML) — `None`
67
74
  si el documento aún no está timbrado.
68
75
 
76
+ Una bolsa que solo se parseó (`DocumentParserService`, o cada bolsa de
77
+ un `DocumentBatch`) trae `document_parsed` (los datos en el formato
78
+ DTE del SII, tal como los entregó el parser) y deja en `None` lo que
79
+ requiere construir el documento: `document`, `document_type`, el XML,
80
+ etc.
81
+
69
82
  No usa `XmlPayloadMixin`: acá `xml_base64` es una `@property` (el
70
83
  documento puede no estar construido), no un campo simple.
71
84
  """
72
85
 
73
- document: dict[str, Any]
86
+ document: dict[str, Any] | None
87
+ document_parsed: dict[str, Any] | None
74
88
  document_normalized: dict[str, Any] | None
75
- document_type: dict[str, Any]
89
+ document_type: dict[str, Any] | None
76
90
  document_stamp_base64: str | None
77
91
  document_extra: dict[str, Any] | None
78
92
  document_auth: dict[str, Any] | None
@@ -116,9 +130,10 @@ class DocumentBag:
116
130
  def from_api(cls, data: dict[str, Any]) -> DocumentBag:
117
131
  """Construye un `DocumentBag` desde el `data` de la API."""
118
132
  return cls(
119
- document=data['document'],
133
+ document=data.get('document'),
134
+ document_parsed=data.get('document_parsed'),
120
135
  document_normalized=data.get('document_normalized'),
121
- document_type=data['document_type'],
136
+ document_type=data.get('document_type'),
122
137
  document_stamp_base64=data.get('document_stamp'),
123
138
  document_extra=data.get('document_extra'),
124
139
  document_auth=data.get('document_auth'),
@@ -136,6 +151,36 @@ class DocumentBag:
136
151
  )
137
152
 
138
153
 
154
+ @api_response(f'{_PHP_SUPPORT}\\DocumentBatch')
155
+ @dataclass(frozen=True, slots=True)
156
+ class DocumentBatch:
157
+ """
158
+ Lote de documentos de una emisión masiva, ya parseado.
159
+
160
+ Devuelto por `batch_processor::parse`. `document_bags` trae una bolsa
161
+ (`DocumentBag`) por cada documento del archivo, en el orden del archivo,
162
+ con los datos parseados en `document_parsed` — todavía no se construyó
163
+ ningún documento. No incluye el contenido enviado ni el certificado.
164
+ """
165
+
166
+ document_bags: tuple[DocumentBag, ...]
167
+ raw: dict[str, Any]
168
+
169
+ @classmethod
170
+ def from_api(cls, data: dict[str, Any]) -> DocumentBatch:
171
+ """Construye un `DocumentBatch` desde el `data` de la API."""
172
+ return cls(
173
+ document_bags=tuple(
174
+ DocumentBag.from_api(item)
175
+ for item in data.get('document_bags') or []
176
+ ),
177
+ raw=data,
178
+ )
179
+
180
+
181
+ @api_response(
182
+ 'libredte\\lib\\Core\\Package\\Billing\\Component\\Document\\Support\\DocumentEnvelope',
183
+ )
139
184
  @dataclass(frozen=True, slots=True)
140
185
  class DocumentEnvelope(XmlPayloadMixin):
141
186
  """
@@ -258,6 +303,9 @@ class Example:
258
303
  )
259
304
 
260
305
 
306
+ @api_response(
307
+ 'libredte\\lib\\Core\\Package\\System\\Component\\Rendering\\Support\\RenderResult',
308
+ )
261
309
  @dataclass(frozen=True, slots=True)
262
310
  class RenderResult:
263
311
  """
@@ -0,0 +1,58 @@
1
+ # Copyright (C) 2026 LibreDTE <https://www.libredte.cl>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """Servicio para `billing.document.parser`."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from ...client import ApiClient
11
+ from ...response_registry import build_response
12
+ from ..common import encode_input_data
13
+ from .models import DocumentBag
14
+
15
+
16
+ class DocumentParserService:
17
+ """
18
+ Parsea los datos de entrada de un documento (`billing.document.parser`).
19
+
20
+ Solo transforma los datos al formato DTE del SII: no normaliza ni
21
+ construye el documento, no necesita CAF ni certificado.
22
+ """
23
+
24
+ _PARSE_OPERATION = 'billing.document.parser::parse'
25
+
26
+ def __init__(self, client: ApiClient) -> None:
27
+ """Guarda el `ApiClient` compartido usado para llamar a la API."""
28
+ self._client = client
29
+
30
+ def parse(
31
+ self,
32
+ input_data: str | bytes | dict[str, Any],
33
+ *,
34
+ strategy: str | None = None,
35
+ ) -> DocumentBag:
36
+ """
37
+ Parsea `input_data` y entrega la bolsa con los datos parseados.
38
+
39
+ `input_data` es el documento en el formato DTE del SII como `dict`,
40
+ o datos en otro formato (XML, YAML, un formulario, etc.) como `bytes`
41
+ o `str`. El contenido de un archivo se debe pasar como `bytes` (leído
42
+ en modo `rb`): se envía sin alterar su codificación. Un `str` ya es
43
+ texto decodificado y se envía en UTF-8.
44
+
45
+ `strategy` es la estrategia de parseo (ej. `'default.xml'`,
46
+ `'form.lite'`); sin ella la API asume `default.json`.
47
+
48
+ El resultado es una `DocumentBag` con `document_parsed`; `document`,
49
+ `document_type` y el XML quedan en `None`.
50
+ """
51
+ bag: dict[str, Any] = {'inputData': encode_input_data(input_data)}
52
+ if strategy is not None:
53
+ bag['options'] = {'parser': {'strategy': strategy}}
54
+
55
+ return build_response(
56
+ DocumentBag,
57
+ self._client.call_response(self._PARSE_OPERATION, bag=bag),
58
+ )
@@ -8,6 +8,7 @@ from __future__ import annotations
8
8
  from typing import Any
9
9
 
10
10
  from ...client import ApiClient
11
+ from ...response_registry import build_response
11
12
  from .models import RenderResult
12
13
 
13
14
 
@@ -68,5 +69,5 @@ class DocumentRendererService:
68
69
  if libredte_data is not None:
69
70
  bag['libredteData'] = libredte_data
70
71
 
71
- data = self._client.call(self._RENDER_OPERATION, bag=bag)
72
- return RenderResult.from_api(data)
72
+ response = self._client.call_response(self._RENDER_OPERATION, bag=bag)
73
+ return build_response(RenderResult, response)
@@ -8,6 +8,7 @@ from __future__ import annotations
8
8
  from typing import Any, cast
9
9
 
10
10
  from ...client import ApiClient
11
+ from ...response_registry import build_response
11
12
  from ..trading_parties.models import Certificate
12
13
  from .models import EnvioRecibos, RespuestaEnvio
13
14
 
@@ -53,7 +54,7 @@ class DocumentResponseService:
53
54
  `MntTotal`/`Recinto`, y opcionalmente `RutFirma`/`Declaracion`).
54
55
  `caratula` trae `RutResponde`/`RutRecibe`.
55
56
  """
56
- data = self._client.call(
57
+ response = self._client.call_response(
57
58
  self._BUILD_ENVIO_RECIBOS_OPERATION,
58
59
  bag={
59
60
  'tipo': 'envio_recibos',
@@ -62,7 +63,7 @@ class DocumentResponseService:
62
63
  'certificate': certificate.to_payload(),
63
64
  },
64
65
  )
65
- return EnvioRecibos.from_api(data)
66
+ return build_response(EnvioRecibos, response)
66
67
 
67
68
  def build_respuesta_envio(
68
69
  self,
@@ -78,7 +79,7 @@ class DocumentResponseService:
78
79
  cual el formato SII. `caratula` trae `RutResponde`/`RutRecibe`/
79
80
  `IdRespuesta`.
80
81
  """
81
- result = self._client.call(
82
+ response = self._client.call_response(
82
83
  self._BUILD_RESPUESTA_ENVIO_OPERATION,
83
84
  bag={
84
85
  'tipo': 'respuesta_envio',
@@ -87,7 +88,7 @@ class DocumentResponseService:
87
88
  'certificate': certificate.to_payload(),
88
89
  },
89
90
  )
90
- return RespuestaEnvio.from_api(result)
91
+ return build_response(RespuestaEnvio, response)
91
92
 
92
93
  def validate_schema(self, source: str) -> dict[str, Any]:
93
94
  """
@@ -8,9 +8,13 @@ from __future__ import annotations
8
8
  from dataclasses import dataclass
9
9
  from typing import Any
10
10
 
11
+ from ...response_registry import api_response
11
12
  from ..common import XmlPayloadMixin
12
13
 
13
14
 
15
+ @api_response(
16
+ 'libredte\\lib\\Core\\Package\\Billing\\Component\\Exchange\\Entity\\EnvioRecibos',
17
+ )
14
18
  @dataclass(frozen=True, slots=True)
15
19
  class EnvioRecibos(XmlPayloadMixin):
16
20
  """
@@ -33,6 +37,9 @@ class EnvioRecibos(XmlPayloadMixin):
33
37
  )
34
38
 
35
39
 
40
+ @api_response(
41
+ 'libredte\\lib\\Core\\Package\\Billing\\Component\\Exchange\\Entity\\RespuestaEnvio',
42
+ )
36
43
  @dataclass(frozen=True, slots=True)
37
44
  class RespuestaEnvio(XmlPayloadMixin):
38
45
  """