From f0cdcc26ac9e9656cced8bd0c7e3d81fc23d5470 Mon Sep 17 00:00:00 2001 From: Claus Holbech Date: Wed, 20 May 2026 17:16:32 +0200 Subject: [PATCH] Preserve nanosecond precision for Timestamp Add NanoDatetime to carry sub-microsecond precision through Timestamp conversions and JSON parsing/formatting while keeping generated field types as datetime.datetime. Generate the NanoDatetime import only for known types that need it, and add runtime/compiler coverage for the new behavior. --- README.md | 2 +- betterproto2/docs/getting-started.md | 6 + betterproto2/docs/index.md | 1 + .../src/betterproto2/nano_datetime.py | 163 ++++++++++++++++++ betterproto2/tests/test_nano_datetime.py | 83 +++++++++ betterproto2/tests/test_timestamp.py | 47 +++++ .../known_types/__init__.py | 8 +- .../known_types/timestamp.py | 47 +---- .../lib/google/protobuf/__init__.py | 47 +---- .../betterproto2_compiler/plugin/models.py | 18 +- .../templates/header.py.j2 | 4 +- .../tests/test_known_type_imports.py | 17 ++ plan.md | 159 +++++++++++++++++ 13 files changed, 515 insertions(+), 87 deletions(-) create mode 100644 betterproto2/src/betterproto2/nano_datetime.py create mode 100644 betterproto2/tests/test_nano_datetime.py create mode 100644 betterproto2_compiler/tests/test_known_type_imports.py create mode 100644 plan.md diff --git a/README.md b/README.md index ce9b3190..0ab82a09 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ This project aims to provide an improved experience when using Protobuf / gRPC i - Enums - Dataclasses - `async`/`await` - - Timezone-aware `datetime` and `timedelta` objects + - Timezone-aware `datetime` and `timedelta` objects, including nanosecond-precision `Timestamp` values - Relative imports - Mypy type checking - [Pydantic Models](https://docs.pydantic.dev/) generation diff --git a/betterproto2/docs/getting-started.md b/betterproto2/docs/getting-started.md index 68d07495..2cb3621f 100644 --- a/betterproto2/docs/getting-started.md +++ b/betterproto2/docs/getting-started.md @@ -225,6 +225,12 @@ Message objects include `betterproto.Message.to_json` and `betterproto.Message.to_dict`, `betterproto.Message.from_dict` for converting back and forth from JSON serializable dicts. +`google.protobuf.Timestamp` fields use timezone-aware `datetime.datetime` +values. When binary or JSON data contains sub-microsecond precision, +betterproto2 preserves it by returning a `betterproto2.nano_datetime.NanoDatetime`, +which is a `datetime.datetime` subclass. Timestamp JSON accepts and emits +RFC 3339 strings with up to 9 fractional second digits. + For compatibility the default is to convert field names to `betterproto.Casing.CAMEL`. You can control this behavior by passing a different casing value, e.g: diff --git a/betterproto2/docs/index.md b/betterproto2/docs/index.md index 11fbfd6a..fa9864e0 100644 --- a/betterproto2/docs/index.md +++ b/betterproto2/docs/index.md @@ -12,5 +12,6 @@ Python code, using modern language features. - Generated messages are both binary & JSON serializable - Messages use relevant python types, e.g. ``Enum``, ``datetime`` and ``timedelta`` objects +- ``Timestamp`` values preserve nanosecond precision when binary or JSON data contains it - ``async``/``await`` support for gRPC Clients and Servers - Generates modern, readable, idiomatic python code diff --git a/betterproto2/src/betterproto2/nano_datetime.py b/betterproto2/src/betterproto2/nano_datetime.py new file mode 100644 index 00000000..c2bebab4 --- /dev/null +++ b/betterproto2/src/betterproto2/nano_datetime.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +import datetime +import re +from typing import Any + +import dateutil.parser +from typing_extensions import Self + +_UTC = datetime.timezone.utc +_TIMESTAMP_ZERO = datetime.datetime(1970, 1, 1, tzinfo=_UTC) +_NANOS_PER_MICROSECOND = 1000 +_MICROS_PER_SECOND = 10**6 +_TIMESTAMP_RE = re.compile( + r"^" + r"(?P\d{4}-\d{2}-\d{2})" + r"T" + r"(?P