elody 0.0.225__py3-none-any.whl → 0.0.227__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.
- elody/error_codes.py +1 -1
- elody/policies/authorization/filter_generic_objects_policy_v2.py +5 -52
- elody/policies/helpers.py +13 -2
- elody/policies/permission_handler.py +155 -9
- {elody-0.0.225.dist-info → elody-0.0.227.dist-info}/METADATA +1 -1
- {elody-0.0.225.dist-info → elody-0.0.227.dist-info}/RECORD +9 -9
- {elody-0.0.225.dist-info → elody-0.0.227.dist-info}/WHEEL +0 -0
- {elody-0.0.225.dist-info → elody-0.0.227.dist-info}/licenses/LICENSE +0 -0
- {elody-0.0.225.dist-info → elody-0.0.227.dist-info}/top_level.txt +0 -0
elody/error_codes.py
CHANGED
|
@@ -76,7 +76,7 @@ class ErrorCode(Enum):
|
|
|
76
76
|
INVALID_VALUE = ("5016", ["value", "options", "line_number"])
|
|
77
77
|
ITEM_WITH_VALUE_FOR_KEY_NOT_FOUND = ("5017", ["key", "value", "line_number"])
|
|
78
78
|
ITEM_WITH_VALUE_FOR_KEY_NOT_UNIQUE = ("5018", ["key", "value", "line_number"])
|
|
79
|
-
DUPLICATE_ENTRY_LINKED_ENTITIES = ("
|
|
79
|
+
DUPLICATE_ENTRY_LINKED_ENTITIES = ("5018", ["entity_type", "duplicate_entry"])
|
|
80
80
|
|
|
81
81
|
# Filter error codes
|
|
82
82
|
NO_MATCHER_FOR_FILTER_REQUEST = ("6001", [])
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import re as regex
|
|
2
2
|
|
|
3
3
|
from copy import deepcopy
|
|
4
|
-
from elody.policies.helpers import generate_filter_key_and_lookup_from_restricted_key
|
|
5
4
|
from elody.policies.permission_handler import (
|
|
6
5
|
get_permissions,
|
|
6
|
+
handle_item_overview_request,
|
|
7
7
|
mask_protected_content_post_request_hook,
|
|
8
8
|
)
|
|
9
9
|
from flask import g, Request # pyright: ignore
|
|
@@ -108,58 +108,11 @@ class PostRequestRules:
|
|
|
108
108
|
type_filter_values.remove(type_filter_value)
|
|
109
109
|
continue
|
|
110
110
|
|
|
111
|
-
restrictions_grouped_by_index = {}
|
|
112
111
|
schemas = permissions["read"][type_filter_value]
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
restricted_key, lookup = (
|
|
118
|
-
generate_filter_key_and_lookup_from_restricted_key(
|
|
119
|
-
restricted_key
|
|
120
|
-
)
|
|
121
|
-
)
|
|
122
|
-
key = f"{schema}|{restricted_key}"
|
|
123
|
-
if group := restrictions_grouped_by_index.get(index):
|
|
124
|
-
group["key"].append(key)
|
|
125
|
-
else:
|
|
126
|
-
restrictions_grouped_by_index.update(
|
|
127
|
-
{
|
|
128
|
-
index: {
|
|
129
|
-
"lookup": lookup,
|
|
130
|
-
"key": [key],
|
|
131
|
-
"value": restricting_value,
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
# support false soft call read responses
|
|
137
|
-
for filter in filters:
|
|
138
|
-
key = filter.get("key", "")
|
|
139
|
-
if isinstance(key, list):
|
|
140
|
-
key = ",".join(key)
|
|
141
|
-
for restriction in restrictions_grouped_by_index.values():
|
|
142
|
-
if key not in ",".join(restriction["key"]):
|
|
143
|
-
continue
|
|
144
|
-
values = (
|
|
145
|
-
filter["value"]
|
|
146
|
-
if isinstance(filter["value"], list)
|
|
147
|
-
else [filter["value"]]
|
|
148
|
-
)
|
|
149
|
-
for value in values:
|
|
150
|
-
if value not in restriction["value"] and value not in ["", "*"]:
|
|
151
|
-
return False
|
|
152
|
-
|
|
153
|
-
for restriction in restrictions_grouped_by_index.values():
|
|
154
|
-
user_context.access_restrictions.filters.append( # pyright: ignore
|
|
155
|
-
{
|
|
156
|
-
"lookup": restriction["lookup"],
|
|
157
|
-
"type": "selection",
|
|
158
|
-
"key": restriction["key"],
|
|
159
|
-
"value": restriction["value"],
|
|
160
|
-
"match_exact": True,
|
|
161
|
-
}
|
|
162
|
-
)
|
|
112
|
+
result = handle_item_overview_request(schemas, filters)
|
|
113
|
+
if not isinstance(result, list):
|
|
114
|
+
return result
|
|
115
|
+
user_context.access_restrictions.filters.extend(result) # pyright: ignore
|
|
163
116
|
|
|
164
117
|
if len(type_filter_values) == 0:
|
|
165
118
|
return False
|
elody/policies/helpers.py
CHANGED
|
@@ -9,18 +9,18 @@ def generate_filter_key_and_lookup_from_restricted_key(key):
|
|
|
9
9
|
if (keys := key.split("@", 1)) and len(keys) == 1:
|
|
10
10
|
return key, {}
|
|
11
11
|
|
|
12
|
-
local_field = keys[0]
|
|
13
12
|
document_type, key = keys[1].split("-", 1)
|
|
14
13
|
collection = (
|
|
15
14
|
get_object_configuration_mapper().get(document_type).crud()["collection"]
|
|
16
15
|
)
|
|
16
|
+
local_field, lookup_prefix = parse_optional_filter_key(keys[0])
|
|
17
17
|
lookup = {
|
|
18
18
|
"from": collection,
|
|
19
19
|
"local_field": local_field,
|
|
20
20
|
"foreign_field": "identifiers",
|
|
21
21
|
"as": f"__lookup.virtual_relations.{document_type}",
|
|
22
22
|
}
|
|
23
|
-
return f"{lookup['as']}.{key}", lookup
|
|
23
|
+
return f"{lookup_prefix}{lookup['as']}.{key}", lookup
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def get_content(item, request, content):
|
|
@@ -62,3 +62,14 @@ def get_item(storage_manager, user_context_bag, view_args) -> dict:
|
|
|
62
62
|
raise NotFound(
|
|
63
63
|
f"{get_error_code(ErrorCode.ITEM_NOT_FOUND, get_read())} | id:{id} - Item with id {id} does not exist."
|
|
64
64
|
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def parse_optional_filter_key(key):
|
|
68
|
+
prefix = ""
|
|
69
|
+
if key[0] == "!":
|
|
70
|
+
key = key[1:]
|
|
71
|
+
prefix += "!"
|
|
72
|
+
if key[0] == "?":
|
|
73
|
+
key = key[1:]
|
|
74
|
+
prefix += "?"
|
|
75
|
+
return key, prefix
|
|
@@ -2,11 +2,17 @@ import re as regex
|
|
|
2
2
|
|
|
3
3
|
from copy import deepcopy
|
|
4
4
|
from elody.error_codes import ErrorCode, get_error_code, get_read
|
|
5
|
-
from elody.policies.helpers import
|
|
5
|
+
from elody.policies.helpers import (
|
|
6
|
+
generate_filter_key_and_lookup_from_restricted_key,
|
|
7
|
+
get_flat_item_and_object_lists,
|
|
8
|
+
get_item,
|
|
9
|
+
)
|
|
6
10
|
from elody.util import flatten_dict, interpret_flat_key
|
|
11
|
+
from flask import g
|
|
7
12
|
from inuits_policy_based_auth.contexts.user_context import UserContext
|
|
8
13
|
from logging_elody.log import log # pyright: ignore
|
|
9
14
|
from storage.storagemanager import StorageManager # pyright: ignore
|
|
15
|
+
from werkzeug.exceptions import NotFound
|
|
10
16
|
|
|
11
17
|
|
|
12
18
|
_permissions = {}
|
|
@@ -49,6 +55,109 @@ def __replace_permission_placeholders(data, placeholder_key, placeholder_value):
|
|
|
49
55
|
return data
|
|
50
56
|
|
|
51
57
|
|
|
58
|
+
def handle_item_overview_request(schemas, filters):
|
|
59
|
+
restrictions_grouped_by_index = __group_restrictions_by_index(schemas)
|
|
60
|
+
short_circuit = __short_circuit_item_overview_soft_call(
|
|
61
|
+
filters, restrictions_grouped_by_index
|
|
62
|
+
)
|
|
63
|
+
if short_circuit is not None:
|
|
64
|
+
return short_circuit
|
|
65
|
+
return __generate_restriction_filters(restrictions_grouped_by_index)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def __group_restrictions_by_index(schemas):
|
|
69
|
+
restrictions_grouped_by_index = {}
|
|
70
|
+
for schema in schemas.keys():
|
|
71
|
+
restrictions = schemas[schema].get("object_restrictions", {})
|
|
72
|
+
for restricted_key, restricting_value in restrictions.items():
|
|
73
|
+
index, restricted_key = restricted_key.split(":")
|
|
74
|
+
restricted_key, lookup = generate_filter_key_and_lookup_from_restricted_key(
|
|
75
|
+
restricted_key
|
|
76
|
+
)
|
|
77
|
+
key = f"{schema}|{restricted_key}"
|
|
78
|
+
if group := restrictions_grouped_by_index.get(index):
|
|
79
|
+
group["key"].append(key)
|
|
80
|
+
else:
|
|
81
|
+
restrictions_grouped_by_index.update(
|
|
82
|
+
{
|
|
83
|
+
index: {
|
|
84
|
+
"lookup": lookup,
|
|
85
|
+
"key": [key],
|
|
86
|
+
"value": restricting_value,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
return restrictions_grouped_by_index
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def __short_circuit_item_overview_soft_call(filters, restrictions_grouped_by_index):
|
|
94
|
+
for filter in filters:
|
|
95
|
+
key = filter.get("key", "")
|
|
96
|
+
if isinstance(key, list):
|
|
97
|
+
key = ",".join(key)
|
|
98
|
+
for restriction in restrictions_grouped_by_index.values():
|
|
99
|
+
if key not in ",".join(restriction["key"]):
|
|
100
|
+
continue
|
|
101
|
+
values = (
|
|
102
|
+
filter["value"]
|
|
103
|
+
if isinstance(filter["value"], list)
|
|
104
|
+
else [filter["value"]]
|
|
105
|
+
)
|
|
106
|
+
for value in values:
|
|
107
|
+
if value not in restriction["value"] and value not in ["", "*"]:
|
|
108
|
+
return False
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def __generate_restriction_filters(restrictions_grouped_by_index):
|
|
113
|
+
filters = []
|
|
114
|
+
for restriction in restrictions_grouped_by_index.values():
|
|
115
|
+
try:
|
|
116
|
+
combined_restrictions = [
|
|
117
|
+
value for value in restriction["value"] if isinstance(value, list)
|
|
118
|
+
][0]
|
|
119
|
+
except IndexError:
|
|
120
|
+
combined_restrictions = []
|
|
121
|
+
|
|
122
|
+
filter = {
|
|
123
|
+
"lookup": restriction["lookup"],
|
|
124
|
+
"type": "selection",
|
|
125
|
+
"key": restriction["key"],
|
|
126
|
+
"value": [
|
|
127
|
+
value for value in restriction["value"] if not isinstance(value, list)
|
|
128
|
+
],
|
|
129
|
+
"match_exact": True,
|
|
130
|
+
"or": [],
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for combined_restriction in combined_restrictions:
|
|
134
|
+
combination = []
|
|
135
|
+
for value, combinations in combined_restriction.items():
|
|
136
|
+
combination.append(
|
|
137
|
+
{
|
|
138
|
+
"type": "selection",
|
|
139
|
+
"key": restriction["key"],
|
|
140
|
+
"value": [value],
|
|
141
|
+
"match_exact": True,
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
for key, value in combinations.items():
|
|
145
|
+
key = [f"{restriction['key'][0].split('|')[0]}|{key}"]
|
|
146
|
+
combination.append(
|
|
147
|
+
{
|
|
148
|
+
"lookup": restriction["lookup"],
|
|
149
|
+
"type": "selection",
|
|
150
|
+
"key": key,
|
|
151
|
+
"value": value,
|
|
152
|
+
"match_exact": True,
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
filter["or"].append(combination)
|
|
156
|
+
|
|
157
|
+
filters.append(filter)
|
|
158
|
+
return filters
|
|
159
|
+
|
|
160
|
+
|
|
52
161
|
def handle_single_item_request(
|
|
53
162
|
user_context: UserContext,
|
|
54
163
|
item,
|
|
@@ -232,20 +341,24 @@ def __is_allowed_to_crud_item_keys(
|
|
|
232
341
|
def __item_value_in_values(
|
|
233
342
|
flat_item, key, values: list, flat_request_body, user_context: UserContext
|
|
234
343
|
):
|
|
344
|
+
if __matches_combined_expected_values(
|
|
345
|
+
flat_item, key, values, flat_request_body, user_context
|
|
346
|
+
):
|
|
347
|
+
return True
|
|
348
|
+
|
|
235
349
|
negate_condition = False
|
|
236
350
|
is_optional = False
|
|
237
|
-
|
|
351
|
+
key_of_relation = None
|
|
238
352
|
if key[0] == "!":
|
|
239
353
|
key = key[1:]
|
|
240
354
|
negate_condition = True
|
|
241
355
|
if key[0] == "?":
|
|
242
356
|
key = key[1:]
|
|
243
357
|
is_optional = True
|
|
244
|
-
|
|
245
|
-
key_of_relation = None
|
|
246
358
|
if (keys := key.split("@", 1)) and len(keys) == 2:
|
|
247
359
|
key = keys[0]
|
|
248
360
|
key_of_relation = keys[1].split("-", 1)[1]
|
|
361
|
+
|
|
249
362
|
try:
|
|
250
363
|
item_value = flat_request_body.get(key, flat_item[key])
|
|
251
364
|
if is_optional:
|
|
@@ -260,16 +373,49 @@ def __item_value_in_values(
|
|
|
260
373
|
if key_of_relation:
|
|
261
374
|
if isinstance(item_value, list):
|
|
262
375
|
item_value = item_value[0]
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
376
|
+
try:
|
|
377
|
+
item = get_item(
|
|
378
|
+
StorageManager(),
|
|
379
|
+
user_context.bag,
|
|
380
|
+
{"type": keys[1].split("-", 1)[0], "id": item_value},
|
|
381
|
+
)
|
|
382
|
+
except NotFound as exception:
|
|
383
|
+
if g.get("dry_run") or is_optional:
|
|
384
|
+
return True
|
|
385
|
+
raise exception
|
|
268
386
|
flat_item, _ = get_flat_item_and_object_lists(item)
|
|
269
387
|
return __item_value_in_values(
|
|
270
388
|
flat_item, key_of_relation, values, flat_request_body, user_context
|
|
271
389
|
)
|
|
272
390
|
|
|
391
|
+
return __matches_expected_values(flat_item, item_value, values, negate_condition)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def __matches_combined_expected_values(
|
|
395
|
+
flat_item, key, values, flat_request_body, user_context
|
|
396
|
+
):
|
|
397
|
+
values_deepcopy = deepcopy(values)
|
|
398
|
+
for value_from_values in values_deepcopy:
|
|
399
|
+
if not isinstance(value_from_values, list):
|
|
400
|
+
continue
|
|
401
|
+
for combined_restriction in value_from_values:
|
|
402
|
+
for value, combinations in combined_restriction.items():
|
|
403
|
+
if __item_value_in_values(
|
|
404
|
+
flat_item, key, [value], flat_request_body, user_context
|
|
405
|
+
):
|
|
406
|
+
for combination_key, value in combinations.items():
|
|
407
|
+
if __item_value_in_values(
|
|
408
|
+
flat_item,
|
|
409
|
+
combination_key,
|
|
410
|
+
value,
|
|
411
|
+
flat_request_body,
|
|
412
|
+
user_context,
|
|
413
|
+
):
|
|
414
|
+
return True
|
|
415
|
+
values.remove(value_from_values)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def __matches_expected_values(flat_item, item_value, values, negate_condition):
|
|
273
419
|
expected_values = []
|
|
274
420
|
for value in values:
|
|
275
421
|
if flat_item_key_value := flat_item.get(value):
|
|
@@ -2,7 +2,7 @@ __init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
2
2
|
elody/__init__.py,sha256=d0Q6Fn44e7wFfLabDOBxpcJ1DPKWlFunGYDUBmO-4hA,22
|
|
3
3
|
elody/client.py,sha256=15SBfnLHJXXY5Vf5BnkWdjtvkH21E_AsWTzm2-zcbf0,8799
|
|
4
4
|
elody/csv.py,sha256=f8HphE-KC2OqKFzV0HiifWBgMHb3g0EA_Y82o_6JEiE,16761
|
|
5
|
-
elody/error_codes.py,sha256=
|
|
5
|
+
elody/error_codes.py,sha256=127_NuFIdVYFkc3NiSrBSKGPv2ABjP4zwzffvwoahsU,4393
|
|
6
6
|
elody/exceptions.py,sha256=5KSw2sPCZz3lDIJX4LiR2iL9n4m4KIil04D1d3X5rd0,968
|
|
7
7
|
elody/job.py,sha256=EGfYozNxyx2iLC-j_L5XLzV3hUAOM9UIFuYzyqKpS1w,1908
|
|
8
8
|
elody/loader.py,sha256=xm7BsuZB3VAETMKze4_JRWC3Kyejk-gGDL_wc9jZeio,5500
|
|
@@ -17,15 +17,15 @@ elody/object_configurations/elody_configuration.py,sha256=Bchwd1roBnrMDTdFkJUlUT
|
|
|
17
17
|
elody/object_configurations/job_configuration.py,sha256=T6QI2wE-u7ZcNpYE5XjmrCzIJglyKUGJsifaTn0WGyg,7512
|
|
18
18
|
elody/object_configurations/saved_search_configuration.py,sha256=ddOry4EqYOeEKRF7q2M_fHoqZv8DXpQjFq8VaZ7jhVI,732
|
|
19
19
|
elody/policies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
elody/policies/helpers.py,sha256=
|
|
21
|
-
elody/policies/permission_handler.py,sha256=
|
|
20
|
+
elody/policies/helpers.py,sha256=G9B7IHO1h-dNtAPsRtmHN-wYat7F-1ryk32mcCetsgI,2505
|
|
21
|
+
elody/policies/permission_handler.py,sha256=Ok5ojzwuLVe6H3wDusVeihSG2o4_HF9__1qlX01tdAU,15705
|
|
22
22
|
elody/policies/authentication/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
23
|
elody/policies/authentication/base_user_tenant_validation_policy.py,sha256=p7draxPCqly1vy7vnJX-gpmRfDeyaTxt9Cf0YpH9PZI,5829
|
|
24
24
|
elody/policies/authentication/multi_tenant_policy.py,sha256=g4ZYUQMmCjgLg09wj0-0lGKsJsRt7h4ppI25o1VdZHw,4039
|
|
25
25
|
elody/policies/authentication/x_user_headers_policy.py,sha256=AoOZH7cbVI9X7JTWmbH3HdXV2UUf0NJpAiuSI0qnQCI,1350
|
|
26
26
|
elody/policies/authorization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
27
|
elody/policies/authorization/filter_generic_objects_policy.py,sha256=mF32moh8hRetBgG8vQW-rz4xjoRQD2yOxdI740SFSUo,6522
|
|
28
|
-
elody/policies/authorization/filter_generic_objects_policy_v2.py,sha256=
|
|
28
|
+
elody/policies/authorization/filter_generic_objects_policy_v2.py,sha256=wEQTjXpJ1r5srnpl7Z-3PtEN0CsDUiQbOlVOWqJjUk0,4235
|
|
29
29
|
elody/policies/authorization/generic_object_detail_policy.py,sha256=y6g1i3vdKMKY4xS4H2m0e1DRztrivMEomx6NkDqA0Pk,3672
|
|
30
30
|
elody/policies/authorization/generic_object_mediafiles_policy.py,sha256=1-DMsV-FDkcrQCE4KL-SGlVHjTZSMPwYq1bWln2nXE4,2887
|
|
31
31
|
elody/policies/authorization/generic_object_metadata_policy.py,sha256=0r_DrNBBi4Bh6lMOXB9VqHDtQ9WEVRONigGG2fBFzDw,2882
|
|
@@ -36,13 +36,13 @@ elody/policies/authorization/mediafile_derivatives_policy.py,sha256=OwNpbS8i7-Lz
|
|
|
36
36
|
elody/policies/authorization/mediafile_download_policy.py,sha256=XMsKavBucmTh4W1kWOzpFWxJ_ZXgHVK1RS7JB4HjtQo,1979
|
|
37
37
|
elody/policies/authorization/multi_tenant_policy.py,sha256=SA9H7SBjzuh8mY3gYN7pDG8TV7hdI3GEUtNeiZeNL3M,3164
|
|
38
38
|
elody/policies/authorization/tenant_request_policy.py,sha256=dEgblwRAqwWVcE-O7Jn8hVL3OnwDlQhDEOcPlcElBrk,1185
|
|
39
|
-
elody-0.0.
|
|
39
|
+
elody-0.0.227.dist-info/licenses/LICENSE,sha256=gXf5dRMhNSbfLPYYTY_5hsZ1r7UU1OaKQEAQUhuIBkM,18092
|
|
40
40
|
tests/__init_.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
41
41
|
tests/data.py,sha256=Q3oxduf-E3m-Z5G_p3fcs8jVy6g10I7zXKL1m94UVMI,2906
|
|
42
42
|
tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
43
|
tests/unit/test_csv.py,sha256=NQaOhehfQ4GuXku0Y1SA8DYjJeqqidbF50zEHAi8RZA,15923
|
|
44
44
|
tests/unit/test_utils.py,sha256=g63szcEZyHhCOtrW4BnNbcgVca3oYPIOLjBdIzNwwN0,8784
|
|
45
|
-
elody-0.0.
|
|
46
|
-
elody-0.0.
|
|
47
|
-
elody-0.0.
|
|
48
|
-
elody-0.0.
|
|
45
|
+
elody-0.0.227.dist-info/METADATA,sha256=2DMnYmhSe3FZdq7pD9IYNADfse8_TmcnTPTj0HKLoYI,23358
|
|
46
|
+
elody-0.0.227.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
47
|
+
elody-0.0.227.dist-info/top_level.txt,sha256=E0mImupLj0KmtUUCXRYEoLDRaSkuiGaOIIseAa0oQ-M,21
|
|
48
|
+
elody-0.0.227.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|