diff --git a/.gitignore b/.gitignore index 62fe96a6..03bc61fd 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ Pipfile.lock #pycharm .idea/ +output diff --git a/maestrowf/datastructures/core/executiongraph.py b/maestrowf/datastructures/core/executiongraph.py index 9ebdbf1e..4d5d3d1c 100644 --- a/maestrowf/datastructures/core/executiongraph.py +++ b/maestrowf/datastructures/core/executiongraph.py @@ -338,6 +338,7 @@ def __init__(self, submission_attempts=1, submission_throttle=0, # Member variables for execution. self._adapter = None self._description = OrderedDict() + self.linker = None # Generate tempdir (if specfied) if use_tmp: @@ -573,6 +574,8 @@ def _execute_record(self, record, adapter, restart=False): # Generate the script for execution on the fly. record.setup_workspace() # Generate the workspace. record.generate_script(adapter, self._tmp_dir) + if self.linker: + self.linker.link(record) if self.dry_run: record.mark_end(State.DRYRUN) diff --git a/maestrowf/datastructures/core/linker.py b/maestrowf/datastructures/core/linker.py new file mode 100644 index 00000000..3715a90f --- /dev/null +++ b/maestrowf/datastructures/core/linker.py @@ -0,0 +1,440 @@ +############################################################################### +# Copyright (c) 2017, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory +# Written by Francesco Di Natale, dinatale3@llnl.gov. +# +# LLNL-CODE-734340 +# All rights reserved. +# This file is part of MaestroWF, Version: 1.0.0. +# +# For details, see https://github.com/LLNL/maestrowf. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +############################################################################### + +"""Utility class to make links.""" + +import random +import pprint +import datetime +import logging +import os +import time +from collections import defaultdict + +from filelock import SoftFileLock as FileLock + +from maestrowf.utils import splitall, next_index_and_path, recursive_render + +LOGGER = logging.getLogger(__name__) + + +class Linker: + """Utility class to make links.""" + index_format = '%04d' + mkdir_timeout = 5 # seconds + TEMPLATE_ALLOWED_TOKENS = [ + '{{study_index}}', + '{{output_name}}', + '{{study_time}}', + '{{study_date}}', + '{{date}}', + '{{step}}', + '{{combo_index}}', + '{{combo}}' + ] + HELP_TEXT = ( + "Jinja template for links to run directories.\n" + "NOTE: template must include {{combo}} and {{step}}.\n" + "[Default: %(default)s]\n \n" + "Currently supported Jinja variables:\n" + "{{output_path}} - Parent directory for this maestro study\n" + "{{date}} - Human-readable date (e.g. '2020_07_28')\n" + "{{combo}} - Maestro label for a set of parameters\n" + " (e.g. 'X1.5.X2.5.X3.20')\n" + " [maximum length: 255 characters]\n" + "{{step}} - Maestro label for a given step (e.g. 'run')\n" + "{{study_index}} - Unique number for each maestro execution (e.g. '0001')") # noqa501 + + def __init__( + self, make_links_flag=False, hashws=False, + link_template=None, output_name=None, output_path=None, + spec_name=None, date_string=None, time_string=None, + dir_float_format=['{:.2f}', '{:.2e}'], pgen=None, globals={}): + """ + Initialize a new Linker class instance. + + :param make_links_flag: Enable customizable, human-readable links to + run directories. + :param link_template: Jinja template for links to run directories. + """ + self.make_links_flag = make_links_flag + self.link_template = link_template + self.output_name = output_name + self.output_path = output_path + self.spec_name = spec_name + self.date_string = date_string + self.time_string = time_string + self.dir_float_format = dir_float_format + self.pgen = pgen + self.globals = globals + self.study_index = 0 + self.combo_index = defaultdict(int) + self._study_datetime = datetime.datetime.now() + if make_links_flag: + self.validate_link_template(link_template) + + @staticmethod + def extract_jinja_tokens(string): + result = [] + word = [] + bracket_count = 0 + for char in string: + if char == "}": + if bracket_count == 2: + result.append("".join(word)) + word = [] + bracket_count -= 1 + if bracket_count == 2: + word.append(char) + if char == "{": + bracket_count += 1 + return result + + def validate_link_template(self, link_template): + """ + Validate link template. + The template must have enough information to generate a + unique path for each study and each combo in the study. + """ + # @TODO: generalize to work if there are no combos. + error = False + error_text = "" + link_template_tokens = self.extract_jinja_tokens(link_template) + maestro_var_tokens = ["{{" + var + "}}" for var in self.globals.keys()] + all_allowed_tokens = maestro_var_tokens.copy() + all_allowed_tokens.extend(self.TEMPLATE_ALLOWED_TOKENS) + for token in link_template_tokens: + if "{{" + token + "}}" not in all_allowed_tokens: + LOGGER.warning( + f"template token ({token}) is not in list of allowed tokens " + f"({all_allowed_tokens})") + if ("{{" + token + "}}" in self.TEMPLATE_ALLOWED_TOKENS + and "{{" + token + "}}" in maestro_var_tokens): + error = True + error_text += ( + f"Template error: '{link_template}'\n" + f" ({token}) can not be resolved.\n" + f" Variable tokens: ({maestro_var_tokens}).\n" + f" Template tokens: ({self.TEMPLATE_ALLOWED_TOKENS})\n.") + study_index_index = link_template.find('{{study_index}}') + output_name_index = link_template.find('{{output_name}}') + study_time_index = link_template.find('{{study_time}}') + study_date_index = link_template.find('{{study_date}}') + date_index = link_template.find('{{date}}') + max_study_index = max(study_index_index, output_name_index, + study_time_index, study_date_index, + date_index) + if study_index_index == -1 and output_name_index == -1: + if study_time_index == -1: + error = True + if study_date_index == -1 and date_index == -1: + error = True + if error: + error_text += ( + f"Template error: '{link_template}'\n" + f" does not include required 'study' variables \n" + " {{study_time}} and {{study_date}} or {{date}},\n" + " or {{study_index}}, or {{output_name}}.\n") + step_index = link_template.find('{{step}}') + if step_index == -1: + error = True + error_text += ( + f"Template error: '{link_template}'\n" + " does not include required {{step}} variable.\n") + if self.pgen is not None or self.globals != {}: + combo_index_index = link_template.find('{{combo_index}}') + combo_index = link_template.find('{{combo}}') + min_key_index = float("inf") + var_list = ["{{" + var + "}}" for var in self.globals.keys()] + for key in self.globals.keys(): + key_index = link_template.find('{{' + key + '}}') + if key_index < min_key_index: + min_key_index = key_index + min_combo_list = ( + [var for var in [combo_index, combo_index_index, min_key_index] + if var > -1]) + if min_combo_list: + min_combo_index = min(min_combo_list) + else: + min_combo_index = -1 + if (combo_index_index == -1 + and combo_index == -1 + and min_key_index == -1): + error = True + error_text += ( + f"Template error: '{link_template}'\n" + f" does not include required 'combo' variables \n" + f" {{combo_index}}, or {{combo}},\n" + f" or all global variables ({var_list})\n") + if min_combo_index < max_study_index: + error = True + error_text += ( + f"Template error: in '{link_template}'\n" + + " This code requires all combo variables to be to the right\n" # noqa: E501 + " of all study variables.\n" + " The position of the rightmost 'study' variables \n" + " ({{study_index}}, {{study_time}}, {{study_date}}, or {{date}})\n" # noqa: E501 + " is to the right of the leftmost 'combo' variables\n" + " ({{combo}}, {{combo_index}}, or all global variables\n" # noqa: E501 + f" ({var_list})).\n" + ) + if link_template.count('{{combo_index}}') > 1: + error = True + error_text += ( + f"Template error: in '{link_template}'\n" + " '{{combo_index}}' can not be repeated.") + if step_index < max_study_index: + error = True + error_text += ( + f"Template error: in '{link_template}'\n" + " This code requires the {{step}} variable to be to the right\n" # noqa: E501 + " of all study stubstrings.\n" + " The position of the rightmost 'study' variables \n" + " ({{study_index}}, {{study_time}}, {{study_date}}, or {{date}})\n" # noqa: E501 + " is to the right of the {{step}} variable.\n") + error = True + if link_template.count('{{study_index}}') > 1: + error = True + error_text += ( + f"Template error: in '{link_template}'\n" + " '{{study_index}}' can not be repeated.") + if error: + print(error_text) + raise ValueError(error_text) + + @staticmethod + def format_float(num, format_list): + """ + Return num as string using format_list. + + format_list, for example (['{:.2f}','{:.2e}']), + contains "".format() style format strings for + numbers with small exponents and for numbers with + large exponents. + """ + if type(num) != float: + return str(num) + float_string = "{}".format(num) + if float_string.find("e") > -1: + formatted_string = format_list[1].format(num) + else: + formatted_string = format_list[0].format(num) + return formatted_string + + def build_replacements(self, record): + """ build replacements dictionary from StepRecord""" + # {{study-name}} {{step-name}} {{study-index}} {{combo-index}} + replacements = {} + replacements['study_time'] = self.time_string + replacements['study_date'] = self.date_string + replacements['study_name'] = self.spec_name + replacements['link_template'] = self.link_template + replacements['output_name'] = self.output_name + replacements['output_path'] = self.output_path + replacements['date'] = self._study_datetime.strftime('%Y-%m-%d') + if type(self.study_index) == int: + replacements["study_index"] = "{{study_index}}" + else: + replacements["study_index"] = self.study_index + replacements['step'] = record.step.step_name + if record.step.combo is not None and record._params: + # long_combo = os.path.basename(record.workspace.value) + long_combo = record.step._param_string + if type(self.combo_index[long_combo]) == int: + replacements["combo_index"] = "{{combo_index}}" + else: + replacements["combo_index"] = self.combo_index[long_combo] + combo = long_combo + replacements['long_combo'] = long_combo + replacements['nickname'] = record.step.nickname + + for param, name in zip( + record.step.combo._params.values(), + record.step.combo._labels.values()): # noqa: E125 + combo = combo.replace( # noqa: E117 + name, + name.replace( + str(param), + self.format_float(param, self.dir_float_format)) + ) + # LOGGER.info(f"DEBUG step: {step}") + # LOGGER.info(f"DEBUG record.name: {record.name}") + + # replacements['step'] = step + replacements['combo'] = combo + else: + # replacements['step'] = record.name + replacements['combo'] = "all_records" + replacements['long_combo'] = "all_records" + replacements['nickname'] = None + if record.step.combo is not None and record._params: + print(f"debug combo: {combo}") + for param, name in zip( + record.step.combo._params.items(), + record.step.combo._names.items()): + print("param/name debug:", param, name) + key = name[1] + value = param[1] + if (key in replacements + and self.link_template.find("{{" + key + "}}") > -1): + error_text = ( + f"user key/value: {key}/{value} conflicts with " + f"maestro link template key/value: " + f"{key}/{replacements[key]}.") + LOGGER.error(error_text) + raise ValueError(error_text) + replacements[key] = ( + self.format_float(value, self.dir_float_format)) + # print(replacements['step'], "==?", replacements['step2']) + # assert replacements['step'] == replacements['step2'] + return replacements + + @staticmethod + def split_directory(dir, split_string): + """ + Split directory into three pieces + The central piece will contain the 'split_string' + """ + if dir.find(split_string) == -1: + return (dir, "", "") + dir_list = splitall(dir) + left_dirs = [] + right_dirs = [] + found = False + for dir in dir_list: + if found: + right_dirs.append(dir) + else: + if dir.find(split_string) == -1: + left_dirs.append(dir) + else: + index_dir = dir + found = True + return ( + os.path.join(*left_dirs), + index_dir, + os.path.join(*right_dirs)) + + def new_index(self, dir, index_name): + """ + Generate a new index. + `index_name` is {{study-index}} or {{combo-index}} + """ + if dir.find(index_name) == -1: + return self.index_format % 0 + left_dirs, index_dir, right_dirs = ( + self.split_directory(dir, index_name)) + if "{" in left_dirs: + raise ValueError( + "ERROR: the following path should not have any jinja " + f"variables: {left_dirs}") + os.makedirs(left_dirs, exist_ok=True) + return self.next_path_w_lock( + os.path.join(left_dirs, index_dir), index_name) + + def next_path_w_lock(self, path_with_index, index_name): + """ + Thread safe version of next_path. + Returns formatted index string. + """ + pass + success = False + timeout = time.time() + self.mkdir_timeout + lock_file = path_with_index.replace(index_name, "lock") + template = path_with_index.replace(index_name, self.index_format) + lock = FileLock( + lock_file, + timeout=2*self.mkdir_timeout) + with lock: + while not success and time.time() < timeout: + try: + index, index_directory_string = ( + next_index_and_path(template)) + os.makedirs(index_directory_string) + success = True + except OSError as e: + if e.args[1] == 'File exists': + time.sleep(random.uniform( + 0.05*self.mkdir_timeout, + 0.10*self.mkdir_timeout)) + elif e.args[1] == 'Permission denied': + raise(ValueError( + "Could not create a unique directory " + "because of a " + "permissions error.\n\n" + f"Attempted path: {index_directory_string}" + )) + else: + raise(ValueError(e)) + return self.index_format % index + + def link(self, record): + """Create link for StepRecord""" + # @TODO: test cases: index in front, middle, end, no index, two indexes + # with and without hash + if not self.make_links_flag: + return + replacements = self.build_replacements(record) + link_path = recursive_render(self.link_template, replacements) + if type(self.study_index) == int: + new_index = self.new_index(link_path, "{{study_index}}") + self.study_index = new_index + replacements = self.build_replacements(record) + link_path = recursive_render(self.link_template, replacements) + if type(self.combo_index[replacements['long_combo']]) == int: + new_index = self.new_index(link_path, "{{combo_index}}") + self.combo_index[replacements['long_combo']] = new_index + replacements = self.build_replacements(record) + link_path = recursive_render(self.link_template, replacements) + LOGGER.info(f"DEBUG: \n{pprint.pformat(replacements)}") + try: + # make full path; then make link + os.makedirs(link_path) + os.rmdir(link_path) + link_target = record.workspace.value + if replacements['nickname']: + link_target.replace( + replacements['long_combo'], + replacements['nickname']) + os.symlink(link_target, link_path) + except OSError as e: + if e.args[1] == 'File exists': + raise(ValueError( + "Could not create a unique directory.\n\n" + + "Attempted path: " + link_path + "\n" + + "Template string: " + self.link_template)) + elif e.args[1] == 'Permission denied': + raise(ValueError( + "Could not create a unique directory because of a " + + "permissions error.\n\n" + + "Attempted path: " + link_path + "\n" + + "Template string: " + self.link_template + )) + else: + raise(ValueError) diff --git a/maestrowf/datastructures/core/study.py b/maestrowf/datastructures/core/study.py index 2aea20df..4945cb44 100644 --- a/maestrowf/datastructures/core/study.py +++ b/maestrowf/datastructures/core/study.py @@ -68,8 +68,11 @@ class StudyStep: def __init__(self): """Object that represents a single workflow step.""" self._name = "" + self._step_name = "" + self._param_string = "" self.description = "" self.nickname = "" + self.combo = None self.run = { "cmd": "", "depends": "", @@ -94,6 +97,8 @@ def apply_parameters(self, combo): # Create a new StudyStep and populate it with substituted values. tmp = StudyStep() tmp.__dict__ = apply_function(self.__dict__, combo.apply) + tmp._step_name = self.__dict__["_name"] + tmp.combo = combo # Return if the new step is modified and the step itself. return self.__ne__(tmp), tmp @@ -127,6 +132,17 @@ def real_name(self): """ return self._name + @property + def step_name(self): + """ + Get the name to assign to a task for this step. + + :returns: A utf-8 formatted string of the task name. + """ + if self._step_name: + return self._step_name + return self.name + def __eq__(self, other): """ Equality operator for the StudyStep class. @@ -421,7 +437,7 @@ def setup_environment(self): def configure_study(self, submission_attempts=1, restart_limit=1, throttle=0, use_tmp=False, hash_ws=False, - dry_run=False): + dry_run=False, linker=None): """ Perform initial configuration of a study. \ @@ -438,6 +454,7 @@ def configure_study(self, submission_attempts=1, restart_limit=1, ExecutionGraph dumps its information into a temporary directory. \ :param dry_run: Boolean value that toggles dry run to just generate \ study workspaces and scripts without execution or status checking. \ + :param linker: Linker object. :returns: True if the Study is successfully setup, False otherwise. \ """ @@ -447,6 +464,10 @@ def configure_study(self, submission_attempts=1, restart_limit=1, self._use_tmp = use_tmp self._hash_ws = hash_ws self._dry_run = dry_run + self.linker = linker + make_links_flag = False + if linker: + make_links_flag = linker.make_links_flag LOGGER.info( "\n------------------------------------------\n" @@ -456,10 +477,11 @@ def configure_study(self, submission_attempts=1, restart_limit=1, "Use temporary directory = %s\n" "Hash workspaces = %s\n" "Dry run enabled = %s\n" + "Make links enabled = %s\n" "Output path = %s\n" "------------------------------------------", submission_attempts, restart_limit, throttle, - use_tmp, hash_ws, dry_run, self._out_path + use_tmp, hash_ws, dry_run, make_links_flag, self._out_path ) self.is_configured = True @@ -655,20 +677,21 @@ def _stage(self, dag): str(combo)) # Compute this step's combination name and workspace. nickname = None - combo_str = combo.get_param_string(self.used_params[step]) + param_str = combo.get_param_string(self.used_params[step]) # We must encode explicitly to utf-8 - # combo_str = combo_str.encode("utf-8") + # param_str = param_str.encode("utf-8") if self._hash_ws: - nickname = md5(combo_str.encode("utf-8")).hexdigest() + nickname = md5(param_str.encode("utf-8")).hexdigest() workspace = make_safe_path( self._out_path, *[step, nickname]) else: workspace = \ - make_safe_path(self._out_path, *[step, combo_str]) + make_safe_path(self._out_path, *[step, param_str]) LOGGER.debug("Workspace: %s", workspace) - combo_str = "{}_{}".format(step, combo_str) + combo_str = "{}_{}".format(step, param_str) self.workspaces[combo_str] = workspace + LOGGER.debug("Workspace: %s", workspace) # Check if the step combination has been processed. if combo_str in self.step_combos: @@ -678,6 +701,7 @@ def _stage(self, dag): modified, step_exp = node.apply_parameters(combo) step_exp.name = combo_str + step_exp._param_string = param_str step_exp.nickname = nickname # Substitute workspaces into the combination. @@ -806,6 +830,7 @@ def _stage_linear(self, dag): r_cmd = r_cmd.replace(workspace_var, ws) node.run["cmd"] = cmd node.run["restart"] = r_cmd + node.study_label = step # Add the step dag.add_step(step, node, ws, rlimit) @@ -874,6 +899,7 @@ def stage(self): use_tmp=self._use_tmp, dry_run=self._dry_run) dag.add_description(**self.description) dag.log_description() + dag.linker = self.linker # Because we're working within a Study class whose steps have already # been verified to not contain a cycle, we can override the check for diff --git a/maestrowf/maestro.py b/maestrowf/maestro.py index 935f7c64..d8af9836 100644 --- a/maestrowf/maestro.py +++ b/maestrowf/maestro.py @@ -43,11 +43,11 @@ from maestrowf.specification import YAMLSpecification from maestrowf.datastructures.core import Study from maestrowf.datastructures.environment import Variable +from maestrowf.datastructures.core.linker import Linker from maestrowf.utils import \ create_parentdir, create_dictionary, LoggerUtility, make_safe_path, \ start_process - # Program Globals LOGGER = logging.getLogger(__name__) LOG_UTIL = LoggerUtility(LOGGER) @@ -202,6 +202,9 @@ def run_study(args): # Set up the output directory. out_dir = environment.remove("OUTPUT_PATH") + out_name = "" + date_string = time.strftime("%Y%m%d") + time_string = time.strftime("%H%M%S") if args.out: # If out is specified in the args, ignore OUTPUT_PATH. output_path = os.path.abspath(args.out) @@ -234,7 +237,7 @@ def run_study(args): out_name = "{}_{}".format( spec.name.replace(" ", "_"), - time.strftime("%Y%m%d-%H%M%S") + time.strftime(f"{date_string}-{time_string}") ) output_path = make_safe_path(out_dir, *[out_name]) environment.add(Variable("OUTPUT_PATH", output_path)) @@ -298,11 +301,24 @@ def run_study(args): raise ArgumentError(_msg) # Set up the study workspace and configure it for execution. + linker = Linker( + make_links_flag=args.make_links, + link_template=args.link_template, + hashws=args.hashws, + output_name=out_name, + output_path=output_path, + spec_name=spec.name.replace(" ", "_"), + date_string=date_string, + time_string=time_string, + dir_float_format=args.dir_float_format, + pgen=args.pgen, + globals=spec.globals, + ) study.setup_workspace() study.configure_study( throttle=args.throttle, submission_attempts=args.attempts, restart_limit=args.rlimit, use_tmp=args.usetmp, hash_ws=args.hashws, - dry_run=args.dry) + dry_run=args.dry, linker=linker) study.setup_environment() if args.dry: @@ -384,8 +400,12 @@ def setup_argparser(): cancel.set_defaults(func=cancel_study) # subparser for a run subcommand - run = subparsers.add_parser('run', - help="Launch a study based on a specification") + # need manual line breaks to allow formatted template documentation. + run = subparsers.add_parser( + 'run', + help="Launch a study based on a specification", + formatter_class=RawTextHelpFormatter) + run.add_argument("-a", "--attempts", type=int, default=1, help="Maximum number of submission attempts before a " "step is marked as failed. [Default: %(default)d]") @@ -394,23 +414,24 @@ def setup_argparser(): "specify a restart command (0 denotes no limit). " "[Default: %(default)d]") run.add_argument("-t", "--throttle", type=int, default=0, - help="Maximum number of inflight jobs allowed to execute " - "simultaneously (0 denotes not throttling). " + help="Maximum number of inflight jobs allowed to " + "execute simultaneously (0 denotes not throttling). " "[Default: %(default)d]") run.add_argument("-s", "--sleeptime", type=int, default=60, help="Amount of time (in seconds) for the manager to " "wait between job status checks. [Default: %(default)d]") run.add_argument("--dry", action="store_true", default=False, - help="Generate the directory structure and scripts for a " - "study but do not launch it. [Default: %(default)s]") + help="Generate the directory structure and scripts for " + "a study but do not launch it. [Default: %(default)s]") run.add_argument("-p", "--pgen", type=str, help="Path to a Python code file containing a function " - "that returns a custom filled ParameterGenerator " + "that returns a custom filled ParameterGenerator \n" "instance.") run.add_argument("--pargs", type=str, action="append", default=[], - help="A string that represents a single argument to pass " - "a custom parameter generation function. Reuse '--parg' " - "to pass multiple arguments. [Use with '--pgen']") + help="A string that represents a single argument to " + "pass a custom parameter generation function.\n " + "Reuse '--parg' to pass multiple arguments. " + "[Use with '--pgen']") run.add_argument("-o", "--out", type=str, help="Output path to place study in. [NOTE: overrides " "OUTPUT_PATH in the specified specification]") @@ -418,9 +439,27 @@ def setup_argparser(): help="Runs the backend conductor in the foreground " "instead of using nohup. [Default: %(default)s]") run.add_argument("--hashws", action="store_true", default=False, - help="Enable hashing of subdirectories in parameterized " - "studies (NOTE: breaks commands that use parameter labels" - " to search directories). [Default: %(default)s]") + help="Enable hashing of subdirectories in \n" + "parameterized studies (NOTE: breaks commands that use " + "parameter labels to search directories). \n" + " [Default: %(default)s]") + run.add_argument("--dir-float-format", nargs=2, + metavar=( + '(small-exponent-format)', + '(large-exponent-format)'), + default=['{:.2f}', '{:.2e}'], + help=("Format for float parameters when used in " + "directory names [Default: %(default)s].")) + run.add_argument("--make-links", action="store_true", default=False, + help="Automatically make customizable, human-readable " + "links to run directories. [Default: %(default)s]") + run.add_argument( + "--link-template", + type=str, + default=( + "{{output_path}}/../links/{{date}}/" + "run-{{study_index}}/{{combo}}/{{step}}"), + help=Linker.HELP_TEXT) prompt_opts = run.add_mutually_exclusive_group() prompt_opts.add_argument( @@ -433,12 +472,12 @@ def setup_argparser(): # The only required positional argument for 'run' is a specification path. run.add_argument( "specification", type=str, - help="The path to a Study YAML specification that will be loaded and " - "executed.") + help="The path to a Study YAML specification that will be loaded " + "and executed.") run.add_argument( "--usetmp", action="store_true", default=False, - help="Make use of a temporary directory for dumping scripts and other " - "Maestro related files.") + help="Make use of a temporary directory for dumping scripts and " + "other Maestro related files.") run.set_defaults(func=run_study) # subparser for a status subcommand diff --git a/maestrowf/readme_make_links.txt b/maestrowf/readme_make_links.txt new file mode 100644 index 00000000..b10f9bd4 --- /dev/null +++ b/maestrowf/readme_make_links.txt @@ -0,0 +1,41 @@ +# add suffix to var_real abbrev if needed. + +# validate that all variables in template are valid + +# Write yaml index_directory index path +# labels.yaml +# update default template in maestro.py +# update --link-template help in maestro.py +# spell check +# lint flake8 pylint + +# using {{data}} as maestro input and in template should cause error + +# add test for maestro user key/value conflicts with maestro template keyvalue +# add test for maestro user key/value substitutes properly + +# maestro run -s 1 -fg -y --make-links tests/specification/test_specs/link_integration_fast.yml + +NOTE: template must include {{combo}} and {{step}}. +[Default: {{link_directory}}/{{date}}/run-{{INDEX}}/{{combo}}/{{step}} +[Default: {{link_directory}}/{{date}}/{study_name}-{{study_index}}/combo-{{combo_index}}-{{combo}}/{{step}} + +# use variable list below first. raise warning if there is a conflict. + +* {{study_time}} +* {{study_date}} +* {{maestro_variable_names}} # make sure maestro variable names don't conflict with other variables +* {{study_name}} +* {{output_path}} - Parent directory for this maestro study +* {{date}} - Human-readable date (e.g. '2020_07_28') +* {{long_combo}} - Maestro label for a set of parameters, +* {{combo}} - Maestro label for a set of parameters, with reals rounded + (e.g. 'X1.5.X2.5.X3.20') + [maximum length: 255 characters] +* {{step}} - Maestro label for a given step (e.g. 'run') + +{{study_index}} - Unique number for each maestro execution (e.g. '0001') +{{output_path}} / {{study_name}} / {{study_date}} / {{study_time}} + +{{combo_index}} - Unique number for each maestro combination (e.g. '0001') + diff --git a/maestrowf/utils.py b/maestrowf/utils.py index 1aa9bf9d..c52618b3 100644 --- a/maestrowf/utils.py +++ b/maestrowf/utils.py @@ -31,6 +31,7 @@ from collections import OrderedDict import coloredlogs +from jinja2 import Template import logging import os import string @@ -279,6 +280,86 @@ def create_dictionary(list_keyvalues, token=":"): return _dict +def splitall(path): + """ + Split path into a list of component directories. + https://www.oreilly.com/library/view/python-cookbook/0596001673/ch04s16.html + """ + allparts = [] + while 1: + parts = os.path.split(path) + if parts[0] == path: # sentinel for absolute paths + allparts.insert(0, parts[0]) + break + elif parts[1] == path: # sentinel for relative paths + allparts.insert(0, parts[1]) + break + else: + path = parts[0] + allparts.insert(0, parts[1]) + return allparts + + +def next_path(path_pattern): + """ + Finds the next free path in an sequentially named list of files + + e.g. path_pattern = 'file-%s.txt': + + file-1.txt + file-2.txt + file-3.txt + + Runs in log(n) time where n is the number of existing files in sequence + https://stackoverflow.com/questions/17984809/how-do-i-create-a-incrementing-filename-in-python + """ + return next_index_and_path(path_pattern)[1] + + +def next_index_and_path(path_pattern): + """ + Finds the next index number and path in sequentially named list of files + + e.g. path_pattern = 'file-%s.txt': + + file-1.txt + file-2.txt + file-3.txt + + Runs in log(n) time where n is the number of existing files in sequence + https://stackoverflow.com/questions/17984809/how-do-i-create-a-incrementing-filename-in-python + """ + i = 1 + + # First do an exponential search + while os.path.exists(path_pattern % i): + i = i * 2 + + # Result lies somewhere in the interval (i/2..i] + # We call this interval (a..b] and narrow it down until a + 1 = b + a, b = (i // 2, i) + while a + 1 < b: + c = (a + b) // 2 # interval midpoint + a, b = (c, b) if os.path.exists(path_pattern % c) else (a, c) + + return b, path_pattern % b + + +def recursive_render(tpl, values): + """ + Repeat rendering of jinja template until there are no changes. + + https://stackoverflow.com/questions/8862731/jinja-nested-rendering-on-variable-content + """ + prev = tpl + while True: + curr = Template(prev).render(values) + if curr != prev: + prev = curr + else: + return curr + + class LoggerUtility: """Utility class for setting up logging consistently.""" diff --git a/poetry.lock b/poetry.lock index 6367c07b..741c1b6d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -22,7 +22,7 @@ wrapt = ">=1.11,<1.14" [[package]] name = "atomicwrites" -version = "1.4.0" +version = "1.4.1" description = "Atomic file writes." category = "dev" optional = false @@ -30,21 +30,21 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "21.4.0" +version = "22.1.0" description = "Classes Without Boilerplate" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.5" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +tests_no_zope = ["cloudpickle", "pytest-mypy-plugins", "mypy (>=0.900,!=0.940)", "pytest (>=4.3.0)", "pympler", "hypothesis", "coverage[toml] (>=5.0.2)"] +tests = ["cloudpickle", "zope.interface", "pytest-mypy-plugins", "mypy (>=0.900,!=0.940)", "pytest (>=4.3.0)", "pympler", "hypothesis", "coverage[toml] (>=5.0.2)"] +docs = ["sphinx-notfound-page", "zope.interface", "sphinx", "furo"] +dev = ["cloudpickle", "pre-commit", "sphinx-notfound-page", "sphinx", "furo", "zope.interface", "pytest-mypy-plugins", "mypy (>=0.900,!=0.940)", "pytest (>=4.3.0)", "pympler", "hypothesis", "coverage[toml] (>=5.0.2)"] [[package]] name = "babel" -version = "2.10.1" +version = "2.10.3" description = "Internationalization utilities" category = "dev" optional = false @@ -55,7 +55,7 @@ pytz = ">=2015.7" [[package]] name = "bcrypt" -version = "3.2.0" +version = "3.2.2" description = "Modern password hashing for your software and your servers" category = "dev" optional = false @@ -63,7 +63,6 @@ python-versions = ">=3.6" [package.dependencies] cffi = ">=1.1" -six = ">=1.4.1" [package.extras] tests = ["pytest (>=3.2.1,!=3.3.0)"] @@ -71,15 +70,15 @@ typecheck = ["mypy"] [[package]] name = "certifi" -version = "2021.10.8" +version = "2022.6.15" description = "Python package for providing Mozilla's CA Bundle." category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "cffi" -version = "1.15.0" +version = "1.15.1" description = "Foreign Function Interface for Python calling C code." category = "dev" optional = false @@ -109,7 +108,7 @@ unicode_backport = ["unicodedata2"] [[package]] name = "colorama" -version = "0.4.4" +version = "0.4.5" description = "Cross-platform colored terminal text." category = "main" optional = false @@ -138,7 +137,7 @@ optional = false python-versions = "*" [package.extras] -test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] +test = ["hypothesis (==3.55.3)", "flake8 (==3.7.8)"] [[package]] name = "coverage" @@ -156,7 +155,7 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "37.0.1" +version = "37.0.4" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "dev" optional = false @@ -194,7 +193,7 @@ graph = ["objgraph (>=1.7.2)"] [[package]] name = "distlib" -version = "0.3.4" +version = "0.3.5" description = "Distribution utilities" category = "dev" optional = false @@ -210,7 +209,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "fabric" -version = "2.7.0" +version = "2.7.1" description = "High level SSH command execution" category = "dev" optional = false @@ -222,8 +221,8 @@ paramiko = ">=2.4" pathlib2 = "*" [package.extras] -pytest = ["mock (>=2.0.0,<3.0)", "pytest (>=3.2.5,<4.0)"] testing = ["mock (>=2.0.0,<3.0)"] +pytest = ["pytest (>=3.2.5,<4.0)", "mock (>=2.0.0,<3.0)"] [[package]] name = "filelock" @@ -234,8 +233,8 @@ optional = false python-versions = ">=3.6" [package.extras] -docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] -testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] +testing = ["pytest-timeout (>=1.4.2)", "pytest-cov", "pytest (>=4)", "coverage (>=4)", "covdefaults (>=1.2.0)"] +docs = ["sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4.1)", "furo (>=2021.8.17b43)"] [[package]] name = "flake8" @@ -284,7 +283,7 @@ python-versions = ">=3.5" [[package]] name = "imagesize" -version = "1.3.0" +version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" category = "dev" optional = false @@ -303,8 +302,8 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +testing = ["importlib-resources (>=1.3)", "pytest-mypy", "pytest-black (>=0.3.7)", "flufl.flake8", "pyfakefs", "pep517", "packaging", "pytest-enabler (>=1.0.1)", "pytest-cov", "pytest-flake8", "pytest-checkdocs (>=2.4)", "pytest (>=4.6)"] +docs = ["rst.linker (>=1.9)", "jaraco.packaging (>=8.2)", "sphinx"] [[package]] name = "importlib-resources" @@ -318,8 +317,8 @@ python-versions = ">=3.6" zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] +testing = ["pytest-mypy", "pytest-black (>=0.3.7)", "pytest-enabler (>=1.0.1)", "pytest-cov", "pytest-flake8", "pytest-checkdocs (>=2.4)", "pytest (>=6)"] +docs = ["rst.linker (>=1.9)", "jaraco.packaging (>=8.2)", "sphinx"] [[package]] name = "iniconfig" @@ -331,7 +330,7 @@ python-versions = "*" [[package]] name = "invoke" -version = "1.7.0" +version = "1.7.1" description = "Pythonic task execution" category = "dev" optional = false @@ -346,15 +345,15 @@ optional = false python-versions = ">=3.6,<4.0" [package.extras] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] +requirements_deprecated_finder = ["pip-api", "pipreqs"] +pipfile_deprecated_finder = ["requirementslib", "pipreqs"] [[package]] name = "jinja2" version = "3.0.3" description = "A very fast and expressive template engine." -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -378,8 +377,8 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" [package.extras] -format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format_nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] +format_nongpl = ["webcolors (>=1.11)", "uri-template", "rfc3986-validator (>0.1.0)", "rfc3339-validator", "jsonpointer (>1.13)", "isoduration", "idna", "fqdn"] +format = ["webcolors (>=1.11)", "uri-template", "rfc3987", "rfc3339-validator", "jsonpointer (>1.13)", "isoduration", "idna", "fqdn"] [[package]] name = "lazy-object-proxy" @@ -393,7 +392,7 @@ python-versions = ">=3.6" name = "markupsafe" version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -426,7 +425,7 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "paramiko" -version = "2.10.4" +version = "2.11.0" description = "SSH2 protocol library" category = "dev" optional = false @@ -439,10 +438,10 @@ pynacl = ">=1.0.1" six = "*" [package.extras] -all = ["pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "bcrypt (>=3.1.3)", "invoke (>=1.3)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] -ed25519 = ["pynacl (>=1.0.1)", "bcrypt (>=3.1.3)"] -gssapi = ["pyasn1 (>=0.1.7)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] invoke = ["invoke (>=1.3)"] +gssapi = ["pywin32 (>=2.1.8)", "gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)"] +ed25519 = ["bcrypt (>=3.1.3)", "pynacl (>=1.0.1)"] +all = ["pywin32 (>=2.1.8)", "gssapi (>=1.4.1)", "invoke (>=1.3)", "bcrypt (>=3.1.3)", "pynacl (>=1.0.1)", "pyasn1 (>=0.1.7)"] [[package]] name = "pathlib2" @@ -464,8 +463,8 @@ optional = false python-versions = ">=3.6" [package.extras] -docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +test = ["pytest-mock (>=3.6)", "pytest-cov (>=2.7)", "pytest (>=6)", "appdirs (==1.4.4)"] +docs = ["sphinx-autodoc-typehints (>=1.12)", "proselint (>=0.10.2)", "furo (>=2021.7.5b38)", "Sphinx (>=4)"] [[package]] name = "pluggy" @@ -479,8 +478,8 @@ python-versions = ">=3.6" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +testing = ["pytest-benchmark", "pytest"] +dev = ["tox", "pre-commit"] [[package]] name = "pre-commit" @@ -595,7 +594,7 @@ optional = false python-versions = ">=3.6" [package.extras] -diagrams = ["jinja2", "railroad-diagrams"] +diagrams = ["railroad-diagrams", "jinja2"] [[package]] name = "pyreadline" @@ -641,7 +640,7 @@ py = ">=1.8.2" tomli = ">=1.0.0" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +testing = ["xmlschema", "requests", "pygments (>=2.7.2)", "nose", "mock", "hypothesis (>=3.56)", "argcomplete"] [[package]] name = "pytest-cov" @@ -656,11 +655,11 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] +testing = ["virtualenv", "pytest-xdist", "six", "process-tests", "hunter", "fields"] [[package]] name = "pytz" -version = "2022.1" +version = "2022.2" description = "World timezone definitions, modern and historical" category = "dev" optional = false @@ -689,8 +688,8 @@ idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} urllib3 = ">=1.21.1,<1.27" [package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +socks = ["win-inet-pton", "PySocks (>=1.5.6,!=1.5.7)"] [[package]] name = "rich" @@ -753,9 +752,9 @@ sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] +test = ["typed-ast", "cython", "html5lib", "pytest-cov", "pytest"] +lint = ["types-requests", "types-pkg-resources", "types-typed-ast", "docutils-stubs", "mypy (>=0.920)", "isort", "flake8 (>=3.5.0)"] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.920)", "docutils-stubs", "types-typed-ast", "types-pkg-resources", "types-requests"] -test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] [[package]] name = "sphinx-rtd-theme" @@ -770,7 +769,7 @@ docutils = "<0.18" sphinx = ">=1.6" [package.extras] -dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] +dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client"] [[package]] name = "sphinxcontrib-applehelp" @@ -781,8 +780,8 @@ optional = false python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-devhelp" @@ -793,8 +792,8 @@ optional = false python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-htmlhelp" @@ -805,8 +804,8 @@ optional = false python-versions = ">=3.6" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] -test = ["pytest", "html5lib"] +test = ["html5lib", "pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-jsmath" @@ -817,7 +816,7 @@ optional = false python-versions = ">=3.5" [package.extras] -test = ["pytest", "flake8", "mypy"] +test = ["mypy", "flake8", "pytest"] [[package]] name = "sphinxcontrib-qthelp" @@ -828,8 +827,8 @@ optional = false python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-serializinghtml" @@ -840,16 +839,16 @@ optional = false python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "tabulate" -version = "0.8.9" +version = "0.8.10" description = "Pretty-print tabular data" category = "main" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] widechars = ["wcwidth"] @@ -872,7 +871,7 @@ python-versions = ">=3.6" [[package]] name = "tox" -version = "3.25.0" +version = "3.25.1" description = "tox is a generic virtualenv management and test command line tool" category = "dev" optional = false @@ -890,8 +889,8 @@ toml = ">=0.9.4" virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" [package.extras] -docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] -testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "psutil (>=5.6.1)", "pathlib2 (>=2.3.3)"] +testing = ["pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest-randomly (>=1.0.0)", "pytest-mock (>=1.10.0)", "pytest-cov (>=2.5.1)", "pytest (>=4.0.0)", "freezegun (>=0.3.11)", "flaky (>=3.4.0)"] +docs = ["towncrier (>=18.5.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "sphinx (>=2.0.0)", "pygments-github-lexers (>=0.0.5)"] [[package]] name = "tox-pyenv" @@ -917,7 +916,7 @@ tox = ">=2.0" [[package]] name = "typed-ast" -version = "1.5.3" +version = "1.5.4" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false @@ -933,24 +932,24 @@ python-versions = "*" [[package]] name = "urllib3" -version = "1.26.9" +version = "1.26.11" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] -brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +secure = ["ipaddress", "certifi", "idna (>=2.0.0)", "cryptography (>=1.3.4)", "pyOpenSSL (>=0.14)"] +brotli = ["brotlipy (>=0.6.0)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] [[package]] name = "virtualenv" -version = "20.14.1" +version = "20.16.2" description = "Virtual Python Environment builder" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" [package.dependencies] distlib = ">=0.3.1,<1" @@ -958,11 +957,10 @@ filelock = ">=3.2,<4" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} platformdirs = ">=2,<3" -six = ">=1.9.0,<2" [package.extras] -docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] -testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] +testing = ["pytest-timeout (>=1)", "pytest-randomly (>=1)", "pytest-mock (>=2)", "pytest-freezegun (>=0.4.1)", "pytest-env (>=0.6.2)", "pytest (>=4)", "packaging (>=20.0)", "flaky (>=3)", "coverage-enable-subprocess (>=1)", "coverage (>=4)"] +docs = ["towncrier (>=21.3)", "sphinx-rtd-theme (>=0.4.3)", "sphinx-argparse (>=0.2.5)", "sphinx (>=3)", "proselint (>=0.10.2)"] [[package]] name = "wrapt" @@ -981,13 +979,13 @@ optional = false python-versions = ">=3.6" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +testing = ["pytest-mypy", "pytest-black (>=0.3.7)", "func-timeout", "jaraco.itertools", "pytest-enabler (>=1.0.1)", "pytest-cov", "pytest-flake8", "pytest-checkdocs (>=2.4)", "pytest (>=4.6)"] +docs = ["rst.linker (>=1.9)", "jaraco.packaging (>=8.2)", "sphinx"] [metadata] lock-version = "1.1" python-versions = ">=3.6,<4.0" -content-hash = "826f7979684a0fc8de2304bbf0a7bbddedae988f4eb9ced098511fc32c40252e" +content-hash = "7fe9513687bdea22e0e8d2d1a0840b522bf47bb92c14cd9a7c27663a024f004d" [metadata.files] alabaster = [ @@ -999,84 +997,98 @@ astroid = [ {file = "astroid-2.9.0.tar.gz", hash = "sha256:5939cf55de24b92bda00345d4d0659d01b3c7dafb5055165c330bc7c568ba273"}, ] atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, + {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, ] attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, + {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, + {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, ] babel = [ - {file = "Babel-2.10.1-py3-none-any.whl", hash = "sha256:3f349e85ad3154559ac4930c3918247d319f21910d5ce4b25d439ed8693b98d2"}, - {file = "Babel-2.10.1.tar.gz", hash = "sha256:98aeaca086133efb3e1e2aad0396987490c8425929ddbcfe0550184fdc54cd13"}, + {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, + {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, ] bcrypt = [ - {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b589229207630484aefe5899122fb938a5b017b0f4349f769b8c13e78d99a8fd"}, - {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, - {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, - {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, - {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, - {file = "bcrypt-3.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a0584a92329210fcd75eb8a3250c5a941633f8bfaf2a18f81009b097732839b7"}, - {file = "bcrypt-3.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:56e5da069a76470679f312a7d3d23deb3ac4519991a0361abc11da837087b61d"}, - {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, - {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, - {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, + {file = "bcrypt-3.2.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:7180d98a96f00b1050e93f5b0f556e658605dd9f524d0b0e68ae7944673f525e"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:61bae49580dce88095d669226d5076d0b9d927754cedbdf76c6c9f5099ad6f26"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88273d806ab3a50d06bc6a2fc7c87d737dd669b76ad955f449c43095389bc8fb"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6d2cb9d969bfca5bc08e45864137276e4c3d3d7de2b162171def3d188bf9d34a"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b02d6bfc6336d1094276f3f588aa1225a598e27f8e3388f4db9948cb707b521"}, + {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2c46100e315c3a5b90fdc53e429c006c5f962529bc27e1dfd656292c20ccc40"}, + {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7d9ba2e41e330d2af4af6b1b6ec9e6128e91343d0b4afb9282e54e5508f31baa"}, + {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cd43303d6b8a165c29ec6756afd169faba9396a9472cdff753fe9f19b96ce2fa"}, + {file = "bcrypt-3.2.2-cp36-abi3-win32.whl", hash = "sha256:4e029cef560967fb0cf4a802bcf4d562d3d6b4b1bf81de5ec1abbe0f1adb027e"}, + {file = "bcrypt-3.2.2-cp36-abi3-win_amd64.whl", hash = "sha256:7ff2069240c6bbe49109fe84ca80508773a904f5a8cb960e02a977f7f519b129"}, + {file = "bcrypt-3.2.2.tar.gz", hash = "sha256:433c410c2177057705da2a9f2cd01dd157493b2a7ac14c8593a16b3dab6b6bfb"}, ] certifi = [ - {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, - {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, + {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, + {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, ] cffi = [ - {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, - {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, - {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, - {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, - {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, - {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, - {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, - {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, - {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, - {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, - {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, - {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, - {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, - {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, - {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, - {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, - {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, - {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, - {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, - {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, - {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, - {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, - {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, - {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, - {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, ] cfgv = [ {file = "cfgv-3.0.0-py2.py3-none-any.whl", hash = "sha256:f22b426ed59cd2ab2b54ff96608d846c33dfb8766a67f0b4a6ce130ce244414f"}, @@ -1087,8 +1099,8 @@ charset-normalizer = [ {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, ] colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] coloredlogs = [ {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, @@ -1148,28 +1160,28 @@ coverage = [ {file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"}, ] cryptography = [ - {file = "cryptography-37.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:74b55f67f4cf026cb84da7a1b04fc2a1d260193d4ad0ea5e9897c8b74c1e76ac"}, - {file = "cryptography-37.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:0db5cf21bd7d092baacb576482b0245102cea2d3cf09f09271ce9f69624ecb6f"}, - {file = "cryptography-37.0.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:faf0f5456c059c7b1c29441bdd5e988f0ba75bdc3eea776520d8dcb1e30e1b5c"}, - {file = "cryptography-37.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:06bfafa6e53ccbfb7a94be4687b211a025ce0625e3f3c60bb15cd048a18f3ed8"}, - {file = "cryptography-37.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf585476fcbcd37bed08072e8e2db3954ce1bfc68087a2dc9c19cfe0b90979ca"}, - {file = "cryptography-37.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4daf890e674d191757d8d7d60dc3a29c58c72c7a76a05f1c0a326013f47e8b"}, - {file = "cryptography-37.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:ae1cd29fbe6b716855454e44f4bf743465152e15d2d317303fe3b58ee9e5af7a"}, - {file = "cryptography-37.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:451aaff8b8adf2dd0597cbb1fdcfc8a7d580f33f843b7cce75307a7f20112dd8"}, - {file = "cryptography-37.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:1858eff6246bb8bbc080eee78f3dd1528739e3f416cba5f9914e8631b8df9871"}, - {file = "cryptography-37.0.1-cp36-abi3-win32.whl", hash = "sha256:e69a0e36e62279120e648e787b76d79b41e0f9e86c1c636a4f38d415595c722e"}, - {file = "cryptography-37.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:a18ff4bfa9d64914a84d7b06c46eb86e0cc03113470b3c111255aceb6dcaf81d"}, - {file = "cryptography-37.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cce90609e01e1b192fae9e13665058ab46b2ea53a3c05a3ea74a3eb8c3af8857"}, - {file = "cryptography-37.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:c4a58eeafbd7409054be41a377e726a7904a17c26f45abf18125d21b1215b08b"}, - {file = "cryptography-37.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:618391152147a1221c87b1b0b7f792cafcfd4b5a685c5c72eeea2ddd29aeceff"}, - {file = "cryptography-37.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ceae26f876aabe193b13a0c36d1bb8e3e7e608d17351861b437bd882f617e9f"}, - {file = "cryptography-37.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:930b829e8a2abaf43a19f38277ae3c5e1ffcf547b936a927d2587769ae52c296"}, - {file = "cryptography-37.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:58021d6e9b1d88b1105269d0da5e60e778b37dfc0e824efc71343dd003726831"}, - {file = "cryptography-37.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b1ee5c82cf03b30f6ae4e32d2bcb1e167ef74d6071cbb77c2af30f101d0b360b"}, - {file = "cryptography-37.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f095988548ec5095e3750cdb30e6962273d239b1998ba1aac66c0d5bee7111c1"}, - {file = "cryptography-37.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:125702572be12bcd318e3a14e9e70acd4be69a43664a75f0397e8650fe3c6cc3"}, - {file = "cryptography-37.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:315af6268de72bcfa0bb3401350ce7d921f216e6b60de12a363dad128d9d459f"}, - {file = "cryptography-37.0.1.tar.gz", hash = "sha256:d610d0ee14dd9109006215c7c0de15eee91230b70a9bce2263461cf7c3720b83"}, + {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"}, + {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280"}, + {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3"}, + {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59"}, + {file = "cryptography-37.0.4-cp36-abi3-win32.whl", hash = "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157"}, + {file = "cryptography-37.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327"}, + {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b"}, + {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"}, + {file = "cryptography-37.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67"}, + {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d"}, + {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282"}, + {file = "cryptography-37.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa"}, + {file = "cryptography-37.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441"}, + {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596"}, + {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a"}, + {file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"}, + {file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"}, ] dataclasses = [ {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, @@ -1180,16 +1192,16 @@ dill = [ {file = "dill-0.3.4.zip", hash = "sha256:9f9734205146b2b353ab3fec9af0070237b6ddae78452af83d2fca84d739e675"}, ] distlib = [ - {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, - {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, + {file = "distlib-0.3.5-py2.py3-none-any.whl", hash = "sha256:b710088c59f06338ca514800ad795a132da19fda270e3ce4affc74abf955a26c"}, + {file = "distlib-0.3.5.tar.gz", hash = "sha256:a7f75737c70be3b25e2bee06288cec4e4c221de18455b2dd037fe2a795cab2fe"}, ] docutils = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, ] fabric = [ - {file = "fabric-2.7.0-py2.py3-none-any.whl", hash = "sha256:e8bfe851719a88be24f40ad7e96ac5bf023ce1af650b561d7641ed1eaed55fe5"}, - {file = "fabric-2.7.0.tar.gz", hash = "sha256:0bf797a68c4b389720dc4dd6181497a58c41ed762e283d9e3c1b0148b32a9aff"}, + {file = "fabric-2.7.1-py2.py3-none-any.whl", hash = "sha256:7610362318ef2d391cc65d4befb684393975d889ed5720f23499394ec0e136fa"}, + {file = "fabric-2.7.1.tar.gz", hash = "sha256:76f8fef59cf2061dbd849bbce4fe49bdd820884385004b0ca59136ac3db129e4"}, ] filelock = [ {file = "filelock-3.4.1-py3-none-any.whl", hash = "sha256:a4bc51381e01502a30e9f06dd4fa19a1712eab852b6fb0f84fd7cce0793d8ca3"}, @@ -1212,8 +1224,8 @@ idna = [ {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] imagesize = [ - {file = "imagesize-1.3.0-py2.py3-none-any.whl", hash = "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c"}, - {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"}, + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] importlib-metadata = [ {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, @@ -1228,8 +1240,8 @@ iniconfig = [ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] invoke = [ - {file = "invoke-1.7.0-py3-none-any.whl", hash = "sha256:a5159fc63dba6ca2a87a1e33d282b99cea69711b03c64a35bb4e1c53c6c4afa0"}, - {file = "invoke-1.7.0.tar.gz", hash = "sha256:e332e49de40463f2016315f51df42313855772be86435686156bc18f45b5cc6c"}, + {file = "invoke-1.7.1-py3-none-any.whl", hash = "sha256:2dc975b4f92be0c0a174ad2d063010c8a1fdb5e9389d69871001118b4fcac4fb"}, + {file = "invoke-1.7.1.tar.gz", hash = "sha256:7b6deaf585eee0a848205d0b8c0014b9bf6f287a8eb798818a642dff1df14b19"}, ] isort = [ {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, @@ -1366,8 +1378,8 @@ packaging = [ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] paramiko = [ - {file = "paramiko-2.10.4-py2.py3-none-any.whl", hash = "sha256:3c9ed6084f4b671ab66dc3c729092d32d96c3258f1426071301cb33654b09027"}, - {file = "paramiko-2.10.4.tar.gz", hash = "sha256:3d2e650b6812ce6d160abff701d6ef4434ec97934b13e95cf1ad3da70ffb5c58"}, + {file = "paramiko-2.11.0-py2.py3-none-any.whl", hash = "sha256:655f25dc8baf763277b933dfcea101d636581df8d6b9774d1fb653426b72c270"}, + {file = "paramiko-2.11.0.tar.gz", hash = "sha256:003e6bee7c034c21fbb051bf83dc0a9ee4106204dd3c53054c71452cc4ec3938"}, ] pathlib2 = [ {file = "pathlib2-2.3.7.post1-py2.py3-none-any.whl", hash = "sha256:5266a0fd000452f1b3467d782f079a4343c63aaa119221fbdc4e39577489ca5b"}, @@ -1470,8 +1482,8 @@ pytest-cov = [ {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, ] pytz = [ - {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, - {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, + {file = "pytz-2022.2-py2.py3-none-any.whl", hash = "sha256:d9b245e63af49c4e51afdec5402f56b99c0cb483a84a12bb8b7db980386baade"}, + {file = "pytz-2022.2.tar.gz", hash = "sha256:bc824559e43e8ab983426a49525079d186b25372ff63aa3430ccd527d95edc3a"}, ] pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, @@ -1557,8 +1569,9 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] tabulate = [ - {file = "tabulate-0.8.9-py3-none-any.whl", hash = "sha256:d7c013fe7abbc5e491394e10fa845f8f32fe54f8dc60c6622c6cf482d25d47e4"}, - {file = "tabulate-0.8.9.tar.gz", hash = "sha256:eb1d13f25760052e8931f2ef80aaf6045a6cceb47514db8beab24cded16f13a7"}, + {file = "tabulate-0.8.10-py3-none-any.whl", hash = "sha256:0ba055423dbaa164b9e456abe7920c5e8ed33fcc16f6d1b2f2d152c8e1e8b4fc"}, + {file = "tabulate-0.8.10-py3.8.egg", hash = "sha256:436f1c768b424654fce8597290d2764def1eea6a77cfa5c33be00b1bc0f4f63d"}, + {file = "tabulate-0.8.10.tar.gz", hash = "sha256:6c57f3f3dd7ac2782770155f3adb2db0b1a269637e42f27599925e64b114f519"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, @@ -1569,8 +1582,8 @@ tomli = [ {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, ] tox = [ - {file = "tox-3.25.0-py2.py3-none-any.whl", hash = "sha256:0805727eb4d6b049de304977dfc9ce315a1938e6619c3ab9f38682bb04662a5a"}, - {file = "tox-3.25.0.tar.gz", hash = "sha256:37888f3092aa4e9f835fc8cc6dadbaaa0782651c41ef359e3a5743fcb0308160"}, + {file = "tox-3.25.1-py2.py3-none-any.whl", hash = "sha256:c38e15f4733683a9cc0129fba078633e07eb0961f550a010ada879e95fb32632"}, + {file = "tox-3.25.1.tar.gz", hash = "sha256:c138327815f53bc6da4fe56baec5f25f00622ae69ef3fe4e1e385720e22486f9"}, ] tox-pyenv = [ {file = "tox-pyenv-1.1.0.tar.gz", hash = "sha256:916c2213577aec0b3b5452c5bfb32fd077f3a3196f50a81ad57d7ef3fc2599e4"}, @@ -1581,30 +1594,30 @@ tox-travis = [ {file = "tox_travis-0.12-py2.py3-none-any.whl", hash = "sha256:442c96b078333c94e272d0e90e4582e35e0529ea98bcd2f7f96053d690c4e7a4"}, ] typed-ast = [ - {file = "typed_ast-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ad3b48cf2b487be140072fb86feff36801487d4abb7382bb1929aaac80638ea"}, - {file = "typed_ast-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:542cd732351ba8235f20faa0fc7398946fe1a57f2cdb289e5497e1e7f48cfedb"}, - {file = "typed_ast-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc2c11ae59003d4a26dda637222d9ae924387f96acae9492df663843aefad55"}, - {file = "typed_ast-1.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd5df1313915dbd70eaaa88c19030b441742e8b05e6103c631c83b75e0435ccc"}, - {file = "typed_ast-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:e34f9b9e61333ecb0f7d79c21c28aa5cd63bec15cb7e1310d7d3da6ce886bc9b"}, - {file = "typed_ast-1.5.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f818c5b81966d4728fec14caa338e30a70dfc3da577984d38f97816c4b3071ec"}, - {file = "typed_ast-1.5.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3042bfc9ca118712c9809201f55355479cfcdc17449f9f8db5e744e9625c6805"}, - {file = "typed_ast-1.5.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4fff9fdcce59dc61ec1b317bdb319f8f4e6b69ebbe61193ae0a60c5f9333dc49"}, - {file = "typed_ast-1.5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8e0b8528838ffd426fea8d18bde4c73bcb4167218998cc8b9ee0a0f2bfe678a6"}, - {file = "typed_ast-1.5.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ef1d96ad05a291f5c36895d86d1375c0ee70595b90f6bb5f5fdbee749b146db"}, - {file = "typed_ast-1.5.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed44e81517364cb5ba367e4f68fca01fba42a7a4690d40c07886586ac267d9b9"}, - {file = "typed_ast-1.5.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f60d9de0d087454c91b3999a296d0c4558c1666771e3460621875021bf899af9"}, - {file = "typed_ast-1.5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9e237e74fd321a55c90eee9bc5d44be976979ad38a29bbd734148295c1ce7617"}, - {file = "typed_ast-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ee852185964744987609b40aee1d2eb81502ae63ee8eef614558f96a56c1902d"}, - {file = "typed_ast-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:27e46cdd01d6c3a0dd8f728b6a938a6751f7bd324817501c15fb056307f918c6"}, - {file = "typed_ast-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d64dabc6336ddc10373922a146fa2256043b3b43e61f28961caec2a5207c56d5"}, - {file = "typed_ast-1.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8cdf91b0c466a6c43f36c1964772918a2c04cfa83df8001ff32a89e357f8eb06"}, - {file = "typed_ast-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:9cc9e1457e1feb06b075c8ef8aeb046a28ec351b1958b42c7c31c989c841403a"}, - {file = "typed_ast-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e20d196815eeffb3d76b75223e8ffed124e65ee62097e4e73afb5fec6b993e7a"}, - {file = "typed_ast-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:37e5349d1d5de2f4763d534ccb26809d1c24b180a477659a12c4bde9dd677d74"}, - {file = "typed_ast-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f1a27592fac87daa4e3f16538713d705599b0a27dfe25518b80b6b017f0a6d"}, - {file = "typed_ast-1.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8831479695eadc8b5ffed06fdfb3e424adc37962a75925668deeb503f446c0a3"}, - {file = "typed_ast-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:20d5118e494478ef2d3a2702d964dae830aedd7b4d3b626d003eea526be18718"}, - {file = "typed_ast-1.5.3.tar.gz", hash = "sha256:27f25232e2dd0edfe1f019d6bfaaf11e86e657d9bdb7b0956db95f560cceb2b3"}, + {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, + {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, + {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, + {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, + {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, + {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, + {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, + {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, + {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, + {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, ] typing-extensions = [ {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, @@ -1612,12 +1625,12 @@ typing-extensions = [ {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, ] urllib3 = [ - {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, - {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, + {file = "urllib3-1.26.11-py2.py3-none-any.whl", hash = "sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc"}, + {file = "urllib3-1.26.11.tar.gz", hash = "sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a"}, ] virtualenv = [ - {file = "virtualenv-20.14.1-py2.py3-none-any.whl", hash = "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a"}, - {file = "virtualenv-20.14.1.tar.gz", hash = "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"}, + {file = "virtualenv-20.16.2-py2.py3-none-any.whl", hash = "sha256:635b272a8e2f77cb051946f46c60a54ace3cb5e25568228bd6b57fc70eca9ff3"}, + {file = "virtualenv-20.16.2.tar.gz", hash = "sha256:0ef5be6d07181946891f5abc8047fda8bc2f0b4b9bf222c64e6e8963baee76db"}, ] wrapt = [ {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, diff --git a/pyproject.toml b/pyproject.toml index 50b63ec4..3d85e41d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ pyyaml = ">=4.2b1" rich = "*" six = "*" tabulate = "*" +jinja2 = "*" [tool.poetry.dev-dependencies] fabric = "*" diff --git a/tests/conftest.py b/tests/conftest.py index 82b8fe21..09a88cf7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,11 @@ import pytest import os +import tempfile +@pytest.fixture +def temp_dir(): + # return lambda: "/tmp/rdisk" + return tempfile.mkdtemp @pytest.fixture def spec_path(): diff --git a/tests/specification/test_specs/link_integration.yml b/tests/specification/test_specs/link_integration.yml new file mode 100644 index 00000000..15069b1e --- /dev/null +++ b/tests/specification/test_specs/link_integration.yml @@ -0,0 +1,89 @@ +description: + name: link_integration_test + description: A link integration test, modified from run_lulesh + +env: + variables: + OUTPUT_PATH: ./output + + labels: + outfile: $(SIZE.label).$(ITERATIONS.label).log + +batch: + shell: /bin/bash + +study: + + - name: start + description: Step with no variables. + run: + cmd: | + echo start > out.txt + depends: [] + + - name: echo + description: Echo inputs. + run: + cmd: | + echo size $(SIZE) iter $(ITERATIONS) > $(outfile) + depends: [start] + + - name: test-directory-hashing + description: Test directory hashing. + run: + cmd: + echo $(VAR1) - $(VAR2) - $(VAR3) - $(VAR4) > out.txt + depends: [] + + - name: post-process-echo + description: Post process all results. + run: + cmd: | + echo "Unparameterized step with Parameter Independent dependencies." >> out.log + echo $(echo.workspace) >> out.log + ls $(echo.workspace) >> out.log + depends: [echo_*] + + - name: post-process-echo-trials + description: Post process all results. + run: + cmd: | + echo "Parameterized step that has Parameter Independent dependencies" >> out.log + echo "TRIAL = $(TRIAL)" >> out.log + echo $(echo.workspace) >> out.log + ls $(echo.workspace) >> out.log + depends: [echo_*] + + - name: post-process-echo-size + description: Post process all results. + run: + cmd: | + echo "Parameterized step that has Parameter Independent dependencies" >> out.log + echo "SIZE = $(SIZE)" >> out.log + echo $(echo.workspace) >> out.log + ls $(echo.workspace) | grep $(SIZE.label) >> out.log + depends: [echo_*] + +global.parameters: + TRIAL: + values : [1, 2, 3 ] + label : TRIAL.%% + SIZE: + values : [10, 10, 10 ] + label : SIZE.%% + ITERATIONS: + values : [10, 20, 30 ] + label : ITER.%% + VAR1: + values : [ 0.3874309076, 0.3585516934, 0.8368954934 ] + label : VAR1.%% + VAR2: + values : [ 0.7520078045, 0.1707261687, 0.7296721416 ] + label : VAR2.%% + VAR3: + values : [ 0.7187181590, 0.4406243939, 0.8958327389 ] + label : VAR3.%% + VAR4: + values : [ 0.5491000152, 0.3920015696, 0.1895291838 ] + label : VAR4.%% + \ No newline at end of file diff --git a/tests/specification/test_specs/link_integration_fast.yml b/tests/specification/test_specs/link_integration_fast.yml new file mode 100644 index 00000000..8151f801 --- /dev/null +++ b/tests/specification/test_specs/link_integration_fast.yml @@ -0,0 +1,33 @@ +description: + name: link_integration_test + description: A link integration test, modified from run_lulesh + +env: + variables: + OUTPUT_PATH: ./output + +batch: + shell: /bin/bash + +study: + - name: test-directory-hashing + description: Test directory hashing. + run: + cmd: + echo $(VAR1) - $(VAR2) - $(VAR3) - $(VAR4) > out.txt + depends: [] + +global.parameters: + VAR1: + values : [ 0.3874309076, 0.3585516934, 0.8368954934 ] + label : VAR1.%% + VAR2: + values : [ 0.7520078045, 0.1707261687, 0.7296721416 ] + label : VAR2.%% + VAR3: + values : [ 0.7187181590, 0.4406243939, 0.8958327389 ] + label : VAR3.%% + VAR4: + values : [ 0.5491000152, 0.3920015696, 0.1895291838 ] + label : VAR4.%% + \ No newline at end of file diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 00000000..1151c05b --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,326 @@ +""" +Test module for testing full maestro integration. +""" + +import subprocess + +import os +import pytest +import shutil +import unittest +from itertools import permutations + + +class TestLinkIntegration(unittest.TestCase): + """Test link integration""" + + @pytest.fixture(autouse=True) + def spec_path(self, spec_path): + self.spec_path = spec_path + + @pytest.fixture(autouse=True) + def temp_dir(self, temp_dir): + self.temp_dir = temp_dir + + # @pytest.fixture(autouse=True) + def setUp(self): + self.tmp_dir = self.temp_dir() + + def tearDown(self): + shutil.rmtree(self.tmp_dir, ignore_errors=True) + + # keys and values must have the same number of '/'s + LINKS_0001_test_study_index = { + "run-0001/VAR1.0.36.VAR2.0.17.VAR3.0.44.VAR4.0.39/test-directory-hashing": + "test-directory-hashing/VAR1.0.3585516934.VAR2.0.1707261687.VAR3.0.4406243939.VAR4.0.3920015696", + "run-0001/VAR1.0.39.VAR2.0.75.VAR3.0.72.VAR4.0.55/test-directory-hashing": + "test-directory-hashing/VAR1.0.3874309076.VAR2.0.7520078045.VAR3.0.718718159.VAR4.0.5491000152", + "run-0001/VAR1.0.84.VAR2.0.73.VAR3.0.90.VAR4.0.19/test-directory-hashing": + "test-directory-hashing/VAR1.0.8368954934.VAR2.0.7296721416.VAR3.0.8958327389.VAR4.0.1895291838", + } + LINKS_0002_test_study_index = { + "run-0001/VAR1.0.36.VAR2.0.17.VAR3.0.44.VAR4.0.39/test-directory-hashing": + "test-directory-hashing/VAR1.0.3585516934.VAR2.0.1707261687.VAR3.0.4406243939.VAR4.0.3920015696", + "run-0001/VAR1.0.39.VAR2.0.75.VAR3.0.72.VAR4.0.55/test-directory-hashing": + "test-directory-hashing/VAR1.0.3874309076.VAR2.0.7520078045.VAR3.0.718718159.VAR4.0.5491000152", + "run-0001/VAR1.0.84.VAR2.0.73.VAR3.0.90.VAR4.0.19/test-directory-hashing": + "test-directory-hashing/VAR1.0.8368954934.VAR2.0.7296721416.VAR3.0.8958327389.VAR4.0.1895291838", + "run-0002/VAR1.0.359.VAR2.0.171.VAR3.0.441.VAR4.0.392/test-directory-hashing": + "test-directory-hashing/VAR1.0.3585516934.VAR2.0.1707261687.VAR3.0.4406243939.VAR4.0.3920015696", + "run-0002/VAR1.0.387.VAR2.0.752.VAR3.0.719.VAR4.0.549/test-directory-hashing": + "test-directory-hashing/VAR1.0.3874309076.VAR2.0.7520078045.VAR3.0.718718159.VAR4.0.5491000152", + "run-0002/VAR1.0.837.VAR2.0.730.VAR3.0.896.VAR4.0.190/test-directory-hashing": + "test-directory-hashing/VAR1.0.8368954934.VAR2.0.7296721416.VAR3.0.8958327389.VAR4.0.1895291838", + } + LINKS_0001_test_combo_index = { + "link_integration_test-0001/combo-0001-VAR1.0.36.VAR2.0.17.VAR3.0.44.VAR4.0.39/test-directory-hashing": + "test-directory-hashing/VAR1.0.3585516934.VAR2.0.1707261687.VAR3.0.4406243939.VAR4.0.3920015696", + "link_integration_test-0001/combo-0001-VAR1.0.39.VAR2.0.75.VAR3.0.72.VAR4.0.55/test-directory-hashing": + "test-directory-hashing/VAR1.0.3874309076.VAR2.0.7520078045.VAR3.0.718718159.VAR4.0.5491000152", + "link_integration_test-0001/combo-0001-VAR1.0.84.VAR2.0.73.VAR3.0.90.VAR4.0.19/test-directory-hashing": + "test-directory-hashing/VAR1.0.8368954934.VAR2.0.7296721416.VAR3.0.8958327389.VAR4.0.1895291838", + } + LINKS_0001_test_combo_var_index = { + "VAR1.0.36/VAR2.0.17/VAR3.0.44/VAR4.0.39/test-directory-hashing": + "test-directory-hashing/VAR1.0.3585516934.VAR2.0.1707261687.VAR3.0.4406243939.VAR4.0.3920015696", + "VAR1.0.39/VAR2.0.75/VAR3.0.72/VAR4.0.55/test-directory-hashing": + "test-directory-hashing/VAR1.0.3874309076.VAR2.0.7520078045.VAR3.0.718718159.VAR4.0.5491000152", + "VAR1.0.84/VAR2.0.73/VAR3.0.90/VAR4.0.19/test-directory-hashing": + "test-directory-hashing/VAR1.0.8368954934.VAR2.0.7296721416.VAR3.0.8958327389.VAR4.0.1895291838", + } + LINKS_0001_test_hashed_combo_index = { + "link_integration_test-0001/combo-0001-VAR1.0.36.VAR2.0.17.VAR3.0.44.VAR4.0.39/test-directory-hashing": + "test-directory-hashing/6df755a183b9e4329be759a718a54f80", + "link_integration_test-0001/combo-0001-VAR1.0.39.VAR2.0.75.VAR3.0.72.VAR4.0.55/test-directory-hashing": + "test-directory-hashing/7eb63184e109da27e172188be6e32598", + "link_integration_test-0001/combo-0001-VAR1.0.84.VAR2.0.73.VAR3.0.90.VAR4.0.19/test-directory-hashing": + "test-directory-hashing/c74742ecea5a2b58e67350b5a1e0a234", + } + LINKS_0001_test_all_links = { + "run-0001/ITER.10.SIZE.10/echo": + "echo/ITER.10.SIZE.10", + "run-0001/ITER.20.SIZE.10/echo": + "echo/ITER.20.SIZE.10", + "run-0001/ITER.30.SIZE.10/echo": + "echo/ITER.30.SIZE.10", + "run-0001/SIZE.10/post-process-echo-size": + "post-process-echo-size/SIZE.10", + "run-0001/TRIAL.1/post-process-echo-trials": + "post-process-echo-trials/TRIAL.1", + "run-0001/TRIAL.2/post-process-echo-trials": + "post-process-echo-trials/TRIAL.2", + "run-0001/TRIAL.3/post-process-echo-trials": + "post-process-echo-trials/TRIAL.3", + "run-0001/VAR1.0.36.VAR2.0.17.VAR3.0.44.VAR4.0.39/test-directory-hashing": + "test-directory-hashing/VAR1.0.3585516934.VAR2.0.1707261687.VAR3.0.4406243939.VAR4.0.3920015696", + "run-0001/VAR1.0.39.VAR2.0.75.VAR3.0.72.VAR4.0.55/test-directory-hashing": + "test-directory-hashing/VAR1.0.3874309076.VAR2.0.7520078045.VAR3.0.718718159.VAR4.0.5491000152", + "run-0001/VAR1.0.84.VAR2.0.73.VAR3.0.90.VAR4.0.19/test-directory-hashing": + "test-directory-hashing/VAR1.0.8368954934.VAR2.0.7296721416.VAR3.0.8958327389.VAR4.0.1895291838", + "run-0001/all_records/post-process-echo": + "post-process-echo", + "run-0001/all_records/start": + "start", + } + LINKS_0001_test_all_hashed_links = { + "run-0001/ITER.10.SIZE.10/echo": + "echo/d0d800ff9711b3dc32cb29136142d7f8", + "run-0001/ITER.20.SIZE.10/echo": + "echo/67570ad31cf4fb34283f26ae4571e372", + "run-0001/ITER.30.SIZE.10/echo": + "echo/b847acec2511954db96eceb1685ee475", + "run-0001/SIZE.10/post-process-echo-size": + "post-process-echo-size/a42cf0810d88778d90a2facf493c0916", + "run-0001/TRIAL.1/post-process-echo-trials": + "post-process-echo-trials/d8fc97f2f216f2ffa61981529d1f62c3", + "run-0001/TRIAL.2/post-process-echo-trials": + "post-process-echo-trials/550f8a61527a7538dcac92e8fed94d6d", + "run-0001/TRIAL.3/post-process-echo-trials": + "post-process-echo-trials/3018c327f3bd46d1d263d74efc319abf", + "run-0001/VAR1.0.36.VAR2.0.17.VAR3.0.44.VAR4.0.39/test-directory-hashing": + "test-directory-hashing/6df755a183b9e4329be759a718a54f80", + "run-0001/VAR1.0.39.VAR2.0.75.VAR3.0.72.VAR4.0.55/test-directory-hashing": + "test-directory-hashing/7eb63184e109da27e172188be6e32598", + "run-0001/VAR1.0.84.VAR2.0.73.VAR3.0.90.VAR4.0.19/test-directory-hashing": + "test-directory-hashing/c74742ecea5a2b58e67350b5a1e0a234", + "run-0001/all_records/post-process-echo": + "post-process-echo", + "run-0001/all_records/start": + "start", + } + + def compare_tree_to_reference(self, tree, reference): + tree_lines = tree.split("\n") + links = [] + for line in tree_lines: + if line.find(" -> ") > -1: + links.append(line.split(" -> ")) + links[-1][0] = links[-1][0].split("/") + links[-1][1] = links[-1][1].split("/") + assert len(links) == len(reference) + target_list = list(reference.items()) + for i in range(len(target_list)): + target_list[i] = [target_list[i][0].split("/"), target_list[i][1].split("/")] + try: + for a, b in zip(links, target_list): + a0, a1 = a[0][-len(b[0]):], a[1][-len(b[1]):] + print(a0, "==", b[0]) + print(a1, "==", b[1]) + assert a0 == b[0] + assert a1 == b[1] + except AssertionError: + success = False + for permutation in permutations(links): + try: + for a, b in zip(permutation, target_list): + a0, a1 = a[0][-len(b[0]):], a[1][-len(b[1]):] + print(a0, "==", b[0]) + print(a1, "==", b[1]) + assert a0 == b[0] + assert a1 == b[1] + except AssertionError: + pass + else: + success = True + print(f"success debug: {permutation} / {target_list}") + if not success: + raise AssertionError(f"links don't match target: {links} != {target_list}") + + def test_study_index(self): + """ + test simple study links + """ + os.chdir(self.tmp_dir) + integration_spec_path = self.spec_path("link_integration_fast.yml") + + maestro_cmd = ["maestro", "run", "-fg", "-y", "-s", "0", "--make-links", + "--link-template", + "{{output_path}}/../links/{{date}}/run-{{study_index}}/{{combo}}/{{step}}", + integration_spec_path] + tree_cmd = ["tree", "-f", "-i", os.path.join(self.tmp_dir, "output", "links")] + + subprocess.run(maestro_cmd) + print(subprocess.run(["tree", "-f", "-i", self.tmp_dir, "output", "links"], + capture_output=True).stdout.decode()) + cmd_output = subprocess.run(tree_cmd, capture_output=True) + tree_result = cmd_output.stdout.decode() + self.compare_tree_to_reference(tree_result, self.LINKS_0001_test_study_index) + + maestro_cmd = ["maestro", "run", "-fg", "-y", "-s", "0", "--make-links", + "--dir-float-format", '{:.3f}', '{:.3e}', + "--link-template", + "{{output_path}}/../links/{{date}}/run-{{study_index}}/{{combo}}/{{step}}", + integration_spec_path] + + subprocess.run(maestro_cmd) + cmd_output = subprocess.run(tree_cmd, capture_output=True) + tree_result = cmd_output.stdout.decode() + self.compare_tree_to_reference(tree_result, self.LINKS_0002_test_study_index) + + def test_combo_index(self): + """ + test simple combo links + """ + os.chdir(self.tmp_dir) + integration_spec_path = self.spec_path("link_integration_fast.yml") + + maestro_cmd = ["maestro", "run", "-fg", "-y", "-s", "0", "--make-links", + "--link-template", + ("{{output_path}}/../links/{{date}}/{{study_name}}-{{study_index}}/" + "combo-{{combo_index}}-{{combo}}/{{step}}"), + integration_spec_path] + tree_cmd = ["tree", "-f", "-i", os.path.join(self.tmp_dir, "output", "links")] + + subprocess.run(maestro_cmd) + print("tmp_dir") + print(subprocess.run(["tree", "-f", "-i", self.tmp_dir], + capture_output=True).stdout.decode()) + cmd_output = subprocess.run(tree_cmd, capture_output=True) + tree_result = cmd_output.stdout.decode() + print(f"tree_result:\n{tree_result}") + self.compare_tree_to_reference(tree_result, self.LINKS_0001_test_combo_index) + + def test_combo_var_index(self): + """ + test simple combo links with var variables + """ + os.chdir(self.tmp_dir) + integration_spec_path = self.spec_path("link_integration_fast.yml") + + maestro_cmd = ["maestro", "run", "-fg", "-y", "-s", "0", "--make-links", + "--link-template", + ("{{output_path}}/../links/{{date}}/{{study_name}}-{{study_index}}/" + "combo-{{combo_index}}/VAR1.{{VAR1}}/VAR2.{{VAR2}}/" + "VAR3.{{VAR3}}/VAR4.{{VAR4}}/{{step}}"), + integration_spec_path] + print(" ".join(maestro_cmd)) + tree_cmd = ["tree", "-f", "-i", os.path.join(self.tmp_dir, "output", "links")] + + subprocess.run(maestro_cmd) + print("tmp_dir") + print(subprocess.run(["tree", "-f", "-i", self.tmp_dir], + capture_output=True).stdout.decode()) + cmd_output = subprocess.run(tree_cmd, capture_output=True) + tree_result = cmd_output.stdout.decode() + print(f"tree_result:\n{tree_result}") + self.compare_tree_to_reference(tree_result, self.LINKS_0001_test_combo_var_index) + + def test_hashed_combo_index(self): + """ + test simple hashed combo links + """ + os.chdir(self.tmp_dir) + integration_spec_path = self.spec_path("link_integration_fast.yml") + + maestro_cmd = ["maestro", "run", "-fg", "-y", "-s", "0", "--make-links", "--hashws", + "--link-template", + ("{{output_path}}/../links/{{date}}/{{study_name}}-{{study_index}}/" + "combo-{{combo_index}}-{{combo}}/{{step}}"), + integration_spec_path] + tree_cmd = ["tree", "-f", "-i", os.path.join(self.tmp_dir, "output", "links")] + + subprocess.run(maestro_cmd) + # print(subprocess.run(["tree", "-f", "-i", self.tmp_dir, "output", "links"], + # capture_output=True).stdout.decode()) + print("tmp_dir") + print(subprocess.run(["tree", "-f", "-i", self.tmp_dir], + capture_output=True).stdout.decode()) + cmd_output = subprocess.run(tree_cmd, capture_output=True) + tree_result = cmd_output.stdout.decode() + print(f"tree_result:\n{tree_result}") + self.compare_tree_to_reference(tree_result, self.LINKS_0001_test_hashed_combo_index) + + + def test_all_links(self): + """ + test all links + """ + os.chdir(self.tmp_dir) + integration_spec_path = self.spec_path("link_integration.yml") + + maestro_cmd = ["maestro", "run", "-fg", "-y", "-s", "0", "--make-links", + "--link-template", + "{{output_path}}/../links/{{date}}/run-{{study_index}}/{{combo}}/{{step}}", + integration_spec_path] + tree_cmd = ["tree", "-f", "-i", os.path.join(self.tmp_dir, "output", "links")] + + subprocess.run(maestro_cmd) + cmd_output = subprocess.run(tree_cmd, capture_output=True) + tree_result = cmd_output.stdout.decode() + self.compare_tree_to_reference(tree_result, self.LINKS_0001_test_all_links) + + def test_all_hashed_links(self): + """ + test all links + """ + os.chdir(self.tmp_dir) + integration_spec_path = self.spec_path("link_integration.yml") + + maestro_cmd = ["maestro", "run", "-fg", "-y", "-s", "0", "--make-links", "--hashws", + "--link-template", + "{{output_path}}/../links/{{date}}/run-{{study_index}}/{{combo}}/{{step}}", + integration_spec_path] + tree_cmd = ["tree", "-f", "-i", os.path.join(self.tmp_dir, "output", "links")] + + subprocess.run(maestro_cmd) + cmd_output = subprocess.run(tree_cmd, capture_output=True) + tree_result = cmd_output.stdout.decode() + print("tree_result\n", tree_result) + self.compare_tree_to_reference(tree_result, self.LINKS_0001_test_all_hashed_links) + + # def test_var_template_validatation(self): + # """ + # test template error testing + # """ + # os.chdir(self.tmp_dir) + # integration_spec_path = self.spec_path("link_integration_template_error.yml") + + # maestro_cmd = ["maestro", "run", "-fg", "-y", "-s", "0", "--make-links", + # "--link-template", + # "{{output_path}}/../links/{{date}}/run-{{study_index}}/{{combo}}/{{step}}", + # integration_spec_path] + # with self.assertRaises(ValueError) as context: + # subprocess.run(maestro_cmd) + # self.assertTrue( + # "does not include required 'study' variables" + # in str(context.exception)) \ No newline at end of file diff --git a/tests/test_link.py b/tests/test_link.py new file mode 100644 index 00000000..5eed9163 --- /dev/null +++ b/tests/test_link.py @@ -0,0 +1,220 @@ +""" +Test module for testing link methods. +""" + +# @TODO: test cases: index in front, middle, end, no index, two indexes +# with and without hash +# @TODO: other error checking on template_string? + +import os +import pytest +import shutil +import tempfile +import unittest +import logging + +from maestrowf.utils import ( + splitall, recursive_render, next_path) +from maestrowf.datastructures.core.study import StudyStep +from maestrowf.datastructures.core.linker import Linker + +class TestLinkUtilsUnits(unittest.TestCase): + """Unit tests for Linker helper functions""" + + def setUp(self): + self.tmp_dir = tempfile.mkdtemp() + self.sleep_time = 5 + + def tearDown(self): + shutil.rmtree(self.tmp_dir, ignore_errors=True) + + @pytest.fixture(autouse=True) + def inject_fixtures(self, caplog): + self._caplog = caplog + + def test_splitall(self): + """ + tests splitall method + """ + dir = os.path.join("foo") + self.assertEqual(splitall(dir), ["foo"]) + dir = os.path.join("foo", "bar") + self.assertEqual(splitall(dir), ["foo", "bar"]) + dir = os.path.join("foo", "bar", "foo") + self.assertEqual(splitall(dir), ["foo", "bar", "foo"]) + + def test_validate_study_template(self): + """ + tests validation of study templates + """ + # Validate link template: date+time or index; + linker = Linker() + linker.validate_link_template("{{study_index}}/{{step}}") + linker.validate_link_template("{{output_name}}/{{step}}") + linker.validate_link_template("{{study_date}}{{study_time}}/{{step}}") + linker.validate_link_template("{{date}}{{study_time}}/{{step}}") + with self.assertRaises(ValueError) as context: + linker.validate_link_template("foo") + self.assertTrue( + "does not include required 'study' variables" + in str(context.exception)) + with self.assertRaises(ValueError) as context: + linker.validate_link_template("{{study_date}}") + self.assertTrue( + "does not include required 'study' variables" + in str(context.exception)) + with self.assertRaises(ValueError) as context: + linker.validate_link_template("{{date}}") + self.assertTrue( + "does not include required 'study' variables" + in str(context.exception)) + with self.assertRaises(ValueError) as context: + linker.validate_link_template("{{study_time}}") + self.assertTrue( + "does not include required 'study' variables" + in str(context.exception)) + + def test_validate_combo_template(self): + """ + tests validation of study+combo templates + """ + # Validate link template: date+time or index; + linker = Linker(pgen=(lambda x:x)) + linker.validate_link_template("{{study_index}}/{{combo_index}}/{{step}}") + linker.validate_link_template("{{study_index}}/{{combo}}/{{step}}") + linker = Linker( + globals={ + 'VAR1': {'label': 'VAR1.%%', + 'values': [0.3874309076, 0.3585516934, 0.8368954934]}, + 'VAR2': {'label': 'VAR2.%%', + 'values': [0.7520078045, 0.1707261687, 0.7296721416]}}) + linker.validate_link_template("{{study_index}}/{{combo_index}}/{{step}}") + linker.validate_link_template("{{study_index}}/{{combo}}/{{step}}") + linker.validate_link_template("{{study_index}}/{{VAR1}}-{{VAR2}}/{{step}}") + + with self.assertRaises(ValueError) as context: + linker.validate_link_template("{{study_date}}") + print(context.exception) + self.assertTrue( + "does not include required 'study' variables" + in str(context.exception)) + self.assertTrue( + "does not include required 'combo' variables" + in str(context.exception)) + + with self.assertRaises(ValueError) as context: + linker.validate_link_template("{{study_index}}") + self.assertFalse( + "does not include required 'study' variables" + in str(context.exception)) + self.assertTrue( + "does not include required 'combo' variables" + in str(context.exception)) + + with self.assertRaises(ValueError) as context: + linker.validate_link_template("{{combo_index}}") + self.assertTrue( + "does not include required 'study' variables" + in str(context.exception)) + self.assertFalse( + "does not include required 'combo' variables" + in str(context.exception)) + + with self.assertRaises(ValueError) as context: + linker.validate_link_template("{{VAR1}}") + self.assertTrue( + "does not include required 'combo' variables" + in str(context.exception)) + + with self.assertRaises(ValueError) as context: + linker.validate_link_template("{{combo_index}}{{combo_index}}") + self.assertTrue( + "'{{combo_index}}' can not be repeated" + in str(context.exception)) + + with self.assertRaises(ValueError) as context: + linker.validate_link_template("{{study_index}}{{study_index}}") + self.assertTrue( + "'{{study_index}}' can not be repeated" + in str(context.exception)) + + with self.assertRaises(ValueError) as context: + linker.validate_link_template("{{study_index}}") + self.assertTrue( + "does not include required {{step}} variable" + in str(context.exception)) + + with self.assertRaises(ValueError) as context: + linker.validate_link_template("{{step}}/{{study_index}}") + self.assertTrue( + "This code requires the {{step}} variable to be to the right" + in str(context.exception)) + + def test_validate_variable_conflict(self): + """ + tests validation of template and variable conflicts + """ + # Validate link template: date+time or index; + linker = Linker( + globals={ + 'date': {'label': 'date.%%', + 'values': [0.3874309076, 0.3585516934, 0.8368954934]}, + 'VAR2': {'label': 'VAR2.%%', + 'values': [0.7520078045, 0.1707261687, 0.7296721416]}}) + linker.validate_link_template("{{study_index}}/{{combo_index}}/{{step}}") + linker.validate_link_template("{{study_index}}/{{combo}}/{{step}}") + with self._caplog.at_level(logging.WARNING): + linker.validate_link_template("{{study_index}}/{{combo}}/{{foo}}/{{step}}") + self.assertTrue( + 'is not in list of allowed tokens' + in self._caplog.text) + with self.assertRaises(ValueError) as context: + linker.validate_link_template("{{study_index}}/{{date}}-{{VAR2}}/{{step}}") + self.assertTrue( + "can not be resolved" + in str(context.exception)) + + def test_recursive_render(self): + self.assertEqual( + recursive_render("Hello {{X}}!", dict(X="{{name}}", name="world")), + 'Hello world!') + self.assertEqual( + recursive_render("Hello {{X}}!", dict(X="world")), + 'Hello world!') + + def test_next_path(self): + """ + tests next_path method + """ + template = os.path.join(self.tmp_dir, "file-%s.txt") + file = next_path(template) + self.assertEqual(file, template % 1) + max_range = 3 + for i in range(max_range): + os.system("touch " + (template % i)) + file = next_path(template) + print(file) + self.assertEqual(file, template % max_range) + os.system("touch "+(template % max_range)) + file = next_path(template) + self.assertEqual(file, template % (max_range + 1)) + + +class TestLinkUtilUnits(unittest.TestCase): + """Unit tests for Linker class methods""" + + # @TODO rename link_directory to link_directory template + def setUp(self): + self.tmp_dir = tempfile.mkdtemp() + self.sleep_time = 5 + self.linker = Linker() + self.record = StudyStep() + + def tearDown(self): + shutil.rmtree(self.tmp_dir, ignore_errors=True) + + def test_build_replacements(self): + """ + tests split_indexed_directory method + """ + pass