clear-skies-aws 1.10.2__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. {clear_skies_aws-1.10.2.dist-info → clear_skies_aws-2.0.1.dist-info}/METADATA +36 -35
  2. clear_skies_aws-2.0.1.dist-info/RECORD +4 -0
  3. {clear_skies_aws-1.10.2.dist-info → clear_skies_aws-2.0.1.dist-info}/WHEEL +1 -1
  4. clear_skies_aws-2.0.1.dist-info/licenses/LICENSE +21 -0
  5. clear_skies_aws-1.10.2.dist-info/LICENSE +0 -7
  6. clear_skies_aws-1.10.2.dist-info/RECORD +0 -71
  7. clearskies_aws/__init__.py +0 -2
  8. clearskies_aws/actions/__init__.py +0 -108
  9. clearskies_aws/actions/action_aws.py +0 -118
  10. clearskies_aws/actions/assume_role.py +0 -102
  11. clearskies_aws/actions/assume_role_test.py +0 -72
  12. clearskies_aws/actions/ses.py +0 -194
  13. clearskies_aws/actions/ses_test.py +0 -89
  14. clearskies_aws/actions/sns.py +0 -64
  15. clearskies_aws/actions/sns_test.py +0 -77
  16. clearskies_aws/actions/sqs.py +0 -82
  17. clearskies_aws/actions/sqs_test.py +0 -127
  18. clearskies_aws/actions/step_function.py +0 -66
  19. clearskies_aws/actions/step_function_test.py +0 -103
  20. clearskies_aws/backends/__init__.py +0 -12
  21. clearskies_aws/backends/dynamo_db_backend.py +0 -614
  22. clearskies_aws/backends/dynamo_db_backend_test.py +0 -300
  23. clearskies_aws/backends/dynamo_db_condition_parser.py +0 -365
  24. clearskies_aws/backends/dynamo_db_condition_parser_test.py +0 -266
  25. clearskies_aws/backends/dynamo_db_parti_ql_backend.py +0 -1123
  26. clearskies_aws/backends/dynamo_db_parti_ql_backend_test.py +0 -544
  27. clearskies_aws/backends/sqs_backend.py +0 -80
  28. clearskies_aws/backends/sqs_backend_test.py +0 -31
  29. clearskies_aws/contexts/__init__.py +0 -10
  30. clearskies_aws/contexts/cli.py +0 -19
  31. clearskies_aws/contexts/cli_websocket_mock.py +0 -33
  32. clearskies_aws/contexts/lambda_api_gateway.py +0 -30
  33. clearskies_aws/contexts/lambda_api_gateway_web_socket.py +0 -30
  34. clearskies_aws/contexts/lambda_elb.py +0 -30
  35. clearskies_aws/contexts/lambda_http_gateway.py +0 -30
  36. clearskies_aws/contexts/lambda_invocation.py +0 -48
  37. clearskies_aws/contexts/lambda_sns.py +0 -43
  38. clearskies_aws/contexts/lambda_sqs_standard_partial_batch.py +0 -51
  39. clearskies_aws/contexts/lambda_sqs_standard_partial_batch_test.py +0 -66
  40. clearskies_aws/contexts/wsgi.py +0 -19
  41. clearskies_aws/di/__init__.py +0 -1
  42. clearskies_aws/di/standard_dependencies.py +0 -60
  43. clearskies_aws/handlers/__init__.py +0 -2
  44. clearskies_aws/handlers/secrets_manager_rotation.py +0 -174
  45. clearskies_aws/handlers/simple_body_routing.py +0 -39
  46. clearskies_aws/input_outputs/__init__.py +0 -8
  47. clearskies_aws/input_outputs/cli_websocket_mock.py +0 -12
  48. clearskies_aws/input_outputs/lambda_api_gateway.py +0 -105
  49. clearskies_aws/input_outputs/lambda_api_gateway_test.py +0 -87
  50. clearskies_aws/input_outputs/lambda_api_gateway_web_socket.py +0 -8
  51. clearskies_aws/input_outputs/lambda_elb.py +0 -21
  52. clearskies_aws/input_outputs/lambda_http_gateway.py +0 -12
  53. clearskies_aws/input_outputs/lambda_invocation.py +0 -34
  54. clearskies_aws/input_outputs/lambda_sns.py +0 -52
  55. clearskies_aws/input_outputs/lambda_sqs_standard.py +0 -54
  56. clearskies_aws/mocks/__init__.py +0 -1
  57. clearskies_aws/mocks/actions/__init__.py +0 -6
  58. clearskies_aws/mocks/actions/ses.py +0 -28
  59. clearskies_aws/mocks/actions/sns.py +0 -23
  60. clearskies_aws/mocks/actions/sqs.py +0 -23
  61. clearskies_aws/mocks/actions/step_function.py +0 -26
  62. clearskies_aws/secrets/__init__.py +0 -7
  63. clearskies_aws/secrets/additional_configs/__init__.py +0 -54
  64. clearskies_aws/secrets/additional_configs/iam_db_auth.py +0 -29
  65. clearskies_aws/secrets/additional_configs/iam_db_auth_with_ssm.py +0 -92
  66. clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +0 -81
  67. clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssm_bastion.py +0 -141
  68. clearskies_aws/secrets/akeyless_with_ssm_cache.py +0 -46
  69. clearskies_aws/secrets/parameter_store.py +0 -50
  70. clearskies_aws/secrets/parameter_store_test.py +0 -18
  71. clearskies_aws/secrets/secrets_manager.py +0 -75
  72. clearskies_aws/secrets/secrets_manager_test.py +0 -18
  73. clearskies_aws/web_socket_connection_model.py +0 -43
