@@ -118,6 +118,96 @@ def _clean_attribute(
118118 return None
119119
120120
121+ def _clean_extended_attribute_value (
122+ value : types .AttributeValue , max_len : Optional [int ]
123+ ) -> Optional [Union [types .AttributeValue , Tuple [Union [str , int , float ], ...]]]:
124+ # for primitive types just return the value and eventually shorten the string length
125+ if value is None or isinstance (value , _VALID_ATTR_VALUE_TYPES ):
126+ if max_len is not None and isinstance (value , str ):
127+ value = value [:max_len ]
128+ return value
129+
130+ if isinstance (value , Mapping ):
131+ cleaned_dict = {}
132+ for key , element in value .items ():
133+ # skip invalid keys
134+ if not (key and isinstance (key , str )):
135+ _logger .warning (
136+ "invalid key `%s`. must be non-empty string." , key
137+ )
138+ continue
139+
140+ cleaned_dict [key ] = _clean_extended_attribute (
141+ key = key , value = element , max_len = max_len
142+ )
143+
144+ return cleaned_dict
145+
146+ if isinstance (value , Sequence ):
147+ sequence_first_valid_type = None
148+ cleaned_seq = []
149+
150+ for element in value :
151+ if element is None :
152+ cleaned_seq .append (element )
153+ continue
154+
155+ if max_len is not None and isinstance (element , str ):
156+ element = element [:max_len ]
157+
158+ element_type = type (element )
159+ if element_type not in _VALID_ATTR_VALUE_TYPES :
160+ return _clean_extended_attribute (element , max_len = max_len )
161+
162+ # The type of the sequence must be homogeneous. The first non-None
163+ # element determines the type of the sequence
164+ if sequence_first_valid_type is None :
165+ sequence_first_valid_type = element_type
166+ # use equality instead of isinstance as isinstance(True, int) evaluates to True
167+ elif element_type != sequence_first_valid_type :
168+ _logger .warning (
169+ "Mixed types %s and %s in attribute value sequence" ,
170+ sequence_first_valid_type .__name__ ,
171+ type (element ).__name__ ,
172+ )
173+ return None
174+
175+ cleaned_seq .append (element )
176+
177+ # Freeze mutable sequences defensively
178+ return tuple (cleaned_seq )
179+
180+ raise TypeError (
181+ "Invalid type %s for attribute value. Expected one of %s or a "
182+ "sequence of those types" ,
183+ type (value ).__name__ ,
184+ [valid_type .__name__ for valid_type in _VALID_ANY_VALUE_TYPES ],
185+ )
186+
187+
188+ def _clean_extended_attribute (
189+ key : str , value : types .AttributeValue , max_len : Optional [int ]
190+ ) -> Optional [Union [types .AttributeValue , Tuple [Union [str , int , float ], ...]]]:
191+ """Checks if attribute value is valid and cleans it if required.
192+
193+ The function returns the cleaned value or None if the value is not valid.
194+
195+ An attribute value is valid if it is an AnyValue.
196+ An attribute needs cleansing if:
197+ - Its length is greater than the maximum allowed length.
198+ """
199+
200+ if not (key and isinstance (key , str )):
201+ _logger .warning ("invalid key `%s`. must be non-empty string." , key )
202+ return None
203+
204+ try :
205+ return _clean_extended_attribute_value (value , max_len = max_len )
206+ except TypeError as exception :
207+ _logger .warning (f"Attribute { key } : { exception } " )
208+ return None
209+
210+
121211def _clean_attribute_value (
122212 value : types .AttributeValue , limit : Optional [int ]
123213) -> Optional [types .AttributeValue ]:
@@ -149,6 +239,7 @@ def __init__(
149239 attributes : types .Attributes = None ,
150240 immutable : bool = True ,
151241 max_value_len : Optional [int ] = None ,
242+ extended_attributes : bool = False ,
152243 ):
153244 if maxlen is not None :
154245 if not isinstance (maxlen , int ) or maxlen < 0 :
@@ -158,6 +249,7 @@ def __init__(
158249 self .maxlen = maxlen
159250 self .dropped = 0
160251 self .max_value_len = max_value_len
252+ self ._extended_attributes = extended_attributes
161253 # OrderedDict is not used until the maxlen is reached for efficiency.
162254
163255 self ._dict : Union [
@@ -184,19 +276,24 @@ def __setitem__(self, key: str, value: types.AttributeValue) -> None:
184276 self .dropped += 1
185277 return
186278
187- value = _clean_attribute (key , value , self .max_value_len ) # type: ignore
188- if value is not None :
189- if key in self ._dict :
190- del self ._dict [key ]
191- elif (
192- self .maxlen is not None and len (self ._dict ) == self .maxlen
193- ):
194- if not isinstance (self ._dict , OrderedDict ):
195- self ._dict = OrderedDict (self ._dict )
196- self ._dict .popitem (last = False ) # type: ignore
197- self .dropped += 1
198-
199- self ._dict [key ] = value # type: ignore
279+ if self ._extended_attributes :
280+ value = _clean_extended_attribute (
281+ key , value , self .max_value_len
282+ ) # type: ignore
283+ else :
284+ value = _clean_attribute (key , value , self .max_value_len ) # type: ignore
285+ if value is None :
286+ return
287+
288+ if key in self ._dict :
289+ del self ._dict [key ]
290+ elif self .maxlen is not None and len (self ._dict ) == self .maxlen :
291+ if not isinstance (self ._dict , OrderedDict ):
292+ self ._dict = OrderedDict (self ._dict )
293+ self ._dict .popitem (last = False ) # type: ignore
294+ self .dropped += 1
295+
296+ self ._dict [key ] = value # type: ignore
200297
201298 def __delitem__ (self , key : str ) -> None :
202299 if getattr (self , "_immutable" , False ): # type: ignore
0 commit comments