Skip to content

Commit

Permalink
Merge branch 'develop': v1.0.3
Browse files Browse the repository at this point in the history
  • Loading branch information
mahenzon committed Jul 29, 2020
2 parents 4309186 + e233a33 commit 9816284
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 39 deletions.
13 changes: 12 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ Changelog
*********


**1.0.3**
=========

Enhancements
============

* Add custom marshmallow fields for PostgreSQL filtering (PostgreSqlJSONB plugin) #- `@Znbiz`_
* Filtering and sorting nested JSONB fields (PostgreSqlJSONB plugin) #- `@tarasovdg1`_


**1.0.0**
=========

Expand Down Expand Up @@ -61,4 +71,5 @@ Enhancements

.. _`@mahenzon`: https://github.com/mahenzon
.. _`@Znbiz`: https://github.com/znbiz
.. _`@Yakov Shapovalov`: https://github.com/photovirus
.. _`@Yakov Shapovalov`: https://github.com/photovirus
.. _`@tarasovdg1`: https://github.com/tarasovdg1
99 changes: 63 additions & 36 deletions combojsonapi/postgresql_jsonb/plugin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import datetime
from decimal import Decimal
from typing import Any
from typing import Any, Optional, Union, Dict, Type

import sqlalchemy
from sqlalchemy import cast, String, Integer, Boolean, DECIMAL, not_
Expand All @@ -18,6 +18,13 @@
from combojsonapi.postgresql_jsonb.schema import SchemaJSONB


TYPE_MARSHMALLOW_FIELDS = Type[Union[
ma_fields.Email, ma_fields.Dict, ma_fields.List,
ma_fields.Decimal, ma_fields.Url, ma_fields.DateTime, Any
]]
TYPE_PYTHON = Type[Union[str, dict, list, Decimal, datetime.datetime]]


def is_seq_collection(obj):
"""
является ли переданный объект set, list, tuple
Expand All @@ -28,6 +35,27 @@ def is_seq_collection(obj):


class PostgreSqlJSONB(BasePlugin):
mapping_ma_field_to_type: Dict[TYPE_MARSHMALLOW_FIELDS, TYPE_PYTHON] = {
ma_fields.Email: str,
ma_fields.Dict: dict,
ma_fields.List: list,
ma_fields.Decimal: Decimal,
ma_fields.Url: str,
ma_fields.DateTime: datetime.datetime,
}

def get_property_type(
self, marshmallow_field: TYPE_MARSHMALLOW_FIELDS, schema: Optional[Schema] = None
) -> TYPE_PYTHON:
if schema is not None:
self.mapping_ma_field_to_type.update({
v: k for k, v in schema.TYPE_MAPPING.items()
})
return self.mapping_ma_field_to_type[type(marshmallow_field)]

def add_mapping_field_to_python_type(self, marshmallow_field: Any, type_python: TYPE_PYTHON) -> None:
self.mapping_ma_field_to_type[marshmallow_field] = type_python

def before_data_layers_sorting_alchemy_nested_resolve(self, self_nested: Any) -> Any:
"""
Вызывается до создания сортировки в функции Nested.resolve, если после выполнения вернёт None, то
Expand Down Expand Up @@ -77,7 +105,7 @@ def _isinstance_jsonb(cls, schema: Schema, filter_name):
fields = filter_name.split(SPLIT_REL)
for i, i_field in enumerate(fields):
if isinstance(getattr(schema._declared_fields[i_field], "schema", None), SchemaJSONB):
if i != (len(fields) - 2):
if i == (len(fields) - 1):
raise InvalidFilters(f"Invalid JSONB filter: {filter_name}")
return True
elif isinstance(schema._declared_fields[i_field], Relationship):
Expand All @@ -86,8 +114,7 @@ def _isinstance_jsonb(cls, schema: Schema, filter_name):
return False
return False

