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:
Max Metz 2024-04-24 08:26:37 +02:00
parent fcb889d2bc
commit b32b466f74
13 changed files with 244 additions and 33 deletions

View File

@ -3,7 +3,8 @@ from webargs.flaskparser import parser
from marshmallow import Schema, fields, ValidationError from marshmallow import Schema, fields, ValidationError
from ..schemas import model from ..schemas import model
from .. import impl 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 logging
import json import json
@ -40,6 +41,26 @@ def PostShipcalls():
try: try:
content = request.get_json(force=True) content = request.get_json(force=True)
loadedModel = model.ShipcallSchema().load(data=content, many=False, partial=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: except ValidationError as ex:
logging.error(ex) logging.error(ex)

View File

@ -2,7 +2,7 @@ from enum import IntEnum, Enum, IntFlag
class ParticipantType(IntFlag): class ParticipantType(IntFlag):
"""determines the type of a participant""" """determines the type of a participant"""
NONE = 0 undefined = 0
BSMD = 1 BSMD = 1
TERMINAL = 2 TERMINAL = 2
PILOT = 4 PILOT = 4
@ -13,10 +13,15 @@ class ParticipantType(IntFlag):
class ShipcallType(IntEnum): class ShipcallType(IntEnum):
"""determines the type of a shipcall, as this changes the applicable validation rules""" """determines the type of a shipcall, as this changes the applicable validation rules"""
undefined = 0
INCOMING = 1 INCOMING = 1
OUTGOING = 2 OUTGOING = 2
SHIFTING = 3 SHIFTING = 3
@classmethod
def _missing_(cls, value):
return cls.undefined
class ParticipantwiseTimeDelta(): class ParticipantwiseTimeDelta():
"""stores the time delta for every participant, which triggers the validation rules in the rule set '0001'""" """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 AGENCY = 1200.0 # 20 h * 60 min/h = 1200 min
@ -42,10 +47,23 @@ class PierSide(IntEnum):
"""These enumerators determine the pier side of a shipcall.""" """These enumerators determine the pier side of a shipcall."""
PORTSIDE = 0 # Port/Backbord PORTSIDE = 0 # Port/Backbord
STARBOARD_SIDE = 1 # Starboard / Steuerbord STARBOARD_SIDE = 1 # Starboard / Steuerbord
class NotificationType(IntFlag): class NotificationType(IntFlag):
"""determines the method by which a notification is distributed to users. Flagging allows selecting multiple notification types.""" """determines the method by which a notification is distributed to users. Flagging allows selecting multiple notification types."""
UNDEFINED = 0 UNDEFINED = 0
EMAIL = 1 EMAIL = 1
POPUP = 2 POPUP = 2
MESSENGER = 4 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

View File

@ -2,7 +2,7 @@ import numpy as np
import pandas as pd import pandas as pd
import datetime import datetime
import typing 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 from BreCal.database.enums import ParticipantType
def pandas_series_to_data_model(): def pandas_series_to_data_model():
@ -50,7 +50,8 @@ class SQLHandler():
'ship'->BreCal.schemas.model.Ship object 'ship'->BreCal.schemas.model.Ship object
""" """
self.str_to_model_dict = { 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 return
@ -70,12 +71,16 @@ class SQLHandler():
data = [{k:v for k,v in zip(column_names, dat)} for dat in data] 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) # 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) data_model = self.str_to_model_dict.get(table_name)
if data_model is not None: 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: else:
df = pd.DataFrame([dat for dat in data]) df = pd.DataFrame([dat for dat in data])
return df return df
def mysql_to_df(self, query, table_name): def mysql_to_df(self, query, table_name):
"""provide an arbitrary sql query that should be read from a mysql server {sql_connection}. returns a pandas DataFrame with the obtained data""" """provide an arbitrary sql query that should be read from a mysql server {sql_connection}. returns a pandas DataFrame with the obtained data"""
@ -94,11 +99,7 @@ class SQLHandler():
# 4.) build a dataframe from the respective data models (which ensures the correct data type) # 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) data_model = self.str_to_model_dict.get(table_name)
if data_model is not None: df = self.build_df_from_data_and_name(data, table_name)
df = pd.DataFrame([data_model(**dat) for dat in data])
else:
df = pd.DataFrame([dat for dat in data])
if 'id' in df.columns: if 'id' in df.columns:
df = df.set_index('id', inplace=False) # avoid inplace updates, so the raw sql remains unchanged df = df.set_index('id', inplace=False) # avoid inplace updates, so the raw sql remains unchanged
return df return df

View File

@ -60,8 +60,13 @@ def PostShipcalls(schemaModel):
""" """
:param schemaModel: The deserialized dict of the request :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 # TODO: Validate the upload data
# This creates a *new* entry # This creates a *new* entry
@ -133,7 +138,6 @@ def PostShipcalls(schemaModel):
# if not full_id_existances: # if not full_id_existances:
# pooledConnection.close() # pooledConnection.close()
# return json.dumps({"message" : "call failed. missing mandatory keywords."}), 500, {'Content-Type': 'application/json; charset=utf-8'} # return json.dumps({"message" : "call failed. missing mandatory keywords."}), 500, {'Content-Type': 'application/json; charset=utf-8'}
commands.execute(query, schemaModel) commands.execute(query, schemaModel)
new_id = commands.execute_scalar("select last_insert_id()") new_id = commands.execute_scalar("select last_insert_id()")

View File

@ -7,11 +7,11 @@ import sys
config_path = None config_path = None
def initPool(instancePath): def initPool(instancePath, connection_filename="connection_data_devel.json"):
try: try:
global config_path global config_path
if(config_path == None): 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) print (config_path)

View File

@ -10,6 +10,9 @@ from typing import List
import json import json
import datetime import datetime
from BreCal.validators.time_logic import validate_time_exceeds_threshold 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): def obj_dict(obj):
if isinstance(obj, datetime.datetime): if isinstance(obj, datetime.datetime):
@ -54,6 +57,7 @@ class NotificationType(IntEnum):
undefined = 0 undefined = 0
email = 1 email = 1
push = 2 push = 2
@classmethod @classmethod
def _missing_(cls, value): def _missing_(cls, value):
return cls.undefined return cls.undefined
@ -143,12 +147,34 @@ class Participant(Schema):
street: str street: str
postal_code: str postal_code: str
city: str city: str
type: int type: int # fields.Enum(ParticipantType ...)
flags: int flags: int
created: datetime created: datetime
modified: datetime modified: datetime
deleted: bool 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): class ParticipantList(Participant):
pass pass
@ -163,7 +189,8 @@ class ShipcallSchema(Schema):
id = fields.Integer() id = fields.Integer()
ship_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) 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} 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) etd = fields.DateTime(metadata={'required':False}, allow_none=True)
@ -196,15 +223,23 @@ class ShipcallSchema(Schema):
@post_load @post_load
def make_shipcall(self, data, **kwargs): def make_shipcall(self, data, **kwargs):
if 'type' in data: if 'type' in data:
data['type_value'] = data['type'].value data['type_value'] = int(data['type'])
else: else:
data['type_value'] = ShipcallType.undefined data['type_value'] = int(ShipcallType.undefined)
if 'evaluation' in data: if 'evaluation' in data:
if data['evaluation']: if data['evaluation']:
data['evaluation_value'] = data['evaluation'].value data['evaluation_value'] = int(data['evaluation'])
else: else:
data['evaluation_value'] = EvaluationType.undefined data['evaluation_value'] = int(EvaluationType.undefined)
return data 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 @dataclass
class Participant_Assignment: class Participant_Assignment:
@ -321,7 +356,7 @@ class TimesSchema(Schema):
berth_info = fields.String(metadata={'required':False}, allow_none=True, validate=[validate.Length(max=256)]) 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) pier_side = fields.Bool(metadata={'required':False}, allow_none = True)
shipcall_id = fields.Integer(metadata={'required':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) ata = fields.DateTime(metadata={'required':False}, allow_none=True)
atd = 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) eta_interval_end = fields.DateTime(metadata={'required':False}, allow_none=True)
@ -463,3 +498,22 @@ class Shipcalls(Shipcall):
class TimesList(Times): class TimesList(Times):
pass 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 "",
}

View File

@ -9,6 +9,7 @@ def check_jwt():
if not token: if not token:
raise Exception('Missing access token') raise Exception('Missing access token')
jwt = token.split('Bearer ')[1] jwt = token.split('Bearer ')[1]
try: try:
return decode_jwt(jwt) return decode_jwt(jwt)
except Exception as e: except Exception as e:

View File

@ -7,11 +7,26 @@ def create_api_key():
return secrets.token_urlsafe(16) return secrets.token_urlsafe(16)
def generate_jwt(payload, lifetime=None): 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: if lifetime:
payload['exp'] = (datetime.datetime.now() + datetime.timedelta(minutes=lifetime)).timestamp() payload['exp'] = (datetime.datetime.now() + datetime.timedelta(minutes=lifetime)).timestamp()
return jwt.encode(payload, os.environ.get('SECRET_KEY'), algorithm="HS256") return jwt.encode(payload, os.environ.get('SECRET_KEY'), algorithm="HS256")
def decode_jwt(token): 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"]) return jwt.decode(token, os.environ.get('SECRET_KEY'), algorithms=["HS256"])

View File

@ -7,21 +7,21 @@ def get_shipcall_simple():
# only used for the stub # only used for the stub
base_time = datetime.datetime.now() base_time = datetime.datetime.now()
shipcall_id = generate_uuid1_int() shipcall_id = 124 # generate_uuid1_int()
ship_id = generate_uuid1_int() ship_id = 5 # generate_uuid1_int()
eta = base_time+datetime.timedelta(hours=3, minutes=12) eta = base_time+datetime.timedelta(hours=3, minutes=12)
role_type = 1 role_type = 1
voyage = "987654321" voyage = "987654321"
etd = base_time+datetime.timedelta(hours=6, minutes=12) # should never be before eta etd = base_time+datetime.timedelta(hours=6, minutes=12) # should never be before eta
arrival_berth_id = generate_uuid1_int() arrival_berth_id = 140 #generate_uuid1_int()
departure_berth_id = generate_uuid1_int() departure_berth_id = 140 #generate_uuid1_int()
tug_required = False tug_required = False
pilot_required = False pilot_required = False
flags = 0 # #TODO_shipcall_flags. What is meant here? What should be tested? flags = 0 # #TODO_shipcall_flags. What is meant here? What should be tested?
pier_side = False # whether a ship will be fixated on the pier side. en: pier side, de: Anlegestelle. From 'BremenCalling_Datenmodell.xlsx': gedreht/ungedreht pier_side = False # whether a ship will be fixated on the pier side. en: pier side, de: Anlegestelle. From 'BremenCalling_Datenmodell.xlsx': gedreht/ungedreht
bunkering = False # #TODO_bunkering_unclear bunkering = False # #TODO_bunkering_unclear
replenishing_terminal = False # en: replenishing terminal, de: Nachfüll-Liegeplatz replenishing_terminal = False # en: replenishing terminal, de: Nachfüll-Liegeplatz
@ -38,11 +38,13 @@ def get_shipcall_simple():
anchored = False anchored = False
moored_lock = False # de: 'Festmacherschleuse', en: 'moored lock' moored_lock = False # de: 'Festmacherschleuse', en: 'moored lock'
canceled = False canceled = False
time_ref_point = 0
evaluation = None evaluation = None
evaluation_message = "" evaluation_message = ""
evaluation_time = None evaluation_time = datetime.datetime.now()
evaluation_notifications_sent = None evaluation_notifications_sent = False
created = datetime.datetime.now() created = datetime.datetime.now()
modified = created+datetime.timedelta(seconds=10) modified = created+datetime.timedelta(seconds=10)
@ -76,6 +78,7 @@ def get_shipcall_simple():
evaluation_message, evaluation_message,
evaluation_time, evaluation_time,
evaluation_notifications_sent, evaluation_notifications_sent,
time_ref_point,
created, created,
modified, modified,
participants, participants,

View File

@ -1,8 +1,91 @@
####################################### InputValidation ####################################### ####################################### InputValidation #######################################
import json
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from BreCal.schemas.model import Ship, Shipcall, Berth, User, Participant 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(): class InputValidation():
def __init__(self): def __init__(self):

View File

@ -8,6 +8,7 @@ webargs==6.1.1
Werkzeug==1.0.1 Werkzeug==1.0.1
pydapper[mysql-connector-python] pydapper[mysql-connector-python]
marshmallow-dataclass marshmallow-dataclass
marshmallow-enum
bcrypt bcrypt
pyjwt pyjwt
flask-jwt-extended flask-jwt-extended
@ -20,4 +21,4 @@ pytest
pytest-cov pytest-cov
coverage coverage
../server/. -e ../server/.

View File

@ -8,7 +8,7 @@ def test_create_app():
import sys import sys
from BreCal import get_project_root 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") lib_location = os.path.join(project_root, "src", "server")
sys.path.append(lib_location) sys.path.append(lib_location)

View File

@ -12,7 +12,17 @@ from BreCal.stubs.df_times import get_df_times, random_time_perturbation, get_df
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def build_sql_proxy_connection(): def build_sql_proxy_connection():
import mysql.connector 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) sql_handler = SQLHandler(sql_connection=conn_from_pool, read_all=True)
vr = ValidationRules(sql_handler) vr = ValidationRules(sql_handler)
return locals() return locals()