From 7d4baa54d2fb44ae617e9309671628f28ad695b3 Mon Sep 17 00:00:00 2001 From: Emma Honkamaa Date: Tue, 12 Jun 2018 09:56:17 +0300 Subject: [PATCH 1/3] validator.py and some schema-files for testing --- apluslms_roman/cli.py | 23 +++- apluslms_roman/schemas/config_v1_1.yaml | 68 +++++++++ apluslms_roman/schemas/index_v1_1.yaml | 175 ++++++++++++++++++++++++ apluslms_roman/validator.py | 94 +++++++++++++ requirements.txt | 1 + requirements_build_linux.txt | 1 + simple_gui/setup.py | 2 +- 7 files changed, 361 insertions(+), 3 deletions(-) create mode 100644 apluslms_roman/schemas/config_v1_1.yaml create mode 100644 apluslms_roman/schemas/index_v1_1.yaml create mode 100644 apluslms_roman/validator.py diff --git a/apluslms_roman/cli.py b/apluslms_roman/cli.py index b00f57d..11354e4 100644 --- a/apluslms_roman/cli.py +++ b/apluslms_roman/cli.py @@ -1,23 +1,37 @@ import argparse -from os import getcwd +from os import getcwd, path from os.path import abspath, expanduser, expandvars -from sys import exit +from sys import exit, stderr, modules +import logging from . import __version__ from. builder import Engine from .backends.docker import DockerBackend from .configuration import CourseConfigError, CourseConfig +from .validator import Validator +LOG_LEVELS = [logging.WARNING, logging.INFO, logging.DEBUG] + +logger = logging.getLogger(__name__) def main(): + + parser = argparse.ArgumentParser(description='Course material builder') parser.add_argument('course', nargs='?', help='Location of the course definition. (default: current working dir)') parser.add_argument('--version', action='store_true', help='Print version info') + parser.add_argument("-v", "--verbose", dest="verbose_count", + action="count", default=0, + help="increases log verbosity for each occurence.") args = parser.parse_args() + logging.basicConfig( + level=LOG_LEVELS[min(args.verbose_count, 2)], + #format='%(name)s (%(levelname)s): %(message)s' + ) if args.version: print("Roman {}\n".format(__version__)) @@ -53,10 +67,15 @@ def main(): print("Invalid course config: {}".format(e)) exit(1) + # create validator (and validate config) + logging.info("Validation step:") + validator = Validator(config) + validator.validate( path.join(getcwd(), "_build/yaml/index.yaml") , "index", 1) # build course builder = engine.create_builder(config) result = builder.build() print(result) + logging.shutdown() exit(result.code or 0) diff --git a/apluslms_roman/schemas/config_v1_1.yaml b/apluslms_roman/schemas/config_v1_1.yaml new file mode 100644 index 0000000..7443a27 --- /dev/null +++ b/apluslms_roman/schemas/config_v1_1.yaml @@ -0,0 +1,68 @@ +--- +$schema: "http://json-schema.org/draft-04/schema" +#$schema: "http://stsci.edu/schemas/yaml-schema/draft-01" +title: Exercise schema +type: object +required: + - title + - description + - instructions + - view_type + - files + - container +optional: + - radar_info + +definitions: + languageType: + type: string + enum: + - python + - scala + - javascript + +additionalProperties: false +properties: + title: + description: The name of the exercise + type: string + description: + type: string + instructions: + type: string + view_type: + type: string + files: + type: array + items: + type: object + required: + - field + properties: + field: + type: string + name: + type: string + container: + type: object + properties: + image: + type: string + mount: + type: string + cmd: + type: string + radar_info: + type: object + required: + - tokenizer + properties: + tokenizer: + $ref: '#/definitions/languageType' + #enum: + # - python + # - scala + # - javascript + minimum_match_tokens: + type: number + minimum: 0 diff --git a/apluslms_roman/schemas/index_v1_1.yaml b/apluslms_roman/schemas/index_v1_1.yaml new file mode 100644 index 0000000..6d8e7bb --- /dev/null +++ b/apluslms_roman/schemas/index_v1_1.yaml @@ -0,0 +1,175 @@ +--- +$schema: "http://json-schema.org/draft-04/schema" +title: Index schema +type: object +definitions: + dateTime: + type: string + pattern: ^\d{4}\-\d{2}\-\d{2}(\s\d{2}\:\d{2})?$ + configFormat: + type: string + pattern: \.(json|yaml|yml)$ + statusEnum: + type: string + enum: + - ready + - unlisted + - enrollment + - enrollment_ext + - hidden + - maintenance + - nototal + + +required: + - categories + - end + - language + - modules + - name + - start + - static_dir + +additionalProperties: false +properties: + categories: + type: object + description: Categories can be added freely + additionalProperties: + type: object + required: + - name + properties: + name: + type: string + status: + $ref: '#/definitions/statusEnum' + end: + $ref: '#/definitions/dateTime' + language: + type: string + enum: + - en + - fi + modules: + type: array + additionalItems: false + items: + type: object + required: + - children + - key + # muita pakollisia? + properties: + children: + type: array + additionalItems: false + items: + type: object + required: + - category + - key + #additionalProperties: false + properties: + category: + type: string + children: + type: array + additionalItems: false + items: + type: object + required: + - category + properties: + category: + type: string + children: + type: array + additionalItems: false + items: + type: object + required: + - category + - key + #additionalProperties: false + properties: + allow_assistant_grading: + type: boolean + category: + type: string + config: + $ref: '#/definitions/configFormat' + confirm_the_level: + type: boolean + difficulty: + type: string + key: + type: string + lti: + type: string + lti_context_id: + type: string + lti_resource_link_id: + type: string + max_group_size: + type: number + minimum: 0 + max_points: + type: number + minimum: 0 + max_submissions: + type: number + minimum: 0 + min_group_size: + type: number + minimum: 0 + points_to_pass: + type: number + minimum: 0 + scale_points: + type: number + status: + $ref: '#/definitions/statusEnum' + title: + type: string + key: + type: string + name: + type: string + static_content: + type: string + use_wide_column: + type: boolean + + key: + type: string + name: + type: string + static_content: + type: string + status: + $ref: '#/definitions/statusEnum' + use_wide_column: + type: boolean + close: + $ref: '#/definitions/dateTime' + key: + type: string + late_close: + $ref: '#/definitions/dateTime' + late_penalty: + type: number + minimum: 0.0 + name: + type: string + open: + $ref: '#/definitions/dateTime' + status: + $ref: '#/definitions/statusEnum' + + name: + type: string + start: + $ref: '#/definitions/dateTime' + static_dir: + type: string diff --git a/apluslms_roman/validator.py b/apluslms_roman/validator.py new file mode 100644 index 0000000..e51c807 --- /dev/null +++ b/apluslms_roman/validator.py @@ -0,0 +1,94 @@ +import yaml +from jsonschema import validate +import re +import os +#from operator import itemgetter +import logging + +logger = logging.getLogger(__name__) + +#mystinen lista schema directoryja - course.yamlissa jossain kentässä, joka sijaitsee käyttäjän paketin juuressa +def get_folders(config): + folders = config.__config__.get("custom_schema_locations", []) + dir_, _ = os.path.split(__file__) + folders.append(os.path.join(dir_, "schemas")) + return folders + + +#idea = { schema_name: {(major, minor): location}} +schema_index = {} + +class Validator: + def __init__(self, config): + logger.info(" Creating validator.") + self.folders = get_folders(config) + logger.info(" Validator ready.") + + def validate(self, filename, schema, major=1, minor=float('inf')): + logger.info(" Preparing to validate %s...", filename) + logger.info(" Locating all valid schemas...") + self.find_all_matches(schema) + logger.info(" Locating matching version v%d...", major) + serial, ext = self.find_newest(schema, major, minor) + schema_fullname = "{}_v{}_{}.{}".format(schema, serial[0], serial[1], ext[1]) + logger.debug(" Using schema: %s", schema_fullname) + schema_path = os.path.join(ext[0], schema_fullname) + logger.info(" Starting validation of %s with %s", filename, schema_fullname) + result = self.assert_valid(schema_path, filename) + logger.info(" Validation done", filename) + return result + + def find_all_matches(self, schema): + prog = re.compile("{}_v(\d+)_(\d+).(yaml|yml|json)".format(schema)) + logger.debug(" Schema-------------%s", schema) + for folder in self.folders: + logger.debug(" Folder-------------%s", folder) + for ff in os.listdir(folder): + logger.debug(" File-------------%s", ff) + if prog.match(ff) is not None: + found = prog.match(ff).groups() + schema_index.setdefault(schema, {}) + logger.debug(" Index-------------%s", schema_index) + current = schema_index[schema].setdefault((int(found[0]), int(found[1])), None) + if current is None: + schema_index[schema][int(found[0]), int(found[1])] = (folder, found[2]) + #matches.append([schema, int(found[0]), int(found[1]), found[2] , folder]) + print(schema_index) + + #Finds the newest schema with 'major' and 'minor' as maximum allowed values. + #If major is not defined, v1.x is assumed. + def find_newest(self, schema, major, minor): + schemas = schema_index.get(schema) + if schemas is None: + print("No schema's with that name!") + return None + + #filter(_ <= major.minor) and reduce(max(_,_)) + i = {k:v for k, v in schemas.items() if k[0]ans[0] or k[0]==ans[0] and k[1]>ans[1] else (ans, f) + return (ans, f) + + #Validate schema + def assert_valid(self, schema, filename): + try: + with open(filename, "r") as f: + ff = f.read() + with open(schema, "r") as s: + ss = s.read() + except: + logger.exception('Error with files, exiting....') + return + + logger.info('Files opened succesfully. Validating...') + validate(yaml.safe_load(ff), yaml.safe_load(ss)) + logger.info('Done') + + # def get_schema_folder_and_type(self, schema, serial): + # return schema_index.get(schema).get(serial) + # + # def get_schema_name_from_file(self, filename): + # prog = re.compile("^\/(.+\/)*(.+)\.(json|yaml|yml)$") + # name = prog.match(filename).groups()[1] + # return name diff --git a/requirements.txt b/requirements.txt index c32bca5..11748dc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ PyYaml>=3.12 Docker>=3.0.0 +jsonschema>=2.6.0 diff --git a/requirements_build_linux.txt b/requirements_build_linux.txt index 64faff7..4241414 100644 --- a/requirements_build_linux.txt +++ b/requirements_build_linux.txt @@ -1 +1,2 @@ PyInstaller >= 3.2.1 +tk diff --git a/simple_gui/setup.py b/simple_gui/setup.py index 8070692..b82fce9 100755 --- a/simple_gui/setup.py +++ b/simple_gui/setup.py @@ -49,7 +49,7 @@ ], zip_safe=True, - #packages=find_packages(exclude=['contrib', 'docs', 'tests']), + packages=find_packages(exclude=['contrib', 'docs', 'tests']), py_modules=['roman_tki'], include_package_data = True, package_data={ From f18d1f79ce2d67ceb9b110ba435b6a466ac3deaf Mon Sep 17 00:00:00 2001 From: Emma Honkamaa Date: Tue, 12 Jun 2018 12:51:25 +0300 Subject: [PATCH 2/3] cleaned some commented sections in validator --- apluslms_roman/cli.py | 1 - apluslms_roman/validator.py | 11 +---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/apluslms_roman/cli.py b/apluslms_roman/cli.py index 11354e4..001d9cb 100644 --- a/apluslms_roman/cli.py +++ b/apluslms_roman/cli.py @@ -70,7 +70,6 @@ def main(): # create validator (and validate config) logging.info("Validation step:") validator = Validator(config) - validator.validate( path.join(getcwd(), "_build/yaml/index.yaml") , "index", 1) # build course builder = engine.create_builder(config) result = builder.build() diff --git a/apluslms_roman/validator.py b/apluslms_roman/validator.py index e51c807..19e665e 100644 --- a/apluslms_roman/validator.py +++ b/apluslms_roman/validator.py @@ -31,7 +31,6 @@ def validate(self, filename, schema, major=1, minor=float('inf')): logger.info(" Locating matching version v%d...", major) serial, ext = self.find_newest(schema, major, minor) schema_fullname = "{}_v{}_{}.{}".format(schema, serial[0], serial[1], ext[1]) - logger.debug(" Using schema: %s", schema_fullname) schema_path = os.path.join(ext[0], schema_fullname) logger.info(" Starting validation of %s with %s", filename, schema_fullname) result = self.assert_valid(schema_path, filename) @@ -40,7 +39,7 @@ def validate(self, filename, schema, major=1, minor=float('inf')): def find_all_matches(self, schema): prog = re.compile("{}_v(\d+)_(\d+).(yaml|yml|json)".format(schema)) - logger.debug(" Schema-------------%s", schema) + logger.debug(" Looking for schema-------------%s", schema) for folder in self.folders: logger.debug(" Folder-------------%s", folder) for ff in os.listdir(folder): @@ -84,11 +83,3 @@ def assert_valid(self, schema, filename): logger.info('Files opened succesfully. Validating...') validate(yaml.safe_load(ff), yaml.safe_load(ss)) logger.info('Done') - - # def get_schema_folder_and_type(self, schema, serial): - # return schema_index.get(schema).get(serial) - # - # def get_schema_name_from_file(self, filename): - # prog = re.compile("^\/(.+\/)*(.+)\.(json|yaml|yml)$") - # name = prog.match(filename).groups()[1] - # return name From c4d39ed6153568779c03c50591ee2e7835220147 Mon Sep 17 00:00:00 2001 From: Emma Honkamaa Date: Fri, 15 Jun 2018 10:09:27 +0300 Subject: [PATCH 3/3] some cleanup, added exception for ValidationError --- apluslms_roman/cli.py | 15 +- apluslms_roman/schemas/config_v1_0.yaml | 68 +++++++++ apluslms_roman/schemas/index_v1_0.yaml | 175 ++++++++++++++++++++++++ apluslms_roman/validator.py | 84 +++++++----- 4 files changed, 301 insertions(+), 41 deletions(-) create mode 100644 apluslms_roman/schemas/config_v1_0.yaml create mode 100644 apluslms_roman/schemas/index_v1_0.yaml diff --git a/apluslms_roman/cli.py b/apluslms_roman/cli.py index 001d9cb..34aed66 100644 --- a/apluslms_roman/cli.py +++ b/apluslms_roman/cli.py @@ -1,7 +1,7 @@ import argparse -from os import getcwd, path +from os import getcwd from os.path import abspath, expanduser, expandvars -from sys import exit, stderr, modules +from sys import exit import logging from . import __version__ @@ -15,8 +15,6 @@ logger = logging.getLogger(__name__) def main(): - - parser = argparse.ArgumentParser(description='Course material builder') parser.add_argument('course', nargs='?', help='Location of the course definition. (default: current working dir)') @@ -68,8 +66,15 @@ def main(): exit(1) # create validator (and validate config) - logging.info("Validation step:") + logging.info(" >> Validation step:") validator = Validator(config) + # validation_result = validator.validate(location of the file here, "config", 1) + # if validation_result is False: + # logging.warning( "Validation unsuccessful. Terminating.") + # exit(0) + logging.info(" >> All files validated succesfully") + + # build course builder = engine.create_builder(config) result = builder.build() diff --git a/apluslms_roman/schemas/config_v1_0.yaml b/apluslms_roman/schemas/config_v1_0.yaml new file mode 100644 index 0000000..7443a27 --- /dev/null +++ b/apluslms_roman/schemas/config_v1_0.yaml @@ -0,0 +1,68 @@ +--- +$schema: "http://json-schema.org/draft-04/schema" +#$schema: "http://stsci.edu/schemas/yaml-schema/draft-01" +title: Exercise schema +type: object +required: + - title + - description + - instructions + - view_type + - files + - container +optional: + - radar_info + +definitions: + languageType: + type: string + enum: + - python + - scala + - javascript + +additionalProperties: false +properties: + title: + description: The name of the exercise + type: string + description: + type: string + instructions: + type: string + view_type: + type: string + files: + type: array + items: + type: object + required: + - field + properties: + field: + type: string + name: + type: string + container: + type: object + properties: + image: + type: string + mount: + type: string + cmd: + type: string + radar_info: + type: object + required: + - tokenizer + properties: + tokenizer: + $ref: '#/definitions/languageType' + #enum: + # - python + # - scala + # - javascript + minimum_match_tokens: + type: number + minimum: 0 diff --git a/apluslms_roman/schemas/index_v1_0.yaml b/apluslms_roman/schemas/index_v1_0.yaml new file mode 100644 index 0000000..6d8e7bb --- /dev/null +++ b/apluslms_roman/schemas/index_v1_0.yaml @@ -0,0 +1,175 @@ +--- +$schema: "http://json-schema.org/draft-04/schema" +title: Index schema +type: object +definitions: + dateTime: + type: string + pattern: ^\d{4}\-\d{2}\-\d{2}(\s\d{2}\:\d{2})?$ + configFormat: + type: string + pattern: \.(json|yaml|yml)$ + statusEnum: + type: string + enum: + - ready + - unlisted + - enrollment + - enrollment_ext + - hidden + - maintenance + - nototal + + +required: + - categories + - end + - language + - modules + - name + - start + - static_dir + +additionalProperties: false +properties: + categories: + type: object + description: Categories can be added freely + additionalProperties: + type: object + required: + - name + properties: + name: + type: string + status: + $ref: '#/definitions/statusEnum' + end: + $ref: '#/definitions/dateTime' + language: + type: string + enum: + - en + - fi + modules: + type: array + additionalItems: false + items: + type: object + required: + - children + - key + # muita pakollisia? + properties: + children: + type: array + additionalItems: false + items: + type: object + required: + - category + - key + #additionalProperties: false + properties: + category: + type: string + children: + type: array + additionalItems: false + items: + type: object + required: + - category + properties: + category: + type: string + children: + type: array + additionalItems: false + items: + type: object + required: + - category + - key + #additionalProperties: false + properties: + allow_assistant_grading: + type: boolean + category: + type: string + config: + $ref: '#/definitions/configFormat' + confirm_the_level: + type: boolean + difficulty: + type: string + key: + type: string + lti: + type: string + lti_context_id: + type: string + lti_resource_link_id: + type: string + max_group_size: + type: number + minimum: 0 + max_points: + type: number + minimum: 0 + max_submissions: + type: number + minimum: 0 + min_group_size: + type: number + minimum: 0 + points_to_pass: + type: number + minimum: 0 + scale_points: + type: number + status: + $ref: '#/definitions/statusEnum' + title: + type: string + key: + type: string + name: + type: string + static_content: + type: string + use_wide_column: + type: boolean + + key: + type: string + name: + type: string + static_content: + type: string + status: + $ref: '#/definitions/statusEnum' + use_wide_column: + type: boolean + close: + $ref: '#/definitions/dateTime' + key: + type: string + late_close: + $ref: '#/definitions/dateTime' + late_penalty: + type: number + minimum: 0.0 + name: + type: string + open: + $ref: '#/definitions/dateTime' + status: + $ref: '#/definitions/statusEnum' + + name: + type: string + start: + $ref: '#/definitions/dateTime' + static_dir: + type: string diff --git a/apluslms_roman/validator.py b/apluslms_roman/validator.py index 19e665e..6bf0521 100644 --- a/apluslms_roman/validator.py +++ b/apluslms_roman/validator.py @@ -1,5 +1,5 @@ import yaml -from jsonschema import validate +from jsonschema import validate, ValidationError import re import os #from operator import itemgetter @@ -7,16 +7,17 @@ logger = logging.getLogger(__name__) -#mystinen lista schema directoryja - course.yamlissa jossain kentässä, joka sijaitsee käyttäjän paketin juuressa +#List of (possible) custom schema locations will be in config file def get_folders(config): - folders = config.__config__.get("custom_schema_locations", []) + folders = config.__config__.get("schema_path", []) + if not isinstance(folders, list): + raise ValueError("Invalid type for 'schema_path' in config. Excpected a list.") dir_, _ = os.path.split(__file__) folders.append(os.path.join(dir_, "schemas")) return folders - -#idea = { schema_name: {(major, minor): location}} -schema_index = {} +#Format of index: { schema_name: {(major, minor): full_filepath}} +SCHEMA_INDEX = {} class Validator: def __init__(self, config): @@ -24,18 +25,17 @@ def __init__(self, config): self.folders = get_folders(config) logger.info(" Validator ready.") - def validate(self, filename, schema, major=1, minor=float('inf')): + def validate(self, filename, schema, major=1, minor=None): logger.info(" Preparing to validate %s...", filename) logger.info(" Locating all valid schemas...") self.find_all_matches(schema) logger.info(" Locating matching version v%d...", major) - serial, ext = self.find_newest(schema, major, minor) - schema_fullname = "{}_v{}_{}.{}".format(schema, serial[0], serial[1], ext[1]) - schema_path = os.path.join(ext[0], schema_fullname) - logger.info(" Starting validation of %s with %s", filename, schema_fullname) - result = self.assert_valid(schema_path, filename) - logger.info(" Validation done", filename) - return result + serial, schema_path = self.find_newest(schema, major, minor) + #schema_fullname = "{}_v{}_{}".format(schema, serial[0], serial[1]) + #schema_path = os.path.join(ext[0], schema_fullname) + logger.info(" Starting validation of %s with %s", filename, schema_path) + return self.assert_valid(schema_path, filename) + def find_all_matches(self, schema): prog = re.compile("{}_v(\d+)_(\d+).(yaml|yml|json)".format(schema)) @@ -45,29 +45,35 @@ def find_all_matches(self, schema): for ff in os.listdir(folder): logger.debug(" File-------------%s", ff) if prog.match(ff) is not None: - found = prog.match(ff).groups() - schema_index.setdefault(schema, {}) - logger.debug(" Index-------------%s", schema_index) - current = schema_index[schema].setdefault((int(found[0]), int(found[1])), None) + major, minor, ext = prog.match(ff).groups() + schema_i = SCHEMA_INDEX.setdefault(schema, {}) + logger.debug(" Index-------------%s", SCHEMA_INDEX) + current = schema_i.setdefault((int(major), int(minor)), None) if current is None: - schema_index[schema][int(found[0]), int(found[1])] = (folder, found[2]) - #matches.append([schema, int(found[0]), int(found[1]), found[2] , folder]) - print(schema_index) + schema_i[int(major), int(minor)] = os.path.join(folder, ff) - #Finds the newest schema with 'major' and 'minor' as maximum allowed values. - #If major is not defined, v1.x is assumed. + #Finds the newest schema with given 'major' and newest 'minor'. + #If major is not defined, v1 is assumed. def find_newest(self, schema, major, minor): - schemas = schema_index.get(schema) + schemas = SCHEMA_INDEX.get(schema) if schemas is None: - print("No schema's with that name!") + logger.warning(" No schema's with that name!") + return None + + if minor is None: + ver = max({v for v in schemas if v[0]==major}, default=None) + if ver is None: + logger.warning(" No schema with version %d", major) + return None + else: + ver = (major, minor) + + _file = schemas.get(ver, None) + if _file is None: + logger.warning(" No schema with version %d.%d", major, minor) return None + return ver, _file - #filter(_ <= major.minor) and reduce(max(_,_)) - i = {k:v for k, v in schemas.items() if k[0]ans[0] or k[0]==ans[0] and k[1]>ans[1] else (ans, f) - return (ans, f) #Validate schema def assert_valid(self, schema, filename): @@ -77,9 +83,15 @@ def assert_valid(self, schema, filename): with open(schema, "r") as s: ss = s.read() except: - logger.exception('Error with files, exiting....') - return + logger.exception(' Error with files, exiting....') + return False - logger.info('Files opened succesfully. Validating...') - validate(yaml.safe_load(ff), yaml.safe_load(ss)) - logger.info('Done') + try: + logger.info(' Files opened succesfully. Validating...') + validate(yaml.safe_load(ff), yaml.safe_load(ss)) + logger.info(' %s validated succesfully', filename) + return True + except ValidationError as e: + logger.warning(" An error occurred!") + logger.warning(" %s", e) + return False