Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7615672
feat: Improve error handling for missing credentials and update JsonV…
tempont Sep 28, 2026
10eb6b0
fix: add sdk auth example
tempont Sep 28, 2026
548a3dd
fix: make product image internal media fields optional
tempont Sep 28, 2026
cbabe67
chore: sync generated artifacts with generators and self-format outputs
tempont Sep 28, 2026
afebae2
ci: add codegen drift gate to CI and pre-commit
tempont Sep 28, 2026
e8e1b68
docs: sync sales_orders.py docstrings with generated response metadata
tempont Sep 29, 2026
2c0d6ab
fix: make sales order item/installment ids optional (Bug 7)
tempont Sep 29, 2026
6669850
chore: fail model generation when field overrides match no schema
tempont Sep 29, 2026
186ae65
fix: move auth example out of the gitignored examples/.auth path
tempont Sep 29, 2026
0d03c4c
docs: document branch changes in CHANGELOG Unreleased
tempont Sep 29, 2026
d721d0f
chore: extend model generation override guard to field level
tempont Sep 29, 2026
f668d58
chore: preserve Field metadata and dedupe generator field insertions
tempont Sep 29, 2026
4dd696a
fix: add explicit JSON serializer to BlingDate
tempont Sep 29, 2026
c13c11b
chore: drop duplicated optional extras in favor of dependency groups
tempont Sep 29, 2026
5671d5c
chore: retire duplicate models.generated.resources reexport tree
tempont Sep 29, 2026
135b7e6
docs: document model field overrides and write-path tradeoff
tempont Sep 29, 2026
ff7f653
fix: normalize raw parent alias when cloning inherited field declarat…
tempont Sep 29, 2026
8205074
docs: changelog for extras removal and wording fix
tempont Sep 29, 2026
e0713e4
refactor: share alias rewrite helper and guard insertion edge cases
tempont Sep 29, 2026
5a62e9e
refactor: module-level serializer for BlingDate
tempont Sep 29, 2026
7759c08
chore: bump v0.3.0
tempont Sep 29, 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
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,19 @@ jobs:
run: uv run basedpyright
- name: Pytest
run: uv run python -m pytest

codegen-drift:
name: Codegen Drift Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
- name: Set up Python 3.12
run: uv python install 3.12
- name: Install dependencies
run: uv sync --all-groups --python 3.12
- name: Codegen drift check
run: make codegen-check
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies
run: uv sync --extra docs
run: uv sync --group docs
- name: Build docs
run: uv run mkdocs build --strict
- name: Upload artifact
Expand Down
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@ repos:
pass_filenames: false
always_run: true
require_serial: true
- id: codegen-drift
name: codegen-drift
entry: make codegen-check
language: system
pass_filenames: false
files: ^specs/|^scripts/generate_|^src/bling_erp_api/(models|contracts)/generated/|^docs/resources/
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,17 @@ make check
8. Raw `JsonObject` bodies are acceptable only for endpoints not yet modeled or deliberately flexible payloads; when using raw dicts, the caller is responsible for Bling wire-format keys.
9. Examples should prefer normal model constructors over `model_construct()` so type checking and validation catch field-name drift.

### Model field overrides

1. `scripts/generate_models.py` owns two override tables keyed `(ClassName, field_name)`: `FIELD_ANNOTATION_OVERRIDES` (dict) rewrites the generated annotation to `X | None`, and `FIELD_NONE_DEFAULT_OVERRIDES` (set) forces default `None`.
2. Rationale: the vendored OpenAPI spec (`specs/bling-openapi-reference.json`) declares fields required that real Bling payloads omit (precedents: product internal images carrying only `link`; sales-order itens/parcelas without `id`).
3. Overrides are the sanctioned way to encode that divergence — never hand-edit files under `src/bling_erp_api/models/generated/`.
4. Shared-DTO tradeoff (accepted): the same generated DTOs back POST/PUT request payloads, so relaxing a response field also relaxes client-side write validation; errors for those fields surface as Bling API 400s instead of local pydantic validation errors.
5. Guard: generation fails (non-zero exit) listing dead `ClassName.field_name` keys when an override matches no generated schema or field.
6. After spec refreshes, update the override tables first; stale keys make generation fail and tests fail until updated.
7. Discipline: with live keys, regenerated output must stay byte-identical (`git diff --exit-code`); `make codegen-check` (CI + pre-commit) enforces sync of generated artifacts.
8. Insertion behavior: overrides for parent-inherited fields re-insert the declaration, cloning the parent `Field(...)` call with `default=None` normalized. A raw `alias="<BlingName>"` keyword (the datamodel-codegen form) is rewritten into the repo-standard `validation_alias=AliasChoices("<snake_field>", "<BlingName>")` + `serialization_alias="<BlingName>"` pair; parents already carrying the normalized keywords keep the existing clone behavior, and alias-less parents fall back to a bare `= None` redeclaration.

