API reference

CWL assertion hint models and the shared input validation interface.

Hints configure JSON Schema, Rego, or CQL2 validators and serialize to annotation payloads. Use extract_assertion_hints to read them from a parsed CWL process.

AssertionHint

Bases: BaseModel

Base model for an assertion hint attached to a CWL process.

Attributes:
  • parent_workflow (Any | None) –

    Parsed CWL process containing the hint, when available.

Source code in src/assertions_mate/__init__.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class AssertionHint(BaseModel):
    """Base model for an assertion hint attached to a CWL process.

    Attributes:
        parent_workflow: Parsed CWL process containing the hint, when available.
    """

    parent_workflow: Any | None = None

    @property
    @computed_field
    @abstractmethod
    def annotation(self) -> str:
        """Return the annotation key used to identify this hint."""
        pass

    @abstractmethod
    def validator(self) -> BaseValidator:
        """Create a validator configured from this hint."""
        pass

annotation abstractmethod property

Return the annotation key used to identify this hint.

parent_workflow = None class-attribute instance-attribute

validator() abstractmethod

Create a validator configured from this hint.

Source code in src/assertions_mate/__init__.py
66
67
68
69
@abstractmethod
def validator(self) -> BaseValidator:
    """Create a validator configured from this hint."""
    pass

BaseValidator

Bases: ABC

Interface for validators that check a mapping of workflow inputs.

Source code in src/assertions_mate/__init__.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class BaseValidator(ABC):
    """Interface for validators that check a mapping of workflow inputs."""

    @abstractmethod
    def validate_inputs(self, data: Mapping[str, Any]) -> ProblemDetails | None:
        """Check workflow inputs against the configured assertions.

        Args:
            data: Workflow input names mapped to their values.

        Returns:
            Problem details describing violations, or None when validation succeeds.

        Implementations may raise an exception when validation cannot be performed.
        """
        pass

validate_inputs(data) abstractmethod

Check workflow inputs against the configured assertions.

Parameters:
  • data (Mapping[str, Any]) –

    Workflow input names mapped to their values.

Returns:
  • ProblemDetails | None –

    Problem details describing violations, or None when validation succeeds.

Implementations may raise an exception when validation cannot be performed.

Source code in src/assertions_mate/__init__.py
35
36
37
38
39
40
41
42
43
44
45
46
47
@abstractmethod
def validate_inputs(self, data: Mapping[str, Any]) -> ProblemDetails | None:
    """Check workflow inputs against the configured assertions.

    Args:
        data: Workflow input names mapped to their values.

    Returns:
        Problem details describing violations, or None when validation succeeds.

    Implementations may raise an exception when validation cannot be performed.
    """
    pass

Cql2FilterHint

Bases: AssertionHint

Configure CQL2 predicates for workflow inputs.

Attributes:
  • custom_functions (str | None) –

    Optional trusted Python source executed to define functions available to the CQL2 evaluator.

  • queries (list[Cql2Query]) –

    Predicates that must evaluate to a truthy value.

Source code in src/assertions_mate/__init__.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
class Cql2FilterHint(AssertionHint):
    """Configure CQL2 predicates for workflow inputs.

    Attributes:
        custom_functions: Optional trusted Python source executed to define
            functions available to the CQL2 evaluator.
        queries: Predicates that must evaluate to a truthy value.
    """

    @staticmethod
    def get_annotation_name() -> str:
        """Return the annotation key for CQL2 input filters."""
        return "eoap.ogc.org/inputs-cql2-filter"

    custom_functions: str | None = None
    queries: list[Cql2Query]

    @property
    def annotation(self) -> str:
        """Return the annotation key for CQL2 input filters."""
        return Cql2FilterHint.get_annotation_name()

    @model_serializer
    def ser_model(self) -> Mapping[str, Any]:
        """Serialize the queries as an annotation payload, omitting custom functions."""
        return {"queries": self.queries}

    def validator(self) -> BaseValidator:
        """Create a CQL2 validator with the configured queries and custom functions."""
        from .cql2_validator import Cql2Validator

        return Cql2Validator(custom_functions=self.custom_functions, queries=self.queries)

annotation property

Return the annotation key for CQL2 input filters.

custom_functions = None class-attribute instance-attribute

queries instance-attribute

get_annotation_name() staticmethod

Return the annotation key for CQL2 input filters.

Source code in src/assertions_mate/__init__.py
170
171
172
173
@staticmethod
def get_annotation_name() -> str:
    """Return the annotation key for CQL2 input filters."""
    return "eoap.ogc.org/inputs-cql2-filter"

ser_model()

Serialize the queries as an annotation payload, omitting custom functions.

