persidict 0.15.1__tar.gz → 0.17.1__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.

Potentially problematic release.


This version of persidict might be problematic. Click here for more details.

@@ -1,11 +1,11 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: persidict
3
- Version: 0.15.1
3
+ Version: 0.17.1
4
4
  Summary: Simple persistent key-value store for Python. Values are stored as files on a disk or as S3 objects on AWS cloud.
5
- Home-page: https://github.com/pythagoras-dev/persidict
6
- Author: Vlad (Volodymyr) Pavlov
7
- Author-email: vlpavlov@ieee.org
8
5
  Keywords: persistence,dicts,distributed,parallel
6
+ Author: Vlad (Volodymyr) Pavlov
7
+ Author-email: Vlad (Volodymyr) Pavlov <vlpavlov@ieee.org>
8
+ License: MIT
9
9
  Classifier: Development Status :: 3 - Alpha
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Intended Audience :: Science/Research
@@ -15,18 +15,20 @@ Classifier: License :: OSI Approved :: MIT License
15
15
  Classifier: Operating System :: OS Independent
16
16
  Classifier: Topic :: Software Development :: Libraries
17
17
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
- Requires-Python: >=3.10
19
- Description-Content-Type: text/markdown
20
- License-File: LICENSE
21
18
  Requires-Dist: lz4
22
19
  Requires-Dist: joblib
23
20
  Requires-Dist: numpy
24
21
  Requires-Dist: pandas
25
22
  Requires-Dist: jsonpickle
26
23
  Requires-Dist: boto3
27
- Requires-Dist: moto
28
- Requires-Dist: pytest
29
24
  Requires-Dist: parameterizable
25
+ Requires-Dist: boto3 ; extra == 'dev'
26
+ Requires-Dist: moto ; extra == 'dev'
27
+ Requires-Dist: pytest ; extra == 'dev'
28
+ Requires-Python: >=3.10
29
+ Project-URL: Homepage, https://github.com/pythagoras-dev/persidict
30
+ Provides-Extra: dev
31
+ Description-Content-Type: text/markdown
30
32
 
31
33
  # persidict
32
34
 
@@ -211,4 +213,4 @@ Binary installers for the latest released version are available at the Python pa
211
213
 
212
214
  ## Key Contacts
213
215
 
