gremlinpython 4.0.0b2__py3-none-any.whl → 4.0.0b3__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.0b3.dist-info}/METADATA +7 -5
- gremlinpython-4.0.0b3.dist-info/RECORD +33 -0
- {gremlinpython-4.0.0b2.dist-info → gremlinpython-4.0.0b3.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.0b3.dist-info}/licenses/LICENSE +0 -0
- {gremlinpython-4.0.0b2.dist-info → gremlinpython-4.0.0b3.dist-info}/licenses/NOTICE +0 -0
- {gremlinpython-4.0.0b2.dist-info → gremlinpython-4.0.0b3.dist-info}/top_level.txt +0 -0
gremlin_python/__init__.py
CHANGED
|
@@ -16,27 +16,163 @@
|
|
|
16
16
|
# specific language governing permissions and limitations
|
|
17
17
|
# under the License.
|
|
18
18
|
#
|
|
19
|
-
import json
|
|
20
|
-
|
|
21
19
|
import aiohttp
|
|
22
20
|
import asyncio
|
|
21
|
+
import socket
|
|
23
22
|
import sys
|
|
24
23
|
|
|
24
|
+
from gremlin_python.driver.exceptions import ReadTimeoutError
|
|
25
|
+
|
|
25
26
|
if sys.version_info >= (3, 11):
|
|
26
27
|
import asyncio as async_timeout
|
|
27
28
|
else:
|
|
28
29
|
import async_timeout
|
|
29
|
-
from aiohttp import ClientPayloadError
|
|
30
|
-
from gremlin_python.driver.protocol import GremlinServerError
|
|
31
|
-
from gremlin_python.driver.transport import AbstractBaseTransport
|
|
32
30
|
|
|
33
31
|
__author__ = 'Lyndon Bauto (lyndonb@bitquilltech.com)'
|
|
34
32
|
|
|
33
|
+
# Default connection option values (canonical TinkerPop 4.x GLV defaults). The millisecond-suffixed
|
|
34
|
+
# options are the primary form (mirroring the other GLVs); aiohttp itself works in seconds, so the
|
|
35
|
+
# values are converted internally.
|
|
36
|
+
DEFAULT_CONNECT_TIMEOUT_MILLIS = 5000
|
|
37
|
+
DEFAULT_IDLE_TIMEOUT_MILLIS = 180000
|
|
38
|
+
DEFAULT_KEEP_ALIVE_TIME_MILLIS = 30000
|
|
39
|
+
# Seconds equivalents retained for internal use / backwards reference.
|
|
40
|
+
DEFAULT_CONNECT_TIMEOUT = DEFAULT_CONNECT_TIMEOUT_MILLIS / 1000
|
|
41
|
+
DEFAULT_IDLE_TIMEOUT = DEFAULT_IDLE_TIMEOUT_MILLIS / 1000
|
|
42
|
+
DEFAULT_KEEP_ALIVE_TIME = DEFAULT_KEEP_ALIVE_TIME_MILLIS / 1000
|
|
43
|
+
DEFAULT_COMPRESSION = 'deflate'
|
|
44
|
+
|
|
45
|
+
def _resolve_timeout_seconds(millis, seconds, default_millis):
|
|
46
|
+
"""Resolve a timeout to seconds (aiohttp's unit) from the ``*_millis`` number or the idiomatic
|
|
47
|
+
unsuffixed seconds number (``None`` means not supplied for either). Supplying both raises
|
|
48
|
+
``ValueError``; if neither is given, ``default_millis`` is used (``None`` leaves it unset).
|
|
49
|
+
"""
|
|
50
|
+
if millis is not None and seconds is not None:
|
|
51
|
+
raise ValueError("provide only one of the milliseconds option or the seconds option, not both")
|
|
52
|
+
if seconds is not None:
|
|
53
|
+
return seconds
|
|
54
|
+
if millis is not None:
|
|
55
|
+
return millis / 1000
|
|
56
|
+
if default_millis is None:
|
|
57
|
+
return None
|
|
58
|
+
return default_millis / 1000
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _normalize_compression(compression):
|
|
62
|
+
"""Normalize the compression option to a canonical string ('none' or 'deflate').
|
|
63
|
+
|
|
64
|
+
Accepts the string forms 'none'/'deflate'.
|
|
65
|
+
"""
|
|
66
|
+
if compression is None:
|
|
67
|
+
return DEFAULT_COMPRESSION
|
|
68
|
+
if isinstance(compression, str):
|
|
69
|
+
normalized = compression.lower()
|
|
70
|
+
if normalized in ('none', 'deflate'):
|
|
71
|
+
return normalized
|
|
72
|
+
raise ValueError("compression must be one of 'none', 'deflate', got '%s'" % compression)
|
|
73
|
+
raise TypeError("compression must be a str ('none'|'deflate'), got %s" % type(compression).__name__)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _run_read(loop, read_timeout, coro):
|
|
77
|
+
"""Run a response-read coroutine on ``loop``, normalizing aiohttp's read timeout
|
|
78
|
+
(SocketTimeoutError / ServerTimeoutError) into a ``ReadTimeoutError`` so a read
|
|
79
|
+
timeout always surfaces as one deterministic, transport-agnostic type. It subclasses
|
|
80
|
+
the builtin ``TimeoutError`` so callers can still ``except TimeoutError``."""
|
|
81
|
+
try:
|
|
82
|
+
return loop.run_until_complete(coro)
|
|
83
|
+
except aiohttp.ServerTimeoutError as e:
|
|
84
|
+
raise ReadTimeoutError(
|
|
85
|
+
f"Read timed out after {read_timeout}s waiting for response data.") from e
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _keep_alive_socket_options(keep_alive_time):
|
|
89
|
+
"""Build the list of socket options that enable TCP keep-alive with the
|
|
90
|
+
given idle time before probes begin. TCP_KEEPIDLE is platform dependent
|
|
91
|
+
(Linux); macOS exposes the equivalent as TCP_KEEPALIVE. Both are guarded so
|
|
92
|
+
platforms lacking the option (e.g. Windows) simply enable SO_KEEPALIVE."""
|
|
93
|
+
options = [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)]
|
|
94
|
+
idle_opt = getattr(socket, 'TCP_KEEPIDLE', None)
|
|
95
|
+
if idle_opt is None:
|
|
96
|
+
# macOS names the idle-before-probe option TCP_KEEPALIVE
|
|
97
|
+
idle_opt = getattr(socket, 'TCP_KEEPALIVE', None)
|
|
98
|
+
if idle_opt is not None:
|
|
99
|
+
options.append((socket.IPPROTO_TCP, idle_opt, int(keep_alive_time)))
|
|
100
|
+
return options
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _keep_alive_socket_factory(keep_alive_time):
|
|
104
|
+
"""Return a socket_factory (aiohttp >= 3.11) that applies the keep-alive
|
|
105
|
+
socket options when each connection socket is created."""
|
|
106
|
+
options = _keep_alive_socket_options(keep_alive_time)
|
|
107
|
+
|
|
108
|
+
def factory(addr_info):
|
|
109
|
+
family, type_, proto, _, _ = addr_info
|
|
110
|
+
sock = socket.socket(family=family, type=type_, proto=proto)
|
|
111
|
+
for level, optname, value in options:
|
|
112
|
+
try:
|
|
113
|
+
sock.setsockopt(level, optname, value)
|
|
114
|
+
except (OSError, AttributeError):
|
|
115
|
+
pass
|
|
116
|
+
return sock
|
|
117
|
+
|
|
118
|
+
return factory
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class AiohttpSyncStream:
|
|
122
|
+
"""Wraps aiohttp's async StreamReader as a synchronous file-like object.
|
|
123
|
+
read(n) blocks until exactly n bytes are available from the HTTP response.
|
|
124
|
+
|
|
125
|
+
Maintains an internal byte buffer that is refilled one HTTP chunk at a time
|
|
126
|
+
so the deserializer's many small read(n) calls don't each pay the cost of a
|
|
127
|
+
full asyncio event-loop turn."""
|
|
128
|
+
|
|
129
|
+
# Max bytes pulled from the response per underlying read. Matches
|
|
130
|
+
# aiohttp.StreamReader's default 64 KB limit, which is the per-connection
|
|
131
|
+
# high-water mark, asking for more in one read() never returns more.
|
|
132
|
+
_FILL_SIZE = 64 * 1024
|
|
133
|
+
|
|
134
|
+
def __init__(self, response, loop, read_timeout):
|
|
135
|
+
self._response = response
|
|
136
|
+
self._loop = loop
|
|
137
|
+
self._read_timeout = read_timeout
|
|
138
|
+
self._buf = bytearray()
|
|
139
|
+
self._pos = 0
|
|
140
|
+
|
|
141
|
+
def read(self, n):
|
|
142
|
+
if n <= 0:
|
|
143
|
+
return b''
|
|
144
|
+
while len(self._buf) - self._pos < n:
|
|
145
|
+
data = self._read_chunk()
|
|
146
|
+
if not data:
|
|
147
|
+
partial = bytes(self._buf[self._pos:])
|
|
148
|
+
self._buf.clear()
|
|
149
|
+
self._pos = 0
|
|
150
|
+
raise asyncio.IncompleteReadError(partial=partial, expected=n)
|
|
151
|
+
self._buf.extend(data)
|
|
152
|
+
end = self._pos + n
|
|
153
|
+
out = bytes(self._buf[self._pos:end])
|
|
154
|
+
self._pos = end
|
|
155
|
+
# Reclaim memory once the buffer is fully drained
|
|
156
|
+
if self._pos == len(self._buf):
|
|
157
|
+
self._buf.clear()
|
|
158
|
+
self._pos = 0
|
|
159
|
+
return out
|
|
160
|
+
|
|
161
|
+
def _read_chunk(self):
|
|
162
|
+
return _run_read(self._loop, self._read_timeout,
|
|
163
|
+
self._response.content.read(self._FILL_SIZE))
|
|
164
|
+
|
|
35
165
|
|
|
36
|
-
class AiohttpHTTPTransport
|
|
166
|
+
class AiohttpHTTPTransport:
|
|
37
167
|
nest_asyncio_applied = False
|
|
38
168
|
|
|
39
|
-
def __init__(self, call_from_event_loop=None,
|
|
169
|
+
def __init__(self, call_from_event_loop=None, write_timeout=None,
|
|
170
|
+
connect_timeout_millis=None, connect_timeout=None,
|
|
171
|
+
idle_timeout_millis=None, idle_timeout=None,
|
|
172
|
+
read_timeout_millis=None, read_timeout=None,
|
|
173
|
+
keep_alive_time_millis=None, keep_alive_time=None,
|
|
174
|
+
compression=DEFAULT_COMPRESSION, proxy=None, trust_env=False,
|
|
175
|
+
max_connections=None, **kwargs):
|
|
40
176
|
if call_from_event_loop is not None and call_from_event_loop and not AiohttpHTTPTransport.nest_asyncio_applied:
|
|
41
177
|
"""
|
|
42
178
|
The AiohttpTransport implementation uses the asyncio event loop. Because of this, it cannot be called
|
|
@@ -53,18 +189,36 @@ class AiohttpHTTPTransport(AbstractBaseTransport):
|
|
|
53
189
|
self._client_session = None
|
|
54
190
|
self._http_req_resp = None
|
|
55
191
|
self._enable_ssl = False
|
|
192
|
+
self._ssl_context = None
|
|
56
193
|
self._url = None
|
|
57
194
|
|
|
58
195
|
# Set all inner variables to parameters passed in.
|
|
59
196
|
self._aiohttp_kwargs = kwargs
|
|
60
197
|
self._write_timeout = write_timeout
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
198
|
+
|
|
199
|
+
# Timeouts accept a millisecond number (*_millis, primary form) or the idiomatic seconds number
|
|
200
|
+
# (unsuffixed). read_timeout defaults off; the others use the canonical millisecond defaults.
|
|
201
|
+
self._read_timeout = _resolve_timeout_seconds(read_timeout_millis, read_timeout, None)
|
|
202
|
+
|
|
203
|
+
# Connection-level pooling / lifecycle options.
|
|
204
|
+
self._connect_timeout = _resolve_timeout_seconds(connect_timeout_millis, connect_timeout, DEFAULT_CONNECT_TIMEOUT_MILLIS)
|
|
205
|
+
self._idle_timeout = _resolve_timeout_seconds(idle_timeout_millis, idle_timeout, DEFAULT_IDLE_TIMEOUT_MILLIS)
|
|
206
|
+
self._keep_alive_time = _resolve_timeout_seconds(keep_alive_time_millis, keep_alive_time, DEFAULT_KEEP_ALIVE_TIME_MILLIS)
|
|
207
|
+
# Caps the aiohttp connector's simultaneous connections per Connection
|
|
208
|
+
# (the Client also sizes its Connection pool by this value).
|
|
209
|
+
self._max_connections = max_connections
|
|
210
|
+
|
|
211
|
+
# Compression negotiation. Default 'deflate' (on); advertises
|
|
212
|
+
# Accept-Encoding: deflate. Set 'none' to opt out.
|
|
213
|
+
self._compression = _normalize_compression(compression)
|
|
214
|
+
|
|
215
|
+
# HTTP proxy support routed through the ClientSession.
|
|
216
|
+
self._proxy = proxy
|
|
217
|
+
self._trust_env = trust_env
|
|
218
|
+
|
|
219
|
+
# ssl: canonical name accepting an ssl.SSLContext.
|
|
220
|
+
if "ssl" in self._aiohttp_kwargs:
|
|
221
|
+
self._ssl_context = self._aiohttp_kwargs.pop("ssl")
|
|
68
222
|
self._enable_ssl = True
|
|
69
223
|
|
|
70
224
|
def __del__(self):
|
|
@@ -76,90 +230,108 @@ class AiohttpHTTPTransport(AbstractBaseTransport):
|
|
|
76
230
|
self._url = url
|
|
77
231
|
# Inner function to perform async connect.
|
|
78
232
|
async def async_connect():
|
|
79
|
-
#
|
|
233
|
+
# Build the TCP connector with the standardized pooling / lifecycle
|
|
234
|
+
# options. keepalive_timeout maps to the idle connection timeout;
|
|
235
|
+
# the socket factory enables TCP keep-alive probes after
|
|
236
|
+
# keep_alive_time idle.
|
|
237
|
+
connector_kwargs = {}
|
|
80
238
|
if self._enable_ssl:
|
|
81
|
-
# ssl context is established through tcp connector
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
self.
|
|
239
|
+
# ssl context is established through the tcp connector
|
|
240
|
+
connector_kwargs['ssl_context'] = self._ssl_context
|
|
241
|
+
if self._idle_timeout is not None:
|
|
242
|
+
connector_kwargs['keepalive_timeout'] = self._idle_timeout
|
|
243
|
+
if self._keep_alive_time is not None:
|
|
244
|
+
self._apply_keep_alive(connector_kwargs)
|
|
245
|
+
# Reflect max_connections at the aiohttp layer so the connector's
|
|
246
|
+
# simultaneous-connection limit matches the driver option.
|
|
247
|
+
if self._max_connections is not None:
|
|
248
|
+
connector_kwargs['limit'] = self._max_connections
|
|
249
|
+
|
|
250
|
+
session_kwargs = {'headers': headers, 'loop': self._loop,
|
|
251
|
+
'trust_env': self._trust_env}
|
|
252
|
+
# Use the per-socket timeouts (sock_connect/sock_read) rather than a
|
|
253
|
+
# whole-request total, which would abort long but legitimate streaming
|
|
254
|
+
# responses. sock_read bounds idle time between chunks so a stalled
|
|
255
|
+
# server cannot hang forever.
|
|
256
|
+
timeout_kwargs = {}
|
|
257
|
+
if self._connect_timeout is not None:
|
|
258
|
+
timeout_kwargs['sock_connect'] = self._connect_timeout
|
|
259
|
+
if self._read_timeout is not None:
|
|
260
|
+
timeout_kwargs['sock_read'] = self._read_timeout
|
|
261
|
+
if timeout_kwargs:
|
|
262
|
+
session_kwargs['timeout'] = aiohttp.ClientTimeout(**timeout_kwargs)
|
|
263
|
+
if connector_kwargs:
|
|
264
|
+
session_kwargs['connector'] = aiohttp.TCPConnector(**connector_kwargs)
|
|
265
|
+
|
|
266
|
+
self._client_session = aiohttp.ClientSession(**session_kwargs)
|
|
87
267
|
|
|
88
268
|
# Execute the async connect synchronously.
|
|
89
269
|
self._loop.run_until_complete(async_connect())
|
|
90
270
|
|
|
271
|
+
def _apply_keep_alive(self, connector_kwargs):
|
|
272
|
+
"""Wire TCP keep-alive into the connector via the aiohttp socket_factory.
|
|
273
|
+
The factory sets SO_KEEPALIVE plus the per-socket idle time; unsupported
|
|
274
|
+
platforms degrade gracefully inside the factory."""
|
|
275
|
+
connector_kwargs['socket_factory'] = _keep_alive_socket_factory(self._keep_alive_time)
|
|
276
|
+
|
|
91
277
|
def write(self, message):
|
|
278
|
+
# Negotiate compression unless the caller already set Accept-Encoding:
|
|
279
|
+
# deflate advertises Accept-Encoding: deflate; none suppresses aiohttp's
|
|
280
|
+
# auto-injected Accept-Encoding so compression is not silently negotiated.
|
|
281
|
+
headers = message['headers']
|
|
282
|
+
has_accept_encoding = any(k.lower() == 'accept-encoding' for k in headers)
|
|
283
|
+
skip_auto_headers = None
|
|
284
|
+
if self._compression == 'deflate':
|
|
285
|
+
if not has_accept_encoding:
|
|
286
|
+
headers['accept-encoding'] = 'deflate'
|
|
287
|
+
elif not has_accept_encoding:
|
|
288
|
+
skip_auto_headers = ['Accept-Encoding']
|
|
289
|
+
|
|
92
290
|
# Inner function to perform async write.
|
|
93
291
|
async def async_write():
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
292
|
+
post_kwargs = dict(self._aiohttp_kwargs)
|
|
293
|
+
if self._proxy is not None:
|
|
294
|
+
post_kwargs['proxy'] = self._proxy
|
|
295
|
+
if skip_auto_headers is not None:
|
|
296
|
+
post_kwargs['skip_auto_headers'] = skip_auto_headers
|
|
99
297
|
async with async_timeout.timeout(self._write_timeout):
|
|
100
298
|
self._http_req_resp = await self._client_session.post(url=self._url,
|
|
101
299
|
data=message['payload'],
|
|
102
|
-
headers=
|
|
103
|
-
**
|
|
300
|
+
headers=headers,
|
|
301
|
+
**post_kwargs)
|
|
104
302
|
|
|
105
303
|
# Execute the async write synchronously.
|
|
106
304
|
self._loop.run_until_complete(async_write())
|
|
107
305
|
|
|
108
|
-
def
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
async def async_read():
|
|
138
|
-
# TODO: potentially refactor to just use streaming and remove transport/protocol
|
|
139
|
-
async with async_timeout.timeout(self._read_timeout):
|
|
140
|
-
read_completed = False
|
|
141
|
-
# aiohttp streaming may not iterate through one whole chunk if it's too large, need to buffer it
|
|
142
|
-
data_buffer = b""
|
|
143
|
-
async for data, end_of_http_chunk in self._http_req_resp.content.iter_chunks():
|
|
144
|
-
try:
|
|
145
|
-
data_buffer += data
|
|
146
|
-
if end_of_http_chunk:
|
|
147
|
-
if self._max_content_len and len(
|
|
148
|
-
data_buffer) > self._max_content_len:
|
|
149
|
-
raise Exception( # TODO: do we need proper exception class for this?
|
|
150
|
-
f'Response size {len(data_buffer)} exceeds limit {self._max_content_len} bytes')
|
|
151
|
-
stream_chunk(data_buffer, read_completed, self._http_req_resp.ok)
|
|
152
|
-
data_buffer = b""
|
|
153
|
-
except ClientPayloadError:
|
|
154
|
-
# server disconnect during streaming will cause ClientPayLoadError from aiohttp
|
|
155
|
-
# TODO: double check during refactoring
|
|
156
|
-
raise GremlinServerError({'code': 500,
|
|
157
|
-
'message': 'Server disconnected - please try to reconnect',
|
|
158
|
-
'exception': ClientPayloadError})
|
|
159
|
-
read_completed = True
|
|
160
|
-
stream_chunk(data_buffer, read_completed, self._http_req_resp.ok)
|
|
161
|
-
|
|
162
|
-
return self._loop.run_until_complete(async_read())
|
|
306
|
+
def get_stream(self):
|
|
307
|
+
"""Returns a synchronous file-like object for the HTTP response body."""
|
|
308
|
+
return AiohttpSyncStream(self._http_req_resp, self._loop, self._read_timeout)
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def content_type(self):
|
|
312
|
+
"""Returns the Content-Type header of the HTTP response."""
|
|
313
|
+
if self._http_req_resp is not None:
|
|
314
|
+
return self._http_req_resp.headers.get('content-type', '')
|
|
315
|
+
return ''
|
|
316
|
+
|
|
317
|
+
@property
|
|
318
|
+
def status_code(self):
|
|
319
|
+
"""Returns the HTTP status code of the response."""
|
|
320
|
+
if self._http_req_resp is not None:
|
|
321
|
+
return self._http_req_resp.status
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
def read_body(self):
|
|
325
|
+
"""Read the entire HTTP response body as bytes."""
|
|
326
|
+
return _run_read(self._loop, self._read_timeout, self._http_req_resp.read())
|
|
327
|
+
|
|
328
|
+
def evict_response(self):
|
|
329
|
+
"""Close the current HTTP response and its underlying connection, evicting it from
|
|
330
|
+
aiohttp's connection pool. Used after a transport failure so a dead/half-closed
|
|
331
|
+
connection is discarded rather than returned to the pool for reuse."""
|
|
332
|
+
if self._http_req_resp is not None:
|
|
333
|
+
self._http_req_resp.close()
|
|
334
|
+
self._http_req_resp = None
|
|
163
335
|
|
|
164
336
|
def close(self):
|
|
165
337
|
# Inner function to perform async close.
|
|
@@ -178,5 +350,5 @@ class AiohttpHTTPTransport(AbstractBaseTransport):
|
|
|
178
350
|
|
|
179
351
|
@property
|
|
180
352
|
def closed(self):
|
|
181
|
-
# Connection is closed when client session is closed.
|
|
182
|
-
return self._client_session.closed
|
|
353
|
+
# Connection is closed when client session is closed (or not yet created).
|
|
354
|
+
return self._client_session is None or self._client_session.closed
|
gremlin_python/driver/auth.py
CHANGED
|
@@ -16,40 +16,65 @@
|
|
|
16
16
|
# specific language governing permissions and limitations
|
|
17
17
|
# under the License.
|
|
18
18
|
#
|
|
19
|
+
import base64
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
def basic(username, password):
|
|
22
|
-
|
|
23
|
+
"""Returns an interceptor that adds Basic auth to the request."""
|
|
24
|
+
def interceptor(request):
|
|
25
|
+
credentials = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8")
|
|
26
|
+
request.headers['authorization'] = f"Basic {credentials}"
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
return request['headers'].update({'authorization': aiohttpBasicAuth(username, password).encode()})
|
|
28
|
+
return interceptor
|
|
26
29
|
|
|
27
|
-
return apply
|
|
28
30
|
|
|
31
|
+
def sigv4(region, service, credentials=None):
|
|
32
|
+
"""Returns an interceptor that signs the request with AWS SigV4.
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
By default credentials are sourced from the standard AWS environment
|
|
35
|
+
variables (``AWS_ACCESS_KEY_ID``, ``AWS_SECRET_ACCESS_KEY`` and the optional
|
|
36
|
+
``AWS_SESSION_TOKEN``). A custom credentials provider may be supplied via
|
|
37
|
+
``credentials``; it accepts either:
|
|
38
|
+
|
|
39
|
+
* a callable returning a botocore ``Credentials`` object (or any object with
|
|
40
|
+
``access_key``/``secret_key``/``token`` attributes), evaluated per request, or
|
|
41
|
+
* a botocore ``Credentials`` object (or ``Session``) used directly.
|
|
42
|
+
|
|
43
|
+
When no provider is given, signing falls back to the environment.
|
|
44
|
+
"""
|
|
31
45
|
import os
|
|
32
46
|
from boto3 import Session
|
|
33
47
|
from botocore.auth import SigV4Auth
|
|
34
48
|
from botocore.awsrequest import AWSRequest
|
|
35
49
|
|
|
36
|
-
def
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
def _resolve_credentials():
|
|
51
|
+
if credentials is None:
|
|
52
|
+
access_key = os.environ.get('AWS_ACCESS_KEY_ID', '')
|
|
53
|
+
secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY', '')
|
|
54
|
+
session_token = os.environ.get('AWS_SESSION_TOKEN', '')
|
|
55
|
+
session = Session(
|
|
56
|
+
aws_access_key_id=access_key,
|
|
57
|
+
aws_secret_access_key=secret_key,
|
|
58
|
+
aws_session_token=session_token,
|
|
59
|
+
region_name=region
|
|
60
|
+
)
|
|
61
|
+
return session.get_credentials()
|
|
62
|
+
|
|
63
|
+
provider = credentials() if callable(credentials) else credentials
|
|
64
|
+
# A botocore Session exposes get_credentials(); a Credentials object is
|
|
65
|
+
# already usable as-is.
|
|
66
|
+
if hasattr(provider, 'get_credentials'):
|
|
67
|
+
return provider.get_credentials()
|
|
68
|
+
return provider
|
|
40
69
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
aws_session_token=session_token,
|
|
45
|
-
region_name=region
|
|
46
|
-
)
|
|
70
|
+
def interceptor(request):
|
|
71
|
+
# Ensure body is serialized so we can sign it
|
|
72
|
+
body_bytes = request.serialize_body()
|
|
47
73
|
|
|
48
|
-
|
|
49
|
-
SigV4Auth(session.get_credentials(), service, region).add_auth(sigv4_request)
|
|
50
|
-
request['headers'].update(sigv4_request.headers)
|
|
51
|
-
request['payload'] = sigv4_request.data
|
|
52
|
-
return request
|
|
74
|
+
resolved = _resolve_credentials()
|
|
53
75
|
|
|
54
|
-
|
|
76
|
+
sigv4_request = AWSRequest(method=request.method, url=request.url, data=body_bytes)
|
|
77
|
+
SigV4Auth(resolved, service, region).add_auth(sigv4_request)
|
|
78
|
+
request.headers.update(dict(sigv4_request.headers))
|
|
55
79
|
|
|
80
|
+
return interceptor
|