fakesnow 0.9.19__py3-none-any.whl → 0.9.21__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 CHANGED
@@ -8,11 +8,11 @@ import unittest.mock as mock
8
8
  from collections.abc import Iterator, Sequence
9
9
  from contextlib import contextmanager
10
10
 
11
- import duckdb
12
11
  import snowflake.connector
13
12
  import snowflake.connector.pandas_tools
14
13
 
15
14
  import fakesnow.fakes as fakes
15
+ from fakesnow.instance import FakeSnow
16
16
 
17
17
 
18
18
  @contextmanager
@@ -51,19 +51,15 @@ def patch(
51
51
  # won't be able to patch extra targets
52
52
  assert not isinstance(snowflake.connector.connect, mock.MagicMock), "Snowflake connector is already patched"
53
53
 
54
- duck_conn = duckdb.connect(database=":memory:")
54
+ fs = FakeSnow(
55
+ create_database_on_connect=create_database_on_connect,
56
+ create_schema_on_connect=create_schema_on_connect,
57
+ db_path=db_path,
58
+ nop_regexes=nop_regexes,
59
+ )
55
60
 
56
61
  fake_fns = {
57
- # every time we connect, create a new cursor (ie: connection) so we can isolate each connection's
58
- # schema setting, see https://duckdb.org/docs/api/python/overview.html#startup--shutdown
59
- snowflake.connector.connect: lambda **kwargs: fakes.FakeSnowflakeConnection(
60
- duck_conn.cursor(),
61
- create_database=create_database_on_connect,
62
- create_schema=create_schema_on_connect,
63
- db_path=db_path,
64
- nop_regexes=nop_regexes,
65
- **kwargs,
66
- ),
62
+ snowflake.connector.connect: fs.connect,
67
63
  snowflake.connector.pandas_tools.write_pandas: fakes.write_pandas,
68
64
  }
69
65
 
fakesnow/arrow.py ADDED
@@ -0,0 +1,32 @@
1
+ import pyarrow as pa
2
+
3
+
4
+ def with_sf_metadata(schema: pa.Schema) -> pa.Schema:
5
+ # see https://github.com/snowflakedb/snowflake-connector-python/blob/e9393a6/src/snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowTableIterator.cpp#L32
6
+ # and https://github.com/snowflakedb/snowflake-connector-python/blob/e9393a6/src/snowflake/connector/nanoarrow_cpp/ArrowIterator/SnowflakeType.cpp#L10
7
+ fms = []
8
+ for i, t in enumerate(schema.types):
9
+ f = schema.field(i)
10
+
11
+ if isinstance(t, pa.Decimal128Type):
12
+ fm = f.with_metadata({"logicalType": "FIXED", "precision": str(t.precision), "scale": str(t.scale)})
13
+ elif t == pa.string():
14
+ fm = f.with_metadata({"logicalType": "TEXT"})
15
+ else:
16
+ raise NotImplementedError(f"Unsupported Arrow type: {t}")
17
+ fms.append(fm)
18
+ return pa.schema(fms)
19
+
20
+
21
+ def to_ipc(table: pa.Table) -> pa.Buffer:
22
+ batches = table.to_batches()
23
+ if len(batches) != 1:
24
+ raise NotImplementedError(f"{len(batches)} batches")
25
+ batch = batches[0]
26
+
27
+ sink = pa.BufferOutputStream()
28
+
29
+ with pa.ipc.new_stream(sink, with_sf_metadata(batch.schema)) as writer:
30
+ writer.write_batch(batch)
31
+
32
+ return sink.getvalue()
fakesnow/fakes.py CHANGED
@@ -32,7 +32,6 @@ import fakesnow.expr as expr
32
32
  import fakesnow.info_schema as info_schema
33
33
  import fakesnow.macros as macros
34
34
  import fakesnow.transforms as transforms
35
- from fakesnow.global_database import create_global_database
36
35
  from fakesnow.variables import Variables
37
36
 
38
37
  SCHEMA_UNSET = "schema_unset"
@@ -204,6 +203,7 @@ class FakeSnowflakeCursor:
204
203
  .transform(transforms.sha256)
205
204
  .transform(transforms.create_clone)
206
205
  .transform(transforms.alias_in_join)
206
+ .transform(transforms.alter_table_strip_cluster_by)
207
207
  )
