elody 0.0.63__py3-none-any.whl → 0.0.163__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.
Files changed (41) hide show
  1. elody/client.py +70 -23
  2. elody/csv.py +128 -33
  3. elody/error_codes.py +112 -0
  4. elody/exceptions.py +14 -0
  5. elody/job.py +95 -0
  6. elody/loader.py +33 -5
  7. elody/migration/__init__.py +0 -0
  8. elody/migration/base_object_migrator.py +18 -0
  9. elody/object_configurations/__init__.py +0 -0
  10. elody/object_configurations/base_object_configuration.py +174 -0
  11. elody/object_configurations/elody_configuration.py +144 -0
  12. elody/object_configurations/job_configuration.py +65 -0
  13. elody/policies/authentication/base_user_tenant_validation_policy.py +48 -15
  14. elody/policies/authorization/filter_generic_objects_policy.py +68 -22
  15. elody/policies/authorization/filter_generic_objects_policy_v2.py +166 -0
  16. elody/policies/authorization/generic_object_detail_policy.py +10 -27
  17. elody/policies/authorization/generic_object_mediafiles_policy.py +82 -0
  18. elody/policies/authorization/generic_object_metadata_policy.py +8 -27
  19. elody/policies/authorization/generic_object_relations_policy.py +12 -29
  20. elody/policies/authorization/generic_object_request_policy.py +56 -55
  21. elody/policies/authorization/generic_object_request_policy_v2.py +133 -0
  22. elody/policies/authorization/mediafile_derivatives_policy.py +92 -0
  23. elody/policies/authorization/mediafile_download_policy.py +71 -0
  24. elody/policies/authorization/multi_tenant_policy.py +14 -6
  25. elody/policies/authorization/tenant_request_policy.py +3 -1
  26. elody/policies/helpers.py +37 -0
  27. elody/policies/permission_handler.py +217 -199
  28. elody/policies/tenant_id_resolver.py +375 -0
  29. elody/schemas.py +0 -3
  30. elody/util.py +165 -11
  31. {elody-0.0.63.dist-info → elody-0.0.163.dist-info}/METADATA +16 -11
  32. elody-0.0.163.dist-info/RECORD +47 -0
  33. {elody-0.0.63.dist-info → elody-0.0.163.dist-info}/WHEEL +1 -1
  34. {elody-0.0.63.dist-info → elody-0.0.163.dist-info}/top_level.txt +1 -0
  35. tests/__init_.py +0 -0
  36. tests/data.py +74 -0
  37. tests/unit/__init__.py +0 -0
  38. tests/unit/test_csv.py +410 -0
  39. tests/unit/test_utils.py +293 -0
  40. elody-0.0.63.dist-info/RECORD +0 -27
  41. {elody-0.0.63.dist-info → elody-0.0.163.dist-info}/LICENSE +0 -0