Source code in src/assertions_mate/__init__.py
183
184
185
186
@model_serializer
def ser_model(self) -> Mapping[str, Any]:
    """Serialize the queries as an annotation payload, omitting custom functions."""
    return {"queries": self.queries}

validator()

Create a CQL2 validator with the configured queries and custom functions.

Source code in src/assertions_mate/__init__.py
188
189
190
191
192
def validator(self) -> BaseValidator:
    """Create a CQL2 validator with the configured queries and custom functions."""
    from .cql2_validator import Cql2Validator

    return Cql2Validator(custom_functions=self.custom_functions, queries=self.queries)

Cql2Query

Bases: BaseModel

Describe a CQL2 assertion and the violation reported when it is false.

Attributes:
  • id (str) –

    Rule identifier used as the error pointer.

  • cql2 (str | Mapping[str, Any]) –

    CQL2 Text expression or CQL2 JSON expression mapping.

  • message (str) –

    Violation message to report when the predicate is false.

Source code in src/assertions_mate/__init__.py
147
148
149
150
151
152
153
154
155
156
157
158
class Cql2Query(BaseModel):
    """Describe a CQL2 assertion and the violation reported when it is false.

    Attributes:
        id: Rule identifier used as the error pointer.
        cql2: CQL2 Text expression or CQL2 JSON expression mapping.
        message: Violation message to report when the predicate is false.
    """

    id: str
    cql2: str | Mapping[str, Any]
    message: str

cql2 instance-attribute

id instance-attribute

message instance-attribute

JSONSchemaHint

Bases: AssertionHint

Configure JSON Schema validation of workflow inputs.

Attributes:
  • json_schema (Mapping[str, Any]) –

    Explicit schema to apply, including an explicitly empty schema. When omitted, derive a schema from parent_workflow if available; otherwise use an empty schema.

Source code in src/assertions_mate/__init__.py
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
class JSONSchemaHint(AssertionHint):
    """Configure JSON Schema validation of workflow inputs.

    Attributes:
        json_schema: Explicit schema to apply, including an explicitly empty schema.
            When omitted, derive a schema from parent_workflow if available;
            otherwise use an empty schema.
    """

    json_schema: Mapping[str, Any] = {}

    @staticmethod
    def get_annotation_name() -> str:
        """Return the annotation key for JSON Schema input constraints."""
        return "eoap.ogc.org/inputs-json-schema"

    @property
    def annotation(self) -> str:
        """Return the annotation key for JSON Schema input constraints."""
        return JSONSchemaHint.get_annotation_name()

    def _get_schema(self) -> Mapping[str, Any]:
        """Return the explicit schema, or derive it from the parent CWL process."""
        if "json_schema" in self.model_fields_set or self.parent_workflow is None:
            return self.json_schema
        schema = BaseCWLtypes2OGCConverter(self.parent_workflow).get_inputs_json_schema()
        if not isinstance(schema, Mapping):
            raise TypeError("The CWL converter must return a JSON Schema mapping")
        return schema

    @model_serializer
    def ser_model(self) -> Mapping[str, Any]:
        """Serialize the hint as its effective JSON Schema document."""
        return self._get_schema()

    def validator(self) -> BaseValidator:
        """Create a JSON Schema validator using the effective schema."""
        from .jsonschema_validator import JSONSchemaValidator

        return JSONSchemaValidator(schema=self._get_schema())

annotation property

Return the annotation key for JSON Schema input constraints.

json_schema = {} class-attribute instance-attribute

get_annotation_name() staticmethod

Return the annotation key for JSON Schema input constraints.

Source code in src/assertions_mate/__init__.py
83
84
85
86
@staticmethod
def get_annotation_name() -> str:
    """Return the annotation key for JSON Schema input constraints."""
    return "eoap.ogc.org/inputs-json-schema"

ser_model()

Serialize the hint as its effective JSON Schema document.

Source code in src/assertions_mate/__init__.py
102
103
104
105
@model_serializer
def ser_model(self) -> Mapping[str, Any]:
    """Serialize the hint as its effective JSON Schema document."""
    return self._get_schema()

validator()

Create a JSON Schema validator using the effective schema.

Source code in src/assertions_mate/__init__.py
107
108
109
110
111
def validator(self) -> BaseValidator:
    """Create a JSON Schema validator using the effective schema."""
    from .jsonschema_validator import JSONSchemaValidator

    return JSONSchemaValidator(schema=self._get_schema())

RegoPolicyHint

Bases: AssertionHint

Configure Rego policy queries for workflow inputs.

Attributes:
  • module (str) –

    Rego source code defining the policy.

  • queries (list[str]) –

    Queries whose returned expressions describe violations.

