Skip to content

Commit 1267cc1

Browse files
authored
Merge pull request #1 from tempont/fix/endpoints-response-fields
Fix/endpoints response fields
2 parents 4710caa + 7759c08 commit 1267cc1

68 files changed

Lines changed: 2559 additions & 1152 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,19 @@ jobs:
3333
run: uv run basedpyright
3434
- name: Pytest
3535
run: uv run python -m pytest
36+
37+
codegen-drift:
38+
name: Codegen Drift Check
39+
runs-on: ubuntu-latest
40+
steps:
41+
- uses: actions/checkout@v4
42+
- name: Install uv
43+
uses: astral-sh/setup-uv@v3
44+
with:
45+
enable-cache: true
46+
- name: Set up Python 3.12
47+
run: uv python install 3.12
48+
- name: Install dependencies
49+
run: uv sync --all-groups --python 3.12
50+
- name: Codegen drift check
51+
run: make codegen-check

‎.github/workflows/docs.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
- name: Set up Python
2727
run: uv python install 3.12
2828
- name: Install dependencies
29-
run: uv sync --extra docs
29+
run: uv sync --group docs
3030
- name: Build docs
3131
run: uv run mkdocs build --strict
3232
- name: Upload artifact

‎.pre-commit-config.yaml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,9 @@ repos:
2525
pass_filenames: false
2626
always_run: true
2727
require_serial: true
28+
- id: codegen-drift
29+
name: codegen-drift
30+
entry: make codegen-check
31+
language: system
32+
pass_filenames: false
33+
files: ^specs/|^scripts/generate_|^src/bling_erp_api/(models|contracts)/generated/|^docs/resources/

‎AGENTS.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,17 @@ make check
109109
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.
110110
9. Examples should prefer normal model constructors over `model_construct()` so type checking and validation catch field-name drift.
111111

112+
### Model field overrides
113+
114+
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`.
115+
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`).
116+
3. Overrides are the sanctioned way to encode that divergence — never hand-edit files under `src/bling_erp_api/models/generated/`.
117+
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.
118+
5. Guard: generation fails (non-zero exit) listing dead `ClassName.field_name` keys when an override matches no generated schema or field.
119+
6. After spec refreshes, update the override tables first; stale keys make generation fail and tests fail until updated.
120+
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.
121+
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.
122+
112123
### Docstring Guidelines
113124

114125
Every resource method MUST include a Google-style docstring with the following structure:

