Skip to content
This repository was archived by the owner on Nov 8, 2024. It is now read-only.

Commit b24b9bc

Browse files
committed
setup: added tox configuration and made it possible to execute tests with various python functions.
1 parent 5d0bb85 commit b24b9bc

File tree

6 files changed

+325
-5
lines changed

6 files changed

+325
-5
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ htmlcov/
9696
.coverage.*
9797
.cache
9898
nosetests.xml
99+
nosexunit.xml
99100
coverage.xml
100101
*,cover
101102
.hypothesis/
@@ -115,3 +116,6 @@ target/
115116

116117
#Ipython Notebook
117118
.ipynb_checkpoints
119+
120+
coverage/
121+
htmlcov/

README.md

Lines changed: 222 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,225 @@
1-
# Filterparams #
1+
# Python Filterparams #
2+
3+
Filterparams is a library for parsing URL paramters for filter
4+
purposes in a backend. It provides a syntax to map SQL-like
5+
queries on top of the query parameters and parses it into a
6+
python object.
7+
8+
This is a helper library for providing filter collection APIs.
9+
The primary use case for developing the library is to
10+
use it with a REST-API which uses the [JSONAPI](http://jsonapi.org/)
11+
standard. Because of this the syntax is completely compatible with
12+
the standard and encapsulates everything in the `filter` query
13+
parameter.
14+
15+
## Example ##
16+
17+
Given the URL (non URL escaped for better readability):
18+
```
19+
/users?filter[param][name][like][no_default_name]=doe&filter[param][first_name]=doe%&filter[binding]=(!no_brand_name&first_name)&filter[order]=name&filter[order]=desc(first_name)
20+
```
21+
22+
It can be parsed by the given function:
23+
24+
```python
25+
from filterparams import build_parser
26+
valid_filters = ['eq', 'like']
27+
default_filter = 'eq'
28+
29+
parser = build_parser(
30+
valid_filters=valid_filters,
31+
default_filter=default_filter,
32+
)
33+
34+
query = parser(
35+
36+
)
37+
```
38+
39+
Would parse the data. You can access the parsed filters through
40+
`.param_order` and the orders through `.orders`. The param order
41+
in this specific case would be resolved to:
42+
43+
```python
44+
And(
45+
left=Parameter(
46+
name='name',
47+
alias='no_default_name',
48+
filter='like',
49+
value='doe%',
50+
),
51+
right=Parameter(
52+
name='first_name',
53+
alias='first_name',
54+
filter='eq',
55+
value='doe',
56+
)
57+
)
58+
```
59+
60+
The orders would be:
61+
62+
```python
63+
[Order(name='name', direction='asc'),
64+
Order(name='first_name', direction='desc')]
65+
```
66+
67+
## Syntax ##
68+
69+
All arguments must be prefixed by "filter". It is possible to
70+
query for specific data with filters, apply orders to the result
71+
and to combine filters through AND, NOT and OR bindings.
72+
73+
The syntax builds under the filter parameter a virtual object.
74+
The keys of the object are simulated through specifying `[{key}]`
75+
in the passed query parameter. Thus `filter[param]` would point
76+
to the param key in the filter object.
77+
78+
### Filter specification ###
79+
80+
The solution supports to query data through the `param` subkey.
81+
82+
```
83+
filter[param][{parameter_name}][{operation}][{alias}] = {to_query_value}
84+
```
85+
86+
The `operation` and `alias` parameters may be omitted. If no
87+
`alias` is provided the given parameter name is used for it.
88+
If no `operation` is given, the default one is used (in the
89+
example this would be equal).
90+
91+
Example:
92+
```
93+
filter[param][phone_number][like]=001%
94+
```
95+
96+
This would add a filter to all phone numbers which start with "001".
97+
98+
### Filter binding ###
99+
100+
Per default all filters are combined through AND clauses.
101+
You can change that by specifying the `filter[binding]` argument.
102+
103+
This is where the aliases which you can define come into place.
104+
The binding provides means to combine filters with AND and OR.
105+
Also you are able to negate filters here.
106+
107+
The filters are addressed by their alias or name, if no alias is
108+
provided.
109+
110+
If you have a filter `search_for_name`, `search_for_phone_number`
111+
and `search_for_account_number` defined you can say
112+
`search_for_name OR NOT search_for_number AND search_for_account_number`
113+
by specifying the following filter:
114+
115+
```
116+
filter[binding]=search_for_name|(!search_for_phone_number&search_for_account_number)
117+
```
118+
119+
Even though the brackets are useless here, you can use them in
120+
more complex filters.
121+
122+
The following table summarizes the possible configuration options:
123+
<table>
124+
<thead>
125+
<tr>
126+
<th>Type</th>
127+
<th>Symbol</th>
128+
<th>Example</th>
129+
</tr>
130+
</thead>
131+
<tbody>
132+
<tr>
133+
<td>AND</td>
134+
<td>&</td>
135+
<td>a&b</td>
136+
</tr>
137+
<tr>
138+
<td>OR</td>
139+
<td>|</td>
140+
<td>a|b</td>
141+
</tr>
142+
<tr>
143+
<td>NOT</td>
144+
<td>!</td>
145+
<td>!a</td>
146+
</tr>
147+
<tr>
148+
<td>Bracket</td>
149+
<td>()</td>
150+
<td>(a|b)&c</td>
151+
</tr>
152+
</tbody>
153+
</table>
154+
155+
### Ordering ###
156+
157+
To specify a sort order of the results the `filter[order]` parameter
158+
may be used. The value can be specified multiple times. To add
159+
ordering you have to provide the name of the parameter which should
160+
be ordered, not its alias!
161+
162+
If you want to order by `name`, `first_name` and in reverse order
163+
`balance` you can do so by specifying the following query url
164+
parameters:
165+
166+
```
167+
filter[order]=name&filter[order]=first_name&filter[order]=desc(balance)
168+
```
169+
170+
As you can see the `desc()` definition can be used to indicate
171+
reverse ordering.
172+
173+
### Filter definition ###
174+
175+
Not every backend does or should support all possible filter
176+
mechanisms. This is why the filters which should be accepted
177+
by the backend have to be added before processing the query
178+
parameters.
179+
180+
You can limit the allowed filters by building a parse through the
181+
`filterparams.build_parser` function. You can configure the allowed
182+
filters through the `valid_filters` definition. Additionally you
183+
have to add the default filter by using the second `default_filter`
184+
parameter.
185+
186+
```python
187+
from filterparams import build_parser
188+
189+
valid_filters = ['eq', 'like']
190+
default_filter = 'eq'
191+
192+
parser = build_parser(
193+
valid_filters=valid_filters,
194+
default_filter=default_filter,
195+
)
196+
197+
query = parser({})
198+
```
199+
200+
If you don't want any validation you can use the `parse` function.
201+
202+
```python
203+
from filterparams import parse
204+
205+
query = parse({})
206+
```
207+
208+
## Notes ##
209+
210+
- There do no yet exist any public projects which use this library to provide transparent mapping to an underlying
211+
backend. I plan long-term to add another library which does use this package and provide a way to map it on sqlalchemy models.
212+
If you are planning to do this or use it for other data mapping please contact me and I'll add a reference to it in
213+
the README.
214+
- The same as mentioned above is valid for client libraries, which generate the filter query structure in any language.
215+
Again, as soon as the API is stable I'll probably add a JavaScript library.
216+
- Depending on your backend it might not make sense to support all features (ordering, parameter binding) of the
217+
language. You might still want to use it to parse your basic parameters though and ignore the rest.
218+
219+
## Used Libraries ##
220+
221+
For evaluating the filter params ordering the [funcparserlib](https://github.com/vlasovskikh/funcparserlib) ([MIT license](https://github.com/vlasovskikh/funcparserlib/blob/master/LICENSE))
222+
module is used. Additionally the [Werkzeug](https://github.com/mitsuhiko/werkzeug/blob/master/LICENSE) package is used for supporting dicts with multiple values in the same key.
2223

3224
## Other Languages ##
4225

setup.cfg

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,17 @@
11
[metadata]
22
description-file = README.md
3+
4+
[nosetests]
5+
where=../test/tests
6+
verbosity=1
7+
detailed-errors=1
8+
cover-html=1
9+
cover-package=filterparams
10+
cover-html-dir=htmlcov
11+
with-xcoverage=1
12+
xcoverage-file=coverage.xml
13+
with-xunit=1
14+
xunit-file=nosexunit.xml
15+
cover-erase=1
16+
pdb=0
17+
pdb-failures=0

setup.py

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# -*- encoding: utf-8 -*-
22

3+
import sys
4+
35
from setuptools import setup, find_packages
6+
from distutils.command.build_py import build_py as _build_py
7+
8+
9+
setup_requires = []
410

511
requires = [
612
'funcparserlib>=0.3.6',
@@ -9,6 +15,72 @@
915

1016
VERSION = '1.0.0'
1117

18+
cmd_class = {}
19+
20+
21+
def match_patterns(path, pattern_list=[]):
22+
from fnmatch import fnmatch
23+
for pattern in pattern_list:
24+
if fnmatch(path, pattern):
25+
return True
26+
return False
27+
28+
29+
class build_py27(_build_py):
30+
def __init__(self, *args, **kwargs):
31+
_build_py.__init__(self, *args, **kwargs)
32+
import logging
33+
import pip
34+
pip.main(['install', '3to2'])
35+
36+
from lib2to3 import refactor
37+
import lib3to2.main
38+
rt_logger = logging.getLogger("RefactoringTool")
39+
rt_logger.addHandler(logging.StreamHandler())
40+
fixers = refactor.get_fixers_from_package('lib3to2.fixes')
41+
self.rtool = lib3to2.main.StdoutRefactoringTool(
42+
fixers,
43+
None,
44+
[],
45+
False,
46+
False
47+
)
48+
49+
def copy_file(self, source, target, preserve_mode=True):
50+
if source.endswith('.py'):
51+
try:
52+
print("3to2 converting: %s => %s" % (source, target))
53+
with open(source, 'rt') as input:
54+
# ensure file contents have trailing newline
55+
source_content = input.read() + "\n"
56+
nval = self.rtool.refactor_string(source_content, source)
57+
if nval is not None:
58+
with open(target, 'wt') as output:
59+
output.write('from __future__ import print_function\n')
60+
output.write(str(nval))
61+
else:
62+
raise(Exception("Failed to parse: %s" % source))
63+
except Exception as e:
64+
print("3to2 error (%s => %s): %s" % (source,target,e))
65+
66+
67+
if sys.version_info[0] < 3:
68+
setup_requires.append('pip')
69+
cmd_class['build_py'] = build_py27
70+
71+
package_dir = {
72+
'': 'src',
73+
}
74+
packages = find_packages('src')
75+
if 'nosetests' in sys.argv:
76+
packages += find_packages('test')
77+
packages = [
78+
item
79+
for item in packages
80+
if item != 'tests'
81+
]
82+
package_dir['tests'] = 'test/tests'
83+
1284
setup(
1385
name='filterparams',
1486
version=VERSION,
@@ -21,12 +93,14 @@
2193
],
2294
author='Christoph Brand',
2395
author_email='christoph@brand.rest',
96+
cmdclass=cmd_class,
2497
keywords=[],
25-
packages=find_packages('src'), # include all packages under src
26-
package_dir={'': 'src'}, # tell distutils packages are under src
98+
packages=packages, # include all packages under src
99+
package_dir=package_dir, # tell distutils packages are under src
27100
namespace_packages=[],
28101
include_package_data=True,
29102
zip_safe=False,
103+
setup_requires=setup_requires,
30104
install_requires=requires,
31105
url='https://github.com/cbrand/python-filterparams',
32106
download_url='https://github.com/cbrand/python-filterparams/tarball/%s' % VERSION,

src/filterparams/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# -*- encoding: utf-8 -*-
22

3-
from functools import partial
4-
53
from .parser import Parser
64
from .safe_parser import SafeParser
75

tox.ini

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# content of: tox.ini , put in same dir as setup.py
2+
[tox]
3+
envlist = py27,py34,py35
4+
[testenv]
5+
deps=nose
6+
nosexcover
7+
commands=python setup.py nosetests
8+

0 commit comments

Comments
 (0)