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/__init__.py +1 -1
- fakesnow/converter.py +7 -7
- fakesnow/copy_into.py +199 -0
- fakesnow/cursor.py +8 -17
- fakesnow/logger.py +11 -0
- fakesnow/macros.py +24 -0
- fakesnow/transforms/__init__.py +52 -1365
- fakesnow/transforms/transforms.py +1335 -0
- fakesnow/variables.py +1 -1
- {fakesnow-0.9.36.dist-info → fakesnow-0.9.38.dist-info}/METADATA +23 -24
- {fakesnow-0.9.36.dist-info → fakesnow-0.9.38.dist-info}/RECORD +15 -13
- {fakesnow-0.9.36.dist-info → fakesnow-0.9.38.dist-info}/WHEEL +1 -1
- fakesnow/transforms/copy_into.py +0 -163
- {fakesnow-0.9.36.dist-info → fakesnow-0.9.38.dist-info}/entry_points.txt +0 -0
- {fakesnow-0.9.36.dist-info → fakesnow-0.9.38.dist-info}/licenses/LICENSE +0 -0
- {fakesnow-0.9.36.dist-info → fakesnow-0.9.38.dist-info}/top_level.txt +0 -0
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"(
|
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.
|
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:
|
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.
|
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
|
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.
|
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
|
-
|
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=
|
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=
|
8
|
-
fakesnow/
|
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/
|
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=
|
20
|
-
fakesnow/transforms/__init__.py,sha256=
|
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
|
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.
|
27
|
-
fakesnow-0.9.
|
28
|
-
fakesnow-0.9.
|
29
|
-
fakesnow-0.9.
|
30
|
-
fakesnow-0.9.
|
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,,
|
fakesnow/transforms/copy_into.py
DELETED
@@ -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)
|
File without changes
|
File without changes
|
File without changes
|