6
6
from itertools import chain
7
7
from typing import Any , FrozenSet , Iterable
8
8
9
- import arrow
9
+ # C-based module confuses pylint, which is why we disable the check below.
10
+ from ciso8601 import parse_datetime # pylint: disable=no-name-in-module
10
11
from graphql import (
11
12
DirectiveLocation ,
12
13
GraphQLArgument ,
@@ -340,45 +341,92 @@ def _serialize_date(value: Any) -> str:
340
341
341
342
def _parse_date_value (value : Any ) -> date :
342
343
"""Deserialize a Date object from its proper ISO-8601 representation."""
343
- return arrow .get (value , "YYYY-MM-DD" ).date ()
344
+ if type (value ) == date :
345
+ # We prefer exact type equality instead of isinstance() because datetime objects
346
+ # are subclasses of date but are not interchangeable for dates for our purposes.
347
+ return value
348
+ elif isinstance (value , str ):
349
+ # ciso8601 only supports parsing into datetime objects, not date objects.
350
+ # This is not a problem in itself: "YYYY-MM-DD" strings will get parsed into datetimes
351
+ # with hour/minute/second/microsecond set to 0, and tzinfo=None.
352
+ # We don't want our parsing to implicitly lose precision, so before we convert the parsed
353
+ # datetime into a date value, we just assert that these fields are set as expected.
354
+ dt = parse_datetime (value ) # This will raise ValueError in case of bad ISO 8601 formatting.
355
+ if (
356
+ dt .hour != 0
357
+ or dt .minute != 0
358
+ or dt .second != 0
359
+ or dt .microsecond != 0
360
+ or dt .tzinfo is not None
361
+ ):
362
+ raise ValueError (
363
+ f"Expected an ISO-8601 date string in 'YYYY-MM-DD' format, but got a datetime "
364
+ f"string with a non-empty time component. This is not supported, since converting "
365
+ f"it to a date would result in an implicit loss of precision. Received value "
366
+ f"{ repr (value )} , parsed as { dt } ."
367
+ )
368
+
369
+ return dt .date ()
370
+ else :
371
+ raise ValueError (
372
+ f"Expected a date object or its ISO-8601 'YYYY-MM-DD' string representation. "
373
+ f"Got { value } of type { type (value )} instead."
374
+ )
344
375
345
376
346
377
def _serialize_datetime (value : Any ) -> str :
347
378
"""Serialize a DateTime object to its proper ISO-8601 representation."""
348
- # Python datetime.datetime is a subclass of datetime.date, but in this case, the two are not
349
- # interchangeable. Rather than using isinstance, we will therefore check for exact type
350
- # equality.
351
- #
352
- # We don't allow Arrow objects as input since it seems that Arrow objects are always tz aware.
353
- # This is supported by the fact that the `.naive` Arrow method returns a datetime object instead
354
- # of an Arrow object.
355
- if type (value ) == datetime and value .tzinfo is None :
379
+ if isinstance (value , datetime ) and value .tzinfo is None :
356
380
return value .isoformat ()
357
381
else :
358
382
raise ValueError (
359
- f"Expected a timezone naive datetime object. Got { value } of type { type (value )} instead."
383
+ f"Expected a timezone- naive datetime object. Got { value } of type { type (value )} instead."
360
384
)
361
385
362
386
363
387
def _parse_datetime_value (value : Any ) -> datetime :
364
- """Deserialize a DateTime object from its proper ISO-8601 representation."""
365
- # attempt to parse with microsecond information
366
- try :
367
- arrow_result = arrow .get (value , "YYYY-MM-DDTHH:mm:ss" )
368
- except arrow .parser .ParserMatchError :
369
- arrow_result = arrow .get (value , "YYYY-MM-DDTHH:mm:ss.S" )
370
-
371
- # arrow parses datetime naive strings into Arrow objects with a UTC timezone.
372
- return arrow_result .naive
388
+ """Deserialize a DateTime object from a date/datetime or a ISO-8601 string representation."""
389
+ if isinstance (value , datetime ) and value .tzinfo is None :
390
+ return value
391
+ elif isinstance (value , str ):
392
+ dt = parse_datetime (value ) # This will raise ValueError in case of bad ISO 8601 formatting.
393
+ if dt .tzinfo is not None :
394
+ raise ValueError (
395
+ f"Expected a timezone-naive datetime value, but got a timezone-aware datetime "
396
+ f"string. This is not supported, since discarding the timezone component would "
397
+ f"result in an implicit loss of precision. Received value { repr (value )} , "
398
+ f"parsed as { dt } ."
399
+ )
400
+
401
+ return dt
402
+ elif type (value ) == date :
403
+ # The date type is a supertype of datetime. We check for exact type equality
404
+ # rather than using isinstance(), to avoid having this branch get hit
405
+ # by timezone-aware datetimes (i.e. ones that fail the value.tzinfo is None check above).
406
+ #
407
+ # This is a widening conversion (there's no loss of precision) so we allow it to be implicit
408
+ # since use ciso8601 parsing logic for parsing datetimes, and ciso8601 successfully parses
409
+ # datetimes that only have data down to day precision.
410
+ return datetime (value .year , value .month , value .day )
411
+ else :
412
+ raise ValueError (
413
+ f"Expected a timezone-naive datetime or an ISO-8601 string representation parseable "
414
+ f"by the ciso8601 library. Got { value } of type { type (value )} instead."
415
+ )
373
416
374
417
375
418
GraphQLDate = GraphQLScalarType (
376
419
name = "Date" ,
377
420
description = (
378
421
"The `Date` scalar type represents day-accuracy date objects."
379
422
"Values are serialized following the ISO-8601 datetime format specification, "
380
- 'for example "2017-03-21". The year, month and day fields must be included, '
381
- "and the format followed exactly, or the behavior is undefined."
423
+ 'for example "2017-03-21". Serialization and parsing support is guaranteed for the format '
424
+ "described here, with the year, month and day fields included and separated by dashes as "
425
+ "in the example. Implementations are allowed to support additional serialization formats, "
426
+ "if they so choose."
427
+ # GraphQL compiler's implementation of GraphQL-based querying uses the ciso8601 library
428
+ # for date and datetime parsing, so it additionally supports the subset of the ISO-8601
429
+ # standard supported by that library.
382
430
),
383
431
serialize = _serialize_date ,
384
432
parse_value = _parse_date_value ,
@@ -389,10 +437,16 @@ def _parse_datetime_value(value: Any) -> datetime:
389
437
GraphQLDateTime = GraphQLScalarType (
390
438
name = "DateTime" ,
391
439
description = (
392
- "The `DateTime` scalar type represents timezone-naive second-accuracy timestamps."
393
- "Values are serialized following the ISO-8601 datetime format specification, "
394
- 'for example "2017-03-21T12:34:56". All of these fields must be included, '
395
- "including the seconds, and the format followed exactly, or the behavior is undefined."
440
+ "The `DateTime` scalar type represents timezone-naive timestamps with up to microsecond "
441
+ "accuracy. Values are serialized following the ISO-8601 datetime format specification, "
442
+ 'for example "2017-03-21T12:34:56.012345" or "2017-03-21T12:34:56". Serialization and '
443
+ "parsing support is guaranteed for the format described here, with all fields down to "
444
+ "and including seconds required to be included, and fractional seconds optional, as in "
445
+ "the example. Implementations are allowed to support additional serialization formats, "
446
+ "if they so choose."
447
+ # GraphQL compiler's implementation of GraphQL-based querying uses the ciso8601 library
448
+ # for date and datetime parsing, so it additionally supports the subset of the ISO-8601
449
+ # standard supported by that library.
396
450
),
397
451
serialize = _serialize_datetime ,
398
452
parse_value = _parse_datetime_value ,
0 commit comments