fiscalapi-br 0.1.0__py3-none-any.whl
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.
- fiscalapi_br/__init__.py +29 -0
- fiscalapi_br/_client.py +427 -0
- fiscalapi_br/_errors.py +99 -0
- fiscalapi_br/_version.py +1 -0
- fiscalapi_br/py.typed +0 -0
- fiscalapi_br/types.py +250 -0
- fiscalapi_br-0.1.0.dist-info/METADATA +203 -0
- fiscalapi_br-0.1.0.dist-info/RECORD +10 -0
- fiscalapi_br-0.1.0.dist-info/WHEEL +4 -0
- fiscalapi_br-0.1.0.dist-info/licenses/LICENSE +21 -0
fiscalapi_br/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""SDK oficial da FiscalAPI para Python.
|
|
2
|
+
|
|
3
|
+
Consulta de Inscrição Estadual, CNPJ, certidões, NF-e, SIGEF, CNIR e perfil
|
|
4
|
+
do produtor rural pela API REST https://api.fiscalapi.com.br.
|
|
5
|
+
"""
|
|
6
|
+
from ._client import DEFAULT_BASE_URL, FiscalAPI
|
|
7
|
+
from ._errors import (
|
|
8
|
+
APIConnectionError,
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
BadRequestError,
|
|
11
|
+
FiscalAPIError,
|
|
12
|
+
InsufficientBalanceError,
|
|
13
|
+
RateLimitError,
|
|
14
|
+
ServerError,
|
|
15
|
+
)
|
|
16
|
+
from ._version import __version__
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"DEFAULT_BASE_URL",
|
|
20
|
+
"FiscalAPI",
|
|
21
|
+
"FiscalAPIError",
|
|
22
|
+
"AuthenticationError",
|
|
23
|
+
"InsufficientBalanceError",
|
|
24
|
+
"RateLimitError",
|
|
25
|
+
"BadRequestError",
|
|
26
|
+
"ServerError",
|
|
27
|
+
"APIConnectionError",
|
|
28
|
+
"__version__",
|
|
29
|
+
]
|
fiscalapi_br/_client.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"""Cliente HTTP da FiscalAPI — somente biblioteca padrão (zero dependências)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import random
|
|
8
|
+
import time
|
|
9
|
+
import urllib.error
|
|
10
|
+
import urllib.parse
|
|
11
|
+
import urllib.request
|
|
12
|
+
from typing import Any, Dict, Mapping, Optional, Union, cast
|
|
13
|
+
|
|
14
|
+
from ._errors import (
|
|
15
|
+
APIConnectionError,
|
|
16
|
+
AuthenticationError,
|
|
17
|
+
BadRequestError,
|
|
18
|
+
FiscalAPIError,
|
|
19
|
+
InsufficientBalanceError,
|
|
20
|
+
RateLimitError,
|
|
21
|
+
ServerError,
|
|
22
|
+
)
|
|
23
|
+
from ._version import __version__
|
|
24
|
+
from .types import (
|
|
25
|
+
CertidaoFederalResponse,
|
|
26
|
+
CertidoesTodasResponse,
|
|
27
|
+
CNDResponse,
|
|
28
|
+
CNDTodosResponse,
|
|
29
|
+
CnirResponse,
|
|
30
|
+
CNPJResponse,
|
|
31
|
+
ConsultaIEResponse,
|
|
32
|
+
ConsultaIETodosResponse,
|
|
33
|
+
NfeXmlResponse,
|
|
34
|
+
PerfilProdutorResponse,
|
|
35
|
+
SigefParcelaResponse,
|
|
36
|
+
SigefResponse,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
DEFAULT_BASE_URL = "https://api.fiscalapi.com.br"
|
|
40
|
+
# Algumas consultas (NF-e, SIGEF) podem levar até 180 s no servidor.
|
|
41
|
+
DEFAULT_TIMEOUT = 200.0
|
|
42
|
+
DEFAULT_MAX_RETRIES = 3
|
|
43
|
+
|
|
44
|
+
_RETRYABLE_ANY_METHOD = {429}
|
|
45
|
+
_RETRYABLE_IDEMPOTENT = {500, 502, 503, 504}
|
|
46
|
+
_MAX_BACKOFF = 30.0
|
|
47
|
+
_MAX_RETRY_AFTER = 60.0
|
|
48
|
+
|
|
49
|
+
Certificado = Union[bytes, bytearray, str, "os.PathLike[str]"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class FiscalAPI:
|
|
53
|
+
"""Cliente da FiscalAPI.
|
|
54
|
+
|
|
55
|
+
Exemplo::
|
|
56
|
+
|
|
57
|
+
from fiscalapi_br import FiscalAPI
|
|
58
|
+
|
|
59
|
+
api = FiscalAPI("sua-chave")
|
|
60
|
+
resp = api.consultar_ie(uf="MT", cpf="12345678909")
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
api_key: chave de API do painel. Se omitida, lê ``FISCALAPI_API_KEY``.
|
|
64
|
+
base_url: URL base da API.
|
|
65
|
+
timeout: tempo máximo, em segundos, de cada tentativa.
|
|
66
|
+
max_retries: novas tentativas em 429 e (para GET) em 5xx/falha de rede.
|
|
67
|
+
user_agent: sufixo opcional para o cabeçalho ``User-Agent``.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
api_key: Optional[str] = None,
|
|
73
|
+
*,
|
|
74
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
75
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
76
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
77
|
+
user_agent: Optional[str] = None,
|
|
78
|
+
) -> None:
|
|
79
|
+
key = api_key or os.environ.get("FISCALAPI_API_KEY")
|
|
80
|
+
if not key:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
"Informe a chave de API: FiscalAPI('sua-chave') ou a variável FISCALAPI_API_KEY."
|
|
83
|
+
)
|
|
84
|
+
self.api_key = key
|
|
85
|
+
self.base_url = base_url.rstrip("/")
|
|
86
|
+
self.timeout = timeout
|
|
87
|
+
self.max_retries = max(0, int(max_retries))
|
|
88
|
+
ua = f"fiscalapi-br-python/{__version__}"
|
|
89
|
+
self.user_agent = f"{ua} {user_agent}" if user_agent else ua
|
|
90
|
+
|
|
91
|
+
# ------------------------------------------------------------------ IE
|
|
92
|
+
|
|
93
|
+
def consultar_ie(
|
|
94
|
+
self,
|
|
95
|
+
uf: str,
|
|
96
|
+
*,
|
|
97
|
+
cpf: Optional[str] = None,
|
|
98
|
+
cnpj: Optional[str] = None,
|
|
99
|
+
ie: Optional[str] = None,
|
|
100
|
+
secundarias: bool = False,
|
|
101
|
+
) -> ConsultaIEResponse:
|
|
102
|
+
"""Inscrição Estadual em uma UF (``GET /api/consultar``).
|
|
103
|
+
|
|
104
|
+
Informe exatamente um entre ``cpf``, ``cnpj`` e ``ie``. Com
|
|
105
|
+
``secundarias=True`` a resposta traz as atividades secundárias
|
|
106
|
+
(``cnaes_secundarios``) — a consulta fica mais lenta.
|
|
107
|
+
"""
|
|
108
|
+
doc = _um_documento(cpf=cpf, cnpj=cnpj, ie=ie)
|
|
109
|
+
params = {"uf": _uf(uf), **doc, "secundarias": secundarias or None}
|
|
110
|
+
return cast(ConsultaIEResponse, self._get("/api/consultar", params))
|
|
111
|
+
|
|
112
|
+
def consultar_ie_todos(
|
|
113
|
+
self,
|
|
114
|
+
*,
|
|
115
|
+
cpf: Optional[str] = None,
|
|
116
|
+
cnpj: Optional[str] = None,
|
|
117
|
+
ie: Optional[str] = None,
|
|
118
|
+
secundarias: bool = False,
|
|
119
|
+
) -> ConsultaIETodosResponse:
|
|
120
|
+
"""Inscrição Estadual em todos os estados (``GET /api/consultar-ie-todos``).
|
|
121
|
+
|
|
122
|
+
Confira ``complete``: quando ``False``, algum estado falhou e um
|
|
123
|
+
resultado vazio não significa "não encontrado".
|
|
124
|
+
"""
|
|
125
|
+
doc = _um_documento(cpf=cpf, cnpj=cnpj, ie=ie)
|
|
126
|
+
params = {**doc, "secundarias": secundarias or None}
|
|
127
|
+
return cast(ConsultaIETodosResponse, self._get("/api/consultar-ie-todos", params))
|
|
128
|
+
|
|
129
|
+
# ---------------------------------------------------------------- CNPJ
|
|
130
|
+
|
|
131
|
+
def consultar_cnpj(self, cnpj: str, *, force_fresh: bool = False) -> CNPJResponse:
|
|
132
|
+
"""Dados cadastrais do CNPJ na Receita Federal (``GET /api/consultar-cnpj``).
|
|
133
|
+
|
|
134
|
+
``force_fresh=True`` consulta o dado atual, sem cache (mais lento e
|
|
135
|
+
consome mais créditos).
|
|
136
|
+
"""
|
|
137
|
+
if not cnpj:
|
|
138
|
+
raise ValueError("Informe o CNPJ.")
|
|
139
|
+
params = {"cnpj": cnpj, "force_fresh": force_fresh or None}
|
|
140
|
+
return cast(CNPJResponse, self._get("/api/consultar-cnpj", params))
|
|
141
|
+
|
|
142
|
+
# ----------------------------------------------------------- Certidões
|
|
143
|
+
|
|
144
|
+
def consultar_cnd(
|
|
145
|
+
self, uf: str, *, cpf: Optional[str] = None, cnpj: Optional[str] = None
|
|
146
|
+
) -> CNDResponse:
|
|
147
|
+
"""CND estadual (ICMS) em uma UF (``GET /api/consultar-cnd``)."""
|
|
148
|
+
params = {"uf": _uf(uf), **_um_documento(cpf=cpf, cnpj=cnpj)}
|
|
149
|
+
return cast(CNDResponse, self._get("/api/consultar-cnd", params))
|
|
150
|
+
|
|
151
|
+
def consultar_cnd_todos(
|
|
152
|
+
self, *, cpf: Optional[str] = None, cnpj: Optional[str] = None
|
|
153
|
+
) -> CNDTodosResponse:
|
|
154
|
+
"""CND estadual em todos os estados disponíveis (``GET /api/consultar-cnd-todos``)."""
|
|
155
|
+
params = _um_documento(cpf=cpf, cnpj=cnpj)
|
|
156
|
+
return cast(CNDTodosResponse, self._get("/api/consultar-cnd-todos", params))
|
|
157
|
+
|
|
158
|
+
def consultar_cnd_federal(
|
|
159
|
+
self,
|
|
160
|
+
*,
|
|
161
|
+
cpf: Optional[str] = None,
|
|
162
|
+
cnpj: Optional[str] = None,
|
|
163
|
+
emitir_nova: bool = False,
|
|
164
|
+
) -> CertidaoFederalResponse:
|
|
165
|
+
"""CND Federal — Receita Federal + PGFN (``GET /api/consultar-cnd-federal``).
|
|
166
|
+
|
|
167
|
+
Devolve a certidão vigente quando ela vale por mais de 3 dias
|
|
168
|
+
(``modo="consulta"``); senão, emite uma nova (``modo="emissao"``).
|
|
169
|
+
``emitir_nova=True`` força uma nova emissão.
|
|
170
|
+
"""
|
|
171
|
+
params = {**_um_documento(cpf=cpf, cnpj=cnpj), "emitir_nova": emitir_nova or None}
|
|
172
|
+
return cast(CertidaoFederalResponse, self._get("/api/consultar-cnd-federal", params))
|
|
173
|
+
|
|
174
|
+
def consultar_cndt(
|
|
175
|
+
self, *, cpf: Optional[str] = None, cnpj: Optional[str] = None
|
|
176
|
+
) -> CertidaoFederalResponse:
|
|
177
|
+
"""CNDT — débitos trabalhistas (``GET /api/consultar-cndt``)."""
|
|
178
|
+
params = _um_documento(cpf=cpf, cnpj=cnpj)
|
|
179
|
+
return cast(CertidaoFederalResponse, self._get("/api/consultar-cndt", params))
|
|
180
|
+
|
|
181
|
+
def consultar_crf_fgts(
|
|
182
|
+
self, *, cpf: Optional[str] = None, cnpj: Optional[str] = None
|
|
183
|
+
) -> CertidaoFederalResponse:
|
|
184
|
+
"""CRF — regularidade do FGTS (``GET /api/consultar-crf-fgts``)."""
|
|
185
|
+
params = _um_documento(cpf=cpf, cnpj=cnpj)
|
|
186
|
+
return cast(CertidaoFederalResponse, self._get("/api/consultar-crf-fgts", params))
|
|
187
|
+
|
|
188
|
+
def consultar_certidoes_todas(
|
|
189
|
+
self,
|
|
190
|
+
*,
|
|
191
|
+
cpf: Optional[str] = None,
|
|
192
|
+
cnpj: Optional[str] = None,
|
|
193
|
+
skip_estaduais: bool = False,
|
|
194
|
+
) -> CertidoesTodasResponse:
|
|
195
|
+
"""CNDs estaduais + CNDT + CND Federal + CRF/FGTS em uma chamada
|
|
196
|
+
(``GET /api/certidoes-todas``)."""
|
|
197
|
+
params = {**_um_documento(cpf=cpf, cnpj=cnpj), "skip_estaduais": skip_estaduais or None}
|
|
198
|
+
return cast(CertidoesTodasResponse, self._get("/api/certidoes-todas", params))
|
|
199
|
+
|
|
200
|
+
# ---------------------------------------------------------------- NF-e
|
|
201
|
+
|
|
202
|
+
def consultar_nfe_xml(
|
|
203
|
+
self,
|
|
204
|
+
chave: str,
|
|
205
|
+
*,
|
|
206
|
+
certificado: Certificado,
|
|
207
|
+
senha: str,
|
|
208
|
+
manifestar: bool = True,
|
|
209
|
+
) -> NfeXmlResponse:
|
|
210
|
+
"""XML completo de uma NF-e pela chave de acesso (``POST /api/consultar-nfe-xml``).
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
chave: chave de acesso com 44 dígitos.
|
|
214
|
+
certificado: certificado A1 (.pfx/.p12) do participante da nota —
|
|
215
|
+
caminho do arquivo, ``bytes`` do arquivo ou o conteúdo já em
|
|
216
|
+
base64. O certificado não é armazenado pela API.
|
|
217
|
+
senha: senha do certificado.
|
|
218
|
+
manifestar: quando só há o resumo da nota, registra a manifestação
|
|
219
|
+
do destinatário para liberar o XML. ``False`` não registra
|
|
220
|
+
nenhum evento.
|
|
221
|
+
"""
|
|
222
|
+
if not chave:
|
|
223
|
+
raise ValueError("Informe a chave de acesso.")
|
|
224
|
+
body = {
|
|
225
|
+
"chave": "".join(ch for ch in chave if ch.isdigit()),
|
|
226
|
+
"certificado": _certificado_base64(certificado),
|
|
227
|
+
"senha": senha,
|
|
228
|
+
"manifestar": manifestar,
|
|
229
|
+
}
|
|
230
|
+
# POST não é repetido em 5xx: a manifestação pode já ter sido registrada.
|
|
231
|
+
return cast(NfeXmlResponse, self._request("POST", "/api/consultar-nfe-xml", json_body=body))
|
|
232
|
+
|
|
233
|
+
# ------------------------------------------------------- Imóvel rural
|
|
234
|
+
|
|
235
|
+
def consultar_sigef(
|
|
236
|
+
self, *, cpf: Optional[str] = None, cnpj: Optional[str] = None
|
|
237
|
+
) -> SigefResponse:
|
|
238
|
+
"""Parcelas certificadas no SIGEF/INCRA do detentor (``GET /api/consultar-sigef``)."""
|
|
239
|
+
params = _um_documento(cpf=cpf, cnpj=cnpj)
|
|
240
|
+
return cast(SigefResponse, self._get("/api/consultar-sigef", params))
|
|
241
|
+
|
|
242
|
+
def consultar_sigef_parcela(
|
|
243
|
+
self, uuid: str, *, force_fresh: bool = False
|
|
244
|
+
) -> SigefParcelaResponse:
|
|
245
|
+
"""Uma parcela SIGEF pelo UUID, com polígono, vértices e confrontantes
|
|
246
|
+
(``GET /api/consultar-sigef-parcela``)."""
|
|
247
|
+
if not uuid:
|
|
248
|
+
raise ValueError("Informe o UUID da parcela.")
|
|
249
|
+
params = {"uuid": uuid.strip(), "force_fresh": force_fresh or None}
|
|
250
|
+
return cast(SigefParcelaResponse, self._get("/api/consultar-sigef-parcela", params))
|
|
251
|
+
|
|
252
|
+
def consultar_cnir(
|
|
253
|
+
self, *, cpf: Optional[str] = None, cnpj: Optional[str] = None
|
|
254
|
+
) -> CnirResponse:
|
|
255
|
+
"""Imóveis rurais no CNIR/CAFIR (``GET /api/consultar-cnir``)."""
|
|
256
|
+
params = _um_documento(cpf=cpf, cnpj=cnpj)
|
|
257
|
+
return cast(CnirResponse, self._get("/api/consultar-cnir", params))
|
|
258
|
+
|
|
259
|
+
# ------------------------------------------------------------- Perfil
|
|
260
|
+
|
|
261
|
+
def perfil_produtor(
|
|
262
|
+
self, *, cpf: Optional[str] = None, cnpj: Optional[str] = None
|
|
263
|
+
) -> PerfilProdutorResponse:
|
|
264
|
+
"""Perfil consolidado do produtor rural (``GET /api/perfil-produtor``)."""
|
|
265
|
+
params = _um_documento(cpf=cpf, cnpj=cnpj)
|
|
266
|
+
return cast(PerfilProdutorResponse, self._get("/api/perfil-produtor", params))
|
|
267
|
+
|
|
268
|
+
# ----------------------------------------------------------- Transporte
|
|
269
|
+
|
|
270
|
+
def _get(self, path: str, params: Mapping[str, Any]) -> Dict[str, Any]:
|
|
271
|
+
return self._request("GET", path, params=params)
|
|
272
|
+
|
|
273
|
+
def _request(
|
|
274
|
+
self,
|
|
275
|
+
method: str,
|
|
276
|
+
path: str,
|
|
277
|
+
*,
|
|
278
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
279
|
+
json_body: Optional[Mapping[str, Any]] = None,
|
|
280
|
+
) -> Dict[str, Any]:
|
|
281
|
+
url = self.base_url + path
|
|
282
|
+
query = _encode_query(params or {})
|
|
283
|
+
if query:
|
|
284
|
+
url = f"{url}?{query}"
|
|
285
|
+
|
|
286
|
+
data: Optional[bytes] = None
|
|
287
|
+
headers = {
|
|
288
|
+
"X-API-Key": self.api_key,
|
|
289
|
+
"Accept": "application/json",
|
|
290
|
+
"User-Agent": self.user_agent,
|
|
291
|
+
}
|
|
292
|
+
if json_body is not None:
|
|
293
|
+
data = json.dumps(json_body).encode("utf-8")
|
|
294
|
+
headers["Content-Type"] = "application/json"
|
|
295
|
+
|
|
296
|
+
idempotent = method == "GET"
|
|
297
|
+
attempt = 0
|
|
298
|
+
while True:
|
|
299
|
+
req = urllib.request.Request(url, data=data, method=method, headers=headers)
|
|
300
|
+
try:
|
|
301
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
302
|
+
raw = resp.read()
|
|
303
|
+
return _decode_success(raw)
|
|
304
|
+
except urllib.error.HTTPError as exc:
|
|
305
|
+
status = exc.code
|
|
306
|
+
resp_headers = dict(exc.headers.items()) if exc.headers else {}
|
|
307
|
+
try:
|
|
308
|
+
raw_err = exc.read()
|
|
309
|
+
except OSError:
|
|
310
|
+
raw_err = b""
|
|
311
|
+
retryable = status in _RETRYABLE_ANY_METHOD or (
|
|
312
|
+
idempotent and status in _RETRYABLE_IDEMPOTENT
|
|
313
|
+
)
|
|
314
|
+
if retryable and attempt < self.max_retries:
|
|
315
|
+
time.sleep(_delay(attempt, resp_headers.get("Retry-After")))
|
|
316
|
+
attempt += 1
|
|
317
|
+
continue
|
|
318
|
+
raise _error_from_response(status, raw_err, resp_headers) from None
|
|
319
|
+
except OSError as exc: # URLError, timeout, conexão recusada
|
|
320
|
+
if idempotent and attempt < self.max_retries:
|
|
321
|
+
time.sleep(_delay(attempt, None))
|
|
322
|
+
attempt += 1
|
|
323
|
+
continue
|
|
324
|
+
raise APIConnectionError(f"Falha ao conectar à FiscalAPI: {exc}") from exc
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# ---------------------------------------------------------------- utilitários
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _um_documento(**docs: Optional[str]) -> Dict[str, str]:
|
|
331
|
+
informados = {k: v.strip() for k, v in docs.items() if v is not None and v.strip()}
|
|
332
|
+
nomes = " ou ".join(docs.keys())
|
|
333
|
+
if not informados:
|
|
334
|
+
raise ValueError(f"Informe {nomes}.")
|
|
335
|
+
if len(informados) > 1:
|
|
336
|
+
raise ValueError(f"Informe apenas um: {nomes}.")
|
|
337
|
+
return informados
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _uf(uf: str) -> str:
|
|
341
|
+
if not uf or len(uf.strip()) != 2:
|
|
342
|
+
raise ValueError("Informe a UF com 2 letras (ex.: MT).")
|
|
343
|
+
return uf.strip().upper()
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _encode_query(params: Mapping[str, Any]) -> str:
|
|
347
|
+
clean = []
|
|
348
|
+
for key, value in params.items():
|
|
349
|
+
if value is None:
|
|
350
|
+
continue
|
|
351
|
+
if isinstance(value, bool):
|
|
352
|
+
value = "true" if value else "false"
|
|
353
|
+
clean.append((key, str(value)))
|
|
354
|
+
return urllib.parse.urlencode(clean)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _certificado_base64(cert: Certificado) -> str:
|
|
358
|
+
if isinstance(cert, (bytes, bytearray)):
|
|
359
|
+
return base64.b64encode(bytes(cert)).decode("ascii")
|
|
360
|
+
if isinstance(cert, os.PathLike):
|
|
361
|
+
with open(cert, "rb") as fh:
|
|
362
|
+
return base64.b64encode(fh.read()).decode("ascii")
|
|
363
|
+
if isinstance(cert, str):
|
|
364
|
+
try:
|
|
365
|
+
is_file = len(cert) < 4096 and os.path.isfile(cert)
|
|
366
|
+
except (OSError, ValueError):
|
|
367
|
+
is_file = False
|
|
368
|
+
if is_file:
|
|
369
|
+
with open(cert, "rb") as fh:
|
|
370
|
+
return base64.b64encode(fh.read()).decode("ascii")
|
|
371
|
+
return "".join(cert.split()) # já está em base64
|
|
372
|
+
raise TypeError("certificado deve ser caminho, bytes ou string base64.")
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _delay(attempt: int, retry_after: Optional[str]) -> float:
|
|
376
|
+
if retry_after:
|
|
377
|
+
try:
|
|
378
|
+
return min(max(float(retry_after), 0.0), _MAX_RETRY_AFTER)
|
|
379
|
+
except ValueError:
|
|
380
|
+
pass
|
|
381
|
+
base = min(_MAX_BACKOFF, 0.5 * (2 ** attempt))
|
|
382
|
+
return base + random.uniform(0, base / 4)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _decode_success(raw: bytes) -> Dict[str, Any]:
|
|
386
|
+
if not raw:
|
|
387
|
+
return {}
|
|
388
|
+
try:
|
|
389
|
+
decoded = json.loads(raw.decode("utf-8"))
|
|
390
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
391
|
+
raise FiscalAPIError("Resposta da API não é JSON válido.", body=raw[:500]) from exc
|
|
392
|
+
return decoded if isinstance(decoded, dict) else {"data": decoded}
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _error_from_response(status: int, raw: bytes, headers: Mapping[str, str]) -> FiscalAPIError:
|
|
396
|
+
body: Any
|
|
397
|
+
try:
|
|
398
|
+
body = json.loads(raw.decode("utf-8")) if raw else None
|
|
399
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
400
|
+
body = raw.decode("utf-8", errors="replace")[:500]
|
|
401
|
+
|
|
402
|
+
code: Optional[str] = None
|
|
403
|
+
message = f"Erro HTTP {status}"
|
|
404
|
+
request_id: Optional[str] = None
|
|
405
|
+
if isinstance(body, dict):
|
|
406
|
+
code = body.get("error") or body.get("title")
|
|
407
|
+
detail = body.get("message") or body.get("detail")
|
|
408
|
+
if isinstance(detail, str) and detail:
|
|
409
|
+
message = detail
|
|
410
|
+
elif isinstance(detail, list) and detail:
|
|
411
|
+
message = "; ".join(str(d.get("msg", d)) if isinstance(d, dict) else str(d) for d in detail)
|
|
412
|
+
request_id = body.get("request_id") or None
|
|
413
|
+
elif isinstance(body, str) and body:
|
|
414
|
+
message = body
|
|
415
|
+
|
|
416
|
+
kwargs = dict(status_code=status, code=code, request_id=request_id, body=body, headers=headers)
|
|
417
|
+
if status in (401, 403):
|
|
418
|
+
return AuthenticationError(message, **kwargs)
|
|
419
|
+
if status == 402:
|
|
420
|
+
return InsufficientBalanceError(message, **kwargs)
|
|
421
|
+
if status == 429:
|
|
422
|
+
return RateLimitError(message, **kwargs)
|
|
423
|
+
if status >= 500:
|
|
424
|
+
return ServerError(message, **kwargs)
|
|
425
|
+
if 400 <= status < 500:
|
|
426
|
+
return BadRequestError(message, **kwargs)
|
|
427
|
+
return FiscalAPIError(message, **kwargs)
|
fiscalapi_br/_errors.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Exceções levantadas pelo cliente da FiscalAPI."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Mapping, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FiscalAPIError(Exception):
|
|
8
|
+
"""Erro base. Toda exceção do SDK herda desta classe.
|
|
9
|
+
|
|
10
|
+
Atributos:
|
|
11
|
+
message: mensagem legível devolvida pela API (ou gerada pelo SDK).
|
|
12
|
+
status_code: status HTTP (``None`` em falha de conexão).
|
|
13
|
+
code: código do erro devolvido pela API (ex.: ``PARAMETRO_AUSENTE``).
|
|
14
|
+
request_id: identificador da requisição — informe-o ao suporte.
|
|
15
|
+
body: corpo da resposta já decodificado (dict) ou texto bruto.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
message: str,
|
|
21
|
+
*,
|
|
22
|
+
status_code: Optional[int] = None,
|
|
23
|
+
code: Optional[str] = None,
|
|
24
|
+
request_id: Optional[str] = None,
|
|
25
|
+
body: Any = None,
|
|
26
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
super().__init__(message)
|
|
29
|
+
self.message = message
|
|
30
|
+
self.status_code = status_code
|
|
31
|
+
self.code = code
|
|
32
|
+
self.request_id = request_id
|
|
33
|
+
self.body = body
|
|
34
|
+
self.headers = dict(headers or {})
|
|
35
|
+
|
|
36
|
+
def __str__(self) -> str:
|
|
37
|
+
parts = []
|
|
38
|
+
if self.status_code is not None:
|
|
39
|
+
parts.append(f"HTTP {self.status_code}")
|
|
40
|
+
if self.code:
|
|
41
|
+
parts.append(self.code)
|
|
42
|
+
prefix = " ".join(parts)
|
|
43
|
+
suffix = f" (request_id={self.request_id})" if self.request_id else ""
|
|
44
|
+
return f"{prefix}: {self.message}{suffix}" if prefix else f"{self.message}{suffix}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AuthenticationError(FiscalAPIError):
|
|
48
|
+
"""Chave de API ausente, inválida ou revogada (HTTP 401/403)."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class InsufficientBalanceError(FiscalAPIError):
|
|
52
|
+
"""Sem plano ativo ou saldo esgotado (HTTP 402).
|
|
53
|
+
|
|
54
|
+
``consultas_restantes`` diz quantas consultas desse tipo o saldo ainda paga
|
|
55
|
+
(quando a API informa).
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def consultas_restantes(self) -> Optional[int]:
|
|
60
|
+
if isinstance(self.body, dict):
|
|
61
|
+
value = self.body.get("consultas_restantes")
|
|
62
|
+
return value if isinstance(value, int) else None
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def plan(self) -> Optional[str]:
|
|
67
|
+
if isinstance(self.body, dict):
|
|
68
|
+
value = self.body.get("plan")
|
|
69
|
+
return value if isinstance(value, str) else None
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class RateLimitError(FiscalAPIError):
|
|
74
|
+
"""Limite de requisições por minuto do plano excedido (HTTP 429).
|
|
75
|
+
|
|
76
|
+
``retry_after`` traz, em segundos, quanto esperar antes de tentar de novo.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def retry_after(self) -> Optional[int]:
|
|
81
|
+
raw = self.headers.get("Retry-After") or self.headers.get("retry-after")
|
|
82
|
+
if raw is None and isinstance(self.body, dict):
|
|
83
|
+
raw = self.body.get("retry_after")
|
|
84
|
+
try:
|
|
85
|
+
return int(raw) if raw is not None else None
|
|
86
|
+
except (TypeError, ValueError):
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class BadRequestError(FiscalAPIError):
|
|
91
|
+
"""Parâmetro ausente, conflitante ou inválido (HTTP 400/404/422)."""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ServerError(FiscalAPIError):
|
|
95
|
+
"""Falha do lado da API ou tempo esgotado na fonte (HTTP 5xx)."""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class APIConnectionError(FiscalAPIError):
|
|
99
|
+
"""Não foi possível falar com a API (DNS, TLS, conexão recusada, timeout)."""
|
fiscalapi_br/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
fiscalapi_br/py.typed
ADDED
|
File without changes
|
fiscalapi_br/types.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Tipos (TypedDict) das respostas mais usadas da FiscalAPI.
|
|
2
|
+
|
|
3
|
+
As respostas são dicionários comuns; estes tipos servem para autocompletar no
|
|
4
|
+
editor e para checagem estática. Campos novos podem aparecer sem aviso — o
|
|
5
|
+
SDK não descarta nada do que a API devolve.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import List, Literal, Optional, TypedDict
|
|
10
|
+
|
|
11
|
+
StatusFonte = Literal["success", "error", "timeout", "skipped"]
|
|
12
|
+
StatusCertidao = Literal[
|
|
13
|
+
"negativa",
|
|
14
|
+
"positiva",
|
|
15
|
+
"positiva_com_efeitos_de_negativa",
|
|
16
|
+
"nao_contribuinte",
|
|
17
|
+
"sem_certidao",
|
|
18
|
+
"erro",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SourceStatus(TypedDict, total=False):
|
|
23
|
+
status: StatusFonte
|
|
24
|
+
error_code: str
|
|
25
|
+
error_message: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CnaeSecundario(TypedDict, total=False):
|
|
29
|
+
codigo: str
|
|
30
|
+
descricao: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class InscricaoEstadual(TypedDict, total=False):
|
|
34
|
+
inscricao_estadual: str
|
|
35
|
+
razao_social: str
|
|
36
|
+
nome_fantasia: str
|
|
37
|
+
situacao_ie: str
|
|
38
|
+
uf_ie: str
|
|
39
|
+
tipo_ie: str
|
|
40
|
+
logradouro: str
|
|
41
|
+
complemento: str
|
|
42
|
+
bairro: str
|
|
43
|
+
numero: str
|
|
44
|
+
municipio: str
|
|
45
|
+
municipio_ibge: str
|
|
46
|
+
cep: str
|
|
47
|
+
regime_pagamento: str
|
|
48
|
+
data_situacao: str
|
|
49
|
+
situacao_contribuinte: str
|
|
50
|
+
data_inicio_atividade: str
|
|
51
|
+
cnae_codigo: str
|
|
52
|
+
cnae_descricao: str
|
|
53
|
+
regime_apuracao_icms: str
|
|
54
|
+
situacao_nfe: str
|
|
55
|
+
cpf_cnpj: str
|
|
56
|
+
situacao_cadastral: str
|
|
57
|
+
cnaes_secundarios: Optional[List[CnaeSecundario]]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ConsultaIEResponse(TypedDict, total=False):
|
|
61
|
+
request_id: str
|
|
62
|
+
document: str
|
|
63
|
+
document_type: str
|
|
64
|
+
uf: str
|
|
65
|
+
cached: bool
|
|
66
|
+
results: List[InscricaoEstadual]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class IEPorUF(TypedDict, total=False):
|
|
70
|
+
uf: str
|
|
71
|
+
source_status: SourceStatus
|
|
72
|
+
results: List[InscricaoEstadual]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ConsultaIETodosResponse(TypedDict, total=False):
|
|
76
|
+
request_id: str
|
|
77
|
+
document: str
|
|
78
|
+
document_type: str
|
|
79
|
+
cached_ufs: List[str]
|
|
80
|
+
results: List[IEPorUF]
|
|
81
|
+
complete: bool
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class CNPJResult(TypedDict, total=False):
|
|
85
|
+
razao_social: str
|
|
86
|
+
nome_fantasia: str
|
|
87
|
+
situacao_cadastral: str
|
|
88
|
+
data_situacao_cadastral: str
|
|
89
|
+
cnae_principal_codigo: str
|
|
90
|
+
cnae_principal_descricao: str
|
|
91
|
+
cnaes_secundarios: List[dict]
|
|
92
|
+
natureza_juridica: str
|
|
93
|
+
capital_social: str
|
|
94
|
+
endereco: dict
|
|
95
|
+
qsa: List[dict]
|
|
96
|
+
porte: str
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class CNPJResponse(TypedDict, total=False):
|
|
100
|
+
request_id: str
|
|
101
|
+
cnpj: str
|
|
102
|
+
cached: bool
|
|
103
|
+
source_status: SourceStatus
|
|
104
|
+
result: Optional[CNPJResult]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Certidao(TypedDict, total=False):
|
|
108
|
+
status: StatusCertidao
|
|
109
|
+
status_raw: str
|
|
110
|
+
validade: str
|
|
111
|
+
validade_iso: str
|
|
112
|
+
emissao: str
|
|
113
|
+
emissao_iso: str
|
|
114
|
+
protocolo: str
|
|
115
|
+
pdf_base64: str
|
|
116
|
+
url_verificacao: str
|
|
117
|
+
codigo_autenticacao: str
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class CNDResponse(TypedDict, total=False):
|
|
121
|
+
request_id: str
|
|
122
|
+
document: str
|
|
123
|
+
document_type: str
|
|
124
|
+
uf: str
|
|
125
|
+
cached: bool
|
|
126
|
+
source_status: SourceStatus
|
|
127
|
+
result: Optional[Certidao]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class CNDPorUF(TypedDict, total=False):
|
|
131
|
+
uf: str
|
|
132
|
+
source_status: SourceStatus
|
|
133
|
+
result: Optional[Certidao]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class CNDTodosResponse(TypedDict, total=False):
|
|
137
|
+
request_id: str
|
|
138
|
+
document: str
|
|
139
|
+
document_type: str
|
|
140
|
+
results: List[CNDPorUF]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class CertidaoFederal(Certidao, total=False):
|
|
144
|
+
tipo: Literal["cndt", "cnd_federal", "crf_fgts"]
|
|
145
|
+
orgao: str
|
|
146
|
+
modo: Literal["consulta", "emissao"]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class CertidaoFederalResponse(TypedDict, total=False):
|
|
150
|
+
request_id: str
|
|
151
|
+
document: str
|
|
152
|
+
document_type: str
|
|
153
|
+
cached: bool
|
|
154
|
+
source_status: SourceStatus
|
|
155
|
+
result: Optional[CertidaoFederal]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class CertidoesTodasResponse(TypedDict, total=False):
|
|
159
|
+
request_id: str
|
|
160
|
+
document: str
|
|
161
|
+
document_type: str
|
|
162
|
+
cnd_estaduais: Optional[CNDTodosResponse]
|
|
163
|
+
certidoes_federais: List[CertidaoFederalResponse]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class NfeXmlResultado(TypedDict, total=False):
|
|
167
|
+
chave: str
|
|
168
|
+
status: str
|
|
169
|
+
cstat: str
|
|
170
|
+
mensagem: str
|
|
171
|
+
from_cache: bool
|
|
172
|
+
manifestacao: Optional[str]
|
|
173
|
+
xml: Optional[str]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class NfeXmlResponse(TypedDict, total=False):
|
|
177
|
+
request_id: str
|
|
178
|
+
total: int
|
|
179
|
+
com_dado: int
|
|
180
|
+
creditos_cobrados: int
|
|
181
|
+
resultados: List[NfeXmlResultado]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class SigefParcela(TypedDict, total=False):
|
|
185
|
+
parcel_id: str
|
|
186
|
+
codigo_parcela: str
|
|
187
|
+
denominacao: str
|
|
188
|
+
area_ha: float
|
|
189
|
+
municipio: str
|
|
190
|
+
uf: str
|
|
191
|
+
codigo_ibge: str
|
|
192
|
+
detentor: str
|
|
193
|
+
matricula: str
|
|
194
|
+
cns: str
|
|
195
|
+
status: str
|
|
196
|
+
data_certificacao: str
|
|
197
|
+
natureza: str
|
|
198
|
+
codigo_sncr: str
|
|
199
|
+
cartorio: str
|
|
200
|
+
geometria_wkt: str
|
|
201
|
+
vertices: List[dict]
|
|
202
|
+
confrontantes: List[dict]
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class SigefResult(TypedDict, total=False):
|
|
206
|
+
total_parcelas: int
|
|
207
|
+
area_total_ha: float
|
|
208
|
+
parcelas: List[SigefParcela]
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class SigefResponse(TypedDict, total=False):
|
|
212
|
+
request_id: str
|
|
213
|
+
document: str
|
|
214
|
+
document_type: str
|
|
215
|
+
cached: bool
|
|
216
|
+
source_status: SourceStatus
|
|
217
|
+
result: Optional[SigefResult]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class SigefParcelaResponse(TypedDict, total=False):
|
|
221
|
+
request_id: str
|
|
222
|
+
parcel_id: str
|
|
223
|
+
cached: bool
|
|
224
|
+
consultado_em: str
|
|
225
|
+
completo: bool
|
|
226
|
+
avisos: List[str]
|
|
227
|
+
source_status: SourceStatus
|
|
228
|
+
result: Optional[SigefParcela]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class CnirResponse(TypedDict, total=False):
|
|
232
|
+
request_id: str
|
|
233
|
+
document: str
|
|
234
|
+
document_type: str
|
|
235
|
+
source_status: SourceStatus
|
|
236
|
+
pessoa_fisica: Optional[dict]
|
|
237
|
+
pessoa_juridica: Optional[dict]
|
|
238
|
+
imoveis_cafir: List[dict]
|
|
239
|
+
total_imoveis: int
|
|
240
|
+
total_imoveis_ativos: int
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class PerfilProdutorResponse(TypedDict, total=False):
|
|
244
|
+
request_id: str
|
|
245
|
+
document: str
|
|
246
|
+
receita_federal: Optional[CNPJResponse]
|
|
247
|
+
inscricoes_estaduais: Optional[ConsultaIETodosResponse]
|
|
248
|
+
cnd_estaduais: Optional[CNDTodosResponse]
|
|
249
|
+
certidoes_federais: List[CertidaoFederalResponse]
|
|
250
|
+
cnir: Optional[CnirResponse]
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fiscalapi-br
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SDK da FiscalAPI (Brasil): Inscrição Estadual, CNPJ, certidões, NF-e, SIGEF, CNIR e perfil do produtor rural.
|
|
5
|
+
Project-URL: Homepage, https://fiscalapi.com.br
|
|
6
|
+
Project-URL: Documentation, https://docs.fiscalapi.com.br
|
|
7
|
+
Project-URL: Source, https://github.com/fiscaIapi/fiscalapi-python
|
|
8
|
+
Project-URL: Issues, https://github.com/fiscaIapi/fiscalapi-python/issues
|
|
9
|
+
Author-email: FiscalAPI <contato@fiscalapi.com.br>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: brasil,certidao,cnd,cnir,cnpj,fiscalapi,inscricao estadual,nfe,produtor rural,sefaz,sigef
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Natural Language :: Portuguese (Brazilian)
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# FiscalAPI — SDK Python
|
|
26
|
+
|
|
27
|
+
Cliente Python da [FiscalAPI](https://fiscalapi.com.br): Inscrição Estadual nos 27 estados, CNPJ, certidões (CND estadual, CND Federal, CNDT, CRF/FGTS), XML de NF-e por chave, SIGEF, CNIR e perfil do produtor rural.
|
|
28
|
+
|
|
29
|
+
- Zero dependências (só a biblioteca padrão).
|
|
30
|
+
- Python 3.9 ou superior.
|
|
31
|
+
- Novas tentativas automáticas em `429` e em falhas temporárias.
|
|
32
|
+
- Erros tipados para chave inválida, falta de saldo e limite de requisições.
|
|
33
|
+
|
|
34
|
+
## Instalação
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install fiscalapi-br
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Antes de começar
|
|
41
|
+
|
|
42
|
+
1. Crie sua conta em [app.fiscalapi.com.br](https://app.fiscalapi.com.br) e copie a chave de API no painel.
|
|
43
|
+
2. **O uso pela API exige um plano ativo ou créditos.** As consultas gratuitas do mês valem só no painel; pela API, uma conta sem plano recebe `402` (`InsufficientBalanceError`).
|
|
44
|
+
3. Planos e consumo de créditos: [docs.fiscalapi.com.br](https://docs.fiscalapi.com.br/docs/referencia/plans-and-limits).
|
|
45
|
+
|
|
46
|
+
## Uso rápido
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from fiscalapi_br import FiscalAPI
|
|
50
|
+
|
|
51
|
+
api = FiscalAPI("sua-chave") # ou defina FISCALAPI_API_KEY
|
|
52
|
+
|
|
53
|
+
resp = api.consultar_ie(uf="MT", cpf="12345678909")
|
|
54
|
+
for ie in resp["results"]:
|
|
55
|
+
print(ie["inscricao_estadual"], ie["situacao_ie"], ie["municipio"])
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Métodos
|
|
59
|
+
|
|
60
|
+
| Método | Endpoint | O que retorna |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `consultar_ie(uf, cpf=/cnpj=/ie=, secundarias=False)` | `GET /api/consultar` | Inscrições estaduais em uma UF |
|
|
63
|
+
| `consultar_ie_todos(cpf=/cnpj=/ie=, secundarias=False)` | `GET /api/consultar-ie-todos` | Inscrições em todos os estados |
|
|
64
|
+
| `consultar_cnpj(cnpj, force_fresh=False)` | `GET /api/consultar-cnpj` | Dados cadastrais da Receita Federal |
|
|
65
|
+
| `consultar_cnd(uf, cpf=/cnpj=)` | `GET /api/consultar-cnd` | CND estadual (ICMS) com PDF |
|
|
66
|
+
| `consultar_cnd_todos(cpf=/cnpj=)` | `GET /api/consultar-cnd-todos` | CND estadual em todos os estados disponíveis |
|
|
67
|
+
| `consultar_cnd_federal(cpf=/cnpj=, emitir_nova=False)` | `GET /api/consultar-cnd-federal` | CND Federal (Receita Federal + PGFN) |
|
|
68
|
+
| `consultar_cndt(cpf=/cnpj=)` | `GET /api/consultar-cndt` | Certidão de débitos trabalhistas |
|
|
69
|
+
| `consultar_crf_fgts(cpf=/cnpj=)` | `GET /api/consultar-crf-fgts` | Regularidade do FGTS |
|
|
70
|
+
| `consultar_certidoes_todas(cpf=/cnpj=, skip_estaduais=False)` | `GET /api/certidoes-todas` | Todas as certidões em uma chamada |
|
|
71
|
+
| `consultar_nfe_xml(chave, certificado=, senha=, manifestar=True)` | `POST /api/consultar-nfe-xml` | XML completo da NF-e |
|
|
72
|
+
| `consultar_sigef(cpf=/cnpj=)` | `GET /api/consultar-sigef` | Parcelas certificadas no SIGEF/INCRA |
|
|
73
|
+
| `consultar_sigef_parcela(uuid, force_fresh=False)` | `GET /api/consultar-sigef-parcela` | Uma parcela com polígono, vértices e confrontantes |
|
|
74
|
+
| `consultar_cnir(cpf=/cnpj=)` | `GET /api/consultar-cnir` | Imóveis rurais no CNIR/CAFIR |
|
|
75
|
+
| `perfil_produtor(cpf=/cnpj=)` | `GET /api/perfil-produtor` | Perfil consolidado do produtor |
|
|
76
|
+
|
|
77
|
+
Informe **um** documento por chamada (`cpf`, `cnpj` ou, onde couber, `ie`). Pontuação é aceita. Todas as respostas são `dict` com o JSON da API; os tipos em `fiscalapi_br.types` ajudam no autocompletar.
|
|
78
|
+
|
|
79
|
+
## Exemplos
|
|
80
|
+
|
|
81
|
+
### Inscrição Estadual em todos os estados
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
resp = api.consultar_ie_todos(cnpj="12.345.678/0001-95")
|
|
85
|
+
if not resp.get("complete", True):
|
|
86
|
+
print("Algum estado não respondeu; tente de novo antes de concluir que não há IE.")
|
|
87
|
+
for uf in resp["results"]:
|
|
88
|
+
for ie in uf["results"]:
|
|
89
|
+
print(uf["uf"], ie["inscricao_estadual"], ie["situacao_ie"])
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### CNPJ
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
empresa = api.consultar_cnpj("12345678000195")
|
|
96
|
+
print(empresa["result"]["razao_social"], empresa["result"]["situacao_cadastral"])
|
|
97
|
+
|
|
98
|
+
# Dado atual, sem cache (consome mais créditos):
|
|
99
|
+
api.consultar_cnpj("12345678000195", force_fresh=True)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Certidões com PDF
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import base64
|
|
106
|
+
|
|
107
|
+
cnd = api.consultar_cnd("MT", cpf="12345678909")
|
|
108
|
+
resultado = cnd["result"]
|
|
109
|
+
print(resultado["status"], resultado["validade_iso"])
|
|
110
|
+
if resultado.get("pdf_base64"):
|
|
111
|
+
with open("cnd-mt.pdf", "wb") as f:
|
|
112
|
+
f.write(base64.b64decode(resultado["pdf_base64"]))
|
|
113
|
+
|
|
114
|
+
federal = api.consultar_cnd_federal(cnpj="12345678000195")
|
|
115
|
+
trabalhista = api.consultar_cndt(cnpj="12345678000195")
|
|
116
|
+
fgts = api.consultar_crf_fgts(cnpj="12345678000195")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
O campo `status` vem como `negativa`, `positiva`, `positiva_com_efeitos_de_negativa`, `nao_contribuinte`, `sem_certidao` ou `erro`.
|
|
120
|
+
|
|
121
|
+
### XML de NF-e por chave
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
resp = api.consultar_nfe_xml(
|
|
125
|
+
"35260112345678000195550010000012341000012345",
|
|
126
|
+
certificado="certificado.pfx", # caminho, bytes do arquivo ou base64
|
|
127
|
+
senha="senha-do-certificado",
|
|
128
|
+
)
|
|
129
|
+
nota = resp["resultados"][0]
|
|
130
|
+
if nota["status"] == "ok":
|
|
131
|
+
with open(f"{nota['chave']}.xml", "w", encoding="utf-8") as f:
|
|
132
|
+
f.write(nota["xml"])
|
|
133
|
+
else:
|
|
134
|
+
print(nota["status"], nota["mensagem"])
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
O certificado A1 precisa ser de um participante da nota (emitente, destinatário, transportador ou autorizado) e **não é armazenado** pela API. Só há cobrança quando o XML é devolvido.
|
|
138
|
+
|
|
139
|
+
### Imóvel rural
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
sigef = api.consultar_sigef(cpf="12345678909")
|
|
143
|
+
for parcela in (sigef["result"] or {}).get("parcelas", []):
|
|
144
|
+
print(parcela["denominacao"], parcela["area_ha"], parcela["municipio"], parcela["uf"])
|
|
145
|
+
|
|
146
|
+
parcela = api.consultar_sigef_parcela("0f1e2d3c-4b5a-6978-8a9b-0c1d2e3f4a5b")
|
|
147
|
+
print(parcela["result"]["geometria_wkt"])
|
|
148
|
+
|
|
149
|
+
cnir = api.consultar_cnir(cpf="12345678909")
|
|
150
|
+
print(cnir["total_imoveis"], "imóveis")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Perfil do produtor
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
perfil = api.perfil_produtor(cpf="12345678909")
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Status da fonte
|
|
160
|
+
|
|
161
|
+
Uma resposta `200` pode trazer `source_status.status` diferente de `success` (por exemplo, `timeout` quando o órgão não respondeu a tempo). Confira esse campo antes de concluir que o documento não tem registro.
|
|
162
|
+
|
|
163
|
+
## Erros
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from fiscalapi_br import (
|
|
167
|
+
FiscalAPIError, AuthenticationError, InsufficientBalanceError,
|
|
168
|
+
RateLimitError, BadRequestError, ServerError, APIConnectionError,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
api.consultar_ie(uf="SP", cnpj="12345678000195")
|
|
173
|
+
except AuthenticationError:
|
|
174
|
+
... # chave ausente, inválida ou revogada (401/403)
|
|
175
|
+
except InsufficientBalanceError as e:
|
|
176
|
+
print(e.consultas_restantes) # sem plano ou saldo esgotado (402)
|
|
177
|
+
except RateLimitError as e:
|
|
178
|
+
print(e.retry_after) # limite por minuto do plano (429)
|
|
179
|
+
except BadRequestError as e:
|
|
180
|
+
print(e.code, e.message) # parâmetro ausente ou inválido (400/422)
|
|
181
|
+
except FiscalAPIError as e:
|
|
182
|
+
print(e.status_code, e.request_id) # demais erros; informe o request_id ao suporte
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Novas tentativas e timeout
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
api = FiscalAPI("sua-chave", timeout=200, max_retries=3)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
- `429`: o SDK espera o tempo do cabeçalho `Retry-After` e tenta de novo.
|
|
192
|
+
- `500`, `502`, `503`, `504` e falhas de rede: novas tentativas com espera exponencial, **só em consultas GET**. A consulta de NF-e (POST) não é repetida automaticamente.
|
|
193
|
+
- Algumas consultas (NF-e, SIGEF) podem levar até 3 minutos; o timeout padrão é 200 s.
|
|
194
|
+
|
|
195
|
+
## Links
|
|
196
|
+
|
|
197
|
+
- Documentação: [docs.fiscalapi.com.br](https://docs.fiscalapi.com.br)
|
|
198
|
+
- Painel: [app.fiscalapi.com.br](https://app.fiscalapi.com.br)
|
|
199
|
+
- Suporte: contato@fiscalapi.com.br
|
|
200
|
+
|
|
201
|
+
## Licença
|
|
202
|
+
|
|
203
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
fiscalapi_br/__init__.py,sha256=eb6jgTWRPuSPmDzuxikMpbOGOhi7REMDuiSxjP_qF2Q,698
|
|
2
|
+
fiscalapi_br/_client.py,sha256=T1rUn4kSs7ohrlghUSeoya8bQjpueoEzF8MBL4YB_3U,16364
|
|
3
|
+
fiscalapi_br/_errors.py,sha256=j5P99VOLWr3POhEfs-9I8759PrV2X6WWoMTggXW8fx8,3266
|
|
4
|
+
fiscalapi_br/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
5
|
+
fiscalapi_br/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
fiscalapi_br/types.py,sha256=4E3ZuzqlfVRu4WxSXxChIRw_ieCnoDQZgbINdguLvLw,5636
|
|
7
|
+
fiscalapi_br-0.1.0.dist-info/METADATA,sha256=dWa4hhlvlRm10JAiuUFv630IDQgvb9NtRTe6QuJg0Iw,8298
|
|
8
|
+
fiscalapi_br-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
9
|
+
fiscalapi_br-0.1.0.dist-info/licenses/LICENSE,sha256=JYVGpyx18Er2Yj8Io6pcym-gw0pHA0I0wgNl8HiQFmQ,1066
|
|
10
|
+
fiscalapi_br-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FiscalAPI
|
|
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.
|