Python Cypher Querybuilder

emehrkay, updated 🕥 2022-12-09 04:35:26

Pypher -- Cypher, but in Python

Pypher is a tiny library that focuses on building Cypher queries by constructing pure Python objects.

Binder

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:

```python 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

python 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) or print(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 call q.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. See Relationship for argument details.
  • rel_in(*args, **kwargs) -- this will start an incoming relationship. See Relationship for argument details.
  • alias(alias) -- this is a way to allow for simple AS $name in 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 a Node or Relationship. Simply type q.n.property('name') or q.n__name__ to have it create n.name in Cypher. See Property for 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: the other needs 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. See Property
  • apply_partial -- adds the result of the Partial object to the given Pypher instance.
  • append -- will allow multiple Pypher instances to be combined into a single chain.
  • clone -- will create a copy of the Pypher instance and the Params object that holds the pypher_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, but 99 > p.__field__ will result in p.field < 99

```python 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.

```python 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_param will 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.

```python 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.MATCH is the same as a.match both will result in MATCH being generated.
  • When an undefined attribute is accessed on a Pypher instance, it will create a Statement from it. q.iMade.ThisUp will result in IMADE THISUP
  • Will print out in ALL CAPS and end with an empty space.
  • Can take a list of arguments q.return(1, 2, 3) will print out RETURN 1, 2, 3
  • Can also just exist along the chain a.MATCH.node('m') will print out MATCH (m)
  • Random statements can be created. This is useful for when Cypher grows, but Pypher may not have manually defined the new functionality
    • p.some_statement(1, 2, 3) will return random_statement 1, 2, 3
  • Pypher provides a suite of pre-defined statements out of the box:

| Pypher Object | Resulting Cypher | Aliases | | ------------- | ------------- | ------------- | | Match | MATCH | | | Create | CREATE | | | Merge | MERGE | | | Delete | DELETE | | | Remove | REMOVE | | | Drop | DROP | | | Where | WHERE | | | Distinct | DISTINCT | | | OrderBy | ORDER BY | | | Set | SET | | | Skip | SKIP | | | Limit | LIMIT | | | Return | RETURN | | | Unwind | UNWIND | | | ASSERT | ASSERT | | | Detach | DETACH | | | DetachDelete | DETACH DELETE | | | Foreach | FOREACH | | | Load | LOAD | | | CSV | CSV | | | FROM | FROM | | | Headers | HEADERS | | | LoadCsvFrom | LOAD CSV FROM | | | LoadCSVWithHeadersFrom | LOAD CSV WITH HEADERS FROM | | | WITH | WITH | | | UsingPeriodIcCommit | USING PERIODIC COMMIT | | | Periodic | PERIODIC | | | Commit | COMMIT | | | FieldTerminator | FIELDTERMINATOR | | | Optional | OPTIONAL | | | OptionalMatch | OPTIONAL MATCH | | | Desc | DESC | | | When | WHEN | | | ELSE | ELSE | | | Case | CASE | | | End | END | | | OnCreate | ON CREATE | | | OnCreateSet | ON CREATE SET | | | OnMatchSet | ON MATCH SET | | | CreateIndexOn | CREATE INDEX ON | | | UsingIndex | USING INDEX | | | DropIndexOn | DROP INDEX ON | | | CreateConstraintOn | CREATE CONSTRAINT ON | | | DropConstraintOn | DROP CONSTRAINT ON | | | In | IN | | | Map | {} | | | MapProjection | var {} | map_projection projection | | NOT | NOT | | | IS | IS | | | OR | OR | | | NULL | NULL | | | IS_NULL | IS NULL | | | IS NOT NULL | IS NOT NULL | |

Python keywords will be in all CAPS

  • Pypher provides a way to define a custom Statement class via a function call (this is used to create all of the statements listed above).

```python from pypher import create_statement, Pypher

create_statement('MyStatementName', {'name': 'MY STATEMENT IN CYPHER'})

p = Pypher()

p.MyStatementName.is.cool

str(p) # MY STATEMENT IN CYPHER IS COOL ```

The name definition is optional. If omitted the resulting Cypher will be the class name in call caps