214
- * [Vlad (Volodymyr) Pavlov](https://www.linkedin.com/in/vlpavlov/)
216
+ * [Vlad (Volodymyr) Pavlov](https://www.linkedin.com/in/vlpavlov/)
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["uv_build"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "persidict"
7
+ version = "0.17.1"
8
+ description = "Simple persistent key-value store for Python. Values are stored as files on a disk or as S3 objects on AWS cloud."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Vlad (Volodymyr) Pavlov", email = "vlpavlov@ieee.org"}
14
+ ]
15
+ keywords = ["persistence", "dicts", "distributed", "parallel"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Intended Audience :: Science/Research",
20
+ "Programming Language :: Python",
21
+ "Programming Language :: Python :: 3",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Operating System :: OS Independent",
24
+ "Topic :: Software Development :: Libraries",
25
+ "Topic :: Software Development :: Libraries :: Python Modules"
26
+ ]
27
+ dependencies = [
28
+ "lz4",
29
+ "joblib",
30
+ "numpy",
31
+ "pandas",
32
+ "jsonpickle",
33
+ "boto3",
34
+ "parameterizable"
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/pythagoras-dev/persidict"
39
+
40
+ [project.optional-dependencies]
41
+ dev = [
42
+ "boto3",
43
+ "moto",
44
+ "pytest"
45
+ ]
@@ -156,7 +156,7 @@ class FileDirDict(PersiDict):
156
156
  """Convert a key into a filesystem path."""
157
157
 
158
158
  key = sign_safe_str_tuple(key, self.digest_len)
159
- key = [self.base_dir] + list(key.str_chain)
159
+ key = [self.base_dir] + list(key.strings)
160
160
  dir_names = key[:-1] if is_file_path else key
161
161
 
162
162
  if create_subdirs:
@@ -23,7 +23,6 @@ from __future__ import annotations
23
23
  from abc import abstractmethod
24
24
  import random
25
25
  from parameterizable import ParameterizableClass
26
- from copy import deepcopy
27
26
  from typing import Any, Sequence, Optional
28
27
  from collections.abc import MutableMapping
29
28
 
@@ -83,7 +83,7 @@ class S3Dict(PersiDict):
83
83
  self.s3_client = boto3.client('s3', region_name=region)
84
84
 
85
85
  try:
86
- self.bucket = self.s3_client.create_bucket(Bucket=bucket_name)
86
+ self.s3_client.create_bucket(Bucket=bucket_name)
87
87
  except:
88
88
  pass
89
89
 
@@ -2,6 +2,7 @@ import string
2
2
  from copy import deepcopy
3
3
 
4
4
  SAFE_CHARS_SET = set(string.ascii_letters + string.digits + "()_-~.=")
5
+ SAFE_STRING_MAX_LENGTH = 254
5
6
 
6
7
  def get_safe_chars() -> set[str]:
7
8
  """Return a set of allowed characters."""
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
  from collections.abc import Sequence, Mapping, Hashable
5
5
  from typing import Any
6
- from persidict.safe_chars import SAFE_CHARS_SET
6
+ from safe_chars import SAFE_CHARS_SET, SAFE_STRING_MAX_LENGTH
7
7
 
8
8
 
9
9
  def _is_sequence_not_mapping(obj:Any) -> bool:
@@ -23,7 +23,7 @@ class SafeStrTuple(Sequence, Hashable):
23
23
  """An immutable sequence of non-emtpy URL/filename-safe strings.
24
24
  """
25
25
 
26
- str_chain: tuple[str, ...]
26
+ strings: tuple[str, ...]
27
27
 
28
28
  def __init__(self, *args, **kwargs):
29
29
  """Create a SafeStrTuple from a sequence/tree of strings.
@@ -36,36 +36,42 @@ class SafeStrTuple(Sequence, Hashable):
36
36
  """
37
37
  assert len(kwargs) == 0
38
38
  assert len(args) > 0
39
- candidate_str_chain = []
39
+ candidate_strings = []
40
40
  for a in args:
41
41
  if isinstance(a, SafeStrTuple):
42
- candidate_str_chain.extend(a.str_chain)
42
+ candidate_strings.extend(a.strings)
43
43
  elif isinstance(a, str):
44
44
  assert len(a) > 0
45
+ assert len(a) < SAFE_STRING_MAX_LENGTH
45
46
  assert len(set(a) - SAFE_CHARS_SET) == 0
46
- candidate_str_chain.append(a)
47
+ candidate_strings.append(a)
47
48
  elif _is_sequence_not_mapping(a):
48
49
  if len(a) > 0:
49
- candidate_str_chain.extend(SafeStrTuple(*a).str_chain)
50
+ candidate_strings.extend(SafeStrTuple(*a).strings)
50
51
  else:
51
52
  assert False, f"Invalid argument type: {type(a)}"
52
- self.str_chain = tuple(candidate_str_chain)
53
+ self.strings = tuple(candidate_strings)
54
+
55
+ @property
56
+ def str_chain(self) -> tuple[str, ...]:
57
+ """for backward compatibility"""
58
+ return self.strings
53
59
 
54
60
  def __getitem__(self, key:int)-> str:
55
61
  """Return a string at position key."""
56
- return self.str_chain[key]
62
+ return self.strings[key]
57
63
 
58
64
  def __len__(self) -> int:
59
65
  """Return the number of strings in the tuple."""
60
- return len(self.str_chain)
66
+ return len(self.strings)
61
67
 
62
68
  def __hash__(self):
63
69
  """Return a hash of the tuple."""
64
- return hash(self.str_chain)
70
+ return hash(self.strings)
65
71
 
66
72
  def __repr__(self) -> str:
67
73
  """Return repr(self)."""
68
- return f"{type(self).__name__}({self.str_chain})"
74
+ return f"{type(self).__name__}({self.strings})"
69
75
 
70
76
 
71
77
  def __eq__(self, other) -> bool:
@@ -76,27 +82,27 @@ class SafeStrTuple(Sequence, Hashable):
76
82
  else:
77
83
  other = SafeStrTuple(other)
78
84
 
79
- return self.str_chain == other.str_chain
85
+ return self.strings == other.strings
80
86
 
81
87
 
82
88
  def __add__(self, other) -> SafeStrTuple:
83
89
  """Return self + other."""
84
90
  other = SafeStrTuple(other)
85
- return SafeStrTuple(*(self.str_chain + other.str_chain))
91
+ return SafeStrTuple(*(self.strings + other.strings))
86
92
 
87
93
  def __radd__(self, other) -> SafeStrTuple:
88
94
  """Return other + self."""
89
95
  other = SafeStrTuple(other)
90
- return SafeStrTuple(*(other.str_chain + self.str_chain))
96
+ return SafeStrTuple(*(other.strings + self.strings))
91
97
 
92
98
  def __iter__(self):
93
99
  """Return iter(self)."""
94
- return iter(self.str_chain)
100
+ return iter(self.strings)
95
101
 
96
102
  def __contains__(self, item) -> bool:
97
103
  """Return item in self."""
98
- return item in self.str_chain
104
+ return item in self.strings
99
105
 
100
106
  def __reversed__(self) -> SafeStrTuple:
101
107
  """Return a reversed SafeStrTuple."""
102
- return SafeStrTuple(*reversed(self.str_chain))
108
+ return SafeStrTuple(*reversed(self.strings))
@@ -11,7 +11,7 @@ e.g. MacOS HFS.
11
11
 
12
12
  import base64
13
13
  import hashlib
14
- from persidict.safe_str_tuple import SafeStrTuple
14
+ from src.persidict.safe_str_tuple import SafeStrTuple
15
15
 
16
16
 
17
17
  def _create_signature_suffix(input_str:str, digest_len:int) -> str:
persidict-0.15.1/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2020-2024 Vlad (Volodymyr) Pavlov, Kai Zhao, Illia Shestakov.
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,214 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: persidict
3
- Version: 0.15.1
4
- Summary: Simple persistent key-value store for Python. Values are stored as files on a disk or as S3 objects on AWS cloud.
5
- Home-page: https://github.com/pythagoras-dev/persidict
6
- Author: Vlad (Volodymyr) Pavlov
7
- Author-email: vlpavlov@ieee.org
8
- Keywords: persistence,dicts,distributed,parallel
9
- Classifier: Development Status :: 3 - Alpha
10
- Classifier: Intended Audience :: Developers
11
- Classifier: Intended Audience :: Science/Research
12
- Classifier: Programming Language :: Python
13
- Classifier: Programming Language :: Python :: 3
14
- Classifier: License :: OSI Approved :: MIT License
15
- Classifier: Operating System :: OS Independent
16
- Classifier: Topic :: Software Development :: Libraries
17
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
- Requires-Python: >=3.10
19
- Description-Content-Type: text/markdown
20
- License-File: LICENSE
21
- Requires-Dist: lz4
22
- Requires-Dist: joblib
23
- Requires-Dist: numpy
24
- Requires-Dist: pandas
25
- Requires-Dist: jsonpickle
26
- Requires-Dist: boto3
27
- Requires-Dist: moto
28
- Requires-Dist: pytest
29
- Requires-Dist: parameterizable
30
-
31
- # persidict
32
-
33
- Simple persistent dictionaries for Python.
34
-
35
- ## What Is It?
36
-
37
- `persidict` offers a simple persistent key-value store for Python.
38
- It saves the content of the dictionary in a folder on a disk
39
- or in an S3 bucket on AWS. Each value is stored as a separate file / S3 object.
40
- Only text strings or sequences of strings are allowed as keys.
41
-
42
- Unlike other persistent dictionaries (e.g. Python's native `shelve`),
43
- `persidict` is designed for use in highly **distributed environments**,
44
- where multiple instances of a program run concurrently across many machines.
45
-
46
- ## Usage
47
- Class `FileDirDict` is a persistent dictionary that stores its content
48
- in a folder on a disk.
49
-
50
- from persidict import FileDirDict
51
- my_dictionary = FileDirDict(base_dir="my_folder")
52
-
53
- Once created, it can be used as a regular Python dictionary
54
- that stores key-value pairs. A key must be a sequence of strings,
55
- a value can be any (pickleable) Python object:
56
-
57
- my_dictionary["Eliza"] = "MIT Eliza was a mock psychotherapist."
58
- my_dictionary["Eliza","year"] = 1965
59
- my_dictionary["Eliza","authors"] = ["Joseph Weizenbaum"]
60
-
61
- my_dictionary["Shoebox"] = "IBM Shoebox performed arithmetic operations"
62
- my_dictionary["Shoebox"] += " on voice commands."
63
- my_dictionary["Shoebox", "year"] = 1961
64
- my_dictionary["Shoebox", "authors"] = ["W.C. Dersch", "E.A. Quade"]
65
-
66
- for k in my_dictionary:
67
- print(list(k), "==>", my_dictionary[k])
68
-
69
- if not "Eliza" in my_dictionary:
70
- print("Something is wrong")
71
-
72
- If you run the code above, it will produce the following output:
73
-
74
- >>> ['Eliza'] ==> MIT Eliza was a mock psychotherapist.
75
- >>> ['Shoebox'] ==> IBM Shoebox performed arithmetic operations on voice commands.
76
- >>> ['Shoebox', 'authors'] ==> ['W.C. Dersch', 'E.A. Quade']
77
- >>> ['Shoebox', 'year'] ==> 1961
78
- >>> ['Eliza', 'authors'] ==> ['Joseph Weizenbaum']
79
- >>> ['Eliza', 'year'] ==> 1965
80
-
81
- The dictionary automatically creates a folder named "my_folder"
82
- on the local disk. Each key-value pair is stored as
83
- a separate file within this folder.
84
-
85
- If the key is a string, it becomes the filename for the object.
86
- If the key is a sequence of strings, all strings except the last
87
- are used to create nested subfolders within the main folder.
88
- The final string in the sequence serves as the filename for the object,
89
- which is stored in the deepest subfolder.
90
-
91
- Persistent dictionaries only accept sequences of strings as keys.
92
- Any pickleable Python object can be used as a value.
93
- Unlike regular Python dictionaries, insertion order is not preserved.
94
-
95
- del my_dictionary
96
- new_dict = FileDirDict(base_dir="my_folder")
97
- print("len(new_dict) == ",len(new_dict))
98
-
99
- The code above will create a new object named new_dict and then will
100
- print its length:
101
-
102
- >>> len(new_dict) == 6
103
-
104
- The length is 6, because the dictionary was already stored on a disk
105
- in the "my_folder" directory, which contained 6 pickle files.
106
-
107
- Technically, `FileDirDict` saves its content in a folder on a local disk.
108
- But you can share this folder with other machines
109
- (for example, using Dropbox or NFS), and work with the same dictionary
110
- simultaneously from multiple computers (from multiple instances of your program).
111
- This approach would allow you to use a persistent dictionary in
112
- a system that is distributed over dozens of computers.
113
-
114
- If you need to run your program on hundreds (or more) computers,
115
- class `S3Dict` is a better choice: it's a persistent dictionary that
116
- stores its content in an AWS S3 bucket.
117
-
118
- from persidict import S3Dict
119
- my_cloud_dictionary = S3Dict(bucket_name="my_bucket")
120
-
121
- Once created, it can be used as a regular Python dictionary.
122
-
123
- ## Key Classes
124
-
125
- * `SafeStrTuple` - an immutable sequence of URL/filename-safe non-empty strings.
126
- * `PersiDict` - an abstract base class for persistent dictionaries.
127
- * `FileDirDict` - a persistent dictionary that stores its content
128
- in a folder on a disk.
129
- * `S3Dict` - a persistent dictionary that stores its content
130
- in an AWS S3 bucket.
131
-
132
- ## Key Similarities With Python Built-in Dictionaries
133
-
134
- `PersiDict` and its subclasses can be used as regular Python dictionaries.
135
-
136
- * You can use square brackets to get, set, or delete values.
137
- * You can iterate over keys, values, or items.
138
- * You can check if a key is in the dictionary.
139
- * You can check whether two dicts are equal
140
- (meaning they contain the same key-value pairs).
141
- * You can get the length of the dictionary.
142
- * Methods `keys()`, `values()`, `items()`, `get()`, `clear()`
143
- , `setdefault()`, `update()` etc. work as expected.
144
-
145
- ## Key Differences From Python Built-in Dictionaries
146
-
147
- `PersiDict` and its subclasses persist values between program executions,
148
- as well as make it possible to concurrently run programs
149
- that simultaneously work with the same instance of a dictionary.
150
-
151
- * Keys must be sequences of URL/filename-safe non-empty strings.
152
- * Values must be pickleable Python objects.
153
- * You can constrain values to be an instance of a specific class.
154
- * Insertion order is not preserved.
155
- * You can not assign initial key-value pairs to a dictionary in its constructor.
156
- * `PersiDict` API has additional methods `delete_if_exists()`, `timestamp()`,
157
- `get_subdict()`, `subdicts()`, `random_keys()`, `newest_keys()`,
158
- `oldest_keys()`, `newest_values()`, `oldest_values()`,
159
- `get_params()`, `get_metaparams()`, and `get_default_metaparams()`,
160
- which are not available in native Python dicts.
161
-
162
- ## Fine Tuning
163
-
164
- `PersiDict` subclasses have a number of parameters that can be used
165
- to impact behaviour of a dictionary.
166
-
167
- * `base_class_for_values` - A base class for values stored in a dictionary.
168
- If specified, it will be used to check types of values in the dictionary.
169
- If not specified (if set to `None`), no type checking will be performed
170
- and all types will be allowed.
171
- * `file_type` - a string that specifies the type of files used to store objects.
172
- If `file_type` has one of two values: "pkl" or "json", it defines
173
- which file format will be used by the dictionary to store values.
174
- For all other values of `file_type`, the file format will always be plain
175
- text. "pkl" or "json" allow to store arbitrary Python objects,
176
- while all other file_type-s only work with str objects;
177
- it means `base_class_for_values` must be explicitly set to `str`
178
- if `file_type` is not set to "pkl" or "json".
179
- * `immutable_items` - a boolean that specifies whether items in a dictionary
180
- can be modified/deleted. It enables various distributed cache optimizations
181
- for remote storage. True means an append-only dictionary.
182
- False means normal dict-like behaviour. The default value is False.
183
- * `digest_len` - a length of a hash signature suffix which `PersiDict`
184
- automatically adds to each string in a key while mapping the key to
185
- the address of a value in a persistent storage backend
186
- (e.g. a filename or an S3 objectname). It is needed to ensure correct work
187
- of persistent dictionaries with case-insensitive (even if case-preserving)
188
- filesystems, such as MacOS HFS. The default value is 8.
189
-
190
-
191
- ## How To Get It?
192
-
193
- The source code is hosted on GitHub at:
194
- [https://github.com/pythagoras-dev/persidict](https://github.com/pythagoras-dev/persidict)
195
-
196
- Binary installers for the latest released version are available at the Python package index at:
197
- [https://pypi.org/project/persidict](https://pypi.org/project/persidict)
198
-
199
- pip install persidict
200
-
201
- ## Dependencies
202
-
203
- * [jsonpickle](https://jsonpickle.github.io)
204
- * [joblib](https://joblib.readthedocs.io)
205
- * [lz4](https://python-lz4.readthedocs.io)
206
- * [pandas](https://pandas.pydata.org)
207
- * [numpy](https://numpy.org)
208
- * [boto3](https://boto3.readthedocs.io)
209
- * [pytest](https://pytest.org)
210
- * [moto](http://getmoto.org)
211
-
212
- ## Key Contacts
213
-
214
- * [Vlad (Volodymyr) Pavlov](https://www.linkedin.com/in/vlpavlov/)
@@ -1,15 +0,0 @@
1
- LICENSE
2
- README.md
3
- setup.py
4
- persidict/__init__.py
5
- persidict/file_dir_dict.py
6
- persidict/persi_dict.py
7
- persidict/s3_dict.py
8
- persidict/safe_chars.py
9
- persidict/safe_str_tuple.py
10
- persidict/safe_str_tuple_signing.py
11
- persidict.egg-info/PKG-INFO
12
- persidict.egg-info/SOURCES.txt
13
- persidict.egg-info/dependency_links.txt
14
- persidict.egg-info/requires.txt
15
- persidict.egg-info/top_level.txt
@@ -1,9 +0,0 @@
1
- lz4
2
- joblib
3
- numpy
4
- pandas
5
- jsonpickle
6
- boto3
7
- moto
8
- pytest
9
- parameterizable
@@ -1 +0,0 @@
1
- persidict
@@ -1,4 +0,0 @@
1
- [egg_info]
2
- tag_build =
3
- tag_date = 0
4
-
persidict-0.15.1/setup.py DELETED
@@ -1,42 +0,0 @@
1
- import setuptools
2
-
3
- with open("README.md", "r") as f:
4
- long_description = f.read()
5
-
6
- setuptools.setup(
7
- name="persidict"
8
- ,version="0.15.1"
9
- ,author="Vlad (Volodymyr) Pavlov"
10
- ,author_email="vlpavlov@ieee.org"
11
- ,description= "Simple persistent key-value store for Python. "
12
- "Values are stored as files on a disk or as S3 objects on AWS cloud."
13
- ,long_description=long_description
14
- ,long_description_content_type="text/markdown"
15
- ,url="https://github.com/pythagoras-dev/persidict"
16
- ,packages=["persidict"]
17
- ,classifiers=[
18
- "Development Status :: 3 - Alpha"
19
- , "Intended Audience :: Developers"
20
- , "Intended Audience :: Science/Research"
21
- , "Programming Language :: Python"
22
- , "Programming Language :: Python :: 3"
23
- , "License :: OSI Approved :: MIT License"
24
- , "Operating System :: OS Independent"
25
- , "Topic :: Software Development :: Libraries"
26
- , "Topic :: Software Development :: Libraries :: Python Modules"
27
- ]
28
- ,keywords='persistence, dicts, distributed, parallel'
29
- ,python_requires='>=3.10'
30
- ,install_requires=[
31
- 'lz4'
32
- , 'joblib'
33
- , 'numpy'
34
- , 'pandas'
35
- , 'jsonpickle'
36
- , 'boto3'
37
- , 'moto'
38
- , 'pytest'
39
- , 'parameterizable'
40
- ]
41
-
42
- )
File without changes