208
208
 
209
209
  def _execute(
@@ -523,9 +523,14 @@ class FakeSnowflakeConnection:
523
523
  ):
524
524
  self._duck_conn = duck_conn
525
525
  # upper case database and schema like snowflake unquoted identifiers
526
- # NB: catalog names are not case-sensitive in duckdb but stored as cased in information_schema.schemata
526
+ # so they appear as upper-cased in information_schema
527
+ # catalog and schema names are not actually case-sensitive in duckdb even though
528
+ # they are as cased in information_schema.schemata, so when selecting from
529
+ # information_schema.schemata below we use upper-case to match any existing duckdb
530
+ # catalog or schemas like "information_schema"
527
531
  self.database = database and database.upper()
528
532
  self.schema = schema and schema.upper()
533
+
529
534
  self.database_set = False
530
535
  self.schema_set = False
531
536
  self.db_path = Path(db_path) if db_path else None
@@ -533,15 +538,13 @@ class FakeSnowflakeConnection:
533
538
  self._paramstyle = snowflake.connector.paramstyle
534
539
  self.variables = Variables()
535
540
 
536
- create_global_database(duck_conn)
537
-
538
541
  # create database if needed
539
542
  if (
540
543
  create_database
541
544
  and self.database
542
545
  and not duck_conn.execute(
543
546
  f"""select * from information_schema.schemata
544
- where catalog_name = '{self.database}'"""
547
+ where upper(catalog_name) = '{self.database}'"""
545
548
  ).fetchone()
546
549
  ):
547
550
  db_file = f"{self.db_path/self.database}.db" if self.db_path else ":memory:"
@@ -556,7 +559,7 @@ class FakeSnowflakeConnection:
556
559
  and self.schema
557
560
  and not duck_conn.execute(
558
561
  f"""select * from information_schema.schemata
559
- where catalog_name = '{self.database}' and schema_name = '{self.schema}'"""
562
+ where upper(catalog_name) = '{self.database}' and upper(schema_name) = '{self.schema}'"""
560
563
  ).fetchone()
561
564
  ):
562
565
  duck_conn.execute(f"CREATE SCHEMA {self.database}.{self.schema}")
@@ -567,7 +570,7 @@ class FakeSnowflakeConnection:
567
570
  and self.schema
568
571
  and duck_conn.execute(
569
572
  f"""select * from information_schema.schemata
570
- where catalog_name = '{self.database}' and schema_name = '{self.schema}'"""
573
+ where upper(catalog_name) = '{self.database}' and upper(schema_name) = '{self.schema}'"""
571
574
  ).fetchone()
572
575
  ):
573
576
  duck_conn.execute(f"SET schema='{self.database}.{self.schema}'")
@@ -578,7 +581,7 @@ class FakeSnowflakeConnection:
578
581
  self.database
579
582
  and duck_conn.execute(
580
583
  f"""select * from information_schema.schemata
581
- where catalog_name = '{self.database}'"""
584
+ where upper(catalog_name) = '{self.database}'"""
582
585
  ).fetchone()
583
586
  ):
584
587
  duck_conn.execute(f"SET schema='{self.database}.main'")
@@ -605,6 +608,7 @@ class FakeSnowflakeConnection:
605
608
  self.cursor().execute("COMMIT")
606
609
 
607
610
  def cursor(self, cursor_class: type[SnowflakeCursor] = SnowflakeCursor) -> FakeSnowflakeCursor:
