fakesnow 0.9.36__py3-none-any.whl → 0.9.38__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.
fakesnow/variables.py CHANGED
@@ -62,7 +62,7 @@ class Variables:
62
62
  for name, value in self._variables.items():
63
63
  sql = re.sub(rf"\${name}", value, sql, flags=re.IGNORECASE)
64
64
 
65
- if remaining_variables := re.search(r"(?<!\$)\$\w+", sql):
65
+ if remaining_variables := re.search(r"(?<![\$\w])\$\w+", sql):
66
66
  raise snowflake.connector.errors.ProgrammingError(
67
67
  msg=f"Session variable '{remaining_variables.group().upper()}' does not exist"
68
68
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fakesnow
3
- Version: 0.9.36
3
+ Version: 0.9.38
4
4
  Summary: Fake Snowflake Connector for Python. Run, mock and test Snowflake DB locally.
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -204,7 +204,8 @@ License: Apache License
204
204
  See the License for the specific language governing permissions and
205
205
  limitations under the License.
206
206
 
207
- Project-URL: homepage, https://github.com/tekumara/fakesnow
207
+ Project-URL: Source, https://github.com/tekumara/fakesnow
208
+ Project-URL: Changelog, https://github.com/tekumara/fakesnow/blob/main/CHANGELOG.md
208
209
  Keywords: snowflake,snowflakedb,fake,local,mock,testing
209
210
  Classifier: License :: OSI Approved :: MIT License
210
211
  Requires-Python: >=3.9
@@ -213,25 +214,7 @@ License-File: LICENSE
213
214
  Requires-Dist: duckdb~=1.2.0
214
215
  Requires-Dist: pyarrow
215
216
  Requires-Dist: snowflake-connector-python
216
- Requires-Dist: sqlglot~=26.12.1
217
- Provides-Extra: dev
218
- Requires-Dist: boto3-stubs[s3,sts]; extra == "dev"
219
- Requires-Dist: build~=1.0; extra == "dev"
220
- Requires-Dist: dirty-equals; extra == "dev"
221
- Requires-Dist: pandas-stubs; extra == "dev"
222
- Requires-Dist: snowflake-connector-python[pandas,secure-local-storage]; extra == "dev"
223
- Requires-Dist: pre-commit~=4.0; extra == "dev"
224
- Requires-Dist: pyarrow-stubs==17.19; extra == "dev"
225
- Requires-Dist: pytest~=8.0; extra == "dev"
226
- Requires-Dist: ruff~=0.11.0; extra == "dev"
227
- Requires-Dist: twine~=6.0; extra == "dev"
228
- Requires-Dist: snowflake-sqlalchemy~=1.7.0; extra == "dev"
229
- Requires-Dist: boto3; extra == "dev"
230
- Requires-Dist: moto[server]>=5; extra == "dev"
231
- Provides-Extra: notebook
232
- Requires-Dist: duckdb-engine; extra == "notebook"
233
- Requires-Dist: ipykernel; extra == "notebook"
234
- Requires-Dist: jupysql; extra == "notebook"
217
+ Requires-Dist: sqlglot~=26.16.2
235
218
  Provides-Extra: server
236
219
  Requires-Dist: starlette; extra == "server"
237
220
  Requires-Dist: uvicorn; extra == "server"
@@ -260,7 +243,7 @@ pip install fakesnow[server]
260
243
 
261
244
  ## Usage
262
245
 
263
- fakesnow offers two main approaches for faking Snowflake: in-process patching of the [Snowflake Connector for Python](https://docs.snowflake.com/en/user-guide/python-connector) or a standalone HTTP server.
246
+ fakesnow offers two main approaches for faking Snowflake: [in-process patching](#in-process-patching) of the Snowflake Connector for Python or a [standalone HTTP server](#run-fakesnow-as-a-server).
264
247
 
265
248
  Patching only applies to the current Python process. If a subprocess is spawned it won't be patched. For subprocesses, or for non-Python clients, use the server instead.
266
249
 
@@ -369,7 +352,13 @@ with fakesnow.server(port=12345) as conn_kwargs:
369
352
 
370
353
  ### pytest fixtures
371
354
 
372
- fakesnow provides [fixtures](fakesnow/fixtures.py) for easier test integration. Here's an example _conftest.py_ using them:
355
+ fakesnow provides [fixtures](fakesnow/fixtures.py) for easier test integration. Add them in _conftest.py_:
356
+
357
+ ```python
358
+ pytest_plugins = "fakesnow.fixtures"
359
+ ```
360
+
361
+ To autouse the fixture you can wrap it like this in _conftest.py_:
373
362
 
374
363
  ```python
375
364
  from typing import Iterator
@@ -394,13 +383,23 @@ from typing import Iterator
394
383
  import fakesnow
395
384
  import pytest
396
385
 
386
+ pytest_plugins = "fakesnow.fixtures"
387
+
397
388
  @pytest.fixture(scope="session", autouse=True)
398
389
  def _fakesnow_session() -> Iterator[None]:
399
390
  with fakesnow.patch("mymodule.write_pandas"):
400
391
  yield
401
392
  ```
402
393
 
403
- To start a fakesnow server instance, use the `fakesnow_server` session fixture:
394
+ #### server fixture
395
+
396
+ To start a fakesnow server instance, enable the plugin in _conftest.py_:
397
+
398
+ ```python
399
+ pytest_plugins = "fakesnow.fixtures"
400
+ ```
401
+
402
+ And then use the `fakesnow_server` session fixture like this:
404
403
 
405
404
  ```python
406
405
  import snowflake.connector
@@ -1,30 +1,32 @@
1
- fakesnow/__init__.py,sha256=It-8mTZWBaVi4suZjL7UJlJBGFhLWmPnI-THX02XRJU,5108
1
+ fakesnow/__init__.py,sha256=71Rk_3s_4eTDCi7-bbo-xT71WN0E0MAPf5qjsguIeJU,5117
2
2
  fakesnow/__main__.py,sha256=GDrGyNTvBFuqn_UfDjKs7b3LPtU6gDv1KwosVDrukIM,76
3
3
  fakesnow/arrow.py,sha256=XjTpFyLrD9jULWOtPgpr0RyNMmO6a5yi82y6ivi2CCI,4884
4
4
  fakesnow/checks.py,sha256=be-xo0oMoAUVhlMDCu1_Rkoh_L8p_p8qo9P6reJSHIQ,2874
5
5
  fakesnow/cli.py,sha256=9qfI-Ssr6mo8UmIlXkUAOz2z2YPBgDsrEVaZv9FjGFs,2201
6
6
  fakesnow/conn.py,sha256=diCwcjaCBrlCn9PyjbScfIQTNQjqiPTkQanUTqcvblE,6009
7
- fakesnow/converter.py,sha256=xoBFnfBbGWQyUQAVr6zi-RyglU8A7A3GSlwLPkH1dzI,1621
8
- fakesnow/cursor.py,sha256=3JCxSoBJ2g6bndIGQnJnTAWu8Ad7zK_6kwmAY_b0VKE,22949
7
+ fakesnow/converter.py,sha256=wPOfsFXIUJNJSx5oFNAxh13udxmAVIIHsLK8BiGkXGA,1635
8
+ fakesnow/copy_into.py,sha256=JT4SpynozlcdmWwzlQwwncPiuHCE5UUv-XGX_CI9s90,7235
9
+ fakesnow/cursor.py,sha256=so8OET_ZLlvRyxanKORfGU8gSiIa71FKILsS45lh0cE,22287
9
10
  fakesnow/expr.py,sha256=CAxuYIUkwI339DQIBzvFF0F-m1tcVGKEPA5rDTzmH9A,892
10
11
  fakesnow/fakes.py,sha256=JQTiUkkwPeQrJ8FDWhPFPK6pGwd_aR2oiOrNzCWznlM,187
11
12
  fakesnow/fixtures.py,sha256=2rj0MTZlaZc4PNWhaqC5IiiLa7E9G0QZT3g45YawsL0,633
12
13
  fakesnow/info_schema.py,sha256=AYmTIHxk5Y6xdMTgttgBL1V0VO8qiM2T1-gKwkLmWDs,8720
13
14
  fakesnow/instance.py,sha256=OKoYXwaI6kL9HQpnHx44yzpON_xNfuIT_F4oJNF_XXQ,2114
14
- fakesnow/macros.py,sha256=pX1YJDnQOkFJSHYUjQ6ErEkYIKvFI6Ncz_au0vv1csA,265
15
+ fakesnow/logger.py,sha256=U6EjUENQuTrDeNYqER2hxazoySmXzLmZJ-t-SDZgjkg,363
16
+ fakesnow/macros.py,sha256=bQfZR5ptO4Gk-8fFRK2iksqYWkJUT8e-rPp-000qzu0,999
15
17
  fakesnow/pandas_tools.py,sha256=wI203UQHC8JvDzxE_VjE1NeV4rThek2P-u52oTg2foo,3481
16
18
  fakesnow/py.typed,sha256=B-DLSjYBi7pkKjwxCSdpVj2J02wgfJr-E7B1wOUyxYU,80
17
19
  fakesnow/rowtype.py,sha256=QUp8EaXD5LT0Xv8BXk5ze4WseEn52xoJ6R05pJjs5mM,2729
18
20
  fakesnow/server.py,sha256=RHf7ffKYi5xBH9fh8wZr3tEPmnzFWuvUbziCC8UwTh4,6652
19
- fakesnow/variables.py,sha256=C3y_9u7LuVtARkpcim3ihgVWg6KKdz1hSVeW4YI7oL4,3014
20
- fakesnow/transforms/__init__.py,sha256=RcVGkp95yKByluQ5O6RALJTiRlox8FK4pMl1rt_gJPc,49536
21
- fakesnow/transforms/copy_into.py,sha256=QJ1hh3hVi9kPJgyQHlGO3Vi8sP3qnWmvY4JWO--HWl0,5565
21
+ fakesnow/variables.py,sha256=sWWSvuWY6yAhKhsl9KFzE703bCPeMAJruIa_dY8LaKs,3018
22
+ fakesnow/transforms/__init__.py,sha256=jHbn7T6fSxhiu3KVn_Xupi5JwgY9SbjcfGU-0WpdELU,2769
22
23
  fakesnow/transforms/merge.py,sha256=Pg7_rwbAT_vr1U4ocBofUSyqaK8_e3qdIz_2SDm2S3s,8320
23
24
  fakesnow/transforms/show.py,sha256=0NjuLQjodrukfUw8mcxcAmtBkV_6r02mA3nuE3ad3rE,17458
24
- fakesnow-0.9.36.dist-info/licenses/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
25
+ fakesnow/transforms/transforms.py,sha256=kjkQGTSkZ5lOJ-G13WQL4uB4yTzoIcsa3qcgBAgPH0c,47985
26
+ fakesnow-0.9.38.dist-info/licenses/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
25
27
  tools/decode.py,sha256=kC5kUvLQxdCkMRsnH6BqCajlKxKeN77w6rwCKsY6gqU,1781
26
- fakesnow-0.9.36.dist-info/METADATA,sha256=Nu1iV0SOnWCNeQCVJXbwC2PVxx7mkdQ6YDfDVkfTmI4,21160
27
- fakesnow-0.9.36.dist-info/WHEEL,sha256=lTU6B6eIfYoiQJTZNc-fyaR6BpL6ehTzU3xGYxn2n8k,91
28
- fakesnow-0.9.36.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
29
- fakesnow-0.9.36.dist-info/top_level.txt,sha256=Yos7YveA3f03xVYuURqnBsfMV2DePXfu_yGcsj3pPzI,30
30
- fakesnow-0.9.36.dist-info/RECORD,,
28
+ fakesnow-0.9.38.dist-info/METADATA,sha256=nzZJ-lAiabUJv6a_Mv6xYTJyVvkmZas8bACLfr5xZEE,20680
29
+ fakesnow-0.9.38.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
30
+ fakesnow-0.9.38.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
31
+ fakesnow-0.9.38.dist-info/top_level.txt,sha256=Yos7YveA3f03xVYuURqnBsfMV2DePXfu_yGcsj3pPzI,30
32
+ fakesnow-0.9.38.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.1)
2
+ Generator: setuptools (80.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,163 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass, field, replace
4
- from pathlib import PurePath
5
- from typing import Protocol
6
- from urllib.parse import urlparse, urlunparse
7
-
8
- import snowflake.connector.errors
9
- from sqlglot import exp
10
- from typing_extensions import Self
11
-
12
-
13
- def copy_into(expr: exp.Expression) -> exp.Expression:
14
- if not isinstance(expr, exp.Copy):
15
- return expr
16
-
17
- schema = expr.this
18
-
19
- columns = [exp.Column(this=exp.Identifier(this=f"column{i}")) for i in range(len(schema.expressions))] or [
20
- exp.Column(this=exp.Star())
21
- ]
22
-
23
- params = expr.args.get("params", [])
24
- # TODO: remove columns
25
- file_type_handler = _handle_params(params, [c.name for c in columns])
26
-
27
- # the FROM expression
28
- source = expr.args["files"][0].this
29
- assert isinstance(source, exp.Literal), f"{source.__class__} is not a exp.Literal"
30
-
31
- if len(file_type_handler.files) > 1:
32
- raise NotImplementedError("Multiple files not currently supported")
33
- file = file_type_handler.files[0]
34
-
35
- scheme, netloc, path, params, query, fragment = urlparse(source.name)
36
- if not scheme:
37
- raise snowflake.connector.errors.ProgrammingError(
38
- msg=f"SQL compilation error:\ninvalid URL prefix found in: '{source.name}'", errno=1011, sqlstate="42601"
39
- )
40
- path = str(PurePath(path) / file.name)
41
- url = urlunparse((scheme, netloc, path, params, query, fragment))
42
-
43
- return exp.Insert(
44
- this=schema,
45
- expression=exp.Select(expressions=columns).from_(exp.Table(this=file_type_handler.read_expression(url))),
46
- copy_from=url,
47
- )
48
-
49
-
50
- def _handle_params(params: list[exp.CopyParameter], columns: list[str]) -> FileTypeHandler:
51
- file_type_handler = None
52
- force = False
53
- files = []
54
- for param in params:
55
- var = param.this.name
56
- if var == "FILE_FORMAT":
57
- if file_type_handler:
58
- raise ValueError(params)
59
-
60
- var_type = next((e.args["value"].this for e in param.expressions if e.this.this == "TYPE"), None)
61
- if not var_type:
62
- raise NotImplementedError("FILE_FORMAT without TYPE is not currently implemented")
63
-
64
- if var_type == "CSV":
65
- file_type_handler = handle_csv(param.expressions, columns)
66
- else:
67
- raise NotImplementedError(f"{var_type} FILE_FORMAT is not currently implemented")
68
-
69
- elif var == "FILES":
70
- files = param.expression.expressions if isinstance(param.expression, exp.Tuple) else [param.expression.this]
71
- elif var == "FORCE":
72
- force = True
73
- pass
74
- else:
75
- raise ValueError(f"Unknown copy parameter: {param.this}")
76
-
77
- if not force:
78
- raise NotImplementedError("COPY INTO with FORCE=false (default) is not currently implemented")
79
-
80
- if not files:
81
- raise NotImplementedError("COPY INTO without FILES is not currently implemented")
82
-
83
- if not file_type_handler:
84
- # default to CSV
85
- file_type_handler = handle_csv([], columns)
86
-
87
- file_type_handler = file_type_handler.with_files(files)
88
- return file_type_handler
89
-
90
-
91
- def handle_csv(expressions: list[exp.Property], columns: list[str]) -> ReadCSV:
92
- skip_header = ReadCSV.skip_header
93
- quote = ReadCSV.quote
94
- delimiter = ReadCSV.delimiter
95
-
96
- for expression in expressions:
97
- exp_type = expression.name
98
- if exp_type in {"TYPE"}:
99
- continue
100
-
101
- elif exp_type == "SKIP_HEADER":
102
- skip_header = True
103
- elif exp_type == "FIELD_OPTIONALLY_ENCLOSED_BY":
104
- quote = expression.args["value"].this
105
- elif exp_type == "FIELD_DELIMITER":
106
- delimiter = expression.args["value"].this
107
- else:
108
- raise NotImplementedError(f"{exp_type} is not currently implemented")
109
-
110
- return ReadCSV(
111
- skip_header=skip_header,
112
- quote=quote,
113
- delimiter=delimiter,
114
- columns=columns,
115
- )
116
-
117
-
118
- @dataclass
119
- class FileTypeHandler(Protocol):
120
- files: list = field(default_factory=list)
121
-
122
- def read_expression(self, url: str) -> exp.Expression: ...
123
-
124
- def with_files(self, files: list) -> Self:
125
- return replace(self, files=files)
126
-
127
- @staticmethod
128
- def make_eq(name: str, value: list | str | int | bool) -> exp.EQ:
129
- if isinstance(value, list):
130
- expression = exp.array(*[exp.Literal(this=str(v), is_string=isinstance(v, str)) for v in value])
131
- elif isinstance(value, bool):
132
- expression = exp.Boolean(this=value)
133
- else:
134
- expression = exp.Literal(this=str(value), is_string=isinstance(value, str))
135
-
136
- return exp.EQ(this=exp.Literal(this=name, is_string=False), expression=expression)
137
-
138
-
139
- @dataclass
140
- class ReadCSV(FileTypeHandler):
141
- skip_header: bool = False
142
- quote: str | None = None
143
- delimiter: str = ","
144
- columns: list[str] = field(default_factory=list)
145
-
146
- def read_expression(self, url: str) -> exp.Expression:
147
- args = []
148
-
149
- # don't parse header and use as column names, keep them as column0, column1, etc
150
- args.append(self.make_eq("header", False))
151
-
152
- if self.skip_header:
153
- args.append(self.make_eq("skip", 1))
154
-
155
- if self.quote:
156
- quote = self.quote.replace("'", "''")
157
- args.append(self.make_eq("quote", quote))
158
-
159
- if self.delimiter and self.delimiter != ",":
160
- delimiter = self.delimiter.replace("'", "''")
161
- args.append(self.make_eq("sep", delimiter))
162
-
163
- return exp.func("read_csv", exp.Literal(this=url, is_string=True), *args)