@classmethod
def _create_sort(cls, self_nested: Any, marshmallow_field, model_column, order):
def _create_sort(self, self_nested: Any, marshmallow_field, model_column, order):
"""
Create sqlalchemy sort
:param Nested self_nested:
Expand All @@ -106,14 +133,15 @@ def _create_sort(cls, self_nested: Any, marshmallow_field, model_column, order):
self_nested.sort_["field"] = SPLIT_REL.join(fields[1:])
marshmallow_field = marshmallow_field.schema._declared_fields[fields[1]]
model_column = getattr(mapper, sqlalchemy_relationship_name)
return cls._create_sort(self_nested, marshmallow_field, model_column, order)
return self._create_sort(self_nested, marshmallow_field, model_column, order)
elif not isinstance(getattr(marshmallow_field, "schema", None), SchemaJSONB):
raise InvalidFilters(f"Invalid JSONB sort: {SPLIT_REL.join(self_nested.fields)}")
fields = self_nested.sort_["field"].split(SPLIT_REL)
self_nested.sort_["field"] = SPLIT_REL.join(fields[:-1])
field_in_jsonb = fields[-1]

marshmallow_field = marshmallow_field.schema._declared_fields[field_in_jsonb]
for field in fields[1:]:
marshmallow_field = marshmallow_field.schema._declared_fields[field]
if hasattr(marshmallow_field, f"_{order}_sql_filter_"):
"""
У marshmallow field может быть реализована своя логика создания сортировки для sqlalchemy
Expand All @@ -123,19 +151,20 @@ def _create_sort(cls, self_nested: Any, marshmallow_field, model_column, order):
* marshmallow_field - объект класса поля marshmallow
* model_column - объект класса поля sqlalchemy
"""
# All values between the first and last field will be the path to the desired value by which to sort,
# so we write the path through "->"
for field in fields[1:-1]:
model_column = model_column.op("->")(field)
model_column = model_column.op("->>")(field_in_jsonb)
return getattr(marshmallow_field, f"_{order}_sql_filter_")(
marshmallow_field=marshmallow_field, model_column=model_column
)
mapping_ma_field_to_type = {v: k for k, v in self_nested.schema.TYPE_MAPPING.items()}
mapping_ma_field_to_type[ma_fields.Email] = str
mapping_ma_field_to_type[ma_fields.Dict] = dict
mapping_ma_field_to_type[ma_fields.List] = list
mapping_ma_field_to_type[ma_fields.Decimal] = Decimal
mapping_ma_field_to_type[ma_fields.Url] = str
mapping_ma_field_to_type[ma_fields.DateTime] = datetime.datetime

property_type = self.get_property_type(marshmallow_field=marshmallow_field, schema=self_nested.schema)
mapping_type_to_sql_type = {str: String, bytes: String, Decimal: DECIMAL, int: Integer, bool: Boolean}

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

@classmethod
def _create_filter(cls, self_nested: Any, marshmallow_field, model_column, operator, value):
def _create_filter(self, self_nested: Any, marshmallow_field, model_column, operator, value):
"""
Create sqlalchemy filter
:param Nested self_nested:
Expand All @@ -168,14 +196,15 @@ def _create_filter(cls, self_nested: Any, marshmallow_field, model_column, opera
marshmallow_field = marshmallow_field.schema._declared_fields[fields[1]]
join_list = [[model_column]]
model_column = getattr(mapper, sqlalchemy_relationship_name)
filter, joins = cls._create_filter(self_nested, marshmallow_field, model_column, operator, value)
filter, joins = self._create_filter(self_nested, marshmallow_field, model_column, operator, value)
join_list += joins
return filter, join_list
elif not isinstance(getattr(marshmallow_field, "schema", None), SchemaJSONB):
raise InvalidFilters(f"Invalid JSONB filter: {SPLIT_REL.join(field_in_jsonb)}")
self_nested.filter_["name"] = SPLIT_REL.join(fields[:-1])
try:
marshmallow_field = marshmallow_field.schema._declared_fields[field_in_jsonb]
for field in fields[1:]:
marshmallow_field = marshmallow_field.schema._declared_fields[field]
except KeyError:
raise InvalidFilters(f'There is no "{field_in_jsonb}" attribute in the "{fields[-2]}" field.')
if hasattr(marshmallow_field, f"_{operator}_sql_filter_"):
Expand All @@ -189,49 +218,47 @@ def _create_filter(cls, self_nested: Any, marshmallow_field, model_column, opera
* value - значения для фильтра
* operator - сам оператор, например: "eq", "in"...
"""
for field in fields[1:-1]:
model_column = model_column.op("->")(field)
model_column = model_column.op("->>")(field_in_jsonb)
return (
getattr(marshmallow_field, f"_{operator}_sql_filter_")(
marshmallow_field=marshmallow_field,
model_column=model_column.op("->>")(field_in_jsonb),
model_column=model_column,
value=value,
operator=self_nested.operator,
),
[],
)
mapping = {v: k for k, v in self_nested.schema.TYPE_MAPPING.items()}
mapping[ma_fields.Email] = str
mapping[ma_fields.Dict] = dict
mapping[ma_fields.List] = list
mapping[ma_fields.Decimal] = Decimal
mapping[ma_fields.Url] = str
mapping[ma_fields.DateTime] = datetime.datetime

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

property_type = mapping[type(marshmallow_field)]
property_type = self.get_property_type(marshmallow_field=marshmallow_field, schema=self_nested.schema)
for field in fields[1:-1]:
model_column = model_column.op("->")(field)
extra_field = model_column.op("->>")(field_in_jsonb)
filter = ""
filter_ = ""
if property_type == Decimal:
filter = getattr(cast(extra_field, DECIMAL), self_nested.operator)(value)
filter_ = getattr(cast(extra_field, DECIMAL), self_nested.operator)(value)

if property_type in {str, bytes}:
filter = getattr(cast(extra_field, String), self_nested.operator)(value)
filter_ = getattr(cast(extra_field, String), self_nested.operator)(value)

if property_type == int:
field = cast(extra_field, Integer)
if value:
filter = getattr(field, self_nested.operator)(value)
filter_ = getattr(field, self_nested.operator)(value)
else:
filter = or_(getattr(field, self_nested.operator)(value), field.is_(None))
filter_ = or_(getattr(field, self_nested.operator)(value), field.is_(None))

if property_type == bool:
filter = cast(extra_field, Boolean) == value
filter_ = cast(extra_field, Boolean) == value

if property_type == list:
filter = model_column.op("->")(field_in_jsonb).op("?")(value[0] if is_seq_collection(value) else value)
filter_ = model_column.op("->")(field_in_jsonb).op("?")(value[0] if is_seq_collection(value) else value)
if operator in ["notin", "notin_"]:
filter = not_(filter)
filter_ = not_(filter_)

return filter, []
return filter_, []
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from setuptools import setup, find_packages

__version__ = "1.0.2"
__version__ = "1.0.3"

setup(
name="ComboJSONAPI",
Expand Down
Loading

0 comments on commit 9816284

Please sign in to comment.