foxnfe 1.3.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.
- foxnfe-1.3.0/PKG-INFO +260 -0
- foxnfe-1.3.0/README.md +239 -0
- foxnfe-1.3.0/foxnfe/__init__.py +40 -0
- foxnfe-1.3.0/foxnfe/client.py +172 -0
- foxnfe-1.3.0/foxnfe/distribuicao.py +73 -0
- foxnfe-1.3.0/foxnfe/documents.py +58 -0
- foxnfe-1.3.0/foxnfe/events.py +115 -0
- foxnfe-1.3.0/foxnfe/exceptions.py +30 -0
- foxnfe-1.3.0/foxnfe/mcp.py +45 -0
- foxnfe-1.3.0/foxnfe/nfe.py +93 -0
- foxnfe-1.3.0/foxnfe/nfse.py +58 -0
- foxnfe-1.3.0/foxnfe/reference.py +74 -0
- foxnfe-1.3.0/foxnfe/rtc.py +81 -0
- foxnfe-1.3.0/foxnfe/support.py +38 -0
- foxnfe-1.3.0/foxnfe/types.py +110 -0
- foxnfe-1.3.0/foxnfe/webhook.py +71 -0
- foxnfe-1.3.0/foxnfe.egg-info/PKG-INFO +260 -0
- foxnfe-1.3.0/foxnfe.egg-info/SOURCES.txt +21 -0
- foxnfe-1.3.0/foxnfe.egg-info/dependency_links.txt +1 -0
- foxnfe-1.3.0/foxnfe.egg-info/requires.txt +1 -0
- foxnfe-1.3.0/foxnfe.egg-info/top_level.txt +1 -0
- foxnfe-1.3.0/pyproject.toml +36 -0
- foxnfe-1.3.0/setup.cfg +4 -0
foxnfe-1.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: foxnfe
|
|
3
|
+
Version: 1.3.0
|
|
4
|
+
Summary: SDK oficial FOX NF-e para Python — emissão NF-e, NFSe, cancelamento, consulta e MCP
|
|
5
|
+
Author-email: Central Fox Tecnologia <dev@centralfox.online>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://centralfox.online
|
|
8
|
+
Project-URL: Documentation, https://docs.centralfox.online/sdks/python
|
|
9
|
+
Project-URL: Repository, https://github.com/foxdigital/foxnfe-python
|
|
10
|
+
Keywords: nfe,nfse,nota-fiscal,fiscal,brasil
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: requests>=2.28.0
|
|
21
|
+
|
|
22
|
+
# foxnfe (Python SDK)
|
|
23
|
+
|
|
24
|
+
SDK oficial FOX NF-e para Python — emissão NF-e, NFSe, cancelamento, consulta e integração MCP.
|
|
25
|
+
|
|
26
|
+
## Requisitos
|
|
27
|
+
|
|
28
|
+
- Python 3.9+
|
|
29
|
+
- [requests](https://requests.readthedocs.io) `>=2.28`
|
|
30
|
+
|
|
31
|
+
## Instalação
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install foxnfe
|
|
35
|
+
# ou
|
|
36
|
+
poetry add foxnfe
|
|
37
|
+
# ou
|
|
38
|
+
uv add foxnfe
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from foxnfe import Client
|
|
45
|
+
|
|
46
|
+
client = Client(tenant_slug="minha-empresa")
|
|
47
|
+
|
|
48
|
+
# Autenticar
|
|
49
|
+
auth = client.login("email@empresa.com", "senha-segura")
|
|
50
|
+
print(f"Token: {auth.token}")
|
|
51
|
+
|
|
52
|
+
# Ou usar token existente
|
|
53
|
+
client = Client(tenant_slug="minha-empresa", token="seu-token-aqui")
|
|
54
|
+
# Ou via with_token (retorna nova instância)
|
|
55
|
+
authed = client.with_token("seu-token-aqui")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## NF-e
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from foxnfe import Client, NfeEmitRequest
|
|
62
|
+
|
|
63
|
+
client = Client(tenant_slug="minha-empresa", token="seu-token")
|
|
64
|
+
|
|
65
|
+
payload = NfeEmitRequest(
|
|
66
|
+
ambiente=2, # 2=homologação
|
|
67
|
+
certificate_id=1,
|
|
68
|
+
tomador={
|
|
69
|
+
"cnpj": "12345678000190",
|
|
70
|
+
"razao_social": "Empresa Tomadora Ltda",
|
|
71
|
+
"endereco": {
|
|
72
|
+
"logradouro": "Rua das Flores",
|
|
73
|
+
"numero": "100",
|
|
74
|
+
"municipio": "São Paulo",
|
|
75
|
+
"uf": "SP",
|
|
76
|
+
"cep": "01310100",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
itens=[{
|
|
80
|
+
"codigo": "SRV001",
|
|
81
|
+
"descricao": "Serviço de consultoria",
|
|
82
|
+
"cfop": "5933",
|
|
83
|
+
"quantidade": 1,
|
|
84
|
+
"valor_unitario": 1000.00,
|
|
85
|
+
"valor_total": 1000.00,
|
|
86
|
+
}],
|
|
87
|
+
pagamentos=[{"forma": "01", "valor": 1000.00}],
|
|
88
|
+
total=1000.00,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
result = client.nfe.emit(payload)
|
|
92
|
+
print(f"NF-e ID: {result['id']}")
|
|
93
|
+
|
|
94
|
+
# Aguardar autorização (polling automático)
|
|
95
|
+
nfe_autorizada = client.nfe.wait_for_authorization(result["id"])
|
|
96
|
+
print(f"Status: {nfe_autorizada.status}") # authorized
|
|
97
|
+
|
|
98
|
+
# Baixar XML
|
|
99
|
+
xml_bytes = client.nfe.xml(result["id"])
|
|
100
|
+
with open("nfe.xml", "wb") as f:
|
|
101
|
+
f.write(xml_bytes)
|
|
102
|
+
|
|
103
|
+
# Baixar DANFE PDF
|
|
104
|
+
pdf_bytes = client.nfe.pdf(result["id"])
|
|
105
|
+
with open("danfe.pdf", "wb") as f:
|
|
106
|
+
f.write(pdf_bytes)
|
|
107
|
+
|
|
108
|
+
# Cancelar
|
|
109
|
+
client.nfe.cancel(result["id"], "Cancelamento solicitado pelo cliente")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## NFSe
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from foxnfe import Client, NfseEmitRequest
|
|
116
|
+
|
|
117
|
+
client = Client(tenant_slug="minha-empresa", token="seu-token")
|
|
118
|
+
|
|
119
|
+
payload = NfseEmitRequest(
|
|
120
|
+
ambiente=2,
|
|
121
|
+
certificate_id=1,
|
|
122
|
+
prestador={
|
|
123
|
+
"cnpj": "12345678000190",
|
|
124
|
+
"inscricao_municipal": "123456",
|
|
125
|
+
"razao_social": "Minha Empresa Ltda",
|
|
126
|
+
"codigo_municipio": "3550308", # São Paulo (IBGE)
|
|
127
|
+
},
|
|
128
|
+
tomador={
|
|
129
|
+
"cnpj": "98765432000110",
|
|
130
|
+
"nome": "Cliente S.A.",
|
|
131
|
+
},
|
|
132
|
+
servico={
|
|
133
|
+
"codigo_tributacao_nacional": "01.01.00001",
|
|
134
|
+
"descricao": "Desenvolvimento de software",
|
|
135
|
+
"data_competencia": "2026-05-01",
|
|
136
|
+
"valor": 5000.00,
|
|
137
|
+
"aliquota_iss": 2.0,
|
|
138
|
+
},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
result = client.nfse.emit(payload)
|
|
142
|
+
nfse = client.nfse.get(result["id"])
|
|
143
|
+
print(f"Número NFSe: {nfse.numero_nfse}")
|
|
144
|
+
|
|
145
|
+
# Consultar por RPS ou chave
|
|
146
|
+
client.nfse.consult_by_numero("00000001")
|
|
147
|
+
client.nfse.consult_by_chave("SP3550308202605010000000000001")
|
|
148
|
+
|
|
149
|
+
# Cancelar / Substituir
|
|
150
|
+
client.nfse.cancel(result["id"], "Erro nos dados do tomador")
|
|
151
|
+
client.nfse.substitute(result["id"], payload, "Correção de dados")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## MCP (Model Context Protocol)
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from foxnfe import Client
|
|
158
|
+
|
|
159
|
+
client = Client(tenant_slug="minha-empresa", token="seu-token")
|
|
160
|
+
|
|
161
|
+
# Inicializar sessão MCP
|
|
162
|
+
info = client.mcp.initialize()
|
|
163
|
+
print(f"MCP Server: {info['result']['serverInfo']['name']}")
|
|
164
|
+
|
|
165
|
+
# Listar tools
|
|
166
|
+
tools = client.mcp.list_tools()
|
|
167
|
+
for tool in tools:
|
|
168
|
+
print(f"{tool.name}: {tool.description}")
|
|
169
|
+
|
|
170
|
+
# Chamar uma tool
|
|
171
|
+
result = client.mcp.call_tool("emitir_nfe", {
|
|
172
|
+
"ambiente": 2,
|
|
173
|
+
"certificate_id": 1,
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
if result.get("result", {}).get("isError"):
|
|
177
|
+
print("Tool error:", result["result"]["content"][0]["text"])
|
|
178
|
+
else:
|
|
179
|
+
print("Tool result:", result["result"]["content"][0]["text"])
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Tratamento de Erros
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from foxnfe import Client
|
|
186
|
+
from foxnfe.exceptions import ApiException, AuthException, FoxNfeException
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
client.nfe.emit(payload)
|
|
190
|
+
except AuthException as e:
|
|
191
|
+
# Token inválido ou expirado (401/403)
|
|
192
|
+
print(f"Auth error: {e}")
|
|
193
|
+
except ApiException as e:
|
|
194
|
+
# Erro da API (422, 500, etc.)
|
|
195
|
+
print(f"API error {e.status_code}: {e}")
|
|
196
|
+
print(f"Body: {e.response_body}")
|
|
197
|
+
except FoxNfeException as e:
|
|
198
|
+
# Timeout, erro de conexão, etc.
|
|
199
|
+
print(f"SDK error: {e}")
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Uso com context manager
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from foxnfe import Client
|
|
206
|
+
|
|
207
|
+
# Client usa requests.Session internamente (pode ser fechado manualmente)
|
|
208
|
+
client = Client(tenant_slug="minha-empresa", token="seu-token")
|
|
209
|
+
try:
|
|
210
|
+
result = client.nfe.emit(payload)
|
|
211
|
+
finally:
|
|
212
|
+
client._session.close()
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Configuração avançada
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
client = Client(
|
|
219
|
+
tenant_slug="minha-empresa",
|
|
220
|
+
token="seu-token",
|
|
221
|
+
base_url="https://sandbox.centralfox.online/api/v1",
|
|
222
|
+
timeout=60.0,
|
|
223
|
+
)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Estrutura do pacote
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
foxnfe/
|
|
230
|
+
├── __init__.py # Exports públicos
|
|
231
|
+
├── client.py # Cliente HTTP principal
|
|
232
|
+
├── nfe.py # Módulo NF-e
|
|
233
|
+
├── nfse.py # Módulo NFSe
|
|
234
|
+
├── mcp.py # Módulo MCP
|
|
235
|
+
├── types.py # Dataclasses com tipagem
|
|
236
|
+
└── exceptions.py # Classes de erro
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Links
|
|
240
|
+
|
|
241
|
+
- [Documentação API](https://docs.centralfox.online)
|
|
242
|
+
- [Portal FOX NF-e](https://foxnfe.centralfox.online)
|
|
243
|
+
- [Suporte](mailto:suporte@centralfox.online)
|
|
244
|
+
|
|
245
|
+
## 1.3.0 — eventos, rejeições, homologação, RTC, cobertura NFS-e e suporte
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
ev = client.nfe_events
|
|
249
|
+
ev.ator_interessado(15, "11222333000181") # 110150
|
|
250
|
+
ev.insucesso_entrega(15, "2026-09-08T10:00:00-03:00", tp_motivo=1) # 110192
|
|
251
|
+
ev.inutilizar(serie=1, numero_inicial=10, numero_final=12, justificativa="Numeração pulada por falha do ERP")
|
|
252
|
+
ev.contratos(); ev.registrar_evento(15, "econf", {...}) # eventos por contrato (conciliação financeira, RTC…)
|
|
253
|
+
|
|
254
|
+
client.nfe.rejeicao("539"); client.nfe.homologacao_run(65) # rejeições explicadas / amostras simuladas
|
|
255
|
+
client.nfse.cobertura_municipio("2304400") # driver, operações e provas
|
|
256
|
+
client.rtc.verify_resolution("550e8400-e29b-41d4-a716-446655440000")
|
|
257
|
+
client.support.create_case("Webhook sem entrega desde ontem", priority="high")
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Validação local (ids, dígitos, tamanhos, enums) antes do transporte; regra fiscal fica na API. `NfeResource.rejection` traz a rejeição classificada.
|
foxnfe-1.3.0/README.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# foxnfe (Python SDK)
|
|
2
|
+
|
|
3
|
+
SDK oficial FOX NF-e para Python — emissão NF-e, NFSe, cancelamento, consulta e integração MCP.
|
|
4
|
+
|
|
5
|
+
## Requisitos
|
|
6
|
+
|
|
7
|
+
- Python 3.9+
|
|
8
|
+
- [requests](https://requests.readthedocs.io) `>=2.28`
|
|
9
|
+
|
|
10
|
+
## Instalação
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install foxnfe
|
|
14
|
+
# ou
|
|
15
|
+
poetry add foxnfe
|
|
16
|
+
# ou
|
|
17
|
+
uv add foxnfe
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from foxnfe import Client
|
|
24
|
+
|
|
25
|
+
client = Client(tenant_slug="minha-empresa")
|
|
26
|
+
|
|
27
|
+
# Autenticar
|
|
28
|
+
auth = client.login("email@empresa.com", "senha-segura")
|
|
29
|
+
print(f"Token: {auth.token}")
|
|
30
|
+
|
|
31
|
+
# Ou usar token existente
|
|
32
|
+
client = Client(tenant_slug="minha-empresa", token="seu-token-aqui")
|
|
33
|
+
# Ou via with_token (retorna nova instância)
|
|
34
|
+
authed = client.with_token("seu-token-aqui")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## NF-e
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from foxnfe import Client, NfeEmitRequest
|
|
41
|
+
|
|
42
|
+
client = Client(tenant_slug="minha-empresa", token="seu-token")
|
|
43
|
+
|
|
44
|
+
payload = NfeEmitRequest(
|
|
45
|
+
ambiente=2, # 2=homologação
|
|
46
|
+
certificate_id=1,
|
|
47
|
+
tomador={
|
|
48
|
+
"cnpj": "12345678000190",
|
|
49
|
+
"razao_social": "Empresa Tomadora Ltda",
|
|
50
|
+
"endereco": {
|
|
51
|
+
"logradouro": "Rua das Flores",
|
|
52
|
+
"numero": "100",
|
|
53
|
+
"municipio": "São Paulo",
|
|
54
|
+
"uf": "SP",
|
|
55
|
+
"cep": "01310100",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
itens=[{
|
|
59
|
+
"codigo": "SRV001",
|
|
60
|
+
"descricao": "Serviço de consultoria",
|
|
61
|
+
"cfop": "5933",
|
|
62
|
+
"quantidade": 1,
|
|
63
|
+
"valor_unitario": 1000.00,
|
|
64
|
+
"valor_total": 1000.00,
|
|
65
|
+
}],
|
|
66
|
+
pagamentos=[{"forma": "01", "valor": 1000.00}],
|
|
67
|
+
total=1000.00,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
result = client.nfe.emit(payload)
|
|
71
|
+
print(f"NF-e ID: {result['id']}")
|
|
72
|
+
|
|
73
|
+
# Aguardar autorização (polling automático)
|
|
74
|
+
nfe_autorizada = client.nfe.wait_for_authorization(result["id"])
|
|
75
|
+
print(f"Status: {nfe_autorizada.status}") # authorized
|
|
76
|
+
|
|
77
|
+
# Baixar XML
|
|
78
|
+
xml_bytes = client.nfe.xml(result["id"])
|
|
79
|
+
with open("nfe.xml", "wb") as f:
|
|
80
|
+
f.write(xml_bytes)
|
|
81
|
+
|
|
82
|
+
# Baixar DANFE PDF
|
|
83
|
+
pdf_bytes = client.nfe.pdf(result["id"])
|
|
84
|
+
with open("danfe.pdf", "wb") as f:
|
|
85
|
+
f.write(pdf_bytes)
|
|
86
|
+
|
|
87
|
+
# Cancelar
|
|
88
|
+
client.nfe.cancel(result["id"], "Cancelamento solicitado pelo cliente")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## NFSe
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from foxnfe import Client, NfseEmitRequest
|
|
95
|
+
|
|
96
|
+
client = Client(tenant_slug="minha-empresa", token="seu-token")
|
|
97
|
+
|
|
98
|
+
payload = NfseEmitRequest(
|
|
99
|
+
ambiente=2,
|
|
100
|
+
certificate_id=1,
|
|
101
|
+
prestador={
|
|
102
|
+
"cnpj": "12345678000190",
|
|
103
|
+
"inscricao_municipal": "123456",
|
|
104
|
+
"razao_social": "Minha Empresa Ltda",
|
|
105
|
+
"codigo_municipio": "3550308", # São Paulo (IBGE)
|
|
106
|
+
},
|
|
107
|
+
tomador={
|
|
108
|
+
"cnpj": "98765432000110",
|
|
109
|
+
"nome": "Cliente S.A.",
|
|
110
|
+
},
|
|
111
|
+
servico={
|
|
112
|
+
"codigo_tributacao_nacional": "01.01.00001",
|
|
113
|
+
"descricao": "Desenvolvimento de software",
|
|
114
|
+
"data_competencia": "2026-05-01",
|
|
115
|
+
"valor": 5000.00,
|
|
116
|
+
"aliquota_iss": 2.0,
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
result = client.nfse.emit(payload)
|
|
121
|
+
nfse = client.nfse.get(result["id"])
|
|
122
|
+
print(f"Número NFSe: {nfse.numero_nfse}")
|
|
123
|
+
|
|
124
|
+
# Consultar por RPS ou chave
|
|
125
|
+
client.nfse.consult_by_numero("00000001")
|
|
126
|
+
client.nfse.consult_by_chave("SP3550308202605010000000000001")
|
|
127
|
+
|
|
128
|
+
# Cancelar / Substituir
|
|
129
|
+
client.nfse.cancel(result["id"], "Erro nos dados do tomador")
|
|
130
|
+
client.nfse.substitute(result["id"], payload, "Correção de dados")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## MCP (Model Context Protocol)
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from foxnfe import Client
|
|
137
|
+
|
|
138
|
+
client = Client(tenant_slug="minha-empresa", token="seu-token")
|
|
139
|
+
|
|
140
|
+
# Inicializar sessão MCP
|
|
141
|
+
info = client.mcp.initialize()
|
|
142
|
+
print(f"MCP Server: {info['result']['serverInfo']['name']}")
|
|
143
|
+
|
|
144
|
+
# Listar tools
|
|
145
|
+
tools = client.mcp.list_tools()
|
|
146
|
+
for tool in tools:
|
|
147
|
+
print(f"{tool.name}: {tool.description}")
|
|
148
|
+
|
|
149
|
+
# Chamar uma tool
|
|
150
|
+
result = client.mcp.call_tool("emitir_nfe", {
|
|
151
|
+
"ambiente": 2,
|
|
152
|
+
"certificate_id": 1,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
if result.get("result", {}).get("isError"):
|
|
156
|
+
print("Tool error:", result["result"]["content"][0]["text"])
|
|
157
|
+
else:
|
|
158
|
+
print("Tool result:", result["result"]["content"][0]["text"])
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Tratamento de Erros
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from foxnfe import Client
|
|
165
|
+
from foxnfe.exceptions import ApiException, AuthException, FoxNfeException
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
client.nfe.emit(payload)
|
|
169
|
+
except AuthException as e:
|
|
170
|
+
# Token inválido ou expirado (401/403)
|
|
171
|
+
print(f"Auth error: {e}")
|
|
172
|
+
except ApiException as e:
|
|
173
|
+
# Erro da API (422, 500, etc.)
|
|
174
|
+
print(f"API error {e.status_code}: {e}")
|
|
175
|
+
print(f"Body: {e.response_body}")
|
|
176
|
+
except FoxNfeException as e:
|
|
177
|
+
# Timeout, erro de conexão, etc.
|
|
178
|
+
print(f"SDK error: {e}")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Uso com context manager
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from foxnfe import Client
|
|
185
|
+
|
|
186
|
+
# Client usa requests.Session internamente (pode ser fechado manualmente)
|
|
187
|
+
client = Client(tenant_slug="minha-empresa", token="seu-token")
|
|
188
|
+
try:
|
|
189
|
+
result = client.nfe.emit(payload)
|
|
190
|
+
finally:
|
|
191
|
+
client._session.close()
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Configuração avançada
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
client = Client(
|
|
198
|
+
tenant_slug="minha-empresa",
|
|
199
|
+
token="seu-token",
|
|
200
|
+
base_url="https://sandbox.centralfox.online/api/v1",
|
|
201
|
+
timeout=60.0,
|
|
202
|
+
)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Estrutura do pacote
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
foxnfe/
|
|
209
|
+
├── __init__.py # Exports públicos
|
|
210
|
+
├── client.py # Cliente HTTP principal
|
|
211
|
+
├── nfe.py # Módulo NF-e
|
|
212
|
+
├── nfse.py # Módulo NFSe
|
|
213
|
+
├── mcp.py # Módulo MCP
|
|
214
|
+
├── types.py # Dataclasses com tipagem
|
|
215
|
+
└── exceptions.py # Classes de erro
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Links
|
|
219
|
+
|
|
220
|
+
- [Documentação API](https://docs.centralfox.online)
|
|
221
|
+
- [Portal FOX NF-e](https://foxnfe.centralfox.online)
|
|
222
|
+
- [Suporte](mailto:suporte@centralfox.online)
|
|
223
|
+
|
|
224
|
+
## 1.3.0 — eventos, rejeições, homologação, RTC, cobertura NFS-e e suporte
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
ev = client.nfe_events
|
|
228
|
+
ev.ator_interessado(15, "11222333000181") # 110150
|
|
229
|
+
ev.insucesso_entrega(15, "2026-09-08T10:00:00-03:00", tp_motivo=1) # 110192
|
|
230
|
+
ev.inutilizar(serie=1, numero_inicial=10, numero_final=12, justificativa="Numeração pulada por falha do ERP")
|
|
231
|
+
ev.contratos(); ev.registrar_evento(15, "econf", {...}) # eventos por contrato (conciliação financeira, RTC…)
|
|
232
|
+
|
|
233
|
+
client.nfe.rejeicao("539"); client.nfe.homologacao_run(65) # rejeições explicadas / amostras simuladas
|
|
234
|
+
client.nfse.cobertura_municipio("2304400") # driver, operações e provas
|
|
235
|
+
client.rtc.verify_resolution("550e8400-e29b-41d4-a716-446655440000")
|
|
236
|
+
client.support.create_case("Webhook sem entrega desde ontem", priority="high")
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Validação local (ids, dígitos, tamanhos, enums) antes do transporte; regra fiscal fica na API. `NfeResource.rejection` traz a rejeição classificada.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""SDK oficial FOX NF-e para Python."""
|
|
2
|
+
|
|
3
|
+
from .client import Client
|
|
4
|
+
from .exceptions import ApiException, AuthException, FoxNfeException
|
|
5
|
+
from .types import (
|
|
6
|
+
LoginResponse,
|
|
7
|
+
McpTool,
|
|
8
|
+
NfeEmitRequest,
|
|
9
|
+
NfeResource,
|
|
10
|
+
NfseEmitRequest,
|
|
11
|
+
NfseResource,
|
|
12
|
+
)
|
|
13
|
+
from .webhook import Webhook
|
|
14
|
+
from .reference import Reference
|
|
15
|
+
from .documents import Documents
|
|
16
|
+
from .events import NfeEvents
|
|
17
|
+
from .distribuicao import Distribuicao
|
|
18
|
+
from .rtc import Rtc
|
|
19
|
+
from .support import Support
|
|
20
|
+
|
|
21
|
+
__version__ = "1.3.0"
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Client",
|
|
24
|
+
"FoxNfeException",
|
|
25
|
+
"ApiException",
|
|
26
|
+
"AuthException",
|
|
27
|
+
"NfeEmitRequest",
|
|
28
|
+
"NfeResource",
|
|
29
|
+
"NfseEmitRequest",
|
|
30
|
+
"NfseResource",
|
|
31
|
+
"McpTool",
|
|
32
|
+
"LoginResponse",
|
|
33
|
+
"Webhook",
|
|
34
|
+
"Reference",
|
|
35
|
+
"Documents",
|
|
36
|
+
"NfeEvents",
|
|
37
|
+
"Distribuicao",
|
|
38
|
+
"Rtc",
|
|
39
|
+
"Support",
|
|
40
|
+
]
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Cliente HTTP base do SDK FOX NF-e."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
import requests
|
|
6
|
+
from requests import Response, Session
|
|
7
|
+
|
|
8
|
+
from .exceptions import ApiException, AuthException, FoxNfeException
|
|
9
|
+
from .types import LoginResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Client:
|
|
13
|
+
"""Cliente principal do SDK FOX NF-e."""
|
|
14
|
+
|
|
15
|
+
DEFAULT_BASE_URL = "https://foxnfe.centralfox.online/api/v1"
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
tenant_slug: str,
|
|
20
|
+
token: Optional[str] = None,
|
|
21
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
22
|
+
timeout: float = 30.0,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.tenant_slug = tenant_slug
|
|
25
|
+
self._token = token
|
|
26
|
+
self._base_url = base_url.rstrip("/")
|
|
27
|
+
self._timeout = timeout
|
|
28
|
+
self._session = Session()
|
|
29
|
+
self._session.headers.update({
|
|
30
|
+
"Accept": "application/json",
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
"X-Tenant-ID": tenant_slug,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
def login(self, email: str, password: str) -> LoginResponse:
|
|
36
|
+
"""Autentica e armazena o token internamente."""
|
|
37
|
+
data = self.post("auth/login", {"email": email, "password": password})
|
|
38
|
+
token = data.get("token")
|
|
39
|
+
if not token:
|
|
40
|
+
raise AuthException("Token não retornado pela API.")
|
|
41
|
+
self._token = token
|
|
42
|
+
return LoginResponse.from_dict(data)
|
|
43
|
+
|
|
44
|
+
def with_token(self, token: str) -> "Client":
|
|
45
|
+
"""Retorna uma nova instância com token definido."""
|
|
46
|
+
client = Client(
|
|
47
|
+
tenant_slug=self.tenant_slug,
|
|
48
|
+
token=token,
|
|
49
|
+
base_url=self._base_url,
|
|
50
|
+
timeout=self._timeout,
|
|
51
|
+
)
|
|
52
|
+
return client
|
|
53
|
+
|
|
54
|
+
def logout(self) -> None:
|
|
55
|
+
"""Invalida o token na API e limpa internamente."""
|
|
56
|
+
self.post("auth/logout")
|
|
57
|
+
self._token = None
|
|
58
|
+
|
|
59
|
+
def me(self) -> dict[str, Any]:
|
|
60
|
+
return self.get("auth/me")
|
|
61
|
+
|
|
62
|
+
# ── Módulos ─────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def nfe(self) -> "Nfe": # type: ignore[name-defined]
|
|
66
|
+
from .nfe import Nfe
|
|
67
|
+
return Nfe(self)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def nfse(self) -> "Nfse": # type: ignore[name-defined]
|
|
71
|
+
from .nfse import Nfse
|
|
72
|
+
return Nfse(self)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def mcp(self) -> "Mcp": # type: ignore[name-defined]
|
|
76
|
+
from .mcp import Mcp
|
|
77
|
+
return Mcp(self)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def webhook(self) -> "Webhook": # type: ignore[name-defined]
|
|
81
|
+
from .webhook import Webhook
|
|
82
|
+
return Webhook(self)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def reference(self) -> "Reference": # type: ignore[name-defined]
|
|
86
|
+
from .reference import Reference
|
|
87
|
+
return Reference(self)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def documents(self) -> "Documents": # type: ignore[name-defined]
|
|
91
|
+
from .documents import Documents
|
|
92
|
+
return Documents(self)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def nfe_events(self) -> "NfeEvents": # type: ignore[name-defined]
|
|
96
|
+
from .events import NfeEvents
|
|
97
|
+
return NfeEvents(self)
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def distribuicao(self) -> "Distribuicao": # type: ignore[name-defined]
|
|
101
|
+
from .distribuicao import Distribuicao
|
|
102
|
+
return Distribuicao(self)
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def rtc(self) -> "Rtc": # type: ignore[name-defined]
|
|
106
|
+
from .rtc import Rtc
|
|
107
|
+
return Rtc(self)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def support(self) -> "Support": # type: ignore[name-defined]
|
|
111
|
+
from .support import Support
|
|
112
|
+
return Support(self)
|
|
113
|
+
|
|
114
|
+
# ── HTTP helpers ─────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
def get(self, path: str, params: Optional[dict] = None) -> dict[str, Any]:
|
|
117
|
+
return self._request("GET", path, params=params)
|
|
118
|
+
|
|
119
|
+
def post(self, path: str, body: Optional[dict] = None) -> dict[str, Any]:
|
|
120
|
+
return self._request("POST", path, json=body)
|
|
121
|
+
|
|
122
|
+
def put(self, path: str, body: Optional[dict] = None) -> dict[str, Any]:
|
|
123
|
+
return self._request("PUT", path, json=body)
|
|
124
|
+
|
|
125
|
+
def delete(self, path: str) -> dict[str, Any]:
|
|
126
|
+
return self._request("DELETE", path)
|
|
127
|
+
|
|
128
|
+
def upload(self, path: str, field: str, filename: str, content: bytes, fields: Optional[dict] = None) -> dict[str, Any]:
|
|
129
|
+
"""Envio multipart (upload de XML); o Content-Type JSON da sessão é removido."""
|
|
130
|
+
return self._request("POST", path, files={field: (filename, content, "application/xml")}, data=fields or {})
|
|
131
|
+
|
|
132
|
+
def download(self, path: str) -> bytes:
|
|
133
|
+
"""Retorna bytes do conteúdo (XML, PDF)."""
|
|
134
|
+
resp = self._raw_request("GET", path)
|
|
135
|
+
if not resp.ok:
|
|
136
|
+
raise ApiException.from_response(resp.status_code, resp.json() if resp.content else {})
|
|
137
|
+
return resp.content
|
|
138
|
+
|
|
139
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
|
|
140
|
+
resp = self._raw_request(method, path, **kwargs)
|
|
141
|
+
try:
|
|
142
|
+
body = resp.json()
|
|
143
|
+
except Exception as exc:
|
|
144
|
+
raise FoxNfeException(f"Resposta JSON inválida: {exc}") from exc
|
|
145
|
+
|
|
146
|
+
if not resp.ok:
|
|
147
|
+
if resp.status_code in (401, 403):
|
|
148
|
+
raise AuthException(body.get("message", "Não autorizado."))
|
|
149
|
+
raise ApiException.from_response(resp.status_code, body)
|
|
150
|
+
|
|
151
|
+
return body # type: ignore[return-value]
|
|
152
|
+
|
|
153
|
+
def _raw_request(self, method: str, path: str, **kwargs: Any) -> Response:
|
|
154
|
+
url = f"{self._base_url}/{path.lstrip('/')}"
|
|
155
|
+
headers: dict[str, Any] = {}
|
|
156
|
+
if self._token:
|
|
157
|
+
headers["Authorization"] = f"Bearer {self._token}"
|
|
158
|
+
if "files" in kwargs:
|
|
159
|
+
headers["Content-Type"] = None # requests define o boundary multipart
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
return self._session.request(
|
|
163
|
+
method,
|
|
164
|
+
url,
|
|
165
|
+
headers=headers,
|
|
166
|
+
timeout=self._timeout,
|
|
167
|
+
**kwargs,
|
|
168
|
+
)
|
|
169
|
+
except requests.exceptions.Timeout as exc:
|
|
170
|
+
raise FoxNfeException(f"Timeout após {self._timeout}s") from exc
|
|
171
|
+
except requests.exceptions.ConnectionError as exc:
|
|
172
|
+
raise FoxNfeException(f"Erro de conexão: {exc}") from exc
|