factusmart 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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FactuSmart
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,239 @@
1
+ Metadata-Version: 2.4
2
+ Name: factusmart
3
+ Version: 0.1.0
4
+ Summary: SDK oficial de Python del API de facturación electrónica de FactuSmart (SUNAT, Perú).
5
+ Author: FactuSmart
6
+ License: MIT
7
+ Project-URL: Documentacion, https://factusmart.pe/api-facturacion-electronica/docs/sdks
8
+ Project-URL: Repositorio, https://github.com/factusmart/factusmart-python
9
+ Keywords: factusmart,sunat,facturacion electronica,peru,cpe,boleta,factura
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Natural Language :: Spanish
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Office/Business :: Financial :: Accounting
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.8
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Dynamic: license-file
29
+
30
+ # FactuSmart — SDK de Python
31
+
32
+ SDK oficial del **API de facturación electrónica de FactuSmart** (SUNAT, Perú):
33
+ facturas, boletas, notas de crédito/débito, guías de remisión (GRE 2.0) y
34
+ anulaciones.
35
+
36
+ - Python 3.8 o superior.
37
+ - **Cero dependencias**: solo librería estándar (`urllib`).
38
+ - Con type hints (`py.typed`).
39
+ - Documentación completa del API: https://factusmart.pe/api-facturacion-electronica/docs
40
+
41
+ ## Instalar
42
+
43
+ ```bash
44
+ pip install factusmart
45
+ ```
46
+
47
+ ## Uso en 10 líneas
48
+
49
+ ```python
50
+ import factusmart
51
+
52
+ fs = factusmart.Cliente(api_key="TU_API_KEY", base_url="https://TU_URL_BASE/api/v1/issuer", ruc="20123456789")
53
+
54
+ calc = factusmart.calcular_comprobante([
55
+ {"descripcion": "Servicio de consultoría", "cantidad": 1, "valor_unitario": 100, "afectacion": "10"},
56
+ ])
57
+
58
+ r = fs.comprobantes.emitir({
59
+ "codigo_tipo_documento": "01",
60
+ "datos_del_cliente_o_receptor": {"codigo_tipo_documento_identidad": "6", "numero_documento": "20555555555"},
61
+ "items": calc["items"], "totales": calc["totales"],
62
+ })
63
+
64
+ print(r["aceptado_por_sunat"], r["serie_numero"])
65
+ ```
66
+
67
+ `base_url` **no es igual para todas las cuentas**: cada cuenta vive en un
68
+ servidor distinto. Cópiala de tu panel de integrador (pestaña Configuración,
69
+ "URL base de la API").
70
+
71
+ ## Opciones del cliente
72
+
73
+ | Opción | Obligatoria | Por defecto | Para qué |
74
+ |---|---|---|---|
75
+ | `api_key` | sí | — | Tu API key (va en el header `X-API-Key`) |
76
+ | `base_url` | sí | — | URL base de tu cuenta, sin barra final |
77
+ | `ruc` | no | — | RUC emisor por defecto. Se cambia en cada llamada con `ruc=` |
78
+ | `timeout` | no | `60` | Segundos por petición (SUNAT a veces tarda) |
79
+ | `max_reintentos` | no | `3` | Ver "Reintentos e idempotencia" |
80
+ | `verificar_ssl` | no | `True` | `False` **solo** en pruebas locales con certificado propio |
81
+
82
+ Si falta `api_key` o `base_url`, el cliente lanza `ValueError` al crearse.
83
+
84
+ ## Métodos
85
+
86
+ Los datos se envían y se reciben **tal cual los define el API** (claves en
87
+ español con guion bajo). Todos los métodos que operan sobre un RUC aceptan
88
+ `ruc=` para cambiar el del constructor (en GET va como `?ruc=`; en POST va en el
89
+ cuerpo, y si tú ya pusiste `ruc` en los datos, se respeta el tuyo).
90
+
91
+ | Método | Endpoint | Devuelve |
92
+ |---|---|---|
93
+ | `fs.ping(ruc=None)` | `GET /ping` | dict |
94
+ | `fs.cuenta.saldo()` | `GET /account` | dict |
95
+ | `fs.series.listar(ruc=None)` | `GET /series` | dict |
96
+ | `fs.comprobantes.emitir(datos, ruc=None, idempotency_key=None)` | `POST /documents` | dict |
97
+ | `fs.comprobantes.listar(filtros=None, ruc=None)` | `GET /documents` | dict |
98
+ | `fs.comprobantes.consultar(external_id, ruc=None)` | `GET /documents/{id}` | dict |
99
+ | `fs.comprobantes.descargar(external_id, tipo, ruc=None)` | `GET /documents/{id}/{pdf\|xml\|cdr}` | **bytes** |
100
+ | `fs.comprobantes.reenviar(external_id, ruc=None)` | `POST /documents/{id}/reenviar` | dict |
101
+ | `fs.comprobantes.consultar_sunat(external_id, ruc=None)` | `POST /documents/{id}/consultar-sunat` | dict |
102
+ | `fs.comprobantes.historial(external_id, ruc=None)` | `GET /documents/{id}/historial` | dict |
103
+ | `fs.anulaciones.crear(datos, ruc=None)` | `POST /anulaciones` | dict |
104
+ | `fs.anulaciones.estado(ticket, ruc=None)` | `GET /anulaciones/{ticket}/status` | dict |
105
+ | `fs.guias.emitir(datos, ruc=None)` | `POST /guias` | dict |
106
+ | `fs.guias.estado(external_id, ruc=None)` | `GET /guias/{id}/status` | dict |
107
+ | `fs.rucs.listar()` | `GET /rucs` | dict |
108
+ | `fs.rucs.crear(datos)` | `POST /rucs` | dict |
109
+ | `fs.rucs.activar(ruc, activo)` | `PATCH /rucs/{ruc}/activation` | dict |
110
+
111
+ Filtros de `comprobantes.listar`: `estado`, `desde`, `hasta` (YYYY-MM-DD),
112
+ `limite` (1 a 100) y `antes_de` (pasa el `siguiente_cursor` de la página
113
+ anterior).
114
+
115
+ Guardar el PDF:
116
+
117
+ ```python
118
+ with open("comprobante.pdf", "wb") as f:
119
+ f.write(fs.comprobantes.descargar(r["external_id"], "pdf"))
120
+ ```
121
+
122
+ ### Lo que `comprobantes.emitir` completa por ti
123
+
124
+ Solo si no lo mandas:
125
+
126
+ - `numero_documento`: `"#"` (FactuSmart asigna el correlativo). Si mandas tu
127
+ propio número, se respeta.
128
+ - `fecha_de_emision` y `hora_de_emision`: fecha y hora **de Perú**
129
+ (America/Lima), no la de tu servidor.
130
+ - `codigo_tipo_moneda`: `"PEN"`.
131
+ - `codigo_tipo_operacion`: `"0101"` en facturas y boletas (en notas no aplica).
132
+
133
+ `guias.emitir` completa igual `numero_documento`, `fecha_de_emision` y
134
+ `hora_de_emision`.
135
+
136
+ ## Calcular IGV y totales
137
+
138
+ El API espera los importes ya calculados. `calcular_comprobante` lo hace por ti:
139
+ recibe cada ítem con `descripcion`, `cantidad`, `valor_unitario` (**sin IGV**) y
140
+ `afectacion`, y devuelve `{"items": [...], "totales": {...}}` listo para enviar.
141
+
142
+ | `afectacion` | Significado | IGV |
143
+ |---|---|---|
144
+ | `"10"` | Gravado | 18% |
145
+ | `"20"` | Exonerado | 0 |
146
+ | `"30"` | Inafecto | 0 |
147
+ | `"40"` | Exportación | 0 (va en `total_exportacion`) |
148
+
149
+ Opcionales por ítem: `codigo_interno` (recomendado: el SKU de tu sistema) y
150
+ `unidad_de_medida` (por defecto `NIU`; usa `ZZ` para servicios).
151
+
152
+ Redondea a 2 decimales por ítem con `decimal.Decimal` y ROUND_HALF_UP, así da
153
+ **exactamente los mismos números** que los SDK de PHP y Node.
154
+
155
+ ## La regla de oro: revisa `aceptado_por_sunat`
156
+
157
+ Un HTTP 200 significa que FactuSmart **generó** tu comprobante, no que SUNAT lo
158
+ aceptó. SUNAT puede estar caída o rechazarlo: recibirás `estado: "01"` con
159
+ `aceptado_por_sunat: False` y el motivo en `sunat["descripcion"]`.
160
+
161
+ **Eso NO es una excepción.** El SDK te devuelve la respuesta tal cual y tú
162
+ decides:
163
+
164
+ ```python
165
+ r = fs.comprobantes.emitir(datos)
166
+ if not r["aceptado_por_sunat"]:
167
+ print("SUNAT no lo aceptó todavía:", r.get("sunat"), r.get("accion_requerida"))
168
+ # corrige y reintenta gratis con: fs.comprobantes.reenviar(r["external_id"])
169
+ ```
170
+
171
+ Mira también `entorno` (`"prueba"` o `"produccion"`) y `tiene_validez_fiscal`.
172
+
173
+ ## Errores
174
+
175
+ Todas las excepciones están en `factusmart.errores` (y también en `factusmart`)
176
+ y heredan de `FactuSmartError`, que trae:
177
+
178
+ - `status`: código HTTP (`None` si fue un error de red),
179
+ - `mensaje`: el mensaje del API, en español,
180
+ - `respuesta`: el cuerpo completo de la respuesta.
181
+
182
+ | HTTP | Excepción |
183
+ |---|---|
184
+ | 401 | `ErrorDeAutenticacion` |
185
+ | 402 | `SinCreditos` |
186
+ | 403 | `RucNoAutorizado` |
187
+ | 404 | `NoEncontrado` |
188
+ | 422 | `ErrorDeValidacion` |
189
+ | 429 | `LimiteDePeticiones` (con `segundos_de_espera` si se pudo leer) |
190
+ | otros / red | `FactuSmartError` |
191
+
192
+ ```python
193
+ from factusmart import errores
194
+
195
+ try:
196
+ r = fs.comprobantes.emitir(datos)
197
+ except errores.ErrorDeValidacion as e:
198
+ print("Datos inválidos:", e.mensaje) # no se cobró el crédito
199
+ except errores.SinCreditos:
200
+ print("Recarga créditos")
201
+ except errores.FactuSmartError as e:
202
+ print("Error", e.status, e.mensaje)
203
+ ```
204
+
205
+ ## Reintentos e idempotencia
206
+
207
+ - `comprobantes.emitir` **siempre** manda el header `Idempotency-Key`: la tuya
208
+ (`idempotency_key="id-de-tu-venta"`) o un UUID v4 generado. **La misma clave
209
+ se usa en todos los reintentos de esa llamada**, así un reintento nunca emite
210
+ dos veces ni cobra dos veces. Si llamas dos veces con la misma clave,
211
+ recibes la misma respuesta.
212
+ - Se reintenta solo ante: error de red/timeout, HTTP 429, 502, 503 y 504, con
213
+ espera creciente (1 s, 2 s, 4 s…), hasta `max_reintentos` veces. Ante un
214
+ límite de peticiones se respeta el header `Retry-After` o el "Espera N
215
+ segundos" del mensaje.
216
+ - En `emitir`, el HTTP 409 ("ya hay una emisión en curso con esa
217
+ Idempotency-Key") también se reintenta con la misma clave.
218
+ - Se reintentan las consultas (GET) y `rucs.crear` (el API es idempotente por
219
+ RUC).
220
+ - **Nunca** se reintentan solas: `anulaciones.crear`, `guias.emitir`,
221
+ `comprobantes.reenviar`, `comprobantes.consultar_sunat` y `rucs.activar`.
222
+
223
+ ## Pruebas (para quien mantiene el SDK)
224
+
225
+ ```bash
226
+ python -m unittest discover -s tests -v
227
+ ```
228
+
229
+ - `test_unitarios.py` y `test_calculo.py` no usan red.
230
+ - `test_contrato.py` lee `openapi.yaml` (variable `FACTUSMART_OPENAPI`) y falla
231
+ si algún `operationId` no tiene método en el SDK.
232
+ - `test_integracion.py` va contra el API **local** (`FACTUSMART_TEST_BASE_URL`,
233
+ `FACTUSMART_TEST_API_KEY`, `FACTUSMART_TEST_RUC`; `FACTUSMART_INTEGRACION=0`
234
+ para saltarlas). Gasta 2 créditos por corrida. Se niega a correr contra
235
+ `factusmart.pe`.
236
+
237
+ ## Licencia
238
+
239
+ MIT. Ver [LICENSE](LICENSE).
@@ -0,0 +1,210 @@
1
+ # FactuSmart — SDK de Python
2
+
3
+ SDK oficial del **API de facturación electrónica de FactuSmart** (SUNAT, Perú):
4
+ facturas, boletas, notas de crédito/débito, guías de remisión (GRE 2.0) y
5
+ anulaciones.
6
+
7
+ - Python 3.8 o superior.
8
+ - **Cero dependencias**: solo librería estándar (`urllib`).
9
+ - Con type hints (`py.typed`).
10
+ - Documentación completa del API: https://factusmart.pe/api-facturacion-electronica/docs
11
+
12
+ ## Instalar
13
+
14
+ ```bash
15
+ pip install factusmart
16
+ ```
17
+
18
+ ## Uso en 10 líneas
19
+
20
+ ```python
21
+ import factusmart
22
+
23
+ fs = factusmart.Cliente(api_key="TU_API_KEY", base_url="https://TU_URL_BASE/api/v1/issuer", ruc="20123456789")
24
+
25
+ calc = factusmart.calcular_comprobante([
26
+ {"descripcion": "Servicio de consultoría", "cantidad": 1, "valor_unitario": 100, "afectacion": "10"},
27
+ ])
28
+
29
+ r = fs.comprobantes.emitir({
30
+ "codigo_tipo_documento": "01",
31
+ "datos_del_cliente_o_receptor": {"codigo_tipo_documento_identidad": "6", "numero_documento": "20555555555"},
32
+ "items": calc["items"], "totales": calc["totales"],
33
+ })
34
+
35
+ print(r["aceptado_por_sunat"], r["serie_numero"])
36
+ ```
37
+
38
+ `base_url` **no es igual para todas las cuentas**: cada cuenta vive en un
39
+ servidor distinto. Cópiala de tu panel de integrador (pestaña Configuración,
40
+ "URL base de la API").
41
+
42
+ ## Opciones del cliente
43
+
44
+ | Opción | Obligatoria | Por defecto | Para qué |
45
+ |---|---|---|---|
46
+ | `api_key` | sí | — | Tu API key (va en el header `X-API-Key`) |
47
+ | `base_url` | sí | — | URL base de tu cuenta, sin barra final |
48
+ | `ruc` | no | — | RUC emisor por defecto. Se cambia en cada llamada con `ruc=` |
49
+ | `timeout` | no | `60` | Segundos por petición (SUNAT a veces tarda) |
50
+ | `max_reintentos` | no | `3` | Ver "Reintentos e idempotencia" |
51
+ | `verificar_ssl` | no | `True` | `False` **solo** en pruebas locales con certificado propio |
52
+
53
+ Si falta `api_key` o `base_url`, el cliente lanza `ValueError` al crearse.
54
+
55
+ ## Métodos
56
+
57
+ Los datos se envían y se reciben **tal cual los define el API** (claves en
58
+ español con guion bajo). Todos los métodos que operan sobre un RUC aceptan
59
+ `ruc=` para cambiar el del constructor (en GET va como `?ruc=`; en POST va en el
60
+ cuerpo, y si tú ya pusiste `ruc` en los datos, se respeta el tuyo).
61
+
62
+ | Método | Endpoint | Devuelve |
63
+ |---|---|---|
64
+ | `fs.ping(ruc=None)` | `GET /ping` | dict |
65
+ | `fs.cuenta.saldo()` | `GET /account` | dict |
66
+ | `fs.series.listar(ruc=None)` | `GET /series` | dict |
67
+ | `fs.comprobantes.emitir(datos, ruc=None, idempotency_key=None)` | `POST /documents` | dict |
68
+ | `fs.comprobantes.listar(filtros=None, ruc=None)` | `GET /documents` | dict |
69
+ | `fs.comprobantes.consultar(external_id, ruc=None)` | `GET /documents/{id}` | dict |
70
+ | `fs.comprobantes.descargar(external_id, tipo, ruc=None)` | `GET /documents/{id}/{pdf\|xml\|cdr}` | **bytes** |
71
+ | `fs.comprobantes.reenviar(external_id, ruc=None)` | `POST /documents/{id}/reenviar` | dict |
72
+ | `fs.comprobantes.consultar_sunat(external_id, ruc=None)` | `POST /documents/{id}/consultar-sunat` | dict |
73
+ | `fs.comprobantes.historial(external_id, ruc=None)` | `GET /documents/{id}/historial` | dict |
74
+ | `fs.anulaciones.crear(datos, ruc=None)` | `POST /anulaciones` | dict |
75
+ | `fs.anulaciones.estado(ticket, ruc=None)` | `GET /anulaciones/{ticket}/status` | dict |
76
+ | `fs.guias.emitir(datos, ruc=None)` | `POST /guias` | dict |
77
+ | `fs.guias.estado(external_id, ruc=None)` | `GET /guias/{id}/status` | dict |
78
+ | `fs.rucs.listar()` | `GET /rucs` | dict |
79
+ | `fs.rucs.crear(datos)` | `POST /rucs` | dict |
80
+ | `fs.rucs.activar(ruc, activo)` | `PATCH /rucs/{ruc}/activation` | dict |
81
+
82
+ Filtros de `comprobantes.listar`: `estado`, `desde`, `hasta` (YYYY-MM-DD),
83
+ `limite` (1 a 100) y `antes_de` (pasa el `siguiente_cursor` de la página
84
+ anterior).
85
+
86
+ Guardar el PDF:
87
+
88
+ ```python
89
+ with open("comprobante.pdf", "wb") as f:
90
+ f.write(fs.comprobantes.descargar(r["external_id"], "pdf"))
91
+ ```
92
+
93
+ ### Lo que `comprobantes.emitir` completa por ti
94
+
95
+ Solo si no lo mandas:
96
+
97
+ - `numero_documento`: `"#"` (FactuSmart asigna el correlativo). Si mandas tu
98
+ propio número, se respeta.
99
+ - `fecha_de_emision` y `hora_de_emision`: fecha y hora **de Perú**
100
+ (America/Lima), no la de tu servidor.
101
+ - `codigo_tipo_moneda`: `"PEN"`.
102
+ - `codigo_tipo_operacion`: `"0101"` en facturas y boletas (en notas no aplica).
103
+
104
+ `guias.emitir` completa igual `numero_documento`, `fecha_de_emision` y
105
+ `hora_de_emision`.
106
+
107
+ ## Calcular IGV y totales
108
+
109
+ El API espera los importes ya calculados. `calcular_comprobante` lo hace por ti:
110
+ recibe cada ítem con `descripcion`, `cantidad`, `valor_unitario` (**sin IGV**) y
111
+ `afectacion`, y devuelve `{"items": [...], "totales": {...}}` listo para enviar.
112
+
113
+ | `afectacion` | Significado | IGV |
114
+ |---|---|---|
115
+ | `"10"` | Gravado | 18% |
116
+ | `"20"` | Exonerado | 0 |
117
+ | `"30"` | Inafecto | 0 |
118
+ | `"40"` | Exportación | 0 (va en `total_exportacion`) |
119
+
120
+ Opcionales por ítem: `codigo_interno` (recomendado: el SKU de tu sistema) y
121
+ `unidad_de_medida` (por defecto `NIU`; usa `ZZ` para servicios).
122
+
123
+ Redondea a 2 decimales por ítem con `decimal.Decimal` y ROUND_HALF_UP, así da
124
+ **exactamente los mismos números** que los SDK de PHP y Node.
125
+
126
+ ## La regla de oro: revisa `aceptado_por_sunat`
127
+
128
+ Un HTTP 200 significa que FactuSmart **generó** tu comprobante, no que SUNAT lo
129
+ aceptó. SUNAT puede estar caída o rechazarlo: recibirás `estado: "01"` con
130
+ `aceptado_por_sunat: False` y el motivo en `sunat["descripcion"]`.
131
+
132
+ **Eso NO es una excepción.** El SDK te devuelve la respuesta tal cual y tú
133
+ decides:
134
+
135
+ ```python
136
+ r = fs.comprobantes.emitir(datos)
137
+ if not r["aceptado_por_sunat"]:
138
+ print("SUNAT no lo aceptó todavía:", r.get("sunat"), r.get("accion_requerida"))
139
+ # corrige y reintenta gratis con: fs.comprobantes.reenviar(r["external_id"])
140
+ ```
141
+
142
+ Mira también `entorno` (`"prueba"` o `"produccion"`) y `tiene_validez_fiscal`.
143
+
144
+ ## Errores
145
+
146
+ Todas las excepciones están en `factusmart.errores` (y también en `factusmart`)
147
+ y heredan de `FactuSmartError`, que trae:
148
+
149
+ - `status`: código HTTP (`None` si fue un error de red),
150
+ - `mensaje`: el mensaje del API, en español,
151
+ - `respuesta`: el cuerpo completo de la respuesta.
152
+
153
+ | HTTP | Excepción |
154
+ |---|---|
155
+ | 401 | `ErrorDeAutenticacion` |
156
+ | 402 | `SinCreditos` |
157
+ | 403 | `RucNoAutorizado` |
158
+ | 404 | `NoEncontrado` |
159
+ | 422 | `ErrorDeValidacion` |
160
+ | 429 | `LimiteDePeticiones` (con `segundos_de_espera` si se pudo leer) |
161
+ | otros / red | `FactuSmartError` |
162
+
163
+ ```python
164
+ from factusmart import errores
165
+
166
+ try:
167
+ r = fs.comprobantes.emitir(datos)
168
+ except errores.ErrorDeValidacion as e:
169
+ print("Datos inválidos:", e.mensaje) # no se cobró el crédito
170
+ except errores.SinCreditos:
171
+ print("Recarga créditos")
172
+ except errores.FactuSmartError as e:
173
+ print("Error", e.status, e.mensaje)
174
+ ```
175
+
176
+ ## Reintentos e idempotencia
177
+
178
+ - `comprobantes.emitir` **siempre** manda el header `Idempotency-Key`: la tuya
179
+ (`idempotency_key="id-de-tu-venta"`) o un UUID v4 generado. **La misma clave
180
+ se usa en todos los reintentos de esa llamada**, así un reintento nunca emite
181
+ dos veces ni cobra dos veces. Si llamas dos veces con la misma clave,
182
+ recibes la misma respuesta.
183
+ - Se reintenta solo ante: error de red/timeout, HTTP 429, 502, 503 y 504, con
184
+ espera creciente (1 s, 2 s, 4 s…), hasta `max_reintentos` veces. Ante un
185
+ límite de peticiones se respeta el header `Retry-After` o el "Espera N
186
+ segundos" del mensaje.
187
+ - En `emitir`, el HTTP 409 ("ya hay una emisión en curso con esa
188
+ Idempotency-Key") también se reintenta con la misma clave.
189
+ - Se reintentan las consultas (GET) y `rucs.crear` (el API es idempotente por
190
+ RUC).
191
+ - **Nunca** se reintentan solas: `anulaciones.crear`, `guias.emitir`,
192
+ `comprobantes.reenviar`, `comprobantes.consultar_sunat` y `rucs.activar`.
193
+
194
+ ## Pruebas (para quien mantiene el SDK)
195
+
196
+ ```bash
197
+ python -m unittest discover -s tests -v
198
+ ```
199
+
200
+ - `test_unitarios.py` y `test_calculo.py` no usan red.
201
+ - `test_contrato.py` lee `openapi.yaml` (variable `FACTUSMART_OPENAPI`) y falla
202
+ si algún `operationId` no tiene método en el SDK.
203
+ - `test_integracion.py` va contra el API **local** (`FACTUSMART_TEST_BASE_URL`,
204
+ `FACTUSMART_TEST_API_KEY`, `FACTUSMART_TEST_RUC`; `FACTUSMART_INTEGRACION=0`
205
+ para saltarlas). Gasta 2 créditos por corrida. Se niega a correr contra
206
+ `factusmart.pe`.
207
+
208
+ ## Licencia
209
+
210
+ MIT. Ver [LICENSE](LICENSE).
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "factusmart"
7
+ dynamic = ["version"]
8
+ description = "SDK oficial de Python del API de facturación electrónica de FactuSmart (SUNAT, Perú)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "FactuSmart" }]
13
+ keywords = ["factusmart", "sunat", "facturacion electronica", "peru", "cpe", "boleta", "factura"]
14
+ dependencies = []
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Natural Language :: Spanish",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Programming Language :: Python :: 3.8",
24
+ "Programming Language :: Python :: 3.9",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Topic :: Office/Business :: Financial :: Accounting",
30
+ "Typing :: Typed",
31
+ ]
32
+
33
+ [project.urls]
34
+ Documentacion = "https://factusmart.pe/api-facturacion-electronica/docs/sdks"
35
+ Repositorio = "https://github.com/factusmart/factusmart-python"
36
+
37
+ [tool.setuptools.dynamic]
38
+ version = { attr = "factusmart._version.__version__" }
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
42
+
43
+ [tool.setuptools.package-data]
44
+ factusmart = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,48 @@
1
+ """SDK de Python del API de facturación electrónica de FactuSmart (SUNAT, Perú).
2
+
3
+ Uso::
4
+
5
+ import factusmart
6
+
7
+ fs = factusmart.Cliente(api_key="TU_API_KEY", base_url="https://TU_URL_BASE/api/v1/issuer", ruc="20123456789")
8
+ calc = factusmart.calcular_comprobante([
9
+ {"descripcion": "Servicio de consultoría", "cantidad": 1, "valor_unitario": 100, "afectacion": "10"},
10
+ ])
11
+ r = fs.comprobantes.emitir({
12
+ "codigo_tipo_documento": "01",
13
+ "datos_del_cliente_o_receptor": {"codigo_tipo_documento_identidad": "6", "numero_documento": "20555555555"},
14
+ **calc,
15
+ })
16
+ print(r["aceptado_por_sunat"], r["serie_numero"])
17
+ """
18
+
19
+ from . import errores
20
+ from ._version import __version__
21
+ from .calculo import calcular_comprobante
22
+ from .cliente import OPERACIONES, Cliente, Transporte, hora_peru
23
+ from .errores import (
24
+ ErrorDeAutenticacion,
25
+ ErrorDeValidacion,
26
+ FactuSmartError,
27
+ LimiteDePeticiones,
28
+ NoEncontrado,
29
+ RucNoAutorizado,
30
+ SinCreditos,
31
+ )
32
+
33
+ __all__ = [
34
+ "__version__",
35
+ "Cliente",
36
+ "Transporte",
37
+ "OPERACIONES",
38
+ "calcular_comprobante",
39
+ "hora_peru",
40
+ "errores",
41
+ "FactuSmartError",
42
+ "ErrorDeAutenticacion",
43
+ "SinCreditos",
44
+ "RucNoAutorizado",
45
+ "NoEncontrado",
46
+ "ErrorDeValidacion",
47
+ "LimiteDePeticiones",
48
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"