fakesnow 0.9.1__tar.gz → 0.9.2__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.
- {fakesnow-0.9.1/fakesnow.egg-info → fakesnow-0.9.2}/PKG-INFO +4 -3
- {fakesnow-0.9.1 → fakesnow-0.9.2}/README.md +1 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/fakesnow/fakes.py +5 -0
- fakesnow-0.9.2/fakesnow/global_database.py +46 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/fakesnow/info_schema.py +2 -1
- {fakesnow-0.9.1 → fakesnow-0.9.2}/fakesnow/transforms.py +39 -15
- {fakesnow-0.9.1 → fakesnow-0.9.2/fakesnow.egg-info}/PKG-INFO +4 -3
- {fakesnow-0.9.1 → fakesnow-0.9.2}/fakesnow.egg-info/SOURCES.txt +3 -1
- {fakesnow-0.9.1 → fakesnow-0.9.2}/fakesnow.egg-info/requires.txt +2 -2
- {fakesnow-0.9.1 → fakesnow-0.9.2}/pyproject.toml +3 -3
- {fakesnow-0.9.1 → fakesnow-0.9.2}/tests/test_transforms.py +1 -1
- fakesnow-0.9.2/tests/test_users.py +19 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/LICENSE +0 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/MANIFEST.in +0 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/fakesnow/__init__.py +0 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/fakesnow/__main__.py +0 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/fakesnow/checks.py +0 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/fakesnow/cli.py +0 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/fakesnow/expr.py +0 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/fakesnow/fixtures.py +0 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/fakesnow/macros.py +0 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/fakesnow/py.typed +0 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/fakesnow.egg-info/dependency_links.txt +0 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/fakesnow.egg-info/entry_points.txt +0 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/fakesnow.egg-info/top_level.txt +0 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/setup.cfg +0 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/tests/test_checks.py +0 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/tests/test_cli.py +0 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/tests/test_expr.py +0 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/tests/test_fakes.py +0 -0
- {fakesnow-0.9.1 → fakesnow-0.9.2}/tests/test_patch.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fakesnow
|
3
|
-
Version: 0.9.
|
3
|
+
Version: 0.9.2
|
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
|
@@ -210,10 +210,10 @@ Classifier: License :: OSI Approved :: MIT License
|
|
210
210
|
Requires-Python: >=3.9
|
211
211
|
Description-Content-Type: text/markdown
|
212
212
|
License-File: LICENSE
|
213
|
-
Requires-Dist: duckdb~=0.
|
213
|
+
Requires-Dist: duckdb~=0.10.0
|
214
214
|
Requires-Dist: pyarrow
|
215
215
|
Requires-Dist: snowflake-connector-python
|
216
|
-
Requires-Dist: sqlglot~=21.0
|
216
|
+
Requires-Dist: sqlglot~=21.1.0
|
217
217
|
Provides-Extra: dev
|
218
218
|
Requires-Dist: black~=23.9; extra == "dev"
|
219
219
|
Requires-Dist: build~=1.0; extra == "dev"
|
@@ -349,6 +349,7 @@ Partial support
|
|
349
349
|
- [x] regex functions
|
350
350
|
- [x] semi-structured data
|
351
351
|
- [x] tags
|
352
|
+
- [x] user management (See [tests/test_users.py](tests/test_users.py))
|
352
353
|
|
353
354
|
For more detail see [tests/test_fakes.py](tests/test_fakes.py)
|
354
355
|
|
@@ -30,6 +30,7 @@ import fakesnow.expr as expr
|
|
30
30
|
import fakesnow.info_schema as info_schema
|
31
31
|
import fakesnow.macros as macros
|
32
32
|
import fakesnow.transforms as transforms
|
33
|
+
from fakesnow.global_database import create_global_database
|
33
34
|
|
34
35
|
SCHEMA_UNSET = "schema_unset"
|
35
36
|
SQL_SUCCESS = "SELECT 'Statement executed successfully.' as 'status'"
|
@@ -195,6 +196,8 @@ class FakeSnowflakeCursor:
|
|
195
196
|
.transform(transforms.identifier)
|
196
197
|
.transform(lambda e: transforms.show_schemas(e, self._conn.database))
|
197
198
|
.transform(lambda e: transforms.show_objects_tables(e, self._conn.database))
|
199
|
+
.transform(transforms.show_users)
|
200
|
+
.transform(transforms.create_user)
|
198
201
|
)
|
199
202
|
sql = transformed.sql(dialect="duckdb")
|
200
203
|
result_sql = None
|
@@ -485,6 +488,8 @@ class FakeSnowflakeConnection:
|
|
485
488
|
self.db_path = db_path
|
486
489
|
self._paramstyle = "pyformat"
|
487
490
|
|
491
|
+
create_global_database(duck_conn)
|
492
|
+
|
488
493
|
# create database if needed
|
489
494
|
if (
|
490
495
|
create_database
|
@@ -0,0 +1,46 @@
|
|
1
|
+
from duckdb import DuckDBPyConnection
|
2
|
+
|
3
|
+
GLOBAL_DATABASE_NAME = "_fs_global"
|
4
|
+
USERS_TABLE_FQ_NAME = f"{GLOBAL_DATABASE_NAME}._fs_users_ext"
|
5
|
+
|
6
|
+
# replicates the output structure of https://docs.snowflake.com/en/sql-reference/sql/show-users
|
7
|
+
SQL_CREATE_INFORMATION_SCHEMA_USERS_TABLE_EXT = f"""
|
8
|
+
create table if not exists {USERS_TABLE_FQ_NAME} (
|
9
|
+
name varchar,
|
10
|
+
created_on TIMESTAMPTZ,
|
11
|
+
login_name varchar,
|
12
|
+
display_name varchar,
|
13
|
+
first_name varchar,
|
14
|
+
last_name varchar,
|
15
|
+
email varchar,
|
16
|
+
mins_to_unlock varchar,
|
17
|
+
days_to_expiry varchar,
|
18
|
+
comment varchar,
|
19
|
+
disabled varchar,
|
20
|
+
must_change_password varchar,
|
21
|
+
snowflake_lock varchar,
|
22
|
+
default_warehouse varchar,
|
23
|
+
default_namespace varchar,
|
24
|
+
default_role varchar,
|
25
|
+
default_secondary_roles varchar,
|
26
|
+
ext_authn_duo varchar,
|
27
|
+
ext_authn_uid varchar,
|
28
|
+
mins_to_bypass_mfa varchar,
|
29
|
+
owner varchar,
|
30
|
+
last_success_login TIMESTAMPTZ,
|
31
|
+
expires_at_time TIMESTAMPTZ,
|
32
|
+
locked_until_time TIMESTAMPTZ,
|
33
|
+
has_password varchar,
|
34
|
+
has_rsa_public_key varchar,
|
35
|
+
)
|
36
|
+
"""
|
37
|
+
|
38
|
+
|
39
|
+
def create_global_database(conn: DuckDBPyConnection) -> None:
|
40
|
+
"""Create a "global" database for storing objects which span database.
|
41
|
+
|
42
|
+
Including (but not limited to):
|
43
|
+
- Users
|
44
|
+
"""
|
45
|
+
conn.execute(f"ATTACH IF NOT EXISTS ':memory:' AS {GLOBAL_DATABASE_NAME}")
|
46
|
+
conn.execute(SQL_CREATE_INFORMATION_SCHEMA_USERS_TABLE_EXT)
|
@@ -74,7 +74,8 @@ select
|
|
74
74
|
1 as retention_time,
|
75
75
|
'STANDARD' as type
|
76
76
|
from information_schema.schemata
|
77
|
-
where catalog_name not in ('memory', 'system', 'temp'
|
77
|
+
where catalog_name not in ('memory', 'system', 'temp', '_fs_global')
|
78
|
+
and schema_name = 'information_schema'
|
78
79
|
"""
|
79
80
|
)
|
80
81
|
|
@@ -7,6 +7,8 @@ from typing import cast
|
|
7
7
|
import sqlglot
|
8
8
|
from sqlglot import exp
|
9
9
|
|
10
|
+
from fakesnow.global_database import USERS_TABLE_FQ_NAME
|
11
|
+
|
10
12
|
MISSING_DATABASE = "missing_database"
|
11
13
|
SUCCESS_NOP = sqlglot.parse_one("SELECT 'Statement executed successfully.'")
|
12
14
|
|
@@ -271,22 +273,15 @@ def flatten(expression: exp.Expression) -> exp.Expression:
|
|
271
273
|
return exp.Lateral(
|
272
274
|
this=exp.Unnest(
|
273
275
|
expressions=[
|
274
|
-
exp.
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
to=exp.DataType(
|
282
|
-
this=exp.DataType.Type.ARRAY,
|
283
|
-
expressions=[exp.DataType(this=exp.DataType.Type.JSON, nested=False, prefix=False)],
|
284
|
-
nested=True,
|
285
|
-
),
|
286
|
-
)
|
287
|
-
],
|
276
|
+
exp.Cast(
|
277
|
+
this=explode_expression,
|
278
|
+
to=exp.DataType(
|
279
|
+
this=exp.DataType.Type.ARRAY,
|
280
|
+
expressions=[exp.DataType(this=exp.DataType.Type.JSON, nested=False, prefix=False)],
|
281
|
+
nested=True,
|
282
|
+
),
|
288
283
|
)
|
289
|
-
]
|
284
|
+
],
|
290
285
|
),
|
291
286
|
alias=exp.TableAlias(this=alias.this, columns=[exp.Identifier(this="VALUE", quoted=False)]),
|
292
287
|
)
|
@@ -963,3 +958,32 @@ def values_columns(expression: exp.Expression) -> exp.Expression:
|
|
963
958
|
expression.set("alias", exp.TableAlias(this=exp.Identifier(this="_", quoted=False), columns=columns))
|
964
959
|
|
965
960
|
return expression
|
961
|
+
|
962
|
+
|
963
|
+
def show_users(expression: exp.Expression) -> exp.Expression:
|
964
|
+
"""Transform SHOW USERS to a query against the global database's information_schema._fs_users table.
|
965
|
+
|
966
|
+
https://docs.snowflake.com/en/sql-reference/sql/show-users
|
967
|
+
"""
|
968
|
+
if isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "USERS":
|
969
|
+
return sqlglot.parse_one(f"SELECT * FROM {USERS_TABLE_FQ_NAME}", read="duckdb")
|
970
|
+
|
971
|
+
return expression
|
972
|
+
|
973
|
+
|
974
|
+
def create_user(expression: exp.Expression) -> exp.Expression:
|
975
|
+
"""Transform CREATE USER to a query against the global database's information_schema._fs_users table.
|
976
|
+
|
977
|
+
https://docs.snowflake.com/en/sql-reference/sql/create-user
|
978
|
+
"""
|
979
|
+
# XXX: this is a placeholder. We need to implement the full CREATE USER syntax, but
|
980
|
+
# sqlglot doesnt yet support Create for snowflake.
|
981
|
+
if isinstance(expression, exp.Command) and expression.this == "CREATE":
|
982
|
+
sub_exp = expression.expression.strip()
|
983
|
+
if sub_exp.upper().startswith("USER"):
|
984
|
+
_, name, *ignored = sub_exp.split(" ")
|
985
|
+
if ignored:
|
986
|
+
raise NotImplementedError(f"`CREATE USER` with {ignored} not yet supported")
|
987
|
+
return sqlglot.parse_one(f"INSERT INTO {USERS_TABLE_FQ_NAME} (name) VALUES ('{name}')", read="duckdb")
|
988
|
+
|
989
|
+
return expression
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fakesnow
|
3
|
-
Version: 0.9.
|
3
|
+
Version: 0.9.2
|
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
|
@@ -210,10 +210,10 @@ Classifier: License :: OSI Approved :: MIT License
|
|
210
210
|
Requires-Python: >=3.9
|
211
211
|
Description-Content-Type: text/markdown
|
212
212
|
License-File: LICENSE
|
213
|
-
Requires-Dist: duckdb~=0.
|
213
|
+
Requires-Dist: duckdb~=0.10.0
|
214
214
|
Requires-Dist: pyarrow
|
215
215
|
Requires-Dist: snowflake-connector-python
|
216
|
-
Requires-Dist: sqlglot~=21.0
|
216
|
+
Requires-Dist: sqlglot~=21.1.0
|
217
217
|
Provides-Extra: dev
|
218
218
|
Requires-Dist: black~=23.9; extra == "dev"
|
219
219
|
Requires-Dist: build~=1.0; extra == "dev"
|
@@ -349,6 +349,7 @@ Partial support
|
|
349
349
|
- [x] regex functions
|
350
350
|
- [x] semi-structured data
|
351
351
|
- [x] tags
|
352
|
+
- [x] user management (See [tests/test_users.py](tests/test_users.py))
|
352
353
|
|
353
354
|
For more detail see [tests/test_fakes.py](tests/test_fakes.py)
|
354
355
|
|
@@ -9,6 +9,7 @@ fakesnow/cli.py
|
|
9
9
|
fakesnow/expr.py
|
10
10
|
fakesnow/fakes.py
|
11
11
|
fakesnow/fixtures.py
|
12
|
+
fakesnow/global_database.py
|
12
13
|
fakesnow/info_schema.py
|
13
14
|
fakesnow/macros.py
|
14
15
|
fakesnow/py.typed
|
@@ -24,4 +25,5 @@ tests/test_cli.py
|
|
24
25
|
tests/test_expr.py
|
25
26
|
tests/test_fakes.py
|
26
27
|
tests/test_patch.py
|
27
|
-
tests/test_transforms.py
|
28
|
+
tests/test_transforms.py
|
29
|
+
tests/test_users.py
|
@@ -1,17 +1,17 @@
|
|
1
1
|
[project]
|
2
2
|
name = "fakesnow"
|
3
3
|
description = "Fake Snowflake Connector for Python. Run, mock and test Snowflake DB locally."
|
4
|
-
version = "0.9.
|
4
|
+
version = "0.9.2"
|
5
5
|
readme = "README.md"
|
6
6
|
license = { file = "LICENSE" }
|
7
7
|
classifiers = ["License :: OSI Approved :: MIT License"]
|
8
8
|
keywords = ["snowflake", "snowflakedb", "fake", "local", "mock", "testing"]
|
9
9
|
requires-python = ">=3.9"
|
10
10
|
dependencies = [
|
11
|
-
"duckdb~=0.
|
11
|
+
"duckdb~=0.10.0",
|
12
12
|
"pyarrow",
|
13
13
|
"snowflake-connector-python",
|
14
|
-
"sqlglot~=21.0
|
14
|
+
"sqlglot~=21.1.0",
|
15
15
|
]
|
16
16
|
|
17
17
|
[project.urls]
|
@@ -140,7 +140,7 @@ def test_flatten() -> None:
|
|
140
140
|
)
|
141
141
|
.transform(flatten)
|
142
142
|
.sql(dialect="duckdb")
|
143
|
-
== """SELECT t.id, flat.value -> '$.fruit' FROM (SELECT 1, JSON('[{"fruit":"banana"}]') UNION SELECT 2, JSON('[{"fruit":"coconut"}, {"fruit":"durian"}]')) AS t(id, fruits), LATERAL UNNEST(
|
143
|
+
== """SELECT t.id, flat.value -> '$.fruit' FROM (SELECT 1, JSON('[{"fruit":"banana"}]') UNION SELECT 2, JSON('[{"fruit":"coconut"}, {"fruit":"durian"}]')) AS t(id, fruits), LATERAL UNNEST(CAST(t.fruits AS JSON[])) AS flat(VALUE)""" # noqa: E501
|
144
144
|
)
|
145
145
|
|
146
146
|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import snowflake.connector.cursor
|
2
|
+
|
3
|
+
|
4
|
+
def test_show_users_base_case(cur: snowflake.connector.cursor.SnowflakeCursor):
|
5
|
+
result = cur.execute("SHOW USERS")
|
6
|
+
assert result
|
7
|
+
assert result.fetchall() == []
|
8
|
+
|
9
|
+
|
10
|
+
def test_show_users_with_users(cur: snowflake.connector.cursor.SnowflakeCursor):
|
11
|
+
result = cur.execute("CREATE USER foo")
|
12
|
+
result = cur.execute("CREATE USER bar")
|
13
|
+
|
14
|
+
result = cur.execute("SHOW USERS")
|
15
|
+
assert result
|
16
|
+
|
17
|
+
rows = result.fetchall()
|
18
|
+
names = [row[0] for row in rows]
|
19
|
+
assert names == ["foo", "bar"]
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|