snowflake-sqlalchemy 1.7.7__tar.gz → 1.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/DESCRIPTION.md +9 -1
  2. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/PKG-INFO +3 -5
  3. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/README.md +0 -3
  4. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/ci/docker/sqlalchemy_build/Dockerfile +10 -3
  5. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/ci/test_docker.sh +19 -19
  6. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/ci/test_linux.sh +2 -2
  7. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/pyproject.toml +8 -7
  8. snowflake_sqlalchemy-1.8.0/snyk/requirements.txt +2 -0
  9. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/snowdialect.py +59 -13
  10. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/version.py +1 -1
  11. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/conftest.py +22 -33
  12. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_core.py +3 -3
  13. snowflake_sqlalchemy-1.8.0/tests/test_dialect_connect.py +124 -0
  14. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_pandas.py +3 -1
  15. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_qmark.py +3 -1
  16. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tox.ini +10 -4
  17. snowflake_sqlalchemy-1.7.7/snyk/requirements.txt +0 -2
  18. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/.gitignore +0 -0
  19. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/.gitmodules +0 -0
  20. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/.pre-commit-config.yaml +0 -0
  21. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/LICENSE.txt +0 -0
  22. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/MANIFEST.in +0 -0
  23. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/ci/build.sh +0 -0
  24. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/ci/build_docker.sh +0 -0
  25. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/ci/docker/sqlalchemy_build/scripts/entrypoint.sh +0 -0
  26. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/ci/set_base_image.sh +0 -0
  27. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/ci/test.sh +0 -0
  28. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/license_header.txt +0 -0
  29. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/setup.cfg +0 -0
  30. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/snyk/requiremtnts.txt +0 -0
  31. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/snyk/update_requirements.py +0 -0
  32. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/__init__.py +0 -0
  33. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/_constants.py +0 -0
  34. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/base.py +0 -0
  35. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/compat.py +0 -0
  36. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/custom_commands.py +0 -0
  37. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/custom_types.py +0 -0
  38. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/exc.py +0 -0
  39. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/functions.py +0 -0
  40. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/name_utils.py +0 -0
  41. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/parser/custom_type_parser.py +0 -0
  42. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/provision.py +0 -0
  43. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/requirements.py +0 -0
  44. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/__init__.py +0 -0
  45. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/__init__.py +0 -0
  46. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/clustered_table.py +0 -0
  47. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/custom_table_base.py +0 -0
  48. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/custom_table_prefix.py +0 -0
  49. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/dynamic_table.py +0 -0
  50. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/hybrid_table.py +0 -0
  51. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/iceberg_table.py +0 -0
  52. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/__init__.py +0 -0
  53. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/as_query_option.py +0 -0
  54. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/cluster_by_option.py +0 -0
  55. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/identifier_option.py +0 -0
  56. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/invalid_table_option.py +0 -0
  57. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/keyword_option.py +0 -0
  58. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/keywords.py +0 -0
  59. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/literal_option.py +0 -0
  60. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/table_option.py +0 -0
  61. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/options/target_lag_option.py +0 -0
  62. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/snowflake_table.py +0 -0
  63. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/sql/custom_schema/table_from_query.py +0 -0
  64. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/structured_type_info_manager.py +0 -0
  65. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/src/snowflake/sqlalchemy/util.py +0 -0
  66. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tested_requirements/requirements_310.reqs +0 -0
  67. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tested_requirements/requirements_37.reqs +0 -0
  68. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tested_requirements/requirements_38.reqs +0 -0
  69. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tested_requirements/requirements_39.reqs +0 -0
  70. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/README.rst +0 -0
  71. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/__init__.py +0 -0
  72. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/__snapshots__/test_compile_dynamic_table.ambr +0 -0
  73. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/__snapshots__/test_core.ambr +0 -0
  74. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/__snapshots__/test_orm.ambr +0 -0
  75. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/__snapshots__/test_reflect_dynamic_table.ambr +0 -0
  76. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/__snapshots__/test_structured_datatypes.ambr +0 -0
  77. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/__snapshots__/test_unit_structured_types.ambr +0 -0
  78. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__init__.py +0 -0
  79. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_compile_dynamic_table.ambr +0 -0
  80. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_compile_hybrid_table.ambr +0 -0
  81. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_compile_iceberg_table.ambr +0 -0
  82. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_compile_snowflake_table.ambr +0 -0
  83. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_create_dynamic_table.ambr +0 -0
  84. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_create_hybrid_table.ambr +0 -0
  85. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_create_iceberg_table.ambr +0 -0
  86. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_create_snowflake_table.ambr +0 -0
  87. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_generic_options.ambr +0 -0
  88. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_reflect_hybrid_table.ambr +0 -0
  89. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/__snapshots__/test_reflect_snowflake_table.ambr +0 -0
  90. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_compile_dynamic_table.py +0 -0
  91. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_compile_hybrid_table.py +0 -0
  92. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_compile_iceberg_table.py +0 -0
  93. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_compile_snowflake_table.py +0 -0
  94. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_create_dynamic_table.py +0 -0
  95. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_create_hybrid_table.py +0 -0
  96. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_create_iceberg_table.py +0 -0
  97. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_create_snowflake_table.py +0 -0
  98. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_generic_options.py +0 -0
  99. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_reflect_dynamic_table.py +0 -0
  100. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_reflect_hybrid_table.py +0 -0
  101. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/custom_tables/test_reflect_snowflake_table.py +0 -0
  102. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/data/users.txt +0 -0
  103. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/sqlalchemy_test_suite/README.md +0 -0
  104. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/sqlalchemy_test_suite/__init__.py +0 -0
  105. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/sqlalchemy_test_suite/conftest.py +0 -0
  106. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/sqlalchemy_test_suite/test_suite.py +0 -0
  107. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/sqlalchemy_test_suite/test_suite_20.py +0 -0
  108. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_compiler.py +0 -0
  109. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_copy.py +0 -0
  110. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_create.py +0 -0
  111. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_custom_functions.py +0 -0
  112. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_custom_types.py +0 -0
  113. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_geography.py +0 -0
  114. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_geometry.py +0 -0
  115. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_imports.py +0 -0
  116. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_index_reflection.py +0 -0
  117. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_multivalues_insert.py +0 -0
  118. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_orm.py +0 -0
  119. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_quote.py +0 -0
  120. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_quote_identifiers.py +0 -0
  121. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_semi_structured_datatypes.py +0 -0
  122. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_sequence.py +0 -0
  123. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_structured_datatypes.py +0 -0
  124. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_timestamp.py +0 -0
  125. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_transactions.py +0 -0
  126. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_unit_core.py +0 -0
  127. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_unit_cte.py +0 -0
  128. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_unit_structured_types.py +0 -0
  129. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_unit_types.py +0 -0
  130. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/test_unit_url.py +0 -0
  131. {snowflake_sqlalchemy-1.7.7 → snowflake_sqlalchemy-1.8.0}/tests/util.py +0 -0