Another way is to sub-class the Statement class

```python from pypher import Pypher, Statement

class MyStatement(Statement): _CAPITALIZE = True # will make the resulting name all caps. Defaults to True _ADD_PRECEEDING_WS = True # add whitespace before the resulting Cypher string. Defaults to True _CLEAR_PRECEEDING_WS = True # add whitespace after the resulting Cypher string. Defaults to False _ALIASES = ['myst',] # aliases for your custom statement. Will throw an exception if it is already defined name = 'my statement name' # the string that will be printed in the resulting Cypher. If this isn't defined, the class name will be used ```

Func

Func objects resolve to functions (things that have parenthesis)

  • Func objects take a list of arguments. These can be anything from Python primitives to nested Pypher objects, it must have a __str__ representation to be used.
  • Each argument will be automatically set as a bound parameter unless it is either a Param , Pypher, or Partial object. If the argument is not from the Pypher module, it will be given a randomly generated name in the resulting Cypher query and bound params.
  • Can take an unlimited number of arguments.
  • Pypher provides a suite of pre-defined functions out of the box:

| Pypher Object | Resulting Cypher | | ------------- | ------------- | | size | size | | reverse | reverse | | head | head | | tail | tail | | last | last | | extract | extract | | filter | filter | | reduce | reduce | | Type | type | | startNode | startNode | | endNode | endNode | | count | count | | ID | id | | collect | collect | | sum | sum | | percentileDisc| percentileDisc | | stDev | stDev | | coalesce | coalesce | | timestamp | timestamp | | toInteger | toInteger | | toFloat | toFloat | | toBoolean | toBoolean | | keys | keys | | properties | properties | | length | length | | nodes | nodes | | relationships | relationships | | point | point | | distance | distance | | abs | abs | | rand | rand | | ROUND | round | | CEIL | ceil | | Floor | floor | | sqrt | sqrt | | sign | sign | | sin | sin | | cos | cos | | tan | tan | | cot | cot | | asin | asin | | acos | acos | | atan | atan | | atanZ | atanZ | | haversin | haversin | | degrees | degrees | | radians | radians | | pi | pi | | log10 | log10 | | log | log | | exp | exp | | E | e | | toString | toString | | replace | replace | | substring | substring | | left | left | | right | right | | trim | trim | | ltrim | ltrim | | toUpper | toUpper | | toLower | toLower | | SPLIT | split | | exists | exists | | MAX | max |

Python keywords will be in all CAPS

  • Pypher provides a way to define a custom Func or FuncRaw class via a function call (this is used to create all of the functions listed above)

```python from pypher import create_function, Pypher

create_function('myFunction', {'name': 'mfun'})

p = Pypher()

p.myFunction(1, 2, 3)

str(p) # myFunction(1, 2, 3) note that the arguments will be bound and not "1, 2, 3" ```

The name definition is optional. If omitted the resulting Cypher will be the exact name of the function

Another way is to sub-class the Func or FuncRaw class.

FuncRaw will not bind its arguments.

```python from pypher import Pypher, Func, FuncRaw

class MyCustomFunction(Func): _CAPATILIZE = True # will make the resulting name all caps. Defaults to False _ADD_PRECEEDING_WS = True # add whitespace before the resulting Cypher string. Defaults to True _CLEAR_PRECEEDING_WS = True # add whitespace after the resulting Cypher string. Defaults to False _ALIASES = ['myst',] # aliases for your custom function. Will throw an exception if it is already defined name = 'myCustomFunction' # the string that will be printed in the resulting Cypher. If this isn't defined, the class name will be used ```

Conditionals

Conditional objects allow groupings of values surrounded by parenthesis and separated by a comma or other value.

| Pypher Object | Resulting Cypher | Aliases | | ------------- | ------------- | ---------- | | Conditional | (val, val2, valN) | | | ConditionalAND | (val AND val2 AND valN) | CAND, COND_AND | | ConditionalOR | (val OR val2 OR valN) | COR, COND_OR |

Entity

Entities are Node or Relationship objects. They both sub-class the Entity class and share the same attributes.