‎CHANGELOG.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.3.0] - 2026-09-29
11+
12+
### Breaking Changes
13+
14+
- 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`.
15+
- 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`.
16+
17+
### Added
18+
19+
- 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
20+
- 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
21+
22+
### Changed
23+
24+
- Model and contract generators now format their own outputs with ruff
25+
- Auth example reports missing credentials clearly and verifies the OAuth `state` parameter
26+
- 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`)
27+
28+
### Fixed
29+
30+
- `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)
31+
- `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)
32+
- `BlingDate` fields no longer emit pydantic `PydanticSerializationUnexpectedValue` warnings on JSON serialization; wire output (ISO 8601 strings) is unchanged
33+
1034
## [0.2.0] - 2026-06-26
1135

1236
### Breaking Changes

‎Makefile‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
.PHONY: check build codegen codegen-check deploy sync
22

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

57
sync:
68
$(UV) sync --all-groups
@@ -15,8 +17,21 @@ codegen:
1517
$(UV) run --group codegen python scripts/generate_models.py
1618
$(UV) run python scripts/generate_openapi_contracts.py
1719

18-
codegen-check: codegen
19-
git diff --exit-code -- src/bling_erp_api/models/generated src/bling_erp_api/contracts/generated docs/resources
20+
codegen-check:
21+
@if $(UNTRACKED_GENERATED) | grep -q .; then \
22+
echo "ERROR: untracked files detected in generated directories:" >&2; \
23+
$(UNTRACKED_GENERATED) >&2; \
24+
echo "Run 'make codegen' and commit the generated artifacts." >&2; \
25+
exit 1; \
26+
fi
27+
$(MAKE) codegen
28+
git diff --exit-code -- $(GENERATED_DIRS)
29+
@if $(UNTRACKED_GENERATED) | grep -q .; then \
30+
echo "ERROR: codegen produced files that are not tracked by git:" >&2; \
31+
$(UNTRACKED_GENERATED) >&2; \
32+
echo "Run 'make codegen' and commit the generated artifacts." >&2; \
33+
exit 1; \
34+
fi
2035

2136
build:
2237
$(UV) build

‎examples/.auth/authenticate.py‎

Lines changed: 0 additions & 37 deletions
This file was deleted.

‎examples/auth/authenticate.py‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""Exemplo de código para autenticação com o Bling de forma interativa.
2+
3+
Usa o pacote bling_jwt_auth e valida o ``state`` OAuth com os helpers de
4+
``bling_erp_api.auth`` (``parse_oauth_callback`` + ``verify_oauth_state``).
5+
6+
Documentação: https://github.com/tempont/bling-jwt-auth-python
7+
Variáveis (obrigatórias):
8+
- BLING_CLIENT_ID
9+
- BLING_CLIENT_SECRET
10+
- BLING_REDIRECT_URI
11+
12+
Podem vir do ambiente ou de um arquivo `.env` no diretório corrente.
13+
`pydantic-settings` resolve o `.env` a partir do CWD, não do diretório
14+
deste script.
15+
""" # noqa
16+
17+
import os
18+
import sys
19+
20+
from bling_jwt_auth import BlingAuthSettings, OAuthClient, TokenManager, create_token_store
21+
from pydantic import ValidationError
22+
23+
from bling_erp_api.auth import parse_oauth_callback, verify_oauth_state
24+
25+
_FIELD_TO_ENV = {
26+
"client_id": "BLING_CLIENT_ID",
27+
"client_secret": "BLING_CLIENT_SECRET",
28+
"redirect_uri": "BLING_REDIRECT_URI",
29+
}
30+
31+
# 1. Carrega os settings de variáveis de ambiente.
32+
try:
33+
settings = BlingAuthSettings.load()
34+
except ValidationError as exc:
35+
missing = sorted(
36+
{
37+
_FIELD_TO_ENV.get(str(err["loc"][0]), str(err["loc"][0]))
38+
for err in exc.errors()
39+
if err["type"] == "missing"
40+
}
41+
)
42+
details = "; ".join(f"{'.'.join(map(str, err['loc']))}: {err['msg']}" for err in exc.errors())
43+
print(
44+
"\n⚠️ Falha ao carregar as credenciais OAuth do Bling.\n\n"
45+
f"Variáveis obrigatórias ausentes: {', '.join(missing)}\n"
46+
f"Detalhes: {details}\n\n"
47+
"As credenciais são lidas das variáveis de ambiente `BLING_*` ou de um\n"
48+
"arquivo `.env` no diretório corrente (de onde o comando foi executado).\n\n"
49+
"Como resolver:\n"
50+
" 1. Exporte as variáveis `BLING_*` no ambiente antes de executar\n"
51+
" este script; ou\n"
52+
" 2. Copie `.env.example` para `.env` na raiz do projeto e preencha\n"
53+
" as variáveis com os dados da aplicação OAuth registrada no Bling.\n\n"
54+
"Nota: o `.env` é resolvido a partir do diretório corrente (CWD),\n"
55+
"não do diretório deste script. Se as credenciais vêm de um\n"
56+
"gerenciador de segredos, garanta que as variáveis sejam injetadas\n"
57+
"no processo que executa este comando.\n",
58+
file=sys.stderr,
59+
)
60+
sys.exit(1)
61+
62+
# 2. Cria o armazenamento de tokens. (default: SQLite)
63+
store = create_token_store(settings)
64+
65+
# 3. Cria o cliente Oauth e o TokenManager e inicia a autenticação via CLI/Browser.
66+
with OAuthClient(settings) as oauth:
67+
manager = TokenManager(oauth, store, settings)
68+
69+
# 4. Build authorization URL for the browser with a CSRF state value.
70+
state = os.urandom(16).hex()
71+
auth_url = oauth.build_authorization_url(state=state)
72+
print(f"Open in browser: {auth_url}")
73+
# User opens URL, approves access, Bling redirects to
74+
# BLING_REDIRECT_URI?code=...&state=...
75+
76+
# 5. Parse the callback (full URL or query string), verify the state
77+
# against this session and exchange the code for tokens
78+
# (saves to store automatically).
79+
raw_callback = input(
80+
"Paste callback URL or query string (code=...&state=... even from error page): "
81+
).strip()
82+
code, received_state = parse_oauth_callback(raw_callback)
83+
verify_oauth_state(received_state, state)
84+
manager.save_from_code(code)
85+
86+
# 6. Now ready — every subsequent call auto-refreshes
87+
token = manager.get_access_token()
88+
print("Access token acquired successfully.")

‎pyproject.toml‎

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,6 @@ build-backend = "hatchling.build"
4242
[tool.hatch.version]
4343
path = "src/bling_erp_api/_version.py"
4444

45-
[project.optional-dependencies]
46-
docs = [
47-
"mkdocs>=1.6.1",
48-
"mkdocs-material>=9.6.23",
49-
"mkdocstrings[python]>=0.26.0",
50-
]
51-
codegen = [
52-
"datamodel-code-generator>=0.36.0",
53-
]
54-
5545
[dependency-groups]
5646
dev = [
5747
"basedpyright>=1.39.6",
@@ -140,6 +130,7 @@ convention = "google"
140130
################# Test #################
141131
[tool.pytest.ini_options]
142132
testpaths = ["tests"]
133+
filterwarnings = ["error"]
143134

144135
################# basedpyright #################
145136
[tool.basedpyright]

0 commit comments

Comments
 (0)