Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
76e903b
docs(openspec): desenho do v0.1 com decisões D1–D8 fechadas
andrenfe Oct 8, 2026
2f67580
build: estrutura do pacote nfe-io (hatchling, uv, ruff, mypy, pytest)
andrenfe Oct 8, 2026
e83b9fb
feat(core): núcleo sans-IO, transporte stdlib, retry seguro e erros t…
andrenfe Oct 8, 2026
bb38b92
feat: clientes NfeClient/AsyncNfeClient e recursos do v0.1
andrenfe Oct 8, 2026
f954c39
test: suíte unitária sem rede e checagens de segurança
andrenfe Oct 8, 2026
b7361fb
test(live): integração opt-in e contratos provados em 2026-10-08
andrenfe Oct 8, 2026
dcb0f9c
ci: CI em matriz 3.10–3.14, segurança, CodeQL e release com Trusted P…
andrenfe Oct 8, 2026
8bd7abf
docs: README, CHANGELOG e CONTRIBUTING em pt-BR
andrenfe Oct 8, 2026
fd3c71f
fix: endurecimentos da revisão de segurança final
andrenfe Oct 8, 2026
3e962d8
docs(contrato): sonda da fase 2, decisões pendentes e revisão de segu…
andrenfe Oct 8, 2026
cf5f56e
docs(contrato): registra decisões F12 (manter delete) e F13 (httpbin …
andrenfe Oct 9, 2026
991f45b
chore: ignora arquivos locais do agente
andrenfe Oct 9, 2026
288c230
ci: corrige pin SHA de actions/upload-artifact v4.4.3
andrenfe Oct 9, 2026
0eac7e4
fix(core): limite determinístico de profundidade ao decodificar JSON
andrenfe Oct 9, 2026
6251090
test: ids curtos em parametrizações com entradas grandes; timeout nos…
andrenfe Oct 9, 2026
4a3e8e6
ci: remove workflow próprio do CodeQL (repo usa o default setup do Gi…
andrenfe Oct 9, 2026
e36b3ec
ci: atualiza actions para Node 24 (checkout v7, upload-artifact v7, d…
andrenfe Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: 2
updates:
# Actions fixadas por SHA: o Dependabot atualiza SHA e o comentário de versão juntos.
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
groups:
actions:
patterns: ["*"]

# Só dependências de desenvolvimento existem (o pacote não tem dependências de runtime).
- package-ecosystem: uv
directory: /
schedule:
interval: weekly
groups:
dev:
patterns: ["*"]
91 changes: 91 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: CI

on:
pull_request:
push:
branches: [master]

permissions: {}

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

env:
UV_VERSION: "0.10.8"
UV_LOCKED: "1"

jobs:
lint:
name: Lint e tipos
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Instalar uv
run: pipx install "uv==${UV_VERSION}"
- run: uv sync --python 3.12
- run: uv run ruff check .
- run: uv run ruff format --check .
- run: uv run mypy --strict src tests

test:
name: Testes (${{ matrix.os }}, Python ${{ matrix.python }})
runs-on: ${{ matrix.os }}
timeout-minutes: 15
permissions:
contents: read
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
python: ["3.10", "3.11", "3.12", "3.13", "3.14"]
include:
- os: macos-latest
python: "3.12"
- os: windows-latest
python: "3.12"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Instalar uv
run: pipx install "uv==${{ env.UV_VERSION }}"
- run: uv sync --python ${{ matrix.python }}
- name: Testes com cobertura
run: uv run coverage run -m pytest
- name: Mínimos de cobertura (núcleo, erros e webhooks >= 90%, total >= 85%)
run: |
uv run coverage json -o coverage.json
uv run python scripts/check_coverage.py coverage.json

build:
name: Build e smoke test do wheel
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Instalar uv
run: pipx install "uv==${UV_VERSION}"
- run: uv build
- run: uv run --group dev twine check --strict dist/*
- name: Wheel sem dependências de runtime
run: |
unzip -p dist/*.whl '*/METADATA' > METADATA
if grep -E '^Requires-Dist:' METADATA | grep -v 'extra =='; then
echo "runtime dependency found"; exit 1
fi
- name: Smoke test num venv limpo
run: |
uv venv --python 3.12 /tmp/smoke
uv pip install --python /tmp/smoke/bin/python dist/*.whl
test "$(uv pip list --python /tmp/smoke/bin/python --format freeze | wc -l)" -eq 1
/tmp/smoke/bin/python scripts/smoke_test.py
60 changes: 60 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Release

# Publicação no PyPI só por tag vX.Y.Z, com Trusted Publishing (OIDC, sem token guardado) e
# attestations PEP 740. Os artefatos são construídos uma única vez no job "build"; o job
# "publish" roda no environment protegido "pypi" (aprovação manual) e só baixa e publica.

on:
push:
tags: ["v*.*.*"]

permissions: {}

env:
UV_VERSION: "0.10.8"
UV_LOCKED: "1"

jobs:
build:
name: Build
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: pipx install "uv==${UV_VERSION}"
- name: Tag igual a __version__
run: |
version="$(sed -n 's/^__version__ = "\(.*\)"$/\1/p' src/nfeio/_version.py)"
test "v${version}" = "${GITHUB_REF_NAME}"
- run: uv sync --python 3.12
- run: uv run pytest
- run: uv build
- run: uv run twine check --strict dist/*
# SHA a conferir antes do primeiro push: scripts/verify_action_pins.py (job action-pins).
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: dist
path: dist/
if-no-files-found: error
retention-days: 7

publish:
name: Publicar no PyPI
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/nfe-io/
permissions:
id-token: write # Trusted Publishing (OIDC) e attestations
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
attestations: true
61 changes: 61 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Segurança

on:
pull_request:
push:
branches: [master]
schedule:
- cron: "17 6 * * 1"

permissions: {}

env:
UV_VERSION: "0.10.8"
UV_LOCKED: "1"

jobs:
bandit:
name: bandit
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: pipx install "uv==${UV_VERSION}"
- run: uv sync --python 3.12
- run: uv run bandit -c pyproject.toml -r src

pip-audit:
name: pip-audit (dependências de desenvolvimento travadas)
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: pipx install "uv==${UV_VERSION}"
- run: uv sync --python 3.12
- name: Auditar o lock
run: |
uv export --format requirements-txt --all-groups --no-emit-project -o requirements-audit.txt
uv run pip-audit --strict --disable-pip -r requirements-audit.txt

action-pins:
name: Pins de actions conferem com as tags
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Verificar SHA x tag de cada action
env:
GH_TOKEN: ${{ github.token }}
run: python3 scripts/verify_action_pins.py
25 changes: 25 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.env
.env.*
!.env.example
nfeio-docs
__pycache__/
*.py[cod]
.venv/
dist/
build/
*.egg-info/
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
htmlcov/
coverage.xml
scripts/probes/out/
coverage.json
tests/live/out/
.smoke-venv/
requirements-audit.txt

# arquivos locais do agente (não versionar)
CLAUDE.md
.claude/
54 changes: 54 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Changelog

Todas as mudanças relevantes deste projeto são registradas aqui.

O formato segue o [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/) e o projeto adota o
[Versionamento Semântico](https://semver.org/lang/pt-BR/). Enquanto a versão for 0.x, uma minor
pode trazer mudança incompatível; ela sempre será anunciada aqui.

## [Não lançado]

## [0.1.0] - não publicada

Primeira versão do SDK oficial da NFE.io para Python (`pip install nfe-io`, `import nfeio`).

### Adicionado

- Clientes `NfeClient` (síncrono) e `AsyncNfeClient` (assíncrono, via `asyncio.to_thread`), com a
mesma superfície e o mesmo comportamento, sem estado global.
- Duas chaves por família de API: `api_key` (NFS-e, empresas, certificados, webhooks) e
`data_api_key` (CNPJ, CPF, CEP), lidas também de `NFE_API_KEY` e `NFE_DATA_API_KEY`. Envio em
`Authorization: <chave>`.
- NFS-e (API v1): `create`, `list`, `retrieve`, `find_by_external_id`, `cancel`, `send_email`,
`download_pdf`, `download_xml`, `download_cancellation_xml`, `wait`, `create_and_wait` e
`cancel_and_wait`.
- Empresas (API v2, paginação por cursor): `list`, `retrieve`, `create`, `update`, `delete`.
- Certificados: `upload` (multipart, campo `file`) e `list`, com `expires_on` e `is_expired()`.
- Webhooks da conta: `list`, `retrieve`, `create`, `update`, `delete`, `ping`, `event_types`.
- `nfeio.webhooks.verify_signature` (HMAC-SHA1 em tempo constante, nunca levanta exceção) e
`construct_event` (verifica antes de interpretar o corpo).
- Consultas: `lookups.cnpj` (v3, CNPJ numérico e alfanumérico), `lookups.cnpj_state_taxes`,
`lookups.cpf` e `lookups.cep`, com validação local de dígitos verificadores.
- Modelos `NfeObject`: `Mapping` imutável sobre o JSON do fio, com propriedades tipadas para ids,
status, datas (`datetime` com fuso), valores (`Decimal`) e documentos (CNPJ/CPF normalizados).
- Paginação por índice (1-based) e por cursor com `auto_paging_iter()` síncrono e assíncrono.
- Hierarquia de erros por status com `request_id`, `trace_id`, `error_code` e
`outcome_unknown`; leitura dos cinco formatos de erro usados pela API.
- `RequestOptions` por chamada (`api_key`, `timeout`, `max_retries`, `idempotency_key`,
`extra_headers`) e `app_info` no User-Agent.
- Transporte só com a biblioteca padrão, substituível pelos protocolos `Transport` e
`AsyncTransport`.

### Segurança

- Retry ciente de método e da fase da falha: um `POST` (como a emissão) nunca é repetido depois
que pode ter chegado à API; `Idempotency-Key` não muda essa regra.
- TLS sempre verificado (mínimo 1.2), sem opção para desligar; timeouts finitos; respostas
limitadas a 10 MiB; sem compressão.
- JSON não confiável (respostas, erros e webhooks) limitado a 128 níveis de aninhamento, com o
mesmo resultado em qualquer versão do Python.
- Redirect de download seguido só em https e **sem** a chave de API.
- Ids, `externalId` e documentos validados antes de entrar no path (sem path traversal).
- Chaves, senha de certificado e segredo de webhook nunca aparecem em `repr`, logs ou
exceções; logs não registram documentos nem `externalId`.
- Zero dependências de runtime.
Loading
Loading