_Node__ This represents an actual node in the ascii format.

  • The init can accept a variable\, labels, properties\
  • Can be added to the chain by typing .node or .n_

_Relationship__ This represents an relationship node in the ascii format.

  • The init can accept a variable\, direction\['in', 'out', '>', '<'], labels, hops\, min_hops\, max_hops\, properties\
  • Can be added to the chain by typing .relationship, .rel, .r_, or for directed: .rel_out or .rel_in
  • To create a variable length relationship (e.g. 1..3), use min_hops and max_hops
  • To create variable length relationship with an open bound (e.g. ..3), use min_hops or max_hops
  • To create a fixed length relationship, use hops
  • Using both hops and one of min_hops and max_hops will raise an error

Property

Property objects simply allow for adding .property to the resulting Cypher query.

  • These can be added to the chain by calling .property('name') or .__name__ (double underscore before and after)
  • Python does not allow assignment for function calls so something like this is illegal n.property('name') == 'Mark' if you wanted to use the property method in this scenario, you would have to get back to the Pypher instance like this n.property('name')._ == 'Mark' or use the double underscore method n.property.__name__ == 'Mark'.
  • Property objects work just like any other link and you can add them anywhere, even if it doesn't produce property Cypher. p.RETURN.property('name') will create RETURN.name

Label

Label objects simply add a label to the preceding link.

  • Can be init with *args of labels n.label('Person', 'Male') would produce n:Person:Male
  • This does not bind its arguments
  • Labbels will be wrapped in back ticks to allow for spaces and other special characters

Partial

Partial objects allows for encapsulation of complex Pypher chains. These objects will allow for preset definitions to be added to the current Pypher instance.

  • The sub-class must call super in the __init__
  • The sub-class must define a build method that houses all of the business rules for the Partial
  • The partial can have any interface the developer sees fit
  • Any bound params will be passed up to the parent Pypher instance
  • Partial objects maintain the same interface as Pypher objects, they simply proxy all calls up to the Pypher instance that the Partial contains (this is useful for assignments or math, etc)

Here is an example of the built in Case Partial that provides a CASE $case [WHEN $when THEN $then,...] [ELSE $else] END addition:

```python class Case(Partial):

def __init__(self, case):
    super(Case, self).__init__()

    self._case = case
    self._whens = []
    self._else = None

def WHEN(self, when, then):
    self._whens.append((when, then))

    return self

def ELSE(self, else_case):
    self._else = else_case

    return self

def build(self):
    self.pypher.CASE(self._case)

    for w in self._whens:
        self.pypher.WHEN(w[0]).THEN(w[1])

    if self._else:
        self.pypher.ELSE(self._else)

    self.pypher.END

usage is simple

p = Pypher()

build the partial according to its interface

case = Case(.n.__eyes) case.WHEN('"blue"', 1) case.WHEN('"brown"', 2) case.ELSE(3)

add it to the Pypher instance

p.apply_partial(case)

str(p) # CASE n.eyes WHEN "blue" THEN 1 WHEN "brown" THEN 2 ELSE 3 END ```

As seen in this example, if you want your resulting Cypher to have actual quotes, you must nest quotes when passing in the arguments to the Statement objects

Maps

Cypher allows for Java-style maps to be returned in some complex queries, Pypher provides two classes to assist with map creation: Map and MapProjection

  • Both objects have a signature of *args and **kwargs
    • *args will be printed out in the resoling Cypher exactly how they are defined in Python
    • **kwargs will be printed out as key:value pairs where the values are bound params
    • MapProjection has a name argument that will printed out before the map

``python p = Pypher() p.RETURN.map('one', 'two', three='three') print(str(p)) # RETURN {one, two,three`: $three213bd_0} print(dict(p.bound_params)) # {'three213bd_0': 'three'}

p.reset() p.RETURN.map_projection('user', '.name', '.age') print(str(p)) # 'RETURN user {.name, .age}' ```

Code Examples

This section will simply cover how to write Pypher that will convert to both common and complex Cypher queries.

A Simple Match with WHERE

