Skip to content

Commit 5190046

Browse files
committed
Merge branch 'main' into HEXA-1687-conditional-params
2 parents c0861b5 + 9638745 commit 5190046

9 files changed

Lines changed: 106 additions & 36 deletions

File tree

.github/workflows/schema-compatibility-cron.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323

2424
- name: Notify Slack on failure
2525
if: failure()
26-
uses: rtCamp/action-slack-notify@v2.3.3
26+
uses: rtCamp/action-slack-notify@v2.4.0
2727
env:
2828
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
2929
SLACK_CHANNEL: "#openhexa-alerts"

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "2.21.0"
2+
".": "2.21.1"
33
}

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## [2.21.1](https://github.com/BLSQ/openhexa-sdk-python/compare/v2.21.0...v2.21.1) (2026-06-12)
4+
5+
6+
### Bug Fixes
7+
8+
* ChoicesFromFiles snake_case to camelCase ([#393](https://github.com/BLSQ/openhexa-sdk-python/issues/393)) ([0842110](https://github.com/BLSQ/openhexa-sdk-python/commit/0842110fcce6e79d4c0a56f69cb2249265382ec7))
9+
* pipeline push should rollback on failure (HEXA-1661) ([#391](https://github.com/BLSQ/openhexa-sdk-python/issues/391)) ([ad4e725](https://github.com/BLSQ/openhexa-sdk-python/commit/ad4e72586c6380349ae6251bcc09a07aa809fd6d))
10+
11+
12+
### Miscellaneous
13+
14+
* **deps:** update rtcamp/action-slack-notify action to v2.4.0 ([#395](https://github.com/BLSQ/openhexa-sdk-python/issues/395)) ([222ebd1](https://github.com/BLSQ/openhexa-sdk-python/commit/222ebd1e141d2f3d2ffc145bd34c1104796bf0f9))
15+
* Update GraphQL schema ([#397](https://github.com/BLSQ/openhexa-sdk-python/issues/397)) ([2db4a52](https://github.com/BLSQ/openhexa-sdk-python/commit/2db4a52ff8cb584e032608bbb2bf35c77c853d63))
16+
317
## [2.21.0](https://github.com/BLSQ/openhexa-sdk-python/compare/v2.20.2...v2.21.0) (2026-05-20)
418

519

Makefile

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1-
l lint:
1+
.DEFAULT_GOAL := help
2+
3+
.PHONY: help l lint install-editable
4+
5+
help: ## Show this help message
6+
@echo "Available commands:"
7+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
8+
9+
lint: ## Run linting (pre-commit) on all files
210
@echo "Executing lint in backend code (pre-commit)"
311
pre-commit run --show-diff-on-failure --color=always --all-files
12+
13+
l: lint
14+
15+
install-editable: ## Install the SDK in editable mode with dev dependencies
16+
@echo "Installing the SDK in editable mode"
17+
pip install -e ".[dev]"

openhexa/graphql/schema.generated.graphql

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,8 +1042,8 @@ input CreatePipelineInput {
10421042
name: String!
10431043
notebookPath: String
10441044
tags: [String!]
1045-
version: CreatePipelineVersionInput
10461045
workspaceSlug: String!
1046+
zipfile: String
10471047
}
10481048

10491049
"""Represents the input for adding a recipient to a pipeline."""
@@ -1079,9 +1079,12 @@ input CreatePipelineTemplateVersionInput {
10791079
code: String
10801080
config: String
10811081
description: String
1082+
documentation: String
1083+
extractDocumentationFromReadme: Boolean
10821084
name: String
10831085
pipelineId: UUID!
10841086
pipelineVersionId: UUID!
1087+
versionName: String
10851088
workspaceSlug: String!
10861089
}
10871090

@@ -1092,20 +1095,6 @@ type CreatePipelineTemplateVersionResult {
10921095
success: Boolean!
10931096
}
10941097

1095-
"""
1096-
Configures the first pipeline version, created atomically alongside the pipeline.
1097-
Providing this sub-input signals that a first version should be created.
1098-
"""
1099-
input CreatePipelineVersionInput {
1100-
config: JSON
1101-
description: String
1102-
externalLink: URL
1103-
name: String
1104-
parameters: [ParameterInput!]
1105-
timeout: Int
1106-
zipfile: String!
1107-
}
1108-
11091098
"""
11101099
The CreateTeamError enum represents the possible errors that can occur during the createTeam mutation.
11111100
"""
@@ -3265,6 +3254,7 @@ type OrganizationWorkspaceInvitation {
32653254
"""Represents an input parameter of a pipeline."""
32663255
input ParameterInput {
32673256
choices: [Generic!]
3257+
choicesFromFile: PipelineParameterChoicesFromFileInput
32683258
code: String!
32693259
connection: String
32703260
default: Generic
@@ -3427,6 +3417,7 @@ enum PipelineOrderBy {
34273417
"""Represents a parameter of a pipeline."""
34283418
type PipelineParameter {
34293419
choices: [Generic!]
3420+
choicesFromFile: PipelineParameterChoicesFromFile
34303421
code: String!
34313422
connection: String
34323423
default: Generic
@@ -3439,6 +3430,28 @@ type PipelineParameter {
34393430
widget: ParameterWidget
34403431
}
34413432

3433+
"""File format for a dynamic choices source."""
3434+
enum PipelineParameterChoicesFileFormat {
3435+
csv
3436+
json
3437+
yaml
3438+
yml
3439+
}
3440+
3441+
"""Describes a dynamic choices source backed by a workspace file."""
3442+
type PipelineParameterChoicesFromFile {
3443+
column: String
3444+
format: PipelineParameterChoicesFileFormat
3445+
path: String!
3446+
}
3447+
3448+
"""Input for a dynamic choices source backed by a workspace file."""
3449+
input PipelineParameterChoicesFromFileInput {
3450+
column: String
3451+
format: PipelineParameterChoicesFileFormat
3452+
path: String!
3453+
}
3454+
34423455
"""Represents the permissions for a pipeline."""
34433456
type PipelinePermissions {
34443457
createTemplateVersion: CreateTemplateVersionPermission!
@@ -3552,6 +3565,7 @@ type PipelineTemplate {
35523565
config: String
35533566
currentVersion: PipelineTemplateVersion
35543567
description: String
3568+
documentation: String
35553569
functionalType: PipelineFunctionalType
35563570
id: UUID!
35573571
name: String!
@@ -3611,8 +3625,10 @@ type PipelineTemplateResultPage {
36113625
type PipelineTemplateVersion {
36123626
changelog: String
36133627
createdAt: DateTime!
3628+
documentation: String
36143629
id: UUID!
36153630
isLatestVersion: Boolean!
3631+
name: String
36163632
permissions: PipelineTemplateVersionPermissions!
36173633
sourcePipelineVersion: PipelineVersion!
36183634
template: PipelineTemplate!
@@ -3887,6 +3903,12 @@ type Query {
38873903
"""Retrieves a pipeline by workspace slug and code."""
38883904
pipelineByCode(code: String!, workspaceSlug: String!): Pipeline
38893905

3906+
"""
3907+
Resolves the list of choices for a parameter backed by a workspace file.
3908+
Returns a list of string values read from the file at the time of the call.
3909+
"""
3910+
pipelineParameterChoices(parameterCode: String!, pipelineVersionId: UUID!): [String!]
3911+
38903912
"""Retrieves a pipeline run by ID."""
38913913
pipelineRun(id: UUID!): PipelineRun
38923914

@@ -4997,6 +5019,7 @@ type UpdatePipelineResult {
49975019
Enum representing the possible errors that can occur when updating a pipeline version.
49985020
"""
49995021
enum UpdatePipelineVersionError {
5022+
INVALID_CONFIG
50005023
NOT_FOUND
50015024
PERMISSION_DENIED
50025025
}
@@ -5093,7 +5116,9 @@ enum UpdateTemplateVersionError {
50935116
"""Represents the input for updating a template version."""
50945117
input UpdateTemplateVersionInput {
50955118
changelog: String
5119+
documentation: String
50965120
id: UUID!
5121+
name: String
50975122
}
50985123

50995124
"""Represents the result of updating a template version."""
@@ -5672,4 +5697,4 @@ type WriteFileContentResult {
56725697
filePath: String
56735698
size: Int
56745699
success: Boolean!
5675-
}
5700+
}

openhexa/sdk/pipelines/parameter/decorator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def to_dict(self) -> dict[str, typing.Any]:
133133
"disableWhen": self.disable_when,
134134
}
135135
if isinstance(self.choices, ChoicesFromFile):
136-
d["choices_from_file"] = self.choices.to_dict()
136+
d["choicesFromFile"] = self.choices.to_dict()
137137
return d
138138

139139
def _validate_single(self, value: typing.Any):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "openhexa.sdk"
7-
version = "2.21.0"
7+
version = "2.21.1"
88
description = "OpenHEXA SDK"
99

1010
authors = [{ name = "Bluesquare", email = "dev@bluesquarehub.com" }]

scripts/check_schema_compatibility.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
from pathlib import Path
1212

1313
import requests
14-
from graphql import build_client_schema, build_schema, get_introspection_query
14+
from graphql import GraphQLSchema, build_client_schema, build_schema, get_introspection_query
15+
from graphql.type import specified_directives
1516
from graphql.utilities import find_breaking_changes
1617

1718
from openhexa.graphql import BUNDLED_SCHEMA_PATH
@@ -26,6 +27,19 @@
2627

2728
PRODUCTION_URL = "https://api.openhexa.org"
2829

30+
SPEC_DIRECTIVE_NAMES = {directive.name for directive in specified_directives}
31+
32+
33+
def strip_spec_directives(schema: GraphQLSchema) -> GraphQLSchema:
34+
"""Remove built-in spec directives (@deprecated, @skip, ...) so the diff only covers directives the API owns.
35+
36+
Built-in directives vary with the graphql-core version on each side of the comparison
37+
and produce false-positive breaking changes.
38+
"""
39+
kwargs = schema.to_kwargs()
40+
kwargs["directives"] = tuple(d for d in kwargs["directives"] if d.name not in SPEC_DIRECTIVE_NAMES)
41+
return GraphQLSchema(**kwargs)
42+
2943

3044
def fetch_server_schema(graphql_url: str):
3145
"""Fetch the live GraphQL schema from the server via introspection."""
@@ -46,7 +60,10 @@ def main():
4660
stored_schema = build_schema(BUNDLED_SCHEMA_PATH.read_text(), assume_valid_sdl=True)
4761
server_schema = fetch_server_schema(f"{PRODUCTION_URL}/graphql/")
4862

49-
breaking_changes = find_breaking_changes(stored_schema, server_schema)
63+
breaking_changes = find_breaking_changes(
64+
strip_spec_directives(stored_schema),
65+
strip_spec_directives(server_schema),
66+
)
5067
if breaking_changes:
5168
print(f"⚠️ {len(breaking_changes)} breaking change(s) detected:")
5269
for change in breaking_changes:

tests/test_choices.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def test_ast_string_shorthand_csv(self):
168168
p = get_pipeline(tmpdir)
169169
param_dict = p.to_dict()["parameters"][0]
170170
assert param_dict["choices"] is None
171-
assert param_dict["choices_from_file"] == {"format": None, "path": "districts.csv", "column": None}
171+
assert param_dict["choicesFromFile"] == {"format": None, "path": "districts.csv", "column": None}
172172

173173
def test_ast_string_shorthand_json(self):
174174
with tempfile.TemporaryDirectory() as tmpdir:
@@ -177,7 +177,7 @@ def test_ast_string_shorthand_json(self):
177177
"@parameter('district', type=str, choices='regions.json')",
178178
)
179179
p = get_pipeline(tmpdir)
180-
assert p.to_dict()["parameters"][0]["choices_from_file"]["format"] is None
180+
assert p.to_dict()["parameters"][0]["choicesFromFile"]["format"] is None
181181

182182
def test_ast_string_shorthand_any_extension(self):
183183
with tempfile.TemporaryDirectory() as tmpdir:
@@ -186,7 +186,7 @@ def test_ast_string_shorthand_any_extension(self):
186186
"@parameter('district', type=str, choices='list.yml')",
187187
)
188188
p = get_pipeline(tmpdir)
189-
assert p.to_dict()["parameters"][0]["choices_from_file"]["format"] is None
189+
assert p.to_dict()["parameters"][0]["choicesFromFile"]["format"] is None
190190

191191
def test_ast_string_shorthand_same_output_as_explicit(self):
192192
with tempfile.TemporaryDirectory() as tmpdir:
@@ -229,7 +229,7 @@ def test_ast_static_list_unaffected(self):
229229
p = get_pipeline(tmpdir)
230230
param_dict = p.to_dict()["parameters"][0]
231231
assert param_dict["choices"] == ["UG", "KE"]
232-
assert "choices_from_file" not in param_dict
232+
assert "choicesFromFile" not in param_dict
233233

234234
def test_ast_string_no_extension_accepted(self):
235235
with tempfile.TemporaryDirectory() as tmpdir:
@@ -238,7 +238,7 @@ def test_ast_string_no_extension_accepted(self):
238238
"@parameter('district', type=str, choices='nodot')",
239239
)
240240
p = get_pipeline(tmpdir)
241-
assert p.to_dict()["parameters"][0]["choices_from_file"]["format"] is None
241+
assert p.to_dict()["parameters"][0]["choicesFromFile"]["format"] is None
242242

243243
def test_ast_string_any_extension_accepted(self):
244244
with tempfile.TemporaryDirectory() as tmpdir:
@@ -247,7 +247,7 @@ def test_ast_string_any_extension_accepted(self):
247247
"@parameter('district', type=str, choices='file.xlsx')",
248248
)
249249
p = get_pipeline(tmpdir)
250-
assert p.to_dict()["parameters"][0]["choices_from_file"]["format"] is None
250+
assert p.to_dict()["parameters"][0]["choicesFromFile"]["format"] is None
251251

252252

253253
# ---------------------------------------------------------------------------
@@ -264,13 +264,13 @@ def test_to_dict_emits_file_choices_key(self):
264264
p = Parameter(code="district", type=str, choices=ChoicesFromFile("districts.csv", column="code", format="csv"))
265265
d = p.to_dict()
266266
assert d["choices"] is None
267-
assert d["choices_from_file"] == {"format": "csv", "path": "districts.csv", "column": "code"}
267+
assert d["choicesFromFile"] == {"format": "csv", "path": "districts.csv", "column": "code"}
268268

269269
def test_to_dict_no_file_choices_key_for_static_choices(self):
270270
p = Parameter(code="country", type=str, choices=["UG", "KE"])
271271
d = p.to_dict()
272272
assert d["choices"] == ["UG", "KE"]
273-
assert "choices_from_file" not in d
273+
assert "choicesFromFile" not in d
274274

275275
def test_rejects_file_choices_on_bool_type(self):
276276
with pytest.raises(InvalidParameterError, match="don't accept choices"):
@@ -331,7 +331,7 @@ def test_file_choices_positional_path(self):
331331
p = get_pipeline(tmpdir)
332332
param_dict = p.to_dict()["parameters"][0]
333333
assert param_dict["choices"] is None
334-
assert param_dict["choices_from_file"] == {"format": None, "path": "districts.csv", "column": None}
334+
assert param_dict["choicesFromFile"] == {"format": None, "path": "districts.csv", "column": None}
335335

336336
def test_file_choices_with_column(self):
337337
with tempfile.TemporaryDirectory() as tmpdir:
@@ -341,7 +341,7 @@ def test_file_choices_with_column(self):
341341
)
342342
p = get_pipeline(tmpdir)
343343
param_dict = p.to_dict()["parameters"][0]
344-
assert param_dict["choices_from_file"] == {"format": None, "path": "data/districts.csv", "column": "code"}
344+
assert param_dict["choicesFromFile"] == {"format": None, "path": "data/districts.csv", "column": "code"}
345345

346346
def test_file_choices_with_column_positional(self):
347347
with tempfile.TemporaryDirectory() as tmpdir:
@@ -351,7 +351,7 @@ def test_file_choices_with_column_positional(self):
351351
)
352352
p = get_pipeline(tmpdir)
353353
param_dict = p.to_dict()["parameters"][0]
354-
assert param_dict["choices_from_file"] == {"format": None, "path": "data/districts.csv", "column": "code"}
354+
assert param_dict["choicesFromFile"] == {"format": None, "path": "data/districts.csv", "column": "code"}
355355

356356
def test_file_choices_explicit_format(self):
357357
with tempfile.TemporaryDirectory() as tmpdir:
@@ -361,7 +361,7 @@ def test_file_choices_explicit_format(self):
361361
)
362362
p = get_pipeline(tmpdir)
363363
param_dict = p.to_dict()["parameters"][0]
364-
assert param_dict["choices_from_file"]["format"] == "json"
364+
assert param_dict["choicesFromFile"]["format"] == "json"
365365

366366
def test_file_choices_format_none_by_default(self):
367367
with tempfile.TemporaryDirectory() as tmpdir:
@@ -371,7 +371,7 @@ def test_file_choices_format_none_by_default(self):
371371
)
372372
p = get_pipeline(tmpdir)
373373
param_dict = p.to_dict()["parameters"][0]
374-
assert param_dict["choices_from_file"]["format"] is None
374+
assert param_dict["choicesFromFile"]["format"] is None
375375

376376
def test_unsupported_call_in_choices_raises(self):
377377
with tempfile.TemporaryDirectory() as tmpdir:

0 commit comments

Comments
 (0)