diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c448554..20a4900 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a9bd862..119326d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 45b2e59..212fd73 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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/ diff --git a/AGENTS.md b/AGENTS.md index 807c1f3..eed0fec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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=""` keyword (the datamodel-codegen form) is rewritten into the repo-standard `validation_alias=AliasChoices("", "")` + `serialization_alias=""` 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: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e00212..8c03654 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.` 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 diff --git a/Makefile b/Makefile index 1a46528..29a9bd7 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -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 diff --git a/examples/.auth/authenticate.py b/examples/.auth/authenticate.py deleted file mode 100644 index 3ee8f51..0000000 --- a/examples/.auth/authenticate.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Exemplo de código para autenticação com o Bling de forma interativa. - -Usa o pacote bling_jwt_auth. - -Documentação: https://github.com/tempont/bling-jwt-auth-python -Variáveis: - - BLING_CLIENT_ID - - BLING_CLIENT_SECRET - - BLING_REDIRECT_URI -""" # noqa - -import os - -from bling_jwt_auth import BlingAuthSettings, OAuthClient, TokenManager, create_token_store - -# 1. Carrega os settings de variáveis de ambiente. -settings = BlingAuthSettings.load() - -# 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 - auth_url = oauth.build_authorization_url(state=os.urandom(16).hex()) - print(f"Open in browser: {auth_url}") - # User opens URL, approves access, Bling redirects to BLING_REDIRECT_URI?code=... - - # 5. Exchange the callback code for tokens (saves to store automatically) - code = input("Paste authorization code: ").strip() - manager.save_from_code(code) - - # 6. Now ready — every subsequent call auto-refreshes - token = manager.get_access_token() - print("Access token acquired successfully.") diff --git a/examples/auth/authenticate.py b/examples/auth/authenticate.py new file mode 100644 index 0000000..daad93a --- /dev/null +++ b/examples/auth/authenticate.py @@ -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.") diff --git a/pyproject.toml b/pyproject.toml index c503824..e2c9d41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", @@ -140,6 +130,7 @@ convention = "google" ################# Test ################# [tool.pytest.ini_options] testpaths = ["tests"] +filterwarnings = ["error"] ################# basedpyright ################# [tool.basedpyright] diff --git a/scripts/generate_models.py b/scripts/generate_models.py index 4c2dd9b..08d0a7c 100644 --- a/scripts/generate_models.py +++ b/scripts/generate_models.py @@ -5,6 +5,7 @@ from __future__ import annotations import ast +import copy import json import re import shutil @@ -25,7 +26,6 @@ SPEC_PATH = Path("specs/bling-openapi-reference.json") GENERATED_DIR = Path("src/bling_erp_api/models/generated") -RESOURCE_REEXPORT_DIR = GENERATED_DIR / "resources" SCHEMAS_PACKAGE_DIR = GENERATED_DIR / "schemas" RAW_SCHEMAS_MODULE = GENERATED_DIR / "_schemas_raw.py" LEGACY_SCHEMAS_MODULE = GENERATED_DIR / "schemas.py" @@ -138,6 +138,18 @@ ("OrdensProducaoPostRequest", "id"): "int | None", # Bug 5: ContatosPostRequest.id should be optional on POST ("ContatosPostRequest", "id"): "int | None", + # Bug 6: GET /produtos/{idProduto} frequently omits every field but + # ``link`` in midia.imagens.internas[] entries + ("ProdutosImagemInternaDTO", "link_miniatura"): "str | None", + ("ProdutosImagemInternaDTO", "validade"): "str | None", + ("ProdutosImagemInternaDTO", "ordem"): "int | None", + ("ProdutosImagemInternaDTO", "anexo"): "ProdutosAnexoDTO | None", + ("ProdutosImagemInternaDTO", "anexo_vinculo"): "ProdutosAnexoVinculoDTO | None", + # Bug 7: VendasItemDTO.id / VendasParcelaDTO.id should be optional — the + # spec itself notes "Ignorado no método POST", and GET detail responses + # may omit these ids entirely + ("VendasItemDTO", "id"): "int | None", + ("VendasParcelaDTO", "id"): "int | None", } FIELD_NONE_DEFAULT_OVERRIDES = { ("SituacoesAcaoDTO", "descricao"), @@ -146,12 +158,39 @@ ("OrdensProducaoPostRequest", "situacao"), # Bug 5: ContatosPostRequest.id should be optional on POST ("ContatosPostRequest", "id"), + # Bug 6: GET /produtos/{idProduto} frequently omits every field but + # ``link`` in midia.imagens.internas[] entries + ("ProdutosImagemInternaDTO", "link_miniatura"), + ("ProdutosImagemInternaDTO", "validade"), + ("ProdutosImagemInternaDTO", "ordem"), + ("ProdutosImagemInternaDTO", "anexo"), + ("ProdutosImagemInternaDTO", "anexo_vinculo"), + # Bug 7: VendasItemDTO.id / VendasParcelaDTO.id should be optional — the + # spec itself notes "Ignorado no método POST", and GET detail responses + # may omit these ids entirely + ("VendasItemDTO", "id"), + ("VendasParcelaDTO", "id"), } +# Overrides matched while generating the current run's classes, as +# ``(class_name, field_name)``. Used by ``main`` to fail when an override +# matches no generated schema class or field (e.g. renamed/removed in the +# spec). +_APPLIED_FIELD_OVERRIDES: set[tuple[str, str]] = set() + # Type that replaces bare ``date`` in all generated annotations. DATE_TYPE_REWRITE = "BlingDate" DATE_IMPORT = "from bling_erp_api.models.fields import BlingDate" +# ``Field(...)`` keywords that affect the wire format. Inserted redeclarations +# of parent-inherited fields only clone the parent's ``Field(...)`` call when +# any of these are present; schema-documentation-only kwargs (``examples``, +# ``description``, …) do not justify changing today's emitted output. The raw +# ``alias`` form emitted by datamodel-codegen is normalized into the +# ``validation_alias``/``serialization_alias`` pair by +# ``_rewrite_raw_alias_keyword`` after the clone. +WIRE_FORMAT_FIELD_KEYWORDS = frozenset({"alias", "validation_alias", "serialization_alias"}) + # Map of model name → inner data type for wrapper generation. # Models listed here will have their body replaced with: # data: | None = None @@ -188,12 +227,14 @@ def main() -> None: # Inject response data wrapper models (new classes or body overwrites). # Must happen BEFORE _schema_class_modules so module assignment is correct. _inject_response_model_data_wrappers(class_order, class_nodes) + _APPLIED_FIELD_OVERRIDES.clear() class_names = set(class_order) all_contracts = _contracts_by_module(payload) class_modules = _schema_class_modules(class_order, all_contracts) _write_schema_package(class_order, class_nodes, class_modules) + _check_field_overrides_applied() _write_resource_reexports(all_contracts, class_names, class_modules) _write_generated_init(class_order) _write_operation_models(all_contracts, class_names, class_modules) @@ -201,9 +242,32 @@ def main() -> None: _run_ruff_fix() +def _check_field_overrides_applied() -> None: + """Fail the generation when a field override matches no generated class or field.""" + dead_keys = ( + set(FIELD_ANNOTATION_OVERRIDES) | set(FIELD_NONE_DEFAULT_OVERRIDES) + ) - _APPLIED_FIELD_OVERRIDES + if not dead_keys: + return + + dead_list = "".join( + f" - {class_name}.{field_name}\n" for class_name, field_name in sorted(dead_keys) + ) + message = ( + "Field overrides matched no generated schema class or field:\n" + f"{dead_list}" + "The spec likely renamed or removed these schemas; update " + "FIELD_ANNOTATION_OVERRIDES / FIELD_NONE_DEFAULT_OVERRIDES in " + "scripts/generate_models.py.\n" + "A failed run may leave `src/bling_erp_api/models/generated/_schemas_raw.py` " + "(and unformatted regenerated files) behind; they are replaced by the next " + "successful run." + ) + raise SystemExit(message) + + def _prepare_generated_dirs() -> None: GENERATED_DIR.mkdir(parents=True, exist_ok=True) - RESOURCE_REEXPORT_DIR.mkdir(parents=True, exist_ok=True) OPERATION_MODELS_MODULE.parent.mkdir(parents=True, exist_ok=True) LEGACY_SCHEMAS_MODULE.unlink(missing_ok=True) @@ -531,20 +595,47 @@ def _apply_field_overrides( applied_default_fields.add(stmt.target.id) # Handle overrides for fields inherited from parent classes — the field - # does not exist in this class's body, so we must insert a new declaration. + # does not exist in this class's body, so we re-insert a declaration. If + # the field is absent from every parent as well, the override key is dead: + # no phantom field is emitted and the key is not recorded, so ``main`` + # fails listing ``Class.field``. A key present in both tables inserts the + # field only once (``inserted_fields``), coalescing both fixes into the + # single redeclaration. + inserted_fields: set[str] = set() for (override_name, field_name), annotation in FIELD_ANNOTATION_OVERRIDES.items(): - if override_name == name and field_name not in applied_annotation_fields: - _insert_field_override_with_annotation(node, field_name, annotation) + if override_name != name or field_name in applied_annotation_fields: + continue + if not _field_exists_in_parents(node, field_name, class_nodes): + continue + _insert_field_override_with_annotation( + node, field_name, annotation, class_nodes=class_nodes + ) + inserted_fields.add(field_name) + _APPLIED_FIELD_OVERRIDES.add((name, field_name)) for override_name, field_name in FIELD_NONE_DEFAULT_OVERRIDES: - if override_name == name and field_name not in applied_default_fields: - _insert_field_none_default(node, field_name, class_nodes) + if ( + override_name != name + or field_name in applied_default_fields + or field_name in inserted_fields + ): + continue + if not _field_exists_in_parents(node, field_name, class_nodes): + continue + _insert_field_none_default(node, field_name, class_nodes) + _APPLIED_FIELD_OVERRIDES.add((name, field_name)) + + # Record overrides applied directly in the class body, so ``main`` can + # detect dead override keys (class never rendered, or field gone). + _APPLIED_FIELD_OVERRIDES.update((name, field) for field in applied_annotation_fields) + _APPLIED_FIELD_OVERRIDES.update((name, field) for field in applied_default_fields) def _insert_field_override_with_annotation( node: ast.ClassDef, field_name: str, annotation: str, + class_nodes: Mapping[str, ast.ClassDef] | None = None, ) -> None: """Insert a new field declaration into the class body. @@ -556,7 +647,9 @@ def _insert_field_override_with_annotation( new_field = ast.AnnAssign( target=ast.Name(id=field_name, ctx=ast.Store()), annotation=ast.parse(annotation, mode="eval").body, - value=ast.Constant(value=None), + value=_inherited_field_value( + _find_parent_field_decl(node, field_name, class_nodes), field_name + ), simple=1, ) node.body.append(new_field) @@ -569,47 +662,86 @@ def _insert_field_none_default( ) -> None: """Insert a field with ``= None`` default for a parent-inherited field. - Looks up the original type from the parent class hierarchy so the - generated type is correct (e.g. ``OrdensProducaoSituacaoDTO | None``). - Falls back to ``Any`` if the type cannot be resolved. + Looks up the original declaration from the parent class hierarchy so the + generated type is correct (e.g. ``OrdensProducaoSituacaoDTO | None``) and + the parent ``Field(...)`` metadata (aliases, examples) is preserved on the + redeclaration. Falls back to ``Any`` if the type cannot be resolved. """ _remove_pass_if_empty(node) - # Try to find the original annotation from parent classes - orig_annotation: ast.expr | None = None - if class_nodes is not None: - for base in node.bases: - base_name = _name_from_expr(base) - if base_name and base_name in class_nodes: - orig_annotation = _find_field_annotation(field_name, base_name, class_nodes) - if orig_annotation is not None: - break - - if orig_annotation is not None: + # Try to find the original declaration from parent classes + parent_decl = _find_parent_field_decl(node, field_name, class_nodes) + + annotation: ast.expr + if parent_decl is None: + annotation = ast.Name(id="Any", ctx=ast.Load()) + elif _annotation_is_optional(parent_decl.annotation): + # Already ``X | None``: reuse as-is instead of spelling ``X | None | None``. + annotation = parent_decl.annotation + else: # Build ``OriginalType | None`` annotation = ast.BinOp( - left=orig_annotation, + left=parent_decl.annotation, op=ast.BitOr(), right=ast.Constant(value=None), ) - else: - annotation = ast.Name(id="Any", ctx=ast.Load()) new_field = ast.AnnAssign( target=ast.Name(id=field_name, ctx=ast.Store()), annotation=annotation, - value=ast.Constant(value=None), + value=_inherited_field_value(parent_decl, field_name), simple=1, ) node.body.append(new_field) -def _find_field_annotation( +def _annotation_is_optional(annotation: ast.expr) -> bool: + """Return whether ``annotation`` already ends in a ``| None`` union member.""" + return ( + isinstance(annotation, ast.BinOp) + and isinstance(annotation.op, ast.BitOr) + and isinstance(annotation.right, ast.Constant) + and annotation.right.value is None + ) + + +def _field_exists_in_parents( + node: ast.ClassDef, + field_name: str, + class_nodes: Mapping[str, ast.ClassDef] | None, +) -> bool: + """Return whether ``field_name`` is declared anywhere in the class's parent chain.""" + return _find_parent_field_decl(node, field_name, class_nodes) is not None + + +def _find_parent_field_decl( + node: ast.ClassDef, + field_name: str, + class_nodes: Mapping[str, ast.ClassDef] | None, +) -> ast.AnnAssign | None: + """Find the full declaration of ``field_name`` in the class's parent chain.""" + if class_nodes is None: + return None + for base in node.bases: + base_name = _name_from_expr(base) + if base_name: + decl = _find_field_decl(field_name, base_name, class_nodes) + if decl is not None: + return decl + return None + + +def _find_field_decl( field_name: str, class_name: str, class_nodes: Mapping[str, ast.ClassDef], -) -> ast.expr | None: - """Walk the class hierarchy to find the annotation expression for a field.""" +) -> ast.AnnAssign | None: + """Walk the class hierarchy to find the full declaration node for a field. + + Depth-first through the base classes, returning the first matching + ``AnnAssign``: callers can read ``decl.annotation`` for the type or the + whole node for value/metadata preservation. + """ node = class_nodes.get(class_name) if node is None: return None @@ -619,11 +751,11 @@ def _find_field_annotation( and isinstance(stmt.target, ast.Name) and stmt.target.id == field_name ): - return stmt.annotation + return stmt for base in node.bases: base_name = _name_from_expr(base) if base_name: - result = _find_field_annotation(field_name, base_name, class_nodes) + result = _find_field_decl(field_name, base_name, class_nodes) if result is not None: return result return None @@ -641,23 +773,111 @@ def _rewrite_date_types(node: ast.ClassDef) -> None: stmt.id = "BlingDate" +def _inherited_field_value(parent_decl: ast.AnnAssign | None, field_name: str) -> ast.expr: + """Build the value for an inserted parent-inherited field redeclaration. + + Pydantic v2 replaces the parent field configuration on redeclaration, so + when the parent's ``Field(...)`` call carries wire-format metadata it is + deep-copied to keep that metadata (and every other keyword, e.g. + ``examples``) with its default normalized to ``None``. Raw datamodel-codegen + output spells the alias as ``alias=""``; the clone is normalized + into the repo-standard ``validation_alias=AliasChoices(...)`` / + ``serialization_alias`` pair so ``to_json_object()`` (``by_alias=True``) + serializes the correct Bling key. Parents already carrying the normalized + keywords (synthetic shapes) keep the existing clone behavior. Falls back + to a bare ``None`` constant when the parent declaration has no value or no + alias metadata — keeping the emitted output for alias-less parents + byte-identical. + """ + if parent_decl is None or parent_decl.value is None: + return ast.Constant(value=None) + value = parent_decl.value + if not (isinstance(value, ast.Call) and _name_from_expr(value.func) == "Field") or not any( + keyword.arg in WIRE_FORMAT_FIELD_KEYWORDS for keyword in value.keywords + ): + return ast.Constant(value=None) + cloned = _field_value_default_none(copy.deepcopy(value)) + if isinstance(cloned, ast.Call): + _rewrite_raw_alias_keyword(cloned, field_name) + return cloned + + +def _rewrite_raw_alias_keyword(call: ast.Call, field_name: str) -> bool: + """Rewrite a raw ``alias=""`` keyword into the repo-standard pair. + + Shared by ``_rewrite_field_aliases`` (the per-class rewrite pass) and the + inherited-field clone path, so both normalize identically. An alias equal + to the field name is left as-is. A call already carrying + ``validation_alias`` is treated as already normalized: a stray raw + ``alias`` keyword is dropped instead of rewritten, so a duplicate + ``validation_alias`` kwarg (a SyntaxError when unparsed) can never be + emitted; ``serialization_alias`` is likewise only appended when absent. + Returns whether the rewrite happened. + """ + alias_keyword = next( + ( + keyword + for keyword in call.keywords + if keyword.arg == "alias" and _string_value(keyword.value) + ), + None, + ) + if alias_keyword is None: + return False + + if any(keyword.arg == "validation_alias" for keyword in call.keywords): + # Already-normalized call: drop the stray raw alias instead of + # appending a second ``validation_alias``. + call.keywords.remove(alias_keyword) + return False + + alias = _string_value(alias_keyword.value) + if alias is None or alias == field_name: + return False + + alias_keyword.arg = "validation_alias" + alias_keyword.value = ast.Call( + func=ast.Name(id="AliasChoices", ctx=ast.Load()), + args=[ + ast.Constant(value=field_name), + ast.Constant(value=alias), + ], + keywords=[], + ) + if not any(keyword.arg == "serialization_alias" for keyword in call.keywords): + call.keywords.append( + ast.keyword(arg="serialization_alias", value=ast.Constant(value=alias)) + ) + return True + + def _set_field_default_none(stmt: ast.AnnAssign) -> None: + """Set the field's default value to ``None`` in place.""" + stmt.value = _field_value_default_none(stmt.value) + + +def _field_value_default_none(value: ast.expr | None) -> ast.expr: + """Return ``value`` with its ``Field(...)`` default normalized to ``None``. + + Strips a positional ``...`` default, sets or inserts ``default=None``, and + preserves every other keyword. Non-``Field`` values collapse to ``None``. + """ default = ast.Constant(value=None) - if isinstance(stmt.value, ast.Call) and _name_from_expr(stmt.value.func) == "Field": - stmt.value.args = [ + if isinstance(value, ast.Call) and _name_from_expr(value.func) == "Field": + value.args = [ arg - for arg in stmt.value.args + for arg in value.args if not (isinstance(arg, ast.Constant) and arg.value is Ellipsis) ] - for keyword in stmt.value.keywords: + for keyword in value.keywords: if keyword.arg == "default": keyword.value = default break else: - stmt.value.keywords.insert(0, ast.keyword(arg="default", value=default)) - return + value.keywords.insert(0, ast.keyword(arg="default", value=default)) + return value - stmt.value = default + return default def _rewrite_field_aliases(node: ast.ClassDef) -> None: @@ -668,34 +888,7 @@ def _rewrite_field_aliases(node: ast.ClassDef) -> None: if not isinstance(stmt.value, ast.Call) or _name_from_expr(stmt.value.func) != "Field": continue - field_name = stmt.target.id - alias_keyword = next( - ( - keyword - for keyword in stmt.value.keywords - if keyword.arg == "alias" and _string_value(keyword.value) - ), - None, - ) - if alias_keyword is None: - continue - - alias = _string_value(alias_keyword.value) - if alias is None or alias == field_name: - continue - - alias_keyword.arg = "validation_alias" - alias_keyword.value = ast.Call( - func=ast.Name(id="AliasChoices", ctx=ast.Load()), - args=[ - ast.Constant(value=field_name), - ast.Constant(value=alias), - ], - keywords=[], - ) - stmt.value.keywords.append( - ast.keyword(arg="serialization_alias", value=ast.Constant(value=alias)) - ) + _rewrite_raw_alias_keyword(stmt.value, stmt.target.id) def _model_docstring(name: str, class_nodes: Mapping[str, ast.ClassDef]) -> str: @@ -842,14 +1035,6 @@ def _write_resource_reexports( path = GENERATED_DIR / f"{module}.py" path.unlink() - for path in RESOURCE_REEXPORT_DIR.glob("*.py"): - path.unlink() - - (RESOURCE_REEXPORT_DIR / "__init__.py").write_text( - '"""Resource-scoped generated model reexports."""\n', - encoding="utf-8", - ) - for module, contracts in sorted(all_contracts.items()): names = sorted(_resource_model_names(module, contracts, class_names)) # Include response data wrapper models that belong to this module @@ -860,7 +1045,6 @@ def _write_resource_reexports( names.sort() content = _reexport_module_content(module, names, class_modules) (GENERATED_DIR / f"{module}.py").write_text(content, encoding="utf-8") - (RESOURCE_REEXPORT_DIR / f"{module}.py").write_text(content, encoding="utf-8") def _resource_model_names( diff --git a/scripts/generate_openapi_contracts.py b/scripts/generate_openapi_contracts.py index db2566a..3b35b62 100644 --- a/scripts/generate_openapi_contracts.py +++ b/scripts/generate_openapi_contracts.py @@ -3,6 +3,8 @@ from __future__ import annotations import json +import shutil +import subprocess from pathlib import Path from pprint import pformat from typing import TYPE_CHECKING, NotRequired, TypedDict, cast @@ -813,6 +815,19 @@ def main() -> None: all_contracts[class_name] = contracts _write_docstring_module(all_contracts) + _run_ruff_format() + + +def _run_ruff_format() -> None: + """Normalize generated contract modules with ruff format (mirrors generate_models.py).""" + executable = shutil.which("ruff") + if executable is None: + msg = "ruff was not found. Run `uv sync --all-groups` first." + raise RuntimeError(msg) + subprocess.run( # noqa: S603 + [executable, "format", str(CONTRACTS_DIR)], + check=True, + ) def _resource_contracts( @@ -1036,7 +1051,10 @@ def _write_resource_docs( for operation_name, contract in _operation_mapping(contracts).items(): lines.extend(_operation_docs(operation_name, contract)) - (DOCS_DIR / f"{resource_slug}.md").write_text("\n".join(lines) + "\n", encoding="utf-8") + # Match pre-commit normalization (trailing-whitespace + end-of-file-fixer): + # no trailing spaces on any line and exactly one newline at end of file. + content = "\n".join(line.rstrip() for line in lines).rstrip("\n") + "\n" + (DOCS_DIR / f"{resource_slug}.md").write_text(content, encoding="utf-8") def _operation_docs(operation_name: str, contract: Mapping[str, object]) -> list[str]: diff --git a/src/bling_erp_api/_version.py b/src/bling_erp_api/_version.py index 90931a1..00329cc 100644 --- a/src/bling_erp_api/_version.py +++ b/src/bling_erp_api/_version.py @@ -1,3 +1,3 @@ """Package version.""" -__version__ = "0.2.0" +__version__ = "0.3.0" diff --git a/src/bling_erp_api/contracts/generated/_operation_models.py b/src/bling_erp_api/contracts/generated/_operation_models.py index 9a0f8f9..d953827 100644 --- a/src/bling_erp_api/contracts/generated/_operation_models.py +++ b/src/bling_erp_api/contracts/generated/_operation_models.py @@ -930,6 +930,726 @@ "response_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasIdLogisticaRemessasGetResponse200", "response_item_model": None, }, + { + "resource_class": "LogisticasEtiquetasResource", + "sdk_method": "listar", + "method": "GET", + "path": "/logisticas", + "pattern": r"^/logisticas/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasGetResponse200", + "response_item_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasDadosBaseDTO", + }, + { + "resource_class": "LogisticasEtiquetasResource", + "sdk_method": "criar", + "method": "POST", + "path": "/logisticas", + "pattern": r"^/logisticas/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasPostResponse201", + "response_item_model": None, + }, + { + "resource_class": "LogisticasEtiquetasResource", + "sdk_method": "obter_etiquetas", + "method": "GET", + "path": "/logisticas/etiquetas", + "pattern": r"^/logisticas/etiquetas/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_etiquetas.LogisticasEtiquetasGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasEtiquetasResource", + "sdk_method": "criar_objeto", + "method": "POST", + "path": "/logisticas/objetos", + "pattern": r"^/logisticas/objetos/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_objetos.LogisticasObjetosPostResponse201", + "response_item_model": None, + }, + { + "resource_class": "LogisticasEtiquetasResource", + "sdk_method": "remover_objeto", + "method": "DELETE", + "path": "/logisticas/objetos/{idObjeto}", + "pattern": r"^/logisticas/objetos/[^/]+/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "LogisticasEtiquetasResource", + "sdk_method": "obter_objeto", + "method": "GET", + "path": "/logisticas/objetos/{idObjeto}", + "pattern": r"^/logisticas/objetos/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_objetos.LogisticasObjetosIdObjetoGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasEtiquetasResource", + "sdk_method": "alterar_objeto", + "method": "PUT", + "path": "/logisticas/objetos/{idObjeto}", + "pattern": r"^/logisticas/objetos/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_objetos.LogisticasObjetosIdObjetoPutResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasEtiquetasResource", + "sdk_method": "criar_remessa", + "method": "POST", + "path": "/logisticas/remessas", + "pattern": r"^/logisticas/remessas/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_remessas.LogisticasRemessasPostResponse201", + "response_item_model": None, + }, + { + "resource_class": "LogisticasEtiquetasResource", + "sdk_method": "remover_remessa", + "method": "DELETE", + "path": "/logisticas/remessas/{idRemessa}", + "pattern": r"^/logisticas/remessas/[^/]+/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "LogisticasEtiquetasResource", + "sdk_method": "obter_remessa", + "method": "GET", + "path": "/logisticas/remessas/{idRemessa}", + "pattern": r"^/logisticas/remessas/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_remessas.LogisticasRemessasIdRemessaGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasEtiquetasResource", + "sdk_method": "alterar_remessa", + "method": "PUT", + "path": "/logisticas/remessas/{idRemessa}", + "pattern": r"^/logisticas/remessas/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_remessas.LogisticasRemessasIdRemessaPutResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasEtiquetasResource", + "sdk_method": "listar_servicos", + "method": "GET", + "path": "/logisticas/servicos", + "pattern": r"^/logisticas/servicos/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_servicos.LogisticasServicosGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasEtiquetasResource", + "sdk_method": "criar_servico", + "method": "POST", + "path": "/logisticas/servicos", + "pattern": r"^/logisticas/servicos/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_servicos.LogisticasServicosPostResponse201", + "response_item_model": None, + }, + { + "resource_class": "LogisticasEtiquetasResource", + "sdk_method": "obter_servico", + "method": "GET", + "path": "/logisticas/servicos/{idLogisticaServico}", + "pattern": r"^/logisticas/servicos/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_servicos.LogisticasServicosIdLogisticaServicoGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasEtiquetasResource", + "sdk_method": "alterar_servico", + "method": "PUT", + "path": "/logisticas/servicos/{idLogisticaServico}", + "pattern": r"^/logisticas/servicos/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_servicos.LogisticasServicosIdLogisticaServicoPutResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasEtiquetasResource", + "sdk_method": "alterar_situacao_servico", + "method": "PATCH", + "path": "/logisticas/{idLogisticaServico}/situacoes", + "pattern": r"^/logisticas/[^/]+/situacoes/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "LogisticasEtiquetasResource", + "sdk_method": "remover", + "method": "DELETE", + "path": "/logisticas/{idLogistica}", + "pattern": r"^/logisticas/[^/]+/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "LogisticasEtiquetasResource", + "sdk_method": "obter", + "method": "GET", + "path": "/logisticas/{idLogistica}", + "pattern": r"^/logisticas/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasIdLogisticaGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasEtiquetasResource", + "sdk_method": "alterar", + "method": "PUT", + "path": "/logisticas/{idLogistica}", + "pattern": r"^/logisticas/[^/]+/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "LogisticasEtiquetasResource", + "sdk_method": "listar_remessas_por_logistica", + "method": "GET", + "path": "/logisticas/{idLogistica}/remessas", + "pattern": r"^/logisticas/[^/]+/remessas/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasIdLogisticaRemessasGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasObjetosResource", + "sdk_method": "listar", + "method": "GET", + "path": "/logisticas", + "pattern": r"^/logisticas/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasGetResponse200", + "response_item_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasDadosBaseDTO", + }, + { + "resource_class": "LogisticasObjetosResource", + "sdk_method": "criar", + "method": "POST", + "path": "/logisticas", + "pattern": r"^/logisticas/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasPostResponse201", + "response_item_model": None, + }, + { + "resource_class": "LogisticasObjetosResource", + "sdk_method": "obter_etiquetas", + "method": "GET", + "path": "/logisticas/etiquetas", + "pattern": r"^/logisticas/etiquetas/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_etiquetas.LogisticasEtiquetasGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasObjetosResource", + "sdk_method": "criar_objeto", + "method": "POST", + "path": "/logisticas/objetos", + "pattern": r"^/logisticas/objetos/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_objetos.LogisticasObjetosPostResponse201", + "response_item_model": None, + }, + { + "resource_class": "LogisticasObjetosResource", + "sdk_method": "remover_objeto", + "method": "DELETE", + "path": "/logisticas/objetos/{idObjeto}", + "pattern": r"^/logisticas/objetos/[^/]+/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "LogisticasObjetosResource", + "sdk_method": "obter_objeto", + "method": "GET", + "path": "/logisticas/objetos/{idObjeto}", + "pattern": r"^/logisticas/objetos/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_objetos.LogisticasObjetosIdObjetoGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasObjetosResource", + "sdk_method": "alterar_objeto", + "method": "PUT", + "path": "/logisticas/objetos/{idObjeto}", + "pattern": r"^/logisticas/objetos/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_objetos.LogisticasObjetosIdObjetoPutResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasObjetosResource", + "sdk_method": "criar_remessa", + "method": "POST", + "path": "/logisticas/remessas", + "pattern": r"^/logisticas/remessas/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_remessas.LogisticasRemessasPostResponse201", + "response_item_model": None, + }, + { + "resource_class": "LogisticasObjetosResource", + "sdk_method": "remover_remessa", + "method": "DELETE", + "path": "/logisticas/remessas/{idRemessa}", + "pattern": r"^/logisticas/remessas/[^/]+/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "LogisticasObjetosResource", + "sdk_method": "obter_remessa", + "method": "GET", + "path": "/logisticas/remessas/{idRemessa}", + "pattern": r"^/logisticas/remessas/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_remessas.LogisticasRemessasIdRemessaGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasObjetosResource", + "sdk_method": "alterar_remessa", + "method": "PUT", + "path": "/logisticas/remessas/{idRemessa}", + "pattern": r"^/logisticas/remessas/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_remessas.LogisticasRemessasIdRemessaPutResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasObjetosResource", + "sdk_method": "listar_servicos", + "method": "GET", + "path": "/logisticas/servicos", + "pattern": r"^/logisticas/servicos/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_servicos.LogisticasServicosGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasObjetosResource", + "sdk_method": "criar_servico", + "method": "POST", + "path": "/logisticas/servicos", + "pattern": r"^/logisticas/servicos/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_servicos.LogisticasServicosPostResponse201", + "response_item_model": None, + }, + { + "resource_class": "LogisticasObjetosResource", + "sdk_method": "obter_servico", + "method": "GET", + "path": "/logisticas/servicos/{idLogisticaServico}", + "pattern": r"^/logisticas/servicos/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_servicos.LogisticasServicosIdLogisticaServicoGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasObjetosResource", + "sdk_method": "alterar_servico", + "method": "PUT", + "path": "/logisticas/servicos/{idLogisticaServico}", + "pattern": r"^/logisticas/servicos/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_servicos.LogisticasServicosIdLogisticaServicoPutResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasObjetosResource", + "sdk_method": "alterar_situacao_servico", + "method": "PATCH", + "path": "/logisticas/{idLogisticaServico}/situacoes", + "pattern": r"^/logisticas/[^/]+/situacoes/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "LogisticasObjetosResource", + "sdk_method": "remover", + "method": "DELETE", + "path": "/logisticas/{idLogistica}", + "pattern": r"^/logisticas/[^/]+/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "LogisticasObjetosResource", + "sdk_method": "obter", + "method": "GET", + "path": "/logisticas/{idLogistica}", + "pattern": r"^/logisticas/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasIdLogisticaGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasObjetosResource", + "sdk_method": "alterar", + "method": "PUT", + "path": "/logisticas/{idLogistica}", + "pattern": r"^/logisticas/[^/]+/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "LogisticasObjetosResource", + "sdk_method": "listar_remessas_por_logistica", + "method": "GET", + "path": "/logisticas/{idLogistica}/remessas", + "pattern": r"^/logisticas/[^/]+/remessas/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasIdLogisticaRemessasGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasRemessasResource", + "sdk_method": "listar", + "method": "GET", + "path": "/logisticas", + "pattern": r"^/logisticas/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasGetResponse200", + "response_item_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasDadosBaseDTO", + }, + { + "resource_class": "LogisticasRemessasResource", + "sdk_method": "criar", + "method": "POST", + "path": "/logisticas", + "pattern": r"^/logisticas/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasPostResponse201", + "response_item_model": None, + }, + { + "resource_class": "LogisticasRemessasResource", + "sdk_method": "obter_etiquetas", + "method": "GET", + "path": "/logisticas/etiquetas", + "pattern": r"^/logisticas/etiquetas/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_etiquetas.LogisticasEtiquetasGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasRemessasResource", + "sdk_method": "criar_objeto", + "method": "POST", + "path": "/logisticas/objetos", + "pattern": r"^/logisticas/objetos/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_objetos.LogisticasObjetosPostResponse201", + "response_item_model": None, + }, + { + "resource_class": "LogisticasRemessasResource", + "sdk_method": "remover_objeto", + "method": "DELETE", + "path": "/logisticas/objetos/{idObjeto}", + "pattern": r"^/logisticas/objetos/[^/]+/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "LogisticasRemessasResource", + "sdk_method": "obter_objeto", + "method": "GET", + "path": "/logisticas/objetos/{idObjeto}", + "pattern": r"^/logisticas/objetos/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_objetos.LogisticasObjetosIdObjetoGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasRemessasResource", + "sdk_method": "alterar_objeto", + "method": "PUT", + "path": "/logisticas/objetos/{idObjeto}", + "pattern": r"^/logisticas/objetos/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_objetos.LogisticasObjetosIdObjetoPutResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasRemessasResource", + "sdk_method": "criar_remessa", + "method": "POST", + "path": "/logisticas/remessas", + "pattern": r"^/logisticas/remessas/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_remessas.LogisticasRemessasPostResponse201", + "response_item_model": None, + }, + { + "resource_class": "LogisticasRemessasResource", + "sdk_method": "remover_remessa", + "method": "DELETE", + "path": "/logisticas/remessas/{idRemessa}", + "pattern": r"^/logisticas/remessas/[^/]+/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "LogisticasRemessasResource", + "sdk_method": "obter_remessa", + "method": "GET", + "path": "/logisticas/remessas/{idRemessa}", + "pattern": r"^/logisticas/remessas/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_remessas.LogisticasRemessasIdRemessaGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasRemessasResource", + "sdk_method": "alterar_remessa", + "method": "PUT", + "path": "/logisticas/remessas/{idRemessa}", + "pattern": r"^/logisticas/remessas/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_remessas.LogisticasRemessasIdRemessaPutResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasRemessasResource", + "sdk_method": "listar_servicos", + "method": "GET", + "path": "/logisticas/servicos", + "pattern": r"^/logisticas/servicos/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_servicos.LogisticasServicosGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasRemessasResource", + "sdk_method": "criar_servico", + "method": "POST", + "path": "/logisticas/servicos", + "pattern": r"^/logisticas/servicos/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_servicos.LogisticasServicosPostResponse201", + "response_item_model": None, + }, + { + "resource_class": "LogisticasRemessasResource", + "sdk_method": "obter_servico", + "method": "GET", + "path": "/logisticas/servicos/{idLogisticaServico}", + "pattern": r"^/logisticas/servicos/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_servicos.LogisticasServicosIdLogisticaServicoGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasRemessasResource", + "sdk_method": "alterar_servico", + "method": "PUT", + "path": "/logisticas/servicos/{idLogisticaServico}", + "pattern": r"^/logisticas/servicos/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_servicos.LogisticasServicosIdLogisticaServicoPutResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasRemessasResource", + "sdk_method": "alterar_situacao_servico", + "method": "PATCH", + "path": "/logisticas/{idLogisticaServico}/situacoes", + "pattern": r"^/logisticas/[^/]+/situacoes/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "LogisticasRemessasResource", + "sdk_method": "remover", + "method": "DELETE", + "path": "/logisticas/{idLogistica}", + "pattern": r"^/logisticas/[^/]+/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "LogisticasRemessasResource", + "sdk_method": "obter", + "method": "GET", + "path": "/logisticas/{idLogistica}", + "pattern": r"^/logisticas/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasIdLogisticaGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasRemessasResource", + "sdk_method": "alterar", + "method": "PUT", + "path": "/logisticas/{idLogistica}", + "pattern": r"^/logisticas/[^/]+/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "LogisticasRemessasResource", + "sdk_method": "listar_remessas_por_logistica", + "method": "GET", + "path": "/logisticas/{idLogistica}/remessas", + "pattern": r"^/logisticas/[^/]+/remessas/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasIdLogisticaRemessasGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasServicosResource", + "sdk_method": "listar", + "method": "GET", + "path": "/logisticas", + "pattern": r"^/logisticas/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasGetResponse200", + "response_item_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasDadosBaseDTO", + }, + { + "resource_class": "LogisticasServicosResource", + "sdk_method": "criar", + "method": "POST", + "path": "/logisticas", + "pattern": r"^/logisticas/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasPostResponse201", + "response_item_model": None, + }, + { + "resource_class": "LogisticasServicosResource", + "sdk_method": "obter_etiquetas", + "method": "GET", + "path": "/logisticas/etiquetas", + "pattern": r"^/logisticas/etiquetas/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_etiquetas.LogisticasEtiquetasGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasServicosResource", + "sdk_method": "criar_objeto", + "method": "POST", + "path": "/logisticas/objetos", + "pattern": r"^/logisticas/objetos/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_objetos.LogisticasObjetosPostResponse201", + "response_item_model": None, + }, + { + "resource_class": "LogisticasServicosResource", + "sdk_method": "remover_objeto", + "method": "DELETE", + "path": "/logisticas/objetos/{idObjeto}", + "pattern": r"^/logisticas/objetos/[^/]+/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "LogisticasServicosResource", + "sdk_method": "obter_objeto", + "method": "GET", + "path": "/logisticas/objetos/{idObjeto}", + "pattern": r"^/logisticas/objetos/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_objetos.LogisticasObjetosIdObjetoGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasServicosResource", + "sdk_method": "alterar_objeto", + "method": "PUT", + "path": "/logisticas/objetos/{idObjeto}", + "pattern": r"^/logisticas/objetos/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_objetos.LogisticasObjetosIdObjetoPutResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasServicosResource", + "sdk_method": "criar_remessa", + "method": "POST", + "path": "/logisticas/remessas", + "pattern": r"^/logisticas/remessas/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_remessas.LogisticasRemessasPostResponse201", + "response_item_model": None, + }, + { + "resource_class": "LogisticasServicosResource", + "sdk_method": "remover_remessa", + "method": "DELETE", + "path": "/logisticas/remessas/{idRemessa}", + "pattern": r"^/logisticas/remessas/[^/]+/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "LogisticasServicosResource", + "sdk_method": "obter_remessa", + "method": "GET", + "path": "/logisticas/remessas/{idRemessa}", + "pattern": r"^/logisticas/remessas/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_remessas.LogisticasRemessasIdRemessaGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasServicosResource", + "sdk_method": "alterar_remessa", + "method": "PUT", + "path": "/logisticas/remessas/{idRemessa}", + "pattern": r"^/logisticas/remessas/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_remessas.LogisticasRemessasIdRemessaPutResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasServicosResource", + "sdk_method": "listar_servicos", + "method": "GET", + "path": "/logisticas/servicos", + "pattern": r"^/logisticas/servicos/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_servicos.LogisticasServicosGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasServicosResource", + "sdk_method": "criar_servico", + "method": "POST", + "path": "/logisticas/servicos", + "pattern": r"^/logisticas/servicos/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_servicos.LogisticasServicosPostResponse201", + "response_item_model": None, + }, + { + "resource_class": "LogisticasServicosResource", + "sdk_method": "obter_servico", + "method": "GET", + "path": "/logisticas/servicos/{idLogisticaServico}", + "pattern": r"^/logisticas/servicos/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_servicos.LogisticasServicosIdLogisticaServicoGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasServicosResource", + "sdk_method": "alterar_servico", + "method": "PUT", + "path": "/logisticas/servicos/{idLogisticaServico}", + "pattern": r"^/logisticas/servicos/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas_servicos.LogisticasServicosIdLogisticaServicoPutResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasServicosResource", + "sdk_method": "alterar_situacao_servico", + "method": "PATCH", + "path": "/logisticas/{idLogisticaServico}/situacoes", + "pattern": r"^/logisticas/[^/]+/situacoes/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "LogisticasServicosResource", + "sdk_method": "remover", + "method": "DELETE", + "path": "/logisticas/{idLogistica}", + "pattern": r"^/logisticas/[^/]+/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "LogisticasServicosResource", + "sdk_method": "obter", + "method": "GET", + "path": "/logisticas/{idLogistica}", + "pattern": r"^/logisticas/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasIdLogisticaGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "LogisticasServicosResource", + "sdk_method": "alterar", + "method": "PUT", + "path": "/logisticas/{idLogistica}", + "pattern": r"^/logisticas/[^/]+/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "LogisticasServicosResource", + "sdk_method": "listar_remessas_por_logistica", + "method": "GET", + "path": "/logisticas/{idLogistica}/remessas", + "pattern": r"^/logisticas/[^/]+/remessas/?$", + "response_model": "bling_erp_api.models.generated.schemas.logisticas.LogisticasIdLogisticaRemessasGetResponse200", + "response_item_model": None, + }, { "resource_class": "NaturezasOperacoesResource", "sdk_method": "listar", @@ -948,6 +1668,195 @@ "response_model": "bling_erp_api.models.generated.schemas.naturezas_operacoes.NaturezasOperacoesIdNaturezaOperacaoObterTributacaoPostResponse200", "response_item_model": None, }, + { + "resource_class": "NfceResource", + "sdk_method": "listar", + "method": "GET", + "path": "/nfce", + "pattern": r"^/nfce/?$", + "response_model": "bling_erp_api.models.generated.schemas.notas_fiscais_consumidor.NfceGetResponse200", + "response_item_model": "bling_erp_api.models.generated.schemas.notas_fiscais.NotasFiscaisDadosBaseDTO", + }, + { + "resource_class": "NfceResource", + "sdk_method": "criar", + "method": "POST", + "path": "/nfce", + "pattern": r"^/nfce/?$", + "response_model": "bling_erp_api.models.generated.schemas.notas_fiscais_consumidor.NfcePostResponse201", + "response_item_model": None, + }, + { + "resource_class": "NfceResource", + "sdk_method": "obter", + "method": "GET", + "path": "/nfce/{idNotaFiscalConsumidor}", + "pattern": r"^/nfce/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.notas_fiscais_consumidor.NfceIdNotaFiscalConsumidorGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "NfceResource", + "sdk_method": "alterar", + "method": "PUT", + "path": "/nfce/{idNotaFiscalConsumidor}", + "pattern": r"^/nfce/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.notas_fiscais_consumidor.NfceIdNotaFiscalConsumidorPutResponse200", + "response_item_model": None, + }, + { + "resource_class": "NfceResource", + "sdk_method": "autorizar", + "method": "POST", + "path": "/nfce/{idNotaFiscalConsumidor}/enviar", + "pattern": r"^/nfce/[^/]+/enviar/?$", + "response_model": "bling_erp_api.models.generated.schemas.notas_fiscais_consumidor.NfceIdNotaFiscalConsumidorEnviarPostResponse200", + "response_item_model": None, + }, + { + "resource_class": "NfceResource", + "sdk_method": "estornar_contas", + "method": "POST", + "path": "/nfce/{idNotaFiscalConsumidor}/estornar-contas", + "pattern": r"^/nfce/[^/]+/estornar-contas/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "NfceResource", + "sdk_method": "estornar_estoque", + "method": "POST", + "path": "/nfce/{idNotaFiscalConsumidor}/estornar-estoque", + "pattern": r"^/nfce/[^/]+/estornar-estoque/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "NfceResource", + "sdk_method": "lancar_contas", + "method": "POST", + "path": "/nfce/{idNotaFiscalConsumidor}/lancar-contas", + "pattern": r"^/nfce/[^/]+/lancar-contas/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "NfceResource", + "sdk_method": "lancar_estoque", + "method": "POST", + "path": "/nfce/{idNotaFiscalConsumidor}/lancar-estoque", + "pattern": r"^/nfce/[^/]+/lancar-estoque/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "NfceResource", + "sdk_method": "lancar_estoque_id_deposito", + "method": "POST", + "path": "/nfe/{idNotaFiscal}/lancar-estoque/{idDeposito}", + "pattern": r"^/nfe/[^/]+/lancar-estoque/[^/]+/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "NfceResource", + "sdk_method": "remover_varios", + "method": "DELETE", + "path": "/nfe", + "pattern": r"^/nfe/?$", + "response_model": "bling_erp_api.models.generated.schemas.notas_fiscais.NfeDeleteResponse200", + "response_item_model": None, + }, + { + "resource_class": "NfceResource", + "sdk_method": "listar_nfe", + "method": "GET", + "path": "/nfe", + "pattern": r"^/nfe/?$", + "response_model": "bling_erp_api.models.generated.schemas.notas_fiscais.NfeGetResponse200", + "response_item_model": "bling_erp_api.models.generated.schemas.notas_fiscais.NotasFiscaisDadosBaseDTO", + }, + { + "resource_class": "NfceResource", + "sdk_method": "criar_nfe", + "method": "POST", + "path": "/nfe", + "pattern": r"^/nfe/?$", + "response_model": "bling_erp_api.models.generated.schemas.notas_fiscais.NfePostResponse201", + "response_item_model": None, + }, + { + "resource_class": "NfceResource", + "sdk_method": "obter_documento_nota_fiscal", + "method": "GET", + "path": "/nfe/documento/{chaveAcesso}", + "pattern": r"^/nfe/documento/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.notas_fiscais.NfeDocumentoChaveAcessoGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "NfceResource", + "sdk_method": "obter_id_nota_fiscal", + "method": "GET", + "path": "/nfe/{idNotaFiscal}", + "pattern": r"^/nfe/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.notas_fiscais.NfeIdNotaFiscalGetResponse200", + "response_item_model": None, + }, + { + "resource_class": "NfceResource", + "sdk_method": "alterar_id_nota_fiscal", + "method": "PUT", + "path": "/nfe/{idNotaFiscal}", + "pattern": r"^/nfe/[^/]+/?$", + "response_model": "bling_erp_api.models.generated.schemas.notas_fiscais.NfeIdNotaFiscalPutResponse200", + "response_item_model": None, + }, + { + "resource_class": "NfceResource", + "sdk_method": "autorizar_enviar", + "method": "POST", + "path": "/nfe/{idNotaFiscal}/enviar", + "pattern": r"^/nfe/[^/]+/enviar/?$", + "response_model": "bling_erp_api.models.generated.schemas.notas_fiscais.NfeIdNotaFiscalEnviarPostResponse200", + "response_item_model": None, + }, + { + "resource_class": "NfceResource", + "sdk_method": "estornar_contas_estornar_contas", + "method": "POST", + "path": "/nfe/{idNotaFiscal}/estornar-contas", + "pattern": r"^/nfe/[^/]+/estornar-contas/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "NfceResource", + "sdk_method": "estornar_estoque_estornar_estoque", + "method": "POST", + "path": "/nfe/{idNotaFiscal}/estornar-estoque", + "pattern": r"^/nfe/[^/]+/estornar-estoque/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "NfceResource", + "sdk_method": "lancar_contas_lancar_contas", + "method": "POST", + "path": "/nfe/{idNotaFiscal}/lancar-contas", + "pattern": r"^/nfe/[^/]+/lancar-contas/?$", + "response_model": None, + "response_item_model": None, + }, + { + "resource_class": "NfceResource", + "sdk_method": "lancar_estoque_lancar_estoque", + "method": "POST", + "path": "/nfe/{idNotaFiscal}/lancar-estoque", + "pattern": r"^/nfe/[^/]+/lancar-estoque/?$", + "response_model": None, + "response_item_model": None, + }, { "resource_class": "NfseResource", "sdk_method": "listar", diff --git a/src/bling_erp_api/models/fields.py b/src/bling_erp_api/models/fields.py index 0e3a9c2..f98f5e6 100644 --- a/src/bling_erp_api/models/fields.py +++ b/src/bling_erp_api/models/fields.py @@ -5,6 +5,7 @@ from datetime import date from typing import Annotated +from pydantic.functional_serializers import PlainSerializer from pydantic.functional_validators import PlainValidator @@ -33,4 +34,13 @@ def _parse_bling_date(value: object) -> date | None: raise ValueError(msg) -BlingDate = Annotated[date, PlainValidator(_parse_bling_date)] +def _serialize_bling_date(value: date | None) -> str | None: + """Serialize a ``BlingDate`` as an ISO date string, preserving ``None``.""" + return value.isoformat() if value is not None else None + + +BlingDate = Annotated[ + date, + PlainValidator(_parse_bling_date), + PlainSerializer(_serialize_bling_date, return_type=str | None, when_used="json"), +] diff --git a/src/bling_erp_api/models/generated/logisticas_etiquetas.py b/src/bling_erp_api/models/generated/logisticas_etiquetas.py new file mode 100644 index 0000000..1096aa0 --- /dev/null +++ b/src/bling_erp_api/models/generated/logisticas_etiquetas.py @@ -0,0 +1,89 @@ +"""Generated model reexports for logisticas_etiquetas. Do not edit manually.""" + +from __future__ import annotations + +from bling_erp_api.models.generated.schemas.common import ErrorResponse +from bling_erp_api.models.generated.schemas.logisticas import ( + LogisticasDadosBaseDTO, + LogisticasDadosDTO, + LogisticasDadosPostDTO, + LogisticasDadosPutDTO, + LogisticasGetResponse200, + LogisticasIdLogisticaGetResponse200, + LogisticasIdLogisticaRemessasGetResponse200, + LogisticasLogisticaDTO, + LogisticasPostResponse201, + LogisticasRemessaRemessaDTO, +) +from bling_erp_api.models.generated.schemas.logisticas_etiquetas import ( + LogisticasEtiquetasDadosResponseDTO, + LogisticasEtiquetasGetResponse200, +) +from bling_erp_api.models.generated.schemas.logisticas_objetos import ( + LogisticasObjetosDadosCreateRequestDTO, + LogisticasObjetosDadosDTO, + LogisticasObjetosIdObjetoGetResponse200, + LogisticasObjetosIdObjetoPutResponse200, + LogisticasObjetosObjetoDTO, + LogisticasObjetosPostResponse201, + LogisticasObjetosUpdateRequestDTO, +) +from bling_erp_api.models.generated.schemas.logisticas_remessas import ( + LogisticasRemessasDadosBaseDTO, + LogisticasRemessasDadosBaseDTOCommon, + LogisticasRemessasDadosDTO, + LogisticasRemessasDadosPostDTO, + LogisticasRemessasIdRemessaGetResponse200, + LogisticasRemessasIdRemessaPutResponse200, + LogisticasRemessasPostResponse201, +) +from bling_erp_api.models.generated.schemas.logisticas_servicos import ( + LogisticasServicosDadosCreateRequestDTO, + LogisticasServicosDadosDTO, + LogisticasServicosDadosSaveDTO, + LogisticasServicosDadosSaveRequestDTO, + LogisticasServicosDadosSituationDTO, + LogisticasServicosGetResponse200, + LogisticasServicosIdLogisticaServicoGetResponse200, + LogisticasServicosIdLogisticaServicoPutResponse200, + LogisticasServicosPostResponse201, +) + +__all__ = [ + "ErrorResponse", + "LogisticasDadosBaseDTO", + "LogisticasDadosDTO", + "LogisticasDadosPostDTO", + "LogisticasDadosPutDTO", + "LogisticasEtiquetasDadosResponseDTO", + "LogisticasEtiquetasGetResponse200", + "LogisticasGetResponse200", + "LogisticasIdLogisticaGetResponse200", + "LogisticasIdLogisticaRemessasGetResponse200", + "LogisticasLogisticaDTO", + "LogisticasObjetosDadosCreateRequestDTO", + "LogisticasObjetosDadosDTO", + "LogisticasObjetosIdObjetoGetResponse200", + "LogisticasObjetosIdObjetoPutResponse200", + "LogisticasObjetosObjetoDTO", + "LogisticasObjetosPostResponse201", + "LogisticasObjetosUpdateRequestDTO", + "LogisticasPostResponse201", + "LogisticasRemessaRemessaDTO", + "LogisticasRemessasDadosBaseDTO", + "LogisticasRemessasDadosBaseDTOCommon", + "LogisticasRemessasDadosDTO", + "LogisticasRemessasDadosPostDTO", + "LogisticasRemessasIdRemessaGetResponse200", + "LogisticasRemessasIdRemessaPutResponse200", + "LogisticasRemessasPostResponse201", + "LogisticasServicosDadosCreateRequestDTO", + "LogisticasServicosDadosDTO", + "LogisticasServicosDadosSaveDTO", + "LogisticasServicosDadosSaveRequestDTO", + "LogisticasServicosDadosSituationDTO", + "LogisticasServicosGetResponse200", + "LogisticasServicosIdLogisticaServicoGetResponse200", + "LogisticasServicosIdLogisticaServicoPutResponse200", + "LogisticasServicosPostResponse201", +] diff --git a/src/bling_erp_api/models/generated/resources/logisticas.py b/src/bling_erp_api/models/generated/logisticas_objetos.py similarity index 97% rename from src/bling_erp_api/models/generated/resources/logisticas.py rename to src/bling_erp_api/models/generated/logisticas_objetos.py index 3d9fd08..8325775 100644 --- a/src/bling_erp_api/models/generated/resources/logisticas.py +++ b/src/bling_erp_api/models/generated/logisticas_objetos.py @@ -1,4 +1,4 @@ -"""Generated model reexports for logisticas. Do not edit manually.""" +"""Generated model reexports for logisticas_objetos. Do not edit manually.""" from __future__ import annotations diff --git a/src/bling_erp_api/models/generated/logisticas_remessas.py b/src/bling_erp_api/models/generated/logisticas_remessas.py new file mode 100644 index 0000000..e701afe --- /dev/null +++ b/src/bling_erp_api/models/generated/logisticas_remessas.py @@ -0,0 +1,89 @@ +"""Generated model reexports for logisticas_remessas. Do not edit manually.""" + +from __future__ import annotations + +from bling_erp_api.models.generated.schemas.common import ErrorResponse +from bling_erp_api.models.generated.schemas.logisticas import ( + LogisticasDadosBaseDTO, + LogisticasDadosDTO, + LogisticasDadosPostDTO, + LogisticasDadosPutDTO, + LogisticasGetResponse200, + LogisticasIdLogisticaGetResponse200, + LogisticasIdLogisticaRemessasGetResponse200, + LogisticasLogisticaDTO, + LogisticasPostResponse201, + LogisticasRemessaRemessaDTO, +) +from bling_erp_api.models.generated.schemas.logisticas_etiquetas import ( + LogisticasEtiquetasDadosResponseDTO, + LogisticasEtiquetasGetResponse200, +) +from bling_erp_api.models.generated.schemas.logisticas_objetos import ( + LogisticasObjetosDadosCreateRequestDTO, + LogisticasObjetosDadosDTO, + LogisticasObjetosIdObjetoGetResponse200, + LogisticasObjetosIdObjetoPutResponse200, + LogisticasObjetosObjetoDTO, + LogisticasObjetosPostResponse201, + LogisticasObjetosUpdateRequestDTO, +) +from bling_erp_api.models.generated.schemas.logisticas_remessas import ( + LogisticasRemessasDadosBaseDTO, + LogisticasRemessasDadosBaseDTOCommon, + LogisticasRemessasDadosDTO, + LogisticasRemessasDadosPostDTO, + LogisticasRemessasIdRemessaGetResponse200, + LogisticasRemessasIdRemessaPutResponse200, + LogisticasRemessasPostResponse201, +) +from bling_erp_api.models.generated.schemas.logisticas_servicos import ( + LogisticasServicosDadosCreateRequestDTO, + LogisticasServicosDadosDTO, + LogisticasServicosDadosSaveDTO, + LogisticasServicosDadosSaveRequestDTO, + LogisticasServicosDadosSituationDTO, + LogisticasServicosGetResponse200, + LogisticasServicosIdLogisticaServicoGetResponse200, + LogisticasServicosIdLogisticaServicoPutResponse200, + LogisticasServicosPostResponse201, +) + +__all__ = [ + "ErrorResponse", + "LogisticasDadosBaseDTO", + "LogisticasDadosDTO", + "LogisticasDadosPostDTO", + "LogisticasDadosPutDTO", + "LogisticasEtiquetasDadosResponseDTO", + "LogisticasEtiquetasGetResponse200", + "LogisticasGetResponse200", + "LogisticasIdLogisticaGetResponse200", + "LogisticasIdLogisticaRemessasGetResponse200", + "LogisticasLogisticaDTO", + "LogisticasObjetosDadosCreateRequestDTO", + "LogisticasObjetosDadosDTO", + "LogisticasObjetosIdObjetoGetResponse200", + "LogisticasObjetosIdObjetoPutResponse200", + "LogisticasObjetosObjetoDTO", + "LogisticasObjetosPostResponse201", + "LogisticasObjetosUpdateRequestDTO", + "LogisticasPostResponse201", + "LogisticasRemessaRemessaDTO", + "LogisticasRemessasDadosBaseDTO", + "LogisticasRemessasDadosBaseDTOCommon", + "LogisticasRemessasDadosDTO", + "LogisticasRemessasDadosPostDTO", + "LogisticasRemessasIdRemessaGetResponse200", + "LogisticasRemessasIdRemessaPutResponse200", + "LogisticasRemessasPostResponse201", + "LogisticasServicosDadosCreateRequestDTO", + "LogisticasServicosDadosDTO", + "LogisticasServicosDadosSaveDTO", + "LogisticasServicosDadosSaveRequestDTO", + "LogisticasServicosDadosSituationDTO", + "LogisticasServicosGetResponse200", + "LogisticasServicosIdLogisticaServicoGetResponse200", + "LogisticasServicosIdLogisticaServicoPutResponse200", + "LogisticasServicosPostResponse201", +] diff --git a/src/bling_erp_api/models/generated/logisticas_servicos.py b/src/bling_erp_api/models/generated/logisticas_servicos.py new file mode 100644 index 0000000..392309b --- /dev/null +++ b/src/bling_erp_api/models/generated/logisticas_servicos.py @@ -0,0 +1,89 @@ +"""Generated model reexports for logisticas_servicos. Do not edit manually.""" + +from __future__ import annotations + +from bling_erp_api.models.generated.schemas.common import ErrorResponse +from bling_erp_api.models.generated.schemas.logisticas import ( + LogisticasDadosBaseDTO, + LogisticasDadosDTO, + LogisticasDadosPostDTO, + LogisticasDadosPutDTO, + LogisticasGetResponse200, + LogisticasIdLogisticaGetResponse200, + LogisticasIdLogisticaRemessasGetResponse200, + LogisticasLogisticaDTO, + LogisticasPostResponse201, + LogisticasRemessaRemessaDTO, +) +from bling_erp_api.models.generated.schemas.logisticas_etiquetas import ( + LogisticasEtiquetasDadosResponseDTO, + LogisticasEtiquetasGetResponse200, +) +from bling_erp_api.models.generated.schemas.logisticas_objetos import ( + LogisticasObjetosDadosCreateRequestDTO, + LogisticasObjetosDadosDTO, + LogisticasObjetosIdObjetoGetResponse200, + LogisticasObjetosIdObjetoPutResponse200, + LogisticasObjetosObjetoDTO, + LogisticasObjetosPostResponse201, + LogisticasObjetosUpdateRequestDTO, +) +from bling_erp_api.models.generated.schemas.logisticas_remessas import ( + LogisticasRemessasDadosBaseDTO, + LogisticasRemessasDadosBaseDTOCommon, + LogisticasRemessasDadosDTO, + LogisticasRemessasDadosPostDTO, + LogisticasRemessasIdRemessaGetResponse200, + LogisticasRemessasIdRemessaPutResponse200, + LogisticasRemessasPostResponse201, +) +from bling_erp_api.models.generated.schemas.logisticas_servicos import ( + LogisticasServicosDadosCreateRequestDTO, + LogisticasServicosDadosDTO, + LogisticasServicosDadosSaveDTO, + LogisticasServicosDadosSaveRequestDTO, + LogisticasServicosDadosSituationDTO, + LogisticasServicosGetResponse200, + LogisticasServicosIdLogisticaServicoGetResponse200, + LogisticasServicosIdLogisticaServicoPutResponse200, + LogisticasServicosPostResponse201, +) + +__all__ = [ + "ErrorResponse", + "LogisticasDadosBaseDTO", + "LogisticasDadosDTO", + "LogisticasDadosPostDTO", + "LogisticasDadosPutDTO", + "LogisticasEtiquetasDadosResponseDTO", + "LogisticasEtiquetasGetResponse200", + "LogisticasGetResponse200", + "LogisticasIdLogisticaGetResponse200", + "LogisticasIdLogisticaRemessasGetResponse200", + "LogisticasLogisticaDTO", + "LogisticasObjetosDadosCreateRequestDTO", + "LogisticasObjetosDadosDTO", + "LogisticasObjetosIdObjetoGetResponse200", + "LogisticasObjetosIdObjetoPutResponse200", + "LogisticasObjetosObjetoDTO", + "LogisticasObjetosPostResponse201", + "LogisticasObjetosUpdateRequestDTO", + "LogisticasPostResponse201", + "LogisticasRemessaRemessaDTO", + "LogisticasRemessasDadosBaseDTO", + "LogisticasRemessasDadosBaseDTOCommon", + "LogisticasRemessasDadosDTO", + "LogisticasRemessasDadosPostDTO", + "LogisticasRemessasIdRemessaGetResponse200", + "LogisticasRemessasIdRemessaPutResponse200", + "LogisticasRemessasPostResponse201", + "LogisticasServicosDadosCreateRequestDTO", + "LogisticasServicosDadosDTO", + "LogisticasServicosDadosSaveDTO", + "LogisticasServicosDadosSaveRequestDTO", + "LogisticasServicosDadosSituationDTO", + "LogisticasServicosGetResponse200", + "LogisticasServicosIdLogisticaServicoGetResponse200", + "LogisticasServicosIdLogisticaServicoPutResponse200", + "LogisticasServicosPostResponse201", +] diff --git a/src/bling_erp_api/models/generated/resources/invoices.py b/src/bling_erp_api/models/generated/nfce.py similarity index 96% rename from src/bling_erp_api/models/generated/resources/invoices.py rename to src/bling_erp_api/models/generated/nfce.py index f5f8a52..d83fe73 100644 --- a/src/bling_erp_api/models/generated/resources/invoices.py +++ b/src/bling_erp_api/models/generated/nfce.py @@ -1,4 +1,4 @@ -"""Generated model reexports for invoices. Do not edit manually.""" +"""Generated model reexports for nfce. Do not edit manually.""" from __future__ import annotations diff --git a/src/bling_erp_api/models/generated/resources/__init__.py b/src/bling_erp_api/models/generated/resources/__init__.py deleted file mode 100644 index 67be30b..0000000 --- a/src/bling_erp_api/models/generated/resources/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Resource-scoped generated model reexports. - -TODO: Consolidate duplicate model tree. -The ``resources/`` subdirectory mirrors the parent ``models/generated/`` with -identical reexport modules. This is a codegen duplication — investigate whether -imports from ``models/generated/resources/`` can be retired in favor of -``models/generated/.py``. -""" diff --git a/src/bling_erp_api/models/generated/resources/ad_categories.py b/src/bling_erp_api/models/generated/resources/ad_categories.py deleted file mode 100644 index 8be5d99..0000000 --- a/src/bling_erp_api/models/generated/resources/ad_categories.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Generated model reexports for ad_categories. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.anuncios import ( - AnunciosCategoriaDTO, - AnunciosGetAttributesFromCategoryResponseDTO, -) -from bling_erp_api.models.generated.schemas.anuncios_categorias import ( - AnunciosCategoriasGetResponse200, - AnunciosCategoriasIdCategoriaGetResponse200, -) -from bling_erp_api.models.generated.schemas.common import ErrorResponse - -__all__ = [ - "AnunciosCategoriaDTO", - "AnunciosCategoriasGetResponse200", - "AnunciosCategoriasIdCategoriaGetResponse200", - "AnunciosGetAttributesFromCategoryResponseDTO", - "ErrorResponse", -] diff --git a/src/bling_erp_api/models/generated/resources/ads.py b/src/bling_erp_api/models/generated/resources/ads.py deleted file mode 100644 index cf515cd..0000000 --- a/src/bling_erp_api/models/generated/resources/ads.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Generated model reexports for ads. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.anuncios import ( - AnunciosGetAllResponseDTO, - AnunciosGetByIdResponseDTO, - AnunciosGetResponse200, - AnunciosIdAnuncioGetResponse200, - AnunciosPostResponse201, - AnunciosSaveRequest, - AnunciosSaveResponseDTO, -) -from bling_erp_api.models.generated.schemas.common import ErrorResponse - -__all__ = [ - "AnunciosGetAllResponseDTO", - "AnunciosGetByIdResponseDTO", - "AnunciosGetResponse200", - "AnunciosIdAnuncioGetResponse200", - "AnunciosPostResponse201", - "AnunciosSaveRequest", - "AnunciosSaveResponseDTO", - "ErrorResponse", -] diff --git a/src/bling_erp_api/models/generated/resources/borderos.py b/src/bling_erp_api/models/generated/resources/borderos.py deleted file mode 100644 index c5e2993..0000000 --- a/src/bling_erp_api/models/generated/resources/borderos.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Generated model reexports for borderos. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.borderos import ( - BorderosDadosDTO, - BorderosIdBorderoGetResponse200, -) -from bling_erp_api.models.generated.schemas.common import ErrorResponse - -__all__ = [ - "BorderosDadosDTO", - "BorderosIdBorderoGetResponse200", - "ErrorResponse", -] diff --git a/src/bling_erp_api/models/generated/resources/caixas_bancos.py b/src/bling_erp_api/models/generated/resources/caixas_bancos.py deleted file mode 100644 index 6ad92b4..0000000 --- a/src/bling_erp_api/models/generated/resources/caixas_bancos.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Generated model reexports for caixas_bancos. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.caixas_bancos import ( - CaixasBancosIdCaixaGetResponse200, - CaixasBancosItemLancamentoDTO, - CaixasBancosLancamentoDTO, - CaixasBancosSalvarLancamentoDTO, - CaixasBancosSalvarLancamentoResponseDTO, - CaixasGetResponse200, -) -from bling_erp_api.models.generated.schemas.common import ErrorResponse - -__all__ = [ - "CaixasBancosIdCaixaGetResponse200", - "CaixasBancosItemLancamentoDTO", - "CaixasBancosLancamentoDTO", - "CaixasBancosSalvarLancamentoDTO", - "CaixasBancosSalvarLancamentoResponseDTO", - "CaixasGetResponse200", - "ErrorResponse", -] diff --git a/src/bling_erp_api/models/generated/resources/contacts.py b/src/bling_erp_api/models/generated/resources/contacts.py deleted file mode 100644 index b0ab0ec..0000000 --- a/src/bling_erp_api/models/generated/resources/contacts.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Generated model reexports for contacts. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import BasePostResponse, ErrorResponse -from bling_erp_api.models.generated.schemas.contatos import ( - ContatosAlertasResponse, - ContatosConsumidorFinalGetResponse200, - ContatosDadosBaseDTO, - ContatosDadosDTO, - ContatosDeleteResponse200, - ContatosGetResponse200, - ContatosIdContatoGetResponse200, - ContatosIdContatoPutRequest, - ContatosIdContatoSituacoesPatchRequest, - ContatosIdContatoTiposGetResponse200, - ContatosPostRequest, - ContatosPostResponse201, - ContatosSituacoesPostRequest, - ContatosSituacoesPostResponse200, - ContatosTipoContatoDTO, - ContatosTiposGetResponse200, -) - -__all__ = [ - "BasePostResponse", - "ContatosAlertasResponse", - "ContatosConsumidorFinalGetResponse200", - "ContatosDadosBaseDTO", - "ContatosDadosDTO", - "ContatosDeleteResponse200", - "ContatosGetResponse200", - "ContatosIdContatoGetResponse200", - "ContatosIdContatoPutRequest", - "ContatosIdContatoSituacoesPatchRequest", - "ContatosIdContatoTiposGetResponse200", - "ContatosPostRequest", - "ContatosPostResponse201", - "ContatosSituacoesPostRequest", - "ContatosSituacoesPostResponse200", - "ContatosTipoContatoDTO", - "ContatosTiposGetResponse200", - "ErrorResponse", -] diff --git a/src/bling_erp_api/models/generated/resources/contas_contabeis.py b/src/bling_erp_api/models/generated/resources/contas_contabeis.py deleted file mode 100644 index cc3e323..0000000 --- a/src/bling_erp_api/models/generated/resources/contas_contabeis.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Generated model reexports for contas_contabeis. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import ErrorResponse -from bling_erp_api.models.generated.schemas.contas_contabeis import ( - ContasContabeisDadosDTO, - ContasContabeisGetResponse200, - ContasContabeisIdContaContabilGetResponse200, -) - -__all__ = [ - "ContasContabeisDadosDTO", - "ContasContabeisGetResponse200", - "ContasContabeisIdContaContabilGetResponse200", - "ErrorResponse", -] diff --git a/src/bling_erp_api/models/generated/resources/contas_pagar.py b/src/bling_erp_api/models/generated/resources/contas_pagar.py deleted file mode 100644 index f196011..0000000 --- a/src/bling_erp_api/models/generated/resources/contas_pagar.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Generated model reexports for contas_pagar. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import BasePostResponse, ErrorResponse -from bling_erp_api.models.generated.schemas.contas import ContasBaixarContaDTO, ContasDadosBaseDTO -from bling_erp_api.models.generated.schemas.contas_pagar import ( - ContasPagarDadosDTO, - ContasPagarDadosPostDTO, - ContasPagarGetResponse200, - ContasPagarIdContaPagarBaixarPostResponse200, - ContasPagarIdContaPagarGetResponse200, - ContasPagarIdContaPagarPutRequest, - ContasPagarIdContaPagarPutResponse200, - ContasPagarPostRequest, - ContasPagarPostResponse201, -) - -__all__ = [ - "BasePostResponse", - "ContasBaixarContaDTO", - "ContasDadosBaseDTO", - "ContasPagarDadosDTO", - "ContasPagarDadosPostDTO", - "ContasPagarGetResponse200", - "ContasPagarIdContaPagarBaixarPostResponse200", - "ContasPagarIdContaPagarGetResponse200", - "ContasPagarIdContaPagarPutRequest", - "ContasPagarIdContaPagarPutResponse200", - "ContasPagarPostRequest", - "ContasPagarPostResponse201", - "ErrorResponse", -] diff --git a/src/bling_erp_api/models/generated/resources/contas_receber.py b/src/bling_erp_api/models/generated/resources/contas_receber.py deleted file mode 100644 index 28e4e2c..0000000 --- a/src/bling_erp_api/models/generated/resources/contas_receber.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Generated model reexports for contas_receber. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import BasePostResponse, ErrorResponse -from bling_erp_api.models.generated.schemas.contas import ContasBaixarContaDTO, ContasDadosBaseDTO -from bling_erp_api.models.generated.schemas.contas_receber import ( - ContasReceberBoletosCancelarDTO, - ContasReceberBoletosDadosBaseDTO, - ContasReceberDadosBaseDTO, - ContasReceberDadosDTO, - ContasReceberDadosListDTO, - ContasReceberGetResponse200, - ContasReceberIdContaReceberBaixarPostResponse200, - ContasReceberIdContaReceberGetResponse200, - ContasReceberIdContaReceberPutRequest, - ContasReceberPostRequest, - ContasReceberPostResponse201, -) - -__all__ = [ - "BasePostResponse", - "ContasBaixarContaDTO", - "ContasDadosBaseDTO", - "ContasReceberBoletosCancelarDTO", - "ContasReceberBoletosDadosBaseDTO", - "ContasReceberDadosBaseDTO", - "ContasReceberDadosDTO", - "ContasReceberDadosListDTO", - "ContasReceberGetResponse200", - "ContasReceberIdContaReceberBaixarPostResponse200", - "ContasReceberIdContaReceberGetResponse200", - "ContasReceberIdContaReceberPutRequest", - "ContasReceberPostRequest", - "ContasReceberPostResponse201", - "ErrorResponse", -] diff --git a/src/bling_erp_api/models/generated/resources/depositos.py b/src/bling_erp_api/models/generated/resources/depositos.py deleted file mode 100644 index d96d017..0000000 --- a/src/bling_erp_api/models/generated/resources/depositos.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Generated model reexports for depositos. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import ( - BasePostResponse, - ErrorField, - ErrorResponse, -) -from bling_erp_api.models.generated.schemas.depositos import ( - DepositosDadosDTO, - DepositosGetResponse200, - DepositosIdDepositoGetResponse200, - DepositosIdDepositoPutResponse200, - DepositosPostResponse201, -) - -__all__ = [ - "BasePostResponse", - "DepositosDadosDTO", - "DepositosGetResponse200", - "DepositosIdDepositoGetResponse200", - "DepositosIdDepositoPutResponse200", - "DepositosPostResponse201", - "ErrorField", - "ErrorResponse", -] diff --git a/src/bling_erp_api/models/generated/resources/empresas.py b/src/bling_erp_api/models/generated/resources/empresas.py deleted file mode 100644 index 5afa916..0000000 --- a/src/bling_erp_api/models/generated/resources/empresas.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Generated model reexports for empresas. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.empresas import ( - EmpresasDadosBasicosDTO, - EmpresasMeDadosBasicosGetResponse200, -) - -__all__ = [ - "EmpresasDadosBasicosDTO", - "EmpresasMeDadosBasicosGetResponse200", -] diff --git a/src/bling_erp_api/models/generated/resources/estoques.py b/src/bling_erp_api/models/generated/resources/estoques.py deleted file mode 100644 index 11386e7..0000000 --- a/src/bling_erp_api/models/generated/resources/estoques.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Generated model reexports for estoques. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import BasePostResponse, ErrorResponse -from bling_erp_api.models.generated.schemas.estoques import ( - EstoquesDadosBaseDTO, - EstoquesDadosDTO, - EstoquesPostRequest, - EstoquesPostResponse201, - EstoquesSaldosBaseDTO, - EstoquesSaldosDTO, - EstoquesSaldosGetResponse200, - EstoquesSaldosIdDepositoGetResponse200, -) - -__all__ = [ - "BasePostResponse", - "ErrorResponse", - "EstoquesDadosBaseDTO", - "EstoquesDadosDTO", - "EstoquesPostRequest", - "EstoquesPostResponse201", - "EstoquesSaldosBaseDTO", - "EstoquesSaldosDTO", - "EstoquesSaldosGetResponse200", - "EstoquesSaldosIdDepositoGetResponse200", -] diff --git a/src/bling_erp_api/models/generated/resources/homologation.py b/src/bling_erp_api/models/generated/resources/homologation.py deleted file mode 100644 index bb17e52..0000000 --- a/src/bling_erp_api/models/generated/resources/homologation.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Generated model reexports for homologation. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import ErrorResponse -from bling_erp_api.models.generated.schemas.homologacao import ( - HomologacaoDadosBaseDTO, - HomologacaoDadosDTO, - HomologacaoProdutosGetResponse200, - HomologacaoProdutosPostResponse201, - HomologacaoSituacaoDTO, -) - -__all__ = [ - "ErrorResponse", - "HomologacaoDadosBaseDTO", - "HomologacaoDadosDTO", - "HomologacaoProdutosGetResponse200", - "HomologacaoProdutosPostResponse201", - "HomologacaoSituacaoDTO", -] diff --git a/src/bling_erp_api/models/generated/resources/income_expense_categories.py b/src/bling_erp_api/models/generated/resources/income_expense_categories.py deleted file mode 100644 index 1d1b285..0000000 --- a/src/bling_erp_api/models/generated/resources/income_expense_categories.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Generated model reexports for income_expense_categories. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.categorias_receitas_despesas import ( - CategoriasReceitasDespesasDadosBaseDTO, - CategoriasReceitasDespesasDadosDTO, - CategoriasReceitasDespesasDadosPostDTO, - CategoriasReceitasDespesasDeleteResponse200, - CategoriasReceitasDespesasGetResponse200, - CategoriasReceitasDespesasIdCategoriaGetResponse200, - CategoriasReceitasDespesasIdCategoriaPutRequest, - CategoriasReceitasDespesasIdCategoriaPutResponse200, - CategoriasReceitasDespesasPostRequest, - CategoriasReceitasDespesasPostResponse201, -) -from bling_erp_api.models.generated.schemas.common import ErrorResponse - -__all__ = [ - "CategoriasReceitasDespesasDadosBaseDTO", - "CategoriasReceitasDespesasDadosDTO", - "CategoriasReceitasDespesasDadosPostDTO", - "CategoriasReceitasDespesasDeleteResponse200", - "CategoriasReceitasDespesasGetResponse200", - "CategoriasReceitasDespesasIdCategoriaGetResponse200", - "CategoriasReceitasDespesasIdCategoriaPutRequest", - "CategoriasReceitasDespesasIdCategoriaPutResponse200", - "CategoriasReceitasDespesasPostRequest", - "CategoriasReceitasDespesasPostResponse201", - "ErrorResponse", -] diff --git a/src/bling_erp_api/models/generated/resources/naturezas_operacoes.py b/src/bling_erp_api/models/generated/resources/naturezas_operacoes.py deleted file mode 100644 index 6cf6650..0000000 --- a/src/bling_erp_api/models/generated/resources/naturezas_operacoes.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Generated model reexports for naturezas_operacoes. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.calculos_impostos import ( - CalculosImpostosCalculoDTO, - CalculosImpostosDadosDTO, -) -from bling_erp_api.models.generated.schemas.common import ErrorResponse -from bling_erp_api.models.generated.schemas.naturezas_operacoes import ( - NaturezasOperacoesDadosDTO, - NaturezasOperacoesGetResponse200, - NaturezasOperacoesIdNaturezaOperacaoObterTributacaoPostResponse200, -) - -__all__ = [ - "CalculosImpostosCalculoDTO", - "CalculosImpostosDadosDTO", - "ErrorResponse", - "NaturezasOperacoesDadosDTO", - "NaturezasOperacoesGetResponse200", - "NaturezasOperacoesIdNaturezaOperacaoObterTributacaoPostResponse200", -] diff --git a/src/bling_erp_api/models/generated/resources/nfse.py b/src/bling_erp_api/models/generated/resources/nfse.py deleted file mode 100644 index 322bc81..0000000 --- a/src/bling_erp_api/models/generated/resources/nfse.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Generated model reexports for nfse. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import BasePostResponse, ErrorResponse -from bling_erp_api.models.generated.schemas.configuracoes import ConfiguracaoNotaServicoDadosBaseDTO -from bling_erp_api.models.generated.schemas.notas_servicos import ( - NfseGetResponse200, - NfseIdNotaServicoEnviarPostResponse200, - NfseIdNotaServicoGetResponse200, - NfsePostRequest, - NfsePostResponse201, - NotasServicosCancelamentoDTO, - NotasServicosDadosBaseDTO, - NotasServicosDadosBaseDTOPOST, - NotasServicosDadosDTO, - NotasServicosDadosDTOPOST, - NotasServicosResponsePOSTPUT, -) - -__all__ = [ - "BasePostResponse", - "ConfiguracaoNotaServicoDadosBaseDTO", - "ErrorResponse", - "NfseGetResponse200", - "NfseIdNotaServicoEnviarPostResponse200", - "NfseIdNotaServicoGetResponse200", - "NfsePostRequest", - "NfsePostResponse201", - "NotasServicosCancelamentoDTO", - "NotasServicosDadosBaseDTO", - "NotasServicosDadosBaseDTOPOST", - "NotasServicosDadosDTO", - "NotasServicosDadosDTOPOST", - "NotasServicosResponsePOSTPUT", -] diff --git a/src/bling_erp_api/models/generated/resources/notificacoes.py b/src/bling_erp_api/models/generated/resources/notificacoes.py deleted file mode 100644 index 9e682ad..0000000 --- a/src/bling_erp_api/models/generated/resources/notificacoes.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Generated model reexports for notificacoes. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import ErrorResponse -from bling_erp_api.models.generated.schemas.notificacoes import ( - NotificacoesDadosBaseDTO, - NotificacoesGetResponse200, - NotificacoesIdNotificacaoConfirmarLeituraPostResponse200, - NotificacoesQuantidadeDTO, - NotificacoesQuantidadeGetResponse200, - NotificacoesUlidsDTO, -) - -__all__ = [ - "ErrorResponse", - "NotificacoesDadosBaseDTO", - "NotificacoesGetResponse200", - "NotificacoesIdNotificacaoConfirmarLeituraPostResponse200", - "NotificacoesQuantidadeDTO", - "NotificacoesQuantidadeGetResponse200", - "NotificacoesUlidsDTO", -] diff --git a/src/bling_erp_api/models/generated/resources/ordens_producao.py b/src/bling_erp_api/models/generated/resources/ordens_producao.py deleted file mode 100644 index c3a00cc..0000000 --- a/src/bling_erp_api/models/generated/resources/ordens_producao.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Generated model reexports for ordens_producao. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import BasePostResponse, ErrorResponse -from bling_erp_api.models.generated.schemas.ordens_producao import ( - OrdensProducaoDadosBaseDTO, - OrdensProducaoDadosDTO, - OrdensProducaoDadosGeradosPorDemandaDTO, - OrdensProducaoDadosPostDTO, - OrdensProducaoGerarSobDemandaPostResponse201, - OrdensProducaoGetResponse200, - OrdensProducaoIdOrdemProducaoGetResponse200, - OrdensProducaoIdOrdemProducaoPutRequest, - OrdensProducaoPostRequest, - OrdensProducaoPostResponse201, - OrdensProducaoSituacaoDadosDTO, -) - -__all__ = [ - "BasePostResponse", - "ErrorResponse", - "OrdensProducaoDadosBaseDTO", - "OrdensProducaoDadosDTO", - "OrdensProducaoDadosGeradosPorDemandaDTO", - "OrdensProducaoDadosPostDTO", - "OrdensProducaoGerarSobDemandaPostResponse201", - "OrdensProducaoGetResponse200", - "OrdensProducaoIdOrdemProducaoGetResponse200", - "OrdensProducaoIdOrdemProducaoPutRequest", - "OrdensProducaoPostRequest", - "OrdensProducaoPostResponse201", - "OrdensProducaoSituacaoDadosDTO", -] diff --git a/src/bling_erp_api/models/generated/resources/payment_methods.py b/src/bling_erp_api/models/generated/resources/payment_methods.py deleted file mode 100644 index 89bc355..0000000 --- a/src/bling_erp_api/models/generated/resources/payment_methods.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Generated model reexports for payment_methods. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import BasePostResponse, ErrorResponse -from bling_erp_api.models.generated.schemas.formas_pagamentos import ( - FormasPagamentosAlterarSituacaoDTO, - FormasPagamentosDadosBaseDTO, - FormasPagamentosDadosDTO, - FormasPagamentosDefinirPadraoDTO, - FormasPagamentosGetResponse200, - FormasPagamentosIdFormaPagamentoGetResponse200, - FormasPagamentosIdFormaPagamentoPutRequest, - FormasPagamentosIdFormaPagamentoPutResponse200, - FormasPagamentosPostRequest, - FormasPagamentosPostResponse201, -) - -__all__ = [ - "BasePostResponse", - "ErrorResponse", - "FormasPagamentosAlterarSituacaoDTO", - "FormasPagamentosDadosBaseDTO", - "FormasPagamentosDadosDTO", - "FormasPagamentosDefinirPadraoDTO", - "FormasPagamentosGetResponse200", - "FormasPagamentosIdFormaPagamentoGetResponse200", - "FormasPagamentosIdFormaPagamentoPutRequest", - "FormasPagamentosIdFormaPagamentoPutResponse200", - "FormasPagamentosPostRequest", - "FormasPagamentosPostResponse201", -] diff --git a/src/bling_erp_api/models/generated/resources/product_batch_entries.py b/src/bling_erp_api/models/generated/resources/product_batch_entries.py deleted file mode 100644 index 2e80902..0000000 --- a/src/bling_erp_api/models/generated/resources/product_batch_entries.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Generated model reexports for product_batch_entries. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import ErrorResponse -from bling_erp_api.models.generated.schemas.lotes import ( - ProdutosLotesIdLoteLancamentosGetResponse200, - ProdutosLotesIdLoteLancamentosPostResponse200, - ProdutosLotesLancamentosIdLancamentoGetResponse200, -) -from bling_erp_api.models.generated.schemas.lotes_lancamentos import ( - LoteLancamentoDTO, - LoteLancamentoObservacaoDTO, - SaldoLoteDTO, - SaldoSomaLotesDTO, - SaldoSomaLotesTodosDepositosDTO, -) -from bling_erp_api.models.generated.schemas.produtos import ( - ProdutosIdProdutoLotesDepositosIdDepositoSaldoGetResponse200, - ProdutosIdProdutoLotesDepositosIdDepositoSaldoSomaGetResponse200, - ProdutosIdProdutoLotesIdLoteDepositosIdDepositoSaldoGetResponse200, - ProdutosIdProdutoLotesSaldoSomaGetResponse200, -) - -__all__ = [ - "ErrorResponse", - "LoteLancamentoDTO", - "LoteLancamentoObservacaoDTO", - "ProdutosIdProdutoLotesDepositosIdDepositoSaldoGetResponse200", - "ProdutosIdProdutoLotesDepositosIdDepositoSaldoSomaGetResponse200", - "ProdutosIdProdutoLotesIdLoteDepositosIdDepositoSaldoGetResponse200", - "ProdutosIdProdutoLotesSaldoSomaGetResponse200", - "ProdutosLotesIdLoteLancamentosGetResponse200", - "ProdutosLotesIdLoteLancamentosPostResponse200", - "ProdutosLotesLancamentosIdLancamentoGetResponse200", - "SaldoLoteDTO", - "SaldoSomaLotesDTO", - "SaldoSomaLotesTodosDepositosDTO", -] diff --git a/src/bling_erp_api/models/generated/resources/product_batches.py b/src/bling_erp_api/models/generated/resources/product_batches.py deleted file mode 100644 index 82c11e3..0000000 --- a/src/bling_erp_api/models/generated/resources/product_batches.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Generated model reexports for product_batches. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import ErrorResponse -from bling_erp_api.models.generated.schemas.lotes import ( - LotePutRequestDTO, - LotesDTO, - LoteStatusDTO, - ProdutoControlaLotesDTO, - ProdutosLotesControlaLoteGetResponse200, - ProdutosLotesGetResponse200, - ProdutosLotesIdLoteGetResponse200, - ProdutosLotesPutRequest, - ProdutosLotesPutResponse200, - SaveResponseLotsDTO, -) - -__all__ = [ - "ErrorResponse", - "LotePutRequestDTO", - "LoteStatusDTO", - "LotesDTO", - "ProdutoControlaLotesDTO", - "ProdutosLotesControlaLoteGetResponse200", - "ProdutosLotesGetResponse200", - "ProdutosLotesIdLoteGetResponse200", - "ProdutosLotesPutRequest", - "ProdutosLotesPutResponse200", - "SaveResponseLotsDTO", -] diff --git a/src/bling_erp_api/models/generated/resources/product_categories.py b/src/bling_erp_api/models/generated/resources/product_categories.py deleted file mode 100644 index 1d7aa3e..0000000 --- a/src/bling_erp_api/models/generated/resources/product_categories.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Generated model reexports for product_categories. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.categorias_produtos import ( - CategoriasProdutosCategoriPaiDTO, - CategoriasProdutosDadosDTO, - CategoriasProdutosGetResponse200, - CategoriasProdutosIdCategoriaProdutoGetResponse200, - CategoriasProdutosPostRequest, - CategoriasProdutosPostResponse201, -) -from bling_erp_api.models.generated.schemas.common import BasePostResponse, ErrorResponse - -__all__ = [ - "BasePostResponse", - "CategoriasProdutosCategoriPaiDTO", - "CategoriasProdutosDadosDTO", - "CategoriasProdutosGetResponse200", - "CategoriasProdutosIdCategoriaProdutoGetResponse200", - "CategoriasProdutosPostRequest", - "CategoriasProdutosPostResponse201", - "ErrorResponse", -] diff --git a/src/bling_erp_api/models/generated/resources/product_groups.py b/src/bling_erp_api/models/generated/resources/product_groups.py deleted file mode 100644 index 8fa0469..0000000 --- a/src/bling_erp_api/models/generated/resources/product_groups.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Generated model reexports for product_groups. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import BasePostResponse, ErrorResponse -from bling_erp_api.models.generated.schemas.grupos_produtos import ( - GruposProdutosDadosDTO, - GruposProdutosDeleteResponse200, - GruposProdutosGetResponse200, - GruposProdutosGrupoProdutoPaiDTO, - GruposProdutosIdGrupoProdutoGetResponse200, - GruposProdutosIdGrupoProdutoPutRequest, - GruposProdutosPostRequest, - GruposProdutosPostResponse201, -) - -__all__ = [ - "BasePostResponse", - "ErrorResponse", - "GruposProdutosDadosDTO", - "GruposProdutosDeleteResponse200", - "GruposProdutosGetResponse200", - "GruposProdutosGrupoProdutoPaiDTO", - "GruposProdutosIdGrupoProdutoGetResponse200", - "GruposProdutosIdGrupoProdutoPutRequest", - "GruposProdutosPostRequest", - "GruposProdutosPostResponse201", -] diff --git a/src/bling_erp_api/models/generated/resources/product_stores.py b/src/bling_erp_api/models/generated/resources/product_stores.py deleted file mode 100644 index 898157c..0000000 --- a/src/bling_erp_api/models/generated/resources/product_stores.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Generated model reexports for product_stores. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import BasePostResponse, ErrorResponse -from bling_erp_api.models.generated.schemas.produtos_lojas import ( - ProdutosLojasDadosBaseDTO, - ProdutosLojasDadosDTO, - ProdutosLojasGetResponse200, - ProdutosLojasIdProdutoLojaGetResponse200, - ProdutosLojasIdProdutoLojaPutRequest, - ProdutosLojasIdProdutoLojaPutResponse200, - ProdutosLojasPostRequest, - ProdutosLojasPostResponse201, - ProdutosLojasResponsePOSTPUT, -) - -__all__ = [ - "BasePostResponse", - "ErrorResponse", - "ProdutosLojasDadosBaseDTO", - "ProdutosLojasDadosDTO", - "ProdutosLojasGetResponse200", - "ProdutosLojasIdProdutoLojaGetResponse200", - "ProdutosLojasIdProdutoLojaPutRequest", - "ProdutosLojasIdProdutoLojaPutResponse200", - "ProdutosLojasPostRequest", - "ProdutosLojasPostResponse201", - "ProdutosLojasResponsePOSTPUT", -] diff --git a/src/bling_erp_api/models/generated/resources/product_structures.py b/src/bling_erp_api/models/generated/resources/product_structures.py deleted file mode 100644 index 1627147..0000000 --- a/src/bling_erp_api/models/generated/resources/product_structures.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Generated model reexports for product_structures. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import Error, ErrorResponse -from bling_erp_api.models.generated.schemas.produtos import ( - ProdutosComponenteDTO, - ProdutosEstruturaDTO, -) -from bling_erp_api.models.generated.schemas.produtos_estruturas import ( - ProdutosEstruturasDeleteResponse200, - ProdutosEstruturasIdProdutoEstruturaComponentesPostRequest, - ProdutosEstruturasIdProdutoEstruturaGetResponse200, -) - -__all__ = [ - "Error", - "ErrorResponse", - "ProdutosComponenteDTO", - "ProdutosEstruturaDTO", - "ProdutosEstruturasDeleteResponse200", - "ProdutosEstruturasIdProdutoEstruturaComponentesPostRequest", - "ProdutosEstruturasIdProdutoEstruturaGetResponse200", -] diff --git a/src/bling_erp_api/models/generated/resources/product_suppliers.py b/src/bling_erp_api/models/generated/resources/product_suppliers.py deleted file mode 100644 index 0e9ca8f..0000000 --- a/src/bling_erp_api/models/generated/resources/product_suppliers.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Generated model reexports for product_suppliers. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import BasePostResponse, ErrorResponse -from bling_erp_api.models.generated.schemas.produtos_fornecedores import ( - ProdutosFornecedoresDadosBaseDTO, - ProdutosFornecedoresDadosBaseUpdateDTO, - ProdutosFornecedoresDadosDTO, - ProdutosFornecedoresDadosUpdateDTO, - ProdutosFornecedoresGetResponse200, - ProdutosFornecedoresIdProdutoFornecedorGetResponse200, - ProdutosFornecedoresIdProdutoFornecedorPutRequest, - ProdutosFornecedoresIdProdutoFornecedorPutResponse200, - ProdutosFornecedoresPostRequest, - ProdutosFornecedoresPostResponse201, -) - -__all__ = [ - "BasePostResponse", - "ErrorResponse", - "ProdutosFornecedoresDadosBaseDTO", - "ProdutosFornecedoresDadosBaseUpdateDTO", - "ProdutosFornecedoresDadosDTO", - "ProdutosFornecedoresDadosUpdateDTO", - "ProdutosFornecedoresGetResponse200", - "ProdutosFornecedoresIdProdutoFornecedorGetResponse200", - "ProdutosFornecedoresIdProdutoFornecedorPutRequest", - "ProdutosFornecedoresIdProdutoFornecedorPutResponse200", - "ProdutosFornecedoresPostRequest", - "ProdutosFornecedoresPostResponse201", -] diff --git a/src/bling_erp_api/models/generated/resources/product_variations.py b/src/bling_erp_api/models/generated/resources/product_variations.py deleted file mode 100644 index 09a78a8..0000000 --- a/src/bling_erp_api/models/generated/resources/product_variations.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Generated model reexports for product_variations. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import BasePostResponse, ErrorResponse -from bling_erp_api.models.generated.schemas.produtos import ProdutosDadosDTO -from bling_erp_api.models.generated.schemas.produtos_variacoes import ( - ProdutosVariacoesAtributosGerarCombinacoesPostResponse200, - ProdutosVariacoesCombinacaoDadosDTO, - ProdutosVariacoesDadosAtributoDTO, - ProdutosVariacoesIdProdutoPaiAtributosPatchResponse200, - ProdutosVariacoesIdProdutoPaiGetResponse200, -) - -__all__ = [ - "BasePostResponse", - "ErrorResponse", - "ProdutosDadosDTO", - "ProdutosVariacoesAtributosGerarCombinacoesPostResponse200", - "ProdutosVariacoesCombinacaoDadosDTO", - "ProdutosVariacoesDadosAtributoDTO", - "ProdutosVariacoesIdProdutoPaiAtributosPatchResponse200", - "ProdutosVariacoesIdProdutoPaiGetResponse200", -] diff --git a/src/bling_erp_api/models/generated/resources/products.py b/src/bling_erp_api/models/generated/resources/products.py deleted file mode 100644 index 0a0ccee..0000000 --- a/src/bling_erp_api/models/generated/resources/products.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Generated model reexports for products. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import ErrorResponse -from bling_erp_api.models.generated.schemas.produtos import ( - ProdutosAlertasResponse, - ProdutosDadosBaseDTO, - ProdutosDadosDTO, - ProdutosDadosPatchDTO, - ProdutosDeleteResponse200, - ProdutosGetResponse200, - ProdutosIdProdutoGetResponse200, - ProdutosIdProdutoSituacoesPatchRequest, - ProdutosResponsePOSTPUT, - ProdutosSituacoesPostRequest, - ProdutosSituacoesPostResponse200, -) - -__all__ = [ - "ErrorResponse", - "ProdutosAlertasResponse", - "ProdutosDadosBaseDTO", - "ProdutosDadosDTO", - "ProdutosDadosPatchDTO", - "ProdutosDeleteResponse200", - "ProdutosGetResponse200", - "ProdutosIdProdutoGetResponse200", - "ProdutosIdProdutoSituacoesPatchRequest", - "ProdutosResponsePOSTPUT", - "ProdutosSituacoesPostRequest", - "ProdutosSituacoesPostResponse200", -] diff --git a/src/bling_erp_api/models/generated/resources/propostas_comerciais.py b/src/bling_erp_api/models/generated/resources/propostas_comerciais.py deleted file mode 100644 index 05f5433..0000000 --- a/src/bling_erp_api/models/generated/resources/propostas_comerciais.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Generated model reexports for propostas_comerciais. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import BasePostResponse, ErrorResponse -from bling_erp_api.models.generated.schemas.orcamentos import ( - OrcamentosDadosBaseDTO, - OrcamentosDadosDTO, - OrcamentosSituacaoDTO, -) -from bling_erp_api.models.generated.schemas.propostas_comerciais import ( - PropostasComerciaisDeleteResponse200, - PropostasComerciaisGetResponse200, - PropostasComerciaisIdPropostaComercialGetResponse200, - PropostasComerciaisIdPropostaComercialPutRequest, - PropostasComerciaisPostRequest, - PropostasComerciaisPostResponse201, -) - -__all__ = [ - "BasePostResponse", - "ErrorResponse", - "OrcamentosDadosBaseDTO", - "OrcamentosDadosDTO", - "OrcamentosSituacaoDTO", - "PropostasComerciaisDeleteResponse200", - "PropostasComerciaisGetResponse200", - "PropostasComerciaisIdPropostaComercialGetResponse200", - "PropostasComerciaisIdPropostaComercialPutRequest", - "PropostasComerciaisPostRequest", - "PropostasComerciaisPostResponse201", -] diff --git a/src/bling_erp_api/models/generated/resources/purchase_orders.py b/src/bling_erp_api/models/generated/resources/purchase_orders.py deleted file mode 100644 index e59dca4..0000000 --- a/src/bling_erp_api/models/generated/resources/purchase_orders.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Generated model reexports for purchase_orders. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import BasePostResponse, ErrorResponse -from bling_erp_api.models.generated.schemas.pedidos_compras import ( - PedidosCompraResponsePOSTPUT, - PedidosComprasDadosBaseDTO, - PedidosComprasDadosDTO, - PedidosComprasGetResponse200, - PedidosComprasIdPedidoCompraGetResponse200, - PedidosComprasIdPedidoCompraPutRequest, - PedidosComprasIdPedidoCompraPutResponse200, - PedidosComprasPostRequest, - PedidosComprasPostResponse201, -) - -__all__ = [ - "BasePostResponse", - "ErrorResponse", - "PedidosCompraResponsePOSTPUT", - "PedidosComprasDadosBaseDTO", - "PedidosComprasDadosDTO", - "PedidosComprasGetResponse200", - "PedidosComprasIdPedidoCompraGetResponse200", - "PedidosComprasIdPedidoCompraPutRequest", - "PedidosComprasIdPedidoCompraPutResponse200", - "PedidosComprasPostRequest", - "PedidosComprasPostResponse201", -] diff --git a/src/bling_erp_api/models/generated/resources/sales_orders.py b/src/bling_erp_api/models/generated/resources/sales_orders.py deleted file mode 100644 index ad3555a..0000000 --- a/src/bling_erp_api/models/generated/resources/sales_orders.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Generated model reexports for sales_orders. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import BasePostResponse, ErrorResponse -from bling_erp_api.models.generated.schemas.pedidos_vendas import ( - PedidosVendasDeleteResponse200, - PedidosVendasGetResponse200, - PedidosVendasIdPedidoVendaGetResponse200, - PedidosVendasIdPedidoVendaPutRequest, - PedidosVendasIdPedidoVendaPutResponse200, - PedidosVendasPostRequest, - PedidosVendasPostResponse201, - VendasCreateInvoiceResponseDTO, - VendasDadosBaseDTO, - VendasDadosBaseDTOPUT, - VendasDadosDTO, - VendasResponsePOSTPUT, -) - -__all__ = [ - "BasePostResponse", - "ErrorResponse", - "PedidosVendasDeleteResponse200", - "PedidosVendasGetResponse200", - "PedidosVendasIdPedidoVendaGetResponse200", - "PedidosVendasIdPedidoVendaPutRequest", - "PedidosVendasIdPedidoVendaPutResponse200", - "PedidosVendasPostRequest", - "PedidosVendasPostResponse201", - "VendasCreateInvoiceResponseDTO", - "VendasDadosBaseDTO", - "VendasDadosBaseDTOPUT", - "VendasDadosDTO", - "VendasResponsePOSTPUT", -] diff --git a/src/bling_erp_api/models/generated/resources/situacoes.py b/src/bling_erp_api/models/generated/resources/situacoes.py deleted file mode 100644 index 642efa5..0000000 --- a/src/bling_erp_api/models/generated/resources/situacoes.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Generated model reexports for situacoes. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import BasePostResponse, ErrorResponse -from bling_erp_api.models.generated.schemas.situacoes import ( - SituacoesDadosDTO, - SituacoesDTO, - SituacoesIdSituacaoGetResponse200, - SituacoesIdSituacaoPutRequest, - SituacoesIdSituacaoPutResponse200, - SituacoesPostRequest, - SituacoesPostResponse201, -) - -__all__ = [ - "BasePostResponse", - "ErrorResponse", - "SituacoesDTO", - "SituacoesDadosDTO", - "SituacoesIdSituacaoGetResponse200", - "SituacoesIdSituacaoPutRequest", - "SituacoesIdSituacaoPutResponse200", - "SituacoesPostRequest", - "SituacoesPostResponse201", -] diff --git a/src/bling_erp_api/models/generated/resources/situacoes_modulos.py b/src/bling_erp_api/models/generated/resources/situacoes_modulos.py deleted file mode 100644 index e07922c..0000000 --- a/src/bling_erp_api/models/generated/resources/situacoes_modulos.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Generated model reexports for situacoes_modulos. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import ErrorResponse -from bling_erp_api.models.generated.schemas.situacoes import ( - SituacoesAcaoDTO, - SituacoesDadosDTO, - SituacoesDTO, - SituacoesModuloBaseDTO, - SituacoesModuloDTO, - SituacoesModulosGetResponse200, - SituacoesModulosIdModuloSistemaAcoesGetResponse200, - SituacoesModulosIdModuloSistemaGetResponse200, - SituacoesModulosIdModuloSistemaTransicoesGetResponse200, - SituacoesTransicaoDTO, -) - -__all__ = [ - "ErrorResponse", - "SituacoesAcaoDTO", - "SituacoesDTO", - "SituacoesDadosDTO", - "SituacoesModuloBaseDTO", - "SituacoesModuloDTO", - "SituacoesModulosGetResponse200", - "SituacoesModulosIdModuloSistemaAcoesGetResponse200", - "SituacoesModulosIdModuloSistemaGetResponse200", - "SituacoesModulosIdModuloSistemaTransicoesGetResponse200", - "SituacoesTransicaoDTO", -] diff --git a/src/bling_erp_api/models/generated/resources/situacoes_transicoes.py b/src/bling_erp_api/models/generated/resources/situacoes_transicoes.py deleted file mode 100644 index 807a26c..0000000 --- a/src/bling_erp_api/models/generated/resources/situacoes_transicoes.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Generated model reexports for situacoes_transicoes. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import BasePostResponse, ErrorResponse -from bling_erp_api.models.generated.schemas.situacoes import ( - SituacoesTransicaoDTO, - SituacoesTransicoesIdTransicaoGetResponse200, - SituacoesTransicoesIdTransicaoPutRequest, - SituacoesTransicoesIdTransicaoPutResponse200, - SituacoesTransicoesPostRequest, - SituacoesTransicoesPostResponse201, -) - -__all__ = [ - "BasePostResponse", - "ErrorResponse", - "SituacoesTransicaoDTO", - "SituacoesTransicoesIdTransicaoGetResponse200", - "SituacoesTransicoesIdTransicaoPutRequest", - "SituacoesTransicoesIdTransicaoPutResponse200", - "SituacoesTransicoesPostRequest", - "SituacoesTransicoesPostResponse201", -] diff --git a/src/bling_erp_api/models/generated/resources/store_categories.py b/src/bling_erp_api/models/generated/resources/store_categories.py deleted file mode 100644 index 5d137aa..0000000 --- a/src/bling_erp_api/models/generated/resources/store_categories.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Generated model reexports for store_categories. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.categorias_lojas import ( - CategoriasLojasDadosDTO, - CategoriasLojasGetResponse200, - CategoriasLojasIdCategoriaLojaGetResponse200, - CategoriasLojasPostResponse201, -) -from bling_erp_api.models.generated.schemas.common import BasePostResponse, ErrorResponse - -__all__ = [ - "BasePostResponse", - "CategoriasLojasDadosDTO", - "CategoriasLojasGetResponse200", - "CategoriasLojasIdCategoriaLojaGetResponse200", - "CategoriasLojasPostResponse201", - "ErrorResponse", -] diff --git a/src/bling_erp_api/models/generated/resources/vendedores.py b/src/bling_erp_api/models/generated/resources/vendedores.py deleted file mode 100644 index a0ea0e6..0000000 --- a/src/bling_erp_api/models/generated/resources/vendedores.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Generated model reexports for vendedores. Do not edit manually.""" - -from __future__ import annotations - -from bling_erp_api.models.generated.schemas.common import ErrorResponse -from bling_erp_api.models.generated.schemas.vendedores import ( - VendedoresDadosBaseDTO, - VendedoresDadosDTO, - VendedoresGetResponse200, - VendedoresIdVendedorGetResponse200, -) - -__all__ = [ - "ErrorResponse", - "VendedoresDadosBaseDTO", - "VendedoresDadosDTO", - "VendedoresGetResponse200", - "VendedoresIdVendedorGetResponse200", -] diff --git a/src/bling_erp_api/models/generated/schemas/pedidos_vendas.py b/src/bling_erp_api/models/generated/schemas/pedidos_vendas.py index f953923..44fa44b 100644 --- a/src/bling_erp_api/models/generated/schemas/pedidos_vendas.py +++ b/src/bling_erp_api/models/generated/schemas/pedidos_vendas.py @@ -473,7 +473,7 @@ class VendasItemDTO(BlingModel): quando ele aparecer como request body ou response schema nos métodos do SDK. Fields: - id: Bling ``id``; type ``int``; obrigatório. Ignorado no método POST. + id: Bling ``id``; type ``int | None``; opcional. Ignorado no método POST. codigo: Bling ``codigo``; type ``str | None``; opcional. unidade: Bling ``unidade``; type ``str | None``; opcional. quantidade: Bling ``quantidade``; type ``float``; obrigatório. @@ -486,7 +486,7 @@ class VendasItemDTO(BlingModel): comissao: Bling ``comissao``; type ``VendasItemComissaoDTO | None``; opcional. natureza_operacao: Bling ``naturezaOperacao``; type ``VendasItemNaturezaOperacaoDTO | None``; opcional.""" - id: int = Field(..., examples=[12345678]) + id: int | None = Field(default=None, examples=[12345678]) codigo: str | None = Field(default=None, examples=["BLG-5"]) unidade: str | None = Field(default=None, examples=["UN"]) quantidade: float = Field(..., examples=[1]) @@ -521,14 +521,14 @@ class VendasParcelaDTO(BlingModel): quando ele aparecer como request body ou response schema nos métodos do SDK. Fields: - id: Bling ``id``; type ``int``; obrigatório. Ignorado no método POST. + id: Bling ``id``; type ``int | None``; opcional. Ignorado no método POST. data_vencimento: Bling ``dataVencimento``; type ``BlingDate``; obrigatório. valor: Bling ``valor``; type ``float``; obrigatório. observacoes: Bling ``observacoes``; type ``str | None``; opcional. caut: Bling ``caut``; type ``str | None``; opcional. cAut (ou NSU): código de autorização da operação financeira forma_pagamento: Bling ``formaPagamento``; type ``VendasParcelaFormaPagamentoDTO``; obrigatório.""" - id: int = Field(..., examples=[12345678]) + id: int | None = Field(default=None, examples=[12345678]) data_vencimento: BlingDate = Field( ..., validation_alias=AliasChoices("data_vencimento", "dataVencimento"), diff --git a/src/bling_erp_api/models/generated/schemas/produtos.py b/src/bling_erp_api/models/generated/schemas/produtos.py index 4672aba..5d17f71 100644 --- a/src/bling_erp_api/models/generated/schemas/produtos.py +++ b/src/bling_erp_api/models/generated/schemas/produtos.py @@ -288,24 +288,24 @@ class ProdutosImagemInternaDTO(BlingModel): Fields: link: Bling ``link``; type ``str``; obrigatório. - link_miniatura: Bling ``linkMiniatura``; type ``str``; obrigatório. - validade: Bling ``validade``; type ``str``; obrigatório. - ordem: Bling ``ordem``; type ``int``; obrigatório. - anexo: Bling ``anexo``; type ``ProdutosAnexoDTO``; obrigatório. - anexo_vinculo: Bling ``anexoVinculo``; type ``ProdutosAnexoVinculoDTO``; obrigatório.""" + link_miniatura: Bling ``linkMiniatura``; type ``str | None``; opcional. + validade: Bling ``validade``; type ``str | None``; opcional. + ordem: Bling ``ordem``; type ``int | None``; opcional. + anexo: Bling ``anexo``; type ``ProdutosAnexoDTO | None``; opcional. + anexo_vinculo: Bling ``anexoVinculo``; type ``ProdutosAnexoVinculoDTO | None``; opcional.""" link: str = Field(..., examples=["https://www.bling.com.br/imagens/miniatura.jpg"]) - link_miniatura: str = Field( - ..., + link_miniatura: str | None = Field( + default=None, validation_alias=AliasChoices("link_miniatura", "linkMiniatura"), examples=["https://www.bling.com.br/imagens/miniatura.jpg"], serialization_alias="linkMiniatura", ) - validade: str = Field(..., examples=["2020-01-01 00:00:00"]) - ordem: int = Field(..., examples=[1]) - anexo: ProdutosAnexoDTO - anexo_vinculo: ProdutosAnexoVinculoDTO = Field( - ..., + validade: str | None = Field(default=None, examples=["2020-01-01 00:00:00"]) + ordem: int | None = Field(default=None, examples=[1]) + anexo: ProdutosAnexoDTO | None = None + anexo_vinculo: ProdutosAnexoVinculoDTO | None = Field( + default=None, validation_alias=AliasChoices("anexo_vinculo", "anexoVinculo"), serialization_alias="anexoVinculo", ) diff --git a/src/bling_erp_api/models/generated/usuarios.py b/src/bling_erp_api/models/generated/usuarios.py new file mode 100644 index 0000000..c50ad24 --- /dev/null +++ b/src/bling_erp_api/models/generated/usuarios.py @@ -0,0 +1,3 @@ +"""Generated model reexports for usuarios. Do not edit manually.""" + +__all__ = [] diff --git a/src/bling_erp_api/resources/sales_orders.py b/src/bling_erp_api/resources/sales_orders.py index 4e18d7e..05d7b95 100644 --- a/src/bling_erp_api/resources/sales_orders.py +++ b/src/bling_erp_api/resources/sales_orders.py @@ -449,17 +449,17 @@ def delete(self, order_id: int) -> JsonObject: return self.remover(order_id) def remover_varios(self, ids_pedidos_vendas: Sequence[int]) -> JsonObject: - """Remove vários pedidos de venda. + """Remove pedidos de vendas. Endpoint: DELETE /pedidos/vendas - Remove vários pedidos de venda pelos IDs. + Remove pedidos de vendas pelos IDs. Args: - ids_pedidos_vendas: IDs dos pedidos de venda (Bling: ``idsPedidosVendas[]``, array, obrigatório) + ids_pedidos_vendas: IDs dos pedidos de vendas (Bling: ``idsPedidosVendas[]``, array, obrigatório) Returns: - Bling API response. Response schemas: 400: ErrorResponse; 404: ErrorResponse + Bling API response. Response schemas: 200: PedidosVendasDeleteResponse200; 204: NoContent """ return self._delete( "/pedidos/vendas", params={"idsPedidosVendas[]": list(ids_pedidos_vendas)} @@ -468,17 +468,17 @@ def remover_varios(self, ids_pedidos_vendas: Sequence[int]) -> JsonObject: def delete_many(self, order_ids: Sequence[int]) -> JsonObject: """Compatibility alias for ``remover_varios()``. - Remove vários pedidos de venda. + Remove pedidos de vendas. Endpoint: DELETE /pedidos/vendas - Remove vários pedidos de venda pelos IDs. + Remove pedidos de vendas pelos IDs. Args: - order_ids: IDs dos pedidos de venda (Bling: ``idsPedidosVendas[]``, array, obrigatório) + order_ids: IDs dos pedidos de vendas (Bling: ``idsPedidosVendas[]``, array, obrigatório) Returns: - Bling API response. Response schemas: 400: ErrorResponse; 404: ErrorResponse + Bling API response. Response schemas: 200: PedidosVendasDeleteResponse200; 204: NoContent """ return self.remover_varios(order_ids) diff --git a/src/bling_erp_api/types.py b/src/bling_erp_api/types.py index 9964d88..54a38e3 100644 --- a/src/bling_erp_api/types.py +++ b/src/bling_erp_api/types.py @@ -4,7 +4,7 @@ from collections.abc import Mapping, Sequence -type JsonValue = None | bool | int | float | str | list["JsonValue"] | dict[str, "JsonValue"] +type JsonValue = bool | int | float | str | list["JsonValue"] | dict[str, "JsonValue"] | None type JsonObject = dict[str, JsonValue] type JsonPayload = JsonObject | Sequence[Mapping[str, JsonValue]] type QueryParamPrimitive = str | int | float | bool diff --git a/tests/fixtures/responses/product_obter.json b/tests/fixtures/responses/product_obter.json new file mode 100644 index 0000000..f949c02 --- /dev/null +++ b/tests/fixtures/responses/product_obter.json @@ -0,0 +1,50 @@ +{ + "data": { + "id": 123456789, + "nome": "Caneta Esferográfica Azul 1.0mm", + "codigo": "CAN-1001", + "preco": 2.5, + "tipo": "P", + "situacao": "A", + "formato": "S", + "descricaoCurta": "Caneta esferográfica azul, ponta 1.0mm", + "unidade": "Un", + "pesoLiquido": 0.01, + "pesoBruto": 0.02, + "imagemURL": "https://cdn.exemplo.com.br/imagens/can-1001.jpg", + "midia": { + "video": { + "url": "https://www.youtube.com/watch?v=exemplo123" + }, + "imagens": { + "imagensURL": [ + { + "link": "https://cdn.exemplo.com.br/imagens/can-1001.jpg" + } + ], + "externas": [ + { + "link": "https://i.ibb.co/exemplo123/caneta-azul.jpg" + } + ], + "internas": [ + { + "link": "https://cdn.exemplo.com.br/bling/imagens/123456789-a.jpg" + }, + { + "link": "https://cdn.exemplo.com.br/bling/imagens/123456789-b.jpg", + "linkMiniatura": "https://cdn.exemplo.com.br/bling/imagens/miniatura/123456789-b.jpg", + "validade": "2027-12-31 00:00:00", + "ordem": 2, + "anexo": { + "id": 111222333 + }, + "anexoVinculo": { + "id": 444555666 + } + } + ] + } + } + } +} diff --git a/tests/fixtures/responses/sales_order_get_minimal.json b/tests/fixtures/responses/sales_order_get_minimal.json new file mode 100644 index 0000000..4e77a98 --- /dev/null +++ b/tests/fixtures/responses/sales_order_get_minimal.json @@ -0,0 +1,35 @@ +{ + "data": { + "id": 12345678, + "numero": 42, + "data": "2024-01-10", + "dataSaida": "2024-01-10", + "dataPrevista": "2024-01-12", + "totalProdutos": 21.00, + "total": 23.50, + "contato": { + "id": 87654321, + "nome": "Cliente Teste" + }, + "situacao": { + "id": 111, + "valor": 1 + }, + "itens": [ + { + "quantidade": 2.0, + "valor": 10.50, + "descricao": "Produto Teste" + } + ], + "parcelas": [ + { + "dataVencimento": "2024-01-20", + "valor": 23.50, + "formaPagamento": { + "id": 666 + } + } + ] + } +} diff --git a/tests/unit/test_bling_date.py b/tests/unit/test_bling_date.py index 819ab7e..15b3100 100644 --- a/tests/unit/test_bling_date.py +++ b/tests/unit/test_bling_date.py @@ -2,6 +2,7 @@ from __future__ import annotations +import warnings from datetime import date import pytest @@ -95,6 +96,22 @@ def test_valid_date_serializes_as_iso_string(self) -> None: dumped = model.model_dump(mode="json") assert dumped["value"] == "2020-01-01" + def test_json_dump_does_not_warn(self) -> None: + """JSON serialization should be warning-free thanks to the explicit serializer.""" + model = ModelWithOptionalDate(value=date(2020, 1, 1)) + with warnings.catch_warnings(): + warnings.simplefilter("error") + dumped = model.model_dump(mode="json") + assert dumped["value"] == "2020-01-01" + + def test_none_serializes_as_none(self) -> None: + """None should serialize as null in JSON mode without warnings.""" + model = ModelWithOptionalDate(value=None) + with warnings.catch_warnings(): + warnings.simplefilter("error") + dumped = model.model_dump(mode="json") + assert dumped["value"] is None + def test_none_excluded_when_exclude_none(self) -> None: """None should be excluded from JSON output with exclude_none=True.""" model = ModelWithOptionalDate(value=None) @@ -106,3 +123,15 @@ def test_none_included_when_not_excluded(self) -> None: model = ModelWithOptionalDate(value=None) dumped = model.model_dump(mode="json") assert dumped["value"] is None + + def test_sentinel_stored_as_none_after_validation(self) -> None: + """Validation behavior is unchanged: sentinel input is still stored as None.""" + model = ModelWithOptionalDate(value="0000-00-00") # type: ignore[reportArgumentType] + assert model.value is None + + def test_python_mode_dump_returns_date(self) -> None: + """Python-mode dump should still yield ``datetime.date`` instances.""" + model = ModelWithOptionalDate(value=date(2020, 1, 1)) + dumped = model.model_dump() + assert dumped["value"] is model.value + assert type(dumped["value"]) is date diff --git a/tests/unit/test_generate_models_overrides.py b/tests/unit/test_generate_models_overrides.py new file mode 100644 index 0000000..8e9202d --- /dev/null +++ b/tests/unit/test_generate_models_overrides.py @@ -0,0 +1,477 @@ +"""Tests for the field-override guard in ``scripts/generate_models.py``.""" + +from __future__ import annotations + +import ast +import importlib.util +import re +import sys +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest + +if TYPE_CHECKING: + from collections.abc import Iterator + from types import ModuleType + +# Use importlib to load the generator scripts since scripts/ has no __init__.py +# and uv run pytest does not make it importable as a package. The contracts +# script is registered in sys.modules so generate_models' bare import resolves. +_scripts_dir = Path(__file__).resolve().parents[2] / "scripts" +_contracts_path = _scripts_dir / "generate_openapi_contracts.py" +_contracts_spec = importlib.util.spec_from_file_location( + "generate_openapi_contracts", _contracts_path +) +assert _contracts_spec is not None, f"Could not load spec from {_contracts_path}" +assert _contracts_spec.loader is not None, f"Loader is None for spec from {_contracts_path}" +_contracts_module = importlib.util.module_from_spec(_contracts_spec) +sys.modules[_contracts_spec.name] = _contracts_module +_contracts_spec.loader.exec_module(_contracts_module) + +_gen_path = _scripts_dir / "generate_models.py" +_gen_spec = importlib.util.spec_from_file_location("generate_models", _gen_path) +assert _gen_spec is not None, f"Could not load spec from {_gen_path}" +assert _gen_spec.loader is not None, f"Loader is None for spec from {_gen_path}" +_gen_module = importlib.util.module_from_spec(_gen_spec) +sys.modules[_gen_spec.name] = _gen_module +_gen_spec.loader.exec_module(_gen_module) + + +@pytest.fixture +def override_guard() -> Iterator[ModuleType]: + """Snapshot the guard's module state and restore it after each test.""" + module = _gen_module + saved_applied = set(module._APPLIED_FIELD_OVERRIDES) # noqa: SLF001 + saved_annotations = dict(module.FIELD_ANNOTATION_OVERRIDES) + saved_defaults = set(module.FIELD_NONE_DEFAULT_OVERRIDES) + module._APPLIED_FIELD_OVERRIDES.clear() # noqa: SLF001 + try: + yield module + finally: + module._APPLIED_FIELD_OVERRIDES.clear() # noqa: SLF001 + module._APPLIED_FIELD_OVERRIDES.update(saved_applied) # noqa: SLF001 + module.FIELD_ANNOTATION_OVERRIDES.clear() + module.FIELD_ANNOTATION_OVERRIDES.update(saved_annotations) + module.FIELD_NONE_DEFAULT_OVERRIDES.clear() + module.FIELD_NONE_DEFAULT_OVERRIDES.update(saved_defaults) + + +def _applied_for_current_overrides(module: ModuleType) -> set[tuple[str, str]]: + annotations = module.FIELD_ANNOTATION_OVERRIDES + defaults = module.FIELD_NONE_DEFAULT_OVERRIDES + return set(annotations) | set(defaults) + + +def _call_name(func: ast.expr) -> str: + """Return the bare name of a ``Name`` function expression.""" + assert isinstance(func, ast.Name) + return func.id + + +_PARENT_WITH_FIELD_SOURCE = """\ +class ParentDTO(BlingModel): + x: str = Field( + default="...", + validation_alias=AliasChoices("x", "xStr"), + serialization_alias="xStr", + ) + + +class ChildDTO(ParentDTO): + pass +""" + + +_PARENT_RAW_ALIAS_FIELD_SOURCE = """\ +class ParentDTO(BlingModel): + x: str = Field(default="...", alias="xStr") + + +class ChildDTO(ParentDTO): + pass +""" + + +_PARENT_DUAL_ALIAS_FIELD_SOURCE = """\ +class ParentDTO(BlingModel): + x: str = Field( + default="...", + alias="xStr", + validation_alias=AliasChoices("x", "xStr"), + ) + + +class ChildDTO(ParentDTO): + pass +""" + + +def _parent_child_classes() -> tuple[ast.ClassDef, ast.ClassDef]: + """Build a parent class carrying ``Field`` metadata and a child body without ``x``.""" + classes = { + stmt.name: stmt + for stmt in ast.parse(_PARENT_WITH_FIELD_SOURCE).body + if isinstance(stmt, ast.ClassDef) + } + return classes["ParentDTO"], classes["ChildDTO"] + + +def _child_x_fields(node: ast.ClassDef) -> list[ast.AnnAssign]: + """Return the ``AnnAssign`` statements for field ``x`` in the class body.""" + return [ + stmt + for stmt in node.body + if isinstance(stmt, ast.AnnAssign) + and isinstance(stmt.target, ast.Name) + and stmt.target.id == "x" + ] + + +def _assert_x_field_metadata(inserted: ast.AnnAssign) -> None: + """Assert the inserted field redeclares ``x`` keeping the parent metadata.""" + value = inserted.value + assert isinstance(value, ast.Call) + assert _call_name(value.func) == "Field" + keywords = {keyword.arg: keyword.value for keyword in value.keywords} + default = keywords["default"] + assert isinstance(default, ast.Constant) + assert default.value is None + alias_call = keywords["validation_alias"] + assert isinstance(alias_call, ast.Call) + assert _call_name(alias_call.func) == "AliasChoices" + assert [arg.value for arg in alias_call.args if isinstance(arg, ast.Constant)] == ["x", "xStr"] + serialization_alias = keywords["serialization_alias"] + assert isinstance(serialization_alias, ast.Constant) + assert serialization_alias.value == "xStr" + + +def test_check_passes_when_all_override_keys_applied(override_guard: ModuleType) -> None: + """Every key applied means the guard returns without exiting.""" + module = override_guard + module._APPLIED_FIELD_OVERRIDES.update( # noqa: SLF001 + _applied_for_current_overrides(module) + ) + + module._check_field_overrides_applied() # noqa: SLF001 + + +def test_check_fails_on_dead_class_key(override_guard: ModuleType) -> None: + """A bogus class key exits listing dead keys sorted, naming both tables.""" + module = override_guard + module.FIELD_ANNOTATION_OVERRIDES[("ClasseBInexistenteDTO", "campo")] = "str | None" + module.FIELD_ANNOTATION_OVERRIDES[("ClasseAInexistenteDTO", "campo")] = "str | None" + applied = _applied_for_current_overrides(module) + applied -= {("ClasseAInexistenteDTO", "campo"), ("ClasseBInexistenteDTO", "campo")} + module._APPLIED_FIELD_OVERRIDES.update(applied) # noqa: SLF001 + + with pytest.raises(SystemExit) as excinfo: + module._check_field_overrides_applied() # noqa: SLF001 + + message = str(excinfo.value) + assert "ClasseAInexistenteDTO.campo" in message + assert "ClasseBInexistenteDTO.campo" in message + assert message.index("ClasseAInexistenteDTO.campo") < message.index( + "ClasseBInexistenteDTO.campo" + ) + assert "FIELD_ANNOTATION_OVERRIDES" in message + assert "FIELD_NONE_DEFAULT_OVERRIDES" in message + + +def test_check_fails_on_dead_field_key(override_guard: ModuleType) -> None: + """A field key absent from the applied set exits listing ``Class.field``.""" + module = override_guard + dead_key = ("ProdutosImagemInternaDTO", "link_miniatura") + applied = _applied_for_current_overrides(module) + applied.discard(dead_key) + module._APPLIED_FIELD_OVERRIDES.update(applied) # noqa: SLF001 + + with pytest.raises(SystemExit) as excinfo: + module._check_field_overrides_applied() # noqa: SLF001 + + assert "ProdutosImagemInternaDTO.link_miniatura" in str(excinfo.value) + + +def test_apply_records_body_match_and_skips_field_absent_from_parents( + override_guard: ModuleType, +) -> None: + """Body-matched fields are recorded; body/parent-absent fields are not inserted.""" + module = override_guard + module.FIELD_ANNOTATION_OVERRIDES.clear() + module.FIELD_ANNOTATION_OVERRIDES.update( + { + ("ExemploDTO", "campo_morto"): "str | None", + ("ExemploDTO", "campo_vivo"): "str | None", + } + ) + node = ast.parse("class ExemploDTO(BlingModel):\n campo_vivo: str\n").body[0] + assert isinstance(node, ast.ClassDef) + + module._apply_field_overrides("ExemploDTO", node, class_nodes={}) # noqa: SLF001 + + applied = module._APPLIED_FIELD_OVERRIDES # noqa: SLF001 + assert ("ExemploDTO", "campo_vivo") in applied + assert ("ExemploDTO", "campo_morto") not in applied + field_names = [ + stmt.target.id + for stmt in node.body + if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name) + ] + assert field_names == ["campo_vivo"] + + +def test_apply_inserts_once_with_parent_metadata_for_both_tables_key( + override_guard: ModuleType, +) -> None: + """A both-tables key inserts one field keeping the parent Field metadata.""" + module = override_guard + module.FIELD_ANNOTATION_OVERRIDES[("ChildDTO", "x")] = "str | None" + module.FIELD_NONE_DEFAULT_OVERRIDES.add(("ChildDTO", "x")) + parent, child = _parent_child_classes() + + module._apply_field_overrides( # noqa: SLF001 + "ChildDTO", + child, + class_nodes={"ParentDTO": parent}, + ) + + fields = _child_x_fields(child) + assert len(fields) == 1 + assert ast.unparse(fields[0].annotation) == "str | None" + _assert_x_field_metadata(fields[0]) + assert ("ChildDTO", "x") in module._APPLIED_FIELD_OVERRIDES # noqa: SLF001 + + +def test_apply_inserts_annotation_only_key_with_parent_metadata( + override_guard: ModuleType, +) -> None: + """An annotation-only key inserts the field with the override annotation.""" + module = override_guard + module.FIELD_ANNOTATION_OVERRIDES[("ChildDTO", "x")] = "int | None" + parent, child = _parent_child_classes() + + module._apply_field_overrides( # noqa: SLF001 + "ChildDTO", + child, + class_nodes={"ParentDTO": parent}, + ) + + fields = _child_x_fields(child) + assert len(fields) == 1 + assert ast.unparse(fields[0].annotation) == "int | None" + _assert_x_field_metadata(fields[0]) + assert ("ChildDTO", "x") in module._APPLIED_FIELD_OVERRIDES # noqa: SLF001 + + +def test_apply_inserts_default_only_key_with_parent_metadata( + override_guard: ModuleType, +) -> None: + """A default-only key inserts the field annotated as the parent type ``| None``.""" + module = override_guard + module.FIELD_NONE_DEFAULT_OVERRIDES.add(("ChildDTO", "x")) + parent, child = _parent_child_classes() + + module._apply_field_overrides( # noqa: SLF001 + "ChildDTO", + child, + class_nodes={"ParentDTO": parent}, + ) + + fields = _child_x_fields(child) + assert len(fields) == 1 + assert ast.unparse(fields[0].annotation) == "str | None" + _assert_x_field_metadata(fields[0]) + assert ("ChildDTO", "x") in module._APPLIED_FIELD_OVERRIDES # noqa: SLF001 + + +def test_apply_inserts_default_only_key_reusing_optional_parent_annotation( + override_guard: ModuleType, +) -> None: + """A default-only key reuses an already-optional parent annotation verbatim. + + The parent ``str | None`` annotation is inserted as-is instead of building + the degenerate ``str | None | None`` union. + """ + module = override_guard + module.FIELD_NONE_DEFAULT_OVERRIDES.add(("ChildDTO", "x")) + parent = ast.parse("class ParentDTO(BlingModel):\n x: str | None\n").body[0] + child = ast.parse("class ChildDTO(ParentDTO):\n pass\n").body[0] + assert isinstance(parent, ast.ClassDef) + assert isinstance(child, ast.ClassDef) + + module._apply_field_overrides( # noqa: SLF001 + "ChildDTO", + child, + class_nodes={"ParentDTO": parent}, + ) + + fields = _child_x_fields(child) + assert len(fields) == 1 + assert ast.unparse(fields[0].annotation) == "str | None" + assert isinstance(fields[0].value, ast.Constant) + assert fields[0].value.value is None + assert ("ChildDTO", "x") in module._APPLIED_FIELD_OVERRIDES # noqa: SLF001 + + +def test_apply_inserts_field_exactly_once_for_both_tables_key( + override_guard: ModuleType, +) -> None: + """A both-tables key inserts the field exactly once (dedup regression pin).""" + module = override_guard + module.FIELD_ANNOTATION_OVERRIDES[("ChildDTO", "x")] = "str | None" + module.FIELD_NONE_DEFAULT_OVERRIDES.add(("ChildDTO", "x")) + parent = ast.parse("class ParentDTO(BlingModel):\n x: str\n").body[0] + child = ast.parse("class ChildDTO(ParentDTO):\n pass\n").body[0] + assert isinstance(parent, ast.ClassDef) + assert isinstance(child, ast.ClassDef) + + module._apply_field_overrides( # noqa: SLF001 + "ChildDTO", + child, + class_nodes={"ParentDTO": parent}, + ) + + field_names = [ + stmt.target.id + for stmt in child.body + if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name) + ] + assert field_names == ["x"] + inserted = _child_x_fields(child)[0] + assert ast.unparse(inserted.annotation) == "str | None" + assert isinstance(inserted.value, ast.Constant) + assert inserted.value.value is None + assert ("ChildDTO", "x") in module._APPLIED_FIELD_OVERRIDES # noqa: SLF001 + + +def _classes_from_source(source: str) -> dict[str, ast.ClassDef]: + """Parse class definitions from ``source`` keyed by class name.""" + return {stmt.name: stmt for stmt in ast.parse(source).body if isinstance(stmt, ast.ClassDef)} + + +def test_apply_normalizes_raw_parent_alias_in_inserted_field( + override_guard: ModuleType, +) -> None: + """A raw ``alias=`` parent (the real datamodel-codegen shape) emits the normalized pair. + + Raw output carries plain ``alias="xStr"``; the inserted redeclaration must + spell ``validation_alias=AliasChoices(...)`` + ``serialization_alias`` so + ``to_json_object()`` (``by_alias=True``) serializes the Bling key. + """ + module = override_guard + module.FIELD_ANNOTATION_OVERRIDES[("ChildDTO", "x")] = "str | None" + classes = _classes_from_source(_PARENT_RAW_ALIAS_FIELD_SOURCE) + + module._apply_field_overrides( # noqa: SLF001 + "ChildDTO", + classes["ChildDTO"], + class_nodes={"ParentDTO": classes["ParentDTO"]}, + ) + + fields = _child_x_fields(classes["ChildDTO"]) + assert len(fields) == 1 + _assert_x_field_metadata(fields[0]) + value = fields[0].value + assert isinstance(value, ast.Call) + assert {keyword.arg for keyword in value.keywords} == { + "default", + "validation_alias", + "serialization_alias", + } + emitted = ast.unparse(fields[0]) + assert "validation_alias=AliasChoices('x', 'xStr')" in emitted + assert "serialization_alias='xStr'" in emitted + + +def test_apply_drops_raw_alias_when_parent_already_normalized( + override_guard: ModuleType, +) -> None: + """A parent carrying raw ``alias`` + ``validation_alias`` emits no duplicates. + + Datamodel-codegen never emits this shape; it is pinned defensively because + rewriting the stray raw ``alias`` would append a second ``validation_alias`` + kwarg (a SyntaxError when unparsed). The clone is treated as already + normalized: the raw ``alias`` is dropped and everything else is kept. + """ + module = override_guard + module.FIELD_ANNOTATION_OVERRIDES[("ChildDTO", "x")] = "str | None" + classes = _classes_from_source(_PARENT_DUAL_ALIAS_FIELD_SOURCE) + + module._apply_field_overrides( # noqa: SLF001 + "ChildDTO", + classes["ChildDTO"], + class_nodes={"ParentDTO": classes["ParentDTO"]}, + ) + + fields = _child_x_fields(classes["ChildDTO"]) + assert len(fields) == 1 + value = fields[0].value + assert isinstance(value, ast.Call) + assert _call_name(value.func) == "Field" + keyword_args = [keyword.arg for keyword in value.keywords] + assert keyword_args.count("validation_alias") == 1 + assert "alias" not in keyword_args + assert set(keyword_args) == {"default", "validation_alias"} + # The emitted call must re-parse: repeated keywords are a SyntaxError. + ast.parse(ast.unparse(fields[0])) + + +def test_apply_falls_back_to_bare_none_for_alias_less_parent_field( + override_guard: ModuleType, +) -> None: + """A parent Field call without alias keywords still inserts a bare ``None``. + + Schema-documentation-only kwargs (``examples``, ``description``, …) do not + justify cloning the parent call; this pins the byte-identical fallback. + """ + module = override_guard + module.FIELD_ANNOTATION_OVERRIDES[("ChildDTO", "x")] = "str | None" + classes = _classes_from_source( + """\ +class ParentDTO(BlingModel): + x: str = Field(default="...", examples=["demo"]) + + +class ChildDTO(ParentDTO): + pass +""" + ) + + module._apply_field_overrides( # noqa: SLF001 + "ChildDTO", + classes["ChildDTO"], + class_nodes={"ParentDTO": classes["ParentDTO"]}, + ) + + fields = _child_x_fields(classes["ChildDTO"]) + assert len(fields) == 1 + assert isinstance(fields[0].value, ast.Constant) + assert fields[0].value.value is None + + +def test_schema_module_content_imports_alias_choices_for_inserted_field( + override_guard: ModuleType, +) -> None: + """End-to-end: the rendered module carries the ``AliasChoices`` import. + + Renders parent + child through ``_schema_module_content`` so the inserted + field flows through the full pipeline; the normalized form must survive + into the emitted module next to the hardcoded pydantic import line. + """ + module = override_guard + module.FIELD_ANNOTATION_OVERRIDES[("ChildDTO", "x")] = "str | None" + class_nodes = _classes_from_source(_PARENT_RAW_ALIAS_FIELD_SOURCE) + + content = module._schema_module_content( # noqa: SLF001 + "exemplo", + ["ParentDTO", "ChildDTO"], + class_nodes, + {"ParentDTO": "exemplo", "ChildDTO": "exemplo"}, + ) + + assert "from pydantic import AliasChoices, AwareDatetime, Field, RootModel" in content + assert "validation_alias=AliasChoices('x', 'xStr')" in content + assert "serialization_alias='xStr'" in content + # ``alias='xStr'`` is a substring of ``serialization_alias='xStr'``; the + # lookbehind only matches a bare raw ``alias`` keyword surviving the run. + assert re.search(r"(? dict[str, Any]: + return json.loads((FIXTURES_DIR / name).read_text()) + + +def _load_product_imagens() -> ProdutosImagensDTO: + """Load the product fixture and return the parsed ``midia.imagens`` node.""" + data = _load_fixture("product_obter.json") + response = ProdutosIdProdutoGetResponse200(**data) + item = response.data + assert item is not None + midia = item.midia + assert midia is not None + imagens = midia.imagens + assert imagens is not None + return imagens + + +class TestProdutosModels: + """Tests for Produtos models deserialization. + + Bug 6: real ``GET /produtos/{idProduto}`` responses often carry + ``midia.imagens.internas[]`` entries with only ``link`` populated, so every + field of ``ProdutosImagemInternaDTO`` except ``link`` must be optional. + """ + + def test_deserialize_get_with_link_only_internal_image(self) -> None: + """Internal image with only ``link`` parses with the other fields as None.""" + imagens = _load_product_imagens() + internas = imagens.internas + assert internas is not None + link_only = internas[0] + assert link_only.link == "https://cdn.exemplo.com.br/bling/imagens/123456789-a.jpg" + assert link_only.link_miniatura is None + assert link_only.validade is None + assert link_only.ordem is None + assert link_only.anexo is None + assert link_only.anexo_vinculo is None + + def test_internal_image_requires_link(self) -> None: + """An internal image entry without ``link`` must fail validation (link stays required).""" + with pytest.raises(ValidationError, match="Field required"): + ProdutosImagemInternaDTO.model_validate({}) + + def test_deserialize_get_with_complete_internal_image(self) -> None: + """Internal image with all fields parses fully populated.""" + imagens = _load_product_imagens() + internas = imagens.internas + assert internas is not None + complete = internas[1] + assert complete.link == "https://cdn.exemplo.com.br/bling/imagens/123456789-b.jpg" + assert ( + complete.link_miniatura + == "https://cdn.exemplo.com.br/bling/imagens/miniatura/123456789-b.jpg" + ) + assert complete.validade == "2027-12-31 00:00:00" + assert complete.ordem == 2 + assert complete.anexo is not None + assert complete.anexo.id == 111222333 + assert complete.anexo_vinculo is not None + assert complete.anexo_vinculo.id == 444555666 + + def test_deserialize_get_with_externas_image(self) -> None: + """Externas entries (``ProdutosImagemDTO``) parse with only ``link``.""" + imagens = _load_product_imagens() + externas = imagens.externas + assert externas is not None + assert externas[0].link == "https://i.ibb.co/exemplo123/caneta-azul.jpg" + + def test_deserialize_get_with_imagens_url_entry(self) -> None: + """The link-only ``imagensURL`` entry (``ProdutosImagemDTO``) parses.""" + imagens = _load_product_imagens() + imagens_url = imagens.imagens_url + assert imagens_url is not None + assert len(imagens_url) == 1 + assert imagens_url[0].link == "https://cdn.exemplo.com.br/imagens/can-1001.jpg" diff --git a/tests/unit/test_resources.py b/tests/unit/test_resources.py index 646c86d..935279c 100644 --- a/tests/unit/test_resources.py +++ b/tests/unit/test_resources.py @@ -156,6 +156,31 @@ def request( return self.payload +class QueuedPayloadTransport(RecordingTransport): + """Transport test double that pops queued payloads per request.""" + + def __init__(self, payloads: list[JsonObject]) -> None: + """Create a recorder with a queue of response payloads.""" + super().__init__() + self.payloads = list(payloads) + + def request( + self, + method: str, + path: str, + *, + params: QueryParams | None = None, + json: JsonPayload | None = None, + headers: dict[str, str] | None = None, + ) -> JsonObject: + """Record a request and return the next queued payload.""" + self.calls.append((method, path, params, json)) + self.header_calls.append(headers) + if self.payloads: + return self.payloads.pop(0) + return {"data": []} + + class TestPathValidation: """Tests for URL path traversal validation in BaseResource.""" @@ -501,6 +526,126 @@ def test_sales_orders_posting_operations_map_to_bling_endpoints() -> None: ] +def test_sales_orders_obter_maps_to_bling_endpoint() -> None: + """Sales order retrieval should map to the GET by ID endpoint.""" + transport = RecordingTransport() + resource = SalesOrdersResource(transport) + + resource.obter(123) + + assert transport.calls == [("GET", "/pedidos/vendas/123", None, None)] + + +def test_sales_orders_english_get_alias_still_maps_to_bling_endpoint() -> None: + """English alias should keep compatibility while Portuguese is canonical.""" + transport = RecordingTransport() + resource = SalesOrdersResource(transport) + + resource.get(order_id=123) + + assert transport.calls == [("GET", "/pedidos/vendas/123", None, None)] + + +def test_sales_orders_iterar_yields_pages_until_empty() -> None: + """Sales order iteration should fetch pages until the Bling envelope empties.""" + transport = QueuedPayloadTransport( + [ + { + "data": [ + { + "id": 123, + "numero": 1001, + "data": "2024-01-10", + "dataSaida": "2024-01-10", + "dataPrevista": "2024-01-12", + "contato": {"id": 456, "nome": "Ana"}, + } + ] + }, + {"data": []}, + ] + ) + resource = SalesOrdersResource(transport) + + first_record = next(resource.iterar(pagina=2, limite=50, id_contato=456)) + + assert first_record == { + "id": 123, + "numero": 1001, + "numero_loja": None, + "data": "2024-01-10", + "data_saida": "2024-01-10", + "data_prevista": "2024-01-12", + "total_produtos": None, + "total": None, + "contato": { + "id": 456, + "nome": "Ana", + "tipo_pessoa": None, + "numero_documento": None, + }, + "situacao": None, + "loja": None, + } + assert transport.calls == [ + ("GET", "/pedidos/vendas", {"pagina": 2, "limite": 50, "idContato": 456}, None), + ] + + +def test_sales_orders_english_iterate_alias_still_paginates() -> None: + """English alias should keep compatibility while Portuguese is canonical.""" + transport = QueuedPayloadTransport([{"data": []}]) + resource = SalesOrdersResource(transport) + + assert list(resource.iterate(page=2, limit=50, contact_id=456)) == [] + + assert transport.calls == [ + ("GET", "/pedidos/vendas", {"pagina": 2, "limite": 50, "idContato": 456}, None) + ] + + +def test_sales_orders_english_write_aliases_map_to_canonical_operations() -> None: + """English write aliases should delegate to their canonical Portuguese methods.""" + transport = RecordingTransport() + resource = SalesOrdersResource(transport) + + resource.update(123, {"numeroLoja": "WEB-123"}) # type: ignore[reportArgumentType] + resource.delete(123) + resource.delete_many([123, 456]) + resource.update_status(123, 9) + + assert transport.calls == [ + ("PUT", "/pedidos/vendas/123", None, {"numeroLoja": "WEB-123"}), + ("DELETE", "/pedidos/vendas/123", None, None), + ("DELETE", "/pedidos/vendas", {"idsPedidosVendas[]": [123, 456]}, None), + ("PATCH", "/pedidos/vendas/123/situacoes/9", None, None), + ] + + +def test_sales_orders_english_posting_aliases_map_to_canonical_operations() -> None: + """English post-action aliases should delegate to their canonical methods.""" + transport = RecordingTransport() + resource = SalesOrdersResource(transport) + + resource.post_stock(123) + resource.post_stock(123, deposit_id=456) + resource.reverse_stock(123) + resource.post_accounts(123) + resource.reverse_accounts(123) + resource.generate_invoice(123) + resource.generate_consumer_invoice(123) + + assert transport.calls == [ + ("POST", "/pedidos/vendas/123/lancar-estoque", None, None), + ("POST", "/pedidos/vendas/123/lancar-estoque/456", None, None), + ("POST", "/pedidos/vendas/123/estornar-estoque", None, None), + ("POST", "/pedidos/vendas/123/lancar-contas", None, None), + ("POST", "/pedidos/vendas/123/estornar-contas", None, None), + ("POST", "/pedidos/vendas/123/gerar-nfe", None, None), + ("POST", "/pedidos/vendas/123/gerar-nfce", None, None), + ] + + def test_product_structures_map_requests_to_bling() -> None: """Structures resource should serialize paths, params, and payloads.""" transport = RecordingTransport() @@ -1549,10 +1694,10 @@ def test_caixas_bancos_criar_maps_to_bling_endpoint(self) -> None: transport = RecordingTransport() resource = CaixasBancosResource(transport) dados = CaixasBancosSalvarLancamentoDTO.model_construct( - data="2025-02-01", + data=date(2025, 2, 1), valor=350.00, deb_cred="C", - competencia="2025-02-01", + competencia=date(2025, 2, 1), observacoes="Teste", ) resource.criar(dados) @@ -1566,10 +1711,10 @@ def test_caixas_bancos_alterar_maps_to_bling_endpoint(self) -> None: transport = RecordingTransport() resource = CaixasBancosResource(transport) dados = CaixasBancosSalvarLancamentoDTO.model_construct( - data="2025-02-01", + data=date(2025, 2, 1), valor=500.00, deb_cred="D", - competencia="2025-02-01", + competencia=date(2025, 2, 1), observacoes="Atualizado", ) resource.alterar(123456, dados) @@ -1603,10 +1748,10 @@ def test_caixas_bancos_create_english_alias(self) -> None: resource = CaixasBancosResource(transport) resource.create( CaixasBancosSalvarLancamentoDTO.model_construct( - data="2025-02-01", + data=date(2025, 2, 1), valor=100.00, deb_cred="C", - competencia="2025-02-01", + competencia=date(2025, 2, 1), observacoes="Test", ) ) @@ -1628,10 +1773,10 @@ def test_caixas_bancos_update_english_alias(self) -> None: resource.update( 123456, CaixasBancosSalvarLancamentoDTO.model_construct( - data="2025-02-01", + data=date(2025, 2, 1), valor=500.00, deb_cred="D", - competencia="2025-02-01", + competencia=date(2025, 2, 1), observacoes="Upd", ), ) diff --git a/tests/unit/test_sales_orders_models.py b/tests/unit/test_sales_orders_models.py index 26e9b85..f35b4b0 100644 --- a/tests/unit/test_sales_orders_models.py +++ b/tests/unit/test_sales_orders_models.py @@ -97,6 +97,66 @@ def test_deserialize_sales_orders_get_itens(self) -> None: assert item.itens[0].codigo == "SKU-1" assert item.itens[0].quantidade == 2.0 + def test_deserialize_sales_orders_get_ids_present(self) -> None: + """Itens and parcelas with ids keep parsing them (Bug 7 regression guard).""" + data = _load_fixture("sales_orders_get.json") + response = PedidosVendasIdPedidoVendaGetResponse200.model_validate(data) # type: ignore[reportUnknownMemberType] + item = response.data + assert item is not None + assert item.itens is not None + assert item.itens[0].id == 333 + assert item.parcelas is not None + assert item.parcelas[0].id == 555 + + +class TestSalesOrdersGetMinimalResponse: + """Tests for minimal sales order detail payloads (Bug 7 class).""" + + def test_deserialize_sales_orders_get_minimal(self) -> None: + """Detail fixture without item/parcela ids should parse with id=None.""" + data = _load_fixture("sales_order_get_minimal.json") + response = PedidosVendasIdPedidoVendaGetResponse200.model_validate(data) # type: ignore[reportUnknownMemberType] + item = response.data + assert item is not None + assert item.id == 12345678 + assert item.data is not None + assert item.data_saida is not None + assert item.data_prevista is not None + assert item.situacao is not None + assert item.situacao.id == 111 + + def test_deserialize_sales_orders_get_minimal_itens_without_id(self) -> None: + """Minimal itens without id/codigo/produto/desconto/aliquotaIPI parse with None.""" + data = _load_fixture("sales_order_get_minimal.json") + response = PedidosVendasIdPedidoVendaGetResponse200.model_validate(data) # type: ignore[reportUnknownMemberType] + item = response.data + assert item is not None + assert item.itens is not None + assert len(item.itens) == 1 + pedido_item = item.itens[0] + assert pedido_item.id is None + assert pedido_item.codigo is None + assert pedido_item.produto is None + assert pedido_item.desconto is None + assert pedido_item.aliquota_ipi is None + assert pedido_item.quantidade == 2.0 + assert pedido_item.valor == 10.5 + assert pedido_item.descricao == "Produto Teste" + + def test_deserialize_sales_orders_get_minimal_parcelas_without_id(self) -> None: + """Minimal parcelas without id parse with id=None.""" + data = _load_fixture("sales_order_get_minimal.json") + response = PedidosVendasIdPedidoVendaGetResponse200.model_validate(data) # type: ignore[reportUnknownMemberType] + item = response.data + assert item is not None + assert item.parcelas is not None + assert len(item.parcelas) == 1 + parcela = item.parcelas[0] + assert parcela.id is None + assert parcela.data_vencimento is not None + assert parcela.valor == 23.5 + assert parcela.forma_pagamento.id == 666 + class TestSalesOrdersRequestModels: """Tests for sales order request models (PedidosVendasPostRequest).""" diff --git a/uv.lock b/uv.lock index cf8e795..df317e2 100644 --- a/uv.lock +++ b/uv.lock @@ -114,16 +114,6 @@ dependencies = [ { name = "python-dotenv" }, ] -[package.optional-dependencies] -codegen = [ - { name = "datamodel-code-generator" }, -] -docs = [ - { name = "mkdocs" }, - { name = "mkdocs-material" }, - { name = "mkdocstrings", extra = ["python"] }, -] - [package.dev-dependencies] codegen = [ { name = "datamodel-code-generator" }, @@ -145,16 +135,11 @@ docs = [ [package.metadata] requires-dist = [ { name = "bling-jwt-auth", specifier = ">=0.4.0,<1.0.0" }, - { name = "datamodel-code-generator", marker = "extra == 'codegen'", specifier = ">=0.36.0" }, { name = "httpx", specifier = ">=0.28.1,<1.0.0" }, - { name = "mkdocs", marker = "extra == 'docs'", specifier = ">=1.6.1" }, - { name = "mkdocs-material", marker = "extra == 'docs'", specifier = ">=9.6.23" }, - { name = "mkdocstrings", extras = ["python"], marker = "extra == 'docs'", specifier = ">=0.26.0" }, { name = "pydantic", specifier = ">=2.13.4,<3.0.0" }, { name = "pydantic-settings", specifier = ">=2.14.1,<3.0.0" }, { name = "python-dotenv", specifier = ">=1.2.2,<2.0.0" }, ] -provides-extras = ["codegen", "docs"] [package.metadata.requires-dev] codegen = [{ name = "datamodel-code-generator", specifier = ">=0.36.0" }]