Pypher
Python Cypher Querybuilder
Install / Use
/learn @emehrkay/PypherREADME
Pypher -- Cypher, but in Python
Pypher is a tiny library that focuses on building Cypher queries by constructing pure Python objects.
Setup
python setup.py install
pip install python_cypher
Running Tests
python setup.py test
Or if the package is already installed
python -m unittest pypher.test.builder
Usage
Pypher is pretty simple and has a small interface. Pypher tries to replicate building a Cypher query by utilizing all of Python's magic methods behind the scenes.
Let's say you wanted to write this Cypher query:
MATCH (mark:Person)
WHERE mark.name = "Mark"
RETURN mark;
Your Pypher would look like this:
from pypher import Pypher
q = Pypher()
q.Match.node('mark', labels='Person').WHERE.mark.property('name') == 'Mark'
q.RETURN.mark
That isn't a one-to-one match, but it is close. More importantly, easy to read, understand, and compose complex queries without string concatenation.
Creating an actual Cypher string from a Pypher query is simple
cypher = str(q) # MATCH (mark:`Person`) WHERE mark.`name` = NEO_9326c_1 RETURN mark
params = q.bound_params # {'NEO_9326c_1': 'Mark'}
Note: Pypher doesn't create the Cypher string until your Pypher instance is converted into a string via
str(p)orprint(p)etc., at the same time all of the bound parameters are collected through the many possible sub-instances of Pypher objects that may be in the chain.
Structure
Pypher is a very simple query builder for Cypher. It works by creating a simple linked list of objects and running __str__ against the list when it is time to render the Cypher. Along the way it stores bound params, allows for complex Cypher queries with deep Pypher nestings, and even direct string inclusion if the abstraction gets too messy.
Pypher Objects
Pypher
Pypher is the root object that all other objects sub-class and it makes everything work. Every operation taken on it (attribute access or assignments or comparisons) will result in link being added to list.
Quoting: by default Pypher will quote labels, properties, and map_keys with backticks
. This behavior can be overwritten by setting the QUOTE value in the builder module.import pyper; pyper.builder.QUOTES['propery'] = '"'` this sets the quote marksf for properties to be a double quote instead of a backtick
Useful Methods and Properties
bind_param(value, name=None)-- this method will add a bound param to to resulting Cypher query. If a name is not passed it, one will be generated.add_link(link)-- this method is used in every interaction with the Pypher object. It lets you manually add a link to the list that you may not had been able to otherwise express with existing methods or objects.func(name, *args)-- this will allow you to call a custom function. Say you want the resulting Cypher to have a Python keyword like__init__, you would callq.func('__init__', 1, 2, 3)which would resolve to__init__(1, 2, 3)(the arguments will be bound).func_raw(name, *args)-- this acts just like the func method, but it will not bind the arguments passed in.raw(*args)-- this will take whatever you put in it and print it out in the resulting Cypher query. This is useful if you want to do something that may not be possible in the Pypher structure.rel_out(*args, **kwargs)-- this will start an outgoing relationship. SeeRelationshipfor argument details.rel_in(*args, **kwargs)-- this will start an incoming relationship. SeeRelationshipfor argument details.alias(alias)-- this is a way to allow for simpleAS $namein the resulting Cypher.property(name)-- since Pypher already co-opted the dot notation for stringing together the object, it needed a way to represent properties on aNodeorRelationship. Simply typeq.n.property('name')orq.n__name__to have it createn.namein Cypher. SeePropertyfor more details. Properties will be wrapped in back ticks to allow for spaces and other special characters.operator(operator, value)-- a simple way to add anything to the chain. All of the Pypher magic methods around assignments and math call this method. Note: theotherneeds to be a different Pypher instance or you will get a funky Cypher string._-- the current Pypher instance. This is useful for special edge cases. SeePropertyapply_partial-- adds the result of the Partial object to the given Pypher instance.append-- will allow multiplePypherinstances to be combined into a single chain.clone-- will create a copy of thePypherinstance and theParamsobject that holds thepypher_instance.bound_params
Operators
Since Pypher is an object whose sole job is to compose a linked list via a fluid interface, adding common operators to the object is tricky. Here are some rules:
- No matter the operator, the right side of the operation must not be the same Pypher instance as found on the left. A common way around this is to import and use the
__Anon Pypher factory. - Operators allow for Python dictionaires to be passed in
p.user += {'name': 'Mark'}
- You can create custom Operators by calling
.operator(name, other_value)on the Pypher instance -- the first operator rule must be followed if the other end is a Pypher object.- Operators always resolve in a space, the operator, and then the other value. Until it doesn't.
- Certain operators (all of the Python magic methods that support it) support reflected, or right, side assignment and will print the resulting Cypher as expected. Something like
99 - p.__field__will work as expected, but99 > p.__field__will result inp.field < 99
- Certain operators (all of the Python magic methods that support it) support reflected, or right, side assignment and will print the resulting Cypher as expected. Something like
- Operators always resolve in a space, the operator, and then the other value. Until it doesn't.
from pypher import Pypher, __
p = Pypher()
p.WHERE.n.name == __.s.__name__
str(p) # WHERE n.`name` = s.`name`
# custom operator
x = Pypher()
x.WHERE.name.operator('**', 'mark') # mark will be a bound param
str(x) # WHERE n.name ** NEO_az23p_0
| Pypher Operator | Resulting Cypher | Supports Referece Assignment |
| ------------- | ------------- | ------------- |
| == | = | - |
| != | <> | - |
| + | + | yes |
| += | += | - |
| - | - | yes |
| -= | -= | - |
| * | * | yes |
| *= | *= | - |
| / | / | yes |
| /= | /= | - |
| % | % | yes |
| %= | %= | - |
| & | & | yes |
| \| | \| | yes |
| ^ | ^ | yes |
| ^= | ^= | - |
| > | > | - |
| >= | >= | - |
| < | < | - |
| <= | <= | - |
Operator Methods
Some methods resolve to Operator instances. These are called on the Pypher instance with parenthesis.
| Pypher Operator | Resulting Cypher |
| ------------- | ------------- |
| .AND(other) | AND other |
| .OR(other) | OR other |
| .ALIAS(other) | AS other |
| .AS(other) | AS other |
| .rexp(other) | =~ $other_bound_param |
| .BAND(right, left) | apoc.bitwise.op(right, "&", left) |
| .BOR(right, left) | apoc.bitwise.op(right, "\|", left) |
| .BXOR(right, left) | apoc.bitwise.op(right, "^", left) |
| .BNOT(right, left) | apoc.bitwise.op(right, "~", left) |
| .BLSHIFT(right, left) | apoc.bitwise.op(right, ">>", left) |
| .BRSHIFT(right, left) | apoc.bitwise.op(right, "<<", left) |
| .BULSHIFT(right, left) | apoc.bitwise.op(right, ">>>", left) |
__ (double underscore)
__ The double underscore object is just an instance of Anon. It is basically a factory class that creates instances of Pypher when attributes are accessed against it.
- Useful for creating Pypher objects that will either be passed in as arguments or used to continue a chain after a math or assignment operation on an existing chain.
from pypher import __, Pypher
p = Pypher()
p.MATCH.node('mark', labels='Person').rel(labels='knows').node('mikey', labels=['Cat', 'Animal'])
p.RETURN(__.mark, __.mikey)
str(p) # MATCH (mark:`Person`)-[:`knows`]-(mikey:`Cat`:`Animal`) RETURN mark, mikey
# OR
p = Pypher()
p.MATCH.node('mark').SET(__.mark.property('name') == 'Mark!!')
print(str(p)) # MATCH (mark) SET mark.`name` = $NEO_2548a_0
print(dict(p.bound_params)) # {'NEO_2548a_0': 'Mark!!'}
The
__is just an instance of the Anon object. You can change what you want your factory name to be, or create an instance of Anon and assign it to another variable as you see fit.
Param
Param objects are simple containers that store a name and a value.
- These objects are useful when you want finer control over the names of the bound params in the resulting Cypher query.
- These can be passed in to Pyper instances and will be referenced by their name once the Cypher string is created.
Pypher.bind_paramwill return an instance of a Param object.- When binding params Pypher will reuse the existing reference if the same value is passed in.
- It will also reuse the same reference if the value passed in is the name of a previously bound param.
from pypher import Param, Pypher, __
p = Pypher()
name = Param(name='namedParam', value='Mark')
p.SET(__.m.__name__ == name)
str(p) # SET m.`name` = namedParam
print(p.bound_params) # {'namedParam': 'Mark'}
# reusing the same reference per value
param = p.bind_param('some value', 'key')
param2 = p.bind_param('some_value')
param.name == param2.name # True
# reusing the same reference when the value is the key
param = p.bind_param('some value', 'some key')
param2 = p.bind_param('some key')
param.name == param2.name # True
param.value == params2.value # True
Statement
Statement objects are simple, they are things like MATCH or CREATE or RETURN.
- Can be added to the list with any casing
q.MATCHis the same asa.matchboth will result inMATCHbeing generated. - When an undefined attribute is accessed on a Pypher instance, it will create a Statement from it.
q.iMade.ThisUpwill
