Skip to content

Commit 1ac6f55

Browse files
TheBigRoomXXLSebastien LOVERGNEsloria
authored
Datetime support for multiple format (#815)
* 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>
1 parent 6651e86 commit 1ac6f55

File tree

5 files changed

+140
-0
lines changed

5 files changed

+140
-0
lines changed

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,4 @@ Contributors (chronological)
7878
- Mounier Florian `@paradoxxxzero <https://github.com/paradoxxxzero>`_
7979
- Renato Damas `@codectl <https://github.com/codectl>`_
8080
- Tayler Sokalski `@tsokalski <https://github.com/tsokalski>`_
81+
- Sebastien Lovergne `@TheBigRoomXXL <https://github.com/TheBigRoomXXL>`_

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ Changelog
44
6.4.0 (unreleased)
55
******************
66

7+
Features:
8+
9+
- ``MarshmallowPlugin``: Support different datetime formats
10+
for ``marshmallow.fields.DateTime`` fields (:issue:`814`).
11+
Thanks :user:`TheBigRoomXXL` for the suggestion and PR.
12+
713
Other changes:
814

915
- Support Python 3.12.

docs/using_plugins.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,22 @@ Schema Modifiers
236236

237237
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.
238238

239+
Custom DateTime formats
240+
***********************
241+
242+
apispec supports all four basic formats of `marshmallow.fields.DateTime`: ``"rfc"`` (for RFC822), ``"iso"`` (for ISO8601),
243+
``"timestamp"``, ``"timestamp_ms"`` (for a POSIX timestamp).
244+
245+
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.
246+
247+
.. code-block:: python
248+
249+
class SchemaWithCustomDate(Schema):
250+
french_date = ma.DateTime(
251+
format="%d-%m%Y %H:%M:%S",
252+
metadata={"pattern": r"^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}$"},
253+
)
254+
239255
Custom Fields
240256
*************
241257

src/apispec/ext/marshmallow/field_converter.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def init_attribute_functions(self):
110110
self.list2properties,
111111
self.dict2properties,
112112
self.timedelta2properties,
113+
self.datetime2properties,
113114
]
114115

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

522+
def datetime2properties(self, field, **kwargs: typing.Any) -> dict:
523+
"""Return a dictionary of properties from :class:`DateTime <marshmallow.fields.DateTime` fields.
524+
525+
:param Field field: A marshmallow field.
526+
:rtype: dict
527+
"""
528+
ret = {}
529+
if isinstance(field, marshmallow.fields.DateTime) and not isinstance(
530+
field, marshmallow.fields.Date
531+
):
532+
if field.format == "iso" or field.format is None:
533+
# Will return { "type": "string", "format": "date-time" }
534+
# as specified inside DEFAULT_FIELD_MAPPING
535+
pass
536+
elif field.format == "rfc":
537+
ret = {
538+
"type": "string",
539+
"format": None,
540+
"example": "Wed, 02 Oct 2002 13:00:00 GMT",
541+
"pattern": r"((Mon|Tue|Wed|Thu|Fri|Sat|Sun), ){0,1}\d{2} "
542+
+ r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} "
543+
+ r"(UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|(Z|A|M|N)|(\+|-)\d{4})",
544+
}
545+
elif field.format == "timestamp":
546+
ret = {
547+
"type": "number",
548+
"format": "float",
549+
"example": "1676451245.596",
550+
"min": "0",
551+
}
552+
elif field.format == "timestamp_ms":
553+
ret = {
554+
"type": "number",
555+
"format": "float",
556+
"example": "1676451277514.654",
557+
"min": "0",
558+
}
559+
else:
560+
ret = {
561+
"type": "string",
562+
"format": None,
563+
"pattern": field.metadata["pattern"]
564+
if field.metadata.get("pattern")
565+
else None,
566+
}
567+
return ret
568+
521569

522570
def make_type_list(types):
523571
"""Return a list of types from a type attribute

tests/test_ext_marshmallow_field.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,75 @@ def test_nested_field_with_property(spec_fixture):
377377
}
378378

379379

380+
def test_datetime2property_iso(spec_fixture):
381+
field = fields.DateTime(format="iso")
382+
res = spec_fixture.openapi.field2property(field)
383+
assert res == {
384+
"type": "string",
385+
"format": "date-time",
386+
}
387+
388+
389+
def test_datetime2property_rfc(spec_fixture):
390+
field = fields.DateTime(format="rfc")
391+
res = spec_fixture.openapi.field2property(field)
392+
assert res == {
393+
"type": "string",
394+
"format": None,
395+
"example": "Wed, 02 Oct 2002 13:00:00 GMT",
396+
"pattern": r"((Mon|Tue|Wed|Thu|Fri|Sat|Sun), ){0,1}\d{2} "
397+
+ r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} "
398+
+ r"(UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|(Z|A|M|N)|(\+|-)\d{4})",
399+
}
400+
401+
402+
def test_datetime2property_timestamp(spec_fixture):
403+
field = fields.DateTime(format="timestamp")
404+
res = spec_fixture.openapi.field2property(field)
405+
assert res == {
406+
"type": "number",
407+
"format": "float",
408+
"min": "0",
409+
"example": "1676451245.596",
410+
}
411+
412+
413+
def test_datetime2property_timestamp_ms(spec_fixture):
414+
field = fields.DateTime(format="timestamp_ms")
415+
res = spec_fixture.openapi.field2property(field)
416+
assert res == {
417+
"type": "number",
418+
"format": "float",
419+
"min": "0",
420+
"example": "1676451277514.654",
421+
}
422+
423+
424+
def test_datetime2property_custom_format(spec_fixture):
425+
field = fields.DateTime(
426+
format="%d-%m%Y %H:%M:%S",
427+
metadata={
428+
"pattern": r"^((?:(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$"
429+
},
430+
)
431+
res = spec_fixture.openapi.field2property(field)
432+
assert res == {
433+
"type": "string",
434+
"format": None,
435+
"pattern": r"^((?:(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$",
436+
}
437+
438+
439+
def test_datetime2property_custom_format_missing_regex(spec_fixture):
440+
field = fields.DateTime(format="%d-%m%Y %H:%M:%S")
441+
res = spec_fixture.openapi.field2property(field)
442+
assert res == {
443+
"type": "string",
444+
"format": None,
445+
"pattern": None,
446+
}
447+
448+
380449
class TestField2PropertyPluck:
381450
@pytest.fixture(autouse=True)
382451
def _setup(self, spec_fixture):

0 commit comments

Comments
 (0)