Skip to content

Commit c1b703c

Browse files
author
Nazar F
authored
feat(Parameter): HEXA-1134 adds dhis2 parameters widget and connection fields plus validation (#225)
* remove deprecated parameter specs * add connection field validation * apply linting * apply linting * fix: assert test for widget parameter * only check supported conneciton types * use dict and change error message
1 parent 8d93d99 commit c1b703c

9 files changed

Lines changed: 181 additions & 88 deletions

File tree

openhexa/cli/api.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,3 +596,23 @@ def upload_pipeline(
596596
raise Exception(data["uploadPipeline"]["errors"])
597597

598598
return data["uploadPipeline"]["pipelineVersion"]
599+
600+
601+
def is_dhis2_connection_up(workspace_slug: str, connection_slug: str) -> bool:
602+
"""DHIS2 connection status."""
603+
response = graphql(
604+
"""
605+
query getConnectionBySlug($workspaceSlug: String!, $connectionSlug: String!) {
606+
connectionBySlug(workspaceSlug:$workspaceSlug, connectionSlug: $connectionSlug){
607+
... on DHIS2Connection {
608+
status
609+
}
610+
}
611+
}
612+
""",
613+
variables={
614+
"workspaceSlug": workspace_slug,
615+
"connectionSlug": connection_slug,
616+
},
617+
)
618+
response["data"]["connectionBySlug"]["status"] == "UP"

openhexa/sdk/pipelines/log_level.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Log levels for the pipeline runs."""
2+
23
from enum import IntEnum
34

45

openhexa/sdk/pipelines/parameter.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ def validate(self, value: typing.Optional[typing.Any]) -> typing.Optional[typing
5858
raise ParameterValueError(
5959
f"Invalid type for value {value} (expected {self.expected_type}, got {type(value)})"
6060
)
61-
6261
return value
6362

6463
def validate_default(self, value: typing.Optional[typing.Any]):
@@ -382,6 +381,8 @@ def __init__(
382381
choices: typing.Optional[typing.Sequence] = None,
383382
help: typing.Optional[str] = None,
384383
default: typing.Optional[typing.Any] = None,
384+
widget: typing.Optional[str] = None,
385+
connection: typing.Optional[str] = None,
385386
required: bool = True,
386387
multiple: bool = False,
387388
):
@@ -419,6 +420,9 @@ def __init__(
419420
raise InvalidParameterError(f"Parameters of type {self.type} can't have multiple values.")
420421
self.multiple = multiple
421422

423+
self.widget = widget
424+
self.connection = connection
425+
422426
self._validate_default(default, multiple)
423427
self.default = default
424428

@@ -438,6 +442,8 @@ def to_dict(self) -> dict[str, typing.Any]:
438442
"choices": self.choices,
439443
"help": self.help,
440444
"default": self.default,
445+
"widget": self.widget,
446+
"connection": self.connection,
441447
"required": self.required,
442448
"multiple": self.multiple,
443449
}
@@ -511,18 +517,17 @@ def _validate_default(self, default: typing.Any, multiple: bool):
511517
f"The default value for {self.code} is not included in the provided choices."
512518
)
513519

514-
def parameter_spec(self) -> dict[str, typing.Any]:
515-
"""Build specification for this parameter, to be provided to the OpenHEXA backend."""
516-
return {
517-
"type": self.type.spec_type,
518-
"required": self.required,
519-
"choices": self.choices,
520-
"code": self.code,
521-
"name": self.name,
522-
"help": self.help,
523-
"multiple": self.multiple,
524-
"default": self.default,
525-
}
520+
521+
def validate_parameters_with_connection(parameters: [Parameter]):
522+
"""Validate the provided connection parameters if they relate to existing connection parameter."""
523+
supported_connection_types = {DHIS2ConnectionType}
524+
connection_parameters = {p.code for p in parameters if type(p.type) in supported_connection_types}
525+
526+
for parameter in parameters:
527+
if parameter.connection and parameter.connection not in connection_parameters:
528+
raise InvalidParameterError(
529+
f"Connection field '{parameter.code}' references a non-existing connection parameter '{parameter.connection}'"
530+
)
526531

527532

528533
def parameter(

openhexa/sdk/pipelines/pipeline.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,6 @@ def run(self, config: dict[str, typing.Any]):
163163
now = datetime.datetime.now(tz=datetime.timezone.utc).replace(microsecond=0).isoformat()
164164
print(f'{now} Successfully completed pipeline "{self.code}"')
165165

166-
def parameters_spec(self) -> list[dict[str, typing.Any]]:
167-
"""Return the individual specifications of all the parameters of this pipeline."""
168-
return [arg.parameter_spec() for arg in self.parameters]
169-
170166
def to_dict(self):
171167
"""Return a dictionary representation of the pipeline."""
172168
return {

openhexa/sdk/pipelines/runtime.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import requests
1616

1717
from openhexa.sdk.pipelines.exceptions import PipelineNotFound
18-
from openhexa.sdk.pipelines.parameter import TYPES_BY_PYTHON_TYPE, Parameter
18+
from openhexa.sdk.pipelines.parameter import TYPES_BY_PYTHON_TYPE, Parameter, validate_parameters_with_connection
1919

2020
from .pipeline import Pipeline
2121

@@ -147,6 +147,8 @@ def get_pipeline(pipeline_path: Path) -> Pipeline:
147147
Argument("choices", [ast.List]),
148148
Argument("help", [ast.Constant]),
149149
Argument("default", [ast.Constant, ast.List]),
150+
Argument("widget", [ast.Constant]),
151+
Argument("connection", [ast.Constant]),
150152
Argument("required", [ast.Constant], default_value=True),
151153
Argument("multiple", [ast.Constant], default_value=False),
152154
),
@@ -159,6 +161,8 @@ def get_pipeline(pipeline_path: Path) -> Pipeline:
159161
parameter = Parameter(type=type_class.expected_type, **parameter_args)
160162
pipelines_parameters.append(parameter)
161163

164+
validate_parameters_with_connection(pipelines_parameters)
165+
162166
pipeline = Pipeline(parameters=pipelines_parameters, function=None, **pipeline_decorator_spec["args"])
163167

164168
if pipeline is None:

tests/test_ast.py

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import tempfile
44
from unittest import TestCase
55

6-
from openhexa.sdk.pipelines.exceptions import PipelineNotFound
6+
from openhexa.sdk.pipelines.exceptions import InvalidParameterError, PipelineNotFound
77
from openhexa.sdk.pipelines.runtime import get_pipeline
88

99

@@ -147,6 +147,8 @@ def test_pipeline_with_int_param(self):
147147
"type": "int",
148148
"name": "Test Param",
149149
"default": 42,
150+
"widget": None,
151+
"connection": None,
150152
"help": "Param help",
151153
"required": True,
152154
}
@@ -188,6 +190,8 @@ def test_pipeline_with_multiple_param(self):
188190
"type": "int",
189191
"name": "Test Param",
190192
"default": [42],
193+
"widget": None,
194+
"connection": None,
191195
"help": "Param help",
192196
"required": True,
193197
}
@@ -230,6 +234,8 @@ def test_pipeline_with_dataset(self):
230234
"type": "dataset",
231235
"name": "Dataset",
232236
"default": None,
237+
"widget": None,
238+
"connection": None,
233239
"help": "Dataset",
234240
"required": False,
235241
}
@@ -271,6 +277,8 @@ def test_pipeline_with_choices(self):
271277
"type": "str",
272278
"name": "Test Param",
273279
"default": None,
280+
"widget": None,
281+
"connection": None,
274282
"help": "Param help",
275283
"required": True,
276284
}
@@ -340,6 +348,8 @@ def test_pipeline_with_bool(self):
340348
"type": "bool",
341349
"name": "Test Param",
342350
"default": True,
351+
"widget": None,
352+
"connection": None,
343353
"help": "Param help",
344354
"required": True,
345355
}
@@ -382,6 +392,8 @@ def test_pipeline_with_multiple_parameters(self):
382392
"type": "int",
383393
"name": "Test Param",
384394
"default": 42,
395+
"widget": None,
396+
"connection": None,
385397
"help": "Param help",
386398
"required": True,
387399
},
@@ -392,6 +404,8 @@ def test_pipeline_with_multiple_parameters(self):
392404
"type": "str",
393405
"name": "Test Param 2",
394406
"default": None,
407+
"widget": None,
408+
"connection": None,
395409
"help": "Param help 2",
396410
"required": True,
397411
},
@@ -419,3 +433,124 @@ def test_pipeline_with_unsupported_parameter(self):
419433
)
420434
with self.assertRaises(KeyError):
421435
get_pipeline(tmpdirname)
436+
437+
def test_pipeline_with_connection_parameter(self):
438+
"""The file contains a @pipeline decorator and a @parameter decorator with a connection type."""
439+
with tempfile.TemporaryDirectory() as tmpdirname:
440+
with open(f"{tmpdirname}/pipeline.py", "w") as f:
441+
f.write(
442+
"\n".join(
443+
[
444+
"from openhexa.sdk.pipelines import pipeline, parameter",
445+
"",
446+
"@parameter('dhis_con', name='DHIS2 Connection', type=DHIS2Connection, required=True)",
447+
"@parameter('data_element_ids', name='Data Elements id', type=str, widget='dhis2.data_elements.picker', connection='dhis_con', required=True)",
448+
"@pipeline('test', 'Test pipeline')",
449+
"def test_pipeline():",
450+
" pass",
451+
"",
452+
]
453+
)
454+
)
455+
pipeline = get_pipeline(tmpdirname)
456+
self.maxDiff = None
457+
self.assertEqual(
458+
pipeline.to_dict(),
459+
{
460+
"code": "test",
461+
"name": "Test pipeline",
462+
"function": None,
463+
"tasks": [],
464+
"parameters": [
465+
{
466+
"code": "dhis_con",
467+
"type": "dhis2",
468+
"name": "DHIS2 Connection",
469+
"default": None,
470+
"multiple": False,
471+
"choices": None,
472+
"widget": None,
473+
"connection": None,
474+
"help": None,
475+
"required": True,
476+
},
477+
{
478+
"code": "data_element_ids",
479+
"type": "str",
480+
"name": "Data Elements id",
481+
"widget": "dhis2.data_elements.picker",
482+
"connection": "dhis_con",
483+
"default": None,
484+
"multiple": False,
485+
"choices": None,
486+
"help": None,
487+
"required": True,
488+
},
489+
],
490+
"timeout": None,
491+
},
492+
)
493+
494+
def test_pipeline_wit_wrong_connection_parameter(self):
495+
"""The file contains a @pipeline decorator and a @parameter decorator with a non-existing connection type."""
496+
with tempfile.TemporaryDirectory() as tmpdirname:
497+
with open(f"{tmpdirname}/pipeline.py", "w") as f:
498+
f.write(
499+
"\n".join(
500+
[
501+
"from openhexa.sdk.pipelines import pipeline, parameter",
502+
"",
503+
"@parameter('dhis_con', name='DHIS2 Connection', type=DHIS2Connection, required=True)",
504+
"@parameter('data_element_ids', name='Data Elements id', type=str, widget='dhis2.data_elements.picker', connection='sds_con', required=True)",
505+
"@pipeline('test', 'Test pipeline')",
506+
"def test_pipeline():",
507+
" pass",
508+
"",
509+
]
510+
)
511+
)
512+
with self.assertRaises(InvalidParameterError):
513+
get_pipeline(tmpdirname)
514+
515+
def test_pipeline_with_widget_without_connection(self):
516+
"""The file contains a @pipeline decorator and a @parameter decorator with a widget parameter field."""
517+
with tempfile.TemporaryDirectory() as tmpdirname:
518+
with open(f"{tmpdirname}/pipeline.py", "w") as f:
519+
f.write(
520+
"\n".join(
521+
[
522+
"from openhexa.sdk.pipelines import pipeline, parameter",
523+
"",
524+
"@parameter('test_field_for_wdiget', name='Widget Param', type=str, widget='custom_picker', help='Param help')",
525+
"@pipeline('test', 'Test pipeline')",
526+
"def test_pipeline():",
527+
" pass",
528+
"",
529+
]
530+
)
531+
)
532+
pipeline = get_pipeline(tmpdirname)
533+
self.assertEqual(
534+
pipeline.to_dict(),
535+
{
536+
"code": "test",
537+
"name": "Test pipeline",
538+
"function": None,
539+
"tasks": [],
540+
"parameters": [
541+
{
542+
"code": "test_field_for_wdiget",
543+
"type": "str",
544+
"name": "Widget Param",
545+
"default": None,
546+
"multiple": False,
547+
"choices": None,
548+
"widget": "custom_picker",
549+
"connection": None,
550+
"help": "Param help",
551+
"required": True,
552+
}
553+
],
554+
"timeout": None,
555+
},
556+
)

tests/test_parameter.py

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -316,43 +316,6 @@ def test_parameter_validate_multiple():
316316
parameter_4.validate(["ab", "xy"])
317317

318318

319-
def test_parameter_parameters_spec():
320-
"""Verify that parameter specifications are built properly and have the proper defaults."""
321-
# required is True by default
322-
an_parameter = Parameter("arg1", type=str, default="yep")
323-
another_parameter = Parameter(
324-
"arg2",
325-
type=str,
326-
name="Arg 2",
327-
help="Help 2",
328-
choices=["ab", "cd"],
329-
required=False,
330-
multiple=True,
331-
)
332-
333-
assert an_parameter.parameter_spec() == {
334-
"code": "arg1",
335-
"name": None,
336-
"type": "str",
337-
"required": True,
338-
"choices": None,
339-
"help": None,
340-
"multiple": False,
341-
"default": "yep",
342-
}
343-
344-
assert another_parameter.parameter_spec() == {
345-
"code": "arg2",
346-
"name": "Arg 2",
347-
"type": "str",
348-
"required": False,
349-
"choices": ["ab", "cd"],
350-
"help": "Help 2",
351-
"multiple": True,
352-
"default": None,
353-
}
354-
355-
356319
def test_parameter_decorator():
357320
"""Ensure that the @parameter decorator behaves as expected (options and defaults)."""
358321

0 commit comments

Comments
 (0)