Skip to content

Commit 6585a10

Browse files
authored
Add custom JSONEncoder for model serialization (Azure#19595)
1 parent 2a927b2 commit 6585a10

File tree

2 files changed

+536
-6
lines changed

2 files changed

+536
-6
lines changed

sdk/core/azure-core/azure/core/serialization.py

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,117 @@
44
# Licensed under the MIT License. See License.txt in the project root for
55
# license information.
66
# --------------------------------------------------------------------------
7+
import base64
8+
from json import JSONEncoder
9+
from typing import TYPE_CHECKING
10+
11+
from .utils._utils import _FixedOffset
12+
13+
if TYPE_CHECKING:
14+
from datetime import timedelta
15+
16+
__all__ = ["NULL", "AzureJSONEncoder"]
717

8-
__all__ = ["NULL"]
918

1019
class _Null(object):
11-
"""To create a Falsy object
12-
"""
20+
"""To create a Falsy object"""
21+
1322
def __bool__(self):
1423
return False
1524

16-
__nonzero__ = __bool__ # Python2 compatibility
25+
__nonzero__ = __bool__ # Python2 compatibility
1726

1827

1928
NULL = _Null()
2029
"""
2130
A falsy sentinel object which is supposed to be used to specify attributes
2231
with no data. This gets serialized to `null` on the wire.
2332
"""
33+
34+
35+
def _timedelta_as_isostr(value):
36+
# type: (timedelta) -> str
37+
"""Converts a datetime.timedelta object into an ISO 8601 formatted string, e.g. 'P4DT12H30M05S'
38+
39+
Function adapted from the Tin Can Python project: https://github.com/RusticiSoftware/TinCanPython
40+
"""
41+
42+
# Split seconds to larger units
43+
seconds = value.total_seconds()
44+
minutes, seconds = divmod(seconds, 60)
45+
hours, minutes = divmod(minutes, 60)
46+
days, hours = divmod(hours, 24)
47+
48+
days, hours, minutes = list(map(int, (days, hours, minutes)))
49+
seconds = round(seconds, 6)
50+
51+
# Build date
52+
date = ""
53+
if days:
54+
date = "%sD" % days
55+
56+
# Build time
57+
time = "T"
58+
59+
# Hours
60+
bigger_exists = date or hours
61+
if bigger_exists:
62+
time += "{:02}H".format(hours)
63+
64+
# Minutes
65+
bigger_exists = bigger_exists or minutes
66+
if bigger_exists:
67+
time += "{:02}M".format(minutes)
68+
69+
# Seconds
70+
try:
71+
if seconds.is_integer():
72+
seconds_string = "{:02}".format(int(seconds))
73+
else:
74+
# 9 chars long w/ leading 0, 6 digits after decimal
75+
seconds_string = "%09.6f" % seconds
76+
# Remove trailing zeros
77+
seconds_string = seconds_string.rstrip("0")
78+
except AttributeError: # int.is_integer() raises
79+
seconds_string = "{:02}".format(seconds)
80+
81+
time += "{}S".format(seconds_string)
82+
83+
return "P" + date + time
84+
85+
86+
try:
87+
from datetime import timezone
88+
89+
TZ_UTC = timezone.utc # type: ignore
90+
except ImportError:
91+
TZ_UTC = _FixedOffset(0) # type: ignore
92+
93+
94+
class AzureJSONEncoder(JSONEncoder):
95+
"""A JSON encoder that's capable of serializing datetime objects and bytes."""
96+
97+
def default(self, o): # pylint: disable=too-many-return-statements
98+
try:
99+
return super(AzureJSONEncoder, self).default(o)
100+
except TypeError:
101+
if isinstance(o, (bytes, bytearray)):
102+
return base64.b64encode(o).decode()
103+
try:
104+
# First try datetime.datetime
105+
if hasattr(o, "year") and hasattr(o, "hour"):
106+
# astimezone() fails for naive times in Python 2.7, so make make sure o is aware (tzinfo is set)
107+
if not o.tzinfo:
108+
return o.replace(tzinfo=TZ_UTC).isoformat()
109+
return o.astimezone(TZ_UTC).isoformat()
110+
# Next try datetime.date or datetime.time
111+
return o.isoformat()
112+
except AttributeError:
113+
pass
114+
# Last, try datetime.timedelta
115+
try:
116+
return _timedelta_as_isostr(o)
117+
except AttributeError:
118+
# This will be raised when it hits value.total_seconds in the method above
119+
pass
120+
return super(AzureJSONEncoder, self).default(o)

0 commit comments

Comments
 (0)