localstack-snapshot 0.1.0__tar.gz → 0.2.0__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.
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/PKG-INFO +3 -4
- localstack_snapshot-0.2.0/localstack_snapshot/__init__.py +1 -0
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot/snapshots/prototype.py +22 -6
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot/snapshots/transformer.py +33 -0
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot/snapshots/transformer_utility.py +14 -0
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot.egg-info/PKG-INFO +3 -4
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot.egg-info/requires.txt +1 -2
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/pyproject.toml +2 -3
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/tests/test_snapshots.py +50 -0
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/tests/test_transformer.py +25 -0
- localstack-snapshot-0.1.0/localstack_snapshot/__init__.py +0 -1
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/LICENSE +0 -0
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/README.md +0 -0
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot/pytest/__init__.py +0 -0
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot/pytest/snapshot.py +0 -0
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot/snapshots/__init__.py +0 -0
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot/snapshots/report.py +0 -0
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot/util/__init__.py +0 -0
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot/util/encoding.py +0 -0
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot.egg-info/SOURCES.txt +0 -0
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot.egg-info/dependency_links.txt +0 -0
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot.egg-info/top_level.txt +0 -0
- {localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: localstack-snapshot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Extracted snapshot testing lib for LocalStack
|
|
5
5
|
Author-email: LocalStack Contributors <info@localstack.cloud>
|
|
6
6
|
License:
|
|
@@ -215,9 +215,8 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
215
215
|
Requires-Python: >=3.10
|
|
216
216
|
Description-Content-Type: text/markdown
|
|
217
217
|
License-File: LICENSE
|
|
218
|
-
Requires-Dist: jsonpath-ng
|
|
218
|
+
Requires-Dist: jsonpath-ng>1.6
|
|
219
219
|
Requires-Dist: deepdiff
|
|
220
|
-
Requires-Dist: botocore
|
|
221
220
|
Provides-Extra: dev
|
|
222
221
|
Requires-Dist: black==23.10.0; extra == "dev"
|
|
223
222
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
{localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot/snapshots/prototype.py
RENAMED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import io
|
|
1
2
|
import json
|
|
2
3
|
import logging
|
|
3
4
|
import os
|
|
@@ -7,7 +8,6 @@ from pathlib import Path
|
|
|
7
8
|
from re import Pattern
|
|
8
9
|
from typing import Dict, List, Optional
|
|
9
10
|
|
|
10
|
-
from botocore.response import StreamingBody
|
|
11
11
|
from deepdiff import DeepDiff
|
|
12
12
|
from jsonpath_ng import DatumInContext
|
|
13
13
|
from jsonpath_ng.ext import parse
|
|
@@ -164,6 +164,23 @@ class SnapshotSession:
|
|
|
164
164
|
def _update(self, key: str, obj_state: dict) -> None:
|
|
165
165
|
self.observed_state[key] = obj_state
|
|
166
166
|
|
|
167
|
+
def match_object(self, key: str, obj: object) -> None:
|
|
168
|
+
def _convert_object_to_dict(obj_):
|
|
169
|
+
if isinstance(obj_, dict):
|
|
170
|
+
for key in list(obj_.keys()):
|
|
171
|
+
if key.startswith("_"):
|
|
172
|
+
del obj_[key]
|
|
173
|
+
else:
|
|
174
|
+
obj_[key] = _convert_object_to_dict(obj_[key])
|
|
175
|
+
elif isinstance(obj_, list):
|
|
176
|
+
for idx, val in enumerate(obj_):
|
|
177
|
+
obj_[idx] = _convert_object_to_dict(val)
|
|
178
|
+
elif hasattr(obj_, "__dict__"):
|
|
179
|
+
return _convert_object_to_dict(obj_.__dict__)
|
|
180
|
+
return obj_
|
|
181
|
+
|
|
182
|
+
return self.match(key, _convert_object_to_dict(obj))
|
|
183
|
+
|
|
167
184
|
def match(self, key: str, obj: dict) -> None:
|
|
168
185
|
if key in self.called_keys:
|
|
169
186
|
raise Exception(
|
|
@@ -243,11 +260,10 @@ class SnapshotSession:
|
|
|
243
260
|
def _transform_dict_to_parseable_values(self, original):
|
|
244
261
|
"""recursively goes through dict and tries to resolve values to strings (& parse them as json if possible)"""
|
|
245
262
|
for k, v in original.items():
|
|
246
|
-
if isinstance(v,
|
|
263
|
+
if isinstance(v, io.IOBase):
|
|
247
264
|
# update v for json parsing below
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
) # TODO: patch boto client so this doesn't break any further read() calls
|
|
265
|
+
# TODO: patch boto client so this doesn't break any further read() calls
|
|
266
|
+
original[k] = v = v.read().decode("utf-8")
|
|
251
267
|
if isinstance(v, list) and v:
|
|
252
268
|
for item in v:
|
|
253
269
|
if isinstance(item, dict):
|
|
@@ -339,7 +355,7 @@ class SnapshotSession:
|
|
|
339
355
|
|
|
340
356
|
def build_full_path_nodes(field_match: DatumInContext):
|
|
341
357
|
"""Traverse the matched Datum to build the path field by field"""
|
|
342
|
-
full_path_nodes = [str(field_match.path)]
|
|
358
|
+
full_path_nodes = [str(field_match.path).replace("'", "")]
|
|
343
359
|
next_node = field_match
|
|
344
360
|
while next_node.context is not None:
|
|
345
361
|
full_path_nodes.append(str(next_node.context.path))
|
{localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot/snapshots/transformer.py
RENAMED
|
@@ -16,6 +16,10 @@ SNAPSHOT_LOGGER.setLevel(logging.DEBUG if os.environ.get("DEBUG_SNAPSHOT") else
|
|
|
16
16
|
GlobalReplacementFn = Callable[[str], str]
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
class TransformerException(Exception):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
19
23
|
class TransformContext:
|
|
20
24
|
_cache: dict
|
|
21
25
|
replacements: list[GlobalReplacementFn]
|
|
@@ -43,6 +47,19 @@ class TransformContext:
|
|
|
43
47
|
def _register_serialized_reference_replacement(
|
|
44
48
|
transform_context: TransformContext, *, reference_value: str, replacement: str
|
|
45
49
|
):
|
|
50
|
+
# Provide a better error message for the TypeError if the reference value is not iterable (e.g., float)
|
|
51
|
+
# Example: `TypeError: argument of type 'float' is not iterable`
|
|
52
|
+
# Using a list would throw an AttributeError because `.replace` is not applicable.
|
|
53
|
+
# The snapshot library currently only supports strings for reference replacements
|
|
54
|
+
if not isinstance(reference_value, str):
|
|
55
|
+
message = (
|
|
56
|
+
f"The reference value {reference_value} of type {type(reference_value)} is not a string."
|
|
57
|
+
f" Consider using `reference_replacement=False` in your transformer"
|
|
58
|
+
f" for the replacement {replacement} because reference replacements are only supported for strings."
|
|
59
|
+
)
|
|
60
|
+
SNAPSHOT_LOGGER.error(message)
|
|
61
|
+
raise TransformerException(message)
|
|
62
|
+
|
|
46
63
|
if '"' in reference_value:
|
|
47
64
|
reference_value = reference_value.replace('"', '\\"')
|
|
48
65
|
|
|
@@ -327,3 +344,19 @@ class TimestampTransformer:
|
|
|
327
344
|
|
|
328
345
|
def _transform_list(self, input_data: list, ctx: TransformContext = None) -> list:
|
|
329
346
|
return [self._transform(e, ctx=ctx) for e in input_data]
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class TextTransformer:
|
|
350
|
+
def __init__(self, text: str, replacement: str):
|
|
351
|
+
self.text = text
|
|
352
|
+
self.replacement = replacement
|
|
353
|
+
|
|
354
|
+
def transform(self, input_data: dict, *, ctx: TransformContext) -> dict:
|
|
355
|
+
def replace_val(s):
|
|
356
|
+
return s.replace(self.text, self.replacement)
|
|
357
|
+
|
|
358
|
+
ctx.register_serialized_replacement(replace_val)
|
|
359
|
+
SNAPSHOT_LOGGER.debug(
|
|
360
|
+
f"Registering text pattern '{self.text}' in snapshot with '{self.replacement}'"
|
|
361
|
+
)
|
|
362
|
+
return input_data
|
|
@@ -5,6 +5,7 @@ from localstack_snapshot.snapshots.transformer import (
|
|
|
5
5
|
JsonpathTransformer,
|
|
6
6
|
KeyValueBasedTransformer,
|
|
7
7
|
RegexTransformer,
|
|
8
|
+
TextTransformer,
|
|
8
9
|
)
|
|
9
10
|
|
|
10
11
|
|
|
@@ -67,3 +68,16 @@ class TransformerUtility:
|
|
|
67
68
|
:return: RegexTransformer
|
|
68
69
|
"""
|
|
69
70
|
return RegexTransformer(regex, replacement)
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def text(text: str, replacement: str):
|
|
74
|
+
"""Creates a new TextTransformer. All occurrences in the string-converted dict will be replaced.
|
|
75
|
+
|
|
76
|
+
Useful if the text contains special characters that would confuse the RegexTransformer, like '+' or '('.
|
|
77
|
+
|
|
78
|
+
:param text: the text that should be replaced
|
|
79
|
+
:param replacement: the value which will replace the original value.
|
|
80
|
+
|
|
81
|
+
:return: TextTransformer
|
|
82
|
+
"""
|
|
83
|
+
return TextTransformer(text, replacement)
|
{localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: localstack-snapshot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Extracted snapshot testing lib for LocalStack
|
|
5
5
|
Author-email: LocalStack Contributors <info@localstack.cloud>
|
|
6
6
|
License:
|
|
@@ -215,9 +215,8 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
215
215
|
Requires-Python: >=3.10
|
|
216
216
|
Description-Content-Type: text/markdown
|
|
217
217
|
License-File: LICENSE
|
|
218
|
-
Requires-Dist: jsonpath-ng
|
|
218
|
+
Requires-Dist: jsonpath-ng>1.6
|
|
219
219
|
Requires-Dist: deepdiff
|
|
220
|
-
Requires-Dist: botocore
|
|
221
220
|
Provides-Extra: dev
|
|
222
221
|
Requires-Dist: black==23.10.0; extra == "dev"
|
|
223
222
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
@@ -7,12 +7,11 @@ name = "localstack-snapshot"
|
|
|
7
7
|
authors = [
|
|
8
8
|
{ name = "LocalStack Contributors", email = "info@localstack.cloud" }
|
|
9
9
|
]
|
|
10
|
-
version = "0.
|
|
10
|
+
version = "0.2.0"
|
|
11
11
|
description = "Extracted snapshot testing lib for LocalStack"
|
|
12
12
|
dependencies = [
|
|
13
|
-
"jsonpath-ng
|
|
13
|
+
"jsonpath-ng>1.6",
|
|
14
14
|
"deepdiff",
|
|
15
|
-
"botocore",
|
|
16
15
|
]
|
|
17
16
|
requires-python = ">=3.10"
|
|
18
17
|
license = {file = "LICENSE"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import io
|
|
2
|
+
|
|
1
3
|
import pytest
|
|
2
4
|
|
|
3
5
|
from localstack_snapshot.snapshots import SnapshotSession
|
|
@@ -20,6 +22,12 @@ class TestSnapshotManager:
|
|
|
20
22
|
sm._assert_all()
|
|
21
23
|
ctx.match("Parity snapshot failed")
|
|
22
24
|
|
|
25
|
+
def test_diff_with_io_stream(self):
|
|
26
|
+
sm = SnapshotSession(scope_key="A", verify=True, base_file_path="", update=False)
|
|
27
|
+
sm.recorded_state = {"key_a": {"a": "data"}}
|
|
28
|
+
sm.match("key_a", {"a": io.BytesIO(b"data")})
|
|
29
|
+
sm._assert_all()
|
|
30
|
+
|
|
23
31
|
def test_multiple_assertmatch_with_same_key_fail(self):
|
|
24
32
|
sm = SnapshotSession(scope_key="A", verify=True, base_file_path="", update=False)
|
|
25
33
|
sm.recorded_state = {"key_a": {"a": 3}}
|
|
@@ -37,6 +45,48 @@ class TestSnapshotManager:
|
|
|
37
45
|
sm.match("key_a", {"aaa": "something", "bbb": "something hello"})
|
|
38
46
|
sm._assert_all()
|
|
39
47
|
|
|
48
|
+
def test_match_object_nochange(self):
|
|
49
|
+
class CustomObject:
|
|
50
|
+
def __init__(self, name, nested=False):
|
|
51
|
+
self.name = name
|
|
52
|
+
if nested:
|
|
53
|
+
self.nested = CustomObject(f"nested{name}")
|
|
54
|
+
self.listed = [CustomObject(f"listed{name}"), "otherobj"]
|
|
55
|
+
|
|
56
|
+
sm = SnapshotSession(scope_key="A", verify=True, base_file_path="", update=False)
|
|
57
|
+
sm.recorded_state = {
|
|
58
|
+
"key_a": {
|
|
59
|
+
"name": "myname",
|
|
60
|
+
"nested": {"name": "nestedmyname"},
|
|
61
|
+
"listed": [{"name": "listedmyname"}, "otherobj"],
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
sm.match_object("key_a", CustomObject(name="myname", nested=True))
|
|
65
|
+
sm._assert_all()
|
|
66
|
+
|
|
67
|
+
def test_match_object_ignore_private_values(self):
|
|
68
|
+
class CustomObject:
|
|
69
|
+
def __init__(self, name):
|
|
70
|
+
self.name = name
|
|
71
|
+
self._internal = "n/a"
|
|
72
|
+
|
|
73
|
+
sm = SnapshotSession(scope_key="A", verify=True, base_file_path="", update=False)
|
|
74
|
+
sm.recorded_state = {"key_a": {"name": "myname"}}
|
|
75
|
+
sm.match_object("key_a", CustomObject(name="myname"))
|
|
76
|
+
sm._assert_all()
|
|
77
|
+
|
|
78
|
+
def test_match_object_change(self):
|
|
79
|
+
class CustomObject:
|
|
80
|
+
def __init__(self, name):
|
|
81
|
+
self.name = name
|
|
82
|
+
|
|
83
|
+
sm = SnapshotSession(scope_key="A", verify=True, base_file_path="", update=False)
|
|
84
|
+
sm.recorded_state = {"key_a": {"name": "myname"}}
|
|
85
|
+
sm.match_object("key_a", CustomObject(name="diffname"))
|
|
86
|
+
with pytest.raises(Exception) as ctx:
|
|
87
|
+
sm._assert_all()
|
|
88
|
+
ctx.match("Parity snapshot failed")
|
|
89
|
+
|
|
40
90
|
# def test_context_replacement_no_change(self):
|
|
41
91
|
# sm = SnapshotSession(scope_key="A", verify=True, base_file_path="", update=False)
|
|
42
92
|
# sm.add_transformer(TransformerUtility.key_value("name"))
|
|
@@ -211,6 +211,31 @@ class TestTransformer:
|
|
|
211
211
|
output = transformer.transform(input, ctx=ctx)
|
|
212
212
|
assert output == expected
|
|
213
213
|
|
|
214
|
+
@pytest.mark.parametrize(
|
|
215
|
+
"value",
|
|
216
|
+
[
|
|
217
|
+
"a+b",
|
|
218
|
+
"question?",
|
|
219
|
+
"amount: $4.00",
|
|
220
|
+
"emoji: ^^",
|
|
221
|
+
"sentence.",
|
|
222
|
+
"others (like so)",
|
|
223
|
+
"special {char}",
|
|
224
|
+
],
|
|
225
|
+
)
|
|
226
|
+
def test_text(self, value):
|
|
227
|
+
input = {"key": f"some {value} with more text"}
|
|
228
|
+
|
|
229
|
+
expected = {"key": "some <value> with more text"}
|
|
230
|
+
|
|
231
|
+
transformer = TransformerUtility.text(value, "<value>")
|
|
232
|
+
|
|
233
|
+
ctx = TransformContext()
|
|
234
|
+
output = transformer.transform(json.dumps(input), ctx=ctx)
|
|
235
|
+
for sr in ctx.serialized_replacements:
|
|
236
|
+
output = sr(output)
|
|
237
|
+
assert json.loads(output) == expected
|
|
238
|
+
|
|
214
239
|
|
|
215
240
|
class TestTimestampTransformer:
|
|
216
241
|
def test_generic_timestamp_transformer(self):
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
File without changes
|
|
File without changes
|
{localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot/pytest/__init__.py
RENAMED
|
File without changes
|
{localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot/pytest/snapshot.py
RENAMED
|
File without changes
|
{localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot/snapshots/__init__.py
RENAMED
|
File without changes
|
{localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot/snapshots/report.py
RENAMED
|
File without changes
|
{localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot/util/__init__.py
RENAMED
|
File without changes
|
{localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot/util/encoding.py
RENAMED
|
File without changes
|
{localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{localstack-snapshot-0.1.0 → localstack_snapshot-0.2.0}/localstack_snapshot.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|