e6data-python-connector 2.3.13rc1__tar.gz → 2.3.14__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.13rc1/e6data_python_connector.egg-info → e6data_python_connector-2.3.14}/PKG-INFO +44 -2
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/README.md +43 -1
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/datainputstream.py +13 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/date_time_utils.py +86 -10
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/dialect.py +16 -4
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14/e6data_python_connector.egg-info}/PKG-INFO +44 -2
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector.egg-info/SOURCES.txt +1 -3
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/setup.py +1 -1
- e6data_python_connector-2.3.14/test/test_dialect_columns.py +86 -0
- e6data_python_connector-2.3.13rc1/test/benchmark_fastbinary_performance.py +0 -303
- e6data_python_connector-2.3.13rc1/test/test_optimizations_integration.py +0 -443
- e6data_python_connector-2.3.13rc1/test/test_ssl_cert.py +0 -548
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/LICENSE +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/MANIFEST.in +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/__init__.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/cluster_manager.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/cluster_server/__init__.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/common.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/connection_pool.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/constants.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/e6data_grpc.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/e6x_vector/__init__.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/e6x_vector/constants.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/exceptions.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/server/QueryEngineService.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/server/__init__.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/server/constants.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/server/e6x_engine_pb2.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/server/ttypes.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/strategy.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/typeId.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector.egg-info/entry_points.txt +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector.egg-info/requires.txt +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector.egg-info/top_level.txt +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/gen-py/__init__.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/gen-py/e6x_vector/__init__.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/gen-py/e6x_vector/constants.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/gen-py/e6x_vector/ttypes.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/pyproject.toml +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/setup.cfg +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/__init__.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/analyze_38_nines.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/analyze_all_cases.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/analyze_binary.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/analyze_correct_value.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/analyze_fields.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/check_decimal_errors.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/cleanup_test_files.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/debug_38_nines.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/debug_binary.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/final_test.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/move_tests.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/quick_test.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_38_nines.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_all_decimal128_cases.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_cluster_manager_efficiency.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_cluster_manager_none_strategy.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_cluster_manager_strategy.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_code_optimizations.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_comprehensive.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_connection_pool.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_connection_pool_e2e.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_current_implementation.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_decimal128_binary_parsing.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_decimal128_parsing.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_fix.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_improved_parsing.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_known_case.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_manual_analysis.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_mock_server.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_multiprocessing_fix.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_new_implementation.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_pool_concurrency_simple.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_pool_threading_multiprocessing.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_simple_connection_pool.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_specific_binary.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_strategy.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_strategy_logic.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_strategy_persistence_fix.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_strategy_sharing_fix.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_user_binary_value.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/tests.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/tests_grpc.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/validate_decimal128.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/validate_implementation.py +0 -0
- {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/verify_decimal_fix.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: e6data-python-connector
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.14
|
|
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.
|
|
@@ -39,7 +39,7 @@ Dynamic: summary
|
|
|
39
39
|
|
|
40
40
|
# e6data Python Connector
|
|
41
41
|
|
|
42
|
-

|
|
43
43
|
|
|
44
44
|
## Introduction
|
|
45
45
|
|
|
@@ -324,6 +324,48 @@ query_id = '<query_id>' # query id from execute function response.
|
|
|
324
324
|
cursor.cancel(query_id)
|
|
325
325
|
```
|
|
326
326
|
|
|
327
|
+
To get the status of a running or completed query:
|
|
328
|
+
```python
|
|
329
|
+
query_id = '<query_id>' # query id from execute function response.
|
|
330
|
+
status_response = cursor.status(query_id)
|
|
331
|
+
|
|
332
|
+
# Check if query execution is complete
|
|
333
|
+
is_complete = status_response.status # Returns True when query is done, False if still running
|
|
334
|
+
|
|
335
|
+
# Get the total row count
|
|
336
|
+
row_count = status_response.rowCount # Total number of rows in the result set
|
|
337
|
+
|
|
338
|
+
print(f"Query complete: {is_complete}")
|
|
339
|
+
print(f"Row count: {row_count}")
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
The `status()` method is useful for:
|
|
343
|
+
- **Monitoring long-running queries**: Poll the status periodically to check if execution is complete
|
|
344
|
+
- **Checking row counts**: Get the total number of rows without fetching all results
|
|
345
|
+
- **Query progress tracking**: Integrate with monitoring systems or progress bars
|
|
346
|
+
- **Conditional fetching**: Decide whether to fetch results based on completion status
|
|
347
|
+
|
|
348
|
+
Example - Polling for query completion:
|
|
349
|
+
```python
|
|
350
|
+
import time
|
|
351
|
+
|
|
352
|
+
cursor = conn.cursor(catalog_name=catalog_name)
|
|
353
|
+
query_id = cursor.execute("SELECT * FROM large_table")
|
|
354
|
+
|
|
355
|
+
# Poll until query is complete
|
|
356
|
+
while True:
|
|
357
|
+
status_response = cursor.status(query_id)
|
|
358
|
+
if status_response.status:
|
|
359
|
+
print(f"Query complete! Total rows: {status_response.rowCount}")
|
|
360
|
+
break
|
|
361
|
+
else:
|
|
362
|
+
print("Query still running...")
|
|
363
|
+
time.sleep(1) # Wait 1 second before checking again
|
|
364
|
+
|
|
365
|
+
# Now fetch the results
|
|
366
|
+
results = cursor.fetchall()
|
|
367
|
+
```
|
|
368
|
+
|
|
327
369
|
Switch database in an existing connection:
|
|
328
370
|
```python
|
|
329
371
|
database = '<new_database_name>' # Replace with the new database.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# e6data Python Connector
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
|
|
5
5
|
## Introduction
|
|
6
6
|
|
|
@@ -285,6 +285,48 @@ query_id = '<query_id>' # query id from execute function response.
|
|
|
285
285
|
cursor.cancel(query_id)
|
|
286
286
|
```
|
|
287
287
|
|
|
288
|
+
To get the status of a running or completed query:
|
|
289
|
+
```python
|
|
290
|
+
query_id = '<query_id>' # query id from execute function response.
|
|
291
|
+
status_response = cursor.status(query_id)
|
|
292
|
+
|
|
293
|
+
# Check if query execution is complete
|
|
294
|
+
is_complete = status_response.status # Returns True when query is done, False if still running
|
|
295
|
+
|
|
296
|
+
# Get the total row count
|
|
297
|
+
row_count = status_response.rowCount # Total number of rows in the result set
|
|
298
|
+
|
|
299
|
+
print(f"Query complete: {is_complete}")
|
|
300
|
+
print(f"Row count: {row_count}")
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
The `status()` method is useful for:
|
|
304
|
+
- **Monitoring long-running queries**: Poll the status periodically to check if execution is complete
|
|
305
|
+
- **Checking row counts**: Get the total number of rows without fetching all results
|
|
306
|
+
- **Query progress tracking**: Integrate with monitoring systems or progress bars
|
|
307
|
+
- **Conditional fetching**: Decide whether to fetch results based on completion status
|
|
308
|
+
|
|
309
|
+
Example - Polling for query completion:
|
|
310
|
+
```python
|
|
311
|
+
import time
|
|
312
|
+
|
|
313
|
+
cursor = conn.cursor(catalog_name=catalog_name)
|
|
314
|
+
query_id = cursor.execute("SELECT * FROM large_table")
|
|
315
|
+
|
|
316
|
+
# Poll until query is complete
|
|
317
|
+
while True:
|
|
318
|
+
status_response = cursor.status(query_id)
|
|
319
|
+
if status_response.status:
|
|
320
|
+
print(f"Query complete! Total rows: {status_response.rowCount}")
|
|
321
|
+
break
|
|
322
|
+
else:
|
|
323
|
+
print("Query still running...")
|
|
324
|
+
time.sleep(1) # Wait 1 second before checking again
|
|
325
|
+
|
|
326
|
+
# Now fetch the results
|
|
327
|
+
results = cursor.fetchall()
|
|
328
|
+
```
|
|
329
|
+
|
|
288
330
|
Switch database in an existing connection:
|
|
289
331
|
```python
|
|
290
332
|
database = '<new_database_name>' # Replace with the new database.
|
|
@@ -561,6 +561,10 @@ def get_column_from_chunk(vector: Vector) -> list:
|
|
|
561
561
|
value_array.append(vector.data.int64Data.data[
|
|
562
562
|
row] if not vector.isConstantVector else vector.data.numericConstantData.data)
|
|
563
563
|
elif d_type == VectorType.DATE:
|
|
564
|
+
# Use the JDBC-parity formatter so years > 9999 emit "+YYYYY-MM-DD"
|
|
565
|
+
# instead of raising. Per-row try/except keeps a single bad value
|
|
566
|
+
# from truncating the column (which previously cascaded to an
|
|
567
|
+
# IndexError in read_rows_from_chunk).
|
|
564
568
|
for row in range(vector.size):
|
|
565
569
|
if get_null(vector, row):
|
|
566
570
|
value_array.append(None)
|
|
@@ -574,6 +578,9 @@ def get_column_from_chunk(vector: Vector) -> list:
|
|
|
574
578
|
_logger.error("Failed to parse DATE row=%s: %s", row, e)
|
|
575
579
|
value_array.append('Failed to parse.')
|
|
576
580
|
elif d_type == VectorType.DATETIME:
|
|
581
|
+
# Default formatter knobs reproduce the prior datetime.isoformat
|
|
582
|
+
# output ("YYYY-MM-DDTHH:MM:SS.sss+HH:MM"). See DATE branch above
|
|
583
|
+
# for the rationale on per-row try/except.
|
|
577
584
|
for row in range(vector.size):
|
|
578
585
|
if get_null(vector, row):
|
|
579
586
|
value_array.append(None)
|
|
@@ -632,6 +639,8 @@ def get_column_from_chunk(vector: Vector) -> list:
|
|
|
632
639
|
for row in range(vector.size):
|
|
633
640
|
value_array.append(None)
|
|
634
641
|
elif d_type == VectorType.TIMESTAMP_TZ:
|
|
642
|
+
# row_zone is scoped to the row (not the outer `zone`) so a
|
|
643
|
+
# per-row zone resolution doesn't leak into later iterations.
|
|
635
644
|
for row in range(vector.size):
|
|
636
645
|
if get_null(vector, row):
|
|
637
646
|
value_array.append(None)
|
|
@@ -687,6 +696,10 @@ def get_column_from_chunk(vector: Vector) -> list:
|
|
|
687
696
|
else:
|
|
688
697
|
value_array.append(None)
|
|
689
698
|
except Exception as e:
|
|
699
|
+
# Safety net: if anything escapes the per-row try/excepts above (or
|
|
700
|
+
# comes from a branch without one), pad value_array to vector.size so
|
|
701
|
+
# read_rows_from_chunk's columns[colIndex][rowIndex] access can never
|
|
702
|
+
# IndexError on a short column.
|
|
690
703
|
_logger.error("get_column_from_chunk failed (vectorType=%s, parsed=%s/%s): %s",
|
|
691
704
|
d_type, len(value_array), vector.size, e)
|
|
692
705
|
while len(value_array) < vector.size:
|
|
@@ -165,10 +165,20 @@ _SECONDS_PER_DAY = 86400
|
|
|
165
165
|
|
|
166
166
|
def _civil_from_days(z):
|
|
167
167
|
"""
|
|
168
|
-
Convert days since 1970-01-01 to proleptic Gregorian (year, month, day).
|
|
168
|
+
Convert days since 1970-01-01 (UTC) to proleptic Gregorian (year, month, day).
|
|
169
169
|
|
|
170
|
-
Howard Hinnant's algorithm
|
|
171
|
-
|
|
170
|
+
Howard Hinnant's algorithm
|
|
171
|
+
(http://howardhinnant.github.io/date_algorithms.html#civil_from_days).
|
|
172
|
+
Pure-integer math: works for any year representable by a Python int, so it
|
|
173
|
+
does not overflow like datetime.fromtimestamp (capped at year 9999). The
|
|
174
|
+
e6data engine emits dates that decode to years up to ~10^9 (matching Java's
|
|
175
|
+
LocalDate range); this is the only conversion path that can represent them.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
z (int): Days since 1970-01-01 (negative = before epoch).
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
tuple[int, int, int]: (year, month, day) in the proleptic Gregorian calendar.
|
|
172
182
|
"""
|
|
173
183
|
z = z + 719468
|
|
174
184
|
era = (z if z >= 0 else z - 146096) // 146097
|
|
@@ -185,7 +195,15 @@ def _civil_from_days(z):
|
|
|
185
195
|
|
|
186
196
|
|
|
187
197
|
def _format_iso_year(year):
|
|
188
|
-
"""
|
|
198
|
+
"""
|
|
199
|
+
Render the year component of an ISO-8601 date the way Java's
|
|
200
|
+
DateTimeFormatter.ISO_LOCAL_DATE does, so connector output matches the
|
|
201
|
+
e6data JDBC driver byte-for-byte:
|
|
202
|
+
|
|
203
|
+
- 0 <= year <= 9999 -> "YYYY" (zero-padded, no prefix)
|
|
204
|
+
- year > 9999 -> "+YYYYY..." (variable width, '+' prefix)
|
|
205
|
+
- year < 0 -> "-YYYY..." (zero-padded if 4 digits, '-' prefix)
|
|
206
|
+
"""
|
|
189
207
|
if 0 <= year <= 9999:
|
|
190
208
|
return "{:04d}".format(year)
|
|
191
209
|
if year > 9999:
|
|
@@ -194,7 +212,25 @@ def _format_iso_year(year):
|
|
|
194
212
|
|
|
195
213
|
|
|
196
214
|
def _tz_offset_minutes(tz, ref_seconds=0):
|
|
197
|
-
"""
|
|
215
|
+
"""
|
|
216
|
+
Best-effort extraction of a UTC offset (in whole minutes) from a pytz tzinfo.
|
|
217
|
+
|
|
218
|
+
Used by the datetime formatter to apply the zone numerically, instead of
|
|
219
|
+
constructing a Python datetime (which can't represent years > 9999). Tries
|
|
220
|
+
tz.utcoffset(None) first — works directly for pytz.UTC and pytz.FixedOffset,
|
|
221
|
+
which is what timezone_from_offset() actually returns in practice. Falls
|
|
222
|
+
back to probing tz.utcoffset(<reference datetime>) for named zones, with
|
|
223
|
+
the probe seconds clamped to [0, year-9999] so it never hits the Python
|
|
224
|
+
datetime ceiling.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
tz: A tzinfo / pytz timezone object, or None.
|
|
228
|
+
ref_seconds (int): Epoch seconds used to construct the probe datetime
|
|
229
|
+
for named zones. Ignored for fixed-offset / UTC.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
int: Offset minutes east of UTC. 0 when tz is None or cannot be probed.
|
|
233
|
+
"""
|
|
198
234
|
if tz is None:
|
|
199
235
|
return 0
|
|
200
236
|
try:
|
|
@@ -217,8 +253,23 @@ def _tz_offset_minutes(tz, ref_seconds=0):
|
|
|
217
253
|
|
|
218
254
|
def format_iso_date_from_epoch_micros(epoch_micros):
|
|
219
255
|
"""
|
|
220
|
-
Format epoch microseconds (UTC) as ISO-8601 date with Java
|
|
221
|
-
expanded
|
|
256
|
+
Format epoch microseconds (UTC) as an ISO-8601 date string with Java
|
|
257
|
+
LocalDate-style expanded-year support.
|
|
258
|
+
|
|
259
|
+
Pipeline: micros -> seconds -> days -> (year, month, day) -> string.
|
|
260
|
+
All steps are pure-int, so this never raises ValueError on year overflow
|
|
261
|
+
the way datetime.fromtimestamp(...).strftime(...) does for years > 9999.
|
|
262
|
+
|
|
263
|
+
Examples:
|
|
264
|
+
format_iso_date_from_epoch_micros(1770854400000000) -> "2026-02-12"
|
|
265
|
+
format_iso_date_from_epoch_micros(308847859200000000) -> "+11756-12-31"
|
|
266
|
+
format_iso_date_from_epoch_micros(0) -> "1970-01-01"
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
epoch_micros (int): Microseconds since 1970-01-01T00:00:00Z (may be negative).
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
str: ISO-8601 date. Years 0-9999 unprefixed, others with '+' / '-'.
|
|
222
273
|
"""
|
|
223
274
|
epoch_seconds = floor_div(epoch_micros, 1_000_000)
|
|
224
275
|
days = floor_div(epoch_seconds, _SECONDS_PER_DAY)
|
|
@@ -229,9 +280,34 @@ def format_iso_date_from_epoch_micros(epoch_micros):
|
|
|
229
280
|
def format_iso_datetime_from_epoch_micros(epoch_micros, tz=None, separator='T',
|
|
230
281
|
include_millis=True, include_offset=True):
|
|
231
282
|
"""
|
|
232
|
-
Format epoch microseconds as ISO-8601 datetime with
|
|
233
|
-
|
|
234
|
-
|
|
283
|
+
Format epoch microseconds as an ISO-8601 datetime string with Java
|
|
284
|
+
LocalDateTime / OffsetDateTime-style expanded-year support.
|
|
285
|
+
|
|
286
|
+
The zone is applied numerically via _tz_offset_minutes(), so we never need
|
|
287
|
+
to construct a Python datetime — that's what allows the formatter to handle
|
|
288
|
+
years outside [1, 9999] without raising.
|
|
289
|
+
|
|
290
|
+
Single function with knobs to cover the three format variants the connector
|
|
291
|
+
historically emitted from datetime.strftime / datetime.isoformat:
|
|
292
|
+
|
|
293
|
+
- Row-wise DATETIME (read_values_from_array):
|
|
294
|
+
separator=' ', include_millis=False, include_offset=False
|
|
295
|
+
-> "2026-02-12 00:00:00"
|
|
296
|
+
|
|
297
|
+
- Chunk-wise DATETIME / TIMESTAMP_TZ (get_column_from_chunk), defaults:
|
|
298
|
+
separator='T', include_millis=True, include_offset=True
|
|
299
|
+
-> "2026-02-12T00:00:00.123+00:00"
|
|
300
|
+
-> "+11756-12-31T00:00:00.000+00:00" (year-overflow case)
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
epoch_micros (int): Microseconds since 1970-01-01T00:00:00Z.
|
|
304
|
+
tz: pytz timezone (UTC / FixedOffset / named); None means UTC.
|
|
305
|
+
separator (str): Between date and time (typically 'T' or ' ').
|
|
306
|
+
include_millis (bool): Append '.sss' from sub-second microseconds.
|
|
307
|
+
include_offset (bool): Append '+HH:MM' / '-HH:MM' UTC offset suffix.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
str: ISO-8601 datetime string.
|
|
235
311
|
"""
|
|
236
312
|
epoch_seconds_utc = floor_div(epoch_micros, 1_000_000)
|
|
237
313
|
offset_minutes = _tz_offset_minutes(tz, ref_seconds=epoch_seconds_utc)
|
|
@@ -14,9 +14,15 @@ from decimal import Decimal
|
|
|
14
14
|
|
|
15
15
|
from dateutil.parser import parse
|
|
16
16
|
from sqlalchemy import exc
|
|
17
|
-
from sqlalchemy import processors
|
|
18
17
|
from sqlalchemy import types
|
|
19
|
-
|
|
18
|
+
try:
|
|
19
|
+
from sqlalchemy import processors
|
|
20
|
+
except ImportError:
|
|
21
|
+
from sqlalchemy.engine import processors
|
|
22
|
+
try:
|
|
23
|
+
from sqlalchemy.databases import mysql
|
|
24
|
+
except ImportError:
|
|
25
|
+
from sqlalchemy.dialects import mysql
|
|
20
26
|
from sqlalchemy.engine import default, Engine, Connection
|
|
21
27
|
from sqlalchemy.sql import compiler
|
|
22
28
|
from sqlalchemy.sql.compiler import SQLCompiler
|
|
@@ -27,6 +33,11 @@ from e6data_python_connector.exceptions import *
|
|
|
27
33
|
|
|
28
34
|
_logger = logging.getLogger(__name__)
|
|
29
35
|
|
|
36
|
+
_MYSQL_TINY_INTEGER = (
|
|
37
|
+
getattr(mysql, "MSTinyInteger", None) or
|
|
38
|
+
getattr(mysql, "TINYINT", types.SmallInteger)
|
|
39
|
+
)
|
|
40
|
+
|
|
30
41
|
|
|
31
42
|
class E6dataStringTypeBase(types.TypeDecorator):
|
|
32
43
|
"""Translates strings returned by Thrift into something else"""
|
|
@@ -120,7 +131,7 @@ class E6dataIdentifierPreparer(compiler.IdentifierPreparer):
|
|
|
120
131
|
|
|
121
132
|
_type_map = {
|
|
122
133
|
'boolean': types.Boolean,
|
|
123
|
-
'tinyint':
|
|
134
|
+
'tinyint': _MYSQL_TINY_INTEGER,
|
|
124
135
|
'smallint': types.SmallInteger,
|
|
125
136
|
'integer': types.Integer,
|
|
126
137
|
'bigint': types.BigInteger,
|
|
@@ -316,7 +327,8 @@ class E6dataDialect(default.DefaultDialect):
|
|
|
316
327
|
for column in columns:
|
|
317
328
|
row = dict()
|
|
318
329
|
row["name"] = column.get('fieldName')
|
|
319
|
-
|
|
330
|
+
field_type = str(column.get('fieldType')).lower()
|
|
331
|
+
row["type"] = _type_map.get(field_type, types.String)
|
|
320
332
|
rows.append(row)
|
|
321
333
|
return rows
|
|
322
334
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: e6data-python-connector
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.14
|
|
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.
|
|
@@ -39,7 +39,7 @@ Dynamic: summary
|
|
|
39
39
|
|
|
40
40
|
# e6data Python Connector
|
|
41
41
|
|
|
42
|
-

|
|
43
43
|
|
|
44
44
|
## Introduction
|
|
45
45
|
|
|
@@ -324,6 +324,48 @@ query_id = '<query_id>' # query id from execute function response.
|
|
|
324
324
|
cursor.cancel(query_id)
|
|
325
325
|
```
|
|
326
326
|
|
|
327
|
+
To get the status of a running or completed query:
|
|
328
|
+
```python
|
|
329
|
+
query_id = '<query_id>' # query id from execute function response.
|
|
330
|
+
status_response = cursor.status(query_id)
|
|
331
|
+
|
|
332
|
+
# Check if query execution is complete
|
|
333
|
+
is_complete = status_response.status # Returns True when query is done, False if still running
|
|
334
|
+
|
|
335
|
+
# Get the total row count
|
|
336
|
+
row_count = status_response.rowCount # Total number of rows in the result set
|
|
337
|
+
|
|
338
|
+
print(f"Query complete: {is_complete}")
|
|
339
|
+
print(f"Row count: {row_count}")
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
The `status()` method is useful for:
|
|
343
|
+
- **Monitoring long-running queries**: Poll the status periodically to check if execution is complete
|
|
344
|
+
- **Checking row counts**: Get the total number of rows without fetching all results
|
|
345
|
+
- **Query progress tracking**: Integrate with monitoring systems or progress bars
|
|
346
|
+
- **Conditional fetching**: Decide whether to fetch results based on completion status
|
|
347
|
+
|
|
348
|
+
Example - Polling for query completion:
|
|
349
|
+
```python
|
|
350
|
+
import time
|
|
351
|
+
|
|
352
|
+
cursor = conn.cursor(catalog_name=catalog_name)
|
|
353
|
+
query_id = cursor.execute("SELECT * FROM large_table")
|
|
354
|
+
|
|
355
|
+
# Poll until query is complete
|
|
356
|
+
while True:
|
|
357
|
+
status_response = cursor.status(query_id)
|
|
358
|
+
if status_response.status:
|
|
359
|
+
print(f"Query complete! Total rows: {status_response.rowCount}")
|
|
360
|
+
break
|
|
361
|
+
else:
|
|
362
|
+
print("Query still running...")
|
|
363
|
+
time.sleep(1) # Wait 1 second before checking again
|
|
364
|
+
|
|
365
|
+
# Now fetch the results
|
|
366
|
+
results = cursor.fetchall()
|
|
367
|
+
```
|
|
368
|
+
|
|
327
369
|
Switch database in an existing connection:
|
|
328
370
|
```python
|
|
329
371
|
database = '<new_database_name>' # Replace with the new database.
|
|
@@ -45,7 +45,6 @@ test/analyze_all_cases.py
|
|
|
45
45
|
test/analyze_binary.py
|
|
46
46
|
test/analyze_correct_value.py
|
|
47
47
|
test/analyze_fields.py
|
|
48
|
-
test/benchmark_fastbinary_performance.py
|
|
49
48
|
test/check_decimal_errors.py
|
|
50
49
|
test/cleanup_test_files.py
|
|
51
50
|
test/debug_38_nines.py
|
|
@@ -65,6 +64,7 @@ test/test_connection_pool_e2e.py
|
|
|
65
64
|
test/test_current_implementation.py
|
|
66
65
|
test/test_decimal128_binary_parsing.py
|
|
67
66
|
test/test_decimal128_parsing.py
|
|
67
|
+
test/test_dialect_columns.py
|
|
68
68
|
test/test_fix.py
|
|
69
69
|
test/test_improved_parsing.py
|
|
70
70
|
test/test_known_case.py
|
|
@@ -72,12 +72,10 @@ test/test_manual_analysis.py
|
|
|
72
72
|
test/test_mock_server.py
|
|
73
73
|
test/test_multiprocessing_fix.py
|
|
74
74
|
test/test_new_implementation.py
|
|
75
|
-
test/test_optimizations_integration.py
|
|
76
75
|
test/test_pool_concurrency_simple.py
|
|
77
76
|
test/test_pool_threading_multiprocessing.py
|
|
78
77
|
test/test_simple_connection_pool.py
|
|
79
78
|
test/test_specific_binary.py
|
|
80
|
-
test/test_ssl_cert.py
|
|
81
79
|
test/test_strategy.py
|
|
82
80
|
test/test_strategy_logic.py
|
|
83
81
|
test/test_strategy_persistence_fix.py
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import types
|
|
5
|
+
|
|
6
|
+
from e6data_python_connector import dialect as dialect_module
|
|
7
|
+
from e6data_python_connector.dialect import (
|
|
8
|
+
E6dataDate,
|
|
9
|
+
E6dataDecimal,
|
|
10
|
+
E6dataDialect,
|
|
11
|
+
E6dataTimestamp,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FakeClient(object):
|
|
16
|
+
def __init__(self, columns):
|
|
17
|
+
self.columns = columns
|
|
18
|
+
self.calls = []
|
|
19
|
+
|
|
20
|
+
def get_columns(self, catalog, schema, table_name):
|
|
21
|
+
self.calls.append((catalog, schema, table_name))
|
|
22
|
+
return self.columns
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FakeCursorOwner(object):
|
|
26
|
+
def __init__(self, client):
|
|
27
|
+
self.client = client
|
|
28
|
+
|
|
29
|
+
def cursor(self, catalog_name=None):
|
|
30
|
+
return FakeCursor(self.client)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FakeCursor(object):
|
|
34
|
+
def __init__(self, client):
|
|
35
|
+
self.connection = client
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FakeSQLAlchemyConnection(object):
|
|
39
|
+
def __init__(self, client):
|
|
40
|
+
self.connection = FakeCursorOwner(client)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TestDialectGetColumns(unittest.TestCase):
|
|
44
|
+
def test_get_columns_maps_each_reflected_column_to_its_own_sqlalchemy_type(self):
|
|
45
|
+
client = FakeClient([
|
|
46
|
+
{"fieldName": "id", "fieldType": "integer"},
|
|
47
|
+
{"fieldName": "customer_name", "fieldType": "varchar"},
|
|
48
|
+
{"fieldName": "amount", "fieldType": "decimal"},
|
|
49
|
+
{"fieldName": "created_on", "fieldType": "date"},
|
|
50
|
+
{"fieldName": "created_at", "fieldType": "timestamp"},
|
|
51
|
+
])
|
|
52
|
+
connection = FakeSQLAlchemyConnection(client)
|
|
53
|
+
dialect = E6dataDialect()
|
|
54
|
+
dialect.catalog_name = "lakehouse"
|
|
55
|
+
|
|
56
|
+
with patch.object(dialect_module, "Connection", FakeSQLAlchemyConnection):
|
|
57
|
+
columns = dialect.get_columns(connection, "orders", "sales")
|
|
58
|
+
|
|
59
|
+
self.assertEqual(
|
|
60
|
+
["id", "customer_name", "amount", "created_on", "created_at"],
|
|
61
|
+
[column["name"] for column in columns],
|
|
62
|
+
)
|
|
63
|
+
self.assertEqual([("lakehouse", "sales", "orders")], client.calls)
|
|
64
|
+
self.assertIs(columns[0]["type"], types.Integer)
|
|
65
|
+
self.assertIs(columns[1]["type"], types.String)
|
|
66
|
+
self.assertIs(columns[2]["type"], E6dataDecimal)
|
|
67
|
+
self.assertIs(columns[3]["type"], E6dataDate)
|
|
68
|
+
self.assertIs(columns[4]["type"], E6dataTimestamp)
|
|
69
|
+
|
|
70
|
+
def test_get_columns_defaults_unknown_e6data_type_to_string(self):
|
|
71
|
+
client = FakeClient([
|
|
72
|
+
{"fieldName": "payload", "fieldType": "variant"},
|
|
73
|
+
])
|
|
74
|
+
connection = FakeSQLAlchemyConnection(client)
|
|
75
|
+
dialect = E6dataDialect()
|
|
76
|
+
dialect.catalog_name = "lakehouse"
|
|
77
|
+
|
|
78
|
+
with patch.object(dialect_module, "Connection", FakeSQLAlchemyConnection):
|
|
79
|
+
columns = dialect.get_columns(connection, "events", "sales")
|
|
80
|
+
|
|
81
|
+
self.assertEqual("payload", columns[0]["name"])
|
|
82
|
+
self.assertIs(columns[0]["type"], types.String)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
unittest.main()
|