Source code in src/assertions_mate/__init__.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
class RegoPolicyHint(AssertionHint):
    """Configure Rego policy queries for workflow inputs.

    Attributes:
        module: Rego source code defining the policy.
        queries: Queries whose returned expressions describe violations.
    """

    @staticmethod
    def get_annotation_name() -> str:
        """Return the annotation key for Rego input policies."""
        return "eoap.ogc.org/inputs-rego-policy"

    module: str
    queries: list[str]

    @property
    def annotation(self) -> str:
        """Return the annotation key for Rego input policies."""
        return RegoPolicyHint.get_annotation_name()

    @model_serializer
    def ser_model(self) -> Mapping[str, Any]:
        """Serialize the policy module and queries as an annotation payload."""
        return {"queries": self.queries, "module": self.module}

    def validator(self) -> BaseValidator:
        """Create a Rego validator using this policy module and its queries."""
        from .rego_validator import RegoValidator

        return RegoValidator(queries=self.queries, module=self.module)

annotation property

Return the annotation key for Rego input policies.

module instance-attribute

queries instance-attribute

get_annotation_name() staticmethod

Return the annotation key for Rego input policies.

Source code in src/assertions_mate/__init__.py
122
123
124
125
@staticmethod
def get_annotation_name() -> str:
    """Return the annotation key for Rego input policies."""
    return "eoap.ogc.org/inputs-rego-policy"

ser_model()

Serialize the policy module and queries as an annotation payload.

Source code in src/assertions_mate/__init__.py
135
136
137
138
@model_serializer
def ser_model(self) -> Mapping[str, Any]:
    """Serialize the policy module and queries as an annotation payload."""
    return {"queries": self.queries, "module": self.module}

validator()

Create a Rego validator using this policy module and its queries.

Source code in src/assertions_mate/__init__.py
140
141
142
143
144
def validator(self) -> BaseValidator:
    """Create a Rego validator using this policy module and its queries."""
    from .rego_validator import RegoValidator

    return RegoValidator(queries=self.queries, module=self.module)

extract_assertion_hints(workflow)

Collect recognized assertion hints from a parsed CWL process.

Parameters:
  • workflow (Any) –

    Parsed process exposing hints and CWL identification fields.

Returns:
  • list[AssertionHint] –

    Instantiated hints in declaration order. Non-dictionary hints, unrelated

  • list[AssertionHint] –

    classes, and hints that fail model lookup or construction are skipped.

Raises:
  • KeyError –

    If a dictionary hint has no class entry.

Source code in src/assertions_mate/__init__.py
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
def extract_assertion_hints(workflow: Any) -> list[AssertionHint]:
    """Collect recognized assertion hints from a parsed CWL process.

    Args:
        workflow: Parsed process exposing hints and CWL identification fields.

    Returns:
        Instantiated hints in declaration order. Non-dictionary hints, unrelated
        classes, and hints that fail model lookup or construction are skipped.

    Raises:
        KeyError: If a dictionary hint has no class entry.
    """
    assertion_hints = []

    if workflow.hints:
        for hint in workflow.hints:
            if isinstance(hint, dict):
                hint_instance = _get_assertion_hint_by_name(parent_workflow=workflow, hint=hint)

                if hint_instance:
                    assertion_hints.append(hint_instance)
    else:
        logger.debug(
            f"No hints defined in current #{workflow.id.split('#')[-1]} {workflow.class_} ({workflow.cwlVersion})"
        )

    return assertion_hints

JSON Schema API

Validate workflow inputs with JSON Schema Draft 2020-12 and remote references.

JSONSchemaRegistry

Retrieve JSON or YAML schema documents through a Requests session.

