Skip to content

Commit 9816284

Browse files
committed
Merge branch 'develop': v1.0.3
2 parents 4309186 + e233a33 commit 9816284

File tree

4 files changed

+227
-39
lines changed

4 files changed

+227
-39
lines changed

CHANGELOG.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@ Changelog
22
*********
33

44

5+
**1.0.3**
6+
=========
7+
8+
Enhancements
9+
============
10+
11+
* Add custom marshmallow fields for PostgreSQL filtering (PostgreSqlJSONB plugin) #- `@Znbiz`_
12+
* Filtering and sorting nested JSONB fields (PostgreSqlJSONB plugin) #- `@tarasovdg1`_
13+
14+
515
**1.0.0**
616
=========
717

@@ -61,4 +71,5 @@ Enhancements
6171

6272
.. _`@mahenzon`: https://github.com/mahenzon
6373
.. _`@Znbiz`: https://github.com/znbiz
64-
.. _`@Yakov Shapovalov`: https://github.com/photovirus
74+
.. _`@Yakov Shapovalov`: https://github.com/photovirus
75+
.. _`@tarasovdg1`: https://github.com/tarasovdg1

combojsonapi/postgresql_jsonb/plugin.py

Lines changed: 63 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import datetime
22
from decimal import Decimal
3-
from typing import Any
3+
from typing import Any, Optional, Union, Dict, Type
44

55
import sqlalchemy
66
from sqlalchemy import cast, String, Integer, Boolean, DECIMAL, not_
@@ -18,6 +18,13 @@
1818
from combojsonapi.postgresql_jsonb.schema import SchemaJSONB
1919

2020

21+
TYPE_MARSHMALLOW_FIELDS = Type[Union[
22+
ma_fields.Email, ma_fields.Dict, ma_fields.List,
23+
ma_fields.Decimal, ma_fields.Url, ma_fields.DateTime, Any
24+
]]
25+
TYPE_PYTHON = Type[Union[str, dict, list, Decimal, datetime.datetime]]
26+
27+
2128
def is_seq_collection(obj):
2229
"""
2330
является ли переданный объект set, list, tuple
@@ -28,6 +35,27 @@ def is_seq_collection(obj):
2835

2936

3037
class PostgreSqlJSONB(BasePlugin):
38+
mapping_ma_field_to_type: Dict[TYPE_MARSHMALLOW_FIELDS, TYPE_PYTHON] = {
39+
ma_fields.Email: str,
40+
ma_fields.Dict: dict,
41+
ma_fields.List: list,
42+
ma_fields.Decimal: Decimal,
43+
ma_fields.Url: str,
44+
ma_fields.DateTime: datetime.datetime,
45+
}
46+
47+
def get_property_type(
48+
self, marshmallow_field: TYPE_MARSHMALLOW_FIELDS, schema: Optional[Schema] = None
49+
) -> TYPE_PYTHON:
50+
if schema is not None:
51+
self.mapping_ma_field_to_type.update({
52+
v: k for k, v in schema.TYPE_MAPPING.items()
53+
})
54+
return self.mapping_ma_field_to_type[type(marshmallow_field)]
55+
56+
def add_mapping_field_to_python_type(self, marshmallow_field: Any, type_python: TYPE_PYTHON) -> None:
57+
self.mapping_ma_field_to_type[marshmallow_field] = type_python
58+
3159
def before_data_layers_sorting_alchemy_nested_resolve(self, self_nested: Any) -> Any:
3260
"""
3361
Вызывается до создания сортировки в функции Nested.resolve, если после выполнения вернёт None, то
@@ -77,7 +105,7 @@ def _isinstance_jsonb(cls, schema: Schema, filter_name):
77105
fields = filter_name.split(SPLIT_REL)
78106
for i, i_field in enumerate(fields):
79107
if isinstance(getattr(schema._declared_fields[i_field], "schema", None), SchemaJSONB):
80-
if i != (len(fields) - 2):
108+
if i == (len(fields) - 1):
81109
raise InvalidFilters(f"Invalid JSONB filter: {filter_name}")
82110
return True
83111
elif isinstance(schema._declared_fields[i_field], Relationship):
@@ -86,8 +114,7 @@ def _isinstance_jsonb(cls, schema: Schema, filter_name):
86114
return False
87115
return False
88116

