commit
39c5990199
@ -10,6 +10,7 @@ from ..services.auth_guard import check_jwt
|
|||||||
from BreCal.database.update_database import evaluate_shipcall_state
|
from BreCal.database.update_database import evaluate_shipcall_state
|
||||||
from BreCal.database.sql_queries import create_sql_query_shipcall_get, create_sql_query_shipcall_post, create_sql_query_shipcall_put, create_sql_query_history_post, create_sql_query_history_put, SQLQuery
|
from BreCal.database.sql_queries import create_sql_query_shipcall_get, create_sql_query_shipcall_post, create_sql_query_shipcall_put, create_sql_query_history_post, create_sql_query_history_put, SQLQuery
|
||||||
from marshmallow import Schema, fields, ValidationError
|
from marshmallow import Schema, fields, ValidationError
|
||||||
|
from BreCal.validators.validation_error import create_validation_error_response
|
||||||
|
|
||||||
def GetShipcalls(options):
|
def GetShipcalls(options):
|
||||||
"""
|
"""
|
||||||
@ -152,9 +153,7 @@ def PostShipcalls(schemaModel):
|
|||||||
return json.dumps({"id" : new_id}), 201, {'Content-Type': 'application/json; charset=utf-8'}
|
return json.dumps({"id" : new_id}), 201, {'Content-Type': 'application/json; charset=utf-8'}
|
||||||
|
|
||||||
except ValidationError as ex:
|
except ValidationError as ex:
|
||||||
logging.error(ex)
|
return create_validation_error_response(ex, status_code=400, create_log=True)
|
||||||
print(ex)
|
|
||||||
return json.dumps({"message":f"bad format. \nError Messages: {ex.messages}. \nValid Data: {ex.valid_data}"}), 400
|
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logging.error(traceback.format_exc())
|
logging.error(traceback.format_exc())
|
||||||
@ -265,9 +264,7 @@ def PutShipcalls(schemaModel):
|
|||||||
return json.dumps({"id" : schemaModel["id"]}), 200
|
return json.dumps({"id" : schemaModel["id"]}), 200
|
||||||
|
|
||||||
except ValidationError as ex:
|
except ValidationError as ex:
|
||||||
logging.error(ex)
|
return create_validation_error_response(ex, status_code=400, create_log=True)
|
||||||
print(ex)
|
|
||||||
return json.dumps({"message":f"bad format. \nError Messages: {ex.messages}. \nValid Data: {ex.valid_data}"}), 400
|
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logging.error(traceback.format_exc())
|
logging.error(traceback.format_exc())
|
||||||
|
|||||||
@ -90,7 +90,7 @@ class InputValidationShipcall():
|
|||||||
InputValidationShipcall.check_referenced_ids(loadedModel)
|
InputValidationShipcall.check_referenced_ids(loadedModel)
|
||||||
|
|
||||||
# check for reasonable values in the shipcall fields and checks for forbidden keys.
|
# check for reasonable values in the shipcall fields and checks for forbidden keys.
|
||||||
InputValidationShipcall.check_shipcall_values(loadedModel, content, forbidden_keys=["evaluation", "evaluation_message"])
|
InputValidationShipcall.check_shipcall_values(loadedModel, content, forbidden_keys=["evaluation", "evaluation_message"], is_put_data=True)
|
||||||
|
|
||||||
# a canceled shipcall cannot be selected
|
# a canceled shipcall cannot be selected
|
||||||
# Note: 'canceled' is allowed in PUT-requests, if it is not already set (which is checked by InputValidationShipcall.check_shipcall_is_cancel)
|
# Note: 'canceled' is allowed in PUT-requests, if it is not already set (which is checked by InputValidationShipcall.check_shipcall_is_cancel)
|
||||||
@ -98,12 +98,15 @@ class InputValidationShipcall():
|
|||||||
return
|
return
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_shipcall_values(loadedModel:dict, content:dict, forbidden_keys:list=["evaluation", "evaluation_message"]):
|
def check_shipcall_values(loadedModel:dict, content:dict, forbidden_keys:list=["evaluation", "evaluation_message"], is_put_data:bool=False):
|
||||||
"""
|
"""
|
||||||
individually checks each value provided in the loadedModel/content.
|
individually checks each value provided in the loadedModel/content.
|
||||||
This function validates, whether the values are reasonable.
|
This function validates, whether the values are reasonable.
|
||||||
|
|
||||||
Also, some data may not be set in a POST-request.
|
Also, some data may not be set in a POST-request.
|
||||||
|
|
||||||
|
options:
|
||||||
|
is_put_data: bool. Some validation rules do not apply to POST data, but apply to PUT data. This flag separates the two.
|
||||||
"""
|
"""
|
||||||
# Note: BreCal.schemas.model.ShipcallSchema has an internal validation, which the marshmallow library provides. This is used
|
# Note: BreCal.schemas.model.ShipcallSchema has an internal validation, which the marshmallow library provides. This is used
|
||||||
# to verify values individually, when the schema is loaded with data.
|
# to verify values individually, when the schema is loaded with data.
|
||||||
@ -120,11 +123,12 @@ class InputValidationShipcall():
|
|||||||
if check_if_int_is_valid_flag(flags_value, enum_object=ParticipantFlag):
|
if check_if_int_is_valid_flag(flags_value, enum_object=ParticipantFlag):
|
||||||
raise ValidationError({"flags":f"incorrect value provided for 'flags'. Must be a valid combination of the flags."})
|
raise ValidationError({"flags":f"incorrect value provided for 'flags'. Must be a valid combination of the flags."})
|
||||||
|
|
||||||
# time values must use future-dates
|
if is_put_data:
|
||||||
InputValidationShipcall.check_times_are_in_future(loadedModel, content)
|
# the type of a shipcall may not be changed. It can only be set with the initial POST-request.
|
||||||
|
InputValidationShipcall.check_shipcall_type_is_unchanged(loadedModel)
|
||||||
# the type of a shipcall may not be changed. It can only be set with the initial POST-request.
|
else:
|
||||||
InputValidationShipcall.check_shipcall_type_is_unchanged(loadedModel)
|
# time values must use future-dates
|
||||||
|
InputValidationShipcall.check_times_are_in_future(loadedModel, content)
|
||||||
|
|
||||||
# some arguments must not be provided
|
# some arguments must not be provided
|
||||||
InputValidationShipcall.check_forbidden_arguments(content, forbidden_keys=forbidden_keys)
|
InputValidationShipcall.check_forbidden_arguments(content, forbidden_keys=forbidden_keys)
|
||||||
|
|||||||
@ -140,7 +140,6 @@ class InputValidationTimes():
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def check_user_is_not_bsmd_type(user_data:dict):
|
def check_user_is_not_bsmd_type(user_data:dict):
|
||||||
"""a new dataset may only be created by a user who is *not* belonging to participant group BSMD"""
|
"""a new dataset may only be created by a user who is *not* belonging to participant group BSMD"""
|
||||||
# this method is deprecated for /times POST requests. As the function may be used elsewhere, it will, for now, not be removed
|
|
||||||
is_bsmd = check_if_user_is_bsmd_type(user_data)
|
is_bsmd = check_if_user_is_bsmd_type(user_data)
|
||||||
if is_bsmd:
|
if is_bsmd:
|
||||||
raise ValidationError({"participant_type":f"current user belongs to BSMD. Cannot post 'times' datasets. Found user data: {user_data}"})
|
raise ValidationError({"participant_type":f"current user belongs to BSMD. Cannot post 'times' datasets. Found user data: {user_data}"})
|
||||||
@ -171,7 +170,6 @@ class InputValidationTimes():
|
|||||||
if not time_end_after_time_start:
|
if not time_end_after_time_start:
|
||||||
raise ValidationError({"etd":f"The provided time interval for the estimated departure time is invalid. The interval end takes place before the interval start. Found interval data: {loadedModel['etd_berth']} to {loadedModel['etd_interval_end']}"})
|
raise ValidationError({"etd":f"The provided time interval for the estimated departure time is invalid. The interval end takes place before the interval start. Found interval data: {loadedModel['etd_berth']} to {loadedModel['etd_interval_end']}"})
|
||||||
|
|
||||||
|
|
||||||
if (loadedModel["eta_interval_end"] is not None) and (loadedModel["eta_berth"] is not None):
|
if (loadedModel["eta_interval_end"] is not None) and (loadedModel["eta_berth"] is not None):
|
||||||
time_end_after_time_start = loadedModel["eta_interval_end"] >= loadedModel["eta_berth"]
|
time_end_after_time_start = loadedModel["eta_interval_end"] >= loadedModel["eta_berth"]
|
||||||
if not time_end_after_time_start:
|
if not time_end_after_time_start:
|
||||||
|
|||||||
@ -19,9 +19,9 @@ def validate_time_is_in_future(value:datetime.datetime):
|
|||||||
|
|
||||||
def validate_time_is_in_not_too_distant_future(raise_validation_error:bool, value:datetime.datetime, seconds:int=60, minutes:int=60, hours:int=24, days:int=30, months:int=12)->bool:
|
def validate_time_is_in_not_too_distant_future(raise_validation_error:bool, value:datetime.datetime, seconds:int=60, minutes:int=60, hours:int=24, days:int=30, months:int=12)->bool:
|
||||||
"""
|
"""
|
||||||
combines two boolean operations. Returns True when both conditions are met.
|
A time entry is considerd valid, when it meets the following condition(s):
|
||||||
a) value is in the future
|
a) value is not too distant (e.g., at max. 1 year in the future)
|
||||||
b) value is not too distant (e.g., at max. 1 year in the future)
|
Previous variants of this function also included validating that a time must be in the future. This is deprecated.
|
||||||
|
|
||||||
When the value is 'None', the validation will be skipped. A ValidationError is never issued, but the method returns 'False'.
|
When the value is 'None', the validation will be skipped. A ValidationError is never issued, but the method returns 'False'.
|
||||||
|
|
||||||
@ -31,17 +31,17 @@ def validate_time_is_in_not_too_distant_future(raise_validation_error:bool, valu
|
|||||||
if value is None:
|
if value is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
is_in_future = validate_time_is_in_future(value)
|
# is_in_future = validate_time_is_in_future(value)
|
||||||
is_too_distant = validate_time_exceeds_threshold(value, seconds, minutes, hours, days, months)
|
is_too_distant = validate_time_exceeds_threshold(value, seconds, minutes, hours, days, months)
|
||||||
|
|
||||||
if raise_validation_error:
|
if raise_validation_error:
|
||||||
if not is_in_future:
|
#if not is_in_future:
|
||||||
raise ValidationError({"any_date":f"The provided value must be in the future. Current Time: {datetime.datetime.now()}, Value: {value}"})
|
#raise ValidationError({"any_date":f"The provided value must be in the future. Current Time: {datetime.datetime.now()}, Value: {value}"})
|
||||||
|
|
||||||
if is_too_distant:
|
if is_too_distant:
|
||||||
raise ValidationError({"any_date":f"The provided value is in the too distant future and exceeds a threshold for 'reasonable' entries. Found: {value}"})
|
raise ValidationError({"any_date":f"The provided value is in the too distant future and exceeds a threshold for 'reasonable' entries. Found: {value}"})
|
||||||
|
|
||||||
return is_in_future & (not is_too_distant)
|
return (not is_too_distant) # & is_in_future
|
||||||
|
|
||||||
class TimeLogic():
|
class TimeLogic():
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|||||||
@ -1,57 +1,57 @@
|
|||||||
import logging
|
import logging
|
||||||
import typing
|
import typing
|
||||||
import json
|
import json
|
||||||
|
import sys
|
||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
import werkzeug
|
|
||||||
from werkzeug.exceptions import Forbidden
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
|
|
||||||
def create_default_error_format(error_description):
|
def create_default_json_response_format(error_field:str, error_description:str):
|
||||||
|
"""
|
||||||
|
The default format of a JSON response for exceptions is:
|
||||||
|
{
|
||||||
|
"error_field":str,
|
||||||
|
"error_description":str
|
||||||
|
}
|
||||||
|
"""
|
||||||
json_response = {
|
json_response = {
|
||||||
"message":f"{error_description}",
|
"error_field":error_field,
|
||||||
"errors":[],
|
"error_description":error_description
|
||||||
"valid_data":{}
|
|
||||||
}
|
}
|
||||||
return json_response
|
return json_response
|
||||||
|
|
||||||
|
def unbundle_(errors, unbundled=[]):
|
||||||
|
"""
|
||||||
|
unbundles a dictionary entry into separate items and appends them to the list {unbundled}.
|
||||||
|
Example:
|
||||||
|
errors = {"key1":{"keya":"keyb","keyc":{"keyc12":12}}}
|
||||||
|
Returns:
|
||||||
|
[{'error_field':'keya', 'error_description':['keyb']}, {'error_field':'keyc12', 'error_description':[12]}]
|
||||||
|
As can be seen, only the subkeys and their respective value are received. Each value is *always* a list.
|
||||||
|
"""
|
||||||
|
{k:unbundle_(v,unbundled=unbundled) if isinstance(v,dict) else unbundled.append({"error_field":k, "error_description":v[0] if isinstance(v,list) else str(v)}) for k,v in errors.items()}
|
||||||
|
return
|
||||||
|
|
||||||
|
def unbundle_validation_error_message(message):
|
||||||
|
"""
|
||||||
|
inputs:
|
||||||
|
message: ValidationError.messages object. A str, list or dictionary
|
||||||
|
"""
|
||||||
|
unbundled = []
|
||||||
|
unbundle_(message, unbundled=unbundled)
|
||||||
|
if len(unbundled)>0:
|
||||||
|
error_field = "ValidationError in the following field(s): " + " & ".join([unb["error_field"] for unb in unbundled])
|
||||||
|
error_description = "Error Description(s): " + " & ".join([unb["error_description"] for unb in unbundled])
|
||||||
|
else:
|
||||||
|
error_field = "ValidationError"
|
||||||
|
error_description = "unknown validation error"
|
||||||
|
return (error_field, error_description)
|
||||||
|
|
||||||
def create_validation_error_response(ex:ValidationError, status_code:int=400, create_log:bool=True)->typing.Tuple[str,int]:
|
def create_validation_error_response(ex:ValidationError, status_code:int=400, create_log:bool=True)->typing.Tuple[str,int]:
|
||||||
# generate an overview the errors
|
# unbundles ValidationError into a dictionary of {'error_field':str, 'error_description':str}-format
|
||||||
#example:
|
message = ex.messages
|
||||||
# {'lock_time': ['The provided value must be in the future. Current Time: 2024-09-02 08:23:32.600791, Value: 2024-09-01 08:20:41.853000']}
|
(error_field, error_description) = unbundle_validation_error_message(message)
|
||||||
# when the model schema returns an error, 'messages' is by default a dictionary.
|
json_response = create_default_json_response_format(error_field=error_field, error_description=error_description)
|
||||||
# e.g., loadedModel = model.TimesSchema().load(data=content, many=False, partial=True)
|
|
||||||
# returns: {'eta_berth': ['The provided value must be in the future}
|
|
||||||
|
|
||||||
# when raising a custom ValidationError, it can return a string, list or dict.
|
|
||||||
# we would like to ensure, that the content of the .messages is a dictionary. This can be accomplished by calling
|
|
||||||
# raise ValidationError({"example_key_which_fails":"the respective error message"})
|
|
||||||
errors = ex.messages
|
|
||||||
|
|
||||||
# raise ValidationError("example error")
|
|
||||||
# creates a .messages object, which is an array. e.g., ex.messages = ["example error"]
|
|
||||||
# the following conversion snipped ensures a dictionary output
|
|
||||||
if isinstance(errors, (str,list)):
|
|
||||||
errors = {"undefined_schema":errors}
|
|
||||||
errors = {k:v if isinstance(v,list) else [v] for k,v in errors.items()}
|
|
||||||
|
|
||||||
# hence, errors always has the following type: dict[str, list[str]]
|
|
||||||
|
|
||||||
|
|
||||||
# example:
|
|
||||||
# "Valid Data": {
|
|
||||||
# "id": 2894,
|
|
||||||
# "eta_berth": "datetime.datetime(2024, 9, 2, 11, 11, 43)",
|
|
||||||
# "eta_berth_fixed": false
|
|
||||||
# }
|
|
||||||
valid_data = ex.valid_data
|
|
||||||
|
|
||||||
message = "ValidationError"
|
|
||||||
json_response = create_default_error_format(error_description=message)
|
|
||||||
json_response.update({
|
|
||||||
"errors":errors,
|
|
||||||
"valid_data":valid_data
|
|
||||||
})
|
|
||||||
|
|
||||||
# json.dumps with default=str automatically converts non-serializable values to strings. Hence, datetime objects (which are not)
|
# json.dumps with default=str automatically converts non-serializable values to strings. Hence, datetime objects (which are not)
|
||||||
# natively serializable are properly serialized.
|
# natively serializable are properly serialized.
|
||||||
@ -66,7 +66,7 @@ def create_werkzeug_error_response(ex:Forbidden, status_code:int=403, create_log
|
|||||||
# json.dumps with default=str automatically converts non-serializable values to strings. Hence, datetime objects (which are not)
|
# json.dumps with default=str automatically converts non-serializable values to strings. Hence, datetime objects (which are not)
|
||||||
# natively serializable are properly serialized.
|
# natively serializable are properly serialized.
|
||||||
message = ex.description
|
message = ex.description
|
||||||
json_response = create_default_error_format(error_description=message)
|
json_response = create_default_json_response_format(error_field=str(repr(ex)), error_description=message)
|
||||||
serialized_response = json.dumps(json_response, default=str)
|
serialized_response = json.dumps(json_response, default=str)
|
||||||
|
|
||||||
if create_log:
|
if create_log:
|
||||||
@ -76,7 +76,8 @@ def create_werkzeug_error_response(ex:Forbidden, status_code:int=403, create_log
|
|||||||
|
|
||||||
def create_dynamic_exception_response(ex, status_code:int=400, message:typing.Optional[str]=None, create_log:bool=True):
|
def create_dynamic_exception_response(ex, status_code:int=400, message:typing.Optional[str]=None, create_log:bool=True):
|
||||||
message = repr(ex) if message is None else message
|
message = repr(ex) if message is None else message
|
||||||
json_response = create_default_error_format(error_description=message)
|
json_response = create_default_json_response_format(error_field="Exception", error_description=message)
|
||||||
|
json_response["message"] = "call failed"
|
||||||
|
|
||||||
serialized_response = json.dumps(json_response, default=str)
|
serialized_response = json.dumps(json_response, default=str)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user