Source code in src/assertions_mate/jsonschema_validator.py
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
class JSONSchemaRegistry:
    """Retrieve JSON or YAML schema documents through a Requests session."""

    def __init__(
        self,
        timeout: int = _REMOTE_SCHEMA_TIMEOUT_SECONDS,
    ):
        """Create a session for schema retrieval.

        Args:
            timeout: Timeout in seconds passed to each schema HTTP request.
        """
        self.session = requests.Session()
        self.session.headers.update({"User-Agent": "assertions-mate"})
        self.timeout = timeout

    @staticmethod
    def _schema_format_from_content_type(content_type: str | None) -> str | None:
        """Infer json or yaml from a media type, or return None if unrecognized."""
        if not content_type:
            return None

        media_type = content_type.split(";", 1)[0].strip().lower()

        if media_type == "application/json" or media_type.endswith("+json"):
            return "json"

        if media_type in {
            "application/yaml",
            "application/x-yaml",
            "text/yaml",
            "text/x-yaml",
        } or media_type.endswith("+yaml"):
            return "yaml"

        return None

    @staticmethod
    def _schema_format_from_uri(uri: str) -> str | None:
        """Infer json or yaml from a URI path suffix, or return None."""
        path = urlparse(uri).path.lower()

        if path.endswith(".json"):
            return "json"

        if path.endswith((".yaml", ".yml")):
            return "yaml"

        return None

    def _parse_schema(
        self,
        payload: bytes,
        uri: str,
        content_type: str | None = None,
    ) -> Any:
        """Decode a UTF-8 schema document as JSON or YAML.

        Args:
            payload: Schema document bytes.
            uri: Document URI, used for format detection and error reporting.
            content_type: Optional HTTP media type, preferred over the URI suffix.

        Returns:
            The parsed document. If the preferred parser fails, try the other format.

        Raises:
            UnicodeDecodeError: If the payload is not valid UTF-8.
            ValueError: If both JSON and YAML parsing fail.
        """
        text = payload.decode("utf-8")
        preferred_format = (
            self._schema_format_from_content_type(content_type)
            or self._schema_format_from_uri(uri)
            or "json"
        )
        parsers: tuple[Callable[[str], object], ...] = (json.loads, YAML().load)
        if preferred_format == "yaml":
            parsers = parsers[::-1]
        preferred_parser, fallback_parser = parsers
        try:
            return preferred_parser(text)
        except (ValueError, YAMLError):
            pass
        try:
            return fallback_parser(text)
        except (ValueError, YAMLError) as error:
            raise ValueError(f"Unable to parse JSON Schema from {uri}") from error

    @staticmethod
    def _schema_request_uri(uri: str) -> str:
        """Preserve a URI scheme or convert an existing path to a file URI.

        Raises:
            NoSuchResource: If a schemeless path does not exist.

        Converting a path does not install a Requests adapter for file URIs.
        """
        parsed_uri = urlparse(uri)
        if parsed_uri.scheme:
            return uri

        path = Path(uri)
        if not path.exists():
            raise NoSuchResource(uri)

        return path.absolute().as_uri()

    def load_schema(self, uri: str) -> Any:
        """Fetch a schema document and parse it as JSON or YAML.

        Args:
            uri: Schema location supported by the configured Requests session.
                The default session supports HTTP and HTTPS, not local files.

        Returns:
            The parsed schema document.

        Raises:
            NoSuchResource: If a path is missing, the URI scheme has no adapter,
                or the server returns HTTP 404.
            requests.exceptions.RequestException: If the request otherwise fails.
            UnicodeDecodeError: If the response is not valid UTF-8.
            ValueError: If the response cannot be parsed as JSON or YAML.
        """
        try:
            response = self.session.get(
                self._schema_request_uri(uri),
                timeout=self.timeout,
            )
        except requests.exceptions.InvalidSchema as error:
            raise NoSuchResource(uri) from error

        if response.status_code == HTTPStatus.NOT_FOUND.value:
            raise NoSuchResource(uri)

        response.raise_for_status()

        return self._parse_schema(
            payload=response.content,
            uri=uri,
            content_type=response.headers.get("Content-Type"),
        )

    def retrieve(self, uri: str) -> Resource[Any]:
        """Load a URI as a Draft 2020-12 resource for reference resolution.

        Args:
            uri: Schema location to fetch through load_schema.

        Returns:
            A schema resource wrapping the parsed document.

        Retrieval and parsing errors from load_schema propagate to the caller.
        """
        return DRAFT202012.create_resource(self.load_schema(uri))

    def as_referencing_registry(self) -> ReferencingRegistry[Any]:
        # mypy does not understand the attrs field alias used by referencing.
        """Create a referencing registry backed by this instance's retrieval callback."""
        return ReferencingRegistry(retrieve=self.retrieve)  # type: ignore[call-arg]

session = requests.Session() instance-attribute

timeout = timeout instance-attribute

__init__(timeout=_REMOTE_SCHEMA_TIMEOUT_SECONDS)

Create a session for schema retrieval.

Parameters:
  • timeout (int, default: _REMOTE_SCHEMA_TIMEOUT_SECONDS ) –

    Timeout in seconds passed to each schema HTTP request.

Source code in src/assertions_mate/jsonschema_validator.py
46
47
48
49
50
51
52
53
54
55
56
57
def __init__(
    self,
    timeout: int = _REMOTE_SCHEMA_TIMEOUT_SECONDS,
):
    """Create a session for schema retrieval.

    Args:
        timeout: Timeout in seconds passed to each schema HTTP request.
    """
    self.session = requests.Session()
    self.session.headers.update({"User-Agent": "assertions-mate"})
    self.timeout = timeout

as_referencing_registry()

Create a referencing registry backed by this instance's retrieval callback.

