Biblioteca Python que tem por objetivo enviar NFe, NFCe e NFSe no Brasil
Este fork usa Python >=3.8.1,<3.9 e Poetry 1.8.5 como fonte de verdade (pyproject.toml + poetry.lock). Use Docker, comandos locais e/ou GitHub Actions (.github/workflows/publish-python.yaml) que publicam no AWS CodeArtifact (mesmo padrão que o pacote Python do monorepo xproto).
Versões (publicação Loggi): CalVer YYYYMMDD.X sem zero à esquerda após o ponto — ex.: 20260515.1, 20260515.2 (esse é o formato a usar em pyproject.toml, tags e workflows; evite 20260515.01). Builds snapshot no CI ganham sufixo local -dev+<git short sha>.
Notas de versão das libs: faixas altas compatíveis com 3.8 (por exemplo urllib3 2.x, zeep 4.x, lxml 5.x); cryptography fica >=42,<47 enquanto o signxml atual referencia curvas ECC legadas removidas no 47; reportlab fica <4 para evitar md5(..., usedforsecurity=False) incompatível com alguns backends hashlib quando OpenSSL está no processo.
Empilhamento lxml / xmlsec: rodas públicas podem causar lxml & xmlsec libxml2 library version mismatch. O Dockerfile usa bookworm, reinstala lxml e xmlsec do código-fonte contra as libs do sistema. Em hosts sem esse alinhamento, use o mesmo comando no venv ou confie na imagem:
poetry run python - <<'PY'
import subprocess, sys
from pkg_resources import get_distribution
lv = get_distribution("lxml").version
xv = get_distribution("xmlsec").version
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "--no-cache-dir", "--force-reinstall",
"--no-binary=lxml", "--no-binary=xmlsec", f"lxml=={lv}", f"xmlsec=={xv}"]
)
PYCache Poetry / hashes reportlab: se um wheel falhar no hash verificado, limpe ~/.cache/pypoetry/artifacts e rode poetry install de novo.
Python 3.8.1+ e Poetry 1.8.5 (pipx install poetry==1.8.5):
poetry install --with dev
poetry run pytestImagem Docker (Debian bookworm; falha no build se pytest falhar):
make build # ou: docker build -t pytrustnfe-build .
make testGerar wheel + sdist no host (copia para ./dist):
make dist # antes: make build ; gera .whl e .tar.gz alinhados ao version do pyproject.tomlPublicação: AWS CodeArtifact (repositório PyPI loggi, URL alinhada ao xproto: pyproject.toml + poetry.toml). O token vem de aws codeartifact get-authorization-token; o Poetry usa usuário aws e a senha igual ao token (POETRY_HTTP_BASIC_LOGGI_*).
Makefile (build em ./dist → publicar no CodeArtifact):
make aws-sso # aws sso login --profile platform-prod-sso (quando precisar)
make build # Update the image and the latest changes
make dist # wheel + sdist em ./dist (imagem Docker)
make publish-ca # publica só o que já está em ./dist
# ou um único comando (equivale a `poetry publish --build`):
make publish-ca-buildBump de versão (CalVer) + build + upload (equivale ao que o CI faz):
make publish-release VERSION=20260515.1Ou só publicar o que já está versionado / em ./dist: use make publish-ca / make publish-ca-build acima.
Variáveis opcionais Make/shell: AWS_PROFILE, CODEARTIFACT_* (ver ops/publish-codeartifact.sh).
CI: .github/workflows/publish-python.yaml (workflow_dispatch ou workflow_call) — mesmo fluxo que make publish-release (poetry version + poetry publish --build --repository loggi). Secrets AWS_ACCESS_KEY_ID_CODE_ARTIFACT, AWS_SECRET_ACCESS_KEY_CODE_ARTIFACT, AWS_REGION_CODE_ARTIFACT, AWS_DOMAIN_OWNER_CODE_ARTIFACT (iguais ao workflow Python do xproto).
Depois de publicar no CodeArtifact: atualize consumidores (loggi-web, nfe-sp, etc.) para a versão pinada e rode poetry lock onde couber.
Dependências (resumo — detalhes em pyproject.toml):
- PyXmlSec /
xmlsec lxmlsignxmlsuds-communitysuds-requests4reportlabJinja2
- Paulistana - São Paulo/SP
- Nota Carioca - Rio de Janeiro/RJ
- Imperial - Petrópolis/RH
- Susesu - 3 cidades atendidas
- Simpliss - 18 cidade atendidas
- GINFES - 79 cidades atendidas
- DSF - 7 cidades atendidas
Teste unitários
Implementar novos provedores de NFSe
- Betha - 81 cidades atendidas WIP
- WebISS - 51 cidades atendidas
- ISSIntel - 32 cidades atendidas
- ISSNET - 32 cidades atendidas
- Saatri - 31 cidades atendidas
Consulta Cadastro por CNPJ:
from pytrustnfe.nfe import consulta_cadastro
from pytrustnfe.certificado import Certificado
certificado = open("/path/certificado.pfx", "r").read()
certificado = Certificado(certificado, 'senha_pfx')
obj = {'cnpj': '12345678901234', 'estado': '42'}
resposta = consulta_cadastro(certificado, obj=obj, ambiente=1, estado='42')Consulta Distribuição NF-e sem Validação de Esquema:
from pytrustnfe.certificado import Certificado
from pytrustnfe.nfe import consulta_distribuicao_nfe, xml_consulta_distribuicao_nfe
certificado = open("/path/certificado.pfx", "r").read()
certificado = Certificado(certificado, 'senha_pfx')
# Gerando xml e enviado consulta por Ultimo NSU
response1 = consulta_distribuicao_nfe(
certificado,
ambiente=1,
estado='42',
modelo='55',
cnpj_cpf='12345678901234',
ultimo_nsu='123456789101213'
)
# Gerando xml e enviado consulta por Chave
response2 = consulta_distribuicao_nfe(
certificado,
ambiente=1,
estado='42',
modelo='55',
cnpj_cpf='12345678901234',
chave_nfe='012345678901234567890123456789012345678912'
)
# Gerando xml e enviado consulta por NSU
response3 = consulta_distribuicao_nfe(
certificado,
ambiente=1,
estado='42',
modelo='55',
cnpj_cpf='12345678901234',
nsu='123456789101213'
)Consulta Distribuição NF-e com Validação de Esquema:
from pytrustnfe.certificado import Certificado
from pytrustnfe.nfe import consulta_distribuicao_nfe, xml_consulta_distribuicao_nfe
from pytrustnfe.xml.validate import valida_nfe, SCHEMA_DFE
certificado = open("/path/certificado.pfx", "r").read()
certificado = Certificado(certificado, 'senha_pfx')
# Gerando XML para Consulta por Ultimo NSU
xml1 = xml_consulta_distribuicao_nfe(
certificado,
ambiente=1,
estado='42',
cnpj_cpf='12345678901234',
ultimo_nsu='123456789101213'
)
# Validando o XML com Esquema
if valida_nfe(xml1, SCHEMA_DFE):
Warning("Erro na validação do esquema")
# Gerando XML para Consulta por Chave
xml2 = xml_consulta_distribuicao_nfe(
certificado,
ambiente=1,
estado='42',
cnpj_cpf='12345678901234',
chave_nfe='012345678901234567890123456789012345678912'
)
# Validando o XML com Esquema
if valida_nfe(xml2, SCHEMA_DFE):
Warning("Erro na validação do esquema")
# Gerando XML para Consulta por NSU
xml3 = xml_consulta_distribuicao_nfe(
certificado,
ambiente=1,
estado='42',
cnpj_cpf='12345678901234',
nsu='123456789101213'
)
# Validando o XML com Esquema
if valida_nfe(xml3, SCHEMA_DFE):
Warning("Erro na validação do esquema")
# Enviando xml de consulta para sefaz
response = consulta_distribuicao_nfe(
certificado,
ambiente=1,
estado='42',
modelo='55',
xml=xml1
)Envio de RPS por lote
certificado = open('/path/certificado.pfx', 'r').read()
certificado = Certificado(certificado, '123456')
# Necessário criar um dicionário com os dados, validação dos dados deve
# ser feita pela aplicação que está utilizando a lib
rps = [
{
'assinatura': '123',
'serie': '1',
'numero': '1',
'data_emissao': '2016-08-29',
'codigo_atividade': '07498',
'valor_servico': '2.00',
'valor_deducao': '3.00',
'prestador': {
'inscricao_municipal': '123456'
},
'tomador': {
'tipo_cpfcnpj': '1',
'cpf_cnpj': '12345678923256',
'inscricao_municipal': '123456',
'razao_social': 'Trustcode',
'tipo_logradouro': '1',
'logradouro': 'Vinicius de Moraes, 42',
'numero': '42',
'bairro': 'Corrego',
'cidade': '4205407', # Código da cidade, de acordo com o IBGE
'uf': 'SC',
'cep': '88037240',
},
'codigo_atividade': '07498',
'aliquota_atividade': '5.00',
'descricao': 'Venda de servico'
}
]
nfse = {
'cpf_cnpj': '12345678901234',
'data_inicio': '2016-08-29',
'data_fim': '2016-08-29',
'total_servicos': '2.00',
'total_deducoes': '3.00',
'lista_rps': rps
}
retorno = envio_lote_rps(certificado, nfse=nfse)
# retorno é um dicionário { 'received_xml':'', 'sent_xml':'', 'object': object() }
print retorno['received_xml']
print retorno['sent_xml']
# retorno['object'] é um objeto python criado apartir do xml de resposta
print retorno['object'].Cabecalho.Sucesso
print retorno['object'].ChaveNFeRPS.ChaveNFe.NumeroNFe
print retorno['object'].ChaveNFeRPS.ChaveRPS.NumeroRPSCancelamento de NFSe:
from pytrustnfe.certificado import Certificado
from pytrustnfe.nfse.paulistana import cancelamento_nfe
certificado = open('/path/certificado.pfx', 'r').read()
certificado = Certificado(certificado, '123456')
cancelamento = {
'cnpj_remetente': '123',
'assinatura': 'assinatura',
'numero_nfse': '456',
'inscricao_municipal': '654',
'codigo_verificacao': '789',
}
retorno = cancelamento_nfe(certificado, cancelamento=cancelamento)
# retorno é um dicionário { 'received_xml':'', 'sent_xml':'', 'object': object() }
print retorno['received_xml']
print retorno['sent_xml']
# retorno['object'] é um objeto python criado apartir do xml de resposta
print retorno['object'].Cabecalho.Sucesso
if not retorno['object'].Cabecalho.Sucesso: # Cancelamento com erro
print retorno['object'].Erro.Codigo
print retorno['object'].Erro.Descricao