elody 0.0.63__py3-none-any.whl → 0.0.162__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/client.py +70 -23
- elody/csv.py +118 -21
- elody/error_codes.py +112 -0
- elody/exceptions.py +14 -0
- elody/job.py +95 -0
- elody/loader.py +33 -5
- elody/migration/__init__.py +0 -0
- elody/migration/base_object_migrator.py +18 -0
- elody/object_configurations/__init__.py +0 -0
- elody/object_configurations/base_object_configuration.py +174 -0
- elody/object_configurations/elody_configuration.py +144 -0
- elody/object_configurations/job_configuration.py +65 -0
- elody/policies/authentication/base_user_tenant_validation_policy.py +48 -15
- elody/policies/authorization/filter_generic_objects_policy.py +68 -22
- elody/policies/authorization/filter_generic_objects_policy_v2.py +166 -0
- elody/policies/authorization/generic_object_detail_policy.py +10 -27
- elody/policies/authorization/generic_object_mediafiles_policy.py +82 -0
- elody/policies/authorization/generic_object_metadata_policy.py +8 -27
- elody/policies/authorization/generic_object_relations_policy.py +12 -29
- elody/policies/authorization/generic_object_request_policy.py +56 -55
- elody/policies/authorization/generic_object_request_policy_v2.py +133 -0
- elody/policies/authorization/mediafile_derivatives_policy.py +92 -0
- elody/policies/authorization/mediafile_download_policy.py +71 -0
- elody/policies/authorization/multi_tenant_policy.py +14 -6
- elody/policies/authorization/tenant_request_policy.py +3 -1
- elody/policies/helpers.py +37 -0
- elody/policies/permission_handler.py +217 -199
- elody/policies/tenant_id_resolver.py +375 -0
- elody/schemas.py +0 -3
- elody/util.py +165 -11
- {elody-0.0.63.dist-info → elody-0.0.162.dist-info}/METADATA +16 -11
- elody-0.0.162.dist-info/RECORD +47 -0
- {elody-0.0.63.dist-info → elody-0.0.162.dist-info}/WHEEL +1 -1
- {elody-0.0.63.dist-info → elody-0.0.162.dist-info}/top_level.txt +1 -0
- tests/__init_.py +0 -0
- tests/data.py +74 -0
- tests/unit/__init__.py +0 -0
- tests/unit/test_csv.py +410 -0
- tests/unit/test_utils.py +293 -0
- elody-0.0.63.dist-info/RECORD +0 -27
- {elody-0.0.63.dist-info → elody-0.0.162.dist-info}/LICENSE +0 -0
tests/unit/test_utils.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from elody.util import (
|
|
5
|
+
parse_string_to_bool,
|
|
6
|
+
interpret_flat_key,
|
|
7
|
+
__flatten_dict_generator,
|
|
8
|
+
get_raw_id,
|
|
9
|
+
get_item_metadata_value,
|
|
10
|
+
get_mimetype_from_filename,
|
|
11
|
+
mediafile_is_public,
|
|
12
|
+
read_json_as_dict,
|
|
13
|
+
parse_url_unfriendly_string,
|
|
14
|
+
CustomJSONEncoder,
|
|
15
|
+
)
|
|
16
|
+
from data import mediafile1, mediafile2
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from unittest.mock import mock_open, patch, MagicMock
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_default_method_with_datetime():
|
|
22
|
+
encoder = CustomJSONEncoder()
|
|
23
|
+
dt = datetime(2023, 10, 1, 12, 0, 0, tzinfo=timezone.utc)
|
|
24
|
+
result = encoder.default(dt)
|
|
25
|
+
assert result == "2023-10-01T12:00:00+00:00"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_default_method_with_naive_datetime():
|
|
29
|
+
encoder = CustomJSONEncoder()
|
|
30
|
+
dt = datetime(2023, 10, 1, 12, 0, 0)
|
|
31
|
+
result = encoder.default(dt)
|
|
32
|
+
assert result == "2023-10-01T10:00:00+00:00"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_encode_method_with_non_datetime():
|
|
36
|
+
encoder = CustomJSONEncoder()
|
|
37
|
+
obj = {"key": "value"}
|
|
38
|
+
result = encoder.encode(obj)
|
|
39
|
+
expected_result = json.dumps(obj)
|
|
40
|
+
assert result == expected_result
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.parametrize(
|
|
44
|
+
"input_value, expected_output",
|
|
45
|
+
[
|
|
46
|
+
# Truthy values
|
|
47
|
+
("true", True),
|
|
48
|
+
("yes", True),
|
|
49
|
+
("1", True),
|
|
50
|
+
("Y", True),
|
|
51
|
+
(" true ", True),
|
|
52
|
+
# Falsy values
|
|
53
|
+
("false", False),
|
|
54
|
+
("no", False),
|
|
55
|
+
("0", False),
|
|
56
|
+
("N", False),
|
|
57
|
+
(" no ", False),
|
|
58
|
+
# Non-string inputs
|
|
59
|
+
(123, 123),
|
|
60
|
+
(None, None),
|
|
61
|
+
(True, True),
|
|
62
|
+
(False, False),
|
|
63
|
+
# Edge cases
|
|
64
|
+
("", ""), # Empty string
|
|
65
|
+
("maybe", "maybe"), # Unexpected string
|
|
66
|
+
("random", "random"), # Another unexpected string
|
|
67
|
+
],
|
|
68
|
+
)
|
|
69
|
+
def test_parse_string_to_bool(input_value, expected_output):
|
|
70
|
+
assert parse_string_to_bool(input_value) == expected_output
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_interpret_flat_key():
|
|
74
|
+
object_lists = {"user": ["name", "email"], "address": ["street", "city"]}
|
|
75
|
+
|
|
76
|
+
# Test case 1
|
|
77
|
+
flat_key = "user.name"
|
|
78
|
+
expected_output = [{"key": "user", "object_list": "user", "object_key": "name"}]
|
|
79
|
+
assert interpret_flat_key(flat_key, object_lists) == expected_output
|
|
80
|
+
|
|
81
|
+
# Test case 2
|
|
82
|
+
flat_key = "user.email"
|
|
83
|
+
expected_output = [{"key": "user", "object_list": "user", "object_key": "email"}]
|
|
84
|
+
assert interpret_flat_key(flat_key, object_lists) == expected_output
|
|
85
|
+
|
|
86
|
+
# Test case 3
|
|
87
|
+
flat_key = "address.street"
|
|
88
|
+
expected_output = [
|
|
89
|
+
{"key": "address", "object_list": "address", "object_key": "street"}
|
|
90
|
+
]
|
|
91
|
+
assert interpret_flat_key(flat_key, object_lists) == expected_output
|
|
92
|
+
|
|
93
|
+
# Test case 4
|
|
94
|
+
flat_key = "address.city"
|
|
95
|
+
expected_output = [
|
|
96
|
+
{"key": "address", "object_list": "address", "object_key": "city"}
|
|
97
|
+
]
|
|
98
|
+
assert interpret_flat_key(flat_key, object_lists) == expected_output
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_flatten_dict_generator():
|
|
102
|
+
object_lists = {"items": "id"}
|
|
103
|
+
|
|
104
|
+
data = {
|
|
105
|
+
"name": "John",
|
|
106
|
+
"address": {"street": "123 Main St", "city": "Anytown"},
|
|
107
|
+
"items": [{"id": "1", "value": "item1"}, {"id": "2", "value": "item2"}],
|
|
108
|
+
"tags": ["tag1", "tag2"],
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
expected_output = {
|
|
112
|
+
"name": "John",
|
|
113
|
+
"address.street": "123 Main St",
|
|
114
|
+
"address.city": "Anytown",
|
|
115
|
+
"items.1.id": "1",
|
|
116
|
+
"items.1.value": "item1",
|
|
117
|
+
"items.2.id": "2",
|
|
118
|
+
"items.2.value": "item2",
|
|
119
|
+
"tags": ["tag1", "tag2"],
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
result = dict(__flatten_dict_generator(object_lists, data, ""))
|
|
123
|
+
assert result == expected_output
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_flatten_dict_generator_nested():
|
|
127
|
+
object_lists = {"items": "id"}
|
|
128
|
+
|
|
129
|
+
data = {
|
|
130
|
+
"user": {
|
|
131
|
+
"name": "John",
|
|
132
|
+
"address": {"street": "123 Main St", "city": "Anytown"},
|
|
133
|
+
"items": [{"id": "1", "value": "item1"}, {"id": "2", "value": "item2"}],
|
|
134
|
+
"tags": ["tag1", "tag2"],
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
expected_output = {
|
|
139
|
+
"user.name": "John",
|
|
140
|
+
"user.address.street": "123 Main St",
|
|
141
|
+
"user.address.city": "Anytown",
|
|
142
|
+
"user.items.1.id": "1",
|
|
143
|
+
"user.items.1.value": "item1",
|
|
144
|
+
"user.items.2.id": "2",
|
|
145
|
+
"user.items.2.value": "item2",
|
|
146
|
+
"user.tags": ["tag1", "tag2"],
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
result = dict(__flatten_dict_generator(object_lists, data, ""))
|
|
150
|
+
assert result == expected_output
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_flatten_dict_generator_no_items():
|
|
154
|
+
object_lists = {"items": "id"}
|
|
155
|
+
|
|
156
|
+
data = {
|
|
157
|
+
"name": "John",
|
|
158
|
+
"address": {"street": "123 Main St", "city": "Anytown"},
|
|
159
|
+
"items": [{"value": "item1"}, {"value": "item2"}],
|
|
160
|
+
"tags": ["tag1", "tag2"],
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
expected_output = {
|
|
164
|
+
"name": "John",
|
|
165
|
+
"address.street": "123 Main St",
|
|
166
|
+
"address.city": "Anytown",
|
|
167
|
+
"items": [{"value": "item1"}, {"value": "item2"}],
|
|
168
|
+
"tags": ["tag1", "tag2"],
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
result = dict(__flatten_dict_generator(object_lists, data, ""))
|
|
172
|
+
assert result == expected_output
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@pytest.mark.parametrize(
|
|
176
|
+
"mediafile_modification, expected_output",
|
|
177
|
+
[
|
|
178
|
+
(lambda mf: mf, "2476c1a2-c323-499e-9e28-6d1562296f3f"),
|
|
179
|
+
(lambda mf: mf.update({"_key": "test-key"}) or mf, "test-key"),
|
|
180
|
+
],
|
|
181
|
+
)
|
|
182
|
+
def test_get_raw_id(mediafile_modification, expected_output):
|
|
183
|
+
modified_mediafile = mediafile_modification(mediafile1.copy())
|
|
184
|
+
assert get_raw_id(modified_mediafile) == expected_output
|
|
185
|
+
|
|
186
|
+
# Test for KeyError
|
|
187
|
+
modified_mediafile.pop("_key", None)
|
|
188
|
+
modified_mediafile.pop("_id", None)
|
|
189
|
+
with pytest.raises(KeyError):
|
|
190
|
+
get_raw_id(modified_mediafile)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@pytest.mark.parametrize(
|
|
194
|
+
"metadata_key, expected_output",
|
|
195
|
+
[
|
|
196
|
+
("copyright_color", "green"),
|
|
197
|
+
("copyright_color_calculation", "automatic"),
|
|
198
|
+
("not_existing", ""),
|
|
199
|
+
],
|
|
200
|
+
)
|
|
201
|
+
def test_get_item_metadata_value(metadata_key, expected_output):
|
|
202
|
+
assert get_item_metadata_value(mediafile1, metadata_key) == expected_output
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@pytest.mark.parametrize(
|
|
206
|
+
"filename, expected_output",
|
|
207
|
+
[
|
|
208
|
+
("test.jpg", "image/jpeg"),
|
|
209
|
+
("test.png", "image/png"),
|
|
210
|
+
("test.tif", "image/tiff"),
|
|
211
|
+
("test.mp4", "video/mp4"),
|
|
212
|
+
("test", "application/octet-stream"),
|
|
213
|
+
],
|
|
214
|
+
)
|
|
215
|
+
def test_get_mimetype_from_filename(filename, expected_output):
|
|
216
|
+
assert get_mimetype_from_filename(filename) == expected_output
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@pytest.mark.parametrize(
|
|
220
|
+
"mediafile, expected_output",
|
|
221
|
+
[
|
|
222
|
+
(mediafile1, True),
|
|
223
|
+
(mediafile2, False),
|
|
224
|
+
],
|
|
225
|
+
)
|
|
226
|
+
def test_mediafile_is_public(mediafile, expected_output):
|
|
227
|
+
assert mediafile_is_public(mediafile) == expected_output
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@pytest.fixture
|
|
231
|
+
def logger():
|
|
232
|
+
return MagicMock()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_read_json_as_dict_success(logger):
|
|
236
|
+
filename = "test.json"
|
|
237
|
+
json_content = '{"key": "value"}'
|
|
238
|
+
expected_output = {"key": "value"}
|
|
239
|
+
|
|
240
|
+
with patch("builtins.open", mock_open(read_data=json_content)):
|
|
241
|
+
result = read_json_as_dict(filename, logger)
|
|
242
|
+
assert result == expected_output
|
|
243
|
+
logger.error.assert_not_called()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_read_json_as_dict_file_not_found(logger):
|
|
247
|
+
filename = "test.json"
|
|
248
|
+
|
|
249
|
+
with patch("builtins.open", side_effect=FileNotFoundError):
|
|
250
|
+
result = read_json_as_dict(filename, logger)
|
|
251
|
+
assert result == {}
|
|
252
|
+
logger.error.assert_called_once_with(f"Could not read {filename} as a dict: ")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_read_json_as_dict_invalid_json(logger):
|
|
256
|
+
filename = "test.json"
|
|
257
|
+
invalid_json_content = '{"key": "value"' # Missing closing brace
|
|
258
|
+
|
|
259
|
+
with patch("builtins.open", mock_open(read_data=invalid_json_content)):
|
|
260
|
+
result = read_json_as_dict(filename, logger)
|
|
261
|
+
assert result == {}
|
|
262
|
+
logger.error.assert_called_once_with(
|
|
263
|
+
f"Could not read {filename} as a dict: Expecting ',' delimiter: line 1 column 16 (char 15)"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@pytest.mark.parametrize(
|
|
268
|
+
"input_str, replace_char, return_unfriendly_chars, expected_output",
|
|
269
|
+
[
|
|
270
|
+
# Test cases with default behavior
|
|
271
|
+
("Hello World!", None, False, "Hello%20World%21"),
|
|
272
|
+
("abc/def", None, False, "abc%2Fdef"),
|
|
273
|
+
# Test cases with replace_char specified
|
|
274
|
+
("Hello World!", "-", False, "Hello-World-"),
|
|
275
|
+
("abc/def", "*", False, "abc*def"),
|
|
276
|
+
# Test cases with return_unfriendly_chars enabled
|
|
277
|
+
("Hello World!", None, True, ("Hello%20World%21", [" ", "!"])),
|
|
278
|
+
("abc/def", None, True, ("abc%2Fdef", ["/"])),
|
|
279
|
+
("Hello World!", "-", True, ("Hello-World-", [" ", "!"])),
|
|
280
|
+
# Edge cases
|
|
281
|
+
("", None, False, ""),
|
|
282
|
+
("NoSpecialChars", None, False, "NoSpecialChars"),
|
|
283
|
+
],
|
|
284
|
+
)
|
|
285
|
+
def test_parse_url_unfriendly_string(
|
|
286
|
+
input_str, replace_char, return_unfriendly_chars, expected_output
|
|
287
|
+
):
|
|
288
|
+
result = parse_url_unfriendly_string(
|
|
289
|
+
input_str,
|
|
290
|
+
replace_char=replace_char,
|
|
291
|
+
return_unfriendly_chars=return_unfriendly_chars,
|
|
292
|
+
)
|
|
293
|
+
assert result == expected_output
|
elody-0.0.63.dist-info/RECORD
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
elody/__init__.py,sha256=d0Q6Fn44e7wFfLabDOBxpcJ1DPKWlFunGYDUBmO-4hA,22
|
|
3
|
-
elody/client.py,sha256=cgkmYHPvRluvyVHJrf0XwNCI1pNBpsvdARFRXUabUG8,6863
|
|
4
|
-
elody/csv.py,sha256=FlFm_vAXx926R486x9uAjlERnbhT-ZUeFG0tkeBlFoM,8883
|
|
5
|
-
elody/exceptions.py,sha256=PQHs4yi5jFCZYWJz_Hujg6KR5tmDqXmbxlLUgXdWbvY,695
|
|
6
|
-
elody/loader.py,sha256=55RB0sAybNwRQPCOZeo69W49SSGbuRRY_1EE1oQuJj8,4382
|
|
7
|
-
elody/schemas.py,sha256=iHWrXWq7b5XOygN3RsWxrkUR-SROkHtmVcgAkzZc5LI,4721
|
|
8
|
-
elody/util.py,sha256=V9e0lMq5qRrkYLZW2XFDlVZxLCwCp4bhZpDlG_HouqM,3661
|
|
9
|
-
elody/validator.py,sha256=G7Ya538EJHCFzOxEri2OcFMabfLBCtTKxuf4os_KuNw,260
|
|
10
|
-
elody/policies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
elody/policies/permission_handler.py,sha256=sx4X9lmkog7kb1wYFbVEIF9IU8EpOPmLNUwLkBkF94A,8321
|
|
12
|
-
elody/policies/authentication/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
elody/policies/authentication/base_user_tenant_validation_policy.py,sha256=V4V4fAPuICAHhyqJ06yx1i-ZsBbVYEfn1vWMISrut00,3715
|
|
14
|
-
elody/policies/authentication/multi_tenant_policy.py,sha256=jmV1RTsApt2IV8BgSRcfnIjWHhDtFjW4OHJgWNUDKRw,3822
|
|
15
|
-
elody/policies/authorization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
elody/policies/authorization/filter_generic_objects_policy.py,sha256=c4_6BRoDdSyICNU3zOlMvwwgU_tCKJLdlBQ3RDwPj5E,4584
|
|
17
|
-
elody/policies/authorization/generic_object_detail_policy.py,sha256=DJi3-pH7zOYB5s0kzE05jfjTdxNUQbkZvuYcUz_hC2g,4125
|
|
18
|
-
elody/policies/authorization/generic_object_metadata_policy.py,sha256=8Hh5KduWKcmhsfcZAZVVRW5n4uYfuFmS7qBM7r9iGDg,3370
|
|
19
|
-
elody/policies/authorization/generic_object_relations_policy.py,sha256=fwiaGGM_JAZhiWFRuHmYkkgB-7sBurYPIi9Lli1y4PI,4187
|
|
20
|
-
elody/policies/authorization/generic_object_request_policy.py,sha256=kv2Big7VCQ7W79juf7W_Kcv_8drddW711vM4v7ZRBoA,5346
|
|
21
|
-
elody/policies/authorization/multi_tenant_policy.py,sha256=o138uz3sANyjlQc4qb-6inbi9YHZbc1bs5GFr8QL4nY,2685
|
|
22
|
-
elody/policies/authorization/tenant_request_policy.py,sha256=jQU8y6S0i96M4l4jMYRx_EwmgFvOkXdfskrvZIr5uX8,1177
|
|
23
|
-
elody-0.0.63.dist-info/LICENSE,sha256=gXf5dRMhNSbfLPYYTY_5hsZ1r7UU1OaKQEAQUhuIBkM,18092
|
|
24
|
-
elody-0.0.63.dist-info/METADATA,sha256=s65eNEAB-w6KrMfTHRILaBdigXHskxCraczzlIk5xKY,23078
|
|
25
|
-
elody-0.0.63.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
26
|
-
elody-0.0.63.dist-info/top_level.txt,sha256=I4Aci8dpg1DrbByPnQsMmuWsBXWCnrwJDfGWUcKG9p8,15
|
|
27
|
-
elody-0.0.63.dist-info/RECORD,,
|
|
File without changes
|