persidict 0.34.3__py3-none-any.whl → 0.35.0__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.
Potentially problematic release.
This version of persidict might be problematic. Click here for more details.
- persidict/file_dir_dict.py +23 -12
- persidict/overlapping_multi_dict.py +8 -4
- persidict/persi_dict.py +20 -9
- persidict/s3_dict.py +22 -5
- persidict/safe_str_tuple.py +16 -9
- persidict/safe_str_tuple_signing.py +30 -9
- persidict/write_once_dict.py +7 -4
- {persidict-0.34.3.dist-info → persidict-0.35.0.dist-info}/METADATA +1 -1
- persidict-0.35.0.dist-info/RECORD +13 -0
- {persidict-0.34.3.dist-info → persidict-0.35.0.dist-info}/WHEEL +1 -1
- persidict-0.34.3.dist-info/RECORD +0 -13
persidict/file_dir_dict.py
CHANGED
|
@@ -74,16 +74,17 @@ class FileDirDict(PersiDict):
|
|
|
74
74
|
|
|
75
75
|
Raises:
|
|
76
76
|
ValueError: If base_dir points to a file; if file_type is "__etag__";
|
|
77
|
-
|
|
78
|
-
unsupported file_type).
|
|
79
|
-
|
|
77
|
+
if file_type contains unsafe characters; or if configuration is
|
|
78
|
+
inconsistent (e.g., non-str values with unsupported file_type).
|
|
79
|
+
RuntimeError: If base_dir cannot be created or is not a directory.
|
|
80
80
|
"""
|
|
81
81
|
|
|
82
82
|
super().__init__(immutable_items = immutable_items
|
|
83
83
|
,digest_len = digest_len
|
|
84
84
|
,base_class_for_values = base_class_for_values)
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
if file_type != replace_unsafe_chars(file_type, ""):
|
|
87
|
+
raise ValueError("file_type contains unsafe characters")
|
|
87
88
|
self.file_type = file_type
|
|
88
89
|
if self.file_type == "__etag__":
|
|
89
90
|
raise ValueError(
|
|
@@ -92,8 +93,8 @@ class FileDirDict(PersiDict):
|
|
|
92
93
|
|
|
93
94
|
if (base_class_for_values is None or
|
|
94
95
|
not issubclass(base_class_for_values,str)):
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
if file_type not in {"json", "pkl"}:
|
|
97
|
+
raise ValueError("For non-string values file_type must be either 'pkl' or 'json'.")
|
|
97
98
|
|
|
98
99
|
base_dir = str(base_dir)
|
|
99
100
|
|
|
@@ -101,7 +102,8 @@ class FileDirDict(PersiDict):
|
|
|
101
102
|
raise ValueError(f"{base_dir} is a file, not a directory.")
|
|
102
103
|
|
|
103
104
|
os.makedirs(base_dir, exist_ok=True)
|
|
104
|
-
|
|
105
|
+
if not os.path.isdir(base_dir):
|
|
106
|
+
raise RuntimeError(f"Failed to create or access directory: {base_dir}")
|
|
105
107
|
|
|
106
108
|
# self.base_dir_param = _base_dir
|
|
107
109
|
self._base_dir = os.path.abspath(base_dir)
|
|
@@ -539,7 +541,8 @@ class FileDirDict(PersiDict):
|
|
|
539
541
|
KeyError: If immutable_items is True or if the key does not exist.
|
|
540
542
|
"""
|
|
541
543
|
key = SafeStrTuple(key)
|
|
542
|
-
|
|
544
|
+
if self.immutable_items:
|
|
545
|
+
raise KeyError("Can't delete immutable items")
|
|
543
546
|
filename = self._build_full_path(key)
|
|
544
547
|
if not os.path.isfile(filename):
|
|
545
548
|
raise KeyError(f"File {filename} does not exist")
|
|
@@ -563,11 +566,19 @@ class FileDirDict(PersiDict):
|
|
|
563
566
|
- Any if result_type == {"values"}
|
|
564
567
|
- tuple[SafeStrTuple, Any] if result_type == {"keys", "values"}
|
|
565
568
|
- tuple[..., float] including POSIX timestamp if "timestamps" is requested.
|
|
569
|
+
|
|
570
|
+
Raises:
|
|
571
|
+
TypeError: If result_type is not a set.
|
|
572
|
+
ValueError: If result_type is empty or contains unsupported labels.
|
|
566
573
|
"""
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
574
|
+
if not isinstance(result_type, set):
|
|
575
|
+
raise TypeError("result_type must be a set")
|
|
576
|
+
if not (1 <= len(result_type) <= 3):
|
|
577
|
+
raise ValueError("result_type must be a non-empty subset of {'keys','values','timestamps'}")
|
|
578
|
+
allowed = {"keys", "values", "timestamps"}
|
|
579
|
+
invalid = result_type - allowed
|
|
580
|
+
if invalid:
|
|
581
|
+
raise ValueError(f"Unsupported result_type entries: {sorted(invalid)}; allowed: {sorted(allowed)}")
|
|
571
582
|
|
|
572
583
|
walk_results = os.walk(self._base_dir)
|
|
573
584
|
ext_len = len(self.file_type) + 1
|
|
@@ -46,18 +46,22 @@ class OverlappingMultiDict:
|
|
|
46
46
|
resulting dict also receives file_type=<key>.
|
|
47
47
|
|
|
48
48
|
Raises:
|
|
49
|
-
|
|
49
|
+
TypeError: If dict_type is not a PersiDict subclass, or if
|
|
50
50
|
shared_subdicts_params is not a dict, or if any individual
|
|
51
51
|
parameter set is not a dict.
|
|
52
52
|
"""
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
if not issubclass(dict_type, PersiDict):
|
|
54
|
+
raise TypeError("dict_type must be a subclass of PersiDict")
|
|
55
|
+
if not isinstance(shared_subdicts_params, dict):
|
|
56
|
+
raise TypeError("shared_subdicts_params must be a dict")
|
|
55
57
|
self.dict_type = dict_type
|
|
56
58
|
self.shared_subdicts_params = shared_subdicts_params
|
|
57
59
|
self.individual_subdicts_params = individual_subdicts_params
|
|
58
60
|
self.subdicts_names = list(individual_subdicts_params.keys())
|
|
59
61
|
for subdict_name in individual_subdicts_params:
|
|
60
|
-
|
|
62
|
+
if not isinstance(individual_subdicts_params[subdict_name], dict):
|
|
63
|
+
raise TypeError(
|
|
64
|
+
f"Params for subdict {subdict_name!r} must be a dict")
|
|
61
65
|
self.__dict__[subdict_name] = dict_type(
|
|
62
66
|
**{**shared_subdicts_params
|
|
63
67
|
,**individual_subdicts_params[subdict_name]
|
persidict/persi_dict.py
CHANGED
|
@@ -240,11 +240,21 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
240
240
|
Returns:
|
|
241
241
|
Any: An iterator yielding keys, values, and/or timestamps based on
|
|
242
242
|
result_type.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
243
|
+
|
|
244
|
+
Raises:
|
|
245
|
+
TypeError: If result_type is not a set.
|
|
246
|
+
ValueError: If result_type contains invalid entries or an invalid number of items.
|
|
247
|
+
NotImplementedError: Subclasses must implement the concrete iterator.
|
|
248
|
+
"""
|
|
249
|
+
if not isinstance(result_type, set):
|
|
250
|
+
raise TypeError("result_type must be a set of strings")
|
|
251
|
+
if not (1 <= len(result_type) <= 3):
|
|
252
|
+
raise ValueError("result_type must contain between 1 and 3 elements")
|
|
253
|
+
allowed = {"keys", "values", "timestamps"}
|
|
254
|
+
if (result_type | allowed) != allowed:
|
|
255
|
+
raise ValueError("result_type can only contain 'keys', 'values', 'timestamps'")
|
|
256
|
+
if not (1 <= len(result_type & allowed) <= 3):
|
|
257
|
+
raise ValueError("result_type must include at least one of 'keys', 'values', 'timestamps'")
|
|
248
258
|
raise NotImplementedError
|
|
249
259
|
|
|
250
260
|
|
|
@@ -322,11 +332,12 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
322
332
|
Any: Existing value if present; otherwise the provided default.
|
|
323
333
|
|
|
324
334
|
Raises:
|
|
325
|
-
|
|
335
|
+
TypeError: If default is a Joker command (KEEP_CURRENT/DELETE_CURRENT).
|
|
326
336
|
"""
|
|
327
337
|
# TODO: check edge cases to ensure the same semantics as standard dicts
|
|
328
338
|
key = SafeStrTuple(key)
|
|
329
|
-
|
|
339
|
+
if isinstance(default, Joker):
|
|
340
|
+
raise TypeError("default must be a regular value, not a Joker command")
|
|
330
341
|
if key in self:
|
|
331
342
|
return self[key]
|
|
332
343
|
else:
|
|
@@ -383,7 +394,7 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
383
394
|
Raises:
|
|
384
395
|
KeyError: If items are immutable (immutable_items is True).
|
|
385
396
|
"""
|
|
386
|
-
if self.immutable_items:
|
|
397
|
+
if self.immutable_items:
|
|
387
398
|
raise KeyError("Can't delete an immutable key-value pair")
|
|
388
399
|
|
|
389
400
|
for k in self.keys():
|
|
@@ -408,7 +419,7 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
408
419
|
KeyError: If items are immutable (immutable_items is True).
|
|
409
420
|
"""
|
|
410
421
|
|
|
411
|
-
if self.immutable_items:
|
|
422
|
+
if self.immutable_items:
|
|
412
423
|
raise KeyError("Can't delete an immutable key-value pair")
|
|
413
424
|
|
|
414
425
|
key = SafeStrTuple(key)
|
persidict/s3_dict.py
CHANGED
|
@@ -433,12 +433,23 @@ class S3Dict(PersiDict):
|
|
|
433
433
|
- Any if result_type == {"values"}
|
|
434
434
|
- tuple[SafeStrTuple, Any] if result_type == {"keys", "values"}
|
|
435
435
|
- tuple[..., float] including POSIX timestamp if "timestamps" is requested.
|
|
436
|
+
|
|
437
|
+
Raises:
|
|
438
|
+
ValueError: If result_type is not a set or contains entries other than
|
|
439
|
+
"keys", "values", and/or "timestamps", or if it is empty.
|
|
436
440
|
"""
|
|
437
441
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
+
if not isinstance(result_type, set):
|
|
443
|
+
raise ValueError(
|
|
444
|
+
"result_type must be a set containing one to three of: 'keys', 'values', 'timestamps'"
|
|
445
|
+
)
|
|
446
|
+
if not (1 <= len(result_type) <= 3):
|
|
447
|
+
raise ValueError("result_type must be a non-empty set with at most three elements")
|
|
448
|
+
allowed = {"keys", "values", "timestamps"}
|
|
449
|
+
if not result_type.issubset(allowed):
|
|
450
|
+
invalid = ", ".join(sorted(result_type - allowed))
|
|
451
|
+
raise ValueError(f"result_type contains invalid entries: {invalid}. Allowed: {sorted(allowed)}")
|
|
452
|
+
# Intersections/length checks are implied by the above conditions.
|
|
442
453
|
|
|
443
454
|
suffix = "." + self.file_type
|
|
444
455
|
ext_len = len(self.file_type) + 1
|
|
@@ -452,8 +463,14 @@ class S3Dict(PersiDict):
|
|
|
452
463
|
|
|
453
464
|
Returns:
|
|
454
465
|
SafeStrTuple: The parsed key parts, still signed.
|
|
466
|
+
|
|
467
|
+
Raises:
|
|
468
|
+
ValueError: If the provided key does not start with this dictionary's root_prefix.
|
|
455
469
|
"""
|
|
456
|
-
|
|
470
|
+
if not full_name.startswith(self.root_prefix):
|
|
471
|
+
raise ValueError(
|
|
472
|
+
f"S3 object key '{full_name}' is outside of root_prefix '{self.root_prefix}'"
|
|
473
|
+
)
|
|
457
474
|
result = full_name[prefix_len:-ext_len].split(sep="/")
|
|
458
475
|
return SafeStrTuple(result)
|
|
459
476
|
|
persidict/safe_str_tuple.py
CHANGED
|
@@ -71,26 +71,33 @@ class SafeStrTuple(Sequence, Hashable):
|
|
|
71
71
|
**kwargs: Not supported.
|
|
72
72
|
|
|
73
73
|
Raises:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
TypeError: If unexpected keyword arguments are provided, if no args
|
|
75
|
+
are provided, or if an argument has an invalid type.
|
|
76
|
+
ValueError: If a string is empty, too long, or contains disallowed
|
|
77
|
+
characters.
|
|
77
78
|
"""
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
if len(kwargs) != 0:
|
|
80
|
+
raise TypeError(f"Unexpected keyword arguments: {list(kwargs.keys())}")
|
|
81
|
+
if len(args) == 0:
|
|
82
|
+
raise TypeError("At least one argument is required")
|
|
80
83
|
candidate_strings = []
|
|
81
84
|
for a in args:
|
|
82
85
|
if isinstance(a, SafeStrTuple):
|
|
83
86
|
candidate_strings.extend(a.strings)
|
|
84
87
|
elif isinstance(a, str):
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
if len(a) == 0:
|
|
89
|
+
raise ValueError("Strings must be non-empty")
|
|
90
|
+
if len(a) >= SAFE_STRING_MAX_LENGTH:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"String length must be < {SAFE_STRING_MAX_LENGTH}, got {len(a)}")
|
|
93
|
+
if not all(c in SAFE_CHARS_SET for c in a):
|
|
94
|
+
raise ValueError("String contains disallowed characters")
|
|
88
95
|
candidate_strings.append(a)
|
|
89
96
|
elif _is_sequence_not_mapping(a):
|
|
90
97
|
if len(a) > 0:
|
|
91
98
|
candidate_strings.extend(SafeStrTuple(*a).strings)
|
|
92
99
|
else:
|
|
93
|
-
|
|
100
|
+
raise TypeError(f"Invalid argument type: {type(a)}")
|
|
94
101
|
self.strings = tuple(candidate_strings)
|
|
95
102
|
|
|
96
103
|
@property
|
|
@@ -34,11 +34,18 @@ def _create_signature_suffix(input_str: str, digest_len: int) -> str:
|
|
|
34
34
|
|
|
35
35
|
Returns:
|
|
36
36
|
str: The computed suffix to append (may be an empty string).
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
TypeError: If input_str is not a str or digest_len is not an int.
|
|
40
|
+
ValueError: If digest_len is negative.
|
|
37
41
|
"""
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
if not isinstance(input_str, str):
|
|
44
|
+
raise TypeError(f"input_str must be str, got {type(input_str)!r}")
|
|
45
|
+
if not isinstance(digest_len, int):
|
|
46
|
+
raise TypeError(f"digest_len must be int, got {type(digest_len)!r}")
|
|
47
|
+
if digest_len < 0:
|
|
48
|
+
raise ValueError(f"digest_len must be >= 0, got {digest_len}")
|
|
42
49
|
|
|
43
50
|
if digest_len == 0:
|
|
44
51
|
return ""
|
|
@@ -64,11 +71,18 @@ def _add_signature_suffix_if_absent(input_str: str, digest_len: int) -> str:
|
|
|
64
71
|
|
|
65
72
|
Returns:
|
|
66
73
|
str: The original or suffixed string.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
TypeError: If input_str is not a str or digest_len is not an int.
|
|
77
|
+
ValueError: If digest_len is negative.
|
|
67
78
|
"""
|
|
68
79
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
if not isinstance(input_str, str):
|
|
81
|
+
raise TypeError(f"input_str must be str, got {type(input_str)!r}")
|
|
82
|
+
if not isinstance(digest_len, int):
|
|
83
|
+
raise TypeError(f"digest_len must be int, got {type(digest_len)!r}")
|
|
84
|
+
if digest_len < 0:
|
|
85
|
+
raise ValueError(f"digest_len must be >= 0, got {digest_len}")
|
|
72
86
|
|
|
73
87
|
if digest_len == 0:
|
|
74
88
|
return input_str
|
|
@@ -121,11 +135,18 @@ def _remove_signature_suffix_if_present(input_str: str, digest_len: int) -> str:
|
|
|
121
135
|
Returns:
|
|
122
136
|
str: The original string without the suffix if detected; otherwise the
|
|
123
137
|
original string.
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
TypeError: If input_str is not a str or digest_len is not an int.
|
|
141
|
+
ValueError: If digest_len is negative.
|
|
124
142
|
"""
|
|
125
143
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
144
|
+
if not isinstance(input_str, str):
|
|
145
|
+
raise TypeError(f"input_str must be str, got {type(input_str)!r}")
|
|
146
|
+
if not isinstance(digest_len, int):
|
|
147
|
+
raise TypeError(f"digest_len must be int, got {type(digest_len)!r}")
|
|
148
|
+
if digest_len < 0:
|
|
149
|
+
raise ValueError(f"digest_len must be >= 0, got {digest_len}")
|
|
129
150
|
|
|
130
151
|
if digest_len == 0:
|
|
131
152
|
return input_str
|
persidict/write_once_dict.py
CHANGED
|
@@ -79,13 +79,16 @@ class WriteOnceDict(PersiDict):
|
|
|
79
79
|
(disabled).
|
|
80
80
|
|
|
81
81
|
Raises:
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
TypeError: If ``wrapped_dict`` is not a PersiDict instance.
|
|
83
|
+
ValueError: If ``wrapped_dict`` does not enforce immutable items.
|
|
84
84
|
"""
|
|
85
85
|
if wrapped_dict is None:
|
|
86
86
|
wrapped_dict = FileDirDict(immutable_items=True)
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
if not isinstance(wrapped_dict, PersiDict):
|
|
88
|
+
raise TypeError("wrapped_dict must be a PersiDict instance")
|
|
89
|
+
if wrapped_dict.immutable_items is not True:
|
|
90
|
+
raise ValueError("wrapped_dict must be append-only "
|
|
91
|
+
"(immutable_items==True)")
|
|
89
92
|
self.p_consistency_checks = p_consistency_checks
|
|
90
93
|
PersiDict.__init__(self,
|
|
91
94
|
base_class_for_values=wrapped_dict.base_class_for_values,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: persidict
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.35.0
|
|
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
5
|
Keywords: persistence,dicts,distributed,parallel
|
|
6
6
|
Author: Vlad (Volodymyr) Pavlov
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
persidict/__init__.py,sha256=CDOSJGgCnyRTkGUTzaeg3Cqsxwx0-0EFieOtldXwAls,1380
|
|
2
|
+
persidict/file_dir_dict.py,sha256=gvCyk_kp_3AC-zkHuSj-0lM4hf_fBK6iz3ffGQ7jtvU,25757
|
|
3
|
+
persidict/jokers.py,sha256=H2MzKllvgm7t2sjX3GaRNLngOiG2ohE-lv0B3g3J1SQ,2710
|
|
4
|
+
persidict/overlapping_multi_dict.py,sha256=v1c2kf3Bhm2Dh6SGEsRV58hQI11YX9ZPxyrDV_1d5s8,5360
|
|
5
|
+
persidict/persi_dict.py,sha256=DIMQaY4gE8NSYTlHlk9rfOJJEYUuLV8kmQ-gc474py4,20052
|
|
6
|
+
persidict/s3_dict.py,sha256=VKDqY9sASffeXtfbavVWk8-umrioIG5Xq57Qqg1wPH4,21522
|
|
7
|
+
persidict/safe_chars.py,sha256=9Qy24fu2dmiJOdmCF8mKZULfQaRp7H4oxfgDXeLgogI,1160
|
|
8
|
+
persidict/safe_str_tuple.py,sha256=YBTcYjUKIffznOawXb9xKjz4HaKdklrgyVtegJFmr5w,7202
|
|
9
|
+
persidict/safe_str_tuple_signing.py,sha256=RQAj4fnpRVaOe0KpwLler1UTaeNOgXCQpU3t80ixtxg,7493
|
|
10
|
+
persidict/write_once_dict.py,sha256=-lPQ_yuU62pczHT0BYO6SFbiZBKFq8Tj9ln3jCzNDzA,11443
|
|
11
|
+
persidict-0.35.0.dist-info/WHEEL,sha256=F3mArEuDT3LDFEqo9fCiUx6ISLN64aIhcGSiIwtu4r8,79
|
|
12
|
+
persidict-0.35.0.dist-info/METADATA,sha256=oo_YL1_W4ux3lTJZvREQPcin1ILJmvxtMDlWuITUwCw,9262
|
|
13
|
+
persidict-0.35.0.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
persidict/__init__.py,sha256=CDOSJGgCnyRTkGUTzaeg3Cqsxwx0-0EFieOtldXwAls,1380
|
|
2
|
-
persidict/file_dir_dict.py,sha256=Lf65HqI00J7Pi2jELWEXo5DUk8cW1GdKdgkVyFgFHGo,25135
|
|
3
|
-
persidict/jokers.py,sha256=H2MzKllvgm7t2sjX3GaRNLngOiG2ohE-lv0B3g3J1SQ,2710
|
|
4
|
-
persidict/overlapping_multi_dict.py,sha256=ZUbkNConqUDTP1LFxKv0WFvsAPcCiHUjhNKe8912wNs,5112
|
|
5
|
-
persidict/persi_dict.py,sha256=mwHng2pjXf_durHKeiTZRDOVJbpHjANkdVR1_y2Bs4E,19408
|
|
6
|
-
persidict/s3_dict.py,sha256=Xl-ZOukY7dl-LkujaPiPNVZo9sK9t5bFhG2lCYLAbjs,20635
|
|
7
|
-
persidict/safe_chars.py,sha256=9Qy24fu2dmiJOdmCF8mKZULfQaRp7H4oxfgDXeLgogI,1160
|
|
8
|
-
persidict/safe_str_tuple.py,sha256=4wt3Jfd7efKczTP1o--XkOSic5_7riVDIgnK19Bzfbk,6741
|
|
9
|
-
persidict/safe_str_tuple_signing.py,sha256=ihV1hLx24N9f64nFbqAvbnsPZDon-kUAVncs1628Q6M,6428
|
|
10
|
-
persidict/write_once_dict.py,sha256=5tGqm51EIEcjnAhP8oOY-eQ1AaQhteQxSd0SPIp934c,11217
|
|
11
|
-
persidict-0.34.3.dist-info/WHEEL,sha256=Jb20R3Ili4n9P1fcwuLup21eQ5r9WXhs4_qy7VTrgPI,79
|
|
12
|
-
persidict-0.34.3.dist-info/METADATA,sha256=nXLxLxK2mZ7NmVfRu6H--W8hVNJOQZl1lAs1mbkALXQ,9262
|
|
13
|
-
persidict-0.34.3.dist-info/RECORD,,
|