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.
@@ -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, protocol, request, serializer
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, protocol_factory=None,
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
- headers=None, enable_user_agent_on_connect=True,
47
- bulk_results=False, **transport_kwargs):
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
- self._headers = headers
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 "max_content_length" not in transport_kwargs:
57
- transport_kwargs["max_content_length"] = 10 * 1024 * 1024
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
- if transport_factory is None:
65
- try:
66
- from gremlin_python.driver.aiohttp.transport import AiohttpHTTPTransport
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 = pool_size
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._pool_size):
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 _get_connection(self):
134
- protocol = self._protocol_factory()
135
- return connection.Connection(
136
- self._url, self._traversal_source, protocol,
137
- self._transport_factory, self._executor, self._pool,
138
- headers=self._headers, enable_user_agent_on_connect=self._enable_user_agent_on_connect)
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
- def submit(self, message, bindings=None, request_options=None):
141
- return self.submit_async(message, bindings=bindings, request_options=request_options).result()
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 submitAsync(self, message, bindings=None, request_options=None):
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, bindings, request_options)
164
+ return self.submit_async(message, parameters, request_options)
149
165
 
150
- def submit_async(self, message, bindings=None, request_options=None):
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
- # TODO: bindings is now part of request_options, evaluate the need to keep it separate in python.
158
- # Note this bindings parameter only applies to string script submissions
159
- if isinstance(message, str) and bindings:
160
- fields['bindings'] = bindings
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 != 'bindings'})
170
- if 'bindings' in request_options:
171
- if 'bindings' in message.fields:
172
- message.fields['bindings'].update(request_options['bindings'])
173
- else:
174
- message.fields['bindings'] = request_options['bindings']
175
- if 'params' in request_options:
176
- if 'bindings' in message.fields:
177
- message.fields['bindings'].update(request_options['params'])
178
- else:
179
- message.fields['bindings'] = request_options['params']
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, protocol, transport_factory,
28
- executor, pool, headers=None, enable_user_agent_on_connect=True,
29
- bulk_results=False):
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
- self._headers = headers
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._protocol = protocol
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._transport_factory()
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._protocol.write, request_message)
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
- GraphSON does not support streaming deserialization, we are aggregating data and bypassing streamed
87
- deserialization while GraphSON is enabled for testing. Remove after GraphSON is removed.
88
- '''
89
- self._protocol.data_received_aggregate(self._transport.read(), self._result_set)
90
- # re-enable streaming after graphSON removal
91
- # self._transport.read(self.stream_chunk)
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", protocol_factory=None,
35
- transport_factory=None, pool_size=None, max_workers=None,
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
- headers=None, enable_user_agent_on_connect=True,
39
- bulk_results=False, **transport_kwargs):
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.__protocol_factory = protocol_factory
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
- protocol_factory=protocol_factory,
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
- results = result_set.all().result()
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
- results = result_set.all().result()
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
- if gremlin_lang.parameters is not None and len(gremlin_lang.parameters) > 0:
130
- request_options["params"] = gremlin_lang.parameters
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
- __author__ = 'David M. Brown (davebshow@gmail.com)'
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
- class AbstractBaseTransport(metaclass=abc.ABCMeta):
25
-
26
- @abc.abstractmethod
27
- def connect(self, url, headers=None):
28
- pass
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
@@ -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', 'bindings', 'g', 'gremlin', 'language',
27
- 'evaluationTimeout', 'materializeProperties', 'timeoutMs', 'userAgent', 'bulkResults']
26
+ Tokens = ['batchSize', 'parameters', 'g', 'gremlin', 'language',
27
+ 'materializeProperties', 'timeoutMillis', 'userAgent', 'bulkResults',
28
+ 'transactionId']