nfe-io 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.
- nfe_io-0.1.0/.gitignore +25 -0
- nfe_io-0.1.0/CHANGELOG.md +54 -0
- nfe_io-0.1.0/CONTRIBUTING.md +85 -0
- nfe_io-0.1.0/LICENSE +21 -0
- nfe_io-0.1.0/PKG-INFO +404 -0
- nfe_io-0.1.0/README.md +374 -0
- nfe_io-0.1.0/SECURITY.md +42 -0
- nfe_io-0.1.0/pyproject.toml +139 -0
- nfe_io-0.1.0/src/nfeio/__init__.py +103 -0
- nfe_io-0.1.0/src/nfeio/_client.py +227 -0
- nfe_io-0.1.0/src/nfeio/_config.py +385 -0
- nfe_io-0.1.0/src/nfeio/_core/__init__.py +1 -0
- nfe_io-0.1.0/src/nfeio/_core/error_mapping.py +181 -0
- nfe_io-0.1.0/src/nfeio/_core/headers.py +66 -0
- nfe_io-0.1.0/src/nfeio/_core/jsonutil.py +95 -0
- nfe_io-0.1.0/src/nfeio/_core/multipart.py +61 -0
- nfe_io-0.1.0/src/nfeio/_core/ops.py +131 -0
- nfe_io-0.1.0/src/nfeio/_core/paths.py +144 -0
- nfe_io-0.1.0/src/nfeio/_core/redact.py +22 -0
- nfe_io-0.1.0/src/nfeio/_core/requestor.py +235 -0
- nfe_io-0.1.0/src/nfeio/_core/retry.py +155 -0
- nfe_io-0.1.0/src/nfeio/_core/transport.py +332 -0
- nfe_io-0.1.0/src/nfeio/_generated/README.md +10 -0
- nfe_io-0.1.0/src/nfeio/_version.py +3 -0
- nfe_io-0.1.0/src/nfeio/errors.py +330 -0
- nfe_io-0.1.0/src/nfeio/models/__init__.py +172 -0
- nfe_io-0.1.0/src/nfeio/models/_base.py +385 -0
- nfe_io-0.1.0/src/nfeio/pagination.py +244 -0
- nfe_io-0.1.0/src/nfeio/py.typed +0 -0
- nfe_io-0.1.0/src/nfeio/resources/__init__.py +1 -0
- nfe_io-0.1.0/src/nfeio/resources/_base.py +56 -0
- nfe_io-0.1.0/src/nfeio/resources/certificates.py +151 -0
- nfe_io-0.1.0/src/nfeio/resources/companies.py +182 -0
- nfe_io-0.1.0/src/nfeio/resources/lookups.py +118 -0
- nfe_io-0.1.0/src/nfeio/resources/service_invoices.py +819 -0
- nfe_io-0.1.0/src/nfeio/resources/webhooks.py +180 -0
- nfe_io-0.1.0/src/nfeio/transport.py +24 -0
- nfe_io-0.1.0/src/nfeio/types.py +196 -0
- nfe_io-0.1.0/src/nfeio/webhooks.py +200 -0
- nfe_io-0.1.0/tests/__init__.py +0 -0
- nfe_io-0.1.0/tests/conftest.py +17 -0
- nfe_io-0.1.0/tests/fixtures/error-envelopes.json +20 -0
- nfe_io-0.1.0/tests/fixtures/live-contracts/README.md +19 -0
- nfe_io-0.1.0/tests/fixtures/live-contracts/auth-cross-key.json +9 -0
- nfe_io-0.1.0/tests/fixtures/live-contracts/certificate-upload-v2.json +8 -0
- nfe_io-0.1.0/tests/fixtures/live-contracts/company-v2-crud.json +17 -0
- nfe_io-0.1.0/tests/fixtures/live-contracts/service-invoice-lifecycle.json +45 -0
- nfe_io-0.1.0/tests/fixtures/live-contracts/webhook-crud.json +18 -0
- nfe_io-0.1.0/tests/fixtures/spec-keys.json +290 -0
- nfe_io-0.1.0/tests/fixtures/webhook-signatures.json +38 -0
- nfe_io-0.1.0/tests/helpers.py +180 -0
- nfe_io-0.1.0/tests/live/__init__.py +0 -0
- nfe_io-0.1.0/tests/live/conftest.py +183 -0
- nfe_io-0.1.0/tests/live/test_live_read.py +113 -0
- nfe_io-0.1.0/tests/live/test_live_write.py +383 -0
- nfe_io-0.1.0/tests/unit/__init__.py +0 -0
- nfe_io-0.1.0/tests/unit/test_async.py +229 -0
- nfe_io-0.1.0/tests/unit/test_config.py +224 -0
- nfe_io-0.1.0/tests/unit/test_errors.py +186 -0
- nfe_io-0.1.0/tests/unit/test_hardening.py +128 -0
- nfe_io-0.1.0/tests/unit/test_headers.py +23 -0
- nfe_io-0.1.0/tests/unit/test_live_contracts.py +148 -0
- nfe_io-0.1.0/tests/unit/test_models.py +200 -0
- nfe_io-0.1.0/tests/unit/test_pagination.py +139 -0
- nfe_io-0.1.0/tests/unit/test_paths_json_multipart.py +152 -0
- nfe_io-0.1.0/tests/unit/test_readme.py +83 -0
- nfe_io-0.1.0/tests/unit/test_resources.py +334 -0
- nfe_io-0.1.0/tests/unit/test_retry.py +203 -0
- nfe_io-0.1.0/tests/unit/test_security_and_packaging.py +140 -0
- nfe_io-0.1.0/tests/unit/test_service_invoices.py +414 -0
- nfe_io-0.1.0/tests/unit/test_spec_alignment.py +114 -0
- nfe_io-0.1.0/tests/unit/test_transport.py +273 -0
- nfe_io-0.1.0/tests/unit/test_webhook_signature.py +140 -0
nfe_io-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.env
|
|
2
|
+
.env.*
|
|
3
|
+
!.env.example
|
|
4
|
+
nfeio-docs
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
.venv/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
*.egg-info/
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.coverage
|
|
15
|
+
htmlcov/
|
|
16
|
+
coverage.xml
|
|
17
|
+
scripts/probes/out/
|
|
18
|
+
coverage.json
|
|
19
|
+
tests/live/out/
|
|
20
|
+
.smoke-venv/
|
|
21
|
+
requirements-audit.txt
|
|
22
|
+
|
|
23
|
+
# arquivos locais do agente (não versionar)
|
|
24
|
+
CLAUDE.md
|
|
25
|
+
.claude/
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Todas as mudanças relevantes deste projeto são registradas aqui.
|
|
4
|
+
|
|
5
|
+
O formato segue o [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/) e o projeto adota o
|
|
6
|
+
[Versionamento Semântico](https://semver.org/lang/pt-BR/). Enquanto a versão for 0.x, uma minor
|
|
7
|
+
pode trazer mudança incompatível; ela sempre será anunciada aqui.
|
|
8
|
+
|
|
9
|
+
## [Não lançado]
|
|
10
|
+
|
|
11
|
+
## [0.1.0] - não publicada
|
|
12
|
+
|
|
13
|
+
Primeira versão do SDK oficial da NFE.io para Python (`pip install nfe-io`, `import nfeio`).
|
|
14
|
+
|
|
15
|
+
### Adicionado
|
|
16
|
+
|
|
17
|
+
- Clientes `NfeClient` (síncrono) e `AsyncNfeClient` (assíncrono, via `asyncio.to_thread`), com a
|
|
18
|
+
mesma superfície e o mesmo comportamento, sem estado global.
|
|
19
|
+
- Duas chaves por família de API: `api_key` (NFS-e, empresas, certificados, webhooks) e
|
|
20
|
+
`data_api_key` (CNPJ, CPF, CEP), lidas também de `NFE_API_KEY` e `NFE_DATA_API_KEY`. Envio em
|
|
21
|
+
`Authorization: <chave>`.
|
|
22
|
+
- NFS-e (API v1): `create`, `list`, `retrieve`, `find_by_external_id`, `cancel`, `send_email`,
|
|
23
|
+
`download_pdf`, `download_xml`, `download_cancellation_xml`, `wait`, `create_and_wait` e
|
|
24
|
+
`cancel_and_wait`.
|
|
25
|
+
- Empresas (API v2, paginação por cursor): `list`, `retrieve`, `create`, `update`, `delete`.
|
|
26
|
+
- Certificados: `upload` (multipart, campo `file`) e `list`, com `expires_on` e `is_expired()`.
|
|
27
|
+
- Webhooks da conta: `list`, `retrieve`, `create`, `update`, `delete`, `ping`, `event_types`.
|
|
28
|
+
- `nfeio.webhooks.verify_signature` (HMAC-SHA1 em tempo constante, nunca levanta exceção) e
|
|
29
|
+
`construct_event` (verifica antes de interpretar o corpo).
|
|
30
|
+
- Consultas: `lookups.cnpj` (v3, CNPJ numérico e alfanumérico), `lookups.cnpj_state_taxes`,
|
|
31
|
+
`lookups.cpf` e `lookups.cep`, com validação local de dígitos verificadores.
|
|
32
|
+
- Modelos `NfeObject`: `Mapping` imutável sobre o JSON do fio, com propriedades tipadas para ids,
|
|
33
|
+
status, datas (`datetime` com fuso), valores (`Decimal`) e documentos (CNPJ/CPF normalizados).
|
|
34
|
+
- Paginação por índice (1-based) e por cursor com `auto_paging_iter()` síncrono e assíncrono.
|
|
35
|
+
- Hierarquia de erros por status com `request_id`, `trace_id`, `error_code` e
|
|
36
|
+
`outcome_unknown`; leitura dos cinco formatos de erro usados pela API.
|
|
37
|
+
- `RequestOptions` por chamada (`api_key`, `timeout`, `max_retries`, `idempotency_key`,
|
|
38
|
+
`extra_headers`) e `app_info` no User-Agent.
|
|
39
|
+
- Transporte só com a biblioteca padrão, substituível pelos protocolos `Transport` e
|
|
40
|
+
`AsyncTransport`.
|
|
41
|
+
|
|
42
|
+
### Segurança
|
|
43
|
+
|
|
44
|
+
- Retry ciente de método e da fase da falha: um `POST` (como a emissão) nunca é repetido depois
|
|
45
|
+
que pode ter chegado à API; `Idempotency-Key` não muda essa regra.
|
|
46
|
+
- TLS sempre verificado (mínimo 1.2), sem opção para desligar; timeouts finitos; respostas
|
|
47
|
+
limitadas a 10 MiB; sem compressão.
|
|
48
|
+
- JSON não confiável (respostas, erros e webhooks) limitado a 128 níveis de aninhamento, com o
|
|
49
|
+
mesmo resultado em qualquer versão do Python.
|
|
50
|
+
- Redirect de download seguido só em https e **sem** a chave de API.
|
|
51
|
+
- Ids, `externalId` e documentos validados antes de entrar no path (sem path traversal).
|
|
52
|
+
- Chaves, senha de certificado e segredo de webhook nunca aparecem em `repr`, logs ou
|
|
53
|
+
exceções; logs não registram documentos nem `externalId`.
|
|
54
|
+
- Zero dependências de runtime.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Contribuindo
|
|
2
|
+
|
|
3
|
+
Obrigado pelo interesse. Este guia cobre o ambiente, as regras do projeto e como testar.
|
|
4
|
+
|
|
5
|
+
## Ambiente
|
|
6
|
+
|
|
7
|
+
Use [uv](https://docs.astral.sh/uv/):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv sync # cria .venv com as dependências de desenvolvimento
|
|
11
|
+
uv run pytest # testes unitários (sem rede)
|
|
12
|
+
uv run ruff check . # lint
|
|
13
|
+
uv run ruff format --check . # formatação
|
|
14
|
+
uv run mypy --strict src tests
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Cobertura com os mínimos do CI (núcleo, erros e webhooks >= 90%, total >= 85%):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv run coverage run -m pytest
|
|
21
|
+
uv run coverage json -o coverage.json
|
|
22
|
+
uv run python scripts/check_coverage.py coverage.json
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Segurança e empacotamento:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv run bandit -c pyproject.toml -r src
|
|
29
|
+
uv export --format requirements-txt --all-groups --no-emit-project -o requirements-audit.txt
|
|
30
|
+
uv run pip-audit --strict --disable-pip -r requirements-audit.txt
|
|
31
|
+
uv build && uv run twine check --strict dist/*
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
No GitHub, o CodeQL roda pelo default setup do repositório; não há workflow próprio dele em
|
|
35
|
+
`.github/workflows/`.
|
|
36
|
+
|
|
37
|
+
## Regras do projeto
|
|
38
|
+
|
|
39
|
+
1. **Zero dependências de runtime.** O pacote usa só a biblioteca padrão. Dependências novas só
|
|
40
|
+
no grupo `dev`.
|
|
41
|
+
2. **Contrato vem da OpenAPI e de sonda ao vivo**, nunca de outro SDK. Quando algo não puder ser
|
|
42
|
+
observado, registre como inferência explícita (`docs/contrato/`).
|
|
43
|
+
3. **Nunca retentar emissão ambígua.** `POST` não é repetido após possível envio. Não mude a
|
|
44
|
+
tabela de `src/nfeio/_core/retry.py` sem evidência da API.
|
|
45
|
+
4. **Segurança:** nada de chave, senha ou documento em log, `repr` ou exceção; TLS sempre
|
|
46
|
+
verificado; todo valor de path passa por `nfeio._core.paths`; nada de `pickle`/`eval`.
|
|
47
|
+
5. **Sync e async não divergem.** Comportamento novo é escrito uma vez como operação em
|
|
48
|
+
`resources/` ou `_core/` (gerador de efeitos) e exposto nas duas fachadas com a mesma
|
|
49
|
+
assinatura (o teste de paridade verifica).
|
|
50
|
+
6. **Modelos enxutos.** Propriedade tipada só para id, status, data, dinheiro ou documento; o
|
|
51
|
+
resto fica no acesso por chave. Toda propriedade aponta para uma chave da spec (ou para uma
|
|
52
|
+
divergência provada listada em `tests/unit/test_spec_alignment.py`).
|
|
53
|
+
7. Código gerado, se um dia existir, fica em `src/nfeio/_generated/` e não é editado à mão.
|
|
54
|
+
8. Código, identificadores e docstrings em inglês; documentação e CHANGELOG em pt-BR.
|
|
55
|
+
|
|
56
|
+
Ao mudar uma spec em `nfeio-docs`, regenere o snapshot usado pelo teste de alinhamento:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uv run python scripts/spec_keys.py
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Testes de integração (opt-in)
|
|
63
|
+
|
|
64
|
+
Os testes em `tests/live/` falam com a API real e ficam pulados por padrão. Eles leem as chaves
|
|
65
|
+
do ambiente ou do arquivo `.env` (fora do git).
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
uv run pytest tests/live --run-integration # só leitura
|
|
69
|
+
uv run pytest tests/live --run-integration --live-write # também escrita
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
As variáveis `NFE_RUN_INTEGRATION=1` e `NFE_LIVE_WRITE=1` têm o mesmo efeito.
|
|
73
|
+
|
|
74
|
+
Escrita é restrita: NFS-e só na empresa `NFE_COMPANY_ID` (homologação), sempre com
|
|
75
|
+
`externalId` único e cancelamento ao final; uma empresa descartável; webhooks de teste removidos
|
|
76
|
+
ao final. Evidências redigidas vão para `tests/live/out/` (fora do git); fixtures versionadas em
|
|
77
|
+
`tests/fixtures/live-contracts/` usam valores sintéticos.
|
|
78
|
+
|
|
79
|
+
## Commits e versões
|
|
80
|
+
|
|
81
|
+
- [Conventional Commits](https://www.conventionalcommits.org/pt-br/) (`feat:`, `fix:`, `docs:`,
|
|
82
|
+
`test:`, `build:`, `ci:`).
|
|
83
|
+
- Toda mudança visível ao usuário entra no `CHANGELOG.md` (pt-BR, Keep a Changelog).
|
|
84
|
+
- A versão vive só em `src/nfeio/_version.py`. Publicação só pelo workflow de release, por tag,
|
|
85
|
+
com aprovação no environment `pypi`.
|
nfe_io-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NFE.io
|
|
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.
|
nfe_io-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: nfe-io
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SDK oficial da NFE.io para Python: NFS-e, empresas, certificados, webhooks e consultas de CNPJ, CPF e CEP.
|
|
5
|
+
Project-URL: Homepage, https://nfe.io
|
|
6
|
+
Project-URL: Documentation, https://nfe.io/docs
|
|
7
|
+
Project-URL: Source, https://github.com/nfe/client-python
|
|
8
|
+
Project-URL: Changelog, https://github.com/nfe/client-python/blob/master/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/nfe/client-python/issues
|
|
10
|
+
Author-email: "NFE.io" <suporte@nfe.io>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: brasil,fiscal,nfe,nfe.io,nfse,nota fiscal,sdk
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Natural Language :: Portuguese (Brazilian)
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# NFE.io SDK para Python
|
|
32
|
+
|
|
33
|
+
SDK oficial da [NFE.io](https://nfe.io) para Python. Emite e gerencia **NFS-e**, cadastra
|
|
34
|
+
**empresas** e **certificados digitais**, administra **webhooks** e consulta **CNPJ, CPF e CEP**.
|
|
35
|
+
|
|
36
|
+
- Python 3.10 a 3.14, cliente síncrono e assíncrono.
|
|
37
|
+
- **Zero dependências de runtime**: só a biblioteca padrão.
|
|
38
|
+
- Retry seguro: uma emissão nunca é reenviada depois que pode ter chegado à API.
|
|
39
|
+
- Tipado (`py.typed`, mypy `--strict`), sem estado global, TLS sempre verificado.
|
|
40
|
+
|
|
41
|
+
> Versão 0.1 (alfa). NF-e, NFC-e, CT-e, distribuição DF-e e cálculo de impostos chegam nas
|
|
42
|
+
> próximas versões.
|
|
43
|
+
|
|
44
|
+
## Instalação
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install nfe-io
|
|
48
|
+
# ou
|
|
49
|
+
uv add nfe-io
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
O pacote se chama `nfe-io` no PyPI e é importado como `nfeio`.
|
|
53
|
+
|
|
54
|
+
## Chaves de API
|
|
55
|
+
|
|
56
|
+
A NFE.io usa duas chaves, cada uma para um grupo de APIs:
|
|
57
|
+
|
|
58
|
+
| Parâmetro | Variável de ambiente | Usada em |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `api_key` | `NFE_API_KEY` | NFS-e, empresas, certificados, webhooks |
|
|
61
|
+
| `data_api_key` | `NFE_DATA_API_KEY` | consultas de CNPJ, CPF e CEP |
|
|
62
|
+
|
|
63
|
+
O SDK nunca usa uma chave no lugar da outra: os hosts de consulta recusam a chave principal com
|
|
64
|
+
403. Se você tem uma chave só, passe o mesmo valor nos dois parâmetros.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from nfeio import NfeClient
|
|
68
|
+
|
|
69
|
+
client = NfeClient(api_key="SUA_CHAVE", data_api_key="SUA_CHAVE_DE_DADOS")
|
|
70
|
+
# ou, lendo NFE_API_KEY e NFE_DATA_API_KEY do ambiente:
|
|
71
|
+
client = NfeClient()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Não existe host de homologação: o ambiente (produção ou teste) é configurado na empresa.
|
|
75
|
+
|
|
76
|
+
## Início rápido
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from nfeio import NfeClient
|
|
80
|
+
|
|
81
|
+
client = NfeClient()
|
|
82
|
+
company_id = "ID_DA_EMPRESA"
|
|
83
|
+
|
|
84
|
+
invoice = client.service_invoices.create_and_wait(
|
|
85
|
+
company_id,
|
|
86
|
+
{
|
|
87
|
+
"cityServiceCode": "10677",
|
|
88
|
+
"description": "Consultoria em tecnologia",
|
|
89
|
+
"servicesAmount": 150.00,
|
|
90
|
+
"borrower": {
|
|
91
|
+
"type": "NaturalPerson",
|
|
92
|
+
"name": "Maria Silva",
|
|
93
|
+
"federalTaxNumber": "52998224725",
|
|
94
|
+
"email": "maria@example.com",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
external_id="pedido-1001", # sua chave de deduplicação (recomendado sempre)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
print(invoice.flow_status) # "Issued"
|
|
101
|
+
print(invoice["number"]) # qualquer campo do JSON, pelo nome original
|
|
102
|
+
pdf = client.service_invoices.download_pdf(company_id, invoice.id)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
O corpo da requisição usa **as mesmas chaves (camelCase) da documentação da API**: copie um
|
|
106
|
+
exemplo da doc e ele funciona. Os argumentos dos métodos seguem o padrão Python (snake_case).
|
|
107
|
+
|
|
108
|
+
### Objetos de resposta
|
|
109
|
+
|
|
110
|
+
Toda resposta é um `NfeObject`: um `Mapping` imutável sobre o JSON recebido.
|
|
111
|
+
|
|
112
|
+
- `obj["campo"]` dá acesso a **qualquer** campo, inclusive os que o SDK ainda não conhece.
|
|
113
|
+
Objetos aninhados também são `NfeObject` (`invoice["borrower"]["address"]["city"]["code"]`).
|
|
114
|
+
- Algumas **propriedades tipadas** corrigem ou tipam o que vem no fio: ids, status, datas
|
|
115
|
+
(`datetime` com fuso), valores (`Decimal`) e documentos (CNPJ/CPF como `str` com zeros à
|
|
116
|
+
esquerda). Exemplos: `invoice.flow_status`, `invoice.services_amount`, `invoice.issued_on`,
|
|
117
|
+
`invoice.borrower.federal_tax_number`.
|
|
118
|
+
- `obj.to_dict()` devolve uma cópia em dicionários comuns (serializável com `json.dumps`).
|
|
119
|
+
- `obj.last_response.request_id` traz o `x-request-id`. Informe esse valor ao suporte.
|
|
120
|
+
|
|
121
|
+
Valores monetários aparecem como `float` no acesso por chave (como no JSON) e como `Decimal` nas
|
|
122
|
+
propriedades. Nas requisições, `int`, `float` e `Decimal` são aceitos; `Decimal` é enviado sem
|
|
123
|
+
perda (`Decimal("100.10")` vira `100.10`).
|
|
124
|
+
|
|
125
|
+
## Emissão segura e reconciliação
|
|
126
|
+
|
|
127
|
+
A emissão de NFS-e é assíncrona: a API responde `202` e a nota passa por estados
|
|
128
|
+
(`WaitingCalculateTaxes`, `WaitingSend`, …) até `Issued`. `create_and_wait` e `wait` consultam a
|
|
129
|
+
nota com backoff até um estado final.
|
|
130
|
+
|
|
131
|
+
**O SDK nunca reenvia uma emissão sozinho.** A API pode criar a nota e mesmo assim responder
|
|
132
|
+
500 ou 504. Por isso:
|
|
133
|
+
|
|
134
|
+
- `POST` não é repetido após timeout de leitura, conexão interrompida, 408, 429 ou 5xx;
|
|
135
|
+
- esses erros chegam com `outcome_unknown=True` e o `external_id` usado;
|
|
136
|
+
- reenviar o mesmo `externalId` é rejeitado pela API (`DuplicateExternalIdError`).
|
|
137
|
+
|
|
138
|
+
Receita recomendada: sempre envie `external_id` e, diante de resultado incerto, procure a nota
|
|
139
|
+
antes de tentar de novo.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from nfeio import APIConnectionError, APIError, DuplicateExternalIdError
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def emitir(client, company_id, pedido):
|
|
146
|
+
external_id = f"pedido-{pedido['id']}"
|
|
147
|
+
try:
|
|
148
|
+
return client.service_invoices.create(company_id, pedido["nfse"], external_id=external_id)
|
|
149
|
+
except DuplicateExternalIdError:
|
|
150
|
+
pass # a primeira tentativa foi registrada: busque a nota
|
|
151
|
+
except (APIError, APIConnectionError) as erro:
|
|
152
|
+
if not erro.outcome_unknown:
|
|
153
|
+
raise # erro de validação, chave inválida etc.: corrija e tente de novo
|
|
154
|
+
nota = client.service_invoices.find_by_external_id(company_id, external_id, wait=30)
|
|
155
|
+
if nota is None:
|
|
156
|
+
raise RuntimeError("emissão não registrada: é seguro tentar de novo mais tarde")
|
|
157
|
+
return nota
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`find_by_external_id(..., wait=30)` repete a busca com backoff por até 30 segundos, cobrindo o
|
|
161
|
+
atraso de indexação logo após o `202`.
|
|
162
|
+
|
|
163
|
+
### Cancelamento
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
nota = client.service_invoices.cancel_and_wait(company_id, invoice_id)
|
|
167
|
+
assert nota.flow_status == "Cancelled"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Falhas de processamento e prazo
|
|
171
|
+
|
|
172
|
+
`wait`, `create_and_wait` e `cancel_and_wait` levantam `InvoiceProcessingError` quando a nota
|
|
173
|
+
termina em `IssueFailed`, `CancelFailed` ou `Error` (com `flow_message`, a explicação da
|
|
174
|
+
prefeitura) e `PollingTimeoutError` quando o prazo (`timeout=120` s por padrão) acaba.
|
|
175
|
+
|
|
176
|
+
## Listagens e paginação
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
# NFS-e: paginação por índice, começando em 1 (page_count de 1 a 50)
|
|
180
|
+
page = client.service_invoices.list(company_id, page_count=50, issued_begin="2026-01-01")
|
|
181
|
+
for nota in page.auto_paging_iter(): # busca as próximas páginas sob demanda
|
|
182
|
+
print(nota.id, nota.flow_status)
|
|
183
|
+
|
|
184
|
+
# Empresas (API v2): paginação por cursor
|
|
185
|
+
for empresa in client.companies.list(limit=50).auto_paging_iter():
|
|
186
|
+
print(empresa.id, empresa["name"])
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`page.data`, `page.has_more` e `page.next_page()` também estão disponíveis. Não existe método que
|
|
190
|
+
carregue todas as páginas na memória de uma vez.
|
|
191
|
+
|
|
192
|
+
## Empresas e certificados
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
empresa = client.companies.create({
|
|
196
|
+
"name": "Minha Empresa LTDA",
|
|
197
|
+
"federalTaxNumber": 11222333000181,
|
|
198
|
+
"taxRegime": "SimplesNacional",
|
|
199
|
+
"address": {
|
|
200
|
+
"country": "BRA", "postalCode": "80010000", "street": "Rua Exemplo", "number": "1",
|
|
201
|
+
"district": "Centro", "state": "PR", "city": {"code": "4106902", "name": "Curitiba"},
|
|
202
|
+
},
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
with open("certificado.pfx", "rb") as arquivo:
|
|
206
|
+
client.certificates.upload(empresa.id, arquivo, "senha-do-certificado")
|
|
207
|
+
|
|
208
|
+
for cert in client.certificates.list(empresa.id):
|
|
209
|
+
print(cert.thumbprint, cert.expires_on, cert.is_expired())
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`companies.update` substitui a empresa inteira (envie todos os campos). `companies.delete` é uma
|
|
213
|
+
desativação na API: a empresa continua consultável com `status == "Inactive"`.
|
|
214
|
+
|
|
215
|
+
## Consultas de CNPJ, CPF e CEP
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
empresa = client.lookups.cnpj("00.000.000/0001-91") # numérico ou alfanumérico
|
|
219
|
+
print(empresa["name"], empresa.status, empresa.share_capital)
|
|
220
|
+
|
|
221
|
+
inscricoes = client.lookups.cnpj_state_taxes("00000000000191", "SP")
|
|
222
|
+
pessoa = client.lookups.cpf("529.982.247-25", "1990-01-31") # data divergente -> NotFoundError
|
|
223
|
+
endereco = client.lookups.cep("01310-100")
|
|
224
|
+
print(endereco["city"]["code"], endereco["street"])
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
CNPJ, CPF e CEP são validados localmente (inclusive dígitos verificadores) antes da requisição.
|
|
228
|
+
|
|
229
|
+
## Webhooks
|
|
230
|
+
|
|
231
|
+
### Gerenciar
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
hook = client.webhooks.create({
|
|
235
|
+
"uri": "https://erp.exemplo.com.br/nfeio/webhook",
|
|
236
|
+
"secret": "um-segredo-longo-e-aleatorio",
|
|
237
|
+
"contentType": "json",
|
|
238
|
+
"filters": ["service_invoice.issued_successfully", "service_invoice.cancelled_successfully"],
|
|
239
|
+
})
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
A API chama a URI na criação para verificá-la: o endpoint precisa estar no ar e responder 2xx.
|
|
243
|
+
|
|
244
|
+
`update` **substitui o webhook inteiro**; omitir `status` desativa o webhook. Busque, altere e
|
|
245
|
+
envie o objeto completo:
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
atual = client.webhooks.retrieve(hook.id).to_dict()
|
|
249
|
+
atual["filters"].append("service_invoice.issued_failed")
|
|
250
|
+
client.webhooks.update(hook.id, atual)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Receber
|
|
254
|
+
|
|
255
|
+
Verifique a assinatura `X-Hub-Signature` sobre o **corpo bruto** da requisição (nunca sobre um
|
|
256
|
+
`json.dumps` do corpo já interpretado). `verify_signature` nunca levanta exceção e compara em
|
|
257
|
+
tempo constante; `construct_event` verifica e só então interpreta o JSON.
|
|
258
|
+
|
|
259
|
+
As entregas podem chegar mais de uma vez: use `event.hook_id` (`X-Hook-Id`) para deduplicar.
|
|
260
|
+
|
|
261
|
+
**Flask**
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
from flask import Flask, request
|
|
265
|
+
from nfeio.errors import SignatureVerificationError
|
|
266
|
+
from nfeio.webhooks import construct_event
|
|
267
|
+
|
|
268
|
+
app = Flask(__name__)
|
|
269
|
+
SEGREDO = "um-segredo-longo-e-aleatorio"
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@app.post("/nfeio/webhook")
|
|
273
|
+
def nfeio_webhook():
|
|
274
|
+
try:
|
|
275
|
+
event = construct_event(request.get_data(), request.headers, SEGREDO)
|
|
276
|
+
except SignatureVerificationError:
|
|
277
|
+
return "", 400
|
|
278
|
+
if event.action == "issued_successfully":
|
|
279
|
+
registrar_nota(event.hook_id, event.data.id, event.data.flow_status)
|
|
280
|
+
return "", 204
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Django**
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
from django.http import HttpResponse
|
|
287
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
288
|
+
from django.views.decorators.http import require_POST
|
|
289
|
+
from nfeio.errors import SignatureVerificationError
|
|
290
|
+
from nfeio.webhooks import construct_event
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@csrf_exempt
|
|
294
|
+
@require_POST
|
|
295
|
+
def nfeio_webhook(request):
|
|
296
|
+
try:
|
|
297
|
+
event = construct_event(request.body, request.headers, SEGREDO)
|
|
298
|
+
except SignatureVerificationError:
|
|
299
|
+
return HttpResponse(status=400)
|
|
300
|
+
processar(event)
|
|
301
|
+
return HttpResponse(status=204)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**FastAPI**
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
from fastapi import FastAPI, HTTPException, Request, Response
|
|
308
|
+
from nfeio.errors import SignatureVerificationError
|
|
309
|
+
from nfeio.webhooks import construct_event
|
|
310
|
+
|
|
311
|
+
app = FastAPI()
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@app.post("/nfeio/webhook", status_code=204)
|
|
315
|
+
async def nfeio_webhook(request: Request) -> Response:
|
|
316
|
+
try:
|
|
317
|
+
event = construct_event(await request.body(), request.headers, SEGREDO)
|
|
318
|
+
except SignatureVerificationError:
|
|
319
|
+
raise HTTPException(status_code=400) from None
|
|
320
|
+
await processar(event)
|
|
321
|
+
return Response(status_code=204)
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Cliente assíncrono
|
|
325
|
+
|
|
326
|
+
`AsyncNfeClient` tem os mesmos recursos, métodos e comportamento do `NfeClient`; cada método é
|
|
327
|
+
uma corrotina. A E/S roda em threads (`asyncio.to_thread`) e as esperas usam `asyncio.sleep`,
|
|
328
|
+
então o event loop não é bloqueado.
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
import asyncio
|
|
332
|
+
from nfeio import AsyncNfeClient
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
async def main():
|
|
336
|
+
async with AsyncNfeClient() as client:
|
|
337
|
+
nota = await client.service_invoices.create_and_wait(
|
|
338
|
+
"ID_DA_EMPRESA", {"cityServiceCode": "10677", "description": "x", "servicesAmount": 10},
|
|
339
|
+
external_id="pedido-1002",
|
|
340
|
+
)
|
|
341
|
+
async for item in (await client.companies.list()).auto_paging_iter():
|
|
342
|
+
print(item.id)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
asyncio.run(main())
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Cancelar a task não interrompe uma requisição que já está em voo (ela termina na thread). Se a
|
|
349
|
+
task cancelada estava emitindo uma nota, trate como resultado incerto e reconcilie por
|
|
350
|
+
`external_id`.
|
|
351
|
+
|
|
352
|
+
## Erros
|
|
353
|
+
|
|
354
|
+
Todas as exceções derivam de `nfeio.NfeError`.
|
|
355
|
+
|
|
356
|
+
| Exceção | Quando |
|
|
357
|
+
|---|---|
|
|
358
|
+
| `ConfigurationError` | chave ausente, opção inválida, TLS inseguro |
|
|
359
|
+
| `InvalidParameterError` | parâmetro recusado localmente (id, CNPJ, CPF, CEP, data) |
|
|
360
|
+
| `InvalidRequestError` | 400, 405, 415, 422 … (`DuplicateExternalIdError` para `externalId` repetido) |
|
|
361
|
+
| `AuthenticationError` | 401 |
|
|
362
|
+
| `PermissionDeniedError` | 403 (a mensagem diz qual chave a família usa) |
|
|
363
|
+
| `NotFoundError` | 404 |
|
|
364
|
+
| `ConflictError` | 409 |
|
|
365
|
+
| `RateLimitError` | 429 (`retry_after`) |
|
|
366
|
+
| `ServerError` | 408 e 5xx |
|
|
367
|
+
| `APIConnectionError` / `APITimeoutError` | falha de rede (`phase` diz se a requisição pode ter saído) |
|
|
368
|
+
| `InvoiceProcessingError` / `PollingTimeoutError` | espera por estado final |
|
|
369
|
+
| `SignatureVerificationError` | assinatura de webhook inválida |
|
|
370
|
+
|
|
371
|
+
Erros da API trazem `status_code`, `message`, `error_code`, `request_id`, `trace_id`, `body` e
|
|
372
|
+
`outcome_unknown`. Nenhuma exceção, `repr` ou log contém a chave de API.
|
|
373
|
+
|
|
374
|
+
## Configuração
|
|
375
|
+
|
|
376
|
+
```python
|
|
377
|
+
from nfeio import NfeClient, RequestOptions, Timeout
|
|
378
|
+
|
|
379
|
+
client = NfeClient(
|
|
380
|
+
timeout=Timeout(connect=10, read=60, total=120), # padrões; precisam ser finitos
|
|
381
|
+
max_retries=3, # só GET/PUT/DELETE, ou falha antes de conectar
|
|
382
|
+
app_info=("meu-modulo-erp", "1.2.0"), # vai no User-Agent
|
|
383
|
+
ca_bundle="/etc/ssl/certs/ca-proxy.pem", # CA extra (proxy corporativo)
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
# Por chamada:
|
|
387
|
+
client.service_invoices.list(company_id, options=RequestOptions(timeout=90, max_retries=0))
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
- TLS é sempre verificado (mínimo TLS 1.2). Não existe opção para desligar a verificação.
|
|
391
|
+
- Respostas acima de 10 MiB são recusadas (`max_response_bytes`).
|
|
392
|
+
- O logger `nfeio` registra requisições e novas tentativas em nível `DEBUG`, sem cabeçalhos,
|
|
393
|
+
corpo, query ou documentos (ids e documentos do path aparecem como `*`).
|
|
394
|
+
- O transporte padrão usa só a biblioteca padrão. Para usar outro cliente HTTP, implemente
|
|
395
|
+
`nfeio.transport.Transport` ou `AsyncTransport`.
|
|
396
|
+
|
|
397
|
+
## Desenvolvimento
|
|
398
|
+
|
|
399
|
+
Veja [CONTRIBUTING.md](CONTRIBUTING.md). Vulnerabilidades: [SECURITY.md](SECURITY.md).
|
|
400
|
+
Histórico de versões: [CHANGELOG.md](CHANGELOG.md).
|
|
401
|
+
|
|
402
|
+
## Licença
|
|
403
|
+
|
|
404
|
+
[MIT](LICENSE).
|