Skip to content

Commit

Permalink
Datetime support for multiple format (#815)
Browse files Browse the repository at this point in the history
* Add support for the multiple formats of marshmallow.fields.DateTime

* Merge branch 'origin/datetime-support-for-multiple-format'

* Minor cleanup and changelog update

---------

Co-authored-by: Sebastien LOVERGNE <sebastien.lovergne@rfconception.com>
Co-authored-by: Steven Loria <sloria1@gmail.com>
  • Loading branch information
3 people authored Jan 10, 2024
1 parent 6651e86 commit 1ac6f55
Showing 5 changed files with 140 additions and 0 deletions.
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
@@ -78,3 +78,4 @@ Contributors (chronological)
- Mounier Florian `@paradoxxxzero <https://github.com/paradoxxxzero>`_
- Renato Damas `@codectl <https://github.com/codectl>`_
- Tayler Sokalski `@tsokalski <https://github.com/tsokalski>`_
- Sebastien Lovergne `@TheBigRoomXXL <https://github.com/TheBigRoomXXL>`_
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -4,6 +4,12 @@ Changelog
6.4.0 (unreleased)
******************

Features:

- ``MarshmallowPlugin``: Support different datetime formats
for ``marshmallow.fields.DateTime`` fields (:issue:`814`).
Thanks :user:`TheBigRoomXXL` for the suggestion and PR.

Other changes:

- Support Python 3.12.
16 changes: 16 additions & 0 deletions docs/using_plugins.rst
Original file line number Diff line number Diff line change
@@ -236,6 +236,22 @@ Schema Modifiers

apispec will respect schema modifiers such as ``exclude`` and ``partial`` in the generated schema definition. If a schema is initialized with modifiers, apispec will treat each combination of modifiers as a unique schema definition.

Custom DateTime formats
***********************

apispec supports all four basic formats of `marshmallow.fields.DateTime`: ``"rfc"`` (for RFC822), ``"iso"`` (for ISO8601),
``"timestamp"``, ``"timestamp_ms"`` (for a POSIX timestamp).

If you are using a custom DateTime format you should pass a regex string to the ``pattern`` parameter in your field ``metadata`` so that it is included as documentation.

.. code-block:: python
class SchemaWithCustomDate(Schema):
french_date = ma.DateTime(
format="%d-%m%Y %H:%M:%S",
metadata={"pattern": r"^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}$"},
)
Custom Fields
*************

48 changes: 48 additions & 0 deletions src/apispec/ext/marshmallow/field_converter.py
Original file line number Diff line number Diff line change
@@ -110,6 +110,7 @@ def init_attribute_functions(self):
self.list2properties,
self.dict2properties,
self.timedelta2properties,
self.datetime2properties,
]

def map_to_openapi_type(self, field_cls, *args):
@@ -518,6 +519,53 @@ def enum2properties(self, field, **kwargs: typing.Any) -> dict:
ret["enum"] = [field.field._serialize(v, None, None) for v in choices]
return ret

def datetime2properties(self, field, **kwargs: typing.Any) -> dict:
"""Return a dictionary of properties from :class:`DateTime <marshmallow.fields.DateTime` fields.
:param Field field: A marshmallow field.
:rtype: dict
"""
ret = {}
if isinstance(field, marshmallow.fields.DateTime) and not isinstance(
field, marshmallow.fields.Date
):
if field.format == "iso" or field.format is None:
# Will return { "type": "string", "format": "date-time" }
# as specified inside DEFAULT_FIELD_MAPPING
pass
elif field.format == "rfc":
ret = {
"type": "string",
"format": None,
"example": "Wed, 02 Oct 2002 13:00:00 GMT",
"pattern": r"((Mon|Tue|Wed|Thu|Fri|Sat|Sun), ){0,1}\d{2} "
+ r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} "
+ r"(UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|(Z|A|M|N)|(\+|-)\d{4})",
}
elif field.format == "timestamp":
ret = {
"type": "number",
"format": "float",
"example": "1676451245.596",
"min": "0",
}
elif field.format == "timestamp_ms":
ret = {
"type": "number",
"format": "float",
"example": "1676451277514.654",
"min": "0",
}
else:
ret = {
"type": "string",
"format": None,
"pattern": field.metadata["pattern"]
if field.metadata.get("pattern")
else None,
}
return ret


def make_type_list(types):
"""Return a list of types from a type attribute
69 changes: 69 additions & 0 deletions tests/test_ext_marshmallow_field.py
Original file line number Diff line number Diff line change
@@ -377,6 +377,75 @@ def test_nested_field_with_property(spec_fixture):
}


def test_datetime2property_iso(spec_fixture):
field = fields.DateTime(format="iso")
res = spec_fixture.openapi.field2property(field)
assert res == {
"type": "string",
"format": "date-time",
}


def test_datetime2property_rfc(spec_fixture):
field = fields.DateTime(format="rfc")
res = spec_fixture.openapi.field2property(field)
assert res == {
"type": "string",
"format": None,
"example": "Wed, 02 Oct 2002 13:00:00 GMT",
"pattern": r"((Mon|Tue|Wed|Thu|Fri|Sat|Sun), ){0,1}\d{2} "
+ r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} "
+ r"(UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|(Z|A|M|N)|(\+|-)\d{4})",
}


def test_datetime2property_timestamp(spec_fixture):
field = fields.DateTime(format="timestamp")
res = spec_fixture.openapi.field2property(field)
assert res == {
"type": "number",
"format": "float",
"min": "0",
"example": "1676451245.596",
}


def test_datetime2property_timestamp_ms(spec_fixture):
field = fields.DateTime(format="timestamp_ms")
res = spec_fixture.openapi.field2property(field)
assert res == {
"type": "number",
"format": "float",
"min": "0",
"example": "1676451277514.654",
}


def test_datetime2property_custom_format(spec_fixture):
field = fields.DateTime(
format="%d-%m%Y %H:%M:%S",
metadata={
"pattern": r"^((?:(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$"
},
)
res = spec_fixture.openapi.field2property(field)
assert res == {
"type": "string",
"format": None,
"pattern": r"^((?:(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$",
}


def test_datetime2property_custom_format_missing_regex(spec_fixture):
field = fields.DateTime(format="%d-%m%Y %H:%M:%S")
res = spec_fixture.openapi.field2property(field)
assert res == {
"type": "string",
"format": None,
"pattern": None,
}


class TestField2PropertyPluck:
@pytest.fixture(autouse=True)
def _setup(self, spec_fixture):

0 comments on commit 1ac6f55

Please sign in to comment.