diff --git a/.gitignore b/.gitignore index 77a842c..2e485f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ -*.swp -*.pyc -agtype.egg-info +.eggs/ build dist -.idea +*.egg-info/ +*.pyc +*.swp diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 50c9327..caa767f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,6 @@ -# AgensGraph Python driver - -It is AgensGraph client for Python, supports the graph data types like Vertex, Edge and Path used in AgensGraph. - -It is based on [psycopg2](https://github.com/psycopg/psycopg2). +# AgensGraph Python Driver +AgensGraph Python Driver allows Python programs to connect to an AgensGraph database. Since it is [Psycopg2](http://initd.org/psycopg/) type extension module for AgensGraph, it supports additional data types such as `Vertex`, `Edge`, and `Path` to represent graph data. ## Install @@ -11,35 +8,41 @@ It is based on [psycopg2](https://github.com/psycopg/psycopg2). $ pip install -U pip $ pip install psycopg2 -$ python /path/to/set.py install - +$ python /path/to/agensgraph/python/setup.py install ``` - ## Example ```python import psycopg2 +import agensgraph -connect = psycopg2.connect("dbname=agens user=bylee host=127.0.0.1") +conn = psycopg2.connect("dbname=test host=127.0.0.1 user=agens") cur = connect.cursor() -cur.execute("DROP GRAPH IF EXISTS g CASCADE") -cur.execute("CREATE GRAPH g") -cur.execute("SET graph_path = g") - -cur.execute("CREATE p=(:v{name: 'agens'})-[:e]->() RETURN p") -cur.execute("MATCH ()-[r]->() RETURN count(*)") -print cur.fetchone()[0] - +cur.execute("DROP GRAPH IF EXISTS t CASCADE") +cur.execute("CREATE GRAPH t") +cur.execute("SET graph_path = t") + +cur.execute("CREATE (:v {name: 'AgensGraph'})") +cur.execute("MATCH (n) RETURN n") +v = cur.fetchone()[0] +print(v.props['name']) ``` - - ## Test -```sh -$ python test_agtype [-v] +You may run the following command to test AgensGraph Python Driver. +```sh +$ python setup.py test ``` -The test cases must be executed in Python 2.x +Before running the command, set the following environment variables to specify which database you will use for the test. + +Variable Name | Meaning +---------------------------- | --------------------------- +`AGENSGRAPH_TESTDB` | database name to connect to +`AGENSGRAPH_TESTDB_HOST` | database server host +`AGENSGRAPH_TESTDB_PORT` | database server port +`AGENSGRAPH_TESTDB_USER` | database user name +`AGENSGRAPH_TESTDB_PASSWORD` | user password diff --git a/agensgraph/__init__.py b/agensgraph/__init__.py new file mode 100644 index 0000000..b67ccca --- /dev/null +++ b/agensgraph/__init__.py @@ -0,0 +1,44 @@ +''' +Copyright (c) 2014-2017, Bitnine Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + +from psycopg2 import extensions as _ext + +from agensgraph._graphid import ( + GraphId, cast_graphid as _cast_graphid, adapt_graphid as _adapt_graphid) +from agensgraph._vertex import Vertex, cast_vertex as _cast_vertex +from agensgraph._edge import Edge, cast_edge as _cast_edge +from agensgraph._graphpath import Path, cast_graphpath as _cast_graphpath + +_GRAPHID_OID = 7002 +_VERTEX_OID = 7012 +_EDGE_OID = 7022 +_GRAPHPATH_OID = 7032 + +GRAPHID = _ext.new_type((_GRAPHID_OID,), 'GRAPHID', _cast_graphid) +_ext.register_type(GRAPHID) +_ext.register_adapter(GraphId, _adapt_graphid) + +VERTEX = _ext.new_type((_VERTEX_OID,), 'VERTEX', _cast_vertex) +_ext.register_type(VERTEX) + +EDGE = _ext.new_type((_EDGE_OID,), 'EDGE', _cast_edge) +_ext.register_type(EDGE) + +PATH = _ext.new_type((_GRAPHPATH_OID,), 'PATH', _cast_graphpath) +_ext.register_type(PATH) + +__all__ = ['GraphId', 'Vertex', 'Edge', 'Path', + 'GRAPHID', 'VERTEX', 'EDGE', 'PATH'] diff --git a/agensgraph/_edge.py b/agensgraph/_edge.py new file mode 100644 index 0000000..8865769 --- /dev/null +++ b/agensgraph/_edge.py @@ -0,0 +1,59 @@ +''' +Copyright (c) 2014-2017, Bitnine Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + +import re + +from psycopg2 import InterfaceError +from psycopg2.extras import json + +from agensgraph._graphid import cast_graphid + +_pattern = re.compile(r'(.+?)\[(.+?)\]\[(.+?),(.+?)\](.*)', re.S) + +class Edge(object): + def __init__(self, label, eid, start, end, props): + self.label = label + self.eid = eid + self.start = start + self.end = end + self.props = props + + def __eq__(self, other): + if isinstance(self, other.__class__): + return self.eid == other.eid + return False + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, self) + + def __str__(self): + return "%s[%s][%s,%s]%s" % (self.label, self.eid, self.start, self.end, + json.dumps(self.props)) + +def cast_edge(value, cur): + if value is None: + return None + + m = _pattern.match(value) + if m: + label = m.group(1) + eid = cast_graphid(m.group(2), cur) + start = cast_graphid(m.group(3), cur) + end = cast_graphid(m.group(4), cur) + props = json.loads(m.group(5)) + return Edge(label, eid, start, end, props) + else: + raise InterfaceError("bad edge representation: %s" % value) diff --git a/agensgraph/_graphid.py b/agensgraph/_graphid.py new file mode 100644 index 0000000..3c38d46 --- /dev/null +++ b/agensgraph/_graphid.py @@ -0,0 +1,56 @@ +''' +Copyright (c) 2014-2017, Bitnine Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + +import re + +from psycopg2 import InterfaceError +from psycopg2.extensions import AsIs + +_pattern = re.compile(r'(\d+)\.(\d+)') + +class GraphId(object): + def __init__(self, gid): + self.gid = gid + + def getId(self): + return self.gid + + def __eq__(self, other): + if isinstance(self, other.__class__): + return self.gid == other.gid + return False + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, self) + + def __str__(self): + return "%d.%d" % self.gid + +def cast_graphid(value, cur): + if value is None: + return None + + m = _pattern.match(value) + if m: + labid = int(m.group(1)) + locid = int(m.group(2)) + gid = (labid, locid) + return GraphId(gid) + else: + raise InterfaceError("bad graphid representation: %s" % value) + +def adapt_graphid(graphid): + return AsIs("'%s'" % graphid) diff --git a/agensgraph/_graphpath.py b/agensgraph/_graphpath.py new file mode 100644 index 0000000..11cfb3d --- /dev/null +++ b/agensgraph/_graphpath.py @@ -0,0 +1,102 @@ +''' +Copyright (c) 2014-2017, Bitnine Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + +from psycopg2 import InterfaceError + +from agensgraph._vertex import cast_vertex +from agensgraph._edge import cast_edge + +class Path(object): + def __init__(self, vertices, edges): + self.vertices = vertices + self.edges = edges + + def __eq__(self, other): + if isinstance(self, other.__class__): + return self.vertices == other.vertices and self.edges == other.edges + return False + + def __len__(self): + return len(self.edges) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, self) + + def __str__(self): + p = [None] * (len(self.vertices) + len(self.edges)) + p[::2] = [str(v) for v in self.vertices] + p[1::2] = [str(e) for e in self.edges] + return "[%s]" % ','.join(p) + +def cast_graphpath(value, cur): + if value is None: + return None + + tokens = [] + + # ignore wrapping '[' and ']' characters + pos = 1 + length = len(value) - 1 + + start = pos + depth = 0 + gid = False + + while pos < length: + c = value[pos] + if c == '"': + if depth > 0: + # Parse "string". + # Leave pos unchanged if unmatched right " were found. + + escape = False + i = pos + 1 + + while i < length: + c = value[i] + if c == '\\': + escape = not escape + elif c == '"': + if escape: + escape = False + else: + pos = i + break + else: + escape = False + + i += 1 + elif c == '[' and depth == 0: + gid = True + elif c == ']' and depth == 0: + gid = False + elif c == '{': + depth += 1 + elif c == '}': + depth -= 1 + if depth < 0: + raise InterfaceError("bad graphpath representation: %s" % value) + elif c == ',' and depth == 0 and not gid: + tokens.append(value[start:pos]) + start = pos + 1 + + pos += 1 + + tokens.append(value[start:pos]) + + vertices = [cast_vertex(t, cur) for t in tokens[0::2]] + edges = [cast_edge(t, cur) for t in tokens[1::2]] + return Path(vertices, edges) diff --git a/agensgraph/_vertex.py b/agensgraph/_vertex.py new file mode 100644 index 0000000..eff95e2 --- /dev/null +++ b/agensgraph/_vertex.py @@ -0,0 +1,54 @@ +''' +Copyright (c) 2014-2017, Bitnine Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + +import re + +from psycopg2 import InterfaceError +from psycopg2.extras import json + +from agensgraph._graphid import cast_graphid + +_pattern = re.compile(r'(.+?)\[(.+?)\](.*)', re.S) + +class Vertex(object): + def __init__(self, label, vid, props): + self.label = label + self.vid = vid + self.props = props + + def __eq__(self, other): + if isinstance(self, other.__class__): + return self.vid == other.vid + return False + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, self) + + def __str__(self): + return "%s[%s]%s" % (self.label, self.vid, json.dumps(self.props)) + +def cast_vertex(value, cur): + if value is None: + return None + + m = _pattern.match(value) + if m: + label = m.group(1) + vid = cast_graphid(m.group(2), cur) + props = json.loads(m.group(3)) + return Vertex(label, vid, props) + else: + raise InterfaceError("bad vertex representation: %s" % value) diff --git a/agtype.py b/agtype.py deleted file mode 100644 index 7a90dbc..0000000 --- a/agtype.py +++ /dev/null @@ -1,128 +0,0 @@ -''' -Copyright (c) 2014-2016, Bitnine Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -''' - -import psycopg2 -import re, json - -_v_matcher = re.compile(r'(.+)\[(\d+)\.(\d+)\](.+)') -_e_matcher = re.compile(r'(.+)\[(\d+)\.(\d+)\]\[(\d+)\.(\d+),(\d+)\.(\d+)\](.*)') - -class GID: - def __init__(self, oident, ident): - self.oid = oident - self.id = ident - def __repr__(self): - return self.oid + "." + self.id - -class Vertex: - def __init__(self, value): - m = _v_matcher.match(value) - if m == None: - return ValueError - self.label = m.group(1) - self.vid = GID(m.group(2), m.group(3)) - self.props = json.loads(m.group(4)) - def __repr__(self): - return self.label + '[' + str(self.vid) + ']' + json.dumps(self.props) - -def _cast_vertex(value, cur): - if value is None: - return None - try: - v = Vertex(value) - except: - return psycopg2.InterfaceError("bad vertex representation: %s" % value) - return v - -class Edge: - def __init__(self, value): - m = _e_matcher.match(value) - if m == None: - raise ValueError - self.label = m.group(1) - self.eid = GID(m.group(2), m.group(3)) - self.svid = GID(m.group(4), m.group(5)) - self.evid = GID(m.group(6), m.group(7)) - self.props = json.loads(m.group(8)) - def __repr__(self): - return self.label + '[' + str(self.eid) + '][' + str(self.svid) + ',' + str(self.evid) + ']' + json.dumps(self.props) - -def _cast_edge(value, cur): - if value is None: - return None - try: - e = Edge(value) - except: - return psycopg2.InterfaceError("bad edge representation: %s" % value) - return e - -class Path: - def __init__(self, value): - l = self._tokenize(value) - if l == None: - raise ValueError - self.vertices = map(Vertex, l[0::2]) - self.edges = map(Edge, l[1::2]) - def _tokenize(self, value): - i = 0 - s = 0 - depth = 0 - inGID = False - l = [] - v = value[1:-1] # remove '[' and ']' - for c in v: - if '{' == c: - depth += 1 - elif '}' == c: - depth -= 1 - elif 0 == depth and '[' == c: # for GID - inGID = True - elif inGID and ']' == c: - inGID = False - elif 0 == depth and False == inGID and ',' == c: - l.append(v[s:i]) - s = i + 1 - if depth < 0: - raise ValueError - i += 1 - l.append(v[s:i]) - return l - def __repr__(self): - return ','.join(map(lambda x,y:repr(x)+','+repr(y),self.vertices,self.edges))[0:-5] - def start(self): - return self.vertices[0] - def end(self): - return self.vertices[-1] - def len(self): - return len(self.edges) - -def _cast_path(value, cur): - if value is None: - return None - try: - p = Path(value) - except: - return psycopg2.InterfaceError("bad path representation: %s" % value) - return p - -VERTEX = psycopg2.extensions.new_type((7012,), "VERTEX", _cast_vertex) -psycopg2.extensions.register_type(VERTEX) - -EDGE = psycopg2.extensions.new_type((7022,), "EDGE", _cast_edge) -psycopg2.extensions.register_type(EDGE) - -PATH = psycopg2.extensions.new_type((7032,), "PATH", _cast_path) -psycopg2.extensions.register_type(PATH) diff --git a/set.py b/set.py deleted file mode 100644 index 7d117cf..0000000 --- a/set.py +++ /dev/null @@ -1,12 +0,0 @@ -from setuptools import setup, find_packages -setup( - name = "agtype", - version = "0.9.0", - scripts = ["agtype.py"], - install_requires=["psycopg2>=2.5.4"], - test_suite = "test_agtype", - author = "Kyungtae Lee", - author_email = "ktlee@bitnine.net", - description = "A type extension module for AgensGraph", - url = "http://bitnine.net" -) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..cededcc --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +from setuptools import setup, find_packages + +setup( + name='agensgraph', + version='1.0.0', + description='Psycopg2 type extension module for AgensGraph', + install_requires=['psycopg2>=2.5.4'], + + packages=find_packages(exclude=['tests']), + test_suite = "tests", + + author='Junseok Yang', + author_email='jsyang@bitnine.net', + maintainer='Gitae Yun', + maintainer_email='gtyun@bitnine.net', + url='https://github.com/bitnine-oss/agensgraph-python', + license='Apache License Version 2.0', +) diff --git a/test_agtype.py b/test_agtype.py deleted file mode 100644 index 0efb685..0000000 --- a/test_agtype.py +++ /dev/null @@ -1,187 +0,0 @@ - -''' -Copyright (c) 2014-2016, Bitnine Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and - -''' - -import unittest -import psycopg2 -import agtype - -class BasicTest(unittest.TestCase): - def setUp(self): - self.conn = psycopg2.connect("dbname=agens user=bylee host=127.0.0.1") - self.cur = self.conn.cursor() - try: - self.cur.execute("DROP GRAPH p CASCADE") - except: - self.conn.commit() - self.cur.execute("CREATE GRAPH p") - self.cur.execute("SET GRAPH_PATH = p") - self.conn.commit() - def tearDown(self): - self.conn.commit() - self.cur.execute("DROP GRAPH p CASCADE") - self.cur.close() - self.conn.close() - -class CreateTest(BasicTest): - def setUp(self): - BasicTest.setUp(self) - self.cur.execute("CREATE VLABEL person") - self.cur.execute("CREATE VLABEL company") - self.cur.execute("CREATE ELABEL employee") - def test_CreateWithBind(self): - self.cur.execute( - "CREATE ( :person { name: 'XXX', from: 'Sweden', klout: %s } )", (99,)) - self.cur.execute("MATCH (n:person {name: %s}) RETURN n", ('XXX',)) - n = self.cur.fetchone()[0] - self.assertEquals(99, n.props["klout"]) - -class LabelInheritTest(BasicTest): - def setUp(self): - BasicTest.setUp(self) - self.cur.execute("CREATE VLABEL parent") - self.cur.execute("CREATE VLABEL child INHERITS (parent)") - self.cur.execute("CREATE (:parent {name: 'father'})") - self.cur.execute("CREATE (:child {name: 'son'})") - pass - def test_MultiLable(self): - self.cur.execute("MATCH (x:parent) RETURN x ORDER BY x.name") - x = self.cur.fetchone()[0] - self.assertEquals("father", x.props["name"]) - x = self.cur.fetchone()[0] - self.assertEquals("son", x.props["name"]) - self.cur.execute("MATCH (x:child) RETURN x") - x = self.cur.fetchone()[0] - self.assertEquals("son", x.props["name"]) - -class MatchTest(BasicTest): - def setUp(self): - BasicTest.setUp(self) - self.cur.execute("CREATE VLABEL company") - self.cur.execute("CREATE VLABEL person") - self.cur.execute("CREATE ELABEL employee") - self.cur.execute("CREATE ELABEL manage") - self.cur.execute("CREATE (:company {name: 'bitnine'})" - + "-[:employee]" - + "->(:person {name: 'kskim'})" - + "-[:manage]" - + "->(:person {name: 'ktlee'})"); - self.cur.execute("CREATE (c:company {name: 'bitnine'}) " - + "CREATE (c)-[:employee]" - + "->(:person {name: 'jsyang'})") - self.cur.execute("CREATE (c:company {name: 'bitnine'}) " - + ", (p:person {name: 'ktlee'}) " - + "CREATE (c)-[:employee]->(p)") - self.cur.execute("MATCH (m:person {name: 'kskim'})" - + ", (p:person {name: 'jsyang'}) " - + "CREATE (m)-[:manage]->(p)") - def test_Match(self): - self.cur.execute("MATCH (c)-[e]->(p1)-[m]->(p2) RETURN p1, p2 ORDER BY p2.name") - row = self.cur.fetchone() - boss = row[0] - self.assertEquals("person", boss.label) - self.assertEquals("kskim", boss.props["name"]) - member = row[1] - self.assertEquals("jsyang", member.props["name"]) - row = self.cur.fetchone() - boss = row[0] - self.assertEquals("person", boss.label) - self.assertEquals("kskim", boss.props["name"]) - member = row[1] - self.assertEquals("ktlee", member.props["name"]) - def test_Path(self): - self.cur.execute("MATCH p=()-[]->()-[]->({name:'ktlee'}) RETURN p") - p = self.cur.fetchone()[0] - self.assertEquals('company[3.1]{"name": "bitnine"},employee[5.1][3.1,4.1]{},' - + 'person[4.1]{"name": "kskim"},manage[6.1][4.1,4.2]{},' - + 'person[4.2]{"name": "ktlee"}', - str(p)) - - self.assertEquals(["bitnine","kskim","ktlee"], - [v.props["name"] for v in p.vertices]) - self.assertEquals(["employee","manage"], - [e.label for e in p.edges]) - self.assertEquals(2, p.len()) - -class PropertyTest(BasicTest): - def setUp(self): - BasicTest.setUp(self) - self.cur.execute("CREATE VLABEL company") - self.cur.execute("CREATE VLABEL person") - self.cur.execute("CREATE ELABEL employee") - self.cur.execute("CREATE (:company {name:'bitnine'})" - + "-[:employee {no:1}]" - + "->(:person {name:'jsyang', age:20, height:178.5, married:false})") - self.cur.execute("MATCH (:company {name:'bitnine'})" - + "CREATE (c)-[:employee {no:2}]" - + "->(:person {\"name\":\'ktlee\', \"hobbies\":[\'reading\', \'climbing\'], \"age\":null})") - self.cur.execute("CREATE (:person {name: 'Emil', from: 'Sweden', klout: 99})") - def test_Property(self): - self.cur.execute("MATCH (n)-[:employee {no:1}]->(m) RETURN n, m") - row = self.cur.fetchone() - self.assertEquals(20, row[1].props["age"]) - self.assertEquals(178.5, row[1].props["height"]) - self.assertFalse(row[1].props["married"]) - self.cur.execute("MATCH (n)-[:employee {no:2}]->(m) RETURN n, m") - row = self.cur.fetchone() - self.assertEquals("climbing", row[1].props["hobbies"][1]) - self.cur.execute("MATCH (n)-[{no:2}]->(m) return m.hobbies as hobbies") - hobbies = self.cur.fetchone()[0] - self.assertEquals("reading", hobbies[0]) - self.cur.execute("MATCH (ee:person) WHERE ee.klout = 99 " - + "RETURN ee.name, to_jsonb(ee.name)") - row = self.cur.fetchone() - self.assertIsInstance(row[0], basestring) # if "str", error is occurred instead of "basestring" - self.assertEquals("Emil", row[0]) - self.assertIsInstance(row[1], unicode) - self.assertEquals('Emil', row[1]) - -class ReturnTest(BasicTest): - def test_Return(self): - self.cur.execute("RETURN 'be happy!', 1+1") - row = self.cur.fetchone() - self.assertEquals("be happy!", row[0]) - self.assertEquals(2, row[1]) - -class WhereTest(BasicTest): - def setUp(self): - BasicTest.setUp(self) - self.cur.execute("CREATE VLABEL person") - self.cur.execute("CREATE (:person { name: 'Emil', from: 'Sweden', klout: 99})") - def test_Where(self): - self.cur.execute("MATCH (ee:person) WHERE ee.name = 'Emil' RETURN ee") - row = self.cur.fetchone()[0] - self.assertEquals(99, row.props["klout"]) - self.cur.execute("MATCH (ee:person) WHERE ee.klout = 99 RETURN ee") - self.cur.execute("MATCH (ee:person) WHERE ee.klout = %s RETURN ee", (99,)) - row = self.cur.fetchone()[0] - self.assertEquals(99, row.props["klout"]) - def test_WhereBind(self): - self.cur.execute("MATCH (ee:person) WHERE ee.from = %s RETURN ee", ('Sweden',)) - row = self.cur.fetchone()[0] - self.assertEquals(99, row.props["klout"]) - self.cur.execute("MATCH (ee:person {'klout': %s}) RETURN ee.name", (99,)) - row = self.cur.fetchone()[0] - self.assertIsInstance(row, basestring) - self.assertEquals("Emil", row) - def test_WhereBindJson(self): - self.cur.execute("MATCH (ee:person) WHERE ee.name = %s RETURN ee", ("Emil",)) - row = self.cur.fetchone()[0] - self.assertIsInstance(row, agtype.Vertex) - self.assertEquals("Emil", row.props["name"]) - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_agensgraph.py b/tests/test_agensgraph.py new file mode 100644 index 0000000..ff64eb4 --- /dev/null +++ b/tests/test_agensgraph.py @@ -0,0 +1,128 @@ +''' +Copyright (c) 2014-2017, Bitnine Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + +import os +import unittest + +import psycopg2 + +import agensgraph + +dbname = os.environ.get('AGENSGRAPH_TESTDB', 'agensgraph_test') +dbhost = os.environ.get('AGENSGRAPH_TESTDB_HOST', None) +dbport = os.environ.get('AGENSGRAPH_TESTDB_PORT', None) +dbuser = os.environ.get('AGENSGRAPH_TESTDB_USER', None) +dbpass = os.environ.get('AGENSGRAPH_TESTDB_PASSWORD', None) + +dsn = 'dbname=%s' % dbname +if dbhost is not None: + dsn += ' host=%s' % dbhost +if dbport is not None: + dsn += ' port=%s' % dbport +if dbuser is not None: + dsn += ' user=%s' % dbuser +if dbpass is not None: + dsn += ' password=%s' % dbpass + +class TestConnection(unittest.TestCase): + def setUp(self): + self.conn = psycopg2.connect(dsn) + self.cur = self.conn.cursor() + self.cur.execute('DROP GRAPH IF EXISTS t CASCADE') + self.cur.execute('CREATE GRAPH t') + self.cur.execute('SET graph_path = t') + self.conn.commit() + + def tearDown(self): + self.cur.execute('DROP GRAPH t CASCADE') + self.cur.close() + self.conn.close() + +class TestGraphId(TestConnection): + def test_graphid(self): + self.cur.execute('CREATE (n {}) RETURN id(n)') + self.conn.commit() + + gid0 = self.cur.fetchone()[0] + + self.cur.execute("MATCH (n) WHERE id(n) = %s RETURN id(n)", (gid0,)) + gid1 = self.cur.fetchone()[0] + + self.assertEqual(gid1, gid0) + +class TestVertex(TestConnection): + def test_vertex(self): + self.cur.execute( + "CREATE (n:v {s: '', i: 0, b: false, a: [], o: {}}) RETURN n") + self.conn.commit() + + v = self.cur.fetchone()[0] + self.assertEqual('v', v.label) + self.assertEqual('', v.props['s']) + self.assertEqual(0, v.props['i']) + self.assertFalse(v.props['b']) + self.assertEqual([], v.props['a']) + self.assertEqual({}, v.props['o']) + + self.cur.execute("MATCH (n) WHERE id(n) = %s RETURN count(*)", (v.vid,)) + self.assertEqual(1, self.cur.fetchone()[0]) + +class TestEdge(TestConnection): + def test_edge(self): + self.cur.execute( + "CREATE (n)-[r:e {s: '', i: 0, b: false, a: [], o: {}}]->(m)\n" + 'RETURN n, r, m') + self.conn.commit() + + t = self.cur.fetchone() + v0 = t[0] + e = t[1] + v1 = t[2] + + self.assertEqual('e', e.label) + self.assertEqual(e.start, v0.vid) + self.assertEqual(e.end, v1.vid) + self.assertEqual('', e.props['s']) + self.assertEqual(0, e.props['i']) + self.assertFalse(e.props['b']) + self.assertEqual([], e.props['a']) + self.assertEqual({}, e.props['o']) + + self.cur.execute("MATCH ()-[r]->() WHERE id(r) = %s RETURN count(*)", + (e.eid,)) + self.assertEqual(1, self.cur.fetchone()[0]) + +class TestPath(TestConnection): + def test_path(self): + self.cur.execute("CREATE p=({s: '[}\\\\\"'})-[:e]->() RETURN p") + self.conn.commit() + + p = self.cur.fetchone()[0] + self.assertEqual(1, len(p)) + + for v in p.vertices: + self.cur.execute("MATCH (n) WHERE id(n) = %s RETURN count(*)", + (v.vid,)) + self.assertEqual(1, self.cur.fetchone()[0]) + + for e in p.edges: + self.cur.execute( + "MATCH ()-[r]->() WHERE id(r) = %s RETURN count(*)", + (e.eid,)) + self.assertEqual(1, self.cur.fetchone()[0]) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_edge.py b/tests/test_edge.py new file mode 100644 index 0000000..fa81348 --- /dev/null +++ b/tests/test_edge.py @@ -0,0 +1,57 @@ +''' +Copyright (c) 2014-2017, Bitnine Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + +import unittest + +from psycopg2.extras import json + +from agensgraph._edge import Edge, cast_edge +from agensgraph._graphid import GraphId + +class TestEdge(unittest.TestCase): + def setUp(self): + out = 'e[5.7][7.3,7.9]{"s": "", "i": 0, "b": false, "a": [], "o": {}}' + self.e = cast_edge(out, None) + + def test_label(self): + self.assertEqual('e', self.e.label) + + def test_eid(self): + self.assertEqual(GraphId((5, 7)), self.e.eid) + + def test_start(self): + self.assertEqual(GraphId((7, 3)), self.e.start) + + def test_end(self): + self.assertEqual(GraphId((7, 9)), self.e.end) + + def test_props(self): + self.assertEqual('', self.e.props['s']) + self.assertEqual(0, self.e.props['i']) + self.assertFalse(self.e.props['b']) + self.assertEqual([], self.e.props['a']) + self.assertEqual({}, self.e.props['o']) + + def test_eq(self): + self.assertEqual(self.e, self.e) + + def test_str(self): + props = '{"s": "", "i": 0, "b": false, "a": [], "o": {}}' + out = "e[5.7][7.3,7.9]%s" % json.dumps(json.loads(props)) + self.assertEqual(out, str(self.e)) + + def test_repr(self): + self.assertEqual("%s(%s)" % (Edge.__name__, self.e), repr(self.e)) diff --git a/tests/test_graphid.py b/tests/test_graphid.py new file mode 100644 index 0000000..041dbfb --- /dev/null +++ b/tests/test_graphid.py @@ -0,0 +1,40 @@ +''' +Copyright (c) 2014-2017, Bitnine Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + +import unittest + +from agensgraph._graphid import GraphId, cast_graphid, adapt_graphid + +class TestGraphId(unittest.TestCase): + def setUp(self): + self.out = '7.9' + self.gid = cast_graphid(self.out, None) + + def test_getId(self): + self.assertEqual((7, 9), self.gid.getId()) + + def test_eq(self): + self.assertEqual(self.gid, self.gid) + + def test_str(self): + self.assertEqual(self.out, str(self.gid)) + + def test_repr(self): + self.assertEqual("%s(%s)" % (GraphId.__name__, self.gid), + repr(self.gid)) + + def test_adapt(self): + self.assertEqual(b"'7.9'", adapt_graphid(self.gid).getquoted()) diff --git a/tests/test_graphpath.py b/tests/test_graphpath.py new file mode 100644 index 0000000..0b6534d --- /dev/null +++ b/tests/test_graphpath.py @@ -0,0 +1,56 @@ +''' +Copyright (c) 2014-2017, Bitnine Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + +import unittest + +from psycopg2.extras import json + +from agensgraph._graphpath import Path, cast_graphpath + +class TestPath(unittest.TestCase): + def setUp(self): + self.o0 = '[n[7.3]{}]' + self.p0 = cast_graphpath(self.o0, None) + self.o1 = '[n[7.3]{},r[5.7][7.3,7.9]{},n[7.9]{}]' + self.p1 = cast_graphpath(self.o1, None) + + def test_vertices(self): + self.assertEqual(1, len(self.p0.vertices)) + self.assertEqual('n[7.3]{}', str(self.p0.vertices[0])) + self.assertEqual(2, len(self.p1.vertices)) + self.assertEqual('n[7.3]{}', str(self.p1.vertices[0])) + self.assertEqual('n[7.9]{}', str(self.p1.vertices[1])) + + def test_edges(self): + self.assertEqual(0, len(self.p0.edges)) + self.assertEqual(1, len(self.p1.edges)) + self.assertEqual('r[5.7][7.3,7.9]{}', str(self.p1.edges[0])) + + def test_eq(self): + self.assertEqual(self.p0, self.p0) + self.assertEqual(self.p1, self.p1) + + def test_len(self): + self.assertEqual(0, len(self.p0)) + self.assertEqual(1, len(self.p1)) + + def test_str(self): + self.assertEqual(self.o0, str(self.p0)) + self.assertEqual(self.o1, str(self.p1)) + + def test_repr(self): + self.assertEqual("%s(%s)" % (Path.__name__, self.p0), repr(self.p0)) + self.assertEqual("%s(%s)" % (Path.__name__, self.p1), repr(self.p1)) diff --git a/tests/test_vertex.py b/tests/test_vertex.py new file mode 100644 index 0000000..0926712 --- /dev/null +++ b/tests/test_vertex.py @@ -0,0 +1,51 @@ +''' +Copyright (c) 2014-2017, Bitnine Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + +import unittest + +from psycopg2.extras import json + +from agensgraph._graphid import GraphId +from agensgraph._vertex import Vertex, cast_vertex + +class TestVertex(unittest.TestCase): + def setUp(self): + out = 'v[7.9]{"s": "", "i": 0, "b": false, "a": [], "o": {}}' + self.v = cast_vertex(out, None) + + def test_label(self): + self.assertEqual('v', self.v.label) + + def test_vid(self): + self.assertEqual(GraphId((7, 9)), self.v.vid) + + def test_props(self): + self.assertEqual('', self.v.props['s']) + self.assertEqual(0, self.v.props['i']) + self.assertFalse(self.v.props['b']) + self.assertEqual([], self.v.props['a']) + self.assertEqual({}, self.v.props['o']) + + def test_eq(self): + self.assertEqual(self.v, self.v) + + def test_str(self): + props = '{"s": "", "i": 0, "b": false, "a": [], "o": {}}' + out = "v[7.9]%s" % json.dumps(json.loads(props)) + self.assertEqual(out, str(self.v)) + + def test_repr(self): + self.assertEqual("%s(%s)" % (Vertex.__name__, self.v), repr(self.v))