Skip to content

Commit e6d2f80

Browse files
Merge pull request #89 from Project-OMOTES/81-add-variable-conditional-relations-in-workflow-definition-including-smaller-and-greater-than
81 add variable conditional relations in workflow definition including smaller and greater than
2 parents 359072a + f916cd1 commit e6d2f80

File tree

2 files changed

+170
-15
lines changed

2 files changed

+170
-15
lines changed

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ classifiers = [
2525

2626
dependencies = [
2727
"aio-pika ~= 9.4, < 9.5",
28-
"omotes-sdk-protocol ~= 1.1",
28+
"omotes-sdk-protocol ~= 1.2",
2929
"pyesdl ~= 24.2",
3030
"pamqp ~= 3.3",
3131
"celery ~= 5.3",
@@ -75,7 +75,7 @@ enabled = true
7575
starting_version = "0.0.1"
7676

7777
[tool.pytest.ini_options]
78-
addopts = "--cov=omotes_sdk --cov-report html --cov-report term-missing --cov-fail-under 62"
78+
addopts = "--cov=omotes_sdk --cov-report html --cov-report term-missing --cov-fail-under 60"
7979

8080
[tool.coverage.run]
8181
source = ["src"]

src/omotes_sdk/workflow_type.py

Lines changed: 168 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import json
22
import logging
3+
import pprint
34
from abc import ABC, abstractmethod
45
from dataclasses import dataclass, field
56
from 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
78
from typing_extensions import Self, override
89

910
from 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)
135188
class 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)
847995
class 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

8601007
class 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

Comments
 (0)