e6data-python-connector 2.3.14__tar.gz → 3.0.0__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.
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/PKG-INFO +103 -6
- e6data_python_connector-2.3.14/e6data_python_connector.egg-info/PKG-INFO → e6data_python_connector-3.0.0/README.md +94 -43
- e6data_python_connector-3.0.0/e6data_python_connector/aio.py +26 -0
- e6data_python_connector-3.0.0/e6data_python_connector/async_cluster_manager.py +267 -0
- e6data_python_connector-3.0.0/e6data_python_connector/async_connection.py +547 -0
- e6data_python_connector-3.0.0/e6data_python_connector/async_connection_pool.py +425 -0
- e6data_python_connector-3.0.0/e6data_python_connector/async_cursor.py +562 -0
- e6data_python_connector-3.0.0/e6data_python_connector/async_dialect.py +284 -0
- e6data_python_connector-3.0.0/e6data_python_connector/async_oauth.py +248 -0
- e6data_python_connector-3.0.0/e6data_python_connector/async_work.py +105 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/cluster_manager.py +106 -1
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/common.py +2 -1
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/datainputstream.py +43 -9
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/date_time_utils.py +4 -8
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/dialect.py +18 -1
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/e6data_grpc.py +536 -79
- e6data_python_connector-3.0.0/e6data_python_connector/exceptions.py +43 -0
- e6data_python_connector-3.0.0/e6data_python_connector/oauth.py +185 -0
- e6data_python_connector-3.0.0/e6data_python_connector/oauth_cache.py +238 -0
- e6data_python_connector-3.0.0/e6data_python_connector/oauth_common.py +66 -0
- e6data_python_connector-3.0.0/e6data_python_connector/server/e6x_engine_pb2.py +207 -0
- e6data_python_connector-2.3.14/README.md → e6data_python_connector-3.0.0/e6data_python_connector.egg-info/PKG-INFO +140 -4
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector.egg-info/SOURCES.txt +34 -5
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector.egg-info/entry_points.txt +1 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector.egg-info/requires.txt +7 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector.egg-info/top_level.txt +0 -1
- e6data_python_connector-3.0.0/pyproject.toml +12 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/setup.py +7 -2
- e6data_python_connector-3.0.0/test/conftest.py +18 -0
- e6data_python_connector-3.0.0/test/integration/__init__.py +1 -0
- e6data_python_connector-3.0.0/test/integration/benchmark_preflight.py +30 -0
- e6data_python_connector-3.0.0/test/integration/benchmark_sync_async.py +177 -0
- e6data_python_connector-3.0.0/test/integration/expiry_preflight.py +37 -0
- e6data_python_connector-3.0.0/test/integration/support.py +173 -0
- e6data_python_connector-3.0.0/test/integration/test_async_connection.py +24 -0
- e6data_python_connector-3.0.0/test/integration/test_async_pool.py +51 -0
- e6data_python_connector-3.0.0/test/integration/test_async_queries.py +62 -0
- e6data_python_connector-3.0.0/test/integration/test_async_resume.py +26 -0
- e6data_python_connector-3.0.0/test/integration/test_async_sqlalchemy.py +45 -0
- e6data_python_connector-3.0.0/test/integration/test_async_token_lifecycle.py +35 -0
- e6data_python_connector-3.0.0/test/integration/test_e2e_auth_boundary.py +34 -0
- e6data_python_connector-3.0.0/test/integration/test_e2e_oauth_continuation.py +57 -0
- e6data_python_connector-3.0.0/test/integration/test_e2e_readonly_matrix.py +173 -0
- e6data_python_connector-3.0.0/test/integration/test_shared_token_issuance.py +97 -0
- e6data_python_connector-3.0.0/test/integration/test_sync_oauth_lifecycle.py +20 -0
- e6data_python_connector-3.0.0/test/integration/token_observation.py +55 -0
- e6data_python_connector-3.0.0/test/test_cluster_manager_efficiency.py +105 -0
- e6data_python_connector-3.0.0/test/test_cluster_manager_oauth.py +193 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_cluster_manager_strategy.py +1 -1
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_connection_pool.py +33 -20
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_dialect_columns.py +32 -0
- e6data_python_connector-3.0.0/test/test_oauth_authentication.py +592 -0
- e6data_python_connector-3.0.0/test/test_oauth_auto_resume.py +409 -0
- e6data_python_connector-3.0.0/test/test_oauth_resume_integration.py +244 -0
- e6data_python_connector-2.3.14/e6data_python_connector/exceptions.py +0 -15
- e6data_python_connector-2.3.14/e6data_python_connector/server/e6x_engine_pb2.py +0 -207
- e6data_python_connector-2.3.14/gen-py/__init__.py +0 -0
- e6data_python_connector-2.3.14/gen-py/e6x_vector/__init__.py +0 -1
- e6data_python_connector-2.3.14/gen-py/e6x_vector/constants.py +0 -15
- e6data_python_connector-2.3.14/gen-py/e6x_vector/ttypes.py +0 -1875
- e6data_python_connector-2.3.14/pyproject.toml +0 -5
- e6data_python_connector-2.3.14/test/test_cluster_manager_efficiency.py +0 -198
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/LICENSE +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/MANIFEST.in +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/__init__.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/cluster_server/__init__.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/connection_pool.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/constants.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/e6x_vector/__init__.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/e6x_vector/constants.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/server/QueryEngineService.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/server/__init__.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/server/constants.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/server/ttypes.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/strategy.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector/typeId.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/setup.cfg +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/__init__.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/analyze_38_nines.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/analyze_all_cases.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/analyze_binary.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/analyze_correct_value.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/analyze_fields.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/check_decimal_errors.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/cleanup_test_files.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/debug_38_nines.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/debug_binary.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/final_test.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/move_tests.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/quick_test.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_38_nines.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_all_decimal128_cases.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_cluster_manager_none_strategy.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_code_optimizations.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_comprehensive.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_connection_pool_e2e.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_current_implementation.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_decimal128_binary_parsing.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_decimal128_parsing.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_fix.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_improved_parsing.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_known_case.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_manual_analysis.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_mock_server.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_multiprocessing_fix.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_new_implementation.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_pool_concurrency_simple.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_pool_threading_multiprocessing.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_simple_connection_pool.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_specific_binary.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_strategy.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_strategy_logic.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_strategy_persistence_fix.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_strategy_sharing_fix.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/test_user_binary_value.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/tests.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/tests_grpc.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/validate_decimal128.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/validate_implementation.py +0 -0
- {e6data_python_connector-2.3.14 → e6data_python_connector-3.0.0}/test/verify_decimal_fix.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: e6data-python-connector
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Client for the e6data distributed SQL Engine.
|
|
5
5
|
Home-page: https://github.com/e6x-labs/e6data-python-connector
|
|
6
6
|
Author: e6data, Inc.
|
|
@@ -27,6 +27,11 @@ Requires-Dist: pytz>=2024.1
|
|
|
27
27
|
Requires-Dist: thrift>=0.20.0
|
|
28
28
|
Requires-Dist: grpcio>=1.65.1
|
|
29
29
|
Requires-Dist: grpcio-tools>=1.65.1
|
|
30
|
+
Provides-Extra: async
|
|
31
|
+
Requires-Dist: httpx<1,>=0.28.1; extra == "async"
|
|
32
|
+
Provides-Extra: async-sqlalchemy
|
|
33
|
+
Requires-Dist: httpx<1,>=0.28.1; extra == "async-sqlalchemy"
|
|
34
|
+
Requires-Dist: SQLAlchemy[asyncio]<2.1,>=2.0; extra == "async-sqlalchemy"
|
|
30
35
|
Dynamic: author
|
|
31
36
|
Dynamic: author-email
|
|
32
37
|
Dynamic: classifier
|
|
@@ -34,16 +39,38 @@ Dynamic: description
|
|
|
34
39
|
Dynamic: description-content-type
|
|
35
40
|
Dynamic: home-page
|
|
36
41
|
Dynamic: license
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
Dynamic: provides-extra
|
|
37
44
|
Dynamic: requires-dist
|
|
38
45
|
Dynamic: summary
|
|
39
46
|
|
|
40
47
|
# e6data Python Connector
|
|
41
48
|
|
|
42
|
-
|
|
49
|
+
Package version and extras are defined in `setup.py`.
|
|
43
50
|
|
|
44
51
|
## Introduction
|
|
45
52
|
|
|
46
|
-
The e6data Connector for Python provides an interface for writing Python applications that can connect to e6data and perform operations. It includes
|
|
53
|
+
The e6data Connector for Python provides an interface for writing Python applications that can connect to e6data and perform operations. It includes routing support for blue-green deployments. Active query handles remain tied to the engine that accepted them; failover does not guarantee uninterrupted results.
|
|
54
|
+
|
|
55
|
+
## Optional native asyncio API
|
|
56
|
+
|
|
57
|
+
This branch adds native async connection, cursor, pooling and SQLAlchemy support for Python 3.11+. Release qualification remains incomplete. Install this branch's built package with `[async]` or `[async-sqlalchemy]`; these instructions do not assert that the feature is published.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from e6data_python_connector.aio import AsyncConnection
|
|
61
|
+
|
|
62
|
+
async def query(config, sql):
|
|
63
|
+
async with AsyncConnection(**config) as connection:
|
|
64
|
+
async with connection.cursor() as cursor:
|
|
65
|
+
await cursor.execute(sql)
|
|
66
|
+
return await cursor.fetchall()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`config` is application-supplied connection configuration. OAuth requires verified TLS. Native `fetchone()` preserves the one-row outer list, while SQLAlchemy `e6data+asyncio` adapts it to a normal row. Ambiguous submission and incomplete result errors must not be handled by blindly retrying the query. Use context managers for bounded cleanup.
|
|
70
|
+
|
|
71
|
+
See [Async API](docs/ASYNC_API.md) for the complete API map, deadlines, pooling, SQLAlchemy and test configuration, [OAuth lifecycle](docs/OAUTH_LIFECYCLE.md) for renewal and failure behavior, and [the application example](examples/async_query.py).
|
|
72
|
+
|
|
73
|
+
The new CI keeps the full-package coverage denominator and a greater-than-80% gate. The initial baseline was about 19%, so focused async test success is not a release pass. Real-service tests require explicit configuration and are qualified separately.
|
|
47
74
|
|
|
48
75
|
### Dependencies
|
|
49
76
|
Make sure to install below dependencies and wheel before install e6data-python-connector.
|
|
@@ -120,8 +147,8 @@ The `Connection` class supports the following parameters:
|
|
|
120
147
|
|-----------|------|----------|---------|-------------|
|
|
121
148
|
| `host` | str | Yes | - | IP address or hostname of the e6data cluster |
|
|
122
149
|
| `port` | int | Yes | - | Port of the e6data engine (typically 80) |
|
|
123
|
-
| `username` | str |
|
|
124
|
-
| `password` | str |
|
|
150
|
+
| `username` | str | Conditional | None | Your e6data Email ID. Required unless authenticating with OAuth |
|
|
151
|
+
| `password` | str | Conditional | None | Access Token generated in the e6data console. Required unless authenticating with OAuth |
|
|
125
152
|
| `database` | str | No | None | Database to perform queries on |
|
|
126
153
|
| `catalog` | str | No | None | Catalog name |
|
|
127
154
|
| `cluster_name` | str | No | None | Name of the cluster for cluster-specific operations |
|
|
@@ -131,6 +158,76 @@ The `Connection` class supports the following parameters:
|
|
|
131
158
|
| `grpc_options` | dict | No | None | Additional gRPC configuration options |
|
|
132
159
|
| `debug` | bool | No | False | Enable debug logging for troubleshooting |
|
|
133
160
|
| `require_fastbinary` | bool | No | True | Require fastbinary module for Thrift deserialization. Set to False to use pure Python implementation if system dependencies cannot be installed |
|
|
161
|
+
| `client_id` | str | No | None | OAuth 2.0 client id. Use with `client_secret` and `token_url` |
|
|
162
|
+
| `client_secret` | str | No | None | OAuth 2.0 client secret |
|
|
163
|
+
| `token_url` | str | No | None | Token endpoint of the authorization server |
|
|
164
|
+
| `oauth_scope` | str | No | None | Space-delimited scopes to request. Omit for the client's full registered set |
|
|
165
|
+
| `access_token` | str | No | None | A previously obtained access token, for callers that mint their own |
|
|
166
|
+
| `client_auth_method` | str | No | `'basic'` | How client credentials reach the token endpoint: `'basic'` or `'post'` |
|
|
167
|
+
|
|
168
|
+
#### Authenticating with OAuth 2.0
|
|
169
|
+
|
|
170
|
+
As an alternative to username and password, a connection can authenticate with an OAuth 2.0 access
|
|
171
|
+
token. The connector obtains a token using the client-credentials grant, caches it, and refreshes it
|
|
172
|
+
shortly before it expires.
|
|
173
|
+
|
|
174
|
+
Matching client-credentials configurations share one token cache and concurrent
|
|
175
|
+
refresh across sync and async connections, separate pools and threads in the same
|
|
176
|
+
process. Connection and transport ownership remain local. See the
|
|
177
|
+
[OAuth lifecycle](docs/OAUTH_LIFECYCLE.md) for matching settings and expiry behavior.
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
conn = Connection(
|
|
181
|
+
host=host,
|
|
182
|
+
port=443,
|
|
183
|
+
database=database,
|
|
184
|
+
cluster_name='<cluster-name>',
|
|
185
|
+
secure=True,
|
|
186
|
+
client_id='<client_id>',
|
|
187
|
+
client_secret='<client_secret>',
|
|
188
|
+
token_url='https://<your-workspace>/oauth2/token',
|
|
189
|
+
)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
If you mint tokens yourself, pass one directly instead. The connector will not refresh it, so a
|
|
193
|
+
long-lived connection may outlive the token:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
conn = Connection(
|
|
197
|
+
host=host, port=443, database=database, cluster_name='<cluster-name>',
|
|
198
|
+
secure=True, access_token='<token>',
|
|
199
|
+
)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Supply exactly one authentication method.** Passing both username/password and OAuth settings
|
|
203
|
+
raises a `ValueError` rather than picking one, so a stale value left in a config file cannot quietly
|
|
204
|
+
win.
|
|
205
|
+
|
|
206
|
+
OAuth sends `authorization: Bearer <token>` on every RPC. It does not create an
|
|
207
|
+
authentication session; protobuf username, password and session ID fields remain
|
|
208
|
+
empty. The server stack must support and enable this bearer-only flow.
|
|
209
|
+
|
|
210
|
+
Use `secure=True` with your TLS endpoint and an HTTPS `token_url` to protect tokens
|
|
211
|
+
and client credentials. OAuth does not enable TLS automatically; `secure` defaults
|
|
212
|
+
to `False`. Set `cluster_name` to identify the target cluster explicitly.
|
|
213
|
+
|
|
214
|
+
With `auto_resume=True` (the default), OAuth can recover an initial query prepare
|
|
215
|
+
that returns the exact suspended-cluster response. This requires compatible,
|
|
216
|
+
enabled server components that authorize resume and report readiness. The connector
|
|
217
|
+
performs one recovery sequence and retries prepare after readiness. It never
|
|
218
|
+
replays query execution, metadata retrieval or result fetching, and it does not
|
|
219
|
+
treat generic connection failures as suspension.
|
|
220
|
+
|
|
221
|
+
Recovery has a shared 300-second default deadline covering lock waits, token
|
|
222
|
+
acquisition, status/resume calls, polling and the resumed prepare. Configure it with
|
|
223
|
+
`grpc_options={'grpc_auto_resume_timeout_seconds': 300}`. The deadline starts after
|
|
224
|
+
the suspended response; initial prepare has its own timeout. A dispatched resume
|
|
225
|
+
may still complete after the client times out.
|
|
226
|
+
|
|
227
|
+
Set `auto_resume=False` to disable recovery. Invalid credentials, denied permissions,
|
|
228
|
+
unsupported servers and failed or unknown cluster states stop recovery without
|
|
229
|
+
falling back to username/password authentication. Externally supplied access tokens
|
|
230
|
+
are not refreshed.
|
|
134
231
|
|
|
135
232
|
#### Secure Connection Example
|
|
136
233
|
|
|
@@ -1,49 +1,30 @@
|
|
|
1
|
-
Metadata-Version: 2.2
|
|
2
|
-
Name: e6data-python-connector
|
|
3
|
-
Version: 2.3.14
|
|
4
|
-
Summary: Client for the e6data distributed SQL Engine.
|
|
5
|
-
Home-page: https://github.com/e6x-labs/e6data-python-connector
|
|
6
|
-
Author: e6data, Inc.
|
|
7
|
-
Author-email: info@e6data.com
|
|
8
|
-
License: Apache 2.0
|
|
9
|
-
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.5
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
-
Description-Content-Type: text/markdown
|
|
21
|
-
License-File: LICENSE
|
|
22
|
-
Requires-Dist: sqlalchemy>=1.0.0
|
|
23
|
-
Requires-Dist: future>=1.0.0
|
|
24
|
-
Requires-Dist: python-dateutil>=2.9.0
|
|
25
|
-
Requires-Dist: pycryptodome>=3.19.1
|
|
26
|
-
Requires-Dist: pytz>=2024.1
|
|
27
|
-
Requires-Dist: thrift>=0.20.0
|
|
28
|
-
Requires-Dist: grpcio>=1.65.1
|
|
29
|
-
Requires-Dist: grpcio-tools>=1.65.1
|
|
30
|
-
Dynamic: author
|
|
31
|
-
Dynamic: author-email
|
|
32
|
-
Dynamic: classifier
|
|
33
|
-
Dynamic: description
|
|
34
|
-
Dynamic: description-content-type
|
|
35
|
-
Dynamic: home-page
|
|
36
|
-
Dynamic: license
|
|
37
|
-
Dynamic: requires-dist
|
|
38
|
-
Dynamic: summary
|
|
39
|
-
|
|
40
1
|
# e6data Python Connector
|
|
41
2
|
|
|
42
|
-
|
|
3
|
+
Package version and extras are defined in `setup.py`.
|
|
43
4
|
|
|
44
5
|
## Introduction
|
|
45
6
|
|
|
46
|
-
The e6data Connector for Python provides an interface for writing Python applications that can connect to e6data and perform operations. It includes
|
|
7
|
+
The e6data Connector for Python provides an interface for writing Python applications that can connect to e6data and perform operations. It includes routing support for blue-green deployments. Active query handles remain tied to the engine that accepted them; failover does not guarantee uninterrupted results.
|
|
8
|
+
|
|
9
|
+
## Optional native asyncio API
|
|
10
|
+
|
|
11
|
+
This branch adds native async connection, cursor, pooling and SQLAlchemy support for Python 3.11+. Release qualification remains incomplete. Install this branch's built package with `[async]` or `[async-sqlalchemy]`; these instructions do not assert that the feature is published.
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from e6data_python_connector.aio import AsyncConnection
|
|
15
|
+
|
|
16
|
+
async def query(config, sql):
|
|
17
|
+
async with AsyncConnection(**config) as connection:
|
|
18
|
+
async with connection.cursor() as cursor:
|
|
19
|
+
await cursor.execute(sql)
|
|
20
|
+
return await cursor.fetchall()
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`config` is application-supplied connection configuration. OAuth requires verified TLS. Native `fetchone()` preserves the one-row outer list, while SQLAlchemy `e6data+asyncio` adapts it to a normal row. Ambiguous submission and incomplete result errors must not be handled by blindly retrying the query. Use context managers for bounded cleanup.
|
|
24
|
+
|
|
25
|
+
See [Async API](docs/ASYNC_API.md) for the complete API map, deadlines, pooling, SQLAlchemy and test configuration, [OAuth lifecycle](docs/OAUTH_LIFECYCLE.md) for renewal and failure behavior, and [the application example](examples/async_query.py).
|
|
26
|
+
|
|
27
|
+
The new CI keeps the full-package coverage denominator and a greater-than-80% gate. The initial baseline was about 19%, so focused async test success is not a release pass. Real-service tests require explicit configuration and are qualified separately.
|
|
47
28
|
|
|
48
29
|
### Dependencies
|
|
49
30
|
Make sure to install below dependencies and wheel before install e6data-python-connector.
|
|
@@ -120,8 +101,8 @@ The `Connection` class supports the following parameters:
|
|
|
120
101
|
|-----------|------|----------|---------|-------------|
|
|
121
102
|
| `host` | str | Yes | - | IP address or hostname of the e6data cluster |
|
|
122
103
|
| `port` | int | Yes | - | Port of the e6data engine (typically 80) |
|
|
123
|
-
| `username` | str |
|
|
124
|
-
| `password` | str |
|
|
104
|
+
| `username` | str | Conditional | None | Your e6data Email ID. Required unless authenticating with OAuth |
|
|
105
|
+
| `password` | str | Conditional | None | Access Token generated in the e6data console. Required unless authenticating with OAuth |
|
|
125
106
|
| `database` | str | No | None | Database to perform queries on |
|
|
126
107
|
| `catalog` | str | No | None | Catalog name |
|
|
127
108
|
| `cluster_name` | str | No | None | Name of the cluster for cluster-specific operations |
|
|
@@ -131,6 +112,76 @@ The `Connection` class supports the following parameters:
|
|
|
131
112
|
| `grpc_options` | dict | No | None | Additional gRPC configuration options |
|
|
132
113
|
| `debug` | bool | No | False | Enable debug logging for troubleshooting |
|
|
133
114
|
| `require_fastbinary` | bool | No | True | Require fastbinary module for Thrift deserialization. Set to False to use pure Python implementation if system dependencies cannot be installed |
|
|
115
|
+
| `client_id` | str | No | None | OAuth 2.0 client id. Use with `client_secret` and `token_url` |
|
|
116
|
+
| `client_secret` | str | No | None | OAuth 2.0 client secret |
|
|
117
|
+
| `token_url` | str | No | None | Token endpoint of the authorization server |
|
|
118
|
+
| `oauth_scope` | str | No | None | Space-delimited scopes to request. Omit for the client's full registered set |
|
|
119
|
+
| `access_token` | str | No | None | A previously obtained access token, for callers that mint their own |
|
|
120
|
+
| `client_auth_method` | str | No | `'basic'` | How client credentials reach the token endpoint: `'basic'` or `'post'` |
|
|
121
|
+
|
|
122
|
+
#### Authenticating with OAuth 2.0
|
|
123
|
+
|
|
124
|
+
As an alternative to username and password, a connection can authenticate with an OAuth 2.0 access
|
|
125
|
+
token. The connector obtains a token using the client-credentials grant, caches it, and refreshes it
|
|
126
|
+
shortly before it expires.
|
|
127
|
+
|
|
128
|
+
Matching client-credentials configurations share one token cache and concurrent
|
|
129
|
+
refresh across sync and async connections, separate pools and threads in the same
|
|
130
|
+
process. Connection and transport ownership remain local. See the
|
|
131
|
+
[OAuth lifecycle](docs/OAUTH_LIFECYCLE.md) for matching settings and expiry behavior.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
conn = Connection(
|
|
135
|
+
host=host,
|
|
136
|
+
port=443,
|
|
137
|
+
database=database,
|
|
138
|
+
cluster_name='<cluster-name>',
|
|
139
|
+
secure=True,
|
|
140
|
+
client_id='<client_id>',
|
|
141
|
+
client_secret='<client_secret>',
|
|
142
|
+
token_url='https://<your-workspace>/oauth2/token',
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
If you mint tokens yourself, pass one directly instead. The connector will not refresh it, so a
|
|
147
|
+
long-lived connection may outlive the token:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
conn = Connection(
|
|
151
|
+
host=host, port=443, database=database, cluster_name='<cluster-name>',
|
|
152
|
+
secure=True, access_token='<token>',
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Supply exactly one authentication method.** Passing both username/password and OAuth settings
|
|
157
|
+
raises a `ValueError` rather than picking one, so a stale value left in a config file cannot quietly
|
|
158
|
+
win.
|
|
159
|
+
|
|
160
|
+
OAuth sends `authorization: Bearer <token>` on every RPC. It does not create an
|
|
161
|
+
authentication session; protobuf username, password and session ID fields remain
|
|
162
|
+
empty. The server stack must support and enable this bearer-only flow.
|
|
163
|
+
|
|
164
|
+
Use `secure=True` with your TLS endpoint and an HTTPS `token_url` to protect tokens
|
|
165
|
+
and client credentials. OAuth does not enable TLS automatically; `secure` defaults
|
|
166
|
+
to `False`. Set `cluster_name` to identify the target cluster explicitly.
|
|
167
|
+
|
|
168
|
+
With `auto_resume=True` (the default), OAuth can recover an initial query prepare
|
|
169
|
+
that returns the exact suspended-cluster response. This requires compatible,
|
|
170
|
+
enabled server components that authorize resume and report readiness. The connector
|
|
171
|
+
performs one recovery sequence and retries prepare after readiness. It never
|
|
172
|
+
replays query execution, metadata retrieval or result fetching, and it does not
|
|
173
|
+
treat generic connection failures as suspension.
|
|
174
|
+
|
|
175
|
+
Recovery has a shared 300-second default deadline covering lock waits, token
|
|
176
|
+
acquisition, status/resume calls, polling and the resumed prepare. Configure it with
|
|
177
|
+
`grpc_options={'grpc_auto_resume_timeout_seconds': 300}`. The deadline starts after
|
|
178
|
+
the suspended response; initial prepare has its own timeout. A dispatched resume
|
|
179
|
+
may still complete after the client times out.
|
|
180
|
+
|
|
181
|
+
Set `auto_resume=False` to disable recovery. Invalid credentials, denied permissions,
|
|
182
|
+
unsupported servers and failed or unknown cluster states stop recovery without
|
|
183
|
+
falling back to username/password authentication. Externally supplied access tokens
|
|
184
|
+
are not refreshed.
|
|
134
185
|
|
|
135
186
|
#### Secure Connection Example
|
|
136
187
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Explicit optional native asyncio entry point (Python 3.11+)."""
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
if sys.version_info < (3, 11):
|
|
5
|
+
raise RuntimeError('The async API requires Python 3.11 or newer.')
|
|
6
|
+
try:
|
|
7
|
+
import httpx as _httpx
|
|
8
|
+
except ImportError:
|
|
9
|
+
raise ImportError('Install e6data-python-connector[async] for the async API.') from None
|
|
10
|
+
|
|
11
|
+
from .async_connection import AsyncConnection
|
|
12
|
+
|
|
13
|
+
__all__ = ['connect', 'AsyncConnection', 'AsyncCursor', 'AsyncConnectionPool', 'AsyncClusterManager']
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def connect(*args, **kwargs):
|
|
17
|
+
return await AsyncConnection(*args, **kwargs).open()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def __getattr__(name):
|
|
21
|
+
modules = {'AsyncCursor': 'async_cursor', 'AsyncConnectionPool': 'async_connection_pool',
|
|
22
|
+
'AsyncClusterManager': 'async_cluster_manager'}
|
|
23
|
+
if name not in modules:
|
|
24
|
+
raise AttributeError(name)
|
|
25
|
+
from importlib import import_module
|
|
26
|
+
return getattr(import_module('.' + modules[name], __package__), name)
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Native async cluster recovery with owner-scoped mutation tracking."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import math
|
|
4
|
+
import os
|
|
5
|
+
import threading
|
|
6
|
+
|
|
7
|
+
import grpc
|
|
8
|
+
|
|
9
|
+
from .cluster_server import cluster_pb2 as pb, cluster_pb2_grpc as bindings
|
|
10
|
+
from .exceptions import OperationalError, ProgrammingError
|
|
11
|
+
from .oauth_common import validate_positive_timeout
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_strategy_mismatch(error):
|
|
15
|
+
return error.code() == grpc.StatusCode.UNKNOWN and error.details() == 'status: 456'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_suspended_error(error):
|
|
19
|
+
return (error.code() == grpc.StatusCode.UNAVAILABLE
|
|
20
|
+
and error.details() == 'status: 503, cluster is suspended')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _remaining(deadline):
|
|
24
|
+
result = deadline - asyncio.get_running_loop().time()
|
|
25
|
+
if result <= 0:
|
|
26
|
+
raise TimeoutError('Cluster recovery deadline exceeded; dispatched resume may still complete.')
|
|
27
|
+
return result
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _read_certificate(path):
|
|
31
|
+
with open(path, 'rb') as source:
|
|
32
|
+
return source.read()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _ResumeState:
|
|
36
|
+
"""Mutation state deliberately outlives any individual recovery flight."""
|
|
37
|
+
def __init__(self):
|
|
38
|
+
self.pending = None
|
|
39
|
+
|
|
40
|
+
def observe(self, status):
|
|
41
|
+
if status not in ('active', 'suspended', 'resuming'):
|
|
42
|
+
raise RuntimeError('Cluster returned failed or unsupported status: {!r}'.format(status))
|
|
43
|
+
if status == 'active':
|
|
44
|
+
self.pending = None
|
|
45
|
+
return status == 'suspended' and self.pending is None
|
|
46
|
+
|
|
47
|
+
def dispatched(self):
|
|
48
|
+
self.pending = 'unknown'
|
|
49
|
+
|
|
50
|
+
def acknowledged(self):
|
|
51
|
+
self.pending = 'acknowledged'
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class _ResumeFlight:
|
|
55
|
+
"""One phase deadline, independently bounded shielded waiters."""
|
|
56
|
+
def __init__(self, timeout):
|
|
57
|
+
self.timeout = timeout
|
|
58
|
+
self.task = None
|
|
59
|
+
self.waiters = 0
|
|
60
|
+
self.retiring = False
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def _consume_result(task):
|
|
64
|
+
if not task.cancelled():
|
|
65
|
+
task.exception()
|
|
66
|
+
|
|
67
|
+
async def wait(self, work, deadline):
|
|
68
|
+
# Last-waiter disposal must finish before a replacement can start.
|
|
69
|
+
while self.retiring and self.task is not None:
|
|
70
|
+
retiring_task = self.task
|
|
71
|
+
try:
|
|
72
|
+
async with asyncio.timeout_at(deadline):
|
|
73
|
+
await asyncio.shield(retiring_task)
|
|
74
|
+
except asyncio.CancelledError:
|
|
75
|
+
if asyncio.current_task().cancelling():
|
|
76
|
+
raise
|
|
77
|
+
except Exception:
|
|
78
|
+
if not retiring_task.done():
|
|
79
|
+
raise
|
|
80
|
+
if self.task is retiring_task and retiring_task.done():
|
|
81
|
+
self.task = None
|
|
82
|
+
self.retiring = False
|
|
83
|
+
if deadline is not None:
|
|
84
|
+
_remaining(deadline)
|
|
85
|
+
if self.task is None or (self.task.done() and self.waiters == 0):
|
|
86
|
+
phase_deadline = asyncio.get_running_loop().time() + self.timeout
|
|
87
|
+
self.task = asyncio.create_task(work(phase_deadline))
|
|
88
|
+
self.task.add_done_callback(self._consume_result)
|
|
89
|
+
task = self.task
|
|
90
|
+
self.waiters += 1
|
|
91
|
+
try:
|
|
92
|
+
async with asyncio.timeout_at(deadline):
|
|
93
|
+
return await asyncio.shield(task)
|
|
94
|
+
finally:
|
|
95
|
+
self.waiters -= 1
|
|
96
|
+
if self.waiters == 0 and not task.done():
|
|
97
|
+
self.retiring = True
|
|
98
|
+
task.cancel()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class AsyncClusterManager:
|
|
102
|
+
"""Local constructor; channels belong to the first invoking loop/thread/process.
|
|
103
|
+
|
|
104
|
+
Sharing this explicit object shares recovery only within its target and auth
|
|
105
|
+
owner. OAuth metadata providers must be async and own their refresh policy.
|
|
106
|
+
"""
|
|
107
|
+
def __init__(self, host, port, user='', password='', secure_channel=False,
|
|
108
|
+
timeout=300, cluster_uuid=None, grpc_options=None, debug=False,
|
|
109
|
+
ssl_cert=None, metadata_provider=None, initial_strategy=None,
|
|
110
|
+
auto_resume_timeout=None, cleanup_timeout=5):
|
|
111
|
+
if not isinstance(host, str) or not host or isinstance(port, bool) or not isinstance(port, int) or not 0 < port < 65536:
|
|
112
|
+
raise ValueError('host and a valid port are required.')
|
|
113
|
+
if metadata_provider is not None and not secure_channel:
|
|
114
|
+
raise ValueError('Async OAuth cluster recovery requires verified TLS.')
|
|
115
|
+
if metadata_provider is not None and (user or password):
|
|
116
|
+
raise ValueError('OAuth and password authentication cannot be combined.')
|
|
117
|
+
if metadata_provider is None and (not user or not password):
|
|
118
|
+
raise ValueError('user and password are required for legacy recovery.')
|
|
119
|
+
if initial_strategy not in (None, 'blue', 'green'):
|
|
120
|
+
raise ValueError('initial_strategy must be blue or green.')
|
|
121
|
+
if ssl_cert is not None and not isinstance(ssl_cert, (str, bytes)):
|
|
122
|
+
raise ValueError('ssl_cert must be a certificate path or PEM bytes.')
|
|
123
|
+
options = dict(grpc_options or {})
|
|
124
|
+
if any(key.removeprefix('grpc.') in ('ssl_target_name_override', 'default_authority') for key in options):
|
|
125
|
+
raise ValueError('TLS authority overrides are unsupported.')
|
|
126
|
+
limit = options.get('grpc.max_receive_message_length', 64 * 1024 * 1024)
|
|
127
|
+
if isinstance(limit, bool) or not isinstance(limit, int) or limit <= 0:
|
|
128
|
+
raise ValueError('A finite positive receive limit is required.')
|
|
129
|
+
options['grpc.max_receive_message_length'] = limit
|
|
130
|
+
self._host, self._port = host, port
|
|
131
|
+
self._user, self._password = user, password
|
|
132
|
+
self._secure_channel, self._ssl_cert = secure_channel, ssl_cert
|
|
133
|
+
self._grpc_options = tuple(options.items())
|
|
134
|
+
self.cluster_uuid = cluster_uuid
|
|
135
|
+
self._metadata_provider = metadata_provider
|
|
136
|
+
self.last_successful_strategy = initial_strategy
|
|
137
|
+
self._timeout = validate_positive_timeout(timeout if auto_resume_timeout is None else auto_resume_timeout)
|
|
138
|
+
self._cleanup_timeout = validate_positive_timeout(cleanup_timeout)
|
|
139
|
+
self._flight = _ResumeFlight(self._timeout)
|
|
140
|
+
self._mutation = _ResumeState()
|
|
141
|
+
self._owner = None
|
|
142
|
+
self._closed = False
|
|
143
|
+
|
|
144
|
+
def _check_owner(self):
|
|
145
|
+
owner = (os.getpid(), threading.get_ident(), asyncio.get_running_loop())
|
|
146
|
+
if self._owner is None:
|
|
147
|
+
self._owner = owner
|
|
148
|
+
elif self._owner != owner:
|
|
149
|
+
raise ProgrammingError('AsyncClusterManager cannot cross loops, threads or processes.')
|
|
150
|
+
if self._closed:
|
|
151
|
+
raise ProgrammingError('AsyncClusterManager is closed.')
|
|
152
|
+
|
|
153
|
+
async def resume(self, *, timeout=None, deadline=None):
|
|
154
|
+
self._check_owner()
|
|
155
|
+
if deadline is not None and (isinstance(deadline, bool) or not isinstance(deadline, (int, float)) or not math.isfinite(deadline)):
|
|
156
|
+
raise ValueError('deadline must be finite.')
|
|
157
|
+
if timeout is not None:
|
|
158
|
+
bound = asyncio.get_running_loop().time() + validate_positive_timeout(timeout)
|
|
159
|
+
deadline = bound if deadline is None else min(bound, deadline)
|
|
160
|
+
try:
|
|
161
|
+
if deadline is not None:
|
|
162
|
+
_remaining(deadline)
|
|
163
|
+
return await self._flight.wait(self._recover, deadline)
|
|
164
|
+
except (TimeoutError, grpc.RpcError) as error:
|
|
165
|
+
raise OperationalError('Cluster recovery failed; a dispatched resume may still complete.') from error
|
|
166
|
+
|
|
167
|
+
async def _metadata(self, strategy, deadline, rejected_token=None):
|
|
168
|
+
_remaining(deadline)
|
|
169
|
+
if self._metadata_provider is not None:
|
|
170
|
+
async with asyncio.timeout_at(deadline):
|
|
171
|
+
if rejected_token is None:
|
|
172
|
+
return await self._metadata_provider(strategy, deadline)
|
|
173
|
+
return await self._metadata_provider(strategy, deadline, rejected_token=rejected_token)
|
|
174
|
+
values = [('strategy', strategy)]
|
|
175
|
+
if self.cluster_uuid:
|
|
176
|
+
values.append(('cluster-name', self.cluster_uuid))
|
|
177
|
+
return values
|
|
178
|
+
|
|
179
|
+
async def _request(self, client, operation, deadline):
|
|
180
|
+
request_type = pb.ClusterStatusRequest if operation == 'status' else pb.ResumeRequest
|
|
181
|
+
request = request_type() if self._metadata_provider else request_type(user=self._user, password=self._password)
|
|
182
|
+
first = self.last_successful_strategy or 'blue'
|
|
183
|
+
refreshed = False
|
|
184
|
+
for strategy in (first, 'green' if first == 'blue' else 'blue'):
|
|
185
|
+
metadata = await self._metadata(strategy, deadline)
|
|
186
|
+
while True:
|
|
187
|
+
try:
|
|
188
|
+
remaining = _remaining(deadline)
|
|
189
|
+
if operation == 'resume':
|
|
190
|
+
self._mutation.dispatched()
|
|
191
|
+
response = await getattr(client, operation)(request, metadata=metadata, timeout=remaining)
|
|
192
|
+
if operation == 'resume':
|
|
193
|
+
self._mutation.acknowledged()
|
|
194
|
+
self.last_successful_strategy = strategy
|
|
195
|
+
return response
|
|
196
|
+
except grpc.RpcError as error:
|
|
197
|
+
if (error.code() == grpc.StatusCode.UNAUTHENTICATED and not refreshed
|
|
198
|
+
and self._metadata_provider is not None):
|
|
199
|
+
refreshed = True
|
|
200
|
+
rejected = dict(metadata).get('authorization', '')[len('Bearer '):]
|
|
201
|
+
replacement = await self._metadata(strategy, deadline, rejected)
|
|
202
|
+
if replacement is not None:
|
|
203
|
+
metadata = replacement
|
|
204
|
+
continue
|
|
205
|
+
if strategy == first and is_strategy_mismatch(error):
|
|
206
|
+
break
|
|
207
|
+
raise
|
|
208
|
+
|
|
209
|
+
async def _recover(self, deadline):
|
|
210
|
+
channel = None
|
|
211
|
+
try:
|
|
212
|
+
async with asyncio.timeout_at(deadline):
|
|
213
|
+
certificate = self._ssl_cert
|
|
214
|
+
if isinstance(certificate, str):
|
|
215
|
+
from .async_work import run_blocking
|
|
216
|
+
certificate = await run_blocking(_read_certificate, certificate, deadline=deadline)
|
|
217
|
+
target = '{}:{}'.format(self._host, self._port)
|
|
218
|
+
if self._secure_channel:
|
|
219
|
+
channel = grpc.aio.secure_channel(target, grpc.ssl_channel_credentials(root_certificates=certificate), options=self._grpc_options)
|
|
220
|
+
else:
|
|
221
|
+
channel = grpc.aio.insecure_channel(target, options=self._grpc_options)
|
|
222
|
+
client = bindings.ClusterServiceStub(channel)
|
|
223
|
+
while True:
|
|
224
|
+
response = await self._request(client, 'status', deadline)
|
|
225
|
+
should_resume = self._mutation.observe(response.status)
|
|
226
|
+
if response.status == 'active':
|
|
227
|
+
return True
|
|
228
|
+
if should_resume:
|
|
229
|
+
try:
|
|
230
|
+
result = await self._request(client, 'resume', deadline)
|
|
231
|
+
self._mutation.observe(result.status)
|
|
232
|
+
# Only a status read resolves readiness, even if resume says active.
|
|
233
|
+
self._mutation.acknowledged()
|
|
234
|
+
except grpc.RpcError as error:
|
|
235
|
+
if error.code() not in (grpc.StatusCode.UNAVAILABLE, grpc.StatusCode.DEADLINE_EXCEEDED, grpc.StatusCode.CANCELLED):
|
|
236
|
+
raise
|
|
237
|
+
continue
|
|
238
|
+
await asyncio.sleep(min(1.0, _remaining(deadline)))
|
|
239
|
+
finally:
|
|
240
|
+
if channel is not None:
|
|
241
|
+
async with asyncio.timeout(self._cleanup_timeout):
|
|
242
|
+
await channel.close()
|
|
243
|
+
|
|
244
|
+
async def close(self, *, deadline=None):
|
|
245
|
+
if self._owner is not None:
|
|
246
|
+
owner = (os.getpid(), threading.get_ident(), asyncio.get_running_loop())
|
|
247
|
+
if self._owner != owner:
|
|
248
|
+
raise ProgrammingError('AsyncClusterManager cannot cross loops, threads or processes.')
|
|
249
|
+
if self._closed and (self._flight.task is None or self._flight.task.done()):
|
|
250
|
+
return
|
|
251
|
+
if not self._closed:
|
|
252
|
+
self._check_owner()
|
|
253
|
+
self._closed = True
|
|
254
|
+
bound = asyncio.get_running_loop().time() + self._cleanup_timeout
|
|
255
|
+
deadline = bound if deadline is None else min(bound, deadline)
|
|
256
|
+
task = self._flight.task
|
|
257
|
+
if task is not None:
|
|
258
|
+
task.cancel()
|
|
259
|
+
try:
|
|
260
|
+
async with asyncio.timeout_at(deadline):
|
|
261
|
+
await asyncio.shield(task)
|
|
262
|
+
except asyncio.CancelledError:
|
|
263
|
+
if asyncio.current_task().cancelling():
|
|
264
|
+
raise
|
|
265
|
+
except Exception:
|
|
266
|
+
if not task.done():
|
|
267
|
+
raise
|