11import json
22import logging
3+ import pprint
34from abc import ABC , abstractmethod
45from dataclasses import dataclass , field
56from datetime import datetime , timedelta
6- from typing import List , Optional , Dict , Union , Any , Type , TypeVar , cast
7+ from typing import List , Optional , Dict , Union , Any , Type , TypeVar , cast , Literal
78from typing_extensions import Self , override
89
910from omotes_sdk_protocol .workflow_pb2 import (
@@ -49,6 +50,10 @@ class WorkflowParameter(ABC):
4950 """Optional description (displayed below the input field)."""
5051 type_name : str = ""
5152 """Parameter type name, set in child class."""
53+ constraints : List [WorkflowParameterPb .Constraint ] = field (
54+ default_factory = list , hash = False , compare = False
55+ )
56+ """Optional list of non-ESDL workflow parameters."""
5257
5358 @staticmethod
5459 @abstractmethod
@@ -130,6 +135,54 @@ def to_pb_value(value: ParamsDictValues) -> PBStructCompatibleTypes:
130135 """
131136 ... # pragma: no cover
132137
138+ def check_parameter_constraint (
139+ self ,
140+ value1 : ParamsDictValues ,
141+ value2 : ParamsDictValues ,
142+ check : WorkflowParameterPb .Constraint ,
143+ ) -> Literal [True ]:
144+ """Check if the values adhere to the parameter constraint.
145+
146+ :param value1: The left-hand value to be checked.
147+ :param value2: The right-hand value to the checked.
148+ :param check: The parameter constraint to check between `value1` and `value2`
149+ :return: Always true if the function returns noting the parameter constraint is adhered to.
150+ :raises RuntimeError: In case the parameter constraint is not adhered to.
151+ """
152+ supported_types = (float , int , datetime , timedelta )
153+ if not isinstance (value1 , supported_types ) or not isinstance (value2 , supported_types ):
154+ raise RuntimeError (
155+ f"Values { value1 } , { value2 } are of a type that are not supported "
156+ f"by parameter constraint { check } "
157+ )
158+
159+ same_type_required = (datetime , timedelta )
160+ if (
161+ isinstance (value1 , same_type_required ) or isinstance (value2 , same_type_required )
162+ ) and type (value1 ) is not type (value2 ):
163+ raise RuntimeError (
164+ f"Values { value1 } , { value2 } are required to be of the same type to be"
165+ f"supported by parameter constraint { check } "
166+ )
167+
168+ if check .relation == WorkflowParameterPb .Constraint .RelationType .GREATER :
169+ result = value1 > value2 # type: ignore[operator]
170+ elif check .relation == WorkflowParameterPb .Constraint .RelationType .GREATER_OR_EQ :
171+ result = value1 >= value2 # type: ignore[operator]
172+ elif check .relation == WorkflowParameterPb .Constraint .RelationType .SMALLER :
173+ result = value1 < value2 # type: ignore[operator]
174+ elif check .relation == WorkflowParameterPb .Constraint .RelationType .SMALLER_OR_EQ :
175+ result = value1 <= value2 # type: ignore[operator]
176+ else :
177+ raise RuntimeError ("Unknown parameter constraint. Please implement." )
178+
179+ if not result :
180+ raise RuntimeError (
181+ f"Check failed for constraint { check .relation } with "
182+ f"{ self .key_name } : { value1 } and { check .other_key_name } : { value2 } "
183+ )
184+ return result
185+
133186
134187@dataclass (eq = True , frozen = True )
135188class StringEnumOption :
@@ -196,6 +249,7 @@ def from_pb_message(
196249 description = parameter_pb .description ,
197250 default = parameter_type_pb .default ,
198251 enum_options = [],
252+ constraints = list (parameter_pb .constraints ),
199253 )
200254 for enum_option_pb in parameter_type_pb .enum_options :
201255 if parameter_type_pb .enum_options and parameter .enum_options is not None :
@@ -221,6 +275,16 @@ def from_json_config(cls, json_config: Dict) -> Self:
221275 if "enum_options" in json_config and not isinstance (json_config ["enum_options" ], List ):
222276 raise TypeError ("'enum_options' for StringParameter must be a 'list'" )
223277
278+ if "constraints" in json_config :
279+ if not isinstance (json_config ["constraints" ], list ):
280+ raise TypeError ("'constraints' for StringParameter must be a 'list'" )
281+
282+ parsed_constraints = [
283+ convert_json_to_parameter_constraint (constraint )
284+ for constraint in json_config ["constraints" ]
285+ ]
286+ json_config ["constraints" ] = parsed_constraints
287+
224288 if "enum_options" in json_config :
225289 enum_options = []
226290 for enum_option in json_config ["enum_options" ]:
@@ -316,6 +380,7 @@ def from_pb_message(
316380 title = parameter_pb .title ,
317381 description = parameter_pb .description ,
318382 default = parameter_type_pb .default ,
383+ constraints = list (parameter_pb .constraints ),
319384 )
320385
321386 @classmethod
@@ -331,6 +396,17 @@ def from_json_config(cls, json_config: Dict) -> Self:
331396 f"'default' for BooleanParameter must be in 'bool' format:"
332397 f" '{ json_config ['default' ]} '"
333398 )
399+
400+ if "constraints" in json_config :
401+ if not isinstance (json_config ["constraints" ], list ):
402+ raise TypeError ("'constraints' for BooleanParameter must be a 'list'" )
403+
404+ parsed_constraints = [
405+ convert_json_to_parameter_constraint (constraint )
406+ for constraint in json_config ["constraints" ]
407+ ]
408+ json_config ["constraints" ] = parsed_constraints
409+
334410 return cls (** json_config )
335411
336412 @staticmethod
@@ -415,6 +491,7 @@ def from_pb_message(
415491 maximum = (
416492 parameter_type_pb .maximum if parameter_type_pb .HasField ("maximum" ) else None
417493 ), # protobuf has '0' default value for int instead of None
494+ constraints = list (parameter_pb .constraints ),
418495 )
419496
420497 @classmethod
@@ -432,6 +509,17 @@ def from_json_config(cls, json_config: Dict) -> Self:
432509 f"'{ int_param } ' for IntegerParameter must be in 'int' format:"
433510 f" '{ json_config [int_param ]} '"
434511 )
512+
513+ if "constraints" in json_config :
514+ if not isinstance (json_config ["constraints" ], list ):
515+ raise TypeError ("'constraints' for IntegerParameter must be a 'list'" )
516+
517+ parsed_constraints = [
518+ convert_json_to_parameter_constraint (constraint )
519+ for constraint in json_config ["constraints" ]
520+ ]
521+ json_config ["constraints" ] = parsed_constraints
522+
435523 return cls (** json_config )
436524
437525 @staticmethod
@@ -526,6 +614,7 @@ def from_pb_message(
526614 maximum = (
527615 parameter_type_pb .maximum if parameter_type_pb .HasField ("maximum" ) else None
528616 ), # protobuf has '0' default value for int instead of None
617+ constraints = list (parameter_pb .constraints ),
529618 )
530619
531620 @classmethod
@@ -548,6 +637,16 @@ def from_json_config(cls, json_config: Dict) -> Self:
548637 f" '{ json_config [float_param ]} '"
549638 )
550639
640+ if "constraints" in json_config :
641+ if not isinstance (json_config ["constraints" ], list ):
642+ raise TypeError ("'constraints' for FloatParameter must be a 'list'" )
643+
644+ parsed_constraints = [
645+ convert_json_to_parameter_constraint (constraint )
646+ for constraint in json_config ["constraints" ]
647+ ]
648+ json_config ["constraints" ] = parsed_constraints
649+
551650 return cls (** json_config )
552651
553652 @staticmethod
@@ -636,6 +735,7 @@ def from_pb_message(
636735 title = parameter_pb .title ,
637736 description = parameter_pb .description ,
638737 default = default ,
738+ constraints = list (parameter_pb .constraints ),
639739 )
640740
641741 @classmethod
@@ -656,6 +756,16 @@ def from_json_config(cls, json_config: Dict) -> Self:
656756 )
657757 json_config ["default" ] = default
658758
759+ if "constraints" in json_config :
760+ if not isinstance (json_config ["constraints" ], list ):
761+ raise TypeError ("'constraints' for DateTimeParameter must be a 'list'" )
762+
763+ parsed_constraints = [
764+ convert_json_to_parameter_constraint (constraint )
765+ for constraint in json_config ["constraints" ]
766+ ]
767+ json_config ["constraints" ] = parsed_constraints
768+
659769 return cls (** json_config )
660770
661771 @staticmethod
@@ -752,6 +862,7 @@ def from_pb_message(
752862 if parameter_type_pb .HasField ("maximum" )
753863 else None
754864 ),
865+ constraints = list (parameter_pb .constraints ),
755866 )
756867
757868 @classmethod
@@ -779,6 +890,16 @@ def from_json_config(cls, json_config: Dict) -> Self:
779890 elif duration_param in json_config :
780891 args [duration_param ] = timedelta (seconds = json_config [duration_param ])
781892
893+ if "constraints" in json_config :
894+ if not isinstance (json_config ["constraints" ], list ):
895+ raise TypeError ("'constraints' for StringParameter must be a 'list'" )
896+
897+ parsed_constraints = [
898+ convert_json_to_parameter_constraint (constraint )
899+ for constraint in json_config ["constraints" ]
900+ ]
901+ args ["constraints" ] = parsed_constraints
902+
782903 return cls (** args )
783904
784905 @staticmethod
@@ -843,6 +964,33 @@ def to_pb_value(value: ParamsDictValues) -> float:
843964}
844965
845966
967+ def convert_str_to_parameter_relation (
968+ parameter_constraint_name : str ,
969+ ) -> WorkflowParameterPb .Constraint .RelationType .ValueType :
970+ """Translate the name of a parameter constraint to the relevant enum.
971+
972+ :param parameter_constraint_name: String name of the parameter constraint.
973+ :return: The parameter constraint as an enum value of `Constraint.RelationType`
974+ :raises RuntimeError: In case the parameter constraint name is unknown.
975+ """
976+ return WorkflowParameterPb .Constraint .RelationType .Value (parameter_constraint_name .upper ())
977+
978+
979+ def convert_json_to_parameter_constraint (
980+ parameter_constraint_json : dict ,
981+ ) -> WorkflowParameterPb .Constraint :
982+ """Convert a json document containing a parameter constraint definition to a `Constraint`.
983+
984+ :param parameter_constraint_json: The json document which contains the parameter constraint
985+ definition.
986+ :return: The converted parameter constraint definition.
987+ """
988+ return WorkflowParameterPb .Constraint (
989+ other_key_name = parameter_constraint_json ["other_key_name" ],
990+ relation = convert_str_to_parameter_relation (parameter_constraint_json ["relation" ]),
991+ )
992+
993+
846994@dataclass (eq = True , frozen = True )
847995class WorkflowType :
848996 """Define a type of workflow this SDK supports."""
@@ -854,7 +1002,6 @@ class WorkflowType:
8541002 workflow_parameters : Optional [List [WorkflowParameter ]] = field (
8551003 default = None , hash = False , compare = False
8561004 )
857- """Optional list of non-ESDL workflow parameters."""
8581005
8591006
8601007class WorkflowTypeManager :
@@ -910,6 +1057,7 @@ def to_pb_message(self) -> AvailableWorkflows:
9101057 key_name = _parameter .key_name ,
9111058 title = _parameter .title ,
9121059 description = _parameter .description ,
1060+ constraints = _parameter .constraints ,
9131061 )
9141062 parameter_type_to_pb_type_oneof = {
9151063 StringParameter : parameter_pb .string_parameter ,
@@ -938,6 +1086,7 @@ def from_pb_message(cls, available_workflows_pb: AvailableWorkflows) -> Self:
9381086 :return: WorkflowTypeManager instance.
9391087 """
9401088 workflow_types = []
1089+ workflow_pb : Workflow
9411090 for workflow_pb in available_workflows_pb .workflows :
9421091 workflow_parameters : List [WorkflowParameter ] = []
9431092 for parameter_pb in workflow_pb .parameters :
@@ -956,6 +1105,7 @@ def from_pb_message(cls, available_workflows_pb: AvailableWorkflows) -> Self:
9561105 workflow_parameters .append (parameter )
9571106 else :
9581107 raise RuntimeError (f"Unknown PB class { type (one_of_parameter_type_pb )} " )
1108+
9591109 workflow_types .append (
9601110 WorkflowType (
9611111 workflow_type_name = workflow_pb .type_name ,
@@ -974,20 +1124,20 @@ def from_json_config_file(cls, json_config_file_path: str) -> Self:
9741124 """
9751125 with open (json_config_file_path , "r" ) as f :
9761126 json_config_dict = json .load (f )
1127+ logger .debug ("Loading workflow config: %s" , pprint .pformat (json_config_dict ))
9771128 workflow_types = []
9781129 for _workflow in json_config_dict :
9791130 workflow_parameters = []
980- if "workflow_parameters" in _workflow :
981- for parameter_config in _workflow ["workflow_parameters" ]:
982- parameter_type_name = parameter_config ["parameter_type" ]
983- parameter_config .pop ("parameter_type" )
984-
985- for parameter_type_class in PARAMETER_CLASS_TO_PB_CLASS :
986- if parameter_type_class .type_name == parameter_type_name :
987- workflow_parameters .append (
988- parameter_type_class .from_json_config (parameter_config )
989- )
990- break
1131+ for parameter_config in _workflow .get ("workflow_parameters" , []):
1132+ parameter_type_name = parameter_config ["parameter_type" ]
1133+ parameter_config .pop ("parameter_type" )
1134+
1135+ for parameter_type_class in PARAMETER_CLASS_TO_PB_CLASS :
1136+ if parameter_type_class .type_name == parameter_type_name :
1137+ workflow_parameters .append (
1138+ parameter_type_class .from_json_config (parameter_config )
1139+ )
1140+ break
9911141
9921142 workflow_types .append (
9931143 WorkflowType (
@@ -1023,6 +1173,11 @@ def convert_params_dict_to_struct(workflow: WorkflowType, params_dict: ParamsDic
10231173
10241174 normalized_dict [parameter .key_name ] = parameter .to_pb_value (param_value )
10251175
1176+ for constraint in parameter .constraints :
1177+ other_value = params_dict [constraint .other_key_name ]
1178+
1179+ parameter .check_parameter_constraint (param_value , other_value , constraint )
1180+
10261181 params_dict_struct = Struct ()
10271182 params_dict_struct .update (normalized_dict )
10281183
0 commit comments