Skip to content

Commit

Permalink
Merge pull request #17 from Harborn-digital/upstream-changes
Browse files Browse the repository at this point in the history
Upstream changes
  • Loading branch information
RonRademaker committed Feb 16, 2024
2 parents 5f3a60e + c23fc82 commit c1bc0fe
Show file tree
Hide file tree
Showing 18 changed files with 1,393 additions and 1,236 deletions.
8 changes: 8 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[coverage:run]
plugins =
coverage_conditional_plugin

[coverage:coverage_conditional_plugin]
rules =
"package_version('sqlalchemy') < (1, 4)": no_cover_sqlalchemy_lt_1_4
"package_version('sqlalchemy') >= (1, 4)": no_cover_sqlalchemy_gte_1_4
14 changes: 14 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Pull request checks

on:
pull_request:

jobs:
ci:
runs-on: self-hosted
steps:
- name: Install project and dependencies
uses: actions/checkout@v4
- name: Run tests
uses: Harborn-digital/[email protected]

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ __pycache__/
.coverage.*
.cache
.tox
.python-version
87 changes: 0 additions & 87 deletions .travis.yml

This file was deleted.

8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ Release Notes
Here you can see the full list of changes between sqlalchemy-filters
versions, where semantic versioning is used: *major.minor.patch*.

0.13.0
------

Released 2023-04-13

* Add support for SQLAlchemy 1.4 (#69) thanks to @bodik
* Add support for Python 3.9 & Python 3.10
* Drop support for Python 2.7, 3.5 & 3.6

0.12.0
------
Expand Down
441 changes: 295 additions & 146 deletions poetry.lock

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,17 @@ psycopg2-binary = { version = "2.9.1", optional = true }
pytest = "^6.2.5"
sqlalchemy-utils = "^0.37.8"

[tool.poetry.group.dev.dependencies]
black = "^24.2.0"
poethepoet = "^0.24.4"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

[tool.poetry.extras]
mysql = ["mysql-connector-python-rf"]
postgresql = ["psycopg2-binary"]

[tool.poe.tasks]
test = "pytest test"
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[flake8]
max-line-length = 88
134 changes: 61 additions & 73 deletions sqlalchemy_filters/filters.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,7 @@
# -*- coding: utf-8 -*-
from collections import namedtuple

try:
from collections.abc import Iterable
except ImportError: # pragma: no cover
# For python2 capability.
from collections import Iterable
try:
from inspect import signature
except ImportError: # pragma: no cover
# For python2 capability. NOTE: This is in not handled in install_requires
# but rather in extras_require. You can install with
# 'pip install sqlalchemy-filters[python2]'
from funcsigs import signature
from collections.abc import Iterable
from inspect import signature
from itertools import chain

from six import string_types
Expand All @@ -28,12 +17,12 @@
)

