From 2cacfa7fef2e814dde8f303123eb1618f1a4e07d Mon Sep 17 00:00:00 2001 From: MartinBelthle <102529366+MartinBelthle@users.noreply.github.com> Date: Mon, 18 Sep 2023 18:47:28 +0200 Subject: [PATCH 01/23] feat(parameters): handle the `--partition` and `--qos` parameters for the `sbatch` command (#58) * feat(parameters): add partition in MainParameters * refactor: simplify the implementation of `SlurmScriptFeatures.compose_launch_command` * refactor(parameters-reader): simplify the implementation of the parameters reader * refactor(parameters-reader): change the signature of the `SlurmScriptFeatures` constructor * style(parameters-reader): sort imports and reformat source code * feat(parameters-reader): make the `partition` parameter optional * feat(parameters-reader): handle the `--qos` (quality of service) parameter for the `sbatch` command --------- Co-authored-by: Laurent LAPORTE --- antareslauncher/antares_launcher.py | 12 +- antareslauncher/config.py | 2 +- antareslauncher/data_repo/data_repo_tinydb.py | 3 +- antareslauncher/main.py | 43 +++++-- antareslauncher/parameters_reader.py | 109 +++++++--------- .../slurm_script_features.py | 116 +++++++++--------- data/README.md | 7 ++ ...test_integration_check_queue_controller.py | 10 +- .../test_integration_job_kill_controller.py | 10 +- .../test_integration_launch_controller.py | 27 ++-- tests/unit/test_antares_launcher.py | 2 +- tests/unit/test_check_queue_controller.py | 4 +- tests/unit/test_config.py | 1 + tests/unit/test_job_kill_controller.py | 4 +- tests/unit/test_main_option_parser.py | 2 +- tests/unit/test_parameters_reader.py | 74 ++++++----- .../test_remote_environment_with_slurm.py | 36 ++++-- tests/unit/test_slurm_queue_show.py | 4 +- tests/unit/test_ssh_connection.py | 3 +- tests/unit/test_study_list_composer.py | 1 - 20 files changed, 252 insertions(+), 218 deletions(-) diff --git a/antareslauncher/antares_launcher.py b/antareslauncher/antares_launcher.py index 1e8abdb..a9a11ee 100644 --- a/antareslauncher/antares_launcher.py +++ b/antareslauncher/antares_launcher.py @@ -4,16 +4,10 @@ from antareslauncher.use_cases.check_remote_queue.check_queue_controller import ( CheckQueueController, ) -from antareslauncher.use_cases.create_list.study_list_composer import ( - StudyListComposer, -) -from antareslauncher.use_cases.kill_job.job_kill_controller import ( - JobKillController, -) +from antareslauncher.use_cases.create_list.study_list_composer import StudyListComposer +from antareslauncher.use_cases.kill_job.job_kill_controller import JobKillController from antareslauncher.use_cases.launch.launch_controller import LaunchController -from antareslauncher.use_cases.retrieve.retrieve_controller import ( - RetrieveController, -) +from antareslauncher.use_cases.retrieve.retrieve_controller import RetrieveController from antareslauncher.use_cases.wait_loop_controller.wait_controller import ( WaitController, ) diff --git a/antareslauncher/config.py b/antareslauncher/config.py index 5fe9342..760d526 100644 --- a/antareslauncher/config.py +++ b/antareslauncher/config.py @@ -13,9 +13,9 @@ from antareslauncher import __author__, __project_name__, __version__ from antareslauncher.exceptions import ( + ConfigFileNotFoundError, InvalidConfigValueError, UnknownFileSuffixError, - ConfigFileNotFoundError, ) APP_NAME = __project_name__ diff --git a/antareslauncher/data_repo/data_repo_tinydb.py b/antareslauncher/data_repo/data_repo_tinydb.py index 3a34067..c40a281 100644 --- a/antareslauncher/data_repo/data_repo_tinydb.py +++ b/antareslauncher/data_repo/data_repo_tinydb.py @@ -2,9 +2,10 @@ from typing import List import tinydb +from tinydb import TinyDB, where + from antareslauncher.data_repo.idata_repo import IDataRepo from antareslauncher.study_dto import StudyDTO -from tinydb import TinyDB, where class DataRepoTinydb(IDataRepo): diff --git a/antareslauncher/main.py b/antareslauncher/main.py index 076c054..f05eced 100644 --- a/antareslauncher/main.py +++ b/antareslauncher/main.py @@ -1,7 +1,7 @@ import argparse -from dataclasses import dataclass +import dataclasses from pathlib import Path -from typing import List, Dict +from typing import Dict, List from antareslauncher import __version__ from antareslauncher.antares_launcher import AntaresLauncher @@ -19,9 +19,7 @@ from antareslauncher.use_cases.check_remote_queue.check_queue_controller import ( CheckQueueController, ) -from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import ( - SlurmQueueShow, -) +from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import SlurmQueueShow from antareslauncher.use_cases.create_list.study_list_composer import ( StudyListComposer, StudyListComposerParameters, @@ -29,13 +27,9 @@ from antareslauncher.use_cases.generate_tree_structure.tree_structure_initializer import ( TreeStructureInitializer, ) -from antareslauncher.use_cases.kill_job.job_kill_controller import ( - JobKillController, -) +from antareslauncher.use_cases.kill_job.job_kill_controller import JobKillController from antareslauncher.use_cases.launch.launch_controller import LaunchController -from antareslauncher.use_cases.retrieve.retrieve_controller import ( - RetrieveController, -) +from antareslauncher.use_cases.retrieve.retrieve_controller import RetrieveController from antareslauncher.use_cases.retrieve.state_updater import StateUpdater from antareslauncher.use_cases.wait_loop_controller.wait_controller import ( WaitController, @@ -65,14 +59,33 @@ class SshConnectionNotEstablishedException(Exception): # fmt: on -@dataclass +@dataclasses.dataclass class MainParameters: + """ + Represents the main parameters of the application. + + Attributes: + json_dir: Path to the directory where the JSON database will be stored. + default_json_db_name: The default JSON database name. + slurm_script_path: Path to the SLURM script used to launch studies (a Shell script). + antares_versions_on_remote_server: A list of available Antares Solver versions on the remote server. + default_ssh_dict: A dictionary containing the SSH settings read from `ssh_config.json`. + db_primary_key: The primary key for the database, default to "name". + partition: Extra `sbatch` option to request a specific partition for resource allocation. + If not specified, the default behavior is to allow the SLURM controller + to select the default partition as designated by the system administrator. + quality_of_service: Extra `sbatch` option to request a quality of service for the job. + QOS values can be defined for each user/cluster/account association in the Slurm database. + """ + json_dir: Path default_json_db_name: str slurm_script_path: str antares_versions_on_remote_server: List[str] default_ssh_dict: Dict db_primary_key: str + partition: str = "" + quality_of_service: str = "" def run_with( @@ -114,7 +127,11 @@ def run_with( connection = ssh_connection.SshConnection(config=ssh_dict) verify_connection(connection, display) - slurm_script_features = SlurmScriptFeatures(parameters.slurm_script_path) + slurm_script_features = SlurmScriptFeatures( + parameters.slurm_script_path, + partition=parameters.partition, + quality_of_service=parameters.quality_of_service, + ) environment = RemoteEnvironmentWithSlurm(connection, slurm_script_features) data_repo = DataRepoTinydb( database_file_path=db_json_file_path, db_primary_key=parameters.db_primary_key diff --git a/antareslauncher/parameters_reader.py b/antareslauncher/parameters_reader.py index c9189d6..0b94695 100644 --- a/antareslauncher/parameters_reader.py +++ b/antareslauncher/parameters_reader.py @@ -1,10 +1,11 @@ +import getpass import json import os.path +import typing as t from pathlib import Path -from typing import Dict, Any import yaml -import getpass + from antareslauncher.main import MainParameters from antareslauncher.main_option_parser import ParserParameters @@ -13,45 +14,53 @@ DEFAULT_JSON_DB_NAME = f"{getpass.getuser()}_antares_launcher_db.json" -class ParametersReader: - class EmptyFileException(TypeError): - pass +class MissingValueException(Exception): + def __init__(self, yaml_filepath: Path, key: str) -> None: + super().__init__(f"Missing key '{key}' in '{yaml_filepath}'") - class MissingValueException(KeyError): - pass +class ParametersReader: def __init__(self, json_ssh_conf: Path, yaml_filepath: Path): self.json_ssh_conf = json_ssh_conf - with open(Path(yaml_filepath)) as yaml_file: - self.yaml_content = yaml.load(yaml_file, Loader=yaml.FullLoader) or {} + with open(yaml_filepath) as yaml_file: + obj = yaml.load(yaml_file, Loader=yaml.FullLoader) or {} - # fmt: off - self._wait_time = self._get_compulsory_value("DEFAULT_WAIT_TIME") - self.time_limit = self._get_compulsory_value("DEFAULT_TIME_LIMIT") - self.n_cpu = self._get_compulsory_value("DEFAULT_N_CPU") - self.studies_in_dir = os.path.expanduser(self._get_compulsory_value("STUDIES_IN_DIR")) - self.log_dir = os.path.expanduser(self._get_compulsory_value("LOG_DIR")) - self.finished_dir = os.path.expanduser(self._get_compulsory_value("FINISHED_DIR")) - self.ssh_conf_file_is_required = self._get_compulsory_value("SSH_CONFIG_FILE_IS_REQUIRED") - # fmt: on + try: + self.default_wait_time = obj["DEFAULT_WAIT_TIME"] + self.time_limit = obj["DEFAULT_TIME_LIMIT"] + self.n_cpu = obj["DEFAULT_N_CPU"] + self.studies_in_dir = os.path.expanduser(obj["STUDIES_IN_DIR"]) + self.log_dir = os.path.expanduser(obj["LOG_DIR"]) + self.finished_dir = os.path.expanduser(obj["FINISHED_DIR"]) + self.ssh_conf_file_is_required = obj["SSH_CONFIG_FILE_IS_REQUIRED"] + default_ssh_configfile_name = obj["DEFAULT_SSH_CONFIGFILE_NAME"] + except KeyError as e: + raise MissingValueException(yaml_filepath, str(e)) from None - alt1, alt2 = self._get_ssh_conf_file_alts() - self.ssh_conf_alt1, self.ssh_conf_alt2 = alt1, alt2 - self.default_ssh_dict = self._get_ssh_dict_from_json() - self.remote_slurm_script_path = self._get_compulsory_value("SLURM_SCRIPT_PATH") - self.antares_versions = self._get_compulsory_value( - "ANTARES_VERSIONS_ON_REMOTE_SERVER" - ) - self.db_primary_key = self._get_compulsory_value("DB_PRIMARY_KEY") - self.json_dir = Path(self._get_compulsory_value("JSON_DIR")).expanduser() - self.json_db_name = self.yaml_content.get( - "DEFAULT_JSON_DB_NAME", DEFAULT_JSON_DB_NAME - ) + default_alternate1 = ALT1_PARENT / default_ssh_configfile_name + default_alternate2 = ALT2_PARENT / default_ssh_configfile_name + + alt1 = obj.get("SSH_CONFIGFILE_PATH_ALTERNATE1", default_alternate1) + alt2 = obj.get("SSH_CONFIGFILE_PATH_ALTERNATE2", default_alternate2) + + try: + self.ssh_conf_alt1 = alt1 + self.ssh_conf_alt2 = alt2 + self.default_ssh_dict = self._get_ssh_dict_from_json() + self.remote_slurm_script_path = obj["SLURM_SCRIPT_PATH"] + self.partition = obj.get("PARTITION", "") + self.quality_of_service = obj.get("QUALITY_OF_SERVICE", "") + self.antares_versions = obj["ANTARES_VERSIONS_ON_REMOTE_SERVER"] + self.db_primary_key = obj["DB_PRIMARY_KEY"] + self.json_dir = Path(obj["JSON_DIR"]).expanduser() + self.json_db_name = obj.get("DEFAULT_JSON_DB_NAME", DEFAULT_JSON_DB_NAME) + except KeyError as e: + raise MissingValueException(yaml_filepath, str(e)) from None def get_parser_parameters(self): - options = ParserParameters( - default_wait_time=self._wait_time, + return ParserParameters( + default_wait_time=self.default_wait_time, default_time_limit=self.time_limit, default_n_cpu=self.n_cpu, studies_in_dir=self.studies_in_dir, @@ -61,48 +70,20 @@ def get_parser_parameters(self): ssh_configfile_path_alternate1=self.ssh_conf_alt1, ssh_configfile_path_alternate2=self.ssh_conf_alt2, ) - return options def get_main_parameters(self) -> MainParameters: - main_parameters = MainParameters( + return MainParameters( json_dir=self.json_dir, default_json_db_name=self.json_db_name, slurm_script_path=self.remote_slurm_script_path, + partition=self.partition, + quality_of_service=self.quality_of_service, antares_versions_on_remote_server=self.antares_versions, default_ssh_dict=self.default_ssh_dict, db_primary_key=self.db_primary_key, ) - return main_parameters - - def _get_ssh_conf_file_alts(self): - default_alternate1, default_alternate2 = self._get_default_alternate_values() - ssh_conf_alternate1 = self.yaml_content.get( - "SSH_CONFIGFILE_PATH_ALTERNATE1", - default_alternate1, - ) - ssh_conf_alternate2 = self.yaml_content.get( - "SSH_CONFIGFILE_PATH_ALTERNATE2", - default_alternate2, - ) - return ssh_conf_alternate1, ssh_conf_alternate2 - - def _get_default_alternate_values(self): - default_ssh_configfile_name = self._get_compulsory_value( - "DEFAULT_SSH_CONFIGFILE_NAME" - ) - default_alternate1 = ALT1_PARENT / default_ssh_configfile_name - default_alternate2 = ALT2_PARENT / default_ssh_configfile_name - return default_alternate1, default_alternate2 - - def _get_compulsory_value(self, key: str): - try: - value = self.yaml_content[key] - except KeyError as e: - print(f"missing value: {str(e)}") - raise ParametersReader.MissingValueException(e) from None - return value - def _get_ssh_dict_from_json(self) -> Dict[str, Any]: + def _get_ssh_dict_from_json(self) -> t.Dict[str, t.Any]: with open(self.json_ssh_conf) as ssh_connection_json: ssh_dict = json.load(ssh_connection_json) if "private_key_file" in ssh_dict: diff --git a/antareslauncher/remote_environnement/slurm_script_features.py b/antareslauncher/remote_environnement/slurm_script_features.py index 6d3de9c..b233bbf 100644 --- a/antareslauncher/remote_environnement/slurm_script_features.py +++ b/antareslauncher/remote_environnement/slurm_script_features.py @@ -1,9 +1,10 @@ -from dataclasses import dataclass +import dataclasses +import shlex from antareslauncher.study_dto import Modes -@dataclass +@dataclasses.dataclass class ScriptParametersDTO: study_dir_name: str input_zipfile_name: str @@ -19,77 +20,72 @@ class SlurmScriptFeatures: """Class that returns data related to the remote SLURM script Installed on the remote server""" - def __init__(self, slurm_script_path: str): - self.JOB_TYPE_PLACEHOLDER = "TO_BE_REPLACED_WITH_JOB_TYPE" - self.JOB_TYPE_ANTARES = "ANTARES" - self.JOB_TYPE_XPANSION_R = "ANTARES_XPANSION_R" - self.JOB_TYPE_XPANSION_CPP = "ANTARES_XPANSION_CPP" + def __init__( + self, + slurm_script_path: str, + *, + partition: str, + quality_of_service: str, + ): + """ + Initialize the slurm script feature. + + Args: + slurm_script_path: Path to the SLURM script used to launch studies (a Shell script). + partition: Request a specific partition for the resource allocation. + If not specified, the default behavior is to allow the slurm controller + to select the default partition as designated by the system administrator. + quality_of_service: Request a quality of service for the job. + QOS values can be defined for each user/cluster/account association in the Slurm database. + """ self.solver_script_path = slurm_script_path - self._script_params = None - self._remote_launch_dir = None + self.partition = partition + self.quality_of_service = quality_of_service def compose_launch_command( self, remote_launch_dir: str, script_params: ScriptParametersDTO, ) -> str: - """Compose and return the complete command to be executed to launch the Antares Solver script. - It includes the change of directory to remote_base_path + """ + Compose and return the complete command to be executed to launch the Antares Solver script. Args: - script_params: ScriptFeaturesDTO dataclass container for script parameters remote_launch_dir: remote directory where the script is launched + script_params: ScriptFeaturesDTO dataclass container for script parameters Returns: - str: the complete command to be executed to launch the including the change of directory to remote_base_path - + str: the complete command to be executed to launch a study on the SLURM server """ - self._script_params = script_params - self._remote_launch_dir = remote_launch_dir - complete_command = self._get_complete_command_with_placeholders() + # The following options can be added to the `sbatch` command + # if they are not empty (or null for integer options). + _opts = { + "--partition": self.partition, # non-empty string + "--qos": self.quality_of_service, # non-empty string + "--job-name": script_params.study_dir_name, # non-empty string + "--time": script_params.time_limit, # greater than 0 + "--cpus-per-task": script_params.n_cpu, # greater than 0 + } - if script_params.run_mode == Modes.antares: - complete_command = complete_command.replace( - self.JOB_TYPE_PLACEHOLDER, self.JOB_TYPE_ANTARES - ) - elif script_params.run_mode == Modes.xpansion_r: - complete_command = complete_command.replace( - self.JOB_TYPE_PLACEHOLDER, self.JOB_TYPE_XPANSION_R - ) - elif script_params.run_mode == Modes.xpansion_cpp: - complete_command = complete_command.replace( - self.JOB_TYPE_PLACEHOLDER, self.JOB_TYPE_XPANSION_CPP - ) + _job_type = { + Modes.antares: "ANTARES", # Mode for Antares Solver + Modes.xpansion_r: "ANTARES_XPANSION_R", # Mode for Old Xpansion implemented in R + Modes.xpansion_cpp: "ANTARES_XPANSION_CPP", # Mode for Xpansion implemented in C++ + }[script_params.run_mode] - return complete_command - - def _bash_options(self): - option1_zipfile_name = f' "{self._script_params.input_zipfile_name}"' - option2_antares_version = f" {self._script_params.antares_version}" - option3_job_type = f" {self.JOB_TYPE_PLACEHOLDER}" - option4_post_processing = f" {self._script_params.post_processing}" - option5_other_options = f" '{self._script_params.other_options}'" - bash_options = ( - option1_zipfile_name - + option2_antares_version - + option3_job_type - + option4_post_processing - + option5_other_options + # Construct the `sbatch` command + args = ["sbatch"] + args.extend(f"{k}={shlex.quote(str(v))}" for k, v in _opts.items() if v) + args.extend( + shlex.quote(arg) + for arg in [ + self.solver_script_path, + script_params.input_zipfile_name, + script_params.antares_version, + _job_type, + str(script_params.post_processing), + script_params.other_options, + ] ) - return bash_options - - def _sbatch_command_with_slurm_options(self): - call_sbatch = f"sbatch" - job_name = f' --job-name="{self._script_params.study_dir_name}"' - time_limit_opt = f" --time={self._script_params.time_limit}" - cpu_per_task = f" --cpus-per-task={self._script_params.n_cpu}" - slurm_options = call_sbatch + job_name + time_limit_opt + cpu_per_task - return slurm_options - - def _get_complete_command_with_placeholders(self): - change_dir = f"cd {self._remote_launch_dir}" - slurm_options = self._sbatch_command_with_slurm_options() - bash_options = self._bash_options() - submit_command = slurm_options + " " + self.solver_script_path + bash_options - complete_command = change_dir + " && " + submit_command - return complete_command + launch_cmd = f"cd {remote_launch_dir} && {' '.join(args)}" + return launch_cmd diff --git a/data/README.md b/data/README.md index 1895066..851382c 100644 --- a/data/README.md +++ b/data/README.md @@ -25,6 +25,8 @@ DB_PRIMARY_KEY : "name" DEFAULT_SSH_CONFIGFILE_NAME: "ssh_config.json" SSH_CONFIG_FILE_IS_REQUIRED : False SLURM_SCRIPT_PATH : "/opt/antares/launchAntares.sh" +PARTITION : "compute1" +QUALITY_OR_SERVICE : "user1_qos" ANTARES_VERSIONS_ON_REMOTE_SERVER : - "610" @@ -51,6 +53,11 @@ Below is a description of the parameters: - `DEFAULT_SSH_CONFIGFILE_NAME`: The default name of the SSH configuration file, it should be "ssh_config.json". - `SSH_CONFIG_FILE_IS_REQUIRED`: A flag indicating whether an SSH configuration file is required. - `SLURM_SCRIPT_PATH`: Path to the SLURM script used to launch studies (a Shell script). +- `PARTITION`: Extra `sbatch` option to request a specific partition for resource allocation. + If not specified, the default behavior is to allow the SLURM controller + to select the default partition as designated by the system administrator. +- `QUALITY_OF_SERVICE`: Extra `sbatch` option to request a quality of service for the job. + QOS values can be defined for each user/cluster/account association in the Slurm database. - `ANTARES_VERSIONS_ON_REMOTE_SERVER`: A list of strings representing the available Antares Solver versions on the remote server. ## SSH Configuration diff --git a/tests/integration/test_integration_check_queue_controller.py b/tests/integration/test_integration_check_queue_controller.py index 91e781f..144bac8 100644 --- a/tests/integration/test_integration_check_queue_controller.py +++ b/tests/integration/test_integration_check_queue_controller.py @@ -12,9 +12,7 @@ from antareslauncher.use_cases.check_remote_queue.check_queue_controller import ( CheckQueueController, ) -from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import ( - SlurmQueueShow, -) +from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import SlurmQueueShow from antareslauncher.use_cases.retrieve.state_updater import StateUpdater @@ -23,7 +21,11 @@ def setup_method(self): self.connection_mock = mock.Mock(home_dir="path/to/home") self.connection_mock.username = "username" self.connection_mock.execute_command = mock.Mock(return_value=("", "")) - slurm_script_features = SlurmScriptFeatures("slurm_script_path") + slurm_script_features = SlurmScriptFeatures( + "slurm_script_path", + partition="fake_partition", + quality_of_service="user1_qos", + ) env_mock = RemoteEnvironmentWithSlurm( _connection=self.connection_mock, slurm_script_features=slurm_script_features, diff --git a/tests/integration/test_integration_job_kill_controller.py b/tests/integration/test_integration_job_kill_controller.py index e8c8731..f36039f 100644 --- a/tests/integration/test_integration_job_kill_controller.py +++ b/tests/integration/test_integration_job_kill_controller.py @@ -8,14 +8,16 @@ from antareslauncher.remote_environnement.slurm_script_features import ( SlurmScriptFeatures, ) -from antareslauncher.use_cases.kill_job.job_kill_controller import ( - JobKillController, -) +from antareslauncher.use_cases.kill_job.job_kill_controller import JobKillController class TestIntegrationJobKilController: def setup_method(self): - slurm_script_features = SlurmScriptFeatures("slurm_script_path") + slurm_script_features = SlurmScriptFeatures( + "slurm_script_path", + partition="fake_partition", + quality_of_service="user1_qos", + ) connection = mock.Mock(home_dir="path/to/home") env = RemoteEnvironmentWithSlurm(connection, slurm_script_features) self.job_kill_controller = JobKillController(env, mock.Mock(), repo=mock.Mock()) diff --git a/tests/integration/test_integration_launch_controller.py b/tests/integration/test_integration_launch_controller.py index 412e5fa..40a4fe4 100644 --- a/tests/integration/test_integration_launch_controller.py +++ b/tests/integration/test_integration_launch_controller.py @@ -20,7 +20,11 @@ class TestIntegrationLaunchController: @pytest.fixture(scope="function") def launch_controller(self): connection = mock.Mock(home_dir="path/to/home") - slurm_script_features = SlurmScriptFeatures("slurm_script_path") + slurm_script_features = SlurmScriptFeatures( + "slurm_script_path", + partition="fake_partition", + quality_of_service="user1_qos", + ) environment = RemoteEnvironmentWithSlurm(connection, slurm_script_features) study1 = mock.Mock() study1.zipfile_path = "filepath" @@ -35,15 +39,13 @@ def launch_controller(self): data_repo.get_list_of_studies = mock.Mock(return_value=[study1, study2]) file_manager = mock.Mock() display = DisplayTerminal() - launch_controller = LaunchController( + return LaunchController( repo=data_repo, env=environment, file_manager=file_manager, display=display, ) - return launch_controller - @pytest.mark.integration_test def test_upload_file__called_twice(self, launch_controller): """ @@ -71,7 +73,11 @@ def test_execute_command__called_with_the_correct_parameters( connection = mock.Mock() connection.execute_command = mock.Mock(return_value=["Submitted 42", ""]) connection.home_dir = "Submitted" - slurm_script_features = SlurmScriptFeatures("slurm_script_path") + slurm_script_features = SlurmScriptFeatures( + "slurm_script_path", + partition="fake_partition", + quality_of_service="user1_qos", + ) environment = RemoteEnvironmentWithSlurm(connection, slurm_script_features) study1 = StudyDTO( path="dummy_path", @@ -84,7 +90,7 @@ def test_execute_command__called_with_the_correct_parameters( home_dir = "Submitted" remote_base_path = ( - str(home_dir) + "/REMOTE_" + getpass.getuser() + "_" + socket.gethostname() + f"{home_dir}/REMOTE_{getpass.getuser()}_{socket.gethostname()}" ) zipfile_name = Path(study1.zipfile_path).name @@ -92,7 +98,7 @@ def test_execute_command__called_with_the_correct_parameters( post_processing = False other_options = "" bash_options = ( - f'"{zipfile_name}"' + f" {zipfile_name}" f" {study1.antares_version}" f" {job_type}" f" {post_processing}" @@ -100,11 +106,14 @@ def test_execute_command__called_with_the_correct_parameters( ) command = ( f"cd {remote_base_path} && " - f'sbatch --job-name="{Path(study1.path).name}"' + f"sbatch" + f" --partition={slurm_script_features.partition}" + f" --qos={slurm_script_features.quality_of_service}" + f" --job-name={Path(study1.path).name}" f" --time={study1.time_limit // 60}" f" --cpus-per-task={study1.n_cpu}" f" {environment.slurm_script_features.solver_script_path}" - f" {bash_options}" + f"{bash_options}" ) data_repo = mock.Mock() diff --git a/tests/unit/test_antares_launcher.py b/tests/unit/test_antares_launcher.py index f8188a8..4803f52 100644 --- a/tests/unit/test_antares_launcher.py +++ b/tests/unit/test_antares_launcher.py @@ -1,5 +1,5 @@ from unittest import mock -from unittest.mock import PropertyMock, Mock +from unittest.mock import Mock, PropertyMock import pytest diff --git a/tests/unit/test_check_queue_controller.py b/tests/unit/test_check_queue_controller.py index d1f53d7..127294b 100644 --- a/tests/unit/test_check_queue_controller.py +++ b/tests/unit/test_check_queue_controller.py @@ -7,9 +7,7 @@ from antareslauncher.use_cases.check_remote_queue.check_queue_controller import ( CheckQueueController, ) -from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import ( - SlurmQueueShow, -) +from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import SlurmQueueShow from antareslauncher.use_cases.retrieve.state_updater import StateUpdater diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 5850686..f33997a 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -7,6 +7,7 @@ import pytest import yaml + from antareslauncher.config import ( APP_AUTHOR, APP_NAME, diff --git a/tests/unit/test_job_kill_controller.py b/tests/unit/test_job_kill_controller.py index 7167577..f12e8c4 100644 --- a/tests/unit/test_job_kill_controller.py +++ b/tests/unit/test_job_kill_controller.py @@ -2,9 +2,7 @@ import pytest -from antareslauncher.use_cases.kill_job.job_kill_controller import ( - JobKillController, -) +from antareslauncher.use_cases.kill_job.job_kill_controller import JobKillController class TestJobKillController: diff --git a/tests/unit/test_main_option_parser.py b/tests/unit/test_main_option_parser.py index c31dfe8..e394688 100644 --- a/tests/unit/test_main_option_parser.py +++ b/tests/unit/test_main_option_parser.py @@ -5,8 +5,8 @@ from antareslauncher.main_option_parser import ( MainOptionParser, ParserParameters, + look_for_default_ssh_conf_file, ) -from antareslauncher.main_option_parser import look_for_default_ssh_conf_file class TestMainOptionParser: diff --git a/tests/unit/test_parameters_reader.py b/tests/unit/test_parameters_reader.py index 042f9f8..1ba30d6 100644 --- a/tests/unit/test_parameters_reader.py +++ b/tests/unit/test_parameters_reader.py @@ -3,13 +3,16 @@ from pathlib import Path import pytest +import yaml -from antareslauncher.parameters_reader import ParametersReader +from antareslauncher.parameters_reader import MissingValueException, ParametersReader class TestParametersReader: def setup_method(self): self.SLURM_SCRIPT_PATH = "/path/to/launchAntares_v1.1.3.sh" + self.PARTITION = "compute1" + self.QUALITY_OF_SERVICE = "user1_qos" self.SSH_CONFIG_FILE_IS_REQUIRED = False self.DEFAULT_SSH_CONFIGFILE_NAME = "ssh_config.json" self.DB_PRIMARY_KEY = "name" @@ -22,40 +25,43 @@ def setup_method(self): self.JSON_DIR = "JSON" self.ANTARES_SUPPORTED_VERSIONS = ["610", "700"] - self.yaml_compulsory_content = ( - f'LOG_DIR : "{self.LOG_DIR}"\n' - f'JSON_DIR : "{self.JSON_DIR}"\n' - f'STUDIES_IN_DIR : "{self.STUDIES_IN_DIR}"\n' - f'FINISHED_DIR : "{self.FINISHED_DIR}"\n' - f"DEFAULT_TIME_LIMIT : {self.DEFAULT_TIME_LIMIT}\n" - f"DEFAULT_N_CPU : {self.DEFAULT_N_CPU}\n" - f"DEFAULT_WAIT_TIME : {self.DEFAULT_WAIT_TIME}\n" - f'DB_PRIMARY_KEY : "{self.DB_PRIMARY_KEY}"\n' - f'DEFAULT_SSH_CONFIGFILE_NAME: "{self.DEFAULT_SSH_CONFIGFILE_NAME}"\n' - f"SSH_CONFIG_FILE_IS_REQUIRED : {self.SSH_CONFIG_FILE_IS_REQUIRED}\n" - f'SLURM_SCRIPT_PATH : "{self.SLURM_SCRIPT_PATH}"\n' - f"ANTARES_VERSIONS_ON_REMOTE_SERVER :\n" - f' - "{self.ANTARES_SUPPORTED_VERSIONS[0]}"\n' - f' - "{self.ANTARES_SUPPORTED_VERSIONS[1]}"\n' + self.yaml_compulsory_content = yaml.dump( + { + "LOG_DIR": self.LOG_DIR, + "JSON_DIR": self.JSON_DIR, + "STUDIES_IN_DIR": self.STUDIES_IN_DIR, + "FINISHED_DIR": self.FINISHED_DIR, + "DEFAULT_TIME_LIMIT": self.DEFAULT_TIME_LIMIT, + "DEFAULT_N_CPU": self.DEFAULT_N_CPU, + "DEFAULT_WAIT_TIME": self.DEFAULT_WAIT_TIME, + "DB_PRIMARY_KEY": self.DB_PRIMARY_KEY, + "DEFAULT_SSH_CONFIGFILE_NAME": self.DEFAULT_SSH_CONFIGFILE_NAME, + "SSH_CONFIG_FILE_IS_REQUIRED": self.SSH_CONFIG_FILE_IS_REQUIRED, + "SLURM_SCRIPT_PATH": self.SLURM_SCRIPT_PATH, + "PARTITION": self.PARTITION, + "QUALITY_OF_SERVICE": self.QUALITY_OF_SERVICE, + "ANTARES_VERSIONS_ON_REMOTE_SERVER": self.ANTARES_SUPPORTED_VERSIONS, + }, + default_flow_style=False, ) - self.DEFAULT_JSON_DB_NAME = "db_file.json" - self.yaml_opt_content = ( - f'DEFAULT_JSON_DB_NAME : "{self.DEFAULT_JSON_DB_NAME}\n' - f'DEFAULT_SSH_CONFIGFILE_NAME: "{self.DEFAULT_SSH_CONFIGFILE_NAME}"\n' + + self.yaml_opt_content = yaml.dump( + { + "DEFAULT_JSON_DB_NAME": "db_file.json", + "DEFAULT_SSH_CONFIGFILE_NAME": self.DEFAULT_SSH_CONFIGFILE_NAME, + }, + default_flow_style=False, ) - self.USER = "user" - self.HOST = "host" - self.KEY = "C:\\home\\hello" - self.KEY_PSWD = "hello" + self.json_dict = { - "username": self.USER, - "hostname": self.HOST, - "private_key_file": self.KEY, - "key_password": self.KEY_PSWD, + "username": "user", + "hostname": "host", + "private_key_file": "C:\\home\\hello", + "key_password": "hello", } @pytest.mark.unit_test - def test_ParametersReader_raises_exception_with_no_file(self, tmp_path): + def test_parameters_reader_raises_exception_with_no_file(self, tmp_path): with pytest.raises(FileNotFoundError): ParametersReader(Path(tmp_path), Path("empty.yaml")) @@ -64,7 +70,7 @@ def test_get_option_parameters_raises_exception_with_empty_file(self, tmp_path): empty_json = tmp_path / "dummy.json" empty_yaml = tmp_path / "empty.yaml" empty_yaml.write_text("") - with pytest.raises(ParametersReader.MissingValueException): + with pytest.raises(MissingValueException): ParametersReader(empty_json, empty_yaml).get_parser_parameters() @pytest.mark.unit_test @@ -72,7 +78,7 @@ def test_get_main_parameters_raises_exception_with_empty_file(self, tmp_path): empty_json = tmp_path / "dummy.json" empty_yaml = tmp_path / "empty.yaml" empty_yaml.write_text("") - with pytest.raises(ParametersReader.MissingValueException): + with pytest.raises(MissingValueException): ParametersReader(empty_json, empty_yaml).get_main_parameters() @pytest.mark.unit_test @@ -89,7 +95,7 @@ def test_get_option_parameters_raises_exception_if_params_are_missing( "DEFAULT_TIME_LIMIT : 172800\n" "DEFAULT_N_CPU : 2\n" ) - with pytest.raises(ParametersReader.MissingValueException): + with pytest.raises(MissingValueException): ParametersReader(empty_json, config_yaml).get_parser_parameters() @pytest.mark.unit_test @@ -104,7 +110,7 @@ def test_get_main_parameters_raises_exception_if_params_are_missing(self, tmp_pa "DEFAULT_TIME_LIMIT : 172800\n" "DEFAULT_N_CPU : 2\n" ) - with pytest.raises(ParametersReader.MissingValueException): + with pytest.raises(MissingValueException): ParametersReader(empty_json, config_yaml).get_main_parameters() @pytest.mark.unit_test @@ -149,6 +155,8 @@ def test_get_main_parameters_initializes_parameters_correctly(self, tmp_path): main_parameters.default_json_db_name == f"{getpass.getuser()}_antares_launcher_db.json" ) + assert main_parameters.partition == self.PARTITION + assert main_parameters.quality_of_service == self.QUALITY_OF_SERVICE assert main_parameters.db_primary_key == self.DB_PRIMARY_KEY assert not main_parameters.default_ssh_dict assert ( diff --git a/tests/unit/test_remote_environment_with_slurm.py b/tests/unit/test_remote_environment_with_slurm.py index 8116ec0..4c80536 100644 --- a/tests/unit/test_remote_environment_with_slurm.py +++ b/tests/unit/test_remote_environment_with_slurm.py @@ -64,7 +64,11 @@ def remote_env(self) -> RemoteEnvironmentWithSlurm: remote_home_dir = "remote_home_dir" connection = mock.Mock(home_dir="path/to/home") connection.home_dir = remote_home_dir - slurm_script_features = SlurmScriptFeatures("slurm_script_path") + slurm_script_features = SlurmScriptFeatures( + "slurm_script_path", + partition="fake_partition", + quality_of_service="user1_qos", + ) return RemoteEnvironmentWithSlurm(connection, slurm_script_features) @pytest.mark.unit_test @@ -80,7 +84,11 @@ def test_initialise_remote_path_calls_connection_make_dir_with_correct_arguments connection.home_dir = remote_home_dir connection.make_dir = mock.Mock(return_value=True) connection.check_file_not_empty = mock.Mock(return_value=True) - slurm_script_features = SlurmScriptFeatures("slurm_script_path") + slurm_script_features = SlurmScriptFeatures( + "slurm_script_path", + partition="fake_partition", + quality_of_service="user1_qos", + ) # when RemoteEnvironmentWithSlurm(connection, slurm_script_features) # then @@ -92,7 +100,11 @@ def test_when_constructor_is_called_and_remote_base_path_cannot_be_created_then_ ): # given connection = mock.Mock(home_dir="path/to/home") - slurm_script_features = SlurmScriptFeatures("slurm_script_path") + slurm_script_features = SlurmScriptFeatures( + "slurm_script_path", + partition="fake_partition", + quality_of_service="user1_qos", + ) # when connection.make_dir = mock.Mock(return_value=False) # then @@ -107,7 +119,11 @@ def test_when_constructor_is_called_then_connection_check_file_not_empty_is_call connection = mock.Mock(home_dir="path/to/home") connection.make_dir = mock.Mock(return_value=True) connection.check_file_not_empty = mock.Mock(return_value=True) - slurm_script_features = SlurmScriptFeatures("slurm_script_path") + slurm_script_features = SlurmScriptFeatures( + "slurm_script_path", + partition="fake_partition", + quality_of_service="user1_qos", + ) # when RemoteEnvironmentWithSlurm(connection, slurm_script_features) # then @@ -123,7 +139,11 @@ def test_when_constructor_is_called_and_connection_check_file_not_empty_is_false connection = mock.Mock(home_dir="path/to/home") connection.home_dir = remote_home_dir connection.make_dir = mock.Mock(return_value=True) - slurm_script_features = SlurmScriptFeatures("slurm_script_path") + slurm_script_features = SlurmScriptFeatures( + "slurm_script_path", + partition="fake_partition", + quality_of_service="user1_qos", + ) # when connection.check_file_not_empty = mock.Mock(return_value=False) # then @@ -689,11 +709,13 @@ def test_compose_launch_command( change_dir = f"cd {remote_env.remote_base_path}" reference_submit_command = ( f"sbatch" - f' --job-name="{Path(study.path).name}"' + " --partition=fake_partition" + " --qos=user1_qos" + f" --job-name={Path(study.path).name}" f" --time={study.time_limit // 60}" f" --cpus-per-task={study.n_cpu}" f" {filename_launch_script}" - f' "{Path(study.zipfile_path).name}"' + f" {Path(study.zipfile_path).name}" f" {study.antares_version}" f" {job_type}" f" {post_processing}" diff --git a/tests/unit/test_slurm_queue_show.py b/tests/unit/test_slurm_queue_show.py index f6b42a2..87a12d2 100644 --- a/tests/unit/test_slurm_queue_show.py +++ b/tests/unit/test_slurm_queue_show.py @@ -2,9 +2,7 @@ import pytest -from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import ( - SlurmQueueShow, -) +from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import SlurmQueueShow @pytest.mark.unit_test diff --git a/tests/unit/test_ssh_connection.py b/tests/unit/test_ssh_connection.py index 4086db3..340d689 100644 --- a/tests/unit/test_ssh_connection.py +++ b/tests/unit/test_ssh_connection.py @@ -7,12 +7,13 @@ import paramiko import pytest +from paramiko.sftp_attr import SFTPAttributes + from antareslauncher.remote_environnement.ssh_connection import ( ConnectionFailedException, DownloadMonitor, SshConnection, ) -from paramiko.sftp_attr import SFTPAttributes LOGGER = DownloadMonitor.__module__ diff --git a/tests/unit/test_study_list_composer.py b/tests/unit/test_study_list_composer.py index b31ecf1..2ad06e0 100644 --- a/tests/unit/test_study_list_composer.py +++ b/tests/unit/test_study_list_composer.py @@ -37,7 +37,6 @@ def study_mock(self): def test_given_repo_when_get_list_of_studies_called_then_repo_get_list_of_studies_is_called( self, ): - # given repo_mock = mock.Mock() repo_mock.get_list_of_studies = mock.Mock() From 21996a9da5c0987d8107e6c25a3d52294e45c8bf Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE <43534797+laurent-laporte-pro@users.noreply.github.com> Date: Mon, 18 Sep 2023 18:49:53 +0200 Subject: [PATCH 02/23] fix(results-retrieval): handle exceptions in log and ZIP result retrival (#60) * build(GitHub): update GitHub workflow to build the Windows/Ubuntu executables on support branch * fix(results-retrieval): handle exceptions in log and ZIP result retrieval The log and ZIP result retrieval function now properly handles exceptions. In this scenario, the retrieval job is marked as 'completed with error' and the error message is recorded in the Job state. --- .../use_cases/retrieve/retrieve_controller.py | 8 +- .../use_cases/retrieve/study_retriever.py | 66 +++---- tests/unit/retriever/test_study_retriever.py | 167 +++++++++++------- 3 files changed, 131 insertions(+), 110 deletions(-) diff --git a/antareslauncher/use_cases/retrieve/retrieve_controller.py b/antareslauncher/use_cases/retrieve/retrieve_controller.py index ca67f11..181984b 100644 --- a/antareslauncher/use_cases/retrieve/retrieve_controller.py +++ b/antareslauncher/use_cases/retrieve/retrieve_controller.py @@ -51,10 +51,7 @@ def all_studies_done(self): True if all the studies are done, False otherwise """ studies = self.repo.get_list_of_studies() - for study in studies: - if not study.done: - return False - return True + return all(study.done for study in studies) def retrieve_all_studies(self): """Retrieves all the studies and logs from the environment and process them @@ -72,8 +69,7 @@ def retrieve_all_studies(self): __name__ + "." + __class__.__name__, ) for study in studies: - if not study.done: - self.study_retriever.retrieve(study) + self.study_retriever.retrieve(study) if self.all_studies_done: self.display.show_message( "Everything is done", diff --git a/antareslauncher/use_cases/retrieve/study_retriever.py b/antareslauncher/use_cases/retrieve/study_retriever.py index 48fa7c6..18134f9 100644 --- a/antareslauncher/use_cases/retrieve/study_retriever.py +++ b/antareslauncher/use_cases/retrieve/study_retriever.py @@ -23,48 +23,28 @@ def __init__( self.remote_server_cleaner = remote_server_cleaner self.zip_extractor = zip_extractor self.reporter = reporter - self._current_study: StudyDTO = None - - def _update_job_state_flags(self): - self._current_study = self.state_updater.run(self._current_study) - self.reporter.save_study(self._current_study) - - def _download_slurm_logs(self): - self._current_study = self.logs_downloader.run(self._current_study) - self.reporter.save_study(self._current_study) - - def _download_final_zip(self): - self._current_study = self.final_zip_downloader.download(self._current_study) - self.reporter.save_study(self._current_study) - - def _clean_remote_server(self): - self._current_study = self.remote_server_cleaner.clean(self._current_study) - self.reporter.save_study(self._current_study) - - def _extract_study_result(self): - self._current_study = self.zip_extractor.extract_final_zip(self._current_study) - self.reporter.save_study(self._current_study) - - def _check_if_done(self): - done = self.check_if_study_is_done(self._current_study) - self._current_study.done = done - self.reporter.save_study(self._current_study) def retrieve(self, study: StudyDTO): - self._current_study = study - if not self._current_study.done: - self._update_job_state_flags() - self._download_slurm_logs() - self._download_final_zip() - self._clean_remote_server() - self._extract_study_result() - self._check_if_done() - - @staticmethod - def check_if_study_is_done(study: StudyDTO): - return study.with_error or ( - study.logs_downloaded - and study.local_final_zipfile_path - and study.remote_server_is_clean - and study.final_zip_extracted - ) + if not study.done: + try: + self.state_updater.run(study) + self.logs_downloader.run(study) + self.final_zip_downloader.download(study) + self.remote_server_cleaner.clean(study) + self.zip_extractor.extract_final_zip(study) + study.done = study.with_error or ( + study.logs_downloaded + and study.local_final_zipfile_path + and study.remote_server_is_clean + and study.final_zip_extracted + ) + + except Exception as e: + # The exception is not re-raised, but the job is marked as failed + study.done = True + study.finished = True + study.with_error = True + study.job_state = f"Internal error: {e}" + + finally: + self.reporter.save_study(study) diff --git a/tests/unit/retriever/test_study_retriever.py b/tests/unit/retriever/test_study_retriever.py index 3caab2d..50f2736 100644 --- a/tests/unit/retriever/test_study_retriever.py +++ b/tests/unit/retriever/test_study_retriever.py @@ -1,9 +1,6 @@ -from copy import copy from unittest import mock -from unittest.mock import call import pytest - from antareslauncher.data_repo.data_reporter import DataReporter from antareslauncher.data_repo.idata_repo import IDataRepo from antareslauncher.display.idisplay import IDisplay @@ -13,7 +10,10 @@ ) from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.retrieve.clean_remote_server import RemoteServerCleaner -from antareslauncher.use_cases.retrieve.download_final_zip import FinalZipDownloader +from antareslauncher.use_cases.retrieve.download_final_zip import ( + FinalZipDownloader, + FinalZipNotDownloadedException, +) from antareslauncher.use_cases.retrieve.final_zip_extractor import FinalZipExtractor from antareslauncher.use_cases.retrieve.log_downloader import LogDownloader from antareslauncher.use_cases.retrieve.state_updater import StateUpdater @@ -61,70 +61,115 @@ def test_given_done_study_nothing_is_done(self): self.zip_extractor.extract_final_zip.assert_not_called() @pytest.mark.unit_test - def test_given_a_not_done_studies_everything_is_called(self): + def test_retrieve_study(self): + """ + This test function simulates the retrieval process of a study and verifies + that various components and states are updated correctly. + + Test cases covered: + - State updater + - Logs downloader + - Final zip downloader + - Remote server cleaner + - Zip extractor + - Reporter + """ study = StudyDTO(path="hello") - study1 = StudyDTO( + + def state_updater_run(study_: StudyDTO): + study_.job_id = 42 + study_.started = True + study_.finished = True + study_.with_error = False + return study_ + + self.state_updater.run = mock.Mock(side_effect=state_updater_run) + + def logs_downloader_run(study_: StudyDTO): + study_.logs_downloaded = True + return study_ + + self.logs_downloader.run = mock.Mock(side_effect=logs_downloader_run) + + def final_zip_downloader_download(study_: StudyDTO): + study_.local_final_zipfile_path = "final-zipfile.zip" + return study_ + + self.final_zip_downloader.download = mock.Mock( + side_effect=final_zip_downloader_download + ) + + def remote_server_cleaner_clean(study_: StudyDTO): + study_.remote_server_is_clean = True + return study_ + + self.remote_server_cleaner.clean = mock.Mock( + side_effect=remote_server_cleaner_clean + ) + + def zip_extractor_extract_final_zip(study_: StudyDTO): + study_.final_zip_extracted = True + return study_ + + self.zip_extractor.extract_final_zip = mock.Mock( + side_effect=zip_extractor_extract_final_zip + ) + self.reporter.save_study = mock.Mock(return_value=True) + + self.study_retriever.retrieve(study) + + expected = StudyDTO( path="hello", job_id=42, + done=True, started=True, finished=True, with_error=False, + logs_downloaded=True, + local_final_zipfile_path="final-zipfile.zip", + remote_server_is_clean=True, + final_zip_extracted=True, ) - self.state_updater.run = mock.Mock(return_value=study1) - study2 = copy(study1) - study2.logs_downloaded = True - self.logs_downloader.run = mock.Mock(return_value=study2) - study3 = copy(study2) - study3.local_final_zipfile_path = "final-zipfile.zip" - self.final_zip_downloader.download = mock.Mock(return_value=study3) - study4 = copy(study3) - study4.remote_server_is_clean = True - self.remote_server_cleaner.clean = mock.Mock(return_value=study4) - study5 = copy(study4) - study5.final_zip_extracted = True - self.zip_extractor.extract_final_zip = mock.Mock(return_value=study5) - study6 = copy(study5) - study6.done = True - self.reporter.save_study = mock.Mock() + self.reporter.save_study.assert_called_once_with(expected) + + @pytest.mark.unit_test + def test_retrieve_study__exception(self): + """ + This test case specifically checks the behavior of the `retrieve` method + in the presence of an exception. It verifies that the study object and + its components are updated correctly when this exception occurs. + """ + study = StudyDTO(path="hello") + + def state_updater_run(study_: StudyDTO): + study_.job_id = 42 + study_.started = True + study_.finished = True + study_.with_error = False + return study_ + + self.state_updater.run = mock.Mock(side_effect=state_updater_run) + self.logs_downloader.run = mock.Mock( + side_effect=FinalZipNotDownloadedException("download fails") + ) + self.final_zip_downloader.download = mock.Mock() + self.remote_server_cleaner.clean = mock.Mock() + self.zip_extractor.extract_final_zip = mock.Mock() + self.reporter.save_study = mock.Mock(return_value=True) self.study_retriever.retrieve(study) - self.state_updater.run.assert_called_once_with(study) - self.logs_downloader.run.assert_called_once_with(study1) - self.final_zip_downloader.download.assert_called_once_with(study2) - self.remote_server_cleaner.clean.assert_called_once_with(study3) - self.zip_extractor.extract_final_zip.assert_called_once_with(study4) - assert self.reporter.save_study.call_count == 6 - calls = self.reporter.save_study.call_args_list - assert calls[0] == call(study1) - assert calls[1] == call(study2) - assert calls[2] == call(study3) - assert calls[3] == call(study4) - assert calls[4] == call(study5) - assert calls[5] == call(study6) - - @staticmethod - @pytest.mark.unit_test - @pytest.mark.parametrize( - "result, with_error,logs_downloaded, local_final_zipfile_path, remote_server_is_clean, final_zip_extracted", - [ - (False, False, False, None, False, False), - (True, True, False, None, False, False), - (True, False, True, "path.zip", True, True), - ], - ) - def test_when_study_finished_with_error_check_if_study_is_done_returns_true( - result, - with_error, - logs_downloaded, - local_final_zipfile_path, - remote_server_is_clean, - final_zip_extracted, - ): - my_study = StudyDTO(path="hello", job_id=42, started=True, finished=True) - my_study.with_error = with_error - my_study.logs_downloaded = logs_downloaded - my_study.local_final_zipfile_path = local_final_zipfile_path - my_study.remote_server_is_clean = remote_server_is_clean - my_study.final_zip_extracted = final_zip_extracted - assert StudyRetriever.check_if_study_is_done(my_study) is result + expected = StudyDTO( + path="hello", + job_id=42, + done=True, + started=True, + finished=True, + with_error=True, + logs_downloaded=False, + local_final_zipfile_path="", + remote_server_is_clean=False, + final_zip_extracted=False, + job_state="Internal error: download fails", + ) + self.reporter.save_study.assert_called_once_with(expected) From d627ea375deb78f5b5784c47a164fd607de269ad Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE <43534797+laurent-laporte-pro@users.noreply.github.com> Date: Mon, 18 Sep 2023 19:10:54 +0200 Subject: [PATCH 03/23] fix(job-state): consider the `COMPLETING` value as a possible job state (#61) This correction allows for considering the `COMPLETING` value as a possible state of a job. This value is returned by the `scontrol` command. --- .../remote_environnement/remote_environment_with_slurm.py | 4 ++++ tests/unit/retriever/test_study_retriever.py | 1 + tests/unit/test_remote_environment_with_slurm.py | 2 ++ 3 files changed, 7 insertions(+) diff --git a/antareslauncher/remote_environnement/remote_environment_with_slurm.py b/antareslauncher/remote_environnement/remote_environment_with_slurm.py index 0401f79..3ea8b30 100644 --- a/antareslauncher/remote_environnement/remote_environment_with_slurm.py +++ b/antareslauncher/remote_environnement/remote_environment_with_slurm.py @@ -84,6 +84,9 @@ class JobStateCodes(enum.Enum): # Job has terminated all processes on all nodes with an exit code of zero. COMPLETED = "COMPLETED" + # Indicates that the only job on the node or that all jobs on the node are in the process of completing. + COMPETING = "COMPLETING" + # Job terminated on deadline. DEADLINE = "DEADLINE" @@ -288,6 +291,7 @@ def get_job_state_flags( JobStateCodes.BOOT_FAIL: (False, False, False), JobStateCodes.CANCELLED: (True, True, True), JobStateCodes.COMPLETED: (True, True, False), + JobStateCodes.COMPETING: (True, False, False), JobStateCodes.DEADLINE: (True, True, True), # similar to timeout JobStateCodes.FAILED: (True, True, True), JobStateCodes.NODE_FAIL: (True, True, True), diff --git a/tests/unit/retriever/test_study_retriever.py b/tests/unit/retriever/test_study_retriever.py index 50f2736..0e8b256 100644 --- a/tests/unit/retriever/test_study_retriever.py +++ b/tests/unit/retriever/test_study_retriever.py @@ -1,6 +1,7 @@ from unittest import mock import pytest + from antareslauncher.data_repo.data_reporter import DataReporter from antareslauncher.data_repo.idata_repo import IDataRepo from antareslauncher.display.idisplay import IDisplay diff --git a/tests/unit/test_remote_environment_with_slurm.py b/tests/unit/test_remote_environment_with_slurm.py index 4c80536..9b52aba 100644 --- a/tests/unit/test_remote_environment_with_slurm.py +++ b/tests/unit/test_remote_environment_with_slurm.py @@ -376,6 +376,7 @@ def execute_command_mock(cmd: str): call(command), ] + # noinspection SpellCheckingInspection @pytest.mark.unit_test @pytest.mark.parametrize( "state, expected", @@ -387,6 +388,7 @@ def execute_command_mock(cmd: str): ("CANCELLED by 123456", (True, True, True)), ("TIMEOUT", (True, True, True)), ("COMPLETED", (True, True, False)), + ("COMPLETING", (True, False, False)), ("FAILED", (True, True, True)), ], ) From 0dbf971b1ccc924f4b11cf44b0e0cf16562622c9 Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Mon, 18 Sep 2023 19:23:05 +0200 Subject: [PATCH 04/23] fix(console): use the ISO8601 date format to display messages on the console --- antareslauncher/display/display_terminal.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/antareslauncher/display/display_terminal.py b/antareslauncher/display/display_terminal.py index 22ce7c9..ba49d76 100644 --- a/antareslauncher/display/display_terminal.py +++ b/antareslauncher/display/display_terminal.py @@ -8,7 +8,8 @@ class DisplayTerminal(IDisplay): def __init__(self): - self.format = "%Y%m%d %H:%M" + # Use the ISO8601 date format to display messages on the console + self.format = "%Y-%m-%d %H:%M:%S%z" def show_message(self, message: str, class_name: str, end: str = "\n"): """Displays a message on the terminal From 69aecd8532bf2970f6fb82710f52430c0819be6e Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE <43534797+laurent-laporte-pro@users.noreply.github.com> Date: Wed, 20 Sep 2023 09:12:39 +0200 Subject: [PATCH 05/23] refactor: remove IDisplay abstract class (#64) * refactor: drop the `IDisplay` abstract class and replace it by the `DisplayTerminal` class * fix: correct logger initialization --- antareslauncher/data_repo/data_repo_tinydb.py | 2 +- antareslauncher/display/display_terminal.py | 4 +--- antareslauncher/display/idisplay.py | 15 --------------- antareslauncher/file_manager/file_manager.py | 6 +++--- .../check_remote_queue/slurm_queue_show.py | 4 ++-- .../use_cases/create_list/study_list_composer.py | 4 ++-- .../tree_structure_initializer.py | 4 ++-- .../use_cases/kill_job/job_kill_controller.py | 4 ++-- .../use_cases/launch/launch_controller.py | 4 ++-- .../use_cases/launch/study_submitter.py | 4 ++-- .../use_cases/launch/study_zip_cleaner.py | 4 ++-- .../use_cases/launch/study_zip_uploader.py | 4 ++-- antareslauncher/use_cases/launch/study_zipper.py | 4 ++-- .../use_cases/retrieve/clean_remote_server.py | 4 ++-- .../use_cases/retrieve/download_final_zip.py | 4 ++-- .../use_cases/retrieve/final_zip_extractor.py | 4 ++-- .../use_cases/retrieve/log_downloader.py | 4 ++-- .../use_cases/retrieve/retrieve_controller.py | 4 ++-- .../use_cases/retrieve/state_updater.py | 4 ++-- .../wait_loop_controller/wait_controller.py | 4 ++-- tests/unit/launcher/test_launch_controller.py | 4 ++-- tests/unit/launcher/test_submitter.py | 4 ++-- tests/unit/launcher/test_zip_uploader.py | 4 ++-- tests/unit/launcher/test_zipper.py | 4 ++-- tests/unit/retriever/test_download_final_zip.py | 4 ++-- tests/unit/retriever/test_final_zip_extractor.py | 4 ++-- tests/unit/retriever/test_log_downloader.py | 4 ++-- tests/unit/retriever/test_retrieve_controller.py | 4 ++-- tests/unit/retriever/test_server_cleaner.py | 4 ++-- tests/unit/retriever/test_study_retriever.py | 4 ++-- tests/unit/test_wait_controller.py | 4 ++-- 31 files changed, 59 insertions(+), 76 deletions(-) delete mode 100644 antareslauncher/display/idisplay.py diff --git a/antareslauncher/data_repo/data_repo_tinydb.py b/antareslauncher/data_repo/data_repo_tinydb.py index c40a281..4d8cf3d 100644 --- a/antareslauncher/data_repo/data_repo_tinydb.py +++ b/antareslauncher/data_repo/data_repo_tinydb.py @@ -12,7 +12,7 @@ class DataRepoTinydb(IDataRepo): def __init__(self, database_file_path, db_primary_key: str): super(DataRepoTinydb, self).__init__() self.database_file_path = database_file_path - self.logger = logging.getLogger(f"{__name__}.{__class__.__name__}") + self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") self.db_primary_key = db_primary_key @property diff --git a/antareslauncher/display/display_terminal.py b/antareslauncher/display/display_terminal.py index ba49d76..a24487f 100644 --- a/antareslauncher/display/display_terminal.py +++ b/antareslauncher/display/display_terminal.py @@ -3,10 +3,8 @@ from tqdm import tqdm -from antareslauncher.display.idisplay import IDisplay - -class DisplayTerminal(IDisplay): +class DisplayTerminal: def __init__(self): # Use the ISO8601 date format to display messages on the console self.format = "%Y-%m-%d %H:%M:%S%z" diff --git a/antareslauncher/display/idisplay.py b/antareslauncher/display/idisplay.py deleted file mode 100644 index 024969a..0000000 --- a/antareslauncher/display/idisplay.py +++ /dev/null @@ -1,15 +0,0 @@ -from abc import ABC, abstractmethod - - -class IDisplay(ABC): - @abstractmethod - def show_message(self, message, class_name, end="\n"): - raise NotImplementedError - - @abstractmethod - def show_error(self, string, class_name): - raise NotImplementedError - - @abstractmethod - def generate_progress_bar(self, iterator, desc, total=None): - raise NotImplementedError diff --git a/antareslauncher/file_manager/file_manager.py b/antareslauncher/file_manager/file_manager.py index bdd902f..64fe281 100644 --- a/antareslauncher/file_manager/file_manager.py +++ b/antareslauncher/file_manager/file_manager.py @@ -5,12 +5,12 @@ import zipfile from pathlib import Path -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal class FileManager: - def __init__(self, display_terminal: IDisplay): - self.logger = logging.getLogger(__name__ + "." + __class__.__name__) + def __init__(self, display_terminal: DisplayTerminal): + self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") self.display = display_terminal def get_config_from_file(self, file_path): diff --git a/antareslauncher/use_cases/check_remote_queue/slurm_queue_show.py b/antareslauncher/use_cases/check_remote_queue/slurm_queue_show.py index d7e2074..c9563db 100644 --- a/antareslauncher/use_cases/check_remote_queue/slurm_queue_show.py +++ b/antareslauncher/use_cases/check_remote_queue/slurm_queue_show.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, ) @@ -9,7 +9,7 @@ @dataclass class SlurmQueueShow: env: RemoteEnvironmentWithSlurm - display: IDisplay + display: DisplayTerminal def run(self): """Displays all the jobs un the slurm queue""" diff --git a/antareslauncher/use_cases/create_list/study_list_composer.py b/antareslauncher/use_cases/create_list/study_list_composer.py index 3b254f5..60ae581 100644 --- a/antareslauncher/use_cases/create_list/study_list_composer.py +++ b/antareslauncher/use_cases/create_list/study_list_composer.py @@ -3,7 +3,7 @@ from typing import List, Optional from antareslauncher.data_repo.idata_repo import IDataRepo -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.study_dto import Modes, StudyDTO @@ -27,7 +27,7 @@ def __init__( self, repo: IDataRepo, file_manager: FileManager, - display: IDisplay, + display: DisplayTerminal, parameters: StudyListComposerParameters, ): self._repo = repo diff --git a/antareslauncher/use_cases/generate_tree_structure/tree_structure_initializer.py b/antareslauncher/use_cases/generate_tree_structure/tree_structure_initializer.py index 71b1828..5ed387e 100644 --- a/antareslauncher/use_cases/generate_tree_structure/tree_structure_initializer.py +++ b/antareslauncher/use_cases/generate_tree_structure/tree_structure_initializer.py @@ -1,12 +1,12 @@ from dataclasses import dataclass -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager.file_manager import FileManager @dataclass class TreeStructureInitializer: - display: IDisplay + display: DisplayTerminal file_manager: FileManager studies_in: str log_dir: str diff --git a/antareslauncher/use_cases/kill_job/job_kill_controller.py b/antareslauncher/use_cases/kill_job/job_kill_controller.py index fd7fcad..6df1e6a 100644 --- a/antareslauncher/use_cases/kill_job/job_kill_controller.py +++ b/antareslauncher/use_cases/kill_job/job_kill_controller.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from antareslauncher.data_repo.idata_repo import IDataRepo -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, ) @@ -10,7 +10,7 @@ @dataclass class JobKillController: env: RemoteEnvironmentWithSlurm - display: IDisplay + display: DisplayTerminal repo: IDataRepo def _check_if_job_is_killable(self, job_id): diff --git a/antareslauncher/use_cases/launch/launch_controller.py b/antareslauncher/use_cases/launch/launch_controller.py index 730bfd6..f6215df 100644 --- a/antareslauncher/use_cases/launch/launch_controller.py +++ b/antareslauncher/use_cases/launch/launch_controller.py @@ -1,6 +1,6 @@ from antareslauncher.data_repo.data_reporter import DataReporter from antareslauncher.data_repo.idata_repo import IDataRepo -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, @@ -61,7 +61,7 @@ def __init__( repo: IDataRepo, env: RemoteEnvironmentWithSlurm, file_manager: FileManager, - display: IDisplay, + display: DisplayTerminal, ): self.repo = repo self.env = env diff --git a/antareslauncher/use_cases/launch/study_submitter.py b/antareslauncher/use_cases/launch/study_submitter.py index 91c62b9..0d187bf 100644 --- a/antareslauncher/use_cases/launch/study_submitter.py +++ b/antareslauncher/use_cases/launch/study_submitter.py @@ -1,7 +1,7 @@ import copy from pathlib import Path -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, ) @@ -13,7 +13,7 @@ class FailedSubmissionException(Exception): class StudySubmitter(object): - def __init__(self, env: RemoteEnvironmentWithSlurm, display: IDisplay): + def __init__(self, env: RemoteEnvironmentWithSlurm, display: DisplayTerminal): self.env = env self.display = display self._current_study: StudyDTO = None diff --git a/antareslauncher/use_cases/launch/study_zip_cleaner.py b/antareslauncher/use_cases/launch/study_zip_cleaner.py index 5967240..d2635d3 100644 --- a/antareslauncher/use_cases/launch/study_zip_cleaner.py +++ b/antareslauncher/use_cases/launch/study_zip_cleaner.py @@ -1,12 +1,12 @@ import copy -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.study_dto import StudyDTO class StudyZipCleaner: - def __init__(self, file_manager: FileManager, display: IDisplay): + def __init__(self, file_manager: FileManager, display: DisplayTerminal): self.file_manager = file_manager self.display = display self._current_study: StudyDTO = StudyDTO("none") diff --git a/antareslauncher/use_cases/launch/study_zip_uploader.py b/antareslauncher/use_cases/launch/study_zip_uploader.py index b98fabe..9b019cd 100644 --- a/antareslauncher/use_cases/launch/study_zip_uploader.py +++ b/antareslauncher/use_cases/launch/study_zip_uploader.py @@ -1,7 +1,7 @@ import copy from pathlib import Path -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, ) @@ -9,7 +9,7 @@ class StudyZipfileUploader: - def __init__(self, env: RemoteEnvironmentWithSlurm, display: IDisplay): + def __init__(self, env: RemoteEnvironmentWithSlurm, display: DisplayTerminal): self.env = env self.display = display self._current_study: StudyDTO = None diff --git a/antareslauncher/use_cases/launch/study_zipper.py b/antareslauncher/use_cases/launch/study_zipper.py index c578696..69b1212 100644 --- a/antareslauncher/use_cases/launch/study_zipper.py +++ b/antareslauncher/use_cases/launch/study_zipper.py @@ -3,14 +3,14 @@ from dataclasses import dataclass from pathlib import Path -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.study_dto import StudyDTO @dataclass class StudyZipper: - def __init__(self, file_manager: FileManager, display: IDisplay): + def __init__(self, file_manager: FileManager, display: DisplayTerminal): self.file_manager = file_manager self.display = display self._current_study: StudyDTO = None diff --git a/antareslauncher/use_cases/retrieve/clean_remote_server.py b/antareslauncher/use_cases/retrieve/clean_remote_server.py index 744e9f2..b3aa837 100644 --- a/antareslauncher/use_cases/retrieve/clean_remote_server.py +++ b/antareslauncher/use_cases/retrieve/clean_remote_server.py @@ -1,7 +1,7 @@ import copy from pathlib import Path -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, ) @@ -16,7 +16,7 @@ class RemoteServerCleaner: def __init__( self, env: RemoteEnvironmentWithSlurm, - display: IDisplay, + display: DisplayTerminal, ): self._display = display self._env = env diff --git a/antareslauncher/use_cases/retrieve/download_final_zip.py b/antareslauncher/use_cases/retrieve/download_final_zip.py index ed5c96a..d7c8194 100644 --- a/antareslauncher/use_cases/retrieve/download_final_zip.py +++ b/antareslauncher/use_cases/retrieve/download_final_zip.py @@ -1,6 +1,6 @@ import copy -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, ) @@ -15,7 +15,7 @@ class FinalZipDownloader(object): def __init__( self, env: RemoteEnvironmentWithSlurm, - display: IDisplay, + display: DisplayTerminal, ): self._env = env self._display = display diff --git a/antareslauncher/use_cases/retrieve/final_zip_extractor.py b/antareslauncher/use_cases/retrieve/final_zip_extractor.py index c6e99ed..519a51a 100644 --- a/antareslauncher/use_cases/retrieve/final_zip_extractor.py +++ b/antareslauncher/use_cases/retrieve/final_zip_extractor.py @@ -1,6 +1,6 @@ from pathlib import Path -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.study_dto import StudyDTO @@ -10,7 +10,7 @@ class ResultNotExtractedException(Exception): class FinalZipExtractor: - def __init__(self, file_manager: FileManager, display: IDisplay): + def __init__(self, file_manager: FileManager, display: DisplayTerminal): self._file_manager = file_manager self._display = display self._current_study: StudyDTO = None diff --git a/antareslauncher/use_cases/retrieve/log_downloader.py b/antareslauncher/use_cases/retrieve/log_downloader.py index 22248d9..bfbb092 100644 --- a/antareslauncher/use_cases/retrieve/log_downloader.py +++ b/antareslauncher/use_cases/retrieve/log_downloader.py @@ -1,7 +1,7 @@ import copy from pathlib import Path -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, @@ -14,7 +14,7 @@ def __init__( self, env: RemoteEnvironmentWithSlurm, file_manager: FileManager, - display: IDisplay, + display: DisplayTerminal, ): self.env = env self.display = display diff --git a/antareslauncher/use_cases/retrieve/retrieve_controller.py b/antareslauncher/use_cases/retrieve/retrieve_controller.py index 181984b..e79e2ec 100644 --- a/antareslauncher/use_cases/retrieve/retrieve_controller.py +++ b/antareslauncher/use_cases/retrieve/retrieve_controller.py @@ -1,6 +1,6 @@ from antareslauncher.data_repo.data_reporter import DataReporter from antareslauncher.data_repo.idata_repo import IDataRepo -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, @@ -19,7 +19,7 @@ def __init__( repo: IDataRepo, env: RemoteEnvironmentWithSlurm, file_manager: FileManager, - display: IDisplay, + display: DisplayTerminal, state_updater: StateUpdater, ): self.repo = repo diff --git a/antareslauncher/use_cases/retrieve/state_updater.py b/antareslauncher/use_cases/retrieve/state_updater.py index 41b0677..93e43c5 100644 --- a/antareslauncher/use_cases/retrieve/state_updater.py +++ b/antareslauncher/use_cases/retrieve/state_updater.py @@ -1,7 +1,7 @@ from pathlib import Path from typing import List -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, ) @@ -12,7 +12,7 @@ class StateUpdater: def __init__( self, env: RemoteEnvironmentWithSlurm, - display: IDisplay, + display: DisplayTerminal, ): self._env = env self._display = display diff --git a/antareslauncher/use_cases/wait_loop_controller/wait_controller.py b/antareslauncher/use_cases/wait_loop_controller/wait_controller.py index 1d4e2db..9f57386 100644 --- a/antareslauncher/use_cases/wait_loop_controller/wait_controller.py +++ b/antareslauncher/use_cases/wait_loop_controller/wait_controller.py @@ -1,12 +1,12 @@ import time from dataclasses import dataclass -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal @dataclass class WaitController: - display: IDisplay + display: DisplayTerminal def countdown(self, seconds_to_wait: int): """ diff --git a/tests/unit/launcher/test_launch_controller.py b/tests/unit/launcher/test_launch_controller.py index bf58121..930824d 100644 --- a/tests/unit/launcher/test_launch_controller.py +++ b/tests/unit/launcher/test_launch_controller.py @@ -11,7 +11,7 @@ from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.data_repo.data_reporter import DataReporter from antareslauncher.data_repo.idata_repo import IDataRepo -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, @@ -28,7 +28,7 @@ class TestStudyLauncher: def setup_method(self): env = mock.Mock(spec_set=RemoteEnvironmentWithSlurm) - display = mock.Mock(spec_set=IDisplay) + display = mock.Mock(spec_set=DisplayTerminal) file_manager = mock.Mock(spec_set=FileManager) repo = mock.Mock(spec_set=IDataRepo) self.reporter = DataReporter(repo) diff --git a/tests/unit/launcher/test_submitter.py b/tests/unit/launcher/test_submitter.py index fa1384c..72caa03 100644 --- a/tests/unit/launcher/test_submitter.py +++ b/tests/unit/launcher/test_submitter.py @@ -4,7 +4,7 @@ import pytest import antareslauncher.use_cases -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, ) @@ -15,7 +15,7 @@ class TestStudySubmitter: def setup_method(self): self.remote_env = mock.Mock(spec_set=RemoteEnvironmentWithSlurm) - self.display_mock = mock.Mock(spec_set=IDisplay) + self.display_mock = mock.Mock(spec_set=DisplayTerminal) self.study_submitter = StudySubmitter(self.remote_env, self.display_mock) @pytest.mark.unit_test diff --git a/tests/unit/launcher/test_zip_uploader.py b/tests/unit/launcher/test_zip_uploader.py index 5cb013e..128a79f 100644 --- a/tests/unit/launcher/test_zip_uploader.py +++ b/tests/unit/launcher/test_zip_uploader.py @@ -4,7 +4,7 @@ import pytest -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, ) @@ -18,7 +18,7 @@ class TestZipfileUploader: def setup_method(self): self.remote_env = mock.Mock(spec_set=RemoteEnvironmentWithSlurm) - self.display_mock = mock.Mock(spec_set=IDisplay) + self.display_mock = mock.Mock(spec_set=DisplayTerminal) self.study_uploader = StudyZipfileUploader(self.remote_env, self.display_mock) @pytest.mark.unit_test diff --git a/tests/unit/launcher/test_zipper.py b/tests/unit/launcher/test_zipper.py index dfd5562..6c6c5e6 100644 --- a/tests/unit/launcher/test_zipper.py +++ b/tests/unit/launcher/test_zipper.py @@ -3,7 +3,7 @@ import pytest -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.launch.study_zipper import StudyZipper @@ -12,7 +12,7 @@ class TestStudyZipper: def setup_method(self): self.file_manager = mock.Mock(spec_set=FileManager) - self.display_mock = mock.Mock(spec_set=IDisplay) + self.display_mock = mock.Mock(spec_set=DisplayTerminal) self.study_zipper = StudyZipper(self.file_manager, self.display_mock) @pytest.mark.unit_test diff --git a/tests/unit/retriever/test_download_final_zip.py b/tests/unit/retriever/test_download_final_zip.py index b477c7f..a3fb02b 100644 --- a/tests/unit/retriever/test_download_final_zip.py +++ b/tests/unit/retriever/test_download_final_zip.py @@ -5,7 +5,7 @@ import pytest -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, ) @@ -19,7 +19,7 @@ class TestFinalZipDownloader: def setup_method(self): self.remote_env = mock.Mock(spec_set=RemoteEnvironmentWithSlurm) - self.display_mock = mock.Mock(spec_set=IDisplay) + self.display_mock = mock.Mock(spec_set=DisplayTerminal) self.final_zip_downloader = FinalZipDownloader( self.remote_env, self.display_mock ) diff --git a/tests/unit/retriever/test_final_zip_extractor.py b/tests/unit/retriever/test_final_zip_extractor.py index a7dffc4..73a2531 100644 --- a/tests/unit/retriever/test_final_zip_extractor.py +++ b/tests/unit/retriever/test_final_zip_extractor.py @@ -2,7 +2,7 @@ import pytest -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.retrieve.final_zip_extractor import ( @@ -14,7 +14,7 @@ class TestFinalZipExtractor: def setup_method(self): self.file_manager = mock.Mock(spec_set=FileManager) - self.display_mock = mock.Mock(spec_set=IDisplay) + self.display_mock = mock.Mock(spec_set=DisplayTerminal) self.zip_extractor = FinalZipExtractor(self.file_manager, self.display_mock) @pytest.fixture(scope="function") diff --git a/tests/unit/retriever/test_log_downloader.py b/tests/unit/retriever/test_log_downloader.py index 2f729b8..807f7b4 100644 --- a/tests/unit/retriever/test_log_downloader.py +++ b/tests/unit/retriever/test_log_downloader.py @@ -5,7 +5,7 @@ import pytest import antareslauncher.remote_environnement.remote_environment_with_slurm -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, ) @@ -17,7 +17,7 @@ class TestLogDownloader: def setup_method(self): self.remote_env_mock = mock.Mock(spec=RemoteEnvironmentWithSlurm) self.file_manager = mock.Mock() - self.display_mock = mock.Mock(spec_set=IDisplay) + self.display_mock = mock.Mock(spec_set=DisplayTerminal) self.log_downloader = LogDownloader( self.remote_env_mock, self.file_manager, self.display_mock ) diff --git a/tests/unit/retriever/test_retrieve_controller.py b/tests/unit/retriever/test_retrieve_controller.py index 2a970a5..958a29f 100644 --- a/tests/unit/retriever/test_retrieve_controller.py +++ b/tests/unit/retriever/test_retrieve_controller.py @@ -6,7 +6,7 @@ import antareslauncher import antareslauncher.remote_environnement.remote_environment_with_slurm -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, ) @@ -114,7 +114,7 @@ def test_given_a_list_of_done_studies_when_retrieve_all_studies_called_then_mess study = StudyDTO("path") study.done = True study_list = [deepcopy(study), deepcopy(study)] - display_mock = mock.Mock(spec=IDisplay) + display_mock = mock.Mock(spec=DisplayTerminal) my_retriever = RetrieveController( self.data_repo, self.remote_env_mock, diff --git a/tests/unit/retriever/test_server_cleaner.py b/tests/unit/retriever/test_server_cleaner.py index 4e0c77f..8ba756a 100644 --- a/tests/unit/retriever/test_server_cleaner.py +++ b/tests/unit/retriever/test_server_cleaner.py @@ -5,7 +5,7 @@ import pytest import antareslauncher.remote_environnement.remote_environment_with_slurm -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, ) @@ -19,7 +19,7 @@ class TestServerCleaner: def setup_method(self): self.remote_env_mock = mock.Mock(spec=RemoteEnvironmentWithSlurm) - self.display_mock = mock.Mock(spec_set=IDisplay) + self.display_mock = mock.Mock(spec_set=DisplayTerminal) self.remote_server_cleaner = RemoteServerCleaner( self.remote_env_mock, self.display_mock ) diff --git a/tests/unit/retriever/test_study_retriever.py b/tests/unit/retriever/test_study_retriever.py index 0e8b256..358f95f 100644 --- a/tests/unit/retriever/test_study_retriever.py +++ b/tests/unit/retriever/test_study_retriever.py @@ -4,7 +4,7 @@ from antareslauncher.data_repo.data_reporter import DataReporter from antareslauncher.data_repo.idata_repo import IDataRepo -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, @@ -24,7 +24,7 @@ class TestStudyRetriever: def setup_method(self): env = mock.Mock(spec_set=RemoteEnvironmentWithSlurm) - display = mock.Mock(spec_set=IDisplay) + display = mock.Mock(spec_set=DisplayTerminal) file_manager = mock.Mock(spec_set=FileManager) repo = mock.Mock(spec_set=IDataRepo) self.reporter = DataReporter(repo) diff --git a/tests/unit/test_wait_controller.py b/tests/unit/test_wait_controller.py index e7b452c..555dc7a 100644 --- a/tests/unit/test_wait_controller.py +++ b/tests/unit/test_wait_controller.py @@ -2,7 +2,7 @@ import pytest -from antareslauncher.display.idisplay import IDisplay +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.use_cases.wait_loop_controller.wait_controller import ( WaitController, ) @@ -13,7 +13,7 @@ class TestWaitController: def test_countdown_calls_display_message_4_times_if_it_waits_1_seconds( self, ): - display = IDisplay + display = DisplayTerminal() display.show_message = mock.Mock() display.show_message_no_newline = mock.Mock() wait_controller = WaitController(display) From 45152638862744de22dfdc377eee2748c6422af1 Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE <43534797+laurent-laporte-pro@users.noreply.github.com> Date: Wed, 20 Sep 2023 09:53:15 +0200 Subject: [PATCH 06/23] feat(cli): add the `--solver-version` option to the command line (#63) * feat(parameters): handle the `--partition` and `--qos` parameters for the `sbatch` command (#58) * feat(parameters): add partition in MainParameters * refactor: simplify the implementation of `SlurmScriptFeatures.compose_launch_command` * refactor(parameters-reader): simplify the implementation of the parameters reader * refactor(parameters-reader): change the signature of the `SlurmScriptFeatures` constructor * style(parameters-reader): sort imports and reformat source code * feat(parameters-reader): make the `partition` parameter optional * feat(parameters-reader): handle the `--qos` (quality of service) parameter for the `sbatch` command --------- Co-authored-by: Laurent LAPORTE * feat(cli): add the `--solver-version` option to the command line * test: correct unit tests `test_data_repo_tinydb` which were not reentrant * feat(cli): add the `--solver-version` parameter to the command line * test(assets): correct the import used to import the `ASSETS_DIR` constant * chore(typing): correct typing in `remote_environment_with_slurm` module * chore: correct the way class name is displayed with `show_message` --------- Co-authored-by: MartinBelthle <102529366+MartinBelthle@users.noreply.github.com> --- antareslauncher/advanced_launch.py | 11 +- antareslauncher/file_manager/file_manager.py | 16 - antareslauncher/main.py | 1 + antareslauncher/main_option_parser.py | 190 +- .../remote_environment_with_slurm.py | 14 +- .../slurm_script_features.py | 2 +- .../remote_environnement/ssh_connection.py | 2 +- .../create_list/study_list_composer.py | 117 +- .../use_cases/kill_job/job_kill_controller.py | 4 +- .../use_cases/retrieve/state_updater.py | 8 +- .../test_integration_study_list_composer.py | 42 +- tests/unit/assets/__init__.py | 3 + .../013 TS Generation - Solar power/README.md | 3 + .../study.antares | 7 + .../studies/024 Hurdle costs - 1/README.md | 3 + .../024 Hurdle costs - 1/study.antares | 7 + .../069 Hydro Reservoir Model/README.md | 3 + .../069 Hydro Reservoir Model/study.antares | 7 + .../studies/BAD Study Section/README.md | 6 + .../studies/BAD Study Section/study.antares | 6 + .../studies/MISSING Study version/README.md | 5 + .../MISSING Study version/study.antares | 5 + .../studies/SMTA-case/README.md | 3 + .../studies/SMTA-case/study.antares | 7 + .../SMTA-case/user/expansion/candidates.ini | 535 + .../user/expansion/capa/windcapaSEAS.txt | 8760 +++++++++++++++++ .../user/expansion/capa/windcapaUPS.txt | 8760 +++++++++++++++++ .../SMTA-case/user/expansion/settings.ini | 6 + tests/unit/test_data_repo_tinydb.py | 188 +- tests/unit/test_main_option_parser.py | 6 +- tests/unit/test_study_list_composer.py | 582 +- 31 files changed, 18538 insertions(+), 771 deletions(-) create mode 100644 tests/unit/assets/__init__.py create mode 100644 tests/unit/assets/study_list_composer/studies/013 TS Generation - Solar power/README.md create mode 100644 tests/unit/assets/study_list_composer/studies/013 TS Generation - Solar power/study.antares create mode 100644 tests/unit/assets/study_list_composer/studies/024 Hurdle costs - 1/README.md create mode 100644 tests/unit/assets/study_list_composer/studies/024 Hurdle costs - 1/study.antares create mode 100644 tests/unit/assets/study_list_composer/studies/069 Hydro Reservoir Model/README.md create mode 100644 tests/unit/assets/study_list_composer/studies/069 Hydro Reservoir Model/study.antares create mode 100644 tests/unit/assets/study_list_composer/studies/BAD Study Section/README.md create mode 100644 tests/unit/assets/study_list_composer/studies/BAD Study Section/study.antares create mode 100644 tests/unit/assets/study_list_composer/studies/MISSING Study version/README.md create mode 100644 tests/unit/assets/study_list_composer/studies/MISSING Study version/study.antares create mode 100644 tests/unit/assets/study_list_composer/studies/SMTA-case/README.md create mode 100644 tests/unit/assets/study_list_composer/studies/SMTA-case/study.antares create mode 100644 tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/candidates.ini create mode 100644 tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/capa/windcapaSEAS.txt create mode 100644 tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/capa/windcapaUPS.txt create mode 100644 tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/settings.ini diff --git a/antareslauncher/advanced_launch.py b/antareslauncher/advanced_launch.py index 6fc2f61..219bde4 100644 --- a/antareslauncher/advanced_launch.py +++ b/antareslauncher/advanced_launch.py @@ -1,3 +1,4 @@ +import sys from pathlib import Path from antareslauncher.config import Config, get_config_path @@ -15,8 +16,14 @@ def main(): ) parser_parameters: ParserParameters = param_reader.get_parser_parameters() parser: MainOptionParser = MainOptionParser(parser_parameters) - parser.add_basic_arguments().add_advanced_arguments() - arguments = parser.parse_args() + parser.add_basic_arguments(antares_versions=param_reader.antares_versions) + ssh_config_required = parser_parameters.ssh_config_file_is_required + alt_ssh_paths = [ + parser_parameters.ssh_configfile_path_alternate1, + parser_parameters.ssh_configfile_path_alternate1, + ] + parser.add_advanced_arguments(ssh_config_required, alt_ssh_paths=alt_ssh_paths) + arguments = parser.parser.parse_args(sys.argv[1:]) main_parameters: MainParameters = param_reader.get_main_parameters() run_with(arguments=arguments, parameters=main_parameters, show_banner=True) diff --git a/antareslauncher/file_manager/file_manager.py b/antareslauncher/file_manager/file_manager.py index 64fe281..72bb18d 100644 --- a/antareslauncher/file_manager/file_manager.py +++ b/antareslauncher/file_manager/file_manager.py @@ -1,4 +1,3 @@ -import configparser import json import logging import os @@ -13,21 +12,6 @@ def __init__(self, display_terminal: DisplayTerminal): self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") self.display = display_terminal - def get_config_from_file(self, file_path): - """Reads the configuration file of antares - - Args: - file_path: Path to the configuration file - - Returns: - The corresponding config object - """ - self.logger.info(f"Getting config from file {file_path}") - config_parser = configparser.ConfigParser() - if Path(file_path).exists(): - config_parser.read(file_path) - return config_parser - def listdir_of(self, directory): """Make a list of all the folders inside a directory diff --git a/antareslauncher/main.py b/antareslauncher/main.py index f05eced..c487d34 100644 --- a/antareslauncher/main.py +++ b/antareslauncher/main.py @@ -150,6 +150,7 @@ def run_with( post_processing=arguments.post_processing, antares_versions_on_remote_server=parameters.antares_versions_on_remote_server, other_options=arguments.other_options or "", + antares_version=arguments.antares_version, ), ) launch_controller = LaunchController( diff --git a/antareslauncher/main_option_parser.py b/antareslauncher/main_option_parser.py index 7030e82..b6a099d 100644 --- a/antareslauncher/main_option_parser.py +++ b/antareslauncher/main_option_parser.py @@ -1,11 +1,13 @@ from __future__ import annotations import argparse +import datetime import getpass import pathlib +import typing as t from argparse import RawTextHelpFormatter from dataclasses import dataclass -from typing import List, Optional +from pathlib import Path @dataclass @@ -17,92 +19,79 @@ class ParserParameters: log_dir: str finished_dir: str ssh_config_file_is_required: bool - ssh_configfile_path_alternate1: Optional[pathlib.Path] - ssh_configfile_path_alternate2: Optional[pathlib.Path] + ssh_configfile_path_alternate1: t.Optional[pathlib.Path] + ssh_configfile_path_alternate2: t.Optional[pathlib.Path] class MainOptionParser: def __init__(self, parameters: ParserParameters) -> None: self.parser = argparse.ArgumentParser(formatter_class=RawTextHelpFormatter) - self.default_argument_values = {} - self.parameters = parameters - self._set_default_argument_values() - - def _set_default_argument_values(self) -> None: - """Fills the "default_argument_values" dictionary""" - self.default_argument_values = { + defaults = { "wait_mode": False, - "wait_time": self.parameters.default_wait_time, - "studies_in": str(self.parameters.studies_in_dir), - "output_dir": str(self.parameters.finished_dir), + "wait_time": parameters.default_wait_time, + "studies_in": str(parameters.studies_in_dir), + "output_dir": str(parameters.finished_dir), "check_queue": False, - "time_limit": self.parameters.default_time_limit, - "json_ssh_config": look_for_default_ssh_conf_file(self.parameters), - "log_dir": str(self.parameters.log_dir), - "n_cpu": self.parameters.default_n_cpu, + "time_limit": parameters.default_time_limit, + "json_ssh_config": look_for_default_ssh_conf_file(parameters), + "log_dir": str(parameters.log_dir), + "n_cpu": parameters.default_n_cpu, + "antares_version": 0, "job_id_to_kill": None, "xpansion_mode": None, "version": False, "post_processing": False, "other_options": None, } + self.parser.set_defaults(**defaults) - def parse_args(self, args: List[str] = None) -> argparse.Namespace: - """Parses the args given with the selected options. - If args is None, the standard input will be parsed - - Args: - args: Arguments given to the program - - Returns: - argparse.Namespace: namespace containing all the options - - """ - output: argparse.Namespace = self.parser.parse_args(args) - - for key, value in self.default_argument_values.items(): - if not hasattr(output, key): - setattr(output, key, value) - return output - - def add_basic_arguments(self) -> MainOptionParser: + def add_basic_arguments( + self, *, antares_versions: t.Sequence[str] = () + ) -> MainOptionParser: """Adds to the parser all the arguments for the light mode""" self.parser.add_argument( "-w", "--wait-mode", action="store_true", dest="wait_mode", - default=self.default_argument_values["wait_mode"], - help="Activate the wait mode: the Antares_Launcher waits for all the jobs to finish\n" - "it check every WAIT_TIME seconds (default value = 900 = 15 minutes).", + help=( + "Activate the wait mode: the Antares_Launcher waits for all the jobs to finish\n" + "it check every WAIT_TIME seconds (default value = 900 = 15 minutes)." + ), ) + wait_time = self.parser.get_default("wait_time") + delta = datetime.timedelta(seconds=wait_time) self.parser.add_argument( "--wait-time", dest="wait_time", type=int, - default=self.default_argument_values["wait_time"], - help="Number of seconds between each verification of the end of the simulations\n" - "changes the value of WAIT_TIME used for the wait-mode.", + help=( + "Number of seconds between each verification of the end of the simulations\n" + "changes the value of WAIT_TIME used for the wait-mode.\n" + f"The default value will be used: {delta.total_seconds():.0f}s = {delta}." + ), ) self.parser.add_argument( "-i", "--studies-in-dir", dest="studies_in", - default=self.default_argument_values["studies_in"], - help="Directory containing the studies to be executed.\n" - "If the directory does not exist, it will be created (empty).\n" - "if no directory is indicated, the default value will be used STUDIES-IN", + help=( + "Directory containing the studies to be executed.\n" + "If the directory does not exist, it will be created (empty).\n" + "if no directory is indicated, the default value will be used STUDIES-IN" + ), ) self.parser.add_argument( "-o", "--output-dir", dest="output_dir", - default=self.default_argument_values["output_dir"], - help="Directory where the finished studies will be downloaded and extracted.\n" - 'If the directory does not exist, it will be created (default value "FINISHED").', + help=( + "Directory where the finished studies will be downloaded and extracted.\n" + 'If the directory does not exist, it will be created (default value "FINISHED").' + ), ) self.parser.add_argument( @@ -110,33 +99,38 @@ def add_basic_arguments(self) -> MainOptionParser: "--check-queue", action="store_true", dest="check_queue", - default=self.default_argument_values["check_queue"], - help="Displays from the remote queue all job statuses.\n" - "If the option is used, it will override the standard execution.\n" - "It can be overridden by the kill job option (-k).", + help=( + "Displays from the remote queue all job statuses.\n" + "If the option is used, it will override the standard execution.\n" + "It can be overridden by the kill job option (-k)." + ), ) - seconds_in_hour = 3600 + + time_limit = self.parser.get_default("time_limit") + delta = datetime.timedelta(seconds=time_limit) self.parser.add_argument( "-t", "--time-limit", dest="time_limit", type=int, - default=self.default_argument_values["time_limit"], - help="Time limit in seconds of a single job.\n" - "If nothing is specified here and" - "if the study is not initialised with a specific value,\n" - f"the default value will be used: {self.parameters.default_time_limit}={int(self.parameters.default_time_limit / seconds_in_hour)}h.", + help=( + "Time limit in seconds of a single job.\n" + "If nothing is specified here and" + "if the study is not initialised with a specific value,\n" + f"The default value will be used: {delta.total_seconds():.0f}s = {delta}." + ), ) self.parser.add_argument( "-x", "--xpansion-mode", dest="xpansion_mode", - default=None, - help="Activate the xpansion mode:\n" - "Antares_Launcher will launch all the new studies in xpansion mode if\n" - "the studies contains the information necessary for AntaresXpansion.\n" - 'if rhe flag is set to "r", the xpansion mode will be activated with the R version.\n', + help=( + "Activate the xpansion mode:\n" + "Antares_Launcher will launch all the new studies in xpansion mode if\n" + "the studies contains the information necessary for AntaresXpansion.\n" + 'if rhe flag is set to "r", the xpansion mode will be activated with the R version.\n' + ), ) self.parser.add_argument( @@ -144,7 +138,6 @@ def add_basic_arguments(self) -> MainOptionParser: "--version", action="store_true", dest="version", - default=False, help="Shows the version of Antares_Launcher", ) @@ -153,7 +146,6 @@ def add_basic_arguments(self) -> MainOptionParser: "--post-processing", action="store_true", dest="post_processing", - default=False, help='Enables the post processing of the antares study by executing the "post_processing.R" file', ) @@ -168,71 +160,85 @@ def add_basic_arguments(self) -> MainOptionParser: "--kill-job", dest="job_id_to_kill", type=int, - default=self.default_argument_values["job_id_to_kill"], - help=f"JobID of the run to be cancelled on the remote server.\n" - f"the JobID can be retrieved with option -q to show the queue." - f"If option is given it overrides the -q and the standard execution.", + help=( + f"JobID of the run to be cancelled on the remote server.\n" + f"the JobID can be retrieved with option -q to show the queue." + f"If option is given it overrides the -q and the standard execution." + ), + ) + + self.parser.add_argument( + "--solver-version", + dest="antares_version", + type=int, + choices=[int(v) for v in antares_versions], + help="Antares Solver version to use for simulation", ) + return self - def add_advanced_arguments(self) -> MainOptionParser: + def add_advanced_arguments( + self, ssh_config_required: bool, *, alt_ssh_paths: t.Sequence[Path] = () + ) -> MainOptionParser: """Adds to the parser all the arguments for the advanced mode""" + n_cpu = self.parser.get_default("n_cpu") self.parser.add_argument( "-n", "--n-cores", dest="n_cpu", type=int, - default=self.default_argument_values["n_cpu"], - help=f"Number of cores to be used for a single job.\n" - f"If nothing is specified here and " - f"if the study is not initialised with a specific value,\n" - f"the default value will be used: n_cpu=={self.parameters.default_n_cpu}", + help=( + f"Number of cores to be used for a single job.\n" + f"If nothing is specified here and " + f"if the study is not initialised with a specific value,\n" + f"the default value will be used: n_cpu=={n_cpu}" + ), ) self.parser.add_argument( "--log-dir", dest="log_dir", - default=self.default_argument_values["log_dir"], - help="Directory where the logs of the jobs will be found.\n" - "If the directory does not exist, it will be created.", + help=( + "Directory where the logs of the jobs will be found.\n" + "If the directory does not exist, it will be created." + ), ) + ssh_paths = "\n".join(f"'{p}'" for p in dict.fromkeys(alt_ssh_paths)) self.parser.add_argument( "--ssh-settings-file", dest="json_ssh_config", - default=self.default_argument_values["json_ssh_config"], - required=self.parameters.ssh_config_file_is_required, - help=f"Path to the configuration file for the ssh connection.\n" - f"If no value is given, " - f"it will look for it in default location with this order:\n" - f"1st: {self.parameters.ssh_configfile_path_alternate1}\n" - f"2nd: {self.parameters.ssh_configfile_path_alternate2}\n", + required=ssh_config_required, + help=( + f"Path to the configuration file for the ssh connection.\n" + f"If no value is given, " + f"it will look for it in default location with this order:\n" + f"{ssh_paths}" + ), ) return self def look_for_default_ssh_conf_file( parameters: ParserParameters, -) -> pathlib.Path: +) -> t.Union[None, pathlib.Path]: """Checks if the ssh config file exists. Returns: path to the ssh config file is it exists, None otherwise """ - ssh_conf_file: pathlib.Path if ( parameters.ssh_configfile_path_alternate1 and parameters.ssh_configfile_path_alternate1.is_file() ): - ssh_conf_file = parameters.ssh_configfile_path_alternate1 + return parameters.ssh_configfile_path_alternate1 elif ( parameters.ssh_configfile_path_alternate2 and parameters.ssh_configfile_path_alternate2.is_file() ): - ssh_conf_file = parameters.ssh_configfile_path_alternate2 + return parameters.ssh_configfile_path_alternate2 else: - ssh_conf_file = None - return ssh_conf_file + return None def get_default_db_name() -> str: diff --git a/antareslauncher/remote_environnement/remote_environment_with_slurm.py b/antareslauncher/remote_environnement/remote_environment_with_slurm.py index 3ea8b30..535f838 100644 --- a/antareslauncher/remote_environnement/remote_environment_with_slurm.py +++ b/antareslauncher/remote_environnement/remote_environment_with_slurm.py @@ -6,8 +6,8 @@ import socket import textwrap import time +import typing as t from pathlib import Path, PurePosixPath -from typing import List, Optional from antareslauncher.remote_environnement.slurm_script_features import ( ScriptParametersDTO, @@ -250,7 +250,7 @@ def get_job_state_flags( *, attempts=5, sleep_time=0.5, - ) -> [bool, bool, bool]: + ) -> t.Tuple[bool, bool, bool]: """ Retrieves the current state of a SLURM job with the given job ID and name. @@ -310,7 +310,7 @@ def _retrieve_slurm_control_state( self, job_id: int, job_name: str, - ) -> Optional[JobStateCodes]: + ) -> t.Optional[JobStateCodes]: """ Use the `scontrol` command to retrieve job status information in SLURM. See: https://slurm.schedmd.com/scontrol.html @@ -346,7 +346,7 @@ def _retrieve_slurm_acct_state( *, attempts: int = 5, sleep_time: float = 0.5, - ) -> Optional[JobStateCodes]: + ) -> t.Optional[JobStateCodes]: # Construct the command line arguments used to check the jobs state. # See the man page: https://slurm.schedmd.com/sacct.html # noinspection SpellCheckingInspection @@ -365,7 +365,7 @@ def _retrieve_slurm_acct_state( # Makes several attempts to get the job state. # I don't really know why, but it's better to reproduce the old behavior. - output: Optional[str] + output: t.Optional[str] last_error: str = "" for attempt in range(attempts): output, error = self.connection.execute_command(command) @@ -413,7 +413,7 @@ def upload_file(self, src): dst = f"{self.remote_base_path}/{Path(src).name}" return self.connection.upload_file(src, dst) - def download_logs(self, study: StudyDTO) -> List[Path]: + def download_logs(self, study: StudyDTO) -> t.List[Path]: """ Download the slurm logs of a given study. @@ -435,7 +435,7 @@ def download_logs(self, study: StudyDTO) -> List[Path]: remove=study.finished, ) - def download_final_zip(self, study: StudyDTO) -> Optional[Path]: + def download_final_zip(self, study: StudyDTO) -> t.Optional[Path]: """ Download the final ZIP file for the specified study from the remote server and save it to the local output directory. diff --git a/antareslauncher/remote_environnement/slurm_script_features.py b/antareslauncher/remote_environnement/slurm_script_features.py index b233bbf..adb017b 100644 --- a/antareslauncher/remote_environnement/slurm_script_features.py +++ b/antareslauncher/remote_environnement/slurm_script_features.py @@ -81,7 +81,7 @@ def compose_launch_command( for arg in [ self.solver_script_path, script_params.input_zipfile_name, - script_params.antares_version, + str(script_params.antares_version), _job_type, str(script_params.post_processing), script_params.other_options, diff --git a/antareslauncher/remote_environnement/ssh_connection.py b/antareslauncher/remote_environnement/ssh_connection.py index ed8a92a..e74b98b 100644 --- a/antareslauncher/remote_environnement/ssh_connection.py +++ b/antareslauncher/remote_environnement/ssh_connection.py @@ -140,7 +140,7 @@ def __init__(self, config: dict = None): "password" (not compulsory if private_key_file is given), "private_key_file": path to private rsa key """ super(SshConnection, self).__init__() - self.logger = logging.getLogger(f"{__name__}.{__class__.__name__}") + self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") self.__client = None self.__home_dir = None self.timeout = 10 diff --git a/antareslauncher/use_cases/create_list/study_list_composer.py b/antareslauncher/use_cases/create_list/study_list_composer.py index 60ae581..757b12a 100644 --- a/antareslauncher/use_cases/create_list/study_list_composer.py +++ b/antareslauncher/use_cases/create_list/study_list_composer.py @@ -1,6 +1,7 @@ +import configparser +import typing as t from dataclasses import dataclass from pathlib import Path -from typing import List, Optional from antareslauncher.data_repo.idata_repo import IDataRepo from antareslauncher.display.display_terminal import DisplayTerminal @@ -8,17 +9,42 @@ from antareslauncher.study_dto import Modes, StudyDTO +def get_solver_version(study_dir: Path, *, default: int = 0) -> int: + """ + Retrieve the solver version number or else the study version number + from the "study.antares" file. + + Args: + study_dir: Directory path which contains the "study.antares" file. + default: Default version number to use if the version is not found. + + Returns: + The value of `solver_version`, `version` or the default version number. + """ + study_path = study_dir.joinpath("study.antares") + config = configparser.ConfigParser() + config.read(study_path) + if "antares" not in config: + return default + section = config["antares"] + for key in "solver_version", "version": + if key in section: + return int(section[key]) + return default + + @dataclass class StudyListComposerParameters: studies_in_dir: str time_limit: int log_dir: str n_cpu: int - xpansion_mode: Optional[str] + xpansion_mode: t.Optional[str] output_dir: str post_processing: bool - antares_versions_on_remote_server: List[str] + antares_versions_on_remote_server: t.List[str] other_options: str + antares_version: int = 0 @dataclass @@ -41,11 +67,12 @@ def __init__( self.output_dir = parameters.output_dir self.post_processing = parameters.post_processing self.other_options = parameters.other_options + self.antares_version = parameters.antares_version self._new_study_added = False self.DEFAULT_JOB_LOG_DIR_PATH = str(Path(self.log_dir) / "JOB_LOGS") - self.ANTARES_VERSIONS_ON_REMOTE_SERVER = ( - parameters.antares_versions_on_remote_server - ) + self.ANTARES_VERSIONS_ON_REMOTE_SERVER = [ + int(v) for v in parameters.antares_versions_on_remote_server + ] def get_list_of_studies(self): """Retrieve the list of studies from the repo @@ -78,51 +105,6 @@ def _create_study(self, path, antares_version, xpansion_mode: str): return new_study - def get_antares_version(self, directory_path: str): - """Checks if the directory is an antares study and returns the version - - Checks if the directory is an antares study by checking the presence of the study.antares file - and by checking the presence of the 'antares' field in this file. - - Args: - directory_path: Path of the directory to test - - Returns: - The version if the directory is an antares study, None otherwise - """ - file_path = Path(directory_path) / "study.antares" - config = self._file_manager.get_config_from_file(file_path) - if "antares" in config: - solver_version = config["antares"].get("solver_version", None) - return solver_version or config["antares"].get("version", None) - - def _is_valid_antares_study(self, antares_version): - if antares_version is None: - self._display.show_message( - "... not a valid Antares study", - __name__ + "." + __class__.__name__, - ) - return False - - elif antares_version in self.ANTARES_VERSIONS_ON_REMOTE_SERVER: - return True - else: - message = f"... Antares version ({antares_version}) is not supported (supported versions: {self.ANTARES_VERSIONS_ON_REMOTE_SERVER})" - self._display.show_message( - message, - __name__ + "." + __class__.__name__, - ) - return False - - def _is_there_candidates_file(self, directory_path: Path): - candidates_file_path = str( - Path.joinpath(directory_path, "user", "expansion", "candidates.ini") - ) - return self._file_manager.file_exists(candidates_file_path) - - def _is_xpansion_study(self, xpansion_study_path: str): - return self._is_there_candidates_file(Path(xpansion_study_path)) - def update_study_database(self): """List all directories inside the STUDIES_IN_DIR folder, if a directory is a valid antares study and is new, then creates a StudyDTO object then saves it in the repo @@ -154,16 +136,35 @@ def _update_database_with_new_study( ) self._update_database_with_study(buffer_study) - def _update_database_with_directory(self, directory_path): - antares_version = self.get_antares_version(directory_path) - if self._is_valid_antares_study(antares_version): - is_xpansion_study = self._is_xpansion_study(directory_path) - xpansion_mode = self.xpansion_mode if is_xpansion_study else None + def _update_database_with_directory(self, directory_path: t.Union[str, Path]): + directory_path = Path(directory_path) + solver_version = get_solver_version(directory_path) + antares_version = self.antares_version or solver_version + if not antares_version: + self._display.show_message( + "... not a valid Antares study", + __name__ + "." + self.__class__.__name__, + ) + elif antares_version not in self.ANTARES_VERSIONS_ON_REMOTE_SERVER: + message = ( + f"... Antares version {antares_version} is not supported" + f" (supported versions: {self.ANTARES_VERSIONS_ON_REMOTE_SERVER})" + ) + self._display.show_message( + message, + __name__ + "." + self.__class__.__name__, + ) + else: + candidates_file_path = directory_path.joinpath( + "user", "expansion", "candidates.ini" + ) + is_xpansion_study = candidates_file_path.is_file() + xpansion_mode = is_xpansion_study and self.xpansion_mode valid_xpansion_candidate = ( self.xpansion_mode in ["r", "cpp"] and is_xpansion_study ) - valid_antares_candidate = self.xpansion_mode is None + valid_antares_candidate = not self.xpansion_mode if valid_antares_candidate or valid_xpansion_candidate: self._update_database_with_new_study( @@ -181,6 +182,6 @@ def _add_study_to_database(self, buffer_study): f"(mode = {buffer_study.run_mode.name}, " f"version={buffer_study.antares_version}): " f'"{buffer_study.path}"', - __name__ + "." + __class__.__name__, + __name__ + "." + self.__class__.__name__, ) self._new_study_added = True diff --git a/antareslauncher/use_cases/kill_job/job_kill_controller.py b/antareslauncher/use_cases/kill_job/job_kill_controller.py index 6df1e6a..dbf0091 100644 --- a/antareslauncher/use_cases/kill_job/job_kill_controller.py +++ b/antareslauncher/use_cases/kill_job/job_kill_controller.py @@ -24,11 +24,11 @@ def kill_job(self, job_id: int): """ if self._check_if_job_is_killable(job_id): self.display.show_message( - f"Killing job {job_id}", __name__ + "." + __class__.__name__ + f"Killing job {job_id}", __name__ + "." + self.__class__.__name__ ) self.env.kill_remote_job(job_id) else: self.display.show_message( f"You are not authorized to kill job {job_id}", - __name__ + "." + __class__.__name__, + __name__ + "." + self.__class__.__name__, ) diff --git a/antareslauncher/use_cases/retrieve/state_updater.py b/antareslauncher/use_cases/retrieve/state_updater.py index 93e43c5..559531a 100644 --- a/antareslauncher/use_cases/retrieve/state_updater.py +++ b/antareslauncher/use_cases/retrieve/state_updater.py @@ -22,18 +22,18 @@ def _show_job_state_message(self, study: StudyDTO): if study.done is True: self._display.show_message( f'"{Path(study.path).name}" (JOBID={study.job_id}): everything is done', - __name__ + "." + __class__.__name__, + __name__ + "." + self.__class__.__name__, ) else: if study.job_id: self._display.show_message( f'"{Path(study.path).name}" (JOBID={study.job_id}): {study.job_state}', - __name__ + "." + __class__.__name__, + __name__ + "." + self.__class__.__name__, ) else: self._display.show_error( f'"{Path(study.path).name}": Job was not submitted', - __name__ + "." + __class__.__name__, + __name__ + "." + self.__class__.__name__, ) def run(self, study: StudyDTO) -> StudyDTO: @@ -72,7 +72,7 @@ def run_on_list(self, study_list: List[StudyDTO]): message = "Checking status of the studies:" self._display.show_message( message, - __name__ + "." + __class__.__name__, + __name__ + "." + self.__class__.__name__, ) study_list.sort(key=lambda x: x.done, reverse=True) for study in study_list: diff --git a/tests/integration/test_integration_study_list_composer.py b/tests/integration/test_integration_study_list_composer.py index 1155ba1..889cd8a 100644 --- a/tests/integration/test_integration_study_list_composer.py +++ b/tests/integration/test_integration_study_list_composer.py @@ -1,8 +1,10 @@ -from pathlib import Path from unittest import mock import pytest +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.use_cases.create_list.study_list_composer import ( StudyListComposer, StudyListComposerParameters, @@ -11,10 +13,13 @@ class TestIntegrationStudyListComposer: def setup_method(self): + self.repo = mock.Mock(spec=DataRepoTinydb) + self.file_manager = mock.Mock(spec=FileManager) + self.display = mock.Mock(spec=DisplayTerminal) self.study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock(), - display=mock.Mock(), + repo=self.repo, + file_manager=self.file_manager, + display=self.display, parameters=StudyListComposerParameters( studies_in_dir="studies_in", time_limit=42, @@ -29,27 +34,14 @@ def setup_method(self): ) @pytest.mark.integration_test - def test_study_list_composer_get_list_of_studies_calls_repo_get_list_of_studies( - self, - ): + def test_get_list_of_studies(self): self.study_list_composer.get_list_of_studies() - self.study_list_composer._repo.get_list_of_studies.assert_called_once() + self.repo.get_list_of_studies.assert_called_once_with() @pytest.mark.integration_test - def test_study_list_composer_get_antares_version_calls_file_manager_get_config_from_file( - self, - ): - # given - directory_path = "directory_path" - file_path = Path(directory_path) / "study.antares" - self.study_list_composer._file_manager.get_config_from_file = mock.Mock( - return_value={} - ) - # when - self.study_list_composer.get_antares_version(directory_path) - # then - self.study_list_composer._file_manager.get_config_from_file.assert_called_once_with( - file_path - ) - - # TODO: test_update_study_database already in unit tests? + def test_update_study_database(self): + self.file_manager.listdir_of = mock.Mock(return_value=["study1", "study2"]) + self.file_manager.is_dir = mock.Mock(return_value=True) + self.study_list_composer.xpansion_mode = "r" # "r", "cpp" or None + self.study_list_composer.update_study_database() + self.display.show_message.assert_called() diff --git a/tests/unit/assets/__init__.py b/tests/unit/assets/__init__.py new file mode 100644 index 0000000..773f16e --- /dev/null +++ b/tests/unit/assets/__init__.py @@ -0,0 +1,3 @@ +from pathlib import Path + +ASSETS_DIR = Path(__file__).parent.resolve() diff --git a/tests/unit/assets/study_list_composer/studies/013 TS Generation - Solar power/README.md b/tests/unit/assets/study_list_composer/studies/013 TS Generation - Solar power/README.md new file mode 100644 index 0000000..641398f --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/013 TS Generation - Solar power/README.md @@ -0,0 +1,3 @@ +# Description + +This directory contains a study with a valid `version` number and a valide `solver_version` number. \ No newline at end of file diff --git a/tests/unit/assets/study_list_composer/studies/013 TS Generation - Solar power/study.antares b/tests/unit/assets/study_list_composer/studies/013 TS Generation - Solar power/study.antares new file mode 100644 index 0000000..9918bed --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/013 TS Generation - Solar power/study.antares @@ -0,0 +1,7 @@ +[antares] +version = 800 +caption = 013 TS Generation - Solar power +created = 1246524135 +lastsave = 1608213721 +author = Robert SMITH +solver_version = 850 diff --git a/tests/unit/assets/study_list_composer/studies/024 Hurdle costs - 1/README.md b/tests/unit/assets/study_list_composer/studies/024 Hurdle costs - 1/README.md new file mode 100644 index 0000000..4bd9f24 --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/024 Hurdle costs - 1/README.md @@ -0,0 +1,3 @@ +# Description + +This directory contains a classic study with a valid `version` number. diff --git a/tests/unit/assets/study_list_composer/studies/024 Hurdle costs - 1/study.antares b/tests/unit/assets/study_list_composer/studies/024 Hurdle costs - 1/study.antares new file mode 100644 index 0000000..0e69154 --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/024 Hurdle costs - 1/study.antares @@ -0,0 +1,7 @@ +[antares] +version = 840 +caption = 024 Hurdle costs - 1 +created = 1258636851 +lastsave = 1608213740 +author = Pink Floyd + diff --git a/tests/unit/assets/study_list_composer/studies/069 Hydro Reservoir Model/README.md b/tests/unit/assets/study_list_composer/studies/069 Hydro Reservoir Model/README.md new file mode 100644 index 0000000..539264e --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/069 Hydro Reservoir Model/README.md @@ -0,0 +1,3 @@ +# Description + +This directory contains an "old" study: `version` < 800. \ No newline at end of file diff --git a/tests/unit/assets/study_list_composer/studies/069 Hydro Reservoir Model/study.antares b/tests/unit/assets/study_list_composer/studies/069 Hydro Reservoir Model/study.antares new file mode 100644 index 0000000..97ae384 --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/069 Hydro Reservoir Model/study.antares @@ -0,0 +1,7 @@ +[antares] +version = 740 +caption = 069 Hydro Reservoir Model +created = 1293630068 +lastsave = 1608214118 +author = Vercingétorix + diff --git a/tests/unit/assets/study_list_composer/studies/BAD Study Section/README.md b/tests/unit/assets/study_list_composer/studies/BAD Study Section/README.md new file mode 100644 index 0000000..e87f6be --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/BAD Study Section/README.md @@ -0,0 +1,6 @@ +# Description + +This directory contains a BAD study. + +The content of the file [study.antares](study.antares) is wrong because the section is named `[solaris]` instead +of `[antares]`. \ No newline at end of file diff --git a/tests/unit/assets/study_list_composer/studies/BAD Study Section/study.antares b/tests/unit/assets/study_list_composer/studies/BAD Study Section/study.antares new file mode 100644 index 0000000..50d9004 --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/BAD Study Section/study.antares @@ -0,0 +1,6 @@ +[solaris] +version = 820 +caption = BAD Study Section +created = 1258636851 +lastsave = 1608213740 +author = Luc BESSON diff --git a/tests/unit/assets/study_list_composer/studies/MISSING Study version/README.md b/tests/unit/assets/study_list_composer/studies/MISSING Study version/README.md new file mode 100644 index 0000000..d4787fe --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/MISSING Study version/README.md @@ -0,0 +1,5 @@ +# Description + +This directory contains a BAD study. + +The content of the file [study.antares](study.antares) is wrong because the `version` option is missing in the `[antares]` section. \ No newline at end of file diff --git a/tests/unit/assets/study_list_composer/studies/MISSING Study version/study.antares b/tests/unit/assets/study_list_composer/studies/MISSING Study version/study.antares new file mode 100644 index 0000000..9879b83 --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/MISSING Study version/study.antares @@ -0,0 +1,5 @@ +[solaris] +caption = MISSING Study version +created = 1258636851 +lastsave = 1608213740 +author = Lady GAGA diff --git a/tests/unit/assets/study_list_composer/studies/SMTA-case/README.md b/tests/unit/assets/study_list_composer/studies/SMTA-case/README.md new file mode 100644 index 0000000..4a76da0 --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/SMTA-case/README.md @@ -0,0 +1,3 @@ +# Description + +This directory contains a study parametrized for Xpansion. diff --git a/tests/unit/assets/study_list_composer/studies/SMTA-case/study.antares b/tests/unit/assets/study_list_composer/studies/SMTA-case/study.antares new file mode 100644 index 0000000..01ad4a4 --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/SMTA-case/study.antares @@ -0,0 +1,7 @@ +[antares] +version = 810 +caption = SMTA-case +created = 1480683452 +lastsave = 1555333928 +author = John DOE + diff --git a/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/candidates.ini b/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/candidates.ini new file mode 100644 index 0000000..9c514f0 --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/candidates.ini @@ -0,0 +1,535 @@ +[1] +name = gridNEASUPS +link = North-East Asia - UPS +annual-cost-per-mw = 35104 +max-investment = 18750000 + +[2] +name = gridCASNEAS +link = Central Asia - North-East Asia +annual-cost-per-mw = 36454 +max-investment = 18750000 + +[3] +name = gridCASUPS +link = Central Asia - UPS +annual-cost-per-mw = 27003 +max-investment = 18750000 + +[4] +name = gridEURUPS +link = Europe - UPS +annual-cost-per-mw = 36454 +max-investment = 18750000 + +[5] +name = gridEURMEAST +link = Europe - Middle East +annual-cost-per-mw = 51306 +max-investment = 18750000 + +[6] +name = gridEURNAF +link = Europe - North Africa +annual-cost-per-mw = 85585 +max-investment = 18750000 + +[7] +name = gridAFRNAF +link = Africa - North Africa +annual-cost-per-mw = 87086 +max-investment = 18750000 + +[8] +name = gridAFRMEAST +link = Africa - Middle East +annual-cost-per-mw = 52019 +max-investment = 18750000 + +[9] +name = gridMEASTNAF +link = Middle East - North Africa +annual-cost-per-mw = 36454 +max-investment = 18750000 + +[10] +name = gridMEASTUPS +link = Middle East - UPS +annual-cost-per-mw = 55357 +max-investment = 18750000 + +[11] +name = gridCASMEAST +link = Central Asia - Middle East +annual-cost-per-mw = 36454 +max-investment = 18750000 + +[12] +name = gridCASSAS +link = Central Asia - South Asia +annual-cost-per-mw = 10651 +max-investment = 18750000 + +[13] +name = gridNEASSAS +link = North-East Asia - South Asia +annual-cost-per-mw = 27003 +max-investment = 18750000 + +[14] +name = gridSASSEAS +link = South Asia - South-East Asia +annual-cost-per-mw = 29704 +max-investment = 18750000 + +[15] +name = gridNEASSEAS +link = North-East Asia - South-East Asia +annual-cost-per-mw = 10651 +max-investment = 18750000 + +[16] +name = gridOCEASEAS +link = Oceania - South-East Asia +annual-cost-per-mw = 232603 +max-investment = 18750000 + +[17] +name = gridNAMUPS +link = North America - UPS +annual-cost-per-mw = 149943 +max-investment = 18750000 + +[18] +name = gridNAMNAT +link = North America - North Atlantic +annual-cost-per-mw = 123615 +max-investment = 18750000 + +[19] +name = gridEURNAT +link = Europe - North Atlantic +annual-cost-per-mw = 213476 +max-investment = 18750000 + +[20] +name = gridLAMNAM +link = Latin America - North America +annual-cost-per-mw = 48606 +max-investment = 18750000 + +[21] +name = PVAFR +link = Africa - PV_AFR +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaAFR.txt + +[22] +name = PVCAS +link = Central Asia - PV_CAS +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaCAS.txt + +[23] +name = PVEUR +link = Europe - PV_EUR +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaEUR.txt + +[24] +name = PVLAM +link = Latin America - PV_LAM +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaLAM.txt + +[25] +name = PVMEAST +link = Middle East - PV_MEAST +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaMEAST.txt + +[26] +name = PVNAF +link = North Africa - PV_NAF +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaNAF.txt + +[27] +name = PVNAM +link = North America - PV_NAM +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaNAM.txt + +[28] +name = PVNAT +link = North Atlantic - PV_NAT +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaNAT.txt + +[29] +name = PVNEAS +link = North-East Asia - PV_NEAS +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaNEAS.txt + +[30] +name = PVOCEA +link = Oceania - PV_OCEA +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaOCEA.txt + +[31] +name = PVSAS +link = PV_SAS - South Asia +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaSAS.txt + +[32] +name = PVSEAS +link = PV_SEAS - South-East Asia +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaSEAS.txt + +[33] +name = PVUPS +link = PV_UPS - UPS +annual-cost-per-mw = 42910 +max-investment = 100000000 +link-profile = solarcapaUPS.txt + +[34] +name = windAFR +link = Africa - wind_AFR +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaAFR.txt + +[35] +name = windCAS +link = Central Asia - wind_CAS +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaCAS.txt + +[36] +name = windEUR +link = Europe - wind_EUR +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaEUR.txt + +[37] +name = windLAM +link = Latin America - wind_LAM +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaLAM.txt + +[38] +name = windMEAST +link = Middle East - wind_MEAST +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaMEAST.txt + +[39] +name = windNAF +link = North Africa - wind_NAF +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaNAF.txt + +[40] +name = windNAM +link = North America - wind_NAM +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaNAM.txt + +[41] +name = windNAT +link = North Atlantic - wind_NAT +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaNAT.txt + +[42] +name = windNEAS +link = North-East Asia - wind_NEAS +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaNEAS.txt + +[43] +name = windOCEA +link = Oceania - wind_OCEA +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaOCEA.txt + +[44] +name = windSAS +link = South Asia - wind_SAS +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaSAS.txt + +[45] +name = windSEAS +link = South-East Asia - wind_SEAS +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaSEAS.txt + +[46] +name = windUPS +link = UPS - wind_UPS +annual-cost-per-mw = 92680 +max-investment = 100000000 +link-profile = windcapaUPS.txt + +[47] +name = OCGTAFR +link = Africa - OCGT_AFR +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[48] +name = OCGTCAS +link = Central Asia - OCGT_CAS +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[49] +name = OCGTEUR +link = Europe - OCGT_EUR +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[50] +name = OCGTLAM +link = Latin America - OCGT_LAM +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[51] +name = OCGTMEAST +link = Middle East - OCGT_MEAST +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[52] +name = OCGTNAF +link = North Africa - OCGT_NAF +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[53] +name = OCGTNAM +link = North America - OCGT_NAM +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[54] +name = OCGTNAT +link = North Atlantic - OCGT_NAT +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[55] +name = OCGTNEAS +link = North-East Asia - OCGT_NEAS +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[56] +name = OCGTOCEA +link = Oceania - OCGT_OCEA +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[57] +name = OCGTSAS +link = OCGT_SAS - South Asia +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[58] +name = OCGTSEAS +link = OCGT_SEAS - South-East Asia +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[59] +name = OCGTUPS +link = OCGT_UPS - UPS +annual-cost-per-mw = 46740 +max-investment = 100000000 + +[60] +name = CCGTAFR +link = Africa - CCGT_AFR +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[61] +name = CCGTCAS +link = CCGT_CAS - Central Asia +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[62] +name = CCGTEUR +link = CCGT_EUR - Europe +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[63] +name = CCGTLAM +link = CCGT_LAM - Latin America +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[64] +name = CCGTMEAST +link = CCGT_MEAST - Middle East +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[65] +name = CCGTNAF +link = CCGT_NAF - North Africa +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[66] +name = CCGTNAM +link = CCGT_NAM - North America +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[67] +name = CCGTNAT +link = CCGT_NAT - North Atlantic +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[68] +name = CCGTNEAS +link = CCGT_NEAS - North-East Asia +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[69] +name = CCGTOCEA +link = CCGT_OCEA - Oceania +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[70] +name = CCGTSAS +link = CCGT_SAS - South Asia +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[71] +name = CCGTSEAS +link = CCGT_SEAS - South-East Asia +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[72] +name = CCGTUPS +link = CCGT_UPS - UPS +annual-cost-per-mw = 66890 +max-investment = 100000000 + +[73] +name = CCGTCCSAFR +link = Africa - CCGT_CCS_AFR +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[74] +name = CCGTCCSCAS +link = CCGT_CCS_CAS - Central Asia +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[75] +name = CCGTCCSEUR +link = CCGT_CCS_EUR - Europe +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[76] +name = CCGTCCSLAM +link = CCGT_CCS_LAM - Latin America +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[77] +name = CCGTCCSMEAST +link = CCGT_CCS_MEAST - Middle East +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[78] +name = CCGTCCSNAF +link = CCGT_CCS_NAF - North Africa +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[79] +name = CCGTCCSNAM +link = CCGT_CCS_NAM - North America +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[80] +name = CCGTCCSNAT +link = CCGT_CCS_NAT - North Atlantic +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[81] +name = CCGTCCSNEAS +link = CCGT_CCS_NEAS - North-East Asia +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[82] +name = CCGTCCSOCEA +link = CCGT_CCS_OCEA - Oceania +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[83] +name = CCGTCCSSAS +link = CCGT_CCS_SAS - South Asia +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[84] +name = CCGTCCSSEAS +link = CCGT_CCS_SEAS - South-East Asia +annual-cost-per-mw = 141030 +max-investment = 100000000 + +[85] +name = CCGTCCSUPS +link = CCGT_CCS_UPS - UPS +annual-cost-per-mw = 141030 +max-investment = 100000000 \ No newline at end of file diff --git a/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/capa/windcapaSEAS.txt b/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/capa/windcapaSEAS.txt new file mode 100644 index 0000000..a919e8d --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/capa/windcapaSEAS.txt @@ -0,0 +1,8760 @@ +0.301656541 +0.280889889 +0.26076965 +0.174149339 +0.087267559 +0.062949088 +0.037356322 +0.021674556 +0.013776659 +0.009991971 +0.010834289 +0.02074014 +0.085305839 +0.11735713 +0.093565102 +0.071996919 +0.07378086 +0.079094604 +0.077744548 +0.068354918 +0.055847017 +0.047477843 +0.04537514 +0.045599291 +0.046799505 +0.048188513 +0.052764779 +0.056212466 +0.015431623 +0.010158999 +0.012042272 +0.015211695 +0.019042084 +0.021566866 +0.02177806 +0.025170883 +0.07380215 +0.097347945 +0.078933793 +0.065336447 +0.072035577 +0.085529617 +0.093224101 +0.102384691 +0.111280759 +0.112135556 +0.114335693 +0.123510234 +0.14250836 +0.162142216 +0.174039576 +0.145217344 +0.067045734 +0.042715444 +0.028472919 +0.034746016 +0.050325153 +0.066657721 +0.080290075 +0.099624968 +0.228156846 +0.340920954 +0.351521557 +0.348558692 +0.358893371 +0.377465595 +0.414302272 +0.455158011 +0.480378409 +0.491157773 +0.490999362 +0.482550117 +0.464346521 +0.4418994 +0.422989291 +0.262944621 +0.203901305 +0.188783823 +0.162349849 +0.145892277 +0.141317367 +0.141392751 +0.147244384 +0.166551037 +0.281324972 +0.421627432 +0.462191859 +0.48939836 +0.516960916 +0.552184614 +0.594209078 +0.631982129 +0.644070007 +0.632883469 +0.613897034 +0.595012203 +0.571424145 +0.548550847 +0.527491871 +0.347473407 +0.25678261 +0.218433985 +0.189205556 +0.183191114 +0.196117785 +0.220052835 +0.241789925 +0.269367655 +0.38864628 +0.552669425 +0.60121879 +0.617540075 +0.610239611 +0.606742243 +0.606638467 +0.597372051 +0.578919873 +0.553054448 +0.54014727 +0.529208853 +0.507910971 +0.48611561 +0.465756927 +0.272397059 +0.168283028 +0.139500372 +0.11319445 +0.079992618 +0.05460596 +0.049991703 +0.062264313 +0.089746496 +0.1641645 +0.209861502 +0.202766201 +0.182318207 +0.151063998 +0.143521403 +0.154143836 +0.17186721 +0.187416351 +0.185922655 +0.17247527 +0.148662721 +0.132095055 +0.122723327 +0.119000329 +0.084583681 +0.045529562 +0.113560583 +0.205659211 +0.284294894 +0.335348501 +0.347792304 +0.364550165 +0.384227676 +0.438593932 +0.478764326 +0.482683132 +0.474213495 +0.472446092 +0.478168198 +0.494956994 +0.510551206 +0.530382938 +0.517138579 +0.463640209 +0.39843279 +0.319569286 +0.314035659 +0.33021376 +0.311968316 +0.362526997 +0.346535781 +0.299104619 +0.267163556 +0.251406705 +0.249390859 +0.262009332 +0.283544775 +0.356577322 +0.490295182 +0.50730662 +0.509000722 +0.499630596 +0.484993489 +0.472550562 +0.467027467 +0.464546624 +0.470641114 +0.473558563 +0.46973727 +0.455994004 +0.439528831 +0.418297882 +0.295468056 +0.270379941 +0.207782672 +0.148870864 +0.110366091 +0.088392006 +0.085228843 +0.104022784 +0.14046156 +0.241971501 +0.38181499 +0.431977297 +0.474100425 +0.510333579 +0.533534223 +0.540861237 +0.547437807 +0.562487928 +0.597617139 +0.640541027 +0.67775152 +0.700971787 +0.70114487 +0.687500523 +0.571369337 +0.658211783 +0.716465775 +0.720489815 +0.712176987 +0.690183673 +0.65193121 +0.609062919 +0.574626142 +0.610974011 +0.65419273 +0.656200886 +0.661987702 +0.657951209 +0.63279535 +0.595788603 +0.549903966 +0.509360914 +0.471510454 +0.432607693 +0.402801005 +0.385820418 +0.377022749 +0.371149892 +0.233037706 +0.140116143 +0.108446383 +0.061855694 +0.030938388 +0.016537404 +0.011470931 +0.011498035 +0.016340584 +0.052747718 +0.092551091 +0.098092117 +0.096759727 +0.103814682 +0.126250213 +0.156707361 +0.176576958 +0.183280592 +0.179707869 +0.182695596 +0.189486033 +0.195708296 +0.210819187 +0.223266855 +0.191448615 +0.091710995 +0.105736243 +0.128777851 +0.140802489 +0.14826984 +0.139928897 +0.126702336 +0.114912811 +0.143470953 +0.189200661 +0.217386601 +0.243216433 +0.267131838 +0.291628575 +0.315381679 +0.333846873 +0.345073573 +0.338007323 +0.335249453 +0.335682591 +0.33905809 +0.338380546 +0.335432642 +0.190440176 +0.115837533 +0.074609729 +0.048046023 +0.045832066 +0.049719943 +0.050241699 +0.046998246 +0.044267132 +0.089567159 +0.172579591 +0.204917862 +0.207529162 +0.215914263 +0.237489488 +0.262903473 +0.280001527 +0.291668521 +0.299879861 +0.300230042 +0.28315142 +0.256704945 +0.232873054 +0.211070255 +0.101604307 +0.036996815 +0.017102569 +0.00987478 +0.010743691 +0.015559839 +0.023028613 +0.032490805 +0.042073043 +0.091700915 +0.15820233 +0.157618757 +0.138431364 +0.124706338 +0.119205579 +0.123610184 +0.136734587 +0.155627557 +0.177290417 +0.208870977 +0.249295644 +0.289166501 +0.31550976 +0.316567636 +0.176744169 +0.084697742 +0.075245895 +0.057691834 +0.045548968 +0.038337968 +0.037507374 +0.040624152 +0.048025421 +0.131270236 +0.206931175 +0.191994616 +0.152333461 +0.132955331 +0.132938468 +0.131332475 +0.131585162 +0.139339039 +0.139988904 +0.138257797 +0.144237905 +0.150913479 +0.149114813 +0.141351608 +0.084457348 +0.018651619 +0.007877024 +0.008659563 +0.013212713 +0.022746237 +0.039984754 +0.065477802 +0.107256394 +0.243851558 +0.382851922 +0.418748297 +0.423121235 +0.430916605 +0.441081367 +0.460731664 +0.472588801 +0.472408867 +0.483894592 +0.502617133 +0.506834126 +0.500789849 +0.496771345 +0.495611135 +0.389378533 +0.379734739 +0.317152737 +0.247167737 +0.194461092 +0.158238587 +0.147573081 +0.157654435 +0.179730585 +0.261889323 +0.415363085 +0.431518471 +0.39894137 +0.355395537 +0.314829535 +0.284141862 +0.263604257 +0.25614537 +0.272585568 +0.290792976 +0.292205793 +0.279639464 +0.256072932 +0.229035078 +0.116197674 +0.037209078 +0.017611385 +0.013168941 +0.010714588 +0.010433331 +0.012955815 +0.018532895 +0.02782393 +0.090737323 +0.142376668 +0.132458231 +0.103690912 +0.095181079 +0.13473005 +0.206366763 +0.27274793 +0.318172467 +0.340107262 +0.354836192 +0.363252742 +0.369152797 +0.384877539 +0.409792903 +0.286444385 +0.240853613 +0.363787107 +0.416426115 +0.430069056 +0.430771214 +0.423523401 +0.419826142 +0.431017343 +0.523739419 +0.59497638 +0.618673864 +0.627545255 +0.621612962 +0.601236817 +0.552072938 +0.459719497 +0.374818448 +0.285744515 +0.219691093 +0.195468465 +0.19954718 +0.194411803 +0.193174967 +0.140369067 +0.140233229 +0.18841479 +0.204413163 +0.182444773 +0.14315275 +0.1091245 +0.092967218 +0.08347739 +0.122307345 +0.165956202 +0.155569795 +0.126941738 +0.098369052 +0.078013289 +0.058520989 +0.042821083 +0.0335367 +0.029838202 +0.029006883 +0.024745802 +0.021102313 +0.023216613 +0.031848638 +0.021834019 +0.012573264 +0.013181235 +0.014596049 +0.017457453 +0.021976415 +0.026228686 +0.028464361 +0.028799397 +0.042224683 +0.070946197 +0.075013965 +0.069068678 +0.065988476 +0.072345623 +0.087333889 +0.100682268 +0.106139115 +0.108465559 +0.117677821 +0.129295643 +0.138266695 +0.14241556 +0.137676448 +0.05532273 +0.008874675 +0.003923742 +0.003741782 +0.004974094 +0.007868952 +0.011681598 +0.015144078 +0.017927485 +0.043187954 +0.112629297 +0.132622322 +0.119710484 +0.106515649 +0.104434641 +0.113978713 +0.127180814 +0.143123639 +0.164037505 +0.183491805 +0.192483918 +0.195570098 +0.198989198 +0.2096763 +0.124557203 +0.050563529 +0.040399919 +0.030575779 +0.021830088 +0.016484189 +0.014961081 +0.016738124 +0.026553268 +0.100206839 +0.19085191 +0.209994232 +0.203515025 +0.209754414 +0.245849264 +0.294511473 +0.335644284 +0.36929709 +0.386334216 +0.391643047 +0.396360408 +0.405398318 +0.417733797 +0.429854849 +0.299502135 +0.238337657 +0.34184197 +0.381333438 +0.387098989 +0.389701858 +0.390276065 +0.395341772 +0.413932063 +0.490738768 +0.540746656 +0.499201875 +0.459395208 +0.439363467 +0.415332302 +0.396508348 +0.383158196 +0.378930412 +0.369241472 +0.355486069 +0.349039101 +0.349968202 +0.358976191 +0.365799556 +0.230319664 +0.204270806 +0.252815596 +0.267352846 +0.276837999 +0.285747344 +0.283642085 +0.260666724 +0.220957497 +0.225417611 +0.252178654 +0.282293204 +0.319580444 +0.361327778 +0.387097374 +0.395250557 +0.393310203 +0.378978891 +0.337418333 +0.291026475 +0.252912907 +0.217398094 +0.184619176 +0.1575418 +0.088995603 +0.015220362 +0.002810943 +0.002587044 +0.004486207 +0.007057156 +0.010716439 +0.01520322 +0.019343018 +0.053559329 +0.095623618 +0.086536321 +0.06069513 +0.047970769 +0.055581956 +0.073941633 +0.090246337 +0.098419951 +0.101644324 +0.105171357 +0.112773794 +0.131485486 +0.15522618 +0.175688117 +0.131713445 +0.032649166 +0.022547817 +0.023456803 +0.025000889 +0.027019018 +0.029201786 +0.033753366 +0.045339218 +0.112247742 +0.202753779 +0.217777556 +0.194444566 +0.188373123 +0.21100859 +0.262302126 +0.304878161 +0.321204433 +0.310755466 +0.289761124 +0.268077333 +0.244691023 +0.217436747 +0.19037988 +0.1025622 +0.021601474 +0.020033818 +0.026060888 +0.040236291 +0.057957478 +0.073976932 +0.085422567 +0.091906581 +0.137723762 +0.170800839 +0.135248481 +0.09957827 +0.093697858 +0.113017944 +0.148181092 +0.180967524 +0.205716618 +0.225357705 +0.239563472 +0.255811566 +0.265710409 +0.263255754 +0.259613309 +0.17997995 +0.079167115 +0.107223125 +0.117438034 +0.124644385 +0.123667897 +0.116715554 +0.114634454 +0.127164506 +0.209864648 +0.312170634 +0.3389659 +0.336159461 +0.341635668 +0.362730009 +0.389056059 +0.402682156 +0.402735964 +0.390882964 +0.37159746 +0.350637228 +0.330335225 +0.310785365 +0.285172856 +0.159735683 +0.078745154 +0.085562684 +0.098314254 +0.116747419 +0.133753893 +0.142570789 +0.143886159 +0.149269239 +0.224367224 +0.336207509 +0.353755753 +0.352852552 +0.35224236 +0.370174498 +0.381711245 +0.392125122 +0.407486182 +0.417768434 +0.426882484 +0.434912899 +0.436800378 +0.438795056 +0.438692455 +0.236314796 +0.18521249 +0.204490515 +0.196143064 +0.195512783 +0.205247843 +0.223038628 +0.251642113 +0.288904962 +0.391749063 +0.522524244 +0.559270804 +0.581182219 +0.601105764 +0.610333065 +0.589596496 +0.552078145 +0.496855586 +0.42534896 +0.375642383 +0.343697582 +0.325108605 +0.318339927 +0.319266245 +0.222190618 +0.307458337 +0.361172076 +0.40195794 +0.451114982 +0.484887066 +0.492972154 +0.479561017 +0.443909121 +0.402309781 +0.453966275 +0.446029425 +0.441567225 +0.438021632 +0.429869077 +0.410128563 +0.391807755 +0.367701956 +0.333949037 +0.308725922 +0.285571452 +0.261594553 +0.238250835 +0.21355905 +0.166193846 +0.162218245 +0.199115857 +0.258647385 +0.307729791 +0.335267951 +0.320577786 +0.277867341 +0.2242127 +0.194981464 +0.195591195 +0.198967086 +0.211761035 +0.235576132 +0.266216234 +0.298126329 +0.333119603 +0.369203878 +0.390502695 +0.393241755 +0.387888499 +0.384798753 +0.392433698 +0.402763945 +0.396218322 +0.355209301 +0.291375484 +0.234900555 +0.205410961 +0.217546886 +0.245667525 +0.26341461 +0.277595316 +0.303326538 +0.375568961 +0.394705955 +0.399570521 +0.400349515 +0.409625256 +0.398643974 +0.42216132 +0.443242306 +0.483066642 +0.487073342 +0.427344015 +0.369627428 +0.317502778 +0.270560315 +0.179008152 +0.108846209 +0.09534561 +0.134203005 +0.147716565 +0.157961189 +0.173920973 +0.193267247 +0.193998549 +0.186629359 +0.191322038 +0.169657052 +0.152026129 +0.155601817 +0.174356173 +0.21692505 +0.236809441 +0.228833734 +0.2340744 +0.246475549 +0.257493789 +0.267381283 +0.277905264 +0.242065912 +0.164014336 +0.119882396 +0.082486053 +0.047432019 +0.042479923 +0.057909127 +0.097220315 +0.174199725 +0.195910859 +0.177986826 +0.193249712 +0.190107457 +0.18254231 +0.195886657 +0.26971967 +0.370795212 +0.444698597 +0.511229333 +0.54938606 +0.565430478 +0.56875787 +0.570257499 +0.581628034 +0.563434758 +0.44396319 +0.550951036 +0.667773132 +0.715060343 +0.731282263 +0.716162699 +0.690908784 +0.661193945 +0.615955839 +0.58723485 +0.638646925 +0.629466881 +0.608761459 +0.612655116 +0.611878192 +0.611171437 +0.609707323 +0.604328035 +0.608847939 +0.610652094 +0.604885617 +0.60643646 +0.599696923 +0.553797421 +0.512128678 +0.544781089 +0.496095597 +0.45647772 +0.433169652 +0.423137853 +0.414808036 +0.41695076 +0.436626034 +0.478728834 +0.616548815 +0.652474548 +0.668397867 +0.679925052 +0.68370589 +0.671073408 +0.643223293 +0.617755041 +0.590415743 +0.5624862 +0.548075076 +0.542343427 +0.544762422 +0.522324921 +0.404309273 +0.443561946 +0.423489625 +0.402007224 +0.395341412 +0.390704925 +0.385713771 +0.397557458 +0.425093834 +0.493079573 +0.650268582 +0.705873795 +0.708972138 +0.675536634 +0.637272947 +0.608665951 +0.597710798 +0.591135475 +0.596127909 +0.605735082 +0.608054465 +0.606961383 +0.599827254 +0.554182998 +0.328057011 +0.29540221 +0.258481874 +0.212167477 +0.18897094 +0.17249344 +0.161800718 +0.164368001 +0.181601543 +0.244015602 +0.36362171 +0.367685608 +0.33792588 +0.307008584 +0.315954185 +0.354580798 +0.392084722 +0.420548452 +0.451711545 +0.493113296 +0.517576385 +0.539930916 +0.545794716 +0.502343677 +0.354669253 +0.431027939 +0.450094301 +0.464620902 +0.486245636 +0.509994729 +0.532601539 +0.551776894 +0.563245555 +0.587886578 +0.699648333 +0.724443174 +0.71325425 +0.693642367 +0.678642074 +0.660418473 +0.63952753 +0.611204406 +0.576960184 +0.548679755 +0.520198675 +0.49097222 +0.462171536 +0.426178761 +0.238747249 +0.190317535 +0.151828443 +0.119198785 +0.094006362 +0.078949551 +0.075143941 +0.075863489 +0.07889944 +0.11879184 +0.221817369 +0.240308825 +0.219538298 +0.200676073 +0.198920201 +0.203182433 +0.212356592 +0.2233708 +0.227690051 +0.222154638 +0.211629425 +0.200274753 +0.190205359 +0.19208658 +0.090582623 +0.03205209 +0.022452965 +0.020296196 +0.021742005 +0.025396795 +0.024436573 +0.02223476 +0.026531438 +0.065160683 +0.172726958 +0.174088287 +0.126208944 +0.099156938 +0.086283537 +0.08038493 +0.082650586 +0.084013285 +0.090691567 +0.105039931 +0.124684893 +0.151969055 +0.189485221 +0.238457833 +0.158347565 +0.108545512 +0.084784596 +0.070497647 +0.069388712 +0.082521508 +0.108280739 +0.139271056 +0.174842339 +0.256812201 +0.450735145 +0.531394294 +0.565424441 +0.580759707 +0.586869537 +0.587413748 +0.593040928 +0.607411861 +0.630349291 +0.640820427 +0.635216534 +0.617740785 +0.593080581 +0.533025718 +0.329576236 +0.352403861 +0.335203272 +0.32920772 +0.340757452 +0.364914189 +0.396295296 +0.445277061 +0.508143729 +0.595943459 +0.737706603 +0.778882309 +0.797296157 +0.809145908 +0.797880121 +0.76879572 +0.745503879 +0.731696723 +0.726747781 +0.7241174 +0.720759851 +0.716090994 +0.70678631 +0.65416525 +0.489516816 +0.601135716 +0.601634665 +0.601568117 +0.607543351 +0.614297721 +0.614979142 +0.60703931 +0.593449857 +0.596872269 +0.67744134 +0.680204739 +0.667282038 +0.648960424 +0.640805799 +0.630966021 +0.619116289 +0.601616939 +0.57984531 +0.54843003 +0.5012074 +0.44352816 +0.385079688 +0.332825042 +0.16984718 +0.096927058 +0.067252234 +0.038338214 +0.019203681 +0.010098833 +0.008161297 +0.009934459 +0.015355273 +0.057482895 +0.149973197 +0.163772224 +0.150428918 +0.146127303 +0.15349405 +0.164923742 +0.171299275 +0.169634755 +0.171291804 +0.182510717 +0.197634116 +0.204201373 +0.204188125 +0.205575325 +0.129369002 +0.054795393 +0.072007491 +0.090428131 +0.112952887 +0.139250574 +0.166906046 +0.191374779 +0.216938179 +0.297647277 +0.427372528 +0.47230524 +0.497015342 +0.514248249 +0.521986834 +0.518652611 +0.5055599 +0.489539907 +0.451584903 +0.413493486 +0.383549783 +0.353385661 +0.327783671 +0.294499066 +0.114344341 +0.059816314 +0.053916171 +0.069875155 +0.119203646 +0.190202572 +0.264779062 +0.327144774 +0.372363811 +0.440895025 +0.49439514 +0.491845162 +0.480254209 +0.451498463 +0.425598297 +0.382584967 +0.343187197 +0.327325503 +0.349744894 +0.380838393 +0.38002832 +0.362182789 +0.336864859 +0.275501416 +0.129072567 +0.092942817 +0.045941283 +0.028490159 +0.023859922 +0.023441417 +0.024354668 +0.025949153 +0.029545805 +0.048398192 +0.158852426 +0.223857688 +0.236564044 +0.233402454 +0.23654789 +0.252165935 +0.277440561 +0.302908109 +0.319747529 +0.324740636 +0.321831631 +0.316302017 +0.303679854 +0.272542934 +0.096840549 +0.037990877 +0.015359055 +0.007863246 +0.007866057 +0.013599868 +0.025942672 +0.046887679 +0.080756412 +0.1591855 +0.395117181 +0.52324593 +0.573768494 +0.590273485 +0.595560863 +0.60530684 +0.610142123 +0.602285226 +0.614025617 +0.632983329 +0.626192372 +0.596984448 +0.556477326 +0.464146451 +0.296973778 +0.302420548 +0.230850885 +0.171205931 +0.144142807 +0.133237553 +0.128195694 +0.123562604 +0.118932373 +0.13814327 +0.260381732 +0.311534812 +0.308580337 +0.285840269 +0.260771478 +0.238217368 +0.225395958 +0.221993816 +0.229576484 +0.239936882 +0.243326989 +0.240917472 +0.237485725 +0.22502445 +0.087116728 +0.023614002 +0.013876716 +0.012494949 +0.011048133 +0.009199525 +0.008219262 +0.008945399 +0.013444563 +0.043315892 +0.144555034 +0.17850758 +0.16810406 +0.157490607 +0.168622686 +0.212530069 +0.263275551 +0.298691377 +0.316266053 +0.319278576 +0.317358785 +0.313595113 +0.311834641 +0.311304955 +0.159783833 +0.135246623 +0.136473259 +0.113440712 +0.098017745 +0.09086238 +0.096082007 +0.107760083 +0.126352296 +0.183835809 +0.36233126 +0.463695304 +0.518157804 +0.546509639 +0.559484864 +0.56199477 +0.526649338 +0.471916858 +0.408005007 +0.351683877 +0.307634774 +0.268290061 +0.228005755 +0.192852539 +0.063614747 +0.043682657 +0.053979156 +0.059656821 +0.064268469 +0.068371984 +0.067205871 +0.06218879 +0.058592733 +0.074710664 +0.170996154 +0.236922436 +0.264727366 +0.266433219 +0.274775208 +0.277799897 +0.276246997 +0.271265724 +0.263198807 +0.266021781 +0.277467611 +0.298821012 +0.326729973 +0.305534776 +0.175701466 +0.116595639 +0.078099986 +0.056190308 +0.050839795 +0.055791922 +0.066328907 +0.074720614 +0.078926535 +0.097615949 +0.213832933 +0.272322012 +0.273047288 +0.261547207 +0.263021935 +0.286070302 +0.326144413 +0.374179275 +0.430981728 +0.471409262 +0.483245706 +0.475207101 +0.457520562 +0.381982946 +0.217513266 +0.174492808 +0.129736377 +0.110830216 +0.123135001 +0.155902205 +0.200150651 +0.246437675 +0.289404415 +0.338102223 +0.527714144 +0.585984402 +0.564942845 +0.542063773 +0.529507317 +0.519062084 +0.506791663 +0.499374545 +0.50895475 +0.517068894 +0.51598438 +0.510225939 +0.502217266 +0.4370106 +0.264311335 +0.244018161 +0.202445971 +0.171158049 +0.157442825 +0.156584058 +0.177620409 +0.218653532 +0.260701566 +0.308287778 +0.47293901 +0.517099831 +0.509504651 +0.495231801 +0.482018645 +0.465097695 +0.455400305 +0.447804195 +0.437241888 +0.418154724 +0.387736498 +0.353804476 +0.320585265 +0.276830055 +0.110708275 +0.071083307 +0.056086813 +0.052314442 +0.052361891 +0.054021772 +0.0602327 +0.067611453 +0.070838902 +0.081886143 +0.160713815 +0.154966851 +0.12280521 +0.113391749 +0.129294371 +0.148902825 +0.161902813 +0.173732558 +0.185351499 +0.203876025 +0.231154536 +0.258244344 +0.28480259 +0.296373495 +0.152500306 +0.167233221 +0.164182037 +0.147454626 +0.135201486 +0.13529809 +0.149982139 +0.161237331 +0.164382557 +0.174432865 +0.270930986 +0.359732553 +0.389363732 +0.414692989 +0.419544234 +0.414035614 +0.415645786 +0.426327366 +0.429977378 +0.427248408 +0.418861115 +0.394812973 +0.37276078 +0.305523013 +0.160477364 +0.181517052 +0.19386614 +0.204017958 +0.224958717 +0.255902278 +0.299377024 +0.337573184 +0.365963287 +0.416449662 +0.533050932 +0.597671077 +0.58790896 +0.554974502 +0.513967493 +0.475671711 +0.456950622 +0.43704247 +0.425223488 +0.427153248 +0.427993214 +0.423192679 +0.402508712 +0.317146619 +0.148985105 +0.098744592 +0.077621686 +0.076581335 +0.086227265 +0.093039432 +0.086971453 +0.07117301 +0.058212968 +0.07575835 +0.190610411 +0.262458533 +0.278452968 +0.266023514 +0.242473898 +0.207679023 +0.18733611 +0.180082324 +0.172253846 +0.170679072 +0.177984313 +0.181627766 +0.171483471 +0.149893009 +0.072545822 +0.075102224 +0.078095987 +0.075714933 +0.077025263 +0.08532315 +0.099562742 +0.107148165 +0.11285188 +0.129097314 +0.22040349 +0.296614514 +0.342358509 +0.390700349 +0.400525528 +0.374255513 +0.342334931 +0.321261605 +0.301869028 +0.284904734 +0.270986315 +0.260665765 +0.255418249 +0.205595259 +0.092853004 +0.074237837 +0.070282619 +0.084536436 +0.094242461 +0.095105071 +0.094618455 +0.091907088 +0.089833253 +0.100738033 +0.192432635 +0.30031954 +0.371992312 +0.435211624 +0.479204784 +0.503188655 +0.512513644 +0.500909487 +0.479119348 +0.449915758 +0.411215253 +0.370946327 +0.33701554 +0.235737903 +0.121528076 +0.102851143 +0.083395581 +0.079374696 +0.083861028 +0.093093502 +0.100855471 +0.103840366 +0.105879533 +0.129207522 +0.25779736 +0.350706453 +0.388544991 +0.411750666 +0.437384362 +0.472468758 +0.518186765 +0.5235034 +0.494152287 +0.456675824 +0.422900275 +0.396200826 +0.377405388 +0.282315156 +0.142635217 +0.155439471 +0.143873546 +0.133332834 +0.125149444 +0.119876715 +0.120100739 +0.121694887 +0.122343485 +0.139499614 +0.248388375 +0.325811161 +0.388598398 +0.439600767 +0.478058297 +0.521036659 +0.552749289 +0.554434794 +0.521402883 +0.454011535 +0.400338659 +0.355520409 +0.326347801 +0.268882708 +0.133707938 +0.176291457 +0.1771035 +0.187158335 +0.210833065 +0.239567858 +0.275315562 +0.317549149 +0.369041386 +0.429580163 +0.548008486 +0.553830032 +0.45826722 +0.332887606 +0.244963543 +0.186106079 +0.153837316 +0.142742954 +0.130008332 +0.123558049 +0.136306531 +0.168887546 +0.210347112 +0.194781924 +0.114505455 +0.088437728 +0.070841821 +0.072122169 +0.091269321 +0.125117348 +0.168831296 +0.212933945 +0.251788732 +0.292003545 +0.436841359 +0.517943538 +0.520359958 +0.514511677 +0.512075943 +0.53234958 +0.570090859 +0.614380094 +0.646238618 +0.658454054 +0.658991709 +0.666403213 +0.670077661 +0.629821132 +0.659411908 +0.730576508 +0.746375884 +0.768910225 +0.801505093 +0.829011256 +0.850361961 +0.863606755 +0.866484218 +0.855705352 +0.871486105 +0.848286567 +0.814154646 +0.791905684 +0.783961778 +0.774713407 +0.759923825 +0.74353828 +0.713486154 +0.69037127 +0.678084206 +0.655174325 +0.622166585 +0.500861433 +0.328008047 +0.364368943 +0.300828511 +0.234240937 +0.199592181 +0.19032438 +0.193571597 +0.189289969 +0.170379412 +0.153811364 +0.195372041 +0.184869353 +0.168478525 +0.177514165 +0.216415203 +0.272228023 +0.319601163 +0.338101949 +0.315730302 +0.274939456 +0.247044471 +0.231058357 +0.216587632 +0.179295672 +0.098991494 +0.076689661 +0.065171458 +0.060096262 +0.070247598 +0.089742475 +0.121259698 +0.164966372 +0.214300496 +0.279220769 +0.447471276 +0.575304552 +0.661061924 +0.69437889 +0.685279012 +0.668724379 +0.64992167 +0.624249891 +0.586338985 +0.557138245 +0.541654321 +0.538065514 +0.54338261 +0.476230289 +0.395579556 +0.530668457 +0.541753024 +0.519643657 +0.505393564 +0.508640789 +0.533399816 +0.563628345 +0.587992544 +0.622777033 +0.708461437 +0.720003226 +0.696433993 +0.670816455 +0.634084475 +0.587231335 +0.533464394 +0.475670169 +0.402040993 +0.319893858 +0.271841105 +0.276750919 +0.30969824 +0.29640414 +0.21124932 +0.383003737 +0.495054577 +0.538657459 +0.568249195 +0.599811127 +0.637985762 +0.677083965 +0.690399785 +0.653453285 +0.632557092 +0.563409797 +0.49177022 +0.451700825 +0.418236292 +0.38337746 +0.364610196 +0.356078583 +0.313801869 +0.202689329 +0.108018198 +0.081247857 +0.071960963 +0.066622466 +0.06058942 +0.210798767 +0.41092253 +0.601600115 +0.735673901 +0.805932437 +0.820566915 +0.802076646 +0.757275371 +0.677555265 +0.655702422 +0.576427585 +0.532688265 +0.544711354 +0.562868893 +0.557389726 +0.53255789 +0.497435309 +0.485821176 +0.453621541 +0.367274856 +0.299665248 +0.27389263 +0.22490771 +0.19695605 +0.317003763 +0.513291984 +0.707479642 +0.84421364 +0.909520257 +0.933858025 +0.943930741 +0.946302818 +0.944583537 +0.946785771 +0.926402045 +0.892220703 +0.858524862 +0.829380366 +0.816834793 +0.7925402 +0.76999917 +0.744761559 +0.704945903 +0.645705351 +0.571559011 +0.521915023 +0.337941513 +0.295948391 +0.3337634 +0.337840874 +0.332971751 +0.325163351 +0.318855955 +0.312994537 +0.302734226 +0.287419484 +0.27345492 +0.382127934 +0.404017408 +0.378809632 +0.351422987 +0.337860117 +0.347780457 +0.370328045 +0.397891673 +0.426166541 +0.449901312 +0.46994324 +0.491169824 +0.507247197 +0.44025482 +0.268244393 +0.304844066 +0.25012212 +0.201948524 +0.15453173 +0.118338096 +0.099698445 +0.091160428 +0.090154565 +0.109418246 +0.238288053 +0.295564338 +0.319024517 +0.332975119 +0.348427121 +0.361829423 +0.372924176 +0.377262256 +0.375528698 +0.36023536 +0.338757606 +0.314932633 +0.282090139 +0.220264247 +0.070321627 +0.044765623 +0.04816424 +0.055284459 +0.065102137 +0.075374803 +0.083286846 +0.092864287 +0.110134978 +0.149707655 +0.308059534 +0.363966656 +0.380931715 +0.401564744 +0.430874024 +0.454843533 +0.463934317 +0.455351371 +0.420163497 +0.384390862 +0.368310728 +0.352666935 +0.332856557 +0.287128344 +0.11901203 +0.165004015 +0.222115634 +0.2802216 +0.333944631 +0.377809353 +0.411890279 +0.444801526 +0.479957989 +0.517497756 +0.589672338 +0.569465684 +0.520090668 +0.475210319 +0.457944024 +0.457826497 +0.438713213 +0.426173026 +0.41498722 +0.418044243 +0.423860669 +0.420194225 +0.408343141 +0.258644224 +0.105506085 +0.123698234 +0.090253781 +0.081659163 +0.091621722 +0.116948237 +0.1629569 +0.226039202 +0.277366767 +0.309805518 +0.416700098 +0.45614208 +0.436148044 +0.399898444 +0.36103875 +0.328157305 +0.317293078 +0.323163881 +0.311278215 +0.290541458 +0.274691838 +0.589079678 +0.589585409 +0.47961074 +0.43753545 +0.381070061 +0.336272721 +0.352761668 +0.402752206 +0.433790273 +0.451997615 +0.479734754 +0.495282729 +0.479151815 +0.430137559 +0.386858541 +0.362205103 +0.367692947 +0.390301668 +0.427108498 +0.467863188 +0.498473128 +0.529092868 +0.557120272 +0.575481626 +0.588291703 +0.583990589 +0.506164159 +0.540665281 +0.440417093 +0.340795882 +0.268149783 +0.213550682 +0.180153362 +0.170592034 +0.173282153 +0.179843885 +0.198454673 +0.340988267 +0.546368715 +0.594436304 +0.616784123 +0.611187693 +0.603235373 +0.600420553 +0.598384093 +0.604760674 +0.606452729 +0.590034461 +0.560102603 +0.524948687 +0.337340195 +0.253270068 +0.218583654 +0.143026503 +0.088462527 +0.069354261 +0.064370165 +0.070281661 +0.086142942 +0.10465749 +0.130245728 +0.208726831 +0.290162389 +0.32051747 +0.333236998 +0.343699367 +0.357868234 +0.354449145 +0.338830223 +0.35426178 +0.365580385 +0.367384257 +0.355034598 +0.334354486 +0.246158553 +0.100998462 +0.057902791 +0.034531619 +0.024239642 +0.024176055 +0.027260334 +0.032079062 +0.039948517 +0.061246564 +0.09126973 +0.141848687 +0.149122356 +0.126326508 +0.115858385 +0.114251212 +0.100332315 +0.067529845 +0.042878506 +0.039597264 +0.048968979 +0.06731901 +0.102032197 +0.097732962 +0.062708123 +0.025660978 +0.027066136 +0.050671487 +0.095877606 +0.151596696 +0.208300909 +0.260311365 +0.3015507 +0.321219559 +0.303580021 +0.284160768 +0.257244861 +0.215805228 +0.18526137 +0.157890257 +0.130625998 +0.103896249 +0.08583417 +0.083972192 +0.094135279 +0.098779934 +0.10659343 +0.12050804 +0.062403214 +0.031947661 +0.02051491 +0.016398885 +0.021348331 +0.036320888 +0.057963554 +0.076431419 +0.085780408 +0.086723166 +0.085822437 +0.137024274 +0.218022277 +0.215166105 +0.184204016 +0.158604202 +0.153832187 +0.158242131 +0.154833747 +0.156447908 +0.168514319 +0.189098335 +0.206720707 +0.216608868 +0.13718781 +0.045167465 +0.027524853 +0.017993711 +0.014966849 +0.014573609 +0.016541083 +0.022321553 +0.032643449 +0.047387309 +0.070732804 +0.171864441 +0.296100354 +0.335195308 +0.350408948 +0.364696166 +0.386903469 +0.399810586 +0.401914687 +0.393211597 +0.375514508 +0.339554006 +0.296529056 +0.265608842 +0.145188288 +0.031431859 +0.030243475 +0.057195315 +0.092333306 +0.12669148 +0.157515559 +0.177229848 +0.201410728 +0.237050081 +0.277458738 +0.383887297 +0.471339808 +0.502909753 +0.532167751 +0.564383374 +0.593551759 +0.602774436 +0.586416592 +0.538872102 +0.487878352 +0.441315346 +0.397876258 +0.372274144 +0.231485901 +0.188936546 +0.244504348 +0.249634023 +0.253702171 +0.266392738 +0.2902188 +0.333396939 +0.384484378 +0.424296041 +0.456955901 +0.55079827 +0.601686152 +0.631740877 +0.655558957 +0.679633139 +0.695325979 +0.708867704 +0.721387164 +0.70139348 +0.630932758 +0.540722046 +0.485591424 +0.459364004 +0.262989852 +0.278462341 +0.555703817 +0.624217761 +0.671376869 +0.724246269 +0.781255404 +0.839925998 +0.884963346 +0.90852156 +0.898365839 +0.882901326 +0.854304352 +0.772880987 +0.684906958 +0.608508637 +0.555395894 +0.507151239 +0.46222757 +0.460950492 +0.466835336 +0.457674604 +0.421660958 +0.382565229 +0.207768199 +0.117671235 +0.206752939 +0.249698607 +0.265942263 +0.253921889 +0.234591968 +0.229839533 +0.246677518 +0.26316534 +0.272571925 +0.311675977 +0.29110391 +0.253731526 +0.219053223 +0.190742228 +0.1653909 +0.164989452 +0.194110183 +0.228079499 +0.246539823 +0.274815498 +0.336209847 +0.413655652 +0.400046511 +0.294893699 +0.44442173 +0.575957059 +0.654659956 +0.711808861 +0.751947892 +0.775442658 +0.777611367 +0.746293957 +0.686115146 +0.657263436 +0.648528475 +0.616850081 +0.577287288 +0.550113745 +0.521229781 +0.510043004 +0.478621697 +0.437766744 +0.409308517 +0.376645801 +0.340947773 +0.30886395 +0.151930769 +0.107918239 +0.175828437 +0.182519135 +0.203265325 +0.244569001 +0.31652385 +0.425785734 +0.553169089 +0.652314037 +0.702571917 +0.727514647 +0.664170452 +0.53210959 +0.386330733 +0.26650993 +0.195156697 +0.173816012 +0.16992625 +0.159384672 +0.164548845 +0.192259097 +0.215488736 +0.215736696 +0.109032768 +0.176636927 +0.197575914 +0.201573551 +0.238325352 +0.298309681 +0.367740029 +0.450874027 +0.538414067 +0.60850421 +0.650150041 +0.712081352 +0.728304078 +0.714857033 +0.722698116 +0.729923113 +0.735378282 +0.699829001 +0.670152026 +0.658662166 +0.668562953 +0.684682229 +0.685558319 +0.636999783 +0.458771537 +0.50076119 +0.4259826 +0.372928473 +0.366369015 +0.380307482 +0.407420124 +0.455982718 +0.517489895 +0.576412934 +0.632562471 +0.720806207 +0.830495167 +0.825414206 +0.818557368 +0.803542079 +0.778448571 +0.754822485 +0.743153577 +0.741546726 +0.745170972 +0.740342377 +0.720741455 +0.672235289 +0.441129949 +0.446747712 +0.468153745 +0.383135606 +0.28697817 +0.206389664 +0.15718736 +0.142510409 +0.154725611 +0.182167372 +0.230177327 +0.402474494 +0.603734052 +0.656187856 +0.684626064 +0.704685513 +0.727380845 +0.744777279 +0.747820473 +0.742758193 +0.73347479 +0.716353205 +0.690049842 +0.650450569 +0.385229461 +0.314209581 +0.293189095 +0.246770708 +0.221657707 +0.209021515 +0.202720217 +0.199910288 +0.195220206 +0.188091502 +0.185361975 +0.274209181 +0.383687543 +0.394974541 +0.39790209 +0.398701951 +0.397304455 +0.391990562 +0.384673047 +0.381353768 +0.377596528 +0.368266846 +0.346690138 +0.325505058 +0.199279344 +0.091184033 +0.084451662 +0.066940786 +0.051391845 +0.040094474 +0.034078772 +0.034307504 +0.040662641 +0.05671514 +0.092054373 +0.210535482 +0.314872346 +0.333704494 +0.351146656 +0.383062786 +0.429245376 +0.468903406 +0.48290311 +0.466315058 +0.442939782 +0.4236566 +0.402264667 +0.377595745 +0.244030393 +0.076066851 +0.053852559 +0.038161854 +0.038170777 +0.044400802 +0.052920611 +0.062998262 +0.073239824 +0.086366381 +0.117490605 +0.242019325 +0.34925903 +0.357699229 +0.336141509 +0.332786174 +0.362491354 +0.38626617 +0.383253178 +0.371567604 +0.365704576 +0.366112883 +0.36034136 +0.356817025 +0.213086363 +0.086746788 +0.075638808 +0.055820079 +0.046323028 +0.047750522 +0.058177405 +0.082760566 +0.118976988 +0.155015623 +0.201285759 +0.301218496 +0.357806903 +0.36077454 +0.359376301 +0.384223794 +0.399519735 +0.419748101 +0.418162001 +0.416841902 +0.405403093 +0.403999621 +0.386382448 +0.365424708 +0.187498729 +0.077436347 +0.077678001 +0.081255408 +0.088771351 +0.09843012 +0.114951289 +0.14133527 +0.166167805 +0.191196042 +0.240385576 +0.391478864 +0.516791944 +0.507950046 +0.496270865 +0.495334393 +0.511434364 +0.531268143 +0.538397119 +0.54253361 +0.54716288 +0.570175042 +0.579127121 +0.567777436 +0.397535933 +0.377226504 +0.346753738 +0.286009567 +0.223291508 +0.173285619 +0.147910009 +0.153860098 +0.178719739 +0.200987587 +0.216352194 +0.294793543 +0.452196256 +0.47859299 +0.493992999 +0.494966835 +0.487291109 +0.472587447 +0.456551525 +0.429981865 +0.407009942 +0.397538908 +0.382189114 +0.353494837 +0.191398858 +0.119711038 +0.068662852 +0.032947324 +0.023510858 +0.02162882 +0.023030656 +0.026945083 +0.032808847 +0.043889178 +0.067927307 +0.142587616 +0.223768661 +0.215311047 +0.206231369 +0.245100325 +0.297425325 +0.328393205 +0.325853191 +0.305515136 +0.281975644 +0.28047806 +0.291994096 +0.288380099 +0.14997096 +0.123976474 +0.153779548 +0.192578529 +0.245223044 +0.284604115 +0.295695074 +0.288421715 +0.265693203 +0.250296556 +0.247451978 +0.294797622 +0.368217895 +0.361780017 +0.345579662 +0.328406064 +0.30204201 +0.294742658 +0.297172463 +0.29649378 +0.311863836 +0.360348812 +0.38759562 +0.380833738 +0.203323039 +0.207563812 +0.164162258 +0.083683474 +0.050372827 +0.062190122 +0.095079068 +0.135616591 +0.170177671 +0.19427883 +0.209226766 +0.236154789 +0.293787141 +0.28413517 +0.277278534 +0.262984728 +0.248906062 +0.248276497 +0.250623512 +0.254438084 +0.255526524 +0.257366797 +0.265001728 +0.280092192 +0.173464868 +0.17254821 +0.194312224 +0.165873313 +0.115365508 +0.093949311 +0.095449804 +0.101216553 +0.091314448 +0.07242901 +0.071920313 +0.104864197 +0.221831666 +0.251196684 +0.237681667 +0.230786988 +0.252138927 +0.305675313 +0.376412435 +0.442173758 +0.485902923 +0.513801319 +0.512948733 +0.472373921 +0.395398542 +0.368095719 +0.322490832 +0.292788115 +0.282255024 +0.28867594 +0.312907549 +0.352468382 +0.401759339 +0.450793664 +0.493602451 +0.540461492 +0.657434678 +0.640905657 +0.586163204 +0.5229702 +0.474104686 +0.442692202 +0.438058497 +0.443848374 +0.450659522 +0.454886568 +0.44675856 +0.422240294 +0.222159397 +0.195314068 +0.175168977 +0.143284536 +0.123853755 +0.116719791 +0.116835652 +0.122205939 +0.125987804 +0.124450368 +0.122423238 +0.161479326 +0.184312158 +0.130586826 +0.087984546 +0.083173596 +0.103010136 +0.132190782 +0.158859216 +0.18010637 +0.19221856 +0.194180867 +0.187671307 +0.189639691 +0.133521014 +0.067178931 +0.064933207 +0.069805351 +0.086963746 +0.11898926 +0.165591304 +0.214584165 +0.265025668 +0.323411508 +0.402120697 +0.500873779 +0.551093505 +0.47811796 +0.418599573 +0.36230972 +0.308935096 +0.281131313 +0.275433015 +0.25320983 +0.220107803 +0.204676981 +0.200492567 +0.193086256 +0.112242378 +0.090713269 +0.075163888 +0.06654595 +0.068912693 +0.078236838 +0.090506783 +0.102723346 +0.10762282 +0.109984957 +0.117827736 +0.154038495 +0.237174588 +0.272459111 +0.288921004 +0.296011544 +0.312610834 +0.313597768 +0.307734313 +0.320454582 +0.338852482 +0.339998713 +0.331001718 +0.285407497 +0.123243921 +0.097868584 +0.087382052 +0.077261023 +0.078424196 +0.085424359 +0.09213745 +0.097747622 +0.10164926 +0.104814161 +0.110134209 +0.137394221 +0.210153405 +0.255986737 +0.306130444 +0.33886894 +0.357989633 +0.366363342 +0.371446757 +0.376626595 +0.381680028 +0.376903579 +0.356301659 +0.307723031 +0.131638643 +0.083108992 +0.049000959 +0.033057809 +0.033961722 +0.045001378 +0.069084097 +0.107410113 +0.151865815 +0.20043385 +0.267123306 +0.365564711 +0.49086681 +0.527634453 +0.548667674 +0.554443309 +0.551238369 +0.540236895 +0.531845788 +0.528351914 +0.518239211 +0.484901154 +0.4511967 +0.383394889 +0.206332012 +0.273021792 +0.185015342 +0.10988972 +0.079067197 +0.07456077 +0.085079412 +0.10580385 +0.124120232 +0.138784993 +0.150585347 +0.188188188 +0.270655137 +0.293987021 +0.319680514 +0.349142134 +0.380291843 +0.416484482 +0.451439846 +0.455411774 +0.435975776 +0.418147196 +0.40214303 +0.369137759 +0.17977299 +0.163445692 +0.167137967 +0.12517954 +0.100747812 +0.095562896 +0.099212597 +0.107135743 +0.116505299 +0.130791901 +0.155170633 +0.221315049 +0.331239456 +0.359853623 +0.364044137 +0.354989212 +0.374344847 +0.41442493 +0.44797039 +0.442558372 +0.417286707 +0.405034194 +0.404185346 +0.391850143 +0.3040741 +0.277160184 +0.23452677 +0.176848509 +0.134938148 +0.116779325 +0.112702884 +0.11822021 +0.127462797 +0.135195404 +0.141073971 +0.183844932 +0.333496727 +0.362143656 +0.359861103 +0.353046146 +0.346495692 +0.34805502 +0.355955507 +0.367886498 +0.373942216 +0.361307397 +0.331630657 +0.293066828 +0.115898654 +0.04953087 +0.042230374 +0.048028605 +0.058835589 +0.071614881 +0.085396298 +0.09665443 +0.105658728 +0.115034351 +0.127405032 +0.184477839 +0.2670421 +0.253514694 +0.225691829 +0.209304366 +0.222143733 +0.261900043 +0.301539725 +0.328652944 +0.345621285 +0.35042352 +0.345046168 +0.33609904 +0.172542359 +0.07571684 +0.067796376 +0.05024709 +0.047883614 +0.049150909 +0.052612629 +0.06258582 +0.07716697 +0.096316142 +0.123934226 +0.199273012 +0.292980182 +0.29005963 +0.262227931 +0.260653578 +0.289898166 +0.326346034 +0.351543293 +0.354051111 +0.333868148 +0.305366479 +0.273903223 +0.244424298 +0.111637341 +0.05393802 +0.056874501 +0.041989242 +0.033282492 +0.030697762 +0.032714484 +0.040231003 +0.053706362 +0.074211698 +0.104014116 +0.170107079 +0.245090602 +0.240700041 +0.240850308 +0.288351623 +0.354513491 +0.41799187 +0.475588046 +0.502464138 +0.502477408 +0.480290176 +0.447876607 +0.42449471 +0.203976456 +0.178770662 +0.110968125 +0.040855388 +0.018495925 +0.018039834 +0.027045645 +0.04203688 +0.062806097 +0.092104238 +0.137354426 +0.216471445 +0.346429741 +0.425586957 +0.459409 +0.486040481 +0.505961805 +0.51919711 +0.521749291 +0.50896215 +0.475912661 +0.444954968 +0.420987906 +0.380937618 +0.163468921 +0.103870116 +0.060269125 +0.03588277 +0.035617073 +0.050427017 +0.075476217 +0.113596953 +0.16238059 +0.214539971 +0.268067106 +0.334732333 +0.421287548 +0.446177267 +0.464796072 +0.510515106 +0.557909712 +0.596954122 +0.640805506 +0.667436103 +0.66607919 +0.62894552 +0.572168623 +0.473513019 +0.189197314 +0.203786819 +0.12300316 +0.067452869 +0.056350058 +0.067702824 +0.090224133 +0.123739126 +0.160490844 +0.194259369 +0.228911793 +0.285271211 +0.397671952 +0.443449147 +0.446912269 +0.44268476 +0.42810724 +0.417831515 +0.407674208 +0.374044312 +0.341447003 +0.307063146 +0.269242961 +0.213680038 +0.094043262 +0.056732559 +0.02078436 +0.008309845 +0.009393504 +0.01952414 +0.038153929 +0.063432474 +0.091478126 +0.114904603 +0.132919076 +0.155036561 +0.204637316 +0.19520309 +0.173193131 +0.165757036 +0.186097038 +0.212174893 +0.237205936 +0.249497564 +0.25178853 +0.248570069 +0.251726898 +0.253122349 +0.11137128 +0.120655798 +0.09591883 +0.074645764 +0.065771415 +0.068185285 +0.081556406 +0.101786636 +0.128030705 +0.15678971 +0.187659241 +0.223808507 +0.29125069 +0.334106574 +0.377460983 +0.414779514 +0.459980352 +0.500378899 +0.532128219 +0.54324072 +0.54529251 +0.531458619 +0.472746873 +0.371211977 +0.139891229 +0.189936696 +0.223653014 +0.24930266 +0.290291566 +0.336390707 +0.377793337 +0.406118641 +0.426264474 +0.4411667 +0.453723641 +0.474897519 +0.501012033 +0.486292531 +0.514613377 +0.551298603 +0.598948086 +0.657533902 +0.696467784 +0.720784518 +0.700613278 +0.65111547 +0.580778856 +0.488682481 +0.287305179 +0.304813886 +0.203063748 +0.157258443 +0.161915658 +0.187206525 +0.21431478 +0.242797539 +0.270904056 +0.290058324 +0.293739256 +0.291262636 +0.362338126 +0.3661472 +0.364492684 +0.378817105 +0.401524573 +0.41460864 +0.403073268 +0.372686022 +0.344205171 +0.317359503 +0.29454121 +0.23990639 +0.109403477 +0.078783886 +0.035739261 +0.027518996 +0.033156259 +0.048130578 +0.069120358 +0.092191475 +0.109370734 +0.12018194 +0.134737177 +0.168420845 +0.263848607 +0.308869651 +0.335574075 +0.358786842 +0.370973033 +0.384052317 +0.386794591 +0.383377345 +0.377427197 +0.374459567 +0.360080436 +0.294552051 +0.129048241 +0.138824257 +0.114055207 +0.099583816 +0.10208138 +0.116789288 +0.14109329 +0.175006526 +0.214292697 +0.252060846 +0.291953928 +0.346578931 +0.458147359 +0.478648489 +0.482395356 +0.488015283 +0.48782261 +0.475387901 +0.44828402 +0.436325099 +0.426752289 +0.408848438 +0.379337696 +0.27762059 +0.20931425 +0.190687728 +0.158780386 +0.14823005 +0.146053492 +0.152885542 +0.170360205 +0.190804508 +0.20736593 +0.217995917 +0.225549919 +0.231565189 +0.290839416 +0.309305729 +0.293582928 +0.272455807 +0.260181068 +0.273273295 +0.299119265 +0.324310912 +0.350578378 +0.370937509 +0.38585493 +0.374884215 +0.226908455 +0.234305235 +0.222966061 +0.218785415 +0.21687155 +0.218093353 +0.22302588 +0.238663176 +0.258808469 +0.274732427 +0.281112397 +0.28771775 +0.395756215 +0.404364664 +0.357259658 +0.309276565 +0.26619158 +0.226889715 +0.20336686 +0.180784228 +0.149639611 +0.123523582 +0.107371621 +0.103350166 +0.041942985 +0.029670005 +0.040743846 +0.05698061 +0.080410061 +0.107722801 +0.136325242 +0.166050595 +0.196513378 +0.224282879 +0.248337174 +0.272813478 +0.356887382 +0.341682117 +0.289372442 +0.25114419 +0.24867182 +0.272386649 +0.324181459 +0.374456646 +0.414554058 +0.435854334 +0.432248947 +0.368002461 +0.205608325 +0.203293505 +0.197867827 +0.215925252 +0.264721707 +0.340936698 +0.43678585 +0.537548446 +0.622987868 +0.677027715 +0.703831761 +0.712283521 +0.775824671 +0.751946641 +0.68431986 +0.617164457 +0.575085793 +0.560390314 +0.566444817 +0.569955252 +0.560133447 +0.551122318 +0.537187595 +0.456820762 +0.231937235 +0.29866021 +0.34288699 +0.357617781 +0.361950234 +0.363238019 +0.363262538 +0.369056969 +0.370680166 +0.367016831 +0.354081844 +0.353991352 +0.390946012 +0.336518619 +0.281325473 +0.249252833 +0.245183926 +0.25400638 +0.268047569 +0.272258347 +0.260563112 +0.247755469 +0.235959947 +0.191553516 +0.102401699 +0.086426043 +0.120675263 +0.186632189 +0.26655571 +0.345285981 +0.420050324 +0.475009281 +0.503367796 +0.517474122 +0.517377982 +0.501684418 +0.520919236 +0.544105472 +0.572007099 +0.588269737 +0.601366722 +0.607569373 +0.621122863 +0.62709989 +0.602004462 +0.57760622 +0.55770653 +0.486755347 +0.426133743 +0.509646132 +0.61177524 +0.655577772 +0.670672257 +0.680000584 +0.690434216 +0.705586769 +0.724845004 +0.744061557 +0.766955016 +0.805772601 +0.852998189 +0.862522461 +0.831021851 +0.76234837 +0.679300322 +0.62180363 +0.584427115 +0.537647989 +0.49748316 +0.472550443 +0.463281947 +0.379976666 +0.387449019 +0.464913062 +0.477187129 +0.537252859 +0.608077535 +0.662513511 +0.703951457 +0.730276411 +0.75356178 +0.767730713 +0.774703173 +0.7524746 +0.7274995 +0.639654032 +0.52302516 +0.428030819 +0.392135437 +0.379166466 +0.355270397 +0.303354161 +0.230390984 +0.17446127 +0.132464 +0.074622797 +0.046427761 +0.035716064 +0.023640642 +0.018215926 +0.019976042 +0.02988226 +0.046476133 +0.072996425 +0.106497079 +0.137661659 +0.162147923 +0.180330196 +0.218618301 +0.243982496 +0.240551082 +0.225425864 +0.195384092 +0.162696602 +0.133110115 +0.109225911 +0.098172278 +0.093931487 +0.092548162 +0.06666671 +0.043323577 +0.035606844 +0.033050202 +0.042877833 +0.059670726 +0.081376187 +0.104275992 +0.119670871 +0.127313573 +0.133065665 +0.139351571 +0.150276265 +0.20347446 +0.250458042 +0.266760858 +0.254182691 +0.228943454 +0.221248469 +0.217648484 +0.202941153 +0.190337529 +0.175192644 +0.16027958 +0.111385472 +0.06358879 +0.050137115 +0.04418596 +0.047363912 +0.056268215 +0.072099532 +0.093288367 +0.121556567 +0.1516196 +0.178264038 +0.199274372 +0.223152562 +0.332717603 +0.364731517 +0.344879119 +0.318980938 +0.289517904 +0.267468428 +0.253154977 +0.247997277 +0.25829239 +0.269617502 +0.270834149 +0.223838492 +0.10513709 +0.095292393 +0.05936802 +0.046292982 +0.046409527 +0.051871405 +0.059654707 +0.063969449 +0.069089458 +0.07712319 +0.086158831 +0.102456625 +0.170672252 +0.177216673 +0.164387983 +0.163886099 +0.194737522 +0.240970623 +0.281709475 +0.317820723 +0.337494292 +0.341580624 +0.337036505 +0.295830842 +0.120658265 +0.101552975 +0.075661 +0.069442905 +0.074215601 +0.079513427 +0.082370809 +0.081884505 +0.078021961 +0.075405136 +0.076284121 +0.093290181 +0.160963033 +0.16183597 +0.153293939 +0.171620848 +0.204065467 +0.250745316 +0.294423547 +0.32470342 +0.343592809 +0.352093859 +0.357686693 +0.344763068 +0.214442195 +0.257097825 +0.277979897 +0.27127473 +0.276522354 +0.301874465 +0.340321849 +0.376120591 +0.405544038 +0.427573997 +0.444359317 +0.4742685 +0.611200692 +0.646977782 +0.663189292 +0.681054052 +0.693730845 +0.703635107 +0.707620557 +0.698708947 +0.677205968 +0.661167487 +0.6535618 +0.580815285 +0.49215863 +0.554933759 +0.531374699 +0.496933383 +0.491443013 +0.515287043 +0.553343615 +0.59262274 +0.618319438 +0.62783445 +0.62910704 +0.629858564 +0.738704346 +0.74949095 +0.699147152 +0.635440081 +0.58084432 +0.562458532 +0.572300165 +0.587312211 +0.589094383 +0.579899567 +0.566885024 +0.49186018 +0.317349602 +0.353272014 +0.365501622 +0.408689856 +0.471607419 +0.542273533 +0.615640477 +0.680142887 +0.719337295 +0.727926858 +0.716421275 +0.696606918 +0.723795338 +0.670567723 +0.60979586 +0.576419509 +0.569844372 +0.581477093 +0.59661937 +0.604173706 +0.598139126 +0.579566805 +0.554567133 +0.509024045 +0.316153432 +0.300794341 +0.27975019 +0.245897174 +0.235486772 +0.247358115 +0.270969129 +0.29345087 +0.313805749 +0.32789173 +0.33143966 +0.341290847 +0.460449549 +0.466929844 +0.444087399 +0.417762654 +0.404258206 +0.391233683 +0.378905753 +0.372887066 +0.367929886 +0.361098931 +0.355678916 +0.345506 +0.147203771 +0.094697526 +0.079834474 +0.076624137 +0.077703348 +0.079820694 +0.083792869 +0.088446655 +0.096013756 +0.103488326 +0.109405529 +0.125652689 +0.183720526 +0.149658104 +0.108807817 +0.087544834 +0.082100222 +0.08373742 +0.086796115 +0.088286761 +0.089676894 +0.089179532 +0.087338083 +0.090442671 +0.031684615 +0.015212168 +0.015351683 +0.013392737 +0.01394919 +0.018343567 +0.026625204 +0.040680502 +0.058539672 +0.078174491 +0.102109648 +0.134310442 +0.230985608 +0.25989311 +0.278147284 +0.300698752 +0.329511548 +0.353854431 +0.361107588 +0.364320939 +0.365382945 +0.367825657 +0.369231977 +0.328505819 +0.171784423 +0.201923745 +0.198420441 +0.173265074 +0.166354586 +0.174907536 +0.194848814 +0.225063201 +0.260204497 +0.294017516 +0.325295742 +0.359055788 +0.453256363 +0.484767613 +0.502573549 +0.525798133 +0.521756506 +0.529617085 +0.538811848 +0.545615043 +0.542532024 +0.526977277 +0.509451256 +0.403120292 +0.176944867 +0.172735834 +0.166328534 +0.149011351 +0.145256583 +0.157666638 +0.181487389 +0.210785019 +0.24207271 +0.275740874 +0.313135703 +0.359526621 +0.426765052 +0.469528179 +0.515581515 +0.560899378 +0.61293537 +0.661077569 +0.674188512 +0.624133972 +0.568012558 +0.528693995 +0.508118699 +0.414906976 +0.201215245 +0.226103267 +0.195938957 +0.162318932 +0.150334952 +0.16091093 +0.188400971 +0.237271578 +0.300990359 +0.369248259 +0.441752339 +0.517734845 +0.648282701 +0.662688792 +0.623385497 +0.592978466 +0.54444034 +0.513212308 +0.487949705 +0.488250963 +0.512560922 +0.544167313 +0.560834322 +0.45722911 +0.430782408 +0.530777092 +0.464462303 +0.379891864 +0.322148823 +0.294781037 +0.286538652 +0.288222977 +0.295681625 +0.308480223 +0.32174025 +0.337299834 +0.484663293 +0.531805598 +0.489382395 +0.439126667 +0.405414136 +0.388542315 +0.379753903 +0.386440374 +0.41333601 +0.42764327 +0.421588351 +0.347746309 +0.164313308 +0.15833343 +0.107788571 +0.08709271 +0.082837202 +0.085345849 +0.090285452 +0.100410149 +0.109546331 +0.112585882 +0.110318698 +0.111722077 +0.191603912 +0.228322236 +0.250471873 +0.246657175 +0.226929452 +0.197518211 +0.16417268 +0.136165939 +0.113655143 +0.103436779 +0.104969213 +0.104035973 +0.045162725 +0.060049567 +0.061665092 +0.052255365 +0.04637958 +0.046523617 +0.051668844 +0.058965291 +0.066165344 +0.073493778 +0.085696043 +0.108586027 +0.175267789 +0.229434011 +0.254159917 +0.27648087 +0.303377973 +0.328516494 +0.346825434 +0.366319606 +0.393307193 +0.414610447 +0.429139184 +0.372534738 +0.17955506 +0.253862869 +0.206611629 +0.151075217 +0.126168722 +0.120376278 +0.125128191 +0.132971188 +0.145109389 +0.15921738 +0.179546638 +0.212260995 +0.273393871 +0.300514834 +0.321324122 +0.367158095 +0.439849609 +0.489923482 +0.52660336 +0.536498829 +0.518507183 +0.501462792 +0.491274673 +0.380626962 +0.177772217 +0.183082816 +0.129119338 +0.097682156 +0.08677591 +0.087136085 +0.096993518 +0.123809227 +0.16415728 +0.208558109 +0.252451663 +0.295370185 +0.379162804 +0.381661091 +0.358726148 +0.35071504 +0.364808879 +0.398347883 +0.433509926 +0.461809744 +0.455868967 +0.428773662 +0.3885917 +0.314009648 +0.112553085 +0.095585044 +0.073604128 +0.062678737 +0.072750141 +0.095326384 +0.122992991 +0.149509454 +0.1782648 +0.207983777 +0.241287304 +0.296752884 +0.441221666 +0.512974065 +0.555671112 +0.580667461 +0.598997904 +0.62491146 +0.645215283 +0.638566152 +0.635770067 +0.636045192 +0.642334136 +0.517434609 +0.297377675 +0.411802383 +0.310748707 +0.207588157 +0.151335311 +0.126763638 +0.120260054 +0.125988662 +0.135437856 +0.149574371 +0.172156625 +0.221486686 +0.371563343 +0.421852434 +0.424632294 +0.43031301 +0.458548298 +0.491755432 +0.538370588 +0.571316352 +0.572470803 +0.568054146 +0.568114607 +0.49134258 +0.307844088 +0.392148435 +0.372156442 +0.341546029 +0.340433245 +0.362262091 +0.401683388 +0.460507174 +0.53002136 +0.596985044 +0.655971339 +0.700983044 +0.81637902 +0.831288368 +0.804515472 +0.767237768 +0.722133006 +0.70123266 +0.70539223 +0.720834926 +0.739904671 +0.752577862 +0.76893686 +0.72091718 +0.574369239 +0.785145332 +0.84891957 +0.844847707 +0.82933872 +0.813641804 +0.803666619 +0.800391939 +0.803273207 +0.811891972 +0.822317477 +0.83045634 +0.861813076 +0.861693286 +0.850033432 +0.828287082 +0.803103534 +0.786263587 +0.769644477 +0.757808503 +0.741043256 +0.714084268 +0.685723059 +0.629572886 +0.450669251 +0.451795521 +0.425869112 +0.403279367 +0.391989612 +0.382785241 +0.381244806 +0.395781698 +0.431644561 +0.481173737 +0.534213725 +0.590873897 +0.682996217 +0.661166499 +0.635418276 +0.612292168 +0.589535884 +0.564625697 +0.537788288 +0.524356532 +0.530377344 +0.543605711 +0.55882102 +0.548081728 +0.392091146 +0.396597264 +0.398125932 +0.417817877 +0.45264118 +0.485462107 +0.521059187 +0.561670013 +0.603897775 +0.641172933 +0.669355682 +0.693403541 +0.754729463 +0.737730341 +0.722238685 +0.702389384 +0.681379452 +0.668340087 +0.660155325 +0.65951098 +0.659687351 +0.64825934 +0.633013902 +0.591153647 +0.388315887 +0.404894572 +0.399911396 +0.388854015 +0.384249163 +0.395593106 +0.426668109 +0.470928732 +0.52371599 +0.578213206 +0.618966986 +0.641579482 +0.700114853 +0.632557321 +0.548268841 +0.46872033 +0.420620941 +0.395285857 +0.377571139 +0.369085886 +0.36204785 +0.353270175 +0.346365302 +0.326874192 +0.134989641 +0.092872332 +0.077593113 +0.072951191 +0.077516646 +0.083877883 +0.09683251 +0.114426152 +0.127283419 +0.1360115 +0.14821097 +0.184001973 +0.262324728 +0.315081471 +0.344844499 +0.31991458 +0.313395277 +0.308583934 +0.340866872 +0.384234851 +0.419553516 +0.458443059 +0.469046344 +0.405460327 +0.339105333 +0.290197278 +0.216223793 +0.146963778 +0.094214052 +0.061733975 +0.045367776 +0.04142396 +0.047560653 +0.065649906 +0.101758299 +0.185227676 +0.400821517 +0.534473572 +0.609761949 +0.635085638 +0.637206004 +0.627231843 +0.589971491 +0.541227339 +0.509859581 +0.4803372 +0.459040287 +0.397161471 +0.367436885 +0.312326126 +0.219643642 +0.13481283 +0.080535322 +0.052645036 +0.040870383 +0.040577132 +0.047818096 +0.065108351 +0.102066707 +0.173583502 +0.30780813 +0.377844212 +0.389779896 +0.411348212 +0.43654063 +0.460174569 +0.465850256 +0.450643016 +0.42897698 +0.420475997 +0.428471413 +0.370461655 +0.325454184 +0.346416885 +0.334006048 +0.320359182 +0.315668159 +0.318739371 +0.326120306 +0.333062473 +0.34472667 +0.371399015 +0.416065392 +0.485066349 +0.610248567 +0.643292857 +0.632064734 +0.617298199 +0.606022889 +0.599744365 +0.600576949 +0.607049616 +0.605246394 +0.589006501 +0.564729632 +0.498195145 +0.458091328 +0.461086414 +0.422539482 +0.390099735 +0.373334263 +0.377483807 +0.406568116 +0.450839413 +0.503946973 +0.56453727 +0.629693323 +0.689399579 +0.750582028 +0.739982586 +0.690949587 +0.646849469 +0.615008832 +0.576812097 +0.552494342 +0.549147304 +0.555176394 +0.564824991 +0.578250949 +0.493972419 +0.482025795 +0.578044026 +0.594768587 +0.602109359 +0.627382186 +0.670688865 +0.722078965 +0.772226374 +0.806566236 +0.821858908 +0.824024705 +0.817104858 +0.845640541 +0.821530278 +0.806551254 +0.804196237 +0.802638158 +0.798670267 +0.785462509 +0.762440357 +0.737181718 +0.720277708 +0.706624795 +0.616139226 +0.51850691 +0.555317664 +0.544799556 +0.534564167 +0.533569239 +0.540485804 +0.551257017 +0.56884949 +0.589461355 +0.606644519 +0.625429258 +0.653830751 +0.722161806 +0.710543874 +0.680794611 +0.662369497 +0.656720475 +0.643812548 +0.625556108 +0.605676723 +0.586731232 +0.578338105 +0.575562585 +0.529036189 +0.39780837 +0.411843139 +0.411174002 +0.41248744 +0.417677592 +0.431969128 +0.451040982 +0.476428518 +0.501031411 +0.522030804 +0.541794606 +0.563196271 +0.623494149 +0.621463059 +0.581647168 +0.534060742 +0.491517791 +0.468192637 +0.471295796 +0.490646187 +0.515129682 +0.537254033 +0.553613601 +0.524204853 +0.325034445 +0.341828894 +0.362895559 +0.385877872 +0.40756753 +0.440742201 +0.48620579 +0.531768542 +0.574043228 +0.611205093 +0.640899117 +0.66966552 +0.724082332 +0.720202172 +0.704148706 +0.681856979 +0.652579001 +0.626315017 +0.619513053 +0.636438303 +0.667828929 +0.698099334 +0.714661382 +0.662591675 +0.442011282 +0.555888388 +0.606887112 +0.634112381 +0.655961756 +0.685512084 +0.72194952 +0.752381585 +0.772781768 +0.782873148 +0.789877974 +0.796559801 +0.847558386 +0.837475412 +0.795425603 +0.730213544 +0.670586028 +0.632943127 +0.616714488 +0.606233904 +0.591107743 +0.572249792 +0.558906406 +0.495975025 +0.276918152 +0.35495924 +0.394176796 +0.456261588 +0.515731286 +0.562505051 +0.595714805 +0.618591424 +0.634734037 +0.638663399 +0.628057923 +0.602826264 +0.675371017 +0.650435897 +0.591019377 +0.535323465 +0.510794274 +0.501255601 +0.500207914 +0.498835106 +0.493996024 +0.489773952 +0.483430533 +0.452145614 +0.222863574 +0.216673728 +0.250003572 +0.297691063 +0.354937898 +0.417851781 +0.479652093 +0.519189216 +0.539573982 +0.54938846 +0.550343824 +0.555675369 +0.672193602 +0.645771367 +0.599910296 +0.569428726 +0.546295555 +0.526633031 +0.504653612 +0.493607443 +0.495213708 +0.495858965 +0.49445916 +0.440354183 +0.266714711 +0.278650539 +0.27565958 +0.283357325 +0.30938851 +0.34879191 +0.39351357 +0.435850453 +0.47424188 +0.510752947 +0.54316318 +0.57674077 +0.697123972 +0.708811975 +0.663939891 +0.59823602 +0.530891975 +0.501049307 +0.50114868 +0.496994298 +0.489148301 +0.480415091 +0.469799775 +0.400577987 +0.21135753 +0.197872852 +0.176772626 +0.181164581 +0.188348477 +0.192227382 +0.192964032 +0.196408939 +0.204012697 +0.216910723 +0.230148298 +0.256079846 +0.373339918 +0.36863017 +0.318778957 +0.283465318 +0.267390571 +0.282944248 +0.297052488 +0.315053091 +0.325033001 +0.323623082 +0.312239146 +0.252303983 +0.135608195 +0.089349421 +0.051144861 +0.030042701 +0.025058835 +0.02692934 +0.032497966 +0.043213995 +0.062999984 +0.093846493 +0.142075005 +0.20594021 +0.31017201 +0.30937848 +0.279657874 +0.272676801 +0.286528048 +0.316573208 +0.327235526 +0.325579645 +0.318723322 +0.310154733 +0.293708949 +0.225426806 +0.158603716 +0.107868367 +0.067519388 +0.043406652 +0.032861641 +0.032448978 +0.039653981 +0.057063675 +0.086129656 +0.120046395 +0.157420978 +0.200012008 +0.290108189 +0.372341498 +0.433825187 +0.494965286 +0.535354051 +0.560796566 +0.568214157 +0.569459171 +0.541129172 +0.498642632 +0.453118839 +0.353211169 +0.28636595 +0.228098022 +0.155747248 +0.102483021 +0.078140339 +0.073809502 +0.083066698 +0.101558411 +0.126630544 +0.155996822 +0.195449799 +0.253637597 +0.348417213 +0.402373394 +0.425903477 +0.444242093 +0.455649604 +0.468015534 +0.47589101 +0.473915861 +0.460223905 +0.439855923 +0.421672129 +0.358396788 +0.266810441 +0.239860532 +0.20365822 +0.173276395 +0.160037005 +0.164986522 +0.189828503 +0.222959151 +0.259735875 +0.294645324 +0.330243216 +0.379022752 +0.519798007 +0.586081939 +0.603335999 +0.593322484 +0.586828105 +0.583883269 +0.590550662 +0.606293853 +0.608220373 +0.582840749 +0.544808073 +0.462999025 +0.382863806 +0.340327983 +0.280250205 +0.238424994 +0.226504966 +0.245239952 +0.289817007 +0.337646124 +0.377411906 +0.411729358 +0.441209185 +0.480257155 +0.590073589 +0.61699982 +0.630541858 +0.633525632 +0.619184075 +0.600022427 +0.588036074 +0.584183488 +0.58468523 +0.578454864 +0.565567379 +0.479874515 +0.390176609 +0.377246069 +0.340701386 +0.325628403 +0.316361174 +0.306891679 +0.296030733 +0.286336752 +0.279366743 +0.27364228 +0.271319789 +0.291764897 +0.369398558 +0.374440487 +0.404935537 +0.444792725 +0.484463008 +0.508446942 +0.515411502 +0.502260358 +0.480714076 +0.459275037 +0.438000681 +0.393003142 +0.34947577 +0.322813236 +0.274316998 +0.230729928 +0.198648989 +0.182183168 +0.179013213 +0.192776707 +0.217177952 +0.245793761 +0.288215362 +0.361852976 +0.511118674 +0.565662181 +0.55889197 +0.547341086 +0.536162175 +0.517519512 +0.49307182 +0.478012217 +0.463836747 +0.443000961 +0.425750704 +0.350779775 +0.351175234 +0.351926086 +0.338562547 +0.329576877 +0.3274394 +0.327847921 +0.326150058 +0.327046518 +0.331340507 +0.340278292 +0.359505111 +0.393716183 +0.501815863 +0.487241643 +0.419631778 +0.362130816 +0.332778327 +0.329982637 +0.331584635 +0.337968459 +0.352413835 +0.360978466 +0.366216072 +0.311835614 +0.251050274 +0.28719991 +0.299196153 +0.314806735 +0.322133227 +0.320412383 +0.313530838 +0.310680279 +0.311670617 +0.316798083 +0.33030221 +0.362707922 +0.478557264 +0.470551226 +0.418448404 +0.385364137 +0.38267366 +0.395440835 +0.401493709 +0.396646878 +0.383129239 +0.37624104 +0.376098131 +0.311571586 +0.213946812 +0.244104719 +0.227448586 +0.211133333 +0.207886579 +0.215009358 +0.227210109 +0.248666586 +0.273025642 +0.296324975 +0.321202895 +0.355431076 +0.484931665 +0.479361897 +0.400644407 +0.346212945 +0.32216303 +0.319644115 +0.330942336 +0.346019324 +0.358700797 +0.36310538 +0.35962831 +0.30797259 +0.154521643 +0.165389365 +0.187986561 +0.20727888 +0.221259201 +0.23338233 +0.244363876 +0.251586882 +0.258305336 +0.27149577 +0.288909684 +0.32205392 +0.448215328 +0.47428562 +0.449475607 +0.421213899 +0.396115664 +0.367570626 +0.350069681 +0.339917924 +0.339220719 +0.339680985 +0.334512399 +0.288101326 +0.14798042 +0.151822887 +0.169294693 +0.190520395 +0.210855092 +0.232120233 +0.256786752 +0.276891118 +0.291709936 +0.306013774 +0.319121387 +0.332211849 +0.423461301 +0.408287871 +0.365210442 +0.345668297 +0.34809155 +0.365318545 +0.385493046 +0.398467364 +0.39588278 +0.387163723 +0.374263812 +0.301989155 +0.178425552 +0.138086876 +0.1167562 +0.119501059 +0.135191759 +0.160194095 +0.19202665 +0.226462347 +0.258360067 +0.282987757 +0.305570061 +0.332434684 +0.426859237 +0.41347903 +0.372880171 +0.364382869 +0.375896311 +0.404787824 +0.406759437 +0.390559147 +0.369674061 +0.3541844 +0.340665582 +0.277749121 +0.162520456 +0.0995789 +0.06095635 +0.04356781 +0.043758193 +0.053280419 +0.0708714 +0.095364535 +0.125893563 +0.161348118 +0.202588475 +0.25080931 +0.357643952 +0.360563902 +0.317386689 +0.293921938 +0.287973152 +0.298729527 +0.313536291 +0.32558627 +0.333156674 +0.32635312 +0.311696401 +0.248437688 +0.12438653 +0.083098155 +0.055373812 +0.042196458 +0.040905474 +0.049155107 +0.065671526 +0.087273981 +0.111137479 +0.137462306 +0.166801053 +0.20464428 +0.303382151 +0.329022494 +0.348244916 +0.368041721 +0.36354335 +0.346777568 +0.33900496 +0.329681029 +0.319130874 +0.317943869 +0.313430921 +0.261943837 +0.159166533 +0.105154663 +0.066180895 +0.049683842 +0.062022336 +0.09054373 +0.119343626 +0.147494626 +0.179677173 +0.214058917 +0.252731524 +0.304204207 +0.394902461 +0.418158908 +0.427281283 +0.446374118 +0.457486168 +0.44397908 +0.422197506 +0.398717422 +0.388251242 +0.380143658 +0.366174926 +0.324292307 +0.26151709 +0.242904297 +0.21011176 +0.189764497 +0.180418352 +0.179989774 +0.18129229 +0.184199248 +0.186366596 +0.19039471 +0.202976414 +0.240240423 +0.321798883 +0.312137438 +0.283068345 +0.310262822 +0.367819997 +0.417365684 +0.439512366 +0.426899634 +0.402780555 +0.372727263 +0.344579959 +0.274742581 +0.230216718 +0.204775762 +0.167066781 +0.140696136 +0.122468897 +0.113210964 +0.11227376 +0.113697288 +0.116865137 +0.12364739 +0.141182574 +0.183249981 +0.288375546 +0.358914148 +0.383092212 +0.418211178 +0.44031086 +0.443249586 +0.422236048 +0.373127226 +0.3155899 +0.271698955 +0.245408574 +0.199387389 +0.169414073 +0.166862464 +0.143912645 +0.11863488 +0.095893449 +0.078811972 +0.067724555 +0.063121054 +0.065519606 +0.075682166 +0.102747907 +0.156265925 +0.26173247 +0.309347028 +0.309864492 +0.313046972 +0.318151759 +0.324131699 +0.318329821 +0.304952 +0.276509346 +0.255232834 +0.257910046 +0.243198154 +0.178985594 +0.195979647 +0.201034673 +0.209599717 +0.223608693 +0.237657526 +0.256069299 +0.275428435 +0.292729613 +0.309444014 +0.329227559 +0.357542547 +0.473325267 +0.471330273 +0.40406337 +0.336281743 +0.288039089 +0.272939507 +0.285611295 +0.322632164 +0.373181854 +0.413837427 +0.449576219 +0.426070952 +0.363101017 +0.363807963 +0.320092171 +0.272555592 +0.241177457 +0.229216878 +0.235391504 +0.251724351 +0.27199528 +0.296567721 +0.322848422 +0.359793053 +0.446281022 +0.41997836 +0.403694152 +0.419540501 +0.462965305 +0.509289016 +0.53059424 +0.5264104 +0.516341108 +0.504974702 +0.492784726 +0.427605622 +0.35089081 +0.346666305 +0.310948824 +0.292033662 +0.289427102 +0.309103368 +0.351696296 +0.410667247 +0.470644938 +0.526202414 +0.571250722 +0.604892017 +0.64816284 +0.613944467 +0.559406274 +0.506773716 +0.459718053 +0.424294536 +0.400482193 +0.38439933 +0.380583765 +0.383376257 +0.389450845 +0.339357702 +0.314671664 +0.331795686 +0.317821292 +0.298475496 +0.28569499 +0.283054819 +0.283433972 +0.283065424 +0.275917534 +0.267758357 +0.268157443 +0.301487521 +0.415981303 +0.379979903 +0.326549805 +0.302349627 +0.299043106 +0.311236922 +0.332691201 +0.347899613 +0.353275296 +0.359348005 +0.365966034 +0.346495488 +0.173648067 +0.147043527 +0.143767886 +0.154716937 +0.170811363 +0.189763707 +0.204772817 +0.212979213 +0.214177381 +0.217560602 +0.227324084 +0.257930148 +0.358635582 +0.338523486 +0.281149738 +0.248783787 +0.241160866 +0.251249144 +0.262427465 +0.257986702 +0.243302397 +0.218985022 +0.1938323 +0.169326452 +0.068306301 +0.038709407 +0.031313572 +0.030120419 +0.031953637 +0.035893442 +0.041218417 +0.049163893 +0.059547937 +0.074471289 +0.100348256 +0.154424602 +0.238196849 +0.194663245 +0.157875504 +0.163758377 +0.195328664 +0.234576034 +0.263500042 +0.267721701 +0.252995295 +0.229535071 +0.210652469 +0.174194914 +0.10044699 +0.060948959 +0.034945147 +0.016480582 +0.012759864 +0.015437807 +0.02119836 +0.029294427 +0.041648021 +0.061424746 +0.09368438 +0.144132814 +0.218909199 +0.207389041 +0.18886901 +0.198816734 +0.250095364 +0.30495282 +0.338854348 +0.352297564 +0.338094141 +0.314351502 +0.288041073 +0.228858814 +0.114706172 +0.061783092 +0.035797894 +0.020789913 +0.015680523 +0.018009916 +0.024180581 +0.033409616 +0.047119047 +0.067824594 +0.102833059 +0.165261554 +0.261601707 +0.266839244 +0.266015967 +0.313846903 +0.381831058 +0.412537821 +0.399514442 +0.365929405 +0.333101629 +0.309108317 +0.298217498 +0.254876155 +0.146987113 +0.107063057 +0.070866621 +0.04360491 +0.031243578 +0.030614267 +0.033997437 +0.041684525 +0.052565831 +0.069267648 +0.102215096 +0.155805062 +0.244427184 +0.265703929 +0.278470566 +0.323892242 +0.367401953 +0.390496302 +0.389868872 +0.388415335 +0.373491062 +0.354901139 +0.332014289 +0.272281619 +0.232731353 +0.217934629 +0.177539708 +0.140928259 +0.117924976 +0.1087315 +0.106588146 +0.100145425 +0.098213069 +0.104899303 +0.125282615 +0.179922256 +0.330840236 +0.401992554 +0.414683375 +0.436019317 +0.469875359 +0.466794078 +0.427643138 +0.370538119 +0.312295067 +0.26418949 +0.240208741 +0.204559481 +0.184095566 +0.192495026 +0.183450975 +0.174838847 +0.169484019 +0.167373991 +0.162254277 +0.15425696 +0.152478366 +0.172121492 +0.213864155 +0.2825903 +0.418647752 +0.443247959 +0.420741536 +0.395023106 +0.372449969 +0.34836968 +0.30607117 +0.266585588 +0.224445367 +0.195821636 +0.179136934 +0.144947247 +0.097144011 +0.081590651 +0.064303556 +0.05356066 +0.047842606 +0.047080563 +0.050076613 +0.052686921 +0.055203311 +0.061538083 +0.075442456 +0.11862651 +0.252874909 +0.320026402 +0.32626807 +0.310474883 +0.281376751 +0.268409488 +0.276395071 +0.275222113 +0.261833281 +0.229577685 +0.197284739 +0.155524061 +0.110279113 +0.081209331 +0.059155133 +0.041790916 +0.034255851 +0.032746843 +0.036095216 +0.043904762 +0.057759543 +0.077337906 +0.107362159 +0.16227108 +0.264205548 +0.298349905 +0.314227991 +0.306978355 +0.31928942 +0.340048329 +0.379838011 +0.389196282 +0.361610346 +0.329107449 +0.292621014 +0.228982804 +0.119566713 +0.077233464 +0.047764651 +0.027579751 +0.020362525 +0.020140759 +0.025912729 +0.036722185 +0.053834035 +0.078400113 +0.115200797 +0.166016267 +0.237913943 +0.242483631 +0.261778334 +0.302940614 +0.345921508 +0.386551367 +0.410356143 +0.40427176 +0.383173584 +0.35028292 +0.319202539 +0.252009177 +0.155724993 +0.127068835 +0.087864946 +0.061799378 +0.051641456 +0.052458346 +0.06010675 +0.072334889 +0.09253685 +0.123411069 +0.166834234 +0.216799338 +0.303766221 +0.326442468 +0.347917081 +0.392312311 +0.415891017 +0.406456799 +0.374110414 +0.329878417 +0.28193644 +0.239679805 +0.212942772 +0.16914131 +0.158805577 +0.146358533 +0.120303778 +0.102537554 +0.094098105 +0.0942609 +0.102299678 +0.118266795 +0.137891413 +0.158975249 +0.199885795 +0.266391878 +0.35557081 +0.356668192 +0.319301637 +0.302663216 +0.31269667 +0.333591214 +0.32787458 +0.300942285 +0.267509978 +0.224962 +0.198973592 +0.160807015 +0.155748537 +0.166335304 +0.167733366 +0.166082695 +0.163984663 +0.167023112 +0.179378636 +0.198883991 +0.219683661 +0.236680619 +0.249100434 +0.27276674 +0.346508908 +0.382907437 +0.401576482 +0.411858038 +0.406625571 +0.382932186 +0.351147267 +0.319503587 +0.297139472 +0.281967561 +0.281606455 +0.259685645 +0.247808469 +0.26684961 +0.262234517 +0.250942075 +0.237817507 +0.242979011 +0.261254044 +0.28820516 +0.322534044 +0.360725631 +0.388838652 +0.413987117 +0.485538157 +0.503277367 +0.50477147 +0.491718619 +0.466406464 +0.437493156 +0.404845469 +0.393574207 +0.385081878 +0.383109253 +0.388647187 +0.35673277 +0.334650573 +0.372132527 +0.367190457 +0.364593019 +0.377617584 +0.402249773 +0.434283997 +0.466904304 +0.496254913 +0.521437291 +0.540510565 +0.5581314 +0.570610466 +0.541440738 +0.516016379 +0.496253121 +0.481955656 +0.466088384 +0.446838101 +0.428992784 +0.418995771 +0.413151538 +0.405486465 +0.369421264 +0.293022403 +0.31737108 +0.282345255 +0.257626155 +0.253815072 +0.259860382 +0.268077639 +0.279664786 +0.290806103 +0.299851389 +0.309617899 +0.335565059 +0.388652607 +0.364111082 +0.336328166 +0.326732835 +0.334492874 +0.353515656 +0.371161568 +0.385768226 +0.394009893 +0.392146564 +0.389120392 +0.367386665 +0.197869465 +0.180489732 +0.143760106 +0.118851877 +0.115025769 +0.122937296 +0.134907968 +0.154114621 +0.178999754 +0.204444875 +0.229392844 +0.268147641 +0.314589121 +0.242237179 +0.190368345 +0.178639039 +0.200082712 +0.238033353 +0.257585877 +0.261007141 +0.26097006 +0.25421048 +0.245777293 +0.234366126 +0.12586337 +0.093801833 +0.079065911 +0.071974341 +0.068720275 +0.066619671 +0.06586125 +0.064458106 +0.065721383 +0.069336414 +0.074000991 +0.098687326 +0.149059439 +0.132181418 +0.124878916 +0.134664508 +0.181655131 +0.211106631 +0.216465476 +0.193583464 +0.166555562 +0.157824321 +0.146929288 +0.132218344 +0.073544385 +0.046718401 +0.032769184 +0.027241607 +0.029318026 +0.037088581 +0.048544702 +0.060797092 +0.075854261 +0.092962788 +0.114911597 +0.158701491 +0.241344405 +0.270659206 +0.29491401 +0.305654097 +0.308251025 +0.305039352 +0.301685034 +0.298464634 +0.293635576 +0.303745987 +0.316342646 +0.295085594 +0.20752556 +0.196486999 +0.185757669 +0.171223188 +0.17433175 +0.190433942 +0.218500844 +0.254419572 +0.293622344 +0.333231667 +0.373045012 +0.433911488 +0.507025605 +0.501303565 +0.495585404 +0.473037698 +0.46004734 +0.456841588 +0.447581797 +0.437011528 +0.424950605 +0.399370793 +0.381485682 +0.324306433 +0.209516444 +0.185480755 +0.158159699 +0.138015515 +0.119916664 +0.10601889 +0.09814419 +0.094032088 +0.094819174 +0.101095203 +0.109181269 +0.140408681 +0.205005557 +0.228867886 +0.240071448 +0.290438278 +0.32711791 +0.344229434 +0.344453791 +0.328459931 +0.303514035 +0.268995045 +0.236154566 +0.189729774 +0.120341381 +0.095952629 +0.078020831 +0.067793708 +0.061134066 +0.057373711 +0.056782938 +0.054976643 +0.055540505 +0.060205408 +0.072897265 +0.110989211 +0.194655096 +0.250702047 +0.287190691 +0.298389676 +0.312813307 +0.331494693 +0.313688544 +0.26674599 +0.218598028 +0.185588444 +0.176526552 +0.163318352 +0.111751862 +0.092976373 +0.068999958 +0.053124054 +0.04737099 +0.048521403 +0.055161741 +0.063063321 +0.070085086 +0.078278184 +0.097605239 +0.146495229 +0.244535527 +0.267799619 +0.270374057 +0.288902254 +0.320990037 +0.352630151 +0.372233493 +0.361268586 +0.342010468 +0.323260645 +0.305037769 +0.273688822 +0.185003077 +0.124740874 +0.098030233 +0.089357449 +0.086821821 +0.089231168 +0.097262264 +0.110715495 +0.126096617 +0.143351942 +0.172407225 +0.240468436 +0.342286319 +0.362832275 +0.358050931 +0.364733683 +0.391541397 +0.414329827 +0.425773705 +0.404015429 +0.369773282 +0.337360568 +0.310326963 +0.268201974 +0.196106202 +0.152734891 +0.120928533 +0.095950988 +0.079151723 +0.070400121 +0.068510517 +0.070958572 +0.078796445 +0.094455948 +0.123373912 +0.186103666 +0.282348077 +0.298602706 +0.287399994 +0.289079894 +0.325717818 +0.37251549 +0.415282424 +0.417748392 +0.389073282 +0.358655766 +0.328756566 +0.283068789 +0.193425747 +0.138523297 +0.090271882 +0.056679468 +0.039056739 +0.031631564 +0.030706967 +0.035651729 +0.050833367 +0.07964962 +0.125665091 +0.195612875 +0.298631861 +0.289219312 +0.255404313 +0.243246801 +0.250716427 +0.276400072 +0.299508902 +0.317784898 +0.32807519 +0.321538536 +0.305759154 +0.277922513 +0.193491914 +0.135198884 +0.096382379 +0.075330199 +0.074156448 +0.078104478 +0.084972014 +0.092947128 +0.102768445 +0.123751851 +0.162733539 +0.225053516 +0.292167224 +0.268525901 +0.266587959 +0.311429717 +0.349171624 +0.380440792 +0.410736568 +0.433017097 +0.450801717 +0.456349344 +0.467198025 +0.480721275 +0.388536462 +0.396165278 +0.38836836 +0.377548112 +0.38174205 +0.392708832 +0.409026755 +0.439274305 +0.478068575 +0.523473935 +0.581080804 +0.655511677 +0.714071681 +0.722807897 +0.722166371 +0.721505914 +0.717000216 +0.684678988 +0.658770085 +0.648108921 +0.645321685 +0.652528166 +0.663943989 +0.643922185 +0.611955473 +0.632280942 +0.585762426 +0.546075007 +0.516499379 +0.494754915 +0.47803282 +0.472177538 +0.469996992 +0.460782053 +0.444867448 +0.436144006 +0.50192403 +0.480881659 +0.439730188 +0.400925883 +0.382593191 +0.375058255 +0.364439412 +0.347738363 +0.336576372 +0.330612568 +0.326868209 +0.310544046 +0.184148651 +0.130685003 +0.093923926 +0.088197231 +0.089386736 +0.094021934 +0.09731108 +0.09349923 +0.086053949 +0.084933446 +0.099027203 +0.138457362 +0.200684312 +0.172886976 +0.131460943 +0.129138022 +0.1555186 +0.187742756 +0.216874738 +0.231943538 +0.236941829 +0.235447134 +0.228430206 +0.203183013 +0.089191177 +0.041709412 +0.020331429 +0.012042267 +0.012030479 +0.016887843 +0.025729257 +0.037113591 +0.049018619 +0.061833272 +0.086497585 +0.143520312 +0.227908378 +0.200168473 +0.167420389 +0.177705622 +0.212685756 +0.232092783 +0.227563762 +0.217054659 +0.203047203 +0.185219471 +0.164698827 +0.132915258 +0.056406025 +0.025234161 +0.011802632 +0.005958392 +0.005624915 +0.009169624 +0.0160002 +0.025600968 +0.039906582 +0.064004956 +0.107757128 +0.181494392 +0.271722054 +0.250700569 +0.227072054 +0.218976056 +0.226559493 +0.238437452 +0.231209725 +0.22766773 +0.229605077 +0.229571513 +0.223614544 +0.193384993 +0.107805079 +0.059433043 +0.032949097 +0.021772716 +0.026077537 +0.038700895 +0.057426517 +0.084244685 +0.115273049 +0.149184729 +0.19125403 +0.250532958 +0.328914588 +0.27944445 +0.233592991 +0.224311354 +0.248896106 +0.283147609 +0.302390461 +0.296736934 +0.280900119 +0.264585557 +0.25418783 +0.242662069 +0.143572591 +0.110600474 +0.112147154 +0.118792363 +0.134417332 +0.154963028 +0.180306057 +0.211443157 +0.240805342 +0.266287129 +0.29460145 +0.350278134 +0.437250127 +0.415664322 +0.381798082 +0.347936672 +0.321547081 +0.301854479 +0.286288278 +0.272594068 +0.256163679 +0.236526088 +0.219058276 +0.202831201 +0.108853574 +0.086312564 +0.098292444 +0.107859224 +0.118168436 +0.129318077 +0.140493535 +0.157839045 +0.177873608 +0.197871826 +0.226344943 +0.29059287 +0.372812267 +0.333450108 +0.27710844 +0.251925011 +0.259049802 +0.272020629 +0.272953863 +0.274662063 +0.279841406 +0.271826593 +0.262782439 +0.243977719 +0.125274386 +0.079821688 +0.055118047 +0.038798311 +0.036247933 +0.040292341 +0.045327505 +0.054664473 +0.069651522 +0.089852062 +0.122215115 +0.196407775 +0.286174462 +0.279559506 +0.274103308 +0.303825983 +0.347130429 +0.366628542 +0.377128605 +0.37168687 +0.350649012 +0.323649196 +0.29819821 +0.252778497 +0.147621389 +0.0887292 +0.049219161 +0.030043571 +0.028107607 +0.034592942 +0.043240716 +0.054864231 +0.06727016 +0.082993742 +0.108069564 +0.169942828 +0.279546726 +0.284822911 +0.258967738 +0.236720405 +0.242713706 +0.260066498 +0.273977584 +0.273222318 +0.266772053 +0.257061126 +0.240912803 +0.219333425 +0.149257707 +0.102750114 +0.070381903 +0.053633585 +0.053752787 +0.061040346 +0.070285235 +0.080419286 +0.091734683 +0.107958298 +0.138053688 +0.207825096 +0.308770207 +0.345294487 +0.340054516 +0.326401684 +0.341049552 +0.348709571 +0.353837277 +0.345493531 +0.33361194 +0.322556311 +0.318050905 +0.298847656 +0.177076432 +0.130329893 +0.107440286 +0.101392403 +0.101416104 +0.105063501 +0.112055046 +0.127572567 +0.146983901 +0.16950226 +0.208614734 +0.292879788 +0.400467237 +0.380606462 +0.338011201 +0.30457871 +0.288732132 +0.289510418 +0.296379142 +0.301391967 +0.307175758 +0.314036486 +0.316627695 +0.308034468 +0.192692615 +0.146567295 +0.124844546 +0.103627601 +0.095572942 +0.089711903 +0.082266197 +0.080609775 +0.082953574 +0.086680195 +0.099817221 +0.165369592 +0.302310342 +0.303891976 +0.277990423 +0.2662932 +0.270131094 +0.275082821 +0.270189763 +0.250915132 +0.224317111 +0.200641212 +0.18902719 +0.187508156 +0.100208138 +0.05611345 +0.037137967 +0.033061387 +0.033936804 +0.035532592 +0.037649037 +0.040148879 +0.044199338 +0.050943678 +0.062439004 +0.100714259 +0.179044235 +0.197014164 +0.185912394 +0.18744689 +0.186872302 +0.205651056 +0.217217126 +0.239257719 +0.252457692 +0.247636767 +0.237532209 +0.209270753 +0.115203185 +0.089817557 +0.064393721 +0.045941016 +0.038163803 +0.038902246 +0.045427223 +0.055908136 +0.068304307 +0.081326405 +0.095973032 +0.141994513 +0.234499579 +0.255034696 +0.244550216 +0.240527684 +0.244095724 +0.242874907 +0.241926167 +0.234126249 +0.219809066 +0.204944715 +0.190024133 +0.173513295 +0.084791478 +0.055725043 +0.042773571 +0.035305253 +0.038693629 +0.050755262 +0.071125712 +0.097061364 +0.123306937 +0.14902592 +0.17945056 +0.255441353 +0.351954948 +0.332786766 +0.286196781 +0.243161448 +0.22317218 +0.231771114 +0.25207401 +0.25910513 +0.244693302 +0.22143389 +0.202052218 +0.196899722 +0.123380127 +0.083912064 +0.062322674 +0.052434151 +0.060654943 +0.083172163 +0.117843129 +0.154566337 +0.179474831 +0.1962373 +0.215378241 +0.308340302 +0.410457437 +0.398812589 +0.365420492 +0.330674595 +0.293560268 +0.264449387 +0.247768654 +0.238735884 +0.239259767 +0.256224945 +0.286467975 +0.314377554 +0.212625779 +0.126658809 +0.08883398 +0.067116112 +0.058400424 +0.061567763 +0.070564786 +0.082628422 +0.094155692 +0.106648473 +0.125783023 +0.203091222 +0.293528376 +0.267611603 +0.210209427 +0.165001652 +0.156071948 +0.188860635 +0.238032652 +0.276081628 +0.309564987 +0.328439201 +0.327491981 +0.310973697 +0.166103572 +0.117840406 +0.093495431 +0.070956397 +0.060786585 +0.062779127 +0.073366179 +0.090199375 +0.108407302 +0.130319876 +0.161569495 +0.262429912 +0.378814644 +0.351400677 +0.311042136 +0.302360867 +0.314683141 +0.343007878 +0.387354291 +0.421644511 +0.437388795 +0.433120342 +0.422776367 +0.405926217 +0.267240678 +0.194395865 +0.150909593 +0.111480553 +0.088126715 +0.081849148 +0.085065281 +0.096575661 +0.113297792 +0.137390781 +0.168688984 +0.245372176 +0.338447015 +0.340898599 +0.33542074 +0.339936736 +0.346476132 +0.351390553 +0.37164536 +0.399988971 +0.422874315 +0.436312624 +0.432695102 +0.423576433 +0.290462739 +0.23722735 +0.211662829 +0.180655322 +0.159981528 +0.159456372 +0.168406269 +0.188166453 +0.208670764 +0.23101321 +0.265082524 +0.365766018 +0.487663077 +0.494503915 +0.479931868 +0.466363168 +0.452587678 +0.4456225 +0.450289838 +0.451173177 +0.437762038 +0.426355386 +0.400800495 +0.364512747 +0.197747969 +0.130991555 +0.115277375 +0.113824942 +0.133499999 +0.161736504 +0.188777771 +0.216731833 +0.238703972 +0.252706317 +0.26610898 +0.338941983 +0.38779231 +0.30799201 +0.227665529 +0.182948263 +0.167344833 +0.166642672 +0.173927287 +0.176117744 +0.17230379 +0.166796375 +0.163110339 +0.158523711 +0.112730526 +0.056938328 +0.049849552 +0.052119603 +0.057523237 +0.065185596 +0.074615564 +0.093335522 +0.113683922 +0.128851782 +0.14404852 +0.217963701 +0.240324181 +0.180987213 +0.135607447 +0.120620527 +0.14088015 +0.177927773 +0.220800059 +0.237170246 +0.233662493 +0.225478866 +0.213385905 +0.205884772 +0.155278186 +0.10312709 +0.073186122 +0.053343951 +0.038277125 +0.035833621 +0.044005201 +0.055232807 +0.065339686 +0.077246384 +0.101272141 +0.189729304 +0.231497347 +0.185582756 +0.154132716 +0.188399614 +0.278375252 +0.338214763 +0.359174141 +0.359306327 +0.352449471 +0.344551245 +0.333603871 +0.312185344 +0.156182063 +0.063794246 +0.037054361 +0.023787204 +0.021142869 +0.024923382 +0.037811708 +0.060949497 +0.092808756 +0.128938315 +0.166251048 +0.24840535 +0.307103825 +0.290717921 +0.28850616 +0.306944073 +0.355302689 +0.387262938 +0.381662764 +0.372240017 +0.37531919 +0.389852158 +0.404082788 +0.415925081 +0.285721777 +0.18140466 +0.172766994 +0.1663511 +0.172849118 +0.194363081 +0.23156126 +0.291383112 +0.360492656 +0.421251474 +0.474509544 +0.57060312 +0.63637323 +0.556423913 +0.459558353 +0.368398675 +0.304310492 +0.263443006 +0.246441807 +0.24008367 +0.24250923 +0.253282969 +0.26498849 +0.273243163 +0.207450229 +0.153317173 +0.172534124 +0.184028168 +0.214562721 +0.258311652 +0.307198124 +0.353634343 +0.385785544 +0.406980049 +0.424125881 +0.500128131 +0.517544014 +0.430393719 +0.333644461 +0.292853441 +0.288485412 +0.300440049 +0.314522443 +0.323894096 +0.323104172 +0.318076126 +0.307499062 +0.284375622 +0.159313173 +0.104857912 +0.088963062 +0.080839609 +0.080260422 +0.089956174 +0.107576064 +0.131151104 +0.156575925 +0.182100025 +0.209420833 +0.309509018 +0.360234401 +0.3293185 +0.296120536 +0.276438847 +0.271772957 +0.277326208 +0.282218842 +0.287624932 +0.285181312 +0.275436246 +0.258433625 +0.236905483 +0.158996836 +0.099651685 +0.076646177 +0.062875263 +0.061013028 +0.069556549 +0.084463352 +0.110304259 +0.146156209 +0.180635837 +0.22217247 +0.349389194 +0.412034234 +0.370294036 +0.335990137 +0.31740238 +0.308361549 +0.311312809 +0.331754949 +0.347144554 +0.350140129 +0.34391825 +0.322452945 +0.293436397 +0.180482217 +0.101537848 +0.078389893 +0.065642113 +0.06232099 +0.06926229 +0.083330057 +0.105214799 +0.128189206 +0.150112465 +0.177841658 +0.30003283 +0.376511627 +0.336063536 +0.285382833 +0.248329453 +0.23141899 +0.229197169 +0.235171745 +0.236414449 +0.239119077 +0.247561525 +0.255595116 +0.25654706 +0.189752717 +0.090116262 +0.066938124 +0.049232912 +0.042654849 +0.045790691 +0.056583981 +0.070831758 +0.08358742 +0.09584955 +0.116609963 +0.222408158 +0.273744704 +0.212077498 +0.156815612 +0.132440027 +0.125681294 +0.125878258 +0.134504658 +0.14433062 +0.155142138 +0.171428178 +0.192320119 +0.212303607 +0.167652809 +0.075030751 +0.046201786 +0.025518137 +0.01608412 +0.012897932 +0.014169476 +0.019107383 +0.028293733 +0.041570563 +0.064616923 +0.165580407 +0.24015659 +0.197851082 +0.14518942 +0.12177574 +0.116706107 +0.122834513 +0.137412287 +0.148347931 +0.155809385 +0.156562087 +0.147411696 +0.130457178 +0.078802661 +0.023320696 +0.00922726 +0.005479887 +0.005423689 +0.006887666 +0.00964683 +0.014795937 +0.022336001 +0.034054846 +0.061135889 +0.161554532 +0.194415197 +0.131638258 +0.092697081 +0.091259531 +0.125269509 +0.169314394 +0.189705055 +0.193281024 +0.187269744 +0.180128053 +0.173579242 +0.168244178 +0.110870449 +0.035927471 +0.017214114 +0.008467599 +0.007366435 +0.009447434 +0.013963093 +0.021948006 +0.033825678 +0.051472058 +0.086337975 +0.19341884 +0.226904565 +0.163265024 +0.113244196 +0.106894623 +0.127549227 +0.163674818 +0.190176828 +0.193242203 +0.18920606 +0.180569566 +0.168264253 +0.157119998 +0.108695414 +0.047108495 +0.041854014 +0.037029905 +0.03867119 +0.048821406 +0.070308458 +0.101263451 +0.144552731 +0.198900099 +0.264345946 +0.396255266 +0.433910368 +0.365834564 +0.300934923 +0.269489741 +0.257302181 +0.246566719 +0.228451122 +0.222201174 +0.230415382 +0.24586866 +0.264945314 +0.278721256 +0.22359776 +0.194985042 +0.205333944 +0.205142401 +0.215761951 +0.242638377 +0.273124794 +0.300311291 +0.323783141 +0.34570648 +0.37566599 +0.456097753 +0.47044967 +0.439320445 +0.416389726 +0.409863524 +0.410311707 +0.413638636 +0.415958334 +0.414073553 +0.413896264 +0.415826972 +0.417976621 +0.420628969 +0.319348676 +0.258254479 +0.219419272 +0.167699154 +0.136274691 +0.120865384 +0.11796312 +0.124390511 +0.139639114 +0.167003604 +0.217746839 +0.381809835 +0.457525121 +0.418417026 +0.376937286 +0.356222923 +0.343119758 +0.32830243 +0.315518918 +0.307770689 +0.313410507 +0.325235323 +0.339158381 +0.348207148 +0.243726453 +0.135686206 +0.093539096 +0.060221664 +0.044279051 +0.04054642 +0.045788773 +0.05688684 +0.067312035 +0.078188717 +0.104535305 +0.210948521 +0.208908784 +0.121825349 +0.068914895 +0.056488465 +0.074053841 +0.105870217 +0.129919473 +0.137995744 +0.133212209 +0.120068064 +0.107269319 +0.096994181 +0.06449948 +0.021518488 +0.010203605 +0.007417424 +0.007796416 +0.009179311 +0.01115683 +0.015544985 +0.024296547 +0.042828318 +0.090071456 +0.214379916 +0.246899715 +0.186067682 +0.147873367 +0.158043439 +0.185423824 +0.216426924 +0.225233852 +0.229154785 +0.233293499 +0.230397852 +0.2252037 +0.21920133 +0.1500533 +0.069241447 +0.043378001 +0.028531389 +0.018628243 +0.016025048 +0.017318712 +0.021498733 +0.030029314 +0.048684513 +0.095025674 +0.196780305 +0.223466953 +0.197725333 +0.185941771 +0.211800209 +0.247523208 +0.265511427 +0.275899862 +0.284715559 +0.287642333 +0.281562396 +0.273344605 +0.265322378 +0.18789843 +0.08872563 +0.056556879 +0.039236487 +0.027670089 +0.023299719 +0.022648439 +0.026189656 +0.036714687 +0.059943173 +0.111633787 +0.218388287 +0.24203982 +0.206902369 +0.187896241 +0.211817513 +0.23665054 +0.237183076 +0.227056823 +0.219015407 +0.213669602 +0.208406291 +0.196708467 +0.177746278 +0.114134981 +0.065256725 +0.051423885 +0.041277039 +0.032173736 +0.032258217 +0.039534382 +0.049952132 +0.062334407 +0.084408835 +0.134814838 +0.251949423 +0.315316317 +0.309078197 +0.297210257 +0.317748112 +0.329477288 +0.339485885 +0.356725086 +0.361954806 +0.367138995 +0.372694357 +0.363176331 +0.3435956 +0.235768809 +0.17960865 +0.15347296 +0.124365291 +0.103169135 +0.094836544 +0.093857745 +0.099231267 +0.108863301 +0.12561171 +0.162405527 +0.278245671 +0.370847919 +0.40038514 +0.416926188 +0.435506519 +0.441463378 +0.422924849 +0.400959877 +0.378490264 +0.351120816 +0.325921408 +0.303776189 +0.286380704 +0.221761149 +0.181230037 +0.11718628 +0.062391348 +0.036947134 +0.029773875 +0.032229184 +0.039065639 +0.048676835 +0.061936294 +0.09122281 +0.180447029 +0.279425241 +0.334905927 +0.357786755 +0.348056724 +0.318184752 +0.282686025 +0.248839788 +0.242456374 +0.242218164 +0.228025559 +0.206150302 +0.184584816 +0.10431975 +0.051085265 +0.044101406 +0.039147681 +0.038766211 +0.03962501 +0.041283684 +0.044543776 +0.050905744 +0.060587545 +0.077643136 +0.142455013 +0.160755648 +0.133603332 +0.114529002 +0.114791794 +0.135766015 +0.152259217 +0.161346742 +0.161159174 +0.155296283 +0.150185749 +0.148998042 +0.150605237 +0.096685952 +0.043138683 +0.025302886 +0.016729187 +0.016642193 +0.021106634 +0.02979872 +0.04720351 +0.073210416 +0.099602194 +0.12846909 +0.223260408 +0.243881961 +0.209126265 +0.177729177 +0.157173547 +0.138875346 +0.121069239 +0.111650016 +0.110229223 +0.113661867 +0.115437142 +0.11152105 +0.102258045 +0.091686781 +0.025517687 +0.022727766 +0.02961244 +0.039567722 +0.054826346 +0.074144052 +0.098667989 +0.128995056 +0.1623752 +0.203865299 +0.329413898 +0.333200916 +0.29274426 +0.272318215 +0.271387956 +0.264389005 +0.240542647 +0.224954661 +0.214063526 +0.208184286 +0.211120686 +0.214826587 +0.213073065 +0.179239586 +0.103413194 +0.08792461 +0.068580446 +0.051652593 +0.041047732 +0.039623425 +0.048670612 +0.06299599 +0.08041932 +0.118852906 +0.258140518 +0.279434698 +0.226108337 +0.202351208 +0.208219102 +0.230824669 +0.245796045 +0.256375973 +0.263156211 +0.261384472 +0.248616011 +0.230996331 +0.215460531 +0.156251995 +0.056259544 +0.030002825 +0.018684873 +0.01443994 +0.016913226 +0.024693728 +0.036925851 +0.051237571 +0.066585674 +0.095785894 +0.209099436 +0.237447493 +0.196469283 +0.160332353 +0.1467477 +0.15290117 +0.164242381 +0.166431875 +0.166083295 +0.160734482 +0.15247341 +0.142956408 +0.133705781 +0.099557182 +0.035278821 +0.016823622 +0.008354819 +0.006731307 +0.01068742 +0.018803633 +0.029369008 +0.039606459 +0.052184594 +0.079452598 +0.172994443 +0.168661936 +0.1162814 +0.082155137 +0.072377284 +0.083310301 +0.114553279 +0.156075627 +0.192990035 +0.221254888 +0.234747032 +0.23139905 +0.223633448 +0.175773778 +0.093123322 +0.080753712 +0.066426114 +0.058118406 +0.057141715 +0.064342743 +0.079003991 +0.102192259 +0.134916741 +0.192874049 +0.336251948 +0.366752347 +0.341622733 +0.319238906 +0.306616052 +0.297534269 +0.294301935 +0.29525999 +0.286302118 +0.270638588 +0.254524444 +0.240036896 +0.222531119 +0.15455368 +0.058231897 +0.048095223 +0.045142507 +0.045291149 +0.053347907 +0.066771514 +0.080026254 +0.09327167 +0.110829072 +0.14540971 +0.246167243 +0.23321628 +0.180946306 +0.15481618 +0.152129971 +0.151317981 +0.146830972 +0.145135022 +0.136275946 +0.12784708 +0.121935206 +0.111796101 +0.097752326 +0.086307418 +0.025652587 +0.015102107 +0.018135149 +0.022892986 +0.031689912 +0.046255657 +0.070290934 +0.101489445 +0.140577366 +0.20542075 +0.382081353 +0.429619654 +0.39006632 +0.371555806 +0.379795741 +0.378464877 +0.36514153 +0.350737114 +0.34160148 +0.338888395 +0.338420151 +0.337930376 +0.33454599 +0.282040417 +0.16574647 +0.151026048 +0.122413542 +0.110342056 +0.113075004 +0.130047552 +0.164638874 +0.204097569 +0.241475435 +0.28414005 +0.460191365 +0.525122965 +0.503474487 +0.453275085 +0.391383819 +0.334073439 +0.290368105 +0.265657942 +0.242614051 +0.220565806 +0.207991167 +0.195662145 +0.184379824 +0.171769503 +0.057364123 +0.065003975 +0.064468599 +0.056040928 +0.050772961 +0.049424767 +0.051642938 +0.060199885 +0.075961285 +0.11320308 +0.267399449 +0.346624783 +0.347990297 +0.338493663 +0.347391035 +0.377374276 +0.420357935 +0.445460278 +0.443432983 +0.4387782 +0.436843265 +0.436250456 +0.42801173 +0.388612647 +0.342901547 +0.311605967 +0.268266131 +0.23389965 +0.212972322 +0.205162826 +0.21012865 +0.218582028 +0.233480686 +0.266627955 +0.411416653 +0.484201313 +0.479572098 +0.468092558 +0.464008468 +0.466340489 +0.469000612 +0.467873878 +0.466833688 +0.459299378 +0.452929682 +0.44674543 +0.438939283 +0.35521042 +0.26614828 +0.24036662 +0.208889436 +0.194491568 +0.193667695 +0.207634482 +0.241813708 +0.289763866 +0.343199611 +0.409007715 +0.585887161 +0.644817043 +0.642163657 +0.632668608 +0.624437407 +0.622258862 +0.625743703 +0.617842282 +0.594222776 +0.571463942 +0.553255698 +0.539822427 +0.529840493 +0.455162916 +0.308969692 +0.331750257 +0.316714186 +0.298507734 +0.300277122 +0.321580011 +0.354146077 +0.386582156 +0.419978316 +0.471733246 +0.659089596 +0.711393405 +0.694701328 +0.674311897 +0.657494828 +0.64331717 +0.622419338 +0.58892845 +0.554626227 +0.519224603 +0.479259928 +0.444525268 +0.416772121 +0.350986567 +0.214458783 +0.166837715 +0.121409081 +0.100505939 +0.103172009 +0.129303883 +0.163393118 +0.201921477 +0.249487428 +0.325117785 +0.521081748 +0.58501163 +0.586472805 +0.57386957 +0.57155295 +0.553684771 +0.533086282 +0.528625712 +0.546145688 +0.574950659 +0.60141864 +0.593364455 +0.571367481 +0.506085617 +0.499924527 +0.51089255 +0.509480992 +0.500432623 +0.499823105 +0.515882781 +0.551088381 +0.586995185 +0.617264099 +0.653896495 +0.76903348 +0.79773008 +0.772483353 +0.734401041 +0.702406318 +0.672515823 +0.646229449 +0.634136711 +0.634608939 +0.631899917 +0.619940614 +0.600476264 +0.573126484 +0.462312119 +0.292818928 +0.278404321 +0.211359993 +0.163342063 +0.132105252 +0.116421191 +0.118290192 +0.131783548 +0.149529802 +0.178318065 +0.316772932 +0.346596398 +0.317036007 +0.277138657 +0.244668577 +0.230493358 +0.226384001 +0.220681917 +0.217988238 +0.216085117 +0.213681133 +0.208066705 +0.199769008 +0.190536443 +0.061107185 +0.030369911 +0.026446773 +0.028078743 +0.036315643 +0.048845351 +0.066417286 +0.087251025 +0.107506445 +0.138722716 +0.271756546 +0.291598651 +0.248489154 +0.197709961 +0.161329523 +0.135128318 +0.119751697 +0.113979234 +0.121093934 +0.135217905 +0.144144675 +0.142668494 +0.13327053 +0.125797494 +0.035600966 +0.005250511 +0.003307637 +0.005261896 +0.010696941 +0.01851005 +0.028562925 +0.038730764 +0.047703146 +0.06612868 +0.16151748 +0.161806506 +0.108292475 +0.067351485 +0.050418942 +0.046521779 +0.049814223 +0.057703028 +0.069368496 +0.080235787 +0.086170466 +0.090581709 +0.096144022 +0.107405177 +0.044055285 +0.016702341 +0.013276611 +0.01437833 +0.020094636 +0.027623446 +0.033386568 +0.03742645 +0.041783167 +0.066239741 +0.182562773 +0.227554243 +0.234162035 +0.256249133 +0.297309458 +0.341537997 +0.374713484 +0.394334864 +0.413263227 +0.429488551 +0.440397409 +0.446072984 +0.446758515 +0.422716024 +0.212298067 +0.19226747 +0.201389256 +0.19583173 +0.19541038 +0.204299994 +0.222157504 +0.242041014 +0.262548452 +0.318805037 +0.480725335 +0.514673757 +0.514653446 +0.534244946 +0.584855982 +0.600401183 +0.587089155 +0.567889061 +0.551012338 +0.529619014 +0.509120129 +0.490535704 +0.475646536 +0.410855339 +0.270100883 +0.316579847 +0.300009839 +0.288384415 +0.299533672 +0.323251551 +0.347957966 +0.365394448 +0.374621667 +0.398006603 +0.54733779 +0.566639125 +0.543477571 +0.522666243 +0.504404682 +0.48682249 +0.467857417 +0.438892399 +0.407366644 +0.371193248 +0.325636239 +0.27763935 +0.2257444 +0.174680242 +0.046858353 +0.01745591 +0.016156174 +0.024608119 +0.034688401 +0.04174936 +0.041339486 +0.036736889 +0.034596895 +0.047676364 +0.168635824 +0.257627695 +0.264189081 +0.240283242 +0.215517974 +0.195516453 +0.181528658 +0.16812674 +0.151192299 +0.135103614 +0.121199598 +0.111622249 +0.104751312 +0.093333017 +0.028454669 +0.010919505 +0.012678521 +0.020278454 +0.028761885 +0.03668453 +0.04230027 +0.044573152 +0.049407656 +0.072503273 +0.228257059 +0.331081034 +0.366184343 +0.369600469 +0.358123001 +0.367727369 +0.39006265 +0.406870469 +0.408458502 +0.395630177 +0.383591381 +0.36950027 +0.354190102 +0.323447446 +0.160086361 +0.19896628 +0.240798667 +0.259246666 +0.271739047 +0.278264266 +0.271950651 +0.257393215 +0.251455307 +0.30081882 +0.551175209 +0.652081556 +0.673539705 +0.661275038 +0.63695921 +0.607234786 +0.571700714 +0.534154209 +0.492098401 +0.459436788 +0.42614151 +0.385362507 +0.345472155 +0.284567649 +0.100241977 +0.086806706 +0.067464593 +0.052854278 +0.044804849 +0.038766977 +0.035287933 +0.038185596 +0.048203301 +0.082106649 +0.220645369 +0.30810016 +0.31894774 +0.297214412 +0.266821547 +0.249705375 +0.236326412 +0.21999482 +0.210203119 +0.210558866 +0.220125155 +0.234089716 +0.244044104 +0.222752089 +0.082635086 +0.039518469 +0.019059151 +0.009396743 +0.006950686 +0.009346047 +0.01408303 +0.01872422 +0.023471288 +0.043379937 +0.142167175 +0.175619185 +0.152395336 +0.121523582 +0.103796565 +0.100112823 +0.11094929 +0.134738439 +0.153692615 +0.15814206 +0.149177998 +0.131345395 +0.114294219 +0.108114768 +0.04730233 +0.020970143 +0.021859359 +0.022773109 +0.024386106 +0.02891063 +0.033461253 +0.038675176 +0.052102575 +0.105829304 +0.290465317 +0.366549241 +0.379368693 +0.381152405 +0.397342876 +0.421618992 +0.452855655 +0.464788318 +0.429258975 +0.377340022 +0.336082873 +0.306678261 +0.286646318 +0.263490205 +0.105934359 +0.078530441 +0.0586725 +0.037560187 +0.027722118 +0.023914221 +0.023570699 +0.026125693 +0.032283349 +0.061596533 +0.182948376 +0.268256946 +0.303990597 +0.308893288 +0.309784051 +0.315293172 +0.328298518 +0.333069292 +0.326180388 +0.311934867 +0.295695904 +0.275569666 +0.254252673 +0.231022004 +0.105177953 +0.081225943 +0.075296793 +0.065854265 +0.060336705 +0.058801449 +0.063359251 +0.072705203 +0.086688074 +0.133583711 +0.275708195 +0.342074318 +0.345471756 +0.316738287 +0.300176021 +0.30335406 +0.313749705 +0.331611555 +0.349424855 +0.344011599 +0.336267534 +0.336463267 +0.335279093 +0.319771162 +0.242331516 +0.189865792 +0.146357533 +0.121811787 +0.111066017 +0.107334548 +0.113960974 +0.128900483 +0.15101826 +0.216129381 +0.405746703 +0.480438492 +0.4849656 +0.462938695 +0.452689715 +0.450204619 +0.442760707 +0.437228395 +0.437913912 +0.447421283 +0.453241623 +0.444193008 +0.432444778 +0.395468799 +0.288357627 +0.251273705 +0.193958941 +0.146484532 +0.125137728 +0.113774202 +0.121048945 +0.14215655 +0.178325024 +0.275485412 +0.505325043 +0.565888159 +0.556285011 +0.529521114 +0.510695865 +0.500595483 +0.478469982 +0.449764286 +0.43514402 +0.432099259 +0.43331913 +0.437778413 +0.435406252 +0.393544996 +0.308198887 +0.26734684 +0.211323609 +0.169561029 +0.140363767 +0.12574827 +0.128113993 +0.147314384 +0.18450885 +0.274096731 +0.463056329 +0.493599867 +0.476343945 +0.442787255 +0.400745065 +0.373399197 +0.351296081 +0.326965232 +0.327638582 +0.338518894 +0.346334212 +0.356345544 +0.36112971 +0.331897739 +0.188645648 +0.133829789 +0.083241236 +0.056843757 +0.03933749 +0.035291928 +0.038140541 +0.041852328 +0.048450177 +0.085026267 +0.192984538 +0.19905986 +0.168477319 +0.152419378 +0.152467742 +0.160597842 +0.167658225 +0.16307121 +0.141196111 +0.118611174 +0.105981078 +0.101525709 +0.102965889 +0.10926451 +0.055736266 +0.026834598 +0.040184433 +0.051194625 +0.05941181 +0.065953856 +0.071871442 +0.079522276 +0.095735368 +0.149216603 +0.293951819 +0.364467251 +0.404294712 +0.443400863 +0.48445169 +0.515944324 +0.528096436 +0.521769811 +0.490288064 +0.443686272 +0.396735806 +0.363090269 +0.35253054 +0.335745174 +0.184115057 +0.280185931 +0.319953051 +0.329013912 +0.337530717 +0.355466534 +0.376597155 +0.391051202 +0.40135873 +0.434621861 +0.575367058 +0.616489509 +0.632123761 +0.619011451 +0.600482328 +0.5863436 +0.56009394 +0.525086939 +0.483817797 +0.436364649 +0.396470009 +0.373888257 +0.361634968 +0.326376089 +0.192182055 +0.228330146 +0.224830706 +0.201714937 +0.202823986 +0.219910496 +0.244079079 +0.262574424 +0.272544949 +0.309796843 +0.398697699 +0.40512531 +0.402356498 +0.395364494 +0.396405396 +0.404282709 +0.411239443 +0.403266491 +0.384843391 +0.360188353 +0.335474584 +0.314289595 +0.295438693 +0.25009706 +0.104922174 +0.066659991 +0.035685558 +0.029068835 +0.038486625 +0.057597303 +0.084837915 +0.115109143 +0.144639473 +0.188739205 +0.278965566 +0.284375837 +0.277763234 +0.275383001 +0.324841507 +0.39295356 +0.449209647 +0.470876527 +0.463923403 +0.446636178 +0.435248956 +0.434621076 +0.438466006 +0.421292884 +0.226244218 +0.214383108 +0.32926286 +0.358785462 +0.358728519 +0.360449636 +0.377626506 +0.391277865 +0.397476095 +0.45657563 +0.549428079 +0.547576781 +0.53795954 +0.546639838 +0.577917505 +0.598712107 +0.592769301 +0.559671071 +0.522715701 +0.492797742 +0.472312719 +0.473819398 +0.49120101 +0.480921025 +0.315803671 +0.50952443 +0.600693743 +0.569066284 +0.547469905 +0.538906123 +0.549189987 +0.563237306 +0.573808138 +0.601979003 +0.647580752 +0.636636339 +0.625988506 +0.614937282 +0.595262937 +0.585375513 +0.574336265 +0.555547081 +0.558781932 +0.574733689 +0.586281945 +0.589880653 +0.581979473 +0.551194343 +0.532881985 +0.539775437 +0.488758733 +0.427437912 +0.385799732 +0.370743641 +0.37286395 +0.384545959 +0.413851395 +0.495638827 +0.65732551 +0.732878641 +0.762582162 +0.760400858 +0.747106888 +0.726714731 +0.710386664 +0.698190471 +0.702869374 +0.720032046 +0.721529566 +0.708777325 +0.699478482 +0.656940652 +0.481517443 +0.565093648 +0.553200304 +0.521258012 +0.500854677 +0.489870459 +0.481505846 +0.480666628 +0.493380065 +0.558440313 +0.688615184 +0.703308219 +0.701641287 +0.687416539 +0.665021201 +0.638491667 +0.613889458 +0.580112018 +0.549603521 +0.517233789 +0.487364734 +0.472020345 +0.453154306 +0.418772284 +0.347624727 +0.316420821 +0.255371496 +0.18574535 +0.136837688 +0.106288119 +0.097144481 +0.097292219 +0.102445396 +0.138136754 +0.240016943 +0.26215132 +0.2668242 +0.286382997 +0.326347665 +0.367810992 +0.398958572 +0.404940529 +0.42029566 +0.448037434 +0.468767004 +0.48016987 +0.481503683 +0.449943892 +0.261820168 +0.192236846 +0.132402046 +0.083911504 +0.051708398 +0.035254608 +0.032580304 +0.037874768 +0.050763175 +0.109570075 +0.281280849 +0.346530111 +0.375441544 +0.402630191 +0.422984651 +0.431573217 +0.434284242 +0.436937883 +0.443536675 +0.448199903 +0.443306375 +0.433668068 +0.418293794 +0.383722727 +0.188268315 +0.099007042 +0.055427626 +0.027690166 +0.014643697 +0.011856301 +0.01229654 +0.016282337 +0.028827627 +0.104635167 +0.254775773 +0.303524163 +0.305231162 +0.308908838 +0.323433245 +0.332294227 +0.328083265 +0.316037111 +0.311493114 +0.31629792 +0.323924813 +0.327099456 +0.313412808 +0.289746808 +0.142595207 +0.068037298 +0.033571531 +0.021759298 +0.023889117 +0.030294508 +0.033122849 +0.032641998 +0.033160163 +0.073129522 +0.162748084 +0.160078869 +0.131126496 +0.121407401 +0.130524972 +0.150077235 +0.17629983 +0.2048491 +0.236899373 +0.268849411 +0.290774902 +0.300830319 +0.304706645 +0.296249897 +0.151051599 +0.06656589 +0.03705351 +0.025563327 +0.026069275 +0.03283495 +0.040506185 +0.049575658 +0.067066208 +0.178522206 +0.405407346 +0.483651682 +0.515187493 +0.522680928 +0.519054615 +0.51699158 +0.504537682 +0.487907279 +0.480224651 +0.468671205 +0.450865238 +0.436502622 +0.425107613 +0.410476685 +0.216986657 +0.130251918 +0.092339089 +0.072016796 +0.070322525 +0.080864283 +0.099360712 +0.118602346 +0.145873182 +0.259732173 +0.468305776 +0.536739698 +0.55958721 +0.571631443 +0.579037846 +0.584286193 +0.571928669 +0.544581627 +0.536637575 +0.536147544 +0.526923874 +0.514901195 +0.512844758 +0.505760072 +0.305439176 +0.231879819 +0.214724891 +0.206955242 +0.213942959 +0.227340878 +0.247677686 +0.268936244 +0.291835478 +0.380467814 +0.514459457 +0.548740496 +0.546104503 +0.530495999 +0.515221162 +0.501917749 +0.489297827 +0.476866423 +0.47241524 +0.474659209 +0.475571233 +0.473332207 +0.462828497 +0.444734411 +0.290979412 +0.194247337 +0.16990439 +0.140749009 +0.123877041 +0.115774922 +0.109125796 +0.107370387 +0.114210943 +0.195799039 +0.287670856 +0.29596129 +0.289699511 +0.289689688 +0.2919194 +0.285601085 +0.271153726 +0.254692116 +0.243020787 +0.239834742 +0.233350611 +0.21088894 +0.179725362 +0.163342085 +0.105055112 +0.040048077 +0.016721849 +0.005825543 +0.003030779 +0.004030063 +0.00604126 +0.008913207 +0.016763879 +0.079811869 +0.123962901 +0.099827853 +0.066678218 +0.052331396 +0.058140716 +0.07265054 +0.079784198 +0.074039953 +0.061478782 +0.054624799 +0.056183526 +0.060983418 +0.066086741 +0.071306988 +0.051562297 +0.00826556 +0.003662431 +0.008957828 +0.014309088 +0.019404279 +0.022759841 +0.023960804 +0.02873317 +0.090198988 +0.139743404 +0.133179112 +0.105961912 +0.082109786 +0.072505488 +0.076895828 +0.084898792 +0.092063003 +0.094790955 +0.093792311 +0.091480894 +0.087955617 +0.083061307 +0.077622804 +0.063195904 +0.012562628 +0.014842484 +0.020606525 +0.028727725 +0.03737225 +0.04248864 +0.043568665 +0.04750549 +0.109773669 +0.189624638 +0.191237077 +0.183218077 +0.192404912 +0.213934364 +0.240083109 +0.270140884 +0.296345047 +0.316669761 +0.331501336 +0.339656506 +0.341907947 +0.341257836 +0.344881425 +0.215825269 +0.133296015 +0.117399276 +0.108664441 +0.110038568 +0.119853545 +0.131261733 +0.142563435 +0.160678494 +0.262589333 +0.434791071 +0.47693066 +0.502407884 +0.529063271 +0.554664497 +0.58301148 +0.603435954 +0.607091275 +0.598630994 +0.582270052 +0.576450714 +0.57650207 +0.565614335 +0.540504675 +0.317390372 +0.246444414 +0.189573153 +0.132972351 +0.103599408 +0.091327607 +0.100986712 +0.127247184 +0.165560973 +0.286374937 +0.482655226 +0.534844979 +0.545203741 +0.546537322 +0.551784134 +0.573368763 +0.588503144 +0.587428173 +0.582715882 +0.580339023 +0.568801091 +0.555196685 +0.541457847 +0.521124342 +0.307227748 +0.259941983 +0.238594097 +0.20880656 +0.189960098 +0.176173747 +0.173494895 +0.176883312 +0.182105908 +0.242138273 +0.320044568 +0.312501546 +0.286236741 +0.268457824 +0.269476295 +0.279734167 +0.289217929 +0.291898436 +0.29727719 +0.29903368 +0.292507346 +0.285220108 +0.276988322 +0.268628923 +0.161327641 +0.050283086 +0.026729658 +0.014789428 +0.009261639 +0.007196884 +0.008540927 +0.013522939 +0.026932882 +0.102779916 +0.171123961 +0.197415254 +0.226256294 +0.285291578 +0.353752598 +0.395988623 +0.420961401 +0.425710258 +0.417319875 +0.396634133 +0.370999987 +0.356167905 +0.35272423 +0.351682618 +0.232107996 +0.131160774 +0.154122065 +0.139956454 +0.13216032 +0.13093921 +0.136594415 +0.140581659 +0.13932545 +0.186288731 +0.258416861 +0.276139989 +0.264407703 +0.249363586 +0.249275552 +0.249035408 +0.241401072 +0.232223984 +0.218758321 +0.200612651 +0.180103652 +0.161304088 +0.146967032 +0.139665684 +0.081561998 +0.029225284 +0.016385406 +0.013360476 +0.017183818 +0.026067123 +0.035397168 +0.039662197 +0.043080294 +0.097651929 +0.159214425 +0.151160298 +0.124843346 +0.107768685 +0.095850277 +0.091843174 +0.093149009 +0.094693904 +0.09415403 +0.095090685 +0.093995402 +0.086376372 +0.074888488 +0.072833338 +0.04813036 +0.010265771 +0.018837678 +0.050657565 +0.105957268 +0.174464658 +0.237480748 +0.273981767 +0.284690996 +0.333982588 +0.360477451 +0.322137338 +0.277901474 +0.250017656 +0.245596148 +0.251138158 +0.26401222 +0.288777248 +0.329402333 +0.361505154 +0.373539599 +0.380480218 +0.390353854 +0.394473887 +0.249375217 +0.202566448 +0.116759918 +0.075622972 +0.062832733 +0.062034957 +0.059125318 +0.053213186 +0.052196872 +0.111739414 +0.262127724 +0.322215582 +0.353478842 +0.376787707 +0.380484123 +0.375243555 +0.373053975 +0.367423234 +0.361633408 +0.353739397 +0.345706168 +0.337259037 +0.328937015 +0.310800901 +0.168397642 +0.077953887 +0.03806993 +0.024689343 +0.020995316 +0.021891771 +0.025408239 +0.03236104 +0.043895944 +0.115899042 +0.224689069 +0.237367037 +0.230468725 +0.225123195 +0.234614165 +0.242835078 +0.240879341 +0.22769092 +0.210194148 +0.187376596 +0.161914632 +0.141436381 +0.126877561 +0.115564323 +0.072051411 +0.020161234 +0.017848837 +0.033495069 +0.05621788 +0.080848431 +0.107818037 +0.130548227 +0.138835132 +0.191728273 +0.213420214 +0.179350265 +0.145307967 +0.130013643 +0.139866501 +0.154711598 +0.159455308 +0.163702362 +0.158984393 +0.151300364 +0.146857333 +0.139698717 +0.130616826 +0.122882169 +0.069531435 +0.032726373 +0.039827848 +0.051497543 +0.064405569 +0.072601176 +0.079075393 +0.083297875 +0.087163099 +0.154675987 +0.231282885 +0.239719812 +0.226917719 +0.21930865 +0.214605286 +0.212684879 +0.215018595 +0.223497783 +0.231726536 +0.23709993 +0.239900882 +0.240799858 +0.236283503 +0.228632675 +0.122353203 +0.045496065 +0.03966065 +0.040399441 +0.042433764 +0.043677847 +0.047218117 +0.052505619 +0.059785965 +0.122037238 +0.182314319 +0.190036545 +0.181051797 +0.170617584 +0.172175977 +0.187523845 +0.197662474 +0.202280822 +0.197547198 +0.192357739 +0.192477091 +0.196564366 +0.202334899 +0.210177177 +0.154246653 +0.069619562 +0.111595512 +0.142676727 +0.177243633 +0.214802848 +0.242824792 +0.255238995 +0.258264323 +0.315120243 +0.365043056 +0.368311361 +0.370112679 +0.361151976 +0.354547388 +0.359128934 +0.372228922 +0.393911902 +0.412078372 +0.42314917 +0.436256587 +0.441805627 +0.431179369 +0.408646001 +0.27702816 +0.177532146 +0.153396857 +0.114448362 +0.092094904 +0.084488944 +0.086478451 +0.098272663 +0.125054001 +0.26550045 +0.409650181 +0.441731295 +0.447793384 +0.438179554 +0.411036398 +0.387439738 +0.375094629 +0.363342843 +0.343982582 +0.328946242 +0.328512555 +0.335761154 +0.336506028 +0.334125605 +0.248638321 +0.116937821 +0.101785039 +0.095183103 +0.106779003 +0.132489378 +0.166573121 +0.220850779 +0.312885599 +0.532190385 +0.696152993 +0.745503618 +0.782307119 +0.784955968 +0.759111296 +0.734752285 +0.701977834 +0.663243652 +0.629229172 +0.597382146 +0.569017255 +0.542256932 +0.518756087 +0.496040206 +0.368671548 +0.30173267 +0.241029107 +0.179995375 +0.127795485 +0.094616063 +0.079223233 +0.074892945 +0.083533547 +0.173490117 +0.294912917 +0.317544798 +0.319943421 +0.3234739 +0.321753414 +0.320735698 +0.313928371 +0.299258249 +0.286787761 +0.274295074 +0.261226015 +0.247393529 +0.235143063 +0.222091089 +0.139068309 +0.057901965 +0.028864835 +0.016822746 +0.013822643 +0.015399311 +0.020346488 +0.028968771 +0.044714798 +0.148503525 +0.241845201 +0.242476828 +0.236442271 +0.250724553 +0.279338501 +0.313576146 +0.343638789 +0.369214964 +0.388965528 +0.411427918 +0.438579495 +0.460447768 +0.47564347 +0.490083102 +0.36228411 +0.298002034 +0.320842648 +0.329887776 +0.340932451 +0.362767604 +0.404784429 +0.466985306 +0.548645118 +0.689611777 +0.776463702 +0.810612439 +0.831429731 +0.837034224 +0.826567351 +0.803715467 +0.776037805 +0.747514829 +0.735673098 +0.737736634 +0.736180039 +0.730222664 +0.721025649 +0.703642026 +0.601315963 +0.600806563 +0.613184782 +0.586478758 +0.563699147 +0.546012453 +0.534019674 +0.527836416 +0.532767351 +0.60197791 +0.680023008 +0.70482725 +0.720092473 +0.728240813 +0.725458029 +0.720058233 +0.712618997 +0.699276469 +0.688205053 +0.670903451 +0.645092239 +0.613574923 +0.580038356 +0.545149025 +0.42777349 +0.345183587 +0.355748444 +0.354277055 +0.359825856 +0.366238677 +0.374846498 +0.386405598 +0.400993249 +0.469709808 +0.52409231 +0.527641479 +0.540346987 +0.554228049 +0.562705352 +0.570386216 +0.571967269 +0.569207399 +0.563296718 +0.551931816 +0.531940472 +0.501371242 +0.469347073 +0.439245331 +0.323269135 +0.197356926 +0.186801931 +0.169522526 +0.163490583 +0.16659152 +0.172365363 +0.177259564 +0.187295118 +0.266476071 +0.311264011 +0.293143964 +0.277474033 +0.271792264 +0.276146891 +0.287171098 +0.291237846 +0.27966999 +0.268618428 +0.267478637 +0.269312129 +0.266118992 +0.255646782 +0.24277117 +0.17199644 +0.064182699 +0.034791609 +0.016272939 +0.010521297 +0.011958522 +0.014385164 +0.016793813 +0.024606211 +0.100428597 +0.138267814 +0.105162846 +0.080758828 +0.075597695 +0.076443967 +0.076420637 +0.073749756 +0.072403387 +0.075315719 +0.081857171 +0.094975128 +0.121518847 +0.161830078 +0.213344663 +0.2131347 +0.112282373 +0.098578042 +0.102972465 +0.115656793 +0.131426552 +0.153013109 +0.184795333 +0.237414615 +0.383417455 +0.519423633 +0.553318248 +0.574040301 +0.587874728 +0.59308363 +0.588738513 +0.578444037 +0.568963088 +0.564166196 +0.563138182 +0.559099114 +0.550927606 +0.535978733 +0.51605114 +0.400850468 +0.368565903 +0.361514285 +0.348615697 +0.343507094 +0.359658772 +0.38415304 +0.411138719 +0.439600769 +0.505095341 +0.568839459 +0.574713233 +0.579520486 +0.596752741 +0.619535427 +0.638407539 +0.650876555 +0.646507226 +0.637949857 +0.632129321 +0.621506117 +0.599878066 +0.567680737 +0.529409682 +0.391907324 +0.267966529 +0.292526197 +0.297566734 +0.325380742 +0.365090922 +0.398612954 +0.413462597 +0.411984278 +0.460168488 +0.497154871 +0.503486091 +0.515147591 +0.519411266 +0.515612183 +0.512144325 +0.50827198 +0.499382745 +0.482212072 +0.451560104 +0.403255342 +0.350386806 +0.305044729 +0.269975636 +0.218159503 +0.092527897 +0.062668034 +0.04619032 +0.043597247 +0.053499682 +0.072823109 +0.096956081 +0.127994189 +0.22543143 +0.258918023 +0.248568796 +0.247500415 +0.255883795 +0.264968333 +0.268480084 +0.266826853 +0.26664711 +0.270526632 +0.275349906 +0.276170422 +0.269618203 +0.25160902 +0.228138869 +0.181894753 +0.056067115 +0.027254056 +0.016544879 +0.011667873 +0.012914632 +0.016858483 +0.021237572 +0.038946465 +0.103628595 +0.089594351 +0.049506833 +0.028666721 +0.025800528 +0.030870106 +0.038194925 +0.046148053 +0.057148492 +0.068937313 +0.078841749 +0.088580598 +0.09624241 +0.100780643 +0.100111107 +0.099300213 +0.025891106 +0.008046497 +0.006222382 +0.005811331 +0.006733674 +0.009656944 +0.014919214 +0.031314712 +0.108623277 +0.126870076 +0.093948963 +0.080429034 +0.093184717 +0.128916513 +0.169794729 +0.196756257 +0.207149553 +0.199167115 +0.184484888 +0.171483039 +0.159977857 +0.154095782 +0.15698377 +0.147609507 +0.053010916 +0.039645897 +0.047767051 +0.065574839 +0.087922699 +0.101535514 +0.104171022 +0.113830483 +0.199362148 +0.235540762 +0.198085382 +0.172884347 +0.169557413 +0.17860568 +0.194174549 +0.217797857 +0.252203377 +0.293582182 +0.3372576 +0.359743251 +0.362121002 +0.358763906 +0.355922964 +0.244292555 +0.141368139 +0.119144728 +0.088848509 +0.061389883 +0.04605964 +0.046589576 +0.05878252 +0.082836376 +0.181653587 +0.237778682 +0.212988968 +0.184381293 +0.175307491 +0.174047135 +0.172386093 +0.170829424 +0.165813287 +0.170698003 +0.187313499 +0.207290559 +0.220006326 +0.220807085 +0.212738717 +0.145147123 +0.039009798 +0.015139343 +0.007942954 +0.006679769 +0.009033012 +0.013265041 +0.017888441 +0.025274651 +0.086618326 +0.112939533 +0.08863618 +0.062963146 +0.055831543 +0.062916513 +0.070753086 +0.077725939 +0.085479467 +0.094579735 +0.106762332 +0.12302547 +0.138611732 +0.153578556 +0.175058517 +0.180867666 +0.074630816 +0.106373785 +0.146455225 +0.186606725 +0.224917385 +0.262846461 +0.293031233 +0.310956579 +0.395506731 +0.454332974 +0.465054124 +0.485800054 +0.5212467 +0.549182667 +0.556887292 +0.547681827 +0.528758803 +0.499764336 +0.463155041 +0.428959676 +0.406927449 +0.389994118 +0.374543238 +0.276468067 +0.289246952 +0.341920266 +0.364478489 +0.39776454 +0.445398119 +0.478325039 +0.47297595 +0.426796025 +0.452154875 +0.456718472 +0.387122973 +0.335959387 +0.286721158 +0.23289955 +0.193534688 +0.171926169 +0.167774267 +0.179180455 +0.200368742 +0.22172076 +0.238964851 +0.247246449 +0.244508769 +0.122555766 +0.055303227 +0.023047514 +0.006562033 +0.00477441 +0.008081218 +0.011462537 +0.013684312 +0.018756531 +0.07071961 +0.157317759 +0.186327904 +0.20563281 +0.259488752 +0.353079188 +0.436748265 +0.491130588 +0.517859955 +0.526842883 +0.529105477 +0.540329424 +0.550022041 +0.563119251 +0.57524359 +0.432800626 +0.414785972 +0.552247941 +0.546284614 +0.510853249 +0.480474392 +0.451972782 +0.416168218 +0.362275435 +0.374028959 +0.413141385 +0.398081132 +0.385966675 +0.36690286 +0.34266162 +0.317132108 +0.289334603 +0.273363711 +0.268249783 +0.258214951 +0.245927096 +0.240349823 +0.23382862 +0.225392349 +0.151401509 +0.082431147 +0.085826913 +0.109633762 +0.151901348 +0.212016322 +0.278090276 +0.347565289 +0.419211514 +0.571237972 +0.712959336 +0.759011011 +0.775745993 +0.783463775 +0.778819452 +0.763084652 +0.738147076 +0.697429009 +0.655032534 +0.617411599 +0.584483526 +0.558875598 +0.537893972 +0.519845857 +0.358766006 +0.262573516 +0.244576892 +0.229222121 +0.213614662 +0.209549909 +0.212313851 +0.214076685 +0.226398929 +0.338563184 +0.459984561 +0.488806159 +0.487996624 +0.484171079 +0.480355942 +0.472924018 +0.464380728 +0.455762363 +0.452385694 +0.45079121 +0.438857098 +0.412820111 +0.382383688 +0.360898687 +0.26677038 +0.144606521 +0.125718822 +0.103348697 +0.087225874 +0.080808742 +0.084348675 +0.098321965 +0.126023173 +0.21853189 +0.277579971 +0.2710324 +0.258999878 +0.2587627 +0.259099068 +0.254893896 +0.255198262 +0.2609865 +0.267136642 +0.270162548 +0.268101026 +0.259351606 +0.24430341 +0.226032034 +0.166550348 +0.067330782 +0.044625837 +0.028469971 +0.021650979 +0.022797084 +0.025155106 +0.02487681 +0.031662361 +0.104575613 +0.141521747 +0.120347198 +0.10212769 +0.095156346 +0.093078983 +0.08658934 +0.073714412 +0.058170274 +0.04677956 +0.041964724 +0.043013597 +0.048706825 +0.057257086 +0.065770154 +0.063971349 +0.014136346 +0.001716531 +0.000215785 +0.001727342 +0.006402757 +0.013890989 +0.024423889 +0.043053589 +0.14791011 +0.201650856 +0.18488867 +0.166686977 +0.163750995 +0.177164903 +0.204474479 +0.240689871 +0.277938803 +0.320126052 +0.358022043 +0.384165686 +0.396283084 +0.391298301 +0.374289857 +0.256519139 +0.120740524 +0.109739172 +0.094087885 +0.081754785 +0.07722504 +0.086184503 +0.107972158 +0.13945086 +0.255472076 +0.353815347 +0.339901616 +0.311426926 +0.291278196 +0.286117929 +0.293790518 +0.305649234 +0.312347229 +0.314390076 +0.315265282 +0.310188398 +0.300529859 +0.29020164 +0.282048662 +0.180969364 +0.0759849 +0.058221467 +0.045114741 +0.050620264 +0.064786223 +0.082484788 +0.098577755 +0.122067357 +0.246565154 +0.370536039 +0.388619902 +0.384424826 +0.374532497 +0.373695158 +0.379053779 +0.386742864 +0.387996948 +0.392167651 +0.399093061 +0.402415018 +0.300529859 +0.29020164 +0.282048662 +0.180969364 +0.0759849 +0.058221467 +0.045114741 +0.050620264 +0.064786223 +0.082484788 +0.098577755 +0.122067357 +0.246565154 +0.370536039 +0.388619902 +0.384424826 +0.374532497 +0.373695158 +0.379053779 +0.386742864 +0.387996948 +0.392167651 +0.399093061 +0.402415018 \ No newline at end of file diff --git a/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/capa/windcapaUPS.txt b/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/capa/windcapaUPS.txt new file mode 100644 index 0000000..b005c8c --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/capa/windcapaUPS.txt @@ -0,0 +1,8760 @@ +0.08181809 +0.058233331 +0.052305726 +0.046172336 +0.043205135 +0.048982037 +0.059325475 +0.090484773 +0.159088781 +0.189057852 +0.186944647 +0.199901022 +0.211498589 +0.22734688 +0.247191723 +0.269568201 +0.306789998 +0.330640614 +0.380791528 +0.409069173 +0.417066678 +0.427882227 +0.420931347 +0.40170015 +0.382530284 +0.335697823 +0.293091457 +0.293941816 +0.335972443 +0.359861918 +0.348602795 +0.361764131 +0.396537397 +0.402829201 +0.401142779 +0.391388879 +0.379302157 +0.358553864 +0.338023415 +0.3130777 +0.295594729 +0.266583816 +0.229263509 +0.202317789 +0.183133828 +0.157887788 +0.130898623 +0.102995798 +0.082476736 +0.064171119 +0.046551869 +0.032181348 +0.02865243 +0.025736956 +0.029466278 +0.046785949 +0.067460005 +0.090469242 +0.099490824 +0.097182424 +0.090135522 +0.076929328 +0.065640127 +0.061933781 +0.063187025 +0.064487711 +0.067705589 +0.073009018 +0.074510035 +0.074250868 +0.068823493 +0.062823195 +0.057274346 +0.048684416 +0.035987759 +0.023247622 +0.019778216 +0.020440149 +0.022102449 +0.02749157 +0.035831074 +0.046013381 +0.059035246 +0.073624849 +0.089518663 +0.110389092 +0.122318467 +0.131191374 +0.137415682 +0.147731127 +0.150479444 +0.152616006 +0.151087793 +0.140379202 +0.132595602 +0.132980191 +0.130758707 +0.116311906 +0.093572241 +0.088116142 +0.09049291 +0.088293886 +0.0808561 +0.081733568 +0.098711945 +0.110803834 +0.11786709 +0.134015109 +0.157047473 +0.183872944 +0.215076557 +0.248454827 +0.256799692 +0.259038977 +0.266926388 +0.26291907 +0.270745055 +0.286565841 +0.315192389 +0.343594846 +0.371905059 +0.370286817 +0.353918617 +0.364011136 +0.385506681 +0.405082261 +0.422064189 +0.455339582 +0.507337023 +0.541159649 +0.563919991 +0.573887001 +0.585450686 +0.588879406 +0.577506113 +0.555524722 +0.533421815 +0.509764575 +0.489060261 +0.471531691 +0.455674851 +0.455848628 +0.450743771 +0.434422995 +0.412413242 +0.370399371 +0.384311886 +0.347176765 +0.313786852 +0.324963867 +0.345880466 +0.331017735 +0.365657826 +0.396774099 +0.422402499 +0.431884379 +0.440979167 +0.450445216 +0.461119715 +0.425686977 +0.398194174 +0.378803457 +0.358974637 +0.335690086 +0.314586179 +0.317131368 +0.32561003 +0.323541805 +0.319562411 +0.282113473 +0.322455303 +0.316527948 +0.315537192 +0.335077052 +0.333738723 +0.330753849 +0.362334046 +0.375645244 +0.395404899 +0.421659883 +0.4465191 +0.488676257 +0.517448765 +0.506974918 +0.481696434 +0.460583663 +0.444618712 +0.433194582 +0.424907137 +0.430469806 +0.439028994 +0.443128867 +0.425637486 +0.362921838 +0.354124837 +0.406513884 +0.451763733 +0.471208795 +0.480504246 +0.489048467 +0.52629503 +0.526979572 +0.517081124 +0.507034984 +0.486273879 +0.473422958 +0.454423092 +0.440402854 +0.426897098 +0.419681132 +0.423672866 +0.416233347 +0.416068951 +0.425278749 +0.428488426 +0.417966387 +0.405103894 +0.349107628 +0.304978699 +0.348261625 +0.387180537 +0.398875009 +0.390449529 +0.358178244 +0.355460032 +0.342074336 +0.323995039 +0.310493004 +0.28335035 +0.272742313 +0.264139875 +0.260055597 +0.250850105 +0.243940383 +0.22947018 +0.233060635 +0.238104781 +0.238997334 +0.230047473 +0.211539097 +0.185582814 +0.168902111 +0.165720808 +0.147968091 +0.135827802 +0.136955418 +0.150830762 +0.141442462 +0.109388461 +0.098069998 +0.091902505 +0.087115617 +0.084673268 +0.08585648 +0.087621518 +0.088064099 +0.091317428 +0.093145423 +0.0936907 +0.098445937 +0.103044819 +0.108890634 +0.114475917 +0.12511498 +0.134479724 +0.118361183 +0.09572975 +0.067839463 +0.062360642 +0.064554602 +0.067980971 +0.070687084 +0.095027318 +0.123557874 +0.139803365 +0.147472567 +0.148785814 +0.149915048 +0.155637761 +0.151792987 +0.151234869 +0.150631576 +0.140405863 +0.121844146 +0.1076095 +0.098375098 +0.093835781 +0.093606822 +0.09552607 +0.085366138 +0.064396734 +0.043200677 +0.044372408 +0.057244671 +0.074281196 +0.114071295 +0.199199169 +0.261145087 +0.288633656 +0.31317269 +0.332958283 +0.363409809 +0.395905431 +0.416175272 +0.425566409 +0.42355118 +0.426757444 +0.419274708 +0.399079219 +0.402991941 +0.421748644 +0.442033795 +0.442256755 +0.38016204 +0.315858717 +0.313099787 +0.366617625 +0.416411551 +0.454015033 +0.548809404 +0.637362346 +0.679229473 +0.689560481 +0.668051352 +0.61485285 +0.559628005 +0.510998841 +0.459100818 +0.399873928 +0.329363172 +0.255528848 +0.191993355 +0.14303036 +0.10440381 +0.076422563 +0.055463141 +0.039992241 +0.022482375 +0.011076033 +0.005732747 +0.003321433 +0.002195939 +0.002776666 +0.003829599 +0.006731285 +0.013242841 +0.022440027 +0.034022959 +0.044166584 +0.052207146 +0.058268035 +0.058009015 +0.052818837 +0.047503893 +0.044100975 +0.041343072 +0.040402521 +0.044864023 +0.056615539 +0.06916539 +0.074883972 +0.067017368 +0.052972219 +0.059128876 +0.077572554 +0.1045707 +0.132849812 +0.197418226 +0.308788662 +0.400896812 +0.443025739 +0.47133449 +0.487634956 +0.502217863 +0.507397258 +0.491916183 +0.449519986 +0.404331338 +0.358087367 +0.331086244 +0.317006175 +0.300214235 +0.305380575 +0.32749084 +0.333005121 +0.257111092 +0.170356499 +0.138309345 +0.122920725 +0.117427761 +0.121676558 +0.146809995 +0.195465765 +0.227134955 +0.217763704 +0.191897929 +0.165118725 +0.147020329 +0.135508267 +0.124146922 +0.109193613 +0.091733726 +0.079089568 +0.073933752 +0.071914037 +0.074441692 +0.082248192 +0.082188197 +0.075061237 +0.056715249 +0.033847003 +0.016216688 +0.009705018 +0.008089336 +0.012460907 +0.021904486 +0.044555932 +0.07286812 +0.093338119 +0.106681031 +0.106692662 +0.095319285 +0.081241201 +0.06368211 +0.053271382 +0.05153541 +0.054702955 +0.062548722 +0.07275978 +0.082617556 +0.095129144 +0.107753416 +0.120441461 +0.113431757 +0.082047998 +0.07079952 +0.087488008 +0.10900298 +0.130972953 +0.153279536 +0.203556022 +0.289254184 +0.337709676 +0.361767532 +0.378161027 +0.386969302 +0.389282907 +0.378532177 +0.351823158 +0.335921224 +0.323864034 +0.311219761 +0.295656587 +0.278607055 +0.27700833 +0.277961014 +0.262421888 +0.188807908 +0.133218166 +0.102029582 +0.083591848 +0.084868962 +0.100072422 +0.127990283 +0.182818777 +0.251190381 +0.307156657 +0.322071049 +0.304902319 +0.292344012 +0.281755388 +0.266117194 +0.25844089 +0.257637153 +0.255593784 +0.240897354 +0.23012329 +0.235310813 +0.217603971 +0.195784079 +0.179968891 +0.139033867 +0.094747812 +0.075418909 +0.063941934 +0.059478192 +0.05931504 +0.077341609 +0.114390693 +0.152770481 +0.182186704 +0.202579196 +0.226851966 +0.258640787 +0.288163439 +0.302615767 +0.293178885 +0.281408673 +0.278076233 +0.27039385 +0.258793144 +0.238171934 +0.231447914 +0.242249133 +0.249509449 +0.204510656 +0.132165602 +0.121218968 +0.136459625 +0.146380654 +0.157591353 +0.227568624 +0.354243662 +0.446483156 +0.485333905 +0.489202148 +0.472181995 +0.441082666 +0.412418091 +0.401897112 +0.391388986 +0.380080631 +0.357662644 +0.322698045 +0.287201478 +0.279318801 +0.290111489 +0.305780885 +0.31624882 +0.270036308 +0.202214895 +0.169590617 +0.1765435 +0.20303644 +0.227389632 +0.317562265 +0.446662421 +0.497832291 +0.510377149 +0.506486825 +0.482113693 +0.453795399 +0.438205723 +0.412377133 +0.369588227 +0.337035617 +0.306925966 +0.288723778 +0.273614323 +0.276868608 +0.286278768 +0.301316575 +0.311826402 +0.300224406 +0.328265507 +0.388164153 +0.461494337 +0.523392822 +0.543874431 +0.506909493 +0.487043527 +0.542152408 +0.597506249 +0.615784529 +0.638069551 +0.654739154 +0.65359393 +0.577729234 +0.480373888 +0.425189716 +0.383978084 +0.357800963 +0.324553696 +0.301771159 +0.295198091 +0.297084869 +0.269678255 +0.197495154 +0.171247814 +0.186277937 +0.173887112 +0.161023392 +0.158390039 +0.15829564 +0.215307561 +0.282964564 +0.321541934 +0.37077218 +0.412158969 +0.430643888 +0.418461834 +0.384858006 +0.344798984 +0.309993347 +0.280603152 +0.252614795 +0.219864783 +0.191308411 +0.171040498 +0.148193326 +0.123033865 +0.097284882 +0.075177699 +0.086478449 +0.110696691 +0.153404181 +0.209449969 +0.27831259 +0.377738615 +0.480561817 +0.570119142 +0.640310243 +0.704020493 +0.759457529 +0.792442969 +0.777534117 +0.766507116 +0.759858673 +0.730409948 +0.710379037 +0.680388247 +0.666956494 +0.658353665 +0.640798735 +0.597717136 +0.545695245 +0.539437074 +0.478103054 +0.420830618 +0.384365286 +0.360194815 +0.332966406 +0.283610183 +0.27214655 +0.262736601 +0.238551749 +0.209238814 +0.179834294 +0.157721988 +0.147397662 +0.133835198 +0.114602144 +0.125035541 +0.131694244 +0.140633232 +0.149572829 +0.155665478 +0.157204753 +0.146376089 +0.105986878 +0.069607918 +0.072723342 +0.073388507 +0.067034443 +0.062780171 +0.07116907 +0.091798257 +0.119143682 +0.124663067 +0.110428998 +0.087639346 +0.06912068 +0.06172184 +0.063196157 +0.061374032 +0.060097062 +0.064159067 +0.07425526 +0.089552807 +0.123434294 +0.16147941 +0.199275042 +0.234119307 +0.261254267 +0.296748849 +0.296912251 +0.298881147 +0.309945065 +0.321313962 +0.324543772 +0.31915867 +0.34072546 +0.364316845 +0.367599487 +0.371070252 +0.366923777 +0.35133551 +0.319227228 +0.278332176 +0.239074087 +0.210338208 +0.185036566 +0.160892536 +0.132853915 +0.109395897 +0.093914007 +0.074907433 +0.058924177 +0.05810029 +0.052148446 +0.051191367 +0.052989639 +0.055065354 +0.066032698 +0.072984205 +0.091252418 +0.118850425 +0.15109444 +0.184244853 +0.214847384 +0.238512659 +0.249729337 +0.24285801 +0.224838749 +0.205546474 +0.188946474 +0.167894234 +0.158166896 +0.162617483 +0.167403387 +0.172050037 +0.168528308 +0.132873494 +0.114699198 +0.129391006 +0.139787025 +0.129262401 +0.112683332 +0.127210594 +0.162984569 +0.194207082 +0.22317284 +0.251139723 +0.255700129 +0.23556649 +0.205778307 +0.166334262 +0.129943968 +0.104301636 +0.08918055 +0.085261358 +0.081470288 +0.082426474 +0.094099275 +0.104199043 +0.091124671 +0.089793125 +0.102094189 +0.156682711 +0.235876321 +0.308847541 +0.366534974 +0.421780196 +0.51833827 +0.616401376 +0.675071088 +0.720392079 +0.739551975 +0.754093571 +0.75338237 +0.749608539 +0.744867814 +0.738008172 +0.722596434 +0.714877789 +0.722718087 +0.742428038 +0.756505668 +0.759859131 +0.768723844 +0.764607946 +0.815677364 +0.840248978 +0.842093466 +0.837464823 +0.829828514 +0.836491011 +0.856679251 +0.8744443 +0.882799617 +0.880858606 +0.874018582 +0.854770301 +0.817926653 +0.772796028 +0.750149824 +0.736155623 +0.728523413 +0.730584663 +0.744796793 +0.770247125 +0.788055342 +0.780291246 +0.784636921 +0.850229796 +0.840632928 +0.8095401 +0.779850816 +0.750400652 +0.703834237 +0.649375352 +0.689287992 +0.743889981 +0.772836753 +0.779309032 +0.774111373 +0.759269184 +0.709681952 +0.644915131 +0.591377424 +0.552085384 +0.53221189 +0.499069383 +0.482864234 +0.476406972 +0.469285735 +0.427690213 +0.358233131 +0.391408986 +0.398826973 +0.383515093 +0.381703269 +0.385903529 +0.381148402 +0.374087894 +0.441418168 +0.53285577 +0.585901519 +0.624155009 +0.650748944 +0.660771913 +0.62398459 +0.577758783 +0.546613239 +0.520754655 +0.497360834 +0.468332119 +0.459448064 +0.471414083 +0.485569055 +0.471444215 +0.404658453 +0.409829319 +0.53768601 +0.556058089 +0.548183323 +0.530276018 +0.491889969 +0.487148733 +0.544635731 +0.582015613 +0.584746685 +0.579387117 +0.575311899 +0.566103425 +0.516516967 +0.449322198 +0.396774609 +0.349479397 +0.307646462 +0.273782531 +0.259059212 +0.261725501 +0.277900278 +0.265771769 +0.229524876 +0.167458446 +0.227508303 +0.319922378 +0.390530227 +0.426801304 +0.418527537 +0.444555505 +0.535684367 +0.57825277 +0.576298266 +0.55873651 +0.548623946 +0.530471328 +0.4814916 +0.417011045 +0.359301694 +0.293746131 +0.235138817 +0.196600345 +0.17878992 +0.172283569 +0.177769807 +0.160878694 +0.1370139 +0.105798037 +0.100949494 +0.106532928 +0.109123717 +0.112573883 +0.119076006 +0.125540577 +0.14708434 +0.194957227 +0.223719736 +0.242338092 +0.259192231 +0.26856382 +0.266792676 +0.248227698 +0.232144059 +0.224981542 +0.218053341 +0.215881778 +0.209997528 +0.2055988 +0.203635765 +0.19665811 +0.181527923 +0.129960708 +0.137445421 +0.182217144 +0.207351759 +0.219646087 +0.234492402 +0.255529603 +0.313447674 +0.367792863 +0.397795283 +0.403258484 +0.406693824 +0.399658666 +0.384099427 +0.370999562 +0.350443959 +0.320175762 +0.287716451 +0.26416054 +0.247186326 +0.235188715 +0.217127436 +0.182868409 +0.140646083 +0.109791436 +0.120661406 +0.13529829 +0.139997923 +0.143918829 +0.143565531 +0.126046132 +0.159347327 +0.187789818 +0.212017514 +0.21985812 +0.211474773 +0.194113221 +0.181789009 +0.171466678 +0.152995961 +0.142836717 +0.138909207 +0.145937606 +0.145910611 +0.14310411 +0.141139674 +0.119885779 +0.105294853 +0.074508377 +0.058915689 +0.057046268 +0.061289327 +0.066112313 +0.072009162 +0.084916713 +0.114184384 +0.137908614 +0.137474033 +0.129014785 +0.114383611 +0.103626272 +0.089399913 +0.076002128 +0.069638411 +0.066748259 +0.066137937 +0.064987189 +0.062899975 +0.059277931 +0.054452409 +0.054593036 +0.054716074 +0.04566349 +0.053371842 +0.061935794 +0.071033622 +0.078568344 +0.08223686 +0.077867148 +0.087245613 +0.112216745 +0.114234728 +0.112134688 +0.104987027 +0.098418211 +0.090149471 +0.088943128 +0.094246711 +0.101453906 +0.108715845 +0.110807125 +0.116017816 +0.121264446 +0.117163312 +0.099170427 +0.099656304 +0.088735606 +0.099804795 +0.115469953 +0.125680411 +0.132675204 +0.135004748 +0.121323347 +0.102536115 +0.110563603 +0.105548375 +0.096576035 +0.086837943 +0.078135901 +0.074922882 +0.070327701 +0.064884356 +0.057774359 +0.049103568 +0.041556407 +0.037026934 +0.03602327 +0.035854436 +0.033264031 +0.0276427 +0.020202962 +0.028363757 +0.034598431 +0.035348632 +0.032544864 +0.02709352 +0.026408069 +0.022562945 +0.021871317 +0.027151933 +0.033461481 +0.03792201 +0.039047627 +0.039298227 +0.040298823 +0.039999316 +0.039894496 +0.040154805 +0.042031172 +0.040760113 +0.040732301 +0.040718399 +0.038833753 +0.024058833 +0.01070287 +0.013406062 +0.019997072 +0.028381032 +0.032718357 +0.034353174 +0.04567931 +0.076590529 +0.094880181 +0.101149499 +0.095627662 +0.083521899 +0.07154395 +0.062107321 +0.054318681 +0.048872081 +0.04621674 +0.043466612 +0.04302837 +0.041329675 +0.040212595 +0.038683966 +0.036734981 +0.022932585 +0.010723514 +0.008962102 +0.012221822 +0.016134138 +0.018782467 +0.023117971 +0.029023045 +0.042185866 +0.06508553 +0.082084311 +0.091334828 +0.097224826 +0.094685872 +0.09553216 +0.100759717 +0.103498428 +0.104379203 +0.106043262 +0.108293086 +0.113701555 +0.127302269 +0.139542666 +0.128731568 +0.134785963 +0.156569786 +0.184609389 +0.212535445 +0.24149935 +0.258537432 +0.260620725 +0.241690794 +0.26994732 +0.267950968 +0.245630894 +0.223487602 +0.209412054 +0.206354308 +0.195644466 +0.170215141 +0.146817118 +0.129838713 +0.116381612 +0.107837822 +0.108913099 +0.114322966 +0.111514535 +0.08411044 +0.056696445 +0.032749985 +0.062113081 +0.087192197 +0.094141909 +0.090843223 +0.081497251 +0.079517318 +0.067531954 +0.057847498 +0.053431538 +0.055709475 +0.064747546 +0.079401611 +0.098005348 +0.114848801 +0.124757478 +0.128055506 +0.127402851 +0.118999432 +0.10319417 +0.088403915 +0.07543123 +0.058664452 +0.036861073 +0.023649681 +0.032344339 +0.041284888 +0.043155656 +0.042982435 +0.045781272 +0.073407463 +0.141836673 +0.240824226 +0.357579252 +0.466873063 +0.547549672 +0.595860718 +0.60403288 +0.553322037 +0.494527125 +0.451322615 +0.411274618 +0.372618688 +0.328930883 +0.304485277 +0.301774644 +0.260737266 +0.17767454 +0.230694699 +0.318861295 +0.384332049 +0.401094766 +0.383035973 +0.337220945 +0.324336946 +0.380932864 +0.420889566 +0.435255277 +0.422978023 +0.399555297 +0.334610665 +0.281882698 +0.250274037 +0.233963557 +0.223729092 +0.21601397 +0.193810145 +0.167739884 +0.151175926 +0.141950844 +0.109074484 +0.04248626 +0.019538372 +0.021689269 +0.027933152 +0.033987648 +0.037810969 +0.043028387 +0.105079238 +0.173523828 +0.235268791 +0.274212504 +0.280537038 +0.284241709 +0.275799994 +0.248274637 +0.213485938 +0.181095705 +0.154205632 +0.134971074 +0.121017342 +0.115017759 +0.116225802 +0.128363456 +0.121497382 +0.06425581 +0.043317084 +0.047669245 +0.061252061 +0.070515967 +0.074522121 +0.079136868 +0.133397643 +0.241624656 +0.386411387 +0.495112847 +0.554749752 +0.582471464 +0.597707105 +0.585778041 +0.550986154 +0.515611955 +0.467299827 +0.410113485 +0.358779627 +0.316232528 +0.28947114 +0.275698793 +0.208425052 +0.11090062 +0.08184844 +0.125538932 +0.176046474 +0.208500349 +0.213626609 +0.196454214 +0.257626026 +0.376047409 +0.458768195 +0.500988321 +0.509711226 +0.497600225 +0.473806735 +0.455903594 +0.435563909 +0.407743701 +0.362767701 +0.308002719 +0.26477613 +0.239882276 +0.230748575 +0.22755708 +0.170403065 +0.08572584 +0.061238428 +0.092200519 +0.130644684 +0.149728429 +0.15343845 +0.136081411 +0.157809521 +0.159575532 +0.152178926 +0.151334742 +0.152702722 +0.147260509 +0.145006422 +0.162624454 +0.19138301 +0.215732105 +0.230449406 +0.222693567 +0.20246036 +0.187903354 +0.18858862 +0.194602794 +0.164136749 +0.100464631 +0.065789758 +0.066758069 +0.093223377 +0.106584236 +0.111490645 +0.117606348 +0.170095129 +0.24954715 +0.299414793 +0.311159281 +0.309466614 +0.301783628 +0.289946662 +0.298220568 +0.293685965 +0.311159983 +0.332196815 +0.353423574 +0.373430052 +0.387869442 +0.408949759 +0.425163172 +0.356736853 +0.299603244 +0.350755517 +0.389862949 +0.421875947 +0.445073772 +0.454909173 +0.44416555 +0.439867279 +0.4933823 +0.545818011 +0.555588962 +0.555023178 +0.546372215 +0.534789895 +0.519147322 +0.495093956 +0.480045728 +0.465398441 +0.448686964 +0.432653198 +0.415753827 +0.407603071 +0.39019302 +0.366242147 +0.347032111 +0.381997426 +0.423754823 +0.442178805 +0.452605003 +0.459863457 +0.458455939 +0.434776611 +0.458770741 +0.530872458 +0.548509876 +0.54327427 +0.53445718 +0.517818461 +0.479456826 +0.418906469 +0.371349564 +0.323423819 +0.284537287 +0.260184989 +0.261801848 +0.281628453 +0.280651475 +0.264101807 +0.263142147 +0.3811991 +0.452303872 +0.501814053 +0.551859001 +0.578944321 +0.569163592 +0.533019774 +0.517458472 +0.545290364 +0.509443017 +0.450388715 +0.386518404 +0.319082707 +0.246899437 +0.176875094 +0.128390954 +0.09786637 +0.076620139 +0.062548106 +0.05360238 +0.048630607 +0.040381591 +0.032052927 +0.026547848 +0.022032606 +0.02817696 +0.040994455 +0.060361258 +0.08141019 +0.099188972 +0.115819087 +0.110391895 +0.123721623 +0.119007736 +0.107184568 +0.094068574 +0.083768698 +0.075478081 +0.068626232 +0.061443576 +0.055901032 +0.052429497 +0.048604787 +0.044099728 +0.041161693 +0.036365066 +0.026515174 +0.018310749 +0.021964021 +0.02750154 +0.030250415 +0.032425251 +0.034739122 +0.036532364 +0.040684282 +0.046930878 +0.053687071 +0.053623926 +0.051676328 +0.048824975 +0.045974135 +0.04237713 +0.040945602 +0.043261891 +0.046795288 +0.049210583 +0.049970909 +0.047764589 +0.041795507 +0.032458587 +0.023562157 +0.013416763 +0.012535 +0.01655865 +0.02374436 +0.032788375 +0.040415267 +0.046113327 +0.04982357 +0.054923005 +0.089797172 +0.107383348 +0.10995615 +0.104857937 +0.099096415 +0.087936261 +0.080645522 +0.07813084 +0.080506379 +0.085531393 +0.087543246 +0.089757652 +0.091386621 +0.086694941 +0.080094368 +0.06080012 +0.062589129 +0.068105194 +0.073606704 +0.081150483 +0.090540516 +0.100149573 +0.110586337 +0.12320141 +0.179645059 +0.218405113 +0.22705594 +0.226034044 +0.219926538 +0.201035591 +0.176875116 +0.160409376 +0.154124263 +0.154292786 +0.156031853 +0.154079039 +0.147861199 +0.133811711 +0.118936 +0.102023582 +0.105606227 +0.116468783 +0.132419423 +0.15440772 +0.180316007 +0.204012385 +0.222548186 +0.209358413 +0.276148027 +0.318050972 +0.331794005 +0.331717425 +0.323280891 +0.304780746 +0.278098273 +0.255063165 +0.247246364 +0.247478342 +0.241963407 +0.231470107 +0.221459475 +0.205064116 +0.195525679 +0.166615018 +0.17897819 +0.202587661 +0.220889484 +0.234911771 +0.247321778 +0.255685978 +0.25643785 +0.260586811 +0.35439523 +0.417557276 +0.436529743 +0.437358078 +0.427951333 +0.392604507 +0.342910307 +0.299519186 +0.259602482 +0.232253251 +0.217295067 +0.20833661 +0.21208203 +0.19091254 +0.159412894 +0.121382052 +0.13540903 +0.153685436 +0.167761034 +0.179229847 +0.18668807 +0.187896054 +0.176828061 +0.158194802 +0.209613418 +0.239311393 +0.248556363 +0.239783886 +0.223055597 +0.203187117 +0.183267833 +0.166542634 +0.156089522 +0.151621226 +0.142415086 +0.120339301 +0.099061045 +0.070769669 +0.040123343 +0.026090953 +0.035947432 +0.046383099 +0.05105226 +0.052794158 +0.051164068 +0.046076198 +0.036427483 +0.04584443 +0.057158612 +0.067179605 +0.069375982 +0.066744443 +0.064643824 +0.064084662 +0.066123761 +0.071942799 +0.078818925 +0.089234933 +0.091952794 +0.089756777 +0.094747246 +0.092406028 +0.065865469 +0.044761648 +0.048009565 +0.057464326 +0.057563604 +0.055322828 +0.053382773 +0.052356024 +0.055488781 +0.089386881 +0.170689347 +0.252330159 +0.3041632 +0.332014399 +0.34706065 +0.35029971 +0.341488969 +0.334292542 +0.314372737 +0.292773283 +0.283516073 +0.289143525 +0.301690863 +0.280850651 +0.168516428 +0.11327708 +0.181217954 +0.207288121 +0.213598435 +0.215100988 +0.21194886 +0.209115279 +0.20893703 +0.325199897 +0.462272407 +0.510457978 +0.491465471 +0.442220729 +0.397094898 +0.342683884 +0.301014262 +0.271743167 +0.238974405 +0.210931934 +0.193224104 +0.169739884 +0.150595594 +0.126150224 +0.08865596 +0.101445043 +0.1405503 +0.177586828 +0.214895273 +0.256790824 +0.297529944 +0.322312902 +0.312654853 +0.281362038 +0.344974936 +0.388214519 +0.414148313 +0.444159944 +0.48708095 +0.511395402 +0.496606434 +0.472761307 +0.456179346 +0.443251919 +0.430137606 +0.433940325 +0.444636855 +0.407782674 +0.343408236 +0.391611839 +0.425806058 +0.423707324 +0.406887989 +0.390619814 +0.373748466 +0.352013025 +0.31312515 +0.34155159 +0.460442145 +0.471030871 +0.436686039 +0.384973986 +0.329382939 +0.273896019 +0.233188105 +0.216085231 +0.212856811 +0.207397066 +0.196967956 +0.182105954 +0.160177486 +0.112718726 +0.052930509 +0.031039198 +0.035357353 +0.042592675 +0.050452536 +0.058989394 +0.064669096 +0.067838589 +0.067988463 +0.075190379 +0.081757592 +0.102554892 +0.12269387 +0.137774669 +0.148454516 +0.153297063 +0.149929549 +0.149271226 +0.159387878 +0.178291296 +0.199641669 +0.210045142 +0.202189351 +0.15598455 +0.066170223 +0.046190006 +0.067560061 +0.084665564 +0.107711835 +0.135333833 +0.156285889 +0.162053378 +0.134004736 +0.131005065 +0.163751216 +0.23885395 +0.317543349 +0.380201647 +0.417417088 +0.430721456 +0.432147716 +0.431921169 +0.417956182 +0.387478843 +0.358138222 +0.353911236 +0.367340102 +0.331753067 +0.183407889 +0.129720051 +0.229720607 +0.350304193 +0.408466159 +0.445343488 +0.463534567 +0.443507027 +0.394318974 +0.433590222 +0.449346557 +0.437868459 +0.414258792 +0.374622925 +0.322043152 +0.277395698 +0.243386247 +0.225873834 +0.211699516 +0.193092114 +0.165435452 +0.12520571 +0.108784698 +0.09401455 +0.052084708 +0.031088939 +0.04163321 +0.077877799 +0.096455862 +0.098616028 +0.094280236 +0.09033105 +0.104294008 +0.172037243 +0.246876176 +0.306488832 +0.352808586 +0.389150989 +0.411373447 +0.39603602 +0.360159798 +0.325567306 +0.297823234 +0.285947141 +0.282314521 +0.273958269 +0.265026561 +0.23335156 +0.150060444 +0.075352304 +0.063037896 +0.121154227 +0.153855165 +0.166419478 +0.175368149 +0.174102392 +0.203499523 +0.288276277 +0.382965645 +0.429058106 +0.462015111 +0.50082514 +0.527757864 +0.509780122 +0.475858281 +0.450610522 +0.433030986 +0.403023721 +0.366914843 +0.337582797 +0.313795436 +0.204585357 +0.108091539 +0.110778187 +0.138081272 +0.144018384 +0.143332604 +0.138536958 +0.135336165 +0.138715082 +0.149345028 +0.203592349 +0.280798783 +0.31527147 +0.303701558 +0.273318688 +0.247403203 +0.224679478 +0.21393127 +0.217100518 +0.230061667 +0.245599684 +0.261442589 +0.274227357 +0.269412659 +0.185129345 +0.085212584 +0.086933611 +0.140863687 +0.162920537 +0.187247012 +0.217743158 +0.254841264 +0.290738045 +0.297880975 +0.330231864 +0.516767255 +0.641380033 +0.67257639 +0.655664168 +0.622758628 +0.567646303 +0.50677514 +0.455787443 +0.425484081 +0.416232621 +0.400496157 +0.381580067 +0.339101819 +0.227558323 +0.14448052 +0.174960194 +0.26171169 +0.286084816 +0.30627839 +0.330780622 +0.356841645 +0.370208027 +0.359843471 +0.376277073 +0.467900103 +0.507805353 +0.523170653 +0.524735843 +0.515002637 +0.499314218 +0.468517201 +0.436823946 +0.409976628 +0.38116898 +0.337576898 +0.295948284 +0.270776238 +0.170347498 +0.067845235 +0.050039584 +0.066669474 +0.083868534 +0.085324598 +0.079809852 +0.076331096 +0.075396404 +0.079103341 +0.107691103 +0.132648869 +0.173175674 +0.207608803 +0.227959726 +0.239863618 +0.252088747 +0.264755309 +0.268502017 +0.266916698 +0.267057286 +0.277821401 +0.287727434 +0.278178943 +0.169913537 +0.058809915 +0.051961897 +0.067552568 +0.075432675 +0.092981791 +0.117204813 +0.141340442 +0.157596135 +0.159393546 +0.193565511 +0.200004385 +0.203025946 +0.195113567 +0.192327894 +0.211216094 +0.243000468 +0.272681592 +0.302640086 +0.337688083 +0.372965693 +0.384139425 +0.362306876 +0.346289391 +0.233454999 +0.073672124 +0.050677248 +0.089713795 +0.100488572 +0.112439028 +0.123393747 +0.124837834 +0.113267089 +0.11488165 +0.116224748 +0.096595584 +0.081931971 +0.063886811 +0.043711737 +0.027930178 +0.019210832 +0.019690888 +0.024574006 +0.030461748 +0.035914431 +0.041218541 +0.042982787 +0.044659804 +0.051471142 +0.025412617 +0.007775451 +0.017320793 +0.028988734 +0.036914339 +0.040387094 +0.040537128 +0.03833751 +0.044805743 +0.064151602 +0.082830689 +0.107437386 +0.128722677 +0.141752213 +0.148884425 +0.162287604 +0.176515083 +0.184950192 +0.195064602 +0.211310041 +0.232434936 +0.23689296 +0.233339755 +0.14912211 +0.03309629 +0.031280034 +0.055924492 +0.070958654 +0.089329043 +0.109901242 +0.133081376 +0.153503404 +0.16710001 +0.215073239 +0.201313147 +0.187508731 +0.178238572 +0.172860842 +0.172099889 +0.182544327 +0.208682203 +0.247179962 +0.29981407 +0.352375923 +0.386920667 +0.399109455 +0.392077123 +0.192374953 +0.081704845 +0.17624115 +0.221847736 +0.231752058 +0.26546295 +0.304614699 +0.337104874 +0.348878347 +0.367129075 +0.463572777 +0.599118934 +0.647634438 +0.647319337 +0.631273709 +0.607375016 +0.546183133 +0.489900389 +0.45402853 +0.417441767 +0.384589129 +0.350494474 +0.331799274 +0.303126327 +0.196535179 +0.244625035 +0.272769945 +0.299100215 +0.342282952 +0.388238665 +0.428962555 +0.455789344 +0.470821861 +0.475730524 +0.553897096 +0.69542931 +0.724493662 +0.740850057 +0.744085355 +0.744104155 +0.667551041 +0.553205298 +0.476961021 +0.421879031 +0.367610389 +0.323150241 +0.330669889 +0.334028406 +0.169191683 +0.152943677 +0.277879157 +0.415942697 +0.559958382 +0.643552229 +0.692319066 +0.724220343 +0.735684949 +0.711144833 +0.634136988 +0.663712954 +0.689782764 +0.644396344 +0.59707734 +0.558440227 +0.498132574 +0.421115377 +0.353127135 +0.293221036 +0.240444437 +0.204790589 +0.180070313 +0.152669326 +0.069808165 +0.045712712 +0.091920364 +0.137470057 +0.166999914 +0.193608665 +0.212626304 +0.218866927 +0.208362796 +0.15814244 +0.160474878 +0.227027929 +0.358240592 +0.513252265 +0.622491467 +0.706371681 +0.785093545 +0.786137242 +0.726367927 +0.656438412 +0.61231909 +0.574069731 +0.568861351 +0.543131397 +0.382937336 +0.592427624 +0.726771006 +0.774204762 +0.801689705 +0.81472535 +0.821134624 +0.818055927 +0.803514395 +0.773522809 +0.699732151 +0.674954515 +0.700706916 +0.665769597 +0.625215662 +0.586278821 +0.525793106 +0.457920606 +0.399782304 +0.352985879 +0.317619519 +0.27952653 +0.251858388 +0.185853059 +0.11912124 +0.160283747 +0.164149012 +0.172510277 +0.193480594 +0.222856001 +0.252263245 +0.272862056 +0.278445877 +0.258149894 +0.237888969 +0.295377775 +0.298553492 +0.327100537 +0.386052555 +0.44278791 +0.445030315 +0.397538217 +0.383894525 +0.382053963 +0.380311597 +0.361834393 +0.348386553 +0.286301794 +0.15781664 +0.219899392 +0.255945806 +0.25404755 +0.257200721 +0.267223154 +0.269361464 +0.255555662 +0.220943035 +0.168999966 +0.152593801 +0.185270094 +0.213748535 +0.230540446 +0.239968223 +0.256600881 +0.273396114 +0.266855696 +0.253440799 +0.257117744 +0.261037713 +0.251233867 +0.241801241 +0.210902174 +0.199592549 +0.236053436 +0.255737551 +0.262319746 +0.259804622 +0.253250576 +0.243314225 +0.231830562 +0.223143553 +0.21529234 +0.197535867 +0.214831951 +0.229203042 +0.186768223 +0.13841772 +0.11090764 +0.100901197 +0.107134904 +0.130802434 +0.174427473 +0.217308971 +0.236031465 +0.230972981 +0.179118527 +0.096065311 +0.187197674 +0.292347208 +0.334304009 +0.378298866 +0.399700156 +0.3874525 +0.358913417 +0.320609016 +0.275889673 +0.211720889 +0.18020585 +0.128627666 +0.099234924 +0.118870293 +0.184365692 +0.229836611 +0.234249943 +0.248403755 +0.263072932 +0.275700897 +0.294507534 +0.336876432 +0.355243621 +0.28792118 +0.269028725 +0.392665954 +0.518222926 +0.61558439 +0.692578985 +0.747669684 +0.7944037 +0.820690099 +0.816314978 +0.807085951 +0.835820208 +0.823273821 +0.784512303 +0.736820423 +0.679371498 +0.599815818 +0.523268179 +0.476729932 +0.435529288 +0.403001855 +0.36660407 +0.335528646 +0.236578506 +0.169191295 +0.163471279 +0.116899987 +0.097217182 +0.089797887 +0.086445762 +0.086044288 +0.08801031 +0.088990334 +0.083996353 +0.088688488 +0.093433712 +0.116565345 +0.161663961 +0.202254207 +0.245878083 +0.308337879 +0.38347797 +0.450655324 +0.504955898 +0.535310197 +0.540663995 +0.511886503 +0.408478557 +0.113431972 +0.034315824 +0.048761126 +0.092341804 +0.135870502 +0.173102466 +0.218004596 +0.269563955 +0.307861745 +0.322981284 +0.459244782 +0.568484772 +0.628542947 +0.670540484 +0.69089647 +0.694782135 +0.666686497 +0.623298634 +0.584623455 +0.549605011 +0.516887748 +0.488615652 +0.451670604 +0.336090626 +0.120963904 +0.054951093 +0.090829037 +0.139084702 +0.144908178 +0.165806386 +0.188908266 +0.210285986 +0.225534791 +0.223608513 +0.275850827 +0.28003215 +0.274654646 +0.260724524 +0.241195375 +0.229518516 +0.216569857 +0.198737479 +0.186882457 +0.176775037 +0.178965891 +0.181256668 +0.193069872 +0.178770178 +0.106276945 +0.064383963 +0.05052061 +0.039553085 +0.029144516 +0.026067485 +0.030223806 +0.037112681 +0.045905298 +0.065372657 +0.155679424 +0.333311573 +0.456478371 +0.492237014 +0.500443015 +0.468111945 +0.416073564 +0.370727644 +0.316791953 +0.270276067 +0.241701165 +0.225477819 +0.226616797 +0.172695387 +0.132709201 +0.167537435 +0.189923004 +0.177591274 +0.165914095 +0.161413046 +0.15304598 +0.138988363 +0.121437721 +0.099395882 +0.084150943 +0.093184914 +0.132962826 +0.159866754 +0.171126997 +0.183421444 +0.197475398 +0.209390025 +0.21900008 +0.225807145 +0.231829198 +0.222097068 +0.186671942 +0.094187477 +0.029423759 +0.049980604 +0.086816223 +0.092451585 +0.103005835 +0.119614306 +0.135269913 +0.14640568 +0.150128025 +0.144445833 +0.138332167 +0.164235832 +0.192427099 +0.232507565 +0.258317894 +0.269568541 +0.258848762 +0.233844728 +0.211169077 +0.186363406 +0.159186191 +0.13590853 +0.116736375 +0.074094432 +0.012971127 +0.007971131 +0.013159936 +0.01626103 +0.019474567 +0.023554786 +0.02754095 +0.030677316 +0.031083489 +0.028905424 +0.042344017 +0.078279103 +0.110346404 +0.137218819 +0.153196761 +0.168443192 +0.182468241 +0.201747224 +0.222802769 +0.236443914 +0.242616121 +0.237419579 +0.244058084 +0.231271959 +0.084301523 +0.043737745 +0.077240704 +0.127496491 +0.164346948 +0.200749112 +0.229501202 +0.238838917 +0.227165288 +0.209231899 +0.205757975 +0.213393509 +0.201020021 +0.2018164 +0.203558947 +0.205643738 +0.219554016 +0.234406917 +0.242433737 +0.24386546 +0.245779789 +0.23729265 +0.215515591 +0.163870326 +0.048980901 +0.040772775 +0.090239046 +0.122063868 +0.141860019 +0.153731278 +0.15698175 +0.15132612 +0.135041818 +0.11193249 +0.120016134 +0.09801173 +0.090237549 +0.111286154 +0.143250901 +0.18344641 +0.233484994 +0.311180912 +0.41434644 +0.484072674 +0.522573437 +0.531512034 +0.524121132 +0.453316339 +0.254031111 +0.217088755 +0.289500983 +0.375118544 +0.450406453 +0.517833864 +0.576895114 +0.636875154 +0.683617945 +0.727759917 +0.745414681 +0.715560144 +0.735149352 +0.720560355 +0.68981026 +0.657334289 +0.615986194 +0.57458685 +0.552425328 +0.539662553 +0.527495705 +0.514375687 +0.497365075 +0.491091393 +0.563372147 +0.552306213 +0.547784585 +0.553353796 +0.566455365 +0.576471696 +0.575955888 +0.564697445 +0.543659442 +0.504548339 +0.437066062 +0.391126835 +0.412767776 +0.356411646 +0.289696234 +0.238064396 +0.192883098 +0.151687475 +0.118329734 +0.092040728 +0.077895109 +0.063056444 +0.044568943 +0.020028571 +0.005130156 +0.009039271 +0.013546597 +0.016830766 +0.023044192 +0.03437658 +0.048723436 +0.068811241 +0.091461422 +0.103283727 +0.104633296 +0.137055996 +0.203286654 +0.234318436 +0.237201868 +0.231479477 +0.226951712 +0.215564356 +0.202658406 +0.189267566 +0.183903972 +0.18511658 +0.178572618 +0.086283297 +0.064462954 +0.096977977 +0.115199888 +0.129304275 +0.129168144 +0.1235689 +0.113464201 +0.099643278 +0.087206706 +0.080587155 +0.075808883 +0.142171819 +0.22271645 +0.264299113 +0.255155305 +0.233475227 +0.223148982 +0.212664612 +0.18705716 +0.152868292 +0.123492181 +0.104374198 +0.094287703 +0.046618608 +0.021382785 +0.039387657 +0.045417576 +0.045127712 +0.041667318 +0.036829873 +0.031801841 +0.027093452 +0.02327047 +0.020892697 +0.020757422 +0.042272132 +0.06939472 +0.099967705 +0.120687558 +0.130558637 +0.130994454 +0.124139661 +0.11437378 +0.103569491 +0.095232106 +0.090000613 +0.074138186 +0.027888089 +0.003512866 +0.00608848 +0.008996114 +0.012141078 +0.014240677 +0.015334017 +0.015678396 +0.015741543 +0.015642909 +0.015676239 +0.022023188 +0.056620792 +0.09604201 +0.153799611 +0.202373654 +0.230730671 +0.220908876 +0.192907435 +0.166216005 +0.139890902 +0.131663969 +0.139063646 +0.141012486 +0.077097386 +0.024614785 +0.034118864 +0.043438321 +0.045177477 +0.048133009 +0.051105748 +0.056396368 +0.062063292 +0.067135082 +0.069616692 +0.073539056 +0.114437396 +0.220898793 +0.311331974 +0.36268635 +0.41957313 +0.451387989 +0.473782536 +0.475264392 +0.457065962 +0.430759019 +0.407045027 +0.359359773 +0.147164388 +0.126021777 +0.222336402 +0.256294648 +0.283836169 +0.322044622 +0.362500772 +0.39275762 +0.412394503 +0.417353294 +0.400090655 +0.369200388 +0.439811378 +0.561739122 +0.579462383 +0.56689053 +0.561329364 +0.529850653 +0.48300622 +0.459445714 +0.455888677 +0.448755433 +0.441758864 +0.395230803 +0.166299344 +0.049107256 +0.112491398 +0.197610405 +0.268164617 +0.346315747 +0.415133503 +0.453623206 +0.46623384 +0.462411497 +0.435250948 +0.389051117 +0.451503844 +0.476549829 +0.480901138 +0.487940063 +0.483039457 +0.477152745 +0.465278101 +0.453913734 +0.436297634 +0.41502808 +0.382004306 +0.344827714 +0.179384682 +0.037387413 +0.056900164 +0.171868158 +0.244725548 +0.287636063 +0.320841338 +0.336914237 +0.326302901 +0.291750059 +0.23796573 +0.216192574 +0.263627402 +0.266729829 +0.298788266 +0.326487514 +0.344616901 +0.355065704 +0.370247977 +0.401311875 +0.436669679 +0.481429063 +0.521249869 +0.527496808 +0.351917086 +0.135276848 +0.131719068 +0.284870769 +0.379888708 +0.407352716 +0.411906526 +0.402479824 +0.378577049 +0.344717162 +0.309610703 +0.329889266 +0.421010142 +0.506032317 +0.547358106 +0.525284225 +0.529154524 +0.541882796 +0.552452274 +0.55894111 +0.58272894 +0.590775326 +0.576815444 +0.553903171 +0.481572027 +0.512950222 +0.507443608 +0.472192841 +0.412497974 +0.337238725 +0.272072174 +0.22605292 +0.198820828 +0.197812936 +0.238588456 +0.318555994 +0.414188036 +0.511102698 +0.54761086 +0.524689121 +0.472720026 +0.462501637 +0.45904014 +0.465848628 +0.467503308 +0.451139376 +0.461798132 +0.422783392 +0.378326291 +0.371461903 +0.348478125 +0.330522143 +0.327556533 +0.349011861 +0.373705395 +0.394069318 +0.4089634 +0.416438829 +0.415665795 +0.388597072 +0.339389161 +0.348130756 +0.345809889 +0.331537785 +0.328132361 +0.322912871 +0.313092407 +0.330428826 +0.319770441 +0.297221794 +0.295897171 +0.276926073 +0.219770997 +0.287990923 +0.381673029 +0.379053899 +0.345481889 +0.305076746 +0.271930503 +0.254852263 +0.25051753 +0.246411736 +0.23311999 +0.201385691 +0.200352241 +0.151720814 +0.109073153 +0.096714303 +0.107096812 +0.14756041 +0.214526868 +0.287103504 +0.339753787 +0.357979257 +0.352256908 +0.326605307 +0.145122536 +0.025788177 +0.036443561 +0.074408076 +0.083985695 +0.093856949 +0.105454854 +0.117461135 +0.128893156 +0.137280059 +0.136701586 +0.113531338 +0.111015449 +0.141189032 +0.235135537 +0.356654786 +0.463472052 +0.519895517 +0.528715616 +0.514783731 +0.479020635 +0.440712078 +0.40829935 +0.37909742 +0.204554957 +0.061750642 +0.062527965 +0.17560928 +0.294722127 +0.413239106 +0.516811321 +0.586894841 +0.621521746 +0.625831514 +0.586398291 +0.530696645 +0.537489191 +0.51277111 +0.486852778 +0.472812457 +0.451016803 +0.407305858 +0.372429644 +0.357641137 +0.350486829 +0.356859871 +0.377979812 +0.415043776 +0.348863311 +0.164639365 +0.185658193 +0.312342315 +0.480999808 +0.600729358 +0.701042162 +0.780802851 +0.832905766 +0.848668009 +0.838821378 +0.842364915 +0.817582649 +0.776538184 +0.77278868 +0.76776799 +0.738043149 +0.706141243 +0.685412329 +0.665218284 +0.645134092 +0.622243851 +0.59332323 +0.545534173 +0.5551042 +0.627312194 +0.628903281 +0.625777883 +0.632362585 +0.652230812 +0.672768211 +0.686328334 +0.694261553 +0.697564617 +0.687988669 +0.664644348 +0.614837784 +0.564583296 +0.543003622 +0.490559206 +0.445457872 +0.384632353 +0.33592481 +0.304192437 +0.279475809 +0.246304702 +0.199223153 +0.121778562 +0.059131126 +0.075870236 +0.084818959 +0.085835381 +0.091784497 +0.105300534 +0.127246915 +0.154338727 +0.182088877 +0.203829098 +0.214711759 +0.218818459 +0.204090333 +0.228902274 +0.273005324 +0.283993853 +0.297810469 +0.29262941 +0.256742928 +0.219001292 +0.19058213 +0.163746855 +0.140387826 +0.085729215 +0.043664411 +0.051126251 +0.049120109 +0.054573108 +0.066765508 +0.076312079 +0.082856062 +0.093416899 +0.10697026 +0.122733179 +0.140514198 +0.157276789 +0.175106657 +0.291854617 +0.354802451 +0.346568595 +0.32894734 +0.301477044 +0.265107028 +0.236824523 +0.220460373 +0.204532041 +0.193261232 +0.138632973 +0.088084488 +0.137566629 +0.178238341 +0.204888873 +0.231897505 +0.256210929 +0.280791764 +0.309763045 +0.338411193 +0.361182897 +0.374435625 +0.374826887 +0.325570603 +0.389368485 +0.47788659 +0.45949845 +0.406392113 +0.328781936 +0.257476322 +0.21550994 +0.194941649 +0.176689539 +0.159612191 +0.102832177 +0.042585659 +0.064120817 +0.072240387 +0.066111596 +0.057631369 +0.055871348 +0.061352347 +0.071004119 +0.084936926 +0.101180463 +0.114630608 +0.108988973 +0.104252262 +0.084250776 +0.066068971 +0.061392645 +0.061822723 +0.069053178 +0.081835259 +0.095743504 +0.117305913 +0.134753439 +0.150891291 +0.152808846 +0.06279282 +0.093731299 +0.163139773 +0.188652403 +0.193585528 +0.183009402 +0.177445486 +0.184102371 +0.198754244 +0.216105126 +0.227251557 +0.236247688 +0.271130423 +0.378363332 +0.457901375 +0.469065737 +0.467241465 +0.439648632 +0.394085566 +0.339587102 +0.278376431 +0.240568127 +0.221050018 +0.177494992 +0.069058426 +0.082066304 +0.114841262 +0.103919399 +0.075316156 +0.043237397 +0.022265926 +0.012693168 +0.008713514 +0.007905706 +0.008518192 +0.010318157 +0.022042898 +0.048559574 +0.083823228 +0.122407276 +0.154250957 +0.185102802 +0.193630307 +0.187470362 +0.177510636 +0.16410298 +0.150188251 +0.143906399 +0.046420379 +0.032688355 +0.05135942 +0.063102378 +0.070837842 +0.070079751 +0.066449281 +0.067208593 +0.073935011 +0.086709933 +0.10500843 +0.120456637 +0.130314009 +0.173902701 +0.207491778 +0.20195519 +0.165974699 +0.138315101 +0.115050061 +0.092931579 +0.074381615 +0.057628119 +0.048291883 +0.042671853 +0.016874791 +0.006389535 +0.017551854 +0.025898316 +0.026692702 +0.022444986 +0.017186488 +0.013096829 +0.010723555 +0.010691316 +0.013262301 +0.018362111 +0.038131186 +0.089517995 +0.168626687 +0.233659742 +0.279526825 +0.293108601 +0.278324292 +0.248386539 +0.220424955 +0.191160203 +0.165569558 +0.107547817 +0.023088333 +0.008577003 +0.009896602 +0.013845049 +0.016726851 +0.017504539 +0.018285593 +0.019375114 +0.021421907 +0.024633448 +0.031038889 +0.041133684 +0.057586466 +0.117364136 +0.163665036 +0.185787828 +0.189000535 +0.185809484 +0.179404903 +0.146087392 +0.11161339 +0.102535902 +0.115916271 +0.132498059 +0.114968515 +0.035200457 +0.028788633 +0.081120601 +0.123835015 +0.173213605 +0.219624867 +0.238335858 +0.220571142 +0.184815004 +0.143466349 +0.101584941 +0.082556503 +0.10528889 +0.133849414 +0.145742391 +0.155422153 +0.154911732 +0.155377233 +0.164791848 +0.169860664 +0.167176523 +0.171159945 +0.160109764 +0.065722795 +0.037031361 +0.045489885 +0.045707716 +0.044112964 +0.042019373 +0.038058469 +0.034838547 +0.03348833 +0.035544689 +0.042064767 +0.050666224 +0.072526617 +0.150460295 +0.223278251 +0.237081415 +0.232952498 +0.231150964 +0.236872485 +0.234759854 +0.227411978 +0.211911519 +0.210185772 +0.198295907 +0.149116222 +0.151737475 +0.159671877 +0.157178376 +0.15718786 +0.166671518 +0.186586216 +0.219041143 +0.260388585 +0.30866939 +0.356428776 +0.385497992 +0.435934519 +0.504138135 +0.545308106 +0.548059473 +0.516664675 +0.494683726 +0.479520036 +0.469140685 +0.467200709 +0.465123412 +0.446965011 +0.392900092 +0.424816468 +0.451664124 +0.458234757 +0.464784991 +0.470384602 +0.479805694 +0.483984297 +0.484149316 +0.480168129 +0.472383772 +0.462384892 +0.453718 +0.418855082 +0.402458422 +0.466912964 +0.435748527 +0.388591439 +0.34423881 +0.30513183 +0.281447095 +0.261570867 +0.243184379 +0.231783377 +0.126718781 +0.117893132 +0.113893577 +0.095939482 +0.075327468 +0.053857703 +0.043537137 +0.039928744 +0.039321904 +0.043561351 +0.053840216 +0.069728208 +0.088276927 +0.113317678 +0.220690305 +0.34003992 +0.361412809 +0.364110717 +0.329222509 +0.281868307 +0.244995897 +0.222602349 +0.205259547 +0.177032367 +0.11367011 +0.06217068 +0.076655893 +0.08796283 +0.095894518 +0.101654617 +0.10785989 +0.112390784 +0.120335233 +0.13536712 +0.155120633 +0.17270851 +0.175769573 +0.156384982 +0.205429847 +0.302252395 +0.29467992 +0.26284968 +0.222535177 +0.179017509 +0.150020673 +0.129663867 +0.118699163 +0.111194772 +0.072492743 +0.036613138 +0.046052913 +0.067845087 +0.08549964 +0.094213667 +0.089497361 +0.079310457 +0.071789027 +0.067995382 +0.068172623 +0.067357506 +0.059863519 +0.04889629 +0.072027622 +0.107909964 +0.121015563 +0.120459973 +0.11034026 +0.102413849 +0.094922695 +0.08991192 +0.092848246 +0.095357187 +0.069833401 +0.015955766 +0.014152608 +0.023781285 +0.037365529 +0.045447241 +0.045258635 +0.04390656 +0.042127699 +0.041048027 +0.039447878 +0.036789032 +0.035603376 +0.035261525 +0.046615453 +0.044324866 +0.038645763 +0.034929764 +0.03466408 +0.03468874 +0.036608989 +0.038721413 +0.038908531 +0.039625394 +0.054773001 +0.01560616 +0.006793038 +0.014570799 +0.019897364 +0.022866334 +0.021889172 +0.018263256 +0.013808111 +0.009273402 +0.006159607 +0.005467429 +0.007957182 +0.021096429 +0.092441009 +0.184121666 +0.245754956 +0.261755669 +0.241152776 +0.192339227 +0.145709316 +0.116356344 +0.09650362 +0.085498576 +0.080018149 +0.026015342 +0.012094761 +0.021161719 +0.025568244 +0.029559672 +0.028688716 +0.024685427 +0.021736105 +0.020745352 +0.022475938 +0.026917738 +0.033130097 +0.04992154 +0.103434437 +0.174050194 +0.242237821 +0.277585997 +0.32660645 +0.355653855 +0.362887693 +0.359601985 +0.35967653 +0.352229631 +0.316697103 +0.329160576 +0.358137095 +0.385350524 +0.410227016 +0.431341922 +0.44955341 +0.453671545 +0.441648337 +0.417882964 +0.38726852 +0.351935253 +0.304419375 +0.23522605 +0.188574387 +0.197779912 +0.201010585 +0.190220504 +0.1894891 +0.200997262 +0.207303008 +0.194665596 +0.181104064 +0.160083773 +0.068628605 +0.026849784 +0.051386113 +0.080282264 +0.109400351 +0.140141294 +0.165596838 +0.184271075 +0.19503707 +0.195332875 +0.184711814 +0.161206991 +0.13883684 +0.152318171 +0.192571813 +0.245690679 +0.288206363 +0.294490569 +0.281298602 +0.262979941 +0.239518626 +0.217505205 +0.19542684 +0.175829586 +0.112479393 +0.073743391 +0.09814692 +0.13079967 +0.151416327 +0.169750104 +0.190755667 +0.21015335 +0.232459229 +0.266647279 +0.314824039 +0.370311707 +0.415365817 +0.428278306 +0.479418193 +0.60825917 +0.628623246 +0.609229911 +0.561704705 +0.510045221 +0.46220554 +0.421718295 +0.382679527 +0.344424034 +0.146823961 +0.041973112 +0.072149591 +0.072452425 +0.065947129 +0.07906268 +0.102670879 +0.130002438 +0.147709542 +0.151009199 +0.138980027 +0.119467153 +0.089776391 +0.066921426 +0.07092154 +0.116804369 +0.170057313 +0.218514449 +0.287868906 +0.345448062 +0.367953113 +0.368636038 +0.354223739 +0.35128741 +0.286060109 +0.135847095 +0.137987612 +0.230963897 +0.288570626 +0.384064177 +0.462218548 +0.458348082 +0.43456685 +0.400032431 +0.365965026 +0.336243893 +0.308648411 +0.273564404 +0.243245966 +0.316066614 +0.353075448 +0.366860971 +0.369487226 +0.385254657 +0.387084963 +0.38347034 +0.374956302 +0.359006781 +0.241688919 +0.223128514 +0.339917643 +0.390384961 +0.409376692 +0.428448204 +0.443953779 +0.451756887 +0.450185608 +0.436189724 +0.411206424 +0.37494573 +0.323111987 +0.243129796 +0.229852685 +0.165569849 +0.128944243 +0.107285267 +0.104297559 +0.117735566 +0.137331443 +0.151941557 +0.157645225 +0.147748579 +0.117619587 +0.02199795 +0.020819904 +0.043594991 +0.053362127 +0.064138236 +0.082512345 +0.111818392 +0.145124275 +0.177030482 +0.206080031 +0.232090002 +0.239961329 +0.236962753 +0.257408715 +0.372955686 +0.430795262 +0.449715973 +0.43995974 +0.400955235 +0.379972939 +0.360483262 +0.345047525 +0.319879422 +0.224605745 +0.24076619 +0.317274088 +0.337402636 +0.347625347 +0.350467513 +0.354009315 +0.371534062 +0.383585114 +0.396964398 +0.414771722 +0.41989673 +0.383076971 +0.319814231 +0.321605459 +0.422039731 +0.472277194 +0.466693223 +0.462477531 +0.462345471 +0.464844624 +0.480973111 +0.481508511 +0.453680234 +0.28669591 +0.378186787 +0.456434197 +0.486788546 +0.525810539 +0.564726081 +0.608568876 +0.645076229 +0.666154812 +0.669980678 +0.659951435 +0.639305991 +0.603920878 +0.539114684 +0.478501939 +0.530888072 +0.504212112 +0.467031178 +0.413446136 +0.378712621 +0.357741621 +0.32737497 +0.306451656 +0.264317926 +0.129712327 +0.058791465 +0.04653742 +0.054003075 +0.066589279 +0.082450136 +0.115022937 +0.164938304 +0.216789434 +0.260615942 +0.286305558 +0.28374501 +0.250099609 +0.234735261 +0.297037817 +0.352815558 +0.339215442 +0.282248316 +0.243667529 +0.226082016 +0.218805669 +0.187540359 +0.161870007 +0.139648009 +0.064005252 +0.016642038 +0.010696662 +0.007792981 +0.012393971 +0.027687425 +0.054042085 +0.081845554 +0.104217098 +0.118030947 +0.120182417 +0.107030588 +0.076484588 +0.046371736 +0.143606066 +0.400735648 +0.557388539 +0.570030968 +0.535886002 +0.522736326 +0.505696293 +0.452807738 +0.416342602 +0.380890738 +0.288937306 +0.155158524 +0.132519052 +0.13057305 +0.126225966 +0.126904909 +0.140416202 +0.151718333 +0.15329741 +0.142995655 +0.125129656 +0.110086044 +0.100561222 +0.111913935 +0.193920536 +0.300508636 +0.325826517 +0.29988332 +0.324188786 +0.338617791 +0.338910584 +0.344444113 +0.327584555 +0.311412267 +0.172059812 +0.157624523 +0.196309368 +0.210807974 +0.208948024 +0.205943839 +0.196602206 +0.195141816 +0.198220312 +0.202409377 +0.219593339 +0.256802503 +0.313172526 +0.380507391 +0.406719163 +0.419869976 +0.43308817 +0.422391316 +0.397358015 +0.369082295 +0.339372804 +0.312600367 +0.289327467 +0.260551559 +0.238848616 +0.244537375 +0.259631359 +0.260268097 +0.248603898 +0.234456878 +0.207607285 +0.180317109 +0.159243787 +0.138559324 +0.116741463 +0.096789176 +0.083481339 +0.073898891 +0.091237583 +0.105343669 +0.097071076 +0.089395994 +0.08460664 +0.092845008 +0.115880174 +0.135766705 +0.149146871 +0.154942575 +0.117587091 +0.033218203 +0.021275271 +0.061532729 +0.094184668 +0.097245857 +0.097624008 +0.089502112 +0.070919054 +0.056114846 +0.053626834 +0.057383186 +0.058903272 +0.070167109 +0.138076013 +0.249317387 +0.346875082 +0.370093022 +0.345776337 +0.303093449 +0.25301499 +0.21493435 +0.207754044 +0.212821358 +0.126126206 +0.072272223 +0.136120621 +0.22341849 +0.286128736 +0.317341479 +0.336252209 +0.355304378 +0.370670223 +0.383222058 +0.393533333 +0.405456158 +0.413861712 +0.406831002 +0.376791612 +0.38675519 +0.373249942 +0.363856558 +0.369387392 +0.378238053 +0.363227996 +0.360429381 +0.361178493 +0.341471357 +0.202458911 +0.225682911 +0.372836682 +0.444816037 +0.48552424 +0.516893331 +0.548520817 +0.566912353 +0.572407893 +0.565276117 +0.543570976 +0.503782565 +0.441331007 +0.358930775 +0.34583244 +0.406344041 +0.399656001 +0.364563863 +0.334500364 +0.319066865 +0.301430749 +0.268780215 +0.240121367 +0.217671709 +0.120409832 +0.038504206 +0.064991839 +0.069818222 +0.05567662 +0.048371169 +0.054767559 +0.07036316 +0.087324081 +0.095501485 +0.092930452 +0.084419391 +0.080550883 +0.089561499 +0.130503369 +0.217279085 +0.287925175 +0.337452068 +0.354002049 +0.360235616 +0.362310713 +0.343492977 +0.312790017 +0.296488341 +0.176707542 +0.080528459 +0.120962905 +0.229086559 +0.278962636 +0.315176826 +0.35571289 +0.386968813 +0.397812205 +0.388310924 +0.366201 +0.340058743 +0.303408193 +0.260259496 +0.301445261 +0.366750966 +0.37947066 +0.360734014 +0.328452422 +0.295873029 +0.254312133 +0.206489169 +0.163347891 +0.134098037 +0.044362683 +0.01499769 +0.01620478 +0.034649565 +0.064664583 +0.096390555 +0.112453537 +0.109593816 +0.101578263 +0.09737536 +0.098315106 +0.096650841 +0.09244216 +0.109485446 +0.150369578 +0.184849669 +0.198916286 +0.192694113 +0.167761453 +0.1464572 +0.142985949 +0.137627472 +0.134845812 +0.132623917 +0.127921956 +0.082114855 +0.10237905 +0.138587533 +0.162124403 +0.163876221 +0.132055381 +0.091713997 +0.061777932 +0.042510765 +0.031029599 +0.02579604 +0.030665476 +0.048289152 +0.0961643 +0.203834569 +0.268718916 +0.253705455 +0.198434796 +0.147239079 +0.112096266 +0.092344797 +0.081118814 +0.080968212 +0.037259233 +0.008946851 +0.010075852 +0.007536445 +0.005120083 +0.003118796 +0.001849406 +0.001538258 +0.002071096 +0.003757896 +0.006994286 +0.012092868 +0.018471361 +0.02526333 +0.045823103 +0.080741695 +0.092039803 +0.088192522 +0.081006101 +0.07686735 +0.07901104 +0.084526158 +0.091861363 +0.101811938 +0.069693496 +0.011081828 +0.019276562 +0.033688827 +0.037696342 +0.042769761 +0.043029215 +0.039103878 +0.035011843 +0.032328101 +0.033703075 +0.041084539 +0.054279261 +0.08247668 +0.181207069 +0.403157039 +0.492743071 +0.488563607 +0.470166621 +0.441269407 +0.398480595 +0.358781108 +0.317366985 +0.269891817 +0.158219926 +0.15924358 +0.223459282 +0.248143636 +0.268596816 +0.298674267 +0.322608396 +0.335982515 +0.341768873 +0.339662256 +0.323405225 +0.299035004 +0.260720386 +0.219873698 +0.184010232 +0.22904429 +0.26327555 +0.263517902 +0.266582661 +0.263777202 +0.250361255 +0.248254821 +0.236395367 +0.193500155 +0.0811918 +0.034656662 +0.049765547 +0.041718765 +0.040170853 +0.047791538 +0.06080388 +0.070566076 +0.074082719 +0.073695248 +0.07102899 +0.068886376 +0.064590256 +0.061843561 +0.052294393 +0.047382475 +0.052253459 +0.054458442 +0.048966119 +0.048749825 +0.056687919 +0.065276034 +0.073892608 +0.088456495 +0.096313316 +0.018749404 +0.017979311 +0.027300288 +0.037224658 +0.04910066 +0.062250461 +0.085308997 +0.115632511 +0.144214307 +0.166930594 +0.184655515 +0.18981546 +0.187597514 +0.179504721 +0.242867817 +0.295459767 +0.27884828 +0.270000948 +0.251880478 +0.223496519 +0.203002516 +0.193683554 +0.170479553 +0.078315743 +0.080311089 +0.086659458 +0.071428636 +0.052596421 +0.038638436 +0.030509846 +0.025986199 +0.023374613 +0.022545736 +0.022680083 +0.024511911 +0.031930991 +0.064537526 +0.172790459 +0.27669564 +0.296802871 +0.259391583 +0.206948697 +0.176528932 +0.160185569 +0.158697655 +0.149525053 +0.142758261 +0.131340675 +0.121526943 +0.124708885 +0.127395546 +0.133483772 +0.144424959 +0.139543332 +0.11798568 +0.097631384 +0.085235881 +0.081418409 +0.081342813 +0.085071074 +0.092885167 +0.118129813 +0.186813803 +0.203557534 +0.190189664 +0.169962061 +0.149573164 +0.134225478 +0.118827662 +0.105157886 +0.088771773 +0.051886869 +0.04506915 +0.042268645 +0.037303476 +0.037708606 +0.045053858 +0.050660881 +0.051500002 +0.047039627 +0.040017835 +0.034997136 +0.029877089 +0.028676594 +0.040657669 +0.082255171 +0.132912138 +0.182699404 +0.217094269 +0.206261048 +0.186449149 +0.173675738 +0.172690116 +0.176262577 +0.157872315 +0.067955251 +0.029552196 +0.036516165 +0.057914824 +0.072074164 +0.078370966 +0.077164852 +0.065020541 +0.049393308 +0.03861643 +0.034329703 +0.034979148 +0.034566146 +0.041921072 +0.09336724 +0.165090061 +0.207837228 +0.210004159 +0.196566388 +0.190188688 +0.185756161 +0.180346789 +0.182660065 +0.152911993 +0.065654666 +0.041504824 +0.076949049 +0.144402329 +0.188902019 +0.20876949 +0.201893287 +0.180925707 +0.166449014 +0.156388518 +0.154977829 +0.155293174 +0.150801734 +0.146441027 +0.162107661 +0.253555347 +0.283853014 +0.273081983 +0.253738683 +0.233476342 +0.219004849 +0.212748556 +0.219024199 +0.213570918 +0.11804877 +0.06261284 +0.09497312 +0.155787717 +0.189714874 +0.205546163 +0.217996501 +0.220315853 +0.211040804 +0.196743329 +0.179909349 +0.162930322 +0.144556588 +0.112380337 +0.089014193 +0.134054748 +0.173821261 +0.165521326 +0.14062616 +0.116209668 +0.099144423 +0.09481865 +0.098479791 +0.111741991 +0.08162747 +0.020180828 +0.036295186 +0.084053378 +0.122390577 +0.141987595 +0.1399322 +0.126926016 +0.110143556 +0.093930205 +0.080517542 +0.069345527 +0.061632703 +0.055586894 +0.048605085 +0.034399434 +0.041551205 +0.052724905 +0.058166764 +0.060359506 +0.070586032 +0.085180722 +0.09845203 +0.113660646 +0.117089104 +0.016199728 +0.003679728 +0.009282529 +0.012482737 +0.01472546 +0.014767695 +0.013389812 +0.01169269 +0.010838089 +0.01003866 +0.009883193 +0.012925603 +0.02012847 +0.043416908 +0.087567197 +0.137626461 +0.184652804 +0.185950111 +0.176656501 +0.167044761 +0.158771471 +0.148354189 +0.13477359 +0.050394552 +0.003472504 +0.001730593 +0.001398169 +0.000498145 +0.000178588 +0.000299783 +0.001159612 +0.00244333 +0.00383948 +0.005158084 +0.006551638 +0.009785752 +0.015307352 +0.036545541 +0.056642812 +0.079965701 +0.091896405 +0.085419661 +0.072658007 +0.066170271 +0.068198973 +0.070826289 +0.074721633 +0.042814115 +0.008828865 +0.010797774 +0.013103493 +0.015121131 +0.020611559 +0.03141235 +0.046269008 +0.064706024 +0.086766984 +0.107784586 +0.125784632 +0.124233919 +0.117002745 +0.142652697 +0.222365638 +0.25333776 +0.238441138 +0.206020114 +0.186825764 +0.187498417 +0.207506666 +0.226857032 +0.206641146 +0.135214589 +0.141827974 +0.178251754 +0.202666914 +0.216983845 +0.225975348 +0.225155745 +0.216598432 +0.206311614 +0.195519361 +0.186732199 +0.181503821 +0.17658443 +0.16816773 +0.205950973 +0.262001785 +0.297974233 +0.286926249 +0.267568985 +0.252440477 +0.243364921 +0.239581864 +0.229417436 +0.210197393 +0.091096558 +0.015016556 +0.015738598 +0.024781728 +0.030550188 +0.03443035 +0.035748455 +0.03703364 +0.040474084 +0.045879647 +0.052866927 +0.05995338 +0.066905796 +0.073990621 +0.080607126 +0.073160161 +0.066527043 +0.056387188 +0.046066001 +0.043147876 +0.047629181 +0.064639536 +0.087222253 +0.113361462 +0.11712299 +0.015499883 +0.004542315 +0.012021911 +0.011506015 +0.012246486 +0.013942809 +0.01756456 +0.022312745 +0.02680108 +0.029853474 +0.031854351 +0.039421434 +0.049136253 +0.077966252 +0.139182712 +0.220690621 +0.28335334 +0.296227932 +0.301346547 +0.312048619 +0.316107562 +0.314177872 +0.298457325 +0.166691905 +0.028875897 +0.015525681 +0.035621433 +0.041932554 +0.046482648 +0.059190597 +0.079017268 +0.097891725 +0.110741073 +0.118087878 +0.12137442 +0.124232781 +0.121038824 +0.208222187 +0.329702018 +0.399836506 +0.407543214 +0.393047189 +0.361178168 +0.317738444 +0.278966701 +0.249734069 +0.215299294 +0.082875049 +0.016179824 +0.017696735 +0.018038776 +0.016738247 +0.014617648 +0.013252999 +0.01403236 +0.015350861 +0.017750353 +0.022241082 +0.031549888 +0.045345374 +0.071237194 +0.13081786 +0.176573204 +0.197028109 +0.186698243 +0.168518188 +0.156902145 +0.146007174 +0.134482694 +0.121038631 +0.110685289 +0.057777193 +0.012583975 +0.012245311 +0.017230562 +0.026080778 +0.036578697 +0.046282971 +0.055761402 +0.064001411 +0.068431644 +0.070633483 +0.073704777 +0.079487568 +0.081725323 +0.118247303 +0.189545576 +0.250783988 +0.285234806 +0.311462995 +0.323396109 +0.29961279 +0.275976222 +0.250898805 +0.207217524 +0.083536366 +0.022458895 +0.015187812 +0.01731536 +0.024256472 +0.035280525 +0.052060662 +0.076449355 +0.103722573 +0.125589487 +0.141211251 +0.148611856 +0.145917447 +0.150616833 +0.18434764 +0.222112828 +0.239620152 +0.229402442 +0.223215266 +0.209167335 +0.197792646 +0.183317209 +0.166106032 +0.137506963 +0.101057108 +0.093318238 +0.078773345 +0.065168093 +0.059897136 +0.057088584 +0.061579493 +0.071740055 +0.080982142 +0.089235125 +0.096273682 +0.102681428 +0.110809312 +0.121741168 +0.138030311 +0.227180206 +0.301971672 +0.324495595 +0.32119949 +0.304171734 +0.283179912 +0.263276503 +0.25549961 +0.228864557 +0.101932239 +0.099981777 +0.15517225 +0.174236914 +0.174869731 +0.174469796 +0.18311901 +0.21092994 +0.252927665 +0.298812656 +0.337956391 +0.3693342 +0.390383583 +0.380836669 +0.357881019 +0.47381533 +0.525542199 +0.510504889 +0.455338295 +0.407212547 +0.367845931 +0.336574164 +0.303526638 +0.258719367 +0.161817564 +0.187885178 +0.20109222 +0.19708596 +0.193617184 +0.185807632 +0.187740353 +0.19580104 +0.203149093 +0.206060623 +0.204482602 +0.202734569 +0.192795209 +0.174623173 +0.196095971 +0.22301416 +0.229586787 +0.220083653 +0.213904054 +0.207796531 +0.188763464 +0.170193046 +0.146155233 +0.121975309 +0.036647647 +0.003764463 +0.005472707 +0.008920885 +0.010826243 +0.012340218 +0.013737488 +0.018078558 +0.026750988 +0.038889323 +0.052853128 +0.065227602 +0.073294775 +0.075186742 +0.126178485 +0.188825341 +0.220186149 +0.222133495 +0.230112005 +0.245333204 +0.248885456 +0.243956061 +0.235253017 +0.203224062 +0.099605804 +0.090873499 +0.117888404 +0.119336003 +0.109136819 +0.102316007 +0.112680145 +0.137295493 +0.165931141 +0.190916934 +0.209402299 +0.220916612 +0.232546661 +0.229837653 +0.252379556 +0.320492278 +0.373200527 +0.355517611 +0.320986769 +0.287642809 +0.252006507 +0.224218745 +0.207374421 +0.172545895 +0.068535279 +0.057886929 +0.072146973 +0.086247164 +0.106623986 +0.126938358 +0.144433439 +0.161413437 +0.191870293 +0.234441019 +0.282426702 +0.325082372 +0.344072356 +0.335359705 +0.343504545 +0.514127098 +0.558708446 +0.538724215 +0.501999257 +0.455318449 +0.423863519 +0.391227298 +0.377729734 +0.355780037 +0.268465193 +0.269315358 +0.365419519 +0.428458017 +0.467696577 +0.485150766 +0.474879462 +0.466569834 +0.468134361 +0.456549627 +0.430137341 +0.39589131 +0.352704028 +0.320192518 +0.350234314 +0.421589305 +0.439645243 +0.411281187 +0.372259622 +0.331084338 +0.314826832 +0.298387842 +0.283461651 +0.265349893 +0.2115788 +0.16904644 +0.158298827 +0.15825588 +0.185878938 +0.201413966 +0.185026645 +0.149763657 +0.126239639 +0.116199055 +0.114296103 +0.115882728 +0.113296726 +0.1337354 +0.234688156 +0.336876465 +0.359261775 +0.331746332 +0.288043592 +0.255540404 +0.246515908 +0.26273352 +0.281997676 +0.287620624 +0.131146608 +0.047002076 +0.060083125 +0.147001925 +0.1882663 +0.203914841 +0.211592649 +0.21858239 +0.230675055 +0.244340843 +0.256273888 +0.266584048 +0.26484243 +0.261298708 +0.331392817 +0.400091146 +0.428479109 +0.411072275 +0.374072041 +0.350896184 +0.355356141 +0.365591759 +0.363690179 +0.351172013 +0.17650894 +0.032846586 +0.025939707 +0.086267717 +0.11684662 +0.133876329 +0.154931516 +0.181135414 +0.201025946 +0.214552468 +0.227278654 +0.238430372 +0.22972348 +0.203669686 +0.226924568 +0.272451097 +0.291823823 +0.290819071 +0.270530228 +0.263614081 +0.256840952 +0.249805137 +0.254219186 +0.2563526 +0.1210259 +0.024356171 +0.041140912 +0.059149387 +0.067007613 +0.070827695 +0.07394311 +0.079223286 +0.090437996 +0.107963266 +0.126376573 +0.138625549 +0.140922778 +0.134383389 +0.183850359 +0.247549513 +0.265486136 +0.255591109 +0.18690206 +0.12573599 +0.103892285 +0.102334999 +0.116067224 +0.135420346 +0.072167626 +0.009147187 +0.011793624 +0.014452281 +0.010992087 +0.007053221 +0.004763256 +0.005367886 +0.008949716 +0.014735771 +0.021160314 +0.027105932 +0.034923872 +0.045584217 +0.077194988 +0.101263072 +0.096596899 +0.077510485 +0.062747928 +0.049641408 +0.041688082 +0.03854871 +0.041375057 +0.047335887 +0.030460565 +0.00861048 +0.013321429 +0.022037829 +0.026224111 +0.025993784 +0.018942367 +0.012263161 +0.010058404 +0.010595619 +0.012713832 +0.015961246 +0.020905422 +0.027406442 +0.04612893 +0.073961195 +0.088748725 +0.083725726 +0.079033797 +0.073941554 +0.068164342 +0.065283197 +0.067244432 +0.076468561 +0.057373962 +0.007061293 +0.013762892 +0.023751076 +0.032000877 +0.037322747 +0.034470948 +0.026391426 +0.019423449 +0.01475343 +0.012208203 +0.011346734 +0.015459238 +0.026543152 +0.068514073 +0.124367457 +0.170347036 +0.175046836 +0.15285947 +0.125398221 +0.105391456 +0.094806622 +0.09025957 +0.093182228 +0.061967757 +0.014014508 +0.035351996 +0.069562457 +0.090797384 +0.10192842 +0.090597278 +0.072358237 +0.060517694 +0.054215085 +0.053015477 +0.056655997 +0.074657646 +0.101643076 +0.163570082 +0.310503835 +0.357130425 +0.347080484 +0.30781709 +0.264527361 +0.217537014 +0.180681559 +0.15409998 +0.145882671 +0.076235949 +0.030484309 +0.078548704 +0.116313882 +0.142244679 +0.161098835 +0.16896972 +0.167270433 +0.16284179 +0.157804154 +0.1505111 +0.145046821 +0.148460693 +0.163282143 +0.265057529 +0.469803684 +0.551789757 +0.551199911 +0.50467349 +0.438788533 +0.378247956 +0.341586908 +0.30289597 +0.273830064 +0.179046898 +0.15014698 +0.218465234 +0.283725857 +0.326224015 +0.350111341 +0.363750961 +0.370637857 +0.368953789 +0.35428791 +0.33373484 +0.315422614 +0.323345101 +0.331041473 +0.367702071 +0.470632133 +0.494485278 +0.464285645 +0.409615513 +0.37432557 +0.361529555 +0.331878843 +0.296426487 +0.273615441 +0.158571235 +0.101084241 +0.168435926 +0.199123346 +0.220991082 +0.232855512 +0.240735647 +0.242116269 +0.234730544 +0.217243028 +0.191064939 +0.172762496 +0.147614632 +0.13300855 +0.144335854 +0.198673884 +0.216561891 +0.20444259 +0.250503695 +0.243723262 +0.225466177 +0.191529276 +0.153004934 +0.134667664 +0.080360193 +0.027044372 +0.027037712 +0.039818417 +0.034559331 +0.027044147 +0.027344589 +0.034276528 +0.04714716 +0.061698952 +0.074600852 +0.087087522 +0.097065009 +0.105080706 +0.172993277 +0.283980177 +0.357575818 +0.377733727 +0.36932856 +0.359679776 +0.355667993 +0.355596672 +0.346475187 +0.339469968 +0.215849418 +0.120376922 +0.152403755 +0.188719539 +0.206587846 +0.210460428 +0.20885829 +0.211079544 +0.220898472 +0.237932242 +0.255314546 +0.266083384 +0.261481975 +0.242783929 +0.262092679 +0.332974231 +0.364615326 +0.35672541 +0.344520346 +0.327981911 +0.311045085 +0.295085378 +0.289233026 +0.258140114 +0.111643767 +0.075949334 +0.13592832 +0.136131718 +0.124578596 +0.119774691 +0.119578439 +0.123007628 +0.133509657 +0.151894688 +0.172265652 +0.194781537 +0.207207444 +0.203340861 +0.298982436 +0.46513582 +0.513897665 +0.477948941 +0.441855175 +0.397374399 +0.357943168 +0.331517273 +0.305081871 +0.27169007 +0.134749218 +0.035317161 +0.035627514 +0.04370026 +0.045302434 +0.045724199 +0.045966908 +0.044814091 +0.042042165 +0.039353465 +0.037031594 +0.035102537 +0.031733877 +0.023934155 +0.013879402 +0.010796864 +0.009432779 +0.016119073 +0.031396851 +0.049265587 +0.071584298 +0.086693692 +0.09096989 +0.088233186 +0.077670222 +0.019261225 +0.002421732 +0.006600743 +0.008005573 +0.00819183 +0.007400121 +0.006536402 +0.00658269 +0.007681272 +0.010546883 +0.016241191 +0.026771024 +0.04794533 +0.121521343 +0.233884249 +0.319559024 +0.341183212 +0.329959278 +0.31210588 +0.29715324 +0.277532622 +0.260807749 +0.248168596 +0.194024363 +0.05360516 +0.041060776 +0.094460125 +0.128675126 +0.141605989 +0.137431758 +0.120637994 +0.100620981 +0.080942926 +0.065260609 +0.058553667 +0.068867004 +0.113478313 +0.316523144 +0.51250663 +0.554380873 +0.506831534 +0.460661116 +0.423448073 +0.389049074 +0.344740579 +0.307481453 +0.270813709 +0.144867577 +0.065330575 +0.057513411 +0.082013399 +0.103108264 +0.121161058 +0.134626565 +0.151151443 +0.166855884 +0.170810344 +0.161294934 +0.142984883 +0.127102802 +0.14082458 +0.166334911 +0.166903697 +0.159101195 +0.149737 +0.147528085 +0.158611137 +0.175256373 +0.153215925 +0.125126882 +0.112135928 +0.054647796 +0.014485837 +0.018076161 +0.019293809 +0.024370786 +0.039425552 +0.060855431 +0.081605438 +0.102049055 +0.12271668 +0.141218329 +0.158006641 +0.159913807 +0.146472406 +0.192026878 +0.278856094 +0.304894258 +0.291169149 +0.287296778 +0.278748332 +0.251837969 +0.238836026 +0.22657138 +0.213381356 +0.122443551 +0.099795678 +0.137527146 +0.181105712 +0.212052886 +0.241404173 +0.270149718 +0.297438919 +0.320289306 +0.339762012 +0.34790424 +0.33563057 +0.296617265 +0.233305111 +0.255813754 +0.308946487 +0.299501673 +0.276432982 +0.267274744 +0.253345942 +0.228095036 +0.205515644 +0.183052477 +0.160168092 +0.085117235 +0.029557089 +0.034194753 +0.049374354 +0.055690053 +0.055190951 +0.053231144 +0.049713693 +0.045085813 +0.038617098 +0.029611105 +0.020456188 +0.013365042 +0.013752839 +0.031189771 +0.077937357 +0.160897472 +0.25318242 +0.283980934 +0.273049309 +0.266646317 +0.267313466 +0.257689044 +0.244940137 +0.228510159 +0.133864237 +0.122756152 +0.171959479 +0.220340187 +0.289492232 +0.346122705 +0.372770379 +0.364746528 +0.338053641 +0.301711144 +0.258717529 +0.209035851 +0.174711496 +0.26656012 +0.361816759 +0.387811059 +0.358611333 +0.339555978 +0.325803485 +0.324160458 +0.290490881 +0.268168539 +0.256423932 +0.199955761 +0.1820033 +0.254948277 +0.335490737 +0.393778376 +0.430508448 +0.469274902 +0.509322009 +0.536373732 +0.552075052 +0.559737956 +0.559462306 +0.540968028 +0.493897617 +0.495032713 +0.58893697 +0.582597725 +0.550615119 +0.532587986 +0.519780801 +0.511387589 +0.501715818 +0.506233166 +0.501370232 +0.40093054 +0.365129131 +0.505890094 +0.537229651 +0.550861173 +0.559970345 +0.565356615 +0.553818453 +0.527889545 +0.491161994 +0.444234352 +0.386804999 +0.311586663 +0.245970489 +0.286804476 +0.348602721 +0.336479259 +0.297974273 +0.27004907 +0.240634791 +0.216664619 +0.204976195 +0.192047884 +0.17114017 +0.097698696 +0.022165085 +0.026264727 +0.044872342 +0.057357147 +0.069786323 +0.082943337 +0.096242651 +0.108220626 +0.116872604 +0.122528485 +0.125021955 +0.124213528 +0.123366995 +0.199087315 +0.337070112 +0.339337133 +0.324693226 +0.297070711 +0.267932581 +0.238408787 +0.221682268 +0.201966426 +0.185252356 +0.125739147 +0.124033881 +0.188288628 +0.23730677 +0.270038853 +0.290664423 +0.300108014 +0.298913188 +0.292471005 +0.281684063 +0.264035926 +0.238151734 +0.199640433 +0.160192353 +0.21764989 +0.279297509 +0.292186054 +0.279160037 +0.258516462 +0.2387385 +0.220808944 +0.20720699 +0.20083875 +0.194679955 +0.129842677 +0.02660814 +0.022510113 +0.027099448 +0.024996457 +0.023578151 +0.023189073 +0.024410133 +0.027790875 +0.03375175 +0.042470572 +0.051960299 +0.061923604 +0.080768702 +0.142923978 +0.196023818 +0.211477613 +0.213676313 +0.213146251 +0.218555959 +0.22279451 +0.2237451 +0.214331214 +0.198785495 +0.123408752 +0.023648369 +0.019167122 +0.02947936 +0.032401373 +0.030566559 +0.028338864 +0.027938114 +0.029241985 +0.031209664 +0.032932969 +0.033228107 +0.030536763 +0.031001423 +0.03022504 +0.031552811 +0.042756678 +0.06255647 +0.083096602 +0.102163074 +0.114551368 +0.120533355 +0.124113929 +0.124883645 +0.110407797 +0.015832291 +0.003363496 +0.008546933 +0.010050838 +0.013514634 +0.017870085 +0.022275579 +0.025346829 +0.025950496 +0.024773134 +0.021782622 +0.018532592 +0.018143009 +0.024135424 +0.059732803 +0.143978422 +0.260692455 +0.335523008 +0.34909354 +0.315901589 +0.263070694 +0.218968201 +0.195350423 +0.161682593 +0.032626563 +0.00981876 +0.039129273 +0.061650693 +0.07530601 +0.088117457 +0.099291548 +0.109314258 +0.12007 +0.134195105 +0.148473808 +0.155315293 +0.222083713 +0.38149866 +0.478763613 +0.438666426 +0.383357166 +0.346965388 +0.329232404 +0.304143755 +0.278207261 +0.27155635 +0.269523967 +0.226841704 +0.114733817 +0.089100448 +0.168630993 +0.239780551 +0.298316297 +0.349276706 +0.38684677 +0.398595917 +0.400248923 +0.388546708 +0.359965066 +0.324417843 +0.314478577 +0.355372431 +0.354410712 +0.301610016 +0.277859208 +0.270724805 +0.257496552 +0.241239889 +0.22226084 +0.210106737 +0.199166061 +0.150764877 +0.105447428 +0.113827092 +0.127394799 +0.14492254 +0.150124407 +0.154340229 +0.160943829 +0.161444698 +0.156997265 +0.14647718 +0.140797621 +0.138536082 +0.145555162 +0.201210855 +0.260355645 +0.278656357 +0.289751779 +0.268557397 +0.245665348 +0.22712093 +0.20961106 +0.19973437 +0.187808561 +0.175190256 +0.159105775 +0.220696397 +0.273963904 +0.328127789 +0.383466186 +0.435937189 +0.476233686 +0.506052768 +0.524481769 +0.533538786 +0.532560805 +0.515129607 +0.490640896 +0.571094991 +0.653637519 +0.637281134 +0.603716528 +0.511024749 +0.431488527 +0.398137807 +0.364195514 +0.332674079 +0.303791316 +0.233064517 +0.118869321 +0.182763984 +0.21011369 +0.239105498 +0.266293574 +0.285046285 +0.294618818 +0.301262778 +0.305501921 +0.305981692 +0.297472615 +0.273197013 +0.225848993 +0.25312981 +0.252247846 +0.241109231 +0.218781443 +0.189603859 +0.174295155 +0.178606001 +0.182498759 +0.178309172 +0.164959026 +0.130143023 +0.025432466 +0.033227022 +0.064877052 +0.073971204 +0.077052003 +0.080277508 +0.084794056 +0.092704686 +0.104387904 +0.117146305 +0.129892736 +0.13676594 +0.141907141 +0.190196845 +0.2260824 +0.201503743 +0.172027776 +0.138443266 +0.114045069 +0.108843222 +0.121012447 +0.110027663 +0.087590288 +0.070551535 +0.037854883 +0.036241258 +0.042437962 +0.048747357 +0.055250363 +0.05817596 +0.058897084 +0.06107344 +0.065375664 +0.06918656 +0.071467492 +0.074254177 +0.108223594 +0.182421321 +0.232999359 +0.246087363 +0.233144033 +0.187920386 +0.139558926 +0.104548243 +0.085324943 +0.079918509 +0.081458647 +0.083564736 +0.041956021 +0.011712488 +0.023548921 +0.029559221 +0.029129886 +0.025458109 +0.021310788 +0.017453996 +0.014871673 +0.013713442 +0.013478648 +0.012506423 +0.018086323 +0.029963801 +0.062642471 +0.114593064 +0.177850667 +0.224935882 +0.256521204 +0.259355153 +0.239255547 +0.203999321 +0.171919518 +0.136508427 +0.032001523 +0.002001253 +0.001584204 +0.003579003 +0.003005604 +0.002613108 +0.002579877 +0.003051488 +0.003846605 +0.004675517 +0.005618937 +0.008691387 +0.026793158 +0.08127709 +0.158734914 +0.236053912 +0.292577068 +0.303086801 +0.294090483 +0.284141131 +0.270661722 +0.251925238 +0.236270087 +0.225759319 +0.069691793 +0.010038437 +0.016991885 +0.025479742 +0.023152483 +0.021164541 +0.021205896 +0.022627751 +0.023901925 +0.023731971 +0.022376624 +0.023448451 +0.043415985 +0.094432837 +0.140662428 +0.150145616 +0.148114956 +0.139345714 +0.131976237 +0.123993548 +0.113157244 +0.106197758 +0.102078659 +0.088643902 +0.026101144 +0.001652455 +0.006413391 +0.012394596 +0.017809791 +0.020891103 +0.022057974 +0.023252972 +0.024190515 +0.024469586 +0.024357946 +0.023540461 +0.025582583 +0.039806741 +0.066489928 +0.093054129 +0.116811802 +0.130416915 +0.148195555 +0.165888026 +0.169180912 +0.163729757 +0.160463262 +0.16206107 +0.039347152 +0.003955625 +0.009818809 +0.019466886 +0.02869962 +0.042444681 +0.05792713 +0.068076702 +0.068826022 +0.066501111 +0.065285472 +0.077036641 +0.113076438 +0.150606061 +0.202616491 +0.237176264 +0.252786154 +0.258372324 +0.243431577 +0.246579721 +0.25807127 +0.248410865 +0.251542671 +0.230513942 +0.115300306 +0.122284291 +0.137075755 +0.152892814 +0.178552752 +0.223651206 +0.267588727 +0.295436109 +0.309181335 +0.31399255 +0.311464875 +0.305124615 +0.313013905 +0.455621264 +0.530303383 +0.501917739 +0.464638662 +0.429145511 +0.380574299 +0.32847573 +0.278067343 +0.241236533 +0.229374867 +0.191194632 +0.052952145 +0.034812425 +0.077491615 +0.085517121 +0.090362795 +0.096483371 +0.107585601 +0.123313143 +0.135852222 +0.13889726 +0.127848181 +0.110462537 +0.116401295 +0.112812601 +0.123342589 +0.139567307 +0.163731483 +0.196667235 +0.227802822 +0.25873155 +0.277355721 +0.287397383 +0.290926143 +0.264194269 +0.087046891 +0.0072687 +0.022294114 +0.043518421 +0.047581612 +0.051713099 +0.057637073 +0.065927294 +0.07678708 +0.092490445 +0.115370107 +0.14664204 +0.263122548 +0.423051125 +0.507010497 +0.528666594 +0.545719307 +0.53360813 +0.515317166 +0.477277714 +0.432533131 +0.405321709 +0.390956414 +0.351945108 +0.210597773 +0.155490053 +0.242081294 +0.273622122 +0.272849153 +0.275133898 +0.271407236 +0.27032469 +0.283845423 +0.305088585 +0.321912101 +0.323438017 +0.34363857 +0.376322032 +0.387118401 +0.367800755 +0.348701248 +0.33116272 +0.329810284 +0.316080628 +0.287874826 +0.27466448 +0.258085889 +0.234933108 +0.182050641 +0.197680188 +0.248696739 +0.252438241 +0.237379163 +0.2273874 +0.221043609 +0.215716856 +0.212509275 +0.209891753 +0.202451711 +0.196413226 +0.218735334 +0.329354631 +0.401614786 +0.386936381 +0.363067449 +0.311786112 +0.259988043 +0.231508745 +0.209301073 +0.186674715 +0.17227585 +0.161300644 +0.064752273 +0.041531 +0.062191292 +0.066124786 +0.058491509 +0.055169904 +0.059019033 +0.063899144 +0.069767884 +0.075020313 +0.075374105 +0.065001919 +0.066395377 +0.070185962 +0.063586734 +0.049870113 +0.039861578 +0.033157684 +0.040017991 +0.058832619 +0.073770472 +0.082769514 +0.083110997 +0.080846375 +0.035877693 +0.006847529 +0.02993223 +0.047612193 +0.06272377 +0.074978333 +0.079848132 +0.076987283 +0.07117419 +0.065372769 +0.058169336 +0.058853341 +0.081291315 +0.151999518 +0.196404859 +0.201853346 +0.181687741 +0.158040909 +0.146944106 +0.139091292 +0.135407803 +0.137152115 +0.14390588 +0.126809202 +0.039278568 +0.030726525 +0.044196104 +0.044427624 +0.042274518 +0.043258617 +0.044177438 +0.04203812 +0.038841928 +0.036055118 +0.033254269 +0.029176182 +0.038000578 +0.039969915 +0.045707853 +0.047420951 +0.04485767 +0.046471098 +0.052454323 +0.062004931 +0.068695774 +0.068401061 +0.067271834 +0.071001036 +0.053548213 +0.00675327 +0.008789921 +0.021132477 +0.024138309 +0.025897946 +0.027598978 +0.029389641 +0.030955491 +0.031273807 +0.029992015 +0.027704541 +0.043244842 +0.100066735 +0.219706481 +0.350907378 +0.454300911 +0.531150013 +0.555849516 +0.529647471 +0.484882135 +0.424003675 +0.366755752 +0.323639237 +0.189846298 +0.044035143 +0.069484028 +0.180529082 +0.224268895 +0.229899728 +0.21763246 +0.199375225 +0.182040596 +0.166039904 +0.154266591 +0.150129529 +0.273234825 +0.45390982 +0.599054719 +0.646207095 +0.632870464 +0.584946077 +0.558941032 +0.530799599 +0.511113099 +0.494245439 +0.473145375 +0.435232343 +0.236481171 +0.113458733 +0.140090732 +0.20200004 +0.26266471 +0.307930254 +0.33962471 +0.365286639 +0.38085272 +0.379716207 +0.364374357 +0.366875249 +0.464511857 +0.520940365 +0.503952467 +0.476083674 +0.43936553 +0.393986718 +0.348250854 +0.302362787 +0.261925463 +0.239617135 +0.229982517 +0.214433775 +0.166449093 +0.177667909 +0.179148204 +0.189235057 +0.207895509 +0.223421586 +0.239393822 +0.273146391 +0.315680409 +0.334028712 +0.330013472 +0.32865843 +0.338613897 +0.433833898 +0.471171732 +0.486790567 +0.480048899 +0.43847027 +0.375757876 +0.331612122 +0.301974754 +0.279527557 +0.260533958 +0.220936143 +0.09259226 +0.113455767 +0.125287762 +0.12608379 +0.125566519 +0.13199076 +0.141059905 +0.147475083 +0.151083639 +0.150670633 +0.142949264 +0.122231914 +0.138736784 +0.148865674 +0.168280557 +0.175421143 +0.168778019 +0.163263776 +0.153984101 +0.141491696 +0.134168479 +0.136393682 +0.1358009 +0.131715174 +0.075702497 +0.006744969 +0.005469663 +0.004024101 +0.002873582 +0.003101095 +0.003966318 +0.005770614 +0.008109684 +0.010439009 +0.01165069 +0.010671618 +0.009017814 +0.016117155 +0.051029065 +0.130808879 +0.228492471 +0.311597559 +0.33163039 +0.321255337 +0.29895259 +0.27461725 +0.256462165 +0.235674661 +0.16331856 +0.027863938 +0.014876094 +0.052898464 +0.058328788 +0.053433648 +0.045166855 +0.036831254 +0.029711217 +0.024551167 +0.021401385 +0.024848307 +0.087767426 +0.232790782 +0.363891403 +0.398788796 +0.404224742 +0.410404835 +0.383902307 +0.34198474 +0.291471947 +0.243484498 +0.207162972 +0.182280766 +0.150614521 +0.027105096 +0.005390324 +0.018216563 +0.027333389 +0.032217386 +0.046508929 +0.068657745 +0.093683621 +0.114589995 +0.126669046 +0.145781814 +0.22943212 +0.373886682 +0.446433516 +0.424628311 +0.363809099 +0.288842273 +0.222551832 +0.180111879 +0.153403999 +0.130175559 +0.112018502 +0.085087818 +0.052529991 +0.023055347 +0.01852433 +0.027219496 +0.048073792 +0.067043321 +0.079046368 +0.087932989 +0.094192666 +0.111365244 +0.136665715 +0.184805137 +0.33725273 +0.443106842 +0.446944189 +0.428392166 +0.396678784 +0.373534425 +0.363053845 +0.34433686 +0.329369717 +0.299565494 +0.274937054 +0.2461209 +0.158893305 +0.10483276 +0.126015898 +0.143958799 +0.153548226 +0.171694723 +0.196256316 +0.22059801 +0.23474448 +0.233245787 +0.225154185 +0.214870175 +0.216629883 +0.239577319 +0.24005011 +0.222647894 +0.213465382 +0.212413437 +0.199641029 +0.185067964 +0.18607001 +0.181964622 +0.183457191 +0.167674083 +0.095128786 +0.054519496 +0.06651841 +0.088491325 +0.092174021 +0.082148945 +0.074203707 +0.079388543 +0.099813344 +0.119865816 +0.125817932 +0.162431795 +0.226219672 +0.270197517 +0.27974069 +0.275834212 +0.274665002 +0.282576558 +0.289248914 +0.307936938 +0.325761543 +0.351295924 +0.371364426 +0.381464244 +0.392862424 +0.392101502 +0.412191662 +0.445460811 +0.475241027 +0.524715079 +0.576242085 +0.621355406 +0.658207764 +0.685846814 +0.691607733 +0.661413455 +0.650561341 +0.678191781 +0.638463999 +0.57760766 +0.525081732 +0.503346948 +0.497721111 +0.482696652 +0.468506337 +0.459026213 +0.446770618 +0.419576698 +0.306397781 +0.203817561 +0.287407934 +0.315586573 +0.320458468 +0.367775361 +0.430987829 +0.475413735 +0.502436412 +0.50453766 +0.467600505 +0.397959988 +0.422877669 +0.429003076 +0.393685487 +0.347618276 +0.309075053 +0.283645407 +0.265252138 +0.250300899 +0.255234385 +0.274095254 +0.287765173 +0.29526935 +0.212990247 +0.038346741 +0.0501265 +0.147478402 +0.175802006 +0.202180214 +0.222984286 +0.233017612 +0.228551289 +0.204606786 +0.161600809 +0.112305865 +0.074032303 +0.047455408 +0.050704776 +0.071414338 +0.114291465 +0.173472712 +0.218574752 +0.242306339 +0.261051996 +0.273215336 +0.289013497 +0.300456484 +0.287683494 +0.095658256 +0.054863528 +0.164966842 +0.271865912 +0.314413999 +0.338855155 +0.351350177 +0.349845045 +0.328755175 +0.307266761 +0.423500988 +0.604527666 +0.688850652 +0.681725964 +0.643722558 +0.599505897 +0.528982924 +0.472376978 +0.402708939 +0.336187203 +0.269969582 +0.213708097 +0.173140646 +0.121035341 +0.054504884 +0.041763034 +0.032894582 +0.024445216 +0.017287746 +0.012750201 +0.011116815 +0.010089869 +0.009676769 +0.013094163 +0.039972509 +0.16555997 +0.354619038 +0.445963155 +0.460850974 +0.447712961 +0.417481947 +0.395580768 +0.365094001 +0.320051177 +0.271712703 +0.237831921 +0.229530404 +0.206439453 +0.090862758 +0.100941404 +0.16349901 +0.231745458 +0.301347714 +0.363891449 +0.408873215 +0.431863788 +0.433184627 +0.423675756 +0.430670651 +0.463338066 +0.46663926 +0.437703739 +0.386945273 +0.317836193 +0.279037327 +0.251221071 +0.218899845 +0.193347135 +0.174720543 +0.185735032 +0.181892446 +0.159069081 +0.156683233 +0.208621152 +0.23509189 +0.258743009 +0.297691538 +0.344131756 +0.385071222 +0.421909979 +0.450780553 +0.442770657 +0.408375933 +0.4660278 +0.543825388 +0.590712601 +0.619392898 +0.630497226 +0.612379852 +0.605406311 +0.612225965 +0.622250411 +0.623642378 +0.628037131 +0.616005366 +0.581467724 +0.521821149 +0.62168564 +0.68882276 +0.721886408 +0.757574418 +0.785553864 +0.793428335 +0.790415003 +0.773299341 +0.734537203 +0.678504442 +0.65788027 +0.687125225 +0.674769015 +0.662127825 +0.634358867 +0.575291056 +0.522574135 +0.473328863 +0.416672988 +0.377381684 +0.339856562 +0.31519261 +0.280240179 +0.215286257 +0.28261512 +0.329068972 +0.353925131 +0.356209429 +0.346352704 +0.330232943 +0.303359446 +0.262524684 +0.211676451 +0.197741442 +0.232218097 +0.243038177 +0.280833393 +0.311689963 +0.336096465 +0.333874865 +0.314873325 +0.310900969 +0.303055557 +0.279556468 +0.250422817 +0.223709104 +0.192642433 +0.042441913 +0.004161334 +0.015371063 +0.026864056 +0.037948434 +0.051230984 +0.06071008 +0.063784835 +0.061482492 +0.054868929 +0.053578041 +0.043199945 +0.042933639 +0.042377329 +0.042044663 +0.043806872 +0.041858027 +0.038797779 +0.038269037 +0.043424963 +0.052733238 +0.062350747 +0.065593332 +0.068777466 +0.046471089 +0.00362445 +0.001873611 +0.005343263 +0.006567409 +0.006573396 +0.006796461 +0.006604826 +0.005875066 +0.005632477 +0.012146794 +0.034717656 +0.086610424 +0.149893668 +0.205816196 +0.253780741 +0.278098522 +0.274749342 +0.261218991 +0.247606719 +0.246231686 +0.253130578 +0.283491753 +0.317056277 +0.224334002 +0.135907677 +0.134605725 +0.19460838 +0.20447174 +0.198927841 +0.186991726 +0.168605862 +0.152222866 +0.164891732 +0.262180703 +0.337992024 +0.386693781 +0.397865585 +0.391261779 +0.360131506 +0.310908542 +0.264859833 +0.234592006 +0.206168459 +0.191877965 +0.195199738 +0.201588935 +0.191334094 +0.095003946 +0.055048457 +0.087091442 +0.123333451 +0.128222001 +0.117158004 +0.099932217 +0.081093512 +0.070189341 +0.067195188 +0.080281728 +0.121632582 +0.139771726 +0.152895836 +0.167440168 +0.176197601 +0.175496093 +0.173675045 +0.168186884 +0.176587535 +0.191130482 +0.201927236 +0.191769845 +0.181040331 +0.109090843 +0.054733744 +0.165792125 +0.240100774 +0.267293855 +0.269923642 +0.257456311 +0.237695447 +0.201958248 +0.162796464 +0.222543039 +0.354508461 +0.417460351 +0.405959702 +0.396524321 +0.384325312 +0.380261446 +0.378277602 +0.386090761 +0.405274815 +0.411466249 +0.41541526 +0.419201242 +0.420416756 +0.328930329 +0.352915007 +0.444642064 +0.5271847 +0.586452865 +0.62604014 +0.631104665 +0.615631926 +0.573011814 +0.512182631 +0.437055429 +0.426654817 +0.430710302 +0.409895151 +0.381803181 +0.360806465 +0.32524767 +0.296369693 +0.273144869 +0.259794339 +0.250446748 +0.22721653 +0.205480908 +0.179857411 +0.044848177 +0.034959659 +0.067884024 +0.081794691 +0.103187273 +0.127698429 +0.146198499 +0.15516786 +0.153879437 +0.143566649 +0.128386954 +0.154779178 +0.223442177 +0.309229103 +0.394441668 +0.455129484 +0.483413926 +0.500863653 +0.473880526 +0.426610542 +0.354786533 +0.294746842 +0.250483762 +0.212052963 +0.073957296 +0.042990836 +0.073578318 +0.111692449 +0.141773396 +0.170498499 +0.204940983 +0.233565712 +0.247305943 +0.239566755 +0.2568891 +0.381194878 +0.433164685 +0.395009558 +0.341245398 +0.299987963 +0.267183931 +0.226154129 +0.196013448 +0.172747914 +0.157691617 +0.150883422 +0.140877792 +0.129851143 +0.042378001 +0.037299797 +0.066310318 +0.086567683 +0.104568259 +0.126315213 +0.14905376 +0.169901815 +0.184473191 +0.185624155 +0.237262483 +0.321383683 +0.353517092 +0.356389134 +0.350495254 +0.360693066 +0.359871248 +0.362163074 +0.362168125 +0.368877032 +0.386648827 +0.397826393 +0.400282149 +0.374784665 +0.156983725 +0.070338943 +0.204595658 +0.333145524 +0.351363422 +0.361127189 +0.364563403 +0.353684695 +0.322843173 +0.281892816 +0.39044596 +0.54058601 +0.606090693 +0.604418773 +0.603749636 +0.590816199 +0.564103242 +0.530279291 +0.492225964 +0.458171491 +0.431426465 +0.402772835 +0.395511115 +0.383919864 +0.18572398 +0.068864316 +0.135704782 +0.244003297 +0.288947754 +0.310824802 +0.332804154 +0.354443936 +0.372691518 +0.387371487 +0.434847505 +0.50365026 +0.49131282 +0.470092432 +0.465661293 +0.450959398 +0.383414553 +0.320300233 +0.270020092 +0.238062329 +0.214152799 +0.197400279 +0.196628525 +0.186937908 +0.148277867 +0.191678915 +0.170518659 +0.152035064 +0.162032394 +0.194103332 +0.235730983 +0.277855775 +0.30431946 +0.308870115 +0.291767832 +0.364334209 +0.38413256 +0.376173351 +0.373275587 +0.367045845 +0.335445363 +0.298184296 +0.261496937 +0.231868507 +0.211635195 +0.193090894 +0.179629435 +0.155097339 +0.097400997 +0.141163266 +0.158723353 +0.167991627 +0.15789229 +0.143954081 +0.137342001 +0.135806661 +0.136815685 +0.137102373 +0.159566157 +0.238773501 +0.281906639 +0.269511505 +0.252402543 +0.229756185 +0.191942403 +0.147935799 +0.117127139 +0.098995971 +0.089161432 +0.085680929 +0.079766849 +0.070988608 +0.024475882 +0.033680156 +0.045024287 +0.051243795 +0.058090653 +0.064972365 +0.070610653 +0.072827566 +0.071681988 +0.066529018 +0.082538451 +0.089174662 +0.120370247 +0.144232233 +0.161022448 +0.171594464 +0.168648614 +0.160276399 +0.146482461 +0.126403199 +0.103223968 +0.082986745 +0.070695725 +0.062799777 +0.024531295 +0.004273138 +0.010788661 +0.014061871 +0.016731301 +0.018269266 +0.019310146 +0.019954714 +0.020455557 +0.022417837 +0.024405837 +0.023216401 +0.02555155 +0.028345305 +0.030987303 +0.032474331 +0.040247701 +0.053412458 +0.062961109 +0.067896418 +0.071360683 +0.072349498 +0.065094199 +0.053995714 +0.027554893 +0.002800759 +0.001762456 +0.001892937 +0.002220693 +0.002937823 +0.00483542 +0.00853572 +0.013632691 +0.023941922 +0.0349976 +0.038984573 +0.042801487 +0.045425411 +0.045793683 +0.044614305 +0.049780513 +0.061695339 +0.076476664 +0.091365918 +0.10542653 +0.116099144 +0.118860093 +0.1166992 +0.06226502 +0.009431631 +0.01281222 +0.01161383 +0.00820651 +0.006777237 +0.007975618 +0.011197811 +0.015411306 +0.023500023 +0.041783763 +0.051175386 +0.05657873 +0.055638827 +0.052196464 +0.050417266 +0.053665772 +0.059816492 +0.074740839 +0.095194937 +0.115979779 +0.127562568 +0.139821775 +0.155253649 +0.094054184 +0.028076507 +0.048822737 +0.046504367 +0.038247701 +0.031192125 +0.027369129 +0.028048405 +0.03280102 +0.045039704 +0.099341211 +0.13378344 +0.146634187 +0.125676607 +0.09269725 +0.068507052 +0.053617603 +0.046793625 +0.044521417 +0.045054497 +0.048329645 +0.054269228 +0.059842912 +0.064080956 +0.042108224 +0.00295147 +0.008234397 +0.01302689 +0.016015671 +0.020535333 +0.027583292 +0.036511931 +0.045086423 +0.052566062 +0.062591626 +0.058447643 +0.063101508 +0.06944083 +0.075609529 +0.085399621 +0.089739249 +0.091356155 +0.100506512 +0.112800032 +0.127209627 +0.130932455 +0.131635266 +0.125137086 +0.100872998 +0.023584564 +0.026174151 +0.060513741 +0.091494658 +0.130953857 +0.16693034 +0.18555375 +0.173134241 +0.238948956 +0.328441538 +0.402764076 +0.475995565 +0.502083918 +0.512178766 +0.502385067 +0.452136033 +0.388888416 +0.344197042 +0.301738349 +0.254604267 +0.214037141 +0.209162359 +0.212573834 +0.156208536 +0.057203548 +0.084526591 +0.099955124 +0.113578676 +0.147037525 +0.190182959 +0.231628016 +0.254045261 +0.233399657 +0.24228387 +0.328577822 +0.32783109 +0.282819781 +0.232320712 +0.196823955 +0.168195862 +0.145812598 +0.129544582 +0.119693608 +0.114905301 +0.102680568 +0.08967703 +0.086719378 +0.04849155 +0.020809635 +0.021225737 +0.01816042 +0.020203369 +0.026075003 +0.034416084 +0.045278295 +0.05645157 +0.065882517 +0.097217399 +0.13335369 +0.159151133 +0.174243182 +0.18314437 +0.192946714 +0.19903516 +0.208547632 +0.204357663 +0.195729278 +0.179841173 +0.149395582 +0.126914031 +0.119598762 +0.07356662 +0.026547311 +0.024094131 +0.02407582 +0.019837783 +0.015953927 +0.01344288 +0.01108463 +0.009177829 +0.015079329 +0.02557314 +0.03517448 +0.038262436 +0.043217372 +0.044816724 +0.046481205 +0.0470021 +0.050535033 +0.05572984 +0.056645483 +0.053109192 +0.046670486 +0.042254477 +0.042062289 +0.04187289 +0.024786124 +0.030872549 +0.058683337 +0.096545134 +0.127353789 +0.145910077 +0.148121175 +0.130842364 +0.131240197 +0.172986968 +0.209547633 +0.214035167 +0.224034897 +0.225044201 +0.224740447 +0.193060893 +0.163301213 +0.144564721 +0.134877506 +0.125977389 +0.112702525 +0.101831874 +0.089611094 +0.06124719 +0.040617129 +0.046694891 +0.060077702 +0.074397027 +0.085059249 +0.096569571 +0.110878237 +0.117044416 +0.132670719 +0.176959983 +0.206070671 +0.223107967 +0.243628409 +0.265676561 +0.31415066 +0.346685044 +0.395975538 +0.467564923 +0.526640419 +0.574242542 +0.608127736 +0.632706962 +0.656634899 +0.660577574 +0.702426591 +0.76555696 +0.793474122 +0.815919825 +0.83247269 +0.836115583 +0.828923621 +0.804922609 +0.758788113 +0.687662924 +0.640628086 +0.609853027 +0.589024586 +0.574845749 +0.559581676 +0.505754549 +0.436564857 +0.391138745 +0.34149265 +0.295525912 +0.260304056 +0.235876219 +0.215651694 +0.167558952 +0.242649331 +0.246371533 +0.2272917 +0.220753122 +0.220269803 +0.22570364 +0.227900157 +0.221250059 +0.172019248 +0.199502288 +0.3032018 +0.362722752 +0.39454836 +0.415925724 +0.41964546 +0.38664499 +0.327369529 +0.289884694 +0.251674224 +0.219747526 +0.191423936 +0.172167282 +0.167223831 +0.134435351 +0.096488917 +0.103115701 +0.13644137 +0.169470059 +0.203056936 +0.223416613 +0.215943781 +0.193622379 +0.205706944 +0.2711891 +0.341530595 +0.398210204 +0.426439602 +0.431016465 +0.421596963 +0.387253196 +0.347048553 +0.307168182 +0.27234212 +0.248684273 +0.232957987 +0.216474951 +0.187606199 +0.149687266 +0.06560076 +0.064330974 +0.11992549 +0.188359379 +0.246808366 +0.292629028 +0.32211215 +0.332416663 +0.359530571 +0.380579937 +0.399529214 +0.403569559 +0.39189082 +0.374970827 +0.356108282 +0.321410655 +0.292863785 +0.261780604 +0.240253665 +0.214499822 +0.192052056 +0.187166811 +0.190778261 +0.168582975 +0.173990787 +0.22586712 +0.232478694 +0.229093616 +0.226638043 +0.224599739 +0.213745395 +0.191101725 +0.14775978 +0.116522966 +0.088673555 +0.075337674 +0.086347792 +0.104946667 +0.122732425 +0.132390127 +0.139475167 +0.140826385 +0.128606787 +0.121409818 +0.11724241 +0.116209254 +0.102828995 +0.084529287 +0.043087439 +0.050699745 +0.070118269 +0.076509507 +0.08512417 +0.09493976 +0.099284626 +0.094817969 +0.102069294 +0.128263494 +0.168589776 +0.195562508 +0.201991476 +0.218351578 +0.239186301 +0.259729153 +0.262126471 +0.254177331 +0.254292578 +0.270080645 +0.292246399 +0.3418458 +0.380187698 +0.369795398 +0.288254475 +0.314387597 +0.45926813 +0.569324074 +0.625226131 +0.654630443 +0.653727206 +0.649930438 +0.633264689 +0.671520184 +0.674345747 +0.648155112 +0.598031792 +0.548923692 +0.516588476 +0.460539361 +0.377600335 +0.303507525 +0.227149745 +0.153731947 +0.109402763 +0.078832053 +0.061892327 +0.046176964 +0.025950033 +0.023912916 +0.029545793 +0.040478876 +0.052676955 +0.062072882 +0.059649038 +0.057923066 +0.07471377 +0.118913791 +0.159405205 +0.175306785 +0.182201767 +0.200571544 +0.232362814 +0.281506357 +0.334492361 +0.409977064 +0.46867653 +0.518543191 +0.55853464 +0.59060639 +0.609513118 +0.640363417 +0.68792916 +0.721377919 +0.733019681 +0.752598442 +0.761938987 +0.775538678 +0.785594635 +0.784271093 +0.758763966 +0.708079682 +0.669753914 +0.656958568 +0.645668361 +0.62677835 +0.600382793 +0.565029207 +0.530766497 +0.490225558 +0.451749699 +0.420343577 +0.379566599 +0.335156199 +0.290928185 +0.243885613 +0.331001824 +0.375460353 +0.35212919 +0.354098028 +0.371407864 +0.386290087 +0.390330039 +0.381829409 +0.345914915 +0.304313681 +0.273478735 +0.226226659 +0.200363797 +0.192424151 +0.182017175 +0.16510861 +0.154426264 +0.145718103 +0.131936472 +0.117123546 +0.103738122 +0.093686491 +0.086911408 +0.068773673 +0.033533692 +0.03487322 +0.034909097 +0.034878494 +0.037520113 +0.042642849 +0.04866857 +0.052861659 +0.055316022 +0.097726122 +0.166903913 +0.236681368 +0.292717128 +0.308891359 +0.313954014 +0.306382449 +0.285377825 +0.268390824 +0.254673224 +0.244623376 +0.232850751 +0.222626433 +0.217772699 +0.204835765 +0.104773793 +0.056675528 +0.078835044 +0.106206048 +0.147571589 +0.205792137 +0.252721551 +0.277272757 +0.341109591 +0.547463921 +0.598883762 +0.592497927 +0.590065002 +0.582035653 +0.567685447 +0.538004334 +0.49005025 +0.437777836 +0.390133416 +0.358714829 +0.321539888 +0.303407378 +0.303071683 +0.281311659 +0.186617115 +0.171217597 +0.322832648 +0.350080964 +0.348071952 +0.339231916 +0.309632953 +0.244507196 +0.170179112 +0.134676456 +0.12589751 +0.122579423 +0.126093082 +0.128382703 +0.131439226 +0.134683999 +0.125578298 +0.106079288 +0.092143956 +0.087374778 +0.089637159 +0.087808855 +0.090115825 +0.096207202 +0.06522873 +0.035561019 +0.062383327 +0.126736837 +0.175510577 +0.215271345 +0.218893269 +0.194749549 +0.24439076 +0.367191681 +0.452042194 +0.511169009 +0.537183884 +0.53413025 +0.510227201 +0.48538715 +0.457939776 +0.464704935 +0.490039458 +0.514826844 +0.579192899 +0.648238599 +0.723921413 +0.769768526 +0.796098005 +0.828491276 +0.833187196 +0.825910561 +0.815019312 +0.801183339 +0.779428243 +0.745523078 +0.694328082 +0.646946024 +0.618544075 +0.568921481 +0.51005587 +0.45077127 +0.388718381 +0.31840839 +0.262695037 +0.229542071 +0.213274053 +0.202737762 +0.190786172 +0.175911946 +0.162439694 +0.146512997 +0.097381495 +0.066234129 +0.06820357 +0.070689849 +0.074830456 +0.079073824 +0.084283927 +0.087724428 +0.097258686 +0.16506933 +0.244977456 +0.288158494 +0.289792083 +0.274397779 +0.26050094 +0.258248071 +0.246286257 +0.228934521 +0.209113617 +0.194323326 +0.182547531 +0.170180713 +0.158087209 +0.14494382 +0.107863458 +0.054165875 +0.040763872 +0.065560684 +0.098382143 +0.123033564 +0.142274884 +0.159819334 +0.303012276 +0.488251442 +0.556261916 +0.558454224 +0.524063925 +0.464341537 +0.404171985 +0.360788105 +0.323928826 +0.294437946 +0.268223296 +0.235718301 +0.200649154 +0.181860374 +0.192981161 +0.202570977 +0.17324433 +0.13317432 +0.17598804 +0.21708526 +0.250545936 +0.285535617 +0.316245896 +0.339390274 +0.384074226 +0.429737222 +0.460492526 +0.480882637 +0.507535098 +0.520655494 +0.534109316 +0.523522325 +0.484875357 +0.445771105 +0.406341383 +0.361906292 +0.325202706 +0.29794838 +0.274580577 +0.246874566 +0.152258945 +0.205467297 +0.200177386 +0.184610848 +0.203476883 +0.228368686 +0.24165497 +0.23895596 +0.168195451 +0.148341765 +0.145384356 +0.156470377 +0.175051372 +0.193479732 +0.203583449 +0.198461213 +0.188027648 +0.177642664 +0.164886659 +0.154097978 +0.146584935 +0.138549698 +0.13658599 +0.139967203 +0.090933234 +0.034489598 +0.05000917 +0.109599709 +0.135192158 +0.140453785 +0.139688012 +0.129041812 +0.143281491 +0.222288451 +0.308886393 +0.343911021 +0.35606416 +0.353476691 +0.348574677 +0.339866857 +0.322600602 +0.293660117 +0.257928022 +0.220071674 +0.185580379 +0.151592305 +0.127374321 +0.107936487 +0.06136444 +0.018737147 +0.020137188 +0.033810964 +0.052368029 +0.069012569 +0.08271988 +0.089342753 +0.104376684 +0.173215955 +0.231033166 +0.254600467 +0.296864956 +0.329506233 +0.354101031 +0.362977153 +0.351508754 +0.334233991 +0.317579308 +0.305123642 +0.296984483 +0.301496512 +0.316248791 +0.334787846 +0.239751307 +0.153315634 +0.287450769 +0.528255411 +0.60604527 +0.660707592 +0.675420612 +0.644938122 +0.639968183 +0.70843674 +0.737301755 +0.72946505 +0.705669425 +0.673475961 +0.642436981 +0.566519501 +0.462417175 +0.406068152 +0.363926987 +0.311295881 +0.275039722 +0.268442931 +0.27709824 +0.282353742 +0.231113026 +0.208181483 +0.2902418 +0.423046182 +0.496148971 +0.53972399 +0.552105596 +0.5478766 +0.576713372 +0.671865899 +0.715874187 +0.723253202 +0.702069619 +0.659711227 +0.599916939 +0.508236165 +0.418235093 +0.349514791 +0.290090569 +0.253063749 +0.237130627 +0.231262327 +0.236197268 +0.234813251 +0.193668757 +0.088422532 +0.10617514 +0.175648533 +0.227288031 +0.260755448 +0.256595406 +0.228459515 +0.322814274 +0.443209976 +0.504984794 +0.54308227 +0.561092336 +0.567510969 +0.577024296 +0.540055303 +0.500084822 +0.467758526 +0.439982788 +0.395975148 +0.350752391 +0.356135376 +0.368526351 +0.35629653 +0.293455541 +0.214002424 +0.321634617 +0.481933634 +0.569767455 +0.624343101 +0.660427477 +0.682599946 +0.713660611 +0.757461361 +0.782076167 +0.7764602 +0.757484775 +0.722412113 +0.681179942 +0.631813243 +0.596372798 +0.559340202 +0.529458387 +0.496092254 +0.467073901 +0.44908405 +0.429024276 +0.402011648 +0.324990399 +0.307999787 +0.312487741 +0.281742428 +0.283681269 +0.307642978 +0.334457313 +0.341276233 +0.260669636 +0.297841471 +0.330155472 +0.340348553 +0.326650473 +0.304010109 +0.281517774 +0.244743648 +0.216348888 +0.211173026 +0.208281688 +0.201757873 +0.193045833 +0.190411277 +0.201561879 +0.214448837 +0.159056394 +0.080020824 +0.134893468 +0.214948259 +0.230235183 +0.244776129 +0.257650784 +0.253528383 +0.30477495 +0.408481013 +0.485594207 +0.53670467 +0.561569378 +0.562867342 +0.564303259 +0.538929547 +0.501899106 +0.472709089 +0.448835256 +0.427070305 +0.405148547 +0.397725124 +0.393958673 +0.385982332 +0.290354203 +0.12135576 +0.090717877 +0.150445002 +0.16899198 +0.177460716 +0.179008892 +0.193793158 +0.337764087 +0.45466735 +0.499950157 +0.522745542 +0.537015821 +0.536985765 +0.526012613 +0.506201362 +0.474153883 +0.435486978 +0.405868407 +0.387950645 +0.374154438 +0.376032865 +0.383358632 +0.387034665 +0.323680456 +0.154218622 +0.101371513 +0.121416758 +0.147167678 +0.162067004 +0.155206949 +0.200230324 +0.353189449 +0.461095347 +0.509700001 +0.543878704 +0.566124956 +0.580440726 +0.59008778 +0.583254654 +0.5553925 +0.533352147 +0.496122889 +0.469287951 +0.444304089 +0.43964539 +0.44709493 +0.437792987 +0.375538368 +0.200480488 +0.104324045 +0.100426007 +0.153381623 +0.193738168 +0.217760144 +0.298015698 +0.44676021 +0.577838253 +0.609746969 +0.617464142 +0.624654922 +0.6344741 +0.651086162 +0.620408601 +0.573880454 +0.555176449 +0.539119094 +0.507945584 +0.481474683 +0.476815527 +0.461141497 +0.427222556 +0.354887808 +0.286872489 +0.344867066 +0.342903282 +0.36544754 +0.404201958 +0.440425544 +0.456628146 +0.470135248 +0.517948407 +0.564935141 +0.589187992 +0.59856036 +0.603686316 +0.613069675 +0.581905321 +0.504969601 +0.448708412 +0.41142075 +0.420828162 +0.454168898 +0.532161898 +0.592915159 +0.640370193 +0.667800772 +0.752721358 +0.810469675 +0.820300561 +0.818685487 +0.814257098 +0.804780308 +0.786831795 +0.753529308 +0.729661071 +0.696855948 +0.640005386 +0.580891313 +0.523441727 +0.466625387 +0.414546517 +0.371527134 +0.340968937 +0.328824247 +0.323586771 +0.320685144 +0.344259523 +0.38327842 +0.402990589 +0.388112512 +0.401200822 +0.514032153 +0.572314837 +0.597975332 +0.61686595 +0.626957172 +0.622224398 +0.637758578 +0.677690005 +0.70807448 +0.727184703 +0.716691864 +0.697219033 +0.66352821 +0.602030475 +0.538831667 +0.486292684 +0.433532797 +0.391674668 +0.360495592 +0.350233581 +0.355341575 +0.362766732 +0.299288472 +0.13727074 +0.147348245 +0.186621455 +0.184862487 +0.176091629 +0.171282907 +0.151915021 +0.164335977 +0.242674426 +0.3318687 +0.393114531 +0.439908543 +0.456329203 +0.446062414 +0.418079561 +0.386960329 +0.363552384 +0.3429805 +0.326440169 +0.308130552 +0.290449751 +0.278328549 +0.264365615 +0.208330241 +0.074321601 +0.0432832 +0.048788048 +0.05274156 +0.047686258 +0.044913623 +0.04790007 +0.060368989 +0.069748908 +0.082356647 +0.096146312 +0.11092695 +0.127561048 +0.145451745 +0.167894341 +0.188556895 +0.198992364 +0.201767851 +0.202594526 +0.197181141 +0.182357833 +0.167731486 +0.154166919 +0.132706521 +0.040424513 +0.010946222 +0.013284916 +0.016705824 +0.021064611 +0.022307313 +0.026203369 +0.034543641 +0.038133036 +0.042099514 +0.045162469 +0.047565989 +0.050328446 +0.056768266 +0.071026302 +0.087947938 +0.099224642 +0.106345539 +0.106074685 +0.096152687 +0.081616179 +0.07415083 +0.076123206 +0.072560056 +0.039207252 +0.018319744 +0.020715312 +0.030101087 +0.034877676 +0.035774869 +0.033549251 +0.03092533 +0.030438603 +0.032961941 +0.036981848 +0.041178725 +0.046491019 +0.054154654 +0.063036763 +0.065879771 +0.061515268 +0.053078639 +0.045855675 +0.044319388 +0.052078922 +0.063876647 +0.073687539 +0.071623939 +0.037046903 +0.037285334 +0.05309473 +0.076154176 +0.100643075 +0.120605415 +0.125714682 +0.147477527 +0.206646214 +0.243344255 +0.265777524 +0.287482735 +0.297770485 +0.293456116 +0.281291042 +0.247854314 +0.206963545 +0.161058478 +0.128307792 +0.106683546 +0.094144912 +0.096522046 +0.102418835 +0.098171777 +0.056466617 +0.03710054 +0.055791179 +0.082076314 +0.091144322 +0.088440668 +0.077473908 +0.092867825 +0.127104129 +0.137574992 +0.130348246 +0.110923148 +0.091128626 +0.080413828 +0.074735924 +0.078482281 +0.094847685 +0.111948016 +0.120007271 +0.12603151 +0.13096643 +0.127982461 +0.117790334 +0.111837064 +0.087304358 +0.07873236 +0.092473205 +0.097314408 +0.100736439 +0.102245083 +0.122094629 +0.218265136 +0.285564155 +0.280746151 +0.258303418 +0.220490851 +0.177919012 +0.142484425 +0.11060675 +0.093655133 +0.087795084 +0.084651993 +0.083338593 +0.083778206 +0.087124392 +0.092308142 +0.098907387 +0.106741216 +0.100340601 +0.10074182 +0.103893321 +0.122013364 +0.157712042 +0.200697011 +0.217034541 +0.250371969 +0.275727537 +0.267781404 +0.254368707 +0.258748324 +0.267860506 +0.277016729 +0.293653196 +0.302567301 +0.297232034 +0.280309433 +0.269811633 +0.252587517 +0.244026638 +0.248515802 +0.25584791 +0.236954284 +0.140766581 +0.133936636 +0.213984474 +0.269118594 +0.291425698 +0.292524656 +0.245928034 +0.260340934 +0.264135807 +0.262985716 +0.265133623 +0.268033397 +0.27118342 +0.271294816 +0.272209622 +0.250911521 +0.224858566 +0.203431535 +0.189172468 +0.176966546 +0.154401406 +0.132634656 +0.116801432 +0.107265159 +0.059844476 +0.023869983 +0.017350187 +0.024487079 +0.027357686 +0.027325107 +0.031098913 +0.052816789 +0.083912745 +0.107320428 +0.108859123 +0.100322792 +0.095700855 +0.102938349 +0.11791474 +0.134880856 +0.153518246 +0.171315328 +0.183116842 +0.202328877 +0.226457202 +0.247633713 +0.271943105 +0.280988961 +0.246038758 +0.219894127 +0.236474414 +0.290636432 +0.311275995 +0.292313892 +0.297922286 +0.35943248 +0.401530734 +0.439633176 +0.461744597 +0.483715687 +0.484570504 +0.461690633 +0.429873465 +0.401303827 +0.389496554 +0.4050408 +0.429704514 +0.448700092 +0.430011114 +0.396647174 +0.372725901 +0.351736272 +0.273663294 +0.188161318 +0.210508026 +0.25649361 +0.262071587 +0.246827767 +0.313914787 +0.447175854 +0.510847754 +0.518087518 +0.50751139 +0.488909862 +0.458687656 +0.431046486 +0.411481209 +0.380739599 +0.339967779 +0.289732814 +0.249181684 +0.218789381 +0.196231896 +0.177357878 +0.165151007 +0.162564031 +0.137393915 +0.080953605 +0.067922338 +0.08940542 +0.127798216 +0.172675067 +0.256594319 +0.35317354 +0.384939686 +0.381844639 +0.37604491 +0.390429358 +0.414248811 +0.436846443 +0.471547934 +0.505983203 +0.539229199 +0.556681484 +0.576651929 +0.603820056 +0.631386885 +0.651686603 +0.664317282 +0.676846275 +0.67212326 +0.675437038 +0.679827805 +0.699346023 +0.712934755 +0.702130959 +0.669624247 +0.608334579 +0.590661591 +0.578506521 +0.552887662 +0.520558296 +0.490341015 +0.461721863 +0.3741049 +0.280202489 +0.239672506 +0.215572746 +0.198527602 +0.182969403 +0.166643451 +0.152765877 +0.140130711 +0.118149163 +0.052126108 +0.037128796 +0.047090345 +0.048699873 +0.046181011 +0.045671912 +0.051089122 +0.130155381 +0.265604326 +0.352553557 +0.384446111 +0.35980762 +0.314525198 +0.266950597 +0.225758187 +0.196101579 +0.176687693 +0.163815102 +0.151574189 +0.140606668 +0.132973551 +0.129461628 +0.121154696 +0.107651265 +0.061580409 +0.024209419 +0.01031782 +0.005795966 +0.002770749 +0.002079219 +0.006505704 +0.017684308 +0.022342762 +0.01943443 +0.019629838 +0.023025675 +0.028864598 +0.036418203 +0.043418581 +0.053759622 +0.069245459 +0.084767988 +0.095593451 +0.101819908 +0.103481634 +0.099049547 +0.093047952 +0.077975762 +0.053259768 +0.049925195 +0.050319035 +0.056189648 +0.06967191 +0.090862169 +0.107082983 +0.149640705 +0.188081859 +0.197654236 +0.194560066 +0.188868266 +0.185957723 +0.180658775 +0.173366378 +0.179318759 +0.179563621 +0.180556453 +0.191583664 +0.20582285 +0.223380818 +0.247944749 +0.273151251 +0.290938722 +0.257660166 +0.253555311 +0.260894662 +0.258203189 +0.270181165 +0.286980751 +0.285148355 +0.302571622 +0.303046175 +0.293196077 +0.272190062 +0.247686969 +0.229801172 +0.220629636 +0.205988318 +0.195258235 +0.187867472 +0.184771124 +0.181364777 +0.170082094 +0.16101194 +0.15221321 +0.142542413 +0.130800726 +0.066012011 +0.050025925 +0.072915284 +0.095213664 +0.113831662 +0.125906261 +0.124402315 +0.163494253 +0.178494728 +0.173090798 +0.169052204 +0.169107597 +0.172048684 +0.165882846 +0.161981653 +0.150457526 +0.132380661 +0.11714397 +0.1015294 +0.09244584 +0.097905327 +0.115964344 +0.140368782 +0.156288518 +0.140384079 +0.177064944 +0.192894593 +0.212668466 +0.237512434 +0.27057562 +0.286124825 +0.370916839 +0.41972755 +0.434673128 +0.436781576 +0.435308675 +0.432717428 +0.429509062 +0.425643044 +0.411177229 +0.400104428 +0.384017347 +0.388438846 +0.399024655 +0.410882902 +0.418954173 +0.423751971 +0.419953438 +0.406721664 +0.385105679 +0.399680047 +0.416627463 +0.431059441 +0.439918654 +0.450948383 +0.497299373 +0.51604885 +0.510582351 +0.495314919 +0.473339747 +0.452440073 +0.426554199 +0.362022174 +0.299633125 +0.279925875 +0.268038785 +0.258055116 +0.256859778 +0.273277707 +0.280823786 +0.285935131 +0.279320142 +0.201319918 +0.170200834 +0.180941086 +0.175844546 +0.178553411 +0.187675185 +0.19473709 +0.250147061 +0.277797195 +0.285830216 +0.274860098 +0.254352679 +0.233008848 +0.216917456 +0.196256428 +0.174224118 +0.157885007 +0.152791636 +0.150437384 +0.151871722 +0.147215238 +0.147326157 +0.146191052 +0.134178147 +0.077192148 +0.043328398 +0.040376064 +0.044971209 +0.053116338 +0.067179835 +0.095939662 +0.175999021 +0.226060187 +0.249584745 +0.26105692 +0.272505781 +0.284960475 +0.283776714 +0.259003034 +0.214685915 +0.168088435 +0.133974137 +0.106682794 +0.082251381 +0.068821212 +0.061200851 +0.057109284 +0.052620509 +0.040197559 +0.026893462 +0.023108405 +0.025635396 +0.031868908 +0.041457791 +0.049807258 +0.079875772 +0.10996785 +0.135867993 +0.150911709 +0.162660784 +0.177836542 +0.19373789 +0.196114303 +0.18682877 +0.165344686 +0.145815969 +0.128389456 +0.113751912 +0.102441391 +0.101479846 +0.102758269 +0.105517128 +0.074377584 +0.051931288 +0.044513802 +0.043531103 +0.046492376 +0.053942029 +0.061521464 +0.082075813 +0.103492395 +0.116288337 +0.123951172 +0.119805123 +0.114215565 +0.100706101 +0.085902103 +0.074696807 +0.066753066 +0.060627104 +0.055952849 +0.047942367 +0.037752434 +0.032362389 +0.031047995 +0.030542885 +0.027905767 +0.016568778 +0.021192263 +0.029633191 +0.035205108 +0.040226037 +0.05996179 +0.088454782 +0.106936172 +0.11547924 +0.115690604 +0.111691261 +0.099331756 +0.083448351 +0.081056191 +0.074973959 +0.058946157 +0.045753616 +0.033974945 +0.025815805 +0.025153894 +0.028059113 +0.031413796 +0.032947633 +0.026596568 +0.018387738 +0.013911125 +0.013530022 +0.017357836 +0.024894847 +0.036939415 +0.053330973 +0.06324932 +0.065101907 +0.062578519 +0.060861507 +0.061815659 +0.063752616 +0.06146317 +0.060995308 +0.063466863 +0.069225771 +0.077872301 +0.089006127 +0.105733911 +0.127551176 +0.145862421 +0.153175946 +0.1228437 +0.094351627 +0.082411675 +0.092845192 +0.128406759 +0.158453588 +0.202861659 +0.301773937 +0.337317448 +0.371570987 +0.388163876 +0.386183025 +0.382739134 +0.391609279 +0.364940068 +0.322096118 +0.286573023 +0.257105517 +0.249693021 +0.246883738 +0.244620135 +0.262061432 +0.281001438 +0.280755503 +0.222592001 +0.168797018 +0.193954631 +0.210994516 +0.218179914 +0.217469238 +0.201678266 +0.202066938 +0.209939145 +0.211681341 +0.209762887 +0.198354682 +0.190485762 +0.17695769 +0.164537668 +0.149916901 +0.142435035 +0.132027742 +0.129195645 +0.141041185 +0.138467705 +0.137994353 +0.136163573 +0.125423224 +0.109307819 +0.107103129 +0.117712658 +0.120966195 +0.123924537 +0.12844339 +0.110640312 +0.111093028 +0.120196201 +0.125342604 +0.132916296 +0.142776837 +0.152695076 +0.166816865 +0.17239041 +0.167866375 +0.160773329 +0.150102213 +0.148091543 +0.140490786 +0.127627646 +0.115448492 +0.109466043 +0.10696494 +0.08726614 +0.060553652 +0.069479302 +0.097738138 +0.11706632 +0.122872521 +0.144037018 +0.207279081 +0.240839655 +0.262054008 +0.285676125 +0.29632937 +0.301305972 +0.301687911 +0.282882608 +0.26276875 +0.243872799 +0.254759291 +0.262856129 +0.280707104 +0.321632268 +0.386078059 +0.421174183 +0.437580734 +0.417146563 +0.383722137 +0.404444889 +0.436199008 +0.444819483 +0.446875105 +0.456977004 +0.470934068 +0.471890503 +0.46382294 +0.449299242 +0.444239616 +0.443977638 +0.444256623 +0.429645983 +0.420710974 +0.418181973 +0.416825604 +0.418165624 +0.423677205 +0.421452375 +0.412110247 +0.397358848 +0.387897131 +0.343281366 +0.317124577 +0.371228746 +0.385168297 +0.383370571 +0.378270273 +0.349662112 +0.337811561 +0.346498496 +0.355099153 +0.356768588 +0.357788461 +0.365935367 +0.374716018 +0.344567795 +0.320272025 +0.300206674 +0.291154374 +0.296431666 +0.295035019 +0.290880608 +0.298462887 +0.290045825 +0.284876253 +0.238991097 +0.208621509 +0.208694031 +0.219737486 +0.232897523 +0.229033197 +0.232930625 +0.270256736 +0.286931831 +0.299084868 +0.30904925 +0.312150454 +0.311461461 +0.299135456 +0.282070122 +0.26522514 +0.242518976 +0.221168692 +0.209939239 +0.201058275 +0.203799975 +0.212011342 +0.216470926 +0.214886707 +0.181332922 +0.118394695 +0.091556847 +0.091878535 +0.096061444 +0.104539529 +0.16719676 +0.252950906 +0.299951685 +0.3110752 +0.31151621 +0.312031512 +0.307660178 +0.298782638 +0.279595309 +0.259282302 +0.241180204 +0.236451483 +0.238914082 +0.237916502 +0.255755637 +0.283937064 +0.311958116 +0.350740227 +0.351059589 +0.341072588 +0.347250229 +0.38222247 +0.402254323 +0.405375228 +0.433681824 +0.463538345 +0.486737443 +0.497742388 +0.481219217 +0.475660829 +0.460508418 +0.425492725 +0.392203763 +0.38126856 +0.371494645 +0.360741416 +0.354187499 +0.349972493 +0.350008526 +0.358547289 +0.365720202 +0.362674654 +0.312924506 +0.253739504 +0.27450155 +0.304530222 +0.321888715 +0.319669396 +0.312020421 +0.340893232 +0.369561181 +0.389477025 +0.394544007 +0.389624439 +0.37855648 +0.370217313 +0.331112842 +0.282884662 +0.253538673 +0.239031466 +0.222193248 +0.214099698 +0.20791404 +0.212451005 +0.220868734 +0.232954933 +0.227241426 +0.209184022 +0.184806499 +0.192218057 +0.218066325 +0.24315965 +0.294537124 +0.366856819 +0.403052624 +0.423267165 +0.429308066 +0.437011534 +0.435096573 +0.445054138 +0.443742987 +0.436573082 +0.425438085 +0.40793114 +0.397670602 +0.400613671 +0.420653481 +0.457625574 +0.48928769 +0.512787673 +0.497389536 +0.439477881 +0.40308636 +0.440063197 +0.457112142 +0.454706354 +0.493129865 +0.520857947 +0.535322775 +0.557261344 +0.574429774 +0.582977667 +0.583577612 +0.58010557 +0.543196281 +0.509774265 +0.479297975 +0.449818738 +0.427357041 +0.412996123 +0.419293614 +0.44160755 +0.45235278 +0.460880452 +0.42608724 +0.336237662 +0.292891057 +0.311287444 +0.337623309 +0.359845316 +0.419962858 +0.482488033 +0.530678779 +0.549974787 +0.552935977 +0.561873758 +0.559373965 +0.537049963 +0.475312165 +0.40508429 +0.356844004 +0.314346939 +0.279686163 +0.260654778 +0.264945399 +0.283705554 +0.30645566 +0.325947302 +0.286689651 +0.187725625 +0.125823382 +0.121356843 +0.126507131 +0.125430799 +0.172354012 +0.256432257 +0.308731278 +0.35070582 +0.384229131 +0.417761013 +0.442898424 +0.490842476 +0.522519296 +0.535420298 +0.509940729 +0.486768386 +0.460878087 +0.429223853 +0.426818265 +0.440186325 +0.444723859 +0.439066693 +0.398212424 +0.300074301 +0.223019181 +0.206945119 +0.210888942 +0.232410492 +0.32847836 +0.40266262 +0.415158498 +0.39960944 +0.374498568 +0.342601954 +0.315059142 +0.29331754 +0.261468113 +0.243362544 +0.241532875 +0.238876957 +0.235077721 +0.221089038 +0.193582955 +0.168644052 +0.150591944 +0.142602955 +0.123658335 +0.084230452 +0.048915539 +0.037383771 +0.035598857 +0.041804628 +0.055483784 +0.075932633 +0.08686474 +0.089913443 +0.090660111 +0.088917692 +0.084199026 +0.076806248 +0.072489928 +0.069660864 +0.06821915 +0.066283942 +0.065195954 +0.064846982 +0.059237746 +0.050748529 +0.046841004 +0.046448566 +0.045248692 +0.039551467 +0.036222043 +0.033951132 +0.039161554 +0.056421517 +0.088105442 +0.143185775 +0.184338806 +0.209443871 +0.223860817 +0.227801985 +0.231883468 +0.23243595 +0.229995333 +0.224586832 +0.214458131 +0.208197466 +0.200831668 +0.196449198 +0.186893612 +0.178707773 +0.172716254 +0.164985467 +0.160424895 +0.139381566 +0.11415223 +0.103425606 +0.103210502 +0.109600324 +0.131904668 +0.188711771 +0.205491704 +0.203487422 +0.198265585 +0.189594024 +0.172704373 +0.146818744 +0.126076393 +0.106998291 +0.090849305 +0.077018975 +0.067116554 +0.057352058 +0.048958343 +0.040705041 +0.03561664 +0.032010991 +0.025307347 +0.01666133 +0.009255492 +0.007611943 +0.008033897 +0.010699376 +0.020541891 +0.043743902 +0.07327228 +0.105384374 +0.138497717 +0.172063218 +0.202000677 +0.228345915 +0.246176449 +0.253526882 +0.253424449 +0.250749531 +0.250970887 +0.245368669 +0.233225214 +0.224253996 +0.215441934 +0.214185613 +0.202195821 +0.173347489 +0.143701817 +0.142361123 +0.162647385 +0.198706087 +0.28089654 +0.376545022 +0.413088617 +0.453194257 +0.482565526 +0.499917971 +0.513139744 +0.529051285 +0.523612407 +0.48588821 +0.455251178 +0.436073397 +0.423875325 +0.419200776 +0.435084478 +0.462919451 +0.489500546 +0.49389112 +0.469402423 +0.433065952 +0.431886233 +0.467130169 +0.483223862 +0.501092661 +0.516431592 +0.549013406 +0.573054032 +0.571644927 +0.560912871 +0.541628532 +0.521832145 +0.498612602 +0.468373749 +0.443069184 +0.417944617 +0.400222313 +0.382757735 +0.36617712 +0.345960992 +0.323135132 +0.304142986 +0.277689325 +0.215029312 +0.180008958 +0.152817355 +0.136180812 +0.137363186 +0.14608575 +0.143538008 +0.183669969 +0.206785912 +0.207165889 +0.192281753 +0.178689261 +0.161292437 +0.144880033 +0.131644513 +0.10955277 +0.082635359 +0.063554021 +0.05190968 +0.04735368 +0.048673022 +0.062969108 +0.091559012 \ No newline at end of file diff --git a/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/settings.ini b/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/settings.ini new file mode 100644 index 0000000..015374b --- /dev/null +++ b/tests/unit/assets/study_list_composer/studies/SMTA-case/user/expansion/settings.ini @@ -0,0 +1,6 @@ +uc_type = expansion_fast +master = relaxed +optimality_gap = 0 +max_iteration = Inf +cut_type = weekly +solver = amplxpress \ No newline at end of file diff --git a/tests/unit/test_data_repo_tinydb.py b/tests/unit/test_data_repo_tinydb.py index 48a2b8b..52ad688 100644 --- a/tests/unit/test_data_repo_tinydb.py +++ b/tests/unit/test_data_repo_tinydb.py @@ -1,4 +1,7 @@ +import random +from pathlib import Path from unittest import mock +from uuid import uuid4 import pytest import tinydb @@ -7,144 +10,53 @@ from antareslauncher.study_dto import StudyDTO -class TestDataRepoTinydb: - @pytest.mark.unit_test - def test_given_data_repo_when_save_study_is_called_then_is_study_inside_database_is_called( - self, - ): - # given - repo_mock = DataRepoTinydb("", "name") - type(repo_mock).db = mock.PropertyMock() - repo_mock.is_study_inside_database = mock.Mock() - study_dto = StudyDTO(path="path") - # when - repo_mock.save_study(study_dto) - # then - repo_mock.is_study_inside_database.assert_called_with(study=study_dto) - - @pytest.mark.unit_test - def test_given_data_repo_if_study_is_inside_database_then_db_update_is_called( - self, - ): - # given - repo = DataRepoTinydb("", "name") - repo.is_study_inside_database = mock.Mock(return_value=True) - type(repo).db = mock.PropertyMock() - study_dto = StudyDTO(path="path") - # when - repo.save_study(study_dto) - # then - repo.db.update.assert_called_once() - - @pytest.mark.unit_test - def test_integration_given_data_repo_if_study_is_found_once_in_database_then_db_update_is_called( - self, - ): - # given - repo = DataRepoTinydb("", "name") - type(repo).db = mock.PropertyMock() - repo.db.search = mock.Mock(return_value=["A"]) - study_dto = StudyDTO(path="path") - # when - repo.save_study(study_dto) - # then - repo.db.update.assert_called_once() - - @pytest.mark.unit_test - def test_given_data_repo_if_study_is_not_inside_database_then_db_insert_is_called( - self, - ): - # given - repo = DataRepoTinydb("", "name") - repo.is_study_inside_database = mock.Mock(return_value=False) - type(repo).db = mock.PropertyMock() - study_dto = StudyDTO(path="path") - # when - repo.save_study(study_dto) - # then - repo.db.insert.assert_called_once() - - @pytest.mark.unit_test - def test_given_db_when_get_list_of_studies_is_called_then_db_all_is_called( - self, - ): - # given - repo = DataRepoTinydb("", "name") - repo.doc_to_study = mock.Mock(return_value=42) - type(repo).db = mock.PropertyMock() - repo.db.all = mock.Mock(return_value=[]) - # when - repo.get_list_of_studies() - # then - repo.db.all_assert_calles_once() +@pytest.fixture(name="repo") +def repo_fixture(tmp_path: Path) -> DataRepoTinydb: + return DataRepoTinydb( + database_file_path=tmp_path.joinpath("repo.json"), + db_primary_key="name", + ) - @pytest.mark.unit_test - def test_given_db_of_n_elements_when_get_list_of_studies_is_called_then_doc_to_study_is_called_n_times( - self, - ): - # given - n = 5 - repo = DataRepoTinydb("", "name") - repo.doc_to_study = mock.Mock(return_value=42) - type(repo).db = mock.PropertyMock() - repo.db.all = mock.Mock(return_value=[""] * n) - # when - repo.get_list_of_studies() - # then - assert repo.doc_to_study.call_count == n - - @pytest.mark.unit_test - def test_given_tinydb_document_when_doc_to_study_called_then_return_corresponding_study( - self, - ): - # given - expected_study = StudyDTO(path="path") - expected_study.job_id = 42 - expected_study.n_cpu = 999 - doc = tinydb.database.Document(expected_study.__dict__, 42) - # when - output_study = DataRepoTinydb.doc_to_study(doc) - # then - assert expected_study.__dict__ == output_study.__dict__ - - @pytest.mark.unit_test - def test_is_study_inside_database_returns_true_only_if_one_study_is_found( - self, - ): - # given - repo = DataRepoTinydb("", "name") - type(repo).db = mock.PropertyMock() - dummy_study = StudyDTO(path="path") - repo.db.search = mock.Mock(return_value=["first_element"]) - # when - output = repo.is_study_inside_database(study=dummy_study) - # then - assert output is True - - repo.db.search = mock.Mock(return_value=["first_element", "second_element"]) - # when - output = repo.is_study_inside_database(study=dummy_study) - # then - assert output is False - - repo.db.search = mock.Mock(return_value=[]) - # when - output = repo.is_study_inside_database(study=dummy_study) - # then - assert output is False +class TestDataRepoTinydb: @pytest.mark.unit_test - def test_is_job_id_inside_database_returns_true_only_if_one_job_id_is_found( - self, - ): - # given - repo = DataRepoTinydb("", "name") - type(repo).db = mock.PropertyMock() - study_dto = StudyDTO(path="path") - study_dto.job_id = 6381 - repo.get_list_of_studies = mock.Mock(return_value=[study_dto]) - repo.save_study(study_dto) - # when - output = repo.is_job_id_inside_database(6381) - # then - assert output is True + def test_save_study__insert_and_update(self, repo: DataRepoTinydb): + """ + Test that the 'save_study' method in DataRepoTinydb correctly adds a study to the database + and that 'is_study_inside_database' is called with the same study object. + """ + study = StudyDTO(path="path/to/my_study") + repo.save_study(study) + assert repo.is_study_inside_database(study) + studies = repo.get_list_of_studies() + assert {s.name for s in studies} == {"my_study"} + + study.started = True + repo.save_study(study) + assert repo.is_study_inside_database(study) + studies = repo.get_list_of_studies() + assert {s.name for s in studies} == {"my_study"} + actual_study = next(iter(studies)) + assert actual_study.started is True + + @pytest.mark.unit_test + def test_remove_study__nominal_case(self, repo: DataRepoTinydb): + study = StudyDTO(path="path/to/my_study") + repo.save_study(study) + repo.remove_study("my_study") + studies = repo.get_list_of_studies() + assert not studies + + @pytest.mark.unit_test + def test_remove_study__missing(self, repo: DataRepoTinydb): + repo.remove_study("missing_study") + studies = repo.get_list_of_studies() + assert not studies + + @pytest.mark.unit_test + def test_is_job_id_inside_database(self, repo: DataRepoTinydb): + job_id = random.randint(1, 1000) + study = StudyDTO(path="path/to/my_study", job_id=job_id) + repo.save_study(study) + assert repo.is_job_id_inside_database(job_id) + assert not repo.is_job_id_inside_database(9999) diff --git a/tests/unit/test_main_option_parser.py b/tests/unit/test_main_option_parser.py index e394688..82218aa 100644 --- a/tests/unit/test_main_option_parser.py +++ b/tests/unit/test_main_option_parser.py @@ -46,7 +46,7 @@ def parser(self): @pytest.mark.unit_test def test_check_all_default_values_are_present(self, parser): parser.add_basic_arguments() - output = parser.parse_args([]) + output = parser.parser.parse_args([]) out_dict = vars(output) for key, value in self.DEFAULT_VALUES.items(): assert out_dict[key] == value @@ -54,7 +54,7 @@ def test_check_all_default_values_are_present(self, parser): @pytest.mark.unit_test def test_given_add_basic_arguments_all_default_values_are_present(self, parser): parser.add_basic_arguments() - output = parser.parse_args([]) + output = parser.parser.parse_args([]) out_dict = vars(output) for key, value in self.DEFAULT_VALUES.items(): assert out_dict[key] == value @@ -62,5 +62,5 @@ def test_given_add_basic_arguments_all_default_values_are_present(self, parser): @pytest.mark.unit_test def test_studies_in_get_correctly_set(self, parser): parser.add_basic_arguments() - output = parser.parse_args(["--studies-in-dir=hello"]) + output = parser.parser.parse_args(["--studies-in-dir=hello"]) assert output.studies_in == "hello" diff --git a/tests/unit/test_study_list_composer.py b/tests/unit/test_study_list_composer.py index 2ad06e0..b8bec18 100644 --- a/tests/unit/test_study_list_composer.py +++ b/tests/unit/test_study_list_composer.py @@ -1,441 +1,179 @@ +import shutil from pathlib import Path from unittest import mock -from unittest.mock import call import pytest -from antareslauncher.data_repo.idata_repo import IDataRepo +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb +from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager.file_manager import FileManager -from antareslauncher.study_dto import Modes, StudyDTO from antareslauncher.use_cases.create_list.study_list_composer import ( StudyListComposer, StudyListComposerParameters, + get_solver_version, ) -from tests.data import DATA_DIR - - -class TestStudyListComposer: - def setup_method(self): - self.parameters = StudyListComposerParameters( - studies_in_dir="", - time_limit=0, - n_cpu=1, - log_dir="job_log_dir", +from tests.unit.assets import ASSETS_DIR + +CONFIG_NOMINAL_VERSION = """\ +[antares] +version = 800 +caption = Sample Study +created = 1688740888 +lastsave = 1688740888 +author = john.doe +""" + +CONFIG_NOMINAL_SOLVER_VERSION = """\ +[antares] +version = 800 +caption = Sample Study +created = 1688740888 +lastsave = 1688740888 +author = john.doe +solver_version = 850 +""" + +CONFIG_MISSING_SECTION = """\ +[polaris] +version = 800 +caption = Sample Study +created = 1688740888 +lastsave = 1688740888 +author = john.doe +""" + +CONFIG_MISSING_VERSION = """\ +[antares] +caption = Sample Study +created = 1688740888 +lastsave = 1688740888 +author = john.doe +""" + + +class TestGetSolverVersion: + @pytest.mark.parametrize( + "config_ini, expected", + [ + pytest.param(CONFIG_NOMINAL_VERSION, 800, id="with_version"), + pytest.param(CONFIG_NOMINAL_SOLVER_VERSION, 850, id="with_solver_version"), + pytest.param(CONFIG_MISSING_SECTION, 999, id="bad_missing_section"), + pytest.param(CONFIG_MISSING_VERSION, 999, id="bad_missing_version"), + ], + ) + def test_get_solver_version( + self, + config_ini: str, + expected: int, + tmp_path: Path, + ) -> None: + study_path = tmp_path.joinpath("study.antares") + study_path.write_text(config_ini, encoding="utf-8") + actual = get_solver_version(tmp_path, default=999) + assert actual == expected + + +@pytest.fixture(name="studies_in_dir") +def studies_in_dir_fixture(tmp_path: Path) -> str: + studies_in_dir = tmp_path.joinpath("STUDIES-IN") + assets_dir = ASSETS_DIR.joinpath("study_list_composer/studies") + shutil.copytree(assets_dir, studies_in_dir) + return str(studies_in_dir) + + +@pytest.fixture(name="repo") +def repo_fixture(tmp_path: Path) -> DataRepoTinydb: + return DataRepoTinydb( + database_file_path=tmp_path.joinpath("repo.json"), + db_primary_key="name", + ) + + +@pytest.fixture(name="study_list_composer") +def study_list_composer_fixture( + tmp_path: Path, + repo: DataRepoTinydb, + studies_in_dir: str, +) -> StudyListComposer: + display = mock.Mock(spec=DisplayTerminal) + composer = StudyListComposer( + repo=repo, + file_manager=FileManager(display_terminal=display), + display=display, + parameters=StudyListComposerParameters( + studies_in_dir=studies_in_dir, + time_limit=42, + n_cpu=24, + log_dir=str(tmp_path.joinpath("LOGS")), xpansion_mode=None, - output_dir="output_dir", + output_dir=str(tmp_path.joinpath("FINISHED")), post_processing=False, - antares_versions_on_remote_server=["610", "700", "800"], + antares_versions_on_remote_server=[ + "800", + "810", + "820", + "830", + "840", + "850", + ], other_options="", - ) - - @pytest.fixture(scope="function") - def study_mock(self): - study = mock.Mock() - return study - - @pytest.mark.unit_test - def test_given_repo_when_get_list_of_studies_called_then_repo_get_list_of_studies_is_called( - self, - ): - # given - repo_mock = mock.Mock() - repo_mock.get_list_of_studies = mock.Mock() - study_list_composer = StudyListComposer( - repo=repo_mock, - file_manager=None, - display=None, - parameters=self.parameters, - ) - # when - study_list_composer.get_list_of_studies() - # then - repo_mock.get_list_of_studies.assert_called_once() - - @pytest.mark.unit_test - def test_given_repo_when_get_list_of_studies_called_then_repo_get_list_of_studies_is_called( - self, - ): - # given - - repo_mock = mock.Mock() - repo_mock.get_list_of_studies = mock.Mock() - study_list_composer = StudyListComposer( - repo=repo_mock, - file_manager=None, - display=None, - parameters=self.parameters, - ) - # when - study_list_composer.get_list_of_studies() - # then - repo_mock.get_list_of_studies.assert_called_once() - - @pytest.mark.unit_test - def test_when_is_dir_an_antares_study_is_called_then_the_file_study_antares_is_checked( - self, - ): - # given - - dir_path = "dir_path" - expected_config_file_path = Path(dir_path) / "study.antares" - study_list_composer = StudyListComposer( - repo=None, - file_manager=mock.Mock(), - display=None, - parameters=self.parameters, - ) - # when - study_list_composer._file_manager.get_config_from_file = mock.Mock( - return_value={} - ) - return_value = study_list_composer.get_antares_version(dir_path) - # then - study_list_composer._file_manager.get_config_from_file.assert_called_once_with( - expected_config_file_path - ) - assert not return_value - - @pytest.mark.unit_test - def test_when_antares_is_in_the_config_file_then_is_dir_an_antares_study_return_true( - self, - ): - # given + ), + ) + return composer - dir_path = "dir_path" - expected_config_file_path = Path(dir_path) / "study.antares" - study_list_composer = StudyListComposer( - repo=None, - file_manager=mock.Mock(), - display=None, - parameters=self.parameters, - ) - # when - study_list_composer._file_manager.get_config_from_file = mock.Mock( - return_value={"antares": {"version": 42}} - ) - return_value = study_list_composer.get_antares_version(dir_path) - # then - study_list_composer._file_manager.get_config_from_file.assert_called_once_with( - expected_config_file_path - ) - assert return_value - @pytest.mark.unit_test - def test_given_existing_db_when_no_new_study_then_do_nothing_and_show_message( - self, - ): - # given - self.parameters.studies_in_dir = "studies_in_dir" - - study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock(listdir_of=mock.Mock(return_value=["study"])), - display=mock.Mock(), - parameters=self.parameters, - ) - study_list_composer.get_antares_version = mock.Mock(return_value=True) - study_list_composer._repo.is_study_inside_database = mock.Mock( - return_value=True - ) - - # when - study_list_composer.update_study_database() - - # then - assert study_list_composer._display.show_message.call_count == 3 - - @pytest.mark.unit_test - def test_given_existing_db_when_new_study_then_save_new_study_and_show_message( - self, - ): - self.parameters.studies_in_dir = "studies_in_dir" - self.parameters.time_limit = 24 - self.parameters.n_cpu = 42 - file_manager = mock.create_autospec(FileManager) - file_manager.file_exists = mock.create_autospec( - FileManager.file_exists, return_value=False - ) - file_manager.listdir_of = mock.Mock(return_value=["study_path"]) - repo = mock.create_autospec(IDataRepo, instance=True) - repo.is_study_inside_database = mock.Mock(return_value=False) - # given - study_list_composer = StudyListComposer( - repo=repo, - file_manager=file_manager, - display=mock.Mock(), - parameters=self.parameters, - ) - study_list_composer.get_antares_version = mock.Mock(return_value="700") - study_list_composer._file_manager.is_dir = mock.Mock(return_value=True) - expected_save_study = StudyDTO( - path=str(Path(self.parameters.studies_in_dir) / "study_path"), - antares_version="700", - job_log_dir=str(Path(self.parameters.log_dir) / "JOB_LOGS"), - output_dir=self.parameters.output_dir, - time_limit=self.parameters.time_limit, - n_cpu=self.parameters.n_cpu, - other_options="", - ) - # when - study_list_composer.update_study_database() - - # then - calls = study_list_composer._repo.save_study.call_args_list - assert calls[0] == call(expected_save_study) - assert study_list_composer._display.show_message.call_count == 2 - - @pytest.mark.unit_test - def test_given_empty_study_dir_list_when_update_study_database_called_then_display_show_two_messages( +class TestStudyListComposer: + @pytest.mark.parametrize("xpansion_mode", ["r", "cpp", ""]) + def test_update_study_database__xpansion_mode( self, + study_list_composer: StudyListComposer, + xpansion_mode: str, ): - # given - study_list_composer = StudyListComposer( - repo=None, - file_manager=mock.Mock(listdir_of=mock.Mock(return_value=[])), - display=mock.Mock(), - parameters=self.parameters, - ) - # when + study_list_composer.xpansion_mode = xpansion_mode study_list_composer.update_study_database() - # then - assert study_list_composer._display.show_message.call_count == 2 - - @pytest.mark.unit_test - def test_given_two_new_studies_when_update_study_database_called_then_display_show_three_messages( - self, - ): - # given - self.parameters.studies_in_dir = "studies_in_dir" - study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock( - listdir_of=mock.Mock(return_value=["study1", "study2"]) - ), - display=mock.Mock(), - parameters=self.parameters, - ) - study_list_composer.get_antares_version = mock.Mock(return_value="700") - study_list_composer._repo.is_study_inside_database = mock.Mock( - return_value=False - ) - study_list_composer._file_manager.is_dir = mock.Mock(return_value=True) - # when + studies = study_list_composer.get_list_of_studies() + + # check the found studies + actual_names = {s.name for s in studies} + expected_names = { + "": { + "013 TS Generation - Solar power", + "024 Hurdle costs - 1", + "SMTA-case", + }, + "r": {"SMTA-case"}, + "cpp": {"SMTA-case"}, + }[study_list_composer.xpansion_mode or ""] + assert actual_names == expected_names + + @pytest.mark.parametrize("antares_version", [0, 850, 990]) + def test_update_study_database__antares_version( + self, + study_list_composer: StudyListComposer, + antares_version: int, + ): + study_list_composer.antares_version = antares_version study_list_composer.update_study_database() - # then - assert study_list_composer._display.show_message.call_count == 3 - - @pytest.mark.unit_test - def test_given_directory_path_when_create_study_is_called_then_return_study_dto_with_righ_values( - self, - ): - # given - self.parameters.studies_in_dir = "studies_in_dir" - study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock(), - display=mock.Mock(), - parameters=self.parameters, - ) - - study_dir = study_list_composer._studies_in_dir - antares_version = 700 - - is_xpansion_study = None - # when - new_study_dto = study_list_composer._create_study( - study_dir, antares_version, is_xpansion_study - ) - # then - assert new_study_dto.path == study_list_composer._studies_in_dir - assert new_study_dto.n_cpu == study_list_composer.n_cpu - assert new_study_dto.time_limit == study_list_composer.time_limit - assert new_study_dto.antares_version == antares_version - assert new_study_dto.job_log_dir == str( - Path(study_list_composer.log_dir) / "JOB_LOGS" - ) - - @pytest.mark.unit_test - def test_given_an_antares_version_when_is_valid_antares_study_is_called_return_boolean_value( - self, - ): - # given - study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock(), - display=mock.Mock(), - parameters=self.parameters, - ) - antares_version_700 = "700" - wrong_antares_version = "137" - antares_version_none = None - # when - is_valid_antares_study_expected_true = ( - study_list_composer._is_valid_antares_study(antares_version_700) - ) - is_valid_antares_study_expected_false = ( - study_list_composer._is_valid_antares_study(wrong_antares_version) - ) - is_valid_antares_study_expected_false2 = ( - study_list_composer._is_valid_antares_study(antares_version_none) - ) - # then - assert is_valid_antares_study_expected_true is True - assert is_valid_antares_study_expected_false is False - assert is_valid_antares_study_expected_false2 is False - - @pytest.mark.unit_test - def test_given_a_none_antares_version_when_is_antares_study_is_called_return_false_and_message( - self, - ): - # given - display_mock = mock.Mock() - study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock(), - display=display_mock, - parameters=self.parameters, - ) - display_mock.show_message = mock.Mock() - antares_version = None - # when - is_antares_study = study_list_composer._is_valid_antares_study(antares_version) - # then - assert is_antares_study is False - display_mock.show_message.assert_called_once_with( - "... not a valid Antares study", mock.ANY - ) - - @pytest.mark.unit_test - def test_given_a_non_supported_antares_version_when_is_antares_study_is_called_return_false_and_message( - self, - ): - # given - display_mock = mock.Mock() - study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock(), - display=display_mock, - parameters=self.parameters, - ) - display_mock.show_message = mock.Mock() - antares_version = "600" - message = f"... Antares version ({antares_version}) is not supported (supported versions: {self.parameters.antares_versions_on_remote_server})" - # when - is_antares_study = study_list_composer._is_valid_antares_study(antares_version) - # then - assert is_antares_study is False - display_mock.show_message.assert_called_once_with(message, mock.ANY) - - @pytest.mark.unit_test - def test_given_xpansion_study_path_when_is_xpansion_study_is_called_return_true( - self, - ): - # given - study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock(), - display=mock.Mock(), - parameters=self.parameters, - ) - xpansion_study_path = Path("xpansion_study_path") - study_list_composer._is_there_candidates_file = mock.Mock(return_value=True) - # when - is_xpansion_study = study_list_composer._is_xpansion_study(xpansion_study_path) - # then - assert is_xpansion_study is True - - @pytest.mark.unit_test - def test_given_xpansion_study_path_when_create_study_is_called_then_xpansion_value_of_dto_is_true( - self, - ): - # given - study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock(), - display=mock.Mock(), - parameters=self.parameters, - ) - - study_dir = study_list_composer._studies_in_dir - antares_version = 700 - - is_xpansion_study = "r" - # when - new_study_dto = study_list_composer._create_study( - study_dir, antares_version, is_xpansion_study - ) - # then - assert new_study_dto.xpansion_mode == "r" - - @pytest.mark.unit_test - def test_given_xpansion_mode_option_when_create_study_is_called_then_run_mode_value_of_dto_is_xpansion_mode( - self, - ): - # given - self.parameters.xpansion_mode = "r" - study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock(), - display=mock.Mock(), - parameters=self.parameters, - ) - - study_dir = study_list_composer._studies_in_dir - antares_version = 700 - is_xpansion_study = "r" - # when - new_study_dto = study_list_composer._create_study( - study_dir, antares_version, is_xpansion_study - ) - # then - assert new_study_dto.run_mode == Modes.xpansion_r - - @pytest.mark.unit_test - def test_given_xpansion_mode_option_when_update_study_only_xpansion_studies_are_saved_in_database( - self, - ): - # given - self.parameters.xpansion_mode = "r" - study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=mock.Mock(), - display=mock.Mock(), - parameters=self.parameters, - ) - study_list_composer._update_database_with_study = mock.Mock() - study_list_composer.get_antares_version = mock.Mock(return_value="610") - - study_dir = study_list_composer._studies_in_dir - is_xpansion_study = True - study_list_composer._is_xpansion_study = mock.Mock( - return_value=is_xpansion_study - ) - study_list_composer._update_database_with_directory(study_dir) - - isnot_xpansion_study = False - study_list_composer._is_xpansion_study = mock.Mock( - return_value=isnot_xpansion_study - ) - # when - study_list_composer._update_database_with_directory(study_dir) - # then - study_list_composer._update_database_with_study.assert_called_once() - - @pytest.mark.unit_test - def test_given_a_study_path_when_is_there_candidates_file_is_called_return_true_if_present( - self, - ): - # given - directory_path = DATA_DIR.joinpath("xpansion-reference") - file_manager = FileManager(display_terminal=mock.Mock()) - my_study_list_composer = StudyListComposer( - repo=mock.Mock(), - file_manager=file_manager, - display=mock.Mock(), - parameters=self.parameters, - ) - - # when - output = my_study_list_composer._is_there_candidates_file(directory_path) - # then - assert output + studies = study_list_composer.get_list_of_studies() + + # check the versions + actual_versions = {s.name: s.antares_version for s in studies} + if antares_version == 0: + expected_versions = { + "013 TS Generation - Solar power": 850, # solver_version + "024 Hurdle costs - 1": 840, # versions + "SMTA-case": 810, # version + } + elif antares_version in study_list_composer.ANTARES_VERSIONS_ON_REMOTE_SERVER: + study_names = { + "013 TS Generation - Solar power", + "024 Hurdle costs - 1", + "069 Hydro Reservoir Model", + "BAD Study Section", + "MISSING Study version", + "SMTA-case", + } + expected_versions = dict.fromkeys(study_names, antares_version) + else: + expected_versions = {} + assert {n: expected_versions[n] for n in actual_versions} == actual_versions From 1a91ebba60e2a53c15efd4cde113246a4853d6e2 Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE <43534797+laurent-laporte-pro@users.noreply.github.com> Date: Wed, 20 Sep 2023 16:43:46 +0200 Subject: [PATCH 07/23] fix(cli): preserve backward compatibility in CLI options (#65) * fix(cli): change signature of `add_advanced_arguments` function to preserve backward compatibility * feat(cli): add the `--solver-version` option to the basic command line --- antareslauncher/advanced_launch.py | 7 +++++-- antareslauncher/basic_launch.py | 7 ++++--- antareslauncher/main_option_parser.py | 9 ++++++++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/antareslauncher/advanced_launch.py b/antareslauncher/advanced_launch.py index 219bde4..bcb23aa 100644 --- a/antareslauncher/advanced_launch.py +++ b/antareslauncher/advanced_launch.py @@ -7,7 +7,7 @@ from antareslauncher.parameters_reader import ParametersReader -def main(): +def main() -> None: config_path: Path = get_config_path() config = Config.load_config(config_path) param_reader = ParametersReader( @@ -22,7 +22,10 @@ def main(): parser_parameters.ssh_configfile_path_alternate1, parser_parameters.ssh_configfile_path_alternate1, ] - parser.add_advanced_arguments(ssh_config_required, alt_ssh_paths=alt_ssh_paths) + parser.add_advanced_arguments( + ssh_config_required=ssh_config_required, + alt_ssh_paths=alt_ssh_paths, + ) arguments = parser.parser.parse_args(sys.argv[1:]) main_parameters: MainParameters = param_reader.get_main_parameters() run_with(arguments=arguments, parameters=main_parameters, show_banner=True) diff --git a/antareslauncher/basic_launch.py b/antareslauncher/basic_launch.py index 9b45481..4b734f6 100644 --- a/antareslauncher/basic_launch.py +++ b/antareslauncher/basic_launch.py @@ -1,3 +1,4 @@ +import sys from pathlib import Path from antareslauncher.config import Config, get_config_path @@ -6,7 +7,7 @@ from antareslauncher.parameters_reader import ParametersReader -def main(): +def main() -> None: config_path: Path = get_config_path() config = Config.load_config(config_path) param_reader = ParametersReader( @@ -15,8 +16,8 @@ def main(): ) parser_parameters: ParserParameters = param_reader.get_parser_parameters() parser: MainOptionParser = MainOptionParser(parser_parameters) - parser.add_basic_arguments() - arguments = parser.parse_args() + parser.add_basic_arguments(antares_versions=param_reader.antares_versions) + arguments = parser.parser.parse_args(sys.argv[1:]) main_parameters: MainParameters = param_reader.get_main_parameters() run_with(arguments=arguments, parameters=main_parameters, show_banner=True) diff --git a/antareslauncher/main_option_parser.py b/antareslauncher/main_option_parser.py index b6a099d..e37160a 100644 --- a/antareslauncher/main_option_parser.py +++ b/antareslauncher/main_option_parser.py @@ -45,6 +45,10 @@ def __init__(self, parameters: ParserParameters) -> None: } self.parser.set_defaults(**defaults) + # NOTE: keep this delegation to preserve backward compatibility with v1.3.0 + def parse_args(self, args: t.Union[t.Sequence[str], None]) -> argparse.Namespace: + return self.parser.parse_args(args) + def add_basic_arguments( self, *, antares_versions: t.Sequence[str] = () ) -> MainOptionParser: @@ -178,7 +182,10 @@ def add_basic_arguments( return self def add_advanced_arguments( - self, ssh_config_required: bool, *, alt_ssh_paths: t.Sequence[Path] = () + self, + *, + ssh_config_required: bool = False, + alt_ssh_paths: t.Sequence[t.Optional[Path]] = (), ) -> MainOptionParser: """Adds to the parser all the arguments for the advanced mode""" n_cpu = self.parser.get_default("n_cpu") From a51a0530b363cea122f939c07e3bc160c06da8d8 Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE <43534797+laurent-laporte-pro@users.noreply.github.com> Date: Fri, 22 Sep 2023 10:24:12 +0200 Subject: [PATCH 08/23] feat(database): simplify launcher database implementation (#66) * refactor: remove `IDataRepo` abstract class * feat(database): simplify launcher database implementation --- antareslauncher/data_repo/data_provider.py | 4 +- antareslauncher/data_repo/data_repo_tinydb.py | 72 +++++++++++-------- antareslauncher/data_repo/data_reporter.py | 4 +- antareslauncher/data_repo/idata_repo.py | 25 ------- antareslauncher/study_dto.py | 21 ++++-- .../check_queue_controller.py | 4 +- .../create_list/study_list_composer.py | 4 +- .../use_cases/kill_job/job_kill_controller.py | 4 +- .../use_cases/launch/launch_controller.py | 4 +- .../use_cases/retrieve/retrieve_controller.py | 4 +- .../use_cases/retrieve/state_updater.py | 7 +- ...test_integration_check_queue_controller.py | 4 +- tests/unit/launcher/test_launch_controller.py | 3 +- tests/unit/retriever/test_study_retriever.py | 4 +- tests/unit/test_check_queue_controller.py | 4 +- tests/unit/test_data_provider.py | 4 +- tests/unit/test_data_reporter.py | 4 +- 17 files changed, 84 insertions(+), 92 deletions(-) delete mode 100644 antareslauncher/data_repo/idata_repo.py diff --git a/antareslauncher/data_repo/data_provider.py b/antareslauncher/data_repo/data_provider.py index 85ff87c..f4cd647 100644 --- a/antareslauncher/data_repo/data_provider.py +++ b/antareslauncher/data_repo/data_provider.py @@ -1,11 +1,11 @@ from dataclasses import dataclass -from antareslauncher.data_repo.idata_repo import IDataRepo +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb @dataclass class DataProvider: - data_repo: IDataRepo + data_repo: DataRepoTinydb def get_list_of_studies(self): return self.data_repo.get_list_of_studies() diff --git a/antareslauncher/data_repo/data_repo_tinydb.py b/antareslauncher/data_repo/data_repo_tinydb.py index 4d8cf3d..8231aff 100644 --- a/antareslauncher/data_repo/data_repo_tinydb.py +++ b/antareslauncher/data_repo/data_repo_tinydb.py @@ -1,37 +1,44 @@ import logging -from typing import List +import typing as t import tinydb -from tinydb import TinyDB, where -from antareslauncher.data_repo.idata_repo import IDataRepo from antareslauncher.study_dto import StudyDTO - -class DataRepoTinydb(IDataRepo): +logger = logging.getLogger(__name__) + + +def _calc_diff( + old: t.Mapping[str, t.Any], + new: t.Mapping[str, t.Any], +) -> t.Mapping[str, t.Any]: + old_keys = frozenset(old) + new_keys = frozenset(new) + diff_map = { + "DEL": {k: old[k] for k in old_keys - new_keys}, + "ADD": {k: new[k] for k in new_keys - old_keys}, + "UPD": { + k: f"{old[k]!r} => {new[k]!r}" + for k in old_keys & new_keys + if old[k] != new[k] + }, + } + diff_map = {k: v for k, v in diff_map.items() if v} + return diff_map + + +class DataRepoTinydb: def __init__(self, database_file_path, db_primary_key: str): super(DataRepoTinydb, self).__init__() self.database_file_path = database_file_path - self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") self.db_primary_key = db_primary_key @property def db(self) -> tinydb.database.TinyDB: - return TinyDB(self.database_file_path, sort_keys=True, indent=4) - - @staticmethod - def doc_to_study(doc: tinydb.database.Document): - """Create a studyDTO from a tinydb.database.Document - - Args: - doc: Document representing a study - - Returns: - studyDTO object - """ - study = StudyDTO(path="empty/path") - study.__dict__ = doc - return study + if not hasattr(self, "_tiny_db"): + db = tinydb.TinyDB(self.database_file_path, sort_keys=True, indent=4) + setattr(self, "_tiny_db", db) + return getattr(self, "_tiny_db") def is_study_inside_database(self, study: StudyDTO) -> bool: """Get the study with selected primary key from the database @@ -44,7 +51,7 @@ def is_study_inside_database(self, study: StudyDTO) -> bool: """ pk_name = self.db_primary_key pk_value = getattr(study, pk_name) - found_studies = self.db.search(where(key=pk_name) == pk_value) + found_studies = self.db.search(tinydb.where(key=pk_name) == pk_value) return len(found_studies) == 1 def is_job_id_inside_database(self, job_id: int): @@ -59,12 +66,12 @@ def is_job_id_inside_database(self, job_id: int): studies_list = self.get_list_of_studies() return any(study.job_id == job_id for study in studies_list) - def get_list_of_studies(self) -> List[StudyDTO]: + def get_list_of_studies(self) -> t.Sequence[StudyDTO]: """ Returns: List of all studies inside the database """ - return [self.doc_to_study(doc) for doc in self.db.all()] + return [StudyDTO.from_dict(doc) for doc in self.db.all()] def save_study(self, study: StudyDTO): """Saves the selected study inside the database. If the study already exists inside the @@ -75,14 +82,17 @@ def save_study(self, study: StudyDTO): """ pk_name = self.db_primary_key pk_value = getattr(study, pk_name) - if self.is_study_inside_database(study=study): - self.logger.info(f"Updating study {pk_name}='{pk_value}' in database") - self.db.update(study.__dict__, where(pk_name) == pk_value) + old = self.db.get(tinydb.where(pk_name) == pk_value) + new = vars(study) + if old: + diff = _calc_diff(old, new) + logger.info(f"Updating study '{pk_value}' in database: {diff!r}") + self.db.update(new, tinydb.where(pk_name) == pk_value) else: - self.logger.info(f"Inserting new study {pk_name}='{pk_value}' in database") - self.db.insert(study.__dict__) + logger.info(f"Inserting study '{pk_value}' in database: {new!r}") + self.db.insert(new) def remove_study(self, study_name: str) -> None: pk_name = self.db_primary_key - self.logger.info(f"Removing study {pk_name}='{study_name}' from database") - self.db.remove(where(pk_name) == study_name) + logger.info(f"Removing study '{study_name}' from database") + self.db.remove(tinydb.where(pk_name) == study_name) diff --git a/antareslauncher/data_repo/data_reporter.py b/antareslauncher/data_repo/data_reporter.py index ea94203..c4063ec 100644 --- a/antareslauncher/data_repo/data_reporter.py +++ b/antareslauncher/data_repo/data_reporter.py @@ -1,9 +1,9 @@ -from antareslauncher.data_repo.idata_repo import IDataRepo +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.study_dto import StudyDTO class DataReporter: - def __init__(self, data_repo: IDataRepo): + def __init__(self, data_repo: DataRepoTinydb): self._data_repo = data_repo def save_study(self, study: StudyDTO): diff --git a/antareslauncher/data_repo/idata_repo.py b/antareslauncher/data_repo/idata_repo.py deleted file mode 100644 index 4996536..0000000 --- a/antareslauncher/data_repo/idata_repo.py +++ /dev/null @@ -1,25 +0,0 @@ -from abc import ABC, abstractmethod -from typing import List - -from antareslauncher.study_dto import StudyDTO - - -class IDataRepo(ABC): - def __init__(self): - pass - - @abstractmethod - def get_list_of_studies(self) -> List[StudyDTO]: - raise NotImplementedError - - @abstractmethod - def save_study(self, study: StudyDTO): - raise NotImplementedError - - @abstractmethod - def is_study_inside_database(self, study: StudyDTO) -> bool: - raise NotImplementedError - - @abstractmethod - def is_job_id_inside_database(self, job_id: int): - raise NotImplementedError diff --git a/antareslauncher/study_dto.py b/antareslauncher/study_dto.py index af83e94..1969d23 100644 --- a/antareslauncher/study_dto.py +++ b/antareslauncher/study_dto.py @@ -1,7 +1,7 @@ +import typing as t from dataclasses import dataclass, field from enum import IntEnum from pathlib import Path -from typing import Optional class Modes(IntEnum): @@ -29,15 +29,24 @@ class StudyDTO: remote_server_is_clean: bool = False final_zip_extracted: bool = False done: bool = False - job_id: Optional[int] = None + job_id: t.Optional[int] = None job_state: str = "" - time_limit: Optional[int] = None - n_cpu: Optional[int] = None - antares_version: Optional[str] = None - xpansion_mode: Optional[str] = None + time_limit: t.Optional[int] = None + n_cpu: t.Optional[int] = None + antares_version: t.Optional[str] = None + xpansion_mode: t.Optional[str] = None run_mode: Modes = Modes.antares post_processing: bool = False other_options: str = "" def __post_init__(self): self.name = Path(self.path).name + + @classmethod + def from_dict(cls, doc: t.Mapping) -> "StudyDTO": + """ + Create a Study DTO from a mapping. + """ + attrs = dict(**doc) + attrs.pop("name", None) # calculated + return cls(**attrs) diff --git a/antareslauncher/use_cases/check_remote_queue/check_queue_controller.py b/antareslauncher/use_cases/check_remote_queue/check_queue_controller.py index 1173964..4788edd 100644 --- a/antareslauncher/use_cases/check_remote_queue/check_queue_controller.py +++ b/antareslauncher/use_cases/check_remote_queue/check_queue_controller.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from antareslauncher.data_repo.idata_repo import IDataRepo +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import SlurmQueueShow from antareslauncher.use_cases.retrieve.state_updater import StateUpdater @@ -9,7 +9,7 @@ class CheckQueueController: slurm_queue_show: SlurmQueueShow state_updater: StateUpdater - repo: IDataRepo + repo: DataRepoTinydb def check_queue(self): """Displays all the jobs un the slurm queue""" diff --git a/antareslauncher/use_cases/create_list/study_list_composer.py b/antareslauncher/use_cases/create_list/study_list_composer.py index 757b12a..1a644be 100644 --- a/antareslauncher/use_cases/create_list/study_list_composer.py +++ b/antareslauncher/use_cases/create_list/study_list_composer.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from pathlib import Path -from antareslauncher.data_repo.idata_repo import IDataRepo +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.study_dto import Modes, StudyDTO @@ -51,7 +51,7 @@ class StudyListComposerParameters: class StudyListComposer: def __init__( self, - repo: IDataRepo, + repo: DataRepoTinydb, file_manager: FileManager, display: DisplayTerminal, parameters: StudyListComposerParameters, diff --git a/antareslauncher/use_cases/kill_job/job_kill_controller.py b/antareslauncher/use_cases/kill_job/job_kill_controller.py index dbf0091..f6e9a83 100644 --- a/antareslauncher/use_cases/kill_job/job_kill_controller.py +++ b/antareslauncher/use_cases/kill_job/job_kill_controller.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from antareslauncher.data_repo.idata_repo import IDataRepo +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, @@ -11,7 +11,7 @@ class JobKillController: env: RemoteEnvironmentWithSlurm display: DisplayTerminal - repo: IDataRepo + repo: DataRepoTinydb def _check_if_job_is_killable(self, job_id): return self.repo.is_job_id_inside_database(job_id) diff --git a/antareslauncher/use_cases/launch/launch_controller.py b/antareslauncher/use_cases/launch/launch_controller.py index f6215df..add508f 100644 --- a/antareslauncher/use_cases/launch/launch_controller.py +++ b/antareslauncher/use_cases/launch/launch_controller.py @@ -1,5 +1,5 @@ +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.data_repo.data_reporter import DataReporter -from antareslauncher.data_repo.idata_repo import IDataRepo from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.remote_environnement.remote_environment_with_slurm import ( @@ -58,7 +58,7 @@ def launch_study(self, study): class LaunchController: def __init__( self, - repo: IDataRepo, + repo: DataRepoTinydb, env: RemoteEnvironmentWithSlurm, file_manager: FileManager, display: DisplayTerminal, diff --git a/antareslauncher/use_cases/retrieve/retrieve_controller.py b/antareslauncher/use_cases/retrieve/retrieve_controller.py index e79e2ec..a956d6a 100644 --- a/antareslauncher/use_cases/retrieve/retrieve_controller.py +++ b/antareslauncher/use_cases/retrieve/retrieve_controller.py @@ -1,5 +1,5 @@ +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.data_repo.data_reporter import DataReporter -from antareslauncher.data_repo.idata_repo import IDataRepo from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.remote_environnement.remote_environment_with_slurm import ( @@ -16,7 +16,7 @@ class RetrieveController: def __init__( self, - repo: IDataRepo, + repo: DataRepoTinydb, env: RemoteEnvironmentWithSlurm, file_manager: FileManager, display: DisplayTerminal, diff --git a/antareslauncher/use_cases/retrieve/state_updater.py b/antareslauncher/use_cases/retrieve/state_updater.py index 559531a..b08fe9b 100644 --- a/antareslauncher/use_cases/retrieve/state_updater.py +++ b/antareslauncher/use_cases/retrieve/state_updater.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import List +import typing as t from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import ( @@ -68,12 +68,11 @@ def _set_current_study_job_state(self): else: self._current_study.job_state = "Pending" - def run_on_list(self, study_list: List[StudyDTO]): + def run_on_list(self, studies: t.Sequence[StudyDTO]): message = "Checking status of the studies:" self._display.show_message( message, __name__ + "." + self.__class__.__name__, ) - study_list.sort(key=lambda x: x.done, reverse=True) - for study in study_list: + for study in sorted(studies, key=lambda x: x.done, reverse=True): self.run(study) diff --git a/tests/integration/test_integration_check_queue_controller.py b/tests/integration/test_integration_check_queue_controller.py index 144bac8..a41beb2 100644 --- a/tests/integration/test_integration_check_queue_controller.py +++ b/tests/integration/test_integration_check_queue_controller.py @@ -2,7 +2,7 @@ import pytest -from antareslauncher.data_repo.idata_repo import IDataRepo +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, ) @@ -33,7 +33,7 @@ def setup_method(self): display_mock = mock.Mock() slurm_queue_show = SlurmQueueShow(env_mock, display_mock) state_updater = StateUpdater(env_mock, display_mock) - repo = mock.MagicMock(spec=IDataRepo) + repo = mock.MagicMock(spec=DataRepoTinydb) self.check_queue_controller = CheckQueueController( slurm_queue_show, state_updater, repo ) diff --git a/tests/unit/launcher/test_launch_controller.py b/tests/unit/launcher/test_launch_controller.py index 930824d..a4b10f0 100644 --- a/tests/unit/launcher/test_launch_controller.py +++ b/tests/unit/launcher/test_launch_controller.py @@ -10,7 +10,6 @@ import antareslauncher.use_cases.launch.study_zip_uploader from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.data_repo.data_reporter import DataReporter -from antareslauncher.data_repo.idata_repo import IDataRepo from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.remote_environnement.remote_environment_with_slurm import ( @@ -30,7 +29,7 @@ def setup_method(self): env = mock.Mock(spec_set=RemoteEnvironmentWithSlurm) display = mock.Mock(spec_set=DisplayTerminal) file_manager = mock.Mock(spec_set=FileManager) - repo = mock.Mock(spec_set=IDataRepo) + repo = mock.Mock(spec_set=DataRepoTinydb) self.reporter = DataReporter(repo) self.zipper = StudyZipper(file_manager, display) self.study_uploader = StudyZipfileUploader(env, display) diff --git a/tests/unit/retriever/test_study_retriever.py b/tests/unit/retriever/test_study_retriever.py index 358f95f..f575a02 100644 --- a/tests/unit/retriever/test_study_retriever.py +++ b/tests/unit/retriever/test_study_retriever.py @@ -2,8 +2,8 @@ import pytest +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.data_repo.data_reporter import DataReporter -from antareslauncher.data_repo.idata_repo import IDataRepo from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.remote_environnement.remote_environment_with_slurm import ( @@ -26,7 +26,7 @@ def setup_method(self): env = mock.Mock(spec_set=RemoteEnvironmentWithSlurm) display = mock.Mock(spec_set=DisplayTerminal) file_manager = mock.Mock(spec_set=FileManager) - repo = mock.Mock(spec_set=IDataRepo) + repo = mock.Mock(spec_set=DataRepoTinydb) self.reporter = DataReporter(repo) self.state_updater = StateUpdater(env, display) self.logs_downloader = LogDownloader(env, file_manager, display) diff --git a/tests/unit/test_check_queue_controller.py b/tests/unit/test_check_queue_controller.py index 127294b..8f378c2 100644 --- a/tests/unit/test_check_queue_controller.py +++ b/tests/unit/test_check_queue_controller.py @@ -2,7 +2,7 @@ import pytest -from antareslauncher.data_repo.idata_repo import IDataRepo +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.check_remote_queue.check_queue_controller import ( CheckQueueController, @@ -13,7 +13,7 @@ class TestCheckQueueController: def setup_method(self): - self.repo_mock = mock.Mock(spec=IDataRepo) + self.repo_mock = mock.Mock(spec=DataRepoTinydb) self.env = mock.Mock() self.display = mock.Mock() self.slurm_queue_show = SlurmQueueShow(env=self.env, display=self.display) diff --git a/tests/unit/test_data_provider.py b/tests/unit/test_data_provider.py index bdb7f23..d2e7067 100644 --- a/tests/unit/test_data_provider.py +++ b/tests/unit/test_data_provider.py @@ -1,13 +1,13 @@ from unittest.mock import Mock from antareslauncher.data_repo.data_provider import DataProvider -from antareslauncher.data_repo.idata_repo import IDataRepo +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.study_dto import StudyDTO def test_data_provider_return_list_of_studies_obtained_from_repo(): # given - data_repo = Mock(spec_set=IDataRepo) + data_repo = Mock(spec_set=DataRepoTinydb) study = StudyDTO(path="empty_path") data_repo.get_list_of_studies = Mock(return_value=[study]) data_provider = DataProvider(data_repo) diff --git a/tests/unit/test_data_reporter.py b/tests/unit/test_data_reporter.py index 19eac00..3d55c7b 100644 --- a/tests/unit/test_data_reporter.py +++ b/tests/unit/test_data_reporter.py @@ -1,13 +1,13 @@ from unittest.mock import Mock +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.data_repo.data_reporter import DataReporter -from antareslauncher.data_repo.idata_repo import IDataRepo from antareslauncher.study_dto import StudyDTO def test_data_reporter_calls_repo_to_save_study(): # given - data_repo = Mock(spec_set=IDataRepo) + data_repo = Mock(spec_set=DataRepoTinydb) data_repo.save_study = Mock() data_reporter = DataReporter(data_repo) study = StudyDTO(path="empty_path") From 4ff6abf512b03944d0132d868484ef2d677c8b77 Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Fri, 22 Sep 2023 13:37:17 +0200 Subject: [PATCH 09/23] chore(typing): improve typing in source code --- antareslauncher/__init__.py | 2 +- antareslauncher/antares_launcher.py | 2 +- antareslauncher/display/display_terminal.py | 14 ++++++++++---- antareslauncher/main.py | 1 - .../remote_environment_with_slurm.py | 11 +++++------ antareslauncher/study_dto.py | 4 ++-- 6 files changed, 19 insertions(+), 15 deletions(-) diff --git a/antareslauncher/__init__.py b/antareslauncher/__init__.py index 9ce0ab1..b58b8ab 100644 --- a/antareslauncher/__init__.py +++ b/antareslauncher/__init__.py @@ -19,7 +19,7 @@ __project_name__ = "Antares_Launcher" -def _check_metadata(): +def _check_metadata() -> None: # noinspection SpellCheckingInspection """ Check the project metadata. diff --git a/antareslauncher/antares_launcher.py b/antareslauncher/antares_launcher.py index a9a11ee..07a526f 100644 --- a/antareslauncher/antares_launcher.py +++ b/antareslauncher/antares_launcher.py @@ -41,7 +41,7 @@ def run_once_mode(self): def run_wait_mode(self): """Run antares_launcher once then it keeps checking the status of the unfinished jobs until all jobs are finished, - The code exit when all job are finished the the results are retrieved and extracted + The code exits when all jobs are finished, the results are retrieved and extracted """ self.run_once_mode() while not self.retrieve_controller.all_studies_done: diff --git a/antareslauncher/display/display_terminal.py b/antareslauncher/display/display_terminal.py index a24487f..acebffc 100644 --- a/antareslauncher/display/display_terminal.py +++ b/antareslauncher/display/display_terminal.py @@ -1,15 +1,16 @@ import datetime import logging +import typing as t from tqdm import tqdm class DisplayTerminal: - def __init__(self): + def __init__(self) -> None: # Use the ISO8601 date format to display messages on the console self.format = "%Y-%m-%d %H:%M:%S%z" - def show_message(self, message: str, class_name: str, end: str = "\n"): + def show_message(self, message: str, class_name: str, end: str = "\n") -> None: """Displays a message on the terminal Args: @@ -22,7 +23,7 @@ def show_message(self, message: str, class_name: str, end: str = "\n"): if end != "\r": logging.getLogger(class_name).info(message) - def show_error(self, error: str, class_name: str): + def show_error(self, error: str, class_name: str) -> None: """Displays a error on the terminal Args: @@ -33,7 +34,12 @@ def show_error(self, error: str, class_name: str): print(f"ERROR - [{now.strftime(self.format)}] " + error) logging.getLogger(class_name).error(error) - def generate_progress_bar(self, iterator, desc="", total=None): + def generate_progress_bar( + self, + iterator: t.Iterable[t.Any], + desc: str = "", + total: t.Optional[int] = None, + ) -> t.Iterable[t.Any]: """Generates al loading bar and shows it in the terminal Args: diff --git a/antareslauncher/main.py b/antareslauncher/main.py index c487d34..f47fd38 100644 --- a/antareslauncher/main.py +++ b/antareslauncher/main.py @@ -163,7 +163,6 @@ def run_with( retrieve_controller = RetrieveController( repo=data_repo, env=environment, - file_manager=file_manager, display=display, state_updater=state_updater, ) diff --git a/antareslauncher/remote_environnement/remote_environment_with_slurm.py b/antareslauncher/remote_environnement/remote_environment_with_slurm.py index 535f838..d1f0a03 100644 --- a/antareslauncher/remote_environnement/remote_environment_with_slurm.py +++ b/antareslauncher/remote_environnement/remote_environment_with_slurm.py @@ -401,7 +401,7 @@ def _retrieve_slurm_acct_state( ) raise GetJobStateError(job_id, job_name, reason) - def upload_file(self, src): + def upload_file(self, src) -> bool: """Uploads a file to the remote server Args: @@ -423,8 +423,7 @@ def download_logs(self, study: StudyDTO) -> t.List[Path]: to download the log files. Returns: - True if all the logs have been downloaded, False if all the logs - have not been downloaded or if there are no files to download + The paths of the downloaded logs on the local filesystem. """ src_dir = PurePosixPath(self.remote_base_path) dst_dir = Path(study.job_log_dir) @@ -464,7 +463,7 @@ def download_final_zip(self, study: StudyDTO) -> t.Optional[Path]: ) return next(iter(downloaded_files), None) - def remove_input_zipfile(self, study: StudyDTO): + def remove_input_zipfile(self, study: StudyDTO) -> bool: """Removes initial zipfile Args: @@ -480,7 +479,7 @@ def remove_input_zipfile(self, study: StudyDTO): ) return study.input_zipfile_removed - def remove_remote_final_zipfile(self, study: StudyDTO): + def remove_remote_final_zipfile(self, study: StudyDTO) -> bool: """Removes final zipfile Args: @@ -493,7 +492,7 @@ def remove_remote_final_zipfile(self, study: StudyDTO): f"{self.remote_base_path}/{Path(study.local_final_zipfile_path).name}" ) - def clean_remote_server(self, study: StudyDTO): + def clean_remote_server(self, study: StudyDTO) -> bool: """ Removes the input and the output zipfile from the remote host diff --git a/antareslauncher/study_dto.py b/antareslauncher/study_dto.py index 1969d23..cf48d9d 100644 --- a/antareslauncher/study_dto.py +++ b/antareslauncher/study_dto.py @@ -39,11 +39,11 @@ class StudyDTO: post_processing: bool = False other_options: str = "" - def __post_init__(self): + def __post_init__(self) -> None: self.name = Path(self.path).name @classmethod - def from_dict(cls, doc: t.Mapping) -> "StudyDTO": + def from_dict(cls, doc: t.Mapping[str, t.Any]) -> "StudyDTO": """ Create a Study DTO from a mapping. """ From 88efc98af6a8fd494f07cc9a366a52109eb3ac2d Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Fri, 22 Sep 2023 13:39:22 +0200 Subject: [PATCH 10/23] feat(retrival): correct the retrival of remote files and improve exception handling to avoid infinite loops --- .../use_cases/retrieve/clean_remote_server.py | 70 +++--- .../use_cases/retrieve/download_final_zip.py | 75 ++---- .../use_cases/retrieve/final_zip_extractor.py | 85 +++---- .../use_cases/retrieve/log_downloader.py | 71 +++--- .../use_cases/retrieve/retrieve_controller.py | 23 +- .../use_cases/retrieve/state_updater.py | 79 +++--- .../use_cases/retrieve/study_retriever.py | 2 - tests/unit/retriever/conftest.py | 41 ++++ .../unit/retriever/test_download_final_zip.py | 225 ++++++++++-------- .../retriever/test_final_zip_extractor.py | 191 +++++++++++---- tests/unit/retriever/test_log_downloader.py | 204 ++++++++-------- .../retriever/test_retrieve_controller.py | 79 +----- tests/unit/retriever/test_server_cleaner.py | 165 +++++++------ tests/unit/retriever/test_state_updater.py | 134 +++++------ tests/unit/retriever/test_study_retriever.py | 53 +---- 15 files changed, 754 insertions(+), 743 deletions(-) create mode 100644 tests/unit/retriever/conftest.py diff --git a/antareslauncher/use_cases/retrieve/clean_remote_server.py b/antareslauncher/use_cases/retrieve/clean_remote_server.py index b3aa837..22f5d46 100644 --- a/antareslauncher/use_cases/retrieve/clean_remote_server.py +++ b/antareslauncher/use_cases/retrieve/clean_remote_server.py @@ -1,15 +1,10 @@ -import copy -from pathlib import Path - from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, ) from antareslauncher.study_dto import StudyDTO - -class RemoteServerNotCleanException(Exception): - pass +LOG_NAME = f"{__name__}.RemoteServerCleaner" class RemoteServerCleaner: @@ -20,42 +15,31 @@ def __init__( ): self._display = display self._env = env - self._current_study: StudyDTO = None def clean(self, study: StudyDTO): - self._current_study = copy.copy(study) - if self._should_clean_remote_server(): - self._do_clean_remote_server() - return self._current_study - - def _should_clean_remote_server(self): - return ( - self._current_study.remote_server_is_clean is False - ) and self._final_zip_downloaded() - - def _final_zip_downloaded(self) -> bool: - if isinstance(self._current_study.local_final_zipfile_path, str): - return bool(self._current_study.local_final_zipfile_path) - else: - return False - - def _do_clean_remote_server(self): - success = self._env.clean_remote_server(copy.copy(self._current_study)) - if success is True: - self._current_study.remote_server_is_clean = success - self._display_success_message() - else: - self._display_failure_error() - raise RemoteServerNotCleanException - - def _display_failure_error(self): - self._display.show_error( - f'"{Path(self._current_study.path).name}": Clean remote server failed', - __name__ + "." + __class__.__name__, - ) - - def _display_success_message(self): - self._display.show_message( - f'"{Path(self._current_study.path).name}": Clean remote server finished', - __name__ + "." + __class__.__name__, - ) + if not study.remote_server_is_clean and study.local_final_zipfile_path: + # If the cleanup procedure fails to remove remote files or + # delete the final ZIP, there's no need to raise an exception. + # Instead, it's sufficient to issue a warning to alert the user. + try: + removed = self._env.clean_remote_server(study) + except Exception as exc: + self._display.show_error( + f'"{study.name}": Clean remote server raised: {exc}', + LOG_NAME, + ) + else: + if removed: + self._display.show_message( + f'"{study.name}": Clean remote server finished', + LOG_NAME, + ) + else: + self._display.show_error( + f'"{study.name}": Clean remote server failed', + LOG_NAME, + ) + + # However, in such cases, it's advisable to indicate that the cleanup + # was successful to prevent an infinite loop. + study.remote_server_is_clean = True diff --git a/antareslauncher/use_cases/retrieve/download_final_zip.py b/antareslauncher/use_cases/retrieve/download_final_zip.py index d7c8194..86f814a 100644 --- a/antareslauncher/use_cases/retrieve/download_final_zip.py +++ b/antareslauncher/use_cases/retrieve/download_final_zip.py @@ -1,4 +1,4 @@ -import copy +from pathlib import Path from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import ( @@ -6,9 +6,7 @@ ) from antareslauncher.study_dto import StudyDTO - -class FinalZipNotDownloadedException(Exception): - pass +LOG_NAME = f"{__name__}.FinalZipDownloader" class FinalZipDownloader(object): @@ -19,7 +17,6 @@ def __init__( ): self._env = env self._display = display - self._current_study = None def download(self, study: StudyDTO): """ @@ -32,57 +29,27 @@ def download(self, study: StudyDTO): Returns: The updated data transfer object, with its `local_final_zipfile_path` attribute set if the download was successful. - - Raises: - FinalZipNotDownloadedException: If the download fails or no files are found. """ - self._current_study = copy.copy(study) if ( - self._current_study.finished - and not self._current_study.with_error - and not self._current_study.local_final_zipfile_path - ): - self._do_download() - return self._current_study - - def _do_download(self): - """ - Perform the download of the final ZIP file for the current study, - and update its `local_final_zipfile_path` attribute. - - Raises: - FinalZipNotDownloadedException: If the download fails or no files are found. - - Note: - This function delegates the download operation to the - `_env.download_final_zip` method, which is assumed to return - the path to the downloaded zip file on the local filesystem - or `None` if the download fails or no files are found. - - If the download succeeds, the `local_final_zipfile_path` attribute - of the `_current_study` object is updated with the path to the - downloaded file, and a success message is displayed. - - If the download fails, an error message is displayed and a - `FinalZipNotDownloadedException` exception is raised. - """ - self._display.show_message( - f'"{self._current_study.name}": downloading final ZIP...', - f"{__name__}.{__class__.__name__}", - ) - if local_final_zipfile_path := self._env.download_final_zip( - copy.copy(self._current_study) + study.finished + and not study.with_error + and not study.local_final_zipfile_path ): - self._current_study.local_final_zipfile_path = str(local_final_zipfile_path) self._display.show_message( - f'"{self._current_study.name}": Final ZIP downloaded', - f"{__name__}.{__class__.__name__}", - ) - else: - self._display.show_error( - f'"{self._current_study.name}": Final ZIP not downloaded', - f"{__name__}.{__class__.__name__}", - ) - raise FinalZipNotDownloadedException( - self._current_study.local_final_zipfile_path + f'"{study.name}": downloading final ZIP...', + LOG_NAME, ) + dst_dir = Path(study.output_dir) + dst_dir.mkdir(parents=True, exist_ok=True) + zip_path = self._env.download_final_zip(study) + study.local_final_zipfile_path = str(zip_path) if zip_path else "" + if study.local_final_zipfile_path: + self._display.show_message( + f'"{study.name}": Final ZIP downloaded', + LOG_NAME, + ) + else: + self._display.show_error( + f'"{study.name}": Final ZIP NOT downloaded', + LOG_NAME, + ) diff --git a/antareslauncher/use_cases/retrieve/final_zip_extractor.py b/antareslauncher/use_cases/retrieve/final_zip_extractor.py index 519a51a..1a4fd26 100644 --- a/antareslauncher/use_cases/retrieve/final_zip_extractor.py +++ b/antareslauncher/use_cases/retrieve/final_zip_extractor.py @@ -1,50 +1,55 @@ +import zipfile from pathlib import Path from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.study_dto import StudyDTO - -class ResultNotExtractedException(Exception): - pass +LOG_NAME = f"{__name__}.FinalZipDownloader" class FinalZipExtractor: - def __init__(self, file_manager: FileManager, display: DisplayTerminal): - self._file_manager = file_manager + def __init__(self, display: DisplayTerminal): self._display = display - self._current_study: StudyDTO = None - - def extract_final_zip(self, study: StudyDTO) -> StudyDTO: - self._current_study = study - if self._study_final_zip_should_be_extracted(): - self._do_extract() - return self._current_study - - def _do_extract(self): - zipfile_to_extract = self._current_study.local_final_zipfile_path - success = self._file_manager.unzip(zipfile_to_extract) - if success: - self._current_study.final_zip_extracted = success - self._show_success_message() - else: - self._show_failure_error() - raise ResultNotExtractedException - - def _show_failure_error(self): - self._display.show_error( - f'"{Path(self._current_study.path).name}": Final zip not extracted', - __name__ + "." + __class__.__name__, - ) - - def _show_success_message(self): - self._display.show_message( - f'"{Path(self._current_study.path).name}": Final zip extracted', - __name__ + "." + __class__.__name__, - ) - def _study_final_zip_should_be_extracted(self): - return ( - self._current_study.local_final_zipfile_path - and not self._current_study.final_zip_extracted - ) + def extract_final_zip(self, study: StudyDTO) -> None: + """ + Extracts the simulation results, which are in the form of a ZIP file, + after it has been downloaded from Antares. + + Args: + study: The current study + """ + if ( + study.finished + and not study.with_error + and study.local_final_zipfile_path + and not study.final_zip_extracted + ): + zip_path = Path(study.local_final_zipfile_path) + target_dir = zip_path.with_suffix("") + try: + with zipfile.ZipFile(zip_path) as zf: + names = zf.namelist() + progress_bar = self._display.generate_progress_bar( + names, desc="Extracting archive:", total=len(names) + ) + for file in progress_bar: + zf.extract(member=file, path=target_dir) + except (OSError, zipfile.BadZipFile) as exc: + # If we cannot extract the final ZIP file, either because the file + # doesn't exist or the ZIP file is corrupted, we find ourselves + # in a situation where the results are unusable. + # In such cases, it's best to consider the simulation as failed, + # enabling the user to restart its simulation. + study.final_zip_extracted = False + study.with_error = True + self._display.show_error( + f'"{study.name}": Final zip not extracted: {exc}', + LOG_NAME, + ) + else: + study.final_zip_extracted = True + self._display.show_message( + f'"{study.name}": Final zip extracted', + LOG_NAME, + ) diff --git a/antareslauncher/use_cases/retrieve/log_downloader.py b/antareslauncher/use_cases/retrieve/log_downloader.py index bfbb092..065ad6b 100644 --- a/antareslauncher/use_cases/retrieve/log_downloader.py +++ b/antareslauncher/use_cases/retrieve/log_downloader.py @@ -1,63 +1,52 @@ -import copy from pathlib import Path from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, ) from antareslauncher.study_dto import StudyDTO +LOG_NAME = f"{__name__}.LogDownloader" + class LogDownloader: def __init__( self, env: RemoteEnvironmentWithSlurm, - file_manager: FileManager, display: DisplayTerminal, ): self.env = env self.display = display - self.file_manager = file_manager - self._current_study = None - - def _create_logs_subdirectory(self): - self._set_current_study_log_dir_path() - self.file_manager.make_dir(self._current_study.job_log_dir) - - def _set_current_study_log_dir_path(self): - directory_name = self._get_log_dir_name() - if Path(self._current_study.job_log_dir).name != directory_name: - self._current_study.job_log_dir = str( - Path(self._current_study.job_log_dir) / directory_name - ) - def _get_log_dir_name(self): - return ( - Path(self._current_study.path).name + "_" + str(self._current_study.job_id) - ) - - def run(self, study: StudyDTO): - """Downloads slurm logs from the server then save study if the study is running + def run(self, study: StudyDTO) -> None: + """ + Downloads slurm logs from the server then save study if the study is running Args: study: The study data transfer object """ - self._current_study = copy.copy(study) - if self._current_study.started: - self._create_logs_subdirectory() - self._do_download_logs() - return self._current_study - - def _do_download_logs(self): - if self.env.download_logs(copy.copy(self._current_study)): - self._current_study.logs_downloaded = True - self.display.show_message( - f'"{Path(self._current_study.path).name}": Logs downloaded', - f"{__name__}.{__class__.__name__}", - ) - else: - self.display.show_error( - f'"{Path(self._current_study.path).name}": Logs not downloaded', - f"{__name__}.{__class__.__name__}", - ) + if study.started: + # set_current_study_log_dir_path + directory_name = f"{study.name}_{study.job_id}" + job_log_dir = Path(study.job_log_dir) + if job_log_dir.name != directory_name: + job_log_dir = job_log_dir / directory_name + study.job_log_dir = str(job_log_dir) + + # create logs subdirectory + job_log_dir.mkdir(parents=True, exist_ok=True) + + # make an attempt to download logs + downloaded_logs = self.env.download_logs(study) + if downloaded_logs: + study.logs_downloaded = True + self.display.show_message( + f'"{study.name}": Logs downloaded', + LOG_NAME, + ) + else: + # No file to download + self.display.show_error( + f'"{study.name}": Logs NOT downloaded', + LOG_NAME, + ) diff --git a/antareslauncher/use_cases/retrieve/retrieve_controller.py b/antareslauncher/use_cases/retrieve/retrieve_controller.py index a956d6a..b936b91 100644 --- a/antareslauncher/use_cases/retrieve/retrieve_controller.py +++ b/antareslauncher/use_cases/retrieve/retrieve_controller.py @@ -12,28 +12,25 @@ from antareslauncher.use_cases.retrieve.state_updater import StateUpdater from antareslauncher.use_cases.retrieve.study_retriever import StudyRetriever +LOG_NAME = f"{__name__}.RetrieveController" + class RetrieveController: def __init__( self, repo: DataRepoTinydb, env: RemoteEnvironmentWithSlurm, - file_manager: FileManager, display: DisplayTerminal, state_updater: StateUpdater, ): self.repo = repo self.env = env - self.file_manager = file_manager self.display = display self.state_updater = state_updater - DataReporter(repo) - logs_downloader = LogDownloader( - env=self.env, file_manager=file_manager, display=self.display - ) + logs_downloader = LogDownloader(env=self.env, display=self.display) final_zip_downloader = FinalZipDownloader(env=self.env, display=self.display) - remote_server_cleaner = RemoteServerCleaner(env, display) - zip_extractor = FinalZipExtractor(file_manager, display) + remote_server_cleaner = RemoteServerCleaner(env=self.env, display=self.display) + zip_extractor = FinalZipExtractor(display=self.display) self.study_retriever = StudyRetriever( state_updater, logs_downloader, @@ -64,15 +61,9 @@ def retrieve_all_studies(self): 5. extract result """ studies = self.repo.get_list_of_studies() - self.display.show_message( - "Retrieving all studies", - __name__ + "." + __class__.__name__, - ) + self.display.show_message("Retrieving all studies...", LOG_NAME) for study in studies: self.study_retriever.retrieve(study) if self.all_studies_done: - self.display.show_message( - "Everything is done", - __name__ + "." + __class__.__name__, - ) + self.display.show_message("All retrievals are done.", LOG_NAME) return self.all_studies_done diff --git a/antareslauncher/use_cases/retrieve/state_updater.py b/antareslauncher/use_cases/retrieve/state_updater.py index b08fe9b..f6e495c 100644 --- a/antareslauncher/use_cases/retrieve/state_updater.py +++ b/antareslauncher/use_cases/retrieve/state_updater.py @@ -1,4 +1,3 @@ -from pathlib import Path import typing as t from antareslauncher.display.display_terminal import DisplayTerminal @@ -7,6 +6,8 @@ ) from antareslauncher.study_dto import StudyDTO +LOG_NAME = f"{__name__}.RetrieveController" + class StateUpdater: def __init__( @@ -16,63 +17,53 @@ def __init__( ): self._env = env self._display = display - self._current_study: StudyDTO = None - def _show_job_state_message(self, study: StudyDTO): + def _show_job_state_message(self, study: StudyDTO) -> None: if study.done is True: self._display.show_message( - f'"{Path(study.path).name}" (JOBID={study.job_id}): everything is done', - __name__ + "." + self.__class__.__name__, + f'"{study.name}": (JOBID={study.job_id}): everything is done', + LOG_NAME, + ) + elif study.job_id: + self._display.show_message( + f'"{study.name}": (JOBID={study.job_id}): {study.job_state}', + LOG_NAME, ) else: - if study.job_id: - self._display.show_message( - f'"{Path(study.path).name}" (JOBID={study.job_id}): {study.job_state}', - __name__ + "." + self.__class__.__name__, - ) - else: - self._display.show_error( - f'"{Path(study.path).name}": Job was not submitted', - __name__ + "." + self.__class__.__name__, - ) + self._display.show_error( + f'"{study.name}": Job was NOT submitted', + LOG_NAME, + ) - def run(self, study: StudyDTO) -> StudyDTO: + def run(self, study: StudyDTO) -> None: """Gets the job state flags from the environment and update the IStudyDTO flags then save study Args: study: The study data transfer object """ - self._current_study = study - if not self._current_study.done: - self._set_current_study_job_state_flags() - self._set_current_study_job_state() - self._show_job_state_message(study) - return study + if not study.done: + # set current study job state flags + if study.job_id: + s, f, e = self._env.get_job_state_flags(study) + else: + s, f, e = False, False, False + study.started = s + study.finished = f + study.with_error = e - def _set_current_study_job_state_flags(self): - if self._current_study.job_id: - s, f, e = self._env.get_job_state_flags(self._current_study) + # set current study job state + if study.with_error: + study.job_state = "Ended with error" + elif study.finished: + study.job_state = "Finished" + elif study.started: + study.job_state = "Running" else: - s, f, e = False, False, False - self._current_study.started = s - self._current_study.finished = f - self._current_study.with_error = e + study.job_state = "Pending" - def _set_current_study_job_state(self): - if self._current_study.with_error: - self._current_study.job_state = "Ended with error" - elif self._current_study.finished: - self._current_study.job_state = "Finished" - elif self._current_study.started: - self._current_study.job_state = "Running" - else: - self._current_study.job_state = "Pending" + self._show_job_state_message(study) - def run_on_list(self, studies: t.Sequence[StudyDTO]): - message = "Checking status of the studies:" - self._display.show_message( - message, - __name__ + "." + self.__class__.__name__, - ) + def run_on_list(self, studies: t.Sequence[StudyDTO]) -> None: + self._display.show_message("Checking status of the studies:", LOG_NAME) for study in sorted(studies, key=lambda x: x.done, reverse=True): self.run(study) diff --git a/antareslauncher/use_cases/retrieve/study_retriever.py b/antareslauncher/use_cases/retrieve/study_retriever.py index 18134f9..0c6bcee 100644 --- a/antareslauncher/use_cases/retrieve/study_retriever.py +++ b/antareslauncher/use_cases/retrieve/study_retriever.py @@ -41,8 +41,6 @@ def retrieve(self, study: StudyDTO): except Exception as e: # The exception is not re-raised, but the job is marked as failed - study.done = True - study.finished = True study.with_error = True study.job_state = f"Internal error: {e}" diff --git a/tests/unit/retriever/conftest.py b/tests/unit/retriever/conftest.py new file mode 100644 index 0000000..77f3ae9 --- /dev/null +++ b/tests/unit/retriever/conftest.py @@ -0,0 +1,41 @@ +import dataclasses +from pathlib import Path + +import pytest + +from antareslauncher.study_dto import StudyDTO + + +@pytest.fixture(name="pending_study") +def pending_study_fixture(tmp_path: Path) -> StudyDTO: + study_path = tmp_path.joinpath("My Study") + job_log_dir = tmp_path.joinpath("LOG_DIR") + output_dir = tmp_path.joinpath("OUTPUT_DIR") + return StudyDTO( + path=str(study_path), + started=False, + job_id=46505574, + job_log_dir=str(job_log_dir), + output_dir=str(output_dir), + ) + + +@pytest.fixture(name="started_study") +def started_study_fixture(pending_study: StudyDTO) -> StudyDTO: + return dataclasses.replace( + pending_study, started=True, finished=False, with_error=False + ) + + +@pytest.fixture(name="finished_study") +def finished_study_fixture(pending_study: StudyDTO) -> StudyDTO: + return dataclasses.replace( + pending_study, started=True, finished=True, with_error=False + ) + + +@pytest.fixture(name="with_error_study") +def with_error_study_fixture(pending_study: StudyDTO) -> StudyDTO: + return dataclasses.replace( + pending_study, started=True, finished=True, with_error=True + ) diff --git a/tests/unit/retriever/test_download_final_zip.py b/tests/unit/retriever/test_download_final_zip.py index a3fb02b..87b0526 100644 --- a/tests/unit/retriever/test_download_final_zip.py +++ b/tests/unit/retriever/test_download_final_zip.py @@ -1,7 +1,6 @@ -from copy import copy +import typing as t from pathlib import Path from unittest import mock -from unittest.mock import call import pytest @@ -10,110 +9,146 @@ RemoteEnvironmentWithSlurm, ) from antareslauncher.study_dto import StudyDTO -from antareslauncher.use_cases.retrieve.download_final_zip import ( - FinalZipDownloader, - FinalZipNotDownloadedException, -) +from antareslauncher.use_cases.retrieve.download_final_zip import FinalZipDownloader -class TestFinalZipDownloader: - def setup_method(self): - self.remote_env = mock.Mock(spec_set=RemoteEnvironmentWithSlurm) - self.display_mock = mock.Mock(spec_set=DisplayTerminal) - self.final_zip_downloader = FinalZipDownloader( - self.remote_env, self.display_mock - ) - - @pytest.fixture(scope="function") - def successfully_finished_zip_study(self): - return StudyDTO( - path="path/hello", - started=True, - finished=True, - with_error=False, - job_id=42, - ) +def download_final_zip(study: StudyDTO) -> t.Optional[Path]: + """Simulate the download of the final ZIP.""" + dst_dir = Path(study.output_dir) # must exist + out_path = dst_dir.joinpath(f"finished_{study.name}_{study.job_id}.zip") + out_path.write_bytes(b"PK fake zip") + return out_path + +class TestFinalZipDownloader: @pytest.mark.unit_test - def test_download_study_shows_message_if_succeeds( - self, successfully_finished_zip_study - ): - final_zipfile_path = "results.zip" - self.remote_env.download_final_zip = mock.Mock(return_value=final_zipfile_path) - - self.final_zip_downloader.download(successfully_finished_zip_study) - expected_message1 = '"hello": downloading final ZIP...' - expected_message2 = '"hello": Final ZIP downloaded' - calls = [ - call(expected_message1, mock.ANY), - call(expected_message2, mock.ANY), - ] - self.display_mock.show_message.assert_has_calls(calls) + def test_download__pending_study(self, pending_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download + downloader = FinalZipDownloader(env=env, display=display) + downloader.download(pending_study) + + # Check the result + env.download_final_zip.assert_not_called() + display.show_message.assert_not_called() + display.show_error.assert_not_called() @pytest.mark.unit_test - def test_download_study_shows_error_and_raises_exceptions_if_failure( - self, successfully_finished_zip_study - ): - self.remote_env.download_final_zip = mock.Mock(return_value=None) - - with pytest.raises(FinalZipNotDownloadedException): - self.final_zip_downloader.download(successfully_finished_zip_study) - - expected_welcome_message = '"hello": downloading final ZIP...' - expected_error_message = '"hello": Final ZIP not downloaded' - self.display_mock.show_message.assert_called_once_with( - expected_welcome_message, mock.ANY - ) - self.display_mock.show_error.assert_called_once_with( - expected_error_message, mock.ANY - ) + def test_download__started_study(self, started_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download + downloader = FinalZipDownloader(env=env, display=display) + downloader.download(started_study) + + # Check the result + env.download_final_zip.assert_not_called() + display.show_message.assert_not_called() + display.show_error.assert_not_called() @pytest.mark.unit_test - def test_remote_env_not_called_if_final_zip_already_downloaded(self): - self.remote_env.download_final_zip = mock.Mock() - downloaded_study = StudyDTO("hello") - downloaded_study.local_final_zipfile_path = "results.zip" + def test_download__with_error_study(self, with_error_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download + downloader = FinalZipDownloader(env=env, display=display) + downloader.download(with_error_study) - new_study = self.final_zip_downloader.download(downloaded_study) + # Check the result + env.download_final_zip.assert_not_called() + display.show_message.assert_not_called() + display.show_error.assert_not_called() - self.remote_env.download_final_zip.assert_not_called() - assert new_study == downloaded_study + @pytest.mark.unit_test + def test_download__finished_study__download_ok( + self, finished_study: StudyDTO + ) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.download_final_zip = download_final_zip + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download + downloader = FinalZipDownloader(env=env, display=display) + downloader.download(finished_study) + + # Check the result: one ZIP file is downloaded + assert finished_study.local_final_zipfile_path + assert display.show_message.call_count == 2 # two messages + assert display.show_error.call_count == 0 # no error + output_dir = Path(finished_study.output_dir) + assert output_dir.is_dir() + zip_files = list(output_dir.iterdir()) + assert len(zip_files) == 1 + + @pytest.mark.unit_test + def test_download__finished_study__reentrancy( + self, finished_study: StudyDTO + ) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.download_final_zip = download_final_zip + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download twice + downloader = FinalZipDownloader(env=env, display=display) + downloader.download(finished_study) + + output_dir1 = Path(finished_study.output_dir) + zip_files1 = set(output_dir1.iterdir()) + downloader.download(finished_study) + + # Check the result: one ZIP file is downloaded + assert finished_study.local_final_zipfile_path + assert display.show_message.call_count == 2 + assert display.show_error.call_count == 0 + + # ZIP files are not duplicated + output_dir2 = Path(finished_study.output_dir) + zip_files2 = set(output_dir2.iterdir()) + assert zip_files1 == zip_files2 @pytest.mark.unit_test - @pytest.mark.parametrize( - "finished,with_error", - [ - (False, False), - (True, True), - ], - ) - def test_remote_env_not_called_if_final_zip_not_successfully_finished( - self, finished, with_error - ): - self.remote_env.download_final_zip = mock.Mock() - not_finished_study = StudyDTO("hello", finished=finished, with_error=with_error) - - new_study = self.final_zip_downloader.download(not_finished_study) - - self.remote_env.download_final_zip.assert_not_called() - assert new_study == not_finished_study + def test_download__finished_study__download_nothing( + self, finished_study: StudyDTO + ) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.download_final_zip = lambda _: [] + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download + downloader = FinalZipDownloader(env=env, display=display) + downloader.download(finished_study) + + # Check the result: no ZIP file is downloaded + assert not finished_study.local_final_zipfile_path + assert display.show_message.call_count == 1 # only the first message + assert display.show_error.call_count == 1 + output_dir = Path(finished_study.output_dir) + assert output_dir.is_dir() + zip_files = list(output_dir.iterdir()) + assert not zip_files @pytest.mark.unit_test - def test_remote_env_is_called_if_final_zip_not_yet_downloaded( - self, successfully_finished_zip_study - ): - final_zipfile_path = "results.zip" - self.remote_env.download_final_zip = mock.Mock( - return_value=Path(final_zipfile_path) - ) - - new_study = self.final_zip_downloader.download(successfully_finished_zip_study) - - self.remote_env.download_final_zip.assert_called_once() - first_call = self.remote_env.download_final_zip.call_args_list[0] - first_argument = first_call[0][0] - assert first_argument == successfully_finished_zip_study - - expected_final_study = copy(successfully_finished_zip_study) - expected_final_study.local_final_zipfile_path = final_zipfile_path - assert new_study == expected_final_study + def test_download__finished_study__download_error( + self, finished_study: StudyDTO + ) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.download_final_zip.side_effect = Exception("Connection error") + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download + downloader = FinalZipDownloader(env=env, display=display) + with pytest.raises(Exception, match=r"Connection\s+error"): + downloader.download(finished_study) + + # Check the result: the exception is not managed + assert not finished_study.local_final_zipfile_path + assert display.show_message.call_count == 1 # only the first message + display.show_error.assert_not_called() + output_dir = Path(finished_study.output_dir) + assert output_dir.is_dir() + zip_files = list(output_dir.iterdir()) + assert not zip_files diff --git a/tests/unit/retriever/test_final_zip_extractor.py b/tests/unit/retriever/test_final_zip_extractor.py index 73a2531..0dc9381 100644 --- a/tests/unit/retriever/test_final_zip_extractor.py +++ b/tests/unit/retriever/test_final_zip_extractor.py @@ -1,72 +1,167 @@ +import dataclasses +import zipfile +from pathlib import Path from unittest import mock import pytest from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.study_dto import StudyDTO -from antareslauncher.use_cases.retrieve.final_zip_extractor import ( - FinalZipExtractor, - ResultNotExtractedException, -) +from antareslauncher.use_cases.retrieve.final_zip_extractor import FinalZipExtractor + + +def create_final_zip(study: StudyDTO, *, scenario: str = "nominal") -> str: + """Prepare a final ZIP.""" + dst_dir = Path(study.output_dir) # must exist + dst_dir.mkdir(parents=True, exist_ok=True) + out_path = dst_dir.joinpath(f"finished_{study.name}_{study.job_id}.zip") + if scenario == "nominal": + with zipfile.ZipFile( + out_path, + mode="w", + compression=zipfile.ZIP_DEFLATED, + ) as zf: + zf.writestr("simulation.log", data=b"Simulation OK") + elif scenario == "corrupted": + out_path.write_bytes(b"PK corrupted content") + return str(out_path) class TestFinalZipExtractor: - def setup_method(self): - self.file_manager = mock.Mock(spec_set=FileManager) - self.display_mock = mock.Mock(spec_set=DisplayTerminal) - self.zip_extractor = FinalZipExtractor(self.file_manager, self.display_mock) - - @pytest.fixture(scope="function") - def study_to_extract(self): - local_zip = "results.zip" - study = StudyDTO( - path="hello", - local_final_zipfile_path=local_zip, - final_zip_extracted=False, - ) - return study + @pytest.mark.unit_test + def test_extract_final_zip__pending_study(self, pending_study: StudyDTO) -> None: + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the ZIP extraction + extractor = FinalZipExtractor(display=display) + extractor.extract_final_zip(pending_study) + + # Check the result + display.show_message.assert_not_called() + display.show_error.assert_not_called() + assert not pending_study.final_zip_extracted @pytest.mark.unit_test - def test_extract_zip_show_message_if_zip_succeeds(self, study_to_extract): - self.file_manager.unzip = mock.Mock(return_value=True) + def test_extract_final_zip__started_study(self, started_study: StudyDTO) -> None: + display = mock.Mock(spec=DisplayTerminal) - self.zip_extractor.extract_final_zip(study_to_extract) + # Initialize and execute the ZIP extraction + extractor = FinalZipExtractor(display=display) + extractor.extract_final_zip(started_study) - expected_message = f'"hello": Final zip extracted' - self.display_mock.show_message.assert_called_once_with( - expected_message, mock.ANY - ) + # Check the result + display.show_message.assert_not_called() + display.show_error.assert_not_called() + assert not started_study.final_zip_extracted @pytest.mark.unit_test - def test_extract_zip_show_error_and_raises_exception_if_zip_fails( - self, study_to_extract - ): - self.file_manager.unzip = mock.Mock(return_value=False) + def test_extract_final_zip__finished_study__no_output( + self, finished_study: StudyDTO + ) -> None: + display = mock.Mock(spec=DisplayTerminal) - with pytest.raises(ResultNotExtractedException): - self.zip_extractor.extract_final_zip(study_to_extract) + # Initialize and execute the ZIP extraction + extractor = FinalZipExtractor(display=display) + extractor.extract_final_zip(finished_study) - expected_error = f'"hello": Final zip not extracted' - self.display_mock.show_error.assert_called_once_with(expected_error, mock.ANY) + # Check the result + display.show_message.assert_not_called() + display.show_error.assert_not_called() + assert not finished_study.final_zip_extracted @pytest.mark.unit_test - def test_file_manager_not_called_if_study_should_not_be_extracted(self): - self.file_manager.unzip = mock.Mock() - empty_study = StudyDTO("hello") + def test_extract_final_zip__finished_study__nominal( + self, finished_study: StudyDTO + ) -> None: + display = mock.Mock(spec=DisplayTerminal) + display.generate_progress_bar = lambda names, *args, **kwargs: names + + # Prepare a valid final ZIP + finished_study.local_final_zipfile_path = create_final_zip(finished_study) - new_study = self.zip_extractor.extract_final_zip(empty_study) - self.file_manager.unzip.assert_not_called() - self.display_mock.show_error.assert_not_called() - self.display_mock.show_message.assert_not_called() - assert new_study == empty_study + # Initialize and execute the ZIP extraction + extractor = FinalZipExtractor(display=display) + extractor.extract_final_zip(finished_study) + + # Check the result + display.show_message.assert_called_once() + display.show_error.assert_not_called() + + assert finished_study.final_zip_extracted + assert not finished_study.with_error + + result_dir = Path(finished_study.local_final_zipfile_path).with_suffix("") + assert result_dir.joinpath("simulation.log").is_file() @pytest.mark.unit_test - def test_file_manager_is_called_if_study_is_ready(self, study_to_extract): - self.file_manager.unzip = mock.Mock(return_value=True) + def test_extract_final_zip__finished_study__reentrancy( + self, finished_study: StudyDTO + ) -> None: + display = mock.Mock(spec=DisplayTerminal) + display.generate_progress_bar = lambda names, *args, **kwargs: names + + # Prepare a valid final ZIP + finished_study.local_final_zipfile_path = create_final_zip(finished_study) + + # Initialize and execute the ZIP extraction twice + extractor = FinalZipExtractor(display=display) + extractor.extract_final_zip(finished_study) + study_state1 = dataclasses.asdict(finished_study) + + extractor = FinalZipExtractor(display=display) + extractor.extract_final_zip(finished_study) + study_state2 = dataclasses.asdict(finished_study) + + assert study_state1 == study_state2 - new_study = self.zip_extractor.extract_final_zip(study_to_extract) - self.file_manager.unzip.assert_called_once_with( - study_to_extract.local_final_zipfile_path + @pytest.mark.unit_test + def test_extract_final_zip__finished_study__missing( + self, finished_study: StudyDTO + ) -> None: + display = mock.Mock(spec=DisplayTerminal) + display.generate_progress_bar = lambda names, *args, **kwargs: names + + # Prepare a missing final ZIP + finished_study.local_final_zipfile_path = create_final_zip( + finished_study, scenario="missing" + ) + + # Initialize and execute the ZIP extraction + extractor = FinalZipExtractor(display=display) + extractor.extract_final_zip(finished_study) + + # Check the result + display.show_message.assert_not_called() + display.show_error.assert_called_once() + + assert not finished_study.final_zip_extracted + assert finished_study.with_error + + result_dir = Path(finished_study.local_final_zipfile_path).with_suffix("") + assert not result_dir.joinpath("simulation.log").exists() + + @pytest.mark.unit_test + def test_extract_final_zip__finished_study__corrupted( + self, finished_study: StudyDTO + ) -> None: + display = mock.Mock(spec=DisplayTerminal) + display.generate_progress_bar = lambda names, *args, **kwargs: names + + # Prepare a corrupted final ZIP + finished_study.local_final_zipfile_path = create_final_zip( + finished_study, scenario="corrupted" ) - assert new_study.final_zip_extracted is True + + # Initialize and execute the ZIP extraction + extractor = FinalZipExtractor(display=display) + extractor.extract_final_zip(finished_study) + + # Check the result + display.show_message.assert_not_called() + display.show_error.assert_called_once() + + assert not finished_study.final_zip_extracted + assert finished_study.with_error + + result_dir = Path(finished_study.local_final_zipfile_path).with_suffix("") + assert not result_dir.joinpath("simulation.log").exists() diff --git a/tests/unit/retriever/test_log_downloader.py b/tests/unit/retriever/test_log_downloader.py index 807f7b4..3818748 100644 --- a/tests/unit/retriever/test_log_downloader.py +++ b/tests/unit/retriever/test_log_downloader.py @@ -1,10 +1,9 @@ -from copy import copy +import typing as t from pathlib import Path from unittest import mock import pytest -import antareslauncher.remote_environnement.remote_environment_with_slurm from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, @@ -13,98 +12,111 @@ from antareslauncher.use_cases.retrieve.log_downloader import LogDownloader +def download_logs(study: StudyDTO) -> t.List[Path]: + """Simulate the download of logs.""" + dst_dir = Path(study.job_log_dir) # must exist + out_path = dst_dir.joinpath(f"antares-out-{study.job_id}.txt") + out_path.write_text("Quitting the solver gracefully.") + err_path = dst_dir.joinpath(f"antares-err-{study.job_id}.txt") + err_path.write_text("No error") + return [out_path, err_path] + + class TestLogDownloader: - def setup_method(self): - self.remote_env_mock = mock.Mock(spec=RemoteEnvironmentWithSlurm) - self.file_manager = mock.Mock() - self.display_mock = mock.Mock(spec_set=DisplayTerminal) - self.log_downloader = LogDownloader( - self.remote_env_mock, self.file_manager, self.display_mock - ) - - @pytest.fixture(scope="function") - def started_study(self): - study = StudyDTO(path="path/hello") - study.started = True - study.job_id = 42 - study.job_log_dir = "ROOT_LOG_DIR" - return study - - def test_download_shows_message_if_successful(self, started_study): - self.remote_env_mock.download_logs = mock.Mock(return_value=True) - self.log_downloader.run(started_study) - - expected_message = '"hello": Logs downloaded' - self.display_mock.show_message.assert_called_once_with( - expected_message, mock.ANY - ) - - def test_download_shows_error_if_fails_and_only_study_logdir_is_changed( - self, started_study - ): - self.remote_env_mock.download_logs = mock.Mock(return_value=False) - log_dir_name = f"{started_study.name}_{started_study.job_id}" - expected_job_log_dir = str(Path(started_study.job_log_dir) / log_dir_name) - expected_study = copy(started_study) - expected_study.job_log_dir = expected_job_log_dir - - new_study = self.log_downloader.run(started_study) - - expected_message = '"hello": Logs not downloaded' - self.display_mock.show_error.assert_called_once_with(expected_message, mock.ANY) - assert new_study == expected_study - - def test_file_manager_and_remote_env_not_called_if_study_not_started(self): - self.remote_env_mock.download_logs = mock.Mock() - self.file_manager.make_dir = mock.Mock() - study = StudyDTO(path="hello") - study.started = False - self.log_downloader.run(study) - - self.remote_env_mock.download_logs.assert_not_called() - self.file_manager.make_dir.assert_not_called() - - def test_file_manager_is_called_to_create_logdir_if_study_started( - self, started_study - ): - self.remote_env_mock.download_logs = mock.Mock(return_value=True) - self.file_manager.make_dir = mock.Mock() - - self.log_downloader.run(started_study) - - log_dir_name = f"{started_study.name}_{started_study.job_id}" - expected_job_log_dir = str(Path(started_study.job_log_dir) / log_dir_name) - self.file_manager.make_dir.assert_called_once_with(expected_job_log_dir) - - def test_make_manager_is_called_properly_even_if_logdir_was_already_previously_set( - self, started_study - ): - self.remote_env_mock.download_logs = mock.Mock(return_value=True) - self.file_manager.make_dir = mock.Mock() - log_dir_name = f"{started_study.name}_{started_study.job_id}" - expected_job_log_dir = str(Path(started_study.job_log_dir) / log_dir_name) - started_study.job_log_dir = expected_job_log_dir - - expected_study = copy(started_study) - expected_study.job_log_dir = expected_job_log_dir - - self.log_downloader.run(started_study) - - self.file_manager.make_dir.assert_called_once_with(expected_job_log_dir) - - def test_environment_download_logs_is_called_if_study_started(self, started_study): - log_dir_name = f"{started_study.name}_{started_study.job_id}" - log_path = Path(started_study.job_log_dir) / log_dir_name - self.remote_env_mock.download_logs = mock.Mock(return_value=[log_path]) - - expected_job_log_dir = str(log_path) - expected_study = copy(started_study) - expected_study.job_log_dir = expected_job_log_dir - - new_study = self.log_downloader.run(started_study) - - first_call = self.remote_env_mock.download_logs.call_args_list[0] - first_argument = first_call[0][0] - assert first_argument == expected_study - assert new_study.job_log_dir == expected_job_log_dir - assert new_study.logs_downloaded is True + @pytest.mark.unit_test + def test_run__pending_study(self, pending_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download + downloader = LogDownloader(env=env, display=display) + downloader.run(pending_study) + + # Check the result + env.download_logs.assert_not_called() + display.show_message.assert_not_called() + display.show_error.assert_not_called() + + @pytest.mark.unit_test + def test_run__started_study__download_ok(self, started_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.download_logs = download_logs + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download + downloader = LogDownloader(env=env, display=display) + downloader.run(started_study) + + # Check the result: two log files are downloaded + assert started_study.logs_downloaded + display.show_message.assert_called_once() + display.show_error.assert_not_called() + job_log_dir = Path(started_study.job_log_dir) + assert job_log_dir.is_dir() + log_files = list(job_log_dir.iterdir()) + assert len(log_files) == 2 + + @pytest.mark.unit_test + def test_run__started_study__reentrancy(self, started_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.download_logs = download_logs + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download twice + downloader = LogDownloader(env=env, display=display) + downloader.run(started_study) + + job_log_dir1 = Path(started_study.job_log_dir) + log_files1 = set(job_log_dir1.iterdir()) + downloader.run(started_study) + + # Check the result: two log files are downloaded + assert started_study.logs_downloaded + assert display.show_message.call_count == 2 + assert display.show_error.call_count == 0 + + # Log files are not duplicated + job_log_dir2 = Path(started_study.job_log_dir) + log_files2 = set(job_log_dir2.iterdir()) + assert log_files1 == log_files2 + + @pytest.mark.unit_test + def test_run__started_study__download_nothing( + self, started_study: StudyDTO + ) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.download_logs = lambda _: [] + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download + downloader = LogDownloader(env=env, display=display) + downloader.run(started_study) + + # Check the result: no log file is downloaded + assert not started_study.logs_downloaded + display.show_message.assert_not_called() + display.show_error.assert_called_once() + job_log_dir = Path(started_study.job_log_dir) + assert job_log_dir.is_dir() + log_files = list(job_log_dir.iterdir()) + assert not log_files + + @pytest.mark.unit_test + def test_run__started_study__download_error(self, started_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.download_logs.side_effect = Exception("Connection error") + display = mock.Mock(spec=DisplayTerminal) + + # Initialize and execute the download + downloader = LogDownloader(env=env, display=display) + with pytest.raises(Exception, match=r"Connection\s+error"): + downloader.run(started_study) + + # Check the result: the exception is not managed + assert not started_study.logs_downloaded + display.show_message.assert_not_called() + display.show_error.assert_not_called() + job_log_dir = Path(started_study.job_log_dir) + assert job_log_dir.is_dir() + log_files = list(job_log_dir.iterdir()) + assert not log_files diff --git a/tests/unit/retriever/test_retrieve_controller.py b/tests/unit/retriever/test_retrieve_controller.py index 958a29f..9cef2cc 100644 --- a/tests/unit/retriever/test_retrieve_controller.py +++ b/tests/unit/retriever/test_retrieve_controller.py @@ -4,8 +4,6 @@ import pytest -import antareslauncher -import antareslauncher.remote_environnement.remote_environment_with_slurm from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, @@ -17,63 +15,20 @@ class TestRetrieveController: def setup_method(self): - self.remote_env_mock = mock.Mock(spec=RemoteEnvironmentWithSlurm) - self.file_manager = mock.Mock() + self.env = mock.Mock(spec=RemoteEnvironmentWithSlurm) self.data_repo = mock.Mock() - self.display = mock.Mock() - self.state_updater_mock = StateUpdater(self.remote_env_mock, self.display) - - @pytest.fixture(scope="function") - def my_study(self): - return antareslauncher.study_dto.StudyDTO("") - - @pytest.fixture(scope="function") - def my_running_study(self): - study = antareslauncher.study_dto.StudyDTO( - job_id=42, - started=True, - finished=False, - with_error=False, - path="path", - ) - return study - - @pytest.fixture(scope="function") - def my_finished_study(self): - study = antareslauncher.study_dto.StudyDTO( - job_id=42, - started=True, - finished=True, - with_error=False, - path="path", - ) - return study - - @pytest.fixture(scope="function") - def my_downloaded_study(self): - study = antareslauncher.study_dto.StudyDTO( - job_id=42, - started=True, - finished=True, - with_error=False, - local_final_zipfile_path="local_final_zipfile_path", - path="path", - ) - return study + self.display = mock.Mock(spec=DisplayTerminal) + self.state_updater_mock = StateUpdater(self.env, self.display) @pytest.mark.unit_test def test_given_one_study_when_retrieve_all_studies_call_then_study_retriever_is_called_once( - self, my_study + self, started_study ): # given - list_of_studies = [my_study] + list_of_studies = [started_study] self.data_repo.get_list_of_studies = mock.Mock(return_value=list_of_studies) my_retriever = RetrieveController( - self.data_repo, - self.remote_env_mock, - self.file_manager, - self.display, - self.state_updater_mock, + self.data_repo, self.env, self.display, self.state_updater_mock ) my_retriever.study_retriever.retrieve = mock.Mock() self.display.show_message = mock.Mock() @@ -81,9 +36,9 @@ def test_given_one_study_when_retrieve_all_studies_call_then_study_retriever_is_ my_retriever.retrieve_all_studies() # then self.display.show_message.assert_called_once_with( - "Retrieving all studies", mock.ANY + "Retrieving all studies...", mock.ANY ) - my_retriever.study_retriever.retrieve.assert_called_once_with(my_study) + my_retriever.study_retriever.retrieve.assert_called_once_with(started_study) @pytest.mark.unit_test def test_given_a_list_of_done_studies_when_all_studies_done_called_then_return_true( @@ -94,11 +49,7 @@ def test_given_a_list_of_done_studies_when_all_studies_done_called_then_return_t study.done = True study_list = [deepcopy(study), deepcopy(study)] my_retriever = RetrieveController( - self.data_repo, - self.remote_env_mock, - self.file_manager, - self.display, - self.state_updater_mock, + self.data_repo, self.env, self.display, self.state_updater_mock ) my_retriever.repo.get_list_of_studies = mock.Mock(return_value=study_list) # when @@ -116,22 +67,18 @@ def test_given_a_list_of_done_studies_when_retrieve_all_studies_called_then_mess study_list = [deepcopy(study), deepcopy(study)] display_mock = mock.Mock(spec=DisplayTerminal) my_retriever = RetrieveController( - self.data_repo, - self.remote_env_mock, - self.file_manager, - display_mock, - self.state_updater_mock, + self.data_repo, self.env, display_mock, self.state_updater_mock ) my_retriever.repo.get_list_of_studies = mock.Mock(return_value=study_list) display_mock.show_message = mock.Mock() # when output = my_retriever.retrieve_all_studies() # then - expected_message1 = "Retrieving all studies" - expected_message2 = "Everything is done" + expected_message1 = "Retrieving all studies..." + expected_message2 = "All retrievals are done." calls = [ call(expected_message1, mock.ANY), call(expected_message2, mock.ANY), - ] # , call(my_study3)] + ] # , call(started_study3)] display_mock.show_message.assert_has_calls(calls) assert output is True diff --git a/tests/unit/retriever/test_server_cleaner.py b/tests/unit/retriever/test_server_cleaner.py index 8ba756a..901ec0c 100644 --- a/tests/unit/retriever/test_server_cleaner.py +++ b/tests/unit/retriever/test_server_cleaner.py @@ -1,102 +1,117 @@ -from copy import copy -from pathlib import Path from unittest import mock import pytest -import antareslauncher.remote_environnement.remote_environment_with_slurm from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, ) from antareslauncher.study_dto import StudyDTO -from antareslauncher.use_cases.retrieve.clean_remote_server import ( - RemoteServerCleaner, - RemoteServerNotCleanException, -) +from antareslauncher.use_cases.retrieve.clean_remote_server import RemoteServerCleaner class TestServerCleaner: - def setup_method(self): - self.remote_env_mock = mock.Mock(spec=RemoteEnvironmentWithSlurm) - self.display_mock = mock.Mock(spec_set=DisplayTerminal) - self.remote_server_cleaner = RemoteServerCleaner( - self.remote_env_mock, self.display_mock - ) - - @pytest.fixture(scope="function") - def downloaded_zip_study(self): - study = StudyDTO( - path=Path("path") / "hello", - started=True, - finished=True, - job_id=42, - local_final_zipfile_path=str(Path("final") / "zip" / "path.zip"), - ) - return study - @pytest.mark.unit_test - def test_clean_server_show_message_if_successful(self, downloaded_zip_study): - self.remote_env_mock.clean_remote_server = mock.Mock(return_value=True) - self.remote_server_cleaner.clean(downloaded_zip_study) + def test_clean__finished_study__nominal(self, finished_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.clean_remote_server.return_value = True + display = mock.Mock(spec=DisplayTerminal) + + # Prepare a fake + finished_study.local_final_zipfile_path = "/path/to/result.zip" - expected_message = ( - f'"{downloaded_zip_study.name}": Clean remote server finished' - ) - self.display_mock.show_message.assert_called_once_with( - expected_message, mock.ANY - ) + # Initialize and execute the cleaning + cleaner = RemoteServerCleaner(env, display) + cleaner.clean(finished_study) + + # Check the result + env.clean_remote_server.assert_called_once() + display.show_message.assert_called() + display.show_error.assert_not_called() + + assert finished_study.remote_server_is_clean @pytest.mark.unit_test - def test_clean_server_show_error_and_raise_exception_if_fails( - self, downloaded_zip_study - ): - self.remote_env_mock.clean_remote_server = mock.Mock(return_value=False) + def test_clean__finished_study__no_result(self, finished_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.clean_remote_server.return_value = True + display = mock.Mock(spec=DisplayTerminal) + + # Prepare a fake + finished_study.local_final_zipfile_path = "" - with pytest.raises(RemoteServerNotCleanException): - self.remote_server_cleaner.clean(downloaded_zip_study) + # Initialize and execute the cleaning + cleaner = RemoteServerCleaner(env, display) + cleaner.clean(finished_study) - expected_error = f'"{downloaded_zip_study.name}": Clean remote server failed' - self.display_mock.show_error.assert_called_once_with(expected_error, mock.ANY) + # Check the result + env.clean_remote_server.assert_not_called() + display.show_message.assert_not_called() + display.show_error.assert_not_called() + + assert not finished_study.remote_server_is_clean @pytest.mark.unit_test - def test_remote_environment_not_called_if_final_zip_not_downloaded(self): - self.remote_env_mock.clean_remote_server = mock.Mock() - study = StudyDTO(path="hello") - study.local_final_zipfile_path = "" - new_study = self.remote_server_cleaner.clean(study) + def test_clean__finished_study__reentrancy(self, finished_study: StudyDTO) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.clean_remote_server.return_value = True + display = mock.Mock(spec=DisplayTerminal) + + # Prepare a fake + finished_study.local_final_zipfile_path = "/path/to/result.zip" - self.remote_env_mock.clean_remote_server.assert_not_called() - assert new_study == study + # Initialize and execute the cleaning twice + cleaner = RemoteServerCleaner(env, display) + cleaner.clean(finished_study) + cleaner.clean(finished_study) - study.local_final_zipfile_path = None - new_study = self.remote_server_cleaner.clean(study) + # Check the result + env.clean_remote_server.assert_called_once() + display.show_message.assert_called() + display.show_error.assert_not_called() - self.remote_env_mock.clean_remote_server.assert_not_called() - assert new_study == study + assert finished_study.remote_server_is_clean @pytest.mark.unit_test - def test_remote_environment_not_called_if_remote_server_is_already_clean( - self, - ): - self.remote_env_mock.clean_remote_server = mock.Mock() - study = StudyDTO(path="hello") - study.local_final_zipfile_path = "hello.zip" - study.remote_server_is_clean = True - new_study = self.remote_server_cleaner.clean(study) - - self.remote_env_mock.clean_remote_server.assert_not_called() - assert new_study == study + def test_clean__finished_study__cleaning_failed( + self, finished_study: StudyDTO + ) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.clean_remote_server.return_value = False + display = mock.Mock(spec=DisplayTerminal) + + # Prepare a fake + finished_study.local_final_zipfile_path = "/path/to/result.zip" + + # Initialize and execute the cleaning + cleaner = RemoteServerCleaner(env, display) + cleaner.clean(finished_study) + + # Check the result + env.clean_remote_server.assert_called_once() + display.show_message.assert_not_called() + display.show_error.assert_called() + + assert finished_study.remote_server_is_clean @pytest.mark.unit_test - def test_remote_environment_is_called_if_final_zip_is_downloaded( - self, downloaded_zip_study - ): - self.remote_env_mock.clean_remote_server = mock.Mock(return_value=True) - expected_study = copy(downloaded_zip_study) - - new_study = self.remote_server_cleaner.clean(downloaded_zip_study) - first_call = self.remote_env_mock.clean_remote_server.call_args_list[0] - first_argument = first_call[0][0] - assert first_argument == expected_study - assert new_study.remote_server_is_clean + def test_clean__finished_study__cleaning_raise( + self, finished_study: StudyDTO + ) -> None: + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.clean_remote_server.side_effect = Exception("cleaning error") + display = mock.Mock(spec=DisplayTerminal) + + # Prepare a fake + finished_study.local_final_zipfile_path = "/path/to/result.zip" + + # Initialize and execute the cleaning + cleaner = RemoteServerCleaner(env, display) + cleaner.clean(finished_study) + + # Check the result + env.clean_remote_server.assert_called_once() + display.show_message.assert_not_called() + display.show_error.assert_called() + + assert finished_study.remote_server_is_clean diff --git a/tests/unit/retriever/test_state_updater.py b/tests/unit/retriever/test_state_updater.py index a85a7d8..9ab1639 100644 --- a/tests/unit/retriever/test_state_updater.py +++ b/tests/unit/retriever/test_state_updater.py @@ -1,9 +1,12 @@ -from pathlib import Path from unittest import mock from unittest.mock import call import pytest +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.remote_environnement.remote_environment_with_slurm import ( + RemoteEnvironmentWithSlurm, +) from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.retrieve.state_updater import StateUpdater @@ -21,130 +24,115 @@ def test_given_a_submitted_study_then_study_flags_are_updated( started_flag, finished_flag, with_error_flag, status ): - # given - my_study = StudyDTO(path="study_path", job_id=42) - remote_env_mock = mock.Mock() - display = mock.Mock() - remote_env_mock.get_job_state_flags = mock.Mock( + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.get_job_state_flags = mock.Mock( return_value=(started_flag, finished_flag, with_error_flag) ) - display.show_message = mock.Mock() - state_updater = StateUpdater(remote_env_mock, display) - message = f'"{Path(my_study.path).name}" (JOBID={my_study.job_id}): {status}' - # when - study_test = state_updater.run(my_study) - # then - remote_env_mock.get_job_state_flags.assert_called_once_with(my_study) + display = mock.Mock(spec=DisplayTerminal) + + my_study = StudyDTO(path="study_path", job_id=42) + state_updater = StateUpdater(env, display) + state_updater.run(my_study) + + message = f'"{my_study.name}": (JOBID={my_study.job_id}): {status}' + display.show_message.assert_called_once_with(message, mock.ANY) - assert study_test.started == started_flag - assert study_test.finished == finished_flag - assert study_test.with_error == with_error_flag - assert study_test.job_state == status + assert my_study.started == started_flag + assert my_study.finished == finished_flag + assert my_study.with_error == with_error_flag + assert my_study.job_state == status @pytest.mark.unit_test def test_given_a_non_submitted_study_then_get_job_state_flags_is_not_called(): # given + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + display = mock.Mock(spec=DisplayTerminal) + my_study = StudyDTO(path="study_path", job_id=None) - remote_env_mock = mock.Mock() - remote_env_mock.get_job_state_flags = mock.Mock() - display = mock.Mock() - display.show_error = mock.Mock() - state_updater = StateUpdater(remote_env_mock, display) - message = f'"{Path(my_study.path).name}": Job was not submitted' - # when + state_updater = StateUpdater(env, display) state_updater.run(my_study) - # then - remote_env_mock.get_job_state_flags.assert_not_called() + + message = f'"{my_study.name}": Job was NOT submitted' + env.get_job_state_flags.assert_not_called() display.show_error.assert_called_once_with(message, mock.ANY) @pytest.mark.unit_test def test_given_a_done_study_then_get_job_state_flags_is_not_called(): # given + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.get_job_state_flags = mock.Mock(return_value=(True, False, False)) + display = mock.Mock(spec=DisplayTerminal) + my_study = StudyDTO(path="study_path", job_id=42, done=True) - remote_env_mock = mock.Mock() - remote_env_mock.get_job_state_flags = mock.Mock(return_value=(True, False, False)) - display = mock.Mock() - display.show_message = mock.Mock() - state_updater = StateUpdater(remote_env_mock, display) - message = ( - f'"{Path(my_study.path).name}" (JOBID={my_study.job_id}): everything is done' - ) - # when + state_updater = StateUpdater(env, display) state_updater.run(my_study) - # then - remote_env_mock.get_job_state_flags.assert_not_called() + + message = f'"{my_study.name}": (JOBID={my_study.job_id}): everything is done' + env.get_job_state_flags.assert_not_called() display.show_message.assert_called_once_with(message, mock.ANY) @pytest.mark.unit_test def test_state_updater_run_on_empty_list_of_studies_write_one_message(): - # given - study_list = [] - remote_env_mock = mock.Mock() - display = mock.Mock() - state_updater = StateUpdater(remote_env_mock, display) + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + display = mock.Mock(spec=DisplayTerminal) + + state_updater = StateUpdater(env, display) + state_updater.run_on_list([]) + message = "Checking status of the studies:" - # when - state_updater.run_on_list(study_list) - # then display.show_message.assert_called_once_with(message, mock.ANY) @pytest.mark.unit_test def test_with_a_list_of_one_submitted_study_run_on_list_calls_run_once_on_study(): - # given + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.get_job_state_flags = mock.Mock(return_value=(1, 2, 3)) + display = mock.Mock(spec=DisplayTerminal) + my_study1 = StudyDTO(path="study_path1", job_id=1) study_list = [my_study1] - remote_env_mock = mock.Mock() - remote_env_mock.get_job_state_flags = mock.Mock(return_value=(1, 2, 3)) - display = mock.Mock() - state_updater = StateUpdater(remote_env_mock, display) - # when + state_updater = StateUpdater(env, display) state_updater.run_on_list(study_list) - # then - remote_env_mock.get_job_state_flags.assert_called_once_with(my_study1) + + env.get_job_state_flags.assert_called_once_with(my_study1) @pytest.mark.unit_test def test_run_on_list_calls_run_on_all_submitted_studies_of_the_list(): - # given + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.get_job_state_flags = mock.Mock(return_value=(1, 2, 3)) + display = mock.Mock(spec=DisplayTerminal) + my_study1 = StudyDTO(path="study_path1", job_id=1) my_study2 = StudyDTO(path="study_path2", job_id=None) my_study3 = StudyDTO(path="study_path3", job_id=2) study_list = [my_study1, my_study2, my_study3] - remote_env_mock = mock.Mock() - remote_env_mock.get_job_state_flags = mock.Mock(return_value=(1, 2, 3)) - display = mock.Mock() - state_updater = StateUpdater(remote_env_mock, display) - # when + state_updater = StateUpdater(env, display) state_updater.run_on_list(study_list) - # then + calls = [call(my_study1), call(my_study3)] - remote_env_mock.get_job_state_flags.assert_has_calls(calls) + env.get_job_state_flags.assert_has_calls(calls) @pytest.mark.unit_test def test_run_on_list_calls_run_start__processing_studies_that_are_done(): - # given + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.get_job_state_flags = mock.Mock(return_value=(True, False, False)) + display = mock.Mock(spec=DisplayTerminal) + my_study1 = StudyDTO(path="study_path1", job_id=1, done=False) my_study2 = StudyDTO(path="study_path2", job_id=None) my_study3 = StudyDTO(path="study_path3", job_id=2, done=True) study_list = [my_study1, my_study2, my_study3] - remote_env_mock = mock.Mock() - remote_env_mock.get_job_state_flags = mock.Mock(return_value=(True, False, False)) - display = mock.Mock() - display.show_message = mock.Mock() - state_updater = StateUpdater(remote_env_mock, display) - # when + state_updater = StateUpdater(env, display) state_updater.run_on_list(study_list) - # then + welcome_message = "Checking status of the studies:" - message1 = f'"{Path(my_study1.path).name}" (JOBID={my_study1.job_id}): Running' - message3 = ( - f'"{Path(my_study3.path).name}" (JOBID={my_study3.job_id}): everything is done' - ) + message1 = f'"{my_study1.name}": (JOBID={my_study1.job_id}): Running' + message3 = f'"{my_study3.name}": (JOBID={my_study3.job_id}): everything is done' calls = [ call(welcome_message, mock.ANY), call(message3, mock.ANY), diff --git a/tests/unit/retriever/test_study_retriever.py b/tests/unit/retriever/test_study_retriever.py index f575a02..58b8e1e 100644 --- a/tests/unit/retriever/test_study_retriever.py +++ b/tests/unit/retriever/test_study_retriever.py @@ -5,16 +5,12 @@ from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.data_repo.data_reporter import DataReporter from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.remote_environnement.remote_environment_with_slurm import ( RemoteEnvironmentWithSlurm, ) from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.retrieve.clean_remote_server import RemoteServerCleaner -from antareslauncher.use_cases.retrieve.download_final_zip import ( - FinalZipDownloader, - FinalZipNotDownloadedException, -) +from antareslauncher.use_cases.retrieve.download_final_zip import FinalZipDownloader from antareslauncher.use_cases.retrieve.final_zip_extractor import FinalZipExtractor from antareslauncher.use_cases.retrieve.log_downloader import LogDownloader from antareslauncher.use_cases.retrieve.state_updater import StateUpdater @@ -25,14 +21,13 @@ class TestStudyRetriever: def setup_method(self): env = mock.Mock(spec_set=RemoteEnvironmentWithSlurm) display = mock.Mock(spec_set=DisplayTerminal) - file_manager = mock.Mock(spec_set=FileManager) repo = mock.Mock(spec_set=DataRepoTinydb) self.reporter = DataReporter(repo) self.state_updater = StateUpdater(env, display) - self.logs_downloader = LogDownloader(env, file_manager, display) + self.logs_downloader = LogDownloader(env, display) self.final_zip_downloader = FinalZipDownloader(env, display) self.remote_server_cleaner = RemoteServerCleaner(env, display) - self.zip_extractor = FinalZipExtractor(file_manager, display) + self.zip_extractor = FinalZipExtractor(display) self.study_retriever = StudyRetriever( self.state_updater, self.logs_downloader, @@ -132,45 +127,3 @@ def zip_extractor_extract_final_zip(study_: StudyDTO): final_zip_extracted=True, ) self.reporter.save_study.assert_called_once_with(expected) - - @pytest.mark.unit_test - def test_retrieve_study__exception(self): - """ - This test case specifically checks the behavior of the `retrieve` method - in the presence of an exception. It verifies that the study object and - its components are updated correctly when this exception occurs. - """ - study = StudyDTO(path="hello") - - def state_updater_run(study_: StudyDTO): - study_.job_id = 42 - study_.started = True - study_.finished = True - study_.with_error = False - return study_ - - self.state_updater.run = mock.Mock(side_effect=state_updater_run) - self.logs_downloader.run = mock.Mock( - side_effect=FinalZipNotDownloadedException("download fails") - ) - self.final_zip_downloader.download = mock.Mock() - self.remote_server_cleaner.clean = mock.Mock() - self.zip_extractor.extract_final_zip = mock.Mock() - self.reporter.save_study = mock.Mock(return_value=True) - - self.study_retriever.retrieve(study) - - expected = StudyDTO( - path="hello", - job_id=42, - done=True, - started=True, - finished=True, - with_error=True, - logs_downloaded=False, - local_final_zipfile_path="", - remote_server_is_clean=False, - final_zip_extracted=False, - job_state="Internal error: download fails", - ) - self.reporter.save_study.assert_called_once_with(expected) From fbb60e0efca6989e7ea79324ed746b55da3cfb3d Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Fri, 22 Sep 2023 14:00:26 +0200 Subject: [PATCH 11/23] refactor(file-manager): remove unused or trivial methods from `FileManager` --- antareslauncher/file_manager/file_manager.py | 32 ---------- antareslauncher/main.py | 1 - .../create_list/study_list_composer.py | 12 ++-- .../test_integration_study_list_composer.py | 47 --------------- tests/unit/conftest.py | 60 +++++++++++++++++++ tests/unit/test_file_manager.py | 17 ------ tests/unit/test_study_list_composer.py | 56 ----------------- 7 files changed, 64 insertions(+), 161 deletions(-) delete mode 100644 tests/integration/test_integration_study_list_composer.py create mode 100644 tests/unit/conftest.py diff --git a/antareslauncher/file_manager/file_manager.py b/antareslauncher/file_manager/file_manager.py index 72bb18d..4cc15b3 100644 --- a/antareslauncher/file_manager/file_manager.py +++ b/antareslauncher/file_manager/file_manager.py @@ -26,10 +26,6 @@ def listdir_of(self, directory): list_dir.sort() return list_dir - @staticmethod - def is_dir(dir_path: Path): - return Path(dir_path).is_dir() - def _get_list_dir_without_subdir(self, dir_path, subdir_to_exclude): """Make a list of all the folders inside a directory except one @@ -118,34 +114,6 @@ def zip_dir_excluding_subdir(self, dir_path, zipfile_path, subdir_to_exclude): ) return Path(zipfile_path).is_file() - def unzip(self, file_path: str): - """Unzips the result of the antares job once is has been downloaded - - Args: - file_path: The path to the file - - Returns: - True if the file has been extracted, False otherwise - """ - self.logger.info(f"Unzipping {file_path}") - try: - with zipfile.ZipFile(file=file_path) as zip_file: - progress_bar = self.display.generate_progress_bar( - zip_file.namelist(), - desc="Extracting archive:", - total=len(zip_file.namelist()), - ) - for file in progress_bar: - zip_file.extract( - member=file, - path=Path(file_path).parent, - ) - return True - except ValueError: - return False - except FileNotFoundError: - return False - def make_dir(self, directory_name): self.logger.info(f"Creating directory {directory_name}") os.makedirs(directory_name, exist_ok=True) diff --git a/antareslauncher/main.py b/antareslauncher/main.py index f47fd38..233910d 100644 --- a/antareslauncher/main.py +++ b/antareslauncher/main.py @@ -138,7 +138,6 @@ def run_with( ) study_list_composer = StudyListComposer( repo=data_repo, - file_manager=file_manager, display=display, parameters=StudyListComposerParameters( studies_in_dir=arguments.studies_in, diff --git a/antareslauncher/use_cases/create_list/study_list_composer.py b/antareslauncher/use_cases/create_list/study_list_composer.py index 1a644be..4c25487 100644 --- a/antareslauncher/use_cases/create_list/study_list_composer.py +++ b/antareslauncher/use_cases/create_list/study_list_composer.py @@ -52,12 +52,10 @@ class StudyListComposer: def __init__( self, repo: DataRepoTinydb, - file_manager: FileManager, display: DisplayTerminal, parameters: StudyListComposerParameters, ): self._repo = repo - self._file_manager = file_manager self._display = display self._studies_in_dir = parameters.studies_in_dir self.time_limit = parameters.time_limit @@ -116,10 +114,9 @@ def update_study_database(self): self._new_study_added = False - directories = self._file_manager.listdir_of(self._studies_in_dir) - for directory in directories: - directory_path = Path(self._studies_in_dir) / Path(directory) - if self._file_manager.is_dir(directory_path): + directories = Path(self._studies_in_dir).iterdir() + for directory_path in sorted(directories): + if directory_path.is_dir(): self._update_database_with_directory(directory_path) if not self._new_study_added: @@ -136,8 +133,7 @@ def _update_database_with_new_study( ) self._update_database_with_study(buffer_study) - def _update_database_with_directory(self, directory_path: t.Union[str, Path]): - directory_path = Path(directory_path) + def _update_database_with_directory(self, directory_path: Path): solver_version = get_solver_version(directory_path) antares_version = self.antares_version or solver_version if not antares_version: diff --git a/tests/integration/test_integration_study_list_composer.py b/tests/integration/test_integration_study_list_composer.py deleted file mode 100644 index 889cd8a..0000000 --- a/tests/integration/test_integration_study_list_composer.py +++ /dev/null @@ -1,47 +0,0 @@ -from unittest import mock - -import pytest - -from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb -from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.file_manager.file_manager import FileManager -from antareslauncher.use_cases.create_list.study_list_composer import ( - StudyListComposer, - StudyListComposerParameters, -) - - -class TestIntegrationStudyListComposer: - def setup_method(self): - self.repo = mock.Mock(spec=DataRepoTinydb) - self.file_manager = mock.Mock(spec=FileManager) - self.display = mock.Mock(spec=DisplayTerminal) - self.study_list_composer = StudyListComposer( - repo=self.repo, - file_manager=self.file_manager, - display=self.display, - parameters=StudyListComposerParameters( - studies_in_dir="studies_in", - time_limit=42, - n_cpu=24, - log_dir="job_log_dir", - xpansion_mode=None, - output_dir="output_dir", - post_processing=False, - antares_versions_on_remote_server=["700"], - other_options="", - ), - ) - - @pytest.mark.integration_test - def test_get_list_of_studies(self): - self.study_list_composer.get_list_of_studies() - self.repo.get_list_of_studies.assert_called_once_with() - - @pytest.mark.integration_test - def test_update_study_database(self): - self.file_manager.listdir_of = mock.Mock(return_value=["study1", "study2"]) - self.file_manager.is_dir = mock.Mock(return_value=True) - self.study_list_composer.xpansion_mode = "r" # "r", "cpp" or None - self.study_list_composer.update_study_database() - self.display.show_message.assert_called() diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 0000000..410c237 --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,60 @@ +import shutil +from pathlib import Path +from unittest import mock + +import pytest +from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb +from antareslauncher.display.display_terminal import DisplayTerminal +from antareslauncher.use_cases.create_list.study_list_composer import ( + StudyListComposer, + StudyListComposerParameters, +) +from tests.unit.assets import ASSETS_DIR + + +@pytest.fixture(name="studies_in_dir") +def studies_in_dir_fixture(tmp_path: Path) -> str: + studies_in_dir = tmp_path.joinpath("STUDIES-IN") + assets_dir = ASSETS_DIR.joinpath("study_list_composer/studies") + shutil.copytree(assets_dir, studies_in_dir) + return str(studies_in_dir) + + +@pytest.fixture(name="repo") +def repo_fixture(tmp_path: Path) -> DataRepoTinydb: + return DataRepoTinydb( + database_file_path=tmp_path.joinpath("repo.json"), + db_primary_key="name", + ) + + +@pytest.fixture(name="study_list_composer") +def study_list_composer_fixture( + tmp_path: Path, + repo: DataRepoTinydb, + studies_in_dir: str, +) -> StudyListComposer: + display = mock.Mock(spec=DisplayTerminal) + composer = StudyListComposer( + repo=repo, + display=display, + parameters=StudyListComposerParameters( + studies_in_dir=studies_in_dir, + time_limit=42, + n_cpu=24, + log_dir=str(tmp_path.joinpath("LOGS")), + xpansion_mode=None, + output_dir=str(tmp_path.joinpath("FINISHED")), + post_processing=False, + antares_versions_on_remote_server=[ + "800", + "810", + "820", + "830", + "840", + "850", + ], + other_options="", + ), + ) + return composer diff --git a/tests/unit/test_file_manager.py b/tests/unit/test_file_manager.py index 64f15a4..e751efd 100644 --- a/tests/unit/test_file_manager.py +++ b/tests/unit/test_file_manager.py @@ -7,7 +7,6 @@ from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager import file_manager -from antareslauncher.study_dto import StudyDTO from tests.data import DATA_DIR DIR_TO_ZIP = DATA_DIR / "file-manager-test" / "to-zip" @@ -48,22 +47,6 @@ def test_golden_master_for_zip_study_excluding_output_dir(self, tmp_path): assert result_zip_file.is_file() result_zip_file.unlink() - @pytest.mark.unit_test - def test_unzip(self): - """ - Tests the scenario where the specified zip file does not exist. The expected outcome is - that the function returns False, indicating that the zip file could not be unzipped. - This test is checking that the function correctly handles the case where the input file is not present. - """ - # given - study = StudyDTO("path") - display_terminal = DisplayTerminal() - my_file_manager = file_manager.FileManager(display_terminal) - # when - output = my_file_manager.unzip(study.local_final_zipfile_path) - # then - assert output is False - @pytest.mark.unit_test def test__get_list_dir_without_subdir(self): """ diff --git a/tests/unit/test_study_list_composer.py b/tests/unit/test_study_list_composer.py index b8bec18..1b76ea1 100644 --- a/tests/unit/test_study_list_composer.py +++ b/tests/unit/test_study_list_composer.py @@ -1,18 +1,11 @@ -import shutil from pathlib import Path -from unittest import mock import pytest -from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb -from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.use_cases.create_list.study_list_composer import ( StudyListComposer, - StudyListComposerParameters, get_solver_version, ) -from tests.unit.assets import ASSETS_DIR CONFIG_NOMINAL_VERSION = """\ [antares] @@ -73,55 +66,6 @@ def test_get_solver_version( assert actual == expected -@pytest.fixture(name="studies_in_dir") -def studies_in_dir_fixture(tmp_path: Path) -> str: - studies_in_dir = tmp_path.joinpath("STUDIES-IN") - assets_dir = ASSETS_DIR.joinpath("study_list_composer/studies") - shutil.copytree(assets_dir, studies_in_dir) - return str(studies_in_dir) - - -@pytest.fixture(name="repo") -def repo_fixture(tmp_path: Path) -> DataRepoTinydb: - return DataRepoTinydb( - database_file_path=tmp_path.joinpath("repo.json"), - db_primary_key="name", - ) - - -@pytest.fixture(name="study_list_composer") -def study_list_composer_fixture( - tmp_path: Path, - repo: DataRepoTinydb, - studies_in_dir: str, -) -> StudyListComposer: - display = mock.Mock(spec=DisplayTerminal) - composer = StudyListComposer( - repo=repo, - file_manager=FileManager(display_terminal=display), - display=display, - parameters=StudyListComposerParameters( - studies_in_dir=studies_in_dir, - time_limit=42, - n_cpu=24, - log_dir=str(tmp_path.joinpath("LOGS")), - xpansion_mode=None, - output_dir=str(tmp_path.joinpath("FINISHED")), - post_processing=False, - antares_versions_on_remote_server=[ - "800", - "810", - "820", - "830", - "840", - "850", - ], - other_options="", - ), - ) - return composer - - class TestStudyListComposer: @pytest.mark.parametrize("xpansion_mode", ["r", "cpp", ""]) def test_update_study_database__xpansion_mode( From e243fbab177c46ffc867440b3701d7672566066c Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Fri, 22 Sep 2023 14:52:34 +0200 Subject: [PATCH 12/23] style: reformat source code using iSort and Black --- antareslauncher/antares_launcher.py | 8 +-- antareslauncher/config.py | 28 ++------- antareslauncher/display/display_terminal.py | 4 +- antareslauncher/exceptions.py | 4 +- antareslauncher/file_manager/file_manager.py | 28 +++------ antareslauncher/logger_initializer.py | 8 +-- antareslauncher/main.py | 37 +++-------- antareslauncher/main_option_parser.py | 14 +---- antareslauncher/parameters_reader.py | 4 +- .../remote_environment_with_slurm.py | 62 +++++-------------- .../remote_environnement/ssh_connection.py | 56 +++++------------ .../check_remote_queue/slurm_queue_show.py | 4 +- .../create_list/study_list_composer.py | 24 ++----- .../tree_structure_initializer.py | 4 +- .../use_cases/kill_job/job_kill_controller.py | 8 +-- .../use_cases/launch/launch_controller.py | 8 +-- .../use_cases/launch/study_submitter.py | 4 +- .../use_cases/launch/study_zip_uploader.py | 4 +- .../use_cases/launch/study_zipper.py | 4 +- .../use_cases/retrieve/clean_remote_server.py | 4 +- .../use_cases/retrieve/download_final_zip.py | 10 +-- .../use_cases/retrieve/final_zip_extractor.py | 7 +-- .../use_cases/retrieve/log_downloader.py | 4 +- .../use_cases/retrieve/retrieve_controller.py | 5 +- .../use_cases/retrieve/state_updater.py | 4 +- .../wait_loop_controller/wait_controller.py | 4 +- pyproject.toml | 16 +++++ ...test_integration_check_queue_controller.py | 16 ++--- .../test_integration_job_kill_controller.py | 16 ++--- .../test_integration_launch_controller.py | 14 ++--- tests/unit/conftest.py | 6 +- tests/unit/launcher/test_launch_controller.py | 32 +++------- tests/unit/launcher/test_submitter.py | 16 ++--- tests/unit/launcher/test_zip_uploader.py | 17 ++--- tests/unit/launcher/test_zipper.py | 8 +-- tests/unit/retriever/conftest.py | 12 +--- .../unit/retriever/test_download_final_zip.py | 20 ++---- .../retriever/test_final_zip_extractor.py | 28 +++------ tests/unit/retriever/test_log_downloader.py | 8 +-- .../retriever/test_retrieve_controller.py | 24 ++----- tests/unit/retriever/test_server_cleaner.py | 12 +--- tests/unit/retriever/test_state_updater.py | 12 +--- tests/unit/retriever/test_study_retriever.py | 16 ++--- tests/unit/test_antares_launcher.py | 8 +-- tests/unit/test_check_queue_controller.py | 8 +-- tests/unit/test_config.py | 42 +++---------- tests/unit/test_main_option_parser.py | 10 +-- tests/unit/test_parameters_reader.py | 35 +++-------- .../test_remote_environment_with_slurm.py | 57 +++++------------ tests/unit/test_study_list_composer.py | 5 +- tests/unit/test_wait_controller.py | 4 +- 51 files changed, 205 insertions(+), 588 deletions(-) create mode 100644 pyproject.toml diff --git a/antareslauncher/antares_launcher.py b/antareslauncher/antares_launcher.py index 07a526f..52166d4 100644 --- a/antareslauncher/antares_launcher.py +++ b/antareslauncher/antares_launcher.py @@ -1,16 +1,12 @@ from dataclasses import dataclass from typing import Optional -from antareslauncher.use_cases.check_remote_queue.check_queue_controller import ( - CheckQueueController, -) +from antareslauncher.use_cases.check_remote_queue.check_queue_controller import CheckQueueController from antareslauncher.use_cases.create_list.study_list_composer import StudyListComposer from antareslauncher.use_cases.kill_job.job_kill_controller import JobKillController from antareslauncher.use_cases.launch.launch_controller import LaunchController from antareslauncher.use_cases.retrieve.retrieve_controller import RetrieveController -from antareslauncher.use_cases.wait_loop_controller.wait_controller import ( - WaitController, -) +from antareslauncher.use_cases.wait_loop_controller.wait_controller import WaitController @dataclass diff --git a/antareslauncher/config.py b/antareslauncher/config.py index 760d526..67f2564 100644 --- a/antareslauncher/config.py +++ b/antareslauncher/config.py @@ -12,11 +12,7 @@ import yaml from antareslauncher import __author__, __project_name__, __version__ -from antareslauncher.exceptions import ( - ConfigFileNotFoundError, - InvalidConfigValueError, - UnknownFileSuffixError, -) +from antareslauncher.exceptions import ConfigFileNotFoundError, InvalidConfigValueError, UnknownFileSuffixError APP_NAME = __project_name__ APP_AUTHOR = __author__.split(",")[0] @@ -119,9 +115,7 @@ def load_config(cls, ssh_config_path: pathlib.Path) -> "SSHConfig": obj = parse_config(ssh_config_path) kwargs = {k.lower(): v for k, v in obj.items()} private_key_file = kwargs.pop("private_key_file", None) - kwargs["private_key_file"] = ( - None if private_key_file is None else pathlib.Path(private_key_file) - ) + kwargs["private_key_file"] = None if private_key_file is None else pathlib.Path(private_key_file) try: return cls(config_path=ssh_config_path, **kwargs) except TypeError as exc: @@ -139,11 +133,7 @@ def save_config(self, ssh_config_path: pathlib.Path) -> None: """ obj = dataclasses.asdict(self) del obj["config_path"] - obj = { - k: v - for k, v in obj.items() - if v or k not in {"private_key_file", "key_password", "password"} - } + obj = {k: v for k, v in obj.items() if v or k not in {"private_key_file", "key_password", "password"}} if "private_key_file" in obj: obj["private_key_file"] = obj["private_key_file"].as_posix() dump_config(ssh_config_path, obj) @@ -212,9 +202,7 @@ def load_config(cls, config_path: pathlib.Path) -> "Config": obj = parse_config(config_path) kwargs = {k.lower(): v for k, v in obj.items()} try: - kwargs["remote_solver_versions"] = kwargs.pop( - "antares_versions_on_remote_server" - ) + kwargs["remote_solver_versions"] = kwargs.pop("antares_versions_on_remote_server") # handle paths for key in [ "log_dir", @@ -226,9 +214,7 @@ def load_config(cls, config_path: pathlib.Path) -> "Config": kwargs[key] = pathlib.Path(kwargs[key]) ssh_configfile_name = kwargs.pop("default_ssh_configfile_name") except KeyError as exc: - raise InvalidConfigValueError( - config_path, f"missing parameter '{exc}'" - ) from None + raise InvalidConfigValueError(config_path, f"missing parameter '{exc}'") from None # handle SSH configuration config_dir = config_path.parent ssh_config_path = config_dir.joinpath(ssh_configfile_name) @@ -287,9 +273,7 @@ def get_user_config_dir(system: str = ""): username = getpass.getuser() system = system or sys.platform if system == "win32": - config_dir = pathlib.WindowsPath( - rf"C:\Users\{username}\AppData\Local\{APP_AUTHOR}" - ) + config_dir = pathlib.WindowsPath(rf"C:\Users\{username}\AppData\Local\{APP_AUTHOR}") elif system == "darwin": config_dir = pathlib.PosixPath("~/Library/Preferences").expanduser() else: diff --git a/antareslauncher/display/display_terminal.py b/antareslauncher/display/display_terminal.py index acebffc..cbd0fbc 100644 --- a/antareslauncher/display/display_terminal.py +++ b/antareslauncher/display/display_terminal.py @@ -58,8 +58,6 @@ def generate_progress_bar( desc=desc, leave=False, dynamic_ncols=True, - bar_format="[" - + str(now.strftime(self.format)) - + "] {l_bar}{bar}| {n_fmt}/{total_fmt} ", + bar_format="[" + str(now.strftime(self.format)) + "] {l_bar}{bar}| {n_fmt}/{total_fmt} ", ) return progress_bar diff --git a/antareslauncher/exceptions.py b/antareslauncher/exceptions.py index 201abfa..491e532 100644 --- a/antareslauncher/exceptions.py +++ b/antareslauncher/exceptions.py @@ -12,9 +12,7 @@ class AntaresLauncherException(Exception): class ConfigFileNotFoundError(AntaresLauncherException): """Configuration file not found.""" - def __init__( - self, possible_dirs: Sequence[pathlib.Path], config_name: str, *args - ) -> None: + def __init__(self, possible_dirs: Sequence[pathlib.Path], config_name: str, *args) -> None: super().__init__(possible_dirs, config_name, *args) @property diff --git a/antareslauncher/file_manager/file_manager.py b/antareslauncher/file_manager/file_manager.py index 4cc15b3..ad7d548 100644 --- a/antareslauncher/file_manager/file_manager.py +++ b/antareslauncher/file_manager/file_manager.py @@ -51,9 +51,7 @@ def _get_list_of_files_recursively(self, element_path): Returns: List of all the files inside a directory recursively """ - self.logger.info( - f"Getting list of all files inside the directory {element_path}" - ) + self.logger.info(f"Getting list of all files inside the directory {element_path}") element_file_paths = [] for root, _, files in os.walk(element_path): for filename in files: @@ -71,9 +69,7 @@ def _get_complete_list_of_files_and_dirs_in_list_dir(self, dir_path, list_dir): file_paths.extend(element_file_paths) return file_paths - def zip_file_paths_with_rootdir_to_zipfile_path( - self, zipfile_path, file_paths, root_dir - ): + def zip_file_paths_with_rootdir_to_zipfile_path(self, zipfile_path, file_paths, root_dir): """Zips all the files in file_paths inside zipfile_path while printing a progress bar on the terminal @@ -85,12 +81,8 @@ def zip_file_paths_with_rootdir_to_zipfile_path( root_dir: Root directory """ self.logger.info(f"Zipping list of files to archive {zipfile_path}") - with zipfile.ZipFile( - zipfile_path, "w", compression=zipfile.ZIP_DEFLATED - ) as my_zip: - loading_bar = self.display.generate_progress_bar( - file_paths, desc="Compressing files: " - ) + with zipfile.ZipFile(zipfile_path, "w", compression=zipfile.ZIP_DEFLATED) as my_zip: + loading_bar = self.display.generate_progress_bar(file_paths, desc="Compressing files: ") for f in loading_bar: my_zip.write(f, os.path.relpath(f, root_dir)) @@ -105,13 +97,9 @@ def zip_dir_excluding_subdir(self, dir_path, zipfile_path, subdir_to_exclude): subdir_to_exclude: Subdirectory that will not be zipped """ list_dir = self._get_list_dir_without_subdir(dir_path, subdir_to_exclude) - file_paths = self._get_complete_list_of_files_and_dirs_in_list_dir( - dir_path, list_dir - ) + file_paths = self._get_complete_list_of_files_and_dirs_in_list_dir(dir_path, list_dir) root_dir = str(Path(dir_path).parent) - self.zip_file_paths_with_rootdir_to_zipfile_path( - zipfile_path, file_paths, root_dir - ) + self.zip_file_paths_with_rootdir_to_zipfile_path(zipfile_path, file_paths, root_dir) return Path(zipfile_path).is_file() def make_dir(self, directory_name): @@ -124,9 +112,7 @@ def convert_json_file_to_dict(self, file_path): with open(file_path, "r") as readFile: config = json.load(readFile) except OSError: - self.logger.error( - f"Unable to convert {file_path} to json (file not found or invalid type)" - ) + self.logger.error(f"Unable to convert {file_path} to json (file not found or invalid type)") config = None return config diff --git a/antareslauncher/logger_initializer.py b/antareslauncher/logger_initializer.py index e242193..cca6d11 100644 --- a/antareslauncher/logger_initializer.py +++ b/antareslauncher/logger_initializer.py @@ -13,12 +13,8 @@ def init_logger(self): Returns: """ - formatter = logging.Formatter( - "%(asctime)s - %(levelname)s - %(name)s - %(message)s" - ) - f_handler = RotatingFileHandler( - self.file_path, maxBytes=200000, backupCount=5, mode="a+" - ) + formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + f_handler = RotatingFileHandler(self.file_path, maxBytes=200000, backupCount=5, mode="a+") f_handler.setFormatter(formatter) f_handler.setLevel(logging.DEBUG) logging.basicConfig(level=logging.INFO, handlers=[f_handler]) diff --git a/antareslauncher/main.py b/antareslauncher/main.py index 233910d..2f2eb77 100644 --- a/antareslauncher/main.py +++ b/antareslauncher/main.py @@ -10,30 +10,17 @@ from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.logger_initializer import LoggerInitializer from antareslauncher.remote_environnement import ssh_connection -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) -from antareslauncher.remote_environnement.slurm_script_features import ( - SlurmScriptFeatures, -) -from antareslauncher.use_cases.check_remote_queue.check_queue_controller import ( - CheckQueueController, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm +from antareslauncher.remote_environnement.slurm_script_features import SlurmScriptFeatures +from antareslauncher.use_cases.check_remote_queue.check_queue_controller import CheckQueueController from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import SlurmQueueShow -from antareslauncher.use_cases.create_list.study_list_composer import ( - StudyListComposer, - StudyListComposerParameters, -) -from antareslauncher.use_cases.generate_tree_structure.tree_structure_initializer import ( - TreeStructureInitializer, -) +from antareslauncher.use_cases.create_list.study_list_composer import StudyListComposer, StudyListComposerParameters +from antareslauncher.use_cases.generate_tree_structure.tree_structure_initializer import TreeStructureInitializer from antareslauncher.use_cases.kill_job.job_kill_controller import JobKillController from antareslauncher.use_cases.launch.launch_controller import LaunchController from antareslauncher.use_cases.retrieve.retrieve_controller import RetrieveController from antareslauncher.use_cases.retrieve.state_updater import StateUpdater -from antareslauncher.use_cases.wait_loop_controller.wait_controller import ( - WaitController, -) +from antareslauncher.use_cases.wait_loop_controller.wait_controller import WaitController class NoJsonConfigFileError(Exception): @@ -88,9 +75,7 @@ class MainParameters: quality_of_service: str = "" -def run_with( - arguments: argparse.Namespace, parameters: MainParameters, show_banner=False -): +def run_with(arguments: argparse.Namespace, parameters: MainParameters, show_banner=False): """Instantiates all the objects necessary to antares-launcher, and runs the program""" if arguments.version: print(f"Antares_Launcher v{__version__}") @@ -113,9 +98,7 @@ def run_with( ) tree_structure_initializer.init_tree_structure() - logger_initializer = LoggerInitializer( - str(Path(arguments.log_dir) / "antares_launcher.log") - ) + logger_initializer = LoggerInitializer(str(Path(arguments.log_dir) / "antares_launcher.log")) logger_initializer.init_logger() # connection @@ -133,9 +116,7 @@ def run_with( quality_of_service=parameters.quality_of_service, ) environment = RemoteEnvironmentWithSlurm(connection, slurm_script_features) - data_repo = DataRepoTinydb( - database_file_path=db_json_file_path, db_primary_key=parameters.db_primary_key - ) + data_repo = DataRepoTinydb(database_file_path=db_json_file_path, db_primary_key=parameters.db_primary_key) study_list_composer = StudyListComposer( repo=data_repo, display=display, diff --git a/antareslauncher/main_option_parser.py b/antareslauncher/main_option_parser.py index e37160a..b9780de 100644 --- a/antareslauncher/main_option_parser.py +++ b/antareslauncher/main_option_parser.py @@ -49,9 +49,7 @@ def __init__(self, parameters: ParserParameters) -> None: def parse_args(self, args: t.Union[t.Sequence[str], None]) -> argparse.Namespace: return self.parser.parse_args(args) - def add_basic_arguments( - self, *, antares_versions: t.Sequence[str] = () - ) -> MainOptionParser: + def add_basic_arguments(self, *, antares_versions: t.Sequence[str] = ()) -> MainOptionParser: """Adds to the parser all the arguments for the light mode""" self.parser.add_argument( "-w", @@ -234,15 +232,9 @@ def look_for_default_ssh_conf_file( Returns: path to the ssh config file is it exists, None otherwise """ - if ( - parameters.ssh_configfile_path_alternate1 - and parameters.ssh_configfile_path_alternate1.is_file() - ): + if parameters.ssh_configfile_path_alternate1 and parameters.ssh_configfile_path_alternate1.is_file(): return parameters.ssh_configfile_path_alternate1 - elif ( - parameters.ssh_configfile_path_alternate2 - and parameters.ssh_configfile_path_alternate2.is_file() - ): + elif parameters.ssh_configfile_path_alternate2 and parameters.ssh_configfile_path_alternate2.is_file(): return parameters.ssh_configfile_path_alternate2 else: return None diff --git a/antareslauncher/parameters_reader.py b/antareslauncher/parameters_reader.py index 0b94695..6e04777 100644 --- a/antareslauncher/parameters_reader.py +++ b/antareslauncher/parameters_reader.py @@ -87,7 +87,5 @@ def _get_ssh_dict_from_json(self) -> t.Dict[str, t.Any]: with open(self.json_ssh_conf) as ssh_connection_json: ssh_dict = json.load(ssh_connection_json) if "private_key_file" in ssh_dict: - ssh_dict["private_key_file"] = os.path.expanduser( - ssh_dict["private_key_file"] - ) + ssh_dict["private_key_file"] = os.path.expanduser(ssh_dict["private_key_file"]) return ssh_dict diff --git a/antareslauncher/remote_environnement/remote_environment_with_slurm.py b/antareslauncher/remote_environnement/remote_environment_with_slurm.py index d1f0a03..90b8e4a 100644 --- a/antareslauncher/remote_environnement/remote_environment_with_slurm.py +++ b/antareslauncher/remote_environnement/remote_environment_with_slurm.py @@ -9,10 +9,7 @@ import typing as t from pathlib import Path, PurePosixPath -from antareslauncher.remote_environnement.slurm_script_features import ( - ScriptParametersDTO, - SlurmScriptFeatures, -) +from antareslauncher.remote_environnement.slurm_script_features import ScriptParametersDTO, SlurmScriptFeatures from antareslauncher.remote_environnement.ssh_connection import SshConnection from antareslauncher.study_dto import StudyDTO @@ -25,20 +22,13 @@ class RemoteEnvBaseError(Exception): class GetJobStateError(RemoteEnvBaseError): def __init__(self, job_id: int, job_name: str, reason: str): - msg = ( - f"Unable to retrieve the status of the SLURM job {job_id}" - f" (study job '{job_name})." - f" {reason}" - ) + msg = f"Unable to retrieve the status of the SLURM job {job_id} (study job '{job_name}). {reason}" super().__init__(msg) class JobNotFoundError(RemoteEnvBaseError): def __init__(self, job_id: int, job_name: str): - msg = ( - f"Unable to retrieve the status of the SLURM job {job_id}" - f" (study job '{job_name}): Job not found." - ) + msg = f"Unable to retrieve the status of the SLURM job {job_id} (study job '{job_name}): Job not found." super().__init__(msg) @@ -141,9 +131,7 @@ def __init__( def _initialise_remote_path(self): remote_home_dir = PurePosixPath(self.connection.home_dir) - remote_base_path = remote_home_dir.joinpath( - f"REMOTE_{getpass.getuser()}_{socket.gethostname()}" - ) + remote_base_path = remote_home_dir.joinpath(f"REMOTE_{getpass.getuser()}_{socket.gethostname()}") self.remote_base_path = str(remote_base_path) if not self.connection.make_dir(self.remote_base_path): raise NoRemoteBaseDirError(remote_base_path) @@ -212,9 +200,7 @@ def submit_job(self, my_study: StudyDTO): Raises: SubmitJobErrorException if the job has not been successfully submitted """ - time_limit = self.convert_time_limit_from_seconds_to_minutes( - my_study.time_limit - ) + time_limit = self.convert_time_limit_from_seconds_to_minutes(my_study.time_limit) script_params = ScriptParametersDTO( study_dir_name=Path(my_study.path).name, input_zipfile_name=Path(my_study.zipfile_path).name, @@ -233,15 +219,10 @@ def submit_job(self, my_study: StudyDTO): raise SubmitJobError(my_study.name, reason) # should match "Submitted batch job 123456" - if match := re.match( - r"Submitted.*?(?P\d+)", output, flags=re.IGNORECASE - ): + if match := re.match(r"Submitted.*?(?P\d+)", output, flags=re.IGNORECASE): return int(match["job_id"]) - reason = ( - f"The command [{command}] return an non-parsable output:" - f"\n{textwrap.indent(output, 'OUTPUT> ')}" - ) + reason = f"The command [{command}] return an non-parsable output:\n{textwrap.indent(output, 'OUTPUT> ')}" raise SubmitJobError(my_study.name, reason) def get_job_state_flags( @@ -270,8 +251,7 @@ def get_job_state_flags( if job_state is None: # noinspection SpellCheckingInspection logger.info( - f"Job '{study.job_id}' no longer active in SLURM," - f" the job status is read from the SACCT database..." + f"Job '{study.job_id}' no longer active in SLURM, the job status is read from the SACCT database..." ) job_state = self._retrieve_slurm_acct_state( study.job_id, @@ -333,10 +313,7 @@ def _retrieve_slurm_control_state( if match := re.search(r"JobState=(\w+)", output): return JobStateCodes(match[1]) - reason = ( - f"The command [{command}] return an non-parsable output:" - f"\n{textwrap.indent(output, 'OUTPUT> ')}" - ) + reason = f"The command [{command}] return an non-parsable output:\n{textwrap.indent(output, 'OUTPUT> ')}" raise GetJobStateError(job_id, job_name, reason) def _retrieve_slurm_acct_state( @@ -374,10 +351,7 @@ def _retrieve_slurm_acct_state( last_error = error time.sleep(sleep_time) else: - reason = ( - f"The command [{command}] failed after {attempts} attempts:" - f" {last_error}" - ) + reason = f"The command [{command}] failed after {attempts} attempts: {last_error}" raise GetJobStateError(job_id, job_name, reason) # When the output is empty it mean that the job is not found @@ -395,10 +369,7 @@ def _retrieve_slurm_acct_state( job_state_str = re.match(r"(\w+)", out_state)[1] return JobStateCodes(job_state_str) - reason = ( - f"The command [{command}] return an non-parsable output:" - f"\n{textwrap.indent(output, 'OUTPUT> ')}" - ) + reason = f"The command [{command}] return an non-parsable output:\n{textwrap.indent(output, 'OUTPUT> ')}" raise GetJobStateError(job_id, job_name, reason) def upload_file(self, src) -> bool: @@ -474,9 +445,7 @@ def remove_input_zipfile(self, study: StudyDTO) -> bool: """ if not study.input_zipfile_removed: zip_name = Path(study.zipfile_path).name - study.input_zipfile_removed = self.connection.remove_file( - f"{self.remote_base_path}/{zip_name}" - ) + study.input_zipfile_removed = self.connection.remove_file(f"{self.remote_base_path}/{zip_name}") return study.input_zipfile_removed def remove_remote_final_zipfile(self, study: StudyDTO) -> bool: @@ -488,9 +457,7 @@ def remove_remote_final_zipfile(self, study: StudyDTO) -> bool: Returns: True if the file has been successfully removed, False otherwise """ - return self.connection.remove_file( - f"{self.remote_base_path}/{Path(study.local_final_zipfile_path).name}" - ) + return self.connection.remove_file(f"{self.remote_base_path}/{Path(study.local_final_zipfile_path).name}") def clean_remote_server(self, study: StudyDTO) -> bool: """ @@ -505,6 +472,5 @@ def clean_remote_server(self, study: StudyDTO) -> bool: return ( False if study.remote_server_is_clean - else self.remove_remote_final_zipfile(study) - & self.remove_input_zipfile(study) + else self.remove_remote_final_zipfile(study) & self.remove_input_zipfile(study) ) diff --git a/antareslauncher/remote_environnement/ssh_connection.py b/antareslauncher/remote_environnement/ssh_connection.py index e74b98b..7335bda 100644 --- a/antareslauncher/remote_environnement/ssh_connection.py +++ b/antareslauncher/remote_environnement/ssh_connection.py @@ -109,9 +109,7 @@ def __str__(self) -> str: # 0 duration total_duration # 0% percent 100% duration = time.time() - self._start_time - eta = int( - duration * (self.total_size - total_transferred) / total_transferred - ) + eta = int(duration * (self.total_size - total_transferred) / total_transferred) return f"{self.msg:<20} ETA: {eta}s [{rate:.0%}]" return f"{self.msg:<20} ETA: ??? [{rate:.0%}]" @@ -154,15 +152,11 @@ def __init__(self, config: dict = None): self.logger.info("Loading ssh connection from config dictionary") self.__init_from_config(config) else: - error = InvalidConfigError( - config, "missing values: 'hostname', 'username', 'password'..." - ) + error = InvalidConfigError(config, "missing values: 'hostname', 'username', 'password'...") self.logger.debug(str(error)) raise error self.initialize_home_dir() - self.logger.info( - f"Connection created with host = {self.host} and username = {self.username}" - ) + self.logger.info(f"Connection created with host = {self.host} and username = {self.username}") def __initialise_public_key(self, key_file_name, key_password): """Initialises self.private_key @@ -174,15 +168,11 @@ def __initialise_public_key(self, key_file_name, key_password): True if a valid key was found, False otherwise """ try: - self.private_key = paramiko.Ed25519Key.from_private_key_file( - filename=key_file_name - ) + self.private_key = paramiko.Ed25519Key.from_private_key_file(filename=key_file_name) return True except paramiko.SSHException: try: - self.private_key = paramiko.RSAKey.from_private_key_file( - filename=key_file_name, password=key_password - ) + self.private_key = paramiko.RSAKey.from_private_key_file(filename=key_file_name, password=key_password) return True except paramiko.SSHException: self.private_key = None @@ -195,9 +185,7 @@ def __init_from_config(self, config: dict): self.password = config.get("password") key_password = config.get("key_password") if key_file := config.get("private_key_file"): - self.__initialise_public_key( - key_file_name=key_file, key_password=key_password - ) + self.__initialise_public_key(key_file_name=key_file, key_password=key_password) elif self.password is None: error = InvalidConfigError(config, "missing 'password'") self.logger.debug(str(error)) @@ -250,27 +238,17 @@ def ssh_client(self) -> paramiko.SSHClient: look_for_keys=False, ) except paramiko.AuthenticationException as e: - self.logger.exception( - f"paramiko.AuthenticationException: {paramiko.AuthenticationException}" - ) - raise ConnectionFailedException( - self.host, self.port, self.username - ) from e + self.logger.exception(f"paramiko.AuthenticationException: {paramiko.AuthenticationException}") + raise ConnectionFailedException(self.host, self.port, self.username) from e except paramiko.SSHException as e: self.logger.exception(f"paramiko.SSHException: {paramiko.SSHException}") - raise ConnectionFailedException( - self.host, self.port, self.username - ) from e + raise ConnectionFailedException(self.host, self.port, self.username) from e except socket.timeout as e: self.logger.exception(f"socket.timeout: {socket.timeout}") - raise ConnectionFailedException( - self.host, self.port, self.username - ) from e + raise ConnectionFailedException(self.host, self.port, self.username) from e except socket.error as e: self.logger.exception(f"socket.error: {socket.error}") - raise ConnectionFailedException( - self.host, self.port, self.username - ) from e + raise ConnectionFailedException(self.host, self.port, self.username) from e yield client finally: @@ -394,9 +372,7 @@ def download_files( The paths of the downloaded files on the local filesystem. """ try: - return self._download_files( - src_dir, dst_dir, (pattern,) + patterns, remove=remove - ) + return self._download_files(src_dir, dst_dir, (pattern,) + patterns, remove=remove) except TimeoutError as exc: self.logger.error(f"Timeout: {exc}", exc_info=True) return [] @@ -432,17 +408,13 @@ def _download_files( The paths of the downloaded files on the local filesystem. """ with self.ssh_client() as client: - with contextlib.closing( - client.open_sftp() - ) as sftp: # type: paramiko.sftp_client.SFTPClient + with contextlib.closing(client.open_sftp()) as sftp: # type: paramiko.sftp_client.SFTPClient # Get list of files to download remote_attrs = sftp.listdir_attr(str(src_dir)) remote_files = [file_attr.filename for file_attr in remote_attrs] total_size = sum((file_attr.st_size or 0) for file_attr in remote_attrs) files_to_download = [ - f - for f in remote_files - if any(fnmatch.fnmatch(f, pattern) for pattern in patterns) + f for f in remote_files if any(fnmatch.fnmatch(f, pattern) for pattern in patterns) ] # Monitor the download progression monitor = DownloadMonitor(total_size, logger=self.logger) diff --git a/antareslauncher/use_cases/check_remote_queue/slurm_queue_show.py b/antareslauncher/use_cases/check_remote_queue/slurm_queue_show.py index c9563db..f27c0b2 100644 --- a/antareslauncher/use_cases/check_remote_queue/slurm_queue_show.py +++ b/antareslauncher/use_cases/check_remote_queue/slurm_queue_show.py @@ -1,9 +1,7 @@ from dataclasses import dataclass from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm @dataclass diff --git a/antareslauncher/use_cases/create_list/study_list_composer.py b/antareslauncher/use_cases/create_list/study_list_composer.py index 4c25487..6220226 100644 --- a/antareslauncher/use_cases/create_list/study_list_composer.py +++ b/antareslauncher/use_cases/create_list/study_list_composer.py @@ -68,9 +68,7 @@ def __init__( self.antares_version = parameters.antares_version self._new_study_added = False self.DEFAULT_JOB_LOG_DIR_PATH = str(Path(self.log_dir) / "JOB_LOGS") - self.ANTARES_VERSIONS_ON_REMOTE_SERVER = [ - int(v) for v in parameters.antares_versions_on_remote_server - ] + self.ANTARES_VERSIONS_ON_REMOTE_SERVER = [int(v) for v in parameters.antares_versions_on_remote_server] def get_list_of_studies(self): """Retrieve the list of studies from the repo @@ -125,12 +123,8 @@ def update_study_database(self): f"{__name__}.{__class__.__name__}", ) - def _update_database_with_new_study( - self, antares_version, directory_path, xpansion_mode: str - ): - buffer_study = self._create_study( - directory_path, antares_version, xpansion_mode - ) + def _update_database_with_new_study(self, antares_version, directory_path, xpansion_mode: str): + buffer_study = self._create_study(directory_path, antares_version, xpansion_mode) self._update_database_with_study(buffer_study) def _update_database_with_directory(self, directory_path: Path): @@ -151,21 +145,15 @@ def _update_database_with_directory(self, directory_path: Path): __name__ + "." + self.__class__.__name__, ) else: - candidates_file_path = directory_path.joinpath( - "user", "expansion", "candidates.ini" - ) + candidates_file_path = directory_path.joinpath("user", "expansion", "candidates.ini") is_xpansion_study = candidates_file_path.is_file() xpansion_mode = is_xpansion_study and self.xpansion_mode - valid_xpansion_candidate = ( - self.xpansion_mode in ["r", "cpp"] and is_xpansion_study - ) + valid_xpansion_candidate = self.xpansion_mode in ["r", "cpp"] and is_xpansion_study valid_antares_candidate = not self.xpansion_mode if valid_antares_candidate or valid_xpansion_candidate: - self._update_database_with_new_study( - antares_version, directory_path, xpansion_mode - ) + self._update_database_with_new_study(antares_version, directory_path, xpansion_mode) def _update_database_with_study(self, buffer_study): if not self._repo.is_study_inside_database(buffer_study): diff --git a/antareslauncher/use_cases/generate_tree_structure/tree_structure_initializer.py b/antareslauncher/use_cases/generate_tree_structure/tree_structure_initializer.py index 5ed387e..df933a1 100644 --- a/antareslauncher/use_cases/generate_tree_structure/tree_structure_initializer.py +++ b/antareslauncher/use_cases/generate_tree_structure/tree_structure_initializer.py @@ -17,6 +17,4 @@ def init_tree_structure(self): self.file_manager.make_dir(self.studies_in) self.file_manager.make_dir(self.log_dir) self.file_manager.make_dir(self.finished) - self.display.show_message( - "Tree structure initialized", __name__ + "." + __class__.__name__ - ) + self.display.show_message("Tree structure initialized", __name__ + "." + __class__.__name__) diff --git a/antareslauncher/use_cases/kill_job/job_kill_controller.py b/antareslauncher/use_cases/kill_job/job_kill_controller.py index f6e9a83..a6a56bb 100644 --- a/antareslauncher/use_cases/kill_job/job_kill_controller.py +++ b/antareslauncher/use_cases/kill_job/job_kill_controller.py @@ -2,9 +2,7 @@ from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm @dataclass @@ -23,9 +21,7 @@ def kill_job(self, job_id: int): job_id: The ID of the slurm job to be killed """ if self._check_if_job_is_killable(job_id): - self.display.show_message( - f"Killing job {job_id}", __name__ + "." + self.__class__.__name__ - ) + self.display.show_message(f"Killing job {job_id}", __name__ + "." + self.__class__.__name__) self.env.kill_remote_job(job_id) else: self.display.show_message( diff --git a/antareslauncher/use_cases/launch/launch_controller.py b/antareslauncher/use_cases/launch/launch_controller.py index add508f..45b8067 100644 --- a/antareslauncher/use_cases/launch/launch_controller.py +++ b/antareslauncher/use_cases/launch/launch_controller.py @@ -2,9 +2,7 @@ from antareslauncher.data_repo.data_reporter import DataReporter from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager.file_manager import FileManager -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.launch.study_submitter import StudySubmitter from antareslauncher.use_cases.launch.study_zip_cleaner import StudyZipCleaner @@ -38,9 +36,7 @@ def _upload_zipfile(self): def _remove_input_zipfile(self): if self._current_study.zip_is_sent is True: - self._current_study = self._zipfile_cleaner.remove_input_zipfile( - self._current_study - ) + self._current_study = self._zipfile_cleaner.remove_input_zipfile(self._current_study) self.reporter.save_study(self._current_study) def _submit_job(self): diff --git a/antareslauncher/use_cases/launch/study_submitter.py b/antareslauncher/use_cases/launch/study_submitter.py index 0d187bf..b0af500 100644 --- a/antareslauncher/use_cases/launch/study_submitter.py +++ b/antareslauncher/use_cases/launch/study_submitter.py @@ -2,9 +2,7 @@ from pathlib import Path from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO diff --git a/antareslauncher/use_cases/launch/study_zip_uploader.py b/antareslauncher/use_cases/launch/study_zip_uploader.py index 9b019cd..4559b4c 100644 --- a/antareslauncher/use_cases/launch/study_zip_uploader.py +++ b/antareslauncher/use_cases/launch/study_zip_uploader.py @@ -2,9 +2,7 @@ from pathlib import Path from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO diff --git a/antareslauncher/use_cases/launch/study_zipper.py b/antareslauncher/use_cases/launch/study_zipper.py index 69b1212..d62188a 100644 --- a/antareslauncher/use_cases/launch/study_zipper.py +++ b/antareslauncher/use_cases/launch/study_zipper.py @@ -24,9 +24,7 @@ def zip(self, study) -> StudyDTO: def _do_zip(self): zipfile_path = f"{self._current_study.path}-{getpass.getuser()}.zip" - success = self.file_manager.zip_dir_excluding_subdir( - self._current_study.path, zipfile_path, None - ) + success = self.file_manager.zip_dir_excluding_subdir(self._current_study.path, zipfile_path, None) if success is True: self._current_study.zipfile_path = zipfile_path self._display_success_message() diff --git a/antareslauncher/use_cases/retrieve/clean_remote_server.py b/antareslauncher/use_cases/retrieve/clean_remote_server.py index 22f5d46..05391b4 100644 --- a/antareslauncher/use_cases/retrieve/clean_remote_server.py +++ b/antareslauncher/use_cases/retrieve/clean_remote_server.py @@ -1,7 +1,5 @@ from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO LOG_NAME = f"{__name__}.RemoteServerCleaner" diff --git a/antareslauncher/use_cases/retrieve/download_final_zip.py b/antareslauncher/use_cases/retrieve/download_final_zip.py index 86f814a..6179573 100644 --- a/antareslauncher/use_cases/retrieve/download_final_zip.py +++ b/antareslauncher/use_cases/retrieve/download_final_zip.py @@ -1,9 +1,7 @@ from pathlib import Path from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO LOG_NAME = f"{__name__}.FinalZipDownloader" @@ -30,11 +28,7 @@ def download(self, study: StudyDTO): The updated data transfer object, with its `local_final_zipfile_path` attribute set if the download was successful. """ - if ( - study.finished - and not study.with_error - and not study.local_final_zipfile_path - ): + if study.finished and not study.with_error and not study.local_final_zipfile_path: self._display.show_message( f'"{study.name}": downloading final ZIP...', LOG_NAME, diff --git a/antareslauncher/use_cases/retrieve/final_zip_extractor.py b/antareslauncher/use_cases/retrieve/final_zip_extractor.py index 1a4fd26..e5810ef 100644 --- a/antareslauncher/use_cases/retrieve/final_zip_extractor.py +++ b/antareslauncher/use_cases/retrieve/final_zip_extractor.py @@ -19,12 +19,7 @@ def extract_final_zip(self, study: StudyDTO) -> None: Args: study: The current study """ - if ( - study.finished - and not study.with_error - and study.local_final_zipfile_path - and not study.final_zip_extracted - ): + if study.finished and not study.with_error and study.local_final_zipfile_path and not study.final_zip_extracted: zip_path = Path(study.local_final_zipfile_path) target_dir = zip_path.with_suffix("") try: diff --git a/antareslauncher/use_cases/retrieve/log_downloader.py b/antareslauncher/use_cases/retrieve/log_downloader.py index 065ad6b..8339145 100644 --- a/antareslauncher/use_cases/retrieve/log_downloader.py +++ b/antareslauncher/use_cases/retrieve/log_downloader.py @@ -1,9 +1,7 @@ from pathlib import Path from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO LOG_NAME = f"{__name__}.LogDownloader" diff --git a/antareslauncher/use_cases/retrieve/retrieve_controller.py b/antareslauncher/use_cases/retrieve/retrieve_controller.py index b936b91..a6fab0d 100644 --- a/antareslauncher/use_cases/retrieve/retrieve_controller.py +++ b/antareslauncher/use_cases/retrieve/retrieve_controller.py @@ -1,10 +1,7 @@ from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.data_repo.data_reporter import DataReporter from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.file_manager.file_manager import FileManager -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.use_cases.retrieve.clean_remote_server import RemoteServerCleaner from antareslauncher.use_cases.retrieve.download_final_zip import FinalZipDownloader from antareslauncher.use_cases.retrieve.final_zip_extractor import FinalZipExtractor diff --git a/antareslauncher/use_cases/retrieve/state_updater.py b/antareslauncher/use_cases/retrieve/state_updater.py index f6e495c..63d9b6c 100644 --- a/antareslauncher/use_cases/retrieve/state_updater.py +++ b/antareslauncher/use_cases/retrieve/state_updater.py @@ -1,9 +1,7 @@ import typing as t from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO LOG_NAME = f"{__name__}.RetrieveController" diff --git a/antareslauncher/use_cases/wait_loop_controller/wait_controller.py b/antareslauncher/use_cases/wait_loop_controller/wait_controller.py index 9f57386..c540ddb 100644 --- a/antareslauncher/use_cases/wait_loop_controller/wait_controller.py +++ b/antareslauncher/use_cases/wait_loop_controller/wait_controller.py @@ -38,8 +38,6 @@ def _wait_loop(self, seconds_to_wait: int) -> None: mins, secs = divmod(seconds_to_wait, 60) formatted_time = "{:02d}:{:02d}".format(mins, secs) - self.display.show_message( - text_4_countdown + formatted_time, __name__, end="\r" - ) + self.display.show_message(text_4_countdown + formatted_time, __name__, end="\r") time.sleep(seconds_between_messages) seconds_to_wait -= seconds_between_messages diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e56b84b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[tool.black] +target-version = ["py38"] +line-length = 120 +exclude = "(data/*|docs/*|remote_scripts_templates/*|target/*)" + +[tool.isort] +profile = "black" +line_length = 120 +src_paths = ["antareslauncher", "tests"] +skip_gitignore = true +extend_skip_glob = [ + "data/*", + "doc/*", + "remote_scripts_templates/*", + "target/*", +] diff --git a/tests/integration/test_integration_check_queue_controller.py b/tests/integration/test_integration_check_queue_controller.py index a41beb2..17baf93 100644 --- a/tests/integration/test_integration_check_queue_controller.py +++ b/tests/integration/test_integration_check_queue_controller.py @@ -3,15 +3,9 @@ import pytest from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) -from antareslauncher.remote_environnement.slurm_script_features import ( - SlurmScriptFeatures, -) -from antareslauncher.use_cases.check_remote_queue.check_queue_controller import ( - CheckQueueController, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm +from antareslauncher.remote_environnement.slurm_script_features import SlurmScriptFeatures +from antareslauncher.use_cases.check_remote_queue.check_queue_controller import CheckQueueController from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import SlurmQueueShow from antareslauncher.use_cases.retrieve.state_updater import StateUpdater @@ -34,9 +28,7 @@ def setup_method(self): slurm_queue_show = SlurmQueueShow(env_mock, display_mock) state_updater = StateUpdater(env_mock, display_mock) repo = mock.MagicMock(spec=DataRepoTinydb) - self.check_queue_controller = CheckQueueController( - slurm_queue_show, state_updater, repo - ) + self.check_queue_controller = CheckQueueController(slurm_queue_show, state_updater, repo) @pytest.mark.integration_test def test_check_queue_controller_check_queue_calls_connection_execute_command( diff --git a/tests/integration/test_integration_job_kill_controller.py b/tests/integration/test_integration_job_kill_controller.py index f36039f..861d26a 100644 --- a/tests/integration/test_integration_job_kill_controller.py +++ b/tests/integration/test_integration_job_kill_controller.py @@ -2,12 +2,8 @@ import pytest -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) -from antareslauncher.remote_environnement.slurm_script_features import ( - SlurmScriptFeatures, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm +from antareslauncher.remote_environnement.slurm_script_features import SlurmScriptFeatures from antareslauncher.use_cases.kill_job.job_kill_controller import JobKillController @@ -28,12 +24,8 @@ def test_job_kill_controller_kill_job_calls_connection_execute_command( ): # given job_id = 42 - self.job_kill_controller.env.connection.execute_command = mock.Mock( - return_value=("", "") - ) + self.job_kill_controller.env.connection.execute_command = mock.Mock(return_value=("", "")) # when self.job_kill_controller.kill_job(job_id) # then - self.job_kill_controller.env.connection.execute_command.assert_called_once_with( - f"scancel {job_id}" - ) + self.job_kill_controller.env.connection.execute_command.assert_called_once_with(f"scancel {job_id}") diff --git a/tests/integration/test_integration_launch_controller.py b/tests/integration/test_integration_launch_controller.py index 40a4fe4..25bee7f 100644 --- a/tests/integration/test_integration_launch_controller.py +++ b/tests/integration/test_integration_launch_controller.py @@ -6,12 +6,8 @@ import pytest from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) -from antareslauncher.remote_environnement.slurm_script_features import ( - SlurmScriptFeatures, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm +from antareslauncher.remote_environnement.slurm_script_features import SlurmScriptFeatures from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.launch.launch_controller import LaunchController @@ -89,20 +85,20 @@ def test_execute_command__called_with_the_correct_parameters( ) home_dir = "Submitted" - remote_base_path = ( - f"{home_dir}/REMOTE_{getpass.getuser()}_{socket.gethostname()}" - ) + remote_base_path = f"{home_dir}/REMOTE_{getpass.getuser()}_{socket.gethostname()}" zipfile_name = Path(study1.zipfile_path).name job_type = "ANTARES" post_processing = False other_options = "" bash_options = ( + # fmt: off f" {zipfile_name}" f" {study1.antares_version}" f" {job_type}" f" {post_processing}" f" '{other_options}'" + # fmt: on ) command = ( f"cd {remote_base_path} && " diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 410c237..3465ece 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -3,12 +3,10 @@ from unittest import mock import pytest + from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.use_cases.create_list.study_list_composer import ( - StudyListComposer, - StudyListComposerParameters, -) +from antareslauncher.use_cases.create_list.study_list_composer import StudyListComposer, StudyListComposerParameters from tests.unit.assets import ASSETS_DIR diff --git a/tests/unit/launcher/test_launch_controller.py b/tests/unit/launcher/test_launch_controller.py index a4b10f0..5aa4ec6 100644 --- a/tests/unit/launcher/test_launch_controller.py +++ b/tests/unit/launcher/test_launch_controller.py @@ -12,9 +12,7 @@ from antareslauncher.data_repo.data_reporter import DataReporter from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.file_manager.file_manager import FileManager -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.launch import launch_controller from antareslauncher.use_cases.launch.launch_controller import StudyLauncher @@ -97,20 +95,14 @@ def test_with_one_study_the_compressor_is_called_once(self): file_manager = mock.Mock(spec_set=FileManager) file_manager.zip_dir_excluding_subdir = mock.Mock() - my_launcher = launch_controller.LaunchController( - self.data_repo, remote_env_mock, file_manager, self.display - ) + my_launcher = launch_controller.LaunchController(self.data_repo, remote_env_mock, file_manager, self.display) my_launcher.launch_all_studies() zipfile_path = f"{my_study.path}-{getpass.getuser()}.zip" - file_manager.zip_dir_excluding_subdir.assert_called_once_with( - my_study.path, zipfile_path, None - ) + file_manager.zip_dir_excluding_subdir.assert_called_once_with(my_study.path, zipfile_path, None) @pytest.mark.unit_test - def test_given_one_study_then_repo_is_called_to_save_the_study_with_updated_zip_is_sent( - self, my_launch_controller - ): + def test_given_one_study_then_repo_is_called_to_save_the_study_with_updated_zip_is_sent(self, my_launch_controller): # given my_launcher, expected_study = my_launch_controller # when @@ -142,9 +134,7 @@ def test_given_one_study_when_launcher_is_called_then_study_is_saved_with_job_id assert first_argument.job_id == 42 @pytest.mark.unit_test - def test_given_one_study_when_submit_fails_then_exception_is_raised( - self, my_launch_controller - ): + def test_given_one_study_when_submit_fails_then_exception_is_raised(self, my_launch_controller): # given my_launcher, expected_study = my_launch_controller # when @@ -152,20 +142,14 @@ def test_given_one_study_when_submit_fails_then_exception_is_raised( my_launcher.env.submit_job = mock.Mock(return_value=None) my_launcher.repo.save_study = mock.Mock() # then - with pytest.raises( - antareslauncher.use_cases.launch.study_submitter.FailedSubmissionException - ): + with pytest.raises(antareslauncher.use_cases.launch.study_submitter.FailedSubmissionException): my_launcher.launch_all_studies() @pytest.mark.unit_test - def test_given_one_study_when_zip_fails_then_return_none( - self, my_launch_controller - ): + def test_given_one_study_when_zip_fails_then_return_none(self, my_launch_controller): # given my_launcher, expected_study = my_launch_controller - my_launcher.file_manager.zip_dir_excluding_subdir = mock.Mock( - return_value=False - ) + my_launcher.file_manager.zip_dir_excluding_subdir = mock.Mock(return_value=False) # when my_launcher.launch_all_studies() # then diff --git a/tests/unit/launcher/test_submitter.py b/tests/unit/launcher/test_submitter.py index 72caa03..9ca8991 100644 --- a/tests/unit/launcher/test_submitter.py +++ b/tests/unit/launcher/test_submitter.py @@ -5,9 +5,7 @@ import antareslauncher.use_cases from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.launch.study_submitter import StudySubmitter @@ -26,9 +24,7 @@ def test_submit_study_shows_message_if_submit_succeeds(self): new_study = self.study_submitter.submit_job(study) expected_message = f'"hello": was submitted' - self.display_mock.show_message.assert_called_once_with( - expected_message, mock.ANY - ) + self.display_mock.show_message.assert_called_once_with(expected_message, mock.ANY) assert new_study.job_id == 42 @pytest.mark.unit_test @@ -38,15 +34,11 @@ def test_submit_study_shows_error_if_submit_fails_and_exception_is_raised( self.remote_env.submit_job = mock.Mock(return_value=None) study = StudyDTO(path="hello") - with pytest.raises( - antareslauncher.use_cases.launch.study_submitter.FailedSubmissionException - ): + with pytest.raises(antareslauncher.use_cases.launch.study_submitter.FailedSubmissionException): self.study_submitter.submit_job(study) expected_error_message = f'"hello": was not submitted' - self.display_mock.show_error.assert_called_once_with( - expected_error_message, mock.ANY - ) + self.display_mock.show_error.assert_called_once_with(expected_error_message, mock.ANY) @pytest.mark.unit_test def test_remote_env_not_called_if_study_has_already_a_jobid(self): diff --git a/tests/unit/launcher/test_zip_uploader.py b/tests/unit/launcher/test_zip_uploader.py index 128a79f..64079cd 100644 --- a/tests/unit/launcher/test_zip_uploader.py +++ b/tests/unit/launcher/test_zip_uploader.py @@ -5,14 +5,9 @@ import pytest from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO -from antareslauncher.use_cases.launch.study_zip_uploader import ( - FailedUploadException, - StudyZipfileUploader, -) +from antareslauncher.use_cases.launch.study_zip_uploader import FailedUploadException, StudyZipfileUploader class TestZipfileUploader: @@ -48,12 +43,8 @@ def test_upload_study_shows_error_if_upload_fails_and_exception_is_raised( expected_welcome_message = f'"hello": uploading study ...' expected_error_message = f'"hello": was not uploaded' - self.display_mock.show_message.assert_called_once_with( - expected_welcome_message, mock.ANY - ) - self.display_mock.show_error.assert_called_once_with( - expected_error_message, mock.ANY - ) + self.display_mock.show_message.assert_called_once_with(expected_welcome_message, mock.ANY) + self.display_mock.show_error.assert_called_once_with(expected_error_message, mock.ANY) @pytest.mark.unit_test def test_remote_env_not_called_if_upload_was_done(self): diff --git a/tests/unit/launcher/test_zipper.py b/tests/unit/launcher/test_zipper.py index 6c6c5e6..a25e22f 100644 --- a/tests/unit/launcher/test_zipper.py +++ b/tests/unit/launcher/test_zipper.py @@ -23,9 +23,7 @@ def test_zip_study_show_message_if_zip_succeeds(self): self.study_zipper.zip(study) expected_message = '"hello": was zipped' - self.display_mock.show_message.assert_called_once_with( - expected_message, mock.ANY - ) + self.display_mock.show_message.assert_called_once_with(expected_message, mock.ANY) @pytest.mark.unit_test def test_zip_study_show_error_if_zip_fails(self): @@ -61,7 +59,5 @@ def test_file_manager_is_called_if_zip_doesnt_exist(self): new_study = self.study_zipper.zip(study) expected_zipfile_path = f"{study.path}-{getpass.getuser()}.zip" - self.file_manager.zip_dir_excluding_subdir.assert_called_once_with( - study_path, expected_zipfile_path, None - ) + self.file_manager.zip_dir_excluding_subdir.assert_called_once_with(study_path, expected_zipfile_path, None) assert new_study.zipfile_path == expected_zipfile_path diff --git a/tests/unit/retriever/conftest.py b/tests/unit/retriever/conftest.py index 77f3ae9..b0d3465 100644 --- a/tests/unit/retriever/conftest.py +++ b/tests/unit/retriever/conftest.py @@ -22,20 +22,14 @@ def pending_study_fixture(tmp_path: Path) -> StudyDTO: @pytest.fixture(name="started_study") def started_study_fixture(pending_study: StudyDTO) -> StudyDTO: - return dataclasses.replace( - pending_study, started=True, finished=False, with_error=False - ) + return dataclasses.replace(pending_study, started=True, finished=False, with_error=False) @pytest.fixture(name="finished_study") def finished_study_fixture(pending_study: StudyDTO) -> StudyDTO: - return dataclasses.replace( - pending_study, started=True, finished=True, with_error=False - ) + return dataclasses.replace(pending_study, started=True, finished=True, with_error=False) @pytest.fixture(name="with_error_study") def with_error_study_fixture(pending_study: StudyDTO) -> StudyDTO: - return dataclasses.replace( - pending_study, started=True, finished=True, with_error=True - ) + return dataclasses.replace(pending_study, started=True, finished=True, with_error=True) diff --git a/tests/unit/retriever/test_download_final_zip.py b/tests/unit/retriever/test_download_final_zip.py index 87b0526..b2a0cde 100644 --- a/tests/unit/retriever/test_download_final_zip.py +++ b/tests/unit/retriever/test_download_final_zip.py @@ -5,9 +5,7 @@ import pytest from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.retrieve.download_final_zip import FinalZipDownloader @@ -64,9 +62,7 @@ def test_download__with_error_study(self, with_error_study: StudyDTO) -> None: display.show_error.assert_not_called() @pytest.mark.unit_test - def test_download__finished_study__download_ok( - self, finished_study: StudyDTO - ) -> None: + def test_download__finished_study__download_ok(self, finished_study: StudyDTO) -> None: env = mock.Mock(spec=RemoteEnvironmentWithSlurm) env.download_final_zip = download_final_zip display = mock.Mock(spec=DisplayTerminal) @@ -85,9 +81,7 @@ def test_download__finished_study__download_ok( assert len(zip_files) == 1 @pytest.mark.unit_test - def test_download__finished_study__reentrancy( - self, finished_study: StudyDTO - ) -> None: + def test_download__finished_study__reentrancy(self, finished_study: StudyDTO) -> None: env = mock.Mock(spec=RemoteEnvironmentWithSlurm) env.download_final_zip = download_final_zip display = mock.Mock(spec=DisplayTerminal) @@ -111,9 +105,7 @@ def test_download__finished_study__reentrancy( assert zip_files1 == zip_files2 @pytest.mark.unit_test - def test_download__finished_study__download_nothing( - self, finished_study: StudyDTO - ) -> None: + def test_download__finished_study__download_nothing(self, finished_study: StudyDTO) -> None: env = mock.Mock(spec=RemoteEnvironmentWithSlurm) env.download_final_zip = lambda _: [] display = mock.Mock(spec=DisplayTerminal) @@ -132,9 +124,7 @@ def test_download__finished_study__download_nothing( assert not zip_files @pytest.mark.unit_test - def test_download__finished_study__download_error( - self, finished_study: StudyDTO - ) -> None: + def test_download__finished_study__download_error(self, finished_study: StudyDTO) -> None: env = mock.Mock(spec=RemoteEnvironmentWithSlurm) env.download_final_zip.side_effect = Exception("Connection error") display = mock.Mock(spec=DisplayTerminal) diff --git a/tests/unit/retriever/test_final_zip_extractor.py b/tests/unit/retriever/test_final_zip_extractor.py index 0dc9381..f2c333c 100644 --- a/tests/unit/retriever/test_final_zip_extractor.py +++ b/tests/unit/retriever/test_final_zip_extractor.py @@ -55,9 +55,7 @@ def test_extract_final_zip__started_study(self, started_study: StudyDTO) -> None assert not started_study.final_zip_extracted @pytest.mark.unit_test - def test_extract_final_zip__finished_study__no_output( - self, finished_study: StudyDTO - ) -> None: + def test_extract_final_zip__finished_study__no_output(self, finished_study: StudyDTO) -> None: display = mock.Mock(spec=DisplayTerminal) # Initialize and execute the ZIP extraction @@ -70,9 +68,7 @@ def test_extract_final_zip__finished_study__no_output( assert not finished_study.final_zip_extracted @pytest.mark.unit_test - def test_extract_final_zip__finished_study__nominal( - self, finished_study: StudyDTO - ) -> None: + def test_extract_final_zip__finished_study__nominal(self, finished_study: StudyDTO) -> None: display = mock.Mock(spec=DisplayTerminal) display.generate_progress_bar = lambda names, *args, **kwargs: names @@ -94,9 +90,7 @@ def test_extract_final_zip__finished_study__nominal( assert result_dir.joinpath("simulation.log").is_file() @pytest.mark.unit_test - def test_extract_final_zip__finished_study__reentrancy( - self, finished_study: StudyDTO - ) -> None: + def test_extract_final_zip__finished_study__reentrancy(self, finished_study: StudyDTO) -> None: display = mock.Mock(spec=DisplayTerminal) display.generate_progress_bar = lambda names, *args, **kwargs: names @@ -115,16 +109,12 @@ def test_extract_final_zip__finished_study__reentrancy( assert study_state1 == study_state2 @pytest.mark.unit_test - def test_extract_final_zip__finished_study__missing( - self, finished_study: StudyDTO - ) -> None: + def test_extract_final_zip__finished_study__missing(self, finished_study: StudyDTO) -> None: display = mock.Mock(spec=DisplayTerminal) display.generate_progress_bar = lambda names, *args, **kwargs: names # Prepare a missing final ZIP - finished_study.local_final_zipfile_path = create_final_zip( - finished_study, scenario="missing" - ) + finished_study.local_final_zipfile_path = create_final_zip(finished_study, scenario="missing") # Initialize and execute the ZIP extraction extractor = FinalZipExtractor(display=display) @@ -141,16 +131,12 @@ def test_extract_final_zip__finished_study__missing( assert not result_dir.joinpath("simulation.log").exists() @pytest.mark.unit_test - def test_extract_final_zip__finished_study__corrupted( - self, finished_study: StudyDTO - ) -> None: + def test_extract_final_zip__finished_study__corrupted(self, finished_study: StudyDTO) -> None: display = mock.Mock(spec=DisplayTerminal) display.generate_progress_bar = lambda names, *args, **kwargs: names # Prepare a corrupted final ZIP - finished_study.local_final_zipfile_path = create_final_zip( - finished_study, scenario="corrupted" - ) + finished_study.local_final_zipfile_path = create_final_zip(finished_study, scenario="corrupted") # Initialize and execute the ZIP extraction extractor = FinalZipExtractor(display=display) diff --git a/tests/unit/retriever/test_log_downloader.py b/tests/unit/retriever/test_log_downloader.py index 3818748..b6335b8 100644 --- a/tests/unit/retriever/test_log_downloader.py +++ b/tests/unit/retriever/test_log_downloader.py @@ -5,9 +5,7 @@ import pytest from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.retrieve.log_downloader import LogDownloader @@ -81,9 +79,7 @@ def test_run__started_study__reentrancy(self, started_study: StudyDTO) -> None: assert log_files1 == log_files2 @pytest.mark.unit_test - def test_run__started_study__download_nothing( - self, started_study: StudyDTO - ) -> None: + def test_run__started_study__download_nothing(self, started_study: StudyDTO) -> None: env = mock.Mock(spec=RemoteEnvironmentWithSlurm) env.download_logs = lambda _: [] display = mock.Mock(spec=DisplayTerminal) diff --git a/tests/unit/retriever/test_retrieve_controller.py b/tests/unit/retriever/test_retrieve_controller.py index 9cef2cc..c28372f 100644 --- a/tests/unit/retriever/test_retrieve_controller.py +++ b/tests/unit/retriever/test_retrieve_controller.py @@ -5,9 +5,7 @@ import pytest from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.retrieve.retrieve_controller import RetrieveController from antareslauncher.use_cases.retrieve.state_updater import StateUpdater @@ -21,23 +19,17 @@ def setup_method(self): self.state_updater_mock = StateUpdater(self.env, self.display) @pytest.mark.unit_test - def test_given_one_study_when_retrieve_all_studies_call_then_study_retriever_is_called_once( - self, started_study - ): + def test_given_one_study_when_retrieve_all_studies_call_then_study_retriever_is_called_once(self, started_study): # given list_of_studies = [started_study] self.data_repo.get_list_of_studies = mock.Mock(return_value=list_of_studies) - my_retriever = RetrieveController( - self.data_repo, self.env, self.display, self.state_updater_mock - ) + my_retriever = RetrieveController(self.data_repo, self.env, self.display, self.state_updater_mock) my_retriever.study_retriever.retrieve = mock.Mock() self.display.show_message = mock.Mock() # when my_retriever.retrieve_all_studies() # then - self.display.show_message.assert_called_once_with( - "Retrieving all studies...", mock.ANY - ) + self.display.show_message.assert_called_once_with("Retrieving all studies...", mock.ANY) my_retriever.study_retriever.retrieve.assert_called_once_with(started_study) @pytest.mark.unit_test @@ -48,9 +40,7 @@ def test_given_a_list_of_done_studies_when_all_studies_done_called_then_return_t study = StudyDTO("path") study.done = True study_list = [deepcopy(study), deepcopy(study)] - my_retriever = RetrieveController( - self.data_repo, self.env, self.display, self.state_updater_mock - ) + my_retriever = RetrieveController(self.data_repo, self.env, self.display, self.state_updater_mock) my_retriever.repo.get_list_of_studies = mock.Mock(return_value=study_list) # when output = my_retriever.all_studies_done @@ -66,9 +56,7 @@ def test_given_a_list_of_done_studies_when_retrieve_all_studies_called_then_mess study.done = True study_list = [deepcopy(study), deepcopy(study)] display_mock = mock.Mock(spec=DisplayTerminal) - my_retriever = RetrieveController( - self.data_repo, self.env, display_mock, self.state_updater_mock - ) + my_retriever = RetrieveController(self.data_repo, self.env, display_mock, self.state_updater_mock) my_retriever.repo.get_list_of_studies = mock.Mock(return_value=study_list) display_mock.show_message = mock.Mock() # when diff --git a/tests/unit/retriever/test_server_cleaner.py b/tests/unit/retriever/test_server_cleaner.py index 901ec0c..9cc51a7 100644 --- a/tests/unit/retriever/test_server_cleaner.py +++ b/tests/unit/retriever/test_server_cleaner.py @@ -3,9 +3,7 @@ import pytest from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.retrieve.clean_remote_server import RemoteServerCleaner @@ -73,9 +71,7 @@ def test_clean__finished_study__reentrancy(self, finished_study: StudyDTO) -> No assert finished_study.remote_server_is_clean @pytest.mark.unit_test - def test_clean__finished_study__cleaning_failed( - self, finished_study: StudyDTO - ) -> None: + def test_clean__finished_study__cleaning_failed(self, finished_study: StudyDTO) -> None: env = mock.Mock(spec=RemoteEnvironmentWithSlurm) env.clean_remote_server.return_value = False display = mock.Mock(spec=DisplayTerminal) @@ -95,9 +91,7 @@ def test_clean__finished_study__cleaning_failed( assert finished_study.remote_server_is_clean @pytest.mark.unit_test - def test_clean__finished_study__cleaning_raise( - self, finished_study: StudyDTO - ) -> None: + def test_clean__finished_study__cleaning_raise(self, finished_study: StudyDTO) -> None: env = mock.Mock(spec=RemoteEnvironmentWithSlurm) env.clean_remote_server.side_effect = Exception("cleaning error") display = mock.Mock(spec=DisplayTerminal) diff --git a/tests/unit/retriever/test_state_updater.py b/tests/unit/retriever/test_state_updater.py index 9ab1639..4a9d636 100644 --- a/tests/unit/retriever/test_state_updater.py +++ b/tests/unit/retriever/test_state_updater.py @@ -4,9 +4,7 @@ import pytest from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.retrieve.state_updater import StateUpdater @@ -21,13 +19,9 @@ (None, None, True, "Ended with error"), ], ) -def test_given_a_submitted_study_then_study_flags_are_updated( - started_flag, finished_flag, with_error_flag, status -): +def test_given_a_submitted_study_then_study_flags_are_updated(started_flag, finished_flag, with_error_flag, status): env = mock.Mock(spec=RemoteEnvironmentWithSlurm) - env.get_job_state_flags = mock.Mock( - return_value=(started_flag, finished_flag, with_error_flag) - ) + env.get_job_state_flags = mock.Mock(return_value=(started_flag, finished_flag, with_error_flag)) display = mock.Mock(spec=DisplayTerminal) my_study = StudyDTO(path="study_path", job_id=42) diff --git a/tests/unit/retriever/test_study_retriever.py b/tests/unit/retriever/test_study_retriever.py index 58b8e1e..4d9c506 100644 --- a/tests/unit/retriever/test_study_retriever.py +++ b/tests/unit/retriever/test_study_retriever.py @@ -5,9 +5,7 @@ from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.data_repo.data_reporter import DataReporter from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.remote_environnement.remote_environment_with_slurm import ( - RemoteEnvironmentWithSlurm, -) +from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.retrieve.clean_remote_server import RemoteServerCleaner from antareslauncher.use_cases.retrieve.download_final_zip import FinalZipDownloader @@ -91,25 +89,19 @@ def final_zip_downloader_download(study_: StudyDTO): study_.local_final_zipfile_path = "final-zipfile.zip" return study_ - self.final_zip_downloader.download = mock.Mock( - side_effect=final_zip_downloader_download - ) + self.final_zip_downloader.download = mock.Mock(side_effect=final_zip_downloader_download) def remote_server_cleaner_clean(study_: StudyDTO): study_.remote_server_is_clean = True return study_ - self.remote_server_cleaner.clean = mock.Mock( - side_effect=remote_server_cleaner_clean - ) + self.remote_server_cleaner.clean = mock.Mock(side_effect=remote_server_cleaner_clean) def zip_extractor_extract_final_zip(study_: StudyDTO): study_.final_zip_extracted = True return study_ - self.zip_extractor.extract_final_zip = mock.Mock( - side_effect=zip_extractor_extract_final_zip - ) + self.zip_extractor.extract_final_zip = mock.Mock(side_effect=zip_extractor_extract_final_zip) self.reporter.save_study = mock.Mock(return_value=True) self.study_retriever.retrieve(study) diff --git a/tests/unit/test_antares_launcher.py b/tests/unit/test_antares_launcher.py index 4803f52..4b9c875 100644 --- a/tests/unit/test_antares_launcher.py +++ b/tests/unit/test_antares_launcher.py @@ -4,9 +4,7 @@ import pytest from antareslauncher.antares_launcher import AntaresLauncher -from antareslauncher.use_cases.wait_loop_controller.wait_controller import ( - WaitController, -) +from antareslauncher.use_cases.wait_loop_controller.wait_controller import WaitController class TestAntaresLauncher: @@ -33,9 +31,7 @@ def test_given_job_id_to_kill_when_run_then_job_kill_controller_kills_job_with_g # when antares_launcher.run() # then - antares_launcher.job_kill_controller.kill_job.assert_called_once_with( - job_id_to_kill - ) + antares_launcher.job_kill_controller.kill_job.assert_called_once_with(job_id_to_kill) @pytest.mark.unit_test def test_given_true_check_queue_bool_when_run_then_check_queue_controller_checks_queue( diff --git a/tests/unit/test_check_queue_controller.py b/tests/unit/test_check_queue_controller.py index 8f378c2..5195228 100644 --- a/tests/unit/test_check_queue_controller.py +++ b/tests/unit/test_check_queue_controller.py @@ -4,9 +4,7 @@ from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.study_dto import StudyDTO -from antareslauncher.use_cases.check_remote_queue.check_queue_controller import ( - CheckQueueController, -) +from antareslauncher.use_cases.check_remote_queue.check_queue_controller import CheckQueueController from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import SlurmQueueShow from antareslauncher.use_cases.retrieve.state_updater import StateUpdater @@ -28,9 +26,7 @@ def setup_method(self): def test_check_queue_controller_calls_slurm_queue_show_once(self): # given self.slurm_queue_show.run = mock.Mock() - self.repo_mock.get_list_of_studies = ( - mock.MagicMock() - ) # mock.Mock(return_value=[]) + self.repo_mock.get_list_of_studies = mock.MagicMock() # mock.Mock(return_value=[]) # when self.check_queue_controller.check_queue() # then diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index f33997a..7c1ebbe 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -20,11 +20,7 @@ get_user_config_dir, parse_config, ) -from antareslauncher.exceptions import ( - ConfigFileNotFoundError, - InvalidConfigValueError, - UnknownFileSuffixError, -) +from antareslauncher.exceptions import ConfigFileNotFoundError, InvalidConfigValueError, UnknownFileSuffixError class TestParseConfig: @@ -223,9 +219,7 @@ def test_load_config__nominal(self, tmp_path, ssh_config_path): assert config.db_primary_key == data["db_primary_key"] assert config.ssh_config_file_is_required == data["ssh_config_file_is_required"] assert config.slurm_script_path == slurm_script_path - assert ( - config.remote_solver_versions == data["antares_versions_on_remote_server"] - ) + assert config.remote_solver_versions == data["antares_versions_on_remote_server"] def test_save_config__nominal(self, tmp_path, ssh_config): config_path = tmp_path.joinpath("configuration.yaml") @@ -262,13 +256,9 @@ def test_save_config__nominal(self, tmp_path, ssh_config): assert actual["default_n_cpu"] == config.default_n_cpu assert actual["default_wait_time"] == config.default_wait_time assert actual["db_primary_key"] == config.db_primary_key - assert ( - actual["ssh_config_file_is_required"] == config.ssh_config_file_is_required - ) + assert actual["ssh_config_file_is_required"] == config.ssh_config_file_is_required assert actual["slurm_script_path"] == slurm_script_path.as_posix() - assert ( - actual["antares_versions_on_remote_server"] == config.remote_solver_versions - ) + assert actual["antares_versions_on_remote_server"] == config.remote_solver_versions assert "ssh_config" not in actual @pytest.mark.parametrize( @@ -364,12 +354,8 @@ def test_get_config_path__from_env__not_found(self, monkeypatch, tmp_path): with pytest.raises(ConfigFileNotFoundError): get_config_path() - @pytest.mark.parametrize( - "config_name", [None, CONFIGURATION_YAML, "my_config.yaml"] - ) - def test_get_config_path__from_user_config_dir( - self, monkeypatch, tmp_path, config_name - ): + @pytest.mark.parametrize("config_name", [None, CONFIGURATION_YAML, "my_config.yaml"]) + def test_get_config_path__from_user_config_dir(self, monkeypatch, tmp_path, config_name): config_path = tmp_path.joinpath(config_name or CONFIGURATION_YAML) config_path.touch() monkeypatch.delenv("ANTARES_LAUNCHER_CONFIG_PATH", raising=False) @@ -380,17 +366,11 @@ def test_get_config_path__from_user_config_dir( assert actual == config_path @pytest.mark.parametrize("relpath", ["", "data"]) - @pytest.mark.parametrize( - "config_name", [None, CONFIGURATION_YAML, "my_config.yaml"] - ) - def test_get_config_path__from_curr_dir( - self, monkeypatch, tmp_path, relpath, config_name - ): + @pytest.mark.parametrize("config_name", [None, CONFIGURATION_YAML, "my_config.yaml"]) + def test_get_config_path__from_curr_dir(self, monkeypatch, tmp_path, relpath, config_name): data_dir = tmp_path.joinpath(relpath) data_dir.mkdir(exist_ok=True) - config_path: pathlib.Path = tmp_path.joinpath( - data_dir, config_name or CONFIGURATION_YAML - ) + config_path: pathlib.Path = tmp_path.joinpath(data_dir, config_name or CONFIGURATION_YAML) config_path.touch() monkeypatch.delenv("ANTARES_LAUNCHER_CONFIG_PATH", raising=False) monkeypatch.chdir(tmp_path) @@ -399,9 +379,7 @@ def test_get_config_path__from_curr_dir( assert actual == config_path.relative_to(tmp_path) @pytest.mark.parametrize("relpath", ["", "data"]) - def test_get_config_path__from_curr_dir__not_found( - self, monkeypatch, tmp_path, relpath - ): + def test_get_config_path__from_curr_dir__not_found(self, monkeypatch, tmp_path, relpath): data_dir = tmp_path.joinpath(relpath) data_dir.mkdir(exist_ok=True) monkeypatch.delenv("ANTARES_LAUNCHER_CONFIG_PATH", raising=False) diff --git a/tests/unit/test_main_option_parser.py b/tests/unit/test_main_option_parser.py index 82218aa..128cfac 100644 --- a/tests/unit/test_main_option_parser.py +++ b/tests/unit/test_main_option_parser.py @@ -2,11 +2,7 @@ import pytest -from antareslauncher.main_option_parser import ( - MainOptionParser, - ParserParameters, - look_for_default_ssh_conf_file, -) +from antareslauncher.main_option_parser import MainOptionParser, ParserParameters, look_for_default_ssh_conf_file class TestMainOptionParser: @@ -34,9 +30,7 @@ def setup_method(self): "n_cpu": 42, "job_id_to_kill": None, "post_processing": False, - "json_ssh_config": look_for_default_ssh_conf_file( - self.main_options_parameters - ), + "json_ssh_config": look_for_default_ssh_conf_file(self.main_options_parameters), } @pytest.fixture(scope="function") diff --git a/tests/unit/test_parameters_reader.py b/tests/unit/test_parameters_reader.py index 1ba30d6..58acf2b 100644 --- a/tests/unit/test_parameters_reader.py +++ b/tests/unit/test_parameters_reader.py @@ -82,9 +82,7 @@ def test_get_main_parameters_raises_exception_with_empty_file(self, tmp_path): ParametersReader(empty_json, empty_yaml).get_main_parameters() @pytest.mark.unit_test - def test_get_option_parameters_raises_exception_if_params_are_missing( - self, tmp_path - ): + def test_get_option_parameters_raises_exception_if_params_are_missing(self, tmp_path): empty_json = tmp_path / "dummy.json" config_yaml = tmp_path / "empty.yaml" config_yaml.write_text( @@ -119,23 +117,16 @@ def test_get_option_parameters_initializes_parameters_correctly(self, tmp_path): empty_json.write_text("{}") config_yaml = tmp_path / "empty.yaml" config_yaml.write_text(self.yaml_compulsory_content) - options_parameters = ParametersReader( - empty_json, config_yaml - ).get_parser_parameters() + options_parameters = ParametersReader(empty_json, config_yaml).get_parser_parameters() assert options_parameters.log_dir == self.LOG_DIR assert options_parameters.studies_in_dir == self.STUDIES_IN_DIR assert options_parameters.finished_dir == self.FINISHED_DIR assert options_parameters.default_time_limit == self.DEFAULT_TIME_LIMIT assert options_parameters.default_n_cpu == self.DEFAULT_N_CPU assert options_parameters.default_wait_time == self.DEFAULT_WAIT_TIME - assert ( - options_parameters.ssh_config_file_is_required - == self.SSH_CONFIG_FILE_IS_REQUIRED - ) + assert options_parameters.ssh_config_file_is_required == self.SSH_CONFIG_FILE_IS_REQUIRED alternate1 = Path.cwd() / self.DEFAULT_SSH_CONFIGFILE_NAME - alternate2 = ( - Path.home() / "antares_launcher_settings" / self.DEFAULT_SSH_CONFIGFILE_NAME - ) + alternate2 = Path.home() / "antares_launcher_settings" / self.DEFAULT_SSH_CONFIGFILE_NAME assert options_parameters.ssh_configfile_path_alternate1 == alternate1 assert options_parameters.ssh_configfile_path_alternate2 == alternate2 @@ -146,23 +137,15 @@ def test_get_main_parameters_initializes_parameters_correctly(self, tmp_path): config_yaml.write_text(self.yaml_compulsory_content) empty_json = tmp_path / "dummy.json" empty_json.write_text("{}") - main_parameters = ParametersReader( - empty_json, config_yaml - ).get_main_parameters() + main_parameters = ParametersReader(empty_json, config_yaml).get_main_parameters() assert main_parameters.json_dir == Path(self.JSON_DIR) assert main_parameters.slurm_script_path == self.SLURM_SCRIPT_PATH - assert ( - main_parameters.default_json_db_name - == f"{getpass.getuser()}_antares_launcher_db.json" - ) + assert main_parameters.default_json_db_name == f"{getpass.getuser()}_antares_launcher_db.json" assert main_parameters.partition == self.PARTITION assert main_parameters.quality_of_service == self.QUALITY_OF_SERVICE assert main_parameters.db_primary_key == self.DB_PRIMARY_KEY assert not main_parameters.default_ssh_dict - assert ( - main_parameters.antares_versions_on_remote_server - == self.ANTARES_SUPPORTED_VERSIONS - ) + assert main_parameters.antares_versions_on_remote_server == self.ANTARES_SUPPORTED_VERSIONS @pytest.mark.unit_test def test_get_main_parameters_initializes_default_ssh_dict_correctly(self, tmp_path): @@ -172,7 +155,5 @@ def test_get_main_parameters_initializes_default_ssh_dict_correctly(self, tmp_pa with open(ssh_json, "w") as file: json.dump(self.json_dict, file) - main_parameters = ParametersReader( - json_ssh_conf=ssh_json, yaml_filepath=config_yaml - ).get_main_parameters() + main_parameters = ParametersReader(json_ssh_conf=ssh_json, yaml_filepath=config_yaml).get_main_parameters() assert main_parameters.default_ssh_dict == self.json_dict diff --git a/tests/unit/test_remote_environment_with_slurm.py b/tests/unit/test_remote_environment_with_slurm.py index 9b52aba..1da3d07 100644 --- a/tests/unit/test_remote_environment_with_slurm.py +++ b/tests/unit/test_remote_environment_with_slurm.py @@ -17,10 +17,7 @@ RemoteEnvironmentWithSlurm, SubmitJobError, ) -from antareslauncher.remote_environnement.slurm_script_features import ( - ScriptParametersDTO, - SlurmScriptFeatures, -) +from antareslauncher.remote_environnement.slurm_script_features import ScriptParametersDTO, SlurmScriptFeatures from antareslauncher.study_dto import Modes, StudyDTO @@ -77,9 +74,7 @@ def test_initialise_remote_path_calls_connection_make_dir_with_correct_arguments ): # given remote_home_dir = "remote_home_dir" - remote_base_dir = ( - f"{remote_home_dir}/REMOTE_{getpass.getuser()}_{socket.gethostname()}" - ) + remote_base_dir = f"{remote_home_dir}/REMOTE_{getpass.getuser()}_{socket.gethostname()}" connection = mock.Mock(home_dir="path/to/home") connection.home_dir = remote_home_dir connection.make_dir = mock.Mock(return_value=True) @@ -151,9 +146,7 @@ def test_when_constructor_is_called_and_connection_check_file_not_empty_is_false RemoteEnvironmentWithSlurm(connection, slurm_script_features) @pytest.mark.unit_test - def test_get_queue_info_calls_connection_execute_command_with_correct_argument( - self, remote_env - ): + def test_get_queue_info_calls_connection_execute_command_with_correct_argument(self, remote_env): # given username = "username" host = "host" @@ -169,9 +162,7 @@ def test_get_queue_info_calls_connection_execute_command_with_correct_argument( remote_env.connection.execute_command.assert_called_with(command) @pytest.mark.unit_test - def test_when_connection_exec_command_has_an_error_then_get_queue_info_returns_the_error_string( - self, remote_env - ): + def test_when_connection_exec_command_has_an_error_then_get_queue_info_returns_the_error_string(self, remote_env): # given username = "username" remote_env.connection.username = username @@ -194,9 +185,7 @@ def test_kill_remote_job_execute_scancel_command(self, remote_env): remote_env.connection.execute_command.assert_called_with(command) @pytest.mark.unit_test - def test_when_kill_remote_job_is_called_and_exec_command_returns_error_exception_is_raised( - self, remote_env - ): + def test_when_kill_remote_job_is_called_and_exec_command_returns_error_exception_is_raised(self, remote_env): # when output = None error = "error" @@ -224,15 +213,11 @@ def test_when_submit_job_is_called_then_execute_command_is_called_with_specific_ post_processing=study.post_processing, other_options="", ) - command = remote_env.slurm_script_features.compose_launch_command( - remote_env.remote_base_path, script_params - ) + command = remote_env.slurm_script_features.compose_launch_command(remote_env.remote_base_path, script_params) remote_env.connection.execute_command.assert_called_once_with(command) @pytest.mark.unit_test - def test_when_submit_job_is_called_and_receives_submitted_420_returns_job_id_420( - self, remote_env, study - ): + def test_when_submit_job_is_called_and_receives_submitted_420_returns_job_id_420(self, remote_env, study): # when output = "Submitted 420" error = None @@ -241,9 +226,7 @@ def test_when_submit_job_is_called_and_receives_submitted_420_returns_job_id_420 assert remote_env.submit_job(study) == 420 @pytest.mark.unit_test - def test_when_submit_job_is_called_and_receives_error_then_exception_is_raised( - self, remote_env, study - ): + def test_when_submit_job_is_called_and_receives_error_then_exception_is_raised(self, remote_env, study): # when output = "" error = "error" @@ -392,9 +375,7 @@ def execute_command_mock(cmd: str): ("FAILED", (True, True, True)), ], ) - def test_get_job_state_flags__sacct_nominal_case( - self, remote_env, study, state, expected - ): + def test_get_job_state_flags__sacct_nominal_case(self, remote_env, study, state, expected): """ Check that the "get_job_state_flags" method is correctly returning the status flags ("started", "finished", and "with_error") @@ -552,9 +533,7 @@ def test_given_a_study_with_input_zipfile_removed_when_remove_input_zipfile_then assert output is True @pytest.mark.unit_test - def test_given_a_study_when_remove_input_zipfile_then_connection_remove_file_is_called( - self, remote_env, study - ): + def test_given_a_study_when_remove_input_zipfile_then_connection_remove_file_is_called(self, remote_env, study): # given study.input_zipfile_removed = False study.zipfile_path = "zipfile_path" @@ -586,9 +565,7 @@ def test_given_a_study_when_remove_remote_final_zipfile_then_connection_remove_f # given study.input_zipfile_removed = False study.zipfile_path = "zipfile_path" - command = ( - f"{remote_env.remote_base_path}/{Path(study.local_final_zipfile_path).name}" - ) + command = f"{remote_env.remote_base_path}/{Path(study.local_final_zipfile_path).name}" remote_env.connection.execute_command = mock.Mock(return_value=("", "")) # when remote_env.remove_remote_final_zipfile(study) @@ -608,9 +585,7 @@ def test_given_a_study_with_clean_remote_server_when_clean_remote_server_called_ assert output is False @pytest.mark.unit_test - def test_given_a_study_when_clean_remote_server_called_then_remove_zip_methods_are_called( - self, remote_env, study - ): + def test_given_a_study_when_clean_remote_server_called_then_remove_zip_methods_are_called(self, remote_env, study): # given study.remote_server_is_clean = False remote_env.remove_remote_final_zipfile = mock.Mock(return_value=False) @@ -622,9 +597,7 @@ def test_given_a_study_when_clean_remote_server_called_then_remove_zip_methods_a remote_env.remove_input_zipfile.assert_called_once_with(study) @pytest.mark.unit_test - def test_given_a_study_when_clean_remote_server_called_then_return_correct_result( - self, remote_env, study - ): + def test_given_a_study_when_clean_remote_server_called_then_return_correct_result(self, remote_env, study): # given study.remote_server_is_clean = False remote_env.remove_remote_final_zipfile = mock.Mock(return_value=False) @@ -665,9 +638,7 @@ def test_given_time_limit_lower_than_min_duration_when_convert_time_is_called_re # given time_lim_sec = 42 # when - output = RemoteEnvironmentWithSlurm.convert_time_limit_from_seconds_to_minutes( - time_lim_sec - ) + output = RemoteEnvironmentWithSlurm.convert_time_limit_from_seconds_to_minutes(time_lim_sec) # then assert output == 1 diff --git a/tests/unit/test_study_list_composer.py b/tests/unit/test_study_list_composer.py index 1b76ea1..902e402 100644 --- a/tests/unit/test_study_list_composer.py +++ b/tests/unit/test_study_list_composer.py @@ -2,10 +2,7 @@ import pytest -from antareslauncher.use_cases.create_list.study_list_composer import ( - StudyListComposer, - get_solver_version, -) +from antareslauncher.use_cases.create_list.study_list_composer import StudyListComposer, get_solver_version CONFIG_NOMINAL_VERSION = """\ [antares] diff --git a/tests/unit/test_wait_controller.py b/tests/unit/test_wait_controller.py index 555dc7a..b0a479e 100644 --- a/tests/unit/test_wait_controller.py +++ b/tests/unit/test_wait_controller.py @@ -3,9 +3,7 @@ import pytest from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.use_cases.wait_loop_controller.wait_controller import ( - WaitController, -) +from antareslauncher.use_cases.wait_loop_controller.wait_controller import WaitController class TestWaitController: From 1ffc86e0439814e4549f59c193731c71080c0d59 Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Fri, 22 Sep 2023 16:33:24 +0200 Subject: [PATCH 13/23] feat(zip-extractor): the uncompress directory is calculated according to the content: study directory or simulation output --- .../use_cases/retrieve/final_zip_extractor.py | 14 +++- .../retriever/test_final_zip_extractor.py | 67 ++++++++++++++++--- 2 files changed, 72 insertions(+), 9 deletions(-) diff --git a/antareslauncher/use_cases/retrieve/final_zip_extractor.py b/antareslauncher/use_cases/retrieve/final_zip_extractor.py index e5810ef..81d02cb 100644 --- a/antareslauncher/use_cases/retrieve/final_zip_extractor.py +++ b/antareslauncher/use_cases/retrieve/final_zip_extractor.py @@ -1,3 +1,4 @@ +import os.path import zipfile from pathlib import Path @@ -21,15 +22,25 @@ def extract_final_zip(self, study: StudyDTO) -> None: """ if study.finished and not study.with_error and study.local_final_zipfile_path and not study.final_zip_extracted: zip_path = Path(study.local_final_zipfile_path) - target_dir = zip_path.with_suffix("") try: with zipfile.ZipFile(zip_path) as zf: names = zf.namelist() + if len(names) > 1 and os.path.commonpath(names): + # If all files are in the same directory, we can extract the ZIP + # file directly in the target directory. + target_dir = zip_path.parent + else: + # Otherwise, we need to create a directory to store the results. + # This situation occurs when the ZIP file contains + # only the simulation results and not the entire study. + target_dir = zip_path.with_suffix("") + progress_bar = self._display.generate_progress_bar( names, desc="Extracting archive:", total=len(names) ) for file in progress_bar: zf.extract(member=file, path=target_dir) + except (OSError, zipfile.BadZipFile) as exc: # If we cannot extract the final ZIP file, either because the file # doesn't exist or the ZIP file is corrupted, we find ourselves @@ -42,6 +53,7 @@ def extract_final_zip(self, study: StudyDTO) -> None: f'"{study.name}": Final zip not extracted: {exc}', LOG_NAME, ) + else: study.final_zip_extracted = True self._display.show_message( diff --git a/tests/unit/retriever/test_final_zip_extractor.py b/tests/unit/retriever/test_final_zip_extractor.py index f2c333c..7817dc0 100644 --- a/tests/unit/retriever/test_final_zip_extractor.py +++ b/tests/unit/retriever/test_final_zip_extractor.py @@ -10,12 +10,26 @@ from antareslauncher.use_cases.retrieve.final_zip_extractor import FinalZipExtractor -def create_final_zip(study: StudyDTO, *, scenario: str = "nominal") -> str: +def create_final_zip(study: StudyDTO, *, scenario: str = "nominal_study") -> str: """Prepare a final ZIP.""" dst_dir = Path(study.output_dir) # must exist dst_dir.mkdir(parents=True, exist_ok=True) out_path = dst_dir.joinpath(f"finished_{study.name}_{study.job_id}.zip") - if scenario == "nominal": + if scenario == "nominal_study": + with zipfile.ZipFile( + out_path, + mode="w", + compression=zipfile.ZIP_DEFLATED, + ) as zf: + zf.writestr( + f"{study.name}/input/study.antares", + data=b"[antares]\nversion = 860\n", + ) + zf.writestr( + f"{study.name}/output/20230922-1601eco/simulation.log", + data=b"Simulation OK", + ) + elif scenario == "nominal_results": with zipfile.ZipFile( out_path, mode="w", @@ -24,6 +38,10 @@ def create_final_zip(study: StudyDTO, *, scenario: str = "nominal") -> str: zf.writestr("simulation.log", data=b"Simulation OK") elif scenario == "corrupted": out_path.write_bytes(b"PK corrupted content") + elif scenario == "missing": + pass + else: + raise NotImplementedError(scenario) return str(out_path) @@ -68,12 +86,39 @@ def test_extract_final_zip__finished_study__no_output(self, finished_study: Stud assert not finished_study.final_zip_extracted @pytest.mark.unit_test - def test_extract_final_zip__finished_study__nominal(self, finished_study: StudyDTO) -> None: + def test_extract_final_zip__finished_study__nominal_study(self, finished_study: StudyDTO) -> None: display = mock.Mock(spec=DisplayTerminal) display.generate_progress_bar = lambda names, *args, **kwargs: names # Prepare a valid final ZIP - finished_study.local_final_zipfile_path = create_final_zip(finished_study) + finished_study.local_final_zipfile_path = create_final_zip(finished_study, scenario="nominal_study") + + # Initialize and execute the ZIP extraction + extractor = FinalZipExtractor(display=display) + extractor.extract_final_zip(finished_study) + + # Check the result + display.show_message.assert_called_once() + display.show_error.assert_not_called() + + assert finished_study.final_zip_extracted + assert not finished_study.with_error + + result_dir = Path(finished_study.output_dir).joinpath(finished_study.name) + expected_files = [ + "input/study.antares", + "output/20230922-1601eco/simulation.log", + ] + for file in expected_files: + assert result_dir.joinpath(file).is_file() + + @pytest.mark.unit_test + def test_extract_final_zip__finished_study__nominal_results(self, finished_study: StudyDTO) -> None: + display = mock.Mock(spec=DisplayTerminal) + display.generate_progress_bar = lambda names, *args, **kwargs: names + + # Prepare a valid final ZIP + finished_study.local_final_zipfile_path = create_final_zip(finished_study, scenario="nominal_results") # Initialize and execute the ZIP extraction extractor = FinalZipExtractor(display=display) @@ -127,8 +172,11 @@ def test_extract_final_zip__finished_study__missing(self, finished_study: StudyD assert not finished_study.final_zip_extracted assert finished_study.with_error - result_dir = Path(finished_study.local_final_zipfile_path).with_suffix("") - assert not result_dir.joinpath("simulation.log").exists() + result_dirs = [ + Path(finished_study.output_dir).joinpath(finished_study.name), + Path(finished_study.local_final_zipfile_path).with_suffix(""), + ] + assert not any(result_dir.exists() for result_dir in result_dirs) @pytest.mark.unit_test def test_extract_final_zip__finished_study__corrupted(self, finished_study: StudyDTO) -> None: @@ -149,5 +197,8 @@ def test_extract_final_zip__finished_study__corrupted(self, finished_study: Stud assert not finished_study.final_zip_extracted assert finished_study.with_error - result_dir = Path(finished_study.local_final_zipfile_path).with_suffix("") - assert not result_dir.joinpath("simulation.log").exists() + result_dirs = [ + Path(finished_study.output_dir).joinpath(finished_study.name), + Path(finished_study.local_final_zipfile_path).with_suffix(""), + ] + assert not any(result_dir.exists() for result_dir in result_dirs) From 6f78bd62a5f7c6b61a6fcb4a9a42c7710e986301 Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Mon, 25 Sep 2023 18:47:08 +0200 Subject: [PATCH 14/23] test: correct the test fixtures for study retrival --- tests/unit/retriever/conftest.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/tests/unit/retriever/conftest.py b/tests/unit/retriever/conftest.py index b0d3465..07e5467 100644 --- a/tests/unit/retriever/conftest.py +++ b/tests/unit/retriever/conftest.py @@ -14,22 +14,35 @@ def pending_study_fixture(tmp_path: Path) -> StudyDTO: return StudyDTO( path=str(study_path), started=False, - job_id=46505574, job_log_dir=str(job_log_dir), output_dir=str(output_dir), + zipfile_path="", + zip_is_sent=False, + job_id=0, ) @pytest.fixture(name="started_study") def started_study_fixture(pending_study: StudyDTO) -> StudyDTO: - return dataclasses.replace(pending_study, started=True, finished=False, with_error=False) + study_dir = Path(pending_study.path) + zip_name = f"{study_dir.name}-john_doe.zip" + zip_path = study_dir.parent / zip_name + return dataclasses.replace( + pending_study, + started=True, + finished=False, + with_error=False, + zipfile_path=str(zip_path), + zip_is_sent=True, + job_id=46505574, + ) @pytest.fixture(name="finished_study") -def finished_study_fixture(pending_study: StudyDTO) -> StudyDTO: - return dataclasses.replace(pending_study, started=True, finished=True, with_error=False) +def finished_study_fixture(started_study: StudyDTO) -> StudyDTO: + return dataclasses.replace(started_study, finished=True, with_error=False) @pytest.fixture(name="with_error_study") -def with_error_study_fixture(pending_study: StudyDTO) -> StudyDTO: - return dataclasses.replace(pending_study, started=True, finished=True, with_error=True) +def with_error_study_fixture(started_study: StudyDTO) -> StudyDTO: + return dataclasses.replace(started_study, finished=True, with_error=True) From 4c07551ae8acf15d784553e7877b9017626b306b Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Tue, 26 Sep 2023 05:49:35 +0200 Subject: [PATCH 15/23] refactor(launch-controller): simplification of the `LaunchController` class --- antareslauncher/file_manager/file_manager.py | 119 ----- antareslauncher/main.py | 7 +- .../create_list/study_list_composer.py | 1 - .../use_cases/launch/launch_controller.py | 116 +++-- .../use_cases/launch/study_submitter.py | 33 +- .../use_cases/launch/study_zip_cleaner.py | 17 - .../use_cases/launch/study_zip_uploader.py | 60 +-- .../use_cases/launch/study_zipper.py | 44 -- .../test_integration_launch_controller.py | 143 ------ tests/unit/launcher/conftest.py | 21 + tests/unit/launcher/test_launch_controller.py | 442 ++++++++++++------ tests/unit/launcher/test_submitter.py | 94 ++-- tests/unit/launcher/test_zip_uploader.py | 129 ++--- tests/unit/launcher/test_zipper.py | 63 --- tests/unit/test_file_manager.py | 66 --- 15 files changed, 536 insertions(+), 819 deletions(-) delete mode 100644 antareslauncher/use_cases/launch/study_zip_cleaner.py delete mode 100644 antareslauncher/use_cases/launch/study_zipper.py delete mode 100644 tests/integration/test_integration_launch_controller.py create mode 100644 tests/unit/launcher/conftest.py delete mode 100644 tests/unit/launcher/test_zipper.py delete mode 100644 tests/unit/test_file_manager.py diff --git a/antareslauncher/file_manager/file_manager.py b/antareslauncher/file_manager/file_manager.py index ad7d548..9aab52b 100644 --- a/antareslauncher/file_manager/file_manager.py +++ b/antareslauncher/file_manager/file_manager.py @@ -1,8 +1,6 @@ import json import logging import os -import zipfile -from pathlib import Path from antareslauncher.display.display_terminal import DisplayTerminal @@ -12,96 +10,6 @@ def __init__(self, display_terminal: DisplayTerminal): self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") self.display = display_terminal - def listdir_of(self, directory): - """Make a list of all the folders inside a directory - - Args: - directory: The directory that will be the root of the wanted list - - Returns: - A list of all the folders inside a directory - """ - self.logger.info(f"Getting directory list from path {directory}") - list_dir = os.listdir(directory) - list_dir.sort() - return list_dir - - def _get_list_dir_without_subdir(self, dir_path, subdir_to_exclude): - """Make a list of all the folders inside a directory except one - - Args: - dir_path: The directory that will be the root of the wanted list - - subdir_to_exclude:the subdir to remove from the list - - Returns: - A list of all the folders inside a directory without subdir_to_exclude - """ - list_dir = self.listdir_of(dir_path) - if subdir_to_exclude in list_dir: - list_dir.remove(subdir_to_exclude) - return list_dir - - def _get_list_of_files_recursively(self, element_path): - """Make a list of all the files inside a directory recursively - - Args: - element_path: Root dir of the list of files - - Returns: - List of all the files inside a directory recursively - """ - self.logger.info(f"Getting list of all files inside the directory {element_path}") - element_file_paths = [] - for root, _, files in os.walk(element_path): - for filename in files: - file_path = os.path.join(root, filename) - element_file_paths.append(file_path) - return element_file_paths - - def _get_complete_list_of_files_and_dirs_in_list_dir(self, dir_path, list_dir): - file_paths = [] - for element in list_dir: - element_path = os.path.join(dir_path, element) - file_paths.append(element_path) - if os.path.isdir(element_path): - element_file_paths = self._get_list_of_files_recursively(element_path) - file_paths.extend(element_file_paths) - return file_paths - - def zip_file_paths_with_rootdir_to_zipfile_path(self, zipfile_path, file_paths, root_dir): - """Zips all the files in file_paths inside zipfile_path - while printing a progress bar on the terminal - - Args: - zipfile_path: Path of the zipfile that will be created - - file_paths: Paths of all the files that need to be zipped - - root_dir: Root directory - """ - self.logger.info(f"Zipping list of files to archive {zipfile_path}") - with zipfile.ZipFile(zipfile_path, "w", compression=zipfile.ZIP_DEFLATED) as my_zip: - loading_bar = self.display.generate_progress_bar(file_paths, desc="Compressing files: ") - for f in loading_bar: - my_zip.write(f, os.path.relpath(f, root_dir)) - - def zip_dir_excluding_subdir(self, dir_path, zipfile_path, subdir_to_exclude): - """Zips a whole directory without one subdir - - Args: - dir_path: Path of the directory to zip - - zipfile_path: Path of the zip file that will be created - - subdir_to_exclude: Subdirectory that will not be zipped - """ - list_dir = self._get_list_dir_without_subdir(dir_path, subdir_to_exclude) - file_paths = self._get_complete_list_of_files_and_dirs_in_list_dir(dir_path, list_dir) - root_dir = str(Path(dir_path).parent) - self.zip_file_paths_with_rootdir_to_zipfile_path(zipfile_path, file_paths, root_dir) - return Path(zipfile_path).is_file() - def make_dir(self, directory_name): self.logger.info(f"Creating directory {directory_name}") os.makedirs(directory_name, exist_ok=True) @@ -115,30 +23,3 @@ def convert_json_file_to_dict(self, file_path): self.logger.error(f"Unable to convert {file_path} to json (file not found or invalid type)") config = None return config - - def remove_file(self, file_path: str): - """ - Given a file path, it removes it - - Args: - file_path: File path - - Returns: None - """ - try: - Path(file_path).unlink() - self.logger.info(f"file: {file_path} got deleted") - except FileNotFoundError: - self.logger.warning(f"Could not find path: {str(file_path)}") - - @staticmethod - def file_exists(file_path: str) -> bool: - """Checks if the given file path, is a regular file - - Args: - file_path: file_path - - Returns: - file path, is a regular file - """ - return Path(file_path).is_file() diff --git a/antareslauncher/main.py b/antareslauncher/main.py index 2f2eb77..79a2c06 100644 --- a/antareslauncher/main.py +++ b/antareslauncher/main.py @@ -133,12 +133,7 @@ def run_with(arguments: argparse.Namespace, parameters: MainParameters, show_ban antares_version=arguments.antares_version, ), ) - launch_controller = LaunchController( - repo=data_repo, - env=environment, - file_manager=file_manager, - display=display, - ) + launch_controller = LaunchController(repo=data_repo, env=environment, display=display) state_updater = StateUpdater(env=environment, display=display) retrieve_controller = RetrieveController( repo=data_repo, diff --git a/antareslauncher/use_cases/create_list/study_list_composer.py b/antareslauncher/use_cases/create_list/study_list_composer.py index 6220226..a9bd287 100644 --- a/antareslauncher/use_cases/create_list/study_list_composer.py +++ b/antareslauncher/use_cases/create_list/study_list_composer.py @@ -5,7 +5,6 @@ from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.study_dto import Modes, StudyDTO diff --git a/antareslauncher/use_cases/launch/launch_controller.py b/antareslauncher/use_cases/launch/launch_controller.py index 45b8067..c5fe29c 100644 --- a/antareslauncher/use_cases/launch/launch_controller.py +++ b/antareslauncher/use_cases/launch/launch_controller.py @@ -1,54 +1,102 @@ +import getpass +import zipfile +from pathlib import Path + from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.data_repo.data_reporter import DataReporter from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm -from antareslauncher.study_dto import StudyDTO from antareslauncher.use_cases.launch.study_submitter import StudySubmitter -from antareslauncher.use_cases.launch.study_zip_cleaner import StudyZipCleaner from antareslauncher.use_cases.launch.study_zip_uploader import StudyZipfileUploader -from antareslauncher.use_cases.launch.study_zipper import StudyZipper + +LOG_NAME = f"{__name__}.StudyLauncher" class StudyLauncher: def __init__( self, - zipper: StudyZipper, study_uploader: StudyZipfileUploader, - zipfile_cleaner: StudyZipCleaner, study_submitter: StudySubmitter, reporter: DataReporter, + display: DisplayTerminal, ): - self._zipper = zipper + self.display = display self._study_uploader = study_uploader - self._zipfile_cleaner = zipfile_cleaner self._study_submitter = study_submitter self.reporter = reporter - self._current_study: StudyDTO = None - def _zip_study(self): - self._current_study = self._zipper.zip(self._current_study) - self.reporter.save_study(self._current_study) + def launch_study(self, study): + if study.job_id: + # No need to display a user message here; job already exists. + return + + try: + # Compress the study folder and upload it to the SLURM server. + study_dir = Path(study.path) + zip_name = f"{study_dir.name}-{getpass.getuser()}.zip" + root_dir = study_dir.parent + zip_path = root_dir / zip_name - def _upload_zipfile(self): - self._current_study = self._study_uploader.upload(self._current_study) - self.reporter.save_study(self._current_study) + # Find all files to be compressed. + study_files = set(study_dir.rglob("*")) - def _remove_input_zipfile(self): - if self._current_study.zip_is_sent is True: - self._current_study = self._zipfile_cleaner.remove_input_zipfile(self._current_study) - self.reporter.save_study(self._current_study) + # NOTE: output filtering isn't currently handled. + # + # Antares Web sets up the study directory with pre-filtered outputs when launching, + # but this isn't the case with the CLI. + # We may introduce new parameters in `StudyDTO` for customizable output filtering + # at the study level, especially for scenarios like Xpansion sensitivity mode. + # + # Suggested parameters: + # - `exclude_pattern = "output/**/*"`: Default CLI exclusion. + # - `include_pattern = "output/{output_id}/**/*"`: Inclusion for specific output + # for Xpansion sensitivity mode (e.g., `output_id = "20230926-1230adq"`). + # + # Suggested implementation: + # study_files -= set(study_dir.glob(exclude_pattern)) if exclude_pattern else set() + # study_files |= set(study_dir.glob(include_pattern)) if include_pattern else set() - def _submit_job(self): - self._current_study = self._study_submitter.submit_job(self._current_study) - self.reporter.save_study(self._current_study) + # Compress the study directory + with zipfile.ZipFile(zip_path, mode="w", compression=zipfile.ZIP_DEFLATED) as zf: + loading_bar = self.display.generate_progress_bar(sorted(study_files), desc="Compressing files: ") + for study_file in loading_bar: + zf.write(study_file, study_file.relative_to(root_dir)) - def launch_study(self, study): - self._current_study = study - self._zip_study() - self._upload_zipfile() - self._remove_input_zipfile() - self._submit_job() + # Upload the ZIP file to the SLURM server. + # If the upload is successful, the `zip_is_sent` attribute is updated accordingly. + # In all cases, the ZIP file is removed from the local machine. + study.zipfile_path = str(zip_path) + try: + self._study_uploader.upload(study) + if not study.zip_is_sent: + raise Exception("ZIP upload failed") + except Exception as e: + self.display.show_error(f'"{study.name}": was not uploaded: {e}', LOG_NAME) + # If the ZIP file is partially uploaded, it must be removed anyway. + self._study_uploader.remove(study) + raise + finally: + zip_path.unlink() + + # Now launch the job on the SLURM server. + # If the launch is successful, the `job_id` attribute is updated accordingly. + # If the launch fails, remove the ZIP file from the remote server. + try: + self._study_submitter.submit_job(study) + if not study.job_id: + raise Exception("Job submission failed") + except Exception: + self._study_uploader.remove(study) + raise + + except Exception as e: + # The exception is not re-raised, but the job is marked as failed with an internal error message. + study.with_error = True + study.job_state = f"Internal error: {e}" + + finally: + # Save the study information after processing. + self.reporter.save_study(study) class LaunchController: @@ -56,24 +104,14 @@ def __init__( self, repo: DataRepoTinydb, env: RemoteEnvironmentWithSlurm, - file_manager: FileManager, display: DisplayTerminal, ): self.repo = repo self.env = env - self.file_manager = file_manager self.display = display - zipper = StudyZipper(file_manager, display) study_uploader = StudyZipfileUploader(env, display) - zipfile_cleaner = StudyZipCleaner(file_manager, display) study_submitter = StudySubmitter(env, display) - self.study_launcher = StudyLauncher( - zipper, - study_uploader, - zipfile_cleaner, - study_submitter, - DataReporter(repo), - ) + self.study_launcher = StudyLauncher(study_uploader, study_submitter, DataReporter(repo), display) def launch_all_studies(self): """Processes all the studies and send them to the server to process the job diff --git a/antareslauncher/use_cases/launch/study_submitter.py b/antareslauncher/use_cases/launch/study_submitter.py index b0af500..3dd14e1 100644 --- a/antareslauncher/use_cases/launch/study_submitter.py +++ b/antareslauncher/use_cases/launch/study_submitter.py @@ -1,38 +1,23 @@ -import copy from pathlib import Path from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO - -class FailedSubmissionException(Exception): - pass +LOG_NAME = f"{__name__}.StudySubmitter" class StudySubmitter(object): def __init__(self, env: RemoteEnvironmentWithSlurm, display: DisplayTerminal): self.env = env self.display = display - self._current_study: StudyDTO = None - - def submit_job(self, study: StudyDTO) -> StudyDTO: - self._current_study = copy.deepcopy(study) - if self._current_study.job_id is None: - self._do_submit() - return self._current_study - def _do_submit(self): - job_id = self.env.submit_job(copy.deepcopy(self._current_study)) - if job_id is not None: - self._current_study.job_id = job_id - self.display.show_message( - f'"{Path(self._current_study.path).name}": was submitted', - __name__ + "." + __class__.__name__, - ) + def submit_job(self, study: StudyDTO) -> None: + if study.job_id: + self.display.show_message(f'"{Path(study.path).name}": is already submitted', LOG_NAME) + return + study.job_id = self.env.submit_job(study) # may raise SubmitJobError + if study.job_id: + self.display.show_message(f'"{Path(study.path).name}": was submitted', LOG_NAME) else: - self.display.show_error( - f'"{Path(self._current_study.path).name}": was not submitted', - __name__ + "." + __class__.__name__, - ) - raise FailedSubmissionException + self.display.show_error(f'"{Path(study.path).name}": was not submitted', LOG_NAME) diff --git a/antareslauncher/use_cases/launch/study_zip_cleaner.py b/antareslauncher/use_cases/launch/study_zip_cleaner.py deleted file mode 100644 index d2635d3..0000000 --- a/antareslauncher/use_cases/launch/study_zip_cleaner.py +++ /dev/null @@ -1,17 +0,0 @@ -import copy - -from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.file_manager.file_manager import FileManager -from antareslauncher.study_dto import StudyDTO - - -class StudyZipCleaner: - def __init__(self, file_manager: FileManager, display: DisplayTerminal): - self.file_manager = file_manager - self.display = display - self._current_study: StudyDTO = StudyDTO("none") - - def remove_input_zipfile(self, study: StudyDTO) -> StudyDTO: - self._current_study = copy.deepcopy(study) - self.file_manager.remove_file(self._current_study.zipfile_path) - return self._current_study diff --git a/antareslauncher/use_cases/launch/study_zip_uploader.py b/antareslauncher/use_cases/launch/study_zip_uploader.py index 4559b4c..beb375e 100644 --- a/antareslauncher/use_cases/launch/study_zip_uploader.py +++ b/antareslauncher/use_cases/launch/study_zip_uploader.py @@ -1,51 +1,31 @@ -import copy -from pathlib import Path - from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO +LOG_NAME = f"{__name__}.StudyZipfileUploader" + class StudyZipfileUploader: def __init__(self, env: RemoteEnvironmentWithSlurm, display: DisplayTerminal): self.env = env self.display = display - self._current_study: StudyDTO = None - - def upload(self, study) -> StudyDTO: - self._current_study = copy.deepcopy(study) - if self._current_study.zip_is_sent is False: - self._do_upload() - return self._current_study - def _do_upload(self): - self._display_welcome_message() - success = self.env.upload_file(self._current_study.zipfile_path) - if success: - self._current_study.zip_is_sent = True - self._display_success_message() + def upload(self, study: StudyDTO) -> None: + if study.zip_is_sent: + self.display.show_message(f'"{study.name}": ZIP is already uploaded', LOG_NAME) + return + self.display.show_message(f'"{study.name}": uploading study...', LOG_NAME) + study.zip_is_sent = self.env.upload_file(study.zipfile_path) + if study.zip_is_sent: + self.display.show_message(f'"{study.name}": was uploaded', LOG_NAME) else: - self._display_failure_error() - raise FailedUploadException - - def _display_failure_error(self): - self.display.show_error( - f'"{Path(self._current_study.path).name}": was not uploaded', - __name__ + "." + __class__.__name__, - ) - - def _display_success_message(self): - self.display.show_message( - f'"{Path(self._current_study.path).name}": was uploaded', - __name__ + "." + __class__.__name__, - ) - - def _display_welcome_message(self): - self.display.show_message( - f'"{Path(self._current_study.path).name}": uploading study ...', - __name__ + "." + __class__.__name__, - ) - - -class FailedUploadException(Exception): - pass + self.display.show_error(f'"{study.name}": was NOT uploaded', LOG_NAME) + + def remove(self, study: StudyDTO) -> None: + # The remote ZIP file is always removed even if `zip_is_sent` is `False` + # because the ZIP file may be partially uploaded (before a failure). + study.zip_is_sent = not self.env.remove_input_zipfile(study) + if not study.zip_is_sent: + self.display.show_message(f'"{study.name}": ZIP is removed from remote', LOG_NAME) + else: + self.display.show_error(f'"{study.name}": ZIP is NOT removed from remote', LOG_NAME) diff --git a/antareslauncher/use_cases/launch/study_zipper.py b/antareslauncher/use_cases/launch/study_zipper.py deleted file mode 100644 index d62188a..0000000 --- a/antareslauncher/use_cases/launch/study_zipper.py +++ /dev/null @@ -1,44 +0,0 @@ -import copy -import getpass -from dataclasses import dataclass -from pathlib import Path - -from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.file_manager.file_manager import FileManager -from antareslauncher.study_dto import StudyDTO - - -@dataclass -class StudyZipper: - def __init__(self, file_manager: FileManager, display: DisplayTerminal): - self.file_manager = file_manager - self.display = display - self._current_study: StudyDTO = None - - def zip(self, study) -> StudyDTO: - self._current_study = copy.deepcopy(study) - - if study.zipfile_path == "": - self._do_zip() - return self._current_study - - def _do_zip(self): - zipfile_path = f"{self._current_study.path}-{getpass.getuser()}.zip" - success = self.file_manager.zip_dir_excluding_subdir(self._current_study.path, zipfile_path, None) - if success is True: - self._current_study.zipfile_path = zipfile_path - self._display_success_message() - else: - self._display_failure_error() - - def _display_failure_error(self): - self.display.show_error( - f'"{Path(self._current_study.path).name}": was not zipped', - f"{__name__}.{__class__.__name__}", - ) - - def _display_success_message(self): - self.display.show_message( - f'"{Path(self._current_study.path).name}": was zipped', - f"{__name__}.{__class__.__name__}", - ) diff --git a/tests/integration/test_integration_launch_controller.py b/tests/integration/test_integration_launch_controller.py deleted file mode 100644 index 25bee7f..0000000 --- a/tests/integration/test_integration_launch_controller.py +++ /dev/null @@ -1,143 +0,0 @@ -import getpass -import socket -from pathlib import Path -from unittest import mock - -import pytest - -from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm -from antareslauncher.remote_environnement.slurm_script_features import SlurmScriptFeatures -from antareslauncher.study_dto import StudyDTO -from antareslauncher.use_cases.launch.launch_controller import LaunchController - - -class TestIntegrationLaunchController: - @pytest.fixture(scope="function") - def launch_controller(self): - connection = mock.Mock(home_dir="path/to/home") - slurm_script_features = SlurmScriptFeatures( - "slurm_script_path", - partition="fake_partition", - quality_of_service="user1_qos", - ) - environment = RemoteEnvironmentWithSlurm(connection, slurm_script_features) - study1 = mock.Mock() - study1.zipfile_path = "filepath" - study1.zip_is_sent = False - study1.path = "path" - study2 = mock.Mock() - study2.zipfile_path = "filepath" - study2.zip_is_sent = False - study2.path = "path" - - data_repo = mock.Mock() - data_repo.get_list_of_studies = mock.Mock(return_value=[study1, study2]) - file_manager = mock.Mock() - display = DisplayTerminal() - return LaunchController( - repo=data_repo, - env=environment, - file_manager=file_manager, - display=display, - ) - - @pytest.mark.integration_test - def test_upload_file__called_twice(self, launch_controller): - """ - This test function checks if when launching two studies through the controller, - the call to the 'launch_all_studies' method triggers the 'upload_file' method - of the connection twice. - """ - # when - launch_controller.launch_all_studies() - - # then - # noinspection PyUnresolvedReferences - assert launch_controller.env.connection.upload_file.call_count == 2 - - @pytest.mark.integration_test - def test_execute_command__called_with_the_correct_parameters( - self, - ): - """ - This test function checks if when launching a study through the controller, - the call to the `submit_job` method triggers the `execute_command` method - of the connection only once, with the correct command. - """ - # given - connection = mock.Mock() - connection.execute_command = mock.Mock(return_value=["Submitted 42", ""]) - connection.home_dir = "Submitted" - slurm_script_features = SlurmScriptFeatures( - "slurm_script_path", - partition="fake_partition", - quality_of_service="user1_qos", - ) - environment = RemoteEnvironmentWithSlurm(connection, slurm_script_features) - study1 = StudyDTO( - path="dummy_path", - zipfile_path=str(Path("base_path") / "zip_name"), - zip_is_sent=False, - n_cpu=12, - antares_version="700", - time_limit=120, - ) - home_dir = "Submitted" - - remote_base_path = f"{home_dir}/REMOTE_{getpass.getuser()}_{socket.gethostname()}" - - zipfile_name = Path(study1.zipfile_path).name - job_type = "ANTARES" - post_processing = False - other_options = "" - bash_options = ( - # fmt: off - f" {zipfile_name}" - f" {study1.antares_version}" - f" {job_type}" - f" {post_processing}" - f" '{other_options}'" - # fmt: on - ) - command = ( - f"cd {remote_base_path} && " - f"sbatch" - f" --partition={slurm_script_features.partition}" - f" --qos={slurm_script_features.quality_of_service}" - f" --job-name={Path(study1.path).name}" - f" --time={study1.time_limit // 60}" - f" --cpus-per-task={study1.n_cpu}" - f" {environment.slurm_script_features.solver_script_path}" - f"{bash_options}" - ) - - data_repo = mock.Mock() - data_repo.get_list_of_studies = mock.Mock(return_value=[study1]) - file_manager = mock.Mock() - display = DisplayTerminal() - launch_controller = LaunchController( - repo=data_repo, - env=environment, - file_manager=file_manager, - display=display, - ) - # when - launch_controller.launch_all_studies() # _submit_job(study1) - - # then - connection.execute_command.assert_called_once_with(command) - - @pytest.mark.integration_test - def test_remove_zip_file__called_twice(self, launch_controller): - """ - This test function checks if when executing the `launch_all_studies` with two sent studies, - the `remove_zip_file` method is called twice. - """ - launch_controller.file_manager.remove_file = mock.Mock() - - # when - launch_controller.launch_all_studies() - - # then - assert launch_controller.file_manager.remove_file.call_count == 2 diff --git a/tests/unit/launcher/conftest.py b/tests/unit/launcher/conftest.py new file mode 100644 index 0000000..3797bae --- /dev/null +++ b/tests/unit/launcher/conftest.py @@ -0,0 +1,21 @@ +from pathlib import Path + +import pytest + +from antareslauncher.study_dto import StudyDTO + + +@pytest.fixture(name="pending_study") +def pending_study_fixture(tmp_path: Path) -> StudyDTO: + study_path = tmp_path.joinpath("My Study") + job_log_dir = tmp_path.joinpath("LOG_DIR") + output_dir = tmp_path.joinpath("OUTPUT_DIR") + return StudyDTO( + path=str(study_path), + started=False, + job_log_dir=str(job_log_dir), + output_dir=str(output_dir), + zipfile_path="", + zip_is_sent=False, + job_id=0, + ) diff --git a/tests/unit/launcher/test_launch_controller.py b/tests/unit/launcher/test_launch_controller.py index 5aa4ec6..590419a 100644 --- a/tests/unit/launcher/test_launch_controller.py +++ b/tests/unit/launcher/test_launch_controller.py @@ -1,170 +1,314 @@ -import copy -import getpass +import zipfile +from pathlib import Path, PurePosixPath from unittest import mock -from unittest.mock import call import pytest -import antareslauncher.remote_environnement.remote_environment_with_slurm -import antareslauncher.use_cases.launch.study_submitter -import antareslauncher.use_cases.launch.study_zip_uploader from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb -from antareslauncher.data_repo.data_reporter import DataReporter from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO -from antareslauncher.use_cases.launch import launch_controller -from antareslauncher.use_cases.launch.launch_controller import StudyLauncher +from antareslauncher.use_cases.launch.launch_controller import LaunchController, StudyLauncher from antareslauncher.use_cases.launch.study_submitter import StudySubmitter -from antareslauncher.use_cases.launch.study_zip_cleaner import StudyZipCleaner from antareslauncher.use_cases.launch.study_zip_uploader import StudyZipfileUploader -from antareslauncher.use_cases.launch.study_zipper import StudyZipper + +# noinspection SpellCheckingInspection +STUDY_FILES = [ + "check-config.json", + "Desktop.ini", + "input/areas/dummy.txt", + "input/wind/dummy.txt", + "layers/layers.ini", + "output/20230321-1901eco/dummy.txt", + "output/20230321-1901eco.zip", + "output/20230926-1230adq/dummy.txt", + "settings/comments.txt", + "settings/generaldata.ini", + "settings/resources/dummy.txt", + "settings/scenariobuilder.dat", + "study.antares", +] + + +def prepare_study_data(study_dir: Path) -> None: + for file in STUDY_FILES: + file_path = study_dir.joinpath(file) + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.touch() + + +@pytest.fixture(name="ready_study") +def ready_study_fixture(pending_study: StudyDTO) -> StudyDTO: + """Prepare the study data and return the study.""" + study_dir = Path(pending_study.path) + prepare_study_data(study_dir) + return pending_study + + +@pytest.fixture(name="study_uploaded") +def study_uploaded_fixture(tmp_path: Path) -> StudyDTO: + study_path = tmp_path.joinpath("upload-failure") + job_log_dir = tmp_path.joinpath("LOG_DIR") + output_dir = tmp_path.joinpath("OUTPUT_DIR") + study = StudyDTO( + path=str(study_path), + started=False, + job_log_dir=str(job_log_dir), + output_dir=str(output_dir), + zipfile_path="", + zip_is_sent=False, + job_id=0, + ) + study_dir = Path(study.path) + prepare_study_data(study_dir) + return study + + +@pytest.fixture(name="study_submitted") +def study_submitted_fixture(tmp_path: Path) -> StudyDTO: + study_path = tmp_path.joinpath("submit-failure") + job_log_dir = tmp_path.joinpath("LOG_DIR") + output_dir = tmp_path.joinpath("OUTPUT_DIR") + study = StudyDTO( + path=str(study_path), + started=False, + job_log_dir=str(job_log_dir), + output_dir=str(output_dir), + zipfile_path="", + zip_is_sent=False, + job_id=0, + ) + study_dir = Path(study.path) + prepare_study_data(study_dir) + return study class TestStudyLauncher: - def setup_method(self): - env = mock.Mock(spec_set=RemoteEnvironmentWithSlurm) - display = mock.Mock(spec_set=DisplayTerminal) - file_manager = mock.Mock(spec_set=FileManager) - repo = mock.Mock(spec_set=DataRepoTinydb) - self.reporter = DataReporter(repo) - self.zipper = StudyZipper(file_manager, display) - self.study_uploader = StudyZipfileUploader(env, display) - self.zipfile_cleaner = StudyZipCleaner(file_manager, display) - self.study_submitter = StudySubmitter(env, display) - self.study_launcher = StudyLauncher( - self.zipper, - self.study_uploader, - self.zipfile_cleaner, - self.study_submitter, - self.reporter, - ) - - def test_launch_study_calls_all_four_steps(self): - study = StudyDTO(path="hello") - study1 = StudyDTO(path="hello1") - self.zipper.zip = mock.Mock(return_value=study1) - study2 = StudyDTO(path="hello2", zip_is_sent=True) - self.study_uploader.upload = mock.Mock(return_value=study2) - study3 = StudyDTO(path="hello3") - self.zipfile_cleaner.remove_input_zipfile = mock.Mock(return_value=study3) - study4 = StudyDTO(path="hello4") - self.study_submitter.submit_job = mock.Mock(return_value=study4) - self.reporter.save_study = mock.Mock() - - self.study_launcher.launch_study(study) - - self.zipper.zip.assert_called_once_with(study) - self.study_uploader.upload.assert_called_once_with(study1) - self.zipfile_cleaner.remove_input_zipfile.assert_called_once_with(study2) - self.study_submitter.submit_job.assert_called_once_with(study3) - - assert self.reporter.save_study.call_count == 4 - calls = self.reporter.save_study.call_args_list - assert calls[0] == call(study1) - assert calls[1] == call(study2) - assert calls[2] == call(study3) - assert calls[3] == call(study4) - - -class TestLauncherController: - def setup_method(self): - self.data_repo = DataRepoTinydb("", "name") - self.data_repo.save_study = mock.Mock() - self.display = mock.Mock() - - @pytest.fixture(scope="function") - def my_launch_controller(self): - expected_study = StudyDTO(path="hello") - list_of_studies = [copy.deepcopy(expected_study)] - self.data_repo.get_list_of_studies = mock.Mock(return_value=list_of_studies) - remote_env_mock = mock.Mock(spec=RemoteEnvironmentWithSlurm) - file_manager_mock = mock.Mock() - my_launcher = launch_controller.LaunchController( - self.data_repo, remote_env_mock, file_manager_mock, self.display - ) - return my_launcher, expected_study - - def test_with_one_study_the_compressor_is_called_once(self): - my_study = StudyDTO(path="hello") - list_of_studies = [my_study] - self.data_repo.get_list_of_studies = mock.Mock(return_value=list_of_studies) - - remote_env_mock = mock.Mock(spec=RemoteEnvironmentWithSlurm) - file_manager = mock.Mock(spec_set=FileManager) - file_manager.zip_dir_excluding_subdir = mock.Mock() - - my_launcher = launch_controller.LaunchController(self.data_repo, remote_env_mock, file_manager, self.display) - my_launcher.launch_all_studies() - - zipfile_path = f"{my_study.path}-{getpass.getuser()}.zip" - file_manager.zip_dir_excluding_subdir.assert_called_once_with(my_study.path, zipfile_path, None) + """ + The gaol is to test the launching of a study. + Every call to the remote environment is mocked. + """ @pytest.mark.unit_test - def test_given_one_study_then_repo_is_called_to_save_the_study_with_updated_zip_is_sent(self, my_launch_controller): - # given - my_launcher, expected_study = my_launch_controller - # when - my_launcher.env.upload_file = mock.Mock(return_value=True) - my_launcher.repo.save_study = mock.Mock() - my_launcher.launch_all_studies() - # then - expected_study.zipfile_path = f"{expected_study.path}-{getpass.getuser()}.zip" - second_call = my_launcher.repo.save_study.call_args_list[1] - first_argument = second_call[0][0] - assert first_argument.zip_is_sent + def test_launch_study__nominal_case(self, ready_study: StudyDTO) -> None: + """ + Test the nominal case of launching a study. - @pytest.mark.unit_test - def test_given_one_study_when_launcher_is_called_then_study_is_saved_with_job_id_and_submitted_flag( - self, my_launch_controller - ): - # given - my_launcher, expected_study = my_launch_controller - # when - my_launcher.env.upload_file = mock.Mock(return_value=True) - my_launcher.env.submit_job = mock.Mock(return_value=42) - my_launcher.repo.save_study = mock.Mock() - my_launcher.launch_all_studies() - # then - expected_study.zipfile_path = "ciao.zip" - expected_study.zip_is_sent = True - fourth_call = my_launcher.repo.save_study.call_args_list[3] - first_argument = fourth_call[0][0] - assert first_argument.job_id == 42 + - The study directory must be correctly compressed. + - The `upload` method of the `study_uploader` must be called. + - The ZIP file must be removed after the upload. + - The `submit_job` method of the `study_submitter` must be called. + - The `save_study` method of the `data_repo` must be called. + """ - @pytest.mark.unit_test - def test_given_one_study_when_submit_fails_then_exception_is_raised(self, my_launch_controller): - # given - my_launcher, expected_study = my_launch_controller - # when - my_launcher.env.upload_file = mock.Mock(return_value=True) - my_launcher.env.submit_job = mock.Mock(return_value=None) - my_launcher.repo.save_study = mock.Mock() - # then - with pytest.raises(antareslauncher.use_cases.launch.study_submitter.FailedSubmissionException): - my_launcher.launch_all_studies() + class Uploader: + def __init__(self): + self.actual_names = frozenset() + + def upload(self, study: StudyDTO) -> None: + """Simulate the upload and check that the ZIP file has been correctly created.""" + zip_path = Path(study.zipfile_path) + with zipfile.ZipFile(zip_path, mode="r") as zf: + # keep only file names, excluding directories + self.actual_names = frozenset(name for name in zf.namelist() if "." in name) + study.zip_is_sent = True + + __call__ = upload + + upload = Uploader() + + def submit_job(study: StudyDTO) -> None: + """Simulate the submission of the job.""" + study.job_id = 40414243 + # Given + study_uploader = mock.Mock(spec=StudyZipfileUploader) + study_uploader.upload = upload + study_uploader.remove = mock.Mock() + + study_submitter = mock.Mock(spec=StudySubmitter) + study_submitter.submit_job = submit_job + + data_repo = mock.Mock(spec=DataRepoTinydb) + display = mock.Mock(spec=DisplayTerminal) + display.generate_progress_bar = mock.Mock(side_effect=lambda x, **kwargs: x) + study_launcher = StudyLauncher(study_uploader, study_submitter, data_repo, display) + + # When + study_launcher.launch_study(ready_study) + + # Then + prefix_path = PurePosixPath(ready_study.name) + expected_names = frozenset([prefix_path.joinpath(name).as_posix() for name in STUDY_FILES]) + assert upload.actual_names == expected_names + + assert ready_study.zipfile_path, "The ZIP file should have been created" + assert ready_study.zip_is_sent, "The ZIP file should have been uploaded" + assert ready_study.job_id == 40414243, "The job should have been submitted" + assert not ready_study.with_error, "The study should not be marked as failed" + + assert not Path(ready_study.zipfile_path).exists(), "The ZIP file should have been removed" + + study_uploader.remove.assert_not_called() + data_repo.save_study.assert_called_once() + + @pytest.mark.parametrize("scenario", ["set_false", "raise_exception"]) @pytest.mark.unit_test - def test_given_one_study_when_zip_fails_then_return_none(self, my_launch_controller): - # given - my_launcher, expected_study = my_launch_controller - my_launcher.file_manager.zip_dir_excluding_subdir = mock.Mock(return_value=False) - # when - my_launcher.launch_all_studies() - # then - assert expected_study.zipfile_path is "" + def test_launch_study__upload_fails(self, ready_study: StudyDTO, scenario: str) -> None: + def upload(study: StudyDTO) -> None: + """Simulate the upload that fails""" + if scenario == "set_false": + study.zip_is_sent = False + elif scenario == "raise_exception": + raise Exception("Upload failed") + else: + raise NotImplementedError(scenario) + + # Given + study_uploader = mock.Mock(spec=StudyZipfileUploader) + study_uploader.upload = upload + + study_submitter = mock.Mock(spec=StudySubmitter) + + data_repo = mock.Mock(spec=DataRepoTinydb) + display = mock.Mock(spec=DisplayTerminal) + display.generate_progress_bar = mock.Mock(side_effect=lambda x, **kwargs: x) + study_launcher = StudyLauncher(study_uploader, study_submitter, data_repo, display) + + # When + study_launcher.launch_study(ready_study) + + # Then + assert ready_study.zipfile_path, "The ZIP file should have been created" + assert not ready_study.zip_is_sent, "The ZIP file should not have been uploaded" + assert not ready_study.job_id, "The job should not have been submitted" + assert ready_study.with_error, "The study should be marked as failed" + + assert not Path(ready_study.zipfile_path).exists(), "The ZIP file should have been removed" + study_submitter.submit_job.assert_not_called() + study_uploader.remove.assert_called_once() + data_repo.save_study.assert_called_once() + + @pytest.mark.parametrize("scenario", ["set_null", "raise_exception"]) @pytest.mark.unit_test - def test_given_a_sent_study_when_launch_all_studies_called_then_file_manager_remove_zip_file_is_called_once( - self, my_launch_controller - ): - # given - my_launcher, expected_study = my_launch_controller - my_launcher._upload_zipfile = mock.Mock(return_value=True) - my_launcher.file_manager.remove_file = mock.Mock() - - # when - my_launcher.launch_all_studies() - # then - my_launcher.file_manager.remove_file.assert_called_once() + def test_launch_study__submit_job_fails(self, ready_study: StudyDTO, scenario: str) -> None: + def upload(study: StudyDTO) -> None: + study.zip_is_sent = True + + def submit_job(study: StudyDTO) -> None: + """Simulate the submission of the job that fails""" + if scenario == "set_null": + study.job_id = 0 + elif scenario == "raise_exception": + raise Exception("Simulation of submission failure") + else: + raise NotImplementedError(scenario) + + # Given + study_uploader = mock.Mock(spec=StudyZipfileUploader) + study_uploader.upload = upload + study_submitter = mock.Mock(spec=StudySubmitter) + study_submitter.submit_job = submit_job + + data_repo = mock.Mock(spec=DataRepoTinydb) + display = mock.Mock(spec=DisplayTerminal) + display.generate_progress_bar = mock.Mock(side_effect=lambda x, **kwargs: x) + study_launcher = StudyLauncher(study_uploader, study_submitter, data_repo, display) + + # When + study_launcher.launch_study(ready_study) + + # Then + assert ready_study.zipfile_path, "The ZIP file should have been created" + assert ready_study.zip_is_sent, "The ZIP file should have been uploaded" + assert not ready_study.job_id, "The job should not have been submitted" + assert ready_study.with_error, "The study should be marked as failed" + + assert not Path(ready_study.zipfile_path).exists(), "The ZIP file should have been removed" + + study_uploader.remove.assert_called_once() + data_repo.save_study.assert_called_once() + + +class TestLaunchController: + def test_launch_all_studies__nominal_case(self, ready_study: StudyDTO) -> None: + # Given + data_repo = mock.Mock(spec=DataRepoTinydb) + data_repo.get_list_of_studies = mock.Mock(return_value=[ready_study]) + + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.upload_file = mock.Mock(return_value=True) + env.submit_job = mock.Mock(return_value=40414243) + + display = mock.Mock(spec=DisplayTerminal) + display.generate_progress_bar = mock.Mock(side_effect=lambda x, **kwargs: x) + + launch_controller = LaunchController(data_repo, env, display) + + # When + launch_controller.launch_all_studies() + + # Then + assert ready_study.zipfile_path, "The ZIP file should have been created" + assert ready_study.zip_is_sent, "The ZIP file should have been uploaded" + assert ready_study.job_id == 40414243, "The job should have been submitted" + assert not ready_study.with_error, "The study should not be marked as failed" + + assert not Path(ready_study.zipfile_path).exists(), "The ZIP file should have been removed" + + data_repo.save_study.assert_called_once() + + def test_launch_all_studies__bad_studies( + self, + study_uploaded: StudyDTO, + study_submitted: StudyDTO, + ready_study: StudyDTO, + ) -> None: + """ + We want to check that even if some studies fail on download or submission, + all valid studies are processed correctly. + """ + + def upload_file(src: str) -> bool: + return "upload-failure" not in src + + class JobSubmitter: + def __init__(self): + self.job_id = 0 + + def __call__(self, study: StudyDTO) -> int: + self.job_id += 1 + return 0 if study.name == "submit-failure" else self.job_id + + # Given + data_repo = mock.Mock(spec=DataRepoTinydb) + studies = [study_uploaded, study_submitted, ready_study] + data_repo.get_list_of_studies = mock.Mock(return_value=studies) + + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.upload_file = upload_file + env.submit_job = JobSubmitter() + env.remove_input_zipfile = mock.Mock(return_value=True) + + display = mock.Mock(spec=DisplayTerminal) + display.generate_progress_bar = mock.Mock(side_effect=lambda x, **kwargs: x) + + launch_controller = LaunchController(data_repo, env, display) + + # When + launch_controller.launch_all_studies() + + # Then + actual_states = [ + {"zip_is_sent": study.zip_is_sent, "job_id": study.job_id, "with_error": study.with_error} + for study in studies + ] + + # In case of upload failure, the remote ZIP file is + expected_states = [ + {"zip_is_sent": False, "job_id": 0, "with_error": True}, + {"zip_is_sent": False, "job_id": 0, "with_error": True}, + {"zip_is_sent": True, "job_id": 2, "with_error": False}, + ] + assert actual_states == expected_states diff --git a/tests/unit/launcher/test_submitter.py b/tests/unit/launcher/test_submitter.py index 9ca8991..6bb729b 100644 --- a/tests/unit/launcher/test_submitter.py +++ b/tests/unit/launcher/test_submitter.py @@ -1,9 +1,7 @@ -from dataclasses import asdict from unittest import mock import pytest -import antareslauncher.use_cases from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO @@ -11,56 +9,48 @@ class TestStudySubmitter: - def setup_method(self): - self.remote_env = mock.Mock(spec_set=RemoteEnvironmentWithSlurm) - self.display_mock = mock.Mock(spec_set=DisplayTerminal) - self.study_submitter = StudySubmitter(self.remote_env, self.display_mock) - + @pytest.mark.parametrize("actual_job_id", [0, 123456]) @pytest.mark.unit_test - def test_submit_study_shows_message_if_submit_succeeds(self): - self.remote_env.submit_job = mock.Mock(return_value=42) - study = StudyDTO(path="hello") - - new_study = self.study_submitter.submit_job(study) - - expected_message = f'"hello": was submitted' - self.display_mock.show_message.assert_called_once_with(expected_message, mock.ANY) - assert new_study.job_id == 42 - - @pytest.mark.unit_test - def test_submit_study_shows_error_if_submit_fails_and_exception_is_raised( - self, - ): - self.remote_env.submit_job = mock.Mock(return_value=None) - study = StudyDTO(path="hello") - - with pytest.raises(antareslauncher.use_cases.launch.study_submitter.FailedSubmissionException): - self.study_submitter.submit_job(study) - - expected_error_message = f'"hello": was not submitted' - self.display_mock.show_error.assert_called_once_with(expected_error_message, mock.ANY) + def test_submit_job__nominal_case(self, pending_study: StudyDTO, actual_job_id: int) -> None: + # Given + pending_study.job_id = actual_job_id + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.submit_job = mock.Mock(return_value=987654) + display = mock.Mock(spec=DisplayTerminal) + submitter = StudySubmitter(env, display) + + # When + submitter.submit_job(pending_study) + + # Then + if actual_job_id: + # The study job_id is not changed + assert pending_study.job_id == actual_job_id + # The display shows a message + display.show_message.assert_called_once() + display.show_error.assert_not_called() + else: + # The study job_id is changed + assert pending_study.job_id == 987654 + # The display shows a message + display.show_message.assert_called_once() + display.show_error.assert_not_called() @pytest.mark.unit_test - def test_remote_env_not_called_if_study_has_already_a_jobid(self): - self.remote_env.submit_job = mock.Mock() - study = StudyDTO(path="hello") - study.job_id = 42 - - self.study_submitter.submit_job(study) - - self.remote_env.submit_job.assert_not_called() - - @pytest.mark.unit_test - def test_remote_env_is_called_if_study_has_no_jobid(self): - self.remote_env.submit_job = mock.Mock(return_value=42) - study = StudyDTO(path="hello") - study.zipfile_path = "ciao.zip" - study.job_id = None - - new_study = self.study_submitter.submit_job(study) - - self.remote_env.submit_job.assert_called_once() - first_call = self.remote_env.submit_job.call_args_list[0] - first_argument = first_call[0][0] - assert asdict(first_argument) == asdict(study) - assert new_study.job_id is 42 + def test_submit_job__error_case(self, pending_study: StudyDTO) -> None: + # Given + pending_study.job_id = 0 + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.submit_job = mock.Mock(return_value=0) + display = mock.Mock(spec=DisplayTerminal) + submitter = StudySubmitter(env, display) + + # When + submitter.submit_job(pending_study) + + # Then + # The study job_id is not changed + assert pending_study.job_id == 0 + # The display shows an error + display.show_message.assert_not_called() + display.show_error.assert_called_once() diff --git a/tests/unit/launcher/test_zip_uploader.py b/tests/unit/launcher/test_zip_uploader.py index 64079cd..094f04b 100644 --- a/tests/unit/launcher/test_zip_uploader.py +++ b/tests/unit/launcher/test_zip_uploader.py @@ -1,71 +1,88 @@ -from copy import copy from unittest import mock -from unittest.mock import call import pytest from antareslauncher.display.display_terminal import DisplayTerminal from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm from antareslauncher.study_dto import StudyDTO -from antareslauncher.use_cases.launch.study_zip_uploader import FailedUploadException, StudyZipfileUploader +from antareslauncher.use_cases.launch.study_zip_uploader import StudyZipfileUploader -class TestZipfileUploader: - def setup_method(self): - self.remote_env = mock.Mock(spec_set=RemoteEnvironmentWithSlurm) - self.display_mock = mock.Mock(spec_set=DisplayTerminal) - self.study_uploader = StudyZipfileUploader(self.remote_env, self.display_mock) - +class TestStudyZipfileUploader: + @pytest.mark.parametrize("actual_sent_flag", [True, False]) @pytest.mark.unit_test - def test_upload_study_shows_message_if_upload_succeeds(self): - self.remote_env.upload_file = mock.Mock(return_value=True) - study = StudyDTO(path="hello") - - self.study_uploader.upload(study) - - expected_message1 = f'"hello": uploading study ...' - expected_message2 = f'"hello": was uploaded' - calls = [ - call(expected_message1, mock.ANY), - call(expected_message2, mock.ANY), - ] - self.display_mock.show_message.assert_has_calls(calls) + def test_upload__nominal_case(self, pending_study: StudyDTO, actual_sent_flag: bool) -> None: + # Given + pending_study.zip_is_sent = actual_sent_flag + display = mock.Mock(spec=DisplayTerminal) + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.upload_file = mock.Mock(return_value=True) + uploader = StudyZipfileUploader(env, display) + + # When + uploader.upload(pending_study) + + # Then + if actual_sent_flag: + env.upload_file.assert_not_called() + display.show_message.assert_called_once() + else: + env.upload_file.assert_called_once_with(pending_study.zipfile_path) + assert display.show_message.call_count == 2 + display.show_error.assert_not_called() + assert pending_study.zip_is_sent @pytest.mark.unit_test - def test_upload_study_shows_error_if_upload_fails_and_exception_is_raised( - self, - ): - self.remote_env.upload_file = mock.Mock(return_value=False) - study = StudyDTO(path="hello") - - with pytest.raises(FailedUploadException): - self.study_uploader.upload(study) - - expected_welcome_message = f'"hello": uploading study ...' - expected_error_message = f'"hello": was not uploaded' - self.display_mock.show_message.assert_called_once_with(expected_welcome_message, mock.ANY) - self.display_mock.show_error.assert_called_once_with(expected_error_message, mock.ANY) - + def test_upload__error_case(self, pending_study: StudyDTO) -> None: + # Given + display = mock.Mock(spec=DisplayTerminal) + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.upload_file = mock.Mock(return_value=False) + uploader = StudyZipfileUploader(env, display) + + # When + uploader.upload(pending_study) + + # Then + env.upload_file.assert_called_once_with(pending_study.zipfile_path) + assert display.show_message.call_count == 1 + assert display.show_error.call_count == 1 + assert not pending_study.zip_is_sent + + @pytest.mark.parametrize("actual_sent_flag", [True, False]) @pytest.mark.unit_test - def test_remote_env_not_called_if_upload_was_done(self): - self.remote_env.upload_file = mock.Mock() - study = StudyDTO(path="hello") - study.zip_is_sent = True - - new_study = self.study_uploader.upload(study) - - self.remote_env.upload_file.assert_not_called() - assert new_study == study + def test_remove__nominal_case(self, pending_study: StudyDTO, actual_sent_flag: bool) -> None: + # Given + pending_study.zip_is_sent = actual_sent_flag + display = mock.Mock(spec=DisplayTerminal) + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.remove_input_zipfile = mock.Mock(return_value=True) + uploader = StudyZipfileUploader(env, display) + + # When + uploader.remove(pending_study) + + # Then + # NOTE: The remote ZIP file is always removed even if `zip_is_sent` is `False`. + env.remove_input_zipfile.assert_called_once_with(pending_study) + display.show_message.assert_called_once() + display.show_error.assert_not_called() + assert not pending_study.zip_is_sent @pytest.mark.unit_test - def test_remote_env_is_called_if_upload_was_not_done(self): - self.remote_env.upload_file = mock.Mock() - study = StudyDTO(path="hello") - study.zip_is_sent = False - expected_study = copy(study) - expected_study.zip_is_sent = True - - new_study = self.study_uploader.upload(study) - - self.remote_env.upload_file.assert_called_once_with(study.zipfile_path) - assert new_study == expected_study + def test_remove__error_case(self, pending_study: StudyDTO) -> None: + # Given + pending_study.zip_is_sent = True + display = mock.Mock(spec=DisplayTerminal) + env = mock.Mock(spec=RemoteEnvironmentWithSlurm) + env.remove_input_zipfile = mock.Mock(return_value=False) + uploader = StudyZipfileUploader(env, display) + + # When + uploader.remove(pending_study) + + # Then + env.remove_input_zipfile.assert_called_once_with(pending_study) + display.show_message.assert_not_called() + display.show_error.assert_called_once() + assert pending_study.zip_is_sent diff --git a/tests/unit/launcher/test_zipper.py b/tests/unit/launcher/test_zipper.py deleted file mode 100644 index a25e22f..0000000 --- a/tests/unit/launcher/test_zipper.py +++ /dev/null @@ -1,63 +0,0 @@ -import getpass -from unittest import mock - -import pytest - -from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.file_manager.file_manager import FileManager -from antareslauncher.study_dto import StudyDTO -from antareslauncher.use_cases.launch.study_zipper import StudyZipper - - -class TestStudyZipper: - def setup_method(self): - self.file_manager = mock.Mock(spec_set=FileManager) - self.display_mock = mock.Mock(spec_set=DisplayTerminal) - self.study_zipper = StudyZipper(self.file_manager, self.display_mock) - - @pytest.mark.unit_test - def test_zip_study_show_message_if_zip_succeeds(self): - self.file_manager.zip_dir_excluding_subdir = mock.Mock(return_value=True) - study = StudyDTO(path="hello") - - self.study_zipper.zip(study) - - expected_message = '"hello": was zipped' - self.display_mock.show_message.assert_called_once_with(expected_message, mock.ANY) - - @pytest.mark.unit_test - def test_zip_study_show_error_if_zip_fails(self): - self.file_manager.zip_dir_excluding_subdir = mock.Mock(return_value=False) - study = StudyDTO(path="hello") - - new_study = self.study_zipper.zip(study) - - expected_message = '"hello": was not zipped' - self.display_mock.show_error.assert_called_once_with(expected_message, mock.ANY) - assert new_study.zipfile_path == "" - - @pytest.mark.unit_test - def test_file_manager_not_called_if_zip_exists(self): - self.file_manager.zip_dir_excluding_subdir = mock.Mock() - study = StudyDTO(path="hello") - study.zipfile_path = "ciao.zip" - - new_zip = self.study_zipper.zip(study) - - self.file_manager.zip_dir_excluding_subdir.assert_not_called() - self.display_mock.show_error.assert_not_called() - self.display_mock.show_message.assert_not_called() - assert new_zip == study - - @pytest.mark.unit_test - def test_file_manager_is_called_if_zip_doesnt_exist(self): - self.file_manager.zip_dir_excluding_subdir = mock.Mock(return_value=True) - study_path = "hello" - study = StudyDTO(path=study_path) - study.zipfile_path = "" - - new_study = self.study_zipper.zip(study) - - expected_zipfile_path = f"{study.path}-{getpass.getuser()}.zip" - self.file_manager.zip_dir_excluding_subdir.assert_called_once_with(study_path, expected_zipfile_path, None) - assert new_study.zipfile_path == expected_zipfile_path diff --git a/tests/unit/test_file_manager.py b/tests/unit/test_file_manager.py deleted file mode 100644 index e751efd..0000000 --- a/tests/unit/test_file_manager.py +++ /dev/null @@ -1,66 +0,0 @@ -import os -import shutil -from pathlib import Path -from unittest import mock - -import pytest - -from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.file_manager import file_manager -from tests.data import DATA_DIR - -DIR_TO_ZIP = DATA_DIR / "file-manager-test" / "to-zip" -DIR_REF = DATA_DIR / "file-manager-test" / "reference-without-output" / "to-zip" - - -def get_dict_from_path(path: Path): - if os.path.isdir(path): - return { - "name": path.name, - "type": "directory", - "children": list(map(get_dict_from_path, path.iterdir())), - } - else: - return { - "name": path.name, - "type": "file", - } - - -class TestFileManager: - @pytest.mark.unit_test - def test_golden_master_for_zip_study_excluding_output_dir(self, tmp_path): - dir_to_zip = DIR_TO_ZIP - zip_name = str(dir_to_zip) + ".zip" - display_terminal = DisplayTerminal() - my_file_manager = file_manager.FileManager(display_terminal) - - my_file_manager.zip_dir_excluding_subdir(dir_to_zip, zip_name, "output") - - shutil.unpack_archive(zip_name, tmp_path) - results = tmp_path / dir_to_zip.name - results_dict = get_dict_from_path(results) - reference_dict = get_dict_from_path(DIR_REF) - - assert results_dict == reference_dict - result_zip_file = Path(zip_name) - assert result_zip_file.is_file() - result_zip_file.unlink() - - @pytest.mark.unit_test - def test__get_list_dir_without_subdir(self): - """ - Tests the case where a directory path and a subdirectory name are given as inputs to the function. - The function is expected to return a list of items in the directory, excluding the specified subdirectory. - """ - # given - display_terminal = DisplayTerminal() - my_file_manager = file_manager.FileManager(display_terminal) - listdir = ["dir1", "dir2", "dir42"] - my_file_manager.listdir_of = mock.Mock(return_value=listdir.copy()) - subdir_to_exclude = "dir42" - listdir.remove(subdir_to_exclude) - # when - output = my_file_manager._get_list_dir_without_subdir("", subdir_to_exclude) - # then - assert listdir == output From f11641d4d233d61f91b9cbebf6263780ff14eb88 Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Tue, 26 Sep 2023 11:13:50 +0200 Subject: [PATCH 16/23] chore(typing): improve the typing of study parameters --- antareslauncher/antares_launcher.py | 2 +- antareslauncher/config.py | 1 + antareslauncher/main.py | 21 +++++---- antareslauncher/main_option_parser.py | 2 +- .../remote_environment_with_slurm.py | 8 ++-- .../slurm_script_features.py | 2 +- .../remote_environnement/ssh_connection.py | 45 ++++++++----------- antareslauncher/study_dto.py | 32 ++++++++----- .../create_list/study_list_composer.py | 34 +++++--------- .../use_cases/retrieve/state_updater.py | 2 +- .../use_cases/retrieve/study_retriever.py | 2 +- tests/unit/conftest.py | 2 +- tests/unit/test_config.py | 39 ++++++++-------- .../test_remote_environment_with_slurm.py | 2 +- tests/unit/test_study_list_composer.py | 2 +- 15 files changed, 97 insertions(+), 99 deletions(-) diff --git a/antareslauncher/antares_launcher.py b/antareslauncher/antares_launcher.py index 52166d4..c483555 100644 --- a/antareslauncher/antares_launcher.py +++ b/antareslauncher/antares_launcher.py @@ -19,7 +19,7 @@ class AntaresLauncher: wait_controller: WaitController wait_mode: bool wait_time: int - xpansion_mode: Optional[str] + xpansion_mode: str check_queue_bool: bool job_id_to_kill: Optional[int] = None diff --git a/antareslauncher/config.py b/antareslauncher/config.py index 67f2564..e27a5fc 100644 --- a/antareslauncher/config.py +++ b/antareslauncher/config.py @@ -272,6 +272,7 @@ def get_user_config_dir(system: str = ""): """ username = getpass.getuser() system = system or sys.platform + config_dir: pathlib.Path if system == "win32": config_dir = pathlib.WindowsPath(rf"C:\Users\{username}\AppData\Local\{APP_AUTHOR}") elif system == "darwin": diff --git a/antareslauncher/main.py b/antareslauncher/main.py index 79a2c06..696b1f3 100644 --- a/antareslauncher/main.py +++ b/antareslauncher/main.py @@ -1,7 +1,8 @@ import argparse import dataclasses +import json from pathlib import Path -from typing import Dict, List +import typing as t from antareslauncher import __version__ from antareslauncher.antares_launcher import AntaresLauncher @@ -68,8 +69,8 @@ class MainParameters: json_dir: Path default_json_db_name: str slurm_script_path: str - antares_versions_on_remote_server: List[str] - default_ssh_dict: Dict + antares_versions_on_remote_server: t.Sequence[str] + default_ssh_dict: t.Mapping[str, t.Any] db_primary_key: str partition: str = "" quality_of_service: str = "" @@ -104,7 +105,7 @@ def run_with(arguments: argparse.Namespace, parameters: MainParameters, show_ban # connection ssh_dict = get_ssh_config_dict( file_manager, - arguments.json_ssh_config, + arguments.json_ssh_config, # Path to the configuration file for the ssh connection. parameters.default_ssh_dict, ) connection = ssh_connection.SshConnection(config=ssh_dict) @@ -179,11 +180,13 @@ def verify_connection(connection, display): # fmt: on -def get_ssh_config_dict(file_manager, json_ssh_config, ssh_dict: dict): - if json_ssh_config is None: +def get_ssh_config_dict( + file_manager: FileManager, + json_ssh_config: str, + ssh_dict: t.Mapping[str, t.Any], +) -> t.Mapping[str, t.Any]: + if not json_ssh_config: ssh_dict = ssh_dict else: - ssh_dict = file_manager.convert_json_file_to_dict(json_ssh_config) - if ssh_dict is None: - raise Exception("Could not find any SSH configuration file") + ssh_dict = json.loads(Path(json_ssh_config).read_text(encoding="utf-8")) return ssh_dict diff --git a/antareslauncher/main_option_parser.py b/antareslauncher/main_option_parser.py index b9780de..ad7eb98 100644 --- a/antareslauncher/main_option_parser.py +++ b/antareslauncher/main_option_parser.py @@ -38,7 +38,7 @@ def __init__(self, parameters: ParserParameters) -> None: "n_cpu": parameters.default_n_cpu, "antares_version": 0, "job_id_to_kill": None, - "xpansion_mode": None, + "xpansion_mode": "", "version": False, "post_processing": False, "other_options": None, diff --git a/antareslauncher/remote_environnement/remote_environment_with_slurm.py b/antareslauncher/remote_environnement/remote_environment_with_slurm.py index 90b8e4a..9aecbbe 100644 --- a/antareslauncher/remote_environnement/remote_environment_with_slurm.py +++ b/antareslauncher/remote_environnement/remote_environment_with_slurm.py @@ -366,8 +366,10 @@ def _retrieve_slurm_acct_state( out_job_id, out_job_name, out_state = parts if out_job_id == str(job_id) and out_job_name == job_name: # Match the first word only, e.g.: "CANCEL by 123456798" - job_state_str = re.match(r"(\w+)", out_state)[1] - return JobStateCodes(job_state_str) + match = re.match(r"(\w+)", out_state) + if not match: + raise GetJobStateError(job_id, job_name, f"Unable to parse the job state: '{out_state}'") + return JobStateCodes(match[1]) reason = f"The command [{command}] return an non-parsable output:\n{textwrap.indent(output, 'OUTPUT> ')}" raise GetJobStateError(job_id, job_name, reason) @@ -384,7 +386,7 @@ def upload_file(self, src) -> bool: dst = f"{self.remote_base_path}/{Path(src).name}" return self.connection.upload_file(src, dst) - def download_logs(self, study: StudyDTO) -> t.List[Path]: + def download_logs(self, study: StudyDTO) -> t.Sequence[Path]: """ Download the slurm logs of a given study. diff --git a/antareslauncher/remote_environnement/slurm_script_features.py b/antareslauncher/remote_environnement/slurm_script_features.py index adb017b..e741b04 100644 --- a/antareslauncher/remote_environnement/slurm_script_features.py +++ b/antareslauncher/remote_environnement/slurm_script_features.py @@ -10,7 +10,7 @@ class ScriptParametersDTO: input_zipfile_name: str time_limit: int n_cpu: int - antares_version: str + antares_version: int run_mode: Modes post_processing: bool other_options: str diff --git a/antareslauncher/remote_environnement/ssh_connection.py b/antareslauncher/remote_environnement/ssh_connection.py index 7335bda..7b2e3ff 100644 --- a/antareslauncher/remote_environnement/ssh_connection.py +++ b/antareslauncher/remote_environnement/ssh_connection.py @@ -6,19 +6,12 @@ import textwrap import time from pathlib import Path, PurePosixPath -from typing import List, Tuple +import typing as t import paramiko -try: - # noinspection PyUnresolvedReferences - from typing import TypeAlias -except ImportError: - RemotePath = PurePosixPath - LocalPath = Path -else: - RemotePath: TypeAlias = PurePosixPath - LocalPath: TypeAlias = Path +RemotePath = PurePosixPath +LocalPath = Path class SshConnectionError(Exception): @@ -129,7 +122,7 @@ def accumulate(self): class SshConnection: """Class to _connect to remote server""" - def __init__(self, config: dict = None): + def __init__(self, config: t.Mapping[str, t.Any]): """ Initialize the SSH connection. @@ -139,8 +132,8 @@ def __init__(self, config: dict = None): """ super(SshConnection, self).__init__() self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") - self.__client = None - self.__home_dir = None + self._client = None + self._home_dir = "" self.timeout = 10 self.host = "" self.username = "" @@ -150,7 +143,7 @@ def __init__(self, config: dict = None): if config: self.logger.info("Loading ssh connection from config dictionary") - self.__init_from_config(config) + self._init_from_config(config) else: error = InvalidConfigError(config, "missing values: 'hostname', 'username', 'password'...") self.logger.debug(str(error)) @@ -158,7 +151,7 @@ def __init__(self, config: dict = None): self.initialize_home_dir() self.logger.info(f"Connection created with host = {self.host} and username = {self.username}") - def __initialise_public_key(self, key_file_name, key_password): + def _init_public_key(self, key_file_name, key_password): """Initialises self.private_key Args: @@ -178,37 +171,37 @@ def __initialise_public_key(self, key_file_name, key_password): self.private_key = None return False - def __init_from_config(self, config: dict): + def _init_from_config(self, config: t.Mapping[str, t.Any]) -> None: self.host = config.get("hostname", "") self.username = config.get("username", "") self.port = config.get("port", 22) self.password = config.get("password") key_password = config.get("key_password") if key_file := config.get("private_key_file"): - self.__initialise_public_key(key_file_name=key_file, key_password=key_password) + self._init_public_key(key_file_name=key_file, key_password=key_password) elif self.password is None: error = InvalidConfigError(config, "missing 'password'") self.logger.debug(str(error)) raise error - def initialize_home_dir(self): - """Initializes self.__home_dir with the home directory retrieved by started "echo $HOME" connecting to the + def initialize_home_dir(self) -> None: + """Initializes self._home_dir with the home directory retrieved by started "echo $HOME" connecting to the remote server """ output, _ = self.execute_command("echo $HOME") - self.__home_dir = str(output).split()[0] + self._home_dir = str(output).split()[0] @property - def home_dir(self): + def home_dir(self) -> str: """ Returns: The home directory of the remote server """ - return self.__home_dir + return self._home_dir @contextlib.contextmanager - def ssh_client(self) -> paramiko.SSHClient: + def ssh_client(self) -> t.Generator[paramiko.SSHClient, None, None]: client = paramiko.SSHClient() try: try: @@ -351,7 +344,7 @@ def download_files( pattern: str, *patterns: str, remove: bool = True, - ) -> List[LocalPath]: + ) -> t.Sequence[LocalPath]: """ Download files matching the specified patterns from the remote source directory to the local destination directory, @@ -387,10 +380,10 @@ def _download_files( self, src_dir: RemotePath, dst_dir: LocalPath, - patterns: Tuple[str], + patterns: t.Tuple[str, ...], *, remove: bool = True, - ) -> List[LocalPath]: + ) -> t.Sequence[LocalPath]: """ Download files matching the specified patterns from the remote source directory to the local destination directory. diff --git a/antareslauncher/study_dto.py b/antareslauncher/study_dto.py index cf48d9d..7202686 100644 --- a/antareslauncher/study_dto.py +++ b/antareslauncher/study_dto.py @@ -16,25 +16,35 @@ class StudyDTO: path: str name: str = field(init=False) - zipfile_path: str = "" - zip_is_sent: bool = False + + # Job state flags started: bool = False finished: bool = False + done: bool = False with_error: bool = False - local_final_zipfile_path: str = "" + + # Job state message + job_state: str = "Pending" # "Running", "Finished", "Ended with error", "Internal error: ..." + + # Processing stage flags + zip_is_sent: bool = False input_zipfile_removed: bool = False logs_downloaded: bool = False - job_log_dir: str = "" - output_dir: str = "" remote_server_is_clean: bool = False final_zip_extracted: bool = False - done: bool = False - job_id: t.Optional[int] = None - job_state: str = "" + + # Processing stage data + job_id: int = 0 # sbatch job id + zipfile_path: str = "" + local_final_zipfile_path: str = "" + job_log_dir: str = "" + output_dir: str = "" + + # Simulation stage data time_limit: t.Optional[int] = None - n_cpu: t.Optional[int] = None - antares_version: t.Optional[str] = None - xpansion_mode: t.Optional[str] = None + n_cpu: int = 1 + antares_version: int = 0 + xpansion_mode: str = "" # "", "r", "cpp" run_mode: Modes = Modes.antares post_processing: bool = False other_options: str = "" diff --git a/antareslauncher/use_cases/create_list/study_list_composer.py b/antareslauncher/use_cases/create_list/study_list_composer.py index a9bd287..b874ea7 100644 --- a/antareslauncher/use_cases/create_list/study_list_composer.py +++ b/antareslauncher/use_cases/create_list/study_list_composer.py @@ -38,15 +38,14 @@ class StudyListComposerParameters: time_limit: int log_dir: str n_cpu: int - xpansion_mode: t.Optional[str] + xpansion_mode: str # "", "r", "cpp" output_dir: str post_processing: bool - antares_versions_on_remote_server: t.List[str] + antares_versions_on_remote_server: t.Sequence[str] other_options: str antares_version: int = 0 -@dataclass class StudyListComposer: def __init__( self, @@ -77,14 +76,12 @@ def get_list_of_studies(self): """ return self._repo.get_list_of_studies() - def _create_study(self, path, antares_version, xpansion_mode: str): - if self.xpansion_mode == "r": - run_mode = Modes.xpansion_r - elif self.xpansion_mode == "cpp": - run_mode = Modes.xpansion_cpp - else: - run_mode = Modes.antares - + def _create_study(self, path: Path, antares_version: int, xpansion_mode: str) -> StudyDTO: + run_mode = { + "": Modes.antares, + "r": Modes.xpansion_r, + "cpp": Modes.xpansion_cpp, + }.get(self.xpansion_mode, Modes.antares) new_study = StudyDTO( path=str(path), n_cpu=self.n_cpu, @@ -97,7 +94,6 @@ def _create_study(self, path, antares_version, xpansion_mode: str): post_processing=self.post_processing, other_options=self.other_options, ) - return new_study def update_study_database(self): @@ -122,10 +118,6 @@ def update_study_database(self): f"{__name__}.{__class__.__name__}", ) - def _update_database_with_new_study(self, antares_version, directory_path, xpansion_mode: str): - buffer_study = self._create_study(directory_path, antares_version, xpansion_mode) - self._update_database_with_study(buffer_study) - def _update_database_with_directory(self, directory_path: Path): solver_version = get_solver_version(directory_path) antares_version = self.antares_version or solver_version @@ -146,17 +138,15 @@ def _update_database_with_directory(self, directory_path: Path): else: candidates_file_path = directory_path.joinpath("user", "expansion", "candidates.ini") is_xpansion_study = candidates_file_path.is_file() - xpansion_mode = is_xpansion_study and self.xpansion_mode + xpansion_mode = self.xpansion_mode if is_xpansion_study else "" valid_xpansion_candidate = self.xpansion_mode in ["r", "cpp"] and is_xpansion_study valid_antares_candidate = not self.xpansion_mode if valid_antares_candidate or valid_xpansion_candidate: - self._update_database_with_new_study(antares_version, directory_path, xpansion_mode) - - def _update_database_with_study(self, buffer_study): - if not self._repo.is_study_inside_database(buffer_study): - self._add_study_to_database(buffer_study) + buffer_study = self._create_study(directory_path, antares_version, xpansion_mode) + if not self._repo.is_study_inside_database(buffer_study): + self._add_study_to_database(buffer_study) def _add_study_to_database(self, buffer_study): self._repo.save_study(buffer_study) diff --git a/antareslauncher/use_cases/retrieve/state_updater.py b/antareslauncher/use_cases/retrieve/state_updater.py index 63d9b6c..2c07cf0 100644 --- a/antareslauncher/use_cases/retrieve/state_updater.py +++ b/antareslauncher/use_cases/retrieve/state_updater.py @@ -17,7 +17,7 @@ def __init__( self._display = display def _show_job_state_message(self, study: StudyDTO) -> None: - if study.done is True: + if study.done: self._display.show_message( f'"{study.name}": (JOBID={study.job_id}): everything is done', LOG_NAME, diff --git a/antareslauncher/use_cases/retrieve/study_retriever.py b/antareslauncher/use_cases/retrieve/study_retriever.py index 0c6bcee..8d1ea99 100644 --- a/antareslauncher/use_cases/retrieve/study_retriever.py +++ b/antareslauncher/use_cases/retrieve/study_retriever.py @@ -34,7 +34,7 @@ def retrieve(self, study: StudyDTO): self.zip_extractor.extract_final_zip(study) study.done = study.with_error or ( study.logs_downloaded - and study.local_final_zipfile_path + and bool(study.local_final_zipfile_path) and study.remote_server_is_clean and study.final_zip_extracted ) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 3465ece..5a42243 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -41,7 +41,7 @@ def study_list_composer_fixture( time_limit=42, n_cpu=24, log_dir=str(tmp_path.joinpath("LOGS")), - xpansion_mode=None, + xpansion_mode="", output_dir=str(tmp_path.joinpath("FINISHED")), post_processing=False, antares_versions_on_remote_server=[ diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 7c1ebbe..111601b 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -1,7 +1,6 @@ import contextlib import getpass import json -import pathlib from pathlib import Path from unittest.mock import patch @@ -26,7 +25,7 @@ class TestParseConfig: @pytest.mark.parametrize("suffix", [".yaml", ".yml", ".json", ".py"]) @pytest.mark.parametrize("casing", [None, str.upper, str.title]) - def test_parse_config(self, tmp_path, suffix, casing): + def test_parse_config(self, tmp_path: Path, suffix, casing) -> None: data = {"key1": "value1", "key2": 56} # noinspection PyArgumentList new_suffix = suffix if casing is None else casing(suffix) @@ -49,7 +48,7 @@ def test_parse_config(self, tmp_path, suffix, casing): class TestSaveConfig: @pytest.mark.parametrize("suffix", [".yaml", ".yml", ".json", ".py"]) @pytest.mark.parametrize("casing", [None, str.upper, str.title]) - def test_save_config(self, tmp_path, suffix, casing): + def test_save_config(self, tmp_path: Path, suffix, casing) -> None: data = {"key1": "value1", "key2": 56} # noinspection PyArgumentList new_suffix = suffix if casing is None else casing(suffix) @@ -69,7 +68,7 @@ def test_save_config(self, tmp_path, suffix, casing): class TestSSHConfig: - def test_load_config__with_private_key_file(self, tmp_path): + def test_load_config__with_private_key_file(self, tmp_path) -> None: data = { "username": "john.doe", "hostname": "localhost", @@ -88,7 +87,7 @@ def test_load_config__with_private_key_file(self, tmp_path): assert config.key_password == data["key_password"] assert config.password == "" - def test_load_config__with_password(self, tmp_path): + def test_load_config__with_password(self, tmp_path) -> None: data = { "username": "john.doe", "hostname": "localhost", @@ -107,7 +106,7 @@ def test_load_config__with_password(self, tmp_path): assert config.password == data["password"] @pytest.mark.parametrize("required", ["username", "hostname"]) - def test_load_config__missing_parameter(self, tmp_path, required): + def test_load_config__missing_parameter(self, tmp_path: Path, required) -> None: data = { "username": "john.doe", "hostname": "localhost", @@ -120,14 +119,14 @@ def test_load_config__missing_parameter(self, tmp_path, required): with pytest.raises(InvalidConfigValueError): SSHConfig.load_config(config_path) - def test_save_config__with_private_key_file(self, tmp_path): + def test_save_config__with_private_key_file(self, tmp_path) -> None: config_path = tmp_path.joinpath("my_ssh_config.json") config = SSHConfig( config_path=config_path, username="john.doe", hostname="localhost", port=22, - private_key_file=pathlib.Path("path/to/private.key"), + private_key_file=Path("path/to/private.key"), key_password="key_password", ) config.save_config(config_path) @@ -140,7 +139,7 @@ def test_save_config__with_private_key_file(self, tmp_path): assert actual["key_password"] == config.key_password assert "password" not in actual - def test_save_config__with_password(self, tmp_path): + def test_save_config__with_password(self, tmp_path) -> None: config_path = tmp_path.joinpath("my_ssh_config.json") config = SSHConfig( config_path=config_path, @@ -162,7 +161,7 @@ def test_save_config__with_password(self, tmp_path): class TestConfig: @pytest.fixture(name="ssh_config_path") - def fixture_ssh_config_path(self, tmp_path) -> pathlib.Path: + def fixture_ssh_config_path(self, tmp_path) -> Path: data = { "username": "john.doe", "hostname": "localhost", @@ -184,7 +183,7 @@ def fixture_ssh_config(self, tmp_path) -> SSHConfig: password="S3Cr3T", ) - def test_load_config__nominal(self, tmp_path, ssh_config_path): + def test_load_config__nominal(self, tmp_path: Path, ssh_config_path) -> None: log_dir = tmp_path.joinpath("log_dir") json_dir = tmp_path.joinpath("json_dir") studies_in_dir = tmp_path.joinpath("studies_in_dir") @@ -221,7 +220,7 @@ def test_load_config__nominal(self, tmp_path, ssh_config_path): assert config.slurm_script_path == slurm_script_path assert config.remote_solver_versions == data["antares_versions_on_remote_server"] - def test_save_config__nominal(self, tmp_path, ssh_config): + def test_save_config__nominal(self, tmp_path: Path, ssh_config) -> None: config_path = tmp_path.joinpath("configuration.yaml") log_dir = tmp_path.joinpath("log_dir") json_dir = tmp_path.joinpath("json_dir") @@ -278,7 +277,7 @@ def test_save_config__nominal(self, tmp_path, ssh_config): "antares_versions_on_remote_server", ], ) - def test_load_config__missing_parameter(self, tmp_path, ssh_config_path, required): + def test_load_config__missing_parameter(self, tmp_path: Path, ssh_config_path, required) -> None: log_dir = tmp_path.joinpath("log_dir") json_dir = tmp_path.joinpath("json_dir") studies_in_dir = tmp_path.joinpath("studies_in_dir") @@ -324,7 +323,7 @@ class TestGetUserConfigDir: ), ], ) - def test_get_user_config_dir(self, system, expected, monkeypatch): + def test_get_user_config_dir(self, system, expected, monkeypatch) -> None: # ignore error `XDG_CONFIG_HOME` environment variable monkeypatch.delenv("XDG_CONFIG_HOME", raising=False) # ignore error: "cannot instantiate 'WindowsPath'/'PosixPath' on your system" @@ -341,21 +340,21 @@ def test_get_user_config_dir(self, system, expected, monkeypatch): class TestGetConfigPath: - def test_get_config_path__from_env(self, monkeypatch, tmp_path): + def test_get_config_path__from_env(self, monkeypatch, tmp_path) -> None: config_path = tmp_path.joinpath("my_config.yaml") config_path.touch() monkeypatch.setenv("ANTARES_LAUNCHER_CONFIG_PATH", str(config_path)) actual = get_config_path() assert actual == config_path - def test_get_config_path__from_env__not_found(self, monkeypatch, tmp_path): + def test_get_config_path__from_env__not_found(self, monkeypatch, tmp_path) -> None: config_path = tmp_path.joinpath("my_config.yaml") monkeypatch.setenv("ANTARES_LAUNCHER_CONFIG_PATH", str(config_path)) with pytest.raises(ConfigFileNotFoundError): get_config_path() @pytest.mark.parametrize("config_name", [None, CONFIGURATION_YAML, "my_config.yaml"]) - def test_get_config_path__from_user_config_dir(self, monkeypatch, tmp_path, config_name): + def test_get_config_path__from_user_config_dir(self, monkeypatch, tmp_path: Path, config_name) -> None: config_path = tmp_path.joinpath(config_name or CONFIGURATION_YAML) config_path.touch() monkeypatch.delenv("ANTARES_LAUNCHER_CONFIG_PATH", raising=False) @@ -367,10 +366,10 @@ def test_get_config_path__from_user_config_dir(self, monkeypatch, tmp_path, conf @pytest.mark.parametrize("relpath", ["", "data"]) @pytest.mark.parametrize("config_name", [None, CONFIGURATION_YAML, "my_config.yaml"]) - def test_get_config_path__from_curr_dir(self, monkeypatch, tmp_path, relpath, config_name): + def test_get_config_path__from_curr_dir(self, monkeypatch, tmp_path: Path, relpath, config_name) -> None: data_dir = tmp_path.joinpath(relpath) data_dir.mkdir(exist_ok=True) - config_path: pathlib.Path = tmp_path.joinpath(data_dir, config_name or CONFIGURATION_YAML) + config_path: Path = tmp_path.joinpath(data_dir, config_name or CONFIGURATION_YAML) config_path.touch() monkeypatch.delenv("ANTARES_LAUNCHER_CONFIG_PATH", raising=False) monkeypatch.chdir(tmp_path) @@ -379,7 +378,7 @@ def test_get_config_path__from_curr_dir(self, monkeypatch, tmp_path, relpath, co assert actual == config_path.relative_to(tmp_path) @pytest.mark.parametrize("relpath", ["", "data"]) - def test_get_config_path__from_curr_dir__not_found(self, monkeypatch, tmp_path, relpath): + def test_get_config_path__from_curr_dir__not_found(self, monkeypatch, tmp_path: Path, relpath) -> None: data_dir = tmp_path.joinpath(relpath) data_dir.mkdir(exist_ok=True) monkeypatch.delenv("ANTARES_LAUNCHER_CONFIG_PATH", raising=False) diff --git a/tests/unit/test_remote_environment_with_slurm.py b/tests/unit/test_remote_environment_with_slurm.py index 1da3d07..b472eed 100644 --- a/tests/unit/test_remote_environment_with_slurm.py +++ b/tests/unit/test_remote_environment_with_slurm.py @@ -50,7 +50,7 @@ def study(self) -> StudyDTO: path="path/to/study/91f1f911-4f4a-426f-b127-d0c2a2465b5f", n_cpu=42, zipfile_path="path/to/study/91f1f911-4f4a-426f-b127-d0c2a2465b5f-foo.zip", - antares_version="700", + antares_version=700, local_final_zipfile_path="local_final_zipfile_path", run_mode=Modes.antares, ) diff --git a/tests/unit/test_study_list_composer.py b/tests/unit/test_study_list_composer.py index 902e402..427bb28 100644 --- a/tests/unit/test_study_list_composer.py +++ b/tests/unit/test_study_list_composer.py @@ -117,4 +117,4 @@ def test_update_study_database__antares_version( expected_versions = dict.fromkeys(study_names, antares_version) else: expected_versions = {} - assert {n: expected_versions[n] for n in actual_versions} == actual_versions + assert actual_versions == {n: expected_versions[n] for n in actual_versions} From 9797799df6bf4fd626ea1bc997d11503989d5b94 Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Tue, 26 Sep 2023 14:05:10 +0200 Subject: [PATCH 17/23] refactoring(file-manager): drop the `FileManager` class --- antareslauncher/file_manager/__init__.py | 0 antareslauncher/file_manager/file_manager.py | 25 ------------------- antareslauncher/main.py | 12 ++------- .../tree_structure_initializer.py | 11 ++++---- 4 files changed, 7 insertions(+), 41 deletions(-) delete mode 100644 antareslauncher/file_manager/__init__.py delete mode 100644 antareslauncher/file_manager/file_manager.py diff --git a/antareslauncher/file_manager/__init__.py b/antareslauncher/file_manager/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/antareslauncher/file_manager/file_manager.py b/antareslauncher/file_manager/file_manager.py deleted file mode 100644 index 9aab52b..0000000 --- a/antareslauncher/file_manager/file_manager.py +++ /dev/null @@ -1,25 +0,0 @@ -import json -import logging -import os - -from antareslauncher.display.display_terminal import DisplayTerminal - - -class FileManager: - def __init__(self, display_terminal: DisplayTerminal): - self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") - self.display = display_terminal - - def make_dir(self, directory_name): - self.logger.info(f"Creating directory {directory_name}") - os.makedirs(directory_name, exist_ok=True) - - def convert_json_file_to_dict(self, file_path): - self.logger.info(f"Converting json file {file_path} to dict") - try: - with open(file_path, "r") as readFile: - config = json.load(readFile) - except OSError: - self.logger.error(f"Unable to convert {file_path} to json (file not found or invalid type)") - config = None - return config diff --git a/antareslauncher/main.py b/antareslauncher/main.py index 696b1f3..bd57481 100644 --- a/antareslauncher/main.py +++ b/antareslauncher/main.py @@ -1,14 +1,13 @@ import argparse import dataclasses import json -from pathlib import Path import typing as t +from pathlib import Path from antareslauncher import __version__ from antareslauncher.antares_launcher import AntaresLauncher from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.file_manager.file_manager import FileManager from antareslauncher.logger_initializer import LoggerInitializer from antareslauncher.remote_environnement import ssh_connection from antareslauncher.remote_environnement.remote_environment_with_slurm import RemoteEnvironmentWithSlurm @@ -86,13 +85,11 @@ def run_with(arguments: argparse.Namespace, parameters: MainParameters, show_ban print(ANTARES_LAUNCHER_BANNER) display = DisplayTerminal() - file_manager = FileManager(display) db_json_file_path = parameters.json_dir / parameters.default_json_db_name tree_structure_initializer = TreeStructureInitializer( display, - file_manager, arguments.studies_in, arguments.log_dir, arguments.output_dir, @@ -103,11 +100,7 @@ def run_with(arguments: argparse.Namespace, parameters: MainParameters, show_ban logger_initializer.init_logger() # connection - ssh_dict = get_ssh_config_dict( - file_manager, - arguments.json_ssh_config, # Path to the configuration file for the ssh connection. - parameters.default_ssh_dict, - ) + ssh_dict = get_ssh_config_dict(arguments.json_ssh_config, parameters.default_ssh_dict) connection = ssh_connection.SshConnection(config=ssh_dict) verify_connection(connection, display) @@ -181,7 +174,6 @@ def verify_connection(connection, display): def get_ssh_config_dict( - file_manager: FileManager, json_ssh_config: str, ssh_dict: t.Mapping[str, t.Any], ) -> t.Mapping[str, t.Any]: diff --git a/antareslauncher/use_cases/generate_tree_structure/tree_structure_initializer.py b/antareslauncher/use_cases/generate_tree_structure/tree_structure_initializer.py index df933a1..443c528 100644 --- a/antareslauncher/use_cases/generate_tree_structure/tree_structure_initializer.py +++ b/antareslauncher/use_cases/generate_tree_structure/tree_structure_initializer.py @@ -1,20 +1,19 @@ from dataclasses import dataclass +from pathlib import Path from antareslauncher.display.display_terminal import DisplayTerminal -from antareslauncher.file_manager.file_manager import FileManager @dataclass class TreeStructureInitializer: display: DisplayTerminal - file_manager: FileManager studies_in: str log_dir: str - finished: str + output_dir: str def init_tree_structure(self): """Initialize the structure""" - self.file_manager.make_dir(self.studies_in) - self.file_manager.make_dir(self.log_dir) - self.file_manager.make_dir(self.finished) + Path(self.studies_in).mkdir(parents=True, exist_ok=True) + Path(self.log_dir).mkdir(parents=True, exist_ok=True) + Path(self.output_dir).mkdir(parents=True, exist_ok=True) self.display.show_message("Tree structure initialized", __name__ + "." + __class__.__name__) From 8a119afb06d64f0ccd1e112ef82367f8fdee7ce0 Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Tue, 26 Sep 2023 14:10:59 +0200 Subject: [PATCH 18/23] refactoring(tree-structure): drop the `TreeStructureInitializer` class --- antareslauncher/main.py | 12 ++++-------- .../generate_tree_structure/__init__.py | 0 .../tree_structure_initializer.py | 19 ------------------- 3 files changed, 4 insertions(+), 27 deletions(-) delete mode 100644 antareslauncher/use_cases/generate_tree_structure/__init__.py delete mode 100644 antareslauncher/use_cases/generate_tree_structure/tree_structure_initializer.py diff --git a/antareslauncher/main.py b/antareslauncher/main.py index bd57481..02b3226 100644 --- a/antareslauncher/main.py +++ b/antareslauncher/main.py @@ -15,7 +15,6 @@ from antareslauncher.use_cases.check_remote_queue.check_queue_controller import CheckQueueController from antareslauncher.use_cases.check_remote_queue.slurm_queue_show import SlurmQueueShow from antareslauncher.use_cases.create_list.study_list_composer import StudyListComposer, StudyListComposerParameters -from antareslauncher.use_cases.generate_tree_structure.tree_structure_initializer import TreeStructureInitializer from antareslauncher.use_cases.kill_job.job_kill_controller import JobKillController from antareslauncher.use_cases.launch.launch_controller import LaunchController from antareslauncher.use_cases.retrieve.retrieve_controller import RetrieveController @@ -88,14 +87,11 @@ def run_with(arguments: argparse.Namespace, parameters: MainParameters, show_ban db_json_file_path = parameters.json_dir / parameters.default_json_db_name - tree_structure_initializer = TreeStructureInitializer( - display, - arguments.studies_in, - arguments.log_dir, - arguments.output_dir, - ) + Path(arguments.studies_in).mkdir(parents=True, exist_ok=True) + Path(arguments.log_dir).mkdir(parents=True, exist_ok=True) + Path(arguments.output_dir).mkdir(parents=True, exist_ok=True) + display.show_message("Tree structure initialized", __name__) - tree_structure_initializer.init_tree_structure() logger_initializer = LoggerInitializer(str(Path(arguments.log_dir) / "antares_launcher.log")) logger_initializer.init_logger() diff --git a/antareslauncher/use_cases/generate_tree_structure/__init__.py b/antareslauncher/use_cases/generate_tree_structure/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/antareslauncher/use_cases/generate_tree_structure/tree_structure_initializer.py b/antareslauncher/use_cases/generate_tree_structure/tree_structure_initializer.py deleted file mode 100644 index 443c528..0000000 --- a/antareslauncher/use_cases/generate_tree_structure/tree_structure_initializer.py +++ /dev/null @@ -1,19 +0,0 @@ -from dataclasses import dataclass -from pathlib import Path - -from antareslauncher.display.display_terminal import DisplayTerminal - - -@dataclass -class TreeStructureInitializer: - display: DisplayTerminal - studies_in: str - log_dir: str - output_dir: str - - def init_tree_structure(self): - """Initialize the structure""" - Path(self.studies_in).mkdir(parents=True, exist_ok=True) - Path(self.log_dir).mkdir(parents=True, exist_ok=True) - Path(self.output_dir).mkdir(parents=True, exist_ok=True) - self.display.show_message("Tree structure initialized", __name__ + "." + __class__.__name__) From 272965ed618f94ecf0de718bf7e8e0788c4bbb3a Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Tue, 26 Sep 2023 14:27:34 +0200 Subject: [PATCH 19/23] refactoring(data-provider): drop the `DataProvider` class --- antareslauncher/data_repo/data_provider.py | 11 ----------- tests/unit/test_data_provider.py | 17 ----------------- 2 files changed, 28 deletions(-) delete mode 100644 antareslauncher/data_repo/data_provider.py delete mode 100644 tests/unit/test_data_provider.py diff --git a/antareslauncher/data_repo/data_provider.py b/antareslauncher/data_repo/data_provider.py deleted file mode 100644 index f4cd647..0000000 --- a/antareslauncher/data_repo/data_provider.py +++ /dev/null @@ -1,11 +0,0 @@ -from dataclasses import dataclass - -from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb - - -@dataclass -class DataProvider: - data_repo: DataRepoTinydb - - def get_list_of_studies(self): - return self.data_repo.get_list_of_studies() diff --git a/tests/unit/test_data_provider.py b/tests/unit/test_data_provider.py deleted file mode 100644 index d2e7067..0000000 --- a/tests/unit/test_data_provider.py +++ /dev/null @@ -1,17 +0,0 @@ -from unittest.mock import Mock - -from antareslauncher.data_repo.data_provider import DataProvider -from antareslauncher.data_repo.data_repo_tinydb import DataRepoTinydb -from antareslauncher.study_dto import StudyDTO - - -def test_data_provider_return_list_of_studies_obtained_from_repo(): - # given - data_repo = Mock(spec_set=DataRepoTinydb) - study = StudyDTO(path="empty_path") - data_repo.get_list_of_studies = Mock(return_value=[study]) - data_provider = DataProvider(data_repo) - # when - list_of_studies = data_provider.get_list_of_studies() - # then - assert list_of_studies == [study] From e98b7a8627b09a883e48a9b4b883f6b1560da0e9 Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Wed, 20 Sep 2023 12:11:55 +0200 Subject: [PATCH 20/23] chore: replace `COMPETING` with `COMPLETING` (typo) --- .../remote_environnement/remote_environment_with_slurm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/antareslauncher/remote_environnement/remote_environment_with_slurm.py b/antareslauncher/remote_environnement/remote_environment_with_slurm.py index 9aecbbe..71d75ba 100644 --- a/antareslauncher/remote_environnement/remote_environment_with_slurm.py +++ b/antareslauncher/remote_environnement/remote_environment_with_slurm.py @@ -75,7 +75,7 @@ class JobStateCodes(enum.Enum): COMPLETED = "COMPLETED" # Indicates that the only job on the node or that all jobs on the node are in the process of completing. - COMPETING = "COMPLETING" + COMPLETING = "COMPLETING" # Job terminated on deadline. DEADLINE = "DEADLINE" @@ -271,7 +271,7 @@ def get_job_state_flags( JobStateCodes.BOOT_FAIL: (False, False, False), JobStateCodes.CANCELLED: (True, True, True), JobStateCodes.COMPLETED: (True, True, False), - JobStateCodes.COMPETING: (True, False, False), + JobStateCodes.COMPLETING: (True, False, False), JobStateCodes.DEADLINE: (True, True, True), # similar to timeout JobStateCodes.FAILED: (True, True, True), JobStateCodes.NODE_FAIL: (True, True, True), From 5b59c052070025fafadffb3c78b218489f144a34 Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Mon, 18 Sep 2023 19:33:05 +0200 Subject: [PATCH 21/23] build: prepare next bugfix release v1.3.1 --- CHANGES.md | 31 +++++++++++++++++++++++++++++++ antareslauncher/__init__.py | 4 ++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5c74c68..82ef4de 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,34 @@ # Changelog + + +## [1.3.1] - (unreleased) + +### Changed + +- feat(cli): add the `--solver-version` option to the command line [`#63`](https://github.com/AntaresSimulatorTeam/antares-launcher/pull/63) +- feat(parameters): handle the `--partition` and `--qos` parameters for the `sbatch` command [`#58`](https://github.com/AntaresSimulatorTeam/antares-launcher/pull/58) + +### Fixes + +- fix(job-state): consider the `COMPLETING` value as a possible job state [`#61`](https://github.com/AntaresSimulatorTeam/antares-launcher/pull/61) +- fix(results-retrieval): handle exceptions in log and ZIP result retrival [`#60`](https://github.com/AntaresSimulatorTeam/antares-launcher/pull/60) +- fix(console): use the ISO8601 date format to display messages on the console [`0dbf971`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/0dbf971b1ccc924f4b11cf44b0e0cf16562622c9) + +### Refactorings + +- refactor: remove IDisplay abstract class [`#64`](https://github.com/AntaresSimulatorTeam/antares-launcher/pull/64) + +### Chore + +- replace `COMPETING` with `COMPLETING` (typo) [`6924a2a`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/6924a2a4ff02c815b44ccb5bc02d0b805bd979cc) + ## [1.3.0] - 2023-06-16 ### Changed @@ -95,6 +124,8 @@ - Remove unnecessary Optional - Enable ssh_config_file to be `None` +[1.3.1]: https://github.com/AntaresSimulatorTeam/antares-launcher/releases/tag/v1.3.1 + [1.3.0]: https://github.com/AntaresSimulatorTeam/antares-launcher/releases/tag/v1.3.0 [1.2.4]: https://github.com/AntaresSimulatorTeam/antares-launcher/releases/tag/v1.2.4 diff --git a/antareslauncher/__init__.py b/antareslauncher/__init__.py index b58b8ab..227a2db 100644 --- a/antareslauncher/__init__.py +++ b/antareslauncher/__init__.py @@ -9,9 +9,9 @@ # Standard project metadata -__version__ = "1.3.0" +__version__ = "1.3.1" __author__ = "RTE, Antares Web Team" -__date__ = "2023-06-16" +__date__ = "(unreleased)" # noinspection SpellCheckingInspection __credits__ = "(c) Réseau de Transport de l’Électricité (RTE)" From 0027251a72a21d8c77618c07fb65b78f51c77209 Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Wed, 20 Sep 2023 16:48:39 +0200 Subject: [PATCH 22/23] build: update change log for v1.3.1 --- CHANGES.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 82ef4de..032dd8c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,22 +12,42 @@ npx auto-changelog -l false --hide-empty-releases -v v1.3.1 -o CHANGES.out.md ### Changed +- feat(database): simplify launcher database implementation [`#66`](https://github.com/AntaresSimulatorTeam/antares-launcher/pull/66) - feat(cli): add the `--solver-version` option to the command line [`#63`](https://github.com/AntaresSimulatorTeam/antares-launcher/pull/63) - feat(parameters): handle the `--partition` and `--qos` parameters for the `sbatch` command [`#58`](https://github.com/AntaresSimulatorTeam/antares-launcher/pull/58) +- feat(retrival): correct the retrival of remote files and improve exception handling to avoid infinite loops [`88efc98`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/88efc98af6a8fd494f07cc9a366a52109eb3ac2d) +- feat(zip-extractor): the uncompress directory is calculated according to the content: study directory or simulation output [`1ffc86e`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/1ffc86e0439814e4549f59c193731c71080c0d59) ### Fixes +- fix(cli): preserve backward compatibility in CLI options [`#65`](https://github.com/AntaresSimulatorTeam/antares-launcher/pull/65) - fix(job-state): consider the `COMPLETING` value as a possible job state [`#61`](https://github.com/AntaresSimulatorTeam/antares-launcher/pull/61) - fix(results-retrieval): handle exceptions in log and ZIP result retrival [`#60`](https://github.com/AntaresSimulatorTeam/antares-launcher/pull/60) - fix(console): use the ISO8601 date format to display messages on the console [`0dbf971`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/0dbf971b1ccc924f4b11cf44b0e0cf16562622c9) ### Refactorings -- refactor: remove IDisplay abstract class [`#64`](https://github.com/AntaresSimulatorTeam/antares-launcher/pull/64) +- refactor: remove `IDisplay` abstract class [`#64`](https://github.com/AntaresSimulatorTeam/antares-launcher/pull/64) +- refactor(launch-controller): simplification of the `LaunchController` class [`4c07551`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/4c07551ae8acf15d784553e7877b9017626b306b) +- refactor(file-manager): remove unused or trivial methods from `FileManager` [`fbb60e0`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/fbb60e0efca6989e7ea79324ed746b55da3cfb3d) +- refactoring(file-manager): drop the `FileManager` class [`9797799`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/9797799df6bf4fd626ea1bc997d11503989d5b94) +- refactoring(tree-structure): drop the `TreeStructureInitializer` class [`8a119af`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/8a119afb06d64f0ccd1e112ef82367f8fdee7ce0) +- refactoring(data-provider): drop the `DataProvider` class [`272965e`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/272965ed618f94ecf0de718bf7e8e0788c4bbb3a) + +### Code Style + +- style: reformat source code using iSort and Black [`e243fba`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/e243fbab177c46ffc867440b3701d7672566066c) ### Chore -- replace `COMPETING` with `COMPLETING` (typo) [`6924a2a`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/6924a2a4ff02c815b44ccb5bc02d0b805bd979cc) +- chore(typing): improve the typing of study parameters [`f11641d`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/f11641d4d233d61f91b9cbebf6263780ff14eb88) +- chore(typing): improve typing in source code [`4ff6abf`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/4ff6abf512b03944d0132d868484ef2d677c8b77) +- chore: replace `COMPETING` with `COMPLETING` (typo) [`e98b7a8`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/e98b7a8627b09a883e48a9b4b883f6b1560da0e9) + +### Tests + +- test: correct the test fixtures for study retrival [`6f78bd6`](https://github.com/AntaresSimulatorTeam/antares-launcher/commit/6f78bd62a5f7c6b61a6fcb4a9a42c7710e986301) + ## [1.3.0] - 2023-06-16 From 26082e2e4097f5d056fde3c1572fd8702544848f Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Tue, 26 Sep 2023 14:41:01 +0200 Subject: [PATCH 23/23] build: new bugfix release v1.3.1 (2023-09-26) --- CHANGES.md | 2 +- antareslauncher/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 032dd8c..6adae4a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,7 @@ npx auto-changelog -l false --hide-empty-releases -v v1.3.1 -o CHANGES.out.md ``` --> -## [1.3.1] - (unreleased) +## [1.3.1] - 2023-09-26 ### Changed diff --git a/antareslauncher/__init__.py b/antareslauncher/__init__.py index 227a2db..250662a 100644 --- a/antareslauncher/__init__.py +++ b/antareslauncher/__init__.py @@ -11,7 +11,7 @@ __version__ = "1.3.1" __author__ = "RTE, Antares Web Team" -__date__ = "(unreleased)" +__date__ = "2023-09-26" # noinspection SpellCheckingInspection __credits__ = "(c) Réseau de Transport de l’Électricité (RTE)"