Skip to content

Commit ad9edc7

Browse files
committed
increase filter options for both query and scan operations
1 parent c9c9a41 commit ad9edc7

File tree

7 files changed

+257
-41
lines changed

7 files changed

+257
-41
lines changed

Diff for: README.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ Para os casos de consultas com filtro em determinada propriedade que não seja u
117117

118118
```python
119119
users = User()
120-
online_users = users.find_by('status', 'online').fetch(True)
120+
online_users = users.find_by('status', '=', 'online').fetch(True)
121121
print(f'{users.get_count} online no momento!')
122122
```
123123

@@ -126,7 +126,7 @@ Para os casos de consultas com filtro em determinada chave de partição, pode-s
126126

127127
```python
128128
users = User()
129-
online_users = users.query_by('status', 'online').fetch(True)
129+
online_users = users.query_by('status', '=', 'online').fetch(True)
130130
print(f'{users.get_count} online no momento!')
131131
```
132132

@@ -135,7 +135,7 @@ Serve para ordenar de acordo com determinado atributo, podendo também trazer a
135135

136136
```python
137137
users = User()
138-
online_users = users.find_by('status', 'online').order('first_name').fetch(True)
138+
online_users = users.find_by('status', '=', 'online').order('first_name').fetch(True)
139139
print(f'{users.get_count} online no momento!')
140140
```
141141

@@ -144,15 +144,15 @@ Caso queira contar o total de itens na tabela ou operação que você fizer sem
144144

145145
```python
146146
users = User()
147-
online_users = users.find_by('status', 'online').order('first_name').count()
147+
online_users = users.find_by('status', '=', 'online').order('first_name').count()
148148
print(f'{online_users} online no momento!')
149149
```
150150

151151
### Fetch
152152
Por default retorna os dados como um dict, mas ao passar o argumento object como True ele retorna os registros como um objeto DynoLayer.
153153

154154
```python
155-
users = User().find_by('status', 'online').order('first_name').fetch(object=True)
155+
users = User().find_by('status', '=', 'online').order('first_name').fetch(object=True)
156156
print(users[0].name)
157157
```
158158

Diff for: dynolayer/dynolayer.py

+168-13
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import uuid
44
import os
55
import pytz
6+
import random
7+
import string
68
from datetime import datetime
79
from dotenv import load_dotenv
810
from decimal import Decimal
@@ -29,6 +31,7 @@ def __init__(
2931
self._secondary_index = ''
3032
self._attributes_to_get = ''
3133
self._filter_expression = ''
34+
self._query_filter_expression = ''
3235
self._filter_params = {}
3336
self._filter_params_name = {}
3437
self._last_evaluated_key = {}
@@ -88,14 +91,35 @@ def _load_dynamo(self):
8891
def query_by(
8992
self,
9093
key: str,
91-
comparator: Literal["=", "<>", "<", "<=", ">", ">="],
94+
comparator: Literal["=", "<", "<=", ">", ">=", "BETWEEN", "begins_with"],
9295
key_value,
93-
secondary_index = None
94-
):
96+
secondary_index=None
97+
):
9598
self._is_query_operation = True
9699
self._secondary_index = secondary_index
97100
return self.find_by(key, comparator, key_value)
98101