BooleanFunction = namedtuple(
'BooleanFunction', ('key', 'sqlalchemy_fn', 'only_one_arg')
"BooleanFunction", ("key", "sqlalchemy_fn", "only_one_arg")
)
BOOLEAN_FUNCTIONS = [
BooleanFunction('or', or_, False),
BooleanFunction('and', and_, False),
BooleanFunction('not', not_, True),
BooleanFunction("or", or_, False),
BooleanFunction("and", and_, False),
BooleanFunction("not", not_, True),
]
"""
Sqlalchemy boolean functions that can be parsed from the filter definition.
Expand All @@ -42,36 +31,36 @@

class Operator(object):
OPERATORS = {
'is_null': lambda f: f.is_(None),
'is_not_null': lambda f: f.isnot(None),
'==': lambda f, a: f == a,
'eq': lambda f, a: f == a,
'!=': lambda f, a: f != a,
'ne': lambda f, a: f != a,
'>': lambda f, a: f > a,
'gt': lambda f, a: f > a,
'<': lambda f, a: f < a,
'lt': lambda f, a: f < a,
'>=': lambda f, a: f >= a,
'ge': lambda f, a: f >= a,
'<=': lambda f, a: f <= a,
'le': lambda f, a: f <= a,
'like': lambda f, a: f.like(a),
'ilike': lambda f, a: f.ilike(a),
'not_ilike': lambda f, a: ~f.ilike(a),
'in': lambda f, a: f.in_(a),
'not_in': lambda f, a: ~f.in_(a),
'any': lambda f, a: f.any(a),
'not_any': lambda f, a: func.not_(f.any(a)),
'in_set': lambda f, a: func.find_in_set(a, f),
"is_null": lambda f: f.is_(None),
"is_not_null": lambda f: f.isnot(None),
"==": lambda f, a: f == a,
"eq": lambda f, a: f == a,
"!=": lambda f, a: f != a,
"ne": lambda f, a: f != a,
">": lambda f, a: f > a,
"gt": lambda f, a: f > a,
"<": lambda f, a: f < a,
"lt": lambda f, a: f < a,
">=": lambda f, a: f >= a,
"ge": lambda f, a: f >= a,
"<=": lambda f, a: f <= a,
"le": lambda f, a: f <= a,
"like": lambda f, a: f.like(a),
"ilike": lambda f, a: f.ilike(a),
"not_ilike": lambda f, a: ~f.ilike(a),
"in": lambda f, a: f.in_(a),
"not_in": lambda f, a: ~f.in_(a),
"any": lambda f, a: f.any(a),
"not_any": lambda f, a: func.not_(f.any(a)),
"in_set": lambda f, a: func.find_in_set(a, f),
}

def __init__(self, operator=None):
if not operator:
operator = '=='
operator = "=="

if operator not in self.OPERATORS:
raise BadFilterFormat('Operator `{}` not valid.'.format(operator))
raise BadFilterFormat("Operator `{}` not valid.".format(operator))

self.operator = operator
self.function = self.OPERATORS[operator]
Expand All @@ -84,26 +73,30 @@ def __init__(self, filter_spec):
self.filter_spec = filter_spec

try:
filter_spec['field']
filter_spec["field"]
except KeyError:
raise BadFilterFormat('`field` is a mandatory filter attribute.')
raise BadFilterFormat("`field` is a mandatory filter attribute.")
except TypeError:
raise BadFilterFormat(
'Filter spec `{}` should be a dictionary.'.format(filter_spec)
"Filter spec `{}` should be a dictionary.".format(filter_spec)
)

self.operator = Operator(filter_spec.get('op'))
self.value = filter_spec.get('value')
value_present = True if 'value' in filter_spec else False
self.operator = Operator(filter_spec.get("op"))
self.value = filter_spec.get("value")
value_present = True if "value" in filter_spec else False
if not value_present and self.operator.arity == 2:
raise BadFilterFormat('`value` must be provided.')
raise BadFilterFormat("`value` must be provided.")

def get_named_models(self, model):
field = self.filter_spec['field']
operator = self.filter_spec['op'] if 'op' in self.filter_spec else None
field = self.filter_spec["field"]
operator = self.filter_spec["op"] if "op" in self.filter_spec else None

models = get_relationship_models(model, field)
return (list(), models) if should_filter_outer_join_relationship(operator) else (models, list())
return (
(list(), models)
if should_filter_outer_join_relationship(operator)
else (models, list())
)

def format_for_sqlalchemy(self, query, default_model):
filter_spec = self.filter_spec
Expand All @@ -115,7 +108,7 @@ def format_for_sqlalchemy(self, query, default_model):
function = operator.function
arity = operator.arity

field_name = self.filter_spec['field']
field_name = self.filter_spec["field"]
field = Field(model, field_name)
sqlalchemy_field = field.get_sqlalchemy_field()

Expand Down Expand Up @@ -144,28 +137,26 @@ def get_named_models(self, base_model):
return models_inner_join, models_outer_join

def format_for_sqlalchemy(self, query, default_model):
return self.function(*[
filter.format_for_sqlalchemy(query, default_model)
for filter in self.filters
])
return self.function(
*[
filter.format_for_sqlalchemy(query, default_model)
for filter in self.filters
]
)


def _is_iterable_filter(filter_spec):
""" `filter_spec` may be a list of nested filter specs, or a dict.
"""
return (
isinstance(filter_spec, Iterable) and
not isinstance(filter_spec, (string_types, dict))
"""`filter_spec` may be a list of nested filter specs, or a dict."""
return isinstance(filter_spec, Iterable) and not isinstance(
filter_spec, (string_types, dict)
)


def build_filters(filter_spec):
""" Recursively process `filter_spec` """
"""Recursively process `filter_spec`"""

if _is_iterable_filter(filter_spec):
return list(chain.from_iterable(
build_filters(item) for item in filter_spec
))
return list(chain.from_iterable(build_filters(item) for item in filter_spec))

if isinstance(filter_spec, dict):
# Check if filter spec defines a boolean function.
Expand All @@ -177,18 +168,16 @@ def build_filters(filter_spec):

if not _is_iterable_filter(fn_args):
raise BadFilterFormat(
'`{}` value must be an iterable across the function '
'arguments'.format(boolean_function.key)
"`{}` value must be an iterable across the function "
"arguments".format(boolean_function.key)
)
if boolean_function.only_one_arg and len(fn_args) != 1:
raise BadFilterFormat(
'`{}` must have one argument'.format(
boolean_function.key
)
"`{}` must have one argument".format(boolean_function.key)
)
if not boolean_function.only_one_arg and len(fn_args) < 1:
raise BadFilterFormat(
'`{}` must have one or more arguments'.format(
"`{}` must have one or more arguments".format(
boolean_function.key
)
)
Expand Down Expand Up @@ -258,8 +247,7 @@ def apply_filters(model, query, filter_spec, do_auto_join=True):
query = auto_join(query, inner_join_models, outer_join_models)

sqlalchemy_filters = [
filter.format_for_sqlalchemy(query, model)
for filter in filters
filter.format_for_sqlalchemy(query, model) for filter in filters
]

if sqlalchemy_filters:
Expand Down
Loading

0 comments on commit c1bc0fe

Please sign in to comment.