gremlinpython 4.0.0b2__tar.gz → 4.0.0.dev1__tar.gz

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.
Files changed (47) hide show
  1. {gremlinpython-4.0.0b2/gremlinpython.egg-info → gremlinpython-4.0.0.dev1}/PKG-INFO +7 -5
  2. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlin_python/__init__.py +1 -1
  3. gremlinpython-4.0.0.dev1/gremlin_python/driver/aiohttp/transport.py +354 -0
  4. gremlinpython-4.0.0.dev1/gremlin_python/driver/auth.py +80 -0
  5. gremlinpython-4.0.0.dev1/gremlin_python/driver/client.py +209 -0
  6. gremlinpython-4.0.0.dev1/gremlin_python/driver/connection.py +212 -0
  7. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlin_python/driver/driver_remote_connection.py +16 -39
  8. gremlinpython-4.0.0b2/gremlin_python/driver/transport.py → gremlinpython-4.0.0.dev1/gremlin_python/driver/exceptions.py +10 -24
  9. gremlinpython-4.0.0.dev1/gremlin_python/driver/http_request.py +65 -0
  10. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlin_python/driver/request.py +3 -2
  11. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlin_python/driver/resultset.py +11 -5
  12. gremlinpython-4.0.0.dev1/gremlin_python/driver/serializer.py +130 -0
  13. gremlinpython-4.0.0.dev1/gremlin_python/driver/transaction.py +178 -0
  14. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlin_python/driver/useragent.py +1 -1
  15. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlin_python/process/graph_traversal.py +210 -93
  16. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlin_python/process/traversal.py +114 -51
  17. gremlinpython-4.0.0.dev1/gremlin_python/structure/graph.py +588 -0
  18. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlin_python/structure/io/graphbinaryV4.py +177 -37
  19. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1/gremlinpython.egg-info}/PKG-INFO +7 -5
  20. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlinpython.egg-info/SOURCES.txt +3 -3
  21. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlinpython.egg-info/requires.txt +1 -6
  22. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/pyproject.toml +7 -4
  23. gremlinpython-4.0.0b2/gremlin_python/driver/aiohttp/transport.py +0 -182
  24. gremlinpython-4.0.0b2/gremlin_python/driver/auth.py +0 -55
  25. gremlinpython-4.0.0b2/gremlin_python/driver/client.py +0 -181
  26. gremlinpython-4.0.0b2/gremlin_python/driver/connection.py +0 -108
  27. gremlinpython-4.0.0b2/gremlin_python/driver/protocol.py +0 -175
  28. gremlinpython-4.0.0b2/gremlin_python/driver/serializer.py +0 -210
  29. gremlinpython-4.0.0b2/gremlin_python/structure/graph.py +0 -143
  30. gremlinpython-4.0.0b2/gremlin_python/structure/io/graphsonV4.py +0 -599
  31. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/LICENSE +0 -0
  32. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/MANIFEST.in +0 -0
  33. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/NOTICE +0 -0
  34. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/README.rst +0 -0
  35. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlin_python/driver/__init__.py +0 -0
  36. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlin_python/driver/aiohttp/__init__.py +0 -0
  37. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlin_python/driver/remote_connection.py +0 -0
  38. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlin_python/process/__init__.py +0 -0
  39. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlin_python/process/anonymous_traversal.py +0 -0
  40. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlin_python/process/strategies.py +0 -0
  41. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlin_python/statics.py +0 -0
  42. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlin_python/structure/__init__.py +0 -0
  43. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlin_python/structure/io/__init__.py +0 -0
  44. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlin_python/structure/io/util.py +0 -0
  45. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlinpython.egg-info/dependency_links.txt +0 -0
  46. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/gremlinpython.egg-info/top_level.txt +0 -0
  47. {gremlinpython-4.0.0b2 → gremlinpython-4.0.0.dev1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gremlinpython
3
- Version: 4.0.0b2
3
+ Version: 4.0.0.dev1
4
4
  Summary: Gremlin-Python for Apache TinkerPop
5
5
  Maintainer-email: Apache TinkerPop <dev@tinkerpop.apache.org>
6
6
  License: Apache 2
@@ -9,19 +9,21 @@ Classifier: Intended Audience :: Developers
9
9
  Classifier: License :: OSI Approved :: Apache Software License
10
10
  Classifier: Natural Language :: English
11
11
  Classifier: Programming Language :: Python :: 3
12
- Requires-Python: >=3.10
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Python: <3.14,>=3.10
13
17
  Description-Content-Type: text/x-rst
14
18
  License-File: LICENSE
15
19
  License-File: NOTICE
16
20
  Requires-Dist: nest_asyncio
17
- Requires-Dist: aiohttp<4.0.0,>=3.8.0
21
+ Requires-Dist: aiohttp<4.0.0,>=3.11.0
18
22
  Requires-Dist: aenum<4.0.0,>=1.4.5
19
23
  Requires-Dist: isodate<1.0.0,>=0.6.0
20
24
  Requires-Dist: boto3
21
25
  Requires-Dist: botocore
22
26
  Requires-Dist: async-timeout<5.0,>=4.0.3; python_version < "3.11"
23
- Provides-Extra: kerberos
24
- Requires-Dist: kerberos<2.0.0,>=1.3.0; sys_platform != "win32" and extra == "kerberos"
25
27
  Provides-Extra: ujson
26
28
  Requires-Dist: ujson>=2.0.0; extra == "ujson"
27
29
  Provides-Extra: test
@@ -18,4 +18,4 @@
18
18
  #
19
19
 
20
20
  __author__ = 'Marko A. Rodriguez (http://markorodriguez.com)'
21
- __version__ = '4.0.0b2'
21
+ __version__ = '4.0.0.dev1'
@@ -0,0 +1,354 @@
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 aiohttp
20
+ import asyncio
21
+ import socket
22
+ import sys
23
+
24
+ from gremlin_python.driver.exceptions import ReadTimeoutError
25
+
26
+ if sys.version_info >= (3, 11):
27
+ import asyncio as async_timeout
28
+ else:
29
+ import async_timeout
30
+
31
+ __author__ = 'Lyndon Bauto (lyndonb@bitquilltech.com)'
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
+
165
+
166
+ class AiohttpHTTPTransport:
167
+ nest_asyncio_applied = False
168
+
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):
176
+ if call_from_event_loop is not None and call_from_event_loop and not AiohttpHTTPTransport.nest_asyncio_applied:
177
+ """
178
+ The AiohttpTransport implementation uses the asyncio event loop. Because of this, it cannot be called
179
+ within an event loop without nest_asyncio. If the code is ever refactored so that it can be called
180
+ within an event loop this import and call can be removed. Without this, applications which use the
181
+ event loop to call gremlin-python (such as Jupyter) will not work.
182
+ """
183
+ import nest_asyncio
184
+ nest_asyncio.apply()
185
+ AiohttpHTTPTransport.nest_asyncio_applied = True
186
+
187
+ # Start event loop and initialize client session and response to None
188
+ self._loop = asyncio.new_event_loop()
189
+ self._client_session = None
190
+ self._http_req_resp = None
191
+ self._enable_ssl = False
192
+ self._ssl_context = None
193
+ self._url = None
194
+
195
+ # Set all inner variables to parameters passed in.
196
+ self._aiohttp_kwargs = kwargs
197
+ self._write_timeout = write_timeout
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")
222
+ self._enable_ssl = True
223
+
224
+ def __del__(self):
225
+ # Close will only actually close if things are left open, so this is safe to call.
226
+ # Clean up any connection resources and close the event loop.
227
+ self.close()
228
+
229
+ def connect(self, url, headers=None):
230
+ self._url = url
231
+ # Inner function to perform async connect.
232
+ async def async_connect():
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 = {}
238
+ if self._enable_ssl:
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)
267
+
268
+ # Execute the async connect synchronously.
269
+ self._loop.run_until_complete(async_connect())
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
+
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
+
290
+ # Inner function to perform async write.
291
+ async def async_write():
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
297
+ async with async_timeout.timeout(self._write_timeout):
298
+ self._http_req_resp = await self._client_session.post(url=self._url,
299
+ data=message['payload'],
300
+ headers=headers,
301
+ **post_kwargs)
302
+
303
+ # Execute the async write synchronously.
304
+ self._loop.run_until_complete(async_write())
305
+
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
335
+
336
+ def close(self):
337
+ # Inner function to perform async close.
338
+ async def async_close():
339
+ if self._client_session is not None and not self._client_session.closed:
340
+ await self._client_session.close()
341
+ self._client_session = None
342
+
343
+ # If the loop is not closed (connection hasn't already been closed)
344
+ if not self._loop.is_closed():
345
+ # Execute the async close synchronously.
346
+ self._loop.run_until_complete(async_close())
347
+
348
+ # Close the event loop.
349
+ self._loop.close()
350
+
351
+ @property
352
+ def closed(self):
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
@@ -0,0 +1,80 @@
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 base64
20
+
21
+
22
+ def basic(username, password):
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}"
27
+
28
+ return interceptor
29
+
30
+
31
+ def sigv4(region, service, credentials=None):
32
+ """Returns an interceptor that signs the request with AWS SigV4.
33
+
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
+ """
45
+ import os
46
+ from boto3 import Session
47
+ from botocore.auth import SigV4Auth
48
+ from botocore.awsrequest import AWSRequest
49
+
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
69
+
70
+ def interceptor(request):
71
+ # Ensure body is serialized so we can sign it
72
+ body_bytes = request.serialize_body()
73
+
74
+ resolved = _resolve_credentials()
75
+
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))
79
+
80
+ return interceptor
@@ -0,0 +1,209 @@
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 logging
20
+ import warnings
21
+ import queue
22
+ from concurrent.futures import ThreadPoolExecutor
23
+
24
+ from gremlin_python.driver import connection, request, serializer
25
+
26
+ log = logging.getLogger("gremlinpython")
27
+
28
+ # This is until concurrent.futures backport 3.1.0 release
29
+ try:
30
+ from multiprocessing import cpu_count
31
+ except ImportError:
32
+ # some platforms don't have multiprocessing
33
+ def cpu_count():
34
+ return None
35
+
36
+ __author__ = 'David M. Brown (davebshow@gmail.com), Lyndon Bauto (lyndonb@bitquilltech.com)'
37
+
38
+
39
+ class Client:
40
+
41
+ def __init__(self, url, traversal_source, max_connections=128, max_workers=None,
42
+ response_serializer=None, interceptors=None, auth=None,
43
+ enable_user_agent_on_connect=True,
44
+ bulk_results=False, pdt_registry=None, batch_size=None,
45
+ **transport_kwargs):
46
+ log.info("Creating Client with url '%s'", url)
47
+
48
+ self._closed = False
49
+ self._url = url
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
+ self._enable_user_agent_on_connect = enable_user_agent_on_connect
54
+ self._bulk_results = bulk_results
55
+ self._traversal_source = traversal_source
56
+ if batch_size is None:
57
+ batch_size = 64
58
+ self._batch_size = batch_size
59
+ if response_serializer is None:
60
+ response_serializer = serializer.GraphBinarySerializersV4()
61
+ if pdt_registry is not None:
62
+ response_serializer.configure_pdt_registry(pdt_registry)
63
+
64
+ self._auth = auth
65
+ self._response_serializer = response_serializer
66
+ self._interceptors = interceptors
67
+
68
+ self._transport_kwargs = transport_kwargs
69
+
70
+ self._max_connections = max_connections
71
+ # This is until concurrent.futures backport 3.1.0 release
72
+ if max_workers is None:
73
+ # If your application is overlapping Gremlin I/O on multiple threads
74
+ # consider passing kwarg max_workers = (cpu_count() or 1) * 5
75
+ max_workers = max_connections
76
+ self._executor = ThreadPoolExecutor(max_workers=max_workers)
77
+ # Threadsafe queue
78
+ self._pool = queue.Queue()
79
+ self._fill_pool()
80
+
81
+ @property
82
+ def available_pool_size(self):
83
+ return self._pool.qsize()
84
+
85
+ def response_serializer(self):
86
+ return self._response_serializer
87
+
88
+ @property
89
+ def executor(self):
90
+ return self._executor
91
+
92
+ @property
93
+ def traversal_source(self):
94
+ return self._traversal_source
95
+
96
+ def _fill_pool(self):
97
+ for i in range(self._max_connections):
98
+ conn = self._get_connection()
99
+ self._pool.put_nowait(conn)
100
+
101
+ def is_closed(self):
102
+ return self._closed
103
+
104
+ def close(self):
105
+ # prevent the Client from being closed more than once. it raises errors if new jobby jobs
106
+ # get submitted to the executor when it is shutdown
107
+ if self._closed:
108
+ return
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
+
119
+ log.info("Closing Client with url '%s'", self._url)
120
+ while not self._pool.empty():
121
+ conn = self._pool.get(True)
122
+ conn.close()
123
+ self._executor.shutdown()
124
+ self._closed = True
125
+
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.
137
+
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)
144
+
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):
160
+ warnings.warn(
161
+ "gremlin_python.driver.client.Client.submitAsync will be replaced by "
162
+ "gremlin_python.driver.client.Client.submit_async.",
163
+ DeprecationWarning)
164
+ return self.submit_async(message, parameters, request_options)
165
+
166
+ def submit_async(self, message, parameters=None, request_options=None):
167
+ if self.is_closed():
168
+ raise Exception("Client is closed")
169
+
170
+ log.debug("message '%s'", str(message))
171
+ fields = {'g': self._traversal_source}
172
+
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
180
+
181
+ if isinstance(message, str):
182
+ log.debug("fields='%s', gremlin='%s'", str(fields), str(message))
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))
192
+
193
+ conn = self._pool.get(True)
194
+ if request_options:
195
+ message.fields.update({token: request_options[token] for token in request.Tokens
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
208
+
209
+ return conn.write(message)