diff --git a/docs/driver.rst b/docs/driver.rst new file mode 100644 index 0000000000000000000000000000000000000000..81e68906f58f9734034f3972000a3c4d661a3615 --- /dev/null +++ b/docs/driver.rst @@ -0,0 +1,96 @@ +Using the Driver +================ + +At the its simplest, the driver provides the +:py:meth:`open<goblin.driver.connection.Connection.open>` coroutine classmethod, +which returns a :py:class:`Connection<goblin.driver.connection.Connection>` to the +Gremlin Server:: + + >>> import asyncio + >>> from goblin import driver + >>> loop = asyncio.get_event_loop() + >>> conn = await driver.Connection.open('ws://localhost:8182/gremlin', loop) + +The :py:class:`Connection<goblin.driver.connection.Connection>` object can be +used to :py:meth:`submit<goblin.driver.connection.Connection.submit>` messages +to the Gremlin Server. +:py:meth:`submit<goblin.driver.connection.Connection.submit>` returns a +:py:class:`Response<goblin.driver.connection.Response>` object that implements +the PEP 492 asynchronous iterator protocol:: + + >>> resp = await conn.submit(gremlin='1 + 1') + >>> async for msg in resp: + ... print(msg) + >>> await conn.close() # conn also implements async context manager interface + +Connecting to a :py:class:`Cluster<goblin.driver.cluster.Cluster>` +------------------------------------------------------------------ + +To take advantage of the higher level features of the +:py:mod:`driver<goblin.driver>`, :py:mod:`Goblin` provides the +:py:class:`Cluster<goblin.driver.cluster.Cluster>` object. +:py:class:`Cluster<goblin.driver.cluster.Cluster>` is used to create multi-host +clients that leverage connection pooling and sharing. Its interface is based +on the TinkerPop Java driver:: + + >>> cluster = await driver.Cluster.open() # opens a cluster with default config + >>> client = await cluster.connect() + >>> resp = await client.submit(gremlin='1 + 1') # round robin requests to available hosts + >>> async for msg in resp: + ... print(msg) + >>> await cluster.close() # Close all connections to all hosts + +And that is it. While :py:class:`Cluster<goblin.driver.cluster.Cluster>` +is simple to learn and use, it provides a wide variety of configuration options. + +Configuring :py:class:`Cluster<goblin.driver.cluster.Cluster>` +-------------------------------------------------------------- + +Configuration options can be set on +:py:class:`Cluster<goblin.driver.cluster.Cluster>` in one of two ways, either +passed as keyword arguments to +:py:meth:`open<goblin.driver.cluster.Cluster.open>`, or stored in a configuration +file and passed to the :py:meth:`open<goblin.driver.cluster.Cluster.open>` +using the kwarg `configfile`. Configuration files can be either YAML or JSON +format. Currently, :py:class:`Cluster<goblin.driver.cluster.Cluster>` +uses the following configuration: + ++-------------------+----------------------------------------------+-------------+ +|Key |Description |Default | ++===================+==============================================+=============+ +|scheme |URI scheme, typically 'ws' or 'wss' for secure|'ws' | +| |websockets | | ++-------------------+----------------------------------------------+-------------+ +|hosts |A list of hosts the cluster will connect to |['localhost']| ++-------------------+----------------------------------------------+-------------+ +|port |The port of the Gremlin Server to connect to, |8182 | +| |same for all hosts | | ++-------------------+----------------------------------------------+-------------+ +|ssl_certfile |File containing ssl certificate |'' | ++-------------------+----------------------------------------------+-------------+ +|ssl_keyfile |File containing ssl key |'' | ++-------------------+----------------------------------------------+-------------+ +|ssl_password |File containing password for ssl keyfile |'' | ++-------------------+----------------------------------------------+-------------+ +|username |Username for Gremlin Server authentication |'' | ++-------------------+----------------------------------------------+-------------+ +|password |Password for Gremlin Server authentication |'' | ++-------------------+----------------------------------------------+-------------+ +|response_timeout |Timeout for reading responses from the stream |`None` | ++-------------------+----------------------------------------------+-------------+ +|max_conns |The maximum number of connections open at any |4 | +| |time to this host | | ++-------------------+----------------------------------------------+-------------+ +|min_conns |The minimum number of connection open at any |1 | +| |time to this host | | ++-------------------+----------------------------------------------+-------------+ +|max_times_acquired |The maximum number of times a single pool |16 | +| |connection can be acquired and shared | | ++-------------------+----------------------------------------------+-------------+ +|max_inflight |The maximum number of unresolved messages |64 | +| |that may be pending on any one connection | | ++-------------------+----------------------------------------------+-------------+ +|message_serializer |String denoting the class used for message |'classpath' | +| |serialization, currently only supports | | +| |basic GraphSON2MessageSerializer | | ++-------------------+----------------------------------------------+-------------+ diff --git a/docs/glv.rst b/docs/glv.rst new file mode 100644 index 0000000000000000000000000000000000000000..dd50e61aa7e31755f19a2daba444c1c4520b4dac --- /dev/null +++ b/docs/glv.rst @@ -0,0 +1,95 @@ +Using AsyncGraph (GLV) +====================== + +:py:mod:`Goblin` provides an asynchronous version of the gremlin-python +Gremlin Language Variant (GLV) that is bundled with Apache TinkerPop beginning +with the 3.2.2 release. Traversal are generated using the class +:py:class:`AsyncGraph<goblin.driver.graph.AsyncGraph>` combined with a remote +connection class, either :py:class:`Connection<goblin.driver.connection.Connection>` or +:py:class:`DriverRemoteConnection<goblin.driver.connection.DriverRemoteConnection>`:: + + >>> import asyncio + >>> from goblin import driver + + >>> loop = asyncio.get_event_loop() + >>> remote_conn = loop.run_until_complete( + ... driver.Connection.open( + ... "http://localhost:8182/gremlin", loop)) + >>> graph = driver.AsyncGraph() + >>> g = graph.traversal().withRemote(remote_conn) + +Once you have a traversal source, it's all Gremlin...:: + + >>> traversal = g.addV('query_language').property('name', 'gremlin') + +`traversal` is in an instance of +:py:class:`AsyncGraphTraversal<goblin.driver.graph.AsyncGraphTraversal>`, which +implements the Python 3.5 asynchronous iterator protocol:: + + >>> async def iterate_traversal(traversal): + >>> async for msg in traversal: + >>> print(msg) + + >>> loop.run_until_complete(iterate_traversal(traversal)) + # v[0] + +:py:class:`AsyncGraphTraversal<goblin.driver.graph.AsyncGraphTraversal>` also +provides several convenience methods to help iterate over results: + +- :py:meth:`next<goblin.driver.graph.AsyncGraphTraversal.next>` +- :py:meth:`toList<goblin.driver.graph.AsyncGraphTraversal.toList>` +- :py:meth:`toSet<goblin.driver.graph.AsyncGraphTraversal.toSet>` +- :py:meth:`oneOrNone<goblin.driver.graph.AsyncGraphTraversal.oneOrNone>` + +Notice the mixedCase? Not very pythonic? Well no, but it maintains continuity +with the Gremlin query language, and that's what the GLV is all about... + +Note: Gremlin steps that are reserved words in Python, like `or`, `in`, use a +a trailing underscore `or_` and `in_`. + +The Side Effect Interface +------------------------- + +When using TinkerPop 3.2.2+ with the default +:py:class:`GraphSON2MessageSerializer<goblin.driver.serializer.GraphSON2MessageSerializer>`, +:py:mod:`Goblin` provides an asynchronous side effects interface using the +:py:class:`AsyncRemoteTraversalSideEffects<goblin.driver.graph.AsyncRemoteTraversalSideEffects>` +class. This allows side effects to be retrieved after executing the traversal:: + + >>> traversal = g.V().aggregate('a') + >>> results = loop.run_until_complete(traversal.toList()) + >>> print(results) + # [v[0]] + +Calling +:py:meth:`keys<goblin.driver.graph.AsyncRemoteTraversalSideEffects.keys>` +will then return an asynchronous iterator containing all keys for cached +side effects: + + >>> async def get_side_effect_keys(traversal): + ... resp = await traversal.side_effects.keys() + ... async for key in resp: + ... print(key) + + + >>> loop.run_until_complete(get_side_effect_keys(traversal)) + # 'a' + +Then calling +:py:meth:`get<goblin.driver.graph.AsyncRemoteTraversalSideEffects.get>` +using a valid key will return the cached side effects:: + + >>> async def get_side_effects(traversal): + ... resp = await traversal.side_effects.get('a') + ... async for side_effect in resp: + ... print(side_effect) + + + >>> loop.run_until_complete(get_side_effects(traversal)) + # v[0] + +And that's it! For more information on Gremlin Language Variants, please +visit the `Apache TinkerPop GLV Documentation`_. + + +.. _Apache TinkerPop GLV Documentation: http://tinkerpop.apache.org/docs/3.2.2/tutorials/gremlin-language-variants/ diff --git a/docs/goblin.driver.rst b/docs/goblin.driver.rst index 9f34e04d1e97bba5b794912841e825e629bc820d..776807e89d5fccf56106292e722c4293c8e23851 100644 --- a/docs/goblin.driver.rst +++ b/docs/goblin.driver.rst @@ -4,10 +4,18 @@ goblin.driver package Submodules ---------- -goblin.driver.api module ------------------------- +goblin.driver.client module +--------------------------- -.. automodule:: goblin.driver.api +.. automodule:: goblin.driver.client + :members: + :undoc-members: + :show-inheritance: + +goblin.driver.cluster module +---------------------------- + +.. automodule:: goblin.driver.cluster :members: :undoc-members: :show-inheritance: @@ -27,3 +35,36 @@ goblin.driver.graph module :members: :undoc-members: :show-inheritance: + +goblin.driver.pool module +------------------------- + +.. automodule:: goblin.driver.pool + :members: + :undoc-members: + :show-inheritance: + +goblin.driver.serializer module +------------------------------- + +.. automodule:: goblin.driver.serializer + :members: + :undoc-members: + :show-inheritance: + +goblin.driver.server module +--------------------------- + +.. automodule:: goblin.driver.server + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: goblin.driver + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/goblin.rst b/docs/goblin.rst index ce1353713835fe65807a24dc49d6b69396b48daa..da9f726d77954985d490dfa8e22eb5134fa7d9a9 100644 --- a/docs/goblin.rst +++ b/docs/goblin.rst @@ -27,6 +27,14 @@ goblin.app module :undoc-members: :show-inheritance: +goblin.cardinality module +------------------------- + +.. automodule:: goblin.cardinality + :members: + :undoc-members: + :show-inheritance: + goblin.element module --------------------- @@ -43,7 +51,13 @@ goblin.exception module :undoc-members: :show-inheritance: +goblin.manager module +--------------------- +.. automodule:: goblin.manager + :members: + :undoc-members: + :show-inheritance: goblin.mapper module -------------------- @@ -69,10 +83,10 @@ goblin.session module :undoc-members: :show-inheritance: -goblin.traversal module ------------------------ +Module contents +--------------- -.. automodule:: goblin.traversal +.. automodule:: goblin :members: :undoc-members: :show-inheritance: diff --git a/docs/index.rst b/docs/index.rst index c02ca08b41e4dd877a61c7be0560cd47d7664494..1704c1732e48d19d3f558566c03999a406e2e75f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -46,9 +46,10 @@ Submit scripts and bindings to the `Gremlin Server`_:: >>> async def go(loop): ... script = "g.addV('developer').property(k1, v1)" ... bindings = {'k1': 'name', 'v1': 'Leif'} - ... conn = await driver.GremlinServer.open('ws://localhost:8182/', loop) + ... conn = await driver.Connection.open( + ... 'ws://localhost:8182/gremlin', loop) ... async with conn: - ... resp = await conn.submit(script, bindings=bindings) + ... resp = await conn.submit(gremlin=script, bindings=bindings) ... async for msg in resp: ... print(msg) @@ -56,29 +57,33 @@ Submit scripts and bindings to the `Gremlin Server`_:: >>> loop.run_until_complete(go(loop)) # {'type': 'vertex', 'id': 0, 'label': 'developer', 'properties': {'name': [{'id': 1, 'value': 'Leif'}]}} +For more information on using the driver, see the :doc:`Driver docs</driver>` -**AsyncRemoteGraph** +**AsyncGraph** Generate and submit Gremlin traversals in native Python:: - >>> from gremlin_python import process + >>> remote_conn = loop.run_until_complete( + ... driver.Connection.open( + ... "http://localhost:8182/gremlin", loop)) + >>> graph = driver.AsyncGraph() + >>> g = graph.traversal().withRemote(remote_conn) - >>> connection = loop.run_until_complete( - ... driver.GremlinServer.open("http://localhost:8182/", loop)) - >>> translator = process.GroovyTranslator('g') - >>> graph = driver.AsyncRemoteGraph(translator, connection) - >>> async def go(graph): - ... g = graph.traversal() - ... resp = await g.addV('developer').property('name', 'Leif').next() - ... async for msg in resp: + >>> async def go(g): + ... traversal = g.addV('developer').property('name', 'Leif') + ... async for msg in traversal: ... print(msg) - ... await graph.close() + ... await remote_conn.close() - >>> loop.run_until_complete(go(graph)) + + >>> loop.run_until_complete(go(g)) # {'properties': {'name': [{'value': 'Leif', 'id': 3}]}, 'label': 'developer', 'id': 2, 'type': 'vertex'} +For more information on using the :py:class:`goblin.driver.graph.AsyncGraph<AsyncGraph>`, +see the :doc:`GLV docs</glv>` + **OGM** @@ -100,11 +105,11 @@ Define custom vertex/edge classes using the provided base :py:mod:`classes<gobli Create a :py:class:`Goblin App<goblin.app.Goblin>` and register the element classes:: - >>> from goblin import create_app + >>> from goblin import Goblin >>> app = loop.run_until_complete( - ... create_app('ws://localhost:8182/', loop)) + ... Goblin.open(loop)) >>> app.register(Person, Knows) @@ -114,20 +119,19 @@ database:: >>> async def go(app): ... session = await app.session() - ... async with session: - ... leif = Person() - ... leif.name = 'Leif' - ... leif.age = 28 - ... jon = Person() - ... jon.name = 'Jonathan' - ... works_with = Knows(leif, jon) - ... session.add(leif, jon, works_with) - ... await session.flush() - ... result = await session.g.E(works_with.id).one_or_none() - ... assert result is works_with - ... people = await session.traversal(Person).all() # element class based traversal source - ... async for person in people: - ... print(person) + ... leif = Person() + ... leif.name = 'Leif' + ... leif.age = 28 + ... jon = Person() + ... jon.name = 'Jonathan' + ... works_with = Knows(leif, jon) + ... session.add(leif, jon, works_with) + ... await session.flush() + ... result = await session.g.E(works_with.id).oneOrNone() + ... assert result is works_with + ... people = session.traversal(Person) # element class based traversal source + ... async for person in people: + ... print(person) >>> loop.run_until_complete(go(app)) @@ -142,12 +146,31 @@ for an element, that element will be updated to reflect these changes. For more information on using the OGM, see the :doc:`OGM docs</ogm>` +A note about GraphSON message serialization +------------------------------------------- + +The :py:mod:`goblin.driver` provides support for both GraphSON2 and GraphSON1 +out of the box. By default, it uses the +:py:class:`GraphSON2MessageSerializer<goblin.driver.serializer.GraphSON2MessageSerializer>`. +Since GraphSON2 was only recently included in the TinkerPop 3.2.2 release, +:py:mod:`goblin.driver` also ships with +:py:class:`GraphSONMessageSerializer<goblin.driver.serializer.GraphSONMessageSerializer>`. +In the near future (when projects like Titan and DSE support the 3.2 Gremlin +Server line), support for GraphsSON1 will be dropped. + +The :py:mod:`goblin<Goblin>` OGM still uses GraphSON1 by default and will do so +until :py:mod:`goblin.driver` support is dropped. It will then be updated to +use GraphSON2. + + Contents: .. toctree:: :maxdepth: 4 ogm + glv + driver modules diff --git a/docs/modules.rst b/docs/modules.rst index 7eee4a9125181e7a3461d44fd9750e2cea9fe686..8b7f906082ce89a2a791cbf0277f2ef8ecd04c83 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -1,5 +1,5 @@ -Goblin API -========== +goblin +====== .. toctree:: :maxdepth: 4 diff --git a/docs/ogm.rst b/docs/ogm.rst index 6729ba87feeab7ac6b72658b3b635d58cc320096..cf1b228eab111ae126ab57dc03fe52a1b220744a 100644 --- a/docs/ogm.rst +++ b/docs/ogm.rst @@ -240,10 +240,10 @@ must be wrapped in coroutines and ran using the :py:class:`asyncio.BaseEventLoop but, for convenience, they are shown as if they were run in a Python interpreter. To use a :py:class:`Session<goblin.session.Session>`, first create a :py:class:`Goblin App <goblin.app.Goblin>` using -:py:func:`create_app<goblin.app.create_app>`, then register the defined element +:py:meth:`Goblin.open<goblin.app.Goblin.open>`, then register the defined element classes:: - >>> app = await goblin.create_app('ws://localhost:8182/', loop) + >>> app = await goblin.Goblin.open(loop) >>> app.register(Person, City, BornIn) >>> session = await app.session() @@ -317,14 +317,21 @@ the value to be specified:: >>> traversal = session.traversal(Person) >>> traversal.has(bindprop(Person, 'name', 'Leifur', binding='v1')) -Finally, to submit a traversal, :py:mod:`Goblin` provides two methods: -:py:meth:`all` and :py:meth:`one_or_none`. :py:meth:`all` returns all results -produced by the traversal, while :py:meth:`one_or_none` returns either the last -result, or in the case that the traversal did not return results, `None`. Remember -to `await` the traversal when calling these methods:: +Finally, there are a variety of ways to to submit a traversal to the server. +First of all, all traversals are themselve asynchronous iterators, and using +them as such will cause a traversal to be sent on the wire: + + >>> async for msg in session.g.V().hasLabel('person'): + ... print(msg) + +Furthermore, :py:mod:`Goblin` provides several convenience methods that +submit a traversal as well as process the results :py:meth:`toList`, +:py:meth:`toSet` and :py:meth:`oneOrNone`. These methods both submit a script +to the server and iterate over the results. Remember to `await` the traversal +when calling these methods:: >>> traversal = session.traversal(Person) >>> leif = await traversal.has( - ... bindprop(Person, 'name', 'Leifur', binding='v1')).one_or_none() + ... bindprop(Person, 'name', 'Leifur', binding='v1')).oneOrNone() And that is pretty much it. We hope you enjoy the :py:mod:`Goblin` OGM. diff --git a/goblin/__init__.py b/goblin/__init__.py index f77f95dbe45954f3faa95a77461e1f44efd0d1e8..2127055309e8a5f26dc2443b231e5dc794a1d0a4 100644 --- a/goblin/__init__.py +++ b/goblin/__init__.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU Affero General Public License # along with Goblin. If not, see <http://www.gnu.org/licenses/>. -from goblin.app import create_app, Goblin +from goblin.app import Goblin from goblin.cardinality import Cardinality from goblin.element import Vertex, Edge, VertexProperty from goblin.properties import Property, String, Integer, Float, Boolean diff --git a/goblin/app.py b/goblin/app.py index 83cbf36e402f19ddfa8168ca63e38f937e942d66..c8c56a3453645db9b34c206b6bfa3849fe1fc507 100644 --- a/goblin/app.py +++ b/goblin/app.py @@ -27,46 +27,6 @@ from goblin import driver, element, session logger = logging.getLogger(__name__) -async def create_app(url, loop, get_hashable_id=None, **config): - """ - Constructor function for :py:class:`Goblin`. Connect to database and - build a dictionary of relevant vendor implmentation features. - - :param str url: Database url - :param asyncio.BaseEventLoop loop: Event loop implementation - :param dict config: Config parameters for application - - :returns: :py:class:`Goblin` object - """ - - features = {} - async with await driver.GremlinServer.open(url, loop) as conn: - # Propbably just use a parser to parse the whole feature list - aliases = config.get('aliases', {}) - stream = await conn.submit( - 'graph.features().graph().supportsComputer()', aliases=aliases) - msg = await stream.fetch_data() - features['computer'] = msg - stream = await conn.submit( - 'graph.features().graph().supportsTransactions()', aliases=aliases) - msg = await stream.fetch_data() - features['transactions'] = msg - stream = await conn.submit( - 'graph.features().graph().supportsPersistence()', aliases=aliases) - msg = await stream.fetch_data() - features['persistence'] = msg - stream = await conn.submit( - 'graph.features().graph().supportsConcurrentAccess()', aliases=aliases) - msg = await stream.fetch_data() - features['concurrent_access'] = msg - stream = await conn.submit( - 'graph.features().graph().supportsThreadedTransactions()', aliases=aliases) - msg = await stream.fetch_data() - features['threaded_transactions'] = msg - return Goblin(url, loop, get_hashable_id=get_hashable_id, - features=features, **config) - - # Main API classes class Goblin: """ @@ -80,23 +40,35 @@ class Goblin: :param dict config: Config parameters for application """ - DEFAULT_CONFIG = { - 'translator': process.GroovyTranslator('g') - } - - def __init__(self, url, loop, *, get_hashable_id=None, features=None, - **config): - self._url = url - self._loop = loop - self._features = features - self._config = self.DEFAULT_CONFIG - self._config.update(config) + def __init__(self, cluster, *, get_hashable_id=None, aliases=None): + self._cluster = cluster + self._loop = self._cluster._loop + self._transactions = None + self._cluster = cluster self._vertices = collections.defaultdict( lambda: element.GenericVertex) self._edges = collections.defaultdict(lambda: element.GenericEdge) if not get_hashable_id: get_hashable_id = lambda x: x self._get_hashable_id = get_hashable_id + if aliases is None: + aliases = {} + self._aliases = aliases + + @classmethod + async def open(cls, loop, *, get_hashable_id=None, aliases=None, **config): + # App currently only supports GraphSON 1 + cluster = await driver.Cluster.open( + loop, aliases=aliases, + message_serializer=driver.GraphSONMessageSerializer, + **config) + app = Goblin(cluster, get_hashable_id=get_hashable_id, aliases=aliases) + await app.supports_transactions() + return app + + @property + def config(self): + return self._cluster.config @property def vertices(self): @@ -108,24 +80,6 @@ class Goblin: """Registered edge classes""" return self._edges - @property - def features(self): - """Vendor specific database implementation features""" - return self._features - - def from_file(filepath): - """Load config from filepath. Not implemented""" - raise NotImplementedError - - def from_obj(obj): - """Load config from object. Not implemented""" - raise NotImplementedError - - @property - def translator(self): - """gremlin-python translator class""" - return self._config['translator'] - @property def url(self): """Database url""" @@ -143,7 +97,30 @@ class Goblin: if element.__type__ == 'edge': self._edges[element.__label__] = element - async def session(self, *, use_session=False): + def config_from_file(self, filename): + """ + Load configuration from from file. + + :param str filename: Path to the configuration file. + """ + self._cluster.config_from_file(filename) + + def config_from_yaml(self, filename): + self._cluster.config_from_yaml(filename) + + def config_from_json(self, filename): + """ + Load configuration from from JSON file. + + :param str filename: Path to the configuration file. + """ + self._cluster.config_from_json(filename) + + def register_from_module(self, modulename): + raise NotImplementedError + + async def session(self, *, use_session=False, processor='', op='eval', + aliases=None): """ Create a session object. @@ -151,10 +128,26 @@ class Goblin: :returns: :py:class:`Session<goblin.session.Session>` object """ - aliases = self._config.get('aliases', None) - conn = await driver.GremlinServer.open(self.url, self._loop) + conn = await self._cluster.connect(processor=processor, op=op, + aliases=aliases) + transactions = await self.supports_transactions() return session.Session(self, conn, self._get_hashable_id, - use_session=use_session, - aliases=aliases) + transactions, + use_session=use_session) + + async def supports_transactions(self): + if self._transactions is None: + conn = await self._cluster.get_connection() + stream = await conn.submit( + gremlin='graph.features().graph().supportsTransactions()', + aliases=self._aliases) + msg = await stream.fetch_data() + msg = msg.object + stream.close() + self._transactions = msg + return self._transactions + + async def close(self): + await self._cluster.close() diff --git a/goblin/driver/__init__.py b/goblin/driver/__init__.py index 0e790b6aa15251126a5e0e7ef5e1e0735f90dfef..f60c40ba52da3f22a2c584e4528530f732588036 100644 --- a/goblin/driver/__init__.py +++ b/goblin/driver/__init__.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU Affero General Public License # along with Goblin. If not, see <http://www.gnu.org/licenses/>. -from goblin.driver.api import GremlinServer -from goblin.driver.connection import AbstractConnection -from goblin.driver.graph import AsyncRemoteGraph +from goblin.driver.cluster import Cluster +from goblin.driver.client import Client, SessionedClient +from goblin.driver.connection import AbstractConnection, Connection +from goblin.driver.graph import AsyncGraph +from goblin.driver.serializer import ( + GraphSONMessageSerializer, GraphSON2MessageSerializer) +from goblin.driver.server import GremlinServer diff --git a/goblin/driver/api.py b/goblin/driver/api.py deleted file mode 100644 index 717af379c6ab7382d93c31173ce95073f74498e1..0000000000000000000000000000000000000000 --- a/goblin/driver/api.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2016 ZEROFAIL -# -# This file is part of Goblin. -# -# Goblin is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Goblin is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with Goblin. If not, see <http://www.gnu.org/licenses/>. - -import asyncio -import aiohttp - -from goblin.driver import connection - - -class GremlinServer: - """Factory class that generates connections to the Gremlin Server""" - - @classmethod - async def open(cls, - url, - loop, - *, - client_session=None, - username=None, - password=None): - """ - Open a connection to the Gremlin Server. - - :param str url: Database url - :param asyncio.BaseEventLoop loop: Event loop implementation - :param aiohttp.client.ClientSession client_session: Client session - used to generate websocket connections. - :param str username: Username for server auth - :param str password: Password for server auth - - :returns: :py:class:`Connection<goblin.driver.connection.Connection>` - """ - if client_session is None: - client_session = aiohttp.ClientSession(loop=loop) - ws = await client_session.ws_connect(url) - return connection.Connection(url, ws, loop, client_session, - username=username, password=password) diff --git a/goblin/driver/client.py b/goblin/driver/client.py new file mode 100644 index 0000000000000000000000000000000000000000..b00e51f291afaee75b485fab5dacfb1b98a3738d --- /dev/null +++ b/goblin/driver/client.py @@ -0,0 +1,111 @@ +# Copyright 2016 ZEROFAIL +# +# This file is part of Goblin. +# +# Goblin is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Goblin is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Goblin. If not, see <http://www.gnu.org/licenses/>. + +from goblin import exception + + +class Client: + """ + Client that utilizes a :py:class:`Cluster<goblin.driver.cluster.Cluster>` + to access a cluster of Gremlin Server hosts. Issues requests to hosts using + a round robin strategy. + + :param goblin.driver.cluster.Cluster cluster: Cluster used by + client + :param asyncio.BaseEventLoop loop: + """ + def __init__(self, cluster, loop, *, aliases=None, processor=None, + op=None): + self._cluster = cluster + self._loop = loop + if aliases is None: + aliases ={} + self._aliases = aliases + if processor is None: + processor = '' + self._processor = processor + if op is None: + op = 'eval' + self._op = op + + @property + def message_serializer(self): + return self.cluster.config['message_serializer'] + + @property + def cluster(self): + """ + Readonly property. + + :returns: The instance of + :py:class:`Cluster<goblin.driver.cluster.Cluster>` associated with + client. + """ + return self._cluster + + def alias(self, aliases): + client = Client(self._cluster, self._loop, + aliases=aliases) + return client + + async def submit(self, + *, + processor=None, + op=None, + **args): + """ + **coroutine** Submit a script and bindings to the Gremlin Server. + + :param str processor: Gremlin Server processor argument + :param str op: Gremlin Server op argument + :param args: Keyword arguments for Gremlin Server. Depend on processor + and op. + :returns: :py:class:`Response` object + """ + processor = processor or self._processor + op = op or self._op + # Certain traversal processor ops don't support this arg + if not args.get('aliases') and op not in ['keys', 'close', + 'authentication']: + args['aliases'] = self._aliases + conn = await self.cluster.get_connection() + resp = await conn.submit( + processor=processor, op=op, **args) + self._loop.create_task(conn.release_task(resp)) + return resp + + +class SessionedClient(Client): + + def __init__(self, cluster, loop, session, *, aliases=None): + super().__init__(cluster, loop, aliases=aliases, processor='session', + op='eval') + self._session = session + + @property + def session(self): + return self._session + + async def submit(self, **args): + if not args.get('gremlin', ''): + raise exception.ClientError('Session requires a gremlin string') + return await super().submit(processor='session', op='eval', + session=self.session, + **args) + + async def close(self): + raise NotImplementedError diff --git a/goblin/driver/cluster.py b/goblin/driver/cluster.py new file mode 100644 index 0000000000000000000000000000000000000000..5e0024a3697a52512632f4bf978900c754610876 --- /dev/null +++ b/goblin/driver/cluster.py @@ -0,0 +1,194 @@ +# Copyright 2016 ZEROFAIL +# +# This file is part of Goblin. +# +# Goblin is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Goblin is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Goblin. If not, see <http://www.gnu.org/licenses/>. + +import asyncio +import collections +import configparser +import json +import ssl + +import yaml + +from goblin import driver, exception + + +def my_import(name): + components = name.split('.') + mod = __import__(components[0]) + for comp in components[1:]: + mod = getattr(mod, comp) + return mod + + +class Cluster: + """ + A cluster of Gremlin Server hosts. This object provides the main high + level interface used by the :py:mod:`goblin.driver` module. + + :param asyncio.BaseEventLoop loop: + """ + + DEFAULT_CONFIG = { + 'scheme': 'ws', + 'hosts': ['localhost'], + 'port': 8182, + 'ssl_certfile': '', + 'ssl_keyfile': '', + 'ssl_password': '', + 'username': '', + 'password': '', + 'response_timeout': None, + 'max_conns': 4, + 'min_conns': 1, + 'max_times_acquired': 16, + 'max_inflight': 64, + 'message_serializer': 'goblin.driver.GraphSON2MessageSerializer' + } + + def __init__(self, loop, aliases=None, **config): + self._loop = loop + self._config = self._get_message_serializer(dict(self.DEFAULT_CONFIG)) + self._config.update(config) + self._hosts = collections.deque() + self._closed = False + if aliases is None: + aliases = {} + self._aliases = aliases + + @classmethod + async def open(cls, loop, *, aliases=None, configfile=None, **config): + """ + **coroutine** Open a cluster, connecting to all available hosts as + specified in configuration. + + :param asyncio.BaseEventLoop loop: + :param str configfile: Optional configuration file in .json or + .yml format + """ + cluster = cls(loop, aliases=aliases, **config) + if configfile: + cluster.config_from_file(configfile) + await cluster.establish_hosts() + return cluster + + @property + def hosts(self): + return self._hosts + + @property + def config(self): + """ + Readonly property. + + :returns: `dict` containing the cluster configuration + """ + return self._config + + async def get_connection(self): + """ + **coroutine** Get connection from next available host in a round robin + fashion. + + :returns: :py:class:`Connection<goblin.driver.connection.Connection>` + """ + if not self._hosts: + await self.establish_hosts() + host = self._hosts.popleft() + conn = await host.get_connection() + self._hosts.append(host) + return conn + + async def establish_hosts(self): + """ + **coroutine** Connect to all hosts as specified in configuration. + """ + scheme = self._config['scheme'] + hosts = self._config['hosts'] + port = self._config['port'] + for host in hosts: + url = '{}://{}:{}/gremlin'.format(scheme, host, port) + host = await driver.GremlinServer.open( + url, self._loop, **dict(self._config)) + self._hosts.append(host) + + def config_from_file(self, filename): + """ + Load configuration from from file. + + :param str filename: Path to the configuration file. + """ + if filename.endswith('yml') or filename.endswith('yaml'): + self.config_from_yaml(filename) + elif filename.endswith('.json'): + self.config_from_json(filename) + else: + raise exception.ConfigurationError('Unknown config file format') + + def config_from_yaml(self, filename): + with open(filename, 'r') as f: + config = yaml.load(f) + config = self._get_message_serializer(config) + self._config.update(config) + + def config_from_json(self, filename): + """ + Load configuration from from JSON file. + + :param str filename: Path to the configuration file. + """ + with open(filename, 'r') as f: + config = json.load(f) + config = self._get_message_serializer(config) + self.config.update(config) + + def _get_message_serializer(self, config): + message_serializer = config.get('message_serializer', '') + if message_serializer: + config['message_serializer'] = my_import(message_serializer) + return config + + def config_from_module(self, filename): + raise NotImplementedError + + async def connect(self, processor=None, op=None, aliases=None, + session=None): + """ + **coroutine** Get a connected client. Main API method. + + :returns: A connected instance of `Client<goblin.driver.client.Client>` + """ + aliases = aliases or self._aliases + if not self._hosts: + await self.establish_hosts() + if session: + host = self._hosts.popleft() + client = driver.SessionedClient(host, self._loop, session, + aliases=aliases) + self._hosts.append(host) + else: + client = driver.Client(self, self._loop, processor=processor, + op=op, aliases=aliases) + return client + + async def close(self): + """**coroutine** Close cluster and all connected hosts.""" + waiters = [] + while self._hosts: + host = self._hosts.popleft() + waiters.append(host.close()) + await asyncio.gather(*waiters) + self._closed = True diff --git a/goblin/driver/connection.py b/goblin/driver/connection.py index 7c783d6e4a1362ef1b5414564516b32909e1469f..1b63c42707e98a6f5db172287039ed28e10fe684 100644 --- a/goblin/driver/connection.py +++ b/goblin/driver/connection.py @@ -26,6 +26,7 @@ import uuid import aiohttp from goblin import exception +from goblin.driver import serializer logger = logging.getLogger(__name__) @@ -41,7 +42,8 @@ def error_handler(fn): async def wrapper(self): msg = await fn(self) if msg: - if msg.status_code not in [200, 206, 204]: + if msg.status_code not in [200, 206]: + self.close() raise exception.GremlinServerError( "{0}: {1}".format(msg.status_code, msg.message)) msg = msg.data @@ -51,10 +53,25 @@ def error_handler(fn): class Response: """Gremlin Server response implementated as an async iterator.""" - def __init__(self, response_queue, loop): + def __init__(self, response_queue, request_id, timeout, loop): self._response_queue = response_queue + self._request_id = request_id self._loop = loop - self._done = False + self._timeout = timeout + self._done = asyncio.Event(loop=self._loop) + + @property + def request_id(self): + return self._request_id + + @property + def done(self): + """ + Readonly property. + + :returns: `asyncio.Event` object + """ + return self._done async def __aiter__(self): return self @@ -62,18 +79,30 @@ class Response: async def __anext__(self): msg = await self.fetch_data() if msg: - return msg + return msg.object else: raise StopAsyncIteration + def close(self): + """Close response stream by setting done flag to true.""" + self.done.set() + self._loop = None + self._response_queue = None + @error_handler async def fetch_data(self): """Get a single message from the response stream""" - if self._done: + if self.done.is_set(): return None - msg = await self._response_queue.get() + try: + msg = await asyncio.wait_for(self._response_queue.get(), + timeout=self._timeout, + loop=self._loop) + except asyncio.TimeoutError: + self.close() + raise exception.ResponseTimeoutError('Response timed out') if msg is None: - self._done = True + self.close() return msg @@ -92,167 +121,169 @@ class Connection(AbstractConnection): """ Main classd for interacting with the Gremlin Server. Encapsulates a websocket connection. Not instantiated directly. Instead use - :py:meth:`GremlinServer.open<goblin.driver.api.GremlinServer.open>`. + :py:meth:`Connection.open<goblin.driver.connection.Connection.open>`. + + :param str url: url for host Gremlin Server + :param aiohttp.ClientWebSocketResponse ws: open websocket connection + :param asyncio.BaseEventLoop loop: + :param aiohttp.ClientSession: Client session used to establish websocket + connections + :param float response_timeout: (optional) `None` by default + :param str username: Username for database auth + :param str password: Password for database auth + :param int max_inflight: Maximum number of unprocessed requests at any + one time on the connection """ - def __init__(self, url, ws, loop, conn_factory, *, username=None, - password=None): + def __init__(self, url, ws, loop, client_session, username, password, + max_inflight, response_timeout, message_serializer): self._url = url self._ws = ws self._loop = loop - self._conn_factory = conn_factory + self._client_session = client_session + self._response_timeout = response_timeout self._username = username self._password = password self._closed = False self._response_queues = {} + self._receive_task = self._loop.create_task(self._receive()) + self._semaphore = asyncio.Semaphore(value=max_inflight, + loop=self._loop) + if isinstance(message_serializer, type): + message_serializer = message_serializer() + self._message_serializer = message_serializer + + @classmethod + async def open(cls, url, loop, *, ssl_context=None, username='', + password='', max_inflight=64, response_timeout=None, + message_serializer=serializer.GraphSON2MessageSerializer): + """ + **coroutine** Open a connection to the Gremlin Server. + + :param str url: url for host Gremlin Server + :param asyncio.BaseEventLoop loop: + :param ssl.SSLContext ssl_context: + :param str username: Username for database auth + :param str password: Password for database auth + + :param int max_inflight: Maximum number of unprocessed requests at any + one time on the connection + :param float response_timeout: (optional) `None` by default + + :returns: :py:class:`Connection<goblin.driver.connection.Connection>` + """ + connector = aiohttp.TCPConnector(ssl_context=ssl_context, loop=loop) + client_session = aiohttp.ClientSession(loop=loop, connector=connector) + ws = await client_session.ws_connect(url) + return cls(url, ws, loop, client_session, username, password, + max_inflight, response_timeout, message_serializer) @property - def response_queues(self): - return self._response_queues + def message_serializer(self): + return self._message_serializer @property def closed(self): - return self._closed + """ + Check if connection has been closed. + + :returns: `bool` + """ + return self._closed or self._ws.closed @property def url(self): + """ + Readonly property. + + :returns: str The url association with this connection. + """ return self._url async def submit(self, - gremlin, - *, - bindings=None, - lang='gremlin-groovy', - aliases=None, - op="eval", - processor="", - session=None, - request_id=None): + *, + processor='', + op='eval', + **args): """ Submit a script and bindings to the Gremlin Server - :param str gremlin: Gremlin script to submit to server. - :param dict bindings: A mapping of bindings for Gremlin script. - :param str lang: Language of scripts submitted to the server. - "gremlin-groovy" by default - :param dict aliases: Rebind ``Graph`` and ``TraversalSource`` - objects to different variable names in the current request - :param str op: Gremlin Server op argument. "eval" by default. - :param str processor: Gremlin Server processor argument. "" by default. - :param str session: Session id (optional). Typically a uuid - :param str request_id: Request id (optional). Typically a uuid - + :param str processor: Gremlin Server processor argument + :param str op: Gremlin Server op argument + :param args: Keyword arguments for Gremlin Server. Depend on processor + and op. :returns: :py:class:`Response` object """ - if aliases is None: - aliases = {} - if request_id is None: - request_id = str(uuid.uuid4()) - message = self._prepare_message(gremlin, - bindings, - lang, - aliases, - op, - processor, - session, - request_id) + await self._semaphore.acquire() + request_id = str(uuid.uuid4()) + message = self._message_serializer.serialize_message( + request_id, processor, op, **args) response_queue = asyncio.Queue(loop=self._loop) - self.response_queues[request_id] = response_queue + self._response_queues[request_id] = response_queue if self._ws.closed: - self._ws = await self.conn_factory.ws_connect(self.url) + self._ws = await self.client_session.ws_connect(self.url) self._ws.send_bytes(message) - self._loop.create_task(self._receive()) - return Response(response_queue, self._loop) + resp = Response(response_queue, request_id, self._response_timeout, self._loop) + self._loop.create_task(self._terminate_response(resp, request_id)) + return resp + + def _authenticate(self, username, password, session): + auth = b''.join([b'\x00', username.encode('utf-8'), + b'\x00', password.encode('utf-8')]) + request_id = str(uuid.uuid4()) + args = {'sasl': base64.b64encode(auth).decode()} + message = self._message_serializer.serialize_message( + request_id, '', 'authentication', **args) + self._ws.send_bytes(message, binary=True) async def close(self): - """Close underlying connection and mark as closed.""" + """**coroutine** Close underlying connection and mark as closed.""" + self._receive_task.cancel() await self._ws.close() self._closed = True - await self._conn_factory.close() - - def _prepare_message(self, gremlin, bindings, lang, aliases, op, - processor, session, request_id): - message = { - "requestId": request_id, - "op": op, - "processor": processor, - "args": { - "gremlin": gremlin, - "bindings": bindings, - "language": lang, - "aliases": aliases - } - } - message = self._finalize_message(message, processor, session) - return message - - def _authenticate(self, username, password, processor, session): - auth = b"".join([b"\x00", username.encode("utf-8"), - b"\x00", password.encode("utf-8")]) - message = { - "requestId": str(uuid.uuid4()), - "op": "authentication", - "processor": "", - "args": { - "sasl": base64.b64encode(auth).decode() - } - } - message = self._finalize_message(message, processor, session) - self._ws.submit(message, binary=True) - - def _finalize_message(self, message, processor, session): - if processor == "session": - if session is None: - raise RuntimeError("session processor requires a session id") - else: - message["args"].update({"session": session}) - message = json.dumps(message) - return self._set_message_header(message, "application/json") - - @staticmethod - def _set_message_header(message, mime_type): - if mime_type == "application/json": - mime_len = b"\x10" - mime_type = b"application/json" - else: - raise ValueError("Unknown mime type.") - return b"".join([mime_len, mime_type, message.encode("utf-8")]) + await self._client_session.close() + + async def _terminate_response(self, resp, request_id): + await resp.done.wait() + del self._response_queues[request_id] + self._semaphore.release() async def _receive(self): - data = await self._ws.receive() - if data.tp == aiohttp.MsgType.close: - await self._ws.close() - elif data.tp == aiohttp.MsgType.error: - raise data.data - elif data.tp == aiohttp.MsgType.closed: - pass - else: - if data.tp == aiohttp.MsgType.binary: - data = data.data.decode() - elif data.tp == aiohttp.MsgType.text: - data = data.strip() - message = json.loads(data) - request_id = message['requestId'] - status_code = message['status']['code'] - data = message["result"]["data"] - msg = message["status"]["message"] - response_queue = self._response_queues[request_id] - if status_code == 407: - await self._authenticate(self._username, self._password, - self._processor, self._session) - self._loop.create_task(self._receive()) + while True: + data = await self._ws.receive() + if data.tp == aiohttp.MsgType.close: + await self._ws.close() + elif data.tp == aiohttp.MsgType.error: + raise data.data + elif data.tp == aiohttp.MsgType.closed: + pass else: - if data: - for result in data: - message = Message(status_code, result, msg) - response_queue.put_nowait(message) - else: - message = Message(status_code, data, msg) - response_queue.put_nowait(message) - if status_code == 206: - self._loop.create_task(self._receive()) - else: + if data.tp == aiohttp.MsgType.binary: + data = data.data.decode() + elif data.tp == aiohttp.MsgType.text: + data = data.strip() + message = json.loads(data) + request_id = message['requestId'] + status_code = message['status']['code'] + data = message['result']['data'] + msg = message['status']['message'] + response_queue = self._response_queues[request_id] + if status_code == 407: + await self._authenticate(self._username, self._password, + self._processor) + elif status_code == 204: response_queue.put_nowait(None) - del self._response_queues[request_id] + else: + if data: + for result in data: + result = self._message_serializer.deserialize_message(result) + message = Message(status_code, result, msg) + response_queue.put_nowait(message) + else: + data = self._message_serializer.deserialize_message(data) + message = Message(status_code, data, msg) + response_queue.put_nowait(message) + if status_code != 206: + response_queue.put_nowait(None) async def __aenter__(self): return self @@ -260,3 +291,6 @@ class Connection(AbstractConnection): async def __aexit__(self, exc_type, exc, tb): await self.close() self._conn = None + + +DriverRemoteConnection = Connection diff --git a/goblin/driver/graph.py b/goblin/driver/graph.py index 4d19a6d7c1808fe7bcaa14af22ae78f81ddde2f1..4720b8ddcb7bf9b1e3d986b9498e27d943130b1c 100644 --- a/goblin/driver/graph.py +++ b/goblin/driver/graph.py @@ -17,75 +17,151 @@ """A temporary solution to allow integration with gremlin_python package.""" +import functools + from gremlin_python.process.graph_traversal import ( - GraphTraversalSource, GraphTraversal) -from gremlin_python.process.traversal import ( - TraversalStrategy, TraversalStrategies) + GraphTraversal, GraphTraversalSource) +from gremlin_python.process.traversal import TraversalStrategies +from gremlin_python.driver.remote_connection import ( + RemoteStrategy, RemoteTraversalSideEffects) +from gremlin_python.structure.graph import Graph +from goblin.driver.serializer import GraphSON2MessageSerializer -class AsyncGraphTraversal(GraphTraversal): - def __init__(self, graph, traversal_strategies, bytecode): - GraphTraversal.__init__(self, graph, traversal_strategies, bytecode) +class AsyncRemoteTraversalSideEffects(RemoteTraversalSideEffects): - def __repr__(self): - return self.graph.translator.translate(self.bytecode) + async def keys(self): + return await self.keys_lambda() - def toList(self): - raise NotImplementedError - - def toSet(self): - raise NotImplementedError + async def get(self, key): + return await self.value_lambda(sideEffectKey=key) - async def next(self): - resp = await self.traversal_strategies.apply(self) - return resp +class AsyncRemoteStrategy(RemoteStrategy): -class AsyncRemoteStrategy(TraversalStrategy): async def apply(self, traversal): - result = await traversal.graph.remote_connection.submit( - traversal.graph.translator.translate(traversal.bytecode), - bindings=traversal.bindings, - lang=traversal.graph.translator.target_language) + if isinstance(self.remote_connection.message_serializer, + GraphSON2MessageSerializer): + processor = 'traversal' + op = 'bytecode' + side_effects = AsyncRemoteTraversalSideEffects + else: + processor = '' + op = 'eval' + side_effects = None + if traversal.traversers is None: + resp = await self.remote_connection.submit( + gremlin=traversal.bytecode, processor=processor, op=op) + traversal.traversers = resp + if side_effects: + keys_lambda = functools.partial(self.remote_connection.submit, + processor='traversal', + op='keys', + sideEffect=resp.request_id) + value_lambda = functools.partial(self.remote_connection.submit, + processor='traversal', + op='gather', + sideEffect=resp.request_id) + side_effects = side_effects(keys_lambda, value_lambda) + traversal.side_effects = side_effects + + + +class AsyncGraphTraversal(GraphTraversal): + + async def __aiter__(self): + return self + + async def __anext__(self): + if self.traversers is None: + await self._get_traversers() + if self.last_traverser is None: + self.last_traverser = await self.traversers.fetch_data() + if self.last_traverser is None: + raise StopAsyncIteration + obj = self.last_traverser.object + self.last_traverser.bulk = self.last_traverser.bulk - 1 + if self.last_traverser.bulk <= 0: + self.last_traverser = None + return obj + + async def _get_traversers(self): + for ts in self.traversal_strategies.traversal_strategies: + await ts.apply(self) + + async def next(self, amount=None): + """ + **coroutine** Return the next result from the iterator. + + :param int amount: The number of results returned, defaults to None + (1 result) + """ + if amount is None: + try: + return await self.__anext__() + except StopAsyncIteration: + pass + else: + count = 0 + tempList = [] + while count < amount: + count = count + 1 + try: temp = await self.__anext__() + except StopIteration: return tempList + tempList.append(temp) + return tempList + + async def toList(self): + """**coroutine** Submit the travesal, iterate results, return a list""" + results = [] + async for msg in self: + results.append(msg) + return results + + async def toSet(self): + """**coroutine** Submit the travesal, iterate results, return a set""" + results = set() + async for msg in self: + results.add(msg) + return results + + async def oneOrNone(self): + """ + **coroutine** Get one or zero results from a traveral. Returns last + iterated result. + """ + result = None + async for msg in self: + result = msg return result + def iterate(self): + raise NotImplementedError -class AsyncGraph: - def traversal(self): - return GraphTraversalSource(self, self.traversal_strategy, - graph_traversal=self.graph_traversal) - - -class AsyncRemoteGraph(AsyncGraph): - """ - Generate asynchronous gremlin traversals using native Python. - - :param gremlin_python.process.GroovyTranslator translator: - gremlin_python translator class, typically - :py:class:`GroovyTranslator<gremlin_python.process.GroovyTranslator>` - :param goblin.driver.connection connection: underlying remote - connection - :param gremlin_python.process.GraphTraversal graph_traversal: - Custom graph traversal class - """ - def __init__(self, translator, remote_connection, *, graph_traversal=None): - self.traversal_strategy = AsyncRemoteStrategy() # A single traversal strategy - self.translator = translator - self.remote_connection = remote_connection - if graph_traversal is None: - graph_traversal = AsyncGraphTraversal - self.graph_traversal = graph_traversal + def nextTraverser(self): + raise NotImplementedError - def __repr__(self): - return "remotegraph[" + self.remote_connection.url + "]" - async def close(self): - """Close underlying remote connection""" - await self.remote_connection.close() - self.remote_connection = None +class AsyncGraph(Graph): + """Generate asynchronous gremlin traversals using native Python""" - async def __aenter__(self): - return self + def traversal(self, *, graph_traversal=None, remote_strategy=None): + """ + Get a traversal source from the Graph + + :param gremlin_python.process.GraphTraversal graph_traversal: + Custom graph traversal class + :param gremlin_python.driver.remote_connection.RemoteStrategy remote_strategy: + Custom remote strategy class - async def __aexit__(self, exc_type, exc, tb): - await self.close() + :returns: + :py:class:`gremlin_python.process.graph_traversal.GraphTraversalSource` + """ + if graph_traversal is None: + graph_traversal = AsyncGraphTraversal + if remote_strategy is None: + remote_strategy = AsyncRemoteStrategy + return GraphTraversalSource( + self, TraversalStrategies.global_cache[self.__class__], + remote_strategy=remote_strategy, + graph_traversal=graph_traversal) diff --git a/goblin/driver/pool.py b/goblin/driver/pool.py new file mode 100644 index 0000000000000000000000000000000000000000..3df37dedf0ee5a3194ae2986989eed3db11b445c --- /dev/null +++ b/goblin/driver/pool.py @@ -0,0 +1,224 @@ +# Copyright 2016 ZEROFAIL +# +# This file is part of Goblin. +# +# Goblin is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Goblin is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Goblin. If not, see <http://www.gnu.org/licenses/>. + +import asyncio +import collections + +import aiohttp + +from goblin.driver import connection + + +class PooledConnection: + """ + Wrapper for :py:class:`Connection<goblin.driver.connection.Connection>` + that helps manage tomfoolery associated with connection pooling. + + :param goblin.driver.connection.Connection conn: + :param goblin.driver.pool.ConnectionPool pool: + """ + def __init__(self, conn, pool): + self._conn = conn + self._pool = pool + self._times_acquired = 0 + + @property + def times_acquired(self): + """ + Readonly property. + + :returns: int + """ + return self._times_acquired + + def increment_acquired(self): + """Increment times acquired attribute by 1""" + self._times_acquired += 1 + + def decrement_acquired(self): + """Decrement times acquired attribute by 1""" + self._times_acquired -= 1 + + async def submit(self, + *, + processor='', + op='eval', + **args): + """ + **coroutine** Submit a script and bindings to the Gremlin Server + + :param str processor: Gremlin Server processor argument + :param str op: Gremlin Server op argument + :param args: Keyword arguments for Gremlin Server. Depend on processor + and op. + + :returns: :py:class:`Response` object + """ + return await self._conn.submit(processor=processor, op=op, **args) + + async def release_task(self, resp): + await resp.done.wait() + self.release() + + def release(self): + self._pool.release(self) + + async def close(self): + """Close underlying connection""" + await self._conn.close() + self._conn = None + self._pool = None + + @property + def closed(self): + """ + Readonly property. + + :returns: bool + """ + return self._conn.closed + + +class ConnectionPool: + """ + A pool of connections to a Gremlin Server host. + + :param str url: url for host Gremlin Server + :param asyncio.BaseEventLoop loop: + :param ssl.SSLContext ssl_context: + :param str username: Username for database auth + :param str password: Password for database auth + :param float response_timeout: (optional) `None` by default + :param int max_conns: Maximum number of conns to a host + :param int min_connsd: Minimum number of conns to a host + :param int max_times_acquired: Maximum number of times a conn can be + shared by multiple coroutines (clients) + :param int max_inflight: Maximum number of unprocessed requests at any + one time on the connection + """ + + def __init__(self, url, loop, ssl_context, username, password, max_conns, + min_conns, max_times_acquired, max_inflight, response_timeout, + message_serializer): + self._url = url + self._loop = loop + self._ssl_context = ssl_context + self._username = username + self._password = password + self._max_conns = max_conns + self._min_conns = min_conns + self._max_times_acquired = max_times_acquired + self._max_inflight = max_inflight + self._response_timeout = response_timeout + self._message_serializer = message_serializer + self._condition = asyncio.Condition(loop=self._loop) + self._available = collections.deque() + self._acquired = collections.deque() + + @property + def url(self): + """ + Readonly property. + + :returns: str + """ + return self._url + + async def init_pool(self): + """**coroutine** Open minumum number of connections to host""" + for i in range(self._min_conns): + conn = await self._get_connection(self._username, + self._password, + self._max_inflight, + self._response_timeout, + self._message_serializer) + self._available.append(conn) + + def release(self, conn): + """ + Release connection back to pool after use. + + :param PooledConnection conn: + """ + if conn.closed: + self._acquired.remove(conn) + else: + conn.decrement_acquired() + if not conn.times_acquired: + self._acquired.remove(conn) + self._available.append(conn) + self._loop.create_task(self._notify()) + + async def _notify(self): + async with self._condition: + self._condition.notify() + + async def acquire(self, username=None, password=None, max_inflight=None, + response_timeout=None, message_serializer=None): + """**coroutine** Acquire a new connection from the pool.""" + username = username or self._username + password = password or self._password + response_timeout = response_timeout or self._response_timeout + max_inflight = max_inflight or self._max_inflight + message_serializer = message_serializer or self._message_serializer + async with self._condition: + while True: + while self._available: + conn = self._available.popleft() + if not conn.closed: + conn.increment_acquired() + self._acquired.append(conn) + return conn + if len(self._acquired) < self._max_conns: + conn = await self._get_connection(username, password, + max_inflight, + response_timeout, + message_serializer) + conn.increment_acquired() + self._acquired.append(conn) + return conn + else: + for x in range(len(self._acquired)): + conn = self._acquired.popleft() + if conn.times_acquired < self._max_times_acquired: + conn.increment_acquired() + self._acquired.append(conn) + return conn + self._acquired.append(conn) + else: + await self._condition.wait() + + async def close(self): + """**coroutine** Close connection pool.""" + waiters = [] + while self._available: + conn = self._available.popleft() + waiters.append(conn.close()) + while self._acquired: + conn = self._acquired.popleft() + waiters.append(conn.close()) + await asyncio.gather(*waiters) + + async def _get_connection(self, username, password, max_inflight, + response_timeout, message_serializer): + conn = await connection.Connection.open( + self._url, self._loop, ssl_context=self._ssl_context, + username=username, password=password, + response_timeout=response_timeout, + message_serializer=message_serializer) + conn = PooledConnection(conn, self) + return conn diff --git a/goblin/driver/serializer.py b/goblin/driver/serializer.py new file mode 100644 index 0000000000000000000000000000000000000000..d39a759e6aafd6f94d17641d04528fc79ac49c3c --- /dev/null +++ b/goblin/driver/serializer.py @@ -0,0 +1,148 @@ +# Copyright 2016 ZEROFAIL +# +# This file is part of Goblin. +# +# Goblin is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Goblin is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Goblin. If not, see <http://www.gnu.org/licenses/>. + +import json + +from gremlin_python.process.traversal import Bytecode, Traverser +from gremlin_python.process.translator import GroovyTranslator +from gremlin_python.structure.io.graphson import GraphSONWriter, GraphSONReader + + +class Processor: + """Base class for OpProcessor serialization system.""" + + def get_op(self, op): + op = getattr(self, op, None) + if not op: + raise Exception("Processor does not support op") + return op + + +class GraphSONMessageSerializer: + """Message serializer for GraphSONv1""" + # processors and ops + class standard(Processor): + + def authentication(self, args): + return args + + def eval(self, args): + gremlin = args['gremlin'] + if isinstance(gremlin, Bytecode): + translator = GroovyTranslator('g') + args['gremlin'] = translator.translate(gremlin) + args['bindings'] = gremlin.bindings + return args + + + class session(standard): + pass + + + def get_processor(self, processor): + processor = getattr(self, processor, None) + if not processor: + raise Exception("Unknown processor") + return processor() + + def serialize_message(self, request_id, processor, op, **args): + if not processor: + processor_obj = self.get_processor('standard') + else: + processor_obj = self.get_processor(processor) + op_method = processor_obj.get_op(op) + args = op_method(args) + message = self.build_message(request_id, processor, op, args) + return message + + def build_message(self, request_id, processor, op, args): + message = { + 'requestId': request_id, + 'processor': processor, + 'op': op, + 'args': args + } + return self.finalize_message(message, b'\x10', b'application/json') + + def finalize_message(self, message, mime_len, mime_type): + message = json.dumps(message) + message = b''.join([mime_len, mime_type, message.encode('utf-8')]) + return message + + def deserialize_message(self, message): + return Traverser(message) + + +class GraphSON2MessageSerializer(GraphSONMessageSerializer): + """Message serializer for GraphSONv2""" + + class session(GraphSONMessageSerializer.session): + + def close(self, args): + return args + + + class traversal(Processor): + + def authentication(self, args): + return args + + def bytecode(self, args): + gremlin = args['gremlin'] + args['gremlin'] = GraphSONWriter.writeObject(gremlin) + aliases = args.get('aliases', '') + if not aliases: + aliases = {'g': 'g'} + args['aliases'] = aliases + return args + + def close(self, args): + return self.keys(args) + + def gather(self, args): + side_effect = args['sideEffect'] + args['sideEffect'] = {'@type': 'g:UUID', '@value': side_effect} + aliases = args.get('aliases', '') + if not aliases: + aliases = {'g': 'g'} + args['aliases'] = aliases + return args + + def keys(self, args): + side_effect = args['sideEffect'] + args['sideEffect'] = {'@type': 'g:UUID', '@value': side_effect} + return args + + def build_message(self, request_id, processor, op, args): + message = { + 'requestId': {'@type': 'g:UUID', '@value': request_id}, + 'processor': processor, + 'op': op, + 'args': args + } + return self.finalize_message(message, b"\x21", + b"application/vnd.gremlin-v2.0+json") + + def deserialize_message(self, message): + if isinstance(message, dict): + if message.get('@type', '') == 'g:Traverser': + obj = GraphSONReader._objectify(message) + else: + obj = Traverser(message.get('@value', message)) + else: + obj = Traverser(message) + return obj diff --git a/goblin/driver/server.py b/goblin/driver/server.py new file mode 100644 index 0000000000000000000000000000000000000000..3ba5a24b1fd0763b11cbbdfbad2b2e7a57f1425e --- /dev/null +++ b/goblin/driver/server.py @@ -0,0 +1,113 @@ +# Copyright 2016 ZEROFAIL +# +# This file is part of Goblin. +# +# Goblin is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Goblin is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Goblin. If not, see <http://www.gnu.org/licenses/>. + +from goblin.driver import pool + + +class GremlinServer: + """ + Class that wraps a connection pool. Currently doesn't do much, but may + be useful in the future.... + + :param pool.ConnectionPool pool: + """ + + def __init__(self, url, loop, **config): + self._pool = None + self._url = url + self._loop = loop + self._response_timeout = config['response_timeout'] + self._username = config['username'] + self._password = config['password'] + self._max_times_acquired = config['max_times_acquired'] + self._max_conns = config['max_conns'] + self._min_conns = config['min_conns'] + self._max_inflight = config['max_inflight'] + self._message_serializer = config['message_serializer'] + scheme = config['scheme'] + if scheme in ['https', 'wss']: + certfile = config['ssl_certfile'] + keyfile = config['ssl_keyfile'] + ssl_password = config['ssl_password'] + ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + ssl_context.load_cert_chain( + certfile, keyfile=keyfile, password=ssl_password) + self._ssl_context = ssl_context + else: + self._ssl_context = None + + @property + def url(self): + return self._url + + @property + def pool(self): + """ + Readonly property. + + :returns: :py:class:`ConnectionPool<goblin.driver.pool.ConnectionPool>` + """ + if self._pool: + return self._pool + + async def close(self): + """**coroutine** Close underlying connection pool.""" + if self._pool: + await self._pool.close() + self._pool = None + + async def get_connection(self): + """**coroutine** Acquire a connection from the pool.""" + try: + conn = await self._pool.acquire() + except AttributeError: + raise Exception("Please initialize pool") + return conn + + async def initialize(self): + conn_pool = pool.ConnectionPool( + self._url, self._loop, self._ssl_context, self._username, + self._password, self._max_conns, self._min_conns, + self._max_times_acquired, self._max_inflight, + self._response_timeout, self._message_serializer) + await conn_pool.init_pool() + self._pool = conn_pool + + @classmethod + async def open(cls, url, loop, **config): + """ + **coroutine** Establish connection pool and host to Gremlin Server. + + :param str url: url for host Gremlin Server + :param asyncio.BaseEventLoop loop: + :param ssl.SSLContext ssl_context: + :param str username: Username for database auth + :param str password: Password for database auth + :param float response_timeout: (optional) `None` by default + :param int max_conns: Maximum number of conns to a host + :param int min_connsd: Minimum number of conns to a host + :param int max_times_acquired: Maximum number of times a conn can be + shared by multiple coroutines (clients) + :param int max_inflight: Maximum number of unprocessed requests at any + one time on the connection + + :returns: :py:class:`GremlinServer` + """ + + host = cls(url, loop, **config) + await host.initialize() + return host diff --git a/goblin/exception.py b/goblin/exception.py index 642b7df97cfea30cee2854683f780661d4be43cc..203102b9e8db4217ac6903fc63ccb9492402b011 100644 --- a/goblin/exception.py +++ b/goblin/exception.py @@ -15,6 +15,11 @@ # You should have received a copy of the GNU Affero General Public License # along with Goblin. If not, see <http://www.gnu.org/licenses/>. + +class ClientError(Exception): + pass + + class MappingError(Exception): pass @@ -27,5 +32,13 @@ class ElementError(Exception): pass +class ConfigurationError(Exception): + pass + + class GremlinServerError(Exception): pass + + +class ResponseTimeoutError(Exception): + pass diff --git a/goblin/session.py b/goblin/session.py index 744aa80c80f576d6300ac3e9dd02568b6af13bb7..6c32ce583f7484d8415bda49273b9a1f75beb9aa 100644 --- a/goblin/session.py +++ b/goblin/session.py @@ -22,14 +22,77 @@ import collections import logging import weakref -from goblin import exception, mapper, traversal +from goblin import cardinality, exception, mapper from goblin.driver import connection, graph from goblin.element import GenericVertex +from gremlin_python.driver.remote_connection import RemoteStrategy +from gremlin_python.process.traversal import Cardinality, Traverser + + logger = logging.getLogger(__name__) +def bindprop(element_class, ogm_name, val, *, binding=None): + """ + Helper function for binding ogm properties/values to corresponding db + properties/values for traversals. + + :param goblin.element.Element element_class: User defined element class + :param str ogm_name: Name of property as defined in the ogm + :param val: The property value + :param str binding: The binding for val (optional) + + :returns: tuple object ('db_property_name', ('binding(if passed)', val)) + """ + db_name = getattr(element_class, ogm_name, ogm_name) + _, data_type = element_class.__mapping__.ogm_properties[ogm_name] + val = data_type.to_db(val) + if binding: + val = (binding, val) + return db_name, val + + +class TraversalResponse: + """Asynchronous iterator that encapsulates a traversal response queue""" + def __init__(self, response_queue, request_id): + self._queue = response_queue + self._request_id = request_id + self._done = False + + @property + def request_id(self): + return self._request_id + + async def __aiter__(self): + return self + + async def __anext__(self): + if self._done: + return + msg = await self.fetch_data() + if msg: + return msg + else: + self._done = True + raise StopAsyncIteration + + async def fetch_data(self): + return await self._queue.get() + + +class GoblinAsyncRemoteStrategy(RemoteStrategy): + + async def apply(self, traversal): + + if traversal.traversers is None: + resp = await self.remote_connection.submit( + gremlin=traversal.bytecode, processor='', op='eval') + traversal.traversers = resp + traversal.side_effects = None + + class Session(connection.AbstractConnection): """ Provides the main API for interacting with the database. Does not @@ -41,20 +104,24 @@ class Session(connection.AbstractConnection): :param bool use_session: Support for Gremlin Server session. Not implemented """ - def __init__(self, app, conn, get_hashable_id, *, use_session=False, - aliases=None): + def __init__(self, app, conn, get_hashable_id, transactions, *, + use_session=False): self._app = app self._conn = conn self._loop = self._app._loop self._use_session = False - self._aliases = aliases or dict() self._pending = collections.deque() self._current = weakref.WeakValueDictionary() self._get_hashable_id = get_hashable_id - remote_graph = graph.AsyncRemoteGraph( - self._app.translator, self, - graph_traversal=traversal.GoblinTraversal) - self._traversal_factory = traversal.TraversalFactory(remote_graph) + self._graph = graph.AsyncGraph() + + @property + def graph(self): + return self._graph + + @property + def message_serializer(self): + return self.conn.message_serializer @property def app(self): @@ -64,10 +131,6 @@ class Session(connection.AbstractConnection): def conn(self): return self._conn - @property - def traversal_factory(self): - return self._traversal_factory - @property def current(self): return self._current @@ -76,14 +139,12 @@ class Session(connection.AbstractConnection): return self async def __aexit__(self, exc_type, exc, tb): - await self.close() + self.close() - async def close(self): + def close(self): """ - Close the underlying db connection and disconnect session from Goblin - application. """ - await self.conn.close() + self._conn = None self._app = None # Traversal API @@ -96,44 +157,59 @@ class Session(connection.AbstractConnection): :py:class:`goblin.gremlin_python.process.GraphTraversalSource` object """ - return self.traversal_factory.traversal() + return self.traversal() - def traversal(self, element_class): + @property + def _g(self): + """ + Traversal source for internal use. Uses undelying conn. Doesn't + trigger complex deserailization. """ - Get a traversal spawned from an element class. + return self.graph.traversal( + graph_traversal=graph.AsyncGraphTraversal, + remote_strategy=GoblinAsyncRemoteStrategy).withRemote(self.conn) - :param :goblin.element.Element element_class: Element class - used to spawn traversal. + def traversal(self, element_class=None): + """ + Generate a traversal using a user defined element class as a + starting point. - :returns: :py:class:`GoblinTraversal<goblin.traversal.GoblinTraversal>` - object + :param goblin.element.Element element_class: An optional element + class that will dictate the element type (vertex/edge) as well as + the label for the traversal source + + :returns: :py:class:`AsyncGraphTraversal` """ - return self.traversal_factory.traversal(element_class=element_class) + traversal = self.graph.traversal( + graph_traversal=graph.AsyncGraphTraversal, + remote_strategy=GoblinAsyncRemoteStrategy).withRemote(self) + if element_class: + label = element_class.__mapping__.label + if element_class.__type__ == 'vertex': + traversal = traversal.V() + if element_class.__type__ == 'edge': + traversal = traversal.E() + traversal = traversal.hasLabel(label) + return traversal async def submit(self, - gremlin, - *, - bindings=None, - lang='gremlin-groovy'): + **args): """ Submit a query to the Gremiln Server. :param str gremlin: Gremlin script to submit to server. :param dict bindings: A mapping of bindings for Gremlin script. - :param str lang: Language of scripts submitted to the server. - "gremlin-groovy" by default :returns: :py:class:`TraversalResponse<goblin.traversal.TraversalResponse>` object """ await self.flush() - async_iter = await self.conn.submit( - gremlin, bindings=bindings, lang=lang, aliases=self._aliases) + async_iter = await self.conn.submit(**args) response_queue = asyncio.Queue(loop=self._loop) self._loop.create_task( self._receive(async_iter, response_queue)) - return traversal.TraversalResponse(response_queue) + return TraversalResponse(response_queue, async_iter.request_id) async def _receive(self, async_iter, response_queue): async for result in async_iter: @@ -155,15 +231,16 @@ class Session(connection.AbstractConnection): current.source = GenericVertex() current.target = GenericVertex() element = current.__mapping__.mapper_func(result, current) - return element + return Traverser(element, 1) else: for key in result: result[key] = self._deserialize_result(result[key]) - return result + return Traverser(result, 1) elif isinstance(result, list): - return [self._deserialize_result(item) for item in result] + result = [self._deserialize_result(item) for item in result] + return Traverser(result, 1) else: - return result + return Traverser(result, 1) # Creation API def add(self, *elements): @@ -190,7 +267,7 @@ class Session(connection.AbstractConnection): :param goblin.element.Vertex vertex: Vertex to be removed """ - traversal = self.traversal_factory.remove_vertex(vertex) + traversal = self._g.V(vertex.id).drop() result = await self._simple_traversal(traversal, vertex) hashable_id = self._get_hashable_id(vertex.id) vertex = self.current.pop(hashable_id) @@ -203,7 +280,7 @@ class Session(connection.AbstractConnection): :param goblin.element.Edge edge: Element to be removed """ - traversal = self.traversal_factory.remove_edge(edge) + traversal = self._g.E(edge.id).drop() result = await self._simple_traversal(traversal, edge) hashable_id = self._get_hashable_id(edge.id) edge = self.current.pop(hashable_id) @@ -270,8 +347,7 @@ class Session(connection.AbstractConnection): :returns: :py:class:`Vertex<goblin.element.Vertex>` | None """ - return await self.traversal_factory.get_vertex_by_id( - vertex).one_or_none() + return await self.g.V(vertex.id).oneOrNone() async def get_edge(self, edge): """ @@ -281,8 +357,7 @@ class Session(connection.AbstractConnection): :returns: :py:class:`Edge<goblin.element.Edge>` | None """ - return await self.traversal_factory.get_edge_by_id( - edge).one_or_none() + return await self.g.E(edge.id).oneOrNone() async def update_vertex(self, vertex): """ @@ -294,7 +369,7 @@ class Session(connection.AbstractConnection): """ props = mapper.map_props_to_db(vertex, vertex.__mapping__) # vert_props = mapper.map_vert_props_to_db - traversal = self.g.V(vertex.id) + traversal = self._g.V(vertex.id) return await self._update_vertex_properties(vertex, traversal, props) async def update_edge(self, edge): @@ -306,7 +381,7 @@ class Session(connection.AbstractConnection): :returns: :py:class:`Edge<goblin.element.Edge>` object """ props = mapper.map_props_to_db(edge, edge.__mapping__) - traversal = self.g.E(edge.id) + traversal = self._g.E(edge.id) return await self._update_edge_properties(edge, traversal, props) # Transaction support @@ -314,13 +389,10 @@ class Session(connection.AbstractConnection): """Not implemented""" raise NotImplementedError - def _wrap_in_tx(self): - raise NotImplementedError - async def commit(self): """Not implemented""" await self.flush() - if self.engine._features['transactions'] and self._use_session(): + if self.transactions and self._use_session(): await self.tx() raise NotImplementedError @@ -329,13 +401,10 @@ class Session(connection.AbstractConnection): # *metodos especiales privados for creation API async def _simple_traversal(self, traversal, element): - stream = await self.conn.submit( - repr(traversal), bindings=traversal.bindings, - aliases=self._aliases) - msg = await stream.fetch_data() + msg = await traversal.oneOrNone() if msg: msg = element.__mapping__.mapper_func(msg, element) - return msg + return msg async def _save_element(self, elem, @@ -352,62 +421,57 @@ class Session(connection.AbstractConnection): result = await create_func(elem) return result - async def _add_vertex(self, elem): + async def _add_vertex(self, vertex): """Convenience function for generating crud traversals.""" - props = mapper.map_props_to_db(elem, elem.__mapping__) - traversal = self.g.addV(elem.__mapping__.label) - traversal, _, metaprops = self.traversal_factory.add_properties( - traversal, props) - result = await self._simple_traversal(traversal, elem) + props = mapper.map_props_to_db(vertex, vertex.__mapping__) + traversal = self._g.addV(vertex.__mapping__.label) + traversal, _, metaprops = self._add_properties(traversal, props) + result = await self._simple_traversal(traversal, vertex) if metaprops: await self._add_metaprops(result, metaprops) - traversal = self.traversal_factory.get_vertex_by_id(elem) - result = await self._simple_traversal(traversal, elem) + traversal = self._g.V(vertex.id) + result = await self._simple_traversal(traversal, vertex) return result - async def _add_edge(self, elem): + async def _add_edge(self, edge): """Convenience function for generating crud traversals.""" - props = mapper.map_props_to_db(elem, elem.__mapping__) - traversal = self.g.V(elem.source.id) - traversal = traversal.addE(elem.__mapping__._label) + props = mapper.map_props_to_db(edge, edge.__mapping__) + traversal = self._g.V(edge.source.id) + traversal = traversal.addE(edge.__mapping__._label) traversal = traversal.to( - self.g.V(elem.target.id)) - traversal, _, _ = self.traversal_factory.add_properties( + self._g.V(edge.target.id)) + traversal, _, _ = self._add_properties( traversal, props) - return await self._simple_traversal(traversal, elem) + return await self._simple_traversal(traversal, edge) async def _check_vertex(self, vertex): """Used to check for existence, does not update session vertex""" - traversal = self.g.V(vertex.id) - stream = await self.conn.submit(repr(traversal), aliases=self._aliases) - return await stream.fetch_data() + msg = await self._g.V(vertex.id).oneOrNone() + return msg async def _check_edge(self, edge): """Used to check for existence, does not update session edge""" - traversal = self.g.E(edge.id) - stream = await self.conn.submit(repr(traversal), aliases=self._aliases) - return await stream.fetch_data() + msg = await self._g.E(edge.id).oneOrNone() + return msg async def _update_vertex_properties(self, vertex, traversal, props): - traversal, removals, metaprops = self.traversal_factory.add_properties( - traversal, props) + traversal, removals, metaprops = self._add_properties(traversal, props) for k in removals: - await self.g.V(vertex.id).properties(k).drop().one_or_none() + await self._g.V(vertex.id).properties(k).drop().oneOrNone() result = await self._simple_traversal(traversal, vertex) if metaprops: removals = await self._add_metaprops(result, metaprops) for db_name, key, value in removals: - await self.g.V(vertex.id).properties( - db_name).has(key, value).drop().one_or_none() - traversal = self.traversal_factory.get_vertex_by_id(vertex) + await self._g.V(vertex.id).properties( + db_name).has(key, value).drop().oneOrNone() + traversal = self._g.V(vertex.id) result = await self._simple_traversal(traversal, vertex) return result async def _update_edge_properties(self, edge, traversal, props): - traversal, removals, _ = self.traversal_factory.add_properties( - traversal, props) + traversal, removals, _ = self._add_properties(traversal, props) for k in removals: - await self.g.E(edge.id).properties(k).drop().one_or_none() + await self._g.E(edge.id).properties(k).drop().oneOrNone() return await self._simple_traversal(traversal, edge) async def _add_metaprops(self, result, metaprops): @@ -416,12 +480,35 @@ class Session(connection.AbstractConnection): db_name, (binding, value), metaprops = metaprop for key, val in metaprops.items(): if val: - traversal = self.g.V(result.id).properties( + traversal = self._g.V(result.id).properties( db_name).hasValue(value).property(key, val) - stream = await self.conn.submit( - repr(traversal), bindings=traversal.bindings, - aliases=self._aliases) - await stream.fetch_data() + await traversal.oneOrNone() else: potential_removals.append((db_name, key, value)) return potential_removals + + def _add_properties(self, traversal, props): + binding = 0 + potential_removals = [] + potential_metaprops = [] + for card, db_name, val, metaprops in props: + if val: + key = ('k' + str(binding), db_name) + val = ('v' + str(binding), val) + if card: + # Maybe use a dict here as a translator + if card == cardinality.Cardinality.list: + card = Cardinality.list + elif card == cardinality.Cardinality.set: + card = Cardinality.set + else: + card = Cardinality.single + traversal = traversal.property(card, key, val) + else: + traversal = traversal.property(key, val) + binding += 1 + if metaprops: + potential_metaprops.append((db_name, val, metaprops)) + else: + potential_removals.append(db_name) + return traversal, potential_removals, potential_metaprops diff --git a/goblin/traversal.py b/goblin/traversal.py deleted file mode 100644 index f516a65c55276e12163257628e20ba97a8d39c62..0000000000000000000000000000000000000000 --- a/goblin/traversal.py +++ /dev/null @@ -1,166 +0,0 @@ -# Copyright 2016 ZEROFAIL -# -# This file is part of Goblin. -# -# Goblin is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Goblin is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with Goblin. If not, see <http://www.gnu.org/licenses/>. - -"""Query API and helpers""" - -import asyncio -import functools -import logging - -from goblin import cardinality, element, mapper -from goblin.driver import connection, graph -from gremlin_python import process - - -logger = logging.getLogger(__name__) - - -def bindprop(element_class, ogm_name, val, *, binding=None): - """ - Helper function for binding ogm properties/values to corresponding db - properties/values for traversals. - - :param goblin.element.Element element_class: User defined element class - :param str ogm_name: Name of property as defined in the ogm - :param val: The property value - :param str binding: The binding for val (optional) - - :returns: tuple object ('db_property_name', ('binding(if passed)', val)) - """ - db_name = getattr(element_class, ogm_name, ogm_name) - _, data_type = element_class.__mapping__.ogm_properties[ogm_name] - val = data_type.to_db(val) - if binding: - val = (binding, val) - return db_name, val - - -class TraversalResponse: - """Asynchronous iterator that encapsulates a traversal response queue""" - def __init__(self, response_queue): - self._queue = response_queue - self._done = False - - async def __aiter__(self): - return self - - async def __anext__(self): - if self._done: - return - msg = await self._queue.get() - if msg: - return msg - else: - self._done = True - raise StopAsyncIteration - - -# This is all until we figure out GLV integration... -class GoblinTraversal(graph.AsyncGraphTraversal): - - async def all(self): - """ - Get all results from traversal. - - :returns: :py:class:`TraversalResponse` object - """ - return await self.next() - - async def one_or_none(self): - """ - Get one or zero results from a traveral. - - :returns: :py:class:`Element<goblin.element.Element>` object - """ - result = None - async for msg in await self.next(): - result = msg - return result - - -class TraversalFactory: - """Helper that wraps a AsyncRemoteGraph""" - def __init__(self, graph): - self._graph = graph - - @property - def graph(self): - return self._graph - - def traversal(self, *, element_class=None): - """ - Generate a traversal using a user defined element class as a - starting point. - - :param goblin.element.Element element_class: An optional element - class that will dictate the element type (vertex/edge) as well as - the label for the traversal source - - :returns: :py:class:`GoblinTraversal` - """ - traversal = self.graph.traversal() - if element_class: - label = element_class.__mapping__.label - traversal = self._graph.traversal() - if element_class.__type__ == 'vertex': - traversal = traversal.V() - if element_class.__type__ == 'edge': - traversal = traversal.E() - traversal = traversal.hasLabel(label) - return traversal - - def remove_vertex(self, elem): - """Convenience function for generating crud traversals.""" - return self.traversal().V(elem.id).drop() - - def remove_edge(self, elem): - """Convenience function for generating crud traversals.""" - return self.traversal().E(elem.id).drop() - - def get_vertex_by_id(self, elem): - """Convenience function for generating crud traversals.""" - return self.traversal().V(elem.id) - - def get_edge_by_id(self, elem): - """Convenience function for generating crud traversals.""" - return self.traversal().E(elem.id) - - def add_properties(self, traversal, props): - binding = 0 - potential_removals = [] - potential_metaprops = [] - for card, db_name, val, metaprops in props: - if val: - key = ('k' + str(binding), db_name) - val = ('v' + str(binding), val) - if card: - # Maybe use a dict here as a translator - if card == cardinality.Cardinality.list: - card = process.Cardinality.list - elif card == cardinality.Cardinality.set: - card = process.Cardinality.set - else: - card = process.Cardinality.single - traversal = traversal.property(card, key, val) - else: - traversal = traversal.property(key, val) - binding += 1 - if metaprops: - potential_metaprops.append((db_name, val, metaprops)) - else: - potential_removals.append(db_name) - return traversal, potential_removals, potential_metaprops diff --git a/gremlin_python/__init__.py b/gremlin_python/__init__.py index 518d8e008b256688b90e792343fd0cf1e7bb26e2..7626550738f12ccd5e18b8c06a8d68aaab882066 100644 --- a/gremlin_python/__init__.py +++ b/gremlin_python/__init__.py @@ -16,6 +16,5 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' -from . import statics __author__ = 'Marko A. Rodriguez (http://markorodriguez.com)' diff --git a/gremlin_python/driver/__init__.py b/gremlin_python/driver/__init__.py index 7e1c0f1a812f96353a3b4b12daf4db66b2ce4a51..7626550738f12ccd5e18b8c06a8d68aaab882066 100644 --- a/gremlin_python/driver/__init__.py +++ b/gremlin_python/driver/__init__.py @@ -16,7 +16,5 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' -from .remote_connection import RemoteConnection -from .rest_remote_connection import RESTRemoteConnection __author__ = 'Marko A. Rodriguez (http://markorodriguez.com)' diff --git a/gremlin_python/driver/remote_connection.py b/gremlin_python/driver/remote_connection.py index 1651b92ad7809e01fd6fecf06371edacd3a7b937..452fcaf5f6e5c6915bcdeafaa28851911002a46e 100644 --- a/gremlin_python/driver/remote_connection.py +++ b/gremlin_python/driver/remote_connection.py @@ -16,16 +16,64 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' -from abc import abstractmethod +import abc +import six + +from ..process.traversal import Traversal +from ..process.traversal import TraversalStrategy +from ..process.traversal import TraversalSideEffects __author__ = 'Marko A. Rodriguez (http://markorodriguez.com)' +@six.add_metaclass(abc.ABCMeta) class RemoteConnection(object): - def __init__(self, url): - self.url = url + def __init__(self, url, traversal_source): + self._url = url + self._traversal_source = traversal_source + + @property + def url(self): + return self._url + + @property + def traversal_source(self): + return self._traversal_source + + @abc.abstractmethod + def submit(self, bytecode): + print("sending " + bytecode + " to GremlinServer...") + return RemoteTraversal(iter([]), TraversalSideEffects()) + + def __repr__(self): + return "remoteconnection[" + self._url + "," + self._traversal_source + "]" + + +class RemoteTraversal(Traversal): + def __init__(self, traversers, side_effects): + Traversal.__init__(self, None, None, None) + self.traversers = traversers + self.side_effects = side_effects + + +class RemoteTraversalSideEffects(TraversalSideEffects): + def __init__(self, keys_lambda, value_lambda): + self.keys_lambda = keys_lambda + self.value_lambda = value_lambda + + def keys(self): + return self.keys_lambda() + + def get(self, key): + return self.value_lambda(key) + + +class RemoteStrategy(TraversalStrategy): + def __init__(self, remote_connection): + self.remote_connection = remote_connection - @abstractmethod - def submit(self, target_language, script, bindings): - print "sending " + script + " to GremlinServer..." - return iter([]) + def apply(self, traversal): + if traversal.traversers is None: + remote_traversal = self.remote_connection.submit(traversal.bytecode) + # traversal.side_effects = remote_traversal.side_effects + traversal.traversers = remote_traversal#.traversers diff --git a/gremlin_python/process/__init__.py b/gremlin_python/process/__init__.py index e93356e95724d6eaab0892313c6744643294229b..7626550738f12ccd5e18b8c06a8d68aaab882066 100644 --- a/gremlin_python/process/__init__.py +++ b/gremlin_python/process/__init__.py @@ -17,22 +17,4 @@ specific language governing permissions and limitations under the License. ''' -from .graph_traversal import GraphTraversal -from .graph_traversal import GraphTraversalSource -from .graph_traversal import __ -from .groovy_translator import GroovyTranslator -from .jython_translator import JythonTranslator -from .traversal import Barrier -from .traversal import Bytecode -from .traversal import Cardinality -from .traversal import Column -from .traversal import Direction -from .traversal import Operator -from .traversal import Order -from .traversal import P -from .traversal import Pop -from .traversal import Scope -from .traversal import T -from .traversal import Traversal - __author__ = 'Marko A. Rodriguez (http://markorodriguez.com)' diff --git a/gremlin_python/process/graph_traversal.py b/gremlin_python/process/graph_traversal.py index 37f9deb66a251c8d75ab2e316501ff6755f184c9..9010f603076efc69e6ad08950fa7fbda890ed9b6 100644 --- a/gremlin_python/process/graph_traversal.py +++ b/gremlin_python/process/graph_traversal.py @@ -16,933 +16,402 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' -from .traversal import RawExpression +import sys from .traversal import Traversal +from .traversal import TraversalStrategies from .traversal import Bytecode -from gremlin_python import statics +from ..driver.remote_connection import RemoteStrategy +from .. import statics class GraphTraversalSource(object): - def __init__(self, graph, traversal_strategies, graph_traversal=None, bytecode=Bytecode()): + def __init__(self, graph, traversal_strategies, bytecode=None, + graph_traversal=None, remote_strategy=None): self.graph = graph self.traversal_strategies = traversal_strategies if graph_traversal is None: graph_traversal = GraphTraversal self.graph_traversal = graph_traversal + if remote_strategy is None: + remote_strategy = RemoteStrategy + self.remote_strategy = remote_strategy + if bytecode is None: + bytecode = Bytecode() self.bytecode = bytecode def __repr__(self): return "graphtraversalsource[" + str(self.graph) + "]" - def E(self, *args): - traversal = self.graph_traversal(self.graph, self.traversal_strategies, Bytecode(self.bytecode)) - traversal.bytecode.add_step("E", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) - return traversal - def V(self, *args): - traversal = self.graph_traversal(self.graph, self.traversal_strategies, Bytecode(self.bytecode)) - traversal.bytecode.add_step("V", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) - return traversal - def addV(self, *args): - traversal = self.graph_traversal(self.graph, self.traversal_strategies, Bytecode(self.bytecode)) - traversal.bytecode.add_step("addV", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) - return traversal - def inject(self, *args): - traversal = self.graph_traversal(self.graph, self.traversal_strategies, Bytecode(self.bytecode)) - traversal.bytecode.add_step("inject", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) - return traversal def withBulk(self, *args): - source = GraphTraversalSource(self.graph, self.traversal_strategies, Bytecode(self.bytecode)) + source = GraphTraversalSource( + self.graph, TraversalStrategies(self.traversal_strategies), + Bytecode(self.bytecode), self.graph_traversal, self.remote_strategy) source.bytecode.add_source("withBulk", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return source def withComputer(self, *args): - source = GraphTraversalSource(self.graph, self.traversal_strategies, Bytecode(self.bytecode)) + source = GraphTraversalSource( + self.graph, TraversalStrategies(self.traversal_strategies), + Bytecode(self.bytecode), self.graph_traversal, self.remote_strategy) source.bytecode.add_source("withComputer", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return source def withPath(self, *args): - source = GraphTraversalSource(self.graph, self.traversal_strategies, Bytecode(self.bytecode)) + source = GraphTraversalSource( + self.graph, TraversalStrategies(self.traversal_strategies), + Bytecode(self.bytecode), self.graph_traversal, self.remote_strategy) source.bytecode.add_source("withPath", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return source def withSack(self, *args): - source = GraphTraversalSource(self.graph, self.traversal_strategies, Bytecode(self.bytecode)) + source = GraphTraversalSource( + self.graph, TraversalStrategies(self.traversal_strategies), + Bytecode(self.bytecode), self.graph_traversal, self.remote_strategy) source.bytecode.add_source("withSack", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return source def withSideEffect(self, *args): - source = GraphTraversalSource(self.graph, self.traversal_strategies, Bytecode(self.bytecode)) + source = GraphTraversalSource( + self.graph, TraversalStrategies(self.traversal_strategies), + Bytecode(self.bytecode), self.graph_traversal, self.remote_strategy) source.bytecode.add_source("withSideEffect", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return source def withStrategies(self, *args): - source = GraphTraversalSource(self.graph, self.traversal_strategies, Bytecode(self.bytecode)) + source = GraphTraversalSource( + self.graph, TraversalStrategies(self.traversal_strategies), + Bytecode(self.bytecode), self.graph_traversal, self.remote_strategy) source.bytecode.add_source("withStrategies", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) - return source - def withTranslator(self, *args): - source = GraphTraversalSource(self.graph, self.traversal_strategies, Bytecode(self.bytecode)) - source.bytecode.add_source("withTranslator", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return source def withoutStrategies(self, *args): - source = GraphTraversalSource(self.graph, self.traversal_strategies, Bytecode(self.bytecode)) + source = GraphTraversalSource( + self.graph, TraversalStrategies(self.traversal_strategies), + Bytecode(self.bytecode), self.graph_traversal, self.remote_strategy) source.bytecode.add_source("withoutStrategies", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return source + def withRemote(self, remote_connection): + source = GraphTraversalSource( + self.graph, TraversalStrategies(self.traversal_strategies), + Bytecode(self.bytecode), self.graph_traversal, self.remote_strategy) + source.traversal_strategies.add_strategies([self.remote_strategy(remote_connection)]) + return source + def withBindings(self, bindings): + return self + def E(self, *args): + traversal = self.graph_traversal(self.graph, self.traversal_strategies, Bytecode(self.bytecode)) + traversal.bytecode.add_step("E", *args) + return traversal + def V(self, *args): + traversal = self.graph_traversal(self.graph, self.traversal_strategies, Bytecode(self.bytecode)) + traversal.bytecode.add_step("V", *args) + return traversal + def addV(self, *args): + traversal = self.graph_traversal(self.graph, self.traversal_strategies, Bytecode(self.bytecode)) + traversal.bytecode.add_step("addV", *args) + return traversal + def inject(self, *args): + traversal = self.graph_traversal(self.graph, self.traversal_strategies, Bytecode(self.bytecode)) + traversal.bytecode.add_step("inject", *args) + return traversal class GraphTraversal(Traversal): def __init__(self, graph, traversal_strategies, bytecode): Traversal.__init__(self, graph, traversal_strategies, bytecode) + def __getitem__(self, index): + if isinstance(index, int): + return self.range(index, index + 1) + elif isinstance(index, slice): + return self.range(0 if index.start is None else index.start, sys.maxint if index.stop is None else index.stop) + else: + raise TypeError("Index must be int or slice") + def __getattr__(self, key): + return self.values(key) def V(self, *args): self.bytecode.add_step("V", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) - return self - def _and(self, *args): - self.bytecode.add_step("_and", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) - return self - def _as(self, *args): - self.bytecode.add_step("_as", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) - return self - def _from(self, *args): - self.bytecode.add_step("_from", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) - return self - def _in(self, *args): - self.bytecode.add_step("_in", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) - return self - def _is(self, *args): - self.bytecode.add_step("_is", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) - return self - def _not(self, *args): - self.bytecode.add_step("_not", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) - return self - def _or(self, *args): - self.bytecode.add_step("_or", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def addE(self, *args): self.bytecode.add_step("addE", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def addInE(self, *args): self.bytecode.add_step("addInE", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def addOutE(self, *args): self.bytecode.add_step("addOutE", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def addV(self, *args): self.bytecode.add_step("addV", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def aggregate(self, *args): self.bytecode.add_step("aggregate", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) - return self - def asAdmin(self, *args): - self.bytecode.add_step("asAdmin", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) + return self + def and_(self, *args): + self.bytecode.add_step("and", *args) + return self + def as_(self, *args): + self.bytecode.add_step("as", *args) return self def barrier(self, *args): self.bytecode.add_step("barrier", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def both(self, *args): self.bytecode.add_step("both", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def bothE(self, *args): self.bytecode.add_step("bothE", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def bothV(self, *args): self.bytecode.add_step("bothV", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def branch(self, *args): self.bytecode.add_step("branch", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def by(self, *args): self.bytecode.add_step("by", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def cap(self, *args): self.bytecode.add_step("cap", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def choose(self, *args): self.bytecode.add_step("choose", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def coalesce(self, *args): self.bytecode.add_step("coalesce", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def coin(self, *args): self.bytecode.add_step("coin", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def constant(self, *args): self.bytecode.add_step("constant", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def count(self, *args): self.bytecode.add_step("count", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def cyclicPath(self, *args): self.bytecode.add_step("cyclicPath", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def dedup(self, *args): self.bytecode.add_step("dedup", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def drop(self, *args): self.bytecode.add_step("drop", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def emit(self, *args): self.bytecode.add_step("emit", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def filter(self, *args): self.bytecode.add_step("filter", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def flatMap(self, *args): self.bytecode.add_step("flatMap", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def fold(self, *args): self.bytecode.add_step("fold", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) + return self + def from_(self, *args): + self.bytecode.add_step("from", *args) return self def group(self, *args): self.bytecode.add_step("group", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def groupCount(self, *args): self.bytecode.add_step("groupCount", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def groupV3d0(self, *args): self.bytecode.add_step("groupV3d0", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def has(self, *args): self.bytecode.add_step("has", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def hasId(self, *args): self.bytecode.add_step("hasId", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def hasKey(self, *args): self.bytecode.add_step("hasKey", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def hasLabel(self, *args): self.bytecode.add_step("hasLabel", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def hasNot(self, *args): self.bytecode.add_step("hasNot", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def hasValue(self, *args): self.bytecode.add_step("hasValue", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def id(self, *args): self.bytecode.add_step("id", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def identity(self, *args): self.bytecode.add_step("identity", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def inE(self, *args): self.bytecode.add_step("inE", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def inV(self, *args): self.bytecode.add_step("inV", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) + return self + def in_(self, *args): + self.bytecode.add_step("in", *args) return self def inject(self, *args): self.bytecode.add_step("inject", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) - return self - def iterate(self, *args): - self.bytecode.add_step("iterate", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) + return self + def is_(self, *args): + self.bytecode.add_step("is", *args) return self def key(self, *args): self.bytecode.add_step("key", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def label(self, *args): self.bytecode.add_step("label", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def limit(self, *args): self.bytecode.add_step("limit", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def local(self, *args): self.bytecode.add_step("local", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def loops(self, *args): self.bytecode.add_step("loops", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def map(self, *args): self.bytecode.add_step("map", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def mapKeys(self, *args): self.bytecode.add_step("mapKeys", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def mapValues(self, *args): self.bytecode.add_step("mapValues", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def match(self, *args): self.bytecode.add_step("match", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def max(self, *args): self.bytecode.add_step("max", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def mean(self, *args): self.bytecode.add_step("mean", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def min(self, *args): self.bytecode.add_step("min", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) + return self + def not_(self, *args): + self.bytecode.add_step("not", *args) return self def option(self, *args): self.bytecode.add_step("option", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def optional(self, *args): self.bytecode.add_step("optional", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) + return self + def or_(self, *args): + self.bytecode.add_step("or", *args) return self def order(self, *args): self.bytecode.add_step("order", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def otherV(self, *args): self.bytecode.add_step("otherV", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def out(self, *args): self.bytecode.add_step("out", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def outE(self, *args): self.bytecode.add_step("outE", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def outV(self, *args): self.bytecode.add_step("outV", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def pageRank(self, *args): self.bytecode.add_step("pageRank", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def path(self, *args): self.bytecode.add_step("path", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def peerPressure(self, *args): self.bytecode.add_step("peerPressure", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def profile(self, *args): self.bytecode.add_step("profile", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def program(self, *args): self.bytecode.add_step("program", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def project(self, *args): self.bytecode.add_step("project", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def properties(self, *args): self.bytecode.add_step("properties", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def property(self, *args): self.bytecode.add_step("property", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def propertyMap(self, *args): self.bytecode.add_step("propertyMap", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def range(self, *args): self.bytecode.add_step("range", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def repeat(self, *args): self.bytecode.add_step("repeat", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def sack(self, *args): self.bytecode.add_step("sack", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def sample(self, *args): self.bytecode.add_step("sample", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def select(self, *args): self.bytecode.add_step("select", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def sideEffect(self, *args): self.bytecode.add_step("sideEffect", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def simplePath(self, *args): self.bytecode.add_step("simplePath", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def store(self, *args): self.bytecode.add_step("store", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def subgraph(self, *args): self.bytecode.add_step("subgraph", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def sum(self, *args): self.bytecode.add_step("sum", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def tail(self, *args): self.bytecode.add_step("tail", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def timeLimit(self, *args): self.bytecode.add_step("timeLimit", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def times(self, *args): self.bytecode.add_step("times", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def to(self, *args): self.bytecode.add_step("to", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def toE(self, *args): self.bytecode.add_step("toE", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def toV(self, *args): self.bytecode.add_step("toV", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def tree(self, *args): self.bytecode.add_step("tree", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def unfold(self, *args): self.bytecode.add_step("unfold", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def union(self, *args): self.bytecode.add_step("union", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def until(self, *args): self.bytecode.add_step("until", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def value(self, *args): self.bytecode.add_step("value", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def valueMap(self, *args): self.bytecode.add_step("valueMap", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def values(self, *args): self.bytecode.add_step("values", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self def where(self, *args): self.bytecode.add_step("where", *args) - for arg in args: - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - elif isinstance(arg, RawExpression): - self.bindings.update(arg.bindings) return self @@ -954,24 +423,6 @@ class __(object): def __(*args): return GraphTraversal(None, None, Bytecode()).__(*args) @staticmethod - def _and(*args): - return GraphTraversal(None, None, Bytecode())._and(*args) - @staticmethod - def _as(*args): - return GraphTraversal(None, None, Bytecode())._as(*args) - @staticmethod - def _in(*args): - return GraphTraversal(None, None, Bytecode())._in(*args) - @staticmethod - def _is(*args): - return GraphTraversal(None, None, Bytecode())._is(*args) - @staticmethod - def _not(*args): - return GraphTraversal(None, None, Bytecode())._not(*args) - @staticmethod - def _or(*args): - return GraphTraversal(None, None, Bytecode())._or(*args) - @staticmethod def addE(*args): return GraphTraversal(None, None, Bytecode()).addE(*args) @staticmethod @@ -987,6 +438,12 @@ class __(object): def aggregate(*args): return GraphTraversal(None, None, Bytecode()).aggregate(*args) @staticmethod + def and_(*args): + return GraphTraversal(None, None, Bytecode()).and_(*args) + @staticmethod + def as_(*args): + return GraphTraversal(None, None, Bytecode()).as_(*args) + @staticmethod def barrier(*args): return GraphTraversal(None, None, Bytecode()).barrier(*args) @staticmethod @@ -1080,9 +537,15 @@ class __(object): def inV(*args): return GraphTraversal(None, None, Bytecode()).inV(*args) @staticmethod + def in_(*args): + return GraphTraversal(None, None, Bytecode()).in_(*args) + @staticmethod def inject(*args): return GraphTraversal(None, None, Bytecode()).inject(*args) @staticmethod + def is_(*args): + return GraphTraversal(None, None, Bytecode()).is_(*args) + @staticmethod def key(*args): return GraphTraversal(None, None, Bytecode()).key(*args) @staticmethod @@ -1119,9 +582,15 @@ class __(object): def min(*args): return GraphTraversal(None, None, Bytecode()).min(*args) @staticmethod + def not_(*args): + return GraphTraversal(None, None, Bytecode()).not_(*args) + @staticmethod def optional(*args): return GraphTraversal(None, None, Bytecode()).optional(*args) @staticmethod + def or_(*args): + return GraphTraversal(None, None, Bytecode()).or_(*args) + @staticmethod def order(*args): return GraphTraversal(None, None, Bytecode()).order(*args) @staticmethod @@ -1233,36 +702,6 @@ def V(*args): statics.add_static('V', V) -def _and(*args): - return __._and(*args) - -statics.add_static('_and', _and) - -def _as(*args): - return __._as(*args) - -statics.add_static('_as', _as) - -def _in(*args): - return __._in(*args) - -statics.add_static('_in', _in) - -def _is(*args): - return __._is(*args) - -statics.add_static('_is', _is) - -def _not(*args): - return __._not(*args) - -statics.add_static('_not', _not) - -def _or(*args): - return __._or(*args) - -statics.add_static('_or', _or) - def addE(*args): return __.addE(*args) @@ -1288,6 +727,16 @@ def aggregate(*args): statics.add_static('aggregate', aggregate) +def and_(*args): + return __.and_(*args) + +statics.add_static('and_', and_) + +def as_(*args): + return __.as_(*args) + +statics.add_static('as_', as_) + def barrier(*args): return __.barrier(*args) @@ -1443,11 +892,21 @@ def inV(*args): statics.add_static('inV', inV) +def in_(*args): + return __.in_(*args) + +statics.add_static('in_', in_) + def inject(*args): return __.inject(*args) statics.add_static('inject', inject) +def is_(*args): + return __.is_(*args) + +statics.add_static('is_', is_) + def key(*args): return __.key(*args) @@ -1508,11 +967,21 @@ def min(*args): statics.add_static('min', min) +def not_(*args): + return __.not_(*args) + +statics.add_static('not_', not_) + def optional(*args): return __.optional(*args) statics.add_static('optional', optional) +def or_(*args): + return __.or_(*args) + +statics.add_static('or_', or_) + def order(*args): return __.order(*args) diff --git a/gremlin_python/process/jython_translator.py b/gremlin_python/process/jython_translator.py deleted file mode 100644 index c3169f07c71a0fb0e5a35a33ef2c60bf66e7b5e6..0000000000000000000000000000000000000000 --- a/gremlin_python/process/jython_translator.py +++ /dev/null @@ -1,102 +0,0 @@ -''' -Licensed to the Apache Software Foundation (ASF) under one -or more contributor license agreements. See the NOTICE file -distributed with this work for additional information -regarding copyright ownership. The ASF licenses this file -to you 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 inspect -import sys -from aenum import Enum - -from .traversal import Barrier -from .traversal import Bytecode -from .traversal import Cardinality -from .traversal import Column -from .traversal import P -from .traversal import RawExpression -from .traversal import SymbolHelper -from .traversal import Translator - -if sys.version_info.major > 2: - long = int - -__author__ = 'Marko A. Rodriguez (http://markorodriguez.com)' - - -class JythonTranslator(Translator): - def __init__(self, traversal_source, anonymous_traversal="__", target_language="gremlin-jython"): - Translator.__init__(self, traversal_source, anonymous_traversal, target_language) - - def translate(self, bytecode): - return self.__internalTranslate(self.traversal_source, bytecode) - - def __internalTranslate(self, start, bytecode): - traversal_script = start - for instruction in bytecode.source_instructions: - traversal_script = traversal_script + "." + SymbolHelper.toJava( - instruction[0]) + "(" + self.stringify(*instruction[1]) + ")" - for instruction in bytecode.step_instructions: - traversal_script = traversal_script + "." + SymbolHelper.toJava( - instruction[0]) + "(" + self.stringify(*instruction[1]) + ")" - return traversal_script - - def stringOrObject(self, arg): - if isinstance(arg, str): - return "\"" + arg + "\"" - elif isinstance(arg, long): - return str(arg) - elif isinstance(arg, Barrier): - return "Barrier" + "." + SymbolHelper.toJava(str(arg.name)) - elif isinstance(arg, Column): - return "Column.valueOf('" + SymbolHelper.toJava(str(arg.name)) + "')" - elif isinstance(arg, Cardinality): - return "Cardinality" + "." + SymbolHelper.toJava(str(arg.name)) - elif isinstance(arg, Enum): # Order, Direction, Scope, T, etc. - return SymbolHelper.toJava(type(arg).__name__) + "." + SymbolHelper.toJava(str(arg.name)) - elif isinstance(arg, P): - if arg.other is None: - return "P." + SymbolHelper.toJava(arg.operator) + "(" + self.stringOrObject( - arg.value) + ")" - else: - return self.stringOrObject(arg.other) + "." + SymbolHelper.toJava( - arg.operator) + "(" + self.stringOrObject(arg.value) + ")" - elif isinstance(arg, Bytecode): - return self.__internalTranslate(self.anonymous_traversal, arg) - elif callable(arg): # lambda that produces a string that is a lambda - argLambdaString = arg().strip() - argLength = len(inspect.getargspec(eval(argLambdaString)).args) - if argLength == 0: - return "JythonZeroArgLambda(" + argLambdaString + ")" - elif argLength == 1: - return "JythonOneArgLambda(" + argLambdaString + ")" - elif argLength == 2: - return "JythonTwoArgLambda(" + argLambdaString + ")" - else: - raise - elif isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): # bindings - return arg[0] - elif isinstance(arg, RawExpression): - return "".join(self.stringOrObject(i) for i in arg.parts) - else: - return str(arg) - - def stringify(self, *args): - if len(args) == 0: - return "" - elif len(args) == 1: - return self.stringOrObject(args[0]) - else: - return ", ".join(self.stringOrObject(i) for i in args) diff --git a/gremlin_python/process/groovy_translator.py b/gremlin_python/process/translator.py similarity index 60% rename from gremlin_python/process/groovy_translator.py rename to gremlin_python/process/translator.py index 2c2d79a860da80b70bb427df6351793e06c8df45..ac1ebe73cb7e02db5cc00c07927fcbd164a85b63 100644 --- a/gremlin_python/process/groovy_translator.py +++ b/gremlin_python/process/translator.py @@ -17,19 +17,68 @@ specific language governing permissions and limitations under the License. ''' -import sys +# Translate bytecode. Used for backwards compatiblitity + +from abc import abstractmethod from aenum import Enum -from .traversal import Bytecode -from .traversal import P -from .traversal import RawExpression -from .traversal import SymbolHelper -from .traversal import Translator +from gremlin_python.process.traversal import P, Bytecode, Binding + + +class RawExpression(object): + def __init__(self, *args): + self.bindings = dict() + self.parts = [self._process_arg(arg) for arg in args] + + def _process_arg(self, arg): + if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): + self.bindings[arg[0]] = arg[1] + return Raw(arg[0]) + else: + return Raw(arg) + +class Raw(object): + def __init__(self, value): + self.value = value + + def __str__(self): + return str(self.value) + + +TO_JAVA_MAP = {"_global": "global", "_as": "as", "_in": "in", "_and": "and", + "_or": "or", "_is": "is", "_not": "not", "_from": "from", + "Cardinality": "VertexProperty.Cardinality", "Barrier": "SackFunctions.Barrier"} -if sys.version_info.major > 2: - long = int -__author__ = 'Marko A. Rodriguez (http://markorodriguez.com)' +class Translator(object): + def __init__(self, traversal_source, anonymous_traversal, target_language): + self.traversal_source = traversal_source + self.anonymous_traversal = anonymous_traversal + self.target_language = target_language + + @abstractmethod + def translate(self, bytecode): + return + + @abstractmethod + def __repr__(self): + return "translator[" + self.traversal_source + ":" + self.target_language + "]" + + +class SymbolHelper(object): + @staticmethod + def toJava(symbol): + if (symbol in TO_JAVA_MAP): + return TO_JAVA_MAP[symbol] + else: + return symbol + + @staticmethod + def mapEnum(enum): + if (enum in enumMap): + return enumMap[enum] + else: + return enum class GroovyTranslator(Translator): @@ -37,16 +86,16 @@ class GroovyTranslator(Translator): Translator.__init__(self, traversal_source, anonymous_traversal, target_language) def translate(self, bytecode): - return self.__internalTranslate(self.traversal_source, bytecode) + return self._internalTranslate(self.traversal_source, bytecode) - def __internalTranslate(self, start, bytecode): + def _internalTranslate(self, start, bytecode): traversal_script = start for instruction in bytecode.source_instructions: traversal_script = traversal_script + "." + SymbolHelper.toJava( - instruction[0]) + "(" + self.stringify(*instruction[1]) + ")" + instruction[0]) + "(" + self.stringify(*instruction[1:]) + ")" for instruction in bytecode.step_instructions: traversal_script = traversal_script + "." + SymbolHelper.toJava( - instruction[0]) + "(" + self.stringify(*instruction[1]) + ")" + instruction[0]) + "(" + self.stringify(*instruction[1:]) + ")" return traversal_script def stringOrObject(self, arg): @@ -54,7 +103,7 @@ class GroovyTranslator(Translator): return "\"" + arg + "\"" elif isinstance(arg, bool): return str(arg).lower() - elif isinstance(arg, long): + elif isinstance(arg, int): return str(arg) + "L" elif isinstance(arg, float): return str(arg) + "f" @@ -67,8 +116,10 @@ class GroovyTranslator(Translator): else: return self.stringOrObject(arg.other) + "." + SymbolHelper.toJava( arg.operator) + "(" + self.stringOrObject(arg.value) + ")" + elif isinstance(arg, Binding): + return arg.key elif isinstance(arg, Bytecode): - return self.__internalTranslate(self.anonymous_traversal, arg) + return self._internalTranslate(self.anonymous_traversal, arg) elif callable(arg): # closures lambdaString = arg().strip() if lambdaString.startswith("{"): diff --git a/gremlin_python/process/traversal.py b/gremlin_python/process/traversal.py index 7558151f79f9ca7d837bc11e0c650c810eb52a19..cbba42d8bfd8d16817cf2ac7ca74658262184cde 100644 --- a/gremlin_python/process/traversal.py +++ b/gremlin_python/process/traversal.py @@ -16,53 +16,50 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' -from abc import abstractmethod +import abc +import six from aenum import Enum -from gremlin_python import statics +from .. import statics class Traversal(object): def __init__(self, graph, traversal_strategies, bytecode): self.graph = graph self.traversal_strategies = traversal_strategies self.bytecode = bytecode - self.results = None + self.side_effects = TraversalSideEffects() + self.traversers = None self.last_traverser = None - self.bindings = {} - def __repr__(self): - return self.graph.translator.translate(self.bytecode) - - def __getitem__(self, index): - if isinstance(index, int): - return self.range(index, index + 1) - elif isinstance(index, slice): - return self.range(index.start, index.stop) - else: - raise TypeError("Index must be int or slice") - - def __getattr__(self, key): - return self.values(key) - + return str(self.bytecode) def __iter__(self): return self - def __next__(self): - if self.results is None: + if self.traversers is None: self.traversal_strategies.apply_strategies(self) if self.last_traverser is None: - self.last_traverser = next(self.results) + self.last_traverser = next(self.traversers) object = self.last_traverser.object self.last_traverser.bulk = self.last_traverser.bulk - 1 if self.last_traverser.bulk <= 0: self.last_traverser = None return object - def toList(self): return list(iter(self)) - def toSet(self): return set(iter(self)) - + def iterate(self): + while True: + try: self.nextTraverser() + except StopIteration: return self + def nextTraverser(self): + if self.traversers is None: + self.traversal_strategies.apply_strategies(self) + if self.last_traverser is None: + return next(self.traversers) + else: + temp = self.last_traverser + self.last_traverser = None + return temp def next(self, amount=None): if amount is None: return self.__next__() @@ -76,29 +73,25 @@ class Traversal(object): tempList.append(temp) return tempList -Barrier = Enum('Barrier', 'normSack') +Barrier = Enum('Barrier', 'normSack') statics.add_static('normSack', Barrier.normSack) Cardinality = Enum('Cardinality', 'list set single') - statics.add_static('single', Cardinality.single) statics.add_static('list', Cardinality.list) statics.add_static('set', Cardinality.set) Column = Enum('Column', 'keys values') - statics.add_static('keys', Column.keys) statics.add_static('values', Column.values) Direction = Enum('Direction', 'BOTH IN OUT') - statics.add_static('OUT', Direction.OUT) statics.add_static('IN', Direction.IN) statics.add_static('BOTH', Direction.BOTH) -Operator = Enum('Operator', 'addAll _and assign div max min minus mult _or sum sumLong') - +Operator = Enum('Operator', 'addAll and_ assign div max min minus mult or_ sum sumLong') statics.add_static('sum', Operator.sum) statics.add_static('minus', Operator.minus) statics.add_static('mult', Operator.mult) @@ -106,13 +99,12 @@ statics.add_static('div', Operator.div) statics.add_static('min', Operator.min) statics.add_static('max', Operator.max) statics.add_static('assign', Operator.assign) -statics.add_static('_and', Operator._and) -statics.add_static('_or', Operator._or) +statics.add_static('and_', Operator.and_) +statics.add_static('or_', Operator.or_) statics.add_static('addAll', Operator.addAll) statics.add_static('sumLong', Operator.sumLong) Order = Enum('Order', 'decr incr keyDecr keyIncr shuffle valueDecr valueIncr') - statics.add_static('incr', Order.incr) statics.add_static('decr', Order.decr) statics.add_static('keyIncr', Order.keyIncr) @@ -121,19 +113,16 @@ statics.add_static('keyDecr', Order.keyDecr) statics.add_static('valueDecr', Order.valueDecr) statics.add_static('shuffle', Order.shuffle) -Pop = Enum('Pop', 'all first last') - +Pop = Enum('Pop', 'all_ first last') statics.add_static('first', Pop.first) statics.add_static('last', Pop.last) -statics.add_static('all', Pop.all) +statics.add_static('all_', Pop.all_) -Scope = Enum('Scope', '_global local') - -statics.add_static('_global', Scope._global) +Scope = Enum('Scope', 'global_ local') +statics.add_static('global_', Scope.global_) statics.add_static('local', Scope.local) T = Enum('T', 'id key label value') - statics.add_static('label', T.label) statics.add_static('id', T.id) statics.add_static('key', T.key) @@ -145,9 +134,6 @@ class P(object): self.value = value self.other = other @staticmethod - def _not(*args): - return P("not", *args) - @staticmethod def between(*args): return P("between", *args) @staticmethod @@ -172,6 +158,9 @@ class P(object): def neq(*args): return P("neq", *args) @staticmethod + def not_(*args): + return P("not", *args) + @staticmethod def outside(*args): return P("outside", *args) @staticmethod @@ -184,106 +173,94 @@ class P(object): def without(*args): return P("without", *args) def _and(self, arg): - return P("_and", arg, self) + return P("and", arg, self) def _or(self, arg): - return P("_or", arg, self) - -def _not(*args): - return P._not(*args) - -statics.add_static('_not',_not) + return P("or", arg, self) + def __eq__(self, other): + return isinstance(other, self.__class__) and self.operator == other.operator and self.value == other.value and self.other == other.other + def __repr__(self): + return self.operator + "(" + str(self.value) + ")" if self.other is None else self.operator + "(" + str(self.value) + "," + str(self.other) + ")" def between(*args): return P.between(*args) - statics.add_static('between',between) def eq(*args): return P.eq(*args) - statics.add_static('eq',eq) def gt(*args): return P.gt(*args) - statics.add_static('gt',gt) def gte(*args): return P.gte(*args) - statics.add_static('gte',gte) def inside(*args): return P.inside(*args) - statics.add_static('inside',inside) def lt(*args): return P.lt(*args) - statics.add_static('lt',lt) def lte(*args): return P.lte(*args) - statics.add_static('lte',lte) def neq(*args): return P.neq(*args) - statics.add_static('neq',neq) +def not_(*args): + return P.not_(*args) +statics.add_static('not_',not_) + def outside(*args): return P.outside(*args) - statics.add_static('outside',outside) def test(*args): return P.test(*args) - statics.add_static('test',test) def within(*args): return P.within(*args) - statics.add_static('within',within) def without(*args): return P.without(*args) - statics.add_static('without',without) -class RawExpression(object): - def __init__(self, *args): - self.bindings = dict() - self.parts = [self._process_arg(arg) for arg in args] - - def _process_arg(self, arg): - if isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): - self.bindings[arg[0]] = arg[1] - return Raw(arg[0]) - else: - return Raw(arg) - -class Raw(object): - def __init__(self, value): - self.value = value - - def __str__(self): - return str(self.value) - ''' TRAVERSER ''' class Traverser(object): - def __init__(self, object, bulk): + def __init__(self, object, bulk=1): self.object = object self.bulk = bulk def __repr__(self): return str(self.object) + def __eq__(self, other): + return isinstance(other, self.__class__) and self.object == other.object + +''' +TRAVERSAL SIDE-EFFECTS +''' + +class TraversalSideEffects(object): + def keys(self): + return set() + def get(self, key): + raise KeyError(key) + def __getitem__(self, key): + return self.get(key) + def __repr__(self): + return "sideEffects[size:" + str(len(self.keys())) + "]" ''' TRAVERSAL STRATEGIES @@ -291,87 +268,68 @@ TRAVERSAL STRATEGIES class TraversalStrategies(object): global_cache = {} - - def __init__(self, traversal_strategies): - self.traversal_strategies = traversal_strategies - return - + def __init__(self, traversal_strategies=None): + self.traversal_strategies = traversal_strategies.traversal_strategies if traversal_strategies is not None else [] + def add_strategies(self, traversal_strategies): + self.traversal_strategies = self.traversal_strategies + traversal_strategies def apply_strategies(self, traversal): for traversal_strategy in self.traversal_strategies: traversal_strategy.apply(traversal) - return +@six.add_metaclass(abc.ABCMeta) class TraversalStrategy(object): - @abstractmethod + @abc.abstractmethod def apply(self, traversal): return ''' -BYTECODE AND TRANSLATOR +BYTECODE ''' class Bytecode(object): def __init__(self, bytecode=None): self.source_instructions = [] self.step_instructions = [] + self.bindings = {} if bytecode is not None: self.source_instructions = list(bytecode.source_instructions) self.step_instructions = list(bytecode.step_instructions) - def add_source(self, source_name, *args): - newArgs = () + instruction = [source_name] for arg in args: - newArgs = newArgs + (Bytecode.__convertArgument(arg),) - self.source_instructions.append((source_name, newArgs)) - return - + instruction.append(self._convertArgument(arg)) + self.source_instructions.append(instruction) def add_step(self, step_name, *args): - newArgs = () + instruction = [step_name] for arg in args: - newArgs = newArgs + (Bytecode.__convertArgument(arg),) - self.step_instructions.append((step_name, newArgs)) - return - - @staticmethod - def __convertArgument(arg): + instruction.append(self._convertArgument(arg)) + self.step_instructions.append(instruction) + def _convertArgument(self,arg): if isinstance(arg, Traversal): + self.bindings.update(arg.bytecode.bindings) return arg.bytecode + elif isinstance(arg, tuple) and 2 == len(arg) and isinstance(arg[0], str): + self.bindings[arg[0]] = arg[1] + return Binding(arg[0],arg[1]) else: return arg - - -TO_JAVA_MAP = {"_global": "global", "_as": "as", "_in": "in", "_and": "and", - "_or": "or", "_is": "is", "_not": "not", "_from": "from", - "Cardinality": "VertexProperty.Cardinality", "Barrier": "SackFunctions.Barrier"} - - -class Translator(object): - def __init__(self, traversal_source, anonymous_traversal, target_language): - self.traversal_source = traversal_source - self.anonymous_traversal = anonymous_traversal - self.target_language = target_language - - @abstractmethod - def translate(self, bytecode): - return - - @abstractmethod def __repr__(self): - return "translator[" + self.traversal_source + ":" + self.target_language + "]" + return (str(self.source_instructions) if len(self.source_instructions) > 0 else "") + \ + (str(self.step_instructions) if len(self.step_instructions) > 0 else "") -class SymbolHelper(object): - @staticmethod - def toJava(symbol): - if (symbol in TO_JAVA_MAP): - return TO_JAVA_MAP[symbol] - else: - return symbol +''' +BINDINGS +''' - @staticmethod - def mapEnum(enum): - if (enum in enumMap): - return enumMap[enum] - else: - return enum +class Bindings(object): + def of(self,key,value): + if not isinstance(key, str): + raise TypeError("Key must be str") + return (key,value) + +class Binding(object): + def __init__(self,key,value): + self.key = key + self.value = value diff --git a/gremlin_python/statics.py b/gremlin_python/statics.py index 12cbb702c22b2366eaf0679fc32b941f46eba030..293ff93a6200ac7084fea76ffae509d45bc4d6d1 100644 --- a/gremlin_python/statics.py +++ b/gremlin_python/statics.py @@ -20,6 +20,7 @@ from aenum import Enum staticMethods = {} staticEnums = {} +default_lambda_language = "gremlin-python" def add_static(key, value): diff --git a/gremlin_python/structure/__init__.py b/gremlin_python/structure/__init__.py index 1c69b5c0e00ef94926514351d5f58116fc30f5c6..7626550738f12ccd5e18b8c06a8d68aaab882066 100644 --- a/gremlin_python/structure/__init__.py +++ b/gremlin_python/structure/__init__.py @@ -16,7 +16,5 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' -from .graph import Graph -from .remote_graph import RemoteGraph __author__ = 'Marko A. Rodriguez (http://markorodriguez.com)' diff --git a/gremlin_python/structure/graph.py b/gremlin_python/structure/graph.py index 60b22113b57b5ac40156ad0a4adaaeded39b6586..22403b653b715b2b8b3c4b3f113e3c74864d9156 100644 --- a/gremlin_python/structure/graph.py +++ b/gremlin_python/structure/graph.py @@ -24,5 +24,67 @@ from gremlin_python.process.traversal import TraversalStrategies class Graph(object): + def __init__(self): + if self.__class__ not in TraversalStrategies.global_cache: + TraversalStrategies.global_cache[self.__class__] = TraversalStrategies() + def traversal(self): return GraphTraversalSource(self, TraversalStrategies.global_cache[self.__class__]) + + def __repr__(self): + return "graph[empty]" + + +class Element(object): + def __init__(self, id, label): + self.id = id + self.label = label + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.id == other.id + + def __hash__(self): + return hash(self.id) + + +class Vertex(Element): + def __init__(self, id, label="vertex"): + Element.__init__(self, id, label) + + def __repr__(self): + return "v[" + str(self.id) + "]" + + +class Edge(Element): + def __init__(self, id, outV, label, inV): + Element.__init__(self, id, label) + self.outV = outV + self.inV = inV + + def __repr__(self): + return "e[" + str(self.id) + "][" + str(self.outV.id) + "-" + self.label + "->" + str(self.inV.id) + "]" + + +class VertexProperty(Element): + def __init__(self, id, label, value): + Element.__init__(self, id, label) + self.value = value + self.key = self.label + + def __repr__(self): + return "vp[" + str(self.label) + "->" + str(self.value)[0:20] + "]" + + +class Property(object): + def __init__(self, key, value): + self.key = key + self.value = value + + def __repr__(self): + return "p[" + str(self.key) + "->" + str(self.value)[0:20] + "]" + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.key == other.key and self.value == other.value + + def __hash__(self): + return hash(self.key) + hash(self.value) diff --git a/gremlin_python/driver/rest_remote_connection.py b/gremlin_python/structure/io/__init__.py similarity index 51% rename from gremlin_python/driver/rest_remote_connection.py rename to gremlin_python/structure/io/__init__.py index 6d967f8268efeb7749209879d6ae86a5e9bbd1f3..7626550738f12ccd5e18b8c06a8d68aaab882066 100644 --- a/gremlin_python/driver/rest_remote_connection.py +++ b/gremlin_python/structure/io/__init__.py @@ -16,28 +16,5 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' -import json -import requests - -from gremlin_python.process.traversal import Traverser -from .remote_connection import RemoteConnection __author__ = 'Marko A. Rodriguez (http://markorodriguez.com)' - - -class RESTRemoteConnection(RemoteConnection): - def __init__(self, url): - RemoteConnection.__init__(self, url) - - def __repr__(self): - return "RESTRemoteConnection[" + self.url + "]" - - def submit(self, target_language, script, bindings): - response = requests.post(self.url, data=json.dumps( - {"gremlin": script, "language": target_language, "bindings": bindings})) - if response.status_code != requests.codes.ok: - raise BaseException(response.text) - results = [] - for x in response.json()['result']['data']: - results.append(Traverser(x, 1)) - return iter(results) diff --git a/gremlin_python/structure/io/graphson.py b/gremlin_python/structure/io/graphson.py new file mode 100644 index 0000000000000000000000000000000000000000..ed3114554061a772f5396f4e2dad55dbecaccb57 --- /dev/null +++ b/gremlin_python/structure/io/graphson.py @@ -0,0 +1,292 @@ +''' +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you 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. +''' + +__author__ = 'Marko A. Rodriguez (http://markorodriguez.com)' + +import json +from abc import abstractmethod +from aenum import Enum +from types import FunctionType + +from gremlin_python import statics +from gremlin_python.process.traversal import Binding +from gremlin_python.process.traversal import Bytecode +from gremlin_python.process.traversal import P +from gremlin_python.process.traversal import Traversal +from gremlin_python.process.traversal import Traverser +from gremlin_python.structure.graph import Edge +from gremlin_python.structure.graph import Property +from gremlin_python.structure.graph import Vertex +from gremlin_python.structure.graph import VertexProperty + + +class long(int): pass +FloatType = float +IntType = int +LongType = long + + + +class GraphSONWriter(object): + @staticmethod + def _dictify(object): + for key in serializers: + if isinstance(object, key): + return serializers[key]._dictify(object) + # list and map are treated as normal json objects (could be isolated serializers) + if isinstance(object, list): + newList = [] + for item in object: + newList.append(GraphSONWriter._dictify(item)) + return newList + elif isinstance(object, dict): + newDict = {} + for key in object: + newDict[GraphSONWriter._dictify(key)] = GraphSONWriter._dictify(object[key]) + return newDict + else: + return object + + @staticmethod + def writeObject(objectData): + return json.dumps(GraphSONWriter._dictify(objectData), separators=(',', ':')) + + +class GraphSONReader(object): + @staticmethod + def _objectify(object): + if isinstance(object, dict): + if _SymbolHelper._TYPE in object: + type = object[_SymbolHelper._TYPE] + if type in deserializers: + return deserializers[type]._objectify(object) + # list and map are treated as normal json objects (could be isolated deserializers) + newDict = {} + for key in object: + newDict[GraphSONReader._objectify(key)] = GraphSONReader._objectify(object[key]) + return newDict + elif isinstance(object, list): + newList = [] + for item in object: + newList.append(GraphSONReader._objectify(item)) + return newList + else: + return object + + @staticmethod + def readObject(jsonData): + return GraphSONReader._objectify(json.loads(jsonData)) + + +''' +SERIALIZERS +''' + + +class GraphSONSerializer(object): + @abstractmethod + def _dictify(self, object): + return object + + +class BytecodeSerializer(GraphSONSerializer): + def _dictify(self, bytecode): + if isinstance(bytecode, Traversal): + bytecode = bytecode.bytecode + dict = {} + sources = [] + for instruction in bytecode.source_instructions: + inst = [] + inst.append(instruction[0]) + for arg in instruction[1:]: + inst.append(GraphSONWriter._dictify(arg)) + sources.append(inst) + steps = [] + for instruction in bytecode.step_instructions: + inst = [] + inst.append(instruction[0]) + for arg in instruction[1:]: + inst.append(GraphSONWriter._dictify(arg)) + steps.append(inst) + if len(sources) > 0: + dict["source"] = sources + if len(steps) > 0: + dict["step"] = steps + return _SymbolHelper.objectify("Bytecode", dict) + + +class TraverserSerializer(GraphSONSerializer): + def _dictify(self, traverser): + return _SymbolHelper.objectify("Traverser", {"value": GraphSONWriter._dictify(traverser.object), + "bulk": GraphSONWriter._dictify(traverser.bulk)}) + + +class EnumSerializer(GraphSONSerializer): + def _dictify(self, enum): + return _SymbolHelper.objectify(_SymbolHelper.toGremlin(type(enum).__name__), + _SymbolHelper.toGremlin(str(enum.name))) + + +class PSerializer(GraphSONSerializer): + def _dictify(self, p): + dict = {} + dict["predicate"] = p.operator + if p.other is None: + dict["value"] = GraphSONWriter._dictify(p.value) + else: + dict["value"] = [GraphSONWriter._dictify(p.value), GraphSONWriter._dictify(p.other)] + return _SymbolHelper.objectify("P", dict) + + +class BindingSerializer(GraphSONSerializer): + def _dictify(self, binding): + dict = {} + dict["key"] = binding.key + dict["value"] = GraphSONWriter._dictify(binding.value) + return _SymbolHelper.objectify("Binding", dict) + + +class LambdaSerializer(GraphSONSerializer): + def _dictify(self, lambdaObject): + lambdaResult = lambdaObject() + dict = {} + script = lambdaResult if isinstance(lambdaResult, str) else lambdaResult[0] + language = statics.default_lambda_language if isinstance(lambdaResult, str) else lambdaResult[1] + dict["script"] = script + dict["language"] = language + if language == "gremlin-jython" or language == "gremlin-python": + if not script.strip().startswith("lambda"): + script = "lambda " + script + dict["script"] = script + dict["arguments"] = eval(dict["script"]).func_code.co_argcount + else: + dict["arguments"] = -1 + return _SymbolHelper.objectify("Lambda", dict) + + +class NumberSerializer(GraphSONSerializer): + def _dictify(self, number): + if isinstance(number, bool): # python thinks that 0/1 integers are booleans + return number + elif isinstance(number, int): + return _SymbolHelper.objectify("Int64", number) + elif isinstance(number, float): + return _SymbolHelper.objectify("Float", number) + else: + return number + + +''' +DESERIALIZERS +''' + + +class GraphSONDeserializer(object): + @abstractmethod + def _objectify(self, dict): + return dict + + +class TraverserDeserializer(GraphSONDeserializer): + def _objectify(self, dict): + return Traverser(GraphSONReader._objectify(dict[_SymbolHelper._VALUE]["value"]), + GraphSONReader._objectify(dict[_SymbolHelper._VALUE]["bulk"])) + + +class NumberDeserializer(GraphSONDeserializer): + def _objectify(self, dict): + type = dict[_SymbolHelper._TYPE] + value = dict[_SymbolHelper._VALUE] + if type == "g:Int32": + return int(value) + elif type == "g:Int64": + return long(value) + else: + return float(value) + + +class VertexDeserializer(GraphSONDeserializer): + def _objectify(self, dict): + value = dict[_SymbolHelper._VALUE] + return Vertex(GraphSONReader._objectify(value["id"]), value["label"] if "label" in value else "") + + +class EdgeDeserializer(GraphSONDeserializer): + def _objectify(self, dict): + value = dict[_SymbolHelper._VALUE] + return Edge(GraphSONReader._objectify(value["id"]), + Vertex(GraphSONReader._objectify(value["outV"]), ""), + value["label"] if "label" in value else "vertex", + Vertex(GraphSONReader._objectify(value["inV"]), "")) + + +class VertexPropertyDeserializer(GraphSONDeserializer): + def _objectify(self, dict): + value = dict[_SymbolHelper._VALUE] + return VertexProperty(GraphSONReader._objectify(value["id"]), value["label"], + GraphSONReader._objectify(value["value"])) + + +class PropertyDeserializer(GraphSONDeserializer): + def _objectify(self, dict): + value = dict[_SymbolHelper._VALUE] + return Property(value["key"], GraphSONReader._objectify(value["value"])) + + +class _SymbolHelper(object): + symbolMap = {"global_": "global", "as_": "as", "in_": "in", "and_": "and", + "or_": "or", "is_": "is", "not_": "not", "from_": "from", + "set_": "set", "list_": "list", "all_": "all"} + + _TYPE = "@type" + _VALUE = "@value" + + @staticmethod + def toGremlin(symbol): + return _SymbolHelper.symbolMap[symbol] if symbol in _SymbolHelper.symbolMap else symbol + + @staticmethod + def objectify(type, value, prefix="g"): + return {_SymbolHelper._TYPE: prefix + ":" + type, _SymbolHelper._VALUE: value} + + +serializers = { + Traversal: BytecodeSerializer(), + Traverser: TraverserSerializer(), + Bytecode: BytecodeSerializer(), + Binding: BindingSerializer(), + P: PSerializer(), + Enum: EnumSerializer(), + FunctionType: LambdaSerializer(), + LongType: NumberSerializer(), + IntType: NumberSerializer(), + FloatType: NumberSerializer() +} + +deserializers = { + "g:Traverser": TraverserDeserializer(), + "g:Int32": NumberDeserializer(), + "g:Int64": NumberDeserializer(), + "g:Float": NumberDeserializer(), + "g:Double": NumberDeserializer(), + "g:Vertex": VertexDeserializer(), + "g:Edge": EdgeDeserializer(), + "g:VertexProperty": VertexPropertyDeserializer(), + "g:Property": PropertyDeserializer() +} diff --git a/gremlin_python/structure/remote_graph.py b/gremlin_python/structure/remote_graph.py deleted file mode 100644 index 778895b24a4438477a328b383bedc900935eae5e..0000000000000000000000000000000000000000 --- a/gremlin_python/structure/remote_graph.py +++ /dev/null @@ -1,47 +0,0 @@ -''' -Licensed to the Apache Software Foundation (ASF) under one -or more contributor license agreements. See the NOTICE file -distributed with this work for additional information -regarding copyright ownership. The ASF licenses this file -to you 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. -''' - -__author__ = 'Marko A. Rodriguez (http://markorodriguez.com)' - -from .graph import Graph -from gremlin_python.process.traversal import TraversalStrategies -from gremlin_python.process.traversal import TraversalStrategy - - -class RemoteGraph(Graph): - def __init__(self, translator, remote_connection): - TraversalStrategies.global_cache[self.__class__] = TraversalStrategies([RemoteStrategy()]) - self.translator = translator - self.remote_connection = remote_connection - - def __repr__(self): - return "remotegraph[" + self.remote_connection.url + "]" - - -class RemoteStrategy(TraversalStrategy): - def apply(self, traversal): - if not (traversal.graph.__class__.__name__ == "RemoteGraph"): - raise BaseException( - "RemoteStrategy can only be used with a RemoteGraph: " + traversal.graph.__class__.__name__) - if traversal.results is None: - traversal.results = traversal.graph.remote_connection.submit( - traversal.graph.translator.target_language, # script engine - traversal.graph.translator.translate(traversal.bytecode), # script - traversal.bindings) # bindings - return diff --git a/requirements.txt b/requirements.txt index e11f6e8a34eff7f8e5228f7d9e6653723bdf71f9..1caa67af2382c38fc052e781ef9c0d02d9f1491a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,8 +10,12 @@ inflection==0.3.1 Jinja2==2.8 MarkupSafe==0.23 multidict==1.2.0 +PyYAML==3.12 Pygments==2.1.3 pytz==2016.6.1 +pytest==3.0.2 +pytest-asyncio==0.5.0 +pytest-runner==2.9 six==1.10.0 snowballstemmer==1.2.1 Sphinx==1.4.5 diff --git a/setup.cfg b/setup.cfg index b7e478982ccf9ab1963c74e1084dfccb6e42c583..cfcbfea95cdd0b95dd262f7a44e723e8df284400 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,5 @@ [aliases] test=pytest + +[tool:pytest] +norecursedirs = '.*', 'build', 'dist', 'CVS', '_darcs', '{arch}', '*.egg' lib lib64 diff --git a/setup.py b/setup.py index 84655e153b7b4fe2a6fa7ee56c32e385fdc199d5..db6494c410e87ce07d9db3d40c77fcec84c5f8aa 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup setup( name="goblin", - version="1.0.0a3", + version="1.0.0b1", url="", license="AGPL", author="davebshow", @@ -15,7 +15,8 @@ setup( install_requires=[ "aenum==1.4.5", "aiohttp==0.22.1", - "inflection==0.3.1" + "inflection==0.3.1", + "PyYAML==3.12" ], test_suite="tests", setup_requires=['pytest-runner'], diff --git a/tests/config/config.json b/tests/config/config.json new file mode 100644 index 0000000000000000000000000000000000000000..12e1ba733bbb8350713d31ba59a34faf33cdc508 --- /dev/null +++ b/tests/config/config.json @@ -0,0 +1,13 @@ +{ + "ssl_password":"", + "port":8182, + "ssl_certfile":"", + "scheme":"wss", + "hosts":[ + "localhost" + ], + "username":"dave", + "password":"mypass", + "ssl_keyfile":"", + "message_serializer":"goblin.driver.GraphSONMessageSerializer" +} diff --git a/tests/config/config.yml b/tests/config/config.yml new file mode 100644 index 0000000000000000000000000000000000000000..a02338a20c434836141d16582ba2d99196f9f663 --- /dev/null +++ b/tests/config/config.yml @@ -0,0 +1,4 @@ +scheme: 'wss' +hosts: ['localhost'] +port: 8183 +message_serializer: 'goblin.driver.GraphSONMessageSerializer' diff --git a/tests/conftest.py b/tests/conftest.py index 288d745ccfa4f7e83067eb3c885ff03454275031..9468d106aa32532b57e9ab5fbf4f35d06daeff12 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,12 +14,18 @@ # # You should have received a copy of the GNU Affero General Public License # along with Goblin. If not, see <http://www.gnu.org/licenses/>. - +import asyncio import pytest -from goblin import create_app, driver, element, properties, Cardinality +from goblin import Goblin, driver, element, properties, Cardinality +from goblin.driver import pool, serializer from gremlin_python import process +def pytest_generate_tests(metafunc): + if 'cluster' in metafunc.fixturenames: + metafunc.parametrize("cluster", ['c1', 'c2'], indirect=True) + + class HistoricalName(element.VertexProperty): notes = properties.Property(properties.String) year = properties.Property(properties.Integer) # this is dumb but handy @@ -43,7 +49,6 @@ class Place(element.Vertex): properties.Integer, card=Cardinality.set) - class Knows(element.Edge): __label__ = 'knows' notes = properties.Property(properties.String, default='N/A') @@ -60,34 +65,50 @@ def gremlin_server(): @pytest.fixture def unused_server_url(unused_tcp_port): - return 'http://localhost:{}/'.format(unused_tcp_port) + return 'http://localhost:{}/gremlin'.format(unused_tcp_port) @pytest.fixture -def connection(gremlin_server, event_loop): +def connection(event_loop): conn = event_loop.run_until_complete( - gremlin_server.open("http://localhost:8182/", event_loop)) + driver.Connection.open( + "http://localhost:8182/gremlin", event_loop, + message_serializer=serializer.GraphSONMessageSerializer)) return conn @pytest.fixture -def remote_graph(connection): - translator = process.GroovyTranslator('g') - return driver.AsyncRemoteGraph(translator, connection) +def connection_pool(event_loop): + return pool.ConnectionPool( + "http://localhost:8182/gremlin", event_loop, None, '', '', 4, 1, 16, + 64, None, serializer.GraphSONMessageSerializer) @pytest.fixture -def app(event_loop): - app = event_loop.run_until_complete( - create_app("http://localhost:8182/", event_loop)) - app.register(Person, Place, Knows, LivesIn) - return app +def cluster(request, event_loop): + if request.param == 'c1': + cluster = driver.Cluster( + event_loop, + message_serializer=serializer.GraphSONMessageSerializer) + elif request.param == 'c2': + cluster = driver.Cluster( + event_loop, + message_serializer=serializer.GraphSON2MessageSerializer) + return cluster @pytest.fixture -def session(event_loop, app): - session = event_loop.run_until_complete(app.session()) - return session +def remote_graph(): + return driver.AsyncGraph() + + +@pytest.fixture +def app(request, event_loop): + app = event_loop.run_until_complete( + Goblin.open(event_loop, aliases={'g': 'g'})) + + app.register(Person, Place, Knows, LivesIn) + return app # Instance fixtures @@ -142,6 +163,11 @@ def place_name(): # Class fixtures +@pytest.fixture +def cluster_class(event_loop): + return driver.Cluster + + @pytest.fixture def string_class(): return properties.String diff --git a/tests/test_app.py b/tests/test_app.py index cf2e11d9747a292a8faf953e410f95a512a13bf4..89067f1a3dcdab29121562b75350b88ece3cc8d5 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -15,29 +15,40 @@ # You should have received a copy of the GNU Affero General Public License # along with Goblin. If not, see <http://www.gnu.org/licenses/>. +import pytest + from goblin import element +from goblin.driver import serializer from gremlin_python import process -def test_registry(app, person, place, knows, lives_in): +@pytest.mark.asyncio +async def test_registry(app, person, place, knows, lives_in): assert len(app.vertices) == 2 assert len(app.edges) == 2 assert person.__class__ == app.vertices['person'] assert place.__class__ == app.vertices['place'] assert knows.__class__ == app.edges['knows'] assert lives_in.__class__ == app.edges['lives_in'] + await app.close() -def test_registry_defaults(app): +@pytest.mark.asyncio +async def test_registry_defaults(app): vertex = app.vertices['unregistered'] assert isinstance(vertex(), element.Vertex) edge = app.edges['unregistered'] assert isinstance(edge(), element.Edge) + await app.close() - -def test_features(app): - assert app._features +@pytest.mark.asyncio +async def test_transaction_discovery(app): + assert app._transactions is not None + await app.close() -def test_translator(app): - assert isinstance(app.translator, process.GroovyTranslator) +@pytest.mark.asyncio +async def test_aliases(app): + session = await app.session() + assert session._conn._aliases == {'g': 'g'} + await app.close() diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000000000000000000000000000000000000..889ffe6fad09cb1350544992f7402ddedf08b74a --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,64 @@ +# Copyright 2016 ZEROFAIL +# +# This file is part of Goblin. +# +# Goblin is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Goblin is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Goblin. If not, see <http://www.gnu.org/licenses/>. + +import asyncio +import uuid + +import pytest + +from goblin.driver.server import GremlinServer + + +@pytest.mark.asyncio +async def test_client_auto_release(cluster): + client = await cluster.connect() + resp = await client.submit(gremlin="1 + 1") + async for msg in resp: + pass + await asyncio.sleep(0) + host = cluster._hosts.popleft() + assert len(host._pool._available) == 1 + await host.close() + + +@pytest.mark.asyncio +async def test_alias(cluster): + client = await cluster.connect() + aliased_client = client.alias({"g": "g1"}) + assert aliased_client._aliases == {"g": "g1"} + assert aliased_client._cluster is client._cluster + assert aliased_client._loop is client._loop + await cluster.close() + + +@pytest.mark.asyncio +async def test_sessioned_client(cluster): + session = str(uuid.uuid4()) + client = await cluster.connect(session=session) + assert isinstance(client.cluster, GremlinServer) + resp = await client.submit(gremlin="v = g.addV('person').property('name', 'joe').next(); v") + async for msg in resp: + try: + assert msg['properties']['name'][0]['value'] == 'joe' + except KeyError: + assert msg['properties']['name'][0]['@value']['value'] == 'joe' + + resp = await client.submit(gremlin="g.V(v.id).values('name')") + + async for msg in resp: + assert msg == 'joe' + await cluster.close() diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000000000000000000000000000000000000..72d4bcf4189a5c03e87f25ab43bb6fc543d1ae9c --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,128 @@ +# Copyright 2016 ZEROFAIL +# +# This file is part of Goblin. +# +# Goblin is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Goblin is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Goblin. If not, see <http://www.gnu.org/licenses/>. +import os + +import pytest + +from goblin import driver, exception + + +dirname = os.path.dirname(os.path.dirname(__file__)) + + +def test_cluster_default_config(cluster): + assert cluster.config['scheme'] == 'ws' + assert cluster.config['hosts'] == ['localhost'] + assert cluster.config['port'] == 8182 + assert cluster.config['ssl_certfile'] == '' + assert cluster.config['ssl_keyfile'] == '' + assert cluster.config['ssl_password'] == '' + assert cluster.config['username'] == '' + assert cluster.config['password'] == '' + + +@pytest.mark.asyncio +async def test_app_default_config(app): + assert app.config['scheme'] == 'ws' + assert app.config['hosts'] == ['localhost'] + assert app.config['port'] == 8182 + assert app.config['ssl_certfile'] == '' + assert app.config['ssl_keyfile'] == '' + assert app.config['ssl_password'] == '' + assert app.config['username'] == '' + assert app.config['password'] == '' + assert issubclass(app.config['message_serializer'], + driver.GraphSONMessageSerializer) + await app.close() + + +def test_cluster_custom_config(event_loop, cluster_class): + cluster = cluster_class(event_loop, username='dave', password='mypass', + hosts=['127.0.0.1']) + assert cluster.config['scheme'] == 'ws' + assert cluster.config['hosts'] == ['127.0.0.1'] + assert cluster.config['port'] == 8182 + assert cluster.config['ssl_certfile'] == '' + assert cluster.config['ssl_keyfile'] == '' + assert cluster.config['ssl_password'] == '' + assert cluster.config['username'] == 'dave' + assert cluster.config['password'] == 'mypass' + assert issubclass(cluster.config['message_serializer'], + driver.GraphSONMessageSerializer) + + +def test_cluster_config_from_json(event_loop, cluster_class): + cluster = cluster_class(event_loop) + cluster.config_from_file(dirname + '/tests/config/config.json') + assert cluster.config['scheme'] == 'wss' + assert cluster.config['hosts'] == ['localhost'] + assert cluster.config['port'] == 8182 + assert cluster.config['ssl_certfile'] == '' + assert cluster.config['ssl_keyfile'] == '' + assert cluster.config['ssl_password'] == '' + assert cluster.config['username'] == 'dave' + assert cluster.config['password'] == 'mypass' + + assert issubclass(cluster.config['message_serializer'], + driver.GraphSONMessageSerializer) + + +def test_cluster_config_from_yaml(event_loop, cluster_class): + cluster = cluster_class(event_loop) + cluster.config_from_file(dirname + '/tests/config/config.yml') + assert cluster.config['scheme'] == 'wss' + assert cluster.config['hosts'] == ['localhost'] + assert cluster.config['port'] == 8183 + assert cluster.config['ssl_certfile'] == '' + assert cluster.config['ssl_keyfile'] == '' + assert cluster.config['ssl_password'] == '' + assert cluster.config['username'] == '' + assert cluster.config['password'] == '' + assert issubclass(cluster.config['message_serializer'], + driver.GraphSONMessageSerializer) + +@pytest.mark.asyncio +async def test_app_config_from_json(app): + app.config_from_file(dirname + '/tests/config/config.json') + assert app.config['scheme'] == 'wss' + assert app.config['hosts'] == ['localhost'] + assert app.config['port'] == 8182 + assert app.config['ssl_certfile'] == '' + assert app.config['ssl_keyfile'] == '' + assert app.config['ssl_password'] == '' + assert app.config['username'] == 'dave' + assert app.config['password'] == 'mypass' + + assert issubclass(app.config['message_serializer'], + driver.GraphSONMessageSerializer) + await app.close() + + +@pytest.mark.asyncio +async def test_app_config_from_yaml(app): + app.config_from_file(dirname + '/tests/config/config.yml') + assert app.config['scheme'] == 'wss' + assert app.config['hosts'] == ['localhost'] + assert app.config['port'] == 8183 + assert app.config['ssl_certfile'] == '' + assert app.config['ssl_keyfile'] == '' + assert app.config['ssl_password'] == '' + assert app.config['username'] == '' + assert app.config['password'] == '' + assert issubclass(app.config['message_serializer'], + driver.GraphSONMessageSerializer) + await app.close() diff --git a/tests/test_driver.py b/tests/test_connection.py similarity index 73% rename from tests/test_driver.py rename to tests/test_connection.py index d62b1f132daecfd6e15df15426da751bbf76ebd3..5ed44f641decd3aa836381796bed489c6ffb7fa1 100644 --- a/tests/test_driver.py +++ b/tests/test_connection.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with Goblin. If not, see <http://www.gnu.org/licenses/>. - +import asyncio import pytest from goblin import exception @@ -40,7 +40,7 @@ async def test_conn_context_manager(connection): @pytest.mark.asyncio async def test_submit(connection): async with connection: - stream = await connection.submit("1 + 1") + stream = await connection.submit(gremlin="1 + 1") results = [] async for msg in stream: results.append(msg) @@ -52,7 +52,8 @@ async def test_submit(connection): async def test_204_empty_stream(connection): resp = False async with connection: - stream = await connection.submit('g.V().has("unlikely", "even less likely")') + stream = await connection.submit( + gremlin='g.V().has("unlikely", "even less likely")') async for msg in stream: resp = True assert not resp @@ -61,7 +62,7 @@ async def test_204_empty_stream(connection): @pytest.mark.asyncio async def test_server_error(connection): async with connection: - stream = await connection.submit('g. V jla;sdf') + stream = await connection.submit(gremlin='g. V jla;sdf') with pytest.raises(exception.GremlinServerError): async for msg in stream: pass @@ -70,15 +71,16 @@ async def test_server_error(connection): @pytest.mark.asyncio async def test_cant_connect(event_loop, gremlin_server, unused_server_url): with pytest.raises(Exception): - await gremlin_server.open(unused_server_url, event_loop) + await gremlin_server.get_connection(unused_server_url, event_loop) @pytest.mark.asyncio async def test_resp_queue_removed_from_conn(connection): async with connection: - stream = await connection.submit("1 + 1") + stream = await connection.submit(gremlin="1 + 1") async for msg in stream: pass + await asyncio.sleep(0) assert stream._response_queue not in list( connection._response_queues.values()) @@ -86,7 +88,16 @@ async def test_resp_queue_removed_from_conn(connection): @pytest.mark.asyncio async def test_stream_done(connection): async with connection: - stream = await connection.submit("1 + 1") + stream = await connection.submit(gremlin="1 + 1") async for msg in stream: pass - assert stream._done + assert stream.done + +@pytest.mark.asyncio +async def test_connection_response_timeout(connection): + async with connection: + connection._response_timeout = 0.0000001 + with pytest.raises(exception.ResponseTimeoutError): + stream = await connection.submit(gremlin="1 + 1") + async for msg in stream: + pass diff --git a/tests/test_connection_protocol.py b/tests/test_connection_protocol.py new file mode 100644 index 0000000000000000000000000000000000000000..227cdec02c2580772ca3a5a246b6196ad4491f5d --- /dev/null +++ b/tests/test_connection_protocol.py @@ -0,0 +1,137 @@ +# Copyright 2016 ZEROFAIL +# +# This file is part of Goblin. +# +# Goblin is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Goblin is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Goblin. If not, see <http://www.gnu.org/licenses/>. + +import asyncio +import uuid +import pytest + +from goblin import exception +from goblin.driver import serializer + + +@pytest.mark.asyncio +async def test_eval(remote_graph, connection): + async with connection: + connection._message_serializer = serializer.GraphSON2MessageSerializer() + g = remote_graph.traversal() + traversal = "g.addV('person').property('name', 'leifur')" + resp = await connection.submit( + processor='', op='eval', gremlin=traversal, scriptEvalTimeout=1) + + async for msg in resp: + assert msg['label'] == 'person' + + +@pytest.mark.asyncio +async def test_bytecode(remote_graph, connection): + async with connection: + connection._message_serializer = serializer.GraphSON2MessageSerializer() + g = remote_graph.traversal() + traversal = g.addV('person').property('name', 'leifur') + resp = await connection.submit( + processor='traversal', op='bytecode', gremlin=traversal.bytecode) + async for msg in resp: + vid = msg.id + traversal = g.V(vid).label() + resp = await connection.submit( + processor='traversal', op='bytecode', gremlin=traversal.bytecode) + async for msg in resp: + assert msg == 'person' + traversal = g.V(vid).name + resp = await connection.submit( + processor='traversal', op='bytecode', gremlin=traversal.bytecode) + async for msg in resp: + assert msg == 'leifur' + + +@pytest.mark.asyncio +async def test_side_effects(remote_graph, connection): + async with connection: + connection._message_serializer = serializer.GraphSON2MessageSerializer() + g = remote_graph.traversal() + # Add some nodes + traversal = g.addV('person').property('name', 'leifur') + resp = await connection.submit( + processor='traversal', op='bytecode', gremlin=traversal.bytecode) + async for msg in resp: + pass + traversal = g.addV('person').property('name', 'dave') + resp = await connection.submit( + processor='traversal', op='bytecode', gremlin=traversal.bytecode) + async for msg in resp: + pass + traversal = g.addV('person').property('name', 'jonathan') + resp = await connection.submit( + processor='traversal', op='bytecode', gremlin=traversal.bytecode) + async for msg in resp: + pass + + # # Make a query + traversal = g.V().aggregate('a').aggregate('b') + resp = await connection.submit( + processor='traversal', op='bytecode', gremlin=traversal.bytecode) + request_id = resp.request_id + async for msg in resp: + pass + resp = await connection.submit(processor='traversal', op='keys', + sideEffect=request_id) + keys = [] + async for msg in resp: + keys.append(msg) + assert keys == ['a', 'b'] + + resp = await connection.submit(processor='traversal', op='gather', + sideEffect=request_id, + sideEffectKey='a') + side_effects = [] + async for msg in resp: + side_effects.append(msg) + assert side_effects + + # Close isn't implmented yet + # resp = await connection.submit(processor='traversal', op='close', + # sideEffect=request_id) + # async for msg in resp: + # print(msg) + + +@pytest.mark.asyncio +async def test_session(connection): + async with connection: + connection._message_serializer = serializer.GraphSON2MessageSerializer() + session = str(uuid.uuid4()) + resp = await connection.submit( + gremlin="v = g.addV('person').property('name', 'unused_name').next(); v", + processor='session', + op='eval', + session=session) + async for msg in resp: + assert msg['label'] == 'person' + resp = await connection.submit( + gremlin="v.values('name')", + processor='session', + op='eval', + session=session) + async for msg in resp: + assert msg == 'unused_name' + # Close isnt' implemented yet + # resp = await connection.submit( + # processor='session', + # op='close', + # session=session) + # async for msg in resp: + # print(msg) diff --git a/tests/test_graph.py b/tests/test_graph.py index 415f6f9a37b887b8884b7d838400d268ab9d7322..c4a9ec7f71df191045641217ced28904475602a1 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -17,40 +17,64 @@ import pytest -from gremlin_python import process - - -@pytest.mark.asyncio -async def test_close_graph(remote_graph): - remote_connection = remote_graph.remote_connection - await remote_graph.close() - assert remote_connection.closed - +from goblin.driver import serializer -@pytest.mark.asyncio -async def test_conn_context_manager(remote_graph): - remote_connection = remote_graph.remote_connection - async with remote_graph: - assert not remote_graph.remote_connection.closed - assert remote_connection.closed +from gremlin_python import process @pytest.mark.asyncio -async def test_generate_traversal(remote_graph): - async with remote_graph: - traversal = remote_graph.traversal().V().hasLabel(('v1', 'person')) - assert isinstance(traversal, process.GraphTraversal) - assert traversal.bindings['v1'] == 'person' +async def test_generate_traversal(remote_graph, connection): + async with connection: + g = remote_graph.traversal().withRemote(connection) + traversal = g.V().hasLabel(('v1', 'person')) + assert isinstance(traversal, process.graph_traversal.GraphTraversal) + assert traversal.bytecode.bindings['v1'] == 'person' @pytest.mark.asyncio -async def test_submit_traversal(remote_graph): - async with remote_graph: - g = remote_graph.traversal() - resp = await g.addV('person').property('name', 'leifur').next() - leif = await resp.fetch_data() +async def test_submit_traversal(remote_graph, connection): + async with connection: + g = remote_graph.traversal().withRemote(connection) + resp = g.addV('person').property('name', 'leifur') + leif = await resp.next() + resp.traversers.close() assert leif['properties']['name'][0]['value'] == 'leifur' assert leif['label'] == 'person' - resp = await g.V(leif['id']).drop().next() - none = await resp.fetch_data() + resp = g.V(leif['id']).drop() + none = await resp.next() assert none is None + + +@pytest.mark.asyncio +async def test_side_effects(remote_graph, connection): + async with connection: + connection._message_serializer = serializer.GraphSON2MessageSerializer() + g = remote_graph.traversal().withRemote(connection) + # create some nodes + resp = g.addV('person').property('name', 'leifur') + leif = await resp.next() + resp.traversers.close() + resp = g.addV('person').property('name', 'dave') + dave = await resp.next() + resp.traversers.close() + resp = g.addV('person').property('name', 'jon') + jonthan = await resp.next() + resp.traversers.close() + traversal = g.V().aggregate('a').aggregate('b') + async for msg in traversal: + pass + keys = [] + resp = await traversal.side_effects.keys() + async for msg in resp: + keys.append(msg) + assert keys == ['a', 'b'] + side_effects = [] + resp = await traversal.side_effects.get('a') + async for msg in resp: + side_effects.append(msg) + assert side_effects + side_effects = [] + resp = await traversal.side_effects.get('b') + async for msg in resp: + side_effects.append(msg) + assert side_effects diff --git a/tests/test_pool.py b/tests/test_pool.py new file mode 100644 index 0000000000000000000000000000000000000000..6f9e65b7738492189a1638431a38b97b4953b510 --- /dev/null +++ b/tests/test_pool.py @@ -0,0 +1,105 @@ +# Copyright 2016 ZEROFAIL +# +# This file is part of Goblin. +# +# Goblin is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Goblin is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Goblin. If not, see <http://www.gnu.org/licenses/>. + +import asyncio +import pytest + + +@pytest.mark.asyncio +async def test_pool_init(connection_pool): + await connection_pool.init_pool() + assert len(connection_pool._available) == 1 + await connection_pool.close() + + +@pytest.mark.asyncio +async def test_acquire_release(connection_pool): + conn = await connection_pool.acquire() + assert not len(connection_pool._available) + assert len(connection_pool._acquired) == 1 + assert conn.times_acquired == 1 + connection_pool.release(conn) + assert len(connection_pool._available) == 1 + assert not len(connection_pool._acquired) + assert not conn.times_acquired + await connection_pool.close() + + +@pytest.mark.asyncio +async def test_acquire_multiple(connection_pool): + conn1 = await connection_pool.acquire() + conn2 = await connection_pool.acquire() + assert not conn1 is conn2 + assert len(connection_pool._acquired) == 2 + await connection_pool.close() + + +@pytest.mark.asyncio +async def test_share(connection_pool): + connection_pool._max_conns = 1 + conn1 = await connection_pool.acquire() + conn2 = await connection_pool.acquire() + assert conn1 is conn2 + assert conn1.times_acquired == 2 + await connection_pool.close() + + +@pytest.mark.asyncio +async def test_acquire_multiple_and_share(connection_pool): + connection_pool._max_conns = 2 + connection_pool._max_times_acquired = 2 + conn1 = await connection_pool.acquire() + conn2 = await connection_pool.acquire() + assert not conn1 is conn2 + conn3 = await connection_pool.acquire() + conn4 = await connection_pool.acquire() + assert not conn3 is conn4 + assert conn3 is conn1 + assert conn4 is conn2 + await connection_pool.close() + + +@pytest.mark.asyncio +async def test_max_acquired(connection_pool): + connection_pool._max_conns = 2 + connection_pool._max_times_acquired = 2 + conn1 = await connection_pool.acquire() + conn2 = await connection_pool.acquire() + conn3 = await connection_pool.acquire() + conn4 = await connection_pool.acquire() + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(connection_pool.acquire(), timeout=0.1) + await connection_pool.close() + + +@pytest.mark.asyncio +async def test_release_notify(connection_pool): + connection_pool._max_conns = 2 + connection_pool._max_times_acquired = 2 + conn1 = await connection_pool.acquire() + conn2 = await connection_pool.acquire() + conn3 = await connection_pool.acquire() + conn4 = await connection_pool.acquire() + + async def release(conn): + conn.release() + + results = await asyncio.gather( + *[connection_pool.acquire(), release(conn4)]) + conn4 = results[0] + assert conn4 is conn2 + await connection_pool.close() diff --git a/tests/test_properties.py b/tests/test_properties.py index 3a5c498cbfc09cbb5006c59e618036e4613523b9..d2aad7d5cddb0af84d8193d568463ef52345b1f6 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -57,7 +57,7 @@ def test_setattr_validation(person): def test_set_id_throws(person): with pytest.raises(exception.ElementError): person.id = 1 - + def test_id_class_attr_throws(person_class): with pytest.raises(exception.ElementError): diff --git a/tests/test_session.py b/tests/test_session.py index c29ff0c6b8a263da287376919c078dd5f4d6482f..2200eb3db0a2423229b398b6c9a63f82c864363b 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -20,328 +20,348 @@ import pytest from goblin import element -from goblin.traversal import bindprop +from goblin.session import bindprop +from gremlin_python.process.translator import GroovyTranslator from gremlin_python.process.graph_traversal import __ -@pytest.mark.asyncio -async def test_session_close(session): - assert not session.conn.closed - await session.close() - assert session.conn.closed - - -@pytest.mark.asyncio -async def test_session_ctxt_mngr(session): - async with session: - assert not session.conn.closed - assert session.conn.closed +def test_bindprop(person_class): + db_val, (binding, val) = bindprop(person_class, 'name', 'dave', binding='n1') + assert db_val == 'name' + assert binding == 'n1' + assert val == 'dave' class TestCreationApi: @pytest.mark.asyncio - async def test_create_vertex(self, session, person_class): - async with session: - jon = person_class() - jon.name = 'jonathan' - jon.age = 38 - leif = person_class() - leif.name = 'leifur' - leif.age = 28 - session.add(jon, leif) - assert not hasattr(jon, 'id') - assert not hasattr(leif, 'id') - await session.flush() - assert hasattr(jon, 'id') - assert session.current[jon.id] is jon - assert jon.name == 'jonathan' - assert jon.age == 38 - assert hasattr(leif, 'id') - assert session.current[leif.id] is leif - assert leif.name == 'leifur' - assert leif.age == 28 + async def test_create_vertex(self, app, person_class): + session = await app.session() + jon = person_class() + jon.name = 'jonathan' + jon.age = 38 + leif = person_class() + leif.name = 'leifur' + leif.age = 28 + session.add(jon, leif) + assert not hasattr(jon, 'id') + assert not hasattr(leif, 'id') + await session.flush() + assert hasattr(jon, 'id') + assert session.current[jon.id] is jon + assert jon.name == 'jonathan' + assert jon.age == 38 + assert hasattr(leif, 'id') + assert session.current[leif.id] is leif + assert leif.name == 'leifur' + assert leif.age == 28 + await app.close() @pytest.mark.asyncio - async def test_create_edge(self, session, person_class, place_class, + async def test_create_edge(self, app, person_class, place_class, lives_in_class): - async with session: - jon = person_class() - jon.name = 'jonathan' - jon.age = 38 - montreal = place_class() - montreal.name = 'Montreal' - lives_in = lives_in_class(jon, montreal) - session.add(jon, montreal, lives_in) - await session.flush() - assert hasattr(lives_in, 'id') - assert session.current[lives_in.id] is lives_in - assert lives_in.source is jon - assert lives_in.target is montreal - assert lives_in.source.__label__ == 'person' - assert lives_in.target.__label__ == 'place' + session = await app.session() + jon = person_class() + jon.name = 'jonathan' + jon.age = 38 + montreal = place_class() + montreal.name = 'Montreal' + lives_in = lives_in_class(jon, montreal) + session.add(jon, montreal, lives_in) + await session.flush() + assert hasattr(lives_in, 'id') + assert session.current[lives_in.id] is lives_in + assert lives_in.source is jon + assert lives_in.target is montreal + assert lives_in.source.__label__ == 'person' + assert lives_in.target.__label__ == 'place' + await app.close() @pytest.mark.asyncio - async def test_create_edge_no_source(self, session, lives_in, person): - async with session: - lives_in.source = person - with pytest.raises(Exception): - await session.save(lives_in) + async def test_create_edge_no_source(self, app, lives_in, person): + session = await app.session() + lives_in.source = person + with pytest.raises(Exception): + await session.save(lives_in) + await app.close() @pytest.mark.asyncio - async def test_create_edge_no_target(self, session, lives_in, place): - async with session: - lives_in.target = place - with pytest.raises(Exception): - await session.save(lives_in) + async def test_create_edge_no_target(self, app, lives_in, place): + session = await app.session() + lives_in.target = place + with pytest.raises(Exception): + await session.save(lives_in) + await app.close() @pytest.mark.asyncio - async def test_create_edge_no_source_target(self, session, lives_in): - async with session: - with pytest.raises(Exception): - await session.save(lives_in) + async def test_create_edge_no_source_target(self, app, lives_in): + session = await app.session() + with pytest.raises(Exception): + await session.save(lives_in) + await app.close() @pytest.mark.asyncio - async def test_get_vertex(self, session, person_class): - async with session: - jon = person_class() - jon.name = 'jonathan' - jon.age = 38 - await session.save(jon) - jid = jon.id - result = await session.get_vertex(jon) - assert result.id == jid - assert result is jon + async def test_get_vertex(self, app, person_class): + session = await app.session() + jon = person_class() + jon.name = 'jonathan' + jon.age = 38 + await session.save(jon) + jid = jon.id + result = await session.get_vertex(jon) + assert result.id == jid + assert result is jon + await app.close() @pytest.mark.asyncio - async def test_get_edge(self, session, person_class, place_class, + async def test_get_edge(self, app, person_class, place_class, lives_in_class): - async with session: - jon = person_class() - jon.name = 'jonathan' - jon.age = 38 - montreal = place_class() - montreal.name = 'Montreal' - lives_in = lives_in_class(jon, montreal) - session.add(jon, montreal, lives_in) - await session.flush() - lid = lives_in.id - result = await session.get_edge(lives_in) - assert result.id == lid - assert result is lives_in + session = await app.session() + jon = person_class() + jon.name = 'jonathan' + jon.age = 38 + montreal = place_class() + montreal.name = 'Montreal' + lives_in = lives_in_class(jon, montreal) + session.add(jon, montreal, lives_in) + await session.flush() + lid = lives_in.id + result = await session.get_edge(lives_in) + assert result.id == lid + assert result is lives_in + await app.close() @pytest.mark.asyncio - async def test_get_vertex_doesnt_exist(self, session, person): - async with session: - person._id = 1000000000000000000000000000000000000000000000 - result = await session.get_vertex(person) - assert not result + async def test_get_vertex_doesnt_exist(self, app, person): + session = await app.session() + person._id = 1000000000000000000000000000000000000000000000 + result = await session.get_vertex(person) + assert not result + await app.close() @pytest.mark.asyncio - async def test_get_edge_doesnt_exist(self, session, knows, person_class): - async with session: - jon = person_class() - leif = person_class() - works_with = knows - works_with.source = jon - works_with.target = leif - works_with._id = 1000000000000000000000000000000000000000000000 - result = await session.get_edge(works_with) - assert not result + async def test_get_edge_doesnt_exist(self, app, knows, person_class): + session = await app.session() + jon = person_class() + leif = person_class() + works_with = knows + works_with.source = jon + works_with.target = leif + works_with._id = 1000000000000000000000000000000000000000000000 + result = await session.get_edge(works_with) + assert not result + await app.close() @pytest.mark.asyncio - async def test_remove_vertex(self, session, person): - async with session: - person.name = 'dave' - person.age = 35 - await session.save(person) - result = await session.g.V(person.id).one_or_none() - assert result is person - rid = result.id - await session.remove_vertex(person) - result = await session.g.V(rid).one_or_none() - assert not result - + async def test_remove_vertex(self, app, person): + session = await app.session() + person.name = 'dave' + person.age = 35 + await session.save(person) + result = await session.g.V(person.id).oneOrNone() + assert result is person + rid = result.id + await session.remove_vertex(person) + result = await session.g.V(rid).oneOrNone() + assert not result + await app.close() +# @pytest.mark.asyncio - async def test_remove_edge(self, session, person_class, place_class, + async def test_remove_edge(self, app, person_class, place_class, lives_in_class): - async with session: - jon = person_class() - jon.name = 'jonathan' - jon.age = 38 - montreal = place_class() - montreal.name = 'Montreal' - lives_in = lives_in_class(jon, montreal) - session.add(jon, montreal, lives_in) - await session.flush() - result = await session.g.E(lives_in.id).one_or_none() - assert result is lives_in - rid = result.id - await session.remove_edge(lives_in) - result = await session.g.E(rid).one_or_none() - assert not result + session = await app.session() + jon = person_class() + jon.name = 'jonathan' + jon.age = 38 + montreal = place_class() + montreal.name = 'Montreal' + lives_in = lives_in_class(jon, montreal) + session.add(jon, montreal, lives_in) + await session.flush() + result = await session.g.E(lives_in.id).oneOrNone() + assert result is lives_in + rid = result.id + await session.remove_edge(lives_in) + result = await session.g.E(rid).oneOrNone() + assert not result + await app.close() @pytest.mark.asyncio - async def test_update_vertex(self, session, person): - async with session: - person.name = 'dave' - person.age = 35 - result = await session.save(person) - assert result.name == 'dave' - assert result.age == 35 - person.name = 'david' - person.age = None - result = await session.save(person) - assert result is person - assert result.name == 'david' - assert not result.age + async def test_update_vertex(self, app, person): + session = await app.session() + person.name = 'dave' + person.age = 35 + result = await session.save(person) + assert result.name == 'dave' + assert result.age == 35 + person.name = 'david' + person.age = None + result = await session.save(person) + assert result is person + assert result.name == 'david' + assert not result.age + await app.close() @pytest.mark.asyncio - async def test_update_edge(self, session, person_class, knows): - async with session: - dave = person_class() - leif = person_class() - knows.source = dave - knows.target = leif - knows.notes = 'online' - session.add(dave, leif) - await session.flush() - result = await session.save(knows) - assert knows.notes == 'online' - knows.notes = None - result = await session.save(knows) - assert result is knows - assert not result.notes + async def test_update_edge(self, app, person_class, knows): + session = await app.session() + dave = person_class() + leif = person_class() + knows.source = dave + knows.target = leif + knows.notes = 'online' + session.add(dave, leif) + await session.flush() + result = await session.save(knows) + assert knows.notes == 'online' + knows.notes = None + result = await session.save(knows) + assert result is knows + assert not result.notes + await app.close() class TestTraversalApi: @pytest.mark.asyncio - async def test_traversal_source_generation(self, session, person_class, + async def test_traversal_source_generation(self, app, person_class, knows_class): - async with session: - traversal = session.traversal(person_class) - assert repr(traversal) == 'g.V().hasLabel("person")' - traversal = session.traversal(knows_class) - assert repr(traversal) == 'g.E().hasLabel("knows")' + session = await app.session() + traversal = session.traversal(person_class) + translator = GroovyTranslator('g') + assert translator.translate(traversal.bytecode) == 'g.V().hasLabel("person")' + traversal = session.traversal(knows_class) + assert translator.translate(traversal.bytecode) == 'g.E().hasLabel("knows")' + await app.close() @pytest.mark.asyncio - async def test_all(self, session, person_class): - async with session: - dave = person_class() - leif = person_class() - jon = person_class() - session.add(dave, leif, jon) - await session.flush() - resp = await session.traversal(person_class).all() - results = [] - async for msg in resp: - assert isinstance(msg, person_class) - results.append(msg) - assert len(results) > 2 - - @pytest.mark.asyncio - async def test_one_or_none_one(self, session, person_class): - async with session: - dave = person_class() - leif = person_class() - jon = person_class() - session.add(dave, leif, jon) - await session.flush() - resp = await session.traversal(person_class).one_or_none() - assert isinstance(resp, person_class) - - @pytest.mark.asyncio - async def test_traversal_bindprop(self, session, person_class): - async with session: - itziri = person_class() - itziri.name = 'itziri' - result1 = await session.save(itziri) - bound_name = bindprop(person_class, 'name', 'itziri', binding='v1') - p1 = await session.traversal(person_class).has( - *bound_name).one_or_none() - - @pytest.mark.asyncio - async def test_one_or_none_none(self, session): - async with session: - none = await session.g.V().hasLabel( - 'a very unlikey label').one_or_none() - assert not none - - @pytest.mark.asyncio - async def test_vertex_deserialization(self, session, person_class): - async with session: - resp = await session.g.addV('person').property( - person_class.name, 'leif').property('place_of_birth', 'detroit').one_or_none() - assert isinstance(resp, person_class) - assert resp.name == 'leif' - assert resp.place_of_birth == 'detroit' - - @pytest.mark.asyncio - async def test_edge_desialization(self, session, knows_class): - async with session: - p1 = await session.g.addV('person').one_or_none() - p2 = await session.g.addV('person').one_or_none() - e1 = await session.g.V(p1.id).addE('knows').to( - session.g.V(p2.id)).property( - knows_class.notes, 'somehow').property( - 'how_long', 1).one_or_none() - assert isinstance(e1, knows_class) - assert e1.notes == 'somehow' - assert e1.how_long == 1 - - @pytest.mark.asyncio - async def test_unregistered_vertex_deserialization(self, session): - async with session: - dave = await session.g.addV( - 'unregistered').property('name', 'dave').one_or_none() - assert isinstance(dave, element.GenericVertex) - assert dave.name == 'dave' - assert dave.__label__ == 'unregistered' - - @pytest.mark.asyncio - async def test_unregistered_edge_desialization(self, session): - async with session: - p1 = await session.g.addV('person').one_or_none() - p2 = await session.g.addV('person').one_or_none() - e1 = await session.g.V(p1.id).addE('unregistered').to( - session.g.V(p2.id)).property('how_long', 1).one_or_none() - assert isinstance(e1, element.GenericEdge) - assert e1.how_long == 1 - assert e1.__label__ == 'unregistered' - - @pytest.mark.asyncio - async def test_property_deserialization(self, session): - async with session: - p1 = await session.g.addV('person').property( - 'name', 'leif').one_or_none() - name = await session.g.V(p1.id).properties('name').one_or_none() - assert name['value'] == 'leif' - assert name['label'] == 'name' - - @pytest.mark.asyncio - async def test_non_element_deserialization(self, session): - async with session: - p1 = await session.g.addV('person').property( - 'name', 'leif').one_or_none() - one = await session.g.V(p1.id).count().one_or_none() - assert one == 1 - - @pytest.mark.asyncio - async def test_deserialize_nested_map(self, session, person_class): - async with session: - await session.g.addV('person').property( - person_class.name, 'leif').property('place_of_birth', 'detroit').one_or_none() - - await session.g.addV('person').property(person_class.name, 'David').property( - person_class.nicknames, 'davebshow').property( - person_class.nicknames, 'Dave').one_or_none() - - resp = await (session.g.V().hasLabel('person')._as('x').valueMap()._as('y') - .select('x', 'y').fold().one_or_none()) - - for item in resp: - assert isinstance(item['x'], person_class) - assert isinstance(item['y'], dict) + async def test_all(self, app, person_class): + session = await app.session() + dave = person_class() + leif = person_class() + jon = person_class() + session.add(dave, leif, jon) + await session.flush() + resp = session.traversal(person_class) + results = [] + async for msg in resp: + assert isinstance(msg, person_class) + results.append(msg) + assert len(results) > 2 + await app.close() + + # @pytest.mark.asyncio + # async def test_oneOrNone_one(self, app, person_class): + # session = await app.session() + # dave = person_class() + # leif = person_class() + # jon = person_class() + # session.add(dave, leif, jon) + # await session.flush() + # resp = await session.traversal(person_class).oneOrNone() + # assert isinstance(resp, person_class) + # await app.close() + + # @pytest.mark.asyncio + # async def test_traversal_bindprop(self, app, person_class): + # session = await app.session() + # itziri = person_class() + # itziri.name = 'itziri' + # result1 = await session.save(itziri) + # bound_name = bindprop(person_class, 'name', 'itziri', binding='v1') + # p1 = await session.traversal(person_class).has( + # *bound_name).oneOrNone() + # await app.close() + + # @pytest.mark.asyncio + # async def test_oneOrNone_none(self, app): + # session = await app.session() + # none = await session.g.V().hasLabel( + # 'a very unlikey label').oneOrNone() + # assert not none + # await app.close() + + # @pytest.mark.asyncio + # async def test_vertex_deserialization(self, app, person_class): + # session = await app.session() + # resp = await session.g.addV('person').property( + # person_class.name, 'leif').property('place_of_birth', 'detroit').oneOrNone() + # assert isinstance(resp, person_class) + # assert resp.name == 'leif' + # assert resp.place_of_birth == 'detroit' + # await app.close() + # + # @pytest.mark.asyncio + # async def test_edge_desialization(self, app, knows_class): + # session = await app.session() + # p1 = await session.g.addV('person').oneOrNone() + # p2 = await session.g.addV('person').oneOrNone() + # e1 = await session.g.V(p1.id).addE('knows').to( + # session.g.V(p2.id)).property( + # knows_class.notes, 'somehow').property( + # 'how_long', 1).oneOrNone() + # assert isinstance(e1, knows_class) + # assert e1.notes == 'somehow' + # assert e1.how_long == 1 + # await app.close() + + # @pytest.mark.asyncio + # async def test_unregistered_vertex_deserialization(self, app): + # session = await app.session() + # dave = await session.g.addV( + # 'unregistered').property('name', 'dave').oneOrNone() + # assert isinstance(dave, element.GenericVertex) + # assert dave.name == 'dave' + # assert dave.__label__ == 'unregistered' + # await app.close() + + # @pytest.mark.asyncio + # async def test_unregistered_edge_desialization(self, app): + # session = await app.session() + # p1 = await session.g.addV('person').oneOrNone() + # p2 = await session.g.addV('person').oneOrNone() + # e1 = await session.g.V(p1.id).addE('unregistered').to( + # session.g.V(p2.id)).property('how_long', 1).oneOrNone() + # assert isinstance(e1, element.GenericEdge) + # assert e1.how_long == 1 + # assert e1.__label__ == 'unregistered' + # await app.close() + + # @pytest.mark.asyncio + # async def test_property_deserialization(self, app): + # session = await app.session() + # p1 = await session.g.addV('person').property( + # 'name', 'leif').oneOrNone() + # name = await session.g.V(p1.id).properties('name').oneOrNone() + # assert name['value'] == 'leif' + # assert name['label'] == 'name' + # await app.close() +# +# @pytest.mark.asyncio +# async def test_non_element_deserialization(self, app): +# session = await app.session() +# p1 = await session.g.addV('person').property( +# 'name', 'leif').oneOrNone() +# one = await session.g.V(p1.id).count().oneOrNone() +# assert one == 1 +# await app.close() +# +# +# @pytest.mark.asyncio +# async def test_deserialize_nested_map(self, session, person_class): +# async with session: +# await session.g.addV('person').property( +# person_class.name, 'leif').property('place_of_birth', 'detroit').oneOrNone() +# +# await session.g.addV('person').property(person_class.name, 'David').property( +# person_class.nicknames, 'davebshow').property( +# person_class.nicknames, 'Dave').oneOrNone() +# +# resp = await (session.g.V().hasLabel('person')._as('x').valueMap()._as('y') +# .select('x', 'y').fold().oneOrNone()) +# +# for item in resp: +# assert isinstance(item['x'], person_class) +# assert isinstance(item['y'], dict) diff --git a/tests/test_traversal.py b/tests/test_traversal.py deleted file mode 100644 index 999821c264c3ffda6585bd5a77dcc43a64388b99..0000000000000000000000000000000000000000 --- a/tests/test_traversal.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2016 ZEROFAIL -# -# This file is part of Goblin. -# -# Goblin is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Goblin is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with Goblin. If not, see <http://www.gnu.org/licenses/>. - -from goblin.traversal import bindprop - - -def test_bindprop(person_class): - db_val, (binding, val) = bindprop(person_class, 'name', 'dave', binding='n1') - assert db_val == 'name' - assert binding == 'n1' - assert val == 'dave' diff --git a/tests/test_vertex_properties_functional.py b/tests/test_vertex_properties_functional.py index b488bff4f3288b01aff83f2343dc0a71f0f99618..ae9c9997ca3bacfc40268b5df930dc9fade5de9d 100644 --- a/tests/test_vertex_properties_functional.py +++ b/tests/test_vertex_properties_functional.py @@ -1,119 +1,139 @@ +# Copyright 2016 ZEROFAIL +# +# This file is part of Goblin. +# +# Goblin is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Goblin is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Goblin. If not, see <http://www.gnu.org/licenses/>. + import pytest @pytest.mark.asyncio -async def test_add_update_property(session, person): - async with session: - person.birthplace = 'Iowa City' - result = await session.save(person) - assert result.birthplace.value == 'Iowa City' - person.birthplace = 'unknown' - result = await session.save(person) - assert result.birthplace.value == 'unknown' - person.birthplace = None - result = await session.save(person) - assert not result.birthplace +async def test_add_update_property(app, person): + session = await app.session() + person.birthplace = 'Iowa City' + result = await session.save(person) + assert result.birthplace.value == 'Iowa City' + person.birthplace = 'unknown' + result = await session.save(person) + assert result.birthplace.value == 'unknown' + person.birthplace = None + result = await session.save(person) + assert not result.birthplace + await app.close() @pytest.mark.asyncio -async def test_add_update_list_card_property(session, person): - async with session: - person.nicknames = ['db', 'dirtydb'] - result = await session.save(person) - assert [v.value for v in result.nicknames] == ['db', 'dirtydb'] - person.nicknames.append('davebshow') - result = await session.save(person) - assert [v.value for v in result.nicknames] == [ - 'db', 'dirtydb', 'davebshow'] - person.nicknames = [] - result = await session.save(person) - assert not result.nicknames - person.nicknames = ['none'] - result = await session.save(person) - assert result.nicknames('none').value == 'none' - person.nicknames = None - result = await session.save(person) - assert not result.nicknames +async def test_add_update_list_card_property(app, person): + session = await app.session() + person.nicknames = ['db', 'dirtydb'] + result = await session.save(person) + assert [v.value for v in result.nicknames] == ['db', 'dirtydb'] + person.nicknames.append('davebshow') + result = await session.save(person) + assert [v.value for v in result.nicknames] == [ + 'db', 'dirtydb', 'davebshow'] + person.nicknames = [] + result = await session.save(person) + assert not result.nicknames + person.nicknames = ['none'] + result = await session.save(person) + assert result.nicknames('none').value == 'none' + person.nicknames = None + result = await session.save(person) + assert not result.nicknames + await app.close() @pytest.mark.asyncio -async def test_add_update_set_card_property(session, place): - async with session: - place.important_numbers = set([1, 2]) - result = await session.save(place) - assert {v.value for v in result.important_numbers} == {1, 2} - place.important_numbers = set([3, 4]) - result = await session.save(place) - assert {v.value for v in result.important_numbers} == {3, 4} - place.important_numbers.add(5) - result = await session.save(place) - assert {v.value for v in result.important_numbers} == {3, 4, 5} - place.important_numbers = set() - result = await session.save(place) - assert not result.important_numbers - place.important_numbers = set([1, 2]) - result = await session.save(place) - assert place.important_numbers(1).value == 1 - place.important_numbers = None - result = await session.save(place) - assert not result.important_numbers +async def test_add_update_set_card_property(app, place): + session = await app.session() + place.important_numbers = set([1, 2]) + result = await session.save(place) + assert {v.value for v in result.important_numbers} == {1, 2} + place.important_numbers = set([3, 4]) + result = await session.save(place) + assert {v.value for v in result.important_numbers} == {3, 4} + place.important_numbers.add(5) + result = await session.save(place) + assert {v.value for v in result.important_numbers} == {3, 4, 5} + place.important_numbers = set() + result = await session.save(place) + assert not result.important_numbers + place.important_numbers = set([1, 2]) + result = await session.save(place) + assert place.important_numbers(1).value == 1 + place.important_numbers = None + result = await session.save(place) + assert not result.important_numbers + await app.close() @pytest.mark.asyncio -async def test_add_update_metas(session, place): - async with session: - place.historical_name = ['Detroit'] - place.historical_name('Detroit').notes = 'rock city' - place.historical_name('Detroit').year = 1900 - result = await session.save(place) - assert result.historical_name('Detroit').notes == 'rock city' - assert result.historical_name('Detroit').year == 1900 - - place.historical_name('Detroit').notes = 'comeback city' - place.historical_name('Detroit').year = 2016 - result = await session.save(place) - assert result.historical_name('Detroit').notes == 'comeback city' - assert result.historical_name('Detroit').year == 2016 - - place.historical_name('Detroit').notes = None - place.historical_name('Detroit').year = None - result = await session.save(place) - assert not result.historical_name('Detroit').notes - assert not result.historical_name('Detroit').year - - +async def test_add_update_metas(app, place): + session = await app.session() + place.historical_name = ['Detroit'] + place.historical_name('Detroit').notes = 'rock city' + place.historical_name('Detroit').year = 1900 + result = await session.save(place) + assert result.historical_name('Detroit').notes == 'rock city' + assert result.historical_name('Detroit').year == 1900 + + place.historical_name('Detroit').notes = 'comeback city' + place.historical_name('Detroit').year = 2016 + result = await session.save(place) + assert result.historical_name('Detroit').notes == 'comeback city' + assert result.historical_name('Detroit').year == 2016 + + place.historical_name('Detroit').notes = None + place.historical_name('Detroit').year = None + result = await session.save(place) + assert not result.historical_name('Detroit').notes + assert not result.historical_name('Detroit').year + await app.close() @pytest.mark.asyncio -async def test_add_update_metas_list_card(session, place): - async with session: - place.historical_name = ['Hispania', 'Al-Andalus'] - place.historical_name('Hispania').notes = 'romans' - place.historical_name('Hispania').year = 200 - place.historical_name('Al-Andalus').notes = 'muslims' - place.historical_name('Al-Andalus').year = 700 - result = await session.save(place) - assert result.historical_name('Hispania').notes == 'romans' - assert result.historical_name('Hispania').year == 200 - assert result.historical_name('Al-Andalus').notes == 'muslims' - assert result.historical_name('Al-Andalus').year == 700 - - place.historical_name('Hispania').notes = 'really old' - place.historical_name('Hispania').year = 200 - place.historical_name('Al-Andalus').notes = 'less old' - place.historical_name('Al-Andalus').year = 700 - result = await session.save(place) - assert result.historical_name('Hispania').notes == 'really old' - assert result.historical_name('Hispania').year == 200 - assert result.historical_name('Al-Andalus').notes == 'less old' - assert result.historical_name('Al-Andalus').year == 700 - - place.historical_name('Hispania').notes = None - place.historical_name('Hispania').year = None - place.historical_name('Al-Andalus').notes = None - place.historical_name('Al-Andalus').year = None - result = await session.save(place) - assert not result.historical_name('Hispania').notes - assert not result.historical_name('Hispania').year - assert not result.historical_name('Al-Andalus').notes - assert not result.historical_name('Al-Andalus').year +async def test_add_update_metas_list_card(app, place): + session = await app.session() + place.historical_name = ['Hispania', 'Al-Andalus'] + place.historical_name('Hispania').notes = 'romans' + place.historical_name('Hispania').year = 200 + place.historical_name('Al-Andalus').notes = 'muslims' + place.historical_name('Al-Andalus').year = 700 + result = await session.save(place) + assert result.historical_name('Hispania').notes == 'romans' + assert result.historical_name('Hispania').year == 200 + assert result.historical_name('Al-Andalus').notes == 'muslims' + assert result.historical_name('Al-Andalus').year == 700 + + place.historical_name('Hispania').notes = 'really old' + place.historical_name('Hispania').year = 200 + place.historical_name('Al-Andalus').notes = 'less old' + place.historical_name('Al-Andalus').year = 700 + result = await session.save(place) + assert result.historical_name('Hispania').notes == 'really old' + assert result.historical_name('Hispania').year == 200 + assert result.historical_name('Al-Andalus').notes == 'less old' + assert result.historical_name('Al-Andalus').year == 700 + + place.historical_name('Hispania').notes = None + place.historical_name('Hispania').year = None + place.historical_name('Al-Andalus').notes = None + place.historical_name('Al-Andalus').year = None + result = await session.save(place) + assert not result.historical_name('Hispania').notes + assert not result.historical_name('Hispania').year + assert not result.historical_name('Al-Andalus').notes + assert not result.historical_name('Al-Andalus').year + await app.close()