diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index afff97ee35..bc5713a1f5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -49,6 +49,7 @@ jobs: python -m pip install --upgrade pip pip install pydantic --no-binary pydantic pip install -r requirements-dev.txt + pip install -r installer/requirements.txt - name: 🐍 Install Windows dependencies if: matrix.os == 'windows-latest' diff --git a/antarest/core/exceptions.py b/antarest/core/exceptions.py index 6fce8f0213..8358cc385d 100644 --- a/antarest/core/exceptions.py +++ b/antarest/core/exceptions.py @@ -361,11 +361,8 @@ def __str__(self) -> str: class UnsupportedStudyVersion(HTTPException): - def __init__(self, version: str) -> None: - super().__init__( - HTTPStatus.BAD_REQUEST, - f"Study version {version} is not supported", - ) + def __init__(self, message: str) -> None: + super().__init__(HTTPStatus.BAD_REQUEST, message) class UnsupportedOperationOnArchivedStudy(HTTPException): diff --git a/antarest/study/business/adequacy_patch_management.py b/antarest/study/business/adequacy_patch_management.py index f9ce0f01f3..7d837df7b6 100644 --- a/antarest/study/business/adequacy_patch_management.py +++ b/antarest/study/business/adequacy_patch_management.py @@ -1,6 +1,6 @@ from typing import Any, Dict, List, Optional -from pydantic.types import StrictBool, confloat +from pydantic.types import StrictBool, confloat, conint from antarest.study.business.enum_ignore_case import EnumIgnoreCase from antarest.study.business.utils import GENERAL_DATA_PATH, FieldInfo, FormFieldsBaseModel, execute_or_add_commands @@ -28,7 +28,7 @@ class AdequacyPatchFormFields(FormFieldsBaseModel): check_csr_cost_function: Optional[StrictBool] threshold_initiate_curtailment_sharing_rule: Optional[ThresholdType] # type: ignore threshold_display_local_matching_rule_violations: Optional[ThresholdType] # type: ignore - threshold_csr_variable_bounds_relaxation: Optional[ThresholdType] # type: ignore + threshold_csr_variable_bounds_relaxation: Optional[conint(ge=0, strict=True)] # type: ignore ADEQUACY_PATCH_PATH = f"{GENERAL_DATA_PATH}/adequacy patch" diff --git a/antarest/study/service.py b/antarest/study/service.py index 5a2bfcda2a..d270e8daa3 100644 --- a/antarest/study/service.py +++ b/antarest/study/service.py @@ -32,7 +32,6 @@ StudyVariantUpgradeError, TaskAlreadyRunning, UnsupportedOperationOnArchivedStudy, - UnsupportedStudyVersion, ) from antarest.core.filetransfer.model import FileDownloadTaskDTO from antarest.core.filetransfer.service import FileTransferManager @@ -116,12 +115,7 @@ from antarest.study.storage.rawstudy.raw_study_service import RawStudyService from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.study_download_utils import StudyDownloader, get_output_variables_information -from antarest.study.storage.study_upgrader import ( - find_next_version, - get_current_version, - should_study_be_denormalized, - upgrade_study, -) +from antarest.study.storage.study_upgrader import StudyUpgrader, check_versions_coherence, find_next_version from antarest.study.storage.utils import assert_permission, get_start_date, is_managed, remove_from_cache from antarest.study.storage.variantstudy.business.utils import transform_command_to_dto from antarest.study.storage.variantstudy.model.command.icommand import ICommand @@ -192,13 +186,13 @@ def _upgrade_study(self) -> None: self.storage_service.variant_study_service.clear_snapshot(study_to_upgrade) else: study_path = Path(study_to_upgrade.path) - current_version = get_current_version(study_path) - if is_managed(study_to_upgrade) and should_study_be_denormalized(current_version, target_version): + study_upgrader = StudyUpgrader(study_path, target_version) + if is_managed(study_to_upgrade) and study_upgrader.should_denormalize_study(): # We have to denormalize the study because the upgrade impacts study matrices file_study = self.storage_service.get_storage(study_to_upgrade).get_raw(study_to_upgrade) file_study.tree.denormalize() is_study_denormalized = True - upgrade_study(study_path, target_version) + study_upgrader.upgrade() remove_from_cache(self.cache_service, study_to_upgrade.id) study_to_upgrade.version = target_version self.repository.save(study_to_upgrade) @@ -2426,13 +2420,15 @@ def upgrade_study( # First check if the study is a variant study, if so throw an error if isinstance(study, VariantStudy): raise StudyVariantUpgradeError(True) - # If the study is a parent raw study, throw an error + # If the study is a parent raw study and has variants, throw an error elif self.repository.has_children(study_id): raise StudyVariantUpgradeError(False) - target_version = target_version or find_next_version(study.version) + # Checks versions coherence before launching the task if not target_version: - raise UnsupportedStudyVersion(study.version) + target_version = find_next_version(study.version) + else: + check_versions_coherence(study.version, target_version) task_name = f"Upgrade study {study.name} ({study.id}) to version {target_version}" study_tasks = self.task_service.list_tasks( diff --git a/antarest/study/storage/study_upgrader/__init__.py b/antarest/study/storage/study_upgrader/__init__.py index 6bfbf712be..c2a8427c94 100644 --- a/antarest/study/storage/study_upgrader/__init__.py +++ b/antarest/study/storage/study_upgrader/__init__.py @@ -1,59 +1,14 @@ -import logging -import re -import shutil -import tempfile -import time -import typing as t from http import HTTPStatus from http.client import HTTPException from pathlib import Path -from antarest.core.exceptions import StudyValidationError +from antares.study.version.exceptions import ApplicationError +from antares.study.version.model.study_version import StudyVersion +from antares.study.version.upgrade_app import UpgradeApp -from .upgrader_710 import upgrade_710 -from .upgrader_720 import upgrade_720 -from .upgrader_800 import upgrade_800 -from .upgrader_810 import upgrade_810 -from .upgrader_820 import upgrade_820 -from .upgrader_830 import upgrade_830 -from .upgrader_840 import upgrade_840 -from .upgrader_850 import upgrade_850 -from .upgrader_860 import upgrade_860 -from .upgrader_870 import upgrade_870 -from .upgrader_880 import upgrade_880 +from antarest.core.exceptions import UnsupportedStudyVersion -STUDY_ANTARES = "study.antares" -""" -Main file of an Antares study containing the caption, the version, the creation date, etc. -""" - -logger = logging.getLogger(__name__) - - -class UpgradeMethod(t.NamedTuple): - """Raw study upgrade method (old version, new version, upgrade function).""" - - old: str - new: str - method: t.Callable[[Path], None] - files: t.List[Path] - - -_GENERAL_DATA_PATH = Path("settings/generaldata.ini") - -UPGRADE_METHODS = [ - UpgradeMethod("700", "710", upgrade_710, [_GENERAL_DATA_PATH]), - UpgradeMethod("710", "720", upgrade_720, []), - UpgradeMethod("720", "800", upgrade_800, [_GENERAL_DATA_PATH]), - UpgradeMethod("800", "810", upgrade_810, [_GENERAL_DATA_PATH, Path("input")]), - UpgradeMethod("810", "820", upgrade_820, [Path("input/links")]), - UpgradeMethod("820", "830", upgrade_830, [_GENERAL_DATA_PATH, Path("input/areas")]), - UpgradeMethod("830", "840", upgrade_840, [_GENERAL_DATA_PATH]), - UpgradeMethod("840", "850", upgrade_850, [_GENERAL_DATA_PATH]), - UpgradeMethod("850", "860", upgrade_860, [Path("input"), _GENERAL_DATA_PATH]), - UpgradeMethod("860", "870", upgrade_870, [Path("input/thermal"), Path("input/bindingconstraints")]), - UpgradeMethod("870", "880", upgrade_880, [Path("input/st-storage/clusters")]), -] +AVAILABLE_VERSIONS = ["700", "710", "720", "800", "810", "820", "830", "840", "850", "860", "870", "880"] class InvalidUpgrade(HTTPException): @@ -61,229 +16,55 @@ def __init__(self, message: str) -> None: super().__init__(HTTPStatus.UNPROCESSABLE_ENTITY, message) -def find_next_version(from_version: str) -> str: - """ - Find the next study version from the given version. - - Args: - from_version: The current version as a string. - - Returns: - The next version as a string. - If no next version was found, returns an empty string. - """ - return next( - (meth.new for meth in UPGRADE_METHODS if from_version == meth.old), - "", - ) - - -def upgrade_study(study_path: Path, target_version: str) -> None: - with tempfile.TemporaryDirectory(suffix=".upgrade.tmp", prefix="~", dir=study_path.parent) as path: - tmp_dir = Path(path) +class StudyUpgrader: + def __init__(self, study_path: Path, target_version: str): try: - src_version = get_current_version(study_path) - files_to_upgrade = can_upgrade_version(src_version, target_version) - files_to_retrieve = _copies_only_necessary_files(files_to_upgrade, study_path, tmp_dir) - _do_upgrade(tmp_dir, src_version, target_version) - except (StudyValidationError, InvalidUpgrade) as e: - logger.warning(str(e)) - raise - except Exception as e: - logger.error(f"Unhandled exception : {e}", exc_info=True) - raise + version = StudyVersion.parse(target_version) + except ValueError as e: + raise InvalidUpgrade(str(e)) from e else: - _replace_safely_original_files(files_to_retrieve, study_path, tmp_dir) - + self.app = UpgradeApp(study_path, version=version) -def get_current_version(study_path: Path) -> str: - """ - Get the current version of a study. + def upgrade(self) -> None: + try: + self.app() + except ApplicationError as e: + raise InvalidUpgrade(str(e)) from e - Args: - study_path: Path to the study. + def should_denormalize_study(self) -> bool: + return self.app.should_denormalize - Returns: - The current version of the study. - Raises: - StudyValidationError: If the version number is not found in the - `study.antares` file or does not match the expected format. - """ - - antares_path = study_path / STUDY_ANTARES - pattern = r"version\s*=\s*([\w.-]+)\s*" - with antares_path.open(encoding="utf-8") as lines: - for line in lines: - if match := re.fullmatch(pattern, line): - return match[1].rstrip() - raise StudyValidationError( - f"File parsing error: the version number is not found in '{antares_path}'" - f" or does not match the expected '{pattern}' format." - ) +def _get_version_index(version: str) -> int: + try: + return AVAILABLE_VERSIONS.index(version) + except ValueError: + raise UnsupportedStudyVersion(f"Version '{version}' isn't among supported versions: {AVAILABLE_VERSIONS}") -def can_upgrade_version(from_version: str, to_version: str) -> t.List[Path]: +def find_next_version(from_version: str) -> str: """ - Checks if upgrading from one version to another is possible. + Find the next study version from the given version. Args: - from_version: The current version of the study. - to_version: The target version of the study. + from_version: The current version as a string. Returns: - If the upgrade is possible, the list of concerned folders and files + The next version as a string. Raises: - InvalidUpgrade: If the upgrade is not possible. - """ - list_versions = [] - if from_version == to_version: - raise InvalidUpgrade(f"Your study is already in version '{to_version}'") - - sources = [u.old for u in UPGRADE_METHODS] - if from_version not in sources: - raise InvalidUpgrade(f"Version '{from_version}' unknown: possible versions are {', '.join(sources)}") - - targets = [u.new for u in UPGRADE_METHODS] - if to_version not in targets: - raise InvalidUpgrade(f"Version '{to_version}' unknown: possible versions are {', '.join(targets)}") - - files = [u.files for u in UPGRADE_METHODS] - curr_version = from_version - for src, dst, file in zip(sources, targets, files): - if curr_version == src: - for path in file: - if path not in list_versions: - list_versions.append(path) - curr_version = dst - if curr_version == to_version: - return list_versions - - # This code must be unreachable! - raise InvalidUpgrade( - f"Impossible to upgrade from version '{from_version}'" - f" to version '{to_version}':" - f" missing value in `UPGRADE_METHODS`." - ) - - -def _update_study_antares_file(target_version: str, study_path: Path) -> None: - antares_path = study_path / STUDY_ANTARES - content = antares_path.read_text(encoding="utf-8") - content = re.sub( - r"^version\s*=.*$", - f"version = {target_version}", - content, - flags=re.MULTILINE, - ) - content = re.sub( - r"^lastsave\s*=.*$", - f"lastsave = {int(time.time())}", - content, - flags=re.MULTILINE, - ) - antares_path.write_text(content, encoding="utf-8") - - -def _copies_only_necessary_files(files_to_upgrade: t.List[Path], study_path: Path, tmp_path: Path) -> t.List[Path]: - """ - Copies files concerned by the version upgrader into a temporary directory. - Args: - study_path: Path to the study. - tmp_path: Path to the temporary directory where the file modification will be performed. - files_to_upgrade: List[Path]: List of the files and folders concerned by the upgrade. - Returns: - The list of files and folders that were really copied. It's the same as files_to_upgrade but - without any children that has parents already in the list. + UnsupportedStudyVersion if the current version is not supported or if the study is already in last version. """ - files_to_copy = _filters_out_children_files(files_to_upgrade) - files_to_copy.append(Path(STUDY_ANTARES)) - files_to_retrieve = [] - for path in files_to_copy: - entire_path = study_path / path - if not entire_path.exists(): - # This can happen when upgrading a study to v8.8. - continue - if entire_path.is_dir(): - if not (tmp_path / path).exists(): - shutil.copytree(entire_path, tmp_path / path, dirs_exist_ok=True) - files_to_retrieve.append(path) - elif len(path.parts) == 1: - shutil.copy(entire_path, tmp_path / path) - files_to_retrieve.append(path) - else: - parent_path = path.parent - (tmp_path / parent_path).mkdir(parents=True, exist_ok=True) - shutil.copy(entire_path, tmp_path / parent_path) - files_to_retrieve.append(path) - return files_to_retrieve - - -def _filters_out_children_files(files_to_upgrade: t.List[Path]) -> t.List[Path]: - """ - Filters out children paths of "input" if "input" is already in the list. - Args: - files_to_upgrade: List[Path]: List of the files and folders concerned by the upgrade. - Returns: - The list of files filtered - """ - is_input_in_files_to_upgrade = Path("input") in files_to_upgrade - if is_input_in_files_to_upgrade: - files_to_keep = [Path("input")] - files_to_keep.extend(path for path in files_to_upgrade if "input" not in path.parts) - return files_to_keep - return files_to_upgrade - - -def _replace_safely_original_files(files_to_replace: t.List[Path], study_path: Path, tmp_path: Path) -> None: - """ - Replace files/folders of the study that should be upgraded by their copy already upgraded in the tmp directory. - It uses Path.rename() and an intermediary tmp directory to swap the folders safely. - In the end, all tmp directories are removed. - Args: - study_path: Path to the study. - tmp_path: Path to the temporary directory where the file modification will be performed. - files_to_replace: List[Path]: List of files and folders that were really copied - (cf. _copies_only_necessary_files's doc just above) - """ - for k, path in enumerate(files_to_replace): - backup_dir = Path( - tempfile.mkdtemp( - suffix=f".backup_{k}.tmp", - prefix="~", - dir=study_path.parent, - ) - ) - backup_dir.rmdir() - original_path = study_path / path - original_path.rename(backup_dir) - (tmp_path / path).rename(original_path) - if backup_dir.is_dir(): - shutil.rmtree(backup_dir) - else: - backup_dir.unlink() - - -def _do_upgrade(study_path: Path, src_version: str, target_version: str) -> None: - _update_study_antares_file(target_version, study_path) - curr_version = src_version - for old, new, method, _ in UPGRADE_METHODS: - if curr_version == old and curr_version != target_version: - method(study_path) - curr_version = new - - -def should_study_be_denormalized(src_version: str, target_version: str) -> bool: - try: - can_upgrade_version(src_version, target_version) - except InvalidUpgrade: - return False - curr_version = src_version - list_of_upgrades = [] - for old, new, method, _ in UPGRADE_METHODS: - if curr_version == old and curr_version != target_version: - list_of_upgrades.append(new) - curr_version = new - # These upgrades alter matrices so the study needs to be denormalized - return "820" in list_of_upgrades or "870" in list_of_upgrades + start_pos = _get_version_index(from_version) + if start_pos == len(AVAILABLE_VERSIONS) - 1: + raise UnsupportedStudyVersion(f"Your study is already in the latest supported version: '{from_version}'") + return AVAILABLE_VERSIONS[start_pos + 1] + + +def check_versions_coherence(from_version: str, target_version: str) -> None: + start_pos = _get_version_index(from_version) + final_pos = _get_version_index(target_version) + if final_pos == start_pos: + raise InvalidUpgrade(f"Your study is already in the version you asked: {from_version}") + elif final_pos < start_pos: + raise InvalidUpgrade(f"Cannot downgrade your study version : from {from_version} to {target_version}") diff --git a/antarest/study/storage/study_upgrader/upgrader_710.py b/antarest/study/storage/study_upgrader/upgrader_710.py deleted file mode 100644 index b1679b3342..0000000000 --- a/antarest/study/storage/study_upgrader/upgrader_710.py +++ /dev/null @@ -1,29 +0,0 @@ -from pathlib import Path - -from antarest.study.storage.rawstudy.ini_reader import IniReader -from antarest.study.storage.rawstudy.ini_writer import IniWriter -from antarest.study.storage.rawstudy.model.filesystem.root.settings.generaldata import DUPLICATE_KEYS - -GENERAL_DATA_PATH = "settings/generaldata.ini" - - -def upgrade_710(study_path: Path) -> None: - """ - Upgrade the study configuration to version 710. - - NOTE: - The file `study.antares` is not upgraded here. - - Args: - study_path: path to the study directory. - """ - - reader = IniReader(DUPLICATE_KEYS) - data = reader.read(study_path / GENERAL_DATA_PATH) - data["general"]["geographic-trimming"] = data["general"]["filtering"] - data["general"]["thematic-trimming"] = False - data["optimization"]["link-type"] = "local" - data["other preferences"]["hydro-pricing-mode"] = "fast" - del data["general"]["filtering"] - writer = IniWriter(special_keys=DUPLICATE_KEYS) - writer.write(data, study_path / GENERAL_DATA_PATH) diff --git a/antarest/study/storage/study_upgrader/upgrader_720.py b/antarest/study/storage/study_upgrader/upgrader_720.py deleted file mode 100644 index 25e740baed..0000000000 --- a/antarest/study/storage/study_upgrader/upgrader_720.py +++ /dev/null @@ -1,6 +0,0 @@ -from pathlib import Path - - -def upgrade_720(study_path: Path) -> None: - # There is no input modification between the 7.1.0 and the 7.2.0 version - pass diff --git a/antarest/study/storage/study_upgrader/upgrader_800.py b/antarest/study/storage/study_upgrader/upgrader_800.py deleted file mode 100644 index b53a792985..0000000000 --- a/antarest/study/storage/study_upgrader/upgrader_800.py +++ /dev/null @@ -1,29 +0,0 @@ -from pathlib import Path - -from antarest.study.storage.rawstudy.ini_reader import IniReader -from antarest.study.storage.rawstudy.ini_writer import IniWriter -from antarest.study.storage.rawstudy.model.filesystem.root.settings.generaldata import DUPLICATE_KEYS - -GENERAL_DATA_PATH = "settings/generaldata.ini" - - -def upgrade_800(study_path: Path) -> None: - """ - Upgrade the study configuration to version 800. - - NOTE: - The file `study.antares` is not upgraded here. - - Args: - study_path: path to the study directory. - """ - - reader = IniReader(DUPLICATE_KEYS) - data = reader.read(study_path / GENERAL_DATA_PATH) - data["other preferences"]["hydro-heuristic-policy"] = "accommodate rule curves" - data["optimization"]["include-exportstructure"] = False - data["optimization"]["include-unfeasible-problem-behavior"] = "error-verbose" - data["general"]["custom-scenario"] = data["general"]["custom-ts-numbers"] - del data["general"]["custom-ts-numbers"] - writer = IniWriter(special_keys=DUPLICATE_KEYS) - writer.write(data, study_path / GENERAL_DATA_PATH) diff --git a/antarest/study/storage/study_upgrader/upgrader_810.py b/antarest/study/storage/study_upgrader/upgrader_810.py deleted file mode 100644 index e28ef4d4b6..0000000000 --- a/antarest/study/storage/study_upgrader/upgrader_810.py +++ /dev/null @@ -1,27 +0,0 @@ -from pathlib import Path - -from antarest.study.storage.rawstudy.ini_reader import IniReader -from antarest.study.storage.rawstudy.ini_writer import IniWriter -from antarest.study.storage.rawstudy.model.filesystem.root.settings.generaldata import DUPLICATE_KEYS - -GENERAL_DATA_PATH = "settings/generaldata.ini" - - -def upgrade_810(study_path: Path) -> None: - """ - Upgrade the study configuration to version 810. - - NOTE: - The file `study.antares` is not upgraded here. - - Args: - study_path: path to the study directory. - """ - - reader = IniReader(DUPLICATE_KEYS) - data = reader.read(study_path / GENERAL_DATA_PATH) - data["other preferences"]["renewable-generation-modelling"] = "aggregated" - writer = IniWriter(special_keys=DUPLICATE_KEYS) - writer.write(data, study_path / GENERAL_DATA_PATH) - study_path.joinpath("input", "renewables", "clusters").mkdir(parents=True, exist_ok=True) - study_path.joinpath("input", "renewables", "series").mkdir(parents=True, exist_ok=True) diff --git a/antarest/study/storage/study_upgrader/upgrader_820.py b/antarest/study/storage/study_upgrader/upgrader_820.py deleted file mode 100644 index f5416d0180..0000000000 --- a/antarest/study/storage/study_upgrader/upgrader_820.py +++ /dev/null @@ -1,55 +0,0 @@ -import glob -from pathlib import Path -from typing import cast - -import numpy as np -import numpy.typing as npt -import pandas - - -def upgrade_820(study_path: Path) -> None: - """ - Upgrade the study configuration to version 820. - - NOTE: - The file `study.antares` is not upgraded here. - - Args: - study_path: path to the study directory. - """ - - links = glob.glob(str(study_path / "input" / "links" / "*")) - if len(links) > 0: - for folder in links: - folder_path = Path(folder) - all_txt = glob.glob(str(folder_path / "*.txt")) - if len(all_txt) > 0: - (folder_path / "capacities").mkdir(exist_ok=True) - for txt in all_txt: - df = pandas.read_csv(txt, sep="\t", header=None) - df_parameters = df.iloc[:, 2:8] - df_direct = df.iloc[:, 0] - df_indirect = df.iloc[:, 1] - name = Path(txt).stem - # noinspection PyTypeChecker - np.savetxt( - folder_path / f"{name}_parameters.txt", - cast(npt.NDArray[np.float64], df_parameters.values), - delimiter="\t", - fmt="%.6f", - ) - # noinspection PyTypeChecker - np.savetxt( - folder_path / "capacities" / f"{name}_direct.txt", - cast(npt.NDArray[np.float64], df_direct.values), - delimiter="\t", - fmt="%.6f", - ) - # noinspection PyTypeChecker - np.savetxt( - folder_path / "capacities" / f"{name}_indirect.txt", - cast(npt.NDArray[np.float64], df_indirect.values), - delimiter="\t", - fmt="%.6f", - ) - (folder_path / f"{name}.txt").unlink() diff --git a/antarest/study/storage/study_upgrader/upgrader_830.py b/antarest/study/storage/study_upgrader/upgrader_830.py deleted file mode 100644 index 414db9b8e2..0000000000 --- a/antarest/study/storage/study_upgrader/upgrader_830.py +++ /dev/null @@ -1,40 +0,0 @@ -import glob -from pathlib import Path - -from antarest.study.storage.rawstudy.ini_reader import IniReader -from antarest.study.storage.rawstudy.ini_writer import IniWriter -from antarest.study.storage.rawstudy.model.filesystem.root.settings.generaldata import DUPLICATE_KEYS - -GENERAL_DATA_PATH = "settings/generaldata.ini" - - -def upgrade_830(study_path: Path) -> None: - """ - Upgrade the study configuration to version 830. - - NOTE: - The file `study.antares` is not upgraded here. - - Args: - study_path: path to the study directory. - """ - - reader = IniReader(DUPLICATE_KEYS) - data = reader.read(study_path / GENERAL_DATA_PATH) - data["adequacy patch"] = { - "include-adq-patch": False, - "set-to-null-ntc-between-physical-out-for-first-step": True, - "set-to-null-ntc-from-physical-out-to-physical-in-for-first-step": True, - } - data["optimization"]["include-split-exported-mps"] = False - writer = IniWriter(special_keys=DUPLICATE_KEYS) - writer.write(data, study_path / GENERAL_DATA_PATH) - areas = glob.glob(str(study_path / "input" / "areas" / "*")) - for folder in areas: - folder_path = Path(folder) - if folder_path.is_dir(): - writer = IniWriter(special_keys=DUPLICATE_KEYS) - writer.write( - {"adequacy-patch": {"adequacy-patch-mode": "outside"}}, - folder_path / "adequacy_patch.ini", - ) diff --git a/antarest/study/storage/study_upgrader/upgrader_840.py b/antarest/study/storage/study_upgrader/upgrader_840.py deleted file mode 100644 index a96aa0072a..0000000000 --- a/antarest/study/storage/study_upgrader/upgrader_840.py +++ /dev/null @@ -1,33 +0,0 @@ -from pathlib import Path - -from antarest.study.storage.rawstudy.ini_reader import IniReader -from antarest.study.storage.rawstudy.ini_writer import IniWriter -from antarest.study.storage.rawstudy.model.filesystem.root.settings.generaldata import DUPLICATE_KEYS - -GENERAL_DATA_PATH = "settings/generaldata.ini" -MAPPING_TRANSMISSION_CAPACITIES = { - True: "local-values", - False: "null-for-all-links", - "infinite": "infinite-for-all-links", -} - - -def upgrade_840(study_path: Path) -> None: - """ - Upgrade the study configuration to version 840. - - NOTE: - The file `study.antares` is not upgraded here. - - Args: - study_path: path to the study directory. - """ - - reader = IniReader(DUPLICATE_KEYS) - data = reader.read(study_path / GENERAL_DATA_PATH) - data["optimization"]["transmission-capacities"] = MAPPING_TRANSMISSION_CAPACITIES[ - data["optimization"]["transmission-capacities"] - ] - del data["optimization"]["include-split-exported-mps"] - writer = IniWriter(special_keys=DUPLICATE_KEYS) - writer.write(data, study_path / GENERAL_DATA_PATH) diff --git a/antarest/study/storage/study_upgrader/upgrader_850.py b/antarest/study/storage/study_upgrader/upgrader_850.py deleted file mode 100644 index 08695f1e3d..0000000000 --- a/antarest/study/storage/study_upgrader/upgrader_850.py +++ /dev/null @@ -1,33 +0,0 @@ -from pathlib import Path - -from antarest.study.storage.rawstudy.ini_reader import IniReader -from antarest.study.storage.rawstudy.ini_writer import IniWriter -from antarest.study.storage.rawstudy.model.filesystem.root.settings.generaldata import DUPLICATE_KEYS - -# noinspection SpellCheckingInspection -GENERAL_DATA_PATH = "settings/generaldata.ini" - - -def upgrade_850(study_path: Path) -> None: - """ - Upgrade the study configuration to version 850. - - NOTE: - The file `study.antares` is not upgraded here. - - Args: - study_path: path to the study directory. - """ - - reader = IniReader(DUPLICATE_KEYS) - data = reader.read(study_path / GENERAL_DATA_PATH) - - data["adequacy patch"]["price-taking-order"] = "DENS" - data["adequacy patch"]["include-hurdle-cost-csr"] = False - data["adequacy patch"]["check-csr-cost-function"] = False - data["adequacy patch"]["threshold-initiate-curtailment-sharing-rule"] = 0.0 - data["adequacy patch"]["threshold-display-local-matching-rule-violations"] = 0.0 - data["adequacy patch"]["threshold-csr-variable-bounds-relaxation"] = 3 - - writer = IniWriter(special_keys=DUPLICATE_KEYS) - writer.write(data, study_path / GENERAL_DATA_PATH) diff --git a/antarest/study/storage/study_upgrader/upgrader_860.py b/antarest/study/storage/study_upgrader/upgrader_860.py deleted file mode 100644 index 4d6f873f0d..0000000000 --- a/antarest/study/storage/study_upgrader/upgrader_860.py +++ /dev/null @@ -1,42 +0,0 @@ -from pathlib import Path - -from antarest.study.storage.rawstudy.ini_reader import IniReader -from antarest.study.storage.rawstudy.ini_writer import IniWriter -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id -from antarest.study.storage.rawstudy.model.filesystem.root.settings.generaldata import DUPLICATE_KEYS - -# noinspection SpellCheckingInspection -GENERAL_DATA_PATH = "settings/generaldata.ini" - - -def upgrade_860(study_path: Path) -> None: - """ - Upgrade the study configuration to version 860. - - NOTE: - The file `study.antares` is not upgraded here. - - Args: - study_path: path to the study directory. - """ - - reader = IniReader(DUPLICATE_KEYS) - data = reader.read(study_path / GENERAL_DATA_PATH) - data["adequacy patch"]["enable-first-step "] = True - writer = IniWriter(special_keys=DUPLICATE_KEYS) - writer.write(data, study_path / GENERAL_DATA_PATH) - - study_path.joinpath("input", "st-storage", "clusters").mkdir(parents=True, exist_ok=True) - study_path.joinpath("input", "st-storage", "series").mkdir(parents=True, exist_ok=True) - list_areas = ( - study_path.joinpath("input", "areas", "list.txt").read_text(encoding="utf-8").splitlines(keepends=False) - ) - for area_name in list_areas: - area_id = transform_name_to_id(area_name) - st_storage_path = study_path.joinpath("input", "st-storage", "clusters", area_id) - st_storage_path.mkdir(parents=True, exist_ok=True) - (st_storage_path / "list.ini").touch() - - hydro_series_path = study_path.joinpath("input", "hydro", "series", area_id) - hydro_series_path.mkdir(parents=True, exist_ok=True) - (hydro_series_path / "mingen.txt").touch() diff --git a/antarest/study/storage/study_upgrader/upgrader_870.py b/antarest/study/storage/study_upgrader/upgrader_870.py deleted file mode 100644 index 9b67a77c52..0000000000 --- a/antarest/study/storage/study_upgrader/upgrader_870.py +++ /dev/null @@ -1,64 +0,0 @@ -import typing as t -from pathlib import Path - -import numpy as np -import numpy.typing as npt -import pandas as pd - -from antarest.study.storage.rawstudy.ini_reader import IniReader -from antarest.study.storage.rawstudy.ini_writer import IniWriter - - -# noinspection SpellCheckingInspection -def upgrade_870(study_path: Path) -> None: - """ - Upgrade the study configuration to version 870. - - NOTE: - The file `study.antares` is not upgraded here. - - Args: - study_path: path to the study directory. - """ - - # Split existing binding constraints in 3 different files - binding_constraints_path = study_path / "input" / "bindingconstraints" - binding_constraints_files = binding_constraints_path.glob("*.txt") - for file in binding_constraints_files: - name = file.stem - if file.stat().st_size == 0: - lt, gt, eq = pd.Series(), pd.Series(), pd.Series() - else: - df = pd.read_csv(file, sep="\t", header=None) - lt, gt, eq = df.iloc[:, 0], df.iloc[:, 1], df.iloc[:, 2] - for term, suffix in zip([lt, gt, eq], ["lt", "gt", "eq"]): - # noinspection PyTypeChecker - np.savetxt( - binding_constraints_path / f"{name}_{suffix}.txt", - t.cast(npt.NDArray[np.float64], term.values), - delimiter="\t", - fmt="%.6f", - ) - file.unlink() - - # Add property group for every section in .ini file - ini_file_path = binding_constraints_path / "bindingconstraints.ini" - data = IniReader().read(ini_file_path) - for section in data: - data[section]["group"] = "default" - IniWriter().write(data, ini_file_path) - - # Add properties for thermal clusters in .ini file - ini_files = study_path.glob("input/thermal/clusters/*/list.ini") - thermal_path = study_path / Path("input/thermal/series") - for ini_file_path in ini_files: - data = IniReader().read(ini_file_path) - area_id = ini_file_path.parent.name - for cluster in data: - new_thermal_path = thermal_path / area_id / cluster.lower() - (new_thermal_path / "CO2Cost.txt").touch() - (new_thermal_path / "fuelCost.txt").touch() - data[cluster]["costgeneration"] = "SetManually" - data[cluster]["efficiency"] = 100 - data[cluster]["variableomcost"] = 0 - IniWriter().write(data, ini_file_path) diff --git a/antarest/study/storage/study_upgrader/upgrader_880.py b/antarest/study/storage/study_upgrader/upgrader_880.py deleted file mode 100644 index 0de50cff4b..0000000000 --- a/antarest/study/storage/study_upgrader/upgrader_880.py +++ /dev/null @@ -1,32 +0,0 @@ -import glob -from pathlib import Path - -from antarest.study.storage.rawstudy.ini_reader import IniReader -from antarest.study.storage.rawstudy.ini_writer import IniWriter -from antarest.study.storage.rawstudy.model.filesystem.root.settings.generaldata import DUPLICATE_KEYS - - -# noinspection SpellCheckingInspection -def upgrade_880(study_path: Path) -> None: - """ - Upgrade the study configuration to version 880. - - NOTE: - The file `study.antares` is not upgraded here. - - Args: - study_path: path to the study directory. - """ - st_storage_path = study_path / "input" / "st-storage" / "clusters" - if not st_storage_path.exists(): - # The folder only exists for studies in v8.6+ that have some short term storage clusters. - # For every other case, this upgrader has nothing to do. - return - writer = IniWriter(special_keys=DUPLICATE_KEYS) - cluster_files = glob.glob(str(st_storage_path / "*" / "list.ini")) - for file in cluster_files: - file_path = Path(file) - cluster_list = IniReader().read(file_path) - for cluster in cluster_list: - cluster_list[cluster]["enabled"] = True - writer.write(cluster_list, file_path) diff --git a/antarest/study/storage/utils.py b/antarest/study/storage/utils.py index 5dc97b081b..5fbbf9ccb5 100644 --- a/antarest/study/storage/utils.py +++ b/antarest/study/storage/utils.py @@ -152,7 +152,8 @@ def remove_from_cache(cache: ICache, root_id: str) -> None: def create_new_empty_study(version: str, path_study: Path, path_resources: Path) -> None: version_template: t.Optional[str] = STUDY_REFERENCE_TEMPLATES.get(version, None) if version_template is None: - raise UnsupportedStudyVersion(version) + msg = f"{version} is not a supported version, supported versions are: {list(STUDY_REFERENCE_TEMPLATES.keys())}" + raise UnsupportedStudyVersion(msg) empty_study_zip = path_resources / version_template diff --git a/antarest/tools/cli.py b/antarest/tools/cli.py index 902eee6d3a..9a591b0d9e 100644 --- a/antarest/tools/cli.py +++ b/antarest/tools/cli.py @@ -5,7 +5,7 @@ import click from antarest.study.model import NEW_DEFAULT_STUDY_VERSION -from antarest.study.storage.study_upgrader import upgrade_study +from antarest.study.storage.study_upgrader import StudyUpgrader from antarest.tools.lib import extract_commands, generate_diff, generate_study @@ -164,7 +164,8 @@ def cli_upgrade_study(study_path: Path, target_version: str) -> None: TARGET_VERSION is the version you want your study to be at (example 8.4.0 or 840) """ - upgrade_study(Path(study_path), target_version.replace(".", "")) + study_upgrader = StudyUpgrader(study_path, target_version.replace(".", "")) + study_upgrader.upgrade() if __name__ == "__main__": diff --git a/pyproject.toml b/pyproject.toml index a715fb2753..7031fea678 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,70 @@ +[build-system] +requires = ["setuptools"] + +[project] +name = "AntaREST" +version = "2.17.5" +authors = [{name="RTE, Antares Web Team", email="andrea.sgattoni@rte-france.com" }] +description="Antares Server" +readme = {file = "README.md", content-type = "text/markdown"} +license = {file = "LICENSE"} +requires-python = ">=3.8" +classifiers=[ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "License :: Apache License :: 2.0", + "Operating System :: OS Independent", + ] + +[project.urls] +Repository="https://github.com/AntaresSimulatorTeam/api-iso-antares" + +[tool.setuptools] +platforms = [ + "linux-x86_64", + "macosx-10.14-x86_64", + "macosx-10.15-x86_64", + "macosx-11-x86_64", + "macosx-12-x86_64", + "macosx-13-x86_64", + "win-amd64", +] + +[tool.setuptools.packages.find] +where = ["."] +include = ["antarest*"] # alternatively: `exclude = ["additional*"]` + +[tool.mypy] +strict = true +files = "antarest/**/*.py" + +[[tool.mypy.overrides]] +module = [ + "antareslauncher.*", + "jsonschema", + "pytest", + "httpx", + "jsonref", + "jsonref", +] +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "alembic.*" +no_implicit_reexport = false + + +[tool.pytest.ini_options] +testpaths = ["tests"] +markers = [ + "unit_test", + "integration_test" +] + + [tool.black] target-version = ["py38"] line-length = 120 diff --git a/requirements.txt b/requirements.txt index 5a543c02fc..77f77c8fb2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ Antares-Launcher~=1.3.2 +antares-study-version~=1.0.3 alembic~=1.7.5 asgi-ratelimit[redis]==0.7.0 diff --git a/scripts/package_antares_web.sh b/scripts/package_antares_web.sh index 9a0efb5e15..881ee8fe75 100755 --- a/scripts/package_antares_web.sh +++ b/scripts/package_antares_web.sh @@ -17,6 +17,7 @@ PROJECT_DIR=$(dirname -- "${SCRIPT_DIR}") DIST_DIR="${PROJECT_DIR}/dist/package" RESOURCES_DIR="${PROJECT_DIR}/resources" ANTARES_SOLVER_DIR="${DIST_DIR}/AntaresWeb/antares_solver" +INSTALLER_DIR="${PROJECT_DIR}/installer/src/antares_web_installer/" if [[ "$OSTYPE" == "msys"* ]]; then ANTARES_SOLVER_FOLDER_NAME="rte-antares-$ANTARES_SOLVER_FULL_VERSION-installer-64bits" @@ -45,6 +46,18 @@ else popd fi +echo "INFO: Generating the Installer for the Desktop application..." +echo which python +if [[ "$OSTYPE" == "msys"* ]]; then + pushd ${PROJECT_DIR} + pyinstaller --onefile "${INSTALLER_DIR}gui/__main__.py" --distpath "${DIST_DIR}" --hidden-import antares_web_installer.shortcuts._linux_shell --hidden-import antares_web_installer.shortcuts._win32_shell --noconsole --name AntaresWebInstaller + popd +else + pushd ${PROJECT_DIR} + pyinstaller --onefile "${INSTALLER_DIR}cli/__main__.py" --distpath "${DIST_DIR}" --hidden-import antares_web_installer.shortcuts._linux_shell --hidden-import antares_web_installer.shortcuts._win32_shell --noconsole --name AntaresWebInstallerCLI + popd +fi + echo "INFO: Creating destination directory '${ANTARES_SOLVER_DIR}'..." mkdir -p "${ANTARES_SOLVER_DIR}" diff --git a/scripts/update_version.py b/scripts/update_version.py index a829035ff0..6a981968ea 100755 --- a/scripts/update_version.py +++ b/scripts/update_version.py @@ -117,9 +117,9 @@ def upgrade_version(new_version: str, new_date: str) -> None: # Patching version number files_to_patch = [ ( - "setup.py", - "version=.*", - f'version="{new_version}",', + "pyproject.toml", + "\nversion = .*", + f'\nversion = "{new_version}"', ), ( "sonar-project.properties", diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index b0f3012d17..0000000000 --- a/setup.cfg +++ /dev/null @@ -1,28 +0,0 @@ -[mypy] -strict = True -files = antarest/**/*.py - -[mypy-antareslauncher.*] -ignore_missing_imports = True - -[mypy-jsonschema.*] -ignore_missing_imports = True - -[mypy-pytest.*] -ignore_missing_imports = True - -[mypy-httpx.*] -ignore_missing_imports = True - -[mypy-jsonref.*] -ignore_missing_imports = True - -[mypy-alembic.*] -no_implicit_reexport = false - -[tool:pytest] -testpaths = - tests -markers = - unit_test - integration_test diff --git a/setup.py b/setup.py deleted file mode 100644 index 38e6d70055..0000000000 --- a/setup.py +++ /dev/null @@ -1,37 +0,0 @@ -from pathlib import Path - -from setuptools import setup, find_packages - -excluded_dirs = {"alembic", "conf", "docs", "examples", "resources", "scripts", "tests", "venv", "webapp"} - -setup( - name="AntaREST", - version="2.17.5", - description="Antares Server", - long_description=Path("README.md").read_text(encoding="utf-8"), - long_description_content_type="text/markdown", - author="RTE, Antares Web Team", - author_email="andrea.sgattoni@rte-france.com", - url="https://github.com/AntaresSimulatorTeam/api-iso-antares", - packages=find_packages(exclude=excluded_dirs), - license="Apache Software License", - platforms=[ - "linux-x86_64", - "macosx-10.14-x86_64", - "macosx-10.15-x86_64", - "macosx-11-x86_64", - "macosx-12-x86_64", - "macosx-13-x86_64", - "win-amd64", - ], - classifiers=[ - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "License :: Apache License :: 2.0", - "Operating System :: OS Independent", - ], - python_requires=">=3.8", -) diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 55c1073168..08aa099dd1 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -806,6 +806,15 @@ def test_area_management(client: TestClient, admin_access_token: str) -> None: "thresholdCsrVariableBoundsRelaxation": 3, } + # asserts csr field is an int + res = client.put( + f"/v1/studies/{study_id}/config/adequacypatch/form", + json={"thresholdCsrVariableBoundsRelaxation": 0.8}, + ) + assert res.status_code == 422 + assert res.json()["exception"] == "RequestValidationError" + assert res.json()["description"] == "value is not a valid integer" + # General form res_general_config = client.get(f"/v1/studies/{study_id}/config/general/form") diff --git a/tests/integration/test_studies_upgrade.py b/tests/integration/test_studies_upgrade.py index 3eb92522af..9640fb5cf9 100644 --- a/tests/integration/test_studies_upgrade.py +++ b/tests/integration/test_studies_upgrade.py @@ -38,7 +38,6 @@ def test_upgrade_study__target_version(self, client: TestClient, user_access_tok assert task.status == TaskStatus.COMPLETED assert target_version in task.result.message, f"Version not in {task.result.message=}" - @pytest.mark.skipif(RUN_ON_WINDOWS, reason="This test runs randomly on Windows") def test_upgrade_study__bad_target_version( self, client: TestClient, user_access_token: str, internal_study_id: str ): @@ -48,12 +47,9 @@ def test_upgrade_study__bad_target_version( headers={"Authorization": f"Bearer {user_access_token}"}, params={"target_version": target_version}, ) - assert res.status_code == 200 - task_id = res.json() - assert task_id - task = wait_task_completion(client, user_access_token, task_id) - assert task.status == TaskStatus.FAILED - assert target_version in task.result.message, f"Version not in {task.result.message=}" + assert res.status_code == 400 + assert res.json()["exception"] == "UnsupportedStudyVersion" + assert f"Version '{target_version}' isn't among supported versions" in res.json()["description"] def test_upgrade_study__unmet_requirements(self, client: TestClient, admin_access_token: str): """ diff --git a/tests/storage/business/test_study_version_upgrader.py b/tests/storage/business/test_study_version_upgrader.py index efe1e75315..f406070308 100644 --- a/tests/storage/business/test_study_version_upgrader.py +++ b/tests/storage/business/test_study_version_upgrader.py @@ -5,18 +5,83 @@ import shutil import zipfile from pathlib import Path -from typing import List +from typing import List, Optional import pandas import pytest +from pandas.errors import EmptyDataError +from antarest.core.exceptions import UnsupportedStudyVersion from antarest.study.storage.rawstudy.ini_reader import IniReader from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.root.settings.generaldata import DUPLICATE_KEYS -from antarest.study.storage.study_upgrader import UPGRADE_METHODS, InvalidUpgrade, upgrade_study -from antarest.study.storage.study_upgrader.upgrader_840 import MAPPING_TRANSMISSION_CAPACITIES +from antarest.study.storage.study_upgrader import ( + InvalidUpgrade, + StudyUpgrader, + check_versions_coherence, + find_next_version, +) from tests.storage.business.assets import ASSETS_DIR +MAPPING_TRANSMISSION_CAPACITIES = { + True: "local-values", + False: "null-for-all-links", + "infinite": "infinite-for-all-links", +} + + +class TestFindNextVersion: + @pytest.mark.parametrize( + "from_version, expected", + [("700", "710"), ("870", "880")], + ) + def test_find_next_version_nominal(self, from_version: str, expected: str): + actual = find_next_version(from_version) + assert actual == expected + + @pytest.mark.parametrize( + "from_version, message", + [ + ("3.14", "Version '3.14' isn't among supported versions"), + ("880", "Your study is already in the latest supported version: '880'"), + ("900", "Version '900' isn't among supported versions"), + ], + ) + def test_find_next_version_fails(self, from_version: str, message: str): + with pytest.raises(UnsupportedStudyVersion, match=message): + find_next_version(from_version) + + +class TestCheckVersionCoherence: + @pytest.mark.parametrize( + "from_version, target_version", + [("700", "710"), ("870", "880"), ("820", "840")], + ) + def test_check_version_coherence_nominal(self, from_version: str, target_version: str): + check_versions_coherence(from_version, target_version) + + @pytest.mark.parametrize( + "from_version, target_version, message", + [ + ("1000", "710", "Version '1000' isn't among supported versions"), + ("820", "32", "Version '32' isn't among supported versions"), + ], + ) + def test_invalid_versions_fails(self, from_version: str, target_version: str, message: str): + with pytest.raises(UnsupportedStudyVersion, match=message): + check_versions_coherence(from_version, target_version) + + @pytest.mark.parametrize( + "from_version, target_version, message", + [ + ("860", "860", "Your study is already in the version you asked: 860"), + ("870", "840", "Cannot downgrade your study version : from 870 to 840"), + ], + ) + def test_check_version_coherence_fails(self, from_version: str, target_version: str, message: str): + with pytest.raises(InvalidUpgrade, match=message): + check_versions_coherence(from_version, target_version) + def test_end_to_end_upgrades(tmp_path: Path): # Prepare a study to upgrade @@ -32,7 +97,8 @@ def test_end_to_end_upgrades(tmp_path: Path): old_binding_constraint_values = get_old_binding_constraint_values(study_dir) # Only checks if the study_upgrader can go from the first supported version to the last one target_version = "880" - upgrade_study(study_dir, target_version) + study_upgrader = StudyUpgrader(study_dir, target_version) + study_upgrader.upgrade() assert_study_antares_file_is_updated(study_dir, target_version) assert_settings_are_updated(study_dir, old_values) assert_inputs_are_updated(study_dir, old_areas_values, old_binding_constraint_values) @@ -46,26 +112,20 @@ def test_fails_because_of_versions_asked(tmp_path: Path): with zipfile.ZipFile(path_study) as zip_output: zip_output.extractall(path=study_dir) # Try to upgrade with an unknown version - with pytest.raises( - InvalidUpgrade, - match=f"Version '600' unknown: possible versions are {', '.join([u[1] for u in UPGRADE_METHODS])}", - ): - upgrade_study(study_dir, "600") + with pytest.raises(InvalidUpgrade, match="Cannot downgrade from version '7.2' to '6'"): + StudyUpgrader(study_dir, "600").upgrade() # Try to upgrade with the current version - with pytest.raises(InvalidUpgrade, match="Your study is already in version '720'"): - upgrade_study(study_dir, "720") + with pytest.raises(InvalidUpgrade, match="Your study is already in version '7.2'"): + StudyUpgrader(study_dir, "720").upgrade() # Try to upgrade with an old version with pytest.raises( InvalidUpgrade, - match="Impossible to upgrade from version '720' to version '710'", + match="Cannot downgrade from version '7.2' to '7.1'", ): - upgrade_study(study_dir, "710") + StudyUpgrader(study_dir, "710").upgrade() # Try to upgrade with a version that does not exist - with pytest.raises( - InvalidUpgrade, - match=f"Version '820.rc' unknown: possible versions are {', '.join([u[1] for u in UPGRADE_METHODS])}", - ): - upgrade_study(study_dir, "820.rc") + with pytest.raises(InvalidUpgrade, match="Invalid version number '820.rc'"): + StudyUpgrader(study_dir, "820.rc").upgrade() def test_fallback_if_study_input_broken(tmp_path): @@ -78,10 +138,10 @@ def test_fallback_if_study_input_broken(tmp_path): before_upgrade_dir = tmp_path / "backup" shutil.copytree(study_dir, before_upgrade_dir, dirs_exist_ok=True) with pytest.raises( - expected_exception=pandas.errors.EmptyDataError, + expected_exception=EmptyDataError, match="No columns to parse from file", ): - upgrade_study(study_dir, "850") + StudyUpgrader(study_dir, "850").upgrade() assert are_same_dir(study_dir, before_upgrade_dir) @@ -231,8 +291,8 @@ def assert_folder_is_created(path: Path) -> None: assert (path / "series").is_dir() -def are_same_dir(dir1, dir2) -> bool: - dirs_cmp = filecmp.dircmp(dir1, dir2) +def are_same_dir(dir1, dir2, ignore: Optional[List[str]] = None) -> bool: + dirs_cmp = filecmp.dircmp(dir1, dir2, ignore=ignore) if len(dirs_cmp.left_only) > 0 or len(dirs_cmp.right_only) > 0 or len(dirs_cmp.funny_files) > 0: return False path_dir1 = Path(dir1) diff --git a/tests/storage/study_upgrader/test_upgrade_710.py b/tests/storage/study_upgrader/test_upgrade_710.py index a389178999..6542cd3a3e 100644 --- a/tests/storage/study_upgrader/test_upgrade_710.py +++ b/tests/storage/study_upgrader/test_upgrade_710.py @@ -1,5 +1,5 @@ from antarest.study.storage.rawstudy.ini_reader import IniReader -from antarest.study.storage.study_upgrader import upgrade_710 +from antarest.study.storage.study_upgrader import StudyUpgrader from tests.storage.study_upgrader.conftest import StudyAssets @@ -9,7 +9,8 @@ def test_nominal_case(study_assets: StudyAssets): """ # upgrade the study - upgrade_710(study_assets.study_dir) + study_upgrader = StudyUpgrader(study_assets.study_dir, "710") + study_upgrader.upgrade() # compare generaldata.ini actual_path = study_assets.study_dir.joinpath("settings/generaldata.ini") diff --git a/tests/storage/study_upgrader/test_upgrade_720.py b/tests/storage/study_upgrader/test_upgrade_720.py index 43eb2009f1..f2757b6fd5 100644 --- a/tests/storage/study_upgrader/test_upgrade_720.py +++ b/tests/storage/study_upgrader/test_upgrade_720.py @@ -1,4 +1,4 @@ -from antarest.study.storage.study_upgrader import upgrade_720 +from antarest.study.storage.study_upgrader import StudyUpgrader from tests.storage.business.test_study_version_upgrader import are_same_dir from tests.storage.study_upgrader.conftest import StudyAssets @@ -9,7 +9,8 @@ def test_nominal_case(study_assets: StudyAssets): """ # upgrade the study - upgrade_720(study_assets.study_dir) + study_upgrader = StudyUpgrader(study_assets.study_dir, "720") + study_upgrader.upgrade() # compare folder - assert are_same_dir(study_assets.study_dir, study_assets.expected_dir) + assert are_same_dir(study_assets.study_dir, study_assets.expected_dir, ignore=["study.antares"]) diff --git a/tests/storage/study_upgrader/test_upgrade_800.py b/tests/storage/study_upgrader/test_upgrade_800.py index b2c72fcee1..aded4de2c4 100644 --- a/tests/storage/study_upgrader/test_upgrade_800.py +++ b/tests/storage/study_upgrader/test_upgrade_800.py @@ -1,5 +1,5 @@ from antarest.study.storage.rawstudy.ini_reader import IniReader -from antarest.study.storage.study_upgrader import upgrade_800 +from antarest.study.storage.study_upgrader import StudyUpgrader from tests.storage.study_upgrader.conftest import StudyAssets @@ -9,7 +9,8 @@ def test_nominal_case(study_assets: StudyAssets): """ # upgrade the study - upgrade_800(study_assets.study_dir) + study_upgrader = StudyUpgrader(study_assets.study_dir, "800") + study_upgrader.upgrade() # compare generaldata.ini actual_path = study_assets.study_dir.joinpath("settings/generaldata.ini") diff --git a/tests/storage/study_upgrader/test_upgrade_810.py b/tests/storage/study_upgrader/test_upgrade_810.py index 9639beb94f..2705798eb7 100644 --- a/tests/storage/study_upgrader/test_upgrade_810.py +++ b/tests/storage/study_upgrader/test_upgrade_810.py @@ -1,5 +1,5 @@ from antarest.study.storage.rawstudy.ini_reader import IniReader -from antarest.study.storage.study_upgrader import upgrade_810 +from antarest.study.storage.study_upgrader import StudyUpgrader from tests.storage.business.test_study_version_upgrader import are_same_dir from tests.storage.study_upgrader.conftest import StudyAssets @@ -10,7 +10,8 @@ def test_nominal_case(study_assets: StudyAssets): """ # upgrade the study - upgrade_810(study_assets.study_dir) + study_upgrader = StudyUpgrader(study_assets.study_dir, "810") + study_upgrader.upgrade() # compare generaldata.ini actual_path = study_assets.study_dir.joinpath("settings/generaldata.ini") diff --git a/tests/storage/study_upgrader/test_upgrade_820.py b/tests/storage/study_upgrader/test_upgrade_820.py index 730b7c0127..d535a9b838 100644 --- a/tests/storage/study_upgrader/test_upgrade_820.py +++ b/tests/storage/study_upgrader/test_upgrade_820.py @@ -1,5 +1,5 @@ from antarest.study.storage.rawstudy.ini_reader import IniReader -from antarest.study.storage.study_upgrader import upgrade_820 +from antarest.study.storage.study_upgrader import StudyUpgrader from tests.storage.business.test_study_version_upgrader import are_same_dir from tests.storage.study_upgrader.conftest import StudyAssets @@ -10,7 +10,8 @@ def test_nominal_case(study_assets: StudyAssets): """ # upgrade the study - upgrade_820(study_assets.study_dir) + study_upgrader = StudyUpgrader(study_assets.study_dir, "820") + study_upgrader.upgrade() # compare generaldata.ini actual_path = study_assets.study_dir.joinpath("settings/generaldata.ini") diff --git a/tests/storage/study_upgrader/test_upgrade_830.py b/tests/storage/study_upgrader/test_upgrade_830.py index d0612ef889..c17e110d58 100644 --- a/tests/storage/study_upgrader/test_upgrade_830.py +++ b/tests/storage/study_upgrader/test_upgrade_830.py @@ -1,5 +1,5 @@ from antarest.study.storage.rawstudy.ini_reader import IniReader -from antarest.study.storage.study_upgrader import upgrade_830 +from antarest.study.storage.study_upgrader import StudyUpgrader from tests.storage.business.test_study_version_upgrader import are_same_dir from tests.storage.study_upgrader.conftest import StudyAssets @@ -10,7 +10,8 @@ def test_nominal_case(study_assets: StudyAssets): """ # upgrade the study - upgrade_830(study_assets.study_dir) + study_upgrader = StudyUpgrader(study_assets.study_dir, "830") + study_upgrader.upgrade() # compare generaldata.ini actual_path = study_assets.study_dir.joinpath("settings/generaldata.ini") diff --git a/tests/storage/study_upgrader/test_upgrade_840.py b/tests/storage/study_upgrader/test_upgrade_840.py index 4f95cef265..9cd39a3a49 100644 --- a/tests/storage/study_upgrader/test_upgrade_840.py +++ b/tests/storage/study_upgrader/test_upgrade_840.py @@ -1,5 +1,5 @@ from antarest.study.storage.rawstudy.ini_reader import IniReader -from antarest.study.storage.study_upgrader import upgrade_840 +from antarest.study.storage.study_upgrader import StudyUpgrader from tests.storage.study_upgrader.conftest import StudyAssets @@ -9,7 +9,8 @@ def test_nominal_case(study_assets: StudyAssets): """ # upgrade the study - upgrade_840(study_assets.study_dir) + study_upgrader = StudyUpgrader(study_assets.study_dir, "840") + study_upgrader.upgrade() # compare generaldata.ini actual_path = study_assets.study_dir.joinpath("settings/generaldata.ini") diff --git a/tests/storage/study_upgrader/test_upgrade_850.py b/tests/storage/study_upgrader/test_upgrade_850.py index 362cf1ab50..e40f00aa02 100644 --- a/tests/storage/study_upgrader/test_upgrade_850.py +++ b/tests/storage/study_upgrader/test_upgrade_850.py @@ -1,5 +1,5 @@ from antarest.study.storage.rawstudy.ini_reader import IniReader -from antarest.study.storage.study_upgrader import upgrade_850 +from antarest.study.storage.study_upgrader import StudyUpgrader from tests.storage.study_upgrader.conftest import StudyAssets @@ -10,7 +10,8 @@ def test_nominal_case(study_assets: StudyAssets): """ # upgrade the study - upgrade_850(study_assets.study_dir) + study_upgrader = StudyUpgrader(study_assets.study_dir, "850") + study_upgrader.upgrade() # compare generaldata.ini actual_path = study_assets.study_dir.joinpath("settings/generaldata.ini") diff --git a/tests/storage/study_upgrader/test_upgrade_860.py b/tests/storage/study_upgrader/test_upgrade_860.py index cd20d438cb..772589ee11 100644 --- a/tests/storage/study_upgrader/test_upgrade_860.py +++ b/tests/storage/study_upgrader/test_upgrade_860.py @@ -1,4 +1,4 @@ -from antarest.study.storage.study_upgrader import upgrade_860 +from antarest.study.storage.study_upgrader import StudyUpgrader from tests.storage.business.test_study_version_upgrader import are_same_dir from tests.storage.study_upgrader.conftest import StudyAssets @@ -9,7 +9,8 @@ def test_nominal_case(study_assets: StudyAssets): """ # upgrade the study - upgrade_860(study_assets.study_dir) + study_upgrader = StudyUpgrader(study_assets.study_dir, "860") + study_upgrader.upgrade() # compare input folder actual_input_path = study_assets.study_dir.joinpath("input") diff --git a/tests/storage/study_upgrader/test_upgrade_870.py b/tests/storage/study_upgrader/test_upgrade_870.py index f22a7a30ef..4024e02fee 100644 --- a/tests/storage/study_upgrader/test_upgrade_870.py +++ b/tests/storage/study_upgrader/test_upgrade_870.py @@ -1,4 +1,4 @@ -from antarest.study.storage.study_upgrader import upgrade_870 +from antarest.study.storage.study_upgrader import StudyUpgrader from tests.storage.business.test_study_version_upgrader import are_same_dir from tests.storage.study_upgrader.conftest import StudyAssets @@ -9,7 +9,8 @@ def test_nominal_case(study_assets: StudyAssets): """ # upgrade the study - upgrade_870(study_assets.study_dir) + study_upgrader = StudyUpgrader(study_assets.study_dir, "870") + study_upgrader.upgrade() # compare input folders (bindings + thermals) actual_input_path = study_assets.study_dir.joinpath("input") @@ -23,7 +24,8 @@ def test_empty_binding_constraints(study_assets: StudyAssets): """ # upgrade the study - upgrade_870(study_assets.study_dir) + study_upgrader = StudyUpgrader(study_assets.study_dir, "870") + study_upgrader.upgrade() # compare input folders (bindings + thermals) actual_input_path = study_assets.study_dir.joinpath("input") diff --git a/tests/storage/study_upgrader/test_upgrade_880.py b/tests/storage/study_upgrader/test_upgrade_880.py index 36dfa7dcc6..b0868daec2 100644 --- a/tests/storage/study_upgrader/test_upgrade_880.py +++ b/tests/storage/study_upgrader/test_upgrade_880.py @@ -1,4 +1,4 @@ -from antarest.study.storage.study_upgrader import upgrade_880 +from antarest.study.storage.study_upgrader import StudyUpgrader from tests.storage.business.test_study_version_upgrader import are_same_dir from tests.storage.study_upgrader.conftest import StudyAssets @@ -9,7 +9,8 @@ def test_nominal_case(study_assets: StudyAssets): """ # upgrade the study - upgrade_880(study_assets.study_dir) + study_upgrader = StudyUpgrader(study_assets.study_dir, "880") + study_upgrader.upgrade() # compare st-storage folders (st-storage) actual_input_path = study_assets.study_dir / "input" / "st-storage" diff --git a/tests/storage/test_service.py b/tests/storage/test_service.py index 891099dbcc..7ebf94a09e 100644 --- a/tests/storage/test_service.py +++ b/tests/storage/test_service.py @@ -1,7 +1,9 @@ import contextlib import os +import textwrap import typing as t import uuid +from configparser import MissingSectionHeaderError from datetime import datetime, timedelta, timezone from pathlib import Path from unittest.mock import ANY, Mock, call, patch, seal @@ -1728,7 +1730,7 @@ def test_task_upgrade_study(tmp_path: Path) -> None: @with_db_context -@patch("antarest.study.service.upgrade_study") +@patch("antarest.study.storage.study_upgrader.StudyUpgrader.upgrade") @pytest.mark.parametrize("workspace", ["other_workspace", DEFAULT_WORKSPACE_NAME]) def test_upgrade_study__raw_study__nominal( upgrade_study_mock: Mock, @@ -1740,7 +1742,17 @@ def test_upgrade_study__raw_study__nominal( target_version = "800" current_version = "720" (tmp_path / "study.antares").touch() - (tmp_path / "study.antares").write_text(f"version = {current_version}") + (tmp_path / "study.antares").write_text( + textwrap.dedent( + f""" + [antares] + version = {current_version} + caption = + created = 1682506382.235618 + lastsave = 1682506382.23562 + author = Unknown""" + ) + ) # Prepare a RAW study # noinspection PyArgumentList @@ -1796,8 +1808,7 @@ def test_upgrade_study__raw_study__nominal( notifier = Mock() actual = task(notifier) - # The `upgrade_study()` function must be called with the right parameters - upgrade_study_mock.assert_called_with(tmp_path, target_version) + upgrade_study_mock.assert_called_once_with() # The study must be updated in the database actual_study: RawStudy = db.session.query(Study).get(study_id) @@ -1823,7 +1834,7 @@ def test_upgrade_study__raw_study__nominal( @with_db_context -@patch("antarest.study.service.upgrade_study") +@patch("antarest.study.storage.study_upgrader.StudyUpgrader.upgrade") def test_upgrade_study__variant_study__nominal( upgrade_study_mock: Mock, tmp_path: Path, @@ -1912,14 +1923,14 @@ def test_upgrade_study__variant_study__nominal( @with_db_context -@patch("antarest.study.service.upgrade_study") -def test_upgrade_study__raw_study__failed(upgrade_study_mock: Mock, tmp_path: Path) -> None: +def test_upgrade_study__raw_study__failed(tmp_path: Path) -> None: study_id = str(uuid.uuid4()) study_name = "my_study" target_version = "800" old_version = "720" (tmp_path / "study.antares").touch() (tmp_path / "study.antares").write_text(f"version = {old_version}") + # The study.antares file doesn't have an header the upgrade should fail. # Prepare a RAW study # noinspection PyArgumentList @@ -1960,9 +1971,6 @@ def test_upgrade_study__raw_study__failed(upgrade_study_mock: Mock, tmp_path: Pa # An event of type `STUDY_EDITED` must be pushed when the upgrade is done. event_bus = Mock() - # The `upgrade_study()` function raise an exception - upgrade_study_mock.side_effect = Exception("INVALID_UPGRADE") - # Prepare the task for an upgrade task = StudyUpgraderTask( study_id, @@ -1976,7 +1984,7 @@ def test_upgrade_study__raw_study__failed(upgrade_study_mock: Mock, tmp_path: Pa # The task is called with a `TaskUpdateNotifier` a parameter. # Some messages could be emitted using the notifier (not a requirement). notifier = Mock() - with pytest.raises(Exception, match="INVALID_UPGRADE"): + with pytest.raises(MissingSectionHeaderError, match="File contains no section headers"): task(notifier) # The study must not be updated in the database diff --git a/tests/study/storage/test_study_version_upgrader.py b/tests/study/storage/test_study_version_upgrader.py deleted file mode 100644 index 97c9a479c6..0000000000 --- a/tests/study/storage/test_study_version_upgrader.py +++ /dev/null @@ -1,117 +0,0 @@ -from pathlib import Path - -import pytest - -from antarest.core.exceptions import StudyValidationError -from antarest.study.storage.study_upgrader import ( - UPGRADE_METHODS, - InvalidUpgrade, - can_upgrade_version, - find_next_version, - get_current_version, -) - - -class TestFindNextVersion: - @pytest.mark.parametrize( - "from_version, expected", - [ - (UPGRADE_METHODS[0].old, UPGRADE_METHODS[0].new), - (UPGRADE_METHODS[-1].old, UPGRADE_METHODS[-1].new), - (UPGRADE_METHODS[-1].new, ""), - ("3.14", ""), - ], - ) - def test_find_next_version(self, from_version: str, expected: str): - actual = find_next_version(from_version) - assert actual == expected - - -class TestCanUpgradeVersion: - @pytest.mark.parametrize( - "from_version, to_version", - [ - pytest.param("700", "710"), - pytest.param( - "123", - "123", - marks=pytest.mark.xfail( - reason="same versions", - raises=InvalidUpgrade, - strict=True, - ), - ), - pytest.param( - "000", - "123", - marks=pytest.mark.xfail( - reason="version '000' not in 'old' versions", - raises=InvalidUpgrade, - strict=True, - ), - ), - pytest.param( - "700", - "999", - marks=pytest.mark.xfail( - reason="version '999' not in 'new' versions", - raises=InvalidUpgrade, - strict=True, - ), - ), - pytest.param( - "720", - "710", - marks=pytest.mark.xfail( - reason="versions inverted", - raises=InvalidUpgrade, - strict=True, - ), - ), - pytest.param( - 800, - "456", - marks=pytest.mark.xfail( - reason="integer version 800 not in 'old' versions", - raises=InvalidUpgrade, - strict=True, - ), - ), - pytest.param( - "700", - 800, - marks=pytest.mark.xfail( - reason="integer version 800 not in 'new' versions", - raises=InvalidUpgrade, - strict=True, - ), - ), - ], - ) - def test_can_upgrade_version(self, from_version: str, to_version: str): - can_upgrade_version(from_version, to_version) - - -class TestGetCurrentVersion: - @pytest.mark.parametrize( - "version", - [ - pytest.param("710"), - pytest.param(" 71"), - pytest.param(" 7.1 "), - pytest.param( - "", - marks=pytest.mark.xfail( - reason="empty version", - raises=StudyValidationError, - strict=True, - ), - ), - ], - ) - def test_get_current_version(self, tmp_path: Path, version: str): - # prepare the "study.antares" file - study_antares_path = tmp_path.joinpath("study.antares") - study_antares_path.write_text(f"version = {version}", encoding="utf-8") - actual = get_current_version(tmp_path) - assert actual == version.strip(), f"{actual=}" diff --git a/tests/variantstudy/conftest.py b/tests/variantstudy/conftest.py index b08851b07b..5d10267bbd 100644 --- a/tests/variantstudy/conftest.py +++ b/tests/variantstudy/conftest.py @@ -1,4 +1,5 @@ import hashlib +import re import typing as t import zipfile from pathlib import Path @@ -8,7 +9,7 @@ import numpy.typing as npt import pytest -from antarest.study.storage.study_upgrader import get_current_version +from antarest.core.exceptions import StudyValidationError if t.TYPE_CHECKING: # noinspection PyPackageRequirements @@ -163,7 +164,7 @@ def empty_study_fixture(request: "SubRequest", tmp_path: Path, matrix_service: M zip_empty_study.extractall(empty_study_destination_path) # Detect the version of the study from `study.antares` file. - version = get_current_version(empty_study_destination_path) + version = _get_current_version(empty_study_destination_path) config = FileStudyTreeConfig( study_path=empty_study_destination_path, @@ -185,3 +186,16 @@ def empty_study_fixture(request: "SubRequest", tmp_path: Path, matrix_service: M ), ) return file_study + + +def _get_current_version(study_path: Path) -> str: + antares_path = study_path / "study.antares" + pattern = r"version\s*=\s*([\w.-]+)\s*" + with antares_path.open(encoding="utf-8") as lines: + for line in lines: + if match := re.fullmatch(pattern, line): + return match[1].rstrip() + raise StudyValidationError( + f"File parsing error: the version number is not found in '{antares_path}'" + f" or does not match the expected '{pattern}' format." + ) diff --git a/tests/variantstudy/model/command/test_create_st_storage.py b/tests/variantstudy/model/command/test_create_st_storage.py index c35ea37665..7e2a7fa7b5 100644 --- a/tests/variantstudy/model/command/test_create_st_storage.py +++ b/tests/variantstudy/model/command/test_create_st_storage.py @@ -7,7 +7,7 @@ from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import STStorageConfig from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy -from antarest.study.storage.study_upgrader import upgrade_study +from antarest.study.storage.study_upgrader import StudyUpgrader from antarest.study.storage.variantstudy.business.utils import strip_matrix_protocol from antarest.study.storage.variantstudy.model.command.common import CommandName from antarest.study.storage.variantstudy.model.command.create_area import CreateArea @@ -29,7 +29,7 @@ def recent_study_fixture(empty_study: FileStudy) -> FileStudy: Returns: FileStudy: The FileStudy object upgraded to the required version. """ - upgrade_study(empty_study.config.study_path, str(REQUIRED_VERSION)) + StudyUpgrader(empty_study.config.study_path, str(REQUIRED_VERSION)).upgrade() empty_study.config.version = REQUIRED_VERSION return empty_study diff --git a/tests/variantstudy/model/command/test_remove_st_storage.py b/tests/variantstudy/model/command/test_remove_st_storage.py index 963a6d128a..944f3b57b4 100644 --- a/tests/variantstudy/model/command/test_remove_st_storage.py +++ b/tests/variantstudy/model/command/test_remove_st_storage.py @@ -5,7 +5,7 @@ from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy -from antarest.study.storage.study_upgrader import upgrade_study +from antarest.study.storage.study_upgrader import StudyUpgrader from antarest.study.storage.variantstudy.model.command.common import CommandName from antarest.study.storage.variantstudy.model.command.create_area import CreateArea from antarest.study.storage.variantstudy.model.command.create_st_storage import CreateSTStorage @@ -25,7 +25,7 @@ def recent_study_fixture(empty_study: FileStudy) -> FileStudy: Returns: FileStudy: The FileStudy object upgraded to the required version. """ - upgrade_study(empty_study.config.study_path, str(REQUIRED_VERSION)) + StudyUpgrader(empty_study.config.study_path, str(REQUIRED_VERSION)).upgrade() empty_study.config.version = REQUIRED_VERSION return empty_study diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index 55a60ae4a8..f839e170eb 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -133,6 +133,7 @@ "form.field.minValue": "The minimum value is {{0}}", "form.field.maxValue": "The maximum value is {{0}}", "form.field.invalidNumber": "Invalid number", + "form.field.mustBeInteger": "The value must be an integer", "form.field.notAllowedValue": "Not allowed value", "form.field.specialChars": "Special characters allowed: {{0}}", "form.field.specialCharsNotAllowed": "Special characters are not allowed", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index cf34d3599e..2b8bcd3cbf 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -133,6 +133,7 @@ "form.field.minValue": "La valeur minimum est {{0}}", "form.field.maxValue": "La valeur maximum est {{0}}", "form.field.invalidNumber": "Nombre invalide", + "form.field.mustBeInteger": "La valeur doit être un nombre entier", "form.field.notAllowedValue": "Valeur non autorisée", "form.field.specialChars": "Caractères spéciaux autorisés: {{0}}", "form.field.specialCharsNotAllowed": "Les caractères spéciaux ne sont pas autorisés", diff --git a/webapp/src/components/App/Singlestudy/explore/Configuration/AdequacyPatch/Fields.tsx b/webapp/src/components/App/Singlestudy/explore/Configuration/AdequacyPatch/Fields.tsx index a5687c6a14..d60eb0089f 100644 --- a/webapp/src/components/App/Singlestudy/explore/Configuration/AdequacyPatch/Fields.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Configuration/AdequacyPatch/Fields.tsx @@ -8,6 +8,7 @@ import Fieldset from "../../../../../common/Fieldset"; import { useFormContextPlus } from "../../../../../common/Form"; import { AdequacyPatchFormFields, PRICE_TAKING_ORDER_OPTIONS } from "./utils"; import { StudyMetadata } from "../../../../../../common/types"; +import { validateNumber } from "../../../../../../utils/validationUtils"; function Fields() { const { t } = useTranslation(); @@ -81,10 +82,7 @@ function Fields() { name="thresholdInitiateCurtailmentSharingRule" control={control} rules={{ - min: { - value: 0, - message: t("form.field.minValue", { 0: 0 }), - }, + validate: validateNumber({ min: 0 }), }} /> diff --git a/webapp/src/utils/validationUtils.ts b/webapp/src/utils/validationUtils.ts index 86fb294d8b..e6d85c21ba 100644 --- a/webapp/src/utils/validationUtils.ts +++ b/webapp/src/utils/validationUtils.ts @@ -7,6 +7,7 @@ import { t } from "i18next"; interface NumberValidationOptions { min?: number; max?: number; + integer?: boolean; } interface StringValidationOptions { @@ -197,8 +198,15 @@ export function validateNumber( return t("form.field.invalidNumber", { value }); } - const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = - options; + const { + min = Number.MIN_SAFE_INTEGER, + max = Number.MAX_SAFE_INTEGER, + integer = false, + } = options; + + if (integer && !Number.isInteger(valueOrOpts)) { + return t("form.field.mustBeInteger"); + } if (value < min) { return t("form.field.minValue", { 0: min });