@@ -1,614 +0,0 @@
1
- import base64
2
- import json
3
- from decimal import Decimal
4
- from typing import Any, Callable, Dict, List, Tuple
5
-
6
- from boto3.dynamodb import conditions as dynamodb_conditions
7
- from clearskies import model
8
- from clearskies.autodoc.schema import String as AutoDocString
9
- from clearskies.backends.backend import Backend
10
- from clearskies.column_types.boolean import Boolean
11
- from clearskies.column_types.float import Float
12
- from clearskies.column_types.integer import Integer
13
-
14
-
15
- class DynamoDBBackend(Backend):
16
- """
17
- DynamoDB is complicated.
18
-
19
- The issue is that we can't arbitrarily search/sort on columns (aka attributes). In order to perform meaningful
20
- filtering on an attribute, then there must be an index which has that attriubte set as its HASH/Partition.
21
- Sorting or searching outside of indexes doesn't work the same way as with a typical SQL database (which will
22
- scan all records and search/sort accordingly). With DynamoDB AWS fetches a maximum number of records out of the
23
- table, and then performs sorting/filtering on those. The searching will always happen on a subset of the data,
24
- unless there are a sufficiently small number of records or the there is a supporting index. For sorting, DynamoDB
25
- will not attempt to sort at all unless there is a supporting search attribute set in the index.
26
-
27
- "true" searching is only possible on indexes (either the primary index or a global secondary index). For such
28
- cases, DynamoDB can perform basic searching operations against the HASH/Partition attribute in such an index.
29
- However, this still doesn't let us perform arbitrary sorting. Instead, each index can have an optional RANGE/Sort key.
30
- If this exists, then we can sort in either ascending (the default) or descending order only if we have first
31
- filtered on the HASH/partition attribute. This is the extent of sorting. It is not possible to sort arbitrary attributes
32
- or specify multiple sort conditions. To repeat a bit more succinctly: DynamoDB can only filter against an attribute
33
- that has an index set for it, and then can only sort filtered results if the index has the sort attribute set in the
34
- RANGE/Sort attribute of the index.
35
-
36
- This makes for very limited sorting capabilities. To help with this a little, DynamoDB offers local secondary indexes.
37
- These indexes allow you to specify an additional sort attribute for a column that is already indexed (either via the
38
- primary index or a global secondary index). In practice, changing the sort column means selecting a different index
39
- when filtering results.
40
-
41
- Let's bring it all together with an example. Imagine we have a table that represents books, and has the following
42
- attributes:
43
-
44
- 1. Author
45
- 2. Title
46
- 3. Year Published
47
- 4. Genre
48
-
49
- The primary index for our table has:
50
-
51
- HASH/Partition: Author
52
- RANGE/Sort: Title
53
-
54
- We have a global secondary index:
55
-
56
- HASH/Partition: Genre
57
- RANGE/Sort: Title
58
-
59
- And a local secondary index:
60
-
61
- HASH/Partition: Author
62
- Range/Sort: Year Published
63
-
64
- This combination of indexes would allow us to filter/sort in the following ways:
65
-
66
- 1. Filter by Author, sort by Title
67
- 2. Filter by Author, sort by Year Published
68
- 3. Filter by Genre, sort by Title
69
-
70
- Any other filter/sort options will become unreliable as soon as the table grows past the maximum result size.
71
- """
72
-
73
- _boto3 = None
74
- _environment = None
75
- _dynamodb = None
76
-
77
- _allowed_configs = [
78
- 'table_name',
79
- 'wheres',
80
- 'sorts',
81
- 'limit',
82
- 'pagination',
83
- 'model_columns',
84
- ]
85
-
86
- _required_configs = [
87
- 'table_name',
88
- ]
89
-
90
- _table_indexes = None
91
-
92
- _model_columns_cache = None
93
-
94
- # this is the list of operators that we can use when querying a dynamodb index and their corresponding
95
- # key method name in dynamodb
96
- _index_operators = {
97
- '=': 'eq',
98
- '<': 'lt',
99
- '>': 'gt',
100
- '>=': 'gte',
101
- '<=': 'lte',
102
- }
103
-
104
- # this is a map from clearskies operators to the equivalent dynamodb attribute operators
105
- _attribute_operators = {
106
- '!=': 'ne',
107
- '<=': 'lte',
108
- '>=': 'gte',
109
- '>': 'gt',
110
- '<': 'lt',
111
- '=': 'eq',
112
- 'IS NOT NULL': 'exists',
113
- 'IS NULL': 'not_exists',
114
- 'IS NOT': 'ne',
115
- 'IS': 'eq',
116
- 'LIKE': '', # requires special handling
117
- }
118
-
119
- def __init__(self, boto3, environment):
120
- self._boto3 = boto3
121
- self._environment = environment
122
- if not environment.get('AWS_REGION', True):
123
- raise ValueError('To use DynamoDB you must use set AWS_REGION in the .env file or an environment variable')
124
-
125
- self._dynamodb = self._boto3.resource('dynamodb', region_name=environment.get('AWS_REGION', True))
126
- self._table_indexes = {}
127
- self._model_columns_cache = {}
128
-
129
- def configure(self):
130
- pass
131
-
132
- def update(self, id, data, model):
133
- # when we run an update column we must include the sort column on the primary
134
- # index (if it exists)
135
- sort_column_name = self._find_primary_sort_column(model)
136
- key = {model.id_column_name: model.__getattr__(model.id_column_name)}
137
- if sort_column_name:
138
- key[sort_column_name] = data.get(
139
- sort_column_name,
140
- model.columns()[sort_column_name].to_backend(model._data)
141
- )
142
- table = self._dynamodb.Table(model.table_name())
143
-
144
- data = self.excessive_type_casting(data)
145
-
146
- updated = table.update_item(
147
- Key=key,
148
- UpdateExpression='SET ' + ', '.join([f"#{column_name} = :{column_name}" for column_name in data.keys()]),
149
- ExpressionAttributeValues={
150
- **{f':{column_name}': value
151
- for (column_name, value) in data.items()},
152
- },
153
- ExpressionAttributeNames={
154
- **{f'#{column_name}': column_name
155
- for column_name in data.keys()},
156
- },
157
- ReturnValues="ALL_NEW",
158
- )
159
- return self._map_from_boto3(updated['Attributes'])
160
-
161
- def create(self, data, model):
162
- table = self._dynamodb.Table(model.table_name())
163
- table.put_item(Item=data)
164
- return {**data}
165
-
166
- def excessive_type_casting(self, data):
167
- for (key, value) in data.items():
168
- if isinstance(value, float):
169
- data[key] = Decimal(value)
170
- return data
171
-
172
- def delete(self, id, model):
173
- table = self._dynamodb.Table(model.table_name())
174
- table.delete_item(Key={model.id_column_name: model.__getattr__(model.id_column_name)})
175
- return True
176
-
177
- def count(self, configuration, model):
178
- response = self._dynamodb_query(configuration, model, 'COUNT')
179
- return response['Count']
180
-
181
- def records(self,
182
- configuration: Dict[str, Any],
183
- model: model.Model,
184
- next_page_data: Dict[str, str] = None) -> List[Dict[str, Any]]:
185
- response = self._dynamodb_query(configuration, model, 'ALL_ATTRIBUTES')
186
- if 'LastEvaluatedKey' in response and response['LastEvaluatedKey'] is not None and type(next_page_data) == dict:
187
- next_page_data['next_token'] = self.serialize_next_token_for_response(
188
- self._map_from_boto3(response['LastEvaluatedKey'])
189
- )
190
- return [self._map_from_boto3(item) for item in response['Items']]
191
-
192
- def _dynamodb_query(self, configuration, model, select_type):
193
- [filter_expression, key_condition_expression, index_name,
194
- scan_index_forward] = self._create_dynamodb_query_parameters(configuration, model)
195
- table = self._dynamodb.Table(model.table_name())
196
-
197
- # so we want to put together the kwargs for scan/query:
198
- kwargs = {
199
- 'IndexName': index_name,
200
- 'KeyConditionExpression': key_condition_expression,
201
- 'FilterExpression': filter_expression,
202
- 'Select': select_type,
203
- 'ExclusiveStartKey': self.restore_next_token_from_config(configuration['pagination'].get('next_token')),
204
- 'Limit': configuration['limit'] if configuration['limit'] and select_type != 'COUNT' else None
205
- }
206
- # the trouble is that boto3 isn't okay with parameters of None.
207
- # therefore, we need to remove any of the above keys that are None
208
- kwargs = {key: value for (key, value) in kwargs.items() if value is not None}
209
-
210
- if key_condition_expression:
211
- # add the scan index forward setting for key conditions
212
- kwargs['ScanIndexForward'] = scan_index_forward
213
- return table.query(**kwargs)
214
- return table.scan(**kwargs)
215
-
216
- def _create_dynamodb_query_parameters(self, configuration, model):
217
- # DynamoDB only supports sorting by a single column, and only if we can find a supporting index
218
- # figure out if and what we are sorting by.
219
- sort_column = None
220
- sort_direction = 'asc'
221
- if 'sorts' in configuration and configuration['sorts']:
222
- sort_column = configuration['sorts'][0]['column']
223
- sort_direction = configuration['sorts'][0]['direction']
224
-
225
- # if we have neither sort nor a where then we have a simple query and can finish up now.
226
- if not sort_column and not configuration['wheres']:
227
- return [None, None, None, True]
228
-
229
- # so the thing here is that if we find a condition that corresponds to an indexed
230
- # column, then we may be able to use an index, which allows us to use the `query`
231
- # method of dynamodb. Otherwise though we have to perform a scan operation, which
232
- # only filters over a subset of records. We also have to convert our query conditions
233
- # into dynamodb conditions. Finally, note that not all operators are supported by
234
- # the query operation in dynamodb, so searching on an indexed column doesn't guarantee
235
- # that we can use a query.
236
- [key_condition_expression, index_name, remaining_conditions] = self._find_key_condition_expressions(
237
- configuration['wheres'],
238
- model.id_column_name,
239
- sort_column,
240
- model,
241
- )
242
-
243
- return [
244
- self._as_attr_filter_expressions(remaining_conditions, model),
245
- key_condition_expression,
246
- index_name, # we don't need to specify the name of the primary index
247
- sort_direction.lower() == 'asc',
248
- ]
249
-
250
- def _find_key_condition_expressions(self, conditions, id_column_name, sort_column, model):
251
- indexes = self._get_indexes_for_model(model)
252
- # we're going to do a lot to this array, so let's make sure and work on a copy to avoid
253
- # the potential for subtle errors in the future.
254
- conditions = [*conditions]
255
-
256
- # let's make this easy and sort out conditions that are on an indexed column. While we're at it, apply
257
- # some weights to decide which index to use. This is slightly tricky because there can be more than one
258
- # index to use and we can't necessarily know for sure which to use. In general though, we can only search
259
- # on an index if we have an equals search in the hash attribute. After that, we can either search on the
260
- # range parameter for the index or perform simple searches (=, <, <=, >, >=) on the range parameter of the
261
- # index. Therefore we can have ambiguity if there is an 'equals' search on multiple columns that are the
262
- # hash attribute in different indexes. We also get some ambiguity if, after filtering on the hash index,
263
- # we have a local index that matches the sort parameter and another index that matches a secondary search
264
- # in the query. These are largely edge cases, so for now we'll pick a heuristic and make another approach
265
- # down the road (likely by giving the programmer a way to specify which index to use).
266
-
267
- # So what do we do? From a practical perspective, we want to figure out which conditions correspond
268
- # to a searchable index, and then which ones may be usable as a secondary index. Then we want to
269
- # choose which index to use with which conditions, and shove the rest into the remaining conditions
270
- # which we return. Therefore, we need to collect some information about each condition
271
- id_conditions = []
272
- indexable_conditions = []
273
- secondary_conditions = []
274
- for (index, condition) in enumerate(conditions):
275
- column_name = condition['column']
276
- # if the column isn't a hash index and isn't an equals search, then this condition "anchor" an index search.
277
- if column_name not in indexes or condition['operator'] != '=':
278
- # however, it may still contribute to a secondary condition in an index search, so record it
279
- # if it uses a supporting operator
280
- if condition['operator'] in self._index_operators:
281
- secondary_conditions.append(index)
282
-
283
- # if we get here then we have an '=' condition on a hash attribute in an index - we can use an index!
284
- else:
285
- # even better if it is for the id column!
286
- if column_name == model.id_column_name:
287
- id_conditions.append(index)
288
- else:
289
- indexable_conditions.append(index)
290
-
291
- # Okay then! We can start working through our use cases. First of all, if we have an id=[value]
292
- # search condition, and the id column is indexed, then just use that.
293
- if id_conditions:
294
- return self._finalize_key_condition_expression(
295
- conditions,
296
- id_conditions[0],
297
- secondary_conditions,
298
- sort_column,
299
- indexes,
300
- model,
301
- )
302
-
303
- # if we don't have an id condition but do have conditions that are performing an `=` search
304
- # on HASH attributes, then we can also use an index! Unfortunately, if we have more than one
305
- # of these, then we have no way to know which to use without some hints from the developer.
306
- # for now, just use the first, but we'll add in index-hinting down the line if we need it.
307
- if indexable_conditions:
308
- return self._finalize_key_condition_expression(
309
- conditions,
310
- indexable_conditions[0],
311
- secondary_conditions,
312
- sort_column,
313
- indexes,
314
- model,
315
- )
316
-
317
- # If we get here then we can't use an index :(
318
- return [None, None, conditions]
319
-
320
- def _finalize_key_condition_expression(
321
- self,
322
- conditions,
323
- primary_condition_index,
324
- secondary_condition_indexes,
325
- sort_column,
326
- indexes,
327
- model,
328
- ):
329
- """
330
- Our job is to figure out exactly which index to use, and build the key expression.
331
-
332
- We basically tell it everything we know, including what the "primary" condition is,
333
- i.e. the condition that we expect to match against the HASH attribute of an index. This
334
- is *always* a `[column]=[value]` condition, because that is all DynamoDB supports, and
335
- the calling method must guarantee that there is an index on the table that has the given
336
- column as a HASH attribute.
337
-
338
- So why do we need to do anything else if the caller already knows which column it wants to
339
- sort on, and that there is an index with that column as a HASH attribute? Because of the
340
- RANGE attribute, i.e. the second column in the index! You can specify this second column
341
- to support sorting after searching on the HASH column, or to perform additional filtering
342
- after filtering on the hash column. Local secondary indexes make it possible to create
343
- multiple indexes with the same HASH attribute but different RANGE attributes, which means
344
- that even if we know what the "primary" column is that we want to search on, there is still
345
- a possibility that we want to select different indexes depending on what our sort column
346
- is or what additional conditions we have in our query.
347
-
348
- The goal of this function is to sort that all out, decide which index we want to use
349
- for our query, build the appropriate key expression, and return a new list of conditions
350
- which has the conditions used in the key expression removed. Those left over conditions
351
- are then destined for the FilterExpression.
352
- """
353
- # the condition for the primary condition
354
- index_condition = conditions[primary_condition_index]
355
- index_data = indexes[index_condition['column']]
356
-
357
- # our secondary columns are just suggestions, so see if we can actually use any
358
- index_condition_counts = {}
359
- for condition_index in secondary_condition_indexes:
360
- secondary_condition = conditions[condition_index]
361
- secondary_column = secondary_condition['column']
362
- if secondary_column not in index_data['sortable_columns']:
363
- continue
364
- secondary_index = index_data['sortable_columns'][secondary_column]
365
- if secondary_index not in index_condition_counts:
366
- index_condition_counts[secondary_index] = {'count': 0, 'condition_indexes': []}
367
- index_condition_counts[secondary_index]['count'] += 1
368
- index_condition_counts[secondary_index]['condition_indexes'].append(condition_index)
369
-
370
- # now we can decide which index to use. Prefer an index that hits some secondary conditions,
371
- # or an index that hits the sort column, or the default index.
372
- used_condition_indexes = [primary_condition_index]
373
- if index_condition_counts:
374
- index_to_use = max(index_condition_counts, key=lambda key: index_condition_counts[key]['count'])
375
- used_condition_indexes.extend(index_condition_counts[index_to_use]['condition_indexes'])
376
- elif sort_column in index_data['sortable_columns']:
377
- index_to_use = index_data['sortable_columns'][sort_column]
378
- else:
379
- index_to_use = index_data['default_index_name']
380
-
381
- # now build our key expression. For every condition in used_condition_indexes, add it to
382
- # a key expression, and remove it from the conditions array. Do this backwards to make sure
383
- # that we don't change the meaning of the indexes
384
- used_condition_indexes.sort()
385
- used_condition_indexes.reverse()
386
- key_condition_expression = None
387
- for condition_index in used_condition_indexes:
388
- condition = conditions[condition_index]
389
- dynamodb_operator_method = self._index_operators[condition['operator']]
390
- raw_search_value = condition['values'][0] if condition['values'] else None
391
- value = self._value_for_condition_expression(raw_search_value, condition['column'], model)
392
- condition_expression = getattr(dynamodb_conditions.Key(condition['column']),
393
- dynamodb_operator_method)(value)
394
- # add to our key condition expression
395
- if key_condition_expression is None:
396
- key_condition_expression = condition_expression
397
- else:
398
- key_condition_expression &= condition_expression
399
-
400
- # and remove this condition from our list of conditions
401
- del conditions[condition_index]
402
-
403
- return [
404
- key_condition_expression,
405
- index_to_use,
406
- conditions,
407
- ]
408
-
409
- def _as_attr_filter_expressions(self, conditions, model):
410
- filter_expression = None
411
- for condition in conditions:
412
- operator = condition['operator']
413
- value = condition['values'][0] if condition['values'] else None
414
- column_name = condition['column']
415
- if operator not in self._attribute_operators:
416
- raise ValueError(
417
- f"I was asked to filter by operator '{operator}' but this operator is not supported by DynamoDB"
418
- )
419
-
420
- # a couple of our operators require special handling
421
- if operator == 'LIKE':
422
- if value[0] != '%' and value[-1] == '%':
423
- condition_expression = dynamodb_conditions.Attr(column_name).begins_with(value.rstrip('%'))
424
- elif value[0] == '%' and value[-1] != '%':
425
- raise ValueError("DynamoDB doesn't support the 'ends_with' operator")
426
- elif value[0] == '%' and value[-1] == '%':
427
- condition_expression = dynamodb_conditions.Attr(column_name).contains(value.strip('%'))
428
- else:
429
- condition_expression = dynamodb_conditions.Attr(column_name).eq(value)
430
- elif operator == 'IS NULL':
431
- condition_expression = dynamodb_conditions.Attr(column_name).exists()
432
- elif operator == 'IS NOT NULL':
433
- condition_expression = dynamodb_conditions.Attr(column_name).not_exists()
434
- else:
435
- dynamodb_operator = self._attribute_operators[operator]
436
- value = self._value_for_condition_expression(value, column_name, model)
437
- condition_expression = getattr(dynamodb_conditions.Attr(column_name), dynamodb_operator)(value)
438
-
439
- if filter_expression is None:
440
- filter_expression = condition_expression
441
- else:
442
- filter_expression &= condition_expression
443
-
444
- return filter_expression
445
-
446
- def _value_for_condition_expression(self, value, column_name, model):
447
- # basically, if the column is an integer/float type, then we need to convert to Decimal
448
- # or dynamodb can't search properly.
449
- if id(model) not in self._model_columns_cache:
450
- self._model_columns_cache[id(model)] = model.columns()
451
-
452
- model_columns = self._model_columns_cache[id(model)]
453
- if column_name not in model_columns:
454
- return value
455
-
456
- if isinstance(model_columns[column_name], Float) or isinstance(model_columns[column_name], Integer):
457
- return Decimal(value)
458
-
459
- return value
460
-
461
- def _get_indexes_for_model(self, model):
462
- """ Loads up the indexes for the DynamoDB table for the given model """
463
- if model.table_name() in self._table_indexes:
464
- return self._table_indexes[model.table_name()]
465
-
466
- # Store the indexes by column name. The HASH attribute for each key is basically
467
- # an indexed column, and the RANGE attribute is a column we can sort by.
468
- # Note that a column can have multiple indexes which allows to sort on different
469
- # columns. Therefore we'll combine all of this into a dictionary that looks something
470
- # like this:
471
- # { "column_name": {
472
- # "default_index_name": "index_name",
473
- # "sortable_columns": {
474
- # "column_for_sort": "another_index_name",
475
- # "another_column_for_sort": "a_third_index_name"
476
- # }
477
- # } }
478
- # etc. Therefore, each column with a HASH/Partition index gets an entry in the main dict,
479
- # and then is further subdivided for columns that have RANGE/Sort attributes, giving you
480
- # the index name for that HASH+RANGE combination.
481
- table_indexes = {}
482
- table = self._dynamodb.Table(model.table_name())
483
- schemas = []
484
- # the primary index for the table doesn't have a name, and it will be used by default
485
- # if we don't specify an index name. Therefore, we just pass around None for it's name
486
- schemas.append({'IndexName': None, 'KeySchema': table.key_schema})
487
- global_secondary_indexes = table.global_secondary_indexes
488
- local_secondary_indexes = table.local_secondary_indexes
489
- if global_secondary_indexes is not None:
490
- schemas.extend(table.global_secondary_indexes)
491
- if local_secondary_indexes is not None:
492
- schemas.extend(table.local_secondary_indexes)
493
- for schema in schemas:
494
- hash_column = ''
495
- range_column = ''
496
- for key in schema['KeySchema']:
497
- if key['KeyType'] == 'RANGE':
498
- range_column = key['AttributeName']
499
- if key['KeyType'] == 'HASH':
500
- hash_column = key['AttributeName']
501
- if hash_column not in table_indexes:
502
- table_indexes[hash_column] = {'default_index_name': schema['IndexName'], 'sortable_columns': {}}
503
- if range_column:
504
- table_indexes[hash_column]['sortable_columns'][range_column] = schema['IndexName']
505
-
506
- self._table_indexes[model.table_name()] = table_indexes
507
- return table_indexes
508
-
509
- def _find_primary_sort_column(self, model):
510
- indexes = self._get_indexes_for_model(model)
511
- primary_indexes = indexes.get(model.id_column_name)
512
- if not primary_indexes:
513
- return None
514
- for (column_name, index_name) in primary_indexes['sortable_columns'].items():
515
- # the primary index doesn't have a name, so we want the record with a name of None
516
- if index_name is None:
517
- return column_name
518
- return None
519
-
520
- def _map_from_boto3(self, record):
521
- return {key: self._map_from_boto3_value(value) for (key, value) in record.items()}
522
-
523
- def _map_from_boto3_value(self, value):
524
- if isinstance(value, Decimal):
525
- return float(value)
526
- return value
527
-
528
- def _check_query_configuration(self, configuration, model):
529
- for key in configuration.keys():
530
- if key not in self._allowed_configs:
531
- raise KeyError(f"DynamoDBBackend does not support config '{key}'. You may be using the wrong backend")
532
-
533
- for key in self._required_configs:
534
- if key not in configuration:
535
- raise KeyError(f'Missing required configuration key {key}')
536
-
537
- for key in self._allowed_configs:
538
- if not key in configuration:
539
- configuration[key] = [] if key[-1] == 's' else ''
540
-
541
- return configuration
542
-
543
- def validate_pagination_kwargs(self, kwargs: Dict[str, Any], case_mapping: Callable) -> str:
544
- extra_keys = set(kwargs.keys()) - set(self.allowed_pagination_keys())
545
- if len(extra_keys):
546
- key_name = case_mapping('next_token')
547
- return "Invalid pagination key(s): '" + "','".join(extra_keys) + f"'. Only '{key_name}' is allowed"
548
- if 'next_token' not in kwargs:
549
- key_name = case_mapping('next_token')
550
- return f"You must specify '{key_name}' when setting pagination"
551
- # the next token should be a urlsafe-base64 encoded JSON string
552
- try:
553
- json.loads(base64.urlsafe_b64decode(kwargs['next_token']))
554
- except:
555
- key_name = case_mapping('next_token')
556
- return "The provided '{key_name}' appears to be invalid."
557
- return ''
558
-
559
- def allowed_pagination_keys(self) -> List[str]:
560
- return ['next_token']
561
-
562
- def restore_next_token_from_config(self, next_token):
563
- if not next_token:
564
- return None
565
- try:
566
- return json.loads(base64.urlsafe_b64decode(next_token))
567
- except:
568
- return None
569
-
570
- def serialize_next_token_for_response(self, last_evaluated_key):
571
- return base64.urlsafe_b64encode(json.dumps(last_evaluated_key).encode('utf-8')).decode('utf8')
572
-
573
- def documentation_pagination_next_page_response(self, case_mapping: Callable) -> List[Any]:
574
- return [AutoDocString(case_mapping('next_token'))]
575
-
576
- def documentation_pagination_next_page_example(self, case_mapping: Callable) -> Dict[str, Any]:
577
- return {case_mapping('next_token'): ''}
578
-
579
- def documentation_pagination_parameters(self, case_mapping: Callable) -> List[Tuple[Any]]:
580
- return [(
581
- AutoDocString(case_mapping('next_token'), example=''), 'A token to fetch the next page of results'
582
- )]
583
-
584
- def column_from_backend(self, column, value):
585
- """
586
- We have a couple columns we want to override transformations for
587
- """
588
- # We're pretty much ignoring the BOOL type for dynamodb, because it doesn't work in indexes
589
- # (and 99% of the time when I have a boolean, it gets used in an index). Therefore,
590
- # convert boolean values to "0", "1".
591
- if isinstance(column, Boolean):
592
- if value == "1":
593
- return True
594
- elif value == "0":
595
- return False
596
- else:
597
- return bool(value)
598
- return super().column_from_backend(column, value)
599
-
600
- def column_to_backend(self, column, backend_data):
601
- """
602
- We have a couple columns we want to override transformations for
603
- """
604
- # most importantly, there's no need to transform a JSON column in either direction
605
- if isinstance(column, Boolean):
606
- if column.name not in backend_data:
607
- return backend_data
608
- as_string = "1" if bool(backend_data[column.name]) else "0"
609
- return {**backend_data, column.name: as_string}
610
- if isinstance(column, Float):
611
- if column.name not in backend_data:
612
- return backend_data
613
- return {**backend_data, column.name: Decimal(backend_data[column.name])}
614
- return column.to_backend(backend_data)