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.
Files changed (73) hide show
  1. nfe_io-0.1.0/.gitignore +25 -0
  2. nfe_io-0.1.0/CHANGELOG.md +54 -0
  3. nfe_io-0.1.0/CONTRIBUTING.md +85 -0
  4. nfe_io-0.1.0/LICENSE +21 -0
  5. nfe_io-0.1.0/PKG-INFO +404 -0
  6. nfe_io-0.1.0/README.md +374 -0
  7. nfe_io-0.1.0/SECURITY.md +42 -0
  8. nfe_io-0.1.0/pyproject.toml +139 -0
  9. nfe_io-0.1.0/src/nfeio/__init__.py +103 -0
  10. nfe_io-0.1.0/src/nfeio/_client.py +227 -0
  11. nfe_io-0.1.0/src/nfeio/_config.py +385 -0
  12. nfe_io-0.1.0/src/nfeio/_core/__init__.py +1 -0
  13. nfe_io-0.1.0/src/nfeio/_core/error_mapping.py +181 -0
  14. nfe_io-0.1.0/src/nfeio/_core/headers.py +66 -0
  15. nfe_io-0.1.0/src/nfeio/_core/jsonutil.py +95 -0
  16. nfe_io-0.1.0/src/nfeio/_core/multipart.py +61 -0
  17. nfe_io-0.1.0/src/nfeio/_core/ops.py +131 -0
  18. nfe_io-0.1.0/src/nfeio/_core/paths.py +144 -0
  19. nfe_io-0.1.0/src/nfeio/_core/redact.py +22 -0
  20. nfe_io-0.1.0/src/nfeio/_core/requestor.py +235 -0
  21. nfe_io-0.1.0/src/nfeio/_core/retry.py +155 -0
  22. nfe_io-0.1.0/src/nfeio/_core/transport.py +332 -0
  23. nfe_io-0.1.0/src/nfeio/_generated/README.md +10 -0
  24. nfe_io-0.1.0/src/nfeio/_version.py +3 -0
  25. nfe_io-0.1.0/src/nfeio/errors.py +330 -0
  26. nfe_io-0.1.0/src/nfeio/models/__init__.py +172 -0
  27. nfe_io-0.1.0/src/nfeio/models/_base.py +385 -0
  28. nfe_io-0.1.0/src/nfeio/pagination.py +244 -0
  29. nfe_io-0.1.0/src/nfeio/py.typed +0 -0
  30. nfe_io-0.1.0/src/nfeio/resources/__init__.py +1 -0
  31. nfe_io-0.1.0/src/nfeio/resources/_base.py +56 -0
  32. nfe_io-0.1.0/src/nfeio/resources/certificates.py +151 -0
  33. nfe_io-0.1.0/src/nfeio/resources/companies.py +182 -0
  34. nfe_io-0.1.0/src/nfeio/resources/lookups.py +118 -0
  35. nfe_io-0.1.0/src/nfeio/resources/service_invoices.py +819 -0
  36. nfe_io-0.1.0/src/nfeio/resources/webhooks.py +180 -0
  37. nfe_io-0.1.0/src/nfeio/transport.py +24 -0
  38. nfe_io-0.1.0/src/nfeio/types.py +196 -0
  39. nfe_io-0.1.0/src/nfeio/webhooks.py +200 -0
  40. nfe_io-0.1.0/tests/__init__.py +0 -0
  41. nfe_io-0.1.0/tests/conftest.py +17 -0
  42. nfe_io-0.1.0/tests/fixtures/error-envelopes.json +20 -0
  43. nfe_io-0.1.0/tests/fixtures/live-contracts/README.md +19 -0
  44. nfe_io-0.1.0/tests/fixtures/live-contracts/auth-cross-key.json +9 -0
  45. nfe_io-0.1.0/tests/fixtures/live-contracts/certificate-upload-v2.json +8 -0
  46. nfe_io-0.1.0/tests/fixtures/live-contracts/company-v2-crud.json +17 -0
  47. nfe_io-0.1.0/tests/fixtures/live-contracts/service-invoice-lifecycle.json +45 -0
  48. nfe_io-0.1.0/tests/fixtures/live-contracts/webhook-crud.json +18 -0
  49. nfe_io-0.1.0/tests/fixtures/spec-keys.json +290 -0
  50. nfe_io-0.1.0/tests/fixtures/webhook-signatures.json +38 -0
  51. nfe_io-0.1.0/tests/helpers.py +180 -0
  52. nfe_io-0.1.0/tests/live/__init__.py +0 -0
  53. nfe_io-0.1.0/tests/live/conftest.py +183 -0
  54. nfe_io-0.1.0/tests/live/test_live_read.py +113 -0
  55. nfe_io-0.1.0/tests/live/test_live_write.py +383 -0
  56. nfe_io-0.1.0/tests/unit/__init__.py +0 -0
  57. nfe_io-0.1.0/tests/unit/test_async.py +229 -0
  58. nfe_io-0.1.0/tests/unit/test_config.py +224 -0
  59. nfe_io-0.1.0/tests/unit/test_errors.py +186 -0
  60. nfe_io-0.1.0/tests/unit/test_hardening.py +128 -0
  61. nfe_io-0.1.0/tests/unit/test_headers.py +23 -0
  62. nfe_io-0.1.0/tests/unit/test_live_contracts.py +148 -0
  63. nfe_io-0.1.0/tests/unit/test_models.py +200 -0
  64. nfe_io-0.1.0/tests/unit/test_pagination.py +139 -0
  65. nfe_io-0.1.0/tests/unit/test_paths_json_multipart.py +152 -0
  66. nfe_io-0.1.0/tests/unit/test_readme.py +83 -0
  67. nfe_io-0.1.0/tests/unit/test_resources.py +334 -0
  68. nfe_io-0.1.0/tests/unit/test_retry.py +203 -0
  69. nfe_io-0.1.0/tests/unit/test_security_and_packaging.py +140 -0
  70. nfe_io-0.1.0/tests/unit/test_service_invoices.py +414 -0
  71. nfe_io-0.1.0/tests/unit/test_spec_alignment.py +114 -0
  72. nfe_io-0.1.0/tests/unit/test_transport.py +273 -0
  73. nfe_io-0.1.0/tests/unit/test_webhook_signature.py +140 -0
@@ -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).