elody/job.py ADDED
@@ -0,0 +1,95 @@
1
+ from elody.object_configurations.job_configuration import JobConfiguration
2
+
3
+
4
+ _config = JobConfiguration()
5
+ _create = _config.crud()["creator"]
6
+ _post_crud_hook = _config.crud()["post_crud_hook"]
7
+
8
+
9
+ def start_job(
10
+ name,
11
+ job_type,
12
+ *,
13
+ get_rabbit,
14
+ get_user_context=None,
15
+ user_email=None,
16
+ parent_id=None,
17
+ id_of_document_job_was_initiated_for=None,
18
+ type_of_document_job_was_initiated_for=None,
19
+ ) -> str:
20
+ relations = []
21
+ if parent_id:
22
+ relations.append({"key": parent_id, "type": "hasParentJob"})
23
+ if id_of_document_job_was_initiated_for and type_of_document_job_was_initiated_for:
24
+ relations.append(
25
+ {"key": id_of_document_job_was_initiated_for, "type": "isJobOf"}
26
+ )
27
+
28
+ job = _create(
29
+ {
30
+ "metadata": [
31
+ {"key": "name", "value": name},
32
+ {"key": "status", "value": "running"},
33
+ {"key": "type", "value": job_type},
34
+ ],
35
+ "relations": relations,
36
+ "type": "job",
37
+ },
38
+ get_user_context=get_user_context
39
+ or (lambda: type("UserContext", (object,), {"email": user_email})()),
40
+ )
41
+ del job["computed_values"]["created_at"]
42
+
43
+ _post_crud_hook(
44
+ crud="create", document=job, parent_id=parent_id, get_rabbit=get_rabbit
45
+ )
46
+ __patch_document_job_was_initiated_for(
47
+ id_of_document_job_was_initiated_for,
48
+ type_of_document_job_was_initiated_for,
49
+ get_rabbit,
50
+ )
51
+ return job["_id"]
52
+
53
+
54
+ def finish_job(
55
+ id,
56
+ id_of_document_job_was_initiated_for=None,
57
+ type_of_document_job_was_initiated_for=None,
58
+ *,
59
+ get_rabbit,
60
+ ):
61
+ document = {
62
+ "id": id,
63
+ "patch": {
64
+ "metadata": [{"key": "status", "value": "finished"}],
65
+ "relations": ([] if id_of_document_job_was_initiated_for else []),
66
+ },
67
+ }
68
+ _post_crud_hook(crud="update", document=document, get_rabbit=get_rabbit)
69
+ __patch_document_job_was_initiated_for(
70
+ id_of_document_job_was_initiated_for,
71
+ type_of_document_job_was_initiated_for,
72
+ get_rabbit,
73
+ )
74
+
75
+
76
+ def fail_job(id, exception_message, *, get_rabbit):
77
+ document = {
78
+ "id": id,
79
+ "patch": {
80
+ "metadata": [
81
+ {"key": "info", "value": exception_message},
82
+ {"key": "status", "value": "failed"},
83
+ ]
84
+ },
85
+ }
86
+ _post_crud_hook(crud="update", document=document, get_rabbit=get_rabbit)
87
+
88
+
89
+ def __patch_document_job_was_initiated_for(id, type, get_rabbit):
90
+ if id and type:
91
+ document = {
92
+ "document_info_job_was_initiated_for": {"id": id, "type": type},
93
+ "patch": {"relations": [{"key": id, "type": "hasJob"}]},
94
+ }
95
+ _post_crud_hook(crud="update", document=document, get_rabbit=get_rabbit)
elody/loader.py CHANGED
@@ -2,7 +2,7 @@ import elody.util as util
2
2
  import json
3
3
  import os
4
4
 
5
- from elody.policies.permission_handler import set_permissions
5
+ from apscheduler.triggers.cron import CronTrigger
6
6
  from importlib import import_module
