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.
Files changed (130) hide show
  1. {e6data_python_connector-2.3.15/e6data_python_connector.egg-info → e6data_python_connector-3.0.1rc1}/PKG-INFO +185 -6
  2. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/README.md +176 -4
  3. e6data_python_connector-3.0.1rc1/e6data_python_connector/aio.py +26 -0
  4. e6data_python_connector-3.0.1rc1/e6data_python_connector/async_cluster_manager.py +267 -0
  5. e6data_python_connector-3.0.1rc1/e6data_python_connector/async_connection.py +551 -0
  6. e6data_python_connector-3.0.1rc1/e6data_python_connector/async_connection_pool.py +425 -0
  7. e6data_python_connector-3.0.1rc1/e6data_python_connector/async_cursor.py +718 -0
  8. e6data_python_connector-3.0.1rc1/e6data_python_connector/async_dialect.py +285 -0
  9. e6data_python_connector-3.0.1rc1/e6data_python_connector/async_oauth.py +248 -0
  10. e6data_python_connector-3.0.1rc1/e6data_python_connector/async_work.py +105 -0
  11. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/cluster_manager.py +106 -1
  12. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/common.py +2 -1
  13. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/datainputstream.py +43 -9
  14. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/date_time_utils.py +4 -8
  15. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/dialect.py +7 -2
  16. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/e6data_grpc.py +788 -143
  17. e6data_python_connector-3.0.1rc1/e6data_python_connector/exceptions.py +43 -0
  18. e6data_python_connector-3.0.1rc1/e6data_python_connector/oauth.py +185 -0
  19. e6data_python_connector-3.0.1rc1/e6data_python_connector/oauth_cache.py +238 -0
  20. e6data_python_connector-3.0.1rc1/e6data_python_connector/oauth_common.py +66 -0
  21. e6data_python_connector-3.0.1rc1/e6data_python_connector/result_batch.py +49 -0
  22. e6data_python_connector-3.0.1rc1/e6data_python_connector/server/e6x_engine_pb2.py +209 -0
  23. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +43 -0
  24. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1/e6data_python_connector.egg-info}/PKG-INFO +185 -6
  25. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector.egg-info/SOURCES.txt +38 -5
  26. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector.egg-info/entry_points.txt +1 -0
  27. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector.egg-info/requires.txt +7 -0
  28. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector.egg-info/top_level.txt +0 -1
  29. e6data_python_connector-3.0.1rc1/pyproject.toml +12 -0
  30. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/setup.py +7 -2
  31. e6data_python_connector-3.0.1rc1/test/conftest.py +18 -0
  32. e6data_python_connector-3.0.1rc1/test/integration/__init__.py +1 -0
  33. e6data_python_connector-3.0.1rc1/test/integration/benchmark_preflight.py +30 -0
  34. e6data_python_connector-3.0.1rc1/test/integration/benchmark_result_batch_v2.py +149 -0
  35. e6data_python_connector-3.0.1rc1/test/integration/benchmark_sync_async.py +177 -0
  36. e6data_python_connector-3.0.1rc1/test/integration/expiry_preflight.py +37 -0
  37. e6data_python_connector-3.0.1rc1/test/integration/result_batch_benchmark_support.py +196 -0
  38. e6data_python_connector-3.0.1rc1/test/integration/support.py +176 -0
  39. e6data_python_connector-3.0.1rc1/test/integration/test_async_connection.py +24 -0
  40. e6data_python_connector-3.0.1rc1/test/integration/test_async_pool.py +51 -0
  41. e6data_python_connector-3.0.1rc1/test/integration/test_async_queries.py +62 -0
  42. e6data_python_connector-3.0.1rc1/test/integration/test_async_resume.py +26 -0
  43. e6data_python_connector-3.0.1rc1/test/integration/test_async_sqlalchemy.py +45 -0
  44. e6data_python_connector-3.0.1rc1/test/integration/test_async_token_lifecycle.py +35 -0
  45. e6data_python_connector-3.0.1rc1/test/integration/test_e2e_auth_boundary.py +34 -0
  46. e6data_python_connector-3.0.1rc1/test/integration/test_e2e_oauth_continuation.py +57 -0
  47. e6data_python_connector-3.0.1rc1/test/integration/test_e2e_readonly_matrix.py +173 -0
  48. e6data_python_connector-3.0.1rc1/test/integration/test_result_batch_v2.py +120 -0
  49. e6data_python_connector-3.0.1rc1/test/integration/test_shared_token_issuance.py +97 -0
  50. e6data_python_connector-3.0.1rc1/test/integration/test_sync_oauth_lifecycle.py +20 -0
  51. e6data_python_connector-3.0.1rc1/test/integration/token_observation.py +55 -0
  52. e6data_python_connector-3.0.1rc1/test/test_cluster_manager_efficiency.py +105 -0
  53. e6data_python_connector-3.0.1rc1/test/test_cluster_manager_oauth.py +193 -0
  54. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_cluster_manager_strategy.py +1 -1
  55. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_connection_pool.py +33 -20
  56. e6data_python_connector-3.0.1rc1/test/test_oauth_authentication.py +592 -0
  57. e6data_python_connector-3.0.1rc1/test/test_oauth_auto_resume.py +409 -0
  58. e6data_python_connector-3.0.1rc1/test/test_oauth_resume_integration.py +244 -0
  59. e6data_python_connector-2.3.15/e6data_python_connector/exceptions.py +0 -15
  60. e6data_python_connector-2.3.15/e6data_python_connector/server/e6x_engine_pb2.py +0 -207
  61. e6data_python_connector-2.3.15/gen-py/__init__.py +0 -0
  62. e6data_python_connector-2.3.15/gen-py/e6x_vector/__init__.py +0 -1
  63. e6data_python_connector-2.3.15/gen-py/e6x_vector/constants.py +0 -15
  64. e6data_python_connector-2.3.15/gen-py/e6x_vector/ttypes.py +0 -1875
  65. e6data_python_connector-2.3.15/pyproject.toml +0 -5
  66. e6data_python_connector-2.3.15/test/test_cluster_manager_efficiency.py +0 -198
  67. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/LICENSE +0 -0
  68. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/MANIFEST.in +0 -0
  69. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/__init__.py +0 -0
  70. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/cluster_server/__init__.py +0 -0
  71. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
  72. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
  73. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/connection_pool.py +0 -0
  74. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/constants.py +0 -0
  75. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/e6x_vector/__init__.py +0 -0
  76. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/e6x_vector/constants.py +0 -0
  77. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
  78. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/server/QueryEngineService.py +0 -0
  79. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/server/__init__.py +0 -0
  80. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/server/constants.py +0 -0
  81. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
  82. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/server/ttypes.py +0 -0
  83. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/strategy.py +0 -0
  84. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector/typeId.py +0 -0
  85. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
  86. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/setup.cfg +0 -0
  87. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/__init__.py +0 -0
  88. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/analyze_38_nines.py +0 -0
  89. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/analyze_all_cases.py +0 -0
  90. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/analyze_binary.py +0 -0
  91. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/analyze_correct_value.py +0 -0
  92. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/analyze_fields.py +0 -0
  93. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/check_decimal_errors.py +0 -0
  94. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/cleanup_test_files.py +0 -0
  95. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/debug_38_nines.py +0 -0
  96. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/debug_binary.py +0 -0
  97. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/final_test.py +0 -0
  98. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/move_tests.py +0 -0
  99. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/quick_test.py +0 -0
  100. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_38_nines.py +0 -0
  101. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_all_decimal128_cases.py +0 -0
  102. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_cluster_manager_none_strategy.py +0 -0
  103. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_code_optimizations.py +0 -0
  104. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_comprehensive.py +0 -0
  105. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_connection_pool_e2e.py +0 -0
  106. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_current_implementation.py +0 -0
  107. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_decimal128_binary_parsing.py +0 -0
  108. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_decimal128_parsing.py +0 -0
  109. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_dialect_columns.py +0 -0
  110. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_fix.py +0 -0
  111. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_improved_parsing.py +0 -0
  112. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_known_case.py +0 -0
  113. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_manual_analysis.py +0 -0
  114. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_mock_server.py +0 -0
  115. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_multiprocessing_fix.py +0 -0
  116. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_new_implementation.py +0 -0
  117. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_pool_concurrency_simple.py +0 -0
  118. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_pool_threading_multiprocessing.py +0 -0
  119. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_simple_connection_pool.py +0 -0
  120. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_specific_binary.py +0 -0
  121. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_strategy.py +0 -0
  122. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_strategy_logic.py +0 -0
  123. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_strategy_persistence_fix.py +0 -0
  124. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_strategy_sharing_fix.py +0 -0
  125. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/test_user_binary_value.py +0 -0
  126. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/tests.py +0 -0
  127. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/tests_grpc.py +0 -0
  128. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/validate_decimal128.py +0 -0
  129. {e6data_python_connector-2.3.15 → e6data_python_connector-3.0.1rc1}/test/validate_implementation.py +0 -0
  130. {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.2
1
+ Metadata-Version: 2.4
2
2
  Name: e6data-python-connector
3
- Version: 2.3.15
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
- ![version](https://img.shields.io/badge/version-2.3.15-blue.svg)
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 automatic support for blue-green deployments, ensuring seamless failover during server updates without query interruption.
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 | Yes | - | Your e6data Email ID |
124
- | `password` | str | Yes | - | Access Token generated in the e6data console |
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
- ![version](https://img.shields.io/badge/version-2.3.15-blue.svg)
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 automatic support for blue-green deployments, ensuring seamless failover during server updates without query interruption.
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 | Yes | - | Your e6data Email ID |
85
- | `password` | str | Yes | - | Access Token generated in the e6data console |
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)