gremlinpython 4.0.0b2__py3-none-any.whl → 4.0.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gremlin_python/__init__.py +1 -1
- gremlin_python/driver/aiohttp/transport.py +257 -85
- gremlin_python/driver/auth.py +46 -21
- gremlin_python/driver/client.py +89 -61
- gremlin_python/driver/connection.py +123 -19
- gremlin_python/driver/driver_remote_connection.py +16 -39
- gremlin_python/driver/{transport.py → exceptions.py} +10 -24
- gremlin_python/driver/http_request.py +65 -0
- gremlin_python/driver/request.py +3 -2
- gremlin_python/driver/resultset.py +11 -5
- gremlin_python/driver/serializer.py +50 -130
- gremlin_python/driver/transaction.py +178 -0
- gremlin_python/driver/useragent.py +1 -1
- gremlin_python/process/graph_traversal.py +210 -93
- gremlin_python/process/traversal.py +114 -51
- gremlin_python/structure/graph.py +450 -5
- gremlin_python/structure/io/graphbinaryV4.py +177 -37
- {gremlinpython-4.0.0b2.dist-info → gremlinpython-4.0.0.dev1.dist-info}/METADATA +7 -5
- gremlinpython-4.0.0.dev1.dist-info/RECORD +33 -0
- {gremlinpython-4.0.0b2.dist-info → gremlinpython-4.0.0.dev1.dist-info}/WHEEL +1 -1
- gremlin_python/driver/protocol.py +0 -175
- gremlin_python/structure/io/graphsonV4.py +0 -599
- gremlinpython-4.0.0b2.dist-info/RECORD +0 -33
- {gremlinpython-4.0.0b2.dist-info → gremlinpython-4.0.0.dev1.dist-info}/licenses/LICENSE +0 -0
- {gremlinpython-4.0.0b2.dist-info → gremlinpython-4.0.0.dev1.dist-info}/licenses/NOTICE +0 -0
- {gremlinpython-4.0.0b2.dist-info → gremlinpython-4.0.0.dev1.dist-info}/top_level.txt +0 -0
gremlin_python/driver/client.py
CHANGED
|
@@ -21,7 +21,7 @@ import warnings
|
|
|
21
21
|
import queue
|
|
22
22
|
from concurrent.futures import ThreadPoolExecutor
|
|
23
23
|
|
|
24
|
-
from gremlin_python.driver import connection,
|
|
24
|
+
from gremlin_python.driver import connection, request, serializer
|
|
25
25
|
|
|
26
26
|
log = logging.getLogger("gremlinpython")
|
|
27
27
|
|
|
@@ -36,59 +36,43 @@ except ImportError:
|
|
|
36
36
|
__author__ = 'David M. Brown (davebshow@gmail.com), Lyndon Bauto (lyndonb@bitquilltech.com)'
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
# TODO: remove session, update connection pooling, etc.
|
|
40
39
|
class Client:
|
|
41
40
|
|
|
42
|
-
def __init__(self, url, traversal_source,
|
|
43
|
-
transport_factory=None, pool_size=None, max_workers=None,
|
|
44
|
-
request_serializer=serializer.GraphBinarySerializersV4(),
|
|
41
|
+
def __init__(self, url, traversal_source, max_connections=128, max_workers=None,
|
|
45
42
|
response_serializer=None, interceptors=None, auth=None,
|
|
46
|
-
|
|
47
|
-
bulk_results=False,
|
|
43
|
+
enable_user_agent_on_connect=True,
|
|
44
|
+
bulk_results=False, pdt_registry=None, batch_size=None,
|
|
45
|
+
**transport_kwargs):
|
|
48
46
|
log.info("Creating Client with url '%s'", url)
|
|
49
47
|
|
|
50
48
|
self._closed = False
|
|
51
49
|
self._url = url
|
|
52
|
-
|
|
50
|
+
# A raw list is safe here because Python's GIL ensures list.append and
|
|
51
|
+
# list.remove are atomic at the bytecode level.
|
|
52
|
+
self._tracked_transactions = []
|
|
53
53
|
self._enable_user_agent_on_connect = enable_user_agent_on_connect
|
|
54
54
|
self._bulk_results = bulk_results
|
|
55
55
|
self._traversal_source = traversal_source
|
|
56
|
-
if
|
|
57
|
-
|
|
56
|
+
if batch_size is None:
|
|
57
|
+
batch_size = 64
|
|
58
|
+
self._batch_size = batch_size
|
|
58
59
|
if response_serializer is None:
|
|
59
60
|
response_serializer = serializer.GraphBinarySerializersV4()
|
|
61
|
+
if pdt_registry is not None:
|
|
62
|
+
response_serializer.configure_pdt_registry(pdt_registry)
|
|
60
63
|
|
|
61
64
|
self._auth = auth
|
|
62
65
|
self._response_serializer = response_serializer
|
|
66
|
+
self._interceptors = interceptors
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
except ImportError:
|
|
68
|
-
raise Exception("Please install AIOHTTP or pass "
|
|
69
|
-
"custom transport factory")
|
|
70
|
-
else:
|
|
71
|
-
def transport_factory():
|
|
72
|
-
if self._protocol_factory is None:
|
|
73
|
-
self._protocol_factory = protocol_factory
|
|
74
|
-
return AiohttpHTTPTransport(**transport_kwargs)
|
|
75
|
-
self._transport_factory = transport_factory
|
|
76
|
-
|
|
77
|
-
if protocol_factory is None:
|
|
78
|
-
def protocol_factory():
|
|
79
|
-
return protocol.GremlinServerHTTPProtocol(
|
|
80
|
-
request_serializer, response_serializer, auth=self._auth,
|
|
81
|
-
interceptors=interceptors)
|
|
82
|
-
self._protocol_factory = protocol_factory
|
|
83
|
-
|
|
84
|
-
if pool_size is None:
|
|
85
|
-
pool_size = 8
|
|
86
|
-
self._pool_size = pool_size
|
|
68
|
+
self._transport_kwargs = transport_kwargs
|
|
69
|
+
|
|
70
|
+
self._max_connections = max_connections
|
|
87
71
|
# This is until concurrent.futures backport 3.1.0 release
|
|
88
72
|
if max_workers is None:
|
|
89
73
|
# If your application is overlapping Gremlin I/O on multiple threads
|
|
90
74
|
# consider passing kwarg max_workers = (cpu_count() or 1) * 5
|
|
91
|
-
max_workers =
|
|
75
|
+
max_workers = max_connections
|
|
92
76
|
self._executor = ThreadPoolExecutor(max_workers=max_workers)
|
|
93
77
|
# Threadsafe queue
|
|
94
78
|
self._pool = queue.Queue()
|
|
@@ -110,7 +94,7 @@ class Client:
|
|
|
110
94
|
return self._traversal_source
|
|
111
95
|
|
|
112
96
|
def _fill_pool(self):
|
|
113
|
-
for i in range(self.
|
|
97
|
+
for i in range(self._max_connections):
|
|
114
98
|
conn = self._get_connection()
|
|
115
99
|
self._pool.put_nowait(conn)
|
|
116
100
|
|
|
@@ -123,6 +107,15 @@ class Client:
|
|
|
123
107
|
if self._closed:
|
|
124
108
|
return
|
|
125
109
|
|
|
110
|
+
# Best-effort rollback of any open transactions before closing connections
|
|
111
|
+
txs = list(self._tracked_transactions)
|
|
112
|
+
for tx in txs:
|
|
113
|
+
try:
|
|
114
|
+
tx.close()
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
self._tracked_transactions.clear()
|
|
118
|
+
|
|
126
119
|
log.info("Closing Client with url '%s'", self._url)
|
|
127
120
|
while not self._pool.empty():
|
|
128
121
|
conn = self._pool.get(True)
|
|
@@ -130,52 +123,87 @@ class Client:
|
|
|
130
123
|
self._executor.shutdown()
|
|
131
124
|
self._closed = True
|
|
132
125
|
|
|
133
|
-
def
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
126
|
+
def track_transaction(self, tx):
|
|
127
|
+
self._tracked_transactions.append(tx)
|
|
128
|
+
|
|
129
|
+
def untrack_transaction(self, tx):
|
|
130
|
+
try:
|
|
131
|
+
self._tracked_transactions.remove(tx)
|
|
132
|
+
except ValueError:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
def transact(self):
|
|
136
|
+
"""Creates a new Transaction for executing operations within an explicit transaction.
|
|
139
137
|
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
Transactions are short-lived and single-use. After commit or rollback,
|
|
139
|
+
create a new Transaction for the next unit of work. The traversal source
|
|
140
|
+
(g alias) is inherited from this Client.
|
|
141
|
+
"""
|
|
142
|
+
from gremlin_python.driver.transaction import Transaction
|
|
143
|
+
return Transaction(self)
|
|
142
144
|
|
|
143
|
-
def
|
|
145
|
+
def _get_connection(self):
|
|
146
|
+
return connection.Connection(
|
|
147
|
+
self._url, self._traversal_source,
|
|
148
|
+
self._executor, self._pool,
|
|
149
|
+
response_serializer=self._response_serializer,
|
|
150
|
+
auth=self._auth, interceptors=self._interceptors,
|
|
151
|
+
enable_user_agent_on_connect=self._enable_user_agent_on_connect,
|
|
152
|
+
bulk_results=self._bulk_results,
|
|
153
|
+
max_connections=self._max_connections,
|
|
154
|
+
**self._transport_kwargs)
|
|
155
|
+
|
|
156
|
+
def submit(self, message, parameters=None, request_options=None):
|
|
157
|
+
return self.submit_async(message, parameters=parameters, request_options=request_options).result()
|
|
158
|
+
|
|
159
|
+
def submitAsync(self, message, parameters=None, request_options=None):
|
|
144
160
|
warnings.warn(
|
|
145
161
|
"gremlin_python.driver.client.Client.submitAsync will be replaced by "
|
|
146
162
|
"gremlin_python.driver.client.Client.submit_async.",
|
|
147
163
|
DeprecationWarning)
|
|
148
|
-
return self.submit_async(message,
|
|
164
|
+
return self.submit_async(message, parameters, request_options)
|
|
149
165
|
|
|
150
|
-
def submit_async(self, message,
|
|
166
|
+
def submit_async(self, message, parameters=None, request_options=None):
|
|
151
167
|
if self.is_closed():
|
|
152
168
|
raise Exception("Client is closed")
|
|
153
169
|
|
|
154
170
|
log.debug("message '%s'", str(message))
|
|
155
171
|
fields = {'g': self._traversal_source}
|
|
156
172
|
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
173
|
+
# Note this parameters argument only applies to string script submissions
|
|
174
|
+
if isinstance(message, str) and parameters:
|
|
175
|
+
from gremlin_python.process.traversal import GremlinLang
|
|
176
|
+
if isinstance(parameters, dict):
|
|
177
|
+
fields['parameters'] = GremlinLang.convert_parameters_to_string(parameters)
|
|
178
|
+
else:
|
|
179
|
+
fields['parameters'] = parameters
|
|
161
180
|
|
|
162
181
|
if isinstance(message, str):
|
|
163
182
|
log.debug("fields='%s', gremlin='%s'", str(fields), str(message))
|
|
164
183
|
message = request.RequestMessage(fields=fields, gremlin=message)
|
|
184
|
+
else:
|
|
185
|
+
# A caller-supplied RequestMessage must not be mutated in place:
|
|
186
|
+
# resubmitting the same message (e.g. on retry) would otherwise
|
|
187
|
+
# accumulate request_options/batchSize from prior submits. Clone the
|
|
188
|
+
# fields dict so this submit's mutations stay local, matching the
|
|
189
|
+
# no-mutate contract of the .NET/JS drivers. Freshly built messages
|
|
190
|
+
# (the string path above) already own a private fields dict.
|
|
191
|
+
message = message._replace(fields=dict(message.fields))
|
|
165
192
|
|
|
166
193
|
conn = self._pool.get(True)
|
|
167
194
|
if request_options:
|
|
168
195
|
message.fields.update({token: request_options[token] for token in request.Tokens
|
|
169
|
-
if token in request_options and token != '
|
|
170
|
-
if '
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
196
|
+
if token in request_options and token != 'parameters'})
|
|
197
|
+
if 'parameters' in request_options:
|
|
198
|
+
parameters_val = request_options['parameters']
|
|
199
|
+
if isinstance(parameters_val, dict):
|
|
200
|
+
from gremlin_python.process.traversal import GremlinLang
|
|
201
|
+
parameters_val = GremlinLang.convert_parameters_to_string(parameters_val)
|
|
202
|
+
message.fields['parameters'] = parameters_val
|
|
203
|
+
|
|
204
|
+
# Fill in the connection-level default batch size when the caller did
|
|
205
|
+
# not set a per-request batchSize.
|
|
206
|
+
if self._batch_size is not None and 'batchSize' not in message.fields:
|
|
207
|
+
message.fields['batchSize'] = self._batch_size
|
|
180
208
|
|
|
181
209
|
return conn.write(message)
|
|
@@ -14,29 +14,76 @@
|
|
|
14
14
|
# KIND, either express or implied. See the License for the
|
|
15
15
|
# specific language governing permissions and limitations
|
|
16
16
|
# under the License.
|
|
17
|
+
import asyncio
|
|
17
18
|
import queue
|
|
18
19
|
from concurrent.futures import Future
|
|
19
20
|
|
|
21
|
+
from aiohttp.client_exceptions import (
|
|
22
|
+
ClientOSError,
|
|
23
|
+
ClientPayloadError,
|
|
24
|
+
ServerDisconnectedError,
|
|
25
|
+
)
|
|
26
|
+
|
|
20
27
|
from gremlin_python.driver import resultset, useragent
|
|
28
|
+
from gremlin_python.driver.aiohttp.transport import AiohttpHTTPTransport
|
|
29
|
+
from gremlin_python.driver.http_request import HttpRequest
|
|
21
30
|
|
|
22
31
|
__author__ = 'David M. Brown (davebshow@gmail.com)'
|
|
23
32
|
|
|
33
|
+
_TRANSPORT_ERRORS = (ClientOSError, ClientPayloadError, ServerDisconnectedError, asyncio.IncompleteReadError)
|
|
34
|
+
_CONNECTION_ERROR_MSG = (
|
|
35
|
+
"Connection to server closed unexpectedly. "
|
|
36
|
+
"Ensure that the server is still reachable and the connection has not been closed by the server or a network device."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class GremlinServerError(Exception):
|
|
41
|
+
def __init__(self, status):
|
|
42
|
+
super(GremlinServerError, self).__init__('{0}: {1}'.format(status['code'], status['message']))
|
|
43
|
+
self.status_code = status['code']
|
|
44
|
+
self.status_message = status['message']
|
|
45
|
+
self.status_exception = status['exception']
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class GremlinConnectionError(Exception):
|
|
49
|
+
"""Raised when a transport-level failure occurs communicating with the server."""
|
|
50
|
+
pass
|
|
51
|
+
|
|
24
52
|
|
|
25
53
|
class Connection:
|
|
26
54
|
|
|
27
|
-
def __init__(self, url, traversal_source,
|
|
28
|
-
executor, pool,
|
|
29
|
-
|
|
55
|
+
def __init__(self, url, traversal_source,
|
|
56
|
+
executor, pool,
|
|
57
|
+
response_serializer=None, auth=None, interceptors=None,
|
|
58
|
+
enable_user_agent_on_connect=True,
|
|
59
|
+
bulk_results=False, **transport_kwargs):
|
|
60
|
+
if callable(interceptors):
|
|
61
|
+
interceptors = [interceptors]
|
|
62
|
+
elif isinstance(interceptors, tuple):
|
|
63
|
+
interceptors = list(interceptors)
|
|
64
|
+
elif not (isinstance(interceptors, list) or interceptors is None):
|
|
65
|
+
raise TypeError("interceptors must be a callable, tuple, list or None")
|
|
66
|
+
|
|
67
|
+
# Auth is just an interceptor. As a convenience (and for discoverability), the auth
|
|
68
|
+
# interceptor is appended to the end of the interceptor list so it runs last, after
|
|
69
|
+
# any user interceptors have modified the request.
|
|
70
|
+
if auth is not None:
|
|
71
|
+
interceptors = (interceptors or []) + [auth]
|
|
72
|
+
|
|
30
73
|
self._url = url
|
|
31
|
-
|
|
74
|
+
# Custom request headers are set via interceptors. This internal dict
|
|
75
|
+
# only carries connection-level headers managed by the driver itself
|
|
76
|
+
# (user agent, bulkResults) and is established at construction time.
|
|
77
|
+
self._headers = None
|
|
32
78
|
self._traversal_source = traversal_source
|
|
33
|
-
self.
|
|
34
|
-
self._transport_factory = transport_factory
|
|
79
|
+
self._transport_kwargs = transport_kwargs
|
|
35
80
|
self._executor = executor
|
|
36
81
|
self._transport = None
|
|
37
82
|
self._pool = pool
|
|
38
83
|
self._result_set = None
|
|
39
84
|
self._inited = False
|
|
85
|
+
self._response_serializer = response_serializer
|
|
86
|
+
self._interceptors = interceptors
|
|
40
87
|
self._enable_user_agent_on_connect = enable_user_agent_on_connect
|
|
41
88
|
if self._enable_user_agent_on_connect:
|
|
42
89
|
self.__add_header(useragent.userAgentHeader, useragent.userAgent)
|
|
@@ -47,15 +94,45 @@ class Connection:
|
|
|
47
94
|
def connect(self):
|
|
48
95
|
if self._transport:
|
|
49
96
|
self._transport.close()
|
|
50
|
-
self._transport = self.
|
|
97
|
+
self._transport = AiohttpHTTPTransport(**self._transport_kwargs)
|
|
51
98
|
self._transport.connect(self._url, self._headers)
|
|
52
|
-
self._protocol.connection_made(self._transport)
|
|
53
99
|
self._inited = True
|
|
54
100
|
|
|
55
101
|
def close(self):
|
|
56
102
|
if self._inited:
|
|
57
103
|
self._transport.close()
|
|
58
104
|
|
|
105
|
+
def _write_request(self, request_message):
|
|
106
|
+
accept = str(self._response_serializer.version, encoding='utf-8')
|
|
107
|
+
|
|
108
|
+
headers = {'accept': accept}
|
|
109
|
+
|
|
110
|
+
# Promote transactionId to HTTP header before interceptors run.
|
|
111
|
+
# The field remains in the serialized body as well (dual transmission
|
|
112
|
+
# per the HTTP transaction protocol specification).
|
|
113
|
+
if hasattr(request_message, 'fields') and 'transactionId' in request_message.fields:
|
|
114
|
+
headers['X-Transaction-Id'] = request_message.fields['transactionId']
|
|
115
|
+
|
|
116
|
+
http_request = HttpRequest(
|
|
117
|
+
method="POST",
|
|
118
|
+
url=self._url,
|
|
119
|
+
headers=headers,
|
|
120
|
+
body=request_message
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
for interceptor in self._interceptors or []:
|
|
124
|
+
interceptor(http_request)
|
|
125
|
+
|
|
126
|
+
# Auto-serialize if no interceptor already did so
|
|
127
|
+
http_request.serialize_body()
|
|
128
|
+
|
|
129
|
+
# Build the transport message in the format the transport expects
|
|
130
|
+
message = {
|
|
131
|
+
'headers': http_request.headers,
|
|
132
|
+
'payload': http_request.body
|
|
133
|
+
}
|
|
134
|
+
self._transport.write(message)
|
|
135
|
+
|
|
59
136
|
def write(self, request_message):
|
|
60
137
|
if not self._inited:
|
|
61
138
|
self.connect()
|
|
@@ -63,11 +140,16 @@ class Connection:
|
|
|
63
140
|
# Create write task
|
|
64
141
|
future = Future()
|
|
65
142
|
future_write = self._executor.submit(
|
|
66
|
-
self.
|
|
143
|
+
self._write_request, request_message)
|
|
67
144
|
|
|
68
145
|
def cb(f):
|
|
69
146
|
try:
|
|
70
147
|
f.result()
|
|
148
|
+
except _TRANSPORT_ERRORS as e:
|
|
149
|
+
wrapped = GremlinConnectionError(_CONNECTION_ERROR_MSG)
|
|
150
|
+
wrapped.__cause__ = e
|
|
151
|
+
future.set_exception(wrapped)
|
|
152
|
+
self._pool.put_nowait(self)
|
|
71
153
|
except Exception as e:
|
|
72
154
|
future.set_exception(e)
|
|
73
155
|
self._pool.put_nowait(self)
|
|
@@ -82,19 +164,41 @@ class Connection:
|
|
|
82
164
|
|
|
83
165
|
def _receive(self):
|
|
84
166
|
try:
|
|
85
|
-
'
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
''
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
167
|
+
# Surface error responses whose Content-Type doesn't match the
|
|
168
|
+
# configured response serializer (e.g. plain-text 5xx).
|
|
169
|
+
status = getattr(self._transport, 'status_code', None)
|
|
170
|
+
response_content_type = str(self._response_serializer.version, encoding='utf-8')
|
|
171
|
+
if status is not None and status >= 400:
|
|
172
|
+
content_type = getattr(self._transport, 'content_type', '')
|
|
173
|
+
if response_content_type not in content_type:
|
|
174
|
+
body = self._transport.read_body().decode('utf-8', errors='replace')
|
|
175
|
+
raise GremlinServerError({
|
|
176
|
+
'code': status,
|
|
177
|
+
'message': body,
|
|
178
|
+
'exception': ''
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
# 204 No Content
|
|
182
|
+
if status == 204:
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
# Stream items through the configured response serializer; it
|
|
186
|
+
# raises GremlinServerError if the response ends with a non-success status.
|
|
187
|
+
stream = self._transport.get_stream()
|
|
188
|
+
for obj in self._response_serializer.deserialize_response_stream(stream):
|
|
189
|
+
self._result_set.stream.put_nowait(obj)
|
|
190
|
+
except _TRANSPORT_ERRORS as err:
|
|
191
|
+
# Evict the dead connection from aiohttp's internal pool, ensuring subsequent
|
|
192
|
+
# requests get a fresh connection.
|
|
193
|
+
self._transport.evict_response()
|
|
194
|
+
msg = 'Server returned an empty response body' if isinstance(err, asyncio.IncompleteReadError) and not err.partial else _CONNECTION_ERROR_MSG
|
|
195
|
+
raise GremlinConnectionError(msg) from err
|
|
196
|
+
except Exception:
|
|
197
|
+
self._transport.evict_response()
|
|
198
|
+
raise
|
|
92
199
|
finally:
|
|
93
200
|
self._pool.put_nowait(self)
|
|
94
201
|
|
|
95
|
-
def stream_chunk(self, chunk_data, read_completed=None, http_req_resp=None):
|
|
96
|
-
self._protocol.data_received(chunk_data, self._result_set, read_completed, http_req_resp)
|
|
97
|
-
|
|
98
202
|
def __add_header(self, key, value):
|
|
99
203
|
if self._headers is None:
|
|
100
204
|
self._headers = dict()
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
#
|
|
19
19
|
import logging
|
|
20
20
|
from concurrent.futures import Future
|
|
21
|
-
import warnings
|
|
22
21
|
|
|
23
22
|
from gremlin_python.driver import client, serializer
|
|
24
23
|
from gremlin_python.driver.remote_connection import RemoteConnection, RemoteTraversal
|
|
@@ -31,38 +30,34 @@ __author__ = 'David M. Brown (davebshow@gmail.com), Lyndon Bauto (lyndonb@bitqui
|
|
|
31
30
|
|
|
32
31
|
class DriverRemoteConnection(RemoteConnection):
|
|
33
32
|
|
|
34
|
-
def __init__(self, url, traversal_source="g",
|
|
35
|
-
|
|
36
|
-
request_serializer=serializer.GraphBinarySerializersV4(),
|
|
33
|
+
def __init__(self, url, traversal_source="g",
|
|
34
|
+
max_connections=128, max_workers=None,
|
|
37
35
|
response_serializer=None, interceptors=None, auth=None,
|
|
38
|
-
|
|
39
|
-
bulk_results=False,
|
|
36
|
+
enable_user_agent_on_connect=True,
|
|
37
|
+
bulk_results=False, pdt_registry=None, batch_size=None,
|
|
38
|
+
**transport_kwargs):
|
|
40
39
|
log.info("Creating DriverRemoteConnection with url '%s'", str(url))
|
|
41
40
|
self.__url = url
|
|
42
41
|
self.__traversal_source = traversal_source
|
|
43
|
-
self.
|
|
44
|
-
self.__transport_factory = transport_factory
|
|
45
|
-
self.__pool_size = pool_size
|
|
42
|
+
self.__max_connections = max_connections
|
|
46
43
|
self.__max_workers = max_workers
|
|
47
44
|
self.__auth = auth
|
|
48
|
-
self.__headers = headers
|
|
49
45
|
self.__enable_user_agent_on_connect = enable_user_agent_on_connect
|
|
50
46
|
self.__bulk_results = bulk_results
|
|
51
47
|
self.__transport_kwargs = transport_kwargs
|
|
48
|
+
self.pdt_registry = pdt_registry
|
|
52
49
|
|
|
53
50
|
if response_serializer is None:
|
|
54
51
|
response_serializer = serializer.GraphBinarySerializersV4()
|
|
55
52
|
self._client = client.Client(url, traversal_source,
|
|
56
|
-
|
|
57
|
-
transport_factory=transport_factory,
|
|
58
|
-
pool_size=pool_size,
|
|
53
|
+
max_connections=max_connections,
|
|
59
54
|
max_workers=max_workers,
|
|
60
|
-
request_serializer=request_serializer,
|
|
61
55
|
response_serializer=response_serializer,
|
|
62
56
|
interceptors=interceptors, auth=auth,
|
|
63
|
-
headers=headers,
|
|
64
57
|
enable_user_agent_on_connect=enable_user_agent_on_connect,
|
|
65
58
|
bulk_results=bulk_results,
|
|
59
|
+
pdt_registry=pdt_registry,
|
|
60
|
+
batch_size=batch_size,
|
|
66
61
|
**transport_kwargs)
|
|
67
62
|
self._url = self._client._url
|
|
68
63
|
self._traversal_source = self._client._traversal_source
|
|
@@ -74,31 +69,20 @@ class DriverRemoteConnection(RemoteConnection):
|
|
|
74
69
|
|
|
75
70
|
def submit(self, gremlin_lang):
|
|
76
71
|
log.debug("submit with gremlin lang script '%s'", gremlin_lang.get_gremlin())
|
|
77
|
-
gremlin_lang.add_g(self._traversal_source)
|
|
78
72
|
result_set = self._client.submit(gremlin_lang.get_gremlin(),
|
|
79
73
|
request_options=self.extract_request_options(gremlin_lang))
|
|
80
|
-
|
|
81
|
-
return RemoteTraversal(iter(results))
|
|
82
|
-
|
|
83
|
-
def submitAsync(self, message, bindings=None, request_options=None):
|
|
84
|
-
warnings.warn(
|
|
85
|
-
"gremlin_python.driver.driver_remote_connection.DriverRemoteConnection.submitAsync will be replaced by "
|
|
86
|
-
"gremlin_python.driver.driver_remote_connection.DriverRemoteConnection.submit_async.",
|
|
87
|
-
DeprecationWarning)
|
|
88
|
-
self.submit_async(message, bindings, request_options)
|
|
74
|
+
return RemoteTraversal(result_set)
|
|
89
75
|
|
|
90
76
|
def submit_async(self, gremlin_lang):
|
|
91
77
|
log.debug("submit_async with gremlin lang script '%s'", gremlin_lang.get_gremlin())
|
|
92
78
|
future = Future()
|
|
93
|
-
gremlin_lang.add_g(self._traversal_source)
|
|
94
79
|
future_result_set = self._client.submit_async(gremlin_lang.get_gremlin(),
|
|
95
80
|
request_options=self.extract_request_options(gremlin_lang))
|
|
96
81
|
|
|
97
82
|
def cb(f):
|
|
98
83
|
try:
|
|
99
84
|
result_set = f.result()
|
|
100
|
-
|
|
101
|
-
future.set_result(RemoteTraversal(iter(results)))
|
|
85
|
+
future.set_result(RemoteTraversal(result_set))
|
|
102
86
|
except Exception as e:
|
|
103
87
|
future.set_exception(e)
|
|
104
88
|
|
|
@@ -108,15 +92,6 @@ class DriverRemoteConnection(RemoteConnection):
|
|
|
108
92
|
def is_closed(self):
|
|
109
93
|
return self._client.is_closed()
|
|
110
94
|
|
|
111
|
-
# TODO remove or update once HTTP transaction is implemented
|
|
112
|
-
# def commit(self):
|
|
113
|
-
# log.info("Submitting commit graph operation.")
|
|
114
|
-
# return self._client.submit(Bytecode.GraphOp.commit())
|
|
115
|
-
#
|
|
116
|
-
# def rollback(self):
|
|
117
|
-
# log.info("Submitting rollback graph operation.")
|
|
118
|
-
# return self._client.submit(Bytecode.GraphOp.rollback())
|
|
119
|
-
|
|
120
95
|
@staticmethod
|
|
121
96
|
def extract_request_options(gremlin_lang):
|
|
122
97
|
request_options = {}
|
|
@@ -126,7 +101,9 @@ class DriverRemoteConnection(RemoteConnection):
|
|
|
126
101
|
# request the server to bulk results by default when using drc through request options
|
|
127
102
|
if 'bulkResults' not in request_options:
|
|
128
103
|
request_options['bulkResults'] = True
|
|
129
|
-
|
|
130
|
-
|
|
104
|
+
|
|
105
|
+
parameters_string = gremlin_lang.get_parameters_as_string()
|
|
106
|
+
if parameters_string != '[:]':
|
|
107
|
+
request_options["parameters"] = parameters_string
|
|
131
108
|
|
|
132
109
|
return request_options
|
|
@@ -16,30 +16,16 @@
|
|
|
16
16
|
# specific language governing permissions and limitations
|
|
17
17
|
# under the License.
|
|
18
18
|
#
|
|
19
|
-
import abc
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
class ReadTimeoutError(TimeoutError):
|
|
21
|
+
"""Raised when the client-side read timeout elapses while waiting for response
|
|
22
|
+
data from the server.
|
|
22
23
|
|
|
24
|
+
Subclasses the builtin :class:`TimeoutError` so it can be caught with
|
|
25
|
+
``except TimeoutError`` without depending on the underlying transport.
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@abc.abstractmethod
|
|
31
|
-
def write(self, message):
|
|
32
|
-
pass
|
|
33
|
-
|
|
34
|
-
@abc.abstractmethod
|
|
35
|
-
def read(self):
|
|
36
|
-
pass
|
|
37
|
-
|
|
38
|
-
@abc.abstractmethod
|
|
39
|
-
def close(self):
|
|
40
|
-
pass
|
|
41
|
-
|
|
42
|
-
@property
|
|
43
|
-
@abc.abstractmethod
|
|
44
|
-
def closed(self):
|
|
45
|
-
pass
|
|
27
|
+
This driver-owned type exists only because the driver is currently synchronous and
|
|
28
|
+
should not surface the transport's asyncio/aiohttp timeout types. Once gremlin-python
|
|
29
|
+
is fully asynchronous this should be reverted to raising ``asyncio.TimeoutError``
|
|
30
|
+
directly. See TINKERPOP-2774: https://issues.apache.org/jira/browse/TINKERPOP-2774
|
|
31
|
+
"""
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
+
# or more contributor license agreements. See the NOTICE file
|
|
4
|
+
# distributed with this work for additional information
|
|
5
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
6
|
+
# to you under the Apache License, Version 2.0 (the
|
|
7
|
+
# "License"); you may not use this file except in compliance
|
|
8
|
+
# with the License. You may obtain a copy of the License at
|
|
9
|
+
#
|
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
#
|
|
12
|
+
# Unless required by applicable law or agreed to in writing,
|
|
13
|
+
# software distributed under the License is distributed on an
|
|
14
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
+
# KIND, either express or implied. See the License for the
|
|
16
|
+
# specific language governing permissions and limitations
|
|
17
|
+
# under the License.
|
|
18
|
+
#
|
|
19
|
+
import json
|
|
20
|
+
|
|
21
|
+
from gremlin_python.driver.request import RequestMessage
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class HttpRequest:
|
|
25
|
+
"""Represents the HTTP request passed through the interceptor chain.
|
|
26
|
+
|
|
27
|
+
The body starts as a RequestMessage and can be serialized to JSON bytes
|
|
28
|
+
via serialize_body(). Interceptors mutate this object in place.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, method, url, headers, body):
|
|
32
|
+
self.method = method
|
|
33
|
+
self.url = url
|
|
34
|
+
self.headers = headers
|
|
35
|
+
self.body = body
|
|
36
|
+
|
|
37
|
+
def serialize_body(self):
|
|
38
|
+
"""Serialize the body to JSON bytes if it is still a RequestMessage.
|
|
39
|
+
|
|
40
|
+
If the body is already bytes, returns them as-is (idempotent).
|
|
41
|
+
Sets the Content-Type header to application/json and Content-Length
|
|
42
|
+
to the byte length of the serialized body.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
bytes: the serialized body
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
TypeError: if the body is neither a RequestMessage nor bytes
|
|
49
|
+
"""
|
|
50
|
+
if isinstance(self.body, bytes):
|
|
51
|
+
return self.body
|
|
52
|
+
|
|
53
|
+
if not isinstance(self.body, RequestMessage):
|
|
54
|
+
raise TypeError(
|
|
55
|
+
f"Cannot serialize body of type {type(self.body).__name__}. "
|
|
56
|
+
"Expected RequestMessage or bytes."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
payload = {"gremlin": self.body.gremlin}
|
|
60
|
+
payload.update(self.body.fields)
|
|
61
|
+
data = json.dumps(payload).encode("utf-8")
|
|
62
|
+
self.body = data
|
|
63
|
+
self.headers["content-type"] = "application/json"
|
|
64
|
+
self.headers["content-length"] = str(len(data))
|
|
65
|
+
return data
|
gremlin_python/driver/request.py
CHANGED
|
@@ -23,5 +23,6 @@ __author__ = 'David M. Brown (davebshow@gmail.com)'
|
|
|
23
23
|
RequestMessage = collections.namedtuple(
|
|
24
24
|
'RequestMessage', ['fields', 'gremlin'])
|
|
25
25
|
|
|
26
|
-
Tokens = ['batchSize', '
|
|
27
|
-
'
|
|
26
|
+
Tokens = ['batchSize', 'parameters', 'g', 'gremlin', 'language',
|
|
27
|
+
'materializeProperties', 'timeoutMillis', 'userAgent', 'bulkResults',
|
|
28
|
+
'transactionId']
|