611
+ # TODO: use duck_conn cursor for thread-safety
608
612
  return FakeSnowflakeCursor(conn=self, duck_conn=self._duck_conn, use_dict_result=cursor_class == DictCursor)
609
613
 
610
614
  def execute_string(
fakesnow/info_schema.py CHANGED
@@ -62,8 +62,8 @@ case when columns.data_type='BIGINT' then 10
62
62
  case when columns.data_type='DOUBLE' then NULL else columns.numeric_scale end as numeric_scale,
63
63
  collation_name, is_identity, identity_generation, identity_cycle,
64
64
  ddb_columns.comment as comment,
65
- null as identity_start,
66
- null as identity_increment,
65
+ null::VARCHAR as identity_start,
66
+ null::VARCHAR as identity_increment,
67
67
  from ${catalog}.information_schema.columns columns
68
68
  left join ${catalog}.information_schema._fs_columns_ext ext
69
69
  on ext_table_catalog = columns.table_catalog
@@ -86,7 +86,7 @@ select
86
86
  catalog_name as database_name,
87
87
  'SYSADMIN' as database_owner,
88
88
  'NO' as is_transient,
89
- null as comment,
89
+ null::VARCHAR as comment,
90
90
  to_timestamp(0)::timestamptz as created,
91
91
  to_timestamp(0)::timestamptz as last_altered,
92
92
  1 as retention_time,
@@ -116,7 +116,7 @@ select
116
116
  to_timestamp(0)::timestamptz as last_altered,
117
117
  to_timestamp(0)::timestamptz as last_ddl,
118
118
  'SYSADMIN' as last_ddl_by,
119
- null as comment
119
+ null::VARCHAR as comment
120
120
  from duckdb_views
121
121
  where database_name = '${catalog}'
122
122
  and schema_name != 'information_schema'
fakesnow/instance.py ADDED
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Any
5
+
6
+ import duckdb
7
+
8
+ import fakesnow.fakes as fakes
9
+
10
+ GLOBAL_DATABASE_NAME = "_fs_global"
11
+ USERS_TABLE_FQ_NAME = f"{GLOBAL_DATABASE_NAME}._fs_users_ext"
12
+
13
+ # replicates the output structure of https://docs.snowflake.com/en/sql-reference/sql/show-users
14
+ SQL_CREATE_INFORMATION_SCHEMA_USERS_TABLE_EXT = f"""
15
+ create table if not exists {USERS_TABLE_FQ_NAME} (
16
+ name varchar,
17
+ created_on TIMESTAMPTZ,
18
+ login_name varchar,
19
+ display_name varchar,
20
+ first_name varchar,
21
+ last_name varchar,
22
+ email varchar,
23
+ mins_to_unlock varchar,
24
+ days_to_expiry varchar,
25
+ comment varchar,
26
+ disabled varchar,
27
+ must_change_password varchar,
28
+ snowflake_lock varchar,
29
+ default_warehouse varchar,
30
+ default_namespace varchar,
31
+ default_role varchar,
32
+ default_secondary_roles varchar,
33
+ ext_authn_duo varchar,
34
+ ext_authn_uid varchar,
35
+ mins_to_bypass_mfa varchar,
36
+ owner varchar,
37
+ last_success_login TIMESTAMPTZ,
38
+ expires_at_time TIMESTAMPTZ,
39
+ locked_until_time TIMESTAMPTZ,
40
+ has_password varchar,
41
+ has_rsa_public_key varchar,
42
+ )
43
+ """
44
+
45
+
46
+ def create_global_database(conn: duckdb.DuckDBPyConnection) -> None:
47
+ """Create a "global" database for storing objects which span databases.
48
+
49
+ Including (but not limited to):
50
+ - Users
51
+ """
52
+ conn.execute(f"ATTACH IF NOT EXISTS ':memory:' AS {GLOBAL_DATABASE_NAME}")
53
+ conn.execute(SQL_CREATE_INFORMATION_SCHEMA_USERS_TABLE_EXT)
54
+
55
+
56
+ class FakeSnow:
57
+ def __init__(
58
+ self,
59
+ create_database_on_connect: bool = True,
60
+ create_schema_on_connect: bool = True,
61
+ db_path: str | os.PathLike | None = None,
62
+ nop_regexes: list[str] | None = None,
63
+ ):
64
+ self.create_database_on_connect = create_database_on_connect
65
+ self.create_schema_on_connect = create_schema_on_connect
66
+ self.db_path = db_path
67
+ self.nop_regexes = nop_regexes
68
+
69
+ self.duck_conn = duckdb.connect(database=":memory:")
70
+
71
+ # create a "global" database for storing objects which span databases.
72
+ self.duck_conn.execute(f"ATTACH IF NOT EXISTS ':memory:' AS {GLOBAL_DATABASE_NAME}")
73
+ self.duck_conn.execute(SQL_CREATE_INFORMATION_SCHEMA_USERS_TABLE_EXT)
74
+
75
+ def connect(
76
+ self, database: str | None = None, schema: str | None = None, **kwargs: Any
77
+ ) -> fakes.FakeSnowflakeConnection:
78
+ # every time we connect, create a new cursor (ie: connection) so we can isolate each connection's
79
+ # schema setting see
80
+ # https://github.com/duckdb/duckdb/blob/18254ec/tools/pythonpkg/src/pyconnection.cpp#L1440
81
+ # and to make connections thread-safe see
82
+ # https://duckdb.org/docs/api/python/overview.html#using-connections-in-parallel-python-programs
83
+ return fakes.FakeSnowflakeConnection(
84
+ self.duck_conn.cursor(),
85
+ database,
86
+ schema,
87
+ create_database=self.create_database_on_connect,
88
+ create_schema=self.create_schema_on_connect,
89
+ db_path=self.db_path,
90
+ nop_regexes=self.nop_regexes,
91
+ **kwargs,
92
+ )
fakesnow/server.py ADDED
@@ -0,0 +1,108 @@
1
+ from __future__ import annotations
2
+
3
+ import gzip
4
+ import json
5
+ import secrets
6
+ from base64 import b64encode
7
+ from dataclasses import dataclass
8
+
9
+ from starlette.applications import Starlette
10
+ from starlette.concurrency import run_in_threadpool
11
+ from starlette.requests import Request
12
+ from starlette.responses import JSONResponse
13
+ from starlette.routing import Route
14
+
15
+ from fakesnow.arrow import to_ipc
16
+ from fakesnow.fakes import FakeSnowflakeConnection
17
+ from fakesnow.instance import FakeSnow
18
+
19
+ fs = FakeSnow()
20
+ sessions = {}
21
+
22
+
23
+ @dataclass
24
+ class ServerError(Exception):
25
+ status_code: int
26
+ code: str
27
+ message: str
28
+
29
+
30
+ def login_request(request: Request) -> JSONResponse:
31
+ database = request.query_params.get("databaseName")
32
+ schema = request.query_params.get("schemaName")
33
+ token = secrets.token_urlsafe(32)
34
+ sessions[token] = fs.connect(database, schema)
35
+ return JSONResponse({"data": {"token": token}, "success": True})
36
+
37
+
38
+ async def query_request(request: Request) -> JSONResponse:
39
+ try:
40
+ conn = to_conn(request)
41
+
42
+ body = await request.body()
43
+ body_json = json.loads(gzip.decompress(body))
44
+
45
+ sql_text = body_json["sqlText"]
46
+
47
+ # only a single sql statement is sent at a time by the python snowflake connector
48
+ cur = await run_in_threadpool(conn.cursor().execute, sql_text)
49
+
50
+ assert cur._arrow_table, "No result set" # noqa: SLF001
51
+
52
+ batch_bytes = to_ipc(cur._arrow_table) # noqa: SLF001
53
+ rowset_b64 = b64encode(batch_bytes).decode("utf-8")
54
+
55
+ return JSONResponse(
56
+ {
57
+ "data": {
58
+ "rowtype": [
59
+ {
60
+ "name": "'HELLO WORLD'",
61
+ "nullable": False,
62
+ "type": "text",
63
+ "length": 11,
64
+ "scale": None,
65
+ "precision": None,
66
+ }
67
+ ],
68
+ "rowsetBase64": rowset_b64,
69
+ "total": 1,
70
+ "queryResultFormat": "arrow",
71
+ },
72
+ "success": True,
73
+ }
74
+ )
75
+
76
+ except ServerError as e:
77
+ return JSONResponse(
78
+ {"data": None, "code": e.code, "message": e.message, "success": False, "headers": None},
79
+ status_code=e.status_code,
80
+ )
81
+
82
+
83
+ def to_conn(request: Request) -> FakeSnowflakeConnection:
84
+ if not (auth := request.headers.get("Authorization")):
85
+ raise ServerError(status_code=401, code="390103", message="Session token not found in the request data.")
86
+
87
+ token = auth[17:-1]
88
+
89
+ if not (conn := sessions.get(token)):
90
+ raise ServerError(status_code=401, code="390104", message="User must login again to access the service.")
91
+
92
+ return conn
93
+
94
+
95
+ routes = [
96
+ Route(
97
+ "/session/v1/login-request",
98
+ login_request,
99
+ methods=["POST"],
100
+ ),
101
+ Route(
102
+ "/queries/v1/query-request",
103
+ query_request,
104
+ methods=["POST"],
105
+ ),
106
+ ]
107
+
108
+ app = Starlette(debug=True, routes=routes)
fakesnow/transforms.py CHANGED
@@ -7,11 +7,11 @@ from typing import ClassVar, Literal, cast
7
7
  import sqlglot
8
8
  from sqlglot import exp
9
9
 
10
- from fakesnow.global_database import USERS_TABLE_FQ_NAME
10
+ from fakesnow.instance import USERS_TABLE_FQ_NAME
11
11
  from fakesnow.variables import Variables
12
12
 
13
13
  MISSING_DATABASE = "missing_database"
14
- SUCCESS_NOP = sqlglot.parse_one("SELECT 'Statement executed successfully.'")
14
+ SUCCESS_NOP = sqlglot.parse_one("SELECT 'Statement executed successfully.' as status")
15
15
 
16
16
 
17
17
  def alias_in_join(expression: exp.Expression) -> exp.Expression:
@@ -33,6 +33,18 @@ def alias_in_join(expression: exp.Expression) -> exp.Expression:
33
33
  return expression
34
34
 
35
35
 
36
+ def alter_table_strip_cluster_by(expression: exp.Expression) -> exp.Expression:
37
+ """Turn alter table cluster by into a no-op"""
38
+ if (
39
+ isinstance(expression, exp.AlterTable)
40
+ and (actions := expression.args.get("actions"))
41
+ and len(actions) == 1
42
+ and (isinstance(actions[0], exp.Cluster))
43
+ ):
44
+ return SUCCESS_NOP
45
+ return expression
46
+
47
+
36
48
  def array_size(expression: exp.Expression) -> exp.Expression:
37
49
  if isinstance(expression, exp.ArraySize):
38
50
  # case is used to convert 0 to null, because null is returned by duckdb when no case matches
@@ -551,12 +563,13 @@ def information_schema_fs_columns_snowflake(expression: exp.Expression) -> exp.E
551
563
  """
552
564
 
553
565
  if (
554
- isinstance(expression, exp.Select)
555
- and (tbl_exp := expression.find(exp.Table))
556
- and tbl_exp.name.upper() == "COLUMNS"
557
- and tbl_exp.db.upper() == "INFORMATION_SCHEMA"
566
+ isinstance(expression, exp.Table)
567
+ and expression.db
568
+ and expression.db.upper() == "INFORMATION_SCHEMA"
569
+ and expression.name
570
+ and expression.name.upper() == "COLUMNS"
558
571
  ):
559
- tbl_exp.set("this", exp.Identifier(this="_FS_COLUMNS_SNOWFLAKE", quoted=False))
572
+ expression.set("this", exp.Identifier(this="_FS_COLUMNS_SNOWFLAKE", quoted=False))
560
573
 
561
574
  return expression
562
575
 
@@ -1402,6 +1415,11 @@ def show_keys(
1402
1415
 
1403
1416
  if schema:
1404
1417
  statement += f"AND schema_name = '{schema}' "
1418
+ elif scope_kind == "TABLE":
1419
+ if not table:
1420
+ raise ValueError(f"SHOW PRIMARY KEYS with {scope_kind} scope requires a table")
1421
+
1422
+ statement += f"AND table_name = '{table.name}' "
1405
1423
  else:
1406
1424
  raise NotImplementedError(f"SHOW PRIMARY KEYS with {scope_kind} not yet supported")
1407
1425
  return sqlglot.parse_one(statement)
fakesnow/variables.py CHANGED
@@ -5,10 +5,23 @@ from sqlglot import exp
5
5
 
6
6
 
7
7
  # Implements snowflake variables: https://docs.snowflake.com/en/sql-reference/session-variables#using-variables-in-sql
8
+ # [ ] Add support for setting multiple variables in a single statement
8
9
  class Variables:
9
10
  @classmethod
10
11
  def is_variable_modifier(cls, expr: exp.Expression) -> bool:
11
- return isinstance(expr, exp.Set) or cls._is_unset_expression(expr)
12
+ return cls._is_set_expression(expr) or cls._is_unset_expression(expr)
13
+
14
+ @classmethod
15
+ def _is_set_expression(cls, expr: exp.Expression) -> bool:
16
+ if isinstance(expr, exp.Set):
17
+ is_set = not expr.args.get("unset")
18
+ if is_set: # SET varname = value;
19
+ set_expressions = expr.args.get("expressions")
20
+ assert set_expressions, "SET without values in expression(s) is unexpected."
21
+ # Avoids mistakenly setting variables for statements that use SET in a different context.
22
+ # (eg. WHEN MATCHED THEN UPDATE SET x=7)
23
+ return isinstance(set_expressions[0], exp.SetItem)
24
+ return False
12
25
 
13
26
  @classmethod
14
27
  def _is_unset_expression(cls, expr: exp.Expression) -> bool:
@@ -22,11 +35,11 @@ class Variables:
22
35
 
23
36
  def update_variables(self, expr: exp.Expression) -> None:
24
37
  if isinstance(expr, exp.Set):
25
- unset = expr.args.get("unset")
26
- if not unset: # SET varname = value;
27
- unset_expressions = expr.args.get("expressions")
28
- assert unset_expressions, "SET without values in expression(s) is unexpected."
29
- eq = unset_expressions[0].this
38
+ is_set = not expr.args.get("unset")
39
+ if is_set: # SET varname = value;
40
+ set_expressions = expr.args.get("expressions")
41
+ assert set_expressions, "SET without values in expression(s) is unexpected."
42
+ eq = set_expressions[0].this
30
43
  name = eq.this.sql()
31
44
  value = eq.args.get("expression").sql()
32
45
  self._set(name, value)
@@ -49,8 +62,7 @@ class Variables:
49
62
  for name, value in self._variables.items():
50
63
  sql = re.sub(rf"\${name}", value, sql, flags=re.IGNORECASE)
51
64
 
52
- remaining_variables = re.search(r"\$\w+", sql)
53
- if remaining_variables:
65
+ if remaining_variables := re.search(r"(?<!\$)\$\w+", sql):
54
66
  raise snowflake.connector.errors.ProgrammingError(
55
67
  msg=f"Session variable '{remaining_variables.group().upper()}' does not exist"
56
68
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fakesnow
3
- Version: 0.9.19
3
+ Version: 0.9.21
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
@@ -213,20 +213,25 @@ License-File: LICENSE
213
213
  Requires-Dist: duckdb ~=1.0.0
214
214
  Requires-Dist: pyarrow
215
215
  Requires-Dist: snowflake-connector-python
216
- Requires-Dist: sqlglot ~=25.3.0
216
+ Requires-Dist: sqlglot ~=25.5.1
217
217
  Provides-Extra: dev
218
218
  Requires-Dist: build ~=1.0 ; extra == 'dev'
219
219
  Requires-Dist: pandas-stubs ; extra == 'dev'
220
220
  Requires-Dist: snowflake-connector-python[pandas,secure-local-storage] ; extra == 'dev'
221
221
  Requires-Dist: pre-commit ~=3.4 ; extra == 'dev'
222
+ Requires-Dist: pyarrow-stubs ; extra == 'dev'
222
223
  Requires-Dist: pytest ~=8.0 ; extra == 'dev'
223
- Requires-Dist: ruff ~=0.4.2 ; extra == 'dev'
224
+ Requires-Dist: pytest-asyncio ; extra == 'dev'
225
+ Requires-Dist: ruff ~=0.5.1 ; extra == 'dev'
224
226
  Requires-Dist: twine ~=5.0 ; extra == 'dev'
225
227
  Requires-Dist: snowflake-sqlalchemy ~=1.5.0 ; extra == 'dev'
226
228
  Provides-Extra: notebook
227
229
  Requires-Dist: duckdb-engine ; extra == 'notebook'
228
230
  Requires-Dist: ipykernel ; extra == 'notebook'
229
231
  Requires-Dist: jupysql ; extra == 'notebook'
232
+ Provides-Extra: server
233
+ Requires-Dist: starlette ; extra == 'server'
234
+ Requires-Dist: uvicorn ; extra == 'server'
230
235
 
231
236
  # fakesnow ❄️
232
237
 
@@ -0,0 +1,21 @@
1
+ fakesnow/__init__.py,sha256=9tFJJKvowKNW3vfnlmza6hOLN1I52DwChgNc5Ew6CcA,3499
2
+ fakesnow/__main__.py,sha256=GDrGyNTvBFuqn_UfDjKs7b3LPtU6gDv1KwosVDrukIM,76
3
+ fakesnow/arrow.py,sha256=1ypCsf-r2Ven6CuSm-bTLoeq1G31kBD6JnaLvDxpwhU,1218
4
+ fakesnow/checks.py,sha256=-QMvdcrRbhN60rnzxLBJ0IkUBWyLR8gGGKKmCS0w9mA,2383
5
+ fakesnow/cli.py,sha256=9qfI-Ssr6mo8UmIlXkUAOz2z2YPBgDsrEVaZv9FjGFs,2201
6
+ fakesnow/expr.py,sha256=CAxuYIUkwI339DQIBzvFF0F-m1tcVGKEPA5rDTzmH9A,892
7
+ fakesnow/fakes.py,sha256=wLSjKrNI8wxe3MuUAa97jpUHd5vZTzvrlF1-Hf0FC0M,31208
8
+ fakesnow/fixtures.py,sha256=G-NkVeruSQAJ7fvSS2fR2oysUn0Yra1pohHlOvacKEk,455
9
+ fakesnow/info_schema.py,sha256=LjS_-8YXBtCSvkdU5uL0aJdFcZEsBa6o5zf-Q_aV9i0,6302
10
+ fakesnow/instance.py,sha256=3cJvPRuFy19dMKXbtBLl6imzO48pEw8uTYhZyFDuwhk,3133
11
+ fakesnow/macros.py,sha256=pX1YJDnQOkFJSHYUjQ6ErEkYIKvFI6Ncz_au0vv1csA,265
12
+ fakesnow/py.typed,sha256=B-DLSjYBi7pkKjwxCSdpVj2J02wgfJr-E7B1wOUyxYU,80
13
+ fakesnow/server.py,sha256=HwCAZ5AhU4nRbFGIqDBs2rdwoK70dYZDyw1XfE1cHqU,3082
14
+ fakesnow/transforms.py,sha256=GCpczoFdvnffQMvSG59PNiiTVTld9kROnnR5dWNXQvY,53624
15
+ fakesnow/variables.py,sha256=WXyPnkeNwD08gy52yF66CVe2twiYC50tztNfgXV4q1k,3032
16
+ fakesnow-0.9.21.dist-info/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
17
+ fakesnow-0.9.21.dist-info/METADATA,sha256=6EzLZbGBTS0VEYCHdG4sNNZT-w5SiespKcitL_XEtM4,18043
18
+ fakesnow-0.9.21.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
19
+ fakesnow-0.9.21.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
20
+ fakesnow-0.9.21.dist-info/top_level.txt,sha256=500evXI1IFX9so82cizGIEMHAb_dJNPaZvd2H9dcKTA,24
21
+ fakesnow-0.9.21.dist-info/RECORD,,
@@ -1,46 +0,0 @@
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)
@@ -1,19 +0,0 @@
1
- fakesnow/__init__.py,sha256=7040jPjD-bNJDU4Lel42CD_DU47oeu1IsY9Fx_DlslQ,3799
2
- fakesnow/__main__.py,sha256=GDrGyNTvBFuqn_UfDjKs7b3LPtU6gDv1KwosVDrukIM,76
3
- fakesnow/checks.py,sha256=-QMvdcrRbhN60rnzxLBJ0IkUBWyLR8gGGKKmCS0w9mA,2383
4
- fakesnow/cli.py,sha256=9qfI-Ssr6mo8UmIlXkUAOz2z2YPBgDsrEVaZv9FjGFs,2201
5
- fakesnow/expr.py,sha256=CAxuYIUkwI339DQIBzvFF0F-m1tcVGKEPA5rDTzmH9A,892
6
- fakesnow/fakes.py,sha256=cTPjOaFZJbyLOJgh7yGRKxUVXM_MYQbBj9NiK2bkhSs,30881
7
- fakesnow/fixtures.py,sha256=G-NkVeruSQAJ7fvSS2fR2oysUn0Yra1pohHlOvacKEk,455
8
- fakesnow/global_database.py,sha256=WTVIP1VhNvdCeX7TQncX1TRpGQU5rBf5Pbxim40zeSU,1399
9
- fakesnow/info_schema.py,sha256=CdIcGXHEQ_kmEAzdQKvA-PX41LA6wlK-4p1J45qgKYA,6266
10
- fakesnow/macros.py,sha256=pX1YJDnQOkFJSHYUjQ6ErEkYIKvFI6Ncz_au0vv1csA,265
11
- fakesnow/py.typed,sha256=B-DLSjYBi7pkKjwxCSdpVj2J02wgfJr-E7B1wOUyxYU,80
12
- fakesnow/transforms.py,sha256=7L7hWg98wrscsIV2-BrTaEhJHhSc9mtroxy-SJkpCnA,52994
13
- fakesnow/variables.py,sha256=I7kVVlqttJ_XlYK4fZ-WTSJgGVdvq_DLX3GZR6QoxN8,2361
14
- fakesnow-0.9.19.dist-info/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
15
- fakesnow-0.9.19.dist-info/METADATA,sha256=p4tC5MVPyi6Ud33RZaTBHeYwbYu-Xwu7vNvtWafvkHk,17839
16
- fakesnow-0.9.19.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
17
- fakesnow-0.9.19.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
18
- fakesnow-0.9.19.dist-info/top_level.txt,sha256=500evXI1IFX9so82cizGIEMHAb_dJNPaZvd2H9dcKTA,24
19
- fakesnow-0.9.19.dist-info/RECORD,,