7
7
  from inuits_policy_based_auth.exceptions import (
8
8
  PolicyFactoryException,
@@ -17,9 +17,32 @@ def load_apps(flask_app, logger):
17
17
  flask_app.register_blueprint(api_bp)
18
18
 
19
19
 
20
- def load_policies(policy_factory, logger, permissions={}):
20
+ def load_jobs(scheduler, logger):
21
+ apps = util.read_json_as_dict(os.getenv("APPS_MANIFEST"), logger)
22
+ for app in apps:
23
+ for job, job_properties in apps[app].get("jobs", {}).items():
24
+ try:
25
+ job_class = __get_class_from_module(
26
+ import_module(f"apps.{app}.cron_jobs.{job}")
27
+ )
28
+ scheduler.add_job(
29
+ job_class(),
30
+ CronTrigger.from_crontab(
31
+ job_properties.get("expression", "0 0 * * *")
32
+ ),
33
+ )
34
+ except ModuleNotFoundError:
35
+ pass
36
+
37
+
38
+ def load_policies(
39
+ policy_factory, logger, permissions: dict = {}, placeholders: list[str] = []
40
+ ):
21
41
  if permissions:
22
- set_permissions(permissions)
42
+ from elody.policies.permission_handler import set_permissions
43
+
44
+ set_permissions(permissions, placeholders)
45
+
23
46
  apps = util.read_json_as_dict(os.getenv("APPS_MANIFEST", ""), logger)
24
47
  for app in apps:
25
48
  try:
@@ -43,6 +66,7 @@ def load_policies(policy_factory, logger, permissions={}):
43
66
 
44
67
 
45
68
  def load_queues(logger):
69
+ import_module("resources.queues")
46
70
  apps = util.read_json_as_dict(os.getenv("APPS_MANIFEST"), logger)
47
71
  for app in apps:
48
72
  try:
@@ -66,11 +90,15 @@ def __get_class(app, auth_type, policy_module_name):
66
90
  pass
67
91
  else:
68
92
  raise ModuleNotFoundError(f"Policy {policy_module_name} not found")
69
- policy_class_name = module.__name__.split(".")[-1].title().replace("_", "")
70
- policy = getattr(module, policy_class_name)
93
+ policy = __get_class_from_module(module)
71
94
  return policy
72
95
 
73
96
 
97
+ def __get_class_from_module(module):
98
+ class_name = module.__name__.split(".")[-1].title().replace("_", "")
99
+ return getattr(module, class_name)
100
+
101
+
74
102
  def __instantiate_authentication_policy(policy_module_name, policy, logger):
75
103
  allow_anonymous_users = os.getenv("ALLOW_ANONYMOUS_USERS", False) in [
76
104
  "True",
File without changes
@@ -0,0 +1,18 @@
1
+ class BaseObjectMigrator:
2
+ def __init__(self, *, status, silent=False):
3
+ self._status = status
4
+ self._silent = silent
5
+
6
+ @property
7
+ def status(self):
8
+ return self._status
9
+
10
+ @property
11
+ def silent(self):
12
+ return self._silent
13
+
14
+ def bulk_migrate(self, *, dry_run=False): # pyright: ignore
15
+ pass
16
+
17
+ def lazy_migrate(self, item, *, dry_run=False): # pyright: ignore
18
+ return item
File without changes
@@ -0,0 +1,174 @@
1
+ from abc import ABC, abstractmethod
2
+ from copy import deepcopy
3
+ from elody.migration.base_object_migrator import BaseObjectMigrator
4
+
5
+
6
+ class BaseObjectConfiguration(ABC):
7
+ SCHEMA_TYPE = "elody"
8
+ SCHEMA_VERSION = 1
9
+
10
+ @abstractmethod
11
+ def crud(self):
12
+ return {
13
+ "creator": lambda post_body, **kwargs: post_body,
14
+ "document_content_patcher": lambda *, document, content, overwrite=False, **kwargs: self._document_content_patcher(
15
+ document=document,
16
+ content=content,
17
+ overwrite=overwrite,
18
+ **kwargs,
19
+ ),
20
+ "nested_matcher_builder": lambda object_lists, keys_info, value: self.__build_nested_matcher(
21
+ object_lists, keys_info, value
22
+ ),
23
+ "post_crud_hook": lambda **kwargs: None,
24
+ "pre_crud_hook": lambda *, document, **kwargs: document,
25
+ "storage_type": "db",
26
+ }
27
+
28
+ @abstractmethod
29
+ def document_info(self):
30
+ return {}
31
+
32
+ @abstractmethod
33
+ def logging(self, flat_document, **kwargs):
34
+ info_labels = {
35
+ "uuid": flat_document.get("_id"),
36
+ "type": flat_document.get("type"),
37
+ "schema": f"{flat_document.get('schema.type')}:{flat_document.get('schema.version')}",
38
+ }
39
+ try:
40
+ from policy_factory import get_user_context # pyright: ignore
41
+
42
+ user_context = get_user_context()
43
+ info_labels["http_method"] = user_context.bag.get("http_method")
44
+ info_labels["requested_endpoint"] = user_context.bag.get(
45
+ "requested_endpoint"
46
+ )
47
+ info_labels["full_path"] = user_context.bag.get("full_path")
48
+ info_labels["preferred_username"] = user_context.preferred_username
49
+ info_labels["email"] = user_context.email
50
+ info_labels["user_roles"] = ", ".join(user_context.x_tenant.roles)
51
+ info_labels["x_tenant"] = user_context.x_tenant.id
52
+ except Exception:
53
+ pass
54
+ return {"info_labels": info_labels, "loki_indexed_info_labels": {}}
55
+
56
+ @abstractmethod
57
+ def migration(self):
58
+ return BaseObjectMigrator(status="disabled")
59
+
60
+ @abstractmethod
61
+ def serialization(self, from_format, to_format):
62
+ def serializer(document, **_):
63
+ return document
64
+
65
+ return serializer
66
+
67
+ @abstractmethod
68
+ def validation(self):
69
+ def validator(http_method, content, item, **_):
70
+ pass
71
+
72
+ return "function", validator
73
+
74
+ def _document_content_patcher(
75
+ self, *, document, content, overwrite=False, **kwargs
76
+ ):
77
+ raise NotImplementedError(
78
+ "Provide concrete implementation in child object configuration"
79
+ )
80
+
81
+ def _merge_object_lists(self, source, target, object_list_key):
82
+ for target_item in target:
83
+ for source_item in source:
84
+ if source_item[object_list_key] == target_item[object_list_key]:
85
+ source.remove(source_item)
86
+ return [*source, *target]
87
+
88
+ def _get_user_context_id(self):
89
+ try:
90
+ from policy_factory import get_user_context # pyright: ignore
91
+
92
+ return get_user_context().id
93
+ except Exception:
94
+ return None
95
+
96
+ def _sanitize_document(self, *, document, **kwargs):
97
+ sanitized_document = {}
98
+ document_deepcopy = deepcopy(document)
99
+ for key, value in document_deepcopy.items():
100
+ if isinstance(value, dict):
101
+ sanitized_value = BaseObjectConfiguration._sanitize_document(
102
+ self, document=value
103
+ )
104
+ if sanitized_value:
105
+ sanitized_document[key] = sanitized_value
106
+ elif value:
107
+ sanitized_document[key] = value
108
+ return sanitized_document
109
+
110
+ def _should_create_history_object(self):
111
+ try:
112
+ from policy_factory import get_user_context # pyright: ignore
113
+
114
+ get_user_context()
115
+ return bool(self.crud().get("collection_history"))
116
+ except Exception:
117
+ return False
118
+
119
+ def _sort_document_keys(self, document):
120
+ def sort_keys(data):
121
+ if isinstance(data, dict):
122
+ sorted_items = {key: data.pop(key) for key in sorted(data.keys())}
123
+ for key, value in sorted_items.items():
124
+ data[key] = sort_keys(value)
125
+ return data
126
+ elif isinstance(data, list):
127
+ if all(isinstance(i, str) for i in data):
128
+ data.sort()
129
+ return data
130
+ else:
131
+ for index, item in enumerate(data):
132
+ data[index] = sort_keys(item)
133
+ return data
134
+ else:
135
+ return data
136
+
137
+ for key, value in self.document_info().get("object_lists", {}).items():
138
+ if document.get(key):
139
+ document[key] = sorted(
140
+ document[key], key=lambda property: property[value]
141
+ )
142
+ sort_keys(document)
143
+ return document
144
+
145
+ def __build_nested_matcher(self, object_lists, keys_info, value, index=0):
146
+ if index == 0 and not any(info["object_list"] for info in keys_info):
147
+ if value in ["ANY_MATCH", "NONE_MATCH"]:
148
+ value = {"$exists": value == "ANY_MATCH"}
149
+ return {".".join(info["key"] for info in keys_info): value}
150
+
151
+ info = keys_info[index]
152
+
153
+ if info["object_list"]:
154
+ elem_match = {
155
+ "$elemMatch": {
156
+ object_lists[info["object_list"]]: info["object_key"],
157
+ **self.__build_nested_matcher(
158
+ object_lists, keys_info[index + 1 :], value, 0
159
+ ),
160
+ }
161
+ }
162
+ if value in ["ANY_MATCH", "NONE_MATCH"]:
163
+ elem_match_with_exists_operator = deepcopy(elem_match)
164
+ del elem_match["$elemMatch"][keys_info[index + 1]["key"]]
165
+ if value == "NONE_MATCH":
166
+ return {
167
+ "NOR_MATCHER": [
168
+ {info["key"]: {"$all": [elem_match]}},
169
+ {info["key"]: {"$all": [elem_match_with_exists_operator]}},
170
+ ]
171
+ }
172
+ return elem_match if index > 0 else {info["key"]: {"$all": [elem_match]}}
173
+
174
+ raise Exception(f"Unable to build nested matcher. See keys_info: {keys_info}")
@@ -0,0 +1,144 @@
1
+ from datetime import datetime, timezone
2
+ from elody.object_configurations.base_object_configuration import (
3
+ BaseObjectConfiguration,
4
+ )
5
+ from elody.util import flatten_dict
6
+ from uuid import uuid4
7
+
8
+
9
+ class ElodyConfiguration(BaseObjectConfiguration):
10
+ SCHEMA_TYPE = "elody"
11
+ SCHEMA_VERSION = 1
12
+
13
+ def crud(self):
14
+ crud = {
15
+ "collection": "entities",
16
+ "collection_history": "history",
17
+ "creator": lambda post_body, **kwargs: self._creator(post_body, **kwargs),
18
+ "post_crud_hook": lambda **kwargs: self._post_crud_hook(**kwargs),
19
+ "pre_crud_hook": lambda **kwargs: self._pre_crud_hook(**kwargs),
20
+ }
21
+ return {**super().crud(), **crud}
22
+
23
+ def document_info(self):
24
+ return {"object_lists": {"metadata": "key", "relations": "type"}}
25
+
26
+ def logging(self, flat_document, **kwargs):
27
+ return super().logging(flat_document, **kwargs)
28
+
29
+ def migration(self):
30
+ return super().migration()
31
+
32
+ def serialization(self, from_format, to_format):
33
+ return super().serialization(from_format, to_format)
34
+
35
+ def validation(self):
36
+ return super().validation()
37
+
38
+ def _creator(
39
+ self,
40
+ post_body,
41
+ *,
42
+ flat_post_body={},
43
+ document_defaults={},
44
+ ):
45
+ if not flat_post_body:
46
+ flat_post_body = flatten_dict(
47
+ self.document_info()["object_lists"], post_body
48
+ )
49
+ _id = document_defaults.get("_id", str(uuid4()))
50
+
51
+ identifiers = []
52
+ for property in self.document_info().get("identifier_properties", []):
53
+ if identifier := flat_post_body.get(f"metadata.{property}.value"):
54
+ identifiers.append(identifier)
55
+
56
+ template = {
57
+ "_id": _id,
58
+ "computed_values": {
59
+ "created_at": datetime.now(timezone.utc),
60
+ "event": "create",
61
+ },
62
+ "identifiers": list(
63
+ set([_id, *identifiers, *document_defaults.pop("identifiers", [])])
64
+ ),
65
+ "metadata": [],
66
+ "relations": [],
67
+ "schema": {"type": self.SCHEMA_TYPE, "version": self.SCHEMA_VERSION},
68
+ }
69
+ if user_context_id := self._get_user_context_id():
70
+ template["computed_values"]["created_by"] = user_context_id
71
+
72
+ for key, object_list_key in self.document_info()["object_lists"].items():
73
+ if not key.startswith("lookup.virtual_relations"):
74
+ post_body[key] = self._merge_object_lists(
75
+ document_defaults.get(key, []),
76
+ post_body.get(key, []),
77
+ object_list_key,
78
+ )
79
+ document = {**template, **document_defaults, **post_body}
80
+
81
+ document = self._sanitize_document(
82
+ document=document,
83
+ object_list_name="metadata",
84
+ object_list_value_field_name="value",
85
+ )
86
+ document = self._sort_document_keys(document)
87
+ return document
88
+
89
+ def _document_content_patcher(
90
+ self, *, document, content, overwrite=False, **kwargs
91
+ ):
92
+ object_lists = self.document_info().get("object_lists", {})
93
+ if overwrite:
94
+ document = content
95
+ else:
96
+ for key, value in content.items():
97
+ if key in object_lists:
98
+ if key != "relations":
99
+ for value_element in value:
100
+ for item_element in document[key]:
101
+ if (
102
+ item_element[object_lists[key]]
103
+ == value_element[object_lists[key]]
104
+ ):
105
+ document[key].remove(item_element)
106
+ break
107
+ document[key].extend(value)
108
+ else:
109
+ document[key] = value
110
+
111
+ return document
112
+
113
+ def _post_crud_hook(self, **kwargs):
114
+ pass
115
+
116
+ def _pre_crud_hook(self, *, crud, document={}, **kwargs):
117
+ if document:
118
+ document = self._sanitize_document(
119
+ document=document,
120
+ object_list_name="metadata",
121
+ object_list_value_field_name="value",
122
+ )
123
+ document = self.__patch_document_computed_values(crud, document)
124
+ document = self._sort_document_keys(document)
125
+ return document
126
+
127
+ def _sanitize_document(
128
+ self, *, document, object_list_name, object_list_value_field_name, **kwargs
129
+ ):
130
+ sanitized_document = super()._sanitize_document(document=document)
131
+ object_list = document[object_list_name]
132
+ for element in object_list:
133
+ if not element[object_list_value_field_name]:
134
+ sanitized_document[object_list_name].remove(element)
135
+ return sanitized_document
136
+
137
+ def __patch_document_computed_values(self, crud, document):
138
+ if not document.get("computed_values"):
139
+ document["computed_values"] = {}
140
+ document["computed_values"].update({"event": crud})
141
+ document["computed_values"].update({"modified_at": datetime.now(timezone.utc)})
142
+ if email := self._get_user_context_id():
143
+ document["computed_values"].update({"modified_by": email})
144
+ return document
@@ -0,0 +1,65 @@
1
+ from elody.object_configurations.elody_configuration import (
2
+ ElodyConfiguration,
3
+ )
4
+ from elody.util import send_cloudevent
5
+ from os import getenv
6
+
7
+
8
+ class JobConfiguration(ElodyConfiguration):
9
+ SCHEMA_TYPE = "elody"
10
+ SCHEMA_VERSION = 1
11
+
12
+ def crud(self):
13
+ crud = {"collection": "jobs", "collection_history": ""}
14
+ return {**super().crud(), **crud}
15
+
16
+ def document_info(self):
17
+ return super().document_info()
18
+
19
+ def logging(self, flat_document, **kwargs):
20
+ return super().logging(flat_document, **kwargs)
21
+
22
+ def migration(self):
23
+ return super().migration()
24
+
25
+ def serialization(self, from_format, to_format):
26
+ return super().serialization(from_format, to_format)
27
+
28
+ def validation(self):
29
+ return super().validation()
30
+
31
+ def _creator(self, post_body, *, get_user_context={}, **_):
32
+ document = super()._creator(post_body)
33
+ if email := get_user_context().email:
34
+ document["computed_values"]["created_by"] = email
35
+ return document
36
+
37
+ def _post_crud_hook(self, *, crud, document, get_rabbit, **kwargs):
38
+ if crud == "create":
39
+ send_cloudevent(
40
+ get_rabbit(),
41
+ getenv("MQ_EXCHANGE", "dams"),
42
+ "dams.job_created",
43
+ document,
44
+ )
45
+ if parent_id := kwargs.get("parent_id"):
46
+ send_cloudevent(
47
+ get_rabbit(),
48
+ getenv("MQ_EXCHANGE", "dams"),
49
+ "dams.job_changed",
50
+ {
51
+ "id": parent_id,
52
+ "patch": {
53
+ "relations": [
54
+ {"key": document["_id"], "type": "isParentJobOf"}
55
+ ]
56
+ },
57
+ },
58
+ )
59
+ elif crud == "update":
60
+ send_cloudevent(
61
+ get_rabbit(),
62
+ getenv("MQ_EXCHANGE", "dams"),
63
+ "dams.job_changed",
64
+ document,
65
+ )