89-
@classmethod
90-
def _create_sort(cls, self_nested: Any, marshmallow_field, model_column, order):
117+
def _create_sort(self, self_nested: Any, marshmallow_field, model_column, order):
91118
"""
92119
Create sqlalchemy sort
93120
:param Nested self_nested:
@@ -106,14 +133,15 @@ def _create_sort(cls, self_nested: Any, marshmallow_field, model_column, order):
106133
self_nested.sort_["field"] = SPLIT_REL.join(fields[1:])
107134
marshmallow_field = marshmallow_field.schema._declared_fields[fields[1]]
108135
model_column = getattr(mapper, sqlalchemy_relationship_name)
109-
return cls._create_sort(self_nested, marshmallow_field, model_column, order)
136+
return self._create_sort(self_nested, marshmallow_field, model_column, order)
110137
elif not isinstance(getattr(marshmallow_field, "schema", None), SchemaJSONB):
111138
raise InvalidFilters(f"Invalid JSONB sort: {SPLIT_REL.join(self_nested.fields)}")
112139
fields = self_nested.sort_["field"].split(SPLIT_REL)
113140
self_nested.sort_["field"] = SPLIT_REL.join(fields[:-1])
114141
field_in_jsonb = fields[-1]
115142

116-
marshmallow_field = marshmallow_field.schema._declared_fields[field_in_jsonb]
143+
for field in fields[1:]:
144+
marshmallow_field = marshmallow_field.schema._declared_fields[field]
117145
if hasattr(marshmallow_field, f"_{order}_sql_filter_"):
118146
"""
119147
У marshmallow field может быть реализована своя логика создания сортировки для sqlalchemy
@@ -123,19 +151,20 @@ def _create_sort(cls, self_nested: Any, marshmallow_field, model_column, order):
123151
* marshmallow_field - объект класса поля marshmallow
124152
* model_column - объект класса поля sqlalchemy
125153
"""
154+
# All values between the first and last field will be the path to the desired value by which to sort,
155+
# so we write the path through "->"
156+
for field in fields[1:-1]:
157+
model_column = model_column.op("->")(field)
158+
model_column = model_column.op("->>")(field_in_jsonb)
126159
return getattr(marshmallow_field, f"_{order}_sql_filter_")(
127160
marshmallow_field=marshmallow_field, model_column=model_column
128161
)
129-
mapping_ma_field_to_type = {v: k for k, v in self_nested.schema.TYPE_MAPPING.items()}
130-
mapping_ma_field_to_type[ma_fields.Email] = str
131-
mapping_ma_field_to_type[ma_fields.Dict] = dict
132-
mapping_ma_field_to_type[ma_fields.List] = list
133-
mapping_ma_field_to_type[ma_fields.Decimal] = Decimal
134-
mapping_ma_field_to_type[ma_fields.Url] = str
135-
mapping_ma_field_to_type[ma_fields.DateTime] = datetime.datetime
162+
163+
property_type = self.get_property_type(marshmallow_field=marshmallow_field, schema=self_nested.schema)
136164
mapping_type_to_sql_type = {str: String, bytes: String, Decimal: DECIMAL, int: Integer, bool: Boolean}
137165

138-
property_type = mapping_ma_field_to_type[type(marshmallow_field)]
166+
for field in fields[1:-1]:
167+
model_column = model_column.op("->")(field)
139168
extra_field = model_column.op("->>")(field_in_jsonb)
140169
sort = ""
141170
order_op = desc_op if order == "desc" else asc_op
@@ -146,8 +175,7 @@ def _create_sort(cls, self_nested: Any, marshmallow_field, model_column, order):
146175
sort = order_op(extra_field.cast(mapping_type_to_sql_type[property_type]))
147176
return sort
148177

149-
@classmethod
150-
def _create_filter(cls, self_nested: Any, marshmallow_field, model_column, operator, value):
178+
def _create_filter(self, self_nested: Any, marshmallow_field, model_column, operator, value):
151179
"""
152180
Create sqlalchemy filter
153181
:param Nested self_nested:
@@ -168,14 +196,15 @@ def _create_filter(cls, self_nested: Any, marshmallow_field, model_column, opera
168196
marshmallow_field = marshmallow_field.schema._declared_fields[fields[1]]
169197
join_list = [[model_column]]
170198
model_column = getattr(mapper, sqlalchemy_relationship_name)
171-
filter, joins = cls._create_filter(self_nested, marshmallow_field, model_column, operator, value)
199+
filter, joins = self._create_filter(self_nested, marshmallow_field, model_column, operator, value)
172200
join_list += joins
173201
return filter, join_list
174202
elif not isinstance(getattr(marshmallow_field, "schema", None), SchemaJSONB):
175203
raise InvalidFilters(f"Invalid JSONB filter: {SPLIT_REL.join(field_in_jsonb)}")
176204
self_nested.filter_["name"] = SPLIT_REL.join(fields[:-1])
177205
try:
178-
marshmallow_field = marshmallow_field.schema._declared_fields[field_in_jsonb]
206+
for field in fields[1:]:
207+
marshmallow_field = marshmallow_field.schema._declared_fields[field]
179208
except KeyError:
180209
raise InvalidFilters(f'There is no "{field_in_jsonb}" attribute in the "{fields[-2]}" field.')
181210
if hasattr(marshmallow_field, f"_{operator}_sql_filter_"):
@@ -189,49 +218,47 @@ def _create_filter(cls, self_nested: Any, marshmallow_field, model_column, opera
189218
* value - значения для фильтра
190219
* operator - сам оператор, например: "eq", "in"...
191220
"""
221+
for field in fields[1:-1]:
222+
model_column = model_column.op("->")(field)
223+
model_column = model_column.op("->>")(field_in_jsonb)
192224
return (
193225
getattr(marshmallow_field, f"_{operator}_sql_filter_")(
194226
marshmallow_field=marshmallow_field,
195-
model_column=model_column.op("->>")(field_in_jsonb),
227+
model_column=model_column,
196228
value=value,
197229
operator=self_nested.operator,
198230
),
199231
[],
200232
)
201-
mapping = {v: k for k, v in self_nested.schema.TYPE_MAPPING.items()}
202-
mapping[ma_fields.Email] = str
203-
mapping[ma_fields.Dict] = dict
204-
mapping[ma_fields.List] = list
205-
mapping[ma_fields.Decimal] = Decimal
206-
mapping[ma_fields.Url] = str
207-
mapping[ma_fields.DateTime] = datetime.datetime
208233

209234
# Нужно проводить валидацию и делать десериализацию значение указанных в фильтре, так как поля Enum
210235
# например выгружаются как 'name_value(str)', а в БД хранится как просто число
211236
value = deserialize_field(marshmallow_field, value)
212237

213-
property_type = mapping[type(marshmallow_field)]
238+
property_type = self.get_property_type(marshmallow_field=marshmallow_field, schema=self_nested.schema)
239+
for field in fields[1:-1]:
240+
model_column = model_column.op("->")(field)
214241
extra_field = model_column.op("->>")(field_in_jsonb)
215-
filter = ""
242+
filter_ = ""
216243
if property_type == Decimal:
217-
filter = getattr(cast(extra_field, DECIMAL), self_nested.operator)(value)
244+
filter_ = getattr(cast(extra_field, DECIMAL), self_nested.operator)(value)
218245

219246
if property_type in {str, bytes}:
220-
filter = getattr(cast(extra_field, String), self_nested.operator)(value)
247+
filter_ = getattr(cast(extra_field, String), self_nested.operator)(value)
221248

222249
if property_type == int:
223250
field = cast(extra_field, Integer)
224251
if value:
225-
filter = getattr(field, self_nested.operator)(value)
252+
filter_ = getattr(field, self_nested.operator)(value)
226253
else:
227-
filter = or_(getattr(field, self_nested.operator)(value), field.is_(None))
254+
filter_ = or_(getattr(field, self_nested.operator)(value), field.is_(None))
228255

229256
if property_type == bool:
230-
filter = cast(extra_field, Boolean) == value
257+
filter_ = cast(extra_field, Boolean) == value
231258

232259
if property_type == list:
233-
filter = model_column.op("->")(field_in_jsonb).op("?")(value[0] if is_seq_collection(value) else value)
260+
filter_ = model_column.op("->")(field_in_jsonb).op("?")(value[0] if is_seq_collection(value) else value)
234261
if operator in ["notin", "notin_"]:
235-
filter = not_(filter)
262+
filter_ = not_(filter_)
236263

237-
return filter, []
264+
return filter_, []

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from setuptools import setup, find_packages
22

3-
__version__ = "1.0.2"
3+
__version__ = "1.0.3"
44

55
setup(
66
name="ComboJSONAPI",

0 commit comments

Comments
 (0)