diff --git a/.github/workflows/schema-compatibility-cron.yml b/.github/workflows/schema-compatibility-cron.yml index f95ce238..46c9980c 100644 --- a/.github/workflows/schema-compatibility-cron.yml +++ b/.github/workflows/schema-compatibility-cron.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/setup-python@v6 with: - python-version: "3.14" + python-version: "3.13" - run: pip install --upgrade openhexa.sdk diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1cbd6b51..8dff6b0e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.19.3" + ".": "2.19.4" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b5120bcc..7fc9d8e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [2.19.4](https://github.com/BLSQ/openhexa-sdk-python/compare/v2.19.3...v2.19.4) (2026-04-01) + + +### Bug Fixes + +* parameter typing for File ([#371](https://github.com/BLSQ/openhexa-sdk-python/issues/371)) ([c19a404](https://github.com/BLSQ/openhexa-sdk-python/commit/c19a4048d840a849e7df0669ea100417521417e1)) + + +### Miscellaneous + +* **deps:** update dependency python to 3.14 ([#363](https://github.com/BLSQ/openhexa-sdk-python/issues/363)) ([178b0a7](https://github.com/BLSQ/openhexa-sdk-python/commit/178b0a763455393be282ca9602db25f6182b6471)) + ## [2.19.3](https://github.com/BLSQ/openhexa-sdk-python/compare/v2.19.2...v2.19.3) (2026-03-16) diff --git a/openhexa/graphql/graphql_client/enums.py b/openhexa/graphql/graphql_client/enums.py index b365adb5..e1fd56a0 100644 --- a/openhexa/graphql/graphql_client/enums.py +++ b/openhexa/graphql/graphql_client/enums.py @@ -571,6 +571,7 @@ class ParameterType(str, Enum): int = "int" postgresql = "postgresql" s3 = "s3" + secret = "secret" str = "str" diff --git a/openhexa/graphql/schema.generated.graphql b/openhexa/graphql/schema.generated.graphql index 32aab88f..e1846d05 100644 --- a/openhexa/graphql/schema.generated.graphql +++ b/openhexa/graphql/schema.generated.graphql @@ -3180,6 +3180,7 @@ enum ParameterType { int postgresql s3 + secret str } diff --git a/openhexa/sdk/__init__.py b/openhexa/sdk/__init__.py index ba19b925..ed68efdc 100644 --- a/openhexa/sdk/__init__.py +++ b/openhexa/sdk/__init__.py @@ -3,7 +3,7 @@ from .datasets import Dataset from .files import File from .pipelines import current_pipeline, current_run, parameter, pipeline -from .pipelines.parameter import DHIS2Widget, IASOWidget +from .pipelines.parameter import DHIS2Widget, IASOWidget, Secret from .utils import OpenHexaClient from .workspaces import workspace from .workspaces.connection import ( @@ -32,4 +32,5 @@ "Dataset", "OpenHexaClient", "File", + "Secret", ] diff --git a/openhexa/sdk/pipelines/parameter.py b/openhexa/sdk/pipelines/parameter.py index d4faf7e5..4e6938ed 100644 --- a/openhexa/sdk/pipelines/parameter.py +++ b/openhexa/sdk/pipelines/parameter.py @@ -389,11 +389,77 @@ def validate(self, value: typing.Any | None) -> File: raise ParameterValueError(str(e)) +class Secret(str): + """Marker type for secret/password pipeline parameters. + + Use as the ``type`` argument of the ``@parameter`` decorator to indicate that the parameter value is sensitive + and should be hidden in the OpenHEXA web interface. The pipeline function will receive the value as a plain + ``str`` at runtime. + + Example:: + + @parameter("iaso_token", type=Secret, name="IASO token", required=True) + @pipeline("my-pipeline") + def my_pipeline(iaso_token: str): + ... + """ + + pass + + +class SecretType(ParameterType): + """Type class for secret/password string parameters. Values are treated as plain strings at runtime.""" + + @property + def spec_type(self) -> str: + """Return a type string for the specs that are sent to the backend.""" + return "secret" + + @property + def expected_type(self) -> type: + """Returns the python type expected for values.""" + return Secret + + @property + def accepts_choices(self) -> bool: + """Secrets don't support choices.""" + return False + + @property + def accepts_multiple(self) -> bool: + """Secrets don't support multiple values.""" + return False + + @staticmethod + def normalize(value: typing.Any) -> Secret | None: + """Strip whitespace, convert empty strings to None, and wrap as Secret.""" + if isinstance(value, str): + normalized_value = value.strip() + else: + normalized_value = value + + if normalized_value == "": + return None + + if isinstance(normalized_value, str): + return Secret(normalized_value) + + return normalized_value + + def validate_default(self, value: typing.Any | None): + """Validate the default value configured for this type.""" + if value == "": + raise ParameterValueError("Empty values are not accepted.") + + super().validate_default(value) + + TYPES_BY_PYTHON_TYPE = { "str": StringType, "bool": Boolean, "int": Integer, "float": Float, + "Secret": SecretType, "DHIS2Connection": DHIS2ConnectionType, "PostgreSQLConnection": PostgreSQLConnectionType, "IASOConnection": IASOConnectionType, @@ -438,6 +504,7 @@ def __init__( | int | bool | float + | Secret | DHIS2Connection | IASOConnection | PostgreSQLConnection @@ -445,6 +512,7 @@ def __init__( | S3Connection | CustomConnection | Dataset + | File ], name: str | None = None, choices: typing.Sequence | None = None, @@ -621,6 +689,7 @@ def parameter( | int | bool | float + | Secret | DHIS2Connection | IASOConnection | PostgreSQLConnection @@ -628,6 +697,7 @@ def parameter( | S3Connection | CustomConnection | Dataset + | File ], name: str | None = None, choices: typing.Sequence | None = None, @@ -647,7 +717,7 @@ def parameter( ---------- code : str The parameter identifier (must be unique for a given pipeline) - type : {str, int, bool, float, DHIS2Connection, IASOConnection, PostgreSQLConnection, GCSConnection, S3Connection} + type : {str, int, bool, float, DHIS2Connection, IASOConnection, PostgreSQLConnection, GCSConnection, S3Connection, CustomConnection, Dataset, File} The parameter Python type name : str, optional A name for the parameter (will be used instead of the code in the web interface) @@ -668,7 +738,7 @@ def parameter( Whether this parameter should be provided multiple values (if True, the value must be provided as a list of values of the chosen type) directory : str, optional - An optional parameter to force file selection to specific directory (only used for parater type File). If the directory does not exist, it will be ignored. + An optional parameter to force file selection to specific directory (only used for parameter type File). If the directory does not exist, it will be ignored. Returns ------- diff --git a/pyproject.toml b/pyproject.toml index 2d6e76b4..4f510e5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "openhexa.sdk" -version = "2.19.3" +version = "2.19.4" description = "OpenHEXA SDK" authors = [{ name = "Bluesquare", email = "dev@bluesquarehub.com" }] @@ -20,7 +20,7 @@ requires-python = ">=3.11,<3.15" # the main constraint for supported Python vers dependencies = [ "urllib3<3", "multiprocess~=0.70.15", - "requests>=2.31,<2.33", + "requests>=2.31,<2.34", "PyYAML~=6.0", "click~=8.1.3", "jinja2>3,<4", diff --git a/tests/test_parameter.py b/tests/test_parameter.py index 2dfe93e0..ea405112 100644 --- a/tests/test_parameter.py +++ b/tests/test_parameter.py @@ -32,6 +32,8 @@ ParameterValueError, PostgreSQLConnectionType, S3ConnectionType, + Secret, + SecretType, StringType, parameter, ) @@ -92,6 +94,54 @@ def test_parameter_types_validate(): boolean_parameter_type.validate(86) +def test_secret_type_normalize(): + """Check normalization for SecretType.""" + secret_type = SecretType() + assert secret_type.normalize("my-token") == "my-token" + assert secret_type.normalize(" my-token ") == "my-token" + assert secret_type.normalize("") is None + assert secret_type.normalize(" ") is None + + +def test_secret_type_validate(): + """Check validation for SecretType.""" + secret_type = SecretType() + assert secret_type.validate(Secret("my-token")) == "my-token" + with pytest.raises(ParameterValueError): + secret_type.validate(123) + + +def test_secret_type_does_not_accept_choices(): + """Secret parameters don't support choices.""" + with pytest.raises(InvalidParameterError): + Parameter("token", type=Secret, choices=["a", "b"]) + + +def test_secret_type_does_not_accept_multiple(): + """Secret parameters don't support multiple values.""" + with pytest.raises(InvalidParameterError): + Parameter("token", type=Secret, multiple=True) + + +def test_secret_parameter_spec_type(): + """Secret parameters serialize with spec_type 'secret'.""" + p = Parameter("token", type=Secret) + assert p.to_dict()["type"] == "secret" + + +def test_secret_parameter_validates_string(): + """Secret parameters validate and return plain strings.""" + p = Parameter("token", type=Secret) + assert p.validate("my-secret-token") == "my-secret-token" + + +def test_secret_parameter_required(): + """Secret parameters respect required constraint.""" + p = Parameter("token", type=Secret, required=True) + with pytest.raises(ParameterValueError): + p.validate(None) + + def test_validate_postgres_connection(): """Check PostgreSQL connection validation.""" identifier = "polio-ff3a0d"