Skip to content

Commit

Permalink
feat: rework object configurations
Browse files Browse the repository at this point in the history
  • Loading branch information
eray-inuits committed Dec 27, 2024
1 parent ce22968 commit 4651f06
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 61 deletions.
4 changes: 1 addition & 3 deletions src/elody/csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,7 @@ def __fill_objects_from_csv(self):
mandatory_columns = [
v for k, v in self.index_mapping.items() if not k.startswith("?")
]
missing_columns = [
x for x in mandatory_columns if x not in row.keys()
]
missing_columns = [x for x in mandatory_columns if x not in row.keys()]
if missing_columns:
raise ColumnNotFoundException(f"{', '.join(missing_columns)}")
lang = self.__determine_language(row)
Expand Down
75 changes: 50 additions & 25 deletions src/elody/object_configurations/base_object_configuration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from abc import ABC, abstractmethod
from copy import deepcopy
from elody.migration.base_object_migrator import BaseObjectMigrator
from policy_factory import get_user_context # pyright: ignore


class BaseObjectConfiguration(ABC):
Expand All @@ -10,20 +11,24 @@ class BaseObjectConfiguration(ABC):
@abstractmethod
def crud(self):
return {
"collection": "entities",
"collection_history": "history",
"creator": lambda post_body, **kwargs: post_body,
"document_content_patcher": lambda *, document, content, overwrite=False, **kwargs: self._document_content_patcher(
document=document,
content=content,
overwrite=overwrite,
**kwargs,
),
"nested_matcher_builder": lambda object_lists, keys_info, value: self.__build_nested_matcher(
object_lists, keys_info, value
),
"post_crud_hook": lambda **kwargs: None,
"pre_crud_hook": lambda **kwargs: None,
"pre_crud_hook": lambda *, document, **kwargs: document,
"storage_type": "db",
}

@abstractmethod
def document_info(self):
return {"object_lists": {"metadata": "key", "relations": "type"}}
return {}

