fakesnow 0.7.1__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 ADDED
@@ -0,0 +1,4 @@
1
+ if __name__ == "__main__":
2
+ import fakesnow.cli
3
+
4
+ fakesnow.cli.main()
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, Union, cast
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
- assert self._conn.database, "Not implemented when database is None"
83
- assert self._conn.schema, "Not implemented when schema is None"
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
- # print(f"{sql};")
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(Union[tuple, None], self._duck_conn.fetchone())
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._arrow_table_fetch_one_index = -1
309
+ self._arrow_table_fetch_index = -size
241
310
 
242
- self._arrow_table_fetch_one_index += 1
311
+ self._arrow_table_fetch_index += size
243
312
 
244
- try:
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
@@ -322,13 +388,10 @@ class FakeSnowflakeCursor:
322
388
  return ResultMetadata(
323
389
  name=column_name, type_code=12, display_size=None, internal_size=None, precision=0, scale=9, is_nullable=True # noqa: E501
324
390
  )
325
- elif column_type == "JSON[]":
326
- return ResultMetadata(
327
- name=column_name, type_code=10, display_size=None, internal_size=None, precision=None, scale=None, is_nullable=True # noqa: E501
328
- )
329
391
  elif column_type == "JSON":
392
+ # TODO: correctly map OBJECT and ARRAY see https://github.com/tekumara/fakesnow/issues/26
330
393
  return ResultMetadata(
331
- name=column_name, type_code=9, display_size=None, internal_size=None, precision=None, scale=None, is_nullable=True # noqa: E501
394
+ name=column_name, type_code=5, display_size=None, internal_size=None, precision=None, scale=None, is_nullable=True # noqa: E501
332
395
  )
333
396
  else:
334
397
  # TODO handle more types
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
  """
@@ -40,8 +40,7 @@ case when starts_with(data_type, 'DECIMAL') or data_type='BIGINT' then 'NUMBER'
40
40
  when data_type='DOUBLE' then 'FLOAT'
41
41
  when data_type='BLOB' then 'BINARY'
42
42
  when data_type='TIMESTAMP' then 'TIMESTAMP_NTZ'
43
- when data_type='JSON[]' then 'ARRAY'
44
- when data_type='JSON' then 'OBJECT'
43
+ when data_type='JSON' then 'VARIANT'
45
44
  else data_type end as data_type,
46
45
  ext_character_maximum_length as character_maximum_length, ext_character_octet_length as character_octet_length,
47
46
  case when data_type='BIGINT' then 38
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, or the json_extract function, see
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[] in duckdb.
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 = exp.Literal(this="1", is_string=False)
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
@@ -578,15 +639,14 @@ def semi_structured_types(expression: exp.Expression) -> exp.Expression:
578
639
  exp.Expression: The transformed expression.
579
640
  """
580
641
 
581
- if isinstance(expression, exp.DataType):
582
- if expression.this in [exp.DataType.Type.OBJECT, exp.DataType.Type.VARIANT]:
583
- new = expression.copy()
584
- new.args["this"] = exp.DataType.Type.JSON
585
- return new
586
- elif expression.this == exp.DataType.Type.ARRAY:
587
- new = expression.copy()
588
- new.set("expressions", [exp.DataType(this=exp.DataType.Type.JSON)])
589
- return new
642
+ if isinstance(expression, exp.DataType) and expression.this in [
643
+ exp.DataType.Type.ARRAY,
644
+ exp.DataType.Type.OBJECT,
645
+ exp.DataType.Type.VARIANT,
646
+ ]:
647
+ new = expression.copy()
648
+ new.args["this"] = exp.DataType.Type.JSON
649
+ return new
590
650
 
591
651
  return expression
592
652
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fakesnow
3
- Version: 0.7.1
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 ~=19.5.1
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
- - VARCHAR field sizes are not enforced unlike Snowflake which will error with "User character length limit (xxx) exceeded by string" when you try to insert a string longer than the column limit.
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,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fakesnow = fakesnow.cli:main
@@ -1,2 +1,3 @@
1
1
  dist
2
2
  fakesnow
3
+ notebooks
@@ -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=SMyZ23KV4qHGkb-RVnWi-5m6IrHGJWETT5RtmRN8er0,21508
5
- fakesnow/fixtures.py,sha256=FfVGhfuIQea0_GQKW8H4ZH0DoxrU2ZnMVJj2eobVbnI,518
6
- fakesnow/info_schema.py,sha256=3lkpRI_ByXbA1PwZ3hh4PtB9aLAsRbkI4MM_hTaOce8,4544
7
- fakesnow/py.typed,sha256=B-DLSjYBi7pkKjwxCSdpVj2J02wgfJr-E7B1wOUyxYU,80
8
- fakesnow/transforms.py,sha256=koYkt55qGMJhi8gujqrBKO-nePcBMp4zrTk80Uw_JrE,23054
9
- fakesnow-0.7.1.dist-info/LICENSE,sha256=BL6v_VTnU7xdsocviIQJMFr3stX_-uRfTyByo3gRu4M,1071
10
- fakesnow-0.7.1.dist-info/METADATA,sha256=5vcf4KPwcNwl7xJ-5fBQmHCxHOatcaTZgFDDPFyUY6E,5418
11
- fakesnow-0.7.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
12
- fakesnow-0.7.1.dist-info/top_level.txt,sha256=x8S-sMmvfgNm2_1w0zlIF5YlDs2hR7eNQdVA6TgmPZE,14
13
- fakesnow-0.7.1.dist-info/RECORD,,