102+
"""
103+
Args:
104+
key (str): The table key to filter on.
105+
key_value: The value to use on the filter.
106+
107+
Returns:
108+
self: The DynoLayer.
109+
or_query_by('likes', '<', 20)
110+
"""
111+
112+
def or_query_by(
113+
self,
114+
key: str,
115+
comparator: Literal["=", "<", "<=", ">", ">=", "BETWEEN", "begins_with"],
116+
key_value,
117+
secondary_index=None
118+
):
119+
self._is_query_operation = True
120+
self._secondary_index = secondary_index
121+
return self.or_find_by(key, comparator, key_value)
122+
99123
"""
100124
Args:
101125
filter_expression (str): The filter expression string.
@@ -108,9 +132,13 @@ def query_by(
108132
def find(
109133
self,
110134
filter_expression: str = '',
111-
filter_params: dict = {},
112-
filter_params_name: dict = {}
135+
filter_params=None,
136+
filter_params_name=None
113137
):
138+
if filter_params_name is None:
139+
filter_params_name = {}
140+
if filter_params is None:
141+
filter_params = {}
114142
self._filter_expression = filter_expression
115143
self._filter_params = filter_params
116144
self._filter_params_name = filter_params_name
@@ -142,20 +170,141 @@ def query(
142170
def find_by(
143171
self,
144172
attribute: str,
145-
comparator: Literal["=", "<>", "<", "<=", ">", ">="],
173+
comparator: Literal["=", "<>", "<", "<=", ">", ">=", "BETWEEN", "begins_with"],
146174
attribute_value
147-
):
148-
self._is_find_by = True
175+
):
176+
use_and_operator = ''
177+
old_att = ''
178+
if self._is_find_by:
179+
use_and_operator = ' AND '
180+
if str(self._filter_params.get(f':{attribute}', '')):
181+
letters = string.ascii_lowercase
182+
random_str = ''
183+
for i in range(4):
184+
random_str += random.choice(letters)
185+
old_att = attribute
186+
attribute = random_str + attribute
187+
188+
if not self._is_find_by:
189+
self._is_find_by = True
190+
191+
if isinstance(attribute_value, dict) or (isinstance(attribute_value, list) and comparator != 'BETWEEN'):
192+
attribute_value = json.dumps(attribute_value)
193+
194+
filter_param = {
195+
f':{attribute}': attribute_value
196+
}
197+
if comparator == 'BETWEEN':
198+
self._filter_expression += f'{use_and_operator}(#{attribute} {comparator} :{attribute[0]} AND :{attribute[1]})'
199+
filter_param = {
200+
f':{attribute[0]}': attribute_value[0],
201+
f':{attribute[1]}': attribute_value[1]
202+
}
203+
elif comparator == 'begins_with':
204+
self._filter_expression += f'{use_and_operator}({comparator}(#{attribute},:{attribute}))'
205+
else:
206+
self._filter_expression += f'{use_and_operator}(#{attribute} {comparator} :{attribute})'
207+
self._filter_params.update(filter_param)
208+
self._filter_params_name.update({
209+
f'#{attribute}': old_att if old_att else attribute
210+
})
211+
212+
return self
213+
214+
"""
215+
Args:
216+
attribute (str): The table attribute to filter on.
217+
attribute_value: The value to use on the filter.
218+
219+
Returns:
220+
self: The DynoLayer.
221+
"""
222+
223+
def or_find_by(
224+
self,
225+
attribute: str,
226+
comparator: Literal["=", "<", "<=", ">", ">=", "BETWEEN", "begins_with"],
227+
attribute_value
228+
):
229+
use_or_operator = ' OR '
230+
old_att = ''
231+
if self._is_find_by:
232+
if str(self._filter_params.get(f':{attribute}', '')):
233+
letters = string.ascii_lowercase
234+
random_str = ''
235+
for i in range(4):
236+
random_str += random.choice(letters)
237+
old_att = attribute
238+
attribute = random_str + attribute
239+
240+
if not self._is_find_by:
241+
self._is_find_by = True
242+
243+
if isinstance(attribute_value, dict) or (isinstance(attribute_value, list) and comparator != 'BETWEEN'):
244+
attribute_value = json.dumps(attribute_value)
245+
246+
filter_param = {
247+
f':{attribute}': attribute_value
248+
}
249+
if comparator == 'BETWEEN':
250+
self._filter_expression += f'{use_or_operator}(#{attribute} {comparator} :{attribute[0]} AND :{attribute[1]})'
251+
filter_param = {
252+
f':{attribute[0]}': attribute_value[0],
253+
f':{attribute[1]}': attribute_value[1]
254+
}
255+
elif comparator == 'begins_with':
256+
self._filter_expression += f'{use_or_operator}({comparator}(#{attribute},:{attribute}))'
257+
else:
258+
self._filter_expression += f'{use_or_operator}(#{attribute} {comparator} :{attribute})'
259+
self._filter_params.update(filter_param)
260+
self._filter_params_name.update({
261+
f'#{attribute}': old_att if old_att else attribute
262+
})
263+
264+
return self
265+
266+
"""
267+
This function create a FilterExpression for a query operation
268+
Args:
269+
attributes (str): The specific attributes to return from table.
270+
271+
Returns:
272+
self: The DynoLayer.
273+
"""
274+
275+
def filter(
276+
self,
277+
attribute: str,
278+
comparator: Literal["=", "<>", "<", "<=", ">", ">=", "BETWEEN", "begins_with"],
279+
attribute_value,
280+
logical_operator: Literal['AND', 'OR', 'NOT'] = ''
281+
):
282+
old_att = ''
283+
if self._is_find_by:
284+
if str(self._filter_params.get(f':{attribute}', '')):
285+
letters = string.ascii_lowercase
286+
random_str = ''
287+
for i in range(4):
288+
random_str += random.choice(letters)
289+
old_att = attribute
290+
attribute = random_str + attribute
149291

150292
if isinstance(attribute_value, dict) or isinstance(attribute_value, list):
151293
attribute_value = json.dumps(attribute_value)
152294

153-
self._filter_expression += f'#{attribute} {comparator} :{attribute}'
295+
if logical_operator:
296+
logical_operator = f' {logical_operator} '
297+
if comparator == 'BETWEEN':
298+
self._query_filter_expression += f'{logical_operator}(#{attribute} {comparator} :{attribute[0]} AND :{attribute[1]})'
299+
elif comparator == 'begins_with':
300+
self._query_filter_expression += f'{logical_operator}({comparator}(#{attribute},:{attribute}))'
301+
else:
302+
self._query_filter_expression += f'{logical_operator}(#{attribute} {comparator} :{attribute})'
154303
self._filter_params.update({
155304
f':{attribute}': attribute_value
156305
})
157306
self._filter_params_name.update({
158-
f'#{attribute}': attribute
307+
f'#{attribute}': old_att if old_att else attribute
159308
})
160309

161310
return self
@@ -257,6 +406,10 @@ def _fetch(self, paginate_through_results: bool = False, just_count=False, objec
257406
if self._filter_expression:
258407
filter_key = 'KeyConditionExpression' if self._is_query_operation else 'FilterExpression'
259408
scan_params.update({filter_key: self._filter_expression})
409+
if self._query_filter_expression:
410+
if not scan_params.get('FilterExpression', None):
411+
scan_params['FilterExpression'] = ''
412+
scan_params['FilterExpression'] += self._query_filter_expression
260413
scan_params.update({'ExpressionAttributeValues': self._filter_params})
261414
scan_params.update({'ExpressionAttributeNames': self._filter_params_name})
262415

@@ -267,6 +420,9 @@ def _fetch(self, paginate_through_results: bool = False, just_count=False, objec
267420
scan_params.update({'IndexName': self._secondary_index})
268421

269422
self._is_find_by = False # return to default value
423+
self._filter_expression = '' # return to default value
424+
self._filter_params = {} # return to default value
425+
self._filter_params_name = {} # return to default value
270426
if self._is_query_operation:
271427
return self._order_response(self._fetch_query(scan_params, paginate_through_results), object=object)
272428

@@ -352,7 +508,7 @@ def save(self) -> bool:
352508
self._table.put_item(
353509
Item=data
354510
)
355-
self.id = data[self._partition_key]
511+
self._data['id'] = data[self._partition_key]
356512
return True
357513
except Exception as e:
358514
self._error = str(e)
@@ -494,7 +650,7 @@ def data(self):
494650
"""
495651

496652
def _order_response(self, response, object=False):
497-
if len(response) == 0:
653+
if not response or len(response) == 0:
498654
return response
499655

500656
if self._order_by:
@@ -516,4 +672,3 @@ def _order_response(self, response, object=False):
516672
return transformed_response
517673

518674
return response
519-

Diff for: setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
setup(
99
name='dynolayer',
10-
version='0.3.4',
10+
version='0.4.0',
1111
license='MIT License',
1212
packages=['dynolayer'],
1313
install_requires=['boto3', 'pytz', 'python-dotenv'],

Diff for: tests/unit/test_destroy.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import boto3
22
import pytest
33
from dynolayer.dynolayer import DynoLayer
4-
from moto import mock_aws
4+
from moto import mock_dynamodb
55

66

77
def create_table():
@@ -63,7 +63,7 @@ def __init__(self) -> None:
6363
super().__init__('users', [])
6464

6565

66-
@mock_aws
66+
@mock_dynamodb
6767
def test_it_should_destroy_a_record():
6868
create_table()
6969
save_record()

0 commit comments

Comments
 (0)