Source code in src/assertions_mate/jsonschema_validator.py
200
201
202
203
def as_referencing_registry(self) -> ReferencingRegistry[Any]:
    # mypy does not understand the attrs field alias used by referencing.
    """Create a referencing registry backed by this instance's retrieval callback."""
    return ReferencingRegistry(retrieve=self.retrieve)  # type: ignore[call-arg]

load_schema(uri)

Fetch a schema document and parse it as JSON or YAML.

Parameters:
  • uri (str) –

    Schema location supported by the configured Requests session. The default session supports HTTP and HTTPS, not local files.

Returns:
  • Any –

    The parsed schema document.

Raises:
  • NoSuchResource –

    If a path is missing, the URI scheme has no adapter, or the server returns HTTP 404.

  • RequestException –

    If the request otherwise fails.

  • UnicodeDecodeError –

    If the response is not valid UTF-8.

  • ValueError –

    If the response cannot be parsed as JSON or YAML.

Source code in src/assertions_mate/jsonschema_validator.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
def load_schema(self, uri: str) -> Any:
    """Fetch a schema document and parse it as JSON or YAML.

    Args:
        uri: Schema location supported by the configured Requests session.
            The default session supports HTTP and HTTPS, not local files.

    Returns:
        The parsed schema document.

    Raises:
        NoSuchResource: If a path is missing, the URI scheme has no adapter,
            or the server returns HTTP 404.
        requests.exceptions.RequestException: If the request otherwise fails.
        UnicodeDecodeError: If the response is not valid UTF-8.
        ValueError: If the response cannot be parsed as JSON or YAML.
    """
    try:
        response = self.session.get(
            self._schema_request_uri(uri),
            timeout=self.timeout,
        )
    except requests.exceptions.InvalidSchema as error:
        raise NoSuchResource(uri) from error

    if response.status_code == HTTPStatus.NOT_FOUND.value:
        raise NoSuchResource(uri)

    response.raise_for_status()

    return self._parse_schema(
        payload=response.content,
        uri=uri,
        content_type=response.headers.get("Content-Type"),
    )

retrieve(uri)

Load a URI as a Draft 2020-12 resource for reference resolution.

Parameters:
  • uri (str) –

    Schema location to fetch through load_schema.

Returns:
  • Resource[Any] –

    A schema resource wrapping the parsed document.

Retrieval and parsing errors from load_schema propagate to the caller.

Source code in src/assertions_mate/jsonschema_validator.py
187
188
189
190
191
192
193
194
195
196
197
198
def retrieve(self, uri: str) -> Resource[Any]:
    """Load a URI as a Draft 2020-12 resource for reference resolution.

    Args:
        uri: Schema location to fetch through load_schema.

    Returns:
        A schema resource wrapping the parsed document.

    Retrieval and parsing errors from load_schema propagate to the caller.
    """
    return DRAFT202012.create_resource(self.load_schema(uri))

JSONSchemaValidator

Bases: BaseValidator

Validate input mappings against a JSON Schema Draft 2020-12 schema.

Source code in src/assertions_mate/jsonschema_validator.py
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
class JSONSchemaValidator(BaseValidator):
    """Validate input mappings against a JSON Schema Draft 2020-12 schema."""

    def __init__(
        self,
        schema: Mapping[str, Any],
    ):
        """Configure schema validation and retrieval of referenced schemas.

        Args:
            schema: JSON Schema document describing accepted workflow inputs.
        """
        self.validator = Draft202012Validator(
            schema,
            registry=JSONSchemaRegistry().as_referencing_registry(),
        )

    def validate_inputs(self, data: Mapping[str, Any]) -> ProblemDetails | None:
        """Collect schema violations for a workflow input mapping.

        Args:
            data: Workflow input names mapped to their values.

        Returns:
            InvalidBodyPropertyFormat containing schema violations, or None when
            the inputs conform to the schema.

        Schema evaluation and reference resolution errors propagate to the caller.
        """
        errors_list = []

        for error in self.validator.iter_errors(data):
            print(error.__dict__)

            errors_list.append(
                ErrorDetail(
                    detail=error.message,
                    pointer=f"#/{'/'.join(error.path)}"
                    if error.path and isinstance(error.path, str)
                    else None,
                    code=".".join(error.schema_path),
                )
            )

        if errors_list:
            return InvalidBodyPropertyFormat(errors=errors_list)

        return None

validator = Draft202012Validator(schema, registry=JSONSchemaRegistry().as_referencing_registry()) instance-attribute

__init__(schema)

Configure schema validation and retrieval of referenced schemas.

Parameters:
  • schema (Mapping[str, Any]) –

    JSON Schema document describing accepted workflow inputs.

