1- from typing import Optional , Set
1+ from typing import Optional , Set , Union
22import logging
33import json
44from datetime import datetime
5- from base64 import b64encode , b64decode
65
76import boto3
87from boto3 .dynamodb .conditions import Key , Attr
@@ -80,25 +79,19 @@ def _to_epoch_float(dt):
8079
8180SERIALIZE_MAP = {
8281 "number" : str , # float or decimal
83- "string" : lambda d : d .isoformat () if isinstance (d , datetime ) else d , # string, datetime
82+ "string" : str ,
83+ "string:date-time" : lambda d : d .isoformat (),
8484 "boolean" : lambda d : 1 if d else 0 ,
8585 "object" : json .dumps ,
8686 "array" : json .dumps ,
87- "anyOf" : str , # FIXME - this could be more complicated. This is a hacky fix.
8887}
8988
9089
91- def do_nothing (x ):
92- return x
93-
94-
9590DESERIALIZE_MAP = {
9691 "number" : float ,
97- "string" : do_nothing ,
9892 "boolean" : bool ,
9993 "object" : json .loads ,
10094 "array" : json .loads ,
101- "anyOf" : do_nothing , # FIXME - this could be more complicated. This is a hacky fix.
10295}
10396
10497
@@ -120,22 +113,48 @@ def index_definition(index_name, keys, gsi=False):
120113
121114class DynamoSerializer :
122115 def __init__ (self , schema ):
123- self .schema = schema
116+ self .properties = schema ["properties" ]
117+ self .definitions = schema .get ("definitions" )
118+
119+ def _get_type_possibilities (self , field_name ) -> Set [tuple ]:
120+ field_properties = self .properties [field_name ]
121+
122+ possible_types = []
123+ if "anyOf" in field_properties :
124+ possible_types .extend ([r .get ("$ref" , r ) for r in field_properties ["anyOf" ]])
125+ else :
126+ possible_types .append (field_properties .get ("$ref" , field_properties ))
127+
128+ def type_from_definition (definition_signature : Union [str , dict ]) -> dict :
129+ if isinstance (definition_signature , str ):
130+ t = definition_signature .split ('/' )[- 1 ]
131+ return self .definitions [t ]
132+ return definition_signature
133+
134+ type_dicts = [
135+ type_from_definition (t )
136+ for t in possible_types
137+ ]
138+
139+ return set ([
140+ (t ['type' ], t .get ('format' , '' ))
141+ for t in type_dicts
142+ ])
124143
125144 def _serialize_field (self , field_name , value ):
126- definition = self .schema . get ( "definitions" )
127- schema = self . schema [ "properties" ]
128- if definition :
129- for k , v in definition . items () :
130- schema [ k . lower ()] = v
131- schema = self . schema [ "properties" ]
132- field_type = schema [ field_name ]. get ( "type" , "anyOf" )
133- try :
134- if any ([ field_name in self . schema [ 'required' ], value is not None ]):
135- return SERIALIZE_MAP [ field_type ]( value )
136- except KeyError :
137- log . debug ( f"No serializer for field_type { field_type } " )
138- return value # do nothing but log it.
145+ field_types = self ._get_type_possibilities ( field_name )
146+ if value is not None :
147+ for t in field_types :
148+ try :
149+ type_signature = ":" . join ( t ). rstrip ( ':' )
150+ try :
151+ return SERIALIZE_MAP [ type_signature ]( value )
152+ except KeyError :
153+ return SERIALIZE_MAP [ t [ 0 ]]( value )
154+ except ( ValueError , TypeError , KeyError ):
155+ pass
156+
157+ return value
139158
140159 def serialize_record (self , data_dict ) -> dict :
141160 """
@@ -147,18 +166,19 @@ def serialize_record(self, data_dict) -> dict:
147166 }
148167
149168 def _deserialize_field (self , field_name , value ):
150- definition = self .schema .get ("definitions" )
151- schema = self .schema ["properties" ]
152- if definition :
153- for k , v in definition .items ():
154- schema [k .lower ()] = v
155- field_type = schema [field_name ].get ("type" , "anyOf" )
156- try :
157- if any ([field_name in self .schema ['required' ], value is not None ]):
158- return DESERIALIZE_MAP [field_type ](value )
159- except KeyError :
160- log .debug (f"No deserializer for field_type { field_type } " )
161- return value # do nothing but log it.
169+ field_types = self ._get_type_possibilities (field_name )
170+ if value is not None :
171+ for t in field_types :
172+ try :
173+ type_signature = ":" .join (t ).rstrip (':' )
174+ try :
175+ return DESERIALIZE_MAP [type_signature ](value )
176+ except KeyError :
177+ return DESERIALIZE_MAP [t [0 ]](value )
178+ except (ValueError , TypeError , KeyError ):
179+ pass
180+
181+ return value
162182
163183 def deserialize_record (self , data_dict ) -> dict :
164184 """
@@ -213,7 +233,7 @@ def _key_param_to_dict(self, key):
213233 }
214234 if self .range_key :
215235 if not isinstance (key , tuple ) or not len (key ) == 2 :
216- raise ValueError (f"{ self .table_name } needs both a hash_key and a range_key to delete a record ." )
236+ raise ValueError (f"{ self .table_name } needs both a hash_key and a range_key." )
217237 _key = {
218238 self .hash_key : key [0 ],
219239 self .range_key : key [1 ]
@@ -256,7 +276,7 @@ def initialize(self):
256276 {
257277 "AttributeName" : attr ,
258278 "AttributeType" : DYNAMO_TYPE_MAP .get (
259- schema ["properties" ][attr ].get ("type" , "anyOf" ), "S"
279+ schema ["properties" ][attr ].get ("type" ), "S"
260280 ),
261281 }
262282 for attr in self .possible_keys
0 commit comments