### Docstring Guidelines

Every resource method MUST include a Google-style docstring with the following structure:
Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.0] - 2026-09-29

### Breaking Changes

- Removed `bling_erp_api.models.generated.resources.*`. Import generated resource models from `bling_erp_api.models.generated.<resource>` instead (for example, `bling_erp_api.models.generated.sales_orders`); stable public model aliases remain in `bling_erp_api.models.aliases`.
- Removed the `docs` and `codegen` PyPI extras. `pip install bling-erp-api[docs]` and `pip install bling-erp-api[codegen]` no longer install those tools. In a source checkout, use `uv sync --group docs`, `uv sync --group codegen`, or `uv sync --all-groups`.

### Added

- Codegen drift gate: CI job, pre-commit hook and `make codegen-check` fail when generated artifacts are out of sync with the generators, including untracked generated files
- Model generation now fails when a field override in `scripts/generate_models.py` matches no generated schema or field, preventing silent loss of runtime fixes after spec refreshes

### Changed

- Model and contract generators now format their own outputs with ruff
- Auth example reports missing credentials clearly and verifies the OAuth `state` parameter
- Auth example moved from `examples/.auth/authenticate.py` to `examples/auth/authenticate.py` (the old path is gitignored, which hid the tracked file from `git status`)

### Fixed

- `ProdutosImagemInternaDTO`: `link_miniatura`, `validade`, `ordem`, `anexo` and `anexo_vinculo` are now optional, matching real `GET /produtos/{idProduto}` payloads whose internal images carry only `link` (`link` remains required)
- `VendasItemDTO.id` and `VendasParcelaDTO.id` are now optional, matching sales-order responses whose itens/parcelas omit `id` (`sales_orders.py` docstrings synced with the generated response metadata)
- `BlingDate` fields no longer emit pydantic `PydanticSerializationUnexpectedValue` warnings on JSON serialization; wire output (ISO 8601 strings) is unchanged

## [0.2.0] - 2026-06-26

### Breaking Changes
Expand Down
19 changes: 17 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
.PHONY: check build codegen codegen-check deploy sync

UV := uv
GENERATED_DIRS := src/bling_erp_api/models/generated src/bling_erp_api/contracts/generated docs/resources
UNTRACKED_GENERATED := git ls-files --others --exclude-standard -- $(GENERATED_DIRS)

sync:
$(UV) sync --all-groups
Expand All @@ -15,8 +17,21 @@ codegen:
$(UV) run --group codegen python scripts/generate_models.py
$(UV) run python scripts/generate_openapi_contracts.py

codegen-check: codegen
git diff --exit-code -- src/bling_erp_api/models/generated src/bling_erp_api/contracts/generated docs/resources
codegen-check:
@if $(UNTRACKED_GENERATED) | grep -q .; then \
echo "ERROR: untracked files detected in generated directories:" >&2; \
$(UNTRACKED_GENERATED) >&2; \
echo "Run 'make codegen' and commit the generated artifacts." >&2; \
exit 1; \
fi
$(MAKE) codegen
git diff --exit-code -- $(GENERATED_DIRS)
@if $(UNTRACKED_GENERATED) | grep -q .; then \
echo "ERROR: codegen produced files that are not tracked by git:" >&2; \
$(UNTRACKED_GENERATED) >&2; \
echo "Run 'make codegen' and commit the generated artifacts." >&2; \
exit 1; \
fi

build:
$(UV) build
Expand Down
37 changes: 0 additions & 37 deletions examples/.auth/authenticate.py

This file was deleted.

88 changes: 88 additions & 0 deletions examples/auth/authenticate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Exemplo de código para autenticação com o Bling de forma interativa.

