snowflake-cli 2.8.2__py3-none-any.whl

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 (240) hide show
  1. snowflake/cli/__about__.py +17 -0
  2. snowflake/cli/__init__.py +13 -0
  3. snowflake/cli/api/__init__.py +48 -0
  4. snowflake/cli/api/cli_global_context.py +390 -0
  5. snowflake/cli/api/commands/__init__.py +13 -0
  6. snowflake/cli/api/commands/alias.py +23 -0
  7. snowflake/cli/api/commands/decorators.py +354 -0
  8. snowflake/cli/api/commands/execution_metadata.py +40 -0
  9. snowflake/cli/api/commands/experimental_behaviour.py +19 -0
  10. snowflake/cli/api/commands/flags.py +662 -0
  11. snowflake/cli/api/commands/project_initialisation.py +65 -0
  12. snowflake/cli/api/commands/snow_typer.py +237 -0
  13. snowflake/cli/api/commands/typer_pre_execute.py +26 -0
  14. snowflake/cli/api/config.py +348 -0
  15. snowflake/cli/api/console/__init__.py +17 -0
  16. snowflake/cli/api/console/abc.py +89 -0
  17. snowflake/cli/api/console/console.py +134 -0
  18. snowflake/cli/api/console/enum.py +17 -0
  19. snowflake/cli/api/constants.py +79 -0
  20. snowflake/cli/api/errno.py +27 -0
  21. snowflake/cli/api/exceptions.py +164 -0
  22. snowflake/cli/api/feature_flags.py +55 -0
  23. snowflake/cli/api/identifiers.py +167 -0
  24. snowflake/cli/api/output/__init__.py +13 -0
  25. snowflake/cli/api/output/formats.py +20 -0
  26. snowflake/cli/api/output/types.py +118 -0
  27. snowflake/cli/api/plugins/__init__.py +13 -0
  28. snowflake/cli/api/plugins/command/__init__.py +72 -0
  29. snowflake/cli/api/plugins/command/plugin_hook_specs.py +21 -0
  30. snowflake/cli/api/plugins/plugin_config.py +32 -0
  31. snowflake/cli/api/project/__init__.py +13 -0
  32. snowflake/cli/api/project/definition.py +84 -0
  33. snowflake/cli/api/project/definition_manager.py +134 -0
  34. snowflake/cli/api/project/errors.py +56 -0
  35. snowflake/cli/api/project/project_verification.py +23 -0
  36. snowflake/cli/api/project/schemas/__init__.py +13 -0
  37. snowflake/cli/api/project/schemas/entities/application_entity.py +44 -0
  38. snowflake/cli/api/project/schemas/entities/application_package_entity.py +66 -0
  39. snowflake/cli/api/project/schemas/entities/common.py +78 -0
  40. snowflake/cli/api/project/schemas/entities/entities.py +30 -0
  41. snowflake/cli/api/project/schemas/identifier_model.py +49 -0
  42. snowflake/cli/api/project/schemas/native_app/__init__.py +13 -0
  43. snowflake/cli/api/project/schemas/native_app/application.py +62 -0
  44. snowflake/cli/api/project/schemas/native_app/native_app.py +93 -0
  45. snowflake/cli/api/project/schemas/native_app/package.py +78 -0
  46. snowflake/cli/api/project/schemas/native_app/path_mapping.py +65 -0
  47. snowflake/cli/api/project/schemas/project_definition.py +199 -0
  48. snowflake/cli/api/project/schemas/snowpark/__init__.py +13 -0
  49. snowflake/cli/api/project/schemas/snowpark/argument.py +28 -0
  50. snowflake/cli/api/project/schemas/snowpark/callable.py +69 -0
  51. snowflake/cli/api/project/schemas/snowpark/snowpark.py +36 -0
  52. snowflake/cli/api/project/schemas/streamlit/__init__.py +13 -0
  53. snowflake/cli/api/project/schemas/streamlit/streamlit.py +46 -0
  54. snowflake/cli/api/project/schemas/template.py +77 -0
  55. snowflake/cli/api/project/schemas/updatable_model.py +194 -0
  56. snowflake/cli/api/project/util.py +261 -0
  57. snowflake/cli/api/rendering/__init__.py +13 -0
  58. snowflake/cli/api/rendering/jinja.py +112 -0
  59. snowflake/cli/api/rendering/project_definition_templates.py +39 -0
  60. snowflake/cli/api/rendering/project_templates.py +98 -0
  61. snowflake/cli/api/rendering/sql_templates.py +60 -0
  62. snowflake/cli/api/rest_api.py +172 -0
  63. snowflake/cli/api/sanitizers.py +43 -0
  64. snowflake/cli/api/secure_path.py +362 -0
  65. snowflake/cli/api/secure_utils.py +29 -0
  66. snowflake/cli/api/sql_execution.py +260 -0
  67. snowflake/cli/api/utils/__init__.py +13 -0
  68. snowflake/cli/api/utils/cursor.py +34 -0
  69. snowflake/cli/api/utils/definition_rendering.py +383 -0
  70. snowflake/cli/api/utils/dict_utils.py +73 -0
  71. snowflake/cli/api/utils/error_handling.py +23 -0
  72. snowflake/cli/api/utils/graph.py +97 -0
  73. snowflake/cli/api/utils/models.py +63 -0
  74. snowflake/cli/api/utils/naming_utils.py +13 -0
  75. snowflake/cli/api/utils/path_utils.py +36 -0
  76. snowflake/cli/api/utils/templating_functions.py +144 -0
  77. snowflake/cli/api/utils/types.py +35 -0
  78. snowflake/cli/app/__init__.py +22 -0
  79. snowflake/cli/app/__main__.py +31 -0
  80. snowflake/cli/app/api_impl/__init__.py +13 -0
  81. snowflake/cli/app/api_impl/plugin/__init__.py +13 -0
  82. snowflake/cli/app/api_impl/plugin/plugin_config_provider_impl.py +66 -0
  83. snowflake/cli/app/build_and_push.sh +8 -0
  84. snowflake/cli/app/cli_app.py +243 -0
  85. snowflake/cli/app/commands_registration/__init__.py +33 -0
  86. snowflake/cli/app/commands_registration/builtin_plugins.py +54 -0
  87. snowflake/cli/app/commands_registration/command_plugins_loader.py +169 -0
  88. snowflake/cli/app/commands_registration/commands_registration_with_callbacks.py +105 -0
  89. snowflake/cli/app/commands_registration/exception_logging.py +26 -0
  90. snowflake/cli/app/commands_registration/threadsafe.py +48 -0
  91. snowflake/cli/app/commands_registration/typer_registration.py +153 -0
  92. snowflake/cli/app/constants.py +19 -0
  93. snowflake/cli/app/dev/__init__.py +13 -0
  94. snowflake/cli/app/dev/commands_structure.py +48 -0
  95. snowflake/cli/app/dev/docs/__init__.py +13 -0
  96. snowflake/cli/app/dev/docs/commands_docs_generator.py +100 -0
  97. snowflake/cli/app/dev/docs/generator.py +35 -0
  98. snowflake/cli/app/dev/docs/project_definition_docs_generator.py +58 -0
  99. snowflake/cli/app/dev/docs/project_definition_generate_json_schema.py +227 -0
  100. snowflake/cli/app/dev/docs/template_utils.py +23 -0
  101. snowflake/cli/app/dev/docs/templates/definition_description.rst.jinja2 +38 -0
  102. snowflake/cli/app/dev/docs/templates/overview.rst.jinja2 +9 -0
  103. snowflake/cli/app/dev/docs/templates/usage.rst.jinja2 +57 -0
  104. snowflake/cli/app/dev/pycharm_remote_debug.py +46 -0
  105. snowflake/cli/app/loggers.py +199 -0
  106. snowflake/cli/app/main_typer.py +62 -0
  107. snowflake/cli/app/printing.py +181 -0
  108. snowflake/cli/app/snow_connector.py +243 -0
  109. snowflake/cli/app/telemetry.py +189 -0
  110. snowflake/cli/plugins/__init__.py +13 -0
  111. snowflake/cli/plugins/connection/__init__.py +13 -0
  112. snowflake/cli/plugins/connection/commands.py +330 -0
  113. snowflake/cli/plugins/connection/plugin_spec.py +30 -0
  114. snowflake/cli/plugins/connection/util.py +179 -0
  115. snowflake/cli/plugins/cortex/__init__.py +13 -0
  116. snowflake/cli/plugins/cortex/commands.py +327 -0
  117. snowflake/cli/plugins/cortex/constants.py +17 -0
  118. snowflake/cli/plugins/cortex/manager.py +189 -0
  119. snowflake/cli/plugins/cortex/plugin_spec.py +30 -0
  120. snowflake/cli/plugins/cortex/types.py +22 -0
  121. snowflake/cli/plugins/git/__init__.py +13 -0
  122. snowflake/cli/plugins/git/commands.py +354 -0
  123. snowflake/cli/plugins/git/manager.py +105 -0
  124. snowflake/cli/plugins/git/plugin_spec.py +30 -0
  125. snowflake/cli/plugins/init/__init__.py +13 -0
  126. snowflake/cli/plugins/init/commands.py +248 -0
  127. snowflake/cli/plugins/init/plugin_spec.py +30 -0
  128. snowflake/cli/plugins/nativeapp/__init__.py +13 -0
  129. snowflake/cli/plugins/nativeapp/artifacts.py +742 -0
  130. snowflake/cli/plugins/nativeapp/codegen/__init__.py +13 -0
  131. snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py +91 -0
  132. snowflake/cli/plugins/nativeapp/codegen/compiler.py +130 -0
  133. snowflake/cli/plugins/nativeapp/codegen/sandbox.py +306 -0
  134. snowflake/cli/plugins/nativeapp/codegen/setup/native_app_setup_processor.py +172 -0
  135. snowflake/cli/plugins/nativeapp/codegen/setup/setup_driver.py.source +56 -0
  136. snowflake/cli/plugins/nativeapp/codegen/snowpark/callback_source.py.jinja +181 -0
  137. snowflake/cli/plugins/nativeapp/codegen/snowpark/extension_function_utils.py +217 -0
  138. snowflake/cli/plugins/nativeapp/codegen/snowpark/models.py +61 -0
  139. snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py +528 -0
  140. snowflake/cli/plugins/nativeapp/commands.py +439 -0
  141. snowflake/cli/plugins/nativeapp/common_flags.py +44 -0
  142. snowflake/cli/plugins/nativeapp/constants.py +27 -0
  143. snowflake/cli/plugins/nativeapp/exceptions.py +122 -0
  144. snowflake/cli/plugins/nativeapp/feature_flags.py +24 -0
  145. snowflake/cli/plugins/nativeapp/init.py +345 -0
  146. snowflake/cli/plugins/nativeapp/manager.py +823 -0
  147. snowflake/cli/plugins/nativeapp/plugin_spec.py +30 -0
  148. snowflake/cli/plugins/nativeapp/policy.py +50 -0
  149. snowflake/cli/plugins/nativeapp/project_model.py +195 -0
  150. snowflake/cli/plugins/nativeapp/run_processor.py +389 -0
  151. snowflake/cli/plugins/nativeapp/teardown_processor.py +301 -0
  152. snowflake/cli/plugins/nativeapp/utils.py +98 -0
  153. snowflake/cli/plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +135 -0
  154. snowflake/cli/plugins/nativeapp/version/__init__.py +13 -0
  155. snowflake/cli/plugins/nativeapp/version/commands.py +170 -0
  156. snowflake/cli/plugins/nativeapp/version/version_processor.py +362 -0
  157. snowflake/cli/plugins/notebook/__init__.py +13 -0
  158. snowflake/cli/plugins/notebook/commands.py +85 -0
  159. snowflake/cli/plugins/notebook/exceptions.py +20 -0
  160. snowflake/cli/plugins/notebook/manager.py +71 -0
  161. snowflake/cli/plugins/notebook/plugin_spec.py +30 -0
  162. snowflake/cli/plugins/notebook/types.py +15 -0
  163. snowflake/cli/plugins/object/__init__.py +13 -0
  164. snowflake/cli/plugins/object/command_aliases.py +95 -0
  165. snowflake/cli/plugins/object/commands.py +181 -0
  166. snowflake/cli/plugins/object/common.py +85 -0
  167. snowflake/cli/plugins/object/manager.py +97 -0
  168. snowflake/cli/plugins/object/plugin_spec.py +30 -0
  169. snowflake/cli/plugins/object_stage_deprecated/__init__.py +15 -0
  170. snowflake/cli/plugins/object_stage_deprecated/commands.py +122 -0
  171. snowflake/cli/plugins/object_stage_deprecated/plugin_spec.py +32 -0
  172. snowflake/cli/plugins/snowpark/__init__.py +13 -0
  173. snowflake/cli/plugins/snowpark/commands.py +546 -0
  174. snowflake/cli/plugins/snowpark/common.py +307 -0
  175. snowflake/cli/plugins/snowpark/manager.py +109 -0
  176. snowflake/cli/plugins/snowpark/models.py +157 -0
  177. snowflake/cli/plugins/snowpark/package/__init__.py +13 -0
  178. snowflake/cli/plugins/snowpark/package/anaconda_packages.py +233 -0
  179. snowflake/cli/plugins/snowpark/package/commands.py +256 -0
  180. snowflake/cli/plugins/snowpark/package/manager.py +44 -0
  181. snowflake/cli/plugins/snowpark/package/utils.py +26 -0
  182. snowflake/cli/plugins/snowpark/package_utils.py +354 -0
  183. snowflake/cli/plugins/snowpark/plugin_spec.py +30 -0
  184. snowflake/cli/plugins/snowpark/snowpark_package_paths.py +65 -0
  185. snowflake/cli/plugins/snowpark/snowpark_shared.py +95 -0
  186. snowflake/cli/plugins/snowpark/zipper.py +81 -0
  187. snowflake/cli/plugins/spcs/__init__.py +35 -0
  188. snowflake/cli/plugins/spcs/common.py +99 -0
  189. snowflake/cli/plugins/spcs/compute_pool/__init__.py +13 -0
  190. snowflake/cli/plugins/spcs/compute_pool/commands.py +241 -0
  191. snowflake/cli/plugins/spcs/compute_pool/manager.py +121 -0
  192. snowflake/cli/plugins/spcs/image_registry/__init__.py +13 -0
  193. snowflake/cli/plugins/spcs/image_registry/commands.py +65 -0
  194. snowflake/cli/plugins/spcs/image_registry/manager.py +105 -0
  195. snowflake/cli/plugins/spcs/image_repository/__init__.py +13 -0
  196. snowflake/cli/plugins/spcs/image_repository/commands.py +202 -0
  197. snowflake/cli/plugins/spcs/image_repository/manager.py +84 -0
  198. snowflake/cli/plugins/spcs/jobs/__init__.py +13 -0
  199. snowflake/cli/plugins/spcs/jobs/commands.py +78 -0
  200. snowflake/cli/plugins/spcs/jobs/manager.py +53 -0
  201. snowflake/cli/plugins/spcs/plugin_spec.py +30 -0
  202. snowflake/cli/plugins/spcs/services/__init__.py +13 -0
  203. snowflake/cli/plugins/spcs/services/commands.py +312 -0
  204. snowflake/cli/plugins/spcs/services/manager.py +170 -0
  205. snowflake/cli/plugins/sql/__init__.py +13 -0
  206. snowflake/cli/plugins/sql/commands.py +83 -0
  207. snowflake/cli/plugins/sql/manager.py +92 -0
  208. snowflake/cli/plugins/sql/plugin_spec.py +30 -0
  209. snowflake/cli/plugins/sql/snowsql_templating.py +28 -0
  210. snowflake/cli/plugins/stage/__init__.py +13 -0
  211. snowflake/cli/plugins/stage/commands.py +263 -0
  212. snowflake/cli/plugins/stage/diff.py +326 -0
  213. snowflake/cli/plugins/stage/manager.py +577 -0
  214. snowflake/cli/plugins/stage/md5.py +160 -0
  215. snowflake/cli/plugins/stage/plugin_spec.py +30 -0
  216. snowflake/cli/plugins/streamlit/__init__.py +13 -0
  217. snowflake/cli/plugins/streamlit/commands.py +179 -0
  218. snowflake/cli/plugins/streamlit/manager.py +222 -0
  219. snowflake/cli/plugins/streamlit/plugin_spec.py +30 -0
  220. snowflake/cli/plugins/workspace/__init__.py +13 -0
  221. snowflake/cli/plugins/workspace/commands.py +35 -0
  222. snowflake/cli/plugins/workspace/plugin_spec.py +30 -0
  223. snowflake/cli/templates/default_snowpark/.gitignore +4 -0
  224. snowflake/cli/templates/default_snowpark/app/__init__.py +0 -0
  225. snowflake/cli/templates/default_snowpark/app/common.py +2 -0
  226. snowflake/cli/templates/default_snowpark/app/functions.py +15 -0
  227. snowflake/cli/templates/default_snowpark/app/procedures.py +22 -0
  228. snowflake/cli/templates/default_snowpark/requirements.txt +1 -0
  229. snowflake/cli/templates/default_snowpark/snowflake.yml +23 -0
  230. snowflake/cli/templates/default_streamlit/.gitignore +4 -0
  231. snowflake/cli/templates/default_streamlit/common/hello.py +2 -0
  232. snowflake/cli/templates/default_streamlit/environment.yml +6 -0
  233. snowflake/cli/templates/default_streamlit/pages/my_page.py +3 -0
  234. snowflake/cli/templates/default_streamlit/snowflake.yml +10 -0
  235. snowflake/cli/templates/default_streamlit/streamlit_app.py +4 -0
  236. snowflake_cli-2.8.2.dist-info/METADATA +325 -0
  237. snowflake_cli-2.8.2.dist-info/RECORD +240 -0
  238. snowflake_cli-2.8.2.dist-info/WHEEL +4 -0
  239. snowflake_cli-2.8.2.dist-info/entry_points.txt +2 -0
  240. snowflake_cli-2.8.2.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,160 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ import logging
