e6data-python-connector 2.3.15__tar.gz → 3.0.1rc1__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.15/e6data_python_connector.egg-info → e6data_python_connector-3.0.1rc1}/PKG-INFO +185 -6
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/README.md +176 -4
- e6data_python_connector-3.0.1rc1/e6data_python_connector/aio.py +26 -0
- e6data_python_connector-3.0.1rc1/e6data_python_connector/async_cluster_manager.py +267 -0
- e6data_python_connector-3.0.1rc1/e6data_python_connector/async_connection.py +551 -0
- e6data_python_connector-3.0.1rc1/e6data_python_connector/async_connection_pool.py +425 -0
- e6data_python_connector-3.0.1rc1/e6data_python_connector/async_cursor.py +718 -0
- e6data_python_connector-3.0.1rc1/e6data_python_connector/async_dialect.py +285 -0
- e6data_python_connector-3.0.1rc1/e6data_python_connector/async_oauth.py +248 -0
- e6data_python_connector-3.0.1rc1/e6data_python_connector/async_work.py +105 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/cluster_manager.py +106 -1
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/common.py +2 -1
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/datainputstream.py +43 -9
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/date_time_utils.py +4 -8
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/dialect.py +7 -2
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/e6data_grpc.py +788 -143
- e6data_python_connector-3.0.1rc1/e6data_python_connector/exceptions.py +43 -0
- e6data_python_connector-3.0.1rc1/e6data_python_connector/oauth.py +185 -0
- e6data_python_connector-3.0.1rc1/e6data_python_connector/oauth_cache.py +238 -0
- e6data_python_connector-3.0.1rc1/e6data_python_connector/oauth_common.py +66 -0
- e6data_python_connector-3.0.1rc1/e6data_python_connector/result_batch.py +49 -0
- e6data_python_connector-3.0.1rc1/e6data_python_connector/server/e6x_engine_pb2.py +209 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +43 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1/e6data_python_connector.egg-info}/PKG-INFO +185 -6
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector.egg-info/SOURCES.txt +38 -5
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector.egg-info/entry_points.txt +1 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector.egg-info/requires.txt +7 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector.egg-info/top_level.txt +0 -1
- e6data_python_connector-3.0.1rc1/pyproject.toml +12 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/setup.py +7 -2
- e6data_python_connector-3.0.1rc1/test/conftest.py +18 -0
- e6data_python_connector-3.0.1rc1/test/integration/__init__.py +1 -0
- e6data_python_connector-3.0.1rc1/test/integration/benchmark_preflight.py +30 -0
- e6data_python_connector-3.0.1rc1/test/integration/benchmark_result_batch_v2.py +149 -0
- e6data_python_connector-3.0.1rc1/test/integration/benchmark_sync_async.py +177 -0
- e6data_python_connector-3.0.1rc1/test/integration/expiry_preflight.py +37 -0
- e6data_python_connector-3.0.1rc1/test/integration/result_batch_benchmark_support.py +196 -0
- e6data_python_connector-3.0.1rc1/test/integration/support.py +176 -0
- e6data_python_connector-3.0.1rc1/test/integration/test_async_connection.py +24 -0
- e6data_python_connector-3.0.1rc1/test/integration/test_async_pool.py +51 -0
- e6data_python_connector-3.0.1rc1/test/integration/test_async_queries.py +62 -0
- e6data_python_connector-3.0.1rc1/test/integration/test_async_resume.py +26 -0
- e6data_python_connector-3.0.1rc1/test/integration/test_async_sqlalchemy.py +45 -0
- e6data_python_connector-3.0.1rc1/test/integration/test_async_token_lifecycle.py +35 -0
- e6data_python_connector-3.0.1rc1/test/integration/test_e2e_auth_boundary.py +34 -0
- e6data_python_connector-3.0.1rc1/test/integration/test_e2e_oauth_continuation.py +57 -0
- e6data_python_connector-3.0.1rc1/test/integration/test_e2e_readonly_matrix.py +173 -0
- e6data_python_connector-3.0.1rc1/test/integration/test_result_batch_v2.py +120 -0
- e6data_python_connector-3.0.1rc1/test/integration/test_shared_token_issuance.py +97 -0
- e6data_python_connector-3.0.1rc1/test/integration/test_sync_oauth_lifecycle.py +20 -0
- e6data_python_connector-3.0.1rc1/test/integration/token_observation.py +55 -0
- e6data_python_connector-3.0.1rc1/test/test_cluster_manager_efficiency.py +105 -0
- e6data_python_connector-3.0.1rc1/test/test_cluster_manager_oauth.py +193 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_cluster_manager_strategy.py +1 -1
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_connection_pool.py +33 -20
- e6data_python_connector-3.0.1rc1/test/test_oauth_authentication.py +592 -0
- e6data_python_connector-3.0.1rc1/test/test_oauth_auto_resume.py +409 -0
- e6data_python_connector-3.0.1rc1/test/test_oauth_resume_integration.py +244 -0
- e6data_python_connector-2.3.15/e6data_python_connector/exceptions.py +0 -15
- e6data_python_connector-2.3.15/e6data_python_connector/server/e6x_engine_pb2.py +0 -207
- e6data_python_connector-2.3.15/gen-py/__init__.py +0 -0
- e6data_python_connector-2.3.15/gen-py/e6x_vector/__init__.py +0 -1
- e6data_python_connector-2.3.15/gen-py/e6x_vector/constants.py +0 -15
- e6data_python_connector-2.3.15/gen-py/e6x_vector/ttypes.py +0 -1875
- e6data_python_connector-2.3.15/pyproject.toml +0 -5
- e6data_python_connector-2.3.15/test/test_cluster_manager_efficiency.py +0 -198
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/LICENSE +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/MANIFEST.in +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/__init__.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/cluster_server/__init__.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/connection_pool.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/constants.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/e6x_vector/__init__.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/e6x_vector/constants.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/server/QueryEngineService.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/server/__init__.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/server/constants.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/server/ttypes.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/strategy.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/typeId.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/setup.cfg +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/__init__.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/analyze_38_nines.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/analyze_all_cases.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/analyze_binary.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/analyze_correct_value.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/analyze_fields.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/check_decimal_errors.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/cleanup_test_files.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/debug_38_nines.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/debug_binary.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/final_test.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/move_tests.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/quick_test.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_38_nines.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_all_decimal128_cases.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_cluster_manager_none_strategy.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_code_optimizations.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_comprehensive.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_connection_pool_e2e.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_current_implementation.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_decimal128_binary_parsing.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_decimal128_parsing.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_dialect_columns.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_fix.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_improved_parsing.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_known_case.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_manual_analysis.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_mock_server.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_multiprocessing_fix.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_new_implementation.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_pool_concurrency_simple.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_pool_threading_multiprocessing.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_simple_connection_pool.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_specific_binary.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_strategy.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_strategy_logic.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_strategy_persistence_fix.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_strategy_sharing_fix.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_user_binary_value.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/tests.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/tests_grpc.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/validate_decimal128.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/validate_implementation.py +0 -0
- {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/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.1rc1
|
|
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
|
|
|
@@ -372,6 +469,88 @@ database = '<new_database_name>' # Replace with the new database.
|
|
|
372
469
|
cursor = conn.cursor(database, catalog_name)
|
|
373
470
|
```
|
|
374
471
|
|
|
472
|
+
### Opt in to multi-chunk result batches
|
|
473
|
+
|
|
474
|
+
`enable_result_batch_v2=True` lets a compatible planner return several result
|
|
475
|
+
chunks in one `getNextResultBatchV2` response. The default is `False`, which keeps
|
|
476
|
+
the existing V1 protocol. Increasing `arraysize` or `fetchmany(size)` changes how
|
|
477
|
+
many rows your application receives per call; it does not change the planner's
|
|
478
|
+
response size.
|
|
479
|
+
|
|
480
|
+
The planner must support V2 and have `ENABLE_GET_NEXT_RESULT_BATCH_V2` enabled.
|
|
481
|
+
`ENABLE_GET_NEXT_CHUNK_V2` controls the separate executor-to-planner boundary.
|
|
482
|
+
Record both flags when qualifying a deployment. V2 can reduce network round
|
|
483
|
+
trips, but it does not reduce the number of result bytes or extend the planner's
|
|
484
|
+
query lifetime. Completion of a 20-million-row result within 900 seconds must be
|
|
485
|
+
measured against the actual workload and deployment.
|
|
486
|
+
|
|
487
|
+
The following examples use your existing `connection_options`, `sql`, and
|
|
488
|
+
`consume` function. Choose a positive `result_receive_limit_bytes` from measured
|
|
489
|
+
V2 response sizes and the client's memory budget. The sync API preserves its
|
|
490
|
+
existing gRPC options, so supply a finite receive limit for a V2 rollout:
|
|
491
|
+
|
|
492
|
+
```python
|
|
493
|
+
from e6data_python_connector import Connection
|
|
494
|
+
|
|
495
|
+
sync_options = {
|
|
496
|
+
**connection_options,
|
|
497
|
+
"enable_result_batch_v2": True,
|
|
498
|
+
"grpc_options": {
|
|
499
|
+
**connection_options.get("grpc_options", {}),
|
|
500
|
+
"max_receive_message_length": result_receive_limit_bytes,
|
|
501
|
+
},
|
|
502
|
+
}
|
|
503
|
+
with Connection(**sync_options) as connection:
|
|
504
|
+
with connection.cursor() as cursor:
|
|
505
|
+
cursor.execute(sql)
|
|
506
|
+
for rows in cursor.fetchall_buffer():
|
|
507
|
+
consume(rows)
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
The async API keeps its finite 64 MiB default. Set
|
|
511
|
+
`max_receive_message_bytes` to change that limit. If `grpc_options` also contains
|
|
512
|
+
a receive limit, it must agree with this value.
|
|
513
|
+
|
|
514
|
+
```python
|
|
515
|
+
from e6data_python_connector.aio import AsyncConnection
|
|
516
|
+
|
|
517
|
+
async_options = {
|
|
518
|
+
**connection_options,
|
|
519
|
+
"enable_result_batch_v2": True,
|
|
520
|
+
"max_receive_message_bytes": result_receive_limit_bytes,
|
|
521
|
+
}
|
|
522
|
+
async with AsyncConnection(**async_options) as connection:
|
|
523
|
+
async with connection.cursor() as cursor:
|
|
524
|
+
await cursor.execute(sql)
|
|
525
|
+
async for rows in cursor.fetchall_buffer():
|
|
526
|
+
consume(rows)
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
SQLAlchemy accepts the same opt-in through `connect_args`:
|
|
530
|
+
|
|
531
|
+
```python
|
|
532
|
+
from sqlalchemy import create_engine
|
|
533
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
534
|
+
|
|
535
|
+
engine = create_engine("e6data://", connect_args=sync_options)
|
|
536
|
+
async_engine = create_async_engine("e6data+asyncio://", connect_args=async_options)
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
Fetch return shapes and row order stay the same. `fetchall_buffer()` yields
|
|
540
|
+
decoded chunks; a V2 response can contain several chunks. All chunks in an
|
|
541
|
+
envelope are decoded before any are exposed, so V2 may use more memory and delay
|
|
542
|
+
the first chunk compared with V1. `fetchall()` still retains the full result.
|
|
543
|
+
There is no prefetch or parallel fetching for a query.
|
|
544
|
+
|
|
545
|
+
An `UNIMPLEMENTED` response switches that query to V1. A later query may try V2
|
|
546
|
+
again. Other fetch failures do not trigger a protocol fallback or query replay.
|
|
547
|
+
To disable V2 for new connections, omit the option or set it to `False`.
|
|
548
|
+
|
|
549
|
+
The [result batch qualification instructions](test/README.md#result-batch-v2-qualification)
|
|
550
|
+
cover protocol parity and the explicit large-result benchmark. Real-engine
|
|
551
|
+
qualification and the 900-second acceptance measurement have not been run for
|
|
552
|
+
this change.
|
|
553
|
+
|
|
375
554
|
### Get Query Time Metrics
|
|
376
555
|
```python
|
|
377
556
|
import json
|
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
# e6data Python Connector
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Package version and extras are defined in `setup.py`.
|
|
4
4
|
|
|
5
5
|
## Introduction
|
|
6
6
|
|
|
7
|
-
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.
|
|
8
28
|
|
|
9
29
|
### Dependencies
|
|
10
30
|
Make sure to install below dependencies and wheel before install e6data-python-connector.
|
|
@@ -81,8 +101,8 @@ The `Connection` class supports the following parameters:
|
|
|
81
101
|
|-----------|------|----------|---------|-------------|
|
|
82
102
|
| `host` | str | Yes | - | IP address or hostname of the e6data cluster |
|
|
83
103
|
| `port` | int | Yes | - | Port of the e6data engine (typically 80) |
|
|
84
|
-
| `username` | str |
|
|
85
|
-
| `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 |
|
|
86
106
|
| `database` | str | No | None | Database to perform queries on |
|
|
87
107
|
| `catalog` | str | No | None | Catalog name |
|
|
88
108
|
| `cluster_name` | str | No | None | Name of the cluster for cluster-specific operations |
|
|
@@ -92,6 +112,76 @@ The `Connection` class supports the following parameters:
|
|
|
92
112
|
| `grpc_options` | dict | No | None | Additional gRPC configuration options |
|
|
93
113
|
| `debug` | bool | No | False | Enable debug logging for troubleshooting |
|
|
94
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.
|
|
95
185
|
|
|
96
186
|
#### Secure Connection Example
|
|
97
187
|
|
|
@@ -333,6 +423,88 @@ database = '<new_database_name>' # Replace with the new database.
|
|
|
333
423
|
cursor = conn.cursor(database, catalog_name)
|
|
334
424
|
```
|
|
335
425
|
|
|
426
|
+
### Opt in to multi-chunk result batches
|
|
427
|
+
|
|
428
|
+
`enable_result_batch_v2=True` lets a compatible planner return several result
|
|
429
|
+
chunks in one `getNextResultBatchV2` response. The default is `False`, which keeps
|
|
430
|
+
the existing V1 protocol. Increasing `arraysize` or `fetchmany(size)` changes how
|
|
431
|
+
many rows your application receives per call; it does not change the planner's
|
|
432
|
+
response size.
|
|
433
|
+
|
|
434
|
+
The planner must support V2 and have `ENABLE_GET_NEXT_RESULT_BATCH_V2` enabled.
|
|
435
|
+
`ENABLE_GET_NEXT_CHUNK_V2` controls the separate executor-to-planner boundary.
|
|
436
|
+
Record both flags when qualifying a deployment. V2 can reduce network round
|
|
437
|
+
trips, but it does not reduce the number of result bytes or extend the planner's
|
|
438
|
+
query lifetime. Completion of a 20-million-row result within 900 seconds must be
|
|
439
|
+
measured against the actual workload and deployment.
|
|
440
|
+
|
|
441
|
+
The following examples use your existing `connection_options`, `sql`, and
|
|
442
|
+
`consume` function. Choose a positive `result_receive_limit_bytes` from measured
|
|
443
|
+
V2 response sizes and the client's memory budget. The sync API preserves its
|
|
444
|
+
existing gRPC options, so supply a finite receive limit for a V2 rollout:
|
|
445
|
+
|
|
446
|
+
```python
|
|
447
|
+
from e6data_python_connector import Connection
|
|
448
|
+
|
|
449
|
+
sync_options = {
|
|
450
|
+
**connection_options,
|
|
451
|
+
"enable_result_batch_v2": True,
|
|
452
|
+
"grpc_options": {
|
|
453
|
+
**connection_options.get("grpc_options", {}),
|
|
454
|
+
"max_receive_message_length": result_receive_limit_bytes,
|
|
455
|
+
},
|
|
456
|
+
}
|
|
457
|
+
with Connection(**sync_options) as connection:
|
|
458
|
+
with connection.cursor() as cursor:
|
|
459
|
+
cursor.execute(sql)
|
|
460
|
+
for rows in cursor.fetchall_buffer():
|
|
461
|
+
consume(rows)
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
The async API keeps its finite 64 MiB default. Set
|
|
465
|
+
`max_receive_message_bytes` to change that limit. If `grpc_options` also contains
|
|
466
|
+
a receive limit, it must agree with this value.
|
|
467
|
+
|
|
468
|
+
```python
|
|
469
|
+
from e6data_python_connector.aio import AsyncConnection
|
|
470
|
+
|
|
471
|
+
async_options = {
|
|
472
|
+
**connection_options,
|
|
473
|
+
"enable_result_batch_v2": True,
|
|
474
|
+
"max_receive_message_bytes": result_receive_limit_bytes,
|
|
475
|
+
}
|
|
476
|
+
async with AsyncConnection(**async_options) as connection:
|
|
477
|
+
async with connection.cursor() as cursor:
|
|
478
|
+
await cursor.execute(sql)
|
|
479
|
+
async for rows in cursor.fetchall_buffer():
|
|
480
|
+
consume(rows)
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
SQLAlchemy accepts the same opt-in through `connect_args`:
|
|
484
|
+
|
|
485
|
+
```python
|
|
486
|
+
from sqlalchemy import create_engine
|
|
487
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
488
|
+
|
|
489
|
+
engine = create_engine("e6data://", connect_args=sync_options)
|
|
490
|
+
async_engine = create_async_engine("e6data+asyncio://", connect_args=async_options)
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Fetch return shapes and row order stay the same. `fetchall_buffer()` yields
|
|
494
|
+
decoded chunks; a V2 response can contain several chunks. All chunks in an
|
|
495
|
+
envelope are decoded before any are exposed, so V2 may use more memory and delay
|
|
496
|
+
the first chunk compared with V1. `fetchall()` still retains the full result.
|
|
497
|
+
There is no prefetch or parallel fetching for a query.
|
|
498
|
+
|
|
499
|
+
An `UNIMPLEMENTED` response switches that query to V1. A later query may try V2
|
|
500
|
+
again. Other fetch failures do not trigger a protocol fallback or query replay.
|
|
501
|
+
To disable V2 for new connections, omit the option or set it to `False`.
|
|
502
|
+
|
|
503
|
+
The [result batch qualification instructions](test/README.md#result-batch-v2-qualification)
|
|
504
|
+
cover protocol parity and the explicit large-result benchmark. Real-engine
|
|
505
|
+
qualification and the 900-second acceptance measurement have not been run for
|
|
506
|
+
this change.
|
|
507
|
+
|
|
336
508
|
### Get Query Time Metrics
|
|
337
509
|
```python
|
|
338
510
|
import json
|
|
@@ -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)
|