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.
@@ -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
+ ]
@@ -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)
@@ -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)."""
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.