├── pyproject.toml ├── README.md ├── _mgp.py ├── .gitignore ├── LICENSE └── mgp.py /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "mgp" 3 | version = "0.1.2" 4 | description = "Memgraph's module for developing MAGE modules. Used only for type hinting!" 5 | authors = ["MasterMedo "] 6 | license = "Apache Software License (Apache2)" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.7" 10 | 11 | [tool.poetry.dev-dependencies] 12 | 13 | [build-system] 14 | requires = ["poetry-core>=1.0.0"] 15 | build-backend = "poetry.core.masonry.api" 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mgp 2 | 3 | Pypi package used for type hinting when creating MAGE modules. 4 | 5 | 6 | # how to publish new versions 7 | ## prerequisites 8 | 1. installed poetry 9 | ``` 10 | pip install poetry 11 | ``` 12 | 2. set up [API tokens](https://pypi.org/help/#apitoken) 13 | 3. be a collaborator on [pypi](https://pypi.org/project/mgp/) 14 | 15 | ## making changes 16 | 1. make changes to the package 17 | 2. bump version in `pyproject.tml` 18 | 3. `poetry build` 19 | 4. `poetry publish` 20 | -------------------------------------------------------------------------------- /_mgp.py: -------------------------------------------------------------------------------- 1 | class Vertex: 2 | pass 3 | 4 | 5 | class Edge: 6 | pass 7 | 8 | 9 | class Path: 10 | def make_with_start(vertex): 11 | pass 12 | 13 | 14 | class Graph: 15 | pass 16 | 17 | 18 | class CypherType: 19 | pass 20 | 21 | 22 | def type_nullable(): 23 | pass 24 | 25 | 26 | def type_any(): 27 | pass 28 | 29 | 30 | def type_list(): 31 | pass 32 | 33 | 34 | def type_bool(): 35 | pass 36 | 37 | 38 | def type_string(): 39 | pass 40 | 41 | 42 | def type_int(): 43 | pass 44 | 45 | 46 | def type_float(): 47 | pass 48 | 49 | 50 | def type_number(): 51 | pass 52 | 53 | 54 | def type_map(): 55 | pass 56 | 57 | 58 | def type_node(): 59 | pass 60 | 61 | 62 | def type_relationship(): 63 | pass 64 | 65 | 66 | def type_path(): 67 | pass 68 | 69 | 70 | class _MODULE: 71 | def add_read_procedure(wrapper): 72 | pass 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /mgp.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This module provides the API for usage in custom openCypher procedures. 3 | ''' 4 | 5 | # C API using `mgp_memory` is not exposed in Python, instead the usage of such 6 | # API is hidden behind Python API. Any function requiring an instance of 7 | # `mgp_memory` should go through a `ProcCtx` instance. 8 | # 9 | # `mgp_value` does not exist as such in Python, instead all `mgp_value` 10 | # instances are marshalled to an appropriate Python object. This implies that 11 | # `mgp_list` and `mgp_map` are mapped to `list` and `dict` respectively. 12 | # 13 | # Only the public API is stubbed out here. Any private details are left for the 14 | # actual implementation. Functions have type annotations as supported by Python 15 | # 3.5, but variable type annotations are only available with Python 3.6+ 16 | 17 | from collections import namedtuple 18 | import functools 19 | import inspect 20 | import sys 21 | import typing 22 | 23 | import _mgp 24 | 25 | 26 | class InvalidContextError(Exception): 27 | '''Signals using a graph element instance outside of the registered procedure.''' 28 | pass 29 | 30 | 31 | class Label: 32 | '''Label of a Vertex.''' 33 | __slots__ = ('_name',) 34 | 35 | def __init__(self, name): 36 | self._name = name 37 | 38 | @property 39 | def name(self) -> str: 40 | return self._name 41 | 42 | def __eq__(self, other) -> bool: 43 | if isinstance(other, Label): 44 | return self._name == other.name 45 | if isinstance(other, str): 46 | return self._name == other 47 | return NotImplemented 48 | 49 | 50 | # Named property value of a Vertex or an Edge. 51 | # It would be better to use typing.NamedTuple with typed fields, but that is 52 | # not available in Python 3.5. 53 | Property = namedtuple('Property', ('name', 'value')) 54 | 55 | 56 | class Properties: 57 | '''A collection of properties either on a Vertex or an Edge.''' 58 | __slots__ = ('_vertex_or_edge', '_len',) 59 | 60 | def __init__(self, vertex_or_edge): 61 | if not isinstance(vertex_or_edge, (_mgp.Vertex, _mgp.Edge)): 62 | raise TypeError("Expected '_mgp.Vertex' or '_mgp.Edge', \ 63 | got {}".format(type(vertex_or_edge))) 64 | self._len = None 65 | self._vertex_or_edge = vertex_or_edge 66 | 67 | def __deepcopy__(self, memo): 68 | # This is the same as the shallow copy, as the underlying C API should 69 | # not support deepcopy. Besides, it doesn't make much sense to actually 70 | # copy _mgp.Edge and _mgp.Vertex types as they are actually references 71 | # to graph elements and not proper values. 72 | return Properties(self._vertex_or_edge) 73 | 74 | def get(self, property_name: str, default=None) -> object: 75 | '''Get the value of a property with the given name or return default. 76 | 77 | Raise InvalidContextError. 78 | ''' 79 | if not self._vertex_or_edge.is_valid(): 80 | raise InvalidContextError() 81 | try: 82 | return self[property_name] 83 | except KeyError: 84 | return default 85 | 86 | def items(self) -> typing.Iterable[Property]: 87 | '''Raise InvalidContextError.''' 88 | if not self._vertex_or_edge.is_valid(): 89 | raise InvalidContextError() 90 | properties_it = self._vertex_or_edge.iter_properties() 91 | prop = properties_it.get() 92 | while prop is not None: 93 | yield Property(*prop) 94 | if not self._vertex_or_edge.is_valid(): 95 | raise InvalidContextError() 96 | prop = properties_it.next() 97 | 98 | def keys(self) -> typing.Iterable[str]: 99 | '''Iterate over property names. 100 | 101 | Raise InvalidContextError. 102 | ''' 103 | if not self._vertex_or_edge.is_valid(): 104 | raise InvalidContextError() 105 | for item in self.items(): 106 | yield item.name 107 | 108 | def values(self) -> typing.Iterable[object]: 109 | '''Iterate over property values. 110 | 111 | Raise InvalidContextError. 112 | ''' 113 | if not self._vertex_or_edge.is_valid(): 114 | raise InvalidContextError() 115 | for item in self.items(): 116 | yield item.value 117 | 118 | def __len__(self) -> int: 119 | '''Raise InvalidContextError.''' 120 | if not self._vertex_or_edge.is_valid(): 121 | raise InvalidContextError() 122 | if self._len is None: 123 | self._len = sum(1 for item in self.items()) 124 | return self._len 125 | 126 | def __iter__(self) -> typing.Iterable[str]: 127 | '''Iterate over property names. 128 | 129 | Raise InvalidContextError. 130 | ''' 131 | if not self._vertex_or_edge.is_valid(): 132 | raise InvalidContextError() 133 | for item in self.items(): 134 | yield item.name 135 | 136 | def __getitem__(self, property_name: str) -> object: 137 | '''Get the value of a property with the given name or raise KeyError. 138 | 139 | Raise InvalidContextError.''' 140 | if not self._vertex_or_edge.is_valid(): 141 | raise InvalidContextError() 142 | prop = self._vertex_or_edge.get_property(property_name) 143 | if prop is None: 144 | raise KeyError() 145 | return prop 146 | 147 | def __contains__(self, property_name: str) -> bool: 148 | if not self._vertex_or_edge.is_valid(): 149 | raise InvalidContextError() 150 | try: 151 | _ = self[property_name] 152 | return True 153 | except KeyError: 154 | return False 155 | 156 | 157 | class EdgeType: 158 | '''Type of an Edge.''' 159 | __slots__ = ('_name',) 160 | 161 | def __init__(self, name): 162 | self._name = name 163 | 164 | @property 165 | def name(self) -> str: 166 | return self._name 167 | 168 | def __eq__(self, other) -> bool: 169 | if isinstance(other, EdgeType): 170 | return self.name == other.name 171 | if isinstance(other, str): 172 | return self.name == other 173 | return NotImplemented 174 | 175 | 176 | if sys.version_info >= (3, 5, 2): 177 | EdgeId = typing.NewType('EdgeId', int) 178 | else: 179 | EdgeId = int 180 | 181 | 182 | class Edge: 183 | '''Edge in the graph database. 184 | 185 | Access to an Edge is only valid during a single execution of a procedure in 186 | a query. You should not globally store an instance of an Edge. Using an 187 | invalid Edge instance will raise InvalidContextError. 188 | ''' 189 | __slots__ = ('_edge',) 190 | 191 | def __init__(self, edge): 192 | if not isinstance(edge, _mgp.Edge): 193 | raise TypeError("Expected '_mgp.Edge', got '{}'".format(type(edge))) 194 | self._edge = edge 195 | 196 | def __deepcopy__(self, memo): 197 | # This is the same as the shallow copy, because we want to share the 198 | # underlying C struct. Besides, it doesn't make much sense to actually 199 | # copy _mgp.Edge as that is actually a reference to a graph element 200 | # and not a proper value. 201 | return Edge(self._edge) 202 | 203 | def is_valid(self) -> bool: 204 | '''Return True if `self` is in valid context and may be used.''' 205 | return self._edge.is_valid() 206 | 207 | @property 208 | def id(self) -> EdgeId: 209 | '''Raise InvalidContextError.''' 210 | if not self.is_valid(): 211 | raise InvalidContextError() 212 | return self._edge.get_id() 213 | 214 | @property 215 | def type(self) -> EdgeType: 216 | '''Raise InvalidContextError.''' 217 | if not self.is_valid(): 218 | raise InvalidContextError() 219 | return EdgeType(self._edge.get_type_name()) 220 | 221 | @property 222 | def from_vertex(self): # -> Vertex: 223 | '''Raise InvalidContextError.''' 224 | if not self.is_valid(): 225 | raise InvalidContextError() 226 | return Vertex(self._edge.from_vertex()) 227 | 228 | @property 229 | def to_vertex(self): # -> Vertex: 230 | '''Raise InvalidContextError.''' 231 | if not self.is_valid(): 232 | raise InvalidContextError() 233 | return Vertex(self._edge.to_vertex()) 234 | 235 | @property 236 | def properties(self) -> Properties: 237 | '''Raise InvalidContextError.''' 238 | if not self.is_valid(): 239 | raise InvalidContextError() 240 | return Properties(self._edge) 241 | 242 | def __eq__(self, other) -> bool: 243 | '''Raise InvalidContextError.''' 244 | if not self.is_valid(): 245 | raise InvalidContextError() 246 | if not isinstance(other, Edge): 247 | return NotImplemented 248 | return self._edge == other._edge 249 | 250 | def __hash__(self) -> int: 251 | return hash(self.id) 252 | 253 | 254 | if sys.version_info >= (3, 5, 2): 255 | VertexId = typing.NewType('VertexId', int) 256 | else: 257 | VertexId = int 258 | 259 | 260 | class Vertex: 261 | '''Vertex in the graph database. 262 | 263 | Access to a Vertex is only valid during a single execution of a procedure 264 | in a query. You should not globally store an instance of a Vertex. Using an 265 | invalid Vertex instance will raise InvalidContextError. 266 | ''' 267 | __slots__ = ('_vertex',) 268 | 269 | def __init__(self, vertex): 270 | if not isinstance(vertex, _mgp.Vertex): 271 | raise TypeError("Expected '_mgp.Vertex', got '{}'".format(type(vertex))) 272 | self._vertex = vertex 273 | 274 | def __deepcopy__(self, memo): 275 | # This is the same as the shallow copy, because we want to share the 276 | # underlying C struct. Besides, it doesn't make much sense to actually 277 | # copy _mgp.Vertex as that is actually a reference to a graph element 278 | # and not a proper value. 279 | return Vertex(self._vertex) 280 | 281 | def is_valid(self) -> bool: 282 | '''Return True if `self` is in valid context and may be used''' 283 | return self._vertex.is_valid() 284 | 285 | @property 286 | def id(self) -> VertexId: 287 | '''Raise InvalidContextError.''' 288 | if not self.is_valid(): 289 | raise InvalidContextError() 290 | return self._vertex.get_id() 291 | 292 | @property 293 | def labels(self) -> typing.List[Label]: 294 | '''Raise InvalidContextError.''' 295 | if not self.is_valid(): 296 | raise InvalidContextError() 297 | return tuple(Label(self._vertex.label_at(i)) 298 | for i in range(self._vertex.labels_count())) 299 | 300 | @property 301 | def properties(self) -> Properties: 302 | '''Raise InvalidContextError.''' 303 | if not self.is_valid(): 304 | raise InvalidContextError() 305 | return Properties(self._vertex) 306 | 307 | @property 308 | def in_edges(self) -> typing.Iterable[Edge]: 309 | '''Raise InvalidContextError.''' 310 | if not self.is_valid(): 311 | raise InvalidContextError() 312 | edges_it = self._vertex.iter_in_edges() 313 | edge = edges_it.get() 314 | while edge is not None: 315 | yield Edge(edge) 316 | if not self.is_valid(): 317 | raise InvalidContextError() 318 | edge = edges_it.next() 319 | 320 | @property 321 | def out_edges(self) -> typing.Iterable[Edge]: 322 | '''Raise InvalidContextError.''' 323 | if not self.is_valid(): 324 | raise InvalidContextError() 325 | edges_it = self._vertex.iter_out_edges() 326 | edge = edges_it.get() 327 | while edge is not None: 328 | yield Edge(edge) 329 | if not self.is_valid(): 330 | raise InvalidContextError() 331 | edge = edges_it.next() 332 | 333 | def __eq__(self, other) -> bool: 334 | '''Raise InvalidContextError''' 335 | if not self.is_valid(): 336 | raise InvalidContextError() 337 | if not isinstance(other, Vertex): 338 | return NotImplemented 339 | return self._vertex == other._vertex 340 | 341 | def __hash__(self) -> int: 342 | return hash(self.id) 343 | 344 | 345 | class Path: 346 | '''Path containing Vertex and Edge instances.''' 347 | __slots__ = ('_path', '_vertices', '_edges') 348 | 349 | def __init__(self, starting_vertex_or_path: typing.Union[_mgp.Path, Vertex]): 350 | '''Initialize with a starting Vertex. 351 | 352 | Raise InvalidContextError if passed in Vertex is invalid. 353 | ''' 354 | # We cache calls to `vertices` and `edges`, so as to avoid needless 355 | # allocations at the C level. 356 | self._vertices = None 357 | self._edges = None 358 | # Accepting _mgp.Path is just for internal usage. 359 | if isinstance(starting_vertex_or_path, _mgp.Path): 360 | self._path = starting_vertex_or_path 361 | elif isinstance(starting_vertex_or_path, Vertex): 362 | vertex = starting_vertex_or_path._vertex 363 | if not vertex.is_valid(): 364 | raise InvalidContextError() 365 | self._path = _mgp.Path.make_with_start(vertex) 366 | else: 367 | raise TypeError("Expected '_mgp.Vertex' or '_mgp.Path', got '{}'" 368 | .format(type(starting_vertex_or_path))) 369 | 370 | def __copy__(self): 371 | if not self.is_valid(): 372 | raise InvalidContextError() 373 | assert len(self.vertices) >= 1 374 | path = Path(self.vertices[0]) 375 | for e in self.edges: 376 | path.expand(e) 377 | return path 378 | 379 | def __deepcopy__(self, memo): 380 | try: 381 | return Path(memo[id(self._path)]) 382 | except KeyError: 383 | pass 384 | # This is the same as the shallow copy, as the underlying C API should 385 | # not support deepcopy. Besides, it doesn't make much sense to actually 386 | # copy _mgp.Edge and _mgp.Vertex types as they are actually references 387 | # to graph elements and not proper values. 388 | path = self.__copy__() 389 | memo[id(self._path)] = path._path 390 | return path 391 | 392 | def is_valid(self) -> bool: 393 | return self._path.is_valid() 394 | 395 | def expand(self, edge: Edge): 396 | '''Append an edge continuing from the last vertex on the path. 397 | 398 | The last vertex on the path will become the other endpoint of the given 399 | edge, as continued from the current last vertex. 400 | 401 | Raise ValueError if the current last vertex in the path is not part of 402 | the given edge. 403 | Raise InvalidContextError if using an invalid Path instance or if 404 | passed in edge is invalid. 405 | ''' 406 | if not isinstance(edge, Edge): 407 | raise TypeError("Expected '_mgp.Edge', got '{}'".format(type(edge))) 408 | if not self.is_valid() or not edge.is_valid(): 409 | raise InvalidContextError() 410 | self._path.expand(edge._edge) 411 | # Invalidate our cached tuples 412 | self._vertices = None 413 | self._edges = None 414 | 415 | @property 416 | def vertices(self) -> typing.Tuple[Vertex, ...]: 417 | '''Vertices ordered from the start to the end of the path. 418 | 419 | Raise InvalidContextError if using an invalid Path instance.''' 420 | if not self.is_valid(): 421 | raise InvalidContextError() 422 | if self._vertices is None: 423 | num_vertices = self._path.size() + 1 424 | self._vertices = tuple(Vertex(self._path.vertex_at(i)) 425 | for i in range(num_vertices)) 426 | return self._vertices 427 | 428 | @property 429 | def edges(self) -> typing.Tuple[Edge, ...]: 430 | '''Edges ordered from the start to the end of the path. 431 | 432 | Raise InvalidContextError if using an invalid Path instance.''' 433 | if not self.is_valid(): 434 | raise InvalidContextError() 435 | if self._edges is None: 436 | num_edges = self._path.size() 437 | self._edges = tuple(Edge(self._path.edge_at(i)) 438 | for i in range(num_edges)) 439 | return self._edges 440 | 441 | 442 | class Record: 443 | '''Represents a record of resulting field values.''' 444 | __slots__ = ('fields',) 445 | 446 | def __init__(self, **kwargs): 447 | '''Initialize with name=value fields in kwargs.''' 448 | self.fields = kwargs 449 | 450 | 451 | class Vertices: 452 | '''Iterable over vertices in a graph.''' 453 | __slots__ = ('_graph', '_len') 454 | 455 | def __init__(self, graph): 456 | if not isinstance(graph, _mgp.Graph): 457 | raise TypeError("Expected '_mgp.Graph', got '{}'".format(type(graph))) 458 | self._graph = graph 459 | self._len = None 460 | 461 | def __deepcopy__(self, memo): 462 | # This is the same as the shallow copy, because we want to share the 463 | # underlying C struct. Besides, it doesn't make much sense to actually 464 | # copy _mgp.Graph as that always references the whole graph state. 465 | return Vertices(self._graph) 466 | 467 | def is_valid(self) -> bool: 468 | '''Return True if `self` is in valid context and may be used.''' 469 | return self._graph.is_valid() 470 | 471 | def __iter__(self) -> typing.Iterable[Vertex]: 472 | '''Raise InvalidContextError if context is invalid.''' 473 | if not self.is_valid(): 474 | raise InvalidContextError() 475 | vertices_it = self._graph.iter_vertices() 476 | vertex = vertices_it.get() 477 | while vertex is not None: 478 | yield Vertex(vertex) 479 | if not self.is_valid(): 480 | raise InvalidContextError() 481 | vertex = vertices_it.next() 482 | 483 | def __contains__(self, vertex): 484 | try: 485 | _ = self._graph.get_vertex_by_id(vertex.id) 486 | return True 487 | except IndexError: 488 | return False 489 | 490 | def __len__(self): 491 | if not self._len: 492 | self._len = sum(1 for _ in self) 493 | return self._len 494 | 495 | 496 | class Graph: 497 | '''State of the graph database in current ProcCtx.''' 498 | __slots__ = ('_graph',) 499 | 500 | def __init__(self, graph): 501 | if not isinstance(graph, _mgp.Graph): 502 | raise TypeError("Expected '_mgp.Graph', got '{}'".format(type(graph))) 503 | self._graph = graph 504 | 505 | def __deepcopy__(self, memo): 506 | # This is the same as the shallow copy, because we want to share the 507 | # underlying C struct. Besides, it doesn't make much sense to actually 508 | # copy _mgp.Graph as that always references the whole graph state. 509 | return Graph(self._graph) 510 | 511 | def is_valid(self) -> bool: 512 | '''Return True if `self` is in valid context and may be used.''' 513 | return self._graph.is_valid() 514 | 515 | def get_vertex_by_id(self, vertex_id: VertexId) -> Vertex: 516 | '''Return the Vertex corresponding to given vertex_id from the graph. 517 | 518 | Access to a Vertex is only valid during a single execution of a 519 | procedure in a query. You should not globally store the returned 520 | Vertex. 521 | 522 | Raise IndexError if unable to find the given vertex_id. 523 | Raise InvalidContextError if context is invalid. 524 | ''' 525 | if not self.is_valid(): 526 | raise InvalidContextError() 527 | vertex = self._graph.get_vertex_by_id(vertex_id) 528 | return Vertex(vertex) 529 | 530 | @property 531 | def vertices(self) -> Vertices: 532 | '''All vertices in the graph. 533 | 534 | Access to a Vertex is only valid during a single execution of a 535 | procedure in a query. You should not globally store the returned Vertex 536 | instances. 537 | 538 | Raise InvalidContextError if context is invalid. 539 | ''' 540 | if not self.is_valid(): 541 | raise InvalidContextError() 542 | return Vertices(self._graph) 543 | 544 | 545 | class AbortError(Exception): 546 | '''Signals that the procedure was asked to abort its execution.''' 547 | pass 548 | 549 | 550 | class ProcCtx: 551 | '''Context of a procedure being executed. 552 | 553 | Access to a ProcCtx is only valid during a single execution of a procedure 554 | in a query. You should not globally store a ProcCtx instance. 555 | ''' 556 | __slots__ = ('_graph',) 557 | 558 | def __init__(self, graph): 559 | if not isinstance(graph, _mgp.Graph): 560 | raise TypeError("Expected '_mgp.Graph', got '{}'".format(type(graph))) 561 | self._graph = Graph(graph) 562 | 563 | def is_valid(self) -> bool: 564 | return self._graph.is_valid() 565 | 566 | @property 567 | def graph(self) -> Graph: 568 | '''Raise InvalidContextError if context is invalid.''' 569 | if not self.is_valid(): 570 | raise InvalidContextError() 571 | return self._graph 572 | 573 | def must_abort(self) -> bool: 574 | if not self.is_valid(): 575 | raise InvalidContextError() 576 | return self._graph._graph.must_abort() 577 | 578 | def check_must_abort(self): 579 | if self.must_abort(): 580 | raise AbortError 581 | 582 | 583 | # Additional typing support 584 | 585 | Number = typing.Union[int, float] 586 | 587 | Map = typing.Union[dict, Edge, Vertex] 588 | 589 | Any = typing.Union[bool, str, Number, Map, Path, list] 590 | 591 | List = typing.List 592 | 593 | Nullable = typing.Optional 594 | 595 | 596 | class UnsupportedTypingError(Exception): 597 | '''Signals a typing annotation is not supported as a _mgp.CypherType.''' 598 | 599 | def __init__(self, type_): 600 | super().__init__("Unsupported typing annotation '{}'".format(type_)) 601 | 602 | 603 | def _typing_to_cypher_type(type_): 604 | '''Convert typing annotation to a _mgp.CypherType instance.''' 605 | simple_types = { 606 | typing.Any: _mgp.type_nullable(_mgp.type_any()), 607 | object: _mgp.type_nullable(_mgp.type_any()), 608 | list: _mgp.type_list(_mgp.type_nullable(_mgp.type_any())), 609 | Any: _mgp.type_any(), 610 | bool: _mgp.type_bool(), 611 | str: _mgp.type_string(), 612 | int: _mgp.type_int(), 613 | float: _mgp.type_float(), 614 | Number: _mgp.type_number(), 615 | Map: _mgp.type_map(), 616 | Vertex: _mgp.type_node(), 617 | Edge: _mgp.type_relationship(), 618 | Path: _mgp.type_path() 619 | } 620 | try: 621 | return simple_types[type_] 622 | except KeyError: 623 | pass 624 | if sys.version_info >= (3, 8): 625 | complex_type = typing.get_origin(type_) 626 | type_args = typing.get_args(type_) 627 | if complex_type == typing.Union: 628 | # If we have a Union with NoneType inside, it means we are building 629 | # a nullable type. 630 | if isinstance(None, type_args): 631 | types = tuple(t for t in type_args if not isinstance(None, t)) 632 | if len(types) == 1: 633 | type_arg, = types 634 | else: 635 | # We cannot do typing.Union[*types], so do the equivalent 636 | # with __getitem__ which does not even need arg unpacking. 637 | type_arg = typing.Union.__getitem__(types) 638 | return _mgp.type_nullable(_typing_to_cypher_type(type_arg)) 639 | elif complex_type == list: 640 | type_arg, = type_args 641 | return _mgp.type_list(_typing_to_cypher_type(type_arg)) 642 | raise UnsupportedTypingError(type_) 643 | else: 644 | # We cannot get to type args in any reliable way prior to 3.8, but we 645 | # still want to support typing.Optional and typing.List, so just parse 646 | # their string representations. Hopefully, that is always pretty 647 | # printed the same way. `typing.List[type]` is printed as such, while 648 | # `typing.Optional[type]` is printed as 'typing.Union[type, NoneType]' 649 | def parse_type_args(type_as_str): 650 | return tuple(map(str.strip, 651 | type_as_str[type_as_str.index('[') + 1: -1].split(','))) 652 | 653 | def fully_qualified_name(cls): 654 | if cls.__module__ is None or cls.__module__ == 'builtins': 655 | return cls.__name__ 656 | return cls.__module__ + '.' + cls.__name__ 657 | 658 | def get_simple_type(type_as_str): 659 | for simple_type, cypher_type in simple_types.items(): 660 | if type_as_str == str(simple_type): 661 | return cypher_type 662 | # Fallback to comparing to __name__ if it exits. This handles 663 | # the cases like when we have 'object' which is 664 | # `object.__name__`, but `str(object)` is "" 665 | try: 666 | if type_as_str == fully_qualified_name(simple_type): 667 | return cypher_type 668 | except AttributeError: 669 | pass 670 | 671 | def parse_typing(type_as_str): 672 | if type_as_str.startswith('typing.Union'): 673 | type_args_as_str = parse_type_args(type_as_str) 674 | none_type_as_str = type(None).__name__ 675 | if none_type_as_str in type_args_as_str: 676 | types = tuple(t for t in type_args_as_str if t != none_type_as_str) 677 | if len(types) == 1: 678 | type_arg_as_str, = types 679 | else: 680 | type_arg_as_str = 'typing.Union[' + ', '.join(types) + ']' 681 | simple_type = get_simple_type(type_arg_as_str) 682 | if simple_type is not None: 683 | return _mgp.type_nullable(simple_type) 684 | return _mgp.type_nullable(parse_typing(type_arg_as_str)) 685 | elif type_as_str.startswith('typing.List'): 686 | type_arg_as_str, = parse_type_args(type_as_str) 687 | simple_type = get_simple_type(type_arg_as_str) 688 | if simple_type is not None: 689 | return _mgp.type_list(simple_type) 690 | return _mgp.type_list(parse_typing(type_arg_as_str)) 691 | raise UnsupportedTypingError(type_) 692 | 693 | return parse_typing(str(type_)) 694 | 695 | 696 | # Procedure registration 697 | 698 | class Deprecated: 699 | '''Annotate a resulting Record's field as deprecated.''' 700 | __slots__ = ('field_type',) 701 | 702 | def __init__(self, type_): 703 | self.field_type = type_ 704 | 705 | 706 | def read_proc(func: typing.Callable[..., Record]): 707 | ''' 708 | Register `func` as a a read-only procedure of the current module. 709 | 710 | `read_proc` is meant to be used as a decorator function to register module 711 | procedures. The registered `func` needs to be a callable which optionally 712 | takes `ProcCtx` as the first argument. Other arguments of `func` will be 713 | bound to values passed in the cypherQuery. The full signature of `func` 714 | needs to be annotated with types. The return type must be 715 | `Record(field_name=type, ...)` and the procedure must produce either a 716 | complete Record or None. To mark a field as deprecated, use 717 | `Record(field_name=Deprecated(type), ...)`. Multiple records can be 718 | produced by returning an iterable of them. Registering generator functions 719 | is currently not supported. 720 | 721 | Example usage. 722 | 723 | ``` 724 | import mgp 725 | 726 | @mgp.read_proc 727 | def procedure(context: mgp.ProcCtx, 728 | required_arg: mgp.Nullable[mgp.Any], 729 | optional_arg: mgp.Nullable[mgp.Any] = None 730 | ) -> mgp.Record(result=str, args=list): 731 | args = [required_arg, optional_arg] 732 | # Multiple rows can be produced by returning an iterable of mgp.Record 733 | return mgp.Record(args=args, result='Hello World!') 734 | ``` 735 | 736 | The example procedure above returns 2 fields: `args` and `result`. 737 | * `args` is a copy of arguments passed to the procedure. 738 | * `result` is the result of this procedure, a "Hello World!" string. 739 | Any errors can be reported by raising an Exception. 740 | 741 | The procedure can be invoked in openCypher using the following calls: 742 | CALL example.procedure(1, 2) YIELD args, result; 743 | CALL example.procedure(1) YIELD args, result; 744 | Naturally, you may pass in different arguments or yield less fields. 745 | ''' 746 | if not callable(func): 747 | raise TypeError("Expected a callable object, got an instance of '{}'" 748 | .format(type(func))) 749 | if inspect.iscoroutinefunction(func): 750 | raise TypeError("Callable must not be 'async def' function") 751 | if sys.version_info >= (3, 6): 752 | if inspect.isasyncgenfunction(func): 753 | raise TypeError("Callable must not be 'async def' function") 754 | if inspect.isgeneratorfunction(func): 755 | raise NotImplementedError("Generator functions are not supported") 756 | sig = inspect.signature(func) 757 | params = tuple(sig.parameters.values()) 758 | if params and params[0].annotation is ProcCtx: 759 | @functools.wraps(func) 760 | def wrapper(graph, args): 761 | return func(ProcCtx(graph), *args) 762 | params = params[1:] 763 | mgp_proc = _mgp._MODULE.add_read_procedure(wrapper) 764 | else: 765 | @functools.wraps(func) 766 | def wrapper(graph, args): 767 | return func(*args) 768 | mgp_proc = _mgp._MODULE.add_read_procedure(wrapper) 769 | for param in params: 770 | name = param.name 771 | type_ = param.annotation 772 | if type_ is param.empty: 773 | type_ = object 774 | cypher_type = _typing_to_cypher_type(type_) 775 | if param.default is param.empty: 776 | mgp_proc.add_arg(name, cypher_type) 777 | else: 778 | mgp_proc.add_opt_arg(name, cypher_type, param.default) 779 | if sig.return_annotation is not sig.empty: 780 | record = sig.return_annotation 781 | if not isinstance(record, Record): 782 | raise TypeError("Expected '{}' to return 'mgp.Record', got '{}'" 783 | .format(func.__name__, type(record))) 784 | for name, type_ in record.fields.items(): 785 | if isinstance(type_, Deprecated): 786 | cypher_type = _typing_to_cypher_type(type_.field_type) 787 | mgp_proc.add_deprecated_result(name, cypher_type) 788 | else: 789 | mgp_proc.add_result(name, _typing_to_cypher_type(type_)) 790 | return func 791 | --------------------------------------------------------------------------------