Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add new fields `workflow_input_schema` and `workflow_output_schema` to the spec


## [5.0.0] - 2026-07-01

### Added
Expand Down
3 changes: 3 additions & 0 deletions doc/howtoguides/change_schema.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ The ``SchemaMetadata`` creation needs two arguments:
- ``ewokscore`` version bounds (stored as a 2-tuple of `Version`_): the first tuple value is the lowest ``ewokscore`` version that supports this schema, the second is the highest version that supports this schema (put ``None`` if there is no upper bound).
- a function that converts this schema version to the next version. The function can be defined in the ``update`` submodule. If the version is the latest one, put ``None`` since there is no next version.

Update the schema version changelog
Comment thread
woutdenolf marked this conversation as resolved.
-----------------------------------

Once all the steps above are done, update the :doc:`../reference/specs` page with the new version changes.

.. _Version: https://packaging.pypa.io/en/stable/version.html#packaging.version.Version
7 changes: 7 additions & 0 deletions doc/reference/specs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ Graph attributes
* *id* (optional): graph identifier unique to a database of graphs (Default: "notspecified")
* *label* (optional): non-unique label to be used when identifying a graph for human consumption
* *schema_version* (optional): the schema version of this graph representation (Default: "1.2")
* *workflow_input_schema* (optional): A `JSON schema <https://json-schema.org/draft/2020-12>`_ defining the inputs of the workflow, representing a subset of the node inputs.
* *workflow_output_schema* (optional): A `JSON schema <https://json-schema.org/draft/2020-12>`_ defining the outputs of the workflow, representing a subset of the node outputs.
* *requirements* (optional): a list of projects that should be present in the Python environment for the
graph to be executed.
* *input_nodes* (optional): nodes that are expected to be used as link targets when the graph
Expand Down Expand Up @@ -360,6 +362,11 @@ When a task is defined as a *method*, *script* or *notebook*, a class wrapper wi
``schema_version`` changelog
----------------------------

1.3
^^^

- Added new graph attributes ``workflow_input_schema`` and ``workflow_output_schema``

1.2
^^^

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies = [
"pydantic >= 2",
"typing_extensions;python_version < '3.11'",
"backports.strenum;python_version < '3.11'",
"jsonschema",
]

[project.urls]
Expand Down
4 changes: 3 additions & 1 deletion src/ewokscore/graph/schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .metadata import SchemaMetadata
from .update import from_v1_0_to_v1_1
from .update import from_v1_1_to_v1_2
from .update import from_v1_2_to_v1_3
from .update import v0_update

_VERSIONS = None
Expand All @@ -23,7 +24,8 @@ def get_versions() -> Dict[Version, SchemaMetadata]:
parse_version("0.0"): SchemaMetadata(("0.0", "0.0.1"), v0_update),
parse_version("1.0"): SchemaMetadata(("0.1.0-rc", None), from_v1_0_to_v1_1),
parse_version("1.1"): SchemaMetadata(("0.1.0-rc", None), from_v1_1_to_v1_2),
parse_version("1.2"): SchemaMetadata(("0.1.0-rc", None), None),
parse_version("1.2"): SchemaMetadata(("0.1.0-rc", None), from_v1_2_to_v1_3),
parse_version("1.3"): SchemaMetadata(("0.1.0-rc", None), None),
}
return _VERSIONS

Expand Down
3 changes: 3 additions & 0 deletions src/ewokscore/graph/schema/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from pydantic import model_validator

from . import LATEST_VERSION
from .parameter_schema import EwoksParameterSchema

NodeId = Hashable # Could be recursive: Union[Tuple[str, "Id"], str]

Expand Down Expand Up @@ -123,6 +124,8 @@ class EwoksGraphAttributes(BaseModel):
requirements: Sequence[str] = []
input_nodes: Sequence[EwoksNodeAlias] = []
output_nodes: Sequence[EwoksNodeAlias] = []
workflow_input_schema: Optional[EwoksParameterSchema] = None
workflow_output_schema: Optional[EwoksParameterSchema] = None


class EwoksGraph(BaseModel):
Expand Down
60 changes: 60 additions & 0 deletions src/ewokscore/graph/schema/parameter_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import sys
from typing import Dict
from typing import List
from typing import Literal
from typing import Optional
from typing import Union

from jsonschema import Draft202012Validator
from pydantic import BaseModel
from pydantic import Field
from pydantic import model_validator

if sys.version_info < (3, 11):
from typing_extensions import Self
else:
from typing import Self


class _Target(BaseModel):
name: str
id: Union[str, int, None] = None
label: Optional[str] = None
task_identifier: Optional[str] = None
all: bool = False


class _Property(BaseModel, extra="allow"):
Comment thread
woutdenolf marked this conversation as resolved.
x_ewoks_targets: List[_Target] = Field(default_factory=list)


class EwoksParameterSchema(BaseModel, extra="forbid"):
schema_url: Literal["https://json-schema.org/draft/2020-12/schema"] = Field(
default="https://json-schema.org/draft/2020-12/schema",
alias="$schema",
)

type: Literal["object"] = "object"

