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 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