cypher MATCH (n:Person)-[:KNOWS]->(m:Person) WHERE n.name = 'Alice'

python p.MATCH.node('n', 'Person').rel_out(labels='KNOWS').node('m', 'PERSON').WHERE.n.__name__ == 'Alice'

A Simple Match with IN

cypher MATCH (n:Person)-[:KNOWS]->(m:Person) WHERE n.name IN ['Alice', 'Bob']

python names = ['Alice', 'Bob'] p.MATCH.node('n', 'Person').rel_out(labels='KNOWS').node('m', 'PERSON').WHERE.n.__name__.In(*names)

Create A Node

cypher CREATE (user:User {Name: 'Jim'})

python p.CREATE.node('user', 'User', Name='Jim')

cypher MERGE (user:User { Id: 456 }) ON CREATE user SET user.Name = 'Jim'

python p.MERGE.node('user', 'User', Id=456).ON.CREATE.user.SET(__.user.__Name__ == 'Jim')

Create a variable length relationship

cypher MATCH (martin { name: 'Charlie Sheen' })-[:ACTED_IN*1..3]-(movie:Movie) RETURN movie.title

python p.Match.node('martin', name='Charlie Sheen').rel(labels='ACTED_IN', min_hops=1, max_hops=3).node('movie', 'Movie') p.Return(__.movie.__title__)

Create a fixed length relationship

cypher MATCH (martin { name: 'Charlie Sheen' })-[:ACTED_IN*2]-(movie:Movie) RETURN movie.title

python p.Match.node('martin', name='Charlie Sheen').rel(labels='ACTED_IN', hops=2).node('movie', 'Movie') p.Return(__.movie.__title__)

Tester

Included is a very bare-bones CLI app that will allow you to test your Pypher scripts. After installing Pypher, you can run the script simply by calling python tester.py. Once loaded you are presented with a screen that will allow you to write Pypher code and it will generate the Cypher and bound params. This is a quick way to check if your Pypher is producing the desired Cypher for your project.

Example tester.py usage

Issues

Bump certifi from 2021.10.8 to 2022.12.7

opened on 2022-12-09 04:35:25 by dependabot[bot]

Bumps certifi from 2021.10.8 to 2022.12.7.

Commits


Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) - `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language - `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language - `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language - `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/emehrkay/Pypher/network/alerts).

Property attributes are enclosed in single underscores instead of dou…

opened on 2021-10-15 21:40:57 by CisterMoke

Changed the __getattr__ definition in Pypher to fix issue https://github.com/emehrkay/Pypher/issues/47. Properties are now recognized as attributes enclosed by single underscores instead of double underscored to avoid any interference with the Python subroutines

Pypher behaves strangely in Pycharm Debug mode

opened on 2021-07-28 17:23:42 by rock-it-with-asher

ran with python 3.7.0 on both windows and mac versions, here is an output example: tmp = Pypher().Match.node("yo", labels=["label"]) str(tmp) 'MATCH (yo:`label`) shape shape.`len` shape shape.`len`' str(tmp) 'MATCH (yo:`label`) shape shape.`len` shape shape.`len` shape shape.`len` shape shape.`len`' str(tmp) 'MATCH (yo:`label`) shape shape.`len` shape shape.`len` shape shape.`len` shape shape.`len` shape shape.`len` shape shape.`len`'

Each call to __str__ adds more of the garbage strings. it doesn't happen in a regular run mode

Dynamic Variable Names

opened on 2021-05-17 10:17:29 by hrishikeshrt

Thank you for this package. I wanted to know if the nodes can be specified by strings

For example, I want to write a query

MATCH (n) WHERE n.root = "Something"

Now, I can do this using p.Match.node('n').Where.n.property('root') == "Something" However, I have to do this for many variables for a longer pattern in MATCH, and the name n is generated dynamically, for example it could be n1, n2, n3 etc depending on which iteration of loop it is in. Is it possible to achieve that?

I tried giving Where.raw('n') and Where._('n') but both insert a STATEMENT word in the query.

(Apologies if this is a stupid question)

Mark Henderson
GitHub Repository

cypher-query complex-queries query-builder cypher python