Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.2] - 2026-10-07

### Fixed

- Tracking timestamps in logistics objects and shipment objects now accept offset-free Bling values through the generated `BlingDatetime` type. Naive timestamps use `America/Sao_Paulo` (including historical daylight saving time); explicit offsets are preserved, and JSON serialization emits an ISO timestamp with its offset. The generator and shared model namespace use the same type, with portable timezone data supplied by `tzdata`.
- Tracking dates now normalize MySQL zero-date sentinels, empty strings and null to `None`, matching absent-date handling in `BlingDate`. This fixes a second failure confirmed in read-only live logistics objects. Other malformed dates still raise validation errors; the tracking timestamp key remains required, and unrelated DTO requirements are unchanged.

## [0.3.1] - 2026-10-06

### Fixed
Expand Down
17 changes: 0 additions & 17 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,23 +114,6 @@ with BlingClient.from_env() as client:
print(contato.get("data", {}).get("nome"))
```

### Datas de rastreamento

Os campos `rastreamento.ultima_alteracao` de objetos e remessas usam
`BlingDatetime`. Datas sem fuso, como `"2026-10-07 11:24:02"`, são interpretadas
no fuso IANA `America/Sao_Paulo`, incluindo suas regras históricas de horário
de verão. Datas que já possuem fuso mantêm o offset informado pelo Bling.
Na serialização JSON, o resultado inclui o offset:
`"2026-10-07T11:24:02-03:00"`. A dependência `tzdata` fornece essas regras
também em sistemas sem uma base de fusos instalada.

Sentinelas de data ausente (`"0000-00-00 00:00:00"`, `"0000-00-00"`,
`"0001-01-01"`, `"0001-01-01 00:00:00"`), strings vazias e `null` são
representadas por `None`. `model_dump(mode="json")` preserva `null`;
`to_json_object()` omite esses valores. Outras datas malformadas continuam
causando erro de validação, e a chave `ultimaAlteracao` continua obrigatória
no objeto de rastreamento.

### Paginação

Endpoints de listagem aceitam `page` e `limit`:
Expand Down
65 changes: 0 additions & 65 deletions docs/response-validation-audit.md

This file was deleted.

1 change: 0 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ nav:
- Autenticação: authentication.md
- Paginação: pagination.md
- Erros: errors.md
- Validação de respostas: response-validation-audit.md
- Recursos:
- resources/sales_orders.md
- resources/products.md
Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ dependencies = [
"pydantic>=2.13.4,<3.0.0",
"pydantic-settings>=2.14.1,<3.0.0",
"python-dotenv>=1.2.2,<2.0.0",
"tzdata>=2025.2",
]

[project.urls]
Expand Down
21 changes: 11 additions & 10 deletions scripts/generate_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@
)
BUILTIN_TYPE_NAMES = {
"Any",
"AwareDatetime",
"BlingDate",
"BlingDatetime",
"BlingModel",
"Field",
"None",
Expand Down Expand Up @@ -181,7 +181,7 @@

# Type that replaces bare ``date`` in all generated annotations.
DATE_TYPE_REWRITE = "BlingDate"
DATE_IMPORT = "from bling_erp_api.models.fields import BlingDate, BlingDatetime"
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
Expand Down Expand Up @@ -604,10 +604,10 @@ def _schema_module_content(
"",
"from __future__ import annotations",
"",
DATE_IMPORT,
"from bling_erp_api.models.fields import BlingDate",
"from typing import TYPE_CHECKING, Any",
"",
"from pydantic import AliasChoices, Field, RootModel",
"from pydantic import AliasChoices, AwareDatetime, Field, RootModel",
"",
"from bling_erp_api.models.base import BlingModel",
]
Expand Down Expand Up @@ -653,9 +653,11 @@ def _schema_init_content(
"",
"from __future__ import annotations",
"",
DATE_IMPORT,
"from bling_erp_api.models.fields import BlingDate",
"from typing import Any",
"",
"from pydantic import AwareDatetime",
"",
]
for module, names in sorted(by_module.items()):
imports = ", ".join(names)
Expand All @@ -669,7 +671,7 @@ def _schema_init_content(
exports + "]",
"",
"_MODEL_NAMESPACE = {name: globals()[name] for name in __all__}",
"_MODEL_NAMESPACE.update({'Any': Any, 'BlingDate': BlingDate, 'BlingDatetime': BlingDatetime})",
"_MODEL_NAMESPACE.update({'Any': Any, 'AwareDatetime': AwareDatetime, 'BlingDate': BlingDate})",
"for _model in _MODEL_NAMESPACE.values():",
" if isinstance(_model, type) and hasattr(_model, 'model_rebuild'):",
" _model.model_rebuild(_types_namespace=_MODEL_NAMESPACE)",
Expand Down Expand Up @@ -910,11 +912,10 @@ def _remove_pass_if_empty(node: ast.ClassDef) -> None:


def _rewrite_date_types(node: ast.ClassDef) -> None:
"""Use Bling date and datetime parsing in generated field annotations."""
replacements = {"date": "BlingDate", "AwareDatetime": "BlingDatetime"}
"""Replace ``date`` with ``BlingDate`` in all field annotations."""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug
🛑 Blocker — must fix before merge

Removal of AwareDatetime→BlingDatetime rewrite reintroduces Bling datetime quirk

The _rewrite_date_types function previously converted both date → BlingDate and AwareDatetime → BlingDatetime in generated field annotations. The custom BlingDatetime type (defined in fields.py) provided two critical behaviors that pydantic.AwareDatetime lacks:

  1. Sentinel normalization: _normalize_bling_datetime maps Bling's absent-timestamp sentinels ("0000-00-00", "0000-00-00 00:00:00", "0001-01-01", empty strings) and null to None. Without this, these values will cause a ValidationError.
  2. Timezone localization: _localize_bling_datetime interprets naive timestamps in America/Sao_Paulo (including historical DST rules) and preserves explicit offsets. AwareDatetime rejects naive datetimes outright.

Both behaviors were confirmed fixes for real Bling API quirks (commits a79782a0, ad4a0f5f, 8ae63d2a). By removing the AwareDatetime → BlingDatetime rewrite, the generator now outputs AwareDatetime directly in schema models, which will cause ValidationError at runtime when parsing logistics tracking responses that contain zero-date sentinels or naive timestamps.


Prompt for AI Agents
In scripts/generate_models.py, restore the AwareDatetime→BlingDatetime rewrite inside _rewrite_date_types (lines 912-916). Change the docstring to describe both date and AwareDatetime replacements. Revert the loop body: use a replacements dict {"date": "BlingDate", "AwareDatetime": "BlingDatetime"} and check `stmt.id in replacements` then assign `stmt.id = replacements[stmt.id]`. Also restore DATE_IMPORT to `from bling_erp_api.models.fields import BlingDate, BlingDatetime` and restore the original _schema_module_content/_schema_init_content import lines so they use DATE_IMPORT and import BlingDatetime.

Not useful? Reply @tempont-bot reject to dismiss this suggestion.

for stmt in ast.walk(node):
if isinstance(stmt, ast.Name) and stmt.id in replacements:
stmt.id = replacements[stmt.id]
if isinstance(stmt, ast.Name) and stmt.id == "date":
stmt.id = "BlingDate"


def _inherited_field_value(parent_decl: ast.AnnAssign | None, field_name: str) -> ast.expr:
Expand Down
2 changes: 1 addition & 1 deletion src/bling_erp_api/_version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Package version."""

__version__ = "0.3.2"
__version__ = "0.3.1"
38 changes: 1 addition & 37 deletions src/bling_erp_api/models/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@

from __future__ import annotations

from datetime import date, datetime
from datetime import date
from typing import Annotated
from zoneinfo import ZoneInfo

from pydantic import AfterValidator, BeforeValidator
from pydantic.functional_serializers import PlainSerializer
from pydantic.functional_validators import PlainValidator

Expand Down Expand Up @@ -46,37 +44,3 @@ def _serialize_bling_date(value: date | None) -> str | None:
PlainValidator(_parse_bling_date),
PlainSerializer(_serialize_bling_date, return_type=str | None, when_used="json"),
]


def _normalize_bling_datetime(value: object) -> object:
"""Map Bling's absent timestamp sentinels to None before datetime parsing."""
if isinstance(value, str):
stripped = value.strip()
if not stripped or stripped in {
"0000-00-00",
"0000-00-00 00:00:00",
"0001-01-01",
"0001-01-01 00:00:00",
}:
return None
return stripped
return value


def _localize_bling_datetime(value: datetime | None) -> datetime | None:
"""Interpret offset-free Bling timestamps in America/Sao_Paulo.

Preserve explicit offsets and use the IANA timezone rules for naive values,
including historical daylight saving time. Invalid timestamps still fail
Pydantic's normal datetime validation before this validator is called.
"""
if value is not None and (value.tzinfo is None or value.utcoffset() is None):
return value.replace(tzinfo=ZoneInfo("America/Sao_Paulo"))
return value


BlingDatetime = Annotated[
datetime | None,
BeforeValidator(_normalize_bling_datetime),
AfterValidator(_localize_bling_datetime),
]
6 changes: 4 additions & 2 deletions src/bling_erp_api/models/generated/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

from __future__ import annotations

from bling_erp_api.models.fields import BlingDate, BlingDatetime
from bling_erp_api.models.fields import BlingDate
from typing import Any

from pydantic import AwareDatetime

from .anuncios import (
AnunciosAtributoDTO,
AnunciosCategoriaDTO,
Expand Down Expand Up @@ -1482,7 +1484,7 @@
]

_MODEL_NAMESPACE = {name: globals()[name] for name in __all__}
_MODEL_NAMESPACE.update({"Any": Any, "BlingDate": BlingDate, "BlingDatetime": BlingDatetime})
_MODEL_NAMESPACE.update({"Any": Any, "AwareDatetime": AwareDatetime, "BlingDate": BlingDate})
for _model in _MODEL_NAMESPACE.values():
if isinstance(_model, type) and hasattr(_model, "model_rebuild"):
_model.model_rebuild(_types_namespace=_MODEL_NAMESPACE)
4 changes: 2 additions & 2 deletions src/bling_erp_api/models/generated/schemas/anuncios.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

from __future__ import annotations

from bling_erp_api.models.fields import BlingDate, BlingDatetime
from bling_erp_api.models.fields import BlingDate
from typing import TYPE_CHECKING, Any

from pydantic import AliasChoices, Field, RootModel
from pydantic import AliasChoices, AwareDatetime, Field, RootModel

from bling_erp_api.models.base import BlingModel

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

from __future__ import annotations

from bling_erp_api.models.fields import BlingDate, BlingDatetime
from bling_erp_api.models.fields import BlingDate
from typing import TYPE_CHECKING, Any

from pydantic import AliasChoices, Field, RootModel
from pydantic import AliasChoices, AwareDatetime, Field, RootModel

from bling_erp_api.models.base import BlingModel

Expand Down
4 changes: 2 additions & 2 deletions src/bling_erp_api/models/generated/schemas/borderos.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

from __future__ import annotations

from bling_erp_api.models.fields import BlingDate, BlingDatetime
from bling_erp_api.models.fields import BlingDate
from typing import TYPE_CHECKING, Any

from pydantic import AliasChoices, Field, RootModel
from pydantic import AliasChoices, AwareDatetime, Field, RootModel

from bling_erp_api.models.base import BlingModel

Expand Down
4 changes: 2 additions & 2 deletions src/bling_erp_api/models/generated/schemas/caixas_bancos.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

from __future__ import annotations

from bling_erp_api.models.fields import BlingDate, BlingDatetime
from bling_erp_api.models.fields import BlingDate
from typing import TYPE_CHECKING, Any

from pydantic import AliasChoices, Field, RootModel
from pydantic import AliasChoices, AwareDatetime, Field, RootModel

from bling_erp_api.models.base import BlingModel

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

from __future__ import annotations

from bling_erp_api.models.fields import BlingDate, BlingDatetime
from bling_erp_api.models.fields import BlingDate
from typing import TYPE_CHECKING, Any

from pydantic import AliasChoices, Field, RootModel
from pydantic import AliasChoices, AwareDatetime, Field, RootModel

from bling_erp_api.models.base import BlingModel

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

from __future__ import annotations

from bling_erp_api.models.fields import BlingDate, BlingDatetime
from bling_erp_api.models.fields import BlingDate
from typing import TYPE_CHECKING, Any

from pydantic import AliasChoices, Field, RootModel
from pydantic import AliasChoices, AwareDatetime, Field, RootModel

from bling_erp_api.models.base import BlingModel

Expand Down
4 changes: 2 additions & 2 deletions src/bling_erp_api/models/generated/schemas/canais_venda.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

from __future__ import annotations

from bling_erp_api.models.fields import BlingDate, BlingDatetime
from bling_erp_api.models.fields import BlingDate
from typing import TYPE_CHECKING, Any

from pydantic import AliasChoices, Field, RootModel
from pydantic import AliasChoices, AwareDatetime, Field, RootModel

from bling_erp_api.models.base import BlingModel

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

from __future__ import annotations

from bling_erp_api.models.fields import BlingDate, BlingDatetime
from bling_erp_api.models.fields import BlingDate
from typing import TYPE_CHECKING, Any

from pydantic import AliasChoices, Field, RootModel
from pydantic import AliasChoices, AwareDatetime, Field, RootModel

from bling_erp_api.models.base import BlingModel

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

from __future__ import annotations

from bling_erp_api.models.fields import BlingDate, BlingDatetime
from bling_erp_api.models.fields import BlingDate
from typing import TYPE_CHECKING, Any

from pydantic import AliasChoices, Field, RootModel
from pydantic import AliasChoices, AwareDatetime, Field, RootModel

from bling_erp_api.models.base import BlingModel

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

from __future__ import annotations

from bling_erp_api.models.fields import BlingDate, BlingDatetime
from bling_erp_api.models.fields import BlingDate
from typing import TYPE_CHECKING, Any

from pydantic import AliasChoices, Field, RootModel
from pydantic import AliasChoices, AwareDatetime, Field, RootModel

from bling_erp_api.models.base import BlingModel

Expand Down
Loading
Loading