fakesnow 0.8.0__py3-none-any.whl → 0.8.1__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/__main__.py +4 -0
- fakesnow/cli.py +28 -0
- fakesnow/fakes.py +77 -11
- fakesnow/info_schema.py +2 -2
- fakesnow/transforms.py +66 -5
- {fakesnow-0.8.0.dist-info → fakesnow-0.8.1.dist-info}/METADATA +24 -3
- fakesnow-0.8.1.dist-info/RECORD +16 -0
- fakesnow-0.8.1.dist-info/entry_points.txt +2 -0
- {fakesnow-0.8.0.dist-info → fakesnow-0.8.1.dist-info}/top_level.txt +1 -0
- fakesnow-0.8.0.dist-info/RECORD +0 -13
- {fakesnow-0.8.0.dist-info → fakesnow-0.8.1.dist-info}/LICENSE +0 -0
- {fakesnow-0.8.0.dist-info → fakesnow-0.8.1.dist-info}/WHEEL +0 -0
fakesnow/__main__.py
ADDED
fakesnow/cli.py
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
import runpy
|
2
|
+
import sys
|
3
|
+
from collections.abc import Sequence
|
4
|
+
|
5
|
+
import fakesnow
|
6
|
+
|
7
|
+
USAGE = "Usage: fakesnow <path> | -m <module> [<arg>]..."
|
8
|
+
|
9
|
+
|
10
|
+
def main(args: Sequence[str] = sys.argv) -> int:
|
11
|
+
if len(args) < 2 or (len(args) == 2 and args[1] == "-m"):
|
12
|
+
print(USAGE, file=sys.stderr)
|
13
|
+
return 42
|
14
|
+
|
15
|
+
with fakesnow.patch():
|
16
|
+
if args[1] == "-m":
|
17
|
+
module = args[2]
|
18
|
+
sys.argv = args[2:]
|
19
|
+
|
20
|
+
# add current directory to path to mimic python -m
|
21
|
+
sys.path.insert(0, "")
|
22
|
+
runpy.run_module(module, run_name="__main__", alter_sys=True)
|
23
|
+
else:
|
24
|
+
path = args[1]
|
25
|
+
sys.argv = args[1:]
|
26
|
+
runpy.run_path(path, run_name="__main__")
|
27
|
+
|
28
|
+
return 0
|
fakesnow/fakes.py
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import os
|
3
4
|
import re
|
5
|
+
import sys
|
4
6
|
from collections.abc import Iterable, Iterator, Sequence
|
7
|
+
from string import Template
|
5
8
|
from types import TracebackType
|
6
|
-
from typing import TYPE_CHECKING, Any, Literal, Optional,
|
9
|
+
from typing import TYPE_CHECKING, Any, Literal, Optional, cast
|
7
10
|
|
8
11
|
import duckdb
|
9
12
|
|
@@ -26,6 +29,12 @@ import fakesnow.info_schema as info_schema
|
|
26
29
|
import fakesnow.transforms as transforms
|
27
30
|
|
28
31
|
SCHEMA_UNSET = "schema_unset"
|
32
|
+
SUCCESS_SQL = "SELECT 'Statement executed successfully.' as status"
|
33
|
+
DATABASE_CREATED_SQL = Template("SELECT 'Database ${name} successfully created.' as status")
|
34
|
+
TABLE_CREATED_SQL = Template("SELECT 'Table ${name} successfully created.' as status")
|
35
|
+
DROPPED_SQL = Template("SELECT '${name} successfully dropped.' as status")
|
36
|
+
SCHEMA_CREATED_SQL = Template("SELECT 'Schema ${name} successfully created.' as status")
|
37
|
+
INSERTED_SQL = Template("SELECT ${count} as 'number of rows inserted'")
|
29
38
|
|
30
39
|
|
31
40
|
class FakeSnowflakeCursor:
|
@@ -49,6 +58,7 @@ class FakeSnowflakeCursor:
|
|
49
58
|
self._last_sql = None
|
50
59
|
self._last_params = None
|
51
60
|
self._sqlstate = None
|
61
|
+
self._arraysize = 1
|
52
62
|
self._converter = snowflake.connector.converter.SnowflakeConverter()
|
53
63
|
|
54
64
|
def __enter__(self) -> Self:
|
@@ -62,6 +72,19 @@ class FakeSnowflakeCursor:
|
|
62
72
|
) -> bool:
|
63
73
|
return False
|
64
74
|
|
75
|
+
@property
|
76
|
+
def arraysize(self) -> int:
|
77
|
+
return self._arraysize
|
78
|
+
|
79
|
+
@arraysize.setter
|
80
|
+
def arraysize(self, value: int) -> None:
|
81
|
+
self._arraysize = value
|
82
|
+
|
83
|
+
def close(self) -> bool:
|
84
|
+
self._last_sql = None
|
85
|
+
self._last_params = None
|
86
|
+
return True
|
87
|
+
|
65
88
|
def describe(self, command: str, *args: Any, **kwargs: Any) -> list[ResultMetadata]:
|
66
89
|
"""Return the schema of the result without executing the query.
|
67
90
|
|
@@ -79,8 +102,9 @@ class FakeSnowflakeCursor:
|
|
79
102
|
def description(self) -> list[ResultMetadata]:
|
80
103
|
# use a cursor to avoid destroying an unfetched result on the main connection
|
81
104
|
with self._duck_conn.cursor() as cur:
|
82
|
-
|
83
|
-
assert self._conn.
|
105
|
+
# TODO: allow sql alchemy connection with no database or schema
|
106
|
+
assert self._conn.database, ".description not implemented when database is None"
|
107
|
+
assert self._conn.schema, ".description not implemented when schema is None"
|
84
108
|
|
85
109
|
# match database and schema used on the main connection
|
86
110
|
cur.execute(f"SET SCHEMA = '{self._conn.database}.{self._conn.schema}'")
|
@@ -145,6 +169,8 @@ class FakeSnowflakeCursor:
|
|
145
169
|
.transform(transforms.parse_json)
|
146
170
|
# indices_to_json_extract must be before regex_substr
|
147
171
|
.transform(transforms.indices_to_json_extract)
|
172
|
+
.transform(transforms.json_extract_as_varchar)
|
173
|
+
.transform(transforms.flatten)
|
148
174
|
.transform(transforms.regex_replace)
|
149
175
|
.transform(transforms.regex_substr)
|
150
176
|
.transform(transforms.values_columns)
|
@@ -162,7 +188,8 @@ class FakeSnowflakeCursor:
|
|
162
188
|
try:
|
163
189
|
self._last_sql = sql
|
164
190
|
self._last_params = params
|
165
|
-
|
191
|
+
if os.environ.get("FAKESNOW_DEBUG"):
|
192
|
+
print(f"{sql};", file=sys.stderr)
|
166
193
|
self._duck_conn.execute(sql, params)
|
167
194
|
except duckdb.BinderException as e:
|
168
195
|
msg = e.args[0]
|
@@ -171,6 +198,15 @@ class FakeSnowflakeCursor:
|
|
171
198
|
# minimal processing to make it look like a snowflake exception, message content may differ
|
172
199
|
msg = cast(str, e.args[0]).split("\n")[0]
|
173
200
|
raise snowflake.connector.errors.ProgrammingError(msg=msg, errno=2003, sqlstate="42S02") from None
|
201
|
+
except duckdb.TransactionException as e:
|
202
|
+
if "cannot rollback - no transaction is active" in str(
|
203
|
+
e
|
204
|
+
) or "cannot commit - no transaction is active" in str(e):
|
205
|
+
# snowflake doesn't error on rollback or commit outside a tx
|
206
|
+
self._duck_conn.execute(SUCCESS_SQL)
|
207
|
+
self._last_sql = SUCCESS_SQL
|
208
|
+
else:
|
209
|
+
raise e
|
174
210
|
|
175
211
|
if cmd == "USE DATABASE" and (ident := expression.find(exp.Identifier)) and isinstance(ident.this, str):
|
176
212
|
self._conn.database = ident.this.upper()
|
@@ -183,6 +219,33 @@ class FakeSnowflakeCursor:
|
|
183
219
|
if create_db_name := transformed.args.get("create_db_name"):
|
184
220
|
# we created a new database, so create the info schema extensions
|
185
221
|
self._duck_conn.execute(info_schema.creation_sql(create_db_name))
|
222
|
+
created_sql = DATABASE_CREATED_SQL.substitute(name=create_db_name)
|
223
|
+
self._duck_conn.execute(created_sql)
|
224
|
+
self._last_sql = created_sql
|
225
|
+
|
226
|
+
if cmd == "CREATE SCHEMA" and (ident := expression.find(exp.Identifier)) and isinstance(ident.this, str):
|
227
|
+
name = ident.this if ident.quoted else ident.this.upper()
|
228
|
+
created_sql = SCHEMA_CREATED_SQL.substitute(name=name)
|
229
|
+
self._duck_conn.execute(created_sql)
|
230
|
+
self._last_sql = created_sql
|
231
|
+
|
232
|
+
if cmd == "CREATE TABLE" and (ident := expression.find(exp.Identifier)) and isinstance(ident.this, str):
|
233
|
+
name = ident.this if ident.quoted else ident.this.upper()
|
234
|
+
created_sql = TABLE_CREATED_SQL.substitute(name=name)
|
235
|
+
self._duck_conn.execute(created_sql)
|
236
|
+
self._last_sql = created_sql
|
237
|
+
|
238
|
+
if cmd.startswith("DROP") and (ident := expression.find(exp.Identifier)) and isinstance(ident.this, str):
|
239
|
+
name = ident.this if ident.quoted else ident.this.upper()
|
240
|
+
dropped_sql = DROPPED_SQL.substitute(name=name)
|
241
|
+
self._duck_conn.execute(dropped_sql)
|
242
|
+
self._last_sql = dropped_sql
|
243
|
+
|
244
|
+
if cmd == "INSERT":
|
245
|
+
(count,) = self._duck_conn.fetchall()[0]
|
246
|
+
inserted_sql = INSERTED_SQL.substitute(count=count)
|
247
|
+
self._duck_conn.execute(inserted_sql)
|
248
|
+
self._last_sql = inserted_sql
|
186
249
|
|
187
250
|
if table_comment := cast(tuple[exp.Table, str], transformed.args.get("table_comment")):
|
188
251
|
# record table comment
|
@@ -232,19 +295,22 @@ class FakeSnowflakeCursor:
|
|
232
295
|
return self._duck_conn.fetch_df()
|
233
296
|
|
234
297
|
def fetchone(self) -> dict | tuple | None:
|
298
|
+
result = self.fetchmany(1)
|
299
|
+
return result[0] if result else None
|
300
|
+
|
301
|
+
def fetchmany(self, size: int | None = None) -> list[tuple] | list[dict]:
|
302
|
+
# https://peps.python.org/pep-0249/#fetchmany
|
303
|
+
size = size or self._arraysize
|
235
304
|
if not self._use_dict_result:
|
236
|
-
return cast(
|
305
|
+
return cast(list[tuple], self._duck_conn.fetchmany(size))
|
237
306
|
|
238
307
|
if not self._arrow_table:
|
239
308
|
self._arrow_table = self._duck_conn.fetch_arrow_table()
|
240
|
-
self.
|
309
|
+
self._arrow_table_fetch_index = -size
|
241
310
|
|
242
|
-
self.
|
311
|
+
self._arrow_table_fetch_index += size
|
243
312
|
|
244
|
-
|
245
|
-
return self._arrow_table.take([self._arrow_table_fetch_one_index]).to_pylist()[0]
|
246
|
-
except pyarrow.lib.ArrowIndexError:
|
247
|
-
return None
|
313
|
+
return self._arrow_table.slice(offset=self._arrow_table_fetch_index, length=size).to_pylist()
|
248
314
|
|
249
315
|
def get_result_batches(self) -> list[ResultBatch] | None:
|
250
316
|
# rows_per_batch is approximate
|
fakesnow/info_schema.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
from string import Template
|
2
|
-
|
3
1
|
"""Info schema extension tables/views used for storing snowflake metadata not captured by duckdb."""
|
4
2
|
|
3
|
+
from string import Template
|
4
|
+
|
5
5
|
# use ext prefix in columns to disambiguate when joining with information_schema.tables
|
6
6
|
SQL_CREATE_INFORMATION_SCHEMA_TABLES_EXT = Template(
|
7
7
|
"""
|
fakesnow/transforms.py
CHANGED
@@ -155,6 +155,49 @@ def extract_text_length(expression: exp.Expression) -> exp.Expression:
|
|
155
155
|
return expression
|
156
156
|
|
157
157
|
|
158
|
+
def flatten(expression: exp.Expression) -> exp.Expression:
|
159
|
+
"""Flatten an array.
|
160
|
+
|
161
|
+
See https://docs.snowflake.com/en/sql-reference/functions/flatten
|
162
|
+
|
163
|
+
TODO: return index.
|
164
|
+
TODO: support objects.
|
165
|
+
"""
|
166
|
+
if (
|
167
|
+
isinstance(expression, exp.Lateral)
|
168
|
+
and isinstance(expression.this, exp.Explode)
|
169
|
+
and (alias := expression.args.get("alias"))
|
170
|
+
# always true; when no explicit alias provided this will be _flattened
|
171
|
+
and isinstance(alias, exp.TableAlias)
|
172
|
+
):
|
173
|
+
explode_expression = expression.this.this.expression
|
174
|
+
|
175
|
+
return exp.Lateral(
|
176
|
+
this=exp.Unnest(
|
177
|
+
expressions=[
|
178
|
+
exp.Anonymous(
|
179
|
+
# duckdb unnests in reserve, so we reverse the list to match
|
180
|
+
# the order of the original array (and snowflake)
|
181
|
+
this="list_reverse",
|
182
|
+
expressions=[
|
183
|
+
exp.Cast(
|
184
|
+
this=explode_expression,
|
185
|
+
to=exp.DataType(
|
186
|
+
this=exp.DataType.Type.ARRAY,
|
187
|
+
expressions=[exp.DataType(this=exp.DataType.Type.JSON, nested=False, prefix=False)],
|
188
|
+
nested=True,
|
189
|
+
),
|
190
|
+
)
|
191
|
+
],
|
192
|
+
)
|
193
|
+
]
|
194
|
+
),
|
195
|
+
alias=exp.TableAlias(this=alias.this, columns=[exp.Identifier(this="VALUE", quoted=False)]),
|
196
|
+
)
|
197
|
+
|
198
|
+
return expression
|
199
|
+
|
200
|
+
|
158
201
|
def float_to_double(expression: exp.Expression) -> exp.Expression:
|
159
202
|
"""Convert float to double for 64 bit precision.
|
160
203
|
|
@@ -176,10 +219,10 @@ def indices_to_json_extract(expression: exp.Expression) -> exp.Expression:
|
|
176
219
|
and object indices, see
|
177
220
|
https://docs.snowflake.com/en/sql-reference/data-types-semistructured#accessing-elements-of-an-object-by-key
|
178
221
|
|
179
|
-
Duckdb uses the -> operator,
|
222
|
+
Duckdb uses the -> operator, aka the json_extract function, see
|
180
223
|
https://duckdb.org/docs/extensions/json#json-extraction-functions
|
181
224
|
|
182
|
-
This works for Snowflake arrays too because we convert them to JSON
|
225
|
+
This works for Snowflake arrays too because we convert them to JSON in duckdb.
|
183
226
|
"""
|
184
227
|
if (
|
185
228
|
isinstance(expression, exp.Bracket)
|
@@ -259,6 +302,20 @@ def integer_precision(expression: exp.Expression) -> exp.Expression:
|
|
259
302
|
return expression
|
260
303
|
|
261
304
|
|
305
|
+
def json_extract_as_varchar(expression: exp.Expression) -> exp.Expression:
|
306
|
+
"""Return raw unquoted string when casting json extraction to varchar.
|
307
|
+
|
308
|
+
This must run after indices_to_json_extract.
|
309
|
+
|
310
|
+
Duckdb uses the ->> operator, aka the json_extract_string function, see
|
311
|
+
https://duckdb.org/docs/extensions/json#json-extraction-functions
|
312
|
+
"""
|
313
|
+
if isinstance(expression, exp.Cast) and (extract := expression.this) and isinstance(extract, exp.JSONExtract):
|
314
|
+
return exp.JSONExtractScalar(this=extract.this, expression=extract.expression)
|
315
|
+
|
316
|
+
return expression
|
317
|
+
|
318
|
+
|
262
319
|
def object_construct(expression: exp.Expression) -> exp.Expression:
|
263
320
|
"""Convert object_construct to return a json string
|
264
321
|
|
@@ -355,9 +412,13 @@ def regex_substr(expression: exp.Expression) -> exp.Expression:
|
|
355
412
|
|
356
413
|
# which occurrence of the pattern to match
|
357
414
|
try:
|
358
|
-
occurrence = expression.args["occurrence"]
|
415
|
+
occurrence = int(expression.args["occurrence"].this)
|
359
416
|
except KeyError:
|
360
|
-
occurrence =
|
417
|
+
occurrence = 1
|
418
|
+
|
419
|
+
# the duckdb dialect increments bracket (ie: index) expressions by 1 because duckdb is 1-indexed,
|
420
|
+
# so we need to compensate by subtracting 1
|
421
|
+
occurrence = exp.Literal(this=str(occurrence - 1), is_string=False)
|
361
422
|
|
362
423
|
try:
|
363
424
|
regex_parameters_value = str(expression.args["parameters"].this)
|
@@ -428,7 +489,7 @@ def set_schema(expression: exp.Expression, current_database: str | None) -> exp.
|
|
428
489
|
name = f"{expression.this.name}.main"
|
429
490
|
else:
|
430
491
|
# SCHEMA
|
431
|
-
if db := expression.this.args.get("db"):
|
492
|
+
if db := expression.this.args.get("db"): # noqa: SIM108
|
432
493
|
db_name = db.name
|
433
494
|
else:
|
434
495
|
# isn't qualified with a database
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fakesnow
|
3
|
-
Version: 0.8.
|
3
|
+
Version: 0.8.1
|
4
4
|
Summary: Fake Snowflake Connector for Python. Run Snowflake DB locally.
|
5
5
|
License: MIT License
|
6
6
|
|
@@ -32,7 +32,7 @@ License-File: LICENSE
|
|
32
32
|
Requires-Dist: duckdb ~=0.9.2
|
33
33
|
Requires-Dist: pyarrow
|
34
34
|
Requires-Dist: snowflake-connector-python
|
35
|
-
Requires-Dist: sqlglot ~=
|
35
|
+
Requires-Dist: sqlglot ~=20.5.0
|
36
36
|
Provides-Extra: dev
|
37
37
|
Requires-Dist: black ~=23.9 ; extra == 'dev'
|
38
38
|
Requires-Dist: build ~=1.0 ; extra == 'dev'
|
@@ -63,6 +63,24 @@ pip install fakesnow
|
|
63
63
|
|
64
64
|
## Usage
|
65
65
|
|
66
|
+
Run script.py with fakesnow:
|
67
|
+
|
68
|
+
```shell
|
69
|
+
fakesnow script.py
|
70
|
+
```
|
71
|
+
|
72
|
+
Or pytest
|
73
|
+
|
74
|
+
```shell
|
75
|
+
fakesnow -m pytest
|
76
|
+
```
|
77
|
+
|
78
|
+
`fakesnow` executes `fakesnow.patch` before running the script or module.
|
79
|
+
|
80
|
+
### fakesnow.patch
|
81
|
+
|
82
|
+
eg:
|
83
|
+
|
66
84
|
```python
|
67
85
|
import fakesnow
|
68
86
|
import snowflake.connector
|
@@ -91,6 +109,8 @@ with fakesnow.patch("mymodule.write_pandas"):
|
|
91
109
|
...
|
92
110
|
```
|
93
111
|
|
112
|
+
### pytest fixtures
|
113
|
+
|
94
114
|
pytest [fixtures](fakesnow/fixtures.py) are provided for testing. Example _conftest.py_:
|
95
115
|
|
96
116
|
```python
|
@@ -146,7 +166,8 @@ For more detail see [tests/test_fakes.py](tests/test_fakes.py)
|
|
146
166
|
|
147
167
|
## Caveats
|
148
168
|
|
149
|
-
-
|
169
|
+
- The order of rows is non deterministic and may not match Snowflake unless ORDER BY is fully specified.
|
170
|
+
- VARCHAR field sizes are not enforced. Unlike Snowflake which errors with "User character length limit (xxx) exceeded by string" when an inserted string the exceeds the column limit.
|
150
171
|
|
151
172
|
## Contributing
|
152
173
|
|
@@ -0,0 +1,16 @@
|
|
1
|
+
fakesnow/__init__.py,sha256=JXg0HSEHSAOSjEE1ZeA_ogNQ2iFXGEuoibQWk7Pz-WM,3133
|
2
|
+
fakesnow/__main__.py,sha256=GDrGyNTvBFuqn_UfDjKs7b3LPtU6gDv1KwosVDrukIM,76
|
3
|
+
fakesnow/checks.py,sha256=1qVLR0ZB3z3UPij3Hm8hqlkcNLH2QJnwe8OqkoFCwv8,2356
|
4
|
+
fakesnow/cli.py,sha256=KN9kSEXZGNh1orfWsmxFfRiu4fcAqDrgLOh6nTKmhV8,737
|
5
|
+
fakesnow/expr.py,sha256=CAxuYIUkwI339DQIBzvFF0F-m1tcVGKEPA5rDTzmH9A,892
|
6
|
+
fakesnow/fakes.py,sha256=11uyftf-aQoBPfMcpEWQuMwfEsbDJomof6rKovwFaOk,24491
|
7
|
+
fakesnow/fixtures.py,sha256=FfVGhfuIQea0_GQKW8H4ZH0DoxrU2ZnMVJj2eobVbnI,518
|
8
|
+
fakesnow/info_schema.py,sha256=SQfNp0i46GmL82NUm9IaeZH78Xp1CBDJsEio_0AYjFQ,4503
|
9
|
+
fakesnow/py.typed,sha256=B-DLSjYBi7pkKjwxCSdpVj2J02wgfJr-E7B1wOUyxYU,80
|
10
|
+
fakesnow/transforms.py,sha256=4c46iE8Krq7Bx6i78ybFxK0rLtA9m21XEzPCIz65Yqw,25297
|
11
|
+
fakesnow-0.8.1.dist-info/LICENSE,sha256=BL6v_VTnU7xdsocviIQJMFr3stX_-uRfTyByo3gRu4M,1071
|
12
|
+
fakesnow-0.8.1.dist-info/METADATA,sha256=5c7sxmehggVGW5Y7fGbKPQq-F800vUCFjsNwS4Y95Ow,5740
|
13
|
+
fakesnow-0.8.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
14
|
+
fakesnow-0.8.1.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
|
15
|
+
fakesnow-0.8.1.dist-info/top_level.txt,sha256=500evXI1IFX9so82cizGIEMHAb_dJNPaZvd2H9dcKTA,24
|
16
|
+
fakesnow-0.8.1.dist-info/RECORD,,
|
fakesnow-0.8.0.dist-info/RECORD
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
fakesnow/__init__.py,sha256=JXg0HSEHSAOSjEE1ZeA_ogNQ2iFXGEuoibQWk7Pz-WM,3133
|
2
|
-
fakesnow/checks.py,sha256=1qVLR0ZB3z3UPij3Hm8hqlkcNLH2QJnwe8OqkoFCwv8,2356
|
3
|
-
fakesnow/expr.py,sha256=CAxuYIUkwI339DQIBzvFF0F-m1tcVGKEPA5rDTzmH9A,892
|
4
|
-
fakesnow/fakes.py,sha256=qYYtgVNeRSDOMRZYrDo4lS3YB1vTmpAPiVQaeILUdAs,21352
|
5
|
-
fakesnow/fixtures.py,sha256=FfVGhfuIQea0_GQKW8H4ZH0DoxrU2ZnMVJj2eobVbnI,518
|
6
|
-
fakesnow/info_schema.py,sha256=maLS3k10ed_NUYVDeSaL9aXDuVGgQSX8mCYzTqMiW0U,4503
|
7
|
-
fakesnow/py.typed,sha256=B-DLSjYBi7pkKjwxCSdpVj2J02wgfJr-E7B1wOUyxYU,80
|
8
|
-
fakesnow/transforms.py,sha256=TIZa6uLbqTQ66j-xXzr7T475RTJLLyrTcVUvB5dgL1A,22894
|
9
|
-
fakesnow-0.8.0.dist-info/LICENSE,sha256=BL6v_VTnU7xdsocviIQJMFr3stX_-uRfTyByo3gRu4M,1071
|
10
|
-
fakesnow-0.8.0.dist-info/METADATA,sha256=pqyCHOXBSQ4O_I2QtxoGYEbiVfmesGj7V0nonJXt_LY,5418
|
11
|
-
fakesnow-0.8.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
12
|
-
fakesnow-0.8.0.dist-info/top_level.txt,sha256=x8S-sMmvfgNm2_1w0zlIF5YlDs2hR7eNQdVA6TgmPZE,14
|
13
|
-
fakesnow-0.8.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|