Source code for wfcommons.wfinstances.schema

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2020-2024 The WfCommons Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

import json
import jsonschema
import logging
import pathlib
import requests

from logging import Logger
from typing import Any, Dict, Optional


[docs] class SchemaValidator: """ Validate JSON files against WfCommons schema (WfFormat). If schema file path is not provided, it will look for a local copy of the WfFormat schema, and if not available it will fetch the latest schema from the `WfFormat schema GitHub <https://github.com/wfcommons/wfformat>`_ repository. :param schema_file_path: JSON schema file path. :type schema_file_path: Optional[pathlib.Path] :param logger: The logger where to log information/warning or errors. :type logger: Optional[Logger] """ def __init__(self, schema_file_path: Optional[pathlib.Path] = None, logger: Optional[Logger] = None) -> None: """Create an object of the schema validator class.""" self.logger: Logger = logging.getLogger(__name__) if logger is None else logger self.schema = self._load_schema(schema_file_path)
[docs] def validate_instance(self, data: Dict[str, Any]) -> None: """ Perform syntax validation against the schema, and semantic validation. :param data: Workflow instance in JSON format. :type data: Dict[str, Any] """ self._syntax_validation(data) self._semantic_validation(data)
[docs] def _load_schema(self, schema_file_path: Optional[pathlib.Path] = None) -> json: """ Load the schema file. If schema file path is not provided, it will look for a local copy of the WfFormat schema, and if not available it will fetch the latest schema from the GitHub repository. :param schema_file_path: JSON schema file path. :type schema_file_path: Optional[pathlib.Path] :return: The JSON schema. :rtype: json """ if schema_file_path: self.logger.info(f'Using schema file: {schema_file_path}') return json.loads(open(schema_file_path).read()) # looking for local copy of schema file schema_path = pathlib.Path(f"{pathlib.Path.cwd()}/wfcommons-schema.json") if schema_path.exists(): self.logger.info(f'Using schema file: {schema_path}') return json.loads(open(schema_path).read()) # fetching latest schema file from GitHub repository url = 'https://raw.githubusercontent.com/wfcommons/wfformat/master/wfcommons-schema.json' response = requests.get(url) schema = json.loads(response.content) with open(schema_path, 'w') as outfile: json.dump(schema, outfile) self.logger.info(f"Using latest schema file from GitHub repository (saved local copy into {schema_path}).") return schema
[docs] def _syntax_validation(self, data: Dict[str, Any]): """ Validate the JSON workflow execution instance against the schema. :param data: Workflow instance in JSON format. :type data: Dict[str, Any] """ v = jsonschema.Draft4Validator(self.schema) has_error = False for error in sorted(v.iter_errors(data), key=str): msg = ' > '.join([str(e) for e in error.relative_path]) \ + ': ' + error.message self.logger.error(msg) has_error = True if has_error: raise RuntimeError('The workflow instance has syntax errors.')
[docs] def _semantic_validation(self, data: Dict[str, Any]): """ Validate the semantics of the JSON workflow execution instance. :param data: Workflow instance in JSON format. :type data: Dict[str, Any] """ has_error = False machine_ids = [] if "machines" in data["workflow"]["execution"]: for m in data["workflow"]["execution"]["machines"]: machine_ids.append(m["nodeName"]) else: self.logger.debug("Skipping machines processing.") tasks_ids = [] for j in data["workflow"]["execution"]["tasks"]: tasks_ids.append(j["id"]) if "machines" in j: for m in j["machines"]: if m not in machine_ids: self.logger.error(f"Machine \"{j['machine']}\" is not declared in the list of machines.") has_error = True # since tasks may be declared out of order, their dependencies are only verified here for j in data["workflow"]["specification"]["tasks"]: for p in j["parents"]: if p not in tasks_ids: self.logger.error(f"Parent task \"{p}\" is not declared in the list of workflow tasks.") has_error = True self.logger.debug(f'The workflow has {len(tasks_ids)} tasks.') self.logger.debug(f'The workflow has {len(machine_ids)} machines.') if has_error: raise RuntimeError('The workflow instance has semantic errors.')