Source code in src/assertions_mate/jsonschema_validator.py
209
210
211
212
213
214
215
216
217
218
219
220
221
def __init__(
    self,
    schema: Mapping[str, Any],
):
    """Configure schema validation and retrieval of referenced schemas.

    Args:
        schema: JSON Schema document describing accepted workflow inputs.
    """
    self.validator = Draft202012Validator(
        schema,
        registry=JSONSchemaRegistry().as_referencing_registry(),
    )

validate_inputs(data)

Collect schema violations for a workflow input mapping.

Parameters:
  • data (Mapping[str, Any]) –

    Workflow input names mapped to their values.

Returns:
  • ProblemDetails | None –

    InvalidBodyPropertyFormat containing schema violations, or None when

  • ProblemDetails | None –

    the inputs conform to the schema.

Schema evaluation and reference resolution errors propagate to the caller.

Source code in src/assertions_mate/jsonschema_validator.py
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
def validate_inputs(self, data: Mapping[str, Any]) -> ProblemDetails | None:
    """Collect schema violations for a workflow input mapping.

    Args:
        data: Workflow input names mapped to their values.

    Returns:
        InvalidBodyPropertyFormat containing schema violations, or None when
        the inputs conform to the schema.

    Schema evaluation and reference resolution errors propagate to the caller.
    """
    errors_list = []

    for error in self.validator.iter_errors(data):
        print(error.__dict__)

        errors_list.append(
            ErrorDetail(
                detail=error.message,
                pointer=f"#/{'/'.join(error.path)}"
                if error.path and isinstance(error.path, str)
                else None,
                code=".".join(error.schema_path),
            )
        )

    if errors_list:
        return InvalidBodyPropertyFormat(errors=errors_list)

    return None

Rego API

Evaluate Rego policies that return workflow input violation messages.

RegoValidator

Bases: BaseValidator

Run Rego queries and collect their returned expressions as violations.

Source code in src/assertions_mate/rego_validator.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class RegoValidator(BaseValidator):
    """Run Rego queries and collect their returned expressions as violations."""

    def __init__(self, module: str, queries: list[str]) -> None:
        """Load a policy module into a Rego interpreter.

        Args:
            module: Rego source registered under the interpreter module name workflow.
            queries: Queries to run after setting the workflow inputs.

        Policy loading errors propagate from the interpreter.
        """
        self.rego = Interpreter()
        self.rego.add_module("workflow", module)
        self.queries = queries

    def validate_inputs(self, data: Mapping[str, Any]) -> ProblemDetails | None:
        """Set the Rego input document and collect query result expressions.

        Args:
            data: Workflow input mapping exposed to policies as input.

        Returns:
            BusinessRuleViolation containing one error per returned expression,
            with the query as its pointer, or None when no expressions are returned.
            Expressions are converted to strings without testing their truthiness.

        Interpreter errors propagate to the caller.
        """
        errors_list: list[ErrorDetail] = []

        self.rego.set_input(Input(data))

        for query in self.queries:
            for result in self.rego.query(query):
                # result.expressions is a list of Expression objects
                exprs = result.expressions
                if not exprs:  # safety check
                    continue

                errors_list.extend(ErrorDetail(pointer=query, detail=str(expr)) for expr in exprs)

        if errors_list:
            return BusinessRuleViolation(errors=errors_list)

        return None

queries = queries instance-attribute

rego = Interpreter() instance-attribute

__init__(module, queries)

Load a policy module into a Rego interpreter.

Parameters:
  • module (str) –

    Rego source registered under the interpreter module name workflow.

  • queries (list[str]) –

    Queries to run after setting the workflow inputs.

Policy loading errors propagate from the interpreter.

Source code in src/assertions_mate/rego_validator.py
30
31
32
33
34
35
36
37
38
39
40
41
def __init__(self, module: str, queries: list[str]) -> None:
    """Load a policy module into a Rego interpreter.

    Args:
        module: Rego source registered under the interpreter module name workflow.
        queries: Queries to run after setting the workflow inputs.

    Policy loading errors propagate from the interpreter.
    """
    self.rego = Interpreter()
    self.rego.add_module("workflow", module)
    self.queries = queries

validate_inputs(data)

Set the Rego input document and collect query result expressions.

Parameters:
  • data (Mapping[str, Any]) –

    Workflow input mapping exposed to policies as input.

Returns:
  • ProblemDetails | None –

    BusinessRuleViolation containing one error per returned expression,

  • ProblemDetails | None –

    with the query as its pointer, or None when no expressions are returned.

  • ProblemDetails | None –

    Expressions are converted to strings without testing their truthiness.

Interpreter errors propagate to the caller.