@@ -6,11 +6,19 @@ Snowflake Documentation is available at:
6
6
 
7
7
  Source code is also available at:
8
8
  <https://github.com/snowflakedb/snowflake-sqlalchemy>
9
+
9
10
  # Unreleased Notes
10
11
 
11
12
  # Release Notes
13
+
14
+ - v1.8.0(December 5, 2025)
15
+ - Add logging of SQLAlchemy version
16
+ - Bump `snowflake-connector-python<5.0.0`
17
+ - Add python up to 3.14
18
+ - Add logging of SQLAlchemy version and pandas (if used)
19
+
12
20
  - v1.7.7(September 3, 2025)
13
- - Fix exception for structured type columns dropped while collecting meetadata
21
+ - Fix exception for structured type columns dropped while collecting metadata
14
22
 
15
23
  - v1.7.6(July 10, 2025)
16
24
  - Fix get_multi_indexes issue, wrong assign of returned indexes when processing multiple indexes in a table
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snowflake-sqlalchemy
3
- Version: 1.7.7
3
+ Version: 1.8.0
4
4
  Summary: Snowflake SQLAlchemy Dialect
5
5
  Project-URL: Changelog, https://github.com/snowflakedb/snowflake-sqlalchemy/blob/main/DESCRIPTION.md
6
6
  Project-URL: Documentation, https://docs.snowflake.com/en/user-guide/sqlalchemy.html
@@ -27,6 +27,7 @@ Classifier: Programming Language :: Python :: 3.9
27
27
  Classifier: Programming Language :: Python :: 3.10
28
28
  Classifier: Programming Language :: Python :: 3.11
29
29
  Classifier: Programming Language :: Python :: 3.12
30
+ Classifier: Programming Language :: Python :: 3.13
30
31
  Classifier: Programming Language :: SQL
31
32
  Classifier: Topic :: Database
32
33
  Classifier: Topic :: Scientific/Engineering :: Information Analysis
@@ -35,7 +36,7 @@ Classifier: Topic :: Software Development :: Libraries
35
36
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
36
37
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
37
38
  Requires-Python: >=3.8
38
- Requires-Dist: snowflake-connector-python<4.0.0
39
+ Requires-Dist: snowflake-connector-python<5.0.0
39
40
  Requires-Dist: sqlalchemy>=1.4.19
40
41
  Provides-Extra: development
41
42
  Requires-Dist: mock; extra == 'development'
