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.
Files changed (92) hide show
  1. {e6data_python_connector-2.3.13rc1/e6data_python_connector.egg-info → e6data_python_connector-2.3.14}/PKG-INFO +44 -2
  2. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/README.md +43 -1
  3. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/datainputstream.py +13 -0
  4. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/date_time_utils.py +86 -10
  5. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/dialect.py +16 -4
  6. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14/e6data_python_connector.egg-info}/PKG-INFO +44 -2
  7. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector.egg-info/SOURCES.txt +1 -3
  8. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/setup.py +1 -1
  9. e6data_python_connector-2.3.14/test/test_dialect_columns.py +86 -0
  10. e6data_python_connector-2.3.13rc1/test/benchmark_fastbinary_performance.py +0 -303
  11. e6data_python_connector-2.3.13rc1/test/test_optimizations_integration.py +0 -443
  12. e6data_python_connector-2.3.13rc1/test/test_ssl_cert.py +0 -548
  13. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/LICENSE +0 -0
  14. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/MANIFEST.in +0 -0
  15. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/__init__.py +0 -0
  16. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/cluster_manager.py +0 -0
  17. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/cluster_server/__init__.py +0 -0
  18. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
  19. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
  20. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/common.py +0 -0
  21. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/connection_pool.py +0 -0
  22. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/constants.py +0 -0
  23. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/e6data_grpc.py +0 -0
  24. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/e6x_vector/__init__.py +0 -0
  25. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/e6x_vector/constants.py +0 -0
  26. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
  27. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/exceptions.py +0 -0
  28. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/server/QueryEngineService.py +0 -0
  29. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/server/__init__.py +0 -0
  30. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/server/constants.py +0 -0
  31. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/server/e6x_engine_pb2.py +0 -0
  32. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
  33. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +0 -0
  34. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/server/ttypes.py +0 -0
  35. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/strategy.py +0 -0
  36. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector/typeId.py +0 -0
  37. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
  38. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector.egg-info/entry_points.txt +0 -0
  39. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector.egg-info/requires.txt +0 -0
  40. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/e6data_python_connector.egg-info/top_level.txt +0 -0
  41. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/gen-py/__init__.py +0 -0
  42. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/gen-py/e6x_vector/__init__.py +0 -0
  43. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/gen-py/e6x_vector/constants.py +0 -0
  44. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/gen-py/e6x_vector/ttypes.py +0 -0
  45. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/pyproject.toml +0 -0
  46. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/setup.cfg +0 -0
  47. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/__init__.py +0 -0
  48. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/analyze_38_nines.py +0 -0
  49. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/analyze_all_cases.py +0 -0
  50. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/analyze_binary.py +0 -0
  51. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/analyze_correct_value.py +0 -0
  52. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/analyze_fields.py +0 -0
  53. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/check_decimal_errors.py +0 -0
  54. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/cleanup_test_files.py +0 -0
  55. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/debug_38_nines.py +0 -0
  56. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/debug_binary.py +0 -0
  57. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/final_test.py +0 -0
  58. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/move_tests.py +0 -0
  59. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/quick_test.py +0 -0
  60. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_38_nines.py +0 -0
  61. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_all_decimal128_cases.py +0 -0
  62. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_cluster_manager_efficiency.py +0 -0
  63. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_cluster_manager_none_strategy.py +0 -0
  64. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_cluster_manager_strategy.py +0 -0
  65. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_code_optimizations.py +0 -0
  66. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_comprehensive.py +0 -0
  67. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_connection_pool.py +0 -0
  68. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_connection_pool_e2e.py +0 -0
  69. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_current_implementation.py +0 -0
  70. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_decimal128_binary_parsing.py +0 -0
  71. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_decimal128_parsing.py +0 -0
  72. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_fix.py +0 -0
  73. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_improved_parsing.py +0 -0
  74. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_known_case.py +0 -0
  75. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_manual_analysis.py +0 -0
  76. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_mock_server.py +0 -0
  77. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_multiprocessing_fix.py +0 -0
  78. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_new_implementation.py +0 -0
  79. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_pool_concurrency_simple.py +0 -0
  80. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_pool_threading_multiprocessing.py +0 -0
  81. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_simple_connection_pool.py +0 -0
  82. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_specific_binary.py +0 -0
  83. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_strategy.py +0 -0
  84. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_strategy_logic.py +0 -0
  85. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_strategy_persistence_fix.py +0 -0
  86. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_strategy_sharing_fix.py +0 -0
  87. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/test_user_binary_value.py +0 -0
  88. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/tests.py +0 -0
  89. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/tests_grpc.py +0 -0
  90. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/validate_decimal128.py +0 -0
  91. {e6data_python_connector-2.3.13rc1 → e6data_python_connector-2.3.14}/test/validate_implementation.py +0 -0
  92. {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.13rc1
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
- ![version](https://img.shields.io/badge/version-2.3.13rc1-blue.svg)
42
+ ![version](https://img.shields.io/badge/version-2.3.14-blue.svg)
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
- ![version](https://img.shields.io/badge/version-2.3.13rc1-blue.svg)
3
+ ![version](https://img.shields.io/badge/version-2.3.14-blue.svg)
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. Supports any year representable by a Python int,
171
- so it does not overflow like datetime.fromtimestamp (capped at year 9999).
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
- """Java LocalDate-style year: 0000-9999 unprefixed; outside range gets +/- prefix."""
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
- """Best-effort fixed-offset extraction. Returns 0 if unknown."""
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 LocalDate-style
221
- expanded year support (e.g. '+11756-12-31'). Never raises on year overflow.
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 expanded year support.
233
- Mirrors Java's LocalDateTime/OffsetDateTime toString() semantics so out-of-range
234
- years are emitted instead of raising.
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
- from sqlalchemy.databases import mysql
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': mysql.MSTinyInteger,
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
- row["type"] = lambda: column.get('fieldType')
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.13rc1
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
- ![version](https://img.shields.io/badge/version-2.3.13rc1-blue.svg)
42
+ ![version](https://img.shields.io/badge/version-2.3.14-blue.svg)
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
@@ -12,7 +12,7 @@
12
12
 
13
13
  import setuptools
14
14
 
15
- VERSION = (2, 3, 13, 'rc1')
15
+ VERSION = (2, 3, 14,)
16
16
 
17
17
 
18
18
  def get_long_desc():
@@ -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()