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
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import re as regex
|
|
2
|
+
|
|
3
|
+
from elody.error_codes import ErrorCode, get_error_code, get_read, get_write
|
|
4
|
+
from flask import Request
|
|
5
|
+
from flask_restful import abort
|
|
6
|
+
from storage.storagemanager import StorageManager # pyright: ignore
|
|
7
|
+
from elody.util import get_item_metadata_value
|
|
8
|
+
from elody.exceptions import NotFoundException, NoTenantException
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TenantIdResolver:
|
|
12
|
+
def resolve(self, request, user):
|
|
13
|
+
endpoints = [
|
|
14
|
+
EntityGetRequest,
|
|
15
|
+
EntityPostRequest,
|
|
16
|
+
EntityDetailGetRequest,
|
|
17
|
+
EntityDetailUpdateRequest,
|
|
18
|
+
EntityDetailDeleteRequest,
|
|
19
|
+
EntityDetailGetRelationsRequest,
|
|
20
|
+
EntityDetailUpdateRelationsRequest,
|
|
21
|
+
EntityDetailCreateRelationsRequest,
|
|
22
|
+
EntityDetailDeleteRelationsRequest,
|
|
23
|
+
EntityDetailGetMetadataRequest,
|
|
24
|
+
EntityDetailUpdateMetadataRequest,
|
|
25
|
+
EntityDetailCreateMetadataRequest,
|
|
26
|
+
EntityDetailGetMediafilesRequest,
|
|
27
|
+
EntityDetailCreateMediafilesRequest,
|
|
28
|
+
MediafileGetRequest,
|
|
29
|
+
MediafilePostRequest,
|
|
30
|
+
MediafileDetailGetRequest,
|
|
31
|
+
MediafileDetailUpdateRequest,
|
|
32
|
+
MediafileDetailDeleteRequest,
|
|
33
|
+
MediafileDetailGetDerivativesRequest,
|
|
34
|
+
MediafileDetailCreateDerivativesRequest,
|
|
35
|
+
MediafileDetailDeleteDerivativesRequest,
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
for endpoint in endpoints:
|
|
39
|
+
tenant_id = endpoint().get_tenant_id(request)
|
|
40
|
+
if tenant_id != None:
|
|
41
|
+
relations = user.get("relations", [])
|
|
42
|
+
if len(relations) < 1:
|
|
43
|
+
raise NoTenantException(f"User is not attached to a tenant")
|
|
44
|
+
has_tenant_relation = any(
|
|
45
|
+
relation.get("type") == "hasTenant"
|
|
46
|
+
and relation.get("key") == tenant_id
|
|
47
|
+
for relation in relations
|
|
48
|
+
)
|
|
49
|
+
if has_tenant_relation:
|
|
50
|
+
return tenant_id
|
|
51
|
+
elif len(relations) == 1 and relations[0].get("key") == "tenant:super":
|
|
52
|
+
return "tenant:super"
|
|
53
|
+
else:
|
|
54
|
+
return relations[0].get("key")
|
|
55
|
+
return "tenant:super"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class BaseRequest:
|
|
59
|
+
def __init__(self) -> None:
|
|
60
|
+
self.storage = StorageManager().get_db_engine()
|
|
61
|
+
self.super_tenant_id = "tenant:super"
|
|
62
|
+
# TODO refactor this in a more generic way
|
|
63
|
+
self.global_types = [
|
|
64
|
+
"language",
|
|
65
|
+
"type",
|
|
66
|
+
"collectionForm",
|
|
67
|
+
"institution",
|
|
68
|
+
"tag",
|
|
69
|
+
"triple",
|
|
70
|
+
"person",
|
|
71
|
+
"externalRecord",
|
|
72
|
+
"verzameling",
|
|
73
|
+
"arches_record",
|
|
74
|
+
"manufacturer",
|
|
75
|
+
"photographer",
|
|
76
|
+
"creator",
|
|
77
|
+
"assetPart",
|
|
78
|
+
"set",
|
|
79
|
+
"download",
|
|
80
|
+
"license",
|
|
81
|
+
"share_link"
|
|
82
|
+
# TODO Mediafile should have a link to an asset
|
|
83
|
+
"mediafile",
|
|
84
|
+
"savedSearch",
|
|
85
|
+
"original_data",
|
|
86
|
+
"user",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
def _get_tenant_id_from_entity(self, entity_id):
|
|
90
|
+
entity_relations = self.storage.get_collection_item_relations(
|
|
91
|
+
"entities", entity_id
|
|
92
|
+
)
|
|
93
|
+
entity = self.storage.get_item_from_collection_by_id("entities", entity_id)
|
|
94
|
+
if not entity:
|
|
95
|
+
abort(
|
|
96
|
+
404,
|
|
97
|
+
message=f"{get_error_code(ErrorCode.ITEM_NOT_FOUND, get_read())} | id:{entity_id} - Item with id {entity_id} doesn't exist",
|
|
98
|
+
)
|
|
99
|
+
type = entity.get("type")
|
|
100
|
+
if type in self.global_types:
|
|
101
|
+
return "tenant:super"
|
|
102
|
+
if entity_relations:
|
|
103
|
+
for entity_relation in entity_relations:
|
|
104
|
+
if entity_relation.get("type") == "isIn":
|
|
105
|
+
tenant = self.storage.get_item_from_collection_by_id(
|
|
106
|
+
"entities", entity_relation.get("key")
|
|
107
|
+
)
|
|
108
|
+
return tenant["_id"]
|
|
109
|
+
if entity_relation.get("type") == "hasInstitution":
|
|
110
|
+
instution_relations = self.storage.get_collection_item_relations(
|
|
111
|
+
"entities", entity_relation.get("key")
|
|
112
|
+
)
|
|
113
|
+
for institution_relation in instution_relations:
|
|
114
|
+
if institution_relation.get("type") == "defines":
|
|
115
|
+
return institution_relation.get("key")
|
|
116
|
+
|
|
117
|
+
if entity and get_item_metadata_value(entity, "institution"):
|
|
118
|
+
return f"tenant:{get_item_metadata_value(entity, 'institution')}"
|
|
119
|
+
abort(
|
|
120
|
+
400,
|
|
121
|
+
message=f"{get_error_code(ErrorCode.ENTITY_HAS_NO_TENANT, get_read())} - Entity has no tenant, and is suppose to have one.",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def _get_tenant_id_from_mediafile(self, mediafile_id):
|
|
125
|
+
mediafile_relations = self.storage.get_collection_item_relations(
|
|
126
|
+
"mediafiles", mediafile_id
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
for relation in mediafile_relations:
|
|
130
|
+
if relation.get("type") == "belongsTo":
|
|
131
|
+
return self._get_tenant_id_from_entity(relation.get("key"))
|
|
132
|
+
|
|
133
|
+
# TODO Mediafile should have a link to an asset
|
|
134
|
+
def _get_tenant_id_from_body(self, item, soft_call=False):
|
|
135
|
+
if soft_call:
|
|
136
|
+
return "tenant:super"
|
|
137
|
+
if item.get("type") in self.global_types or item.get("type") == "institution":
|
|
138
|
+
return "tenant:super"
|
|
139
|
+
if item.get("type") == "asset":
|
|
140
|
+
for relation in item.get("relations", []):
|
|
141
|
+
if relation.get("type") in [
|
|
142
|
+
"hasArchesLink",
|
|
143
|
+
"hasAdlibLink",
|
|
144
|
+
"hasBrocadeLink",
|
|
145
|
+
]:
|
|
146
|
+
return "tenant:super"
|
|
147
|
+
institution_id = get_item_metadata_value(item, "institution")
|
|
148
|
+
if not institution_id:
|
|
149
|
+
for relation in item.get("relations", []):
|
|
150
|
+
if relation.get("type") == "hasInstitution":
|
|
151
|
+
institution_id = relation.get("key")
|
|
152
|
+
if institution_id:
|
|
153
|
+
return f"tenant:{institution_id}"
|
|
154
|
+
abort(
|
|
155
|
+
400,
|
|
156
|
+
message=f"{get_error_code(ErrorCode.ENTITY_HAS_NO_TENANT, get_read())} - Item in body doesn't have an institution.",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class EntityGetRequest(BaseRequest):
|
|
161
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
162
|
+
if (
|
|
163
|
+
regex.match(r"^/entities(?:\?(.*))?$", request.path)
|
|
164
|
+
and request.method == "GET"
|
|
165
|
+
):
|
|
166
|
+
return self.super_tenant_id
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class EntityPostRequest(BaseRequest):
|
|
171
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
172
|
+
if (
|
|
173
|
+
regex.match(r"^/entities(?:\?(.*))?$", request.path)
|
|
174
|
+
and request.method == "POST"
|
|
175
|
+
):
|
|
176
|
+
is_soft_call = request.args.get("soft") is not None
|
|
177
|
+
return self._get_tenant_id_from_body(request.json, soft_call=is_soft_call)
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class EntityDetailGetRequest(BaseRequest):
|
|
182
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
183
|
+
if regex.match(r"/entities/(.+)$", request.path) and request.method == "GET":
|
|
184
|
+
return self._get_tenant_id_from_entity(request.view_args.get("id"))
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class EntityDetailUpdateRequest(BaseRequest):
|
|
189
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
190
|
+
if regex.match(r"^/entities/(.+)$", request.path) and request.method in [
|
|
191
|
+
"PUT",
|
|
192
|
+
"PATCH",
|
|
193
|
+
]:
|
|
194
|
+
return self._get_tenant_id_from_entity(request.view_args.get("id"))
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class EntityDetailDeleteRequest(BaseRequest):
|
|
199
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
200
|
+
if (
|
|
201
|
+
regex.match(r"^/entities/([^/]+)$", request.path)
|
|
202
|
+
and request.method == "DELETE"
|
|
203
|
+
):
|
|
204
|
+
return self._get_tenant_id_from_entity(request.view_args.get("id"))
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class EntityDetailGetRelationsRequest(BaseRequest):
|
|
209
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
210
|
+
if (
|
|
211
|
+
regex.match(r"^/entities/(.+)/relations$", request.path)
|
|
212
|
+
and request.method == "GET"
|
|
213
|
+
):
|
|
214
|
+
return self._get_tenant_id_from_entity(request.view_args.get("id"))
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class EntityDetailUpdateRelationsRequest(BaseRequest):
|
|
219
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
220
|
+
if regex.match(
|
|
221
|
+
r"^/entities/(.+)/relations$", request.path
|
|
222
|
+
) and request.method in ["PUT", "PATCH"]:
|
|
223
|
+
return self._get_tenant_id_from_entity(request.view_args.get("id"))
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class EntityDetailCreateRelationsRequest(BaseRequest):
|
|
228
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
229
|
+
if (
|
|
230
|
+
regex.match(r"^/entities/(.+)/relations$", request.path)
|
|
231
|
+
and request.method == "POST"
|
|
232
|
+
):
|
|
233
|
+
return self._get_tenant_id_from_entity(request.view_args.get("id"))
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class EntityDetailDeleteRelationsRequest(BaseRequest):
|
|
238
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
239
|
+
if (
|
|
240
|
+
regex.match(r"^/entities/(.+)/relations$", request.path)
|
|
241
|
+
and request.method == "DELETE"
|
|
242
|
+
):
|
|
243
|
+
return self._get_tenant_id_from_entity(request.view_args.get("id"))
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class EntityDetailGetMetadataRequest(BaseRequest):
|
|
248
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
249
|
+
if (
|
|
250
|
+
regex.match(r"^/entities/(.+)/metadata$", request.path)
|
|
251
|
+
and request.method == "GET"
|
|
252
|
+
):
|
|
253
|
+
return self._get_tenant_id_from_entity(request.view_args.get("id"))
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class EntityDetailUpdateMetadataRequest(BaseRequest):
|
|
258
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
259
|
+
if regex.match(
|
|
260
|
+
r"^/entities/(.+)/metadata$", request.path
|
|
261
|
+
) and request.method in ["PUT", "PATCH"]:
|
|
262
|
+
return self._get_tenant_id_from_entity(request.view_args.get("id"))
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class EntityDetailCreateMetadataRequest(BaseRequest):
|
|
267
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
268
|
+
if (
|
|
269
|
+
regex.match(r"^/entities/(.+)/metadata$", request.path)
|
|
270
|
+
and request.method == "POST"
|
|
271
|
+
):
|
|
272
|
+
return self._get_tenant_id_from_entity(request.view_args.get("id"))
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class EntityDetailGetMediafilesRequest(BaseRequest):
|
|
277
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
278
|
+
if (
|
|
279
|
+
regex.match(r"^/entities/(.+)/mediafiles$", request.path)
|
|
280
|
+
and request.method == "GET"
|
|
281
|
+
):
|
|
282
|
+
return self._get_tenant_id_from_entity(request.view_args.get("id"))
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class EntityDetailCreateMediafilesRequest(BaseRequest):
|
|
287
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
288
|
+
if (
|
|
289
|
+
regex.match(r"^/entities/(.+)/mediafiles$", request.path)
|
|
290
|
+
and request.method == "POST"
|
|
291
|
+
):
|
|
292
|
+
return self._get_tenant_id_from_entity(request.view_args.get("id"))
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# Mediafiles
|
|
297
|
+
class MediafileGetRequest(BaseRequest):
|
|
298
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
299
|
+
if (
|
|
300
|
+
regex.match(r"^/mediafiles(?:\?(.*))?$", request.path)
|
|
301
|
+
and request.method == "GET"
|
|
302
|
+
):
|
|
303
|
+
return self.super_tenant_id
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class MediafilePostRequest(BaseRequest):
|
|
308
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
309
|
+
if (
|
|
310
|
+
regex.match(r"^/mediafiles(?:\?(.*))?$", request.path)
|
|
311
|
+
and request.method == "POST"
|
|
312
|
+
):
|
|
313
|
+
is_soft_call = request.args.get("soft") is not None
|
|
314
|
+
return self._get_tenant_id_from_body(request.json, soft_call=is_soft_call)
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class MediafileDetailGetRequest(BaseRequest):
|
|
319
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
320
|
+
if (
|
|
321
|
+
regex.match(r"^/mediafiles/([^/]+)$", request.path)
|
|
322
|
+
and request.method == "GET"
|
|
323
|
+
):
|
|
324
|
+
return self.super_tenant_id
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class MediafileDetailUpdateRequest(BaseRequest):
|
|
329
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
330
|
+
if regex.match(r"^/mediafiles/(.+)$", request.path) and request.method in [
|
|
331
|
+
"PUT",
|
|
332
|
+
"PATCH",
|
|
333
|
+
]:
|
|
334
|
+
return self._get_tenant_id_from_mediafile(request.view_args.get("id"))
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class MediafileDetailDeleteRequest(BaseRequest):
|
|
339
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
340
|
+
if (
|
|
341
|
+
regex.match(r"^/mediafiles/(.+)$", request.path)
|
|
342
|
+
and request.method == "DELETE"
|
|
343
|
+
):
|
|
344
|
+
return self._get_tenant_id_from_mediafile(request.view_args.get("id"))
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class MediafileDetailGetDerivativesRequest(BaseRequest):
|
|
349
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
350
|
+
if (
|
|
351
|
+
regex.match(r"^/mediafiles/(.+)/derivatives$", request.path)
|
|
352
|
+
and request.method == "GET"
|
|
353
|
+
):
|
|
354
|
+
return self.super_tenant_id
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class MediafileDetailCreateDerivativesRequest(BaseRequest):
|
|
359
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
360
|
+
if (
|
|
361
|
+
regex.match(r"^/mediafiles/(.+)/derivatives$", request.path)
|
|
362
|
+
and request.method == "POST"
|
|
363
|
+
):
|
|
364
|
+
return self._get_tenant_id_from_mediafile(request.view_args.get("id"))
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class MediafileDetailDeleteDerivativesRequest(BaseRequest):
|
|
369
|
+
def get_tenant_id(self, request: Request) -> str | None:
|
|
370
|
+
if (
|
|
371
|
+
regex.match(r"^/mediafiles/(.+)/derivatives$", request.path)
|
|
372
|
+
and request.method == "DELETE"
|
|
373
|
+
):
|
|
374
|
+
return self._get_tenant_id_from_mediafile(request.view_args.get("id"))
|
|
375
|
+
return None
|
elody/schemas.py
CHANGED
elody/util.py
CHANGED
|
@@ -1,9 +1,45 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import mimetypes
|
|
3
|
+
import re as regex
|
|
3
4
|
|
|
4
5
|
from cloudevents.conversion import to_dict
|
|
5
6
|
from cloudevents.http import CloudEvent
|
|
7
|
+
from collections.abc import MutableMapping, Iterable
|
|
8
|
+
from copy import deepcopy
|
|
6
9
|
from datetime import datetime, timezone
|
|
10
|
+
from os import getenv
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
URL_UNFRIENDLY_CHARS = {
|
|
14
|
+
" ": "%20",
|
|
15
|
+
"!": "%21",
|
|
16
|
+
"'": "%22",
|
|
17
|
+
"#": "%23",
|
|
18
|
+
"%": "%25",
|
|
19
|
+
"&": "%26",
|
|
20
|
+
'"': "%27",
|
|
21
|
+
"(": "%28",
|
|
22
|
+
")": "%29",
|
|
23
|
+
"*": "%2A",
|
|
24
|
+
"+": "%2B",
|
|
25
|
+
",": "%2C",
|
|
26
|
+
"/": "%2F",
|
|
27
|
+
";": "%3B",
|
|
28
|
+
"<": "%3C",
|
|
29
|
+
"=": "%3D",
|
|
30
|
+
">": "%3E",
|
|
31
|
+
"?": "%3F",
|
|
32
|
+
"@": "%40",
|
|
33
|
+
"[": "%5B",
|
|
34
|
+
"\\": "%5C",
|
|
35
|
+
"]": "%5D",
|
|
36
|
+
"^": "%5E",
|
|
37
|
+
"`": "%60",
|
|
38
|
+
"{": "%7B",
|
|
39
|
+
"|": "%7C",
|
|
40
|
+
"}": "%7D",
|
|
41
|
+
"~": "%7E",
|
|
42
|
+
}
|
|
7
43
|
|
|
8
44
|
|
|
9
45
|
class CustomJSONEncoder(json.JSONEncoder):
|
|
@@ -36,10 +72,46 @@ def custom_json_dumps(obj):
|
|
|
36
72
|
return json.dumps(obj, cls=CustomJSONEncoder)
|
|
37
73
|
|
|
38
74
|
|
|
39
|
-
def
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
75
|
+
def flatten_dict(object_lists, data: MutableMapping, parent_key=""):
|
|
76
|
+
if not parent_key:
|
|
77
|
+
data = deepcopy(data)
|
|
78
|
+
flat_dict = {}
|
|
79
|
+
for key, value in __flatten_dict_generator(object_lists, data, parent_key):
|
|
80
|
+
if key in flat_dict:
|
|
81
|
+
if not isinstance(flat_dict[key], list):
|
|
82
|
+
flat_dict[key] = [flat_dict[key]]
|
|
83
|
+
if isinstance(value, list):
|
|
84
|
+
flat_dict[key].extend(value)
|
|
85
|
+
else:
|
|
86
|
+
flat_dict[key].append(value)
|
|
87
|
+
else:
|
|
88
|
+
flat_dict[key] = value
|
|
89
|
+
return flat_dict
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def __flatten_dict_generator(object_lists, data: MutableMapping, parent_key):
|
|
93
|
+
for key, value in data.items():
|
|
94
|
+
flattened_key = f"{parent_key}.{key}" if parent_key else key
|
|
95
|
+
if isinstance(value, MutableMapping):
|
|
96
|
+
yield from flatten_dict(object_lists, value, flattened_key).items()
|
|
97
|
+
elif isinstance(value, Iterable) and not isinstance(value, (str, bytes)):
|
|
98
|
+
if all(isinstance(item, MutableMapping) for item in value):
|
|
99
|
+
item_key = None
|
|
100
|
+
for item in value:
|
|
101
|
+
item_key = item.get(object_lists.get(key))
|
|
102
|
+
if item_key:
|
|
103
|
+
if isinstance(item, MutableMapping):
|
|
104
|
+
yield from flatten_dict(
|
|
105
|
+
object_lists, item, f"{flattened_key}.{item_key}"
|
|
106
|
+
).items()
|
|
107
|
+
else:
|
|
108
|
+
yield f"{flattened_key}.{item_key}", item["value"]
|
|
109
|
+
if not item_key:
|
|
110
|
+
yield flattened_key, value
|
|
111
|
+
else:
|
|
112
|
+
yield flattened_key, value
|
|
113
|
+
else:
|
|
114
|
+
yield flattened_key, value
|
|
43
115
|
|
|
44
116
|
|
|
45
117
|
def get_raw_id(item):
|
|
@@ -58,6 +130,35 @@ def get_mimetype_from_filename(filename):
|
|
|
58
130
|
return mime if mime else "application/octet-stream"
|
|
59
131
|
|
|
60
132
|
|
|
133
|
+
def interpret_flat_key(flat_key: str, object_lists):
|
|
134
|
+
keys_info = []
|
|
135
|
+
index = 0
|
|
136
|
+
|
|
137
|
+
flat_key_parts = regex.split(r"\.(?=(?:[^`]*`[^`]*`)*[^`]*$)", flat_key)
|
|
138
|
+
while index < len(flat_key_parts):
|
|
139
|
+
info = {
|
|
140
|
+
"key": flat_key_parts[index],
|
|
141
|
+
"object_list": (
|
|
142
|
+
flat_key_parts[index]
|
|
143
|
+
if flat_key_parts[index] in object_lists.keys()
|
|
144
|
+
else ""
|
|
145
|
+
),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if info["object_list"]:
|
|
149
|
+
combined_key = "".join(
|
|
150
|
+
[f"{info['key']}." for info in keys_info if not info["object_list"]]
|
|
151
|
+
)
|
|
152
|
+
info["key"] = f"{combined_key}{info['key']}"
|
|
153
|
+
info["object_key"] = flat_key_parts[index + 1]
|
|
154
|
+
keys_info = [info for info in keys_info if info["object_list"]]
|
|
155
|
+
|
|
156
|
+
keys_info.append(info)
|
|
157
|
+
index += 2 if info["object_list"] else 1
|
|
158
|
+
|
|
159
|
+
return keys_info
|
|
160
|
+
|
|
161
|
+
|
|
61
162
|
def mediafile_is_public(mediafile):
|
|
62
163
|
publication_status = get_item_metadata_value(mediafile, "publication_status")
|
|
63
164
|
copyright_color = get_item_metadata_value(mediafile, "copyright_color")
|
|
@@ -68,6 +169,16 @@ def mediafile_is_public(mediafile):
|
|
|
68
169
|
] or copyright_color.lower() in ["green", "groen"]
|
|
69
170
|
|
|
70
171
|
|
|
172
|
+
def parse_string_to_bool(value):
|
|
173
|
+
if isinstance(value, str):
|
|
174
|
+
value_lower = value.strip().lower()
|
|
175
|
+
if value_lower in ["true", "yes", "1", "y"]:
|
|
176
|
+
return True
|
|
177
|
+
elif value_lower in ["false", "no", "0", "n"]:
|
|
178
|
+
return False
|
|
179
|
+
return value
|
|
180
|
+
|
|
181
|
+
|
|
71
182
|
def read_json_as_dict(filename, logger):
|
|
72
183
|
try:
|
|
73
184
|
with open(filename) as file:
|
|
@@ -77,16 +188,39 @@ def read_json_as_dict(filename, logger):
|
|
|
77
188
|
return {}
|
|
78
189
|
|
|
79
190
|
|
|
191
|
+
def parse_url_unfriendly_string(
|
|
192
|
+
input: str, *, replace_char=None, return_unfriendly_chars=False
|
|
193
|
+
):
|
|
194
|
+
unfriendly_chars = []
|
|
195
|
+
result = input
|
|
196
|
+
for char, encoded in URL_UNFRIENDLY_CHARS.items():
|
|
197
|
+
if char in input:
|
|
198
|
+
unfriendly_chars.append(char)
|
|
199
|
+
replacement = encoded if replace_char is None else replace_char
|
|
200
|
+
result = result.replace(char, replacement)
|
|
201
|
+
if return_unfriendly_chars:
|
|
202
|
+
return result, unfriendly_chars
|
|
203
|
+
return result
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def send_cloudevent(mq_client, source, routing_key, data, exchange_name=None):
|
|
207
|
+
event = to_dict(CloudEvent({"source": source, "type": routing_key}, data))
|
|
208
|
+
if getenv("AMQP_MANAGER", "amqpstorm_flask") in ["amqpstorm_flask"]:
|
|
209
|
+
mq_client.send(event, routing_key=routing_key, exchange_name=exchange_name)
|
|
210
|
+
else:
|
|
211
|
+
mq_client.send(event, routing_key=routing_key)
|
|
212
|
+
|
|
213
|
+
|
|
80
214
|
def signal_child_relation_changed(mq_client, collection, id):
|
|
81
215
|
data = {"parent_id": id, "collection": collection}
|
|
82
|
-
|
|
216
|
+
send_cloudevent(mq_client, "dams", "dams.child_relation_changed", data)
|
|
83
217
|
|
|
84
218
|
|
|
85
219
|
def signal_edge_changed(mq_client, parent_ids_from_changed_edges):
|
|
86
220
|
data = {
|
|
87
221
|
"location": f'/entities?ids={",".join(parent_ids_from_changed_edges)}&skip_relations=1'
|
|
88
222
|
}
|
|
89
|
-
|
|
223
|
+
send_cloudevent(mq_client, "dams", "dams.edge_changed", data)
|
|
90
224
|
|
|
91
225
|
|
|
92
226
|
def signal_entity_changed(mq_client, entity):
|
|
@@ -94,24 +228,44 @@ def signal_entity_changed(mq_client, entity):
|
|
|
94
228
|
"location": f"/entities/{get_raw_id(entity)}",
|
|
95
229
|
"type": entity.get("type", "unspecified"),
|
|
96
230
|
}
|
|
97
|
-
|
|
231
|
+
send_cloudevent(mq_client, "dams", "dams.entity_changed", data)
|
|
98
232
|
|
|
99
233
|
|
|
100
234
|
def signal_entity_deleted(mq_client, entity):
|
|
101
235
|
data = {"_id": get_raw_id(entity), "type": entity.get("type", "unspecified")}
|
|
102
|
-
|
|
236
|
+
send_cloudevent(mq_client, "dams", "dams.entity_deleted", data)
|
|
103
237
|
|
|
104
238
|
|
|
105
239
|
def signal_mediafiles_added_for_entity(mq_client, entity, mediafiles):
|
|
106
240
|
data = {"entity": entity, "mediafiles": mediafiles}
|
|
107
|
-
|
|
241
|
+
send_cloudevent(mq_client, "dams", "dams.mediafiles_added_for_entity", data)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def signal_relations_deleted_for_entity(mq_client, entity, relations):
|
|
245
|
+
data = {"entity": entity, "relations": relations}
|
|
246
|
+
send_cloudevent(mq_client, "dams", "dams.relations_deleted_for_entity", data)
|
|
108
247
|
|
|
109
248
|
|
|
110
249
|
def signal_mediafile_changed(mq_client, old_mediafile, mediafile):
|
|
111
250
|
data = {"old_mediafile": old_mediafile, "mediafile": mediafile}
|
|
112
|
-
|
|
251
|
+
send_cloudevent(mq_client, "dams", "dams.mediafile_changed", data)
|
|
113
252
|
|
|
114
253
|
|
|
115
254
|
def signal_mediafile_deleted(mq_client, mediafile, linked_entities):
|
|
116
255
|
data = {"mediafile": mediafile, "linked_entities": linked_entities}
|
|
117
|
-
|
|
256
|
+
send_cloudevent(mq_client, "dams", "dams.mediafile_deleted", data)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def signal_update_copyright_color_entity(mq_client, entity_id):
|
|
260
|
+
data = {"_id": entity_id}
|
|
261
|
+
send_cloudevent(mq_client, "dams", "dams.update_copyright_color_entity", data)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def signal_update_copyright_color_mediafile(mq_client, mediafile_id):
|
|
265
|
+
data = {"_id": mediafile_id}
|
|
266
|
+
send_cloudevent(mq_client, "dams", "dams.update_copyright_color_mediafile", data)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def signal_upload_file(mq_client, upload_links, selected_folder):
|
|
270
|
+
data = {"upload_links": upload_links, "selected_folder": selected_folder}
|
|
271
|
+
send_cloudevent(mq_client, "dams", "dams.upload_file", data)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: elody
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.162
|
|
4
4
|
Summary: elody SDK for Python
|
|
5
5
|
Author-email: Inuits <developers@inuits.eu>
|
|
6
|
-
License:
|
|
6
|
+
License: GNU GENERAL PUBLIC LICENSE
|
|
7
7
|
Version 2, June 1991
|
|
8
8
|
|
|
9
9
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
|
@@ -354,16 +354,21 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
354
354
|
Classifier: Programming Language :: Python :: 3.11
|
|
355
355
|
Description-Content-Type: text/markdown
|
|
356
356
|
License-File: LICENSE
|
|
357
|
-
Requires-Dist: certifi
|
|
358
|
-
Requires-Dist: charset-normalizer
|
|
359
|
-
Requires-Dist: idna
|
|
360
|
-
Requires-Dist: requests
|
|
361
|
-
Requires-Dist: urllib3
|
|
357
|
+
Requires-Dist: certifi>=2023.5.7
|
|
358
|
+
Requires-Dist: charset-normalizer>=3.2.0
|
|
359
|
+
Requires-Dist: idna>=3.4
|
|
360
|
+
Requires-Dist: requests>=2.31.0
|
|
361
|
+
Requires-Dist: urllib3>=1.26.16
|
|
362
362
|
Provides-Extra: loader
|
|
363
|
-
Requires-Dist:
|
|
364
|
-
Requires-Dist:
|
|
363
|
+
Requires-Dist: APScheduler>=3.10.4; extra == "loader"
|
|
364
|
+
Requires-Dist: cloudevents>=1.9.0; extra == "loader"
|
|
365
|
+
Requires-Dist: inuits-policy-based-auth>=10.0.1; extra == "loader"
|
|
366
|
+
Requires-Dist: jsonschema>=4.23.0; extra == "loader"
|
|
367
|
+
Requires-Dist: pytz>=2024.1; extra == "loader"
|
|
368
|
+
Requires-Dist: six>=1.16.0; extra == "loader"
|
|
369
|
+
Requires-Dist: tzlocal>=5.2; extra == "loader"
|
|
365
370
|
Provides-Extra: util
|
|
366
|
-
Requires-Dist: cloudevents
|
|
371
|
+
Requires-Dist: cloudevents>=1.9.0; extra == "util"
|
|
367
372
|
|
|
368
373
|
# elody SDK for Python
|
|
369
374
|
|