Usa o pacote bling_jwt_auth e valida o ``state`` OAuth com os helpers de
``bling_erp_api.auth`` (``parse_oauth_callback`` + ``verify_oauth_state``).

Documentação: https://github.com/tempont/bling-jwt-auth-python
Variáveis (obrigatórias):
- BLING_CLIENT_ID
- BLING_CLIENT_SECRET
- BLING_REDIRECT_URI

Podem vir do ambiente ou de um arquivo `.env` no diretório corrente.
`pydantic-settings` resolve o `.env` a partir do CWD, não do diretório
deste script.
""" # noqa

import os
import sys

from bling_jwt_auth import BlingAuthSettings, OAuthClient, TokenManager, create_token_store
from pydantic import ValidationError

from bling_erp_api.auth import parse_oauth_callback, verify_oauth_state

_FIELD_TO_ENV = {
"client_id": "BLING_CLIENT_ID",
"client_secret": "BLING_CLIENT_SECRET",
"redirect_uri": "BLING_REDIRECT_URI",
}

# 1. Carrega os settings de variáveis de ambiente.
try:
settings = BlingAuthSettings.load()
except ValidationError as exc:
missing = sorted(
{
_FIELD_TO_ENV.get(str(err["loc"][0]), str(err["loc"][0]))
for err in exc.errors()
if err["type"] == "missing"
}
)
details = "; ".join(f"{'.'.join(map(str, err['loc']))}: {err['msg']}" for err in exc.errors())
print(
"\n⚠️ Falha ao carregar as credenciais OAuth do Bling.\n\n"
f"Variáveis obrigatórias ausentes: {', '.join(missing)}\n"
f"Detalhes: {details}\n\n"
"As credenciais são lidas das variáveis de ambiente `BLING_*` ou de um\n"
"arquivo `.env` no diretório corrente (de onde o comando foi executado).\n\n"
"Como resolver:\n"
" 1. Exporte as variáveis `BLING_*` no ambiente antes de executar\n"
" este script; ou\n"
" 2. Copie `.env.example` para `.env` na raiz do projeto e preencha\n"
" as variáveis com os dados da aplicação OAuth registrada no Bling.\n\n"
"Nota: o `.env` é resolvido a partir do diretório corrente (CWD),\n"
"não do diretório deste script. Se as credenciais vêm de um\n"
"gerenciador de segredos, garanta que as variáveis sejam injetadas\n"
"no processo que executa este comando.\n",
file=sys.stderr,
)
sys.exit(1)

# 2. Cria o armazenamento de tokens. (default: SQLite)
store = create_token_store(settings)

# 3. Cria o cliente Oauth e o TokenManager e inicia a autenticação via CLI/Browser.
with OAuthClient(settings) as oauth:
manager = TokenManager(oauth, store, settings)

# 4. Build authorization URL for the browser with a CSRF state value.
state = os.urandom(16).hex()
auth_url = oauth.build_authorization_url(state=state)
print(f"Open in browser: {auth_url}")
# User opens URL, approves access, Bling redirects to
# BLING_REDIRECT_URI?code=...&state=...

# 5. Parse the callback (full URL or query string), verify the state
# against this session and exchange the code for tokens
# (saves to store automatically).
raw_callback = input(
"Paste callback URL or query string (code=...&state=... even from error page): "
).strip()
code, received_state = parse_oauth_callback(raw_callback)
verify_oauth_state(received_state, state)
manager.save_from_code(code)

# 6. Now ready — every subsequent call auto-refreshes
token = manager.get_access_token()
print("Access token acquired successfully.")
11 changes: 1 addition & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,6 @@ build-backend = "hatchling.build"
[tool.hatch.version]
path = "src/bling_erp_api/_version.py"

[project.optional-dependencies]
docs = [
"mkdocs>=1.6.1",
"mkdocs-material>=9.6.23",
"mkdocstrings[python]>=0.26.0",
]
codegen = [
"datamodel-code-generator>=0.36.0",
]

[dependency-groups]
dev = [
"basedpyright>=1.39.6",
Expand Down Expand Up @@ -140,6 +130,7 @@ convention = "google"
################# Test #################
[tool.pytest.ini_options]
testpaths = ["tests"]
filterwarnings = ["error"]

################# basedpyright #################
[tool.basedpyright]
Expand Down
Loading
Loading