@abstractmethod
def logging(self, flat_document, **kwargs):
Expand All @@ -33,7 +38,7 @@ def logging(self, flat_document, **kwargs):
"schema": f"{flat_document.get('schema.type')}:{flat_document.get('schema.version')}",
}
try:
user_context = kwargs.get("get_user_context")() # pyright: ignore
user_context = get_user_context()
info_labels["http_method"] = user_context.bag.get("http_method")
info_labels["requested_endpoint"] = user_context.bag.get(
"requested_endpoint"
Expand Down Expand Up @@ -65,20 +70,46 @@ def validator(http_method, content, item, **_):

return "function", validator

def _get_merged_post_body(self, post_body, document_defaults, object_list_name):
key = self.document_info()["object_lists"][object_list_name]
post_body[object_list_name] = self.__merge_object_lists(
document_defaults.get(object_list_name, []),
post_body.get(object_list_name, []),
key,
def _document_content_patcher(
self, *, document, content, overwrite=False, **kwargs
):
raise NotImplementedError(
"Provide concrete implementation in child object configuration"
)
return post_body

def _sanitize_document(self, document, object_list_name, value_field_name):
object_list = deepcopy(document[object_list_name])
for element in object_list:
if not element[value_field_name]:
document[object_list_name].remove(element)
def _merge_object_lists(self, source, target, object_list_key):
for target_item in target:
for source_item in source:
if source_item[object_list_key] == target_item[object_list_key]:
source.remove(source_item)
return [*source, *target]

def _get_user_context_id(self):
try:
return get_user_context().id
except Exception:
return None

def _sanitize_document(self, *, document, **kwargs):
sanitized_document = {}
document_deepcopy = deepcopy(document)
for key, value in document_deepcopy.items():
if isinstance(value, dict):
sanitized_value = BaseObjectConfiguration._sanitize_document(
self, document=value
)
if sanitized_value:
sanitized_document[key] = sanitized_value
elif value:
sanitized_document[key] = value
return sanitized_document

def _should_create_history_object(self):
try:
get_user_context()
return bool(self.crud().get("collection_history"))
except Exception:
return False

def _sort_document_keys(self, document):
def sort_keys(data):
Expand All @@ -98,12 +129,13 @@ def sort_keys(data):
else:
return data

for key, value in self.document_info()["object_lists"].items():
for key, value in self.document_info().get("object_lists", {}).items():
if document.get(key):
document[key] = sorted(
document[key], key=lambda property: property[value]
)
sort_keys(document)
return document

def __build_nested_matcher(self, object_lists, keys_info, value, index=0):
if index == 0 and not any(info["object_list"] for info in keys_info):
Expand Down Expand Up @@ -135,10 +167,3 @@ def __build_nested_matcher(self, object_lists, keys_info, value, index=0):
return elem_match if index > 0 else {info["key"]: {"$all": [elem_match]}}

raise Exception(f"Unable to build nested matcher. See keys_info: {keys_info}")

def __merge_object_lists(self, source, target, key):
for target_item in target:
for source_item in source:
if source_item[key] == target_item[key]:
source.remove(source_item)
return [*source, *target]
88 changes: 66 additions & 22 deletions src/elody/object_configurations/elody_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class ElodyConfiguration(BaseObjectConfiguration):

def crud(self):
crud = {
"collection": "entities",
"collection_history": "history",
"creator": lambda post_body, **kwargs: self._creator(post_body, **kwargs),
"post_crud_hook": lambda **kwargs: self._post_crud_hook(**kwargs),
"pre_crud_hook": lambda **kwargs: self._pre_crud_hook(**kwargs),
Expand All @@ -37,7 +39,6 @@ def _creator(
self,
post_body,
*,
get_user_context,
flat_post_body={},
document_defaults={},
):
Expand Down Expand Up @@ -65,36 +66,79 @@ def _creator(
"relations": [],
"schema": {"type": self.SCHEMA_TYPE, "version": self.SCHEMA_VERSION},
}
if email := self.__get_email(get_user_context):
template["computed_values"]["created_by"] = email

for key in self.document_info()["object_lists"].keys():
post_body = self._get_merged_post_body(post_body, document_defaults, key)
if user_context_id := self._get_user_context_id():
template["computed_values"]["created_by"] = user_context_id

for key, object_list_key in self.document_info()["object_lists"].items():
if not key.startswith("lookup.virtual_relations"):
post_body[key] = self._merge_object_lists(
document_defaults.get(key, []),
post_body.get(key, []),
object_list_key,
)
document = {**template, **document_defaults, **post_body}

self._sanitize_document(document, "metadata", "value")
self._sort_document_keys(document)
document = self._sanitize_document(
document=document,
object_list_name="metadata",
object_list_value_field_name="value",
)
document = self._sort_document_keys(document)
return document

def _document_content_patcher(
self, *, document, content, overwrite=False, **kwargs
):
object_lists = self.document_info().get("object_lists", {})
if overwrite:
document = content
else:
for key, value in content.items():
if key in object_lists:
if key != "relations":
for value_element in value:
for item_element in document[key]:
if (
item_element[object_lists[key]]
== value_element[object_lists[key]]
):
document[key].remove(item_element)
break
document[key].extend(value)
else:
document[key] = value

return document

def _post_crud_hook(self, **_):
def _post_crud_hook(self, **kwargs):
pass

def _pre_crud_hook(self, *, crud, document={}, get_user_context=None, **_):
def _pre_crud_hook(self, *, crud, document={}, **kwargs):
if document:
self._sanitize_document(document, "metadata", "value")
self.__patch_document_computed_values(
crud, document, get_user_context=get_user_context
document = self._sanitize_document(
document=document,
object_list_name="metadata",
object_list_value_field_name="value",
)
self._sort_document_keys(document)

def __get_email(self, get_user_context):
try:
return get_user_context().email
except Exception:
return None
document = self.__patch_document_computed_values(crud, document)
document = self._sort_document_keys(document)
return document

def __patch_document_computed_values(self, crud, document, **kwargs):
def _sanitize_document(
self, *, document, object_list_name, object_list_value_field_name, **kwargs
):
sanitized_document = super()._sanitize_document(document=document)
object_list = document[object_list_name]
for element in object_list:
if not element[object_list_value_field_name]:
sanitized_document[object_list_name].remove(element)
return sanitized_document

def __patch_document_computed_values(self, crud, document):
if not document.get("computed_values"):
document["computed_values"] = {}
document["computed_values"].update({"event": crud})
document["computed_values"].update({"modified_at": datetime.now(timezone.utc)})
if email := self.__get_email(kwargs.get("get_user_context")):
if email := self._get_user_context_id():
document["computed_values"].update({"modified_by": email})
return document
4 changes: 2 additions & 2 deletions src/elody/object_configurations/job_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ def crud(self):
def document_info(self):
return super().document_info()

def logging(self, flat_item, **kwargs):
return super().logging(flat_item, **kwargs)
def logging(self, flat_document, **kwargs):
return super().logging(flat_document, **kwargs)

def migration(self):
return super().migration()
Expand Down
26 changes: 18 additions & 8 deletions src/elody/policies/permission_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ def set_permissions(permissions: dict, placeholders: list[str] = []):
def get_permissions(role: str, user_context: UserContext):
permissions = deepcopy(_permissions)

for placeholder in _placeholders:
for placeholder_key in _placeholders:
placeholder_value = user_context.bag.get(placeholder_key.lower())
permissions = __replace_permission_placeholders(
permissions, placeholder, user_context.bag[placeholder.lower()]
permissions, placeholder_key, placeholder_value
)
return permissions.get(role, {}) # pyright: ignore

Expand Down Expand Up @@ -84,8 +85,8 @@ def handle_single_item_request(

def mask_protected_content_post_request_hook(user_context: UserContext, permissions):
def __post_request_hook(response):
items = response["results"]
for item in items:
items = []
for item in response["results"]:
try:
(
item_in_storage_format,
Expand All @@ -104,13 +105,15 @@ def __post_request_hook(response):
"read",
object_lists,
)
items.append(user_context.bag["requested_item"])
except Exception as exception:
log.debug(
f"{exception.__class__.__name__}: {str(exception)}",
item.get("storage_format", item),
)
raise exception

response["results"] = items
return response

return __post_request_hook
Expand All @@ -122,7 +125,7 @@ def __prepare_item_for_permission_check(item, permissions, crud):
return item, None, None, None

config = get_object_configuration_mapper().get(item["type"])
object_lists = config.document_info()["object_lists"]
object_lists = config.document_info().get("object_lists", {})
flat_item = flatten_dict(object_lists, item)

return (
Expand Down Expand Up @@ -188,16 +191,23 @@ def __is_allowed_to_crud_item_keys(
if condition_match:
if crud == "read":
keys_info = interpret_flat_key(restricted_key, object_lists)
element = item_in_storage_format
for info in keys_info:
if info["object_list"]:
element = __get_element_from_object_list_of_item(
element,
item_in_storage_format,
info["key"],
info["object_key"],
object_lists,
)
element[info["key"]] = "[protected content]" # pyright: ignore
item_in_storage_format[info["key"]].remove(element)
break
else:
try:
del item_in_storage_format[keys_info[0]["key"]][
keys_info[1]["key"]
]
except KeyError:
pass
else:
if flat_request_body.get(restricted_key):
user_context.bag["restricted_keys"].append(restricted_key)
Expand Down
4 changes: 3 additions & 1 deletion src/tests/unit/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
mediafile_is_public,
read_json_as_dict,
parse_url_unfriendly_string,
CustomJSONEncoder
CustomJSONEncoder,
)
from data import mediafile1, mediafile2
from datetime import datetime, timezone
Expand All @@ -24,12 +24,14 @@ def test_default_method_with_datetime():
result = encoder.default(dt)
assert result == "2023-10-01T12:00:00+00:00"


def test_default_method_with_naive_datetime():
encoder = CustomJSONEncoder()
dt = datetime(2023, 10, 1, 12, 0, 0)
result = encoder.default(dt)
assert result == "2023-10-01T10:00:00+00:00"


def test_encode_method_with_non_datetime():
encoder = CustomJSONEncoder()
obj = {"key": "value"}
Expand Down

0 comments on commit 4651f06

Please sign in to comment.