setting up a local mysql database and running the API locally, which requires slight adaptations. Implementing input validation for POST requests of shipcalls and adapting enumerators, as well as data models.
This commit is contained in:
parent
fcb889d2bc
commit
b32b466f74
@ -3,7 +3,8 @@ from webargs.flaskparser import parser
|
||||
from marshmallow import Schema, fields, ValidationError
|
||||
from ..schemas import model
|
||||
from .. import impl
|
||||
from ..services.auth_guard import auth_guard
|
||||
from ..services.auth_guard import auth_guard, check_jwt
|
||||
from BreCal.validators.input_validation import check_if_user_is_bsmd_type, check_if_user_data_has_valid_ship_id, check_if_user_data_has_valid_berth_id, check_if_user_data_has_valid_participant_id
|
||||
|
||||
import logging
|
||||
import json
|
||||
@ -41,6 +42,26 @@ def PostShipcalls():
|
||||
content = request.get_json(force=True)
|
||||
loadedModel = model.ShipcallSchema().load(data=content, many=False, partial=True)
|
||||
|
||||
# read the user data from the JWT token (set when login is performed)
|
||||
user_data = check_jwt()
|
||||
|
||||
# check, whether the user belongs to a participant, which is of type ParticipantType.BSMD
|
||||
# as ParticipantType is an IntFlag, a user belonging to multiple groups is properly evaluated.
|
||||
is_bsmd = check_if_user_is_bsmd_type(user_data)
|
||||
if not is_bsmd:
|
||||
raise ValidationError(f"current user does not belong to BSMD. Cannot post shipcalls. Found user data: {user_data}")
|
||||
|
||||
import logging
|
||||
logging.log(20, loadedModel)
|
||||
logging.log(20, "metz development")
|
||||
"""
|
||||
# loadedModel ...
|
||||
valid_ship_id = check_if_user_data_has_valid_ship_id(ship_id)
|
||||
valid_berth_id = check_if_user_data_has_valid_berth_id(berth_id)
|
||||
valid_participant_id = check_if_user_data_has_valid_participant_id(participant_id)
|
||||
"""
|
||||
|
||||
|
||||
except ValidationError as ex:
|
||||
logging.error(ex)
|
||||
print(ex)
|
||||
|
||||
@ -2,7 +2,7 @@ from enum import IntEnum, Enum, IntFlag
|
||||
|
||||
class ParticipantType(IntFlag):
|
||||
"""determines the type of a participant"""
|
||||
NONE = 0
|
||||
undefined = 0
|
||||
BSMD = 1
|
||||
TERMINAL = 2
|
||||
PILOT = 4
|
||||
@ -13,10 +13,15 @@ class ParticipantType(IntFlag):
|
||||
|
||||
class ShipcallType(IntEnum):
|
||||
"""determines the type of a shipcall, as this changes the applicable validation rules"""
|
||||
undefined = 0
|
||||
INCOMING = 1
|
||||
OUTGOING = 2
|
||||
SHIFTING = 3
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.undefined
|
||||
|
||||
class ParticipantwiseTimeDelta():
|
||||
"""stores the time delta for every participant, which triggers the validation rules in the rule set '0001'"""
|
||||
AGENCY = 1200.0 # 20 h * 60 min/h = 1200 min
|
||||
@ -49,3 +54,16 @@ class NotificationType(IntFlag):
|
||||
EMAIL = 1
|
||||
POPUP = 2
|
||||
MESSENGER = 4
|
||||
|
||||
class ParticipantFlag(IntFlag):
|
||||
"""
|
||||
| 1 | If this flag is set on a shipcall record with participant type Agency (8),
|
||||
all participants of type BSMD (1) may edit the record.
|
||||
"""
|
||||
undefined = 0
|
||||
BSMD = 1
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.undefined
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import numpy as np
|
||||
import pandas as pd
|
||||
import datetime
|
||||
import typing
|
||||
from BreCal.schemas.model import Shipcall, Ship, Participant, Berth, User, Times
|
||||
from BreCal.schemas.model import Shipcall, Ship, Participant, Berth, User, Times, ShipcallParticipantMap
|
||||
from BreCal.database.enums import ParticipantType
|
||||
|
||||
def pandas_series_to_data_model():
|
||||
@ -50,7 +50,8 @@ class SQLHandler():
|
||||
'ship'->BreCal.schemas.model.Ship object
|
||||
"""
|
||||
self.str_to_model_dict = {
|
||||
"shipcall":Shipcall, "ship":Ship, "participant":Participant, "berth":Berth, "user":User, "times":Times
|
||||
"shipcall":Shipcall, "ship":Ship, "participant":Participant, "berth":Berth, "user":User, "times":Times,
|
||||
"shipcall_participant_map":ShipcallParticipantMap
|
||||
}
|
||||
return
|
||||
|
||||
@ -70,9 +71,13 @@ class SQLHandler():
|
||||
data = [{k:v for k,v in zip(column_names, dat)} for dat in data]
|
||||
|
||||
# 4.) build a dataframe from the respective data models (which ensures the correct data type)
|
||||
df = self.build_df_from_data_and_name(data, table_name)
|
||||
return df
|
||||
|
||||
def build_df_from_data_and_name(self, data, table_name):
|
||||
data_model = self.str_to_model_dict.get(table_name)
|
||||
if data_model is not None:
|
||||
df = pd.DataFrame([data_model(**dat) for dat in data])
|
||||
df = pd.DataFrame([data_model(**dat) for dat in data], columns=list(data_model.__annotations__.keys()))
|
||||
else:
|
||||
df = pd.DataFrame([dat for dat in data])
|
||||
return df
|
||||
@ -94,11 +99,7 @@ class SQLHandler():
|
||||
|
||||
# 4.) build a dataframe from the respective data models (which ensures the correct data type)
|
||||
data_model = self.str_to_model_dict.get(table_name)
|
||||
if data_model is not None:
|
||||
df = pd.DataFrame([data_model(**dat) for dat in data])
|
||||
else:
|
||||
df = pd.DataFrame([dat for dat in data])
|
||||
|
||||
df = self.build_df_from_data_and_name(data, table_name)
|
||||
if 'id' in df.columns:
|
||||
df = df.set_index('id', inplace=False) # avoid inplace updates, so the raw sql remains unchanged
|
||||
return df
|
||||
|
||||
@ -60,8 +60,13 @@ def PostShipcalls(schemaModel):
|
||||
"""
|
||||
|
||||
:param schemaModel: The deserialized dict of the request
|
||||
e.g.,
|
||||
{
|
||||
'ship_id': 1, 'type': 1, 'eta': datetime.datetime(2023, 7, 23, 7, 18, 19),
|
||||
'voyage': '43B', 'tug_required': False, 'pilot_required': True, 'flags': 0,
|
||||
'pier_side': False, 'bunkering': True, 'recommended_tugs': 2, 'type_value': 1, 'evaluation_value': 0}
|
||||
}
|
||||
"""
|
||||
|
||||
# TODO: Validate the upload data
|
||||
|
||||
# This creates a *new* entry
|
||||
@ -133,7 +138,6 @@ def PostShipcalls(schemaModel):
|
||||
# if not full_id_existances:
|
||||
# pooledConnection.close()
|
||||
# return json.dumps({"message" : "call failed. missing mandatory keywords."}), 500, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
|
||||
commands.execute(query, schemaModel)
|
||||
new_id = commands.execute_scalar("select last_insert_id()")
|
||||
|
||||
|
||||
@ -7,11 +7,11 @@ import sys
|
||||
|
||||
config_path = None
|
||||
|
||||
def initPool(instancePath):
|
||||
def initPool(instancePath, connection_filename="connection_data_devel.json"):
|
||||
try:
|
||||
global config_path
|
||||
if(config_path == None):
|
||||
config_path = os.path.join(instancePath,'../../../secure/connection_data_devel.json');
|
||||
config_path = os.path.join(instancePath,f'../../../../secure/{connection_filename}') #connection_data_devel.json');
|
||||
|
||||
print (config_path)
|
||||
|
||||
|
||||
@ -10,6 +10,9 @@ from typing import List
|
||||
import json
|
||||
import datetime
|
||||
from BreCal.validators.time_logic import validate_time_exceeds_threshold
|
||||
from BreCal.database.enums import ParticipantType, ParticipantFlag
|
||||
|
||||
# from BreCal. ... import check_if_user_is_bsmd_type
|
||||
|
||||
def obj_dict(obj):
|
||||
if isinstance(obj, datetime.datetime):
|
||||
@ -54,6 +57,7 @@ class NotificationType(IntEnum):
|
||||
undefined = 0
|
||||
email = 1
|
||||
push = 2
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.undefined
|
||||
@ -143,12 +147,34 @@ class Participant(Schema):
|
||||
street: str
|
||||
postal_code: str
|
||||
city: str
|
||||
type: int
|
||||
type: int # fields.Enum(ParticipantType ...)
|
||||
flags: int
|
||||
created: datetime
|
||||
modified: datetime
|
||||
deleted: bool
|
||||
|
||||
@validates("type")
|
||||
def validate_type(self, value):
|
||||
# e.g., when an IntFlag has the values 1,2,4; the maximum valid value is 7
|
||||
max_int = sum([int(val) for val in list(ParticipantType._value2member_map_.values())])
|
||||
min_int = 0
|
||||
|
||||
valid_type = 0 <= value < max_int
|
||||
if not valid_type:
|
||||
raise ValidationError(f"the provided integer is not supported for default behaviour of the ParticipantType IntFlag. Your choice: {value}. Supported values are: 0 <= value {max_int}")
|
||||
|
||||
|
||||
@validates("flags")
|
||||
def validate_type(self, value):
|
||||
# e.g., when an IntFlag has the values 1,2,4; the maximum valid value is 7
|
||||
max_int = sum([int(val) for val in list(ParticipantFlag._value2member_map_.values())])
|
||||
min_int = 0
|
||||
|
||||
valid_type = 0 <= value < max_int
|
||||
if not valid_type:
|
||||
raise ValidationError(f"the provided integer is not supported for default behaviour of the ParticipantFlag IntFlag. Your choice: {value}. Supported values are: 0 <= value {max_int}")
|
||||
|
||||
|
||||
class ParticipantList(Participant):
|
||||
pass
|
||||
|
||||
@ -163,7 +189,8 @@ class ShipcallSchema(Schema):
|
||||
|
||||
id = fields.Integer()
|
||||
ship_id = fields.Integer()
|
||||
type = fields.Integer()
|
||||
#type = fields.Enum(ShipcallType, default=ShipcallType.undefined) # type = fields.Integer() # make enum: shipcall type. add validator
|
||||
type = fields.Integer() # make enum: shipcall type. add validator # type = fields.Enum(ShipcallType, default=ShipcallType.undefined) # type = fields.Integer() # make enum: shipcall type. add validator
|
||||
eta = fields.DateTime(metadata={'required':False}, allow_none=True)
|
||||
voyage = fields.String(allow_none=True, metadata={'Required':False}, validate=[validate.Length(max=16)]) # Solving: RemovedInMarshmallow4Warning: Passing field metadata as keyword arguments is deprecated. Use the explicit `metadata=...` argument instead. Additional metadata: {'Required': False}
|
||||
etd = fields.DateTime(metadata={'required':False}, allow_none=True)
|
||||
@ -196,16 +223,24 @@ class ShipcallSchema(Schema):
|
||||
@post_load
|
||||
def make_shipcall(self, data, **kwargs):
|
||||
if 'type' in data:
|
||||
data['type_value'] = data['type'].value
|
||||
data['type_value'] = int(data['type'])
|
||||
else:
|
||||
data['type_value'] = ShipcallType.undefined
|
||||
data['type_value'] = int(ShipcallType.undefined)
|
||||
if 'evaluation' in data:
|
||||
if data['evaluation']:
|
||||
data['evaluation_value'] = data['evaluation'].value
|
||||
data['evaluation_value'] = int(data['evaluation'])
|
||||
else:
|
||||
data['evaluation_value'] = EvaluationType.undefined
|
||||
data['evaluation_value'] = int(EvaluationType.undefined)
|
||||
return data
|
||||
|
||||
@validates("type")
|
||||
def validate_type(self, value):
|
||||
valid_shipcall_type = int(value) in [item.value for item in ShipcallType]
|
||||
|
||||
if not valid_shipcall_type:
|
||||
raise ValidationError(f"the provided type is not a valid shipcall type.")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Participant_Assignment:
|
||||
def __init__(self, participant_id, type):
|
||||
@ -321,7 +356,7 @@ class TimesSchema(Schema):
|
||||
berth_info = fields.String(metadata={'required':False}, allow_none=True, validate=[validate.Length(max=256)])
|
||||
pier_side = fields.Bool(metadata={'required':False}, allow_none = True)
|
||||
shipcall_id = fields.Integer(metadata={'required':True})
|
||||
participant_type = fields.Integer(metadata={'required':False}, allow_none=True)
|
||||
participant_type = fields.Enum(ParticipantType, metadata={'required':False}, allow_none=True, default=ParticipantType.undefined) #fields.Integer(metadata={'required':False}, allow_none=True)
|
||||
ata = fields.DateTime(metadata={'required':False}, allow_none=True)
|
||||
atd = fields.DateTime(metadata={'required':False}, allow_none=True)
|
||||
eta_interval_end = fields.DateTime(metadata={'required':False}, allow_none=True)
|
||||
@ -463,3 +498,22 @@ class Shipcalls(Shipcall):
|
||||
|
||||
class TimesList(Times):
|
||||
pass
|
||||
|
||||
@dataclass
|
||||
class ShipcallParticipantMap:
|
||||
id: int
|
||||
shipcall_id: int
|
||||
participant_id: int
|
||||
type : ShipcallType
|
||||
created: datetime
|
||||
modified: datetime
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"shipcall_id": self.shipcall_id,
|
||||
"participant_id": self.participant_id,
|
||||
"type": self.type.name,
|
||||
"created": self.created.isoformat() if self.created else "",
|
||||
"modified": self.modified.isoformat() if self.modified else "",
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ def check_jwt():
|
||||
if not token:
|
||||
raise Exception('Missing access token')
|
||||
jwt = token.split('Bearer ')[1]
|
||||
|
||||
try:
|
||||
return decode_jwt(jwt)
|
||||
except Exception as e:
|
||||
|
||||
@ -7,11 +7,26 @@ def create_api_key():
|
||||
return secrets.token_urlsafe(16)
|
||||
|
||||
def generate_jwt(payload, lifetime=None):
|
||||
"""
|
||||
creates an encoded token, which is based on the 'SECRET_KEY' environment variable. The environment variable
|
||||
is set when the .wsgi application is started or can theoretically be set on system-level.
|
||||
|
||||
args:
|
||||
payload:
|
||||
json-dictionary with key:value pairs.
|
||||
|
||||
lifetime:
|
||||
When a 'lifetime' (integer) is provided, the payload will be extended by an expiration key 'exp', which is
|
||||
valid for the next {lifetime} minutes.
|
||||
|
||||
returns: token, a JWT-encoded string
|
||||
"""
|
||||
if lifetime:
|
||||
payload['exp'] = (datetime.datetime.now() + datetime.timedelta(minutes=lifetime)).timestamp()
|
||||
return jwt.encode(payload, os.environ.get('SECRET_KEY'), algorithm="HS256")
|
||||
|
||||
def decode_jwt(token):
|
||||
"""this function reverts the {generate_jwt} function. An encoded JWT token is decoded into a JSON dictionary."""
|
||||
return jwt.decode(token, os.environ.get('SECRET_KEY'), algorithms=["HS256"])
|
||||
|
||||
|
||||
|
||||
@ -7,16 +7,16 @@ def get_shipcall_simple():
|
||||
# only used for the stub
|
||||
base_time = datetime.datetime.now()
|
||||
|
||||
shipcall_id = generate_uuid1_int()
|
||||
ship_id = generate_uuid1_int()
|
||||
shipcall_id = 124 # generate_uuid1_int()
|
||||
ship_id = 5 # generate_uuid1_int()
|
||||
|
||||
eta = base_time+datetime.timedelta(hours=3, minutes=12)
|
||||
role_type = 1
|
||||
voyage = "987654321"
|
||||
etd = base_time+datetime.timedelta(hours=6, minutes=12) # should never be before eta
|
||||
|
||||
arrival_berth_id = generate_uuid1_int()
|
||||
departure_berth_id = generate_uuid1_int()
|
||||
arrival_berth_id = 140 #generate_uuid1_int()
|
||||
departure_berth_id = 140 #generate_uuid1_int()
|
||||
|
||||
tug_required = False
|
||||
pilot_required = False
|
||||
@ -39,10 +39,12 @@ def get_shipcall_simple():
|
||||
moored_lock = False # de: 'Festmacherschleuse', en: 'moored lock'
|
||||
canceled = False
|
||||
|
||||
time_ref_point = 0
|
||||
|
||||
evaluation = None
|
||||
evaluation_message = ""
|
||||
evaluation_time = None
|
||||
evaluation_notifications_sent = None
|
||||
evaluation_time = datetime.datetime.now()
|
||||
evaluation_notifications_sent = False
|
||||
created = datetime.datetime.now()
|
||||
modified = created+datetime.timedelta(seconds=10)
|
||||
|
||||
@ -76,6 +78,7 @@ def get_shipcall_simple():
|
||||
evaluation_message,
|
||||
evaluation_time,
|
||||
evaluation_notifications_sent,
|
||||
time_ref_point,
|
||||
created,
|
||||
modified,
|
||||
participants,
|
||||
|
||||
@ -1,8 +1,91 @@
|
||||
|
||||
####################################### InputValidation #######################################
|
||||
|
||||
import json
|
||||
from abc import ABC, abstractmethod
|
||||
from BreCal.schemas.model import Ship, Shipcall, Berth, User, Participant
|
||||
from BreCal.impl.participant import GetParticipant
|
||||
from BreCal.impl.ships import GetShips
|
||||
from BreCal.impl.berths import GetBerths
|
||||
|
||||
from BreCal.database.enums import ParticipantType
|
||||
|
||||
def get_participant_id_dictionary():
|
||||
# get all participants
|
||||
response,status_code,header = GetParticipant(options={})
|
||||
|
||||
# build a dictionary of id:item pairs, so one can select the respective participant
|
||||
participants = json.loads(response)
|
||||
participants = {items.get("id"):items for items in participants}
|
||||
return participants
|
||||
|
||||
def get_ship_id_dictionary():
|
||||
# get all ships
|
||||
response,status_code,header = GetShips(token=None)
|
||||
|
||||
# build a dictionary of id:item pairs, so one can select the respective participant
|
||||
ships = json.loads(response)
|
||||
ships = {items.get("id"):items for items in ships}
|
||||
return ships
|
||||
|
||||
def get_berth_id_dictionary():
|
||||
# get all berths
|
||||
response,status_code,header = GetBerths(token=None)
|
||||
|
||||
# build a dictionary of id:item pairs, so one can select the respective participant
|
||||
berths = json.loads(response)
|
||||
berths = {items.get("id"):items for items in berths}
|
||||
return berths
|
||||
|
||||
def check_if_user_is_bsmd_type(user_data:dict)->bool:
|
||||
"""
|
||||
given a dictionary of user data, determine the respective participant id and read, whether
|
||||
that participant is a .BSMD-type
|
||||
|
||||
Note: ParticipantType is an IntFlag.
|
||||
Hence, ParticipantType(1) is ParticipantType.BSMD,
|
||||
and ParticipantType(7) is [ParticipantType.BSMD, ParticipantType.TERMINAL, ParticipantType.PILOT]
|
||||
|
||||
both would return 'True'
|
||||
|
||||
returns: boolean. Whether the participant id is a .BSMD type element
|
||||
"""
|
||||
# user_data = decode token
|
||||
participant_id = user_data.get("participant_id")
|
||||
|
||||
# build a dictionary of id:item pairs, so one can select the respective participant
|
||||
participants = get_participant_id_dictionary()
|
||||
participant = participants.get(participant_id,{})
|
||||
|
||||
# boolean check: is the participant of type .BSMD?
|
||||
is_bsmd = ParticipantType.BSMD in ParticipantType(participant.get("type",0))
|
||||
return is_bsmd
|
||||
|
||||
def check_if_user_data_has_valid_ship_id(ship_id):
|
||||
# build a dictionary of id:item pairs, so one can select the respective participant
|
||||
ships = get_ship_id_dictionary()
|
||||
|
||||
# boolean check
|
||||
ship_id_is_valid = ship_id in list(ships.keys())
|
||||
return ship_id_is_valid
|
||||
|
||||
def check_if_user_data_has_valid_berth_id(berth_id):
|
||||
# build a dictionary of id:item pairs, so one can select the respective participant
|
||||
berths = get_berth_id_dictionary()
|
||||
|
||||
# boolean check
|
||||
berth_id_is_valid = berth_id in list(berths.keys())
|
||||
return berth_id_is_valid
|
||||
|
||||
def check_if_user_data_has_valid_participant_id(participant_id):
|
||||
# build a dictionary of id:item pairs, so one can select the respective participant
|
||||
participants = get_participant_id_dictionary()
|
||||
|
||||
# boolean check
|
||||
participant_id_is_valid = participant_id in list(participants.keys())
|
||||
return participant_id_is_valid
|
||||
|
||||
|
||||
|
||||
class InputValidation():
|
||||
def __init__(self):
|
||||
|
||||
@ -8,6 +8,7 @@ webargs==6.1.1
|
||||
Werkzeug==1.0.1
|
||||
pydapper[mysql-connector-python]
|
||||
marshmallow-dataclass
|
||||
marshmallow-enum
|
||||
bcrypt
|
||||
pyjwt
|
||||
flask-jwt-extended
|
||||
@ -20,4 +21,4 @@ pytest
|
||||
pytest-cov
|
||||
coverage
|
||||
|
||||
../server/.
|
||||
-e ../server/.
|
||||
|
||||
@ -8,7 +8,7 @@ def test_create_app():
|
||||
import sys
|
||||
from BreCal import get_project_root
|
||||
|
||||
project_root = get_project_root("brecal")
|
||||
project_root = os.path.join(os.path.expanduser("~"), "brecal")
|
||||
lib_location = os.path.join(project_root, "src", "server")
|
||||
sys.path.append(lib_location)
|
||||
|
||||
|
||||
@ -12,7 +12,17 @@ from BreCal.stubs.df_times import get_df_times, random_time_perturbation, get_df
|
||||
@pytest.fixture(scope="session")
|
||||
def build_sql_proxy_connection():
|
||||
import mysql.connector
|
||||
conn_from_pool = mysql.connector.connect(**{'host':'localhost', 'port':3306, 'user':'root', 'password':'HalloWach_2323XXL!!', 'pool_name':'brecal_pool', 'pool_size':20, 'database':'bremen_calling', 'autocommit': True})
|
||||
import os
|
||||
import json
|
||||
connection_data_path = os.path.join(os.path.expanduser("~"),"secure","connection_data_local.json")
|
||||
assert os.path.exists(connection_data_path)
|
||||
|
||||
with open(connection_data_path, "r") as jr:
|
||||
connection_data = json.load(jr)
|
||||
connection_data = {k:v for k,v in connection_data.items() if k in ["host", "port", "user", "password", "pool_size", "pool_name", "database"]}
|
||||
|
||||
conn_from_pool = mysql.connector.connect(**connection_data)
|
||||
#conn_from_pool = mysql.connector.connect(**{'host':'localhost', 'port':3306, 'user':'root', 'password':'HalloWach_2323XXL!!', 'pool_name':'brecal_pool', 'pool_size':20, 'database':'bremen_calling_local', 'autocommit': True})
|
||||
sql_handler = SQLHandler(sql_connection=conn_from_pool, read_all=True)
|
||||
vr = ValidationRules(sql_handler)
|
||||
return locals()
|
||||
|
||||
Reference in New Issue
Block a user