Source code in src/assertions_mate/rego_validator.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def validate_inputs(self, data: Mapping[str, Any]) -> ProblemDetails | None:
    """Set the Rego input document and collect query result expressions.

    Args:
        data: Workflow input mapping exposed to policies as input.

    Returns:
        BusinessRuleViolation containing one error per returned expression,
        with the query as its pointer, or None when no expressions are returned.
        Expressions are converted to strings without testing their truthiness.

    Interpreter errors propagate to the caller.
    """
    errors_list: list[ErrorDetail] = []

    self.rego.set_input(Input(data))

    for query in self.queries:
        for result in self.rego.query(query):
            # result.expressions is a list of Expression objects
            exprs = result.expressions
            if not exprs:  # safety check
                continue

            errors_list.extend(ErrorDetail(pointer=query, detail=str(expr)) for expr in exprs)

    if errors_list:
        return BusinessRuleViolation(errors=errors_list)

    return None

CQL2 Validator API

Evaluate CQL2 Text and JSON assertions against workflow input mappings.

Cql2EvaluationError

Bases: RuntimeError

A rule could not be evaluated, rather than evaluating to false.

Source code in src/assertions_mate/cql2_validator.py
57
58
class Cql2EvaluationError(RuntimeError):
    """A rule could not be evaluated, rather than evaluating to false."""

Cql2Validator

Bases: BaseValidator

Validate workflow inputs with CQL2 predicates and optional Python functions.

Source code in src/assertions_mate/cql2_validator.py
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
class Cql2Validator(BaseValidator):
    """Validate workflow inputs with CQL2 predicates and optional Python functions."""

    def __init__(self, queries: list[Cql2Query], custom_functions: str | None = None) -> None:
        """Initialize the native evaluator and retain the configured queries.

        Args:
            queries: Rules to evaluate against each input mapping.
            custom_functions: Trusted Python source executed immediately to populate
                the evaluator function map. Exceptions from this code propagate.
        """
        function_map: dict[str, Any] = {}

        if custom_functions:
            # SECURITY: CWL custom functions are trusted application code.
            # Loading untrusted workflows would permit arbitrary code execution.
            exec(custom_functions, function_map)  # nosec B102

        self.evaluator = NativeEvaluator(function_map=function_map, use_getattr=False)

        self.queries = queries

    def validate_inputs(self, data: Mapping[str, Any]) -> ProblemDetails | None:
        """Evaluate every configured CQL2 rule against the inputs.

        Args:
            data: Workflow input names mapped to their values.

        Returns:
            BusinessRuleViolation for malformed rules or false predicates, or None
            when all rules pass. Error pointers identify the corresponding rules.

        Raises:
            Cql2EvaluationError: If a parsed rule cannot be compiled or evaluated.
                Processing stops at that rule rather than returning a violation.
        """
        errors_list = []

        for query in self.queries:
            errors_list.extend(self._validate_query(query, data))

        if errors_list:
            return BusinessRuleViolation(errors=errors_list)

        return None

    def _validate_query(self, query: Cql2Query, data: Mapping[str, Any]) -> list[ErrorDetail]:
        """Parse and evaluate one rule, returning its validation errors.

        Raises:
            Cql2EvaluationError: If compilation or predicate execution fails.
        """
        errors_list = []
        ast = None

        if isinstance(query.cql2, str):
            try:
                ast = parse_cql2_text(query.cql2)
            except Exception as error:
                errors_list.append(
                    ErrorDetail(
                        pointer=query.id,
                        detail=f"Filter does not look like a valid CQL2 Text encoded sentece: {error}",
                    )
                )
        elif isinstance(query.cql2, dict):
            try:
                ast = parse_cql2_json(_to_builtin(query.cql2))
            except Exception as error:
                errors_list.append(
                    ErrorDetail(
                        pointer=query.id,
                        detail=f"Filter does not look like a valid CQL2 JSON encoded structure: {error}",
                    )
                )
        else:
            errors_list.append(
                ErrorDetail(
                    pointer=query.id,
                    detail=f"Filter is expressed in an unrecognizible format: {type(query.cql2)}",
                )
            )

        if ast and not self._evaluate_predicate(ast, data, query.id):
            errors_list.append(ErrorDetail(pointer=query.id, detail=query.message))

        return errors_list

    def _evaluate_predicate(self, ast: object, data: Mapping[str, Any], rule_id: str) -> bool:
        """Evaluate a parsed rule, wrapping evaluator failures with its identifier."""
        try:
            predicate = self.evaluator.evaluate(ast)
            return bool(predicate(data))
        except Exception as error:
            logger.opt(exception=True).debug("Failed to evaluate CQL2 rule '{}'", rule_id)
            raise Cql2EvaluationError(f"Could not evaluate rule '{rule_id}'") from error

evaluator = NativeEvaluator(function_map=function_map, use_getattr=False) instance-attribute

queries = queries instance-attribute

__init__(queries, custom_functions=None)