19
+ import math
20
+ import os.path
21
+ import re
22
+ from pathlib import Path
23
+ from typing import List, Tuple
24
+
25
+ from click.exceptions import ClickException
26
+ from snowflake.cli.api.secure_path import UNLIMITED, SecurePath
27
+ from snowflake.connector.constants import S3_CHUNK_SIZE, S3_MAX_PARTS, S3_MIN_PART_SIZE
28
+
29
+ ONE_MEGABYTE = 1024**2
30
+ READ_BUFFER_BYTES = 64 * 1024
31
+ MD5SUM_REGEX = r"^[A-Fa-f0-9]{32}$"
32
+ MULTIPART_MD5SUM_REGEX = r"^([A-Fa-f0-9]{32})-(\d+)$"
33
+
34
+ log = logging.getLogger(__name__)
35
+
36
+
37
+ class UnknownMD5FormatError(ClickException):
38
+ def __init__(self, md5: str):
39
+ super().__init__(f"Unknown md5 format: {md5}")
40
+
41
+
42
+ def is_md5sum(checksum: str) -> bool:
43
+ """
44
+ Could the provided hexadecimal checksum represent a valid md5sum?
45
+ """
46
+ return re.match(MD5SUM_REGEX, checksum) is not None
47
+
48
+
49
+ def parse_multipart_md5sum(checksum: str) -> Tuple[str, int] | None:
50
+ """
51
+ Does this represent a multi-part md5sum (i.e. "<md5>-<n>")?
52
+ If so, returns the tuple (md5, n), otherwise None.
53
+ """
54
+ multipart_md5 = re.match(MULTIPART_MD5SUM_REGEX, checksum)
55
+ if multipart_md5:
56
+ return (multipart_md5.group(1), int(multipart_md5.group(2)))
57
+ return None
58
+
59
+
60
+ def compute_md5sum(file: Path, chunk_size: int | None = None) -> str:
61
+ """
62
+ Returns a hexadecimal checksum for the file located at the given path.
63
+ If chunk_size is given, computes a multi-part md5sum.
64
+ """
65
+ if not file.is_file():
66
+ raise ValueError(
67
+ "The provided file does not exist or not a (symlink to a) regular file"
68
+ )
69
+
70
+ # If the stage uses SNOWFLAKE_FULL encryption, this will fail to provide
71
+ # a matching md5sum, even when the underlying file is the same, as we do
72
+ # not have access to the encrypted file under checksum.
73
+
74
+ file_size = os.path.getsize(file)
75
+ if file_size == 0:
76
+ # simple md5 with no content
77
+ return hashlib.md5().hexdigest()
78
+
79
+ with SecurePath(file).open("rb", read_file_limit_mb=UNLIMITED) as f:
80
+ md5s: List[hashlib._Hash] = [] # noqa: SLF001
81
+ hasher = hashlib.md5()
82
+
83
+ remains = file_size
84
+ remains_in_chunk: int = min(chunk_size, remains) if chunk_size else remains
85
+ while remains > 0:
86
+ sz = min(READ_BUFFER_BYTES, remains_in_chunk)
87
+ buf = f.read(sz)
88
+ hasher.update(buf)
89
+ remains_in_chunk -= sz
90
+ remains -= sz
91
+ if remains_in_chunk == 0:
92
+ if not chunk_size:
93
+ # simple md5; only one chunk processed
94
+ return hasher.hexdigest()
95
+ else:
96
+ # push the hash of this chunk + reset
97
+ md5s.append(hasher)
98
+ hasher = hashlib.md5()
99
+ remains_in_chunk = min(chunk_size, remains)
100
+
101
+ # multi-part hash (e.g. aws)
102
+ digests = b"".join(m.digest() for m in md5s)
103
+ digests_md5 = hashlib.md5(digests)
104
+ return f"{digests_md5.hexdigest()}-{len(md5s)}"
105
+
106
+
107
+ def file_matches_md5sum(local_file: Path, remote_md5: str | None) -> bool:
108
+ """
109
+ Try a few different md5sums to determine if a local file is identical
110
+ to a file that has a given remote md5sum.
111
+
112
+ Handles the multi-part md5sums generated by e.g. AWS S3, using values
113
+ from the python connector to make educated guesses on chunk size.
114
+
115
+ Assumes that upload time would dominate local hashing time.
116
+ """
117
+ if not remote_md5:
118
+ # no hash available
119
+ return False
120
+
121
+ if is_md5sum(remote_md5):
122
+ # regular hash
123
+ return compute_md5sum(local_file) == remote_md5
124
+
125
+ if md5_and_chunks := parse_multipart_md5sum(remote_md5):
126
+ # multi-part hash (e.g. aws)
127
+ (_, num_chunks) = md5_and_chunks
128
+ file_size = os.path.getsize(local_file)
129
+
130
+ # If this file uses the maximum number of parts supported by the cloud backend,
131
+ # the chunk size is likely not a clean multiple of a megabyte. Try reverse engineering
132
+ # from the file size first, then fall back to the usual detection method.
133
+ # At time of writing this logic would trigger for files >= 80GiB (python connector)
134
+ if num_chunks == S3_MAX_PARTS:
135
+ chunk_size = max(math.ceil(file_size / S3_MAX_PARTS), S3_MIN_PART_SIZE)
136
+ if compute_md5sum(local_file, chunk_size) == remote_md5:
137
+ return True
138
+
139
+ # Estimates the chunk size the multi-part file must have been uploaded with
140
+ # by trying chunk sizes that give the most evenly-sized chunks.
141
+ #
142
+ # First we'll try the chunk size that's a multiple of S3_CHUNK_SIZE (8mb) from
143
+ # the python connector that results in num_chunks, then we'll do the same with
144
+ # a smaller granularity (1mb) that is used by default in some AWS multi-part
145
+ # upload implementations.
146
+ #
147
+ # We're working backwards from num_chunks here because it's the only value we know.
148
+ for chunk_size_alignment in [S3_CHUNK_SIZE, ONE_MEGABYTE]:
149
+ # +1 because we need at least one chunk when file_size < num_chunks * chunk_size_alignment
150
+ # -1 because we don't want to add an extra chunk when file_size is an exact multiple of num_chunks * chunk_size_alignment
151
+ multiplier = 1 + ((file_size - 1) // (num_chunks * chunk_size_alignment))
152
+ chunk_size = multiplier * chunk_size_alignment
153
+ if compute_md5sum(local_file, chunk_size) == remote_md5:
154
+ return True
155
+
156
+ # we were unable to figure out the chunk size, or the files are different
157
+ log.debug("multi-part md5: %s != %s", remote_md5, local_file)
158
+ return False
159
+
160
+ raise UnknownMD5FormatError(remote_md5)
@@ -0,0 +1,30 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from snowflake.cli.api.plugins.command import (
16
+ SNOWCLI_ROOT_COMMAND_PATH,
17
+ CommandSpec,
18
+ CommandType,
19
+ plugin_hook_impl,
20
+ )
21
+ from snowflake.cli.plugins.stage import commands
22
+
23
+
24
+ @plugin_hook_impl
25
+ def command_spec():
26
+ return CommandSpec(
27
+ parent_command_path=SNOWCLI_ROOT_COMMAND_PATH,
28
+ command_type=CommandType.COMMAND_GROUP,
29
+ typer_instance=commands.app.create_instance(),
30
+ )
@@ -0,0 +1,13 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
@@ -0,0 +1,179 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from pathlib import Path
19
+
20
+ import click
21
+ import typer
22
+ from click import ClickException
23
+ from snowflake.cli.api.cli_global_context import cli_context
24
+ from snowflake.cli.api.commands.decorators import (
25
+ with_experimental_behaviour,
26
+ with_project_definition,
27
+ )
28
+ from snowflake.cli.api.commands.flags import (
29
+ ReplaceOption,
30
+ identifier_argument,
31
+ like_option,
32
+ )
33
+ from snowflake.cli.api.commands.project_initialisation import add_init_command
34
+ from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
35
+ from snowflake.cli.api.constants import ObjectType
36
+ from snowflake.cli.api.identifiers import FQN
37
+ from snowflake.cli.api.output.types import (
38
+ CommandResult,
39
+ MessageResult,
40
+ SingleQueryResult,
41
+ )
42
+ from snowflake.cli.api.project.project_verification import assert_project_type
43
+ from snowflake.cli.api.project.schemas.streamlit.streamlit import Streamlit
44
+ from snowflake.cli.plugins.object.command_aliases import (
45
+ add_object_command_aliases,
46
+ scope_option,
47
+ )
48
+ from snowflake.cli.plugins.streamlit.manager import StreamlitManager
49
+
50
+ app = SnowTyperFactory(
51
+ name="streamlit",
52
+ help="Manages a Streamlit app in Snowflake.",
53
+ )
54
+ log = logging.getLogger(__name__)
55
+
56
+ StreamlitNameArgument = identifier_argument(
57
+ sf_object="Streamlit app", example="my_streamlit"
58
+ )
59
+ OpenOption = typer.Option(
60
+ False,
61
+ "--open",
62
+ help="Whether to open the Streamlit app in a browser.",
63
+ is_flag=True,
64
+ )
65
+
66
+ add_init_command(
67
+ app,
68
+ project_type="Streamlit",
69
+ template="default_streamlit",
70
+ help_message="Name of the Streamlit app project directory you want to create. Defaults to `example_streamlit`.",
71
+ )
72
+
73
+ add_object_command_aliases(
74
+ app=app,
75
+ object_type=ObjectType.STREAMLIT,
76
+ name_argument=StreamlitNameArgument,
77
+ like_option=like_option(
78
+ help_example='`list --like "my%"` lists all streamlit apps that begin with “my”'
79
+ ),
80
+ scope_option=scope_option(help_example="`list --in database my_db`"),
81
+ )
82
+
83
+
84
+ @app.command("share", requires_connection=True)
85
+ def streamlit_share(
86
+ name: FQN = StreamlitNameArgument,
87
+ to_role: str = typer.Argument(
88
+ ..., help="Role with which to share the Streamlit app."
89
+ ),
90
+ **options,
91
+ ) -> CommandResult:
92
+ """
93
+ Shares a Streamlit app with another role.
94
+ """
95
+ cursor = StreamlitManager().share(streamlit_name=name, to_role=to_role)
96
+ return SingleQueryResult(cursor)
97
+
98
+
99
+ def _default_file_callback(param_name: str):
100
+ from click.core import ParameterSource # type: ignore
101
+
102
+ def _check_file_exists_if_not_default(ctx: click.Context, value):
103
+ if (
104
+ ctx.get_parameter_source(param_name) != ParameterSource.DEFAULT # type: ignore
105
+ and value
106
+ and not Path(value).exists()
107
+ ):
108
+ raise ClickException(f"Provided file {value} does not exist")
109
+ return Path(value)
110
+
111
+ return _check_file_exists_if_not_default
112
+
113
+
114
+ @app.command("deploy", requires_connection=True)
115
+ @with_project_definition()
116
+ @with_experimental_behaviour()
117
+ def streamlit_deploy(
118
+ replace: bool = ReplaceOption(
119
+ help="Replace the Streamlit app if it already exists."
120
+ ),
121
+ open_: bool = OpenOption,
122
+ **options,
123
+ ) -> CommandResult:
124
+ """
125
+ Deploys a Streamlit app defined in the project definition file (snowflake.yml). By default, the command uploads
126
+ environment.yml and any other pages or folders, if present. If you don’t specify a stage name, the `streamlit`
127
+ stage is used. If the specified stage does not exist, the command creates it.
128
+ """
129
+
130
+ assert_project_type("streamlit")
131
+
132
+ streamlit: Streamlit = cli_context.project_definition.streamlit
133
+ if not streamlit:
134
+ return MessageResult("No streamlit were specified in project definition.")
135
+
136
+ environment_file = streamlit.env_file
137
+ if environment_file and not Path(environment_file).exists():
138
+ raise ClickException(f"Provided file {environment_file} does not exist")
139
+ elif environment_file is None:
140
+ environment_file = "environment.yml"
141
+
142
+ pages_dir = streamlit.pages_dir
143
+ if pages_dir and not Path(pages_dir).exists():
144
+ raise ClickException(f"Provided file {pages_dir} does not exist")
145
+ elif pages_dir is None:
146
+ pages_dir = "pages"
147
+
148
+ streamlit_id = FQN.from_identifier_model(streamlit).using_context()
149
+
150
+ url = StreamlitManager().deploy(
151
+ streamlit_id=streamlit_id,
152
+ environment_file=Path(environment_file),
153
+ pages_dir=Path(pages_dir),
154
+ stage_name=streamlit.stage,
155
+ main_file=Path(streamlit.main_file),
156
+ replace=replace,
157
+ query_warehouse=streamlit.query_warehouse,
158
+ additional_source_files=streamlit.additional_source_files,
159
+ title=streamlit.title,
160
+ **options,
161
+ )
162
+
163
+ if open_:
164
+ typer.launch(url)
165
+
166
+ return MessageResult(f"Streamlit successfully deployed and available under {url}")
167
+
168
+
169
+ @app.command("get-url", requires_connection=True)
170
+ def get_url(
171
+ name: FQN = StreamlitNameArgument,
172
+ open_: bool = OpenOption,
173
+ **options,
174
+ ):
175
+ """Returns a URL to the specified Streamlit app"""
176
+ url = StreamlitManager().get_url(streamlit_name=name)
177
+ if open_:
178
+ typer.launch(url)
179
+ return MessageResult(url)
@@ -0,0 +1,222 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import os
19
+ from pathlib import Path
20
+ from typing import List, Optional
21
+
22
+ from snowflake.cli.api.commands.experimental_behaviour import (
23
+ experimental_behaviour_enabled,
24
+ )
25
+ from snowflake.cli.api.feature_flags import FeatureFlag
26
+ from snowflake.cli.api.identifiers import FQN
27
+ from snowflake.cli.api.sql_execution import SqlExecutionMixin
28
+ from snowflake.cli.plugins.connection.util import (
29
+ MissingConnectionAccountError,
30
+ MissingConnectionRegionError,
31
+ make_snowsight_url,
32
+ )
33
+ from snowflake.cli.plugins.stage.manager import StageManager
34
+ from snowflake.connector.cursor import SnowflakeCursor
35
+ from snowflake.connector.errors import ProgrammingError
36
+
37
+ log = logging.getLogger(__name__)
38
+
39
+
40
+ class StreamlitManager(SqlExecutionMixin):
41
+ def share(self, streamlit_name: FQN, to_role: str) -> SnowflakeCursor:
42
+ return self._execute_query(
43
+ f"grant usage on streamlit {streamlit_name.sql_identifier} to role {to_role}"
44
+ )
45
+
46
+ def _put_streamlit_files(
47
+ self,
48
+ root_location: str,
49
+ main_file: Path,
50
+ environment_file: Optional[Path],
51
+ pages_dir: Optional[Path],
52
+ additional_source_files: Optional[List[Path]],
53
+ ):
54
+ stage_manager = StageManager()
55
+
56
+ stage_manager.put(main_file, root_location, 4, True)
57
+
58
+ if environment_file and environment_file.exists():
59
+ stage_manager.put(environment_file, root_location, 4, True)
60
+
61
+ if pages_dir and pages_dir.exists():
62
+ stage_manager.put(pages_dir / "*.py", f"{root_location}/pages", 4, True)
63
+
64
+ if additional_source_files:
65
+ for file in additional_source_files:
66
+ if os.sep in str(file):
67
+ destination = f"{root_location}/{str(file.parent)}"
68
+ else:
69
+ destination = root_location
70
+ stage_manager.put(file, destination, 4, True)
71
+
72
+ def _create_streamlit(
73
+ self,
74
+ streamlit_id: FQN,
75
+ main_file: Path,
76
+ replace: Optional[bool] = None,
77
+ experimental: Optional[bool] = None,
78
+ query_warehouse: Optional[str] = None,
79
+ from_stage_name: Optional[str] = None,
80
+ title: Optional[str] = None,
81
+ ):
82
+ query = []
83
+ if replace:
84
+ query.append(f"CREATE OR REPLACE STREAMLIT {streamlit_id.sql_identifier}")
85
+ elif experimental:
86
+ # For experimental behaviour, we need to use CREATE STREAMLIT IF NOT EXISTS
87
+ # for a streamlit app with an embedded stage
88
+ # because this is analogous to the behavior for non-experimental
89
+ # deploy which does CREATE STAGE IF NOT EXISTS
90
+ query.append(
91
+ f"CREATE STREAMLIT IF NOT EXISTS {streamlit_id.sql_identifier}"
92
+ )
93
+ else:
94
+ query.append(f"CREATE STREAMLIT {streamlit_id.sql_identifier}")
95
+
96
+ if from_stage_name:
97
+ query.append(f"ROOT_LOCATION = '{from_stage_name}'")
98
+
99
+ query.append(f"MAIN_FILE = '{main_file.name}'")
100
+
101
+ if query_warehouse:
102
+ query.append(f"QUERY_WAREHOUSE = {query_warehouse}")
103
+ if title:
104
+ query.append(f"TITLE = '{title}'")
105
+
106
+ self._execute_query("\n".join(query))
107
+
108
+ def deploy(
109
+ self,
110
+ streamlit_id: FQN,
111
+ main_file: Path,
112
+ environment_file: Optional[Path] = None,
113
+ pages_dir: Optional[Path] = None,
114
+ stage_name: Optional[str] = None,
115
+ query_warehouse: Optional[str] = None,
116
+ replace: Optional[bool] = False,
117
+ additional_source_files: Optional[List[Path]] = None,
118
+ title: Optional[str] = None,
119
+ **options,
120
+ ):
121
+ # for backwards compatibility - quoted stage path might be case-sensitive
122
+ # https://docs.snowflake.com/en/sql-reference/identifiers-syntax#double-quoted-identifiers
123
+ streamlit_name_for_root_location = streamlit_id.name
124
+ use_versioned_stage = FeatureFlag.ENABLE_STREAMLIT_VERSIONED_STAGE.is_enabled()
125
+ if (
126
+ experimental_behaviour_enabled()
127
+ or FeatureFlag.ENABLE_STREAMLIT_EMBEDDED_STAGE.is_enabled()
128
+ or use_versioned_stage
129
+ ):
130
+ """
131
+ 1. Create streamlit object
132
+ 2. Upload files to embedded stage
133
+ """
134
+ # TODO: Support from_stage
135
+ # from_stage_stmt = f"FROM_STAGE = '{stage_name}'" if stage_name else ""
136
+ self._create_streamlit(
137
+ streamlit_id,
138
+ main_file,
139
+ replace=replace,
140
+ query_warehouse=query_warehouse,
141
+ experimental=True,
142
+ title=title,
143
+ )
144
+ try:
145
+ if use_versioned_stage:
146
+ self._execute_query(
147
+ f"ALTER STREAMLIT {streamlit_id.identifier} ADD LIVE VERSION FROM LAST"
148
+ )
149
+ elif not FeatureFlag.ENABLE_STREAMLIT_NO_CHECKOUTS.is_enabled():
150
+ self._execute_query(
151
+ f"ALTER streamlit {streamlit_id.identifier} CHECKOUT"
152
+ )
153
+ except ProgrammingError as e:
154
+ # If an error is raised because a CHECKOUT has already occurred or a LIVE VERSION already exists, simply skip it and continue
155
+ if "Checkout already exists" in str(
156
+ e
157
+ ) or "There is already a live version" in str(e):
158
+ log.info("Checkout already exists, continuing")
159
+ else:
160
+ raise
161
+
162
+ stage_path = streamlit_id.identifier
163
+ embedded_stage_name = f"snow://streamlit/{stage_path}"
164
+ if use_versioned_stage:
165
+ # "LIVE" is the only supported version for now, but this may change later.
166
+ root_location = f"{embedded_stage_name}/versions/live"
167
+ else:
168
+ root_location = f"{embedded_stage_name}/default_checkout"
169
+
170
+ self._put_streamlit_files(
171
+ root_location,
172
+ main_file,
173
+ environment_file,
174
+ pages_dir,
175
+ additional_source_files,
176
+ )
177
+ else:
178
+ """
179
+ 1. Create stage
180
+ 2. Upload files to created stage
181
+ 3. Create streamlit from stage
182
+ """
183
+ stage_manager = StageManager()
184
+
185
+ stage_name = stage_name or "streamlit"
186
+ stage_name = FQN.from_string(stage_name).using_connection(self._conn)
187
+
188
+ stage_manager.create(fqn=stage_name)
189
+
190
+ root_location = stage_manager.get_standard_stage_prefix(
191
+ f"{stage_name}/{streamlit_name_for_root_location}"
192
+ )
193
+
194
+ self._put_streamlit_files(
195
+ root_location,
196
+ main_file,
197
+ environment_file,
198
+ pages_dir,
199
+ additional_source_files,
200
+ )
201
+
202
+ self._create_streamlit(
203
+ streamlit_id,
204
+ main_file,
205
+ replace=replace,
206
+ query_warehouse=query_warehouse,
207
+ from_stage_name=root_location,
208
+ experimental=False,
209
+ title=title,
210
+ )
211
+
212
+ return self.get_url(streamlit_name=streamlit_id)
213
+
214
+ def get_url(self, streamlit_name: FQN) -> str:
215
+ try:
216
+ fqn = streamlit_name.using_connection(self._conn)
217
+ return make_snowsight_url(
218
+ self._conn,
219
+ f"/#/streamlit-apps/{fqn.url_identifier}",
220
+ )
221
+ except (MissingConnectionRegionError, MissingConnectionAccountError) as e:
222
+ return "https://app.snowflake.com"
@@ -0,0 +1,30 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from snowflake.cli.api.plugins.command import (
16
+ SNOWCLI_ROOT_COMMAND_PATH,
17
+ CommandSpec,
18
+ CommandType,
19
+ plugin_hook_impl,
20
+ )
21
+ from snowflake.cli.plugins.streamlit import commands
22
+
23
+
24
+ @plugin_hook_impl
25
+ def command_spec():
26
+ return CommandSpec(
27
+ parent_command_path=SNOWCLI_ROOT_COMMAND_PATH,
28
+ command_type=CommandType.COMMAND_GROUP,
29
+ typer_instance=commands.app.create_instance(),
30
+ )
@@ -0,0 +1,13 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
@@ -0,0 +1,35 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ from snowflake.cli.api.commands.decorators import with_project_definition
18
+ from snowflake.cli.api.commands.snow_typer import SnowTyper
19
+ from snowflake.cli.api.output.types import MessageResult
20
+
21
+ ws = SnowTyper(
22
+ name="ws",
23
+ hidden=True,
24
+ help="Deploy and interact with snowflake.yml-based entities.",
25
+ )
26
+
27
+
28
+ @ws.command(requires_connection=True)
29
+ @with_project_definition()
30
+ def validate(
31
+ **options,
32
+ ):
33
+ """Validates the project definition file."""
34
+ # If we get to this point, @with_project_definition() has already validated the PDF schema
35
+ return MessageResult("Project definition is valid.")