properties: Dict[str, _Property]
required: List[str] = Field(default_factory=list)
Comment thread
woutdenolf marked this conversation as resolved.

additionalProperties: Literal[False] = False

@model_validator(mode="after")
def validate_json_schema(self) -> Self:
schema_dict = self.model_dump(by_alias=True)

# validates that the structure is a valid JSON Schema
Draft202012Validator.check_schema(schema_dict)

return self

@model_validator(mode="after")
def validate_required(self) -> Self:
missing = set(self.required) - self.properties.keys()
if missing:
raise ValueError(
f"'required' contains unknown properties: {sorted(missing)}"
)
return self
5 changes: 5 additions & 0 deletions src/ewokscore/graph/schema/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ def from_v1_1_to_v1_2(graph: networkx.DiGraph) -> None:
- `required` when set explicitly to `False`, ignoring graph analysis
"""
graph.graph["schema_version"] = "1.2"


def from_v1_2_to_v1_3(graph: networkx.DiGraph) -> None:
"""v1.3 adds `workflow_input_schema` and `workflow_output_schema`"""
graph.graph["schema_version"] = "1.3"
1 change: 0 additions & 1 deletion src/ewokscore/tests/examples/graphs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ def wrapper():
attrs = g.setdefault("graph", dict())
assert attrs.get("id") == name
assert attrs.get("label") == name
assert attrs.get("schema_version") == "1.2"
Comment thread
woutdenolf marked this conversation as resolved.
return g, result

if _ALL_GRAPHS is None:
Expand Down
75 changes: 75 additions & 0 deletions src/ewokscore/tests/examples/graphs/with_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from . import graph


@graph
def with_schema():
graph = {
"id": "with_schema",
"label": "with_schema",
"schema_version": "1.3",
"workflow_input_schema": {
"properties": {
"a": {
"type": "number",
"x_ewoks_targets": [{"name": "a", "id": "task1a"}],
},
"b": {
"type": "number",
"default": 0,
"x_ewoks_targets": [{"name": "b", "id": "task1b"}],
},
},
"required": ["a"],
},
"workflow_output_schema": {
"properties": {
"result_sum": {
"type": "number",
"x_ewoks_targets": [{"name": "result", "id": "task"}],
},
},
},
}

nodes = [
{
"id": "task1a",
"task_type": "class",
"task_identifier": "ewokscore.tests.examples.tasks.sumlist.SumList",
"default_inputs": [{"name": "list", "value": [0, 1, 2]}],
},
{
"id": "task1b",
"task_type": "class",
"task_identifier": "ewokscore.tests.examples.tasks.sumtask.SumTask",
"default_inputs": [{"name": "a", "value": 10}],
},
{
"id": "task2",
"task_type": "class",
"task_identifier": "ewokscore.tests.examples.tasks.sumtask.SumTask",
},
]

links = [
{
"source": "task1a",
"target": "task2",
"data_mapping": [{"source_output": "sum", "target_input": "a"}],
},
{
"source": "task1b",
"target": "task2",
"data_mapping": [{"source_output": "result", "target_input": "b"}],
},
]

taskgraph = {"graph": graph, "links": links, "nodes": nodes}

expected_results = {
"task1a": {"sum": 3},
"task1b": {"result": 10},
"task2": {"result": 13},
}

return taskgraph, expected_results
65 changes: 65 additions & 0 deletions src/ewokscore/tests/test_model.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pydantic
import pytest
from jsonschema.exceptions import SchemaError

from ewokscore import load_graph
from ewokscore.graph.schema.model import EwoksGraph
Expand Down Expand Up @@ -127,3 +128,67 @@ def test_link_with_datamapping_and_map_all_data():
match="1 validation error for EwoksGraph\nlinks.0\n",
):
EwoksGraph(**graph_dict)


def test_wrong_type_in_schema():
graph_dict = {
"graph": {
"id": "required",
"workflow_input_schema": {"properties": {"a": {"type": "oo"}}},
},
"nodes": [
{
"id": "node1",
"task_type": "class",
"task_identifier": "task1",
},
],
"links": [],
}
with pytest.raises(SchemaError):
EwoksGraph(**graph_dict)


def test_missing_name_in_ewoks_target():
graph_dict = {
"graph": {
"id": "required",
"workflow_input_schema": {
"properties": {"a": {"type": "number", "x_ewoks_targets": [{}]}}
},
},
"nodes": [
{
"id": "node1",
"task_type": "class",
"task_identifier": "task1",
},
],
"links": [],
}

with pytest.raises(
pydantic.ValidationError,
match="1 validation error for EwoksGraph\ngraph.workflow_input_schema.properties.a.x_ewoks_targets.0.name\n",
):
EwoksGraph(**graph_dict)


def test_wrong_required_inputs():
graph_dict = {
"graph": {
"id": "required",
"workflow_input_schema": {
"properties": {"a": {"type": "number"}},
"required": ["not_a"],
},
},
"nodes": [],
"links": [],
}

with pytest.raises(
pydantic.ValidationError,
match="1 validation error for EwoksGraph\ngraph.workflow_input_schema\n",
):
EwoksGraph(**graph_dict)
Loading