@@ -63,9 +64,6 @@ Description-Content-Type: text/markdown
63
64
  Snowflake SQLAlchemy runs on the top of the Snowflake Connector for Python as a [dialect](http://docs.sqlalchemy.org/en/latest/dialects/) to bridge a Snowflake database and SQLAlchemy applications.
64
65
 
65
66
 
66
- | :exclamation: | Effective May 8th, 2025, Snowflake SQLAlchemy will transition to maintenance mode and will cease active development. Support will be limited to addressing critical bugs and security vulnerabilities. To report such issues, please [create a case with Snowflake Support](https://community.snowflake.com/s/article/How-To-Submit-a-Support-Case-in-Snowflake-Lodge). for individual evaluation. Please note that pull requests from external contributors may not receive action from Snowflake. |
67
- |---------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
68
-
69
67
 
70
68
  ## Prerequisites
71
69
 
@@ -9,9 +9,6 @@
9
9
  Snowflake SQLAlchemy runs on the top of the Snowflake Connector for Python as a [dialect](http://docs.sqlalchemy.org/en/latest/dialects/) to bridge a Snowflake database and SQLAlchemy applications.
10
10
 
11
11
 
12
- | :exclamation: | Effective May 8th, 2025, Snowflake SQLAlchemy will transition to maintenance mode and will cease active development. Support will be limited to addressing critical bugs and security vulnerabilities. To report such issues, please [create a case with Snowflake Support](https://community.snowflake.com/s/article/How-To-Submit-a-Support-Case-in-Snowflake-Lodge). for individual evaluation. Please note that pull requests from external contributors may not receive action from Snowflake. |
13
- |---------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
14
-
15
12
 
16
13
  ## Prerequisites
17
14
 
@@ -4,9 +4,9 @@ FROM $BASE_IMAGE
4
4
 
5
5
  # This is to solve permission issue, read https://denibertovic.com/posts/handling-permissions-with-docker-volumes/
6
6
  ARG GOSU_URL=https://github.com/tianon/gosu/releases/download/1.14/gosu-amd64
7
- ENV GOSU_PATH $GOSU_URL
7
+ ENV GOSU_PATH=$GOSU_URL
8
8
  RUN curl -o /usr/local/bin/gosu -SL $GOSU_PATH \
9
- && chmod +x /usr/local/bin/gosu
9
+ && chmod +x /usr/local/bin/gosu
10
10
 
11
11
  COPY scripts/entrypoint.sh /usr/local/bin/entrypoint.sh
12
12
  RUN chmod +x /usr/local/bin/entrypoint.sh
@@ -14,6 +14,13 @@ RUN chmod +x /usr/local/bin/entrypoint.sh
14
14
  WORKDIR /home/user
15
15
  RUN chmod 777 /home/user
16
16
 
17
- ENV PATH="${PATH}:/opt/python/cp37-cp37m/bin:/opt/python/cp38-cp38/bin:/opt/python/cp39-cp39/bin:/opt/python/cp310-cp310/bin:/opt/python/cp311-cp311/bin"
17
+ ENV PATH="${PATH}:/opt/python/cp37-cp37/bin"
18
+ ENV PATH="${PATH}:/opt/python/cp38-cp38/bin"
19
+ ENV PATH="${PATH}:/opt/python/cp39-cp39/bin"
20
+ ENV PATH="${PATH}:/opt/python/cp310-cp310/bin"
21
+ ENV PATH="${PATH}:/opt/python/cp311-cp311/bin"
22
+ ENV PATH="${PATH}:/opt/python/cp312-cp312/bin"
23
+ ENV PATH="${PATH}:/opt/python/cp313-cp313/bin"
24
+ ENV PATH="${PATH}:/opt/python/cp314-cp314/bin"
18
25
 
19
26
  ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
@@ -7,11 +7,11 @@
7
7
  set -o pipefail
8
8
 
9
9
  # In case this is ran from dev-vm
10
- PYTHON_ENV=${1:-3.7}
10
+ PYTHON_ENV=${1:-3.8}
11
11
 
12
12
  # Set constants
13
- THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
14
- SQLALCHEMY_DIR="$( dirname "${THIS_DIR}")"
13
+ THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ SQLALCHEMY_DIR="$(dirname "${THIS_DIR}")"
15
15
  WORKSPACE=${WORKSPACE:-$SQLALCHEMY_DIR}
16
16
  source $THIS_DIR/set_base_image.sh
17
17
 
@@ -35,19 +35,19 @@ echo "[Info] Start building docker image and testing"
35
35
 
36
36
  user_id=$(id -u ${USER})
37
37
  docker run \
38
- --rm \
39
- --network=host \
40
- -e TERM=vt102 \
41
- -e PIP_DISABLE_PIP_VERSION_CHECK=1 \
42
- -e OPENSSL_FIPS=1 \
43
- -e LOCAL_USER_ID=${user_id} \
44
- -e AWS_ACCESS_KEY_ID \
45
- -e AWS_SECRET_ACCESS_KEY \
46
- -e SF_REGRESS_LOGS \
47
- -e SF_PROJECT_ROOT \
48
- -e cloud_provider \
49
- -e JENKINS_HOME \
50
- -e GITHUB_ACTIONS \
51
- --mount type=bind,source="${SQLALCHEMY_DIR}",target=/home/user/snowflake-sqlalchemy \
52
- $(docker build --pull --build-arg BASE_IMAGE=$BASE_IMAGE --build-arg GOSU_URL="$GOSU_URL" -q .) \
53
- /home/user/snowflake-sqlalchemy/ci/test_linux.sh ${PYTHON_ENV}
38
+ --rm \
39
+ --network=host \
40
+ -e TERM=vt102 \
41
+ -e PIP_DISABLE_PIP_VERSION_CHECK=1 \
42
+ -e OPENSSL_FIPS=1 \
43
+ -e LOCAL_USER_ID=${user_id} \
44
+ -e AWS_ACCESS_KEY_ID \
45
+ -e AWS_SECRET_ACCESS_KEY \
46
+ -e SF_REGRESS_LOGS \
47
+ -e SF_PROJECT_ROOT \
48
+ -e cloud_provider \
49
+ -e JENKINS_HOME \
50
+ -e GITHUB_ACTIONS \
51
+ --mount type=bind,source="${SQLALCHEMY_DIR}",target=/home/user/snowflake-sqlalchemy \
52
+ $(docker build --pull --build-arg BASE_IMAGE=$BASE_IMAGE --build-arg GOSU_URL="$GOSU_URL" -q .) \
53
+ /home/user/snowflake-sqlalchemy/ci/test_linux.sh ${PYTHON_ENV}
@@ -6,7 +6,7 @@
6
6
  # - This script assumes that ../dist/repaired_wheels has the wheel(s) built for all versions to be tested
7
7
  # - This is the script that test_docker.sh runs inside of the docker container
8
8
 
9
- PYTHON_VERSIONS="${1:-3.8 3.9 3.10 3.11}"
9
+ PYTHON_VERSIONS="${1:-3.8 3.9 3.10 3.11 3.12 3.13 3.14}"
10
10
  THIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  SQLALCHEMY_DIR="$(dirname "${THIS_DIR}")"
12
12
 
@@ -19,7 +19,7 @@ for PYTHON_VERSION in ${PYTHON_VERSIONS}; do
19
19
  echo "[Info] Testing with ${PYTHON_VERSION}"
20
20
  SHORT_VERSION=$(python3 -c "print('${PYTHON_VERSION}'.replace('.', ''))")
21
21
  SQLALCHEMY_WHL=$(ls $SQLALCHEMY_DIR/dist/snowflake_sqlalchemy-*-py3-none-any.whl | sort -r | head -n 1)
22
- TEST_ENVLIST=fix_lint,py${SHORT_VERSION}-ci,py${SHORT_VERSION}-coverage
22
+ TEST_ENVLIST=fix_lint,py${SHORT_VERSION}-ci,py${SHORT_VERSION}-coverage,py${SHORT_VERSION}-pandas-ci,py${SHORT_VERSION}-pandas-coverage
23
23
  echo "[Info] Running tox for ${TEST_ENVLIST}"
24
24
  python3 -m tox -e ${TEST_ENVLIST} --installpkg ${SQLALCHEMY_WHL}
25
25
  done
@@ -30,6 +30,7 @@ classifiers = [
30
30
  "Programming Language :: Python :: 3.10",
31
31
  "Programming Language :: Python :: 3.11",
32
32
  "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
33
34
  "Programming Language :: SQL",
34
35
  "Topic :: Database",
35
36
  "Topic :: Scientific/Engineering :: Information Analysis",
@@ -38,7 +39,7 @@ classifiers = [
38
39
  "Topic :: Software Development :: Libraries :: Application Frameworks",
39
40
  "Topic :: Software Development :: Libraries :: Python Modules",
40
41
  ]
41
- dependencies = ["SQLAlchemy>=1.4.19", "snowflake-connector-python<4.0.0"]
42
+ dependencies = ["SQLAlchemy>=1.4.19", "snowflake-connector-python<5.0.0"]
42
43
 
43
44
  [tool.hatch.version]
44
45
  path = "src/snowflake/sqlalchemy/version.py"
@@ -75,17 +76,17 @@ exclude = ["/.github"]
75
76
  packages = ["src/snowflake"]
76
77
 
77
78
  [tool.hatch.envs.default]
78
- path = ".venv"
79
- type = "virtual"
80
79
  extra-dependencies = ["SQLAlchemy>=1.4.19,<2.1.0"]
81
80
  features = ["development", "pandas"]
82
- python = "3.8"
81
+ python = "3.12"
83
82
  installer = "uv"
84
83
 
85
84
  [tool.hatch.envs.sa14]
86
- extra-dependencies = ["SQLAlchemy>=1.4.19,<2.0.0"]
85
+ installer = "uv"
86
+ builder = true
87
+ extra-dependencies = ["SQLAlchemy>=1.4.19,<2.0.0", "pandas<2.1", "numpy<2"]
87
88
  features = ["development", "pandas"]
88
- python = "3.8"
89
+ python = "3.12"
89
90
 
90
91
  [tool.hatch.envs.sa14.scripts]
91
92
  test-dialect = "pytest --ignore_v20_test -ra -vvv --tb=short --cov snowflake.sqlalchemy --cov-append --junitxml ./junit.xml --ignore=tests/sqlalchemy_test_suite tests/"
@@ -105,7 +106,7 @@ gh-cache-sum = "python -VV | sha256sum | cut -d' ' -f1"
105
106
  check-import = "python -c 'import snowflake.sqlalchemy; print(snowflake.sqlalchemy.__version__)'"
106
107
 
107
108
  [[tool.hatch.envs.release.matrix]]
108
- python = ["3.8", "3.9", "3.10", "3.11", "3.12"]
109
+ python = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
109
110
  features = ["development", "pandas"]
110
111
 
111
112
  [tool.hatch.envs.release.scripts]
@@ -0,0 +1,2 @@
1
+ SQLAlchemy>=1.4.19
2
+ snowflake-connector-python<5.0.0
@@ -5,10 +5,13 @@ import operator
5
5
  from collections import defaultdict
6
6
  from enum import Enum
7
7
  from functools import reduce
8
- from typing import Any, Collection, Optional
8
+ from logging import getLogger
9
+ from time import time as time_in_seconds
10
+ from typing import Any, Collection, Optional, cast
9
11
  from urllib.parse import unquote_plus
10
12
 
11
13
  import sqlalchemy.sql.sqltypes as sqltypes
14
+ from sqlalchemy import __version__ as SQLALCHEMY_VERSION
12
15
  from sqlalchemy import event as sa_vnt
13
16
  from sqlalchemy import exc as sa_exc
14
17
  from sqlalchemy import util as sa_util
@@ -19,8 +22,10 @@ from sqlalchemy.sql.sqltypes import NullType
19
22
  from sqlalchemy.types import FLOAT, Date, DateTime, Float, Time
20
23
 
21
24
  from snowflake.connector import errors as sf_errors
22
- from snowflake.connector.connection import DEFAULT_CONFIGURATION
25
+ from snowflake.connector.connection import DEFAULT_CONFIGURATION, SnowflakeConnection
23
26
  from snowflake.connector.constants import UTF8
27
+ from snowflake.connector.network import SnowflakeRestful
28
+ from snowflake.connector.telemetry import TelemetryClient, TelemetryData, TelemetryField
24
29
  from snowflake.sqlalchemy.compat import returns_unicode
25
30
  from snowflake.sqlalchemy.name_utils import _NameUtils
26
31
  from snowflake.sqlalchemy.structured_type_info_manager import _StructuredTypeInfoManager
@@ -59,6 +64,12 @@ colspecs = {
59
64
 
60
65
  _ENABLE_SQLALCHEMY_AS_APPLICATION_NAME = True
61
66
 
67
+ logger = getLogger(__name__)
68
+
69
+
70
+ class TelemetryEvents(Enum):
71
+ NEW_CONNECTION = "sqlalchemy_new_connection"
72
+
62
73
 
63
74
  class SnowflakeIsolationLevel(Enum):
64
75
  READ_COMMITTED = "READ COMMITTED"
@@ -896,18 +907,53 @@ class SnowflakeDialect(default.DefaultDialect):
896
907
  return self._value_or_default(data, table_name, schema)
897
908
 
898
909
  def connect(self, *cargs, **cparams):
899
- return (
900
- super().connect(
901
- *cargs,
902
- **(
903
- _update_connection_application_name(**cparams)
904
- if _ENABLE_SQLALCHEMY_AS_APPLICATION_NAME
905
- else cparams
906
- ),
910
+ if _ENABLE_SQLALCHEMY_AS_APPLICATION_NAME:
911
+ cparams = _update_connection_application_name(**cparams)
912
+
913
+ connection = super().connect(*cargs, **cparams)
914
+ self._log_new_connection_event(connection)
915
+
916
+ return connection
917
+
918
+ def _log_new_connection_event(self, connection):
919
+ try:
920
+ snowflake_connection = cast(SnowflakeConnection, cast(object, connection))
921
+ snowflake_rest_client = SnowflakeRestful(
922
+ host=snowflake_connection.host,
923
+ port=snowflake_connection.port,
924
+ protocol="https",
925
+ connection=snowflake_connection,
926
+ )
927
+ snowflake_telemetry_client = TelemetryClient(rest=snowflake_rest_client)
928
+
929
+ telemetry_value = {
930
+ "SQLAlchemy": SQLALCHEMY_VERSION,
931
+ }
932
+ try:
933
+ from pandas import __version__ as PANDAS_VERSION
934
+
935
+ telemetry_value["pandas"] = PANDAS_VERSION
936
+ except ImportError:
937
+ pass
938
+
939
+ snowflake_telemetry_client.add_log_to_batch(
940
+ TelemetryData.from_telemetry_data_dict(
941
+ from_dict={
942
+ TelemetryField.KEY_TYPE.value: TelemetryEvents.NEW_CONNECTION.value,
943
+ TelemetryField.KEY_VALUE.value: str(telemetry_value),
944
+ },
945
+ timestamp=int(time_in_seconds() * 1000),
946
+ connection=snowflake_connection,
947
+ )
948
+ )
949
+ snowflake_telemetry_client.send_batch()
950
+ except Exception as e:
951
+ logger.debug(
952
+ "Failed to send telemetry data for %s event: %s: %s",
953
+ TelemetryEvents.NEW_CONNECTION.value,
954
+ type(e).__name__,
955
+ str(e),
907
956
  )
908
- if _ENABLE_SQLALCHEMY_AS_APPLICATION_NAME
909
- else super().connect(*cargs, **cparams)
910
- )
911
957
 
912
958
 
913
959
  @sa_vnt.listens_for(Table, "before_create")
@@ -3,4 +3,4 @@
3
3
  #
4
4
  # Update this for the versions
5
5
  # Don't change the forth version number from None
6
- VERSION = "1.7.7"
6
+ VERSION = "1.8.0"
@@ -9,6 +9,7 @@ import sys
9
9
  import time
10
10
  import uuid
11
11
  from logging import getLogger
12
+ from typing import Literal
12
13
 
13
14
  import pytest
14
15
  from sqlalchemy import create_engine
@@ -58,7 +59,6 @@ def pytest_addoption(parser):
58
59
 
59
60
  def pytest_collection_modifyitems(config, items):
60
61
  if config.getoption("--ignore_v20_test"):
61
- # --ignore_v20_test given in cli: skip sqlalchemy 2.0 tests
62
62
  skip_feature_v2 = pytest.mark.skip(
63
63
  reason="need remove --ignore_v20_test option to run"
64
64
  )
@@ -67,37 +67,11 @@ def pytest_collection_modifyitems(config, items):
67
67
  item.add_marker(skip_feature_v2)
68
68
 
69
69
 
70
- @pytest.fixture(scope="session")
71
- def on_travis():
72
- return os.getenv("TRAVIS", "").lower() == "true"
73
-
74
-
75
- @pytest.fixture(scope="session")
76
- def on_appveyor():
77
- return os.getenv("APPVEYOR", "").lower() == "true"
78
-
79
-
80
- @pytest.fixture(scope="session")
81
- def on_public_ci(on_travis, on_appveyor):
82
- return on_travis or on_appveyor
83
-
84
-
85
- def help():
86
- print(
87
- """Connection parameter must be specified in parameters.py,
88
- for example:
89
- CONNECTION_PARAMETERS = {
90
- 'account': 'testaccount',
91
- 'user': 'user1',
92
- 'password': 'test',
93
- 'database': 'testdb',
94
- 'schema': 'public',
95
- }"""
96
- )
97
-
98
-
99
70
  logger = getLogger(__name__)
100
71
 
72
+ TZ_ENV_VAR: Literal["TZ"] = "TZ"
73
+ DEFAULT_TZ_VALUE: Literal["UTC"] = "UTC"
74
+
101
75
  DEFAULT_PARAMETERS = {
102
76
  "account": "<account_name>",
103
77
  "user": "<user_name>",
@@ -133,6 +107,11 @@ def external_stage():
133
107
  raise ValueError("External_stage is not set")
134
108
 
135
109
 
110
+ @pytest.fixture(scope="session")
111
+ def on_public_ci():
112
+ return running_on_public_ci()
113
+
114
+
136
115
  @pytest.fixture(scope="function")
137
116
  def base_location(external_stage, engine_testaccount):
138
117
  unique_id = str(uuid.uuid4())
@@ -150,9 +129,12 @@ def get_db_parameters() -> dict:
150
129
  Sets the db connection parameters
151
130
  """
152
131
  ret = {}
153
- os.environ["TZ"] = "UTC"
154
- if not IS_WINDOWS:
155
- time.tzset()
132
+ os.environ[TZ_ENV_VAR] = DEFAULT_TZ_VALUE
133
+ if hasattr(time, "tzset"):
134
+ if not IS_WINDOWS:
135
+ time.tzset()
136
+ else:
137
+ logger.warning("time.tzset is unavailable on this platform")
156
138
 
157
139
  ret.update(DEFAULT_PARAMETERS)
158
140
  ret.update(CONNECTION_PARAMETERS)
@@ -318,6 +300,7 @@ def running_on_public_ci() -> bool:
318
300
 
319
301
  def pytest_runtest_setup(item) -> None:
320
302
  """Ran before calling each test, used to decide whether a test should be skipped."""
303
+ _ensure_optional_dependencies(item)
321
304
  test_tags = [mark.name for mark in item.iter_markers()]
322
305
 
323
306
  # Get what cloud providers the test is marked for if any
@@ -335,3 +318,9 @@ def pytest_runtest_setup(item) -> None:
335
318
  pytest.skip("cannot run this test on external CI")
336
319
  elif INTERNAL_SKIP_TAGS.intersection(test_tags) and not running_on_public_ci():
337
320
  pytest.skip("cannot run this test on internal CI")
321
+
322
+
323
+ def _ensure_optional_dependencies(item):
324
+ """Skip optional-dependency tests when the dependency is unavailable."""
325
+ if "pandas" in item.keywords:
326
+ pytest.importorskip("pandas")
@@ -171,7 +171,6 @@ def test_connect_args():
171
171
 
172
172
 
173
173
  def test_boolean_query_argument_parsing():
174
-
175
174
  engine = create_engine(
176
175
  URL(
177
176
  **CONNECTION_PARAMETERS,
@@ -1385,12 +1384,12 @@ def test_special_schema_character(db_parameters, on_public_ci):
1385
1384
  with connect(**options) as sf_conn:
1386
1385
  sf_connection = (
1387
1386
  sf_conn.cursor()
1388
- .execute("select current_database(), " "current_schema();")
1387
+ .execute("select current_database(), current_schema();")
1389
1388
  .fetchall()
1390
1389
  )
1391
1390
  with create_engine(URL(**options)).connect() as sa_conn:
1392
1391
  sa_connection = sa_conn.execute(
1393
- text("select current_database(), " "current_schema();")
1392
+ text("select current_database(), current_schema();")
1394
1393
  ).fetchall()
1395
1394
  # Teardown
1396
1395
  with connect(**options) as conn:
@@ -1823,6 +1822,7 @@ CREATE OR REPLACE TEMP TABLE {table_name}
1823
1822
  )
1824
1823
 
1825
1824
 
1825
+ @pytest.mark.pandas
1826
1826
  def test_snowflake_sqlalchemy_as_valid_client_type():
1827
1827
  engine = create_engine(
1828
1828
  URL(**CONNECTION_PARAMETERS),
@@ -0,0 +1,124 @@
1
+ #
2
+ # Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
3
+ #
4
+
5
+ from sys import modules
6
+ from types import SimpleNamespace
7
+ from unittest import mock
8
+
9
+ import pytest
10
+ from sqlalchemy import __version__ as SQLALCHEMY_VERSION
11
+ from sqlalchemy.engine import default as sqla_default
12
+
13
+ from snowflake.sqlalchemy.snowdialect import (
14
+ SnowflakeDialect,
15
+ TelemetryEvents,
16
+ TelemetryField,
17
+ )
18
+
19
+
20
+ @pytest.fixture
21
+ def fake_connection():
22
+ return SimpleNamespace(
23
+ host="example.snowflakecomputing.com",
24
+ port=443,
25
+ application="test_app",
26
+ )
27
+
28
+
29
+ @mock.patch.object(sqla_default.DefaultDialect, "connect")
30
+ @mock.patch("snowflake.sqlalchemy.snowdialect.TelemetryClient")
31
+ @mock.patch("snowflake.sqlalchemy.snowdialect.SnowflakeRestful")
32
+ def test_connect_sends_telemetry(
33
+ mock_restful, mock_telemetry_client, mock_connect, fake_connection
34
+ ):
35
+ """Ensure telemetry is sent with the expected payload on connect."""
36
+ mock_connect.return_value = fake_connection
37
+
38
+ # Mock out pandas to ensure deterministic behavior
39
+ with mock.patch.dict(modules, {"pandas": None}):
40
+ dialect = SnowflakeDialect()
41
+ result = dialect.connect()
42
+
43
+ assert result is fake_connection
44
+
45
+ # Verify add_log_to_batch was called with correct payload
46
+ telemetry_instance = mock_telemetry_client.return_value
47
+ payload = telemetry_instance.add_log_to_batch.call_args[0][0]
48
+ assert (
49
+ payload.message[TelemetryField.KEY_TYPE.value]
50
+ == TelemetryEvents.NEW_CONNECTION.value
51
+ )
52
+ assert payload.message[TelemetryField.KEY_VALUE.value] == str(
53
+ {"SQLAlchemy": SQLALCHEMY_VERSION}
54
+ )
55
+ assert payload.timestamp != 0
56
+
57
+ # Verify send_batch was called
58
+ telemetry_instance.send_batch.assert_called_once()
59
+
60
+
61
+ @mock.patch.object(sqla_default.DefaultDialect, "connect")
62
+ @mock.patch("snowflake.sqlalchemy.snowdialect.TelemetryClient")
63
+ @mock.patch("snowflake.sqlalchemy.snowdialect.SnowflakeRestful")
64
+ def test_connect_telemetry_includes_pandas_when_available(
65
+ mock_restful, mock_telemetry_client, mock_connect, fake_connection
66
+ ):
67
+ """Ensure telemetry includes pandas version when pandas is installed."""
68
+ mock_connect.return_value = fake_connection
69
+
70
+ # Create a mock pandas module with a version
71
+ mock_pandas = mock.MagicMock()
72
+ mock_pandas.__version__ = "2.1.0"
73
+
74
+ with mock.patch.dict(modules, {"pandas": mock_pandas}):
75
+ dialect = SnowflakeDialect()
76
+ dialect.connect()
77
+
78
+ telemetry_instance = mock_telemetry_client.return_value
79
+ payload = telemetry_instance.add_log_to_batch.call_args[0][0]
80
+ telemetry_value = payload.message[TelemetryField.KEY_VALUE.value]
81
+
82
+ assert telemetry_value == str({"SQLAlchemy": SQLALCHEMY_VERSION, "pandas": "2.1.0"})
83
+
84
+
85
+ @mock.patch.object(sqla_default.DefaultDialect, "connect")
86
+ @mock.patch("snowflake.sqlalchemy.snowdialect.TelemetryClient")
87
+ @mock.patch("snowflake.sqlalchemy.snowdialect.SnowflakeRestful")
88
+ def test_connect_telemetry_excludes_pandas_when_not_available(
89
+ mock_restful, mock_telemetry_client, mock_connect, fake_connection
90
+ ):
91
+ """Ensure telemetry does not include pandas when it is not installed."""
92
+ mock_connect.return_value = fake_connection
93
+
94
+ # Simulate pandas not being installed
95
+ with mock.patch.dict(modules, {"pandas": None}):
96
+ dialect = SnowflakeDialect()
97
+ dialect.connect()
98
+
99
+ telemetry_instance = mock_telemetry_client.return_value
100
+ payload = telemetry_instance.add_log_to_batch.call_args[0][0]
101
+ telemetry_value = payload.message[TelemetryField.KEY_VALUE.value]
102
+
103
+ assert telemetry_value == str({"SQLAlchemy": SQLALCHEMY_VERSION})
104
+
105
+
106
+ @mock.patch.object(sqla_default.DefaultDialect, "connect")
107
+ @mock.patch("snowflake.sqlalchemy.snowdialect.TelemetryClient")
108
+ @mock.patch("snowflake.sqlalchemy.snowdialect.SnowflakeRestful")
109
+ def test_connect_logs_when_telemetry_fails(
110
+ mock_restful, mock_telemetry_client, mock_connect, caplog, fake_connection
111
+ ):
112
+ """Ensure failures in telemetry do not break connect and are logged."""
113
+ mock_connect.return_value = fake_connection
114
+ mock_telemetry_client.side_effect = RuntimeError("boom")
115
+
116
+ caplog.set_level("DEBUG", logger="snowflake.sqlalchemy.snowdialect")
117
+
118
+ dialect = SnowflakeDialect()
119
+ result = dialect.connect()
120
+
121
+ assert result is fake_connection
122
+ assert any(
123
+ "Failed to send telemetry data" in message for message in caplog.messages
124
+ )
@@ -10,8 +10,10 @@ import textwrap
10
10
  import uuid
11
11
 
12
12
  import numpy as np
13
- import pandas as pd
14
13
  import pytest
14
+
15
+ pytest.importorskip("pandas")
16
+ import pandas as pd
15
17
  import sqlalchemy
16
18
  from sqlalchemy import (
17
19
  Column,
@@ -5,8 +5,10 @@
5
5
  import os
6
6
  import sys
7
7
 
8
- import pandas as pd
9
8
  import pytest
9
+
10
+ pytest.importorskip("pandas")
11
+ import pandas as pd
10
12
  from sqlalchemy import text
11
13
 
12
14
  THIS_DIR = os.path.dirname(os.path.realpath(__file__))
@@ -1,7 +1,7 @@
1
1
  [tox]
2
2
  min_version = 4.0.0
3
3
  envlist = fix_lint,
4
- py{37,38,39,310,311}{,-pandas},
4
+ py{38,39,310,311,312,313,314}{,-pandas},
5
5
  coverage,
6
6
  skip_missing_interpreters = true
7
7
 
@@ -10,14 +10,17 @@ package = external
10
10
  description = run the tests with pytest under {basepython}
11
11
  extras =
12
12
  development
13
- pandas
13
+ pandas: pandas
14
14
  external_wheels =
15
- py37-ci: dist/*.whl
16
15
  py38-ci: dist/*.whl
17
16
  py39-ci: dist/.whl
18
17
  py310-ci: dist/.whl
19
18
  py311-ci: dist/.whl
19
+ py312-ci: dist/.whl
20
+ py313-ci: dist/.whl
21
+ py314-ci: dist/.whl
20
22
  deps = pip
23
+ pytest-xdist
21
24
  passenv =
22
25
  AWS_ACCESS_KEY_ID
23
26
  AWS_SECRET_ACCESS_KEY
@@ -32,6 +35,7 @@ passenv =
32
35
  USERNAME
33
36
  PYTEST_ADDOPTS
34
37
  setenv =
38
+ PIP_ONLY_BINARY=pyarrow
35
39
  COVERAGE_FILE = {env:COVERAGE_FILE:{toxworkdir}/.coverage.{envname}}
36
40
  SQLALCHEMY_WARN_20 = 1
37
41
  ci: SNOWFLAKE_PYTEST_OPTS = -vvv --tb=long
@@ -40,10 +44,12 @@ commands = pytest \
40
44
  --cov "snowflake.sqlalchemy" \
41
45
  --junitxml {toxworkdir}/junit_{envname}.xml \
42
46
  --ignore=tests/sqlalchemy_test_suite \
47
+ -n8 \
43
48
  {posargs:tests}
44
49
  pytest {env:SNOWFLAKE_PYTEST_OPTS:} \
45
50
  --cov "snowflake.sqlalchemy" --cov-append \
46
51
  --junitxml {toxworkdir}/junit_{envname}.xml \
52
+ -n8 \
47
53
  {posargs:tests/sqlalchemy_test_suite}
48
54
 
49
55
  [testenv:.pkg_external]
@@ -66,7 +72,7 @@ commands = coverage combine
66
72
  coverage xml -o {toxworkdir}/coverage.xml
67
73
  coverage html -d {toxworkdir}/htmlcov
68
74
  ;diff-cover --compare-branch {env:DIFF_AGAINST:origin/main} {toxworkdir}/coverage.xml
69
- depends = py37, py38, py39, py310, py311
75
+ depends = py38, py39, py310, py311, py312, py313, py314
70
76
 
71
77
  [testenv:fix_lint]
72
78
  description = format the code base to adhere to our styles, and complain about what we cannot do automatically
@@ -1,2 +0,0 @@
1
- SQLAlchemy>=1.4.19
2
- snowflake-connector-python<4.0.0