Initialize the native evaluator and retain the configured queries.

Parameters:
  • queries (list[Cql2Query]) –

    Rules to evaluate against each input mapping.

  • custom_functions (str | None, default: None ) –

    Trusted Python source executed immediately to populate the evaluator function map. Exceptions from this code propagate.

Source code in src/assertions_mate/cql2_validator.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def __init__(self, queries: list[Cql2Query], custom_functions: str | None = None) -> None:
    """Initialize the native evaluator and retain the configured queries.

    Args:
        queries: Rules to evaluate against each input mapping.
        custom_functions: Trusted Python source executed immediately to populate
            the evaluator function map. Exceptions from this code propagate.
    """
    function_map: dict[str, Any] = {}

    if custom_functions:
        # SECURITY: CWL custom functions are trusted application code.
        # Loading untrusted workflows would permit arbitrary code execution.
        exec(custom_functions, function_map)  # nosec B102

    self.evaluator = NativeEvaluator(function_map=function_map, use_getattr=False)

    self.queries = queries

validate_inputs(data)

Evaluate every configured CQL2 rule against the inputs.

Parameters:
  • data (Mapping[str, Any]) –

    Workflow input names mapped to their values.

Returns:
  • ProblemDetails | None –

    BusinessRuleViolation for malformed rules or false predicates, or None

  • ProblemDetails | None –

    when all rules pass. Error pointers identify the corresponding rules.

Raises:
  • Cql2EvaluationError –

    If a parsed rule cannot be compiled or evaluated. Processing stops at that rule rather than returning a violation.

Source code in src/assertions_mate/cql2_validator.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def validate_inputs(self, data: Mapping[str, Any]) -> ProblemDetails | None:
    """Evaluate every configured CQL2 rule against the inputs.

    Args:
        data: Workflow input names mapped to their values.

    Returns:
        BusinessRuleViolation for malformed rules or false predicates, or None
        when all rules pass. Error pointers identify the corresponding rules.

    Raises:
        Cql2EvaluationError: If a parsed rule cannot be compiled or evaluated.
            Processing stops at that rule rather than returning a violation.
    """
    errors_list = []

    for query in self.queries:
        errors_list.extend(self._validate_query(query, data))

    if errors_list:
        return BusinessRuleViolation(errors=errors_list)

    return None

Plugin registration

Validate inputs against assertion hints on every workflow in the context.

Parameters:
  • context (TranspilerContext) –

    Transpiler context providing the parsed CWL workflows.

  • options (AssertionsMateOptions) –

    Plugin options identifying the input document to load.

Raises:
  • PluginFailureError –

    If a validator raises during input validation.

  • OSError –

    If the input document cannot be opened or read.

The same YAML-loaded input mapping is passed to each workflow. Document parsing errors propagate; returned validation violations are logged.

Source code in src/assertions_mate/plugin.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
@transpiler_plugin(
    name="assertions-mate",
    description="Scan, detect and perform all JSONSchemaHint/RegoPolicyHint/Cql2FilterHint for all Workflows declared in the CWL, against the inputs",
    options_model=AssertionsMateOptions,
)
def assertions_mate(context: TranspilerContext, options: AssertionsMateOptions) -> None:
    """Validate inputs against assertion hints on every workflow in the context.

    Args:
        context: Transpiler context providing the parsed CWL workflows.
        options: Plugin options identifying the input document to load.

    Raises:
        PluginFailureError: If a validator raises during input validation.
        OSError: If the input document cannot be opened or read.

    The same YAML-loaded input mapping is passed to each workflow. Document
    parsing errors propagate; returned validation violations are logged.
    """
    logger.info(f"Loading inputs from {options.inputs.absolute()}")

    with options.inputs.open() as input_stream:
        inputs_mapping: Mapping[str, Any] = YAML().load(input_stream)

    for workflow in context.get_processes_by_type(Workflow):
        _scan_workflow(workflow, inputs_mapping)

Options

Bases: BaseModel

Options accepted by the Assertions Mate plugin.

Attributes:
  • inputs (Annotated[Path, Field(description='The Workflow inputs to check against the input Workflow')]) –

    Path to the YAML or JSON document containing workflow inputs.

Unknown option fields are rejected.

Source code in src/assertions_mate/plugin.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class AssertionsMateOptions(BaseModel):
    """Options accepted by the Assertions Mate plugin.

    Attributes:
        inputs: Path to the YAML or JSON document containing workflow inputs.

    Unknown option fields are rejected.
    """

    model_config = ConfigDict(extra="forbid")

    inputs: Annotated[
        Path,
        Field(description="The Workflow inputs to check against the input Workflow"),
    ]

inputs instance-attribute

model_config = ConfigDict(extra='forbid') class-attribute instance-attribute