├── .travis.yml ├── LICENSE.txt ├── README.md ├── dist └── PythonCypher-0.1.tar.gz ├── python_cypher ├── __init__.py ├── cypher_parser.py ├── cypher_tokenizer.py ├── parsetab.py └── python_cypher.py ├── requirements.txt ├── setup.py └── test ├── __init__.py └── test.py /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | # command to install dependencies 5 | install: 6 | - "pip install ." 7 | - "pip install -r requirements.txt" 8 | # command to run tests 9 | script: python test/test.py 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Replaced with `pycypher`. 2 | -------------------------------------------------------------------------------- /dist/PythonCypher-0.1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacernst/python_cypher/9b06accce9499db51cc772dfb354f240ae2aff15/dist/PythonCypher-0.1.tar.gz -------------------------------------------------------------------------------- /python_cypher/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacernst/python_cypher/9b06accce9499db51cc772dfb354f240ae2aff15/python_cypher/__init__.py -------------------------------------------------------------------------------- /python_cypher/cypher_parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from cypher_tokenizer import * 4 | from ply import yacc 5 | 6 | next_anonymous_variable = 0 7 | 8 | start = 'full_query' 9 | 10 | 11 | def constraint_function(function_string): 12 | """Translates a string in a WHERE clause into the appropriate 13 | function. The function is stored as an attribute in the 14 | ``Constraint`` object that's generated as the Cypher query 15 | is parsed. 16 | """ 17 | def _equals(arg1, arg2): 18 | return arg1 == arg2 19 | 20 | def _greater_than(arg1, arg2): 21 | return arg1 > arg2 22 | 23 | def _less_than(arg1, arg2): 24 | return arg1 < arg2 25 | 26 | def _greater_or_equal(arg1, arg2): 27 | return arg1 >= arg2 28 | 29 | def _less_or_equal(arg1, arg2): 30 | return arg1 <= arg2 31 | 32 | if function_string == '=': 33 | return _equals 34 | elif function_string == '>': 35 | return _greater_than 36 | elif function_string == '<': 37 | return _less_than 38 | elif function_string == '>=': 39 | return _greater_or_equal 40 | elif function_string == '<=': 41 | return _less_or_equal 42 | else: 43 | raise Exception('Unhandled case in constraint_function.') 44 | 45 | 46 | class ParsingException(Exception): 47 | """A generic Exception class for the parser.""" 48 | def __init__(self, msg): 49 | print msg 50 | 51 | 52 | class AtomicFact(object): 53 | """All facts will inherit this class. Not really used yet.""" 54 | pass 55 | 56 | 57 | class ClassIs(AtomicFact): 58 | """Represents a constraint that a vertex must be of a specific class.""" 59 | def __init__(self, designation, class_name): 60 | self.designation = designation 61 | self.class_name = class_name 62 | 63 | 64 | class EdgeCondition(AtomicFact): 65 | """Represents the constraint that an edge must have a specific 66 | label, or that it must be run in a specific direction.""" 67 | def __init__(self, edge_label=None, direction=None, designation=None): 68 | self.edge_label = edge_label 69 | self.designation = designation 70 | 71 | 72 | class EdgeExists(AtomicFact): 73 | """The constraint that an edge exists between two nodes, possibly 74 | having a specific label.""" 75 | def __init__(self, node_1, node_2, designation=None, edge_label=None): 76 | self.node_1 = node_1 77 | self.node_2 = node_2 78 | self.edge_label = edge_label 79 | self.designation = designation 80 | 81 | 82 | class Node(object): 83 | """A node specification -- a set of conditions and a designation.""" 84 | def __init__(self, node_class=None, designation=None, 85 | attribute_conditions=None, connecting_edges=None): 86 | self.node_class = node_class 87 | self.designation = designation 88 | self.attribute_conditions = attribute_conditions or {} 89 | self.connecting_edges = connecting_edges or [] 90 | 91 | 92 | class NodeHasDocument(object): 93 | """Condition saying that the node has the (entire) dictionary document.""" 94 | def __init__(self, designation=None, document=None): 95 | self.designation = designation 96 | self.document = document 97 | 98 | 99 | class MatchQuery(object): 100 | """A near top-level class representing any Cypher query of the form 101 | MATCH... [WHERE...]""" 102 | def __init__(self, literals=None, return_variables=None, 103 | where_clause=None): 104 | self.literals = literals 105 | self.return_variables = return_variables 106 | self.where_clause = where_clause 107 | 108 | 109 | class Literals(object): 110 | """Class representing a sequence of nodes (which we're calling 111 | literals).""" 112 | def __init__(self, literal_list=None): 113 | self.literal_list = literal_list or [] 114 | 115 | 116 | class ReturnVariables(object): 117 | """Class representing a sequence (possibly length one) of variables 118 | to be returned in a MATCH... RETURN query. This includes variables 119 | with keypaths for attributes as well.""" 120 | def __init__(self, variable): 121 | self.variable_list = [variable] 122 | 123 | 124 | class CreateClause(object): 125 | """Class representing a CREATE... RETURN query, including cases 126 | where the RETURN isn't present.""" 127 | def __init__(self, literals, is_head=False, return_variables=None): 128 | self.literals = literals 129 | self.return_variables = return_variables 130 | self.is_head = False 131 | 132 | 133 | class MatchWhereReturnQuery(object): 134 | def __init__(self, match_clause=None, 135 | where_clause=None, return_variables=None): 136 | self.match_clause = match_clause 137 | self.where_clause = where_clause 138 | self.return_variables = return_variables 139 | 140 | 141 | class Constraint(object): 142 | '''Class representing a constraint for use in a MATCH query. For 143 | example, WHERE x.foo = "bar"''' 144 | def __init__(self, keypath, value, function_string): 145 | self.keypath = keypath 146 | self.value = value 147 | self.function = constraint_function(function_string) 148 | 149 | 150 | class Or(object): 151 | '''A disjunction''' 152 | def __init__(self, left_disjunct, right_disjunct): 153 | self.left_disjunct = left_disjunct 154 | self.right_disjunct = right_disjunct 155 | 156 | 157 | class Not(object): 158 | '''Negation''' 159 | def __init__(self, argument): 160 | self.argument = argument 161 | 162 | 163 | class WhereClause(object): 164 | '''WHERE clause''' 165 | def __init__(self, constraint): 166 | self.constraint = constraint 167 | 168 | 169 | class MatchWhere(object): 170 | '''Match -- where''' 171 | def __init__(self, literals=None, where_clause=None, 172 | return_variables=None): 173 | self.literals = literals or [] 174 | self.return_variables = return_variables 175 | self.where_clause = where_clause 176 | 177 | 178 | class FullQuery(object): 179 | '''Full query is just a list, basically.''' 180 | def __init__(self, *args): 181 | self.clause_list = args 182 | 183 | 184 | def p_node_clause(p): 185 | '''node_clause : LPAREN KEY RPAREN 186 | | LPAREN COLON NAME RPAREN 187 | | LPAREN KEY COLON NAME RPAREN 188 | | LPAREN KEY COLON NAME condition_list RPAREN''' 189 | global next_anonymous_variable 190 | if len(p) == 4: 191 | p[0] = Node(designation=p[2]) 192 | elif len(p) == 5: 193 | # Just a class name 194 | p[0] = Node(node_class=p[3], 195 | designation='_v' + str(next_anonymous_variable)) 196 | next_anonymous_variable += 1 197 | elif len(p) == 6: 198 | # Node class name and variable 199 | p[0] = Node(node_class=p[4], designation=p[2]) 200 | elif len(p) == 7: 201 | p[0] = Node(node_class=p[4], designation=p[2], 202 | attribute_conditions=p[5]) 203 | 204 | 205 | def p_condition(p): 206 | '''condition_list : KEY COLON STRING 207 | | KEY COLON INTEGER 208 | | condition_list COMMA condition_list 209 | | LCURLEY condition_list RCURLEY 210 | | KEY COLON condition_list''' 211 | if len(p) == 4 and p[2] == ':' and isinstance(p[3], str): 212 | p[0] = {p[1]: p[3].replace('"', '')} 213 | elif len(p) == 4 and p[2] == ':' and isinstance(p[3], int): 214 | p[0] = {p[1]: p[3]} 215 | elif len(p) == 4 and p[2] == ':' and isinstance(p[3], dict): 216 | p[0] = {p[1]: p[3]} 217 | elif len(p) == 4 and p[2] == ',': 218 | p[0] = p[1] 219 | p[1].update(p[3]) 220 | elif len(p) == 4 and isinstance(p[2], dict): 221 | p[0] = p[2] 222 | 223 | 224 | def p_constraint(p): 225 | '''constraint : keypath EQUALS STRING 226 | | keypath EQUALS INTEGER 227 | | keypath EQUALS keypath 228 | | keypath NOT_EQUAL INTEGER 229 | | keypath GREATERTHAN INTEGER 230 | | keypath GREATERTHAN_OR_EQUAL INTEGER 231 | | keypath LESSTHAN INTEGER 232 | | keypath LESSTHAN_OR_EQUAL INTEGER 233 | | constraint OR constraint 234 | | constraint AND constraint 235 | | NOT constraint 236 | | LPAREN constraint RPAREN''' 237 | if p[2] == '=': 238 | p[0] = Constraint(p[1], p[3], '=') 239 | elif p[2] == '>': 240 | p[0] = Constraint(p[1], p[3], '>') 241 | elif p[2] == '!=': 242 | p[0] = Not(Constraint(p[1], p[3], '=')) 243 | elif p[2] == '<': 244 | p[0] = Constraint(p[1], p[3], '<') 245 | elif p[2] == '<': 246 | p[0] = Constraint(p[1], p[3], '<=') 247 | elif p[2] == '<=': 248 | p[0] = Or(Constraint(p[1], p[3], '>'), Constraint(p[1], p[3], '=')) 249 | elif p[2] == 'OR': 250 | p[0] = Or(p[1], p[3]) 251 | elif p[2] == 'AND': 252 | p[0] = Not(Or(Not(p[1]), Not(p[3]))) 253 | elif p[1] == 'NOT': 254 | p[0] = Not(p[2]) 255 | elif p[1] == '(': 256 | p[0] = p[2] 257 | else: 258 | raise Exception("Unhandled case in p_constraint.") 259 | 260 | 261 | def p_where_clause(p): 262 | '''where_clause : WHERE constraint''' 263 | if isinstance(p[2], (Constraint, Or, Not,)): 264 | p[0] = WhereClause(p[2]) 265 | else: 266 | raise Exception("Unhandled case in p_where_clause.") 267 | 268 | 269 | def p_keypath(p): 270 | '''keypath : KEY DOT KEY 271 | | keypath DOT KEY''' 272 | if len(p) == 4 and isinstance(p[1], str): 273 | p[1] = [p[1]] 274 | p[1].append(p[3]) 275 | elif len(p) == 4 and isinstance(p[1], list): 276 | p[1].append(p[3]) 277 | else: 278 | raise Exception('unhandled case in keypath.') 279 | p[0] = p[1] 280 | 281 | 282 | def p_edge_condition(p): 283 | '''edge_condition : LBRACKET COLON NAME RBRACKET 284 | | LBRACKET KEY COLON NAME RBRACKET''' 285 | if p[2] == t_COLON: 286 | p[0] = EdgeCondition(edge_label=p[3]) 287 | elif p[3] == t_COLON and len(p) == 6: 288 | p[0] = EdgeCondition(edge_label=p[4], designation=p[2]) 289 | pass 290 | else: 291 | raise Exception("Unhandled case in p_edge_condition") 292 | 293 | 294 | def p_labeled_edge(p): 295 | '''labeled_edge : DASH edge_condition DASH GREATERTHAN 296 | | LESSTHAN DASH edge_condition DASH''' 297 | if p[1] == t_DASH: 298 | p[0] = p[2] 299 | p[0].direction = 'left_right' 300 | elif p[1] == t_LESSTHAN: 301 | p[0] = p[3] 302 | p[0].direction = 'right_left' 303 | else: 304 | raise Exception("Unhandled case in p_labeled_edge.") 305 | 306 | 307 | def p_literals(p): 308 | '''literals : node_clause 309 | | literals COMMA literals 310 | | literals RIGHT_ARROW literals 311 | | literals LEFT_ARROW literals 312 | | literals labeled_edge literals''' 313 | if len(p) == 2: 314 | p[0] = Literals(literal_list=[p[1]]) 315 | elif len(p) == 4 and p[2] == t_COMMA: 316 | p[0] = Literals(p[1].literal_list + p[3].literal_list) 317 | 318 | elif len(p) == 4 and p[2] == t_RIGHT_ARROW: 319 | p[0] = p[1] 320 | edge_fact = EdgeExists(p[1].literal_list[-1].designation, 321 | p[3].literal_list[0].designation) 322 | p[0].literal_list[-1].connecting_edges.append(edge_fact) 323 | p[0].literal_list += p[3].literal_list 324 | elif len(p) == 4 and p[2] == t_LEFT_ARROW: 325 | p[0] = p[1] 326 | edge_fact = EdgeExists(p[3].literal_list[0].designation, 327 | p[1].literal_list[-1].designation) 328 | p[0].literal_list[-1].connecting_edges.append(edge_fact) 329 | p[0].literal_list += p[3].literal_list 330 | elif isinstance(p[2], EdgeCondition) and p[2].direction == 'left_right': 331 | p[0] = p[1] 332 | edge_fact = EdgeExists(p[1].literal_list[-1].designation, 333 | p[3].literal_list[0].designation, 334 | edge_label=p[2].edge_label, 335 | designation=p[2].designation) 336 | p[0].literal_list[-1].connecting_edges.append(edge_fact) 337 | p[0].literal_list += p[3].literal_list 338 | elif isinstance(p[2], EdgeCondition) and p[2].direction == 'right_left': 339 | p[0] = p[1] 340 | edge_fact = EdgeExists(p[3].literal_list[0].designation, 341 | p[1].literal_list[-1].designation, 342 | edge_label=p[2].edge_label, 343 | designation=p[2].designation) 344 | p[0].literal_list[-1].connecting_edges.append(edge_fact) 345 | p[0].literal_list += p[3].literal_list 346 | else: 347 | raise Exception('unhandled case in p_literals') 348 | 349 | 350 | def p_match_where(p): 351 | '''match_where : MATCH literals 352 | | MATCH literals where_clause''' 353 | if len(p) == 3: 354 | p[0] = MatchWhere(literals=p[2]) 355 | elif len(p) == 4: 356 | p[0] = MatchWhere(literals=p[2], where_clause=p[3]) 357 | else: 358 | raise Exception("Unhandled case in p_match_where.") 359 | 360 | 361 | def p_create(p): 362 | '''create_clause : CREATE literals''' 363 | p[0] = CreateClause(p[2]) 364 | 365 | 366 | def p_full_query(p): 367 | '''full_query : match_where return_variables 368 | | create_clause 369 | | create_clause return_variables''' 370 | p[0] = FullQuery(*p[1:]) 371 | if isinstance(p[1], CreateClause): 372 | p[1].is_head = True 373 | 374 | 375 | def p_return_variables(p): 376 | '''return_variables : RETURN KEY 377 | | RETURN keypath 378 | | return_variables COMMA KEY 379 | | return_variables COMMA keypath''' 380 | if len(p) == 3 and isinstance(p[2], (str, list)): 381 | p[0] = ReturnVariables(p[2]) 382 | elif len(p) == 4: 383 | p[1].variable_list.append(p[3]) 384 | p[0] = p[1] 385 | 386 | 387 | def p_error(p): 388 | import pdb; pdb.set_trace() 389 | raise ParsingException("Generic error while parsing.") 390 | 391 | 392 | cypher_parser = yacc.yacc() 393 | -------------------------------------------------------------------------------- /python_cypher/cypher_tokenizer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import ply.lex as lex 4 | # Test 5 | 6 | tokens = ( 7 | 'LBRACKET', 8 | 'RBRACKET', 9 | 'DASH', 10 | 'GREATERTHAN', 11 | 'LESSTHAN', 12 | 'LESSTHAN_OR_EQUAL', 13 | 'GREATERTHAN_OR_EQUAL', 14 | 'EQUALS', 15 | 'NOT_EQUAL', 16 | 'LPAREN', 17 | 'RPAREN', 18 | 'COLON', 19 | 'RIGHT_ARROW', 20 | 'LEFT_ARROW', 21 | 'MATCH', 22 | 'WHERE', 23 | 'CREATE', 24 | 'RETURN', 25 | 'DOT', 26 | 'NAME', 27 | 'WHITESPACE', 28 | 'LCURLEY', 29 | 'RCURLEY', 30 | 'AND', 31 | 'OR', 32 | 'NOT', 33 | 'COMMA', 34 | 'QUOTE', 35 | 'INTEGER', 36 | 'STRING', 37 | 'KEY',) 38 | 39 | 40 | t_LBRACKET = r'\[' 41 | t_RBRACKET = r'\]' 42 | t_DASH = r'-' 43 | t_GREATERTHAN = r'>' 44 | t_GREATERTHAN_OR_EQUAL = r'>=' 45 | t_LESSTHAN = r'<' 46 | t_LESSTHAN_OR_EQUAL = r'<=' 47 | t_EQUALS = r'=' 48 | t_NOT_EQUAL = r'!=' 49 | t_LPAREN = r'\(' 50 | t_RPAREN = r'\)' 51 | t_COLON = r':' 52 | t_WHITESPACE = r'[ ]+' 53 | t_RIGHT_ARROW = r'-->' 54 | t_LEFT_ARROW = r'<--' 55 | t_QUOTE = r'"' 56 | t_LCURLEY = r'{' 57 | t_RCURLEY = r'}' 58 | t_COMMA = r',' 59 | 60 | t_ignore = r' ' 61 | 62 | 63 | def t_error(t): 64 | print 'tokenizer error' 65 | 66 | 67 | def t_MATCH(t): 68 | r'MATCH' 69 | return t 70 | 71 | 72 | def t_AND(t): 73 | r'AND' 74 | return t 75 | 76 | 77 | def t_OR(t): 78 | r'OR' 79 | return t 80 | 81 | 82 | def t_NOT(t): 83 | r'NOT' 84 | return t 85 | 86 | 87 | def t_WHERE(t): 88 | r'WHERE' 89 | return t 90 | 91 | 92 | def t_CREATE(t): 93 | r'CREATE' 94 | return t 95 | 96 | 97 | def t_RETURN(t): 98 | r'RETURN' 99 | return t 100 | 101 | 102 | def t_DOT(t): 103 | r'\.' 104 | return t 105 | 106 | 107 | def t_NAME(t): 108 | r'[A-Z]+[a-z0-9]*' 109 | return t 110 | 111 | 112 | def t_KEY(t): 113 | r'[A-Za-z]+[0-9]*' 114 | return t 115 | 116 | 117 | def t_INTEGER(t): 118 | r'[0-9]+' 119 | t.value = int(t.value) 120 | return t 121 | 122 | 123 | def t_FLOAT(t): 124 | r'[+-]?[0-9]*\.[0-9]+' 125 | return float(t) 126 | 127 | 128 | def t_STRING(t): 129 | r'"[A-Za-z0-9]+"' 130 | t.value = t.value.replace('"', '') 131 | return t 132 | 133 | cypher_tokenizer = lex.lex() 134 | -------------------------------------------------------------------------------- /python_cypher/parsetab.py: -------------------------------------------------------------------------------- 1 | 2 | # parsetab.py 3 | # This file is automatically generated. Do not edit. 4 | _tabversion = '3.8' 5 | 6 | _lr_method = 'LALR' 7 | 8 | _lr_signature = '5E0C4B0BBE2A68D7DACF60D089032C06' 9 | 10 | _lr_action_items = {'RETURN':([1,4,8,10,12,24,30,34,35,36,38,40,51,56,63,66,67,68,69,72,],[6,6,-28,-21,-26,-27,-23,-22,-24,-25,-1,-14,-2,-12,-3,-11,-10,-13,-9,-4,]),'LBRACKET':([17,33,],[31,31,]),'LESSTHAN':([8,10,12,30,34,35,36,38,51,63,72,],[18,-21,18,18,18,18,18,-1,-2,-3,-4,]),'LEFT_ARROW':([8,10,12,30,34,35,36,38,51,63,72,],[20,-21,20,20,20,20,20,-1,-2,-3,-4,]),'DOT':([13,14,28,29,41,44,45,46,],[26,27,26,27,26,27,-15,-16,]),'RPAREN':([23,37,52,55,56,62,66,67,68,69,77,78,79,80,],[38,51,63,68,-12,72,-11,-10,-13,-9,-6,-8,-5,-7,]),'RCURLEY':([75,77,78,79,80,],[80,-6,-8,-5,-7,]),'CREATE':([0,],[2,]),'COMMA':([7,8,10,11,12,13,14,28,29,30,34,35,36,38,45,46,51,62,63,72,75,77,78,79,80,],[15,19,-21,15,19,-32,-33,-34,-35,19,19,19,19,-1,-15,-16,-2,73,-3,-4,73,73,73,-5,-7,]),'RIGHT_ARROW':([8,10,12,30,34,35,36,38,51,63,72,],[16,-21,16,16,16,16,16,-1,-2,-3,-4,]),'COLON':([9,23,31,48,64,],[22,39,47,59,74,]),'$end':([3,4,7,8,10,11,13,14,28,29,30,34,35,36,38,45,46,51,63,72,],[0,-30,-29,-28,-21,-31,-32,-33,-34,-35,-23,-22,-24,-25,-1,-15,-16,-2,-3,-4,]),'STRING':([57,74,],[69,79,]),'EQUALS':([44,45,46,],[57,-15,-16,]),'DASH':([8,10,12,18,30,32,34,35,36,38,50,51,63,70,72,76,],[17,-21,17,33,17,49,17,17,17,-1,61,-2,-3,-17,-4,-18,]),'GREATERTHAN':([49,],[60,]),'LPAREN':([2,5,16,19,20,21,25,42,43,53,54,60,61,],[9,9,9,9,9,9,42,42,42,42,42,-19,-20,]),'WHERE':([10,12,30,34,35,36,38,51,63,72,],[-21,25,-23,-22,-24,-25,-1,-2,-3,-4,]),'MATCH':([0,],[5,]),'AND':([40,55,56,66,67,68,69,],[53,53,53,53,53,-13,-9,]),'NAME':([22,39,47,59,],[37,52,58,71,]),'KEY':([6,9,15,25,26,27,31,42,43,52,53,54,65,73,74,],[13,23,28,41,45,46,48,41,41,64,41,41,64,64,64,]),'NOT':([25,42,43,53,54,],[43,43,43,43,43,]),'RBRACKET':([58,71,],[70,76,]),'LCURLEY':([52,65,73,74,],[65,65,65,65,]),'OR':([40,55,56,66,67,68,69,],[54,54,54,54,54,-13,-9,]),} 11 | 12 | _lr_action = {} 13 | for _k, _v in _lr_action_items.items(): 14 | for _x,_y in zip(_v[0],_v[1]): 15 | if not _x in _lr_action: _lr_action[_x] = {} 16 | _lr_action[_x][_k] = _y 17 | del _lr_action_items 18 | 19 | _lr_goto_items = {'match_where':([0,],[1,]),'constraint':([25,42,43,53,54,],[40,55,56,66,67,]),'literals':([2,5,16,19,20,21,],[8,12,30,34,35,36,]),'where_clause':([12,],[24,]),'edge_condition':([17,33,],[32,50,]),'full_query':([0,],[3,]),'return_variables':([1,4,],[7,11,]),'condition_list':([52,65,73,74,],[62,75,77,78,]),'node_clause':([2,5,16,19,20,21,],[10,10,10,10,10,10,]),'create_clause':([0,],[4,]),'labeled_edge':([8,12,30,34,35,36,],[21,21,21,21,21,21,]),'keypath':([6,15,25,42,43,53,54,],[14,29,44,44,44,44,44,]),} 20 | 21 | _lr_goto = {} 22 | for _k, _v in _lr_goto_items.items(): 23 | for _x, _y in zip(_v[0], _v[1]): 24 | if not _x in _lr_goto: _lr_goto[_x] = {} 25 | _lr_goto[_x][_k] = _y 26 | del _lr_goto_items 27 | _lr_productions = [ 28 | ("S' -> full_query","S'",1,None,None,None), 29 | ('node_clause -> LPAREN KEY RPAREN','node_clause',3,'p_node_clause','cypher_parser.py',181), 30 | ('node_clause -> LPAREN COLON NAME RPAREN','node_clause',4,'p_node_clause','cypher_parser.py',182), 31 | ('node_clause -> LPAREN KEY COLON NAME RPAREN','node_clause',5,'p_node_clause','cypher_parser.py',183), 32 | ('node_clause -> LPAREN KEY COLON NAME condition_list RPAREN','node_clause',6,'p_node_clause','cypher_parser.py',184), 33 | ('condition_list -> KEY COLON STRING','condition_list',3,'p_condition','cypher_parser.py',202), 34 | ('condition_list -> condition_list COMMA condition_list','condition_list',3,'p_condition','cypher_parser.py',203), 35 | ('condition_list -> LCURLEY condition_list RCURLEY','condition_list',3,'p_condition','cypher_parser.py',204), 36 | ('condition_list -> KEY COLON condition_list','condition_list',3,'p_condition','cypher_parser.py',205), 37 | ('constraint -> keypath EQUALS STRING','constraint',3,'p_constraint','cypher_parser.py',218), 38 | ('constraint -> constraint OR constraint','constraint',3,'p_constraint','cypher_parser.py',219), 39 | ('constraint -> constraint AND constraint','constraint',3,'p_constraint','cypher_parser.py',220), 40 | ('constraint -> NOT constraint','constraint',2,'p_constraint','cypher_parser.py',221), 41 | ('constraint -> LPAREN constraint RPAREN','constraint',3,'p_constraint','cypher_parser.py',222), 42 | ('where_clause -> WHERE constraint','where_clause',2,'p_where_clause','cypher_parser.py',240), 43 | ('keypath -> KEY DOT KEY','keypath',3,'p_keypath','cypher_parser.py',248), 44 | ('keypath -> keypath DOT KEY','keypath',3,'p_keypath','cypher_parser.py',249), 45 | ('edge_condition -> LBRACKET COLON NAME RBRACKET','edge_condition',4,'p_edge_condition','cypher_parser.py',261), 46 | ('edge_condition -> LBRACKET KEY COLON NAME RBRACKET','edge_condition',5,'p_edge_condition','cypher_parser.py',262), 47 | ('labeled_edge -> DASH edge_condition DASH GREATERTHAN','labeled_edge',4,'p_labeled_edge','cypher_parser.py',273), 48 | ('labeled_edge -> LESSTHAN DASH edge_condition DASH','labeled_edge',4,'p_labeled_edge','cypher_parser.py',274), 49 | ('literals -> node_clause','literals',1,'p_literals','cypher_parser.py',286), 50 | ('literals -> literals COMMA literals','literals',3,'p_literals','cypher_parser.py',287), 51 | ('literals -> literals RIGHT_ARROW literals','literals',3,'p_literals','cypher_parser.py',288), 52 | ('literals -> literals LEFT_ARROW literals','literals',3,'p_literals','cypher_parser.py',289), 53 | ('literals -> literals labeled_edge literals','literals',3,'p_literals','cypher_parser.py',290), 54 | ('match_where -> MATCH literals','match_where',2,'p_match_where','cypher_parser.py',329), 55 | ('match_where -> MATCH literals where_clause','match_where',3,'p_match_where','cypher_parser.py',330), 56 | ('create_clause -> CREATE literals','create_clause',2,'p_create','cypher_parser.py',340), 57 | ('full_query -> match_where return_variables','full_query',2,'p_full_query','cypher_parser.py',345), 58 | ('full_query -> create_clause','full_query',1,'p_full_query','cypher_parser.py',346), 59 | ('full_query -> create_clause return_variables','full_query',2,'p_full_query','cypher_parser.py',347), 60 | ('return_variables -> RETURN KEY','return_variables',2,'p_return_variables','cypher_parser.py',354), 61 | ('return_variables -> RETURN keypath','return_variables',2,'p_return_variables','cypher_parser.py',355), 62 | ('return_variables -> return_variables COMMA KEY','return_variables',3,'p_return_variables','cypher_parser.py',356), 63 | ('return_variables -> return_variables COMMA keypath','return_variables',3,'p_return_variables','cypher_parser.py',357), 64 | ] 65 | -------------------------------------------------------------------------------- /python_cypher/python_cypher.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This script contains the ``CypherParserBaseClass``, which provides the basic 4 | functionality to parse Cypher queries and run them against graphs. 5 | """ 6 | 7 | import itertools 8 | import networkx as nx 9 | import copy 10 | import hashlib 11 | import random 12 | import time 13 | from cypher_tokenizer import * 14 | from cypher_parser import * 15 | 16 | PRINT_TOKENS = True 17 | PRINT_MATCHING_ASSIGNMENTS = False 18 | 19 | 20 | def designations_from_atomic_facts(atomic_facts): 21 | """Returns a list of all the designations mentioned in the query.""" 22 | designations = [] 23 | for atomic_fact in atomic_facts: 24 | designations.append(getattr(atomic_fact, 'designation', None)) 25 | designations.append(getattr(atomic_fact, 'node_1', None)) 26 | designations.append(getattr(atomic_fact, 'node_2', None)) 27 | designations = [designation for designation in designations if 28 | designation is not None] 29 | designations = list(set(designations)) 30 | designations.sort() 31 | return designations 32 | 33 | 34 | class CypherParserBaseClass(object): 35 | """The base class that specific parsers will inherit from. Certain methods 36 | must be defined in the child class. See the docs.""" 37 | def __init__(self): 38 | self.tokenizer = cypher_tokenizer 39 | self.parser = cypher_parser 40 | 41 | def yield_var_to_element(self, parsed_query, graph_object): 42 | all_designations = set() 43 | # Track down atomic_facts from outer scope. 44 | atomic_facts = extract_atomic_facts(parsed_query) 45 | for fact in atomic_facts: 46 | if hasattr(fact, 'designation') and fact.designation is not None: 47 | all_designations.add(fact.designation) 48 | elif hasattr(fact, 'literals'): 49 | for literal in fact.literals.literal_list: 50 | if (hasattr(literal, 'designation') and 51 | literal.designation is not None): 52 | all_designations.add(literal.designation) 53 | all_designations = sorted(list(all_designations)) 54 | 55 | domain = self._get_domain(graph_object) 56 | for domain_assignment in itertools.product( 57 | *[domain] * len(all_designations)): 58 | var_to_element = {all_designations[index]: element for index, 59 | element in enumerate(domain_assignment)} 60 | yield var_to_element 61 | 62 | def parse(self, query): 63 | """Calls yacc to parse the query string into an AST.""" 64 | self.tokenizer.input(query) 65 | tok = self.tokenizer.token() 66 | while tok: 67 | if PRINT_TOKENS: 68 | print tok 69 | tok = self.tokenizer.token() 70 | return self.parser.parse(query) 71 | 72 | def eval_constraint(self, constraint, assignment, graph_object): 73 | """This is the basis case for the recursive check 74 | on WHERE clauses.""" 75 | # This might be broken because _get_node might expect the actual 76 | # node to be passed to it rather than the name of the node 77 | value = self._attribute_value_from_node_keypath( 78 | self._get_node( 79 | graph_object, 80 | assignment[constraint.keypath[0]]), 81 | constraint.keypath[1:]) 82 | return constraint.function(value, constraint.value) 83 | 84 | def eval_boolean(self, clause, assignment, graph_object): 85 | """Recursive function to evaluate WHERE clauses. ``Or`` 86 | and ``Not`` classes inherit from ``Constraint``.""" 87 | if isinstance(clause, Or): 88 | return (self.eval_boolean(clause.left_disjunct, 89 | assignment, graph_object) or 90 | self.eval_boolean(clause.right_disjunct, 91 | assignment, graph_object)) 92 | elif isinstance(clause, Not): 93 | return not self.eval_boolean(clause.argument, 94 | assignment, graph_object) 95 | elif isinstance(clause, Constraint): 96 | return self.eval_constraint(clause, assignment, graph_object) 97 | 98 | def query(self, graph_object, query_string): 99 | """Top-level function that's called by the parser when a query has 100 | been transformed to its AST. This function routes the parsed 101 | query to a small number of high-level functions for handling 102 | specific types of queries (e.g. MATCH, CREATE, ...)""" 103 | parsed_query = self.parse(query_string) 104 | 105 | def _test_match_where(clause, assignment, graph_object): 106 | sentinal = True # Satisfied unless we fail 107 | for literal in clause.literals.literal_list: 108 | designation = literal.designation 109 | desired_class = literal.node_class 110 | desired_document = literal.attribute_conditions 111 | node = graph_object.node[assignment[designation]] 112 | # Check the class of the node 113 | if (desired_class is not None and 114 | node.get('class', None) != desired_class): 115 | sentinal = False 116 | node_document = copy.deepcopy(node) 117 | # The `node_document` is a temporary copy for comparisons 118 | del node_document['class'] 119 | if sentinal and (len(desired_document) > 0 and 120 | node_document != desired_document): 121 | sentinal = False 122 | # Check all connecting edges from this node (literal) 123 | if not not literal.connecting_edges: # not not -> empty? 124 | edge_sentinal = True 125 | for edge in literal.connecting_edges: 126 | edge_sentinal = False 127 | source_designation = edge.node_1 128 | target_designation = edge.node_2 129 | edge_label = edge.edge_label 130 | source_node = assignment[source_designation] 131 | target_node = assignment[target_designation] 132 | for one_edge_id in self._edges_connecting_nodes( 133 | graph_object, source_node, target_node): 134 | one_edge = self._get_edge_from_id( 135 | graph_object, one_edge_id) 136 | if (edge_label is None or 137 | self._edge_class(one_edge) == edge_label): 138 | edge_sentinal = True 139 | if getattr(edge, 'designation', None) is not None: 140 | assignment[edge.designation] = one_edge_id 141 | sentinal = sentinal and edge_sentinal 142 | # Check the WHERE claused 143 | # Note this isn't in the previous loop, because the WHERE clause 144 | # isn't restricted to a specific node 145 | if sentinal and clause.where_clause is not None: 146 | constraint = clause.where_clause.constraint 147 | constraint_eval = self.eval_boolean( 148 | constraint, assignment, graph_object) 149 | sentinal = sentinal and constraint_eval 150 | return sentinal 151 | 152 | # Two cases: Starts with CREATE; doesn't start with CREATE. 153 | # First doesn't require enumeration of the domain; second does. 154 | 155 | if (isinstance(parsed_query.clause_list[0], CreateClause) and 156 | parsed_query.clause_list[0].is_head): 157 | # Run like before the refactor 158 | self.head_create_query(graph_object, parsed_query) 159 | yield 'foo' # Need to return the created nodes, possibly 160 | else: 161 | # Importantly, we step through each assignment, and then for 162 | # each assignment, we step through each "clause" (need better name) 163 | for assignment in self.yield_var_to_element( 164 | parsed_query, graph_object): 165 | satisfied = True 166 | for clause in parsed_query.clause_list: 167 | if not satisfied: 168 | break 169 | if isinstance(clause, MatchWhere): # MATCH... WHERE... 170 | satisfied = satisfied and _test_match_where( 171 | clause, assignment, graph_object) 172 | elif isinstance(clause, ReturnVariables): 173 | # We've added any edges to the assignment dictionary 174 | # Now we need to step through the keypath lists that 175 | # are stored in the ReturnVariables object under the 176 | # attribute "variable_list" 177 | return_values = [] 178 | for variable_path in clause.variable_list: 179 | # I expect this will choke on edges if we ask for 180 | # their properties to be returned 181 | if not isinstance(variable_path, list): 182 | variable_path = [variable_path] 183 | if self._is_edge( 184 | graph_object, assignment[variable_path[0]]): 185 | _get_node_or_edge = self._get_edge 186 | elif self._is_node( 187 | graph_object, assignment[variable_path[0]]): 188 | _get_node_or_edge = self._get_node 189 | else: 190 | raise Exception("Neither a node nor an edge.") 191 | node_or_edge = _get_node_or_edge( 192 | graph_object, assignment[variable_path[0]]) 193 | return_value = ( 194 | self._attribute_value_from_node_keypath( 195 | node_or_edge, variable_path[1:])) 196 | return_values.append(return_value) 197 | yield return_values 198 | else: 199 | import pdb; pdb.set_trace() 200 | raise Exception("Unhandled case in query function.") 201 | 202 | def head_create_query(self, graph_object, parsed_query): 203 | """For executing queries of the form CREATE... RETURN.""" 204 | atomic_facts = extract_atomic_facts(parsed_query) 205 | designation_to_node = {} 206 | designation_to_edge = {} 207 | for create_clause in parsed_query.clause_list: 208 | if not isinstance(create_clause, CreateClause): 209 | continue 210 | for literal in create_clause.literals.literal_list: 211 | designation_to_node[literal.designation] = self._create_node( 212 | graph_object, literal.node_class, 213 | **literal.attribute_conditions) 214 | for edge_fact in [ 215 | fact for fact in atomic_facts if 216 | isinstance(fact, EdgeExists)]: 217 | source_node = designation_to_node[edge_fact.node_1] 218 | target_node = designation_to_node[edge_fact.node_2] 219 | edge_label = edge_fact.edge_label 220 | new_edge_id = self._create_edge(graph_object, source_node, 221 | target_node, edge_label=edge_label) 222 | # Need an attribute for an edge designation 223 | designation_to_edge['placeholder'] = new_edge_id 224 | 225 | def _get_domain(self, *args, **kwargs): 226 | raise NotImplementedError( 227 | "Method _get_domain needs to be defined in child class.") 228 | 229 | def _get_node(self, *args, **kwargs): 230 | raise NotImplementedError( 231 | "Method _get_domain needs to be defined in child class.") 232 | 233 | def _node_attribute_value(self, *args, **kwargs): 234 | raise NotImplementedError( 235 | "Method _get_domain needs to be defined in child class.") 236 | 237 | def _edge_exists(self, *args, **kwargs): 238 | raise NotImplementedError( 239 | "Method _edge_exists needs to be defined in child class.") 240 | 241 | def _edges_connecting_nodes(self, *args, **kwargs): 242 | raise NotImplementedError( 243 | "Method _edges_connecting_nodes needs to be defined " 244 | "in child class.") 245 | 246 | def _node_class(self, *args, **kwargs): 247 | raise NotImplementedError( 248 | "Method _node_class needs to be defined in child class.") 249 | 250 | def _edge_class(self, *args, **kwargs): 251 | raise NotImplementedError( 252 | "Method _edge_class needs to be defined in child class.") 253 | 254 | def _create_node(self, *args, **kwargs): 255 | raise NotImplementedError( 256 | "Method _create_node needs to be defined in child class.") 257 | 258 | def _create_edge(self, *args, **kwargs): 259 | raise NotImplementedError( 260 | "Method _create_edge needs to be defined in child class.") 261 | 262 | def _get_edge_from_id(self, *args, **kwargs): 263 | raise NotImplementedError( 264 | "Method _get_edge_from_id needs to be defined in child class.") 265 | 266 | def _get_edge(self, *args, **kwargs): 267 | raise NotImplementedError( 268 | "Method _get_edge needs to be defined in child class.") 269 | 270 | def _attribute_value_from_node_keypath(self, *args, **kwargs): 271 | raise NotImplementedError( 272 | "Method _attribute_value_from_node_keypath needs to be defined.") 273 | 274 | def _is_edge(self, *args, **kwargs): 275 | raise NotImplementedError( 276 | "Method _is_edge needs to be defined.") 277 | 278 | def _is_node(self, *args, **kwargs): 279 | raise NotImplementedError( 280 | "Method _is_node needs to be defined.") 281 | 282 | 283 | class CypherToNetworkx(CypherParserBaseClass): 284 | """Child class inheriting from ``CypherParserBaseClass`` to hook up 285 | Cypher functionality to NetworkX. 286 | """ 287 | def _get_domain(self, obj): 288 | return obj.nodes() 289 | 290 | def _is_node(self, graph_object, node_name): 291 | return node_name in graph_object.node 292 | 293 | def _is_edge(self, graph_object, edge_name): 294 | for source_node_id, connections_dict in graph_object.edge.iteritems(): 295 | for _, edges_dict in connections_dict.iteritems(): 296 | for _, edge_dict in edges_dict.iteritems(): 297 | if edge_dict.get('_id', None) == edge_name: 298 | return True 299 | return False 300 | 301 | def _get_node(self, graph_object, node_name): 302 | return graph_object.node[node_name] 303 | 304 | def _get_edge(self, graph_object, edge_name): 305 | for source_node_id, connections_dict in graph_object.edge.iteritems(): 306 | for _, edges_dict in connections_dict.iteritems(): 307 | for _, edge_dict in edges_dict.iteritems(): 308 | if edge_dict.get('_id', None) == edge_name: 309 | return edge_dict 310 | 311 | def _node_attribute_value(self, node, attribute_list): 312 | out = copy.deepcopy(node) 313 | for attribute in attribute_list: 314 | try: 315 | out = out.get(attribute) 316 | except: 317 | raise Exception( 318 | "Asked for non-existent attribute {} in node {}.".format( 319 | attribute, node)) 320 | return out 321 | 322 | def _attribute_value_from_node_keypath(self, node, keypath): 323 | # We will try passing everything through this function 324 | if not isinstance(keypath, list) or len(keypath) == 0: 325 | return node 326 | value = node 327 | for key in keypath: 328 | try: 329 | value = value[key] 330 | except (KeyError, TypeError,): 331 | return None 332 | return value 333 | 334 | def _edge_exists(self, graph_obj, source, target, 335 | edge_class=None, directed=True): 336 | raise NotImplementedError("Haven't finished _edge_exists.") 337 | 338 | def _get_edge_from_id(self, graph_object, edge_id): 339 | for source, target_dict in graph_object.edge.iteritems(): 340 | for target, edge_dict in target_dict.iteritems(): 341 | for index, one_edge in edge_dict.iteritems(): 342 | if one_edge['_id'] == edge_id: 343 | return one_edge 344 | 345 | def _edges_connecting_nodes(self, graph_object, source, target): 346 | try: 347 | for index, data in graph_object.edge[source].get( 348 | target, {}).iteritems(): 349 | yield data['_id'] 350 | except: 351 | raise Exception("Error getting edges connecting nodes.") 352 | 353 | def _node_class(self, node, class_key='class'): 354 | return node.get(class_key, None) 355 | 356 | def _edge_class(self, edge, class_key='edge_label'): 357 | try: 358 | out = edge.get(class_key, None) 359 | except AttributeError: 360 | out = None 361 | return out 362 | 363 | def _create_node(self, graph_object, node_class, **attribute_conditions): 364 | """Create a node and return it so it can be referred to later.""" 365 | new_id = unique_id() 366 | attribute_conditions['class'] = node_class 367 | graph_object.add_node(new_id, **attribute_conditions) 368 | return new_id 369 | 370 | def _create_edge(self, graph_object, source_node, 371 | target_node, edge_label=None): 372 | new_edge_id = unique_id() 373 | graph_object.add_edge( 374 | source_node, target_node, 375 | **{'edge_label': edge_label, '_id': new_edge_id}) 376 | return new_edge_id 377 | 378 | 379 | def random_hash(): 380 | """Return a random hash for naming new nods and edges.""" 381 | hash_value = hashlib.md5(str(random.random() + time.time())).hexdigest() 382 | return hash_value 383 | 384 | 385 | def unique_id(): 386 | """Call ``random_hash`` and prepend ``_id_`` to it.""" 387 | return '_id_' + random_hash() 388 | 389 | 390 | def extract_atomic_facts(query): 391 | my_parser = CypherToNetworkx() 392 | if isinstance(query, str): 393 | query = my_parser.parse(query) 394 | 395 | def _recurse(subquery): 396 | if subquery is None: 397 | return 398 | elif isinstance(subquery, list): 399 | for item in subquery: 400 | _recurse(item) 401 | elif isinstance(subquery, ReturnVariables): 402 | pass # Ignore RETURN clause until after execution 403 | elif isinstance(subquery, FullQuery): 404 | for clause in subquery.clause_list: 405 | _recurse(clause) 406 | elif isinstance(subquery, MatchWhereReturnQuery): 407 | _recurse(subquery.match_clause) 408 | _recurse(subquery.where_clause) 409 | elif isinstance(subquery, MatchQuery): 410 | _recurse(subquery.literals) 411 | _recurse(subquery.where_clause) 412 | elif isinstance(subquery, CreateClause): 413 | _recurse(subquery.literals) 414 | elif isinstance(subquery, Literals): 415 | for literal in subquery.literal_list: 416 | _recurse(literal) 417 | 418 | # Add support for edges here in the recursion 419 | # Need to add an anonymous variable if a designation is not provided 420 | elif isinstance(subquery, EdgeExists): 421 | if (not hasattr(subquery, 'designation') or 422 | subquery.designation is None): 423 | subquery.designation = ( 424 | '_v' + str(_recurse.next_anonymous_variable)) 425 | _recurse.next_anonymous_variable += 1 426 | 427 | elif isinstance(subquery, Node): 428 | if (not hasattr(subquery, 'designation') or 429 | subquery.designation is None): 430 | subquery.designation = ( 431 | '_v' + str(_recurse.next_anonymous_variable)) 432 | _recurse.next_anonymous_variable += 1 433 | _recurse.atomic_facts.append(ClassIs(subquery.designation, 434 | subquery.node_class)) 435 | _recurse(subquery.connecting_edges) 436 | _recurse.atomic_facts += subquery.connecting_edges 437 | # print 'here' 438 | if hasattr(subquery, 'attribute_conditions'): 439 | _recurse.atomic_facts.append( 440 | NodeHasDocument( 441 | designation=subquery.designation, 442 | document=(subquery.attribute_conditions if 443 | len(subquery.attribute_conditions) > 0 444 | else None))) 445 | if hasattr(subquery, 'foobarthingy'): 446 | # Don't think we'll need a case for edges 447 | pass 448 | elif isinstance(subquery, MatchWhere): 449 | _recurse.atomic_facts.append(subquery) 450 | elif isinstance(subquery, CreateClause): 451 | _recurse(subquery.create_clause) 452 | else: 453 | import pdb; pdb.set_trace() 454 | raise Exception( 455 | 'unhandled case in extract_atomic_facts:' + ( 456 | subquery.__class__.__name__)) 457 | _recurse.atomic_facts = [] 458 | _recurse.next_anonymous_variable = 0 459 | _recurse(query) 460 | return _recurse.atomic_facts 461 | 462 | 463 | def main(): 464 | # sample = ','.join(['MATCH (x:SOMECLASS {bar : "baz"', 465 | # 'foo:"goo"})<-[:WHATEVER]-(:ANOTHERCLASS)', 466 | # '(y:LASTCLASS) RETURN x.foo, y']) 467 | 468 | # create = ('CREATE (n:SOMECLASS {foo: "bar", bar: {qux: "baz"}})' 469 | # '-[e:EDGECLASS]->(m:ANOTHERCLASS) RETURN n') 470 | # create = 'CREATE (n:SOMECLASS {foo: "bar", qux: "baz"}) RETURN n' 471 | create_query = ('CREATE (n:SOMECLASS {foo: {goo: "bar"}})' 472 | '-[e:EDGECLASS]->(m:ANOTHERCLASS {qux: "foobar", bar: 10}) ' 473 | 'RETURN n') 474 | test_query = ('MATCH (n:SOMECLASS {foo: {goo: "bar"}})-[e:EDGECLASS]->' 475 | '(m:ANOTHERCLASS) WHERE ' 476 | 'm.bar = 10 ' 477 | 'RETURN n.foo.goo, m.qux, e') 478 | # atomic_facts = extract_atomic_facts(test_query) 479 | graph_object = nx.MultiDiGraph() 480 | my_parser = CypherToNetworkx() 481 | for i in my_parser.query(graph_object, create_query): 482 | pass # a generator, we need to loop over results to run. 483 | for i in my_parser.query(graph_object, test_query): 484 | print i 485 | 486 | 487 | if __name__ == '__main__': 488 | # This main method is just for testing 489 | main() 490 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | networkx>=1.10,<2.0 2 | ply>=3.8,<4.0 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | setup( 3 | name = "PythonCypher", 4 | version = "0.1", 5 | packages = find_packages(), 6 | scripts = ['python_cypher/cypher_tokenizer.py', 7 | 'python_cypher/cypher_parser.py', 8 | 'python_cypher/python_cypher.py'], 9 | 10 | # Project uses reStructuredText, so ensure that the docutils get 11 | # installed or upgraded on the target machine 12 | install_requires = ['docutils>=0.3'], 13 | 14 | package_data = { 15 | }, 16 | classifiers = [ 17 | 'Development Status :: 3 - Alpha' 18 | 'Intended Audience :: Developers' 19 | 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)' 20 | 'Programming Language :: Python :: 2.7'], 21 | # metadata for upload to PyPI 22 | author = "Zachary Ernst", 23 | author_email = "zac.ernst@gmail.com", 24 | description = "Cypher query language for Python", 25 | license = "GPL 2", 26 | keywords = "cypher neo4j ", 27 | url = "http://github.com/zacernst/python_cypher", # project home page, if any 28 | ) 29 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacernst/python_cypher/9b06accce9499db51cc772dfb354f240ae2aff15/test/__init__.py -------------------------------------------------------------------------------- /test/test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import networkx as nx 3 | from python_cypher import python_cypher 4 | 5 | 6 | class TestPythonCypher(unittest.TestCase): 7 | 8 | def test_upper(self): 9 | """Test we can parse a CREATE... RETURN query.""" 10 | g = nx.MultiDiGraph() 11 | query = 'CREATE (n:SOMECLASS) RETURN n' 12 | test_parser = python_cypher.CypherToNetworkx() 13 | test_parser.query(g, query) 14 | 15 | def test_create_node(self): 16 | """Test we can build a query and create a node""" 17 | g = nx.MultiDiGraph() 18 | query = 'CREATE (n) RETURN n' 19 | test_parser = python_cypher.CypherToNetworkx() 20 | for i in test_parser.query(g, query): 21 | pass 22 | self.assertEqual(len(g.node), 1) 23 | 24 | def test_create_node_and_edge(self): 25 | """Test we can build a query and create two nodes and an edge""" 26 | g = nx.MultiDiGraph() 27 | query = 'CREATE (n)-->(m) RETURN n, m' 28 | test_parser = python_cypher.CypherToNetworkx() 29 | for i in test_parser.query(g, query): 30 | pass 31 | self.assertEqual(len(g.node), 2) 32 | self.assertEqual(len(g.edge), 2) 33 | 34 | def test_return_attribute(self): 35 | """Test we can return attribute from matching node""" 36 | g = nx.MultiDiGraph() 37 | create_query = 'CREATE (n:SOMECLASS {foo: "bar"}) RETURN n' 38 | match_query = 'MATCH (n) RETURN n.foo' 39 | test_parser = python_cypher.CypherToNetworkx() 40 | list(test_parser.query(g, create_query)) 41 | out = list(test_parser.query(g, match_query)) 42 | # self.assertEqual(out[0], ['bar']) 43 | 44 | 45 | if __name__ == '__main__': 46 | unittest.main() 47 | --------------------------------------------------------------------------------