├── MANIFEST.in ├── .gitignore ├── Makefile ├── setup.py ├── UNLICENSE ├── test_rkquery.py ├── README.md ├── README.rst └── rkquery.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include UNLICENSE 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | *.pyo 4 | .DS_Store 5 | build 6 | dist 7 | MANIFEST 8 | test/example/*.sqlite3 9 | doc/.build 10 | distribute-*.egg 11 | distribute-*.tar.gz 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all test 2 | 3 | all: test README.rst 4 | 5 | test: 6 | nosetests --with-coverage --with-doctest 7 | 8 | README.rst: README.md 9 | pandoc -f markdown -t rst README.rst 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from distutils.core import setup 4 | 5 | readme_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'README.rst') 6 | readme = open(readme_file).read() 7 | 8 | 9 | setup( 10 | name='rkquery', 11 | version='0.0.1', 12 | description="Build Riak search queries safely and easily.", 13 | long_description=readme, 14 | url='https://github.com/zacharyvoase/rkquery', 15 | author='Zachary Voase', 16 | author_email='z@zacharyvoase.com', 17 | py_modules=['rkquery'], 18 | ) 19 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /test_rkquery.py: -------------------------------------------------------------------------------- 1 | from nose.tools import assert_raises 2 | 3 | import rkquery 4 | 5 | 6 | class ExampleQueryNode(rkquery.QueryNode): 7 | __slots__ = ('a', 'b', 'c') 8 | 9 | 10 | def test_QueryNode_init(): 11 | # Too few arguments 12 | assert_raises(TypeError, ExampleQueryNode, 1) 13 | assert_raises(TypeError, ExampleQueryNode, 1, 2) 14 | 15 | # Too many positional arguments 16 | assert_raises(TypeError, ExampleQueryNode, 1, 2, 3, 4) 17 | 18 | # Too many keyword arguments 19 | assert_raises(TypeError, ExampleQueryNode, 1, 2, 3, key=4) 20 | assert_raises(TypeError, ExampleQueryNode, 1, 2, 3, key=4, key2=5) 21 | 22 | # Exactly the right number of arguments 23 | node = ExampleQueryNode(1, 2, 3) 24 | assert node.a == 1 25 | assert node.b == 2 26 | assert node.c == 3 27 | node = ExampleQueryNode(1, 3, b=2) 28 | assert node.a == 1 29 | assert node.b == 2 30 | assert node.c == 3 31 | node = ExampleQueryNode(a=1, b=2, c=3) 32 | assert node.a == 1 33 | assert node.b == 2 34 | assert node.c == 3 35 | 36 | 37 | def test_cannot_nest_boosts(): 38 | boost1 = rkquery.Q("a").boost(5) 39 | boost2 = boost1.boost(10) 40 | assert boost2.root.node == boost1.root.node 41 | assert boost2.root.factor == 10 42 | 43 | 44 | def test_cannot_nest_proximities(): 45 | proxim1 = rkquery.Q("a").proximity(5) 46 | proxim2 = proxim1.proximity(10) 47 | assert proxim2.root.node == proxim1.root.node 48 | assert proxim2.root.proximity == 10 49 | 50 | 51 | def test_and_is_commutative(): 52 | first = rkquery.Q.all("A", "B") & rkquery.Q.all("C", "D") 53 | second = rkquery.Q.all("A", "B", "C", "D") 54 | assert unicode(first) == unicode(second) 55 | 56 | 57 | def test_or_is_commutative(): 58 | first = rkquery.Q.any("A", "B") | rkquery.Q.any("C", "D") 59 | second = rkquery.Q.any("A", "B", "C", "D") 60 | assert unicode(first) == unicode(second) 61 | 62 | 63 | def test_not_not_x_is_x(): 64 | # NOT (NOT x) => x 65 | x = rkquery.Q("Foo") 66 | not_x = ~x 67 | assert not_x.root.child is x.root 68 | not_not_x = ~not_x 69 | assert not_not_x.root is x.root 70 | 71 | 72 | def test_field_not_becomes_not_field(): 73 | # field:(NOT x) => NOT (field:x) 74 | field_not_x = rkquery.Q(username=~rkquery.Q("a")) 75 | assert isinstance(field_not_x.root, rkquery.Not) 76 | assert isinstance(field_not_x.root.child, rkquery.Field) 77 | assert field_not_x.root.child.field_name == "username" 78 | assert field_not_x.root.child.pattern == rkquery.Literal("a") 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rkQuery 2 | 3 | rkQuery is a library for programmatically building Riak search queries. It aims 4 | to be easy to use, powerful, and protect from injection attacks that would be 5 | possible with simple string interpolation. 6 | 7 | 8 | ## Installation 9 | 10 | pip install rkquery 11 | 12 | 13 | ## Building Queries 14 | 15 | Just start playing around with the ``Q`` object. Literals (that is, raw strings 16 | in queries) will be escaped if necessary: 17 | 18 | ```pycon 19 | >>> from rkquery import Q 20 | >>> Q("some literal") 21 | 22 | >>> Q(field="literal value") 23 | 24 | >>> Q.not_(blocked="yes") 25 | 26 | ``` 27 | 28 | You can provide multiple arguments, too. The default query combinator is `AND`: 29 | 30 | ```pycon 31 | >>> Q("word1", "word2") 32 | 33 | >>> Q(username='foo', password='s3cr3t') 34 | 35 | ``` 36 | 37 | This is just a synonym for `Q.all()`: 38 | 39 | ```pycon 40 | >>> Q.all("word1", "word2") 41 | 42 | >>> Q.all(username='foo', password='s3cr3t') 43 | 44 | ``` 45 | 46 | You can construct `OR` queries using `Q.any()`: 47 | 48 | ```pycon 49 | >>> Q.any("word1", "word2") 50 | 51 | >>> Q.any(username='foo', email='foo@example.com') 52 | 53 | >>> Q(field=Q.any("string1", "string2")) 54 | 55 | ``` 56 | 57 | Or by combining existing `Q` objects with the bitwise logical operators: 58 | 59 | ```pycon 60 | >>> Q.any("word1", "word2") & Q("word3") 61 | 62 | >>> Q("word3") | Q.all("word1", "word2") 63 | 64 | >>> Q.any(email="foo@example.com", username="foo") & Q(password="s3cr3t") 65 | 66 | ``` 67 | 68 | There are helpers for negation as well (note that 'none' means 'not any'): 69 | 70 | ```pycon 71 | >>> Q.none(blocked="yes", cheque_bounced="yes") 72 | 73 | >>> ~Q.any(blocked="yes", cheque_bounced="yes") 74 | 75 | ``` 76 | 77 | You can do range queries with `Q.range()`: 78 | 79 | ```pycon 80 | >>> Q.range("red", "rum") 81 | 82 | >>> Q(field=Q.range("red", "rum")) 83 | 84 | ``` 85 | 86 | Note that the default is an *inclusive* range (square brackets). The full set 87 | of range queries: 88 | 89 | ```pycon 90 | >>> Q.range_inclusive("red", "rum") 91 | 92 | >>> Q.range_exclusive("red", "rum") 93 | 94 | >>> Q.between("red", "rum") 95 | 96 | ``` 97 | 98 | Term boosting is a simple unary operation: 99 | 100 | ```pycon 101 | >>> Q("red").boost(5) 102 | 103 | ``` 104 | 105 | As is proximity: 106 | 107 | ```pycon 108 | >>> Q("See spot run").proximity(20) 109 | 110 | ``` 111 | 112 | 113 | ## Running Queries 114 | 115 | When you’ve built a query and you want to execute it, just call ``unicode()`` 116 | on it to get the full query string: 117 | 118 | ```pycon 119 | >>> query = Q(field1="foo", field2="bar") 120 | >>> unicode(query) 121 | u'field1:foo AND field2:bar' 122 | ``` 123 | 124 | You can then use the standard Riak client search methods with this string. 125 | 126 | 127 | ## Unlicense 128 | 129 | This is free and unencumbered software released into the public domain. 130 | 131 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this 132 | software, either in source code form or as a compiled binary, for any purpose, 133 | commercial or non-commercial, and by any means. 134 | 135 | In jurisdictions that recognize copyright laws, the author or authors of this 136 | software dedicate any and all copyright interest in the software to the public 137 | domain. We make this dedication for the benefit of the public at large and to 138 | the detriment of our heirs and successors. We intend this dedication to be an 139 | overt act of relinquishment in perpetuity of all present and future rights to 140 | this software under copyright law. 141 | 142 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 143 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 144 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 145 | AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 146 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 147 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 148 | 149 | For more information, please refer to 150 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | rkQuery 2 | ======= 3 | 4 | rkQuery is a library for programmatically building Riak search queries. 5 | It aims to be easy to use, powerful, and protect from injection attacks 6 | that would be possible with simple string interpolation. 7 | 8 | Installation 9 | ------------ 10 | 11 | :: 12 | 13 | pip install rkquery 14 | 15 | Building Queries 16 | ---------------- 17 | 18 | Just start playing around with the ``Q`` object. Literals (that is, raw 19 | strings in queries) will be escaped if necessary: 20 | 21 | :: 22 | 23 | >>> from rkquery import Q 24 | >>> Q("some literal") 25 | 26 | >>> Q(field="literal value") 27 | 28 | >>> Q.not_(blocked="yes") 29 | 30 | 31 | You can provide multiple arguments, too. The default query combinator is 32 | ``AND``: 33 | 34 | :: 35 | 36 | >>> Q("word1", "word2") 37 | 38 | >>> Q(username='foo', password='s3cr3t') 39 | 40 | 41 | This is just a synonym for ``Q.all()``: 42 | 43 | :: 44 | 45 | >>> Q.all("word1", "word2") 46 | 47 | >>> Q.all(username='foo', password='s3cr3t') 48 | 49 | 50 | You can construct ``OR`` queries using ``Q.any()``: 51 | 52 | :: 53 | 54 | >>> Q.any("word1", "word2") 55 | 56 | >>> Q.any(username='foo', email='foo@example.com') 57 | 58 | >>> Q(field=Q.any("string1", "string2")) 59 | 60 | 61 | Or by combining existing ``Q`` objects with the bitwise logical 62 | operators: 63 | 64 | :: 65 | 66 | >>> Q.any("word1", "word2") & Q("word3") 67 | 68 | >>> Q("word3") | Q.all("word1", "word2") 69 | 70 | >>> Q.any(email="foo@example.com", username="foo") & Q(password="s3cr3t") 71 | 72 | 73 | There are helpers for negation as well (note that 'none' means 'not 74 | any'): 75 | 76 | :: 77 | 78 | >>> Q.none(blocked="yes", cheque_bounced="yes") 79 | 80 | >>> ~Q.any(blocked="yes", cheque_bounced="yes") 81 | 82 | 83 | You can do range queries with ``Q.range()``: 84 | 85 | :: 86 | 87 | >>> Q.range("red", "rum") 88 | 89 | >>> Q(field=Q.range("red", "rum")) 90 | 91 | 92 | Note that the default is an *inclusive* range (square brackets). The 93 | full set of range queries: 94 | 95 | :: 96 | 97 | >>> Q.range_inclusive("red", "rum") 98 | 99 | >>> Q.range_exclusive("red", "rum") 100 | 101 | >>> Q.between("red", "rum") 102 | 103 | 104 | Term boosting is a simple unary operation: 105 | 106 | :: 107 | 108 | >>> Q("red").boost(5) 109 | 110 | 111 | As is proximity: 112 | 113 | :: 114 | 115 | >>> Q("See spot run").proximity(20) 116 | 117 | 118 | Running Queries 119 | --------------- 120 | 121 | When you’ve built a query and you want to execute it, just call 122 | ``unicode()`` on it to get the full query string: 123 | 124 | :: 125 | 126 | >>> query = Q(field1="foo", field2="bar") 127 | >>> unicode(query) 128 | u'field1:foo AND field2:bar' 129 | 130 | You can then use the standard Riak client search methods with this 131 | string. 132 | 133 | Unlicense 134 | --------- 135 | 136 | This is free and unencumbered software released into the public domain. 137 | 138 | Anyone is free to copy, modify, publish, use, compile, sell, or 139 | distribute this software, either in source code form or as a compiled 140 | binary, for any purpose, commercial or non-commercial, and by any means. 141 | 142 | In jurisdictions that recognize copyright laws, the author or authors of 143 | this software dedicate any and all copyright interest in the software to 144 | the public domain. We make this dedication for the benefit of the public 145 | at large and to the detriment of our heirs and successors. We intend 146 | this dedication to be an overt act of relinquishment in perpetuity of 147 | all present and future rights to this software under copyright law. 148 | 149 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 150 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 151 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 152 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 153 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 154 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 155 | DEALINGS IN THE SOFTWARE. 156 | 157 | For more information, please refer to http://unlicense.org/ 158 | -------------------------------------------------------------------------------- /rkquery.py: -------------------------------------------------------------------------------- 1 | """ 2 | rkQuery is a library for programmatically building Riak search queries. It aims 3 | to be easy to use, powerful, and protect from injection attacks that would be 4 | possible with simple string interpolation. 5 | 6 | Just start playing around with the ``Q`` object: 7 | 8 | >>> from rkquery import Q 9 | >>> Q("some literal") 10 | 11 | >>> Q(field="literal value") 12 | 13 | >>> Q.not_(blocked="yes") 14 | 15 | 16 | You can provide multiple arguments, too. The default query combinator is `AND`: 17 | 18 | >>> Q("word1", "word2") 19 | 20 | >>> Q(username='foo', password='s3cr3t') 21 | 22 | 23 | This is just a synonym for `Q.all()`: 24 | 25 | >>> Q.all("word1", "word2") 26 | 27 | >>> Q.all(username='foo', password='s3cr3t') 28 | 29 | 30 | Of course you can construct `OR` queries, using `Q.any()`: 31 | 32 | >>> Q.any("word1", "word2") 33 | 34 | >>> Q.any(username='foo', email='foo@example.com') 35 | 36 | >>> Q(field=Q.any("string1", "string2")) 37 | 38 | 39 | Or by combining existing `Q` objects: 40 | 41 | >>> Q.any("word1", "word2") & Q("word3") 42 | 43 | >>> Q("word3") | Q.all("word1", "word2") 44 | 45 | >>> Q.any(email="foo@example.com", username="foo") & Q(password="s3cr3t") 46 | 47 | 48 | There are helpers for negation as well (note that 'none' means 'not any'): 49 | 50 | >>> Q.none(blocked="yes", cheque_bounced="yes") 51 | 52 | >>> ~Q.any(blocked="yes", cheque_bounced="yes") 53 | 54 | 55 | You can do range queries with `Q.range()`: 56 | 57 | >>> Q.range("red", "rum") 58 | 59 | >>> Q(field=Q.range("red", "rum")) 60 | 61 | 62 | Note that the default is an *inclusive* range (square brackets). The full set 63 | of range queries: 64 | 65 | >>> Q.range_inclusive("red", "rum") 66 | 67 | >>> Q.range_exclusive("red", "rum") 68 | 69 | >>> Q.between("red", "rum") 70 | 71 | 72 | Term boosting is a simple unary operation: 73 | 74 | >>> Q("red").boost(5) 75 | 76 | 77 | As is proximity: 78 | 79 | >>> Q("See spot run").proximity(20) 80 | 81 | """ 82 | 83 | import itertools as it 84 | import re 85 | 86 | 87 | class Query(object): 88 | """ 89 | A Riak query. 90 | 91 | This object represents a Riak query. You can add more constraints using the 92 | various methods and operators defined on this class. 93 | 94 | To get your generated query, just use ``unicode()``: 95 | 96 | >>> unicode(Q(field1="foo", field2="bar")) 97 | u'field1:foo AND field2:bar' 98 | """ 99 | 100 | __slots__ = ('root', '__weakref__') 101 | 102 | def __init__(self, root): 103 | self.root = root 104 | 105 | def __repr__(self): 106 | return "" % unicode(self.root) 107 | 108 | def __unicode__(self): 109 | return unicode(self.root) 110 | 111 | def __str__(self): 112 | return str(self.root) 113 | 114 | def __or__(self, other): 115 | if hasattr(self.root, '__or__'): 116 | return Query(self.root | make_node(other)) 117 | return Query(Any((self.root, make_node(other)))) 118 | 119 | def __and__(self, other): 120 | if hasattr(self.root, '__and__'): 121 | return Query(self.root & make_node(other)) 122 | return Query(All((self.root, make_node(other)))) 123 | 124 | def __invert__(self): 125 | if not hasattr(self.root, '__invert__'): 126 | return Query(Not(self.root)) 127 | return Query(~self.root) 128 | 129 | def boost(self, factor): 130 | """Set the result importance factor of this term.""" 131 | return Query(Boost(self.root, factor)) 132 | 133 | def proximity(self, proximity): 134 | """Set a proximity for this term.""" 135 | return Query(Proximity(self.root, proximity)) 136 | 137 | 138 | class QueryNode(object): 139 | """Query node base class.""" 140 | 141 | __slots__ = () 142 | # Is it safe to display this node without parentheses as part of a complex 143 | # query? 144 | no_parens = False 145 | 146 | def __init__(self, *args, **kwargs): 147 | argc = len(args) + len(kwargs) 148 | for slot in self.__slots__: 149 | if args and slot not in kwargs: 150 | setattr(self, slot, args[0]) 151 | args = args[1:] 152 | elif slot in kwargs: 153 | setattr(self, slot, kwargs.pop(slot)) 154 | else: 155 | raise TypeError("Expected argument for slot %r" % slot) 156 | if args: 157 | raise TypeError("Too many arguments (expected max %d, got %d)" % ( 158 | len(self.__slots__), argc)) 159 | elif kwargs: 160 | if len(kwargs) == 1: 161 | raise TypeError("Unexpected kwarg: %r" % kwargs.keys()[0]) 162 | raise TypeError("Unexpected kwargs: %r" % kwargs.keys()) 163 | 164 | def __str__(self): 165 | return unicode(self).encode('utf-8') 166 | 167 | def __unicode__(self): 168 | raise NotImplementedError 169 | 170 | def __eq__(self, other): 171 | if type(self) is not type(other): 172 | return False 173 | return all(getattr(self, slot, None) == getattr(other, slot, None) 174 | for slot in self.__slots__) 175 | 176 | def sort_key(self): 177 | """Return a tuple by which this node may be sorted.""" 178 | return (unicode(self),) 179 | 180 | def parens(self): 181 | """Return a unicode representation, in parentheses.""" 182 | if self.no_parens: 183 | return unicode(self) 184 | return u'(%s)' % unicode(self) 185 | 186 | 187 | class Literal(QueryNode): 188 | __slots__ = ('string',) 189 | # string: the string itself 190 | no_parens = True 191 | 192 | def __unicode__(self): 193 | if self.needs_escaping(self.string): 194 | return self.escape(self.string) 195 | return self.string 196 | 197 | @staticmethod 198 | def escape(string): 199 | """Escape a literal string (without adding quotes).""" 200 | return u'"%s"' % (string 201 | .replace(r'\\', r'\\\\') 202 | .replace(r'"', r'\\"') 203 | .replace(r'\'', r'\\\'')) 204 | 205 | @staticmethod 206 | def needs_escaping(string): 207 | """Check if a string requires quoting or escaping.""" 208 | return not re.match(r'^[A-Za-z0-9]+$', string) 209 | 210 | def sort_key(self): 211 | return (self.string,) 212 | 213 | 214 | class Boost(QueryNode): 215 | __slots__ = ('node', 'factor') 216 | # node: the node to boost 217 | # factor: the factor by which to boost it 218 | 219 | def __init__(self, node, factor): 220 | self.factor = factor 221 | if isinstance(node, type(self)): 222 | self.node = node.node 223 | else: 224 | self.node = node 225 | 226 | def __unicode__(self): 227 | return u'%s^%d' % (self.node.parens(), self.factor) 228 | 229 | def sort_key(self): 230 | return self.node.sort_key() 231 | 232 | 233 | class Proximity(QueryNode): 234 | __slots__ = ('node', 'proximity') 235 | # node: the term to apply a proximity search to 236 | # proximity: the size of the block in which to search 237 | 238 | def __init__(self, node, proximity): 239 | self.proximity = proximity 240 | if isinstance(node, type(self)): 241 | self.node = node.node 242 | else: 243 | self.node = node 244 | 245 | def __unicode__(self): 246 | return u'%s~%d' % (self.node.parens(), self.proximity) 247 | 248 | def sort_key(self): 249 | return self.node.sort_key() 250 | 251 | 252 | class Field(QueryNode): 253 | __slots__ = ('field_name', 'pattern') 254 | # field_name: the name of the field to query against. 255 | # pattern: a QueryNode representing the pattern against the field. 256 | no_parens = True 257 | 258 | def __new__(cls, field_name, pattern): 259 | # field:(NOT x) => (NOT field:x) 260 | if isinstance(pattern, Not): 261 | return Not(cls(field_name, pattern.child)) 262 | return QueryNode.__new__(cls, field_name, pattern) 263 | 264 | def __unicode__(self): 265 | return u'%s:%s' % (unicode(self.field_name), self.pattern.parens()) 266 | 267 | def sort_key(self): 268 | return (self.field_name,) + self.pattern.sort_key() 269 | 270 | 271 | class LogicalOperator(QueryNode): 272 | __slots__ = ('children',) 273 | operator = NotImplemented 274 | 275 | def __init__(self, children): 276 | self.children = tuple(sorted(children, key=lambda c: c.sort_key())) 277 | 278 | def __unicode__(self): 279 | return (u' %s ' % self.operator).join( 280 | child.parens() for child in self.children) 281 | 282 | def sort_key(self): 283 | return None 284 | 285 | 286 | class Any(LogicalOperator): 287 | operator = 'OR' 288 | 289 | def __or__(self, other): 290 | if isinstance(other, type(self)): 291 | return type(self)(self.children + other.children) 292 | else: 293 | return type(self)(self.children + (other,)) 294 | 295 | 296 | class All(LogicalOperator): 297 | operator = 'AND' 298 | 299 | def __and__(self, other): 300 | if isinstance(other, type(self)): 301 | return type(self)(self.children + other.children) 302 | else: 303 | return type(self)(self.children + (other,)) 304 | 305 | 306 | class Not(QueryNode): 307 | __slots__ = ('child',) 308 | no_parens = True 309 | 310 | def __unicode__(self): 311 | return u'NOT %s' % self.child.parens() 312 | 313 | def __invert__(self): 314 | return self.child 315 | 316 | 317 | class InclusiveRange(QueryNode): 318 | __slots__ = ('start', 'stop') 319 | no_parens = True 320 | 321 | def __unicode__(self): 322 | return u'[%s TO %s]' % (self.start.parens(), self.stop.parens()) 323 | 324 | 325 | class ExclusiveRange(QueryNode): 326 | __slots__ = ('start', 'stop') 327 | no_parens = True 328 | 329 | def __unicode__(self): 330 | return u'{%s TO %s}' % (self.start.parens(), self.stop.parens()) 331 | 332 | 333 | def make_node(obj): 334 | if isinstance(obj, Query): 335 | return obj.root 336 | elif isinstance(obj, QueryNode): 337 | return obj 338 | elif isinstance(obj, unicode): 339 | return Literal(obj) 340 | elif isinstance(obj, str): 341 | return Literal(obj.decode('utf-8')) 342 | elif isinstance(obj, tuple) and len(obj) == 2: 343 | return Field(obj[0], make_node(obj[1])) 344 | raise TypeError("Cannot make a query node from: %r" % (obj,)) 345 | 346 | 347 | def Q(*args, **kwargs): 348 | 349 | """ 350 | Build Riak search queries safely and easily. 351 | 352 | This is the primary point of interaction with this library. For examples of 353 | how to use it, consult the docstring on the ``rkquery`` module. 354 | """ 355 | 356 | return q_all(*args, **kwargs) 357 | 358 | 359 | def combinator(name, c_type): 360 | def q_combinator(*args, **kwargs): 361 | argc = len(args) + len(kwargs) 362 | if argc == 1: 363 | if args: 364 | return Query(make_node(args[0])) 365 | else: 366 | return Query(make_node(kwargs.items()[0])) 367 | else: 368 | return Query(c_type(make_node(arg) 369 | for arg in it.chain(args, kwargs.iteritems()))) 370 | q_combinator.__name__ = name 371 | return q_combinator 372 | 373 | 374 | q_any = combinator('q_any', Any) 375 | q_all = combinator('q_all', All) 376 | 377 | 378 | def q_none(*args, **kwargs): 379 | return ~q_any(*args, **kwargs) 380 | 381 | 382 | def q_not(*args, **kwargs): 383 | return ~q_all(*args, **kwargs) 384 | 385 | 386 | def q_inclusive_range(start, stop): 387 | return Query(InclusiveRange(make_node(start), make_node(stop))) 388 | 389 | 390 | def q_exclusive_range(start, stop): 391 | return Query(ExclusiveRange(make_node(start), make_node(stop))) 392 | 393 | 394 | Q.all = q_all 395 | Q.any = q_any 396 | Q.none = q_none 397 | Q.not_ = q_not 398 | Q.range = q_inclusive_range 399 | Q.range_inclusive = q_inclusive_range 400 | Q.range_exclusive = q_exclusive_range 401 | Q.between = q_exclusive_range 402 | --------------------------------------------------------------------------------