Skip to content

Commit 114b0b4

Browse files
committed
v20/v3x - epoch datetime values
have a datatype for epoch datetimes type:integer/number with format:date-time is datetime c.f. pydantic/pydantic-extra-types#240
1 parent adc5765 commit 114b0b4

File tree

5 files changed

+119
-4
lines changed

5 files changed

+119
-4
lines changed

aiopenapi3/model.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from .base import DiscriminatorBase
2525
from ._types import SchemaType, ReferenceType, PrimitiveTypes, DiscriminatorType
2626

27-
type_format_to_class: dict[str, dict[str, type]] = collections.defaultdict(dict)
27+
type_format_to_class: dict[str, dict[Optional[str], type]] = collections.defaultdict(dict)
2828

2929
log = logging.getLogger("aiopenapi3.model")
3030

@@ -58,11 +58,17 @@ def generate_type_format_to_class():
5858

5959
type_format_to_class["string"]["byte"] = Base64Str
6060

61+
from aiopenapi3.models import epoch
62+
63+
type_format_to_class["number"]["date-time"] = epoch.Number
64+
65+
if "integer" not in type_format_to_class:
66+
type_format_to_class["integer"][None] = int
67+
type_format_to_class["integer"]["date-time"] = epoch.Integer
68+
6169

6270
def class_from_schema(s, _type):
63-
if _type == "integer":
64-
return int
65-
elif _type == "boolean":
71+
if _type == "boolean":
6672
return bool
6773
a = type_format_to_class[_type]
6874
b = a.get(s.format, a[None])

aiopenapi3/models/epoch.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from __future__ import annotations
2+
3+
import datetime
4+
from typing import Any, Callable
5+
6+
import pydantic_core.core_schema
7+
from pydantic import GetJsonSchemaHandler
8+
from pydantic.json_schema import JsonSchemaValue
9+
from pydantic_core import CoreSchema, core_schema
10+
11+
EPOCH = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
12+
13+
14+
class _Base(datetime.datetime):
15+
TYPE: str = ""
16+
SCHEMA: pydantic_core.core_schema.CoreSchema
17+
18+
@classmethod
19+
def __get_pydantic_json_schema__(
20+
cls, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
21+
) -> JsonSchemaValue:
22+
field_schema: dict[str, Any] = {}
23+
field_schema.update(type=cls.TYPE, format="date-time")
24+
return field_schema
25+
26+
@classmethod
27+
def __get_pydantic_core_schema__(
28+
cls, source: type[Any], handler: Callable[[Any], CoreSchema]
29+
) -> core_schema.CoreSchema:
30+
return core_schema.with_info_after_validator_function(
31+
cls._validate,
32+
cls.SCHEMA,
33+
serialization=core_schema.wrap_serializer_function_ser_schema(cls._f, return_schema=cls.SCHEMA),
34+
)
35+
36+
@classmethod
37+
def _validate(cls, __input_value: Any, _: Any) -> datetime.datetime:
38+
return EPOCH + datetime.timedelta(seconds=__input_value)
39+
40+
@classmethod
41+
def _f(cls, value: Any, serializer: Callable[[Any], Any]) -> Any: # pragma: no cover
42+
raise NotImplementedError(cls)
43+
44+
45+
class Number(_Base):
46+
TYPE = "number"
47+
SCHEMA = core_schema.float_schema()
48+
49+
@classmethod
50+
def _f(cls, value: Any, serializer: Callable[[float], float]) -> float:
51+
ts = value.timestamp()
52+
return serializer(ts)
53+
54+
55+
class Integer(_Base):
56+
TYPE = "integer"
57+
SCHEMA = core_schema.int_schema()
58+
59+
@classmethod
60+
def _f(cls, value: Any, serializer: Callable[[int], int]) -> int:
61+
ts = value.timestamp()
62+
return serializer(int(ts))

tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,11 @@ def with_schema_additionalProperties_and_named_properties():
440440
yield _get_parsed_yaml("schema-additionalProperties-and-named-properties" ".yaml")
441441

442442

443+
@pytest.fixture
444+
def with_schema_date_types():
445+
yield _get_parsed_yaml("schema-date-types.yaml")
446+
447+
443448
@pytest.fixture
444449
def with_schema_boolean_v20():
445450
yield _get_parsed_yaml("schema-boolean-v20.yaml")
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
openapi: "3.1.0"
2+
info:
3+
version: 1.0.0
4+
title: date-time
5+
6+
components:
7+
schemas:
8+
Integer:
9+
type: integer
10+
format: date-time
11+
12+
Number:
13+
type: number
14+
format: date-time
15+
16+
String:
17+
type: string
18+
format: date-time

tests/schema_test.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import copy
22
import typing
33
import uuid
4+
from datetime import datetime
45
from unittest.mock import MagicMock, patch
56

67
from pydantic.fields import FieldInfo
@@ -766,3 +767,26 @@ def test_schema_boolean_v20(with_schema_boolean_v20):
766767

767768
with pytest.raises(ValidationError):
768769
B.model_validate({"b": 1})
770+
771+
772+
def test_schema_date_types(with_schema_date_types):
773+
api = OpenAPI("/", with_schema_date_types)
774+
Integer = api.components.schemas["Integer"].get_type()
775+
Number = api.components.schemas["Number"].get_type()
776+
String = api.components.schemas["String"].get_type()
777+
778+
from datetime import timezone
779+
780+
now = datetime.now(tz=timezone.utc)
781+
ts = now.timestamp()
782+
v = Integer.model_validate(c := int(ts))
783+
assert isinstance(v.root, datetime)
784+
assert v.model_dump() == c
785+
786+
v = Number.model_validate(ts)
787+
assert isinstance(v.root, datetime)
788+
assert v.model_dump() == ts
789+
790+
v = String.model_validate(str(ts))
791+
assert isinstance(v.root, datetime)
792+
assert v.model_dump_json()[1:-1] == now.strftime("%Y-%m-%dT%H:%M:%S.%fZ")

0 commit comments

Comments
 (0)