almztech-sdk 0.2.0b1__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ALMZTECH SOFTWARES LTDA
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.
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: almztech-sdk
3
+ Version: 0.2.0b1
4
+ Summary: SDK oficial Python para a API da Almztech (emissão, consulta, reconsulta e cancelamento de NFS-e, importação por planilha, Emitente, Webhook).
5
+ Author: ALMZTECH SOFTWARES LTDA
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://docs.almztech.com.br
8
+ Project-URL: Documentation, https://docs.almztech.com.br
9
+ Project-URL: Source, https://gitlab.com/almztech/sdks
10
+ Keywords: nfse,nfs-e,nota-fiscal,nota-fiscal-de-servico,almztech,sdk
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Natural Language :: Portuguese (Brazilian)
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Office/Business :: Financial :: Accounting
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: license-file
21
+
22
+ # almztech-sdk (Python)
23
+
24
+ SDK oficial da [Almztech](https://almztech.com.br) para Python. Ele cobre a API de NFS-e:
25
+ - emissão, consulta, reconsulta e cancelamento de notas, inclusive por planilha;
26
+ - emitentes e certificados;
27
+ - webhooks;
28
+ - municípios homologados.
29
+
30
+ > Versão beta: a API pública do SDK ainda pode mudar até a 1.0.
31
+
32
+ ## Instalação
33
+
34
+ ```bash
35
+ pip install --pre almztech-sdk
36
+ ```
37
+
38
+ - Python 3.9+.
39
+ - **Sem dependências de runtime**: usa `urllib` e `json` da biblioteca padrão (dá para injetar outro envio HTTP em `OpcoesDoClient.envio_http`).
40
+ - Os dados são `dict` no formato exato do JSON da API, em camelCase, com `TypedDict` em `almztech.modelos` para o editor. Chaves com `None` não são enviadas.
41
+
42
+ ## Uso
43
+
44
+ ```python
45
+ import os
46
+ from almztech import AlmztechClient, AlmztechErroProtocoloRejeitado, AlmztechErroValidacao, filtros, ler_webhook
47
+
48
+ # A api key está no Portal (portal.almztech.com.br), em Primeiros passos. Guarde-a na configuração do seu
49
+ # sistema, nunca no código.
50
+ client = AlmztechClient(os.environ["ALMZTECH_API_KEY"])
51
+
52
+ try:
53
+ protocolo = client.notas_fiscais.emitir({
54
+ "identificacaoEmitente": "12345678000190",
55
+ "enviarParaProducao": False, # homologação (HML-...): não consome a franquia
56
+ "notas": [{
57
+ "rps": {"numeroRPS": "1001", "serie": "1", "tipo": "1", "dataEmissao": "2026-10-31"},
58
+ "destinatario": {
59
+ "cpfCnpj": "11222333000181",
60
+ "razaoSocial": "Cliente Exemplo Ltda",
61
+ "endereco": {"cep": "50030170", "codigoIBGEMunicipio": 2611606},
62
+ },
63
+ "servicos": [{"codigo": "0105", "descricao": "Licenciamento de software", "quantidade": 1,
64
+ "valorUnitario": 1500, "aliquotaISS": 2, "retemISS": 2}],
65
+ }],
66
+ })
67
+
68
+ resultado = client.notas_fiscais.aguardar_conclusao(protocolo["protocolo"])
69
+ for nota in resultado.get("notas", []):
70
+ pdf = client.notas_fiscais.baixar_pdf(nota["codigoNota"]) # bytes
71
+ except AlmztechErroProtocoloRejeitado as erro:
72
+ print("Rejeitada:", erro.protocolo.get("mensagem"))
73
+ except AlmztechErroValidacao as erro:
74
+ print("Recusada:", erro.mensagem)
75
+
76
+ # No seu endpoint de webhook (a API não assina o payload: proteja a URL):
77
+ evento = ler_webhook(corpo_recebido) # evento.tipo: "Emissao" | "Cancelamento" | "Rejeicao"
78
+ ```
79
+
80
+ Consulta com filtros e paginação automática:
81
+
82
+ ```python
83
+ for nota in client.notas_fiscais.paginas(
84
+ filtros.PRODUCAO,
85
+ [filtros.periodo("2026-10-01", "2026-10-31"), filtros.com_status("EMITIDA")],
86
+ ):
87
+ print(nota["numeroNota"], nota["valorTotal"])
88
+ ```
89
+
90
+ Também estão disponíveis:
91
+ - **Notas:** `consultar_por_numero`, `reconsultar`, `reconsultar_por_chave_acesso`, `cancelar`, `baixar_xml`, `baixar_modelo_planilha` e `importar_planilha` (aceita caminho, bytes ou arquivo aberto).
92
+ - **Emitentes:** `client.emitentes`, inclusive `upload_certificado`.
93
+ - **Webhooks e municípios:** `client.webhooks` e `client.municipios`.
94
+
95
+ Exceções:
96
+ - `AlmztechErroValidacao` (422 e 400);
97
+ - `AlmztechErroAutenticacao` (401);
98
+ - `AlmztechErroAutorizacao` (403);
99
+ - `AlmztechErroLimiteDeRequisicoes` (429, com `retry_after`);
100
+ - `AlmztechErroServidor` (5xx);
101
+ - `AlmztechErroRede`;
102
+ - `AlmztechErroTempoDePollingEsgotado`;
103
+ - `AlmztechErroProtocoloRejeitado`;
104
+ - `AlmztechErroWebhook`.
105
+
106
+ Só as leituras são retentadas automaticamente. `emitir`, `cancelar` e `importar_planilha` nunca são.
107
+
108
+ ## Links
109
+
110
+ - [Documentação da API](https://docs.almztech.com.br)
111
+ - [Código-fonte](https://gitlab.com/almztech/sdks/-/tree/main/python). Os SDKs das outras linguagens (.NET, Node.js,
112
+ PHP, Java e Ruby) seguem o mesmo [contrato](https://gitlab.com/almztech/sdks/-/blob/main/DESIGN.md).
113
+
114
+ ## Licença
115
+
116
+ MIT, © 2026 ALMZTECH SOFTWARES LTDA. Veja [LICENSE](https://gitlab.com/almztech/sdks/-/blob/main/python/LICENSE).
@@ -0,0 +1,95 @@
1
+ # almztech-sdk (Python)
2
+
3
+ SDK oficial da [Almztech](https://almztech.com.br) para Python. Ele cobre a API de NFS-e:
4
+ - emissão, consulta, reconsulta e cancelamento de notas, inclusive por planilha;
5
+ - emitentes e certificados;
6
+ - webhooks;
7
+ - municípios homologados.
8
+
9
+ > Versão beta: a API pública do SDK ainda pode mudar até a 1.0.
10
+
11
+ ## Instalação
12
+
13
+ ```bash
14
+ pip install --pre almztech-sdk
15
+ ```
16
+
17
+ - Python 3.9+.
18
+ - **Sem dependências de runtime**: usa `urllib` e `json` da biblioteca padrão (dá para injetar outro envio HTTP em `OpcoesDoClient.envio_http`).
19
+ - Os dados são `dict` no formato exato do JSON da API, em camelCase, com `TypedDict` em `almztech.modelos` para o editor. Chaves com `None` não são enviadas.
20
+
21
+ ## Uso
22
+
23
+ ```python
24
+ import os
25
+ from almztech import AlmztechClient, AlmztechErroProtocoloRejeitado, AlmztechErroValidacao, filtros, ler_webhook
26
+
27
+ # A api key está no Portal (portal.almztech.com.br), em Primeiros passos. Guarde-a na configuração do seu
28
+ # sistema, nunca no código.
29
+ client = AlmztechClient(os.environ["ALMZTECH_API_KEY"])
30
+
31
+ try:
32
+ protocolo = client.notas_fiscais.emitir({
33
+ "identificacaoEmitente": "12345678000190",
34
+ "enviarParaProducao": False, # homologação (HML-...): não consome a franquia
35
+ "notas": [{
36
+ "rps": {"numeroRPS": "1001", "serie": "1", "tipo": "1", "dataEmissao": "2026-10-31"},
37
+ "destinatario": {
38
+ "cpfCnpj": "11222333000181",
39
+ "razaoSocial": "Cliente Exemplo Ltda",
40
+ "endereco": {"cep": "50030170", "codigoIBGEMunicipio": 2611606},
41
+ },
42
+ "servicos": [{"codigo": "0105", "descricao": "Licenciamento de software", "quantidade": 1,
43
+ "valorUnitario": 1500, "aliquotaISS": 2, "retemISS": 2}],
44
+ }],
45
+ })
46
+
47
+ resultado = client.notas_fiscais.aguardar_conclusao(protocolo["protocolo"])
48
+ for nota in resultado.get("notas", []):
49
+ pdf = client.notas_fiscais.baixar_pdf(nota["codigoNota"]) # bytes
50
+ except AlmztechErroProtocoloRejeitado as erro:
51
+ print("Rejeitada:", erro.protocolo.get("mensagem"))
52
+ except AlmztechErroValidacao as erro:
53
+ print("Recusada:", erro.mensagem)
54
+
55
+ # No seu endpoint de webhook (a API não assina o payload: proteja a URL):
56
+ evento = ler_webhook(corpo_recebido) # evento.tipo: "Emissao" | "Cancelamento" | "Rejeicao"
57
+ ```
58
+
59
+ Consulta com filtros e paginação automática:
60
+
61
+ ```python
62
+ for nota in client.notas_fiscais.paginas(
63
+ filtros.PRODUCAO,
64
+ [filtros.periodo("2026-10-01", "2026-10-31"), filtros.com_status("EMITIDA")],
65
+ ):
66
+ print(nota["numeroNota"], nota["valorTotal"])
67
+ ```
68
+
69
+ Também estão disponíveis:
70
+ - **Notas:** `consultar_por_numero`, `reconsultar`, `reconsultar_por_chave_acesso`, `cancelar`, `baixar_xml`, `baixar_modelo_planilha` e `importar_planilha` (aceita caminho, bytes ou arquivo aberto).
71
+ - **Emitentes:** `client.emitentes`, inclusive `upload_certificado`.
72
+ - **Webhooks e municípios:** `client.webhooks` e `client.municipios`.
73
+
74
+ Exceções:
75
+ - `AlmztechErroValidacao` (422 e 400);
76
+ - `AlmztechErroAutenticacao` (401);
77
+ - `AlmztechErroAutorizacao` (403);
78
+ - `AlmztechErroLimiteDeRequisicoes` (429, com `retry_after`);
79
+ - `AlmztechErroServidor` (5xx);
80
+ - `AlmztechErroRede`;
81
+ - `AlmztechErroTempoDePollingEsgotado`;
82
+ - `AlmztechErroProtocoloRejeitado`;
83
+ - `AlmztechErroWebhook`.
84
+
85
+ Só as leituras são retentadas automaticamente. `emitir`, `cancelar` e `importar_planilha` nunca são.
86
+
87
+ ## Links
88
+
89
+ - [Documentação da API](https://docs.almztech.com.br)
90
+ - [Código-fonte](https://gitlab.com/almztech/sdks/-/tree/main/python). Os SDKs das outras linguagens (.NET, Node.js,
91
+ PHP, Java e Ruby) seguem o mesmo [contrato](https://gitlab.com/almztech/sdks/-/blob/main/DESIGN.md).
92
+
93
+ ## Licença
94
+
95
+ MIT, © 2026 ALMZTECH SOFTWARES LTDA. Veja [LICENSE](https://gitlab.com/almztech/sdks/-/blob/main/python/LICENSE).
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "almztech-sdk"
7
+ version = "0.2.0b1"
8
+ description = "SDK oficial Python para a API da Almztech (emissão, consulta, reconsulta e cancelamento de NFS-e, importação por planilha, Emitente, Webhook)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ authors = [{ name = "ALMZTECH SOFTWARES LTDA" }]
12
+ license = "MIT"
13
+ license-files = ["LICENSE"]
14
+ keywords = ["nfse", "nfs-e", "nota-fiscal", "nota-fiscal-de-servico", "almztech", "sdk"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Natural Language :: Portuguese (Brazilian)",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Office/Business :: Financial :: Accounting",
22
+ ]
23
+ # Sem dependências de runtime: HTTP e JSON da biblioteca padrão (DESIGN.md seção 2).
24
+ dependencies = []
25
+
26
+ [project.urls]
27
+ Homepage = "https://docs.almztech.com.br"
28
+ Documentation = "https://docs.almztech.com.br"
29
+ Source = "https://gitlab.com/almztech/sdks"
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,72 @@
1
+ """SDK oficial Python da Almztech (DESIGN.md).
2
+
3
+ Exemplo::
4
+
5
+ from almztech import AlmztechClient
6
+
7
+ client = AlmztechClient(os.environ["ALMZTECH_API_KEY"])
8
+ protocolo = client.notas_fiscais.emitir(lote)
9
+ resultado = client.notas_fiscais.aguardar_conclusao(protocolo["protocolo"])
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Optional
15
+
16
+ from . import filtros
17
+ from .erros import (
18
+ AlmztechErro,
19
+ AlmztechErroApi,
20
+ AlmztechErroAutenticacao,
21
+ AlmztechErroAutorizacao,
22
+ AlmztechErroLimiteDeRequisicoes,
23
+ AlmztechErroProtocoloRejeitado,
24
+ AlmztechErroRede,
25
+ AlmztechErroServidor,
26
+ AlmztechErroTempoDePollingEsgotado,
27
+ AlmztechErroValidacao,
28
+ AlmztechErroWebhook,
29
+ )
30
+ from .log import mascarar_api_key, mascarar_corpo_json
31
+ from .recursos import Emitentes, Municipios, NotasFiscais, Webhooks
32
+ from .transporte import VERSAO_SDK, OpcoesDeRetry, OpcoesDoClient, Transporte
33
+ from .webhook import EventoWebhook, ler_webhook
34
+
35
+ __version__ = VERSAO_SDK
36
+
37
+
38
+ class AlmztechClient:
39
+ """Client raiz. Crie um por aplicação e reaproveite.
40
+
41
+ A api key está em "Primeiros passos" no Portal; nunca guarde login e senha no seu sistema.
42
+ """
43
+
44
+ def __init__(self, api_key: str, opcoes: Optional[OpcoesDoClient] = None):
45
+ transporte = Transporte(api_key, opcoes)
46
+ self.notas_fiscais = NotasFiscais(transporte)
47
+ self.emitentes = Emitentes(transporte)
48
+ self.webhooks = Webhooks(transporte)
49
+ self.municipios = Municipios(transporte)
50
+
51
+
52
+ __all__ = [
53
+ "AlmztechClient",
54
+ "OpcoesDoClient",
55
+ "OpcoesDeRetry",
56
+ "filtros",
57
+ "ler_webhook",
58
+ "EventoWebhook",
59
+ "mascarar_api_key",
60
+ "mascarar_corpo_json",
61
+ "AlmztechErro",
62
+ "AlmztechErroApi",
63
+ "AlmztechErroValidacao",
64
+ "AlmztechErroAutenticacao",
65
+ "AlmztechErroAutorizacao",
66
+ "AlmztechErroLimiteDeRequisicoes",
67
+ "AlmztechErroServidor",
68
+ "AlmztechErroRede",
69
+ "AlmztechErroTempoDePollingEsgotado",
70
+ "AlmztechErroProtocoloRejeitado",
71
+ "AlmztechErroWebhook",
72
+ ]
@@ -0,0 +1,72 @@
1
+ """Hierarquia de exceções do SDK (DESIGN.md seção 3)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+
8
+ class AlmztechErro(Exception):
9
+ """Base de todas as exceções do SDK."""
10
+
11
+
12
+ class AlmztechErroApi(AlmztechErro):
13
+ """Erro devolvido pela API (``{"mensagem": "..."}``). Não há código estruturado: ``mensagem`` é texto livre."""
14
+
15
+ def __init__(self, status_code: int, mensagem: str, corpo_bruto: Optional[str] = None):
16
+ super().__init__(f"A API da Almztech retornou {status_code}: {mensagem}")
17
+ self.status_code = status_code
18
+ self.mensagem = mensagem
19
+ self.corpo_bruto = corpo_bruto
20
+
21
+
22
+ class AlmztechErroValidacao(AlmztechErroApi):
23
+ """422 (regra de negócio, inclusive franquia do plano esgotada) ou 400 (corpo recusado, com os erros por campo)."""
24
+
25
+
26
+ class AlmztechErroAutenticacao(AlmztechErroApi):
27
+ """401 — token ausente, inválido ou revogado, ou licença inválida para produção. Só a mensagem diferencia."""
28
+
29
+
30
+ class AlmztechErroAutorizacao(AlmztechErroApi):
31
+ """403 — papel sem permissão para o endpoint."""
32
+
33
+
34
+ class AlmztechErroLimiteDeRequisicoes(AlmztechErroApi):
35
+ """429 — limite de requisições. Não é retentado sozinho: ``retry_after`` (segundos) diz quanto esperar."""
36
+
37
+ def __init__(self, mensagem: str, retry_after: Optional[float], corpo_bruto: Optional[str] = None):
38
+ super().__init__(429, mensagem, corpo_bruto)
39
+ self.retry_after = retry_after
40
+
41
+
42
+ class AlmztechErroServidor(AlmztechErroApi):
43
+ """5xx — erro interno; em produção a mensagem pode vir genérica."""
44
+
45
+
46
+ class AlmztechErroRede(AlmztechErro):
47
+ """Falha de rede, timeout ou DNS antes de qualquer resposta HTTP."""
48
+
49
+
50
+ class AlmztechErroTempoDePollingEsgotado(AlmztechErro):
51
+ """O polling estourou o tempo total; ``ultimo_protocolo`` permite continuar manualmente."""
52
+
53
+ def __init__(self, protocolo: str, ultimo_protocolo: Optional[dict[str, Any]]):
54
+ status = (ultimo_protocolo or {}).get("status", "desconhecido")
55
+ super().__init__(f"O protocolo {protocolo} não concluiu no tempo de espera (último status: {status}).")
56
+ self.protocolo = protocolo
57
+ self.ultimo_protocolo = ultimo_protocolo
58
+
59
+
60
+ class AlmztechErroProtocoloRejeitado(AlmztechErro):
61
+ """O protocolo terminou em ``REJEITADO`` ou ``ERRO_INTERNO``; o motivo está em ``protocolo["mensagem"]``."""
62
+
63
+ def __init__(self, protocolo: dict[str, Any]):
64
+ super().__init__(
65
+ f"O protocolo {protocolo.get('protocolo')} terminou em {protocolo.get('status')}: "
66
+ f"{protocolo.get('mensagem') or '(sem mensagem)'}"
67
+ )
68
+ self.protocolo = protocolo
69
+
70
+
71
+ class AlmztechErroWebhook(AlmztechErro):
72
+ """Payload de webhook não reconhecido."""
@@ -0,0 +1,81 @@
1
+ """Filtros de ``consultar_com_filtros`` (DESIGN.md seção 7).
2
+
3
+ O valor vai sempre como texto: datas ``yyyy-MM-dd`` e decimais com ponto. Campo desconhecido ou valor vazio é
4
+ ignorado pela API (não filtra), por isso prefira as constantes e os construtores daqui.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import datetime as dt
10
+ from decimal import Decimal
11
+ from typing import Optional, Union
12
+
13
+ PRODUCAO = "PRD"
14
+ HOMOLOGACAO = "HML"
15
+
16
+
17
+ class CamposFiltro:
18
+ PROTOCOLO = "protocolo"
19
+ CODIGO_NOTA = "codigoNota"
20
+ NOME_EMITENTE = "nomeEmitente"
21
+ CPF_CNPJ_EMITENTE = "cpfCnpjEmitente"
22
+ NUMERO_RPS = "numeroRps"
23
+ SERIE_RPS = "serieRps"
24
+ LOTE_RPS = "loteRps"
25
+ VALOR_TOTAL = "valorTotal"
26
+ #: Data ``yyyy-MM-dd``.
27
+ DATA_EMISSAO = "dataEmissao"
28
+ #: Vários status separados por ``|``; só com ``Igual``.
29
+ STATUS = "status"
30
+
31
+
32
+ class Operador:
33
+ IGUAL = "Igual"
34
+ DIFERENTE = "Diferente"
35
+ MAIOR_QUE = "MaiorQue"
36
+ MAIOR_OU_IGUAL = "MaiorOuIgual"
37
+ MENOR_QUE = "MenorQue"
38
+ MENOR_OU_IGUAL = "MenorOuIgual"
39
+ ENTRE = "Entre"
40
+ CONTEM = "Contem"
41
+ INICIA_COM = "IniciaCom"
42
+ TERMINA_COM = "TerminaCom"
43
+
44
+
45
+ Numero = Union[int, float, Decimal]
46
+ Data = Union[dt.date, str]
47
+
48
+
49
+ def _texto_numero(valor: Numero) -> str:
50
+ return format(Decimal(str(valor)), "f") if not isinstance(valor, int) else str(valor)
51
+
52
+
53
+ def _texto_data(valor: Data) -> str:
54
+ return valor if isinstance(valor, str) else valor.isoformat()
55
+
56
+
57
+ def filtro(campo: str, operador: str, valor: str, valor_final: Optional[str] = None) -> dict:
58
+ item = {"campo": campo, "operador": operador, "valor": valor}
59
+ if valor_final is not None:
60
+ item["valorFinal"] = valor_final
61
+ return item
62
+
63
+
64
+ def numero(campo: str, operador: str, valor: Numero) -> dict:
65
+ return filtro(campo, operador, _texto_numero(valor))
66
+
67
+
68
+ def entre_valores(campo: str, inicio: Numero, fim: Numero) -> dict:
69
+ return filtro(campo, Operador.ENTRE, _texto_numero(inicio), _texto_numero(fim))
70
+
71
+
72
+ def data(operador: str, valor: Data) -> dict:
73
+ return filtro(CamposFiltro.DATA_EMISSAO, operador, _texto_data(valor))
74
+
75
+
76
+ def periodo(inicio: Data, fim: Data) -> dict:
77
+ return filtro(CamposFiltro.DATA_EMISSAO, Operador.ENTRE, _texto_data(inicio), _texto_data(fim))
78
+
79
+
80
+ def com_status(*status: str) -> dict:
81
+ return filtro(CamposFiltro.STATUS, Operador.IGUAL, "|".join(status))
@@ -0,0 +1,41 @@
1
+ """Mascaramento central de log (DESIGN.md seção 6).
2
+
3
+ Cobre request e response: ``senhaCertificado`` e ``senhaPrefeitura`` vêm em claro inclusive nas leituras de
4
+ emitente. Todo log automático do SDK passa por aqui.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from typing import Any, Optional
11
+
12
+ _CAMPOS_SENSIVEIS = {"apikey", "senha", "senhacertificado", "senhaprefeitura", "base64", "arquivobase64", "login"}
13
+ _MASCARA = "***"
14
+
15
+
16
+ def mascarar_api_key(api_key: str) -> str:
17
+ """Mostra só os últimos 4 caracteres da api key."""
18
+ if not api_key:
19
+ return _MASCARA
20
+ return _MASCARA + (api_key[-4:] if len(api_key) > 4 else api_key)
21
+
22
+
23
+ def mascarar_corpo_json(corpo: Optional[str]) -> str:
24
+ """Cópia do JSON com todo campo sensível mascarado. Corpo não-JSON nunca é devolvido cru."""
25
+ if not corpo or not corpo.strip():
26
+ return ""
27
+ try:
28
+ return json.dumps(_mascarar(json.loads(corpo)), ensure_ascii=False)
29
+ except ValueError:
30
+ return "<corpo não-JSON omitido da sanitização de log>"
31
+
32
+
33
+ def _mascarar(valor: Any) -> Any:
34
+ if isinstance(valor, list):
35
+ return [_mascarar(item) for item in valor]
36
+ if isinstance(valor, dict):
37
+ return {
38
+ chave: _MASCARA if chave.lower() in _CAMPOS_SENSIVEIS else _mascarar(item)
39
+ for chave, item in valor.items()
40
+ }
41
+ return valor
@@ -0,0 +1,121 @@
1
+ """Tipos do wire format (``TypedDict``): os dados são ``dict`` em camelCase, exatamente como o JSON da API.
2
+
3
+ Servem para o editor e o mypy; em runtime são dicionários comuns. Chaves com valor ``None`` não são enviadas
4
+ (a API aplica o padrão dela, e null num campo numérico seria recusado com 400).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import List, Optional
10
+
11
+ try:
12
+ from typing import Literal, TypedDict
13
+ except ImportError: # pragma: no cover
14
+ from typing_extensions import Literal, TypedDict # type: ignore
15
+
16
+ StatusProtocolo = Literal[
17
+ "EM_FILA", "EM_PROCESSAMENTO", "PENDENTE_PREFEITURA", "PROCESSADO_COM_SUCESSO", "REJEITADO", "CANCELADO", "ERRO_INTERNO"
18
+ ]
19
+ StatusNota = Literal["EM_PROCESSAMENTO", "EMITIDA", "CANCELADA", "DENEGADA", "REJEITADA", "PENDENTE_PREFEITURA"]
20
+
21
+
22
+ class Endereco(TypedDict, total=False):
23
+ logradouro: str
24
+ numero: str
25
+ complemento: str
26
+ bairro: str
27
+ cep: str
28
+ codigoPais: int
29
+ codigoIBGEUF: int
30
+ #: Obrigatório para o tomador: sem ele a nota falha depois, no processamento assíncrono.
31
+ codigoIBGEMunicipio: int
32
+ siglaUF: str
33
+
34
+
35
+ class Destinatario(TypedDict, total=False):
36
+ cpfCnpj: str
37
+ razaoSocial: str
38
+ nomeFantasia: str
39
+ inscricaoEstadual: str
40
+ inscricaoMunicipal: str
41
+ telefone: str
42
+ endereco: Endereco
43
+
44
+
45
+ class Servico(TypedDict, total=False):
46
+ codigo: str
47
+ descricao: str
48
+ codigoTributacao: str
49
+ localPrestacao: str
50
+ responsavelPelaRetencao: int
51
+ #: ABRASF: 1 Exigível, 2 Não incidência, 3 Isenção, 4 Exportação, 5 Imunidade, 6/7 Suspensa.
52
+ exigibilidadeISS: int
53
+ tipoTributacao: int
54
+ municipioIncidencia: int
55
+ aliquotaISS: float
56
+ #: 1 = retido pelo tomador, 2 = não retido.
57
+ retemISS: int
58
+ quantidade: float
59
+ valorUnitario: float
60
+ valorDesconto: float
61
+ valorAcrescimo: float
62
+ codigoCnae: str
63
+ numeroProcesso: str
64
+ nbs: str
65
+ ibsCbs: dict
66
+ pisCofins: dict
67
+ totaisDosTributos: dict
68
+
69
+
70
+ class IdentificacaoDaNota(TypedDict, total=False):
71
+ #: Opcional com o controle automático de RPS do emitente: a API numera.
72
+ numeroRPS: str
73
+ serie: str
74
+ tipo: str
75
+ status: int
76
+ #: ``yyyy-MM-dd``.
77
+ dataEmissao: str
78
+ dataCompetencia: str
79
+
80
+
81
+ class NotaFiscalResumida(TypedDict, total=False):
82
+ rps: IdentificacaoDaNota
83
+ destinatario: Destinatario
84
+ servicos: List[Servico]
85
+ #: ``[{"condicao": ..., "pagamentos": [{"parcela": 1, "dataPagamento": "yyyy-MM-dd", "valor": ...}]}]``.
86
+ condicoesPagamentos: List[dict]
87
+ observacao: str
88
+ #: Nome com o typo do schema real (``Subtituicao``): numeroRPS, numeroNota, serie, tipo, chaveNota, codigoMotivo, motivo.
89
+ subtituicaoRps: dict
90
+
91
+
92
+ class RecepcaoLote(TypedDict, total=False):
93
+ identificacaoEmitente: str
94
+ notas: List[NotaFiscalResumida]
95
+ #: ``False`` = homologação (``HML-``), ``True`` = produção (``PRD-``). É por lote.
96
+ enviarParaProducao: bool
97
+ urlWebhook: str
98
+
99
+
100
+ class ProtocoloNotaResponse(TypedDict, total=False):
101
+ codigoNota: int
102
+ tipoNota: str
103
+ numero: str
104
+ rps: str
105
+ serie: str
106
+ dataEmissao: str
107
+ situacao: StatusNota
108
+ codigoVerificacao: str
109
+ chaveAcesso: str
110
+ xml: str
111
+ pdf: str
112
+ dataHoraCancelamento: str
113
+ protocolo: str
114
+
115
+
116
+ class ProtocoloResponse(TypedDict, total=False):
117
+ protocolo: str
118
+ status: StatusProtocolo
119
+ mensagem: Optional[str]
120
+ notas: List[ProtocoloNotaResponse]
121
+ dadosCancelamento: dict