diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 5603c5ffa2..fa80c39145 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -43,7 +43,7 @@ jobs: with: node-version: ${{ matrix.node-version }} - name: Install dependencies - run: npm install + run: npm install --legacy-peer-deps working-directory: webapp - name: Build run: npm run build diff --git a/README.md b/README.md index bd17aeae67..09edd1a21c 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Install the front-end dependencies: ```shell script cd webapp -npm install +npm install --legacy-peer-deps cd .. ``` diff --git a/antarest/__init__.py b/antarest/__init__.py index 787e8d3721..ada981b5ca 100644 --- a/antarest/__init__.py +++ b/antarest/__init__.py @@ -7,9 +7,9 @@ # Standard project metadata -__version__ = "2.15.6" +__version__ = "2.16.0" __author__ = "RTE, Antares Web Team" -__date__ = "2023-11-24" +__date__ = "2023-11-30" # noinspection SpellCheckingInspection __credits__ = "(c) Réseau de Transport de l’Électricité (RTE)" diff --git a/antarest/core/exceptions.py b/antarest/core/exceptions.py index 878e1c7d6f..a666394d8b 100644 --- a/antarest/core/exceptions.py +++ b/antarest/core/exceptions.py @@ -278,3 +278,19 @@ def __init__(self) -> None: HTTPStatus.BAD_REQUEST, "You cannot scan the default internal workspace", ) + + +class ClusterNotFound(HTTPException): + def __init__(self, cluster_id: str) -> None: + super().__init__( + HTTPStatus.NOT_FOUND, + f"Cluster: '{cluster_id}' not found", + ) + + +class ClusterConfigNotFound(HTTPException): + def __init__(self, area_id: str) -> None: + super().__init__( + HTTPStatus.NOT_FOUND, + f"Cluster configuration for area: '{area_id}' not found", + ) diff --git a/antarest/core/model.py b/antarest/core/model.py index ce88cb5e60..4c8c0d5f0e 100644 --- a/antarest/core/model.py +++ b/antarest/core/model.py @@ -28,7 +28,6 @@ class StudyPermissionType(str, enum.Enum): READ = "READ" RUN = "RUN" WRITE = "WRITE" - DELETE = "DELETE" MANAGE_PERMISSIONS = "MANAGE_PERMISSIONS" diff --git a/antarest/core/permissions.py b/antarest/core/permissions.py index 36e17d9024..8ecba4d85f 100644 --- a/antarest/core/permissions.py +++ b/antarest/core/permissions.py @@ -30,10 +30,6 @@ "roles": [RoleType.ADMIN, RoleType.WRITER], "public_modes": [PublicMode.FULL, PublicMode.EDIT], }, - StudyPermissionType.DELETE: { - "roles": [RoleType.ADMIN], - "public_modes": [PublicMode.FULL], - }, StudyPermissionType.MANAGE_PERMISSIONS: { "roles": [RoleType.ADMIN], "public_modes": [], diff --git a/antarest/core/roles.py b/antarest/core/roles.py index 36f4ad616b..778ae44df2 100644 --- a/antarest/core/roles.py +++ b/antarest/core/roles.py @@ -1,11 +1,34 @@ import enum +import functools __all__ = ["RoleType"] +@functools.total_ordering class RoleType(enum.Enum): """ Role type privilege + + Usage: + + >>> from antarest.core.roles import RoleType + + >>> RoleType.ADMIN == RoleType.ADMIN + True + >>> RoleType.ADMIN == RoleType.WRITER + False + >>> RoleType.ADMIN > RoleType.WRITER + True + >>> RoleType.ADMIN >= RoleType.WRITER + True + >>> # noinspection PyTypeChecker + >>> RoleType.RUNNER > 10 + True + >>> # noinspection PyTypeChecker + >>> RoleType.READER > "foo" + Traceback (most recent call last): + ... + TypeError: '>' not supported between instances of 'RoleType' and 'str' """ ADMIN = 40 @@ -13,5 +36,14 @@ class RoleType(enum.Enum): RUNNER = 20 READER = 10 - def is_higher_or_equals(self, other: "RoleType") -> bool: - return int(self.value) >= int(other.value) + def __ge__(self, other: object) -> bool: + """ + Returns `True` if the current role has same or greater privilege than other role. + """ + + if isinstance(other, RoleType): + return self.value >= other.value + elif isinstance(other, int): + return self.value >= other + else: + return NotImplemented diff --git a/antarest/core/utils/utils.py b/antarest/core/utils/utils.py index 9154c023a3..0d21538b4a 100644 --- a/antarest/core/utils/utils.py +++ b/antarest/core/utils/utils.py @@ -1,17 +1,18 @@ +import glob import logging import os import shutil import tempfile import time -from glob import escape +import typing as t +import zipfile from pathlib import Path -from typing import IO, Any, Callable, List, Optional, Tuple, TypeVar -from zipfile import ZIP_DEFLATED, BadZipFile, ZipFile +import py7zr import redis from antarest.core.config import RedisConfig -from antarest.core.exceptions import BadZipBinary, ShouldNotHappenException +from antarest.core.exceptions import ShouldNotHappenException logger = logging.getLogger(__name__) @@ -24,7 +25,7 @@ class DTO: def __hash__(self) -> int: return hash(tuple(sorted(self.__dict__.items()))) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: t.Any) -> bool: return isinstance(other, type(self)) and self.__dict__ == other.__dict__ def __str__(self) -> str: @@ -38,27 +39,53 @@ def __repr__(self) -> str: def sanitize_uuid(uuid: str) -> str: - return str(escape(uuid)) + return str(glob.escape(uuid)) -def extract_zip(stream: IO[bytes], dst: Path) -> None: +class BadArchiveContent(Exception): + """ + Exception raised when the archive file is corrupted (or unknown). """ - Extract zip archive - Args: - stream: zip file - dst: destination path - Returns: + def __init__(self, message: str = "Unsupported archive format") -> None: + super().__init__(message) + +def extract_zip(stream: t.BinaryIO, target_dir: Path) -> None: """ - try: - with ZipFile(stream) as zip_output: - zip_output.extractall(path=dst) - except BadZipFile: - raise BadZipBinary("Only zip file are allowed.") + Extract a ZIP archive to a given destination. + + Args: + stream: The stream containing the archive. + target_dir: The directory where to extract the archive. + + Raises: + BadArchiveContent: If the archive is corrupted or in an unknown format. + """ + + # Read the first few bytes to identify the file format + file_format = stream.read(4) + stream.seek(0) + + if file_format[:4] == b"PK\x03\x04": + try: + with zipfile.ZipFile(stream) as zf: + zf.extractall(path=target_dir) + except zipfile.BadZipFile as error: + raise BadArchiveContent("Unsupported ZIP format") from error + + elif file_format[:2] == b"7z": + try: + with py7zr.SevenZipFile(stream, "r") as zf: + zf.extractall(target_dir) + except py7zr.exceptions.Bad7zFile as error: + raise BadArchiveContent("Unsupported 7z format") from error + + else: + raise BadArchiveContent -def get_default_config_path() -> Optional[Path]: +def get_default_config_path() -> t.Optional[Path]: config = Path("config.yaml") if config.exists(): return config @@ -98,17 +125,17 @@ def __init__(self) -> None: def reset_current(self) -> None: self.current_time = time.time() - def log_elapsed(self, logger: Callable[[float], None], since_start: bool = False) -> None: - logger(time.time() - (self.start_time if since_start else self.current_time)) + def log_elapsed(self, logger_: t.Callable[[float], None], since_start: bool = False) -> None: + logger_(time.time() - (self.start_time if since_start else self.current_time)) self.current_time = time.time() -T = TypeVar("T") +T = t.TypeVar("T") -def retry(func: Callable[[], T], attempts: int = 10, interval: float = 0.5) -> T: +def retry(func: t.Callable[[], T], attempts: int = 10, interval: float = 0.5) -> T: attempt = 0 - caught_exception: Optional[Exception] = None + caught_exception: t.Optional[Exception] = None while attempt < attempts: try: attempt += 1 @@ -120,12 +147,12 @@ def retry(func: Callable[[], T], attempts: int = 10, interval: float = 0.5) -> T raise caught_exception or ShouldNotHappenException() -def assert_this(b: Any) -> None: +def assert_this(b: t.Any) -> None: if not b: raise AssertionError -def concat_files(files: List[Path], target: Path) -> None: +def concat_files(files: t.List[Path], target: Path) -> None: with open(target, "w") as fh: for item in files: with open(item, "r") as infile: @@ -133,7 +160,7 @@ def concat_files(files: List[Path], target: Path) -> None: fh.write(line) -def concat_files_to_str(files: List[Path]) -> str: +def concat_files_to_str(files: t.List[Path]) -> str: concat_str = "" for item in files: with open(item, "r") as infile: @@ -143,7 +170,7 @@ def concat_files_to_str(files: List[Path]) -> str: def zip_dir(dir_path: Path, zip_path: Path, remove_source_dir: bool = False) -> None: - with ZipFile(zip_path, mode="w", compression=ZIP_DEFLATED, compresslevel=2) as zipf: + with zipfile.ZipFile(zip_path, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=2) as zipf: len_dir_path = len(str(dir_path)) for root, _, files in os.walk(dir_path): for file in files: @@ -154,7 +181,7 @@ def zip_dir(dir_path: Path, zip_path: Path, remove_source_dir: bool = False) -> def unzip(dir_path: Path, zip_path: Path, remove_source_zip: bool = False) -> None: - with ZipFile(zip_path, mode="r") as zipf: + with zipfile.ZipFile(zip_path, mode="r") as zipf: zipf.extractall(dir_path) if remove_source_zip: zip_path.unlink() @@ -164,11 +191,11 @@ def is_zip(path: Path) -> bool: return path.name.endswith(".zip") -def extract_file_to_tmp_dir(zip_path: Path, inside_zip_path: Path) -> Tuple[Path, Any]: +def extract_file_to_tmp_dir(zip_path: Path, inside_zip_path: Path) -> t.Tuple[Path, t.Any]: str_inside_zip_path = str(inside_zip_path).replace("\\", "/") tmp_dir = tempfile.TemporaryDirectory() try: - with ZipFile(zip_path) as zip_obj: + with zipfile.ZipFile(zip_path) as zip_obj: zip_obj.extract(str_inside_zip_path, tmp_dir.name) except Exception as e: logger.warning( @@ -184,7 +211,7 @@ def extract_file_to_tmp_dir(zip_path: Path, inside_zip_path: Path) -> Tuple[Path def read_in_zip( zip_path: Path, inside_zip_path: Path, - read: Callable[[Optional[Path]], None], + read: t.Callable[[t.Optional[Path]], None], ) -> None: tmp_dir = None try: @@ -199,11 +226,11 @@ def read_in_zip( def suppress_exception( - callback: Callable[[], T], - logger: Callable[[Exception], None], -) -> Optional[T]: + callback: t.Callable[[], T], + logger_: t.Callable[[Exception], None], +) -> t.Optional[T]: try: return callback() except Exception as e: - logger(e) + logger_(e) return None diff --git a/antarest/launcher/model.py b/antarest/launcher/model.py index cc283c57d4..6fb40a98df 100644 --- a/antarest/launcher/model.py +++ b/antarest/launcher/model.py @@ -1,17 +1,17 @@ import enum +import typing as t from datetime import datetime -from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, Sequence, String # type: ignore from sqlalchemy.orm import relationship # type: ignore from antarest.core.persistence import Base -from antarest.login.model import Identity +from antarest.login.model import Identity, UserInfo class XpansionParametersDTO(BaseModel): - output_id: Optional[str] + output_id: t.Optional[str] sensitivity_mode: bool = False enabled: bool = True @@ -20,16 +20,16 @@ class LauncherParametersDTO(BaseModel): # Warning ! This class must be retro-compatible (that's the reason for the weird bool/XpansionParametersDTO union) # The reason is that it's stored in json format in database and deserialized using the latest class version # If compatibility is to be broken, an (alembic) data migration script should be added - adequacy_patch: Optional[Dict[str, Any]] = None - nb_cpu: Optional[int] = None + adequacy_patch: t.Optional[t.Dict[str, t.Any]] = None + nb_cpu: t.Optional[int] = None post_processing: bool = False - time_limit: Optional[int] = None # 3600 ≤ time_limit < 864000 (10 days) - xpansion: Union[XpansionParametersDTO, bool, None] = None + time_limit: t.Optional[int] = None # 3600 ≤ time_limit < 864000 (10 days) + xpansion: t.Union[XpansionParametersDTO, bool, None] = None xpansion_r_version: bool = False archive_output: bool = True auto_unzip: bool = True - output_suffix: Optional[str] = None - other_options: Optional[str] = None + output_suffix: t.Optional[str] = None + other_options: t.Optional[str] = None # add extensions field here @@ -38,7 +38,7 @@ class LogType(str, enum.Enum): STDERR = "STDERR" @staticmethod - def from_filename(filename: str) -> Optional["LogType"]: + def from_filename(filename: str) -> t.Optional["LogType"]: if filename == "antares-err.log": return LogType.STDERR elif filename == "antares-out.log": @@ -83,27 +83,45 @@ class JobResultDTO(BaseModel): - exit_code: The exit code associated with the task. - solver_stats: Global statistics related to the simulation, including processing time, call count, optimization issues, and study-specific statistics (INI file-like format). - - owner_id: The unique identifier of the user or bot that executed the task. + - owner: The user or bot that executed the task or `None` if unknown. """ id: str study_id: str - launcher: Optional[str] - launcher_params: Optional[str] + launcher: t.Optional[str] + launcher_params: t.Optional[str] status: JobStatus creation_date: str - completion_date: Optional[str] - msg: Optional[str] - output_id: Optional[str] - exit_code: Optional[int] - solver_stats: Optional[str] - owner_id: Optional[int] + completion_date: t.Optional[str] + msg: t.Optional[str] + output_id: t.Optional[str] + exit_code: t.Optional[int] + solver_stats: t.Optional[str] + owner: t.Optional[UserInfo] + + class Config: + @staticmethod + def schema_extra(schema: t.MutableMapping[str, t.Any]) -> None: + schema["example"] = JobResultDTO( + id="b2a9f6a7-7f8f-4f7a-9a8b-1f9b4c5d6e7f", + study_id="b2a9f6a7-7f8f-4f7a-9a8b-1f9b4c5d6e7f", + launcher="slurm", + launcher_params='{"nb_cpu": 4, "time_limit": 3600}', + status=JobStatus.SUCCESS, + creation_date="2023-11-25 12:00:00", + completion_date="2023-11-25 12:27:31", + msg="Study successfully executed", + output_id="20231125-1227eco", + exit_code=0, + solver_stats="time: 1651s, call_count: 1, optimization_issues: []", + owner=UserInfo(id=0o007, name="James BOND"), + ) class JobLog(Base): # type: ignore __tablename__ = "launcherjoblog" - id: str = Column(Integer(), Sequence("launcherjoblog_id_sequence"), primary_key=True) + id: int = Column(Integer(), Sequence("launcherjoblog_id_sequence"), primary_key=True) message: str = Column(String, nullable=False) job_id: str = Column( String(), @@ -132,16 +150,21 @@ class JobResult(Base): # type: ignore id: str = Column(String(36), primary_key=True) study_id: str = Column(String(36)) - launcher: Optional[str] = Column(String) - launcher_params: Optional[str] = Column(String, nullable=True) + launcher: t.Optional[str] = Column(String) + launcher_params: t.Optional[str] = Column(String, nullable=True) job_status: JobStatus = Column(Enum(JobStatus)) creation_date = Column(DateTime, default=datetime.utcnow) completion_date = Column(DateTime) - msg: Optional[str] = Column(String()) - output_id: Optional[str] = Column(String()) - exit_code: Optional[int] = Column(Integer) - solver_stats: Optional[str] = Column(String(), nullable=True) - owner_id: Optional[int] = Column(Integer(), ForeignKey(Identity.id, ondelete="SET NULL"), nullable=True) + msg: t.Optional[str] = Column(String()) + output_id: t.Optional[str] = Column(String()) + exit_code: t.Optional[int] = Column(Integer) + solver_stats: t.Optional[str] = Column(String(), nullable=True) + owner_id: t.Optional[int] = Column(Integer(), ForeignKey(Identity.id, ondelete="SET NULL"), nullable=True) + + # Define a many-to-one relationship between `JobResult` and `Identity`. + # This relationship is required to display the owner of a job result in the UI. + # If the owner is deleted, the job result is detached from the owner (but not deleted). + owner: t.Optional[Identity] = relationship(Identity, back_populates="job_results", uselist=False) logs = relationship(JobLog, uselist=True, cascade="all, delete, delete-orphan") @@ -158,7 +181,7 @@ def to_dto(self) -> JobResultDTO: output_id=self.output_id, exit_code=self.exit_code, solver_stats=self.solver_stats, - owner_id=self.owner_id, + owner=self.owner.to_dto() if self.owner else None, ) # SQLAlchemy provides its own way to handle object comparison, which ensures @@ -190,4 +213,4 @@ class JobCreationDTO(BaseModel): class LauncherEnginesDTO(BaseModel): - engines: List[str] + engines: t.List[str] diff --git a/antarest/launcher/service.py b/antarest/launcher/service.py index 514cc54115..2aa2d73802 100644 --- a/antarest/launcher/service.py +++ b/antarest/launcher/service.py @@ -17,7 +17,7 @@ from antarest.core.filetransfer.service import FileTransferManager from antarest.core.interfaces.cache import ICache from antarest.core.interfaces.eventbus import Event, EventChannelDirectory, EventType, IEventBus -from antarest.core.jwt import DEFAULT_ADMIN_USER, JWTUser +from antarest.core.jwt import DEFAULT_ADMIN_USER, JWTGroup, JWTUser from antarest.core.model import PermissionInfo, PublicMode, StudyPermissionType from antarest.core.requests import RequestParameters, UserHasNotPermissionError from antarest.core.tasks.model import TaskResult, TaskType @@ -485,6 +485,14 @@ def _import_output( if not job_result: raise JobNotFound() + # Search for the user who launched the job in the database. + if owner_id := job_result.owner_id: + roles = self.study_service.user_service.roles.get_all_by_user(owner_id) + groups = [JWTGroup(id=role.group_id, name=role.group.name, role=role.type) for role in roles] + launching_user = JWTUser(id=owner_id, impersonator=owner_id, type="users", groups=groups) + else: + launching_user = DEFAULT_ADMIN_USER + study_id = job_result.study_id job_launch_params = LauncherParametersDTO.parse_raw(job_result.launcher_params or "{}") @@ -541,7 +549,7 @@ def _import_output( return self.study_service.import_output( study_id, final_output_path, - RequestParameters(DEFAULT_ADMIN_USER), + RequestParameters(launching_user), output_suffix, job_launch_params.auto_unzip, ) diff --git a/antarest/login/model.py b/antarest/login/model.py index 85c2a0d5e3..52106685bc 100644 --- a/antarest/login/model.py +++ b/antarest/login/model.py @@ -1,6 +1,5 @@ +import typing as t import uuid -from dataclasses import dataclass -from typing import Any, List, Optional import bcrypt from pydantic.main import BaseModel @@ -11,6 +10,10 @@ from antarest.core.persistence import Base from antarest.core.roles import RoleType +if t.TYPE_CHECKING: + # avoid circular import + from antarest.launcher.model import JobResult + class UserInfo(BaseModel): id: int @@ -24,7 +27,7 @@ class BotRoleCreateDTO(BaseModel): class BotCreateDTO(BaseModel): name: str - roles: List[BotRoleCreateDTO] + roles: t.List[BotRoleCreateDTO] is_author: bool = True @@ -34,7 +37,7 @@ class UserCreateDTO(BaseModel): class GroupDTO(BaseModel): - id: Optional[str] = None + id: t.Optional[str] = None name: str @@ -45,7 +48,7 @@ class RoleCreationDTO(BaseModel): class RoleDTO(BaseModel): - group_id: Optional[str] + group_id: t.Optional[str] group_name: str identity_id: int type: RoleType @@ -54,7 +57,7 @@ class RoleDTO(BaseModel): class IdentityDTO(BaseModel): id: int name: str - roles: List[RoleDTO] + roles: t.List[RoleDTO] class RoleDetailDTO(BaseModel): @@ -67,7 +70,7 @@ class BotIdentityDTO(BaseModel): id: int name: str isAuthor: bool - roles: List[RoleDTO] + roles: t.List[RoleDTO] class BotDTO(UserInfo): @@ -82,7 +85,7 @@ class UserRoleDTO(BaseModel): class GroupDetailDTO(GroupDTO): - users: List[UserRoleDTO] + users: t.List[UserRoleDTO] class Password: @@ -106,7 +109,6 @@ def __repr__(self) -> str: return self.__str__() -@dataclass class Identity(Base): # type: ignore """ Abstract entity which represent generic user @@ -118,6 +120,10 @@ class Identity(Base): # type: ignore name = Column(String(255)) type = Column(String(50)) + # Define a one-to-many relationship with `JobResult`. + # If an identity is deleted, all the associated job results are detached from the identity. + job_results: t.List["JobResult"] = relationship("JobResult", back_populates="owner", cascade="save-update, merge") + def to_dto(self) -> UserInfo: return UserInfo(id=self.id, name=self.name) @@ -130,10 +136,9 @@ def get_impersonator(self) -> int: return int(self.id) -@dataclass class User(Identity): """ - Basic user, hosted in this plateform and using UI + Basic user, hosted in this platform and using UI """ __tablename__ = "users" @@ -162,13 +167,10 @@ def password(self, pwd: Password) -> None: def from_dto(data: UserInfo) -> "User": return User(id=data.id, name=data.name) - def __eq__(self, o: Any) -> bool: - if not isinstance(o, User): - return False - return bool((o.id == self.id) and (o.name == self.name)) + # Implementing a `__eq__` method is superfluous, since the default implementation + # is to compare the identity of the objects using the primary key. -@dataclass class UserLdap(Identity): """ User using UI but hosted on LDAP server @@ -189,16 +191,13 @@ class UserLdap(Identity): "polymorphic_identity": "users_ldap", } - def __eq__(self, o: Any) -> bool: - if not isinstance(o, UserLdap): - return False - return bool((o.id == self.id) and (o.name == self.name)) + # Implementing a `__eq__` method is superfluous, since the default implementation + # is to compare the identity of the objects using the primary key. -@dataclass class Bot(Identity): """ - User hosted in this platform but using ony API (belongs to an user) + User hosted in this platform but using ony API (belongs to a user) """ __tablename__ = "bots" @@ -209,6 +208,7 @@ class Bot(Identity): ForeignKey("identities.id"), primary_key=True, ) + # noinspection SpellCheckingInspection owner = Column(Integer, ForeignKey("identities.id", name="bots_owner_fkey")) is_author = Column(Boolean(), default=True) @@ -228,13 +228,10 @@ def to_dto(self) -> BotDTO: is_author=self.is_author, ) - def __eq__(self, other: Any) -> bool: - if not isinstance(other, Bot): - return False - return self.to_dto().dict() == other.to_dto().dict() + # Implementing a `__eq__` method is superfluous, since the default implementation + # is to compare the identity of the objects using the primary key. -@dataclass class Group(Base): # type: ignore """ Group of users @@ -253,17 +250,13 @@ class Group(Base): # type: ignore def to_dto(self) -> GroupDTO: return GroupDTO(id=self.id, name=self.name) - def __eq__(self, other: Any) -> bool: - if not isinstance(other, Group): - return False - - return bool(self.id == other.id and self.name == other.name) + # Implementing a `__eq__` method is superfluous, since the default implementation + # is to compare the identity of the objects using the primary key. def __repr__(self) -> str: return f"Group(id={self.id}, name={self.name})" -@dataclass class Role(Base): # type: ignore """ Enable to link a user to a group with a specific role permission diff --git a/antarest/login/repository.py b/antarest/login/repository.py index 3d9cb80fc9..4f68e1924c 100644 --- a/antarest/login/repository.py +++ b/antarest/login/repository.py @@ -2,6 +2,7 @@ from typing import List, Optional from sqlalchemy import exists # type: ignore +from sqlalchemy.orm import joinedload # type: ignore from antarest.core.config import Config from antarest.core.jwt import ADMIN_ID @@ -88,7 +89,7 @@ def get(self, id: int) -> Optional[User]: user: User = db.session.query(User).get(id) return user - def get_by_name(self, name: str) -> User: + def get_by_name(self, name: str) -> Optional[User]: user: User = db.session.query(User).filter_by(name=name).first() return user @@ -218,8 +219,20 @@ def get(self, user: int, group: str) -> Optional[Role]: role: Role = db.session.query(Role).get((user, group)) return role - def get_all_by_user(self, user: int) -> List[Role]: - roles: List[Role] = db.session.query(Role).filter_by(identity_id=user).all() + def get_all_by_user(self, /, user_id: int) -> List[Role]: + """ + Get all roles (and groups) associated to a user. + + Args: + user_id: The user identifier. + + Returns: + A list of `Role` objects. + """ + # When we fetch the list of roles, we also need to fetch the associated groups. + # We use a SQL query with joins to fetch all these data efficiently. + stm = db.session.query(Role).options(joinedload(Role.group)).filter_by(identity_id=user_id) + roles: List[Role] = stm.all() return roles def get_all_by_group(self, group: str) -> List[Role]: diff --git a/antarest/login/service.py b/antarest/login/service.py index 37c2d9a749..55af0ab12c 100644 --- a/antarest/login/service.py +++ b/antarest/login/service.py @@ -150,8 +150,7 @@ def save_bot(self, bot: BotCreateDTO, params: RequestParameters) -> Bot: if not params.user.is_site_admin(): for role_create in bot.roles: role = self.roles.get(params.user.id, role_create.group) - role_type = RoleType(role_create.role) - if not (role and role.type.is_higher_or_equals(role_type)): + if not role or role.type is None or role.type < role_create.role: raise UserHasNotPermissionError() if not bot.name.strip(): @@ -241,15 +240,17 @@ def get_group(self, id: str, params: RequestParameters) -> Optional[Group]: Returns: group asked """ + if params.user is None: + user_id = params.get_user_id() + err_msg = f"user {user_id} has not permission to get group" + logger.error(err_msg) + raise UserHasNotPermissionError(err_msg) + group = self.groups.get(id) - if ( - group - and params.user - and any( - ( - params.user.is_site_admin(), - id in [group.id for group in params.user.groups], - ) + if group is not None and any( + ( + params.user.is_site_admin(), + id in [group.id for group in params.user.groups], ) ): return group @@ -320,15 +321,14 @@ def get_user(self, id: int, params: RequestParameters) -> Optional[User]: def get_identity(self, id: int, include_token: bool = False) -> Optional[Identity]: """ - Get user - Permission: SADMIN, GADMIN (own group), USER (own user) + Get user, LDAP user or bot. Args: - id: user id - params: request parameters - - Returns: user + id: ID of the user to fetch + include_token: whether to include the bots or not. + Returns: + The user, LDAP user or bot if found, `None` otherwise. """ user = self.ldap.get(id) or self.users.get(id) if include_token: @@ -337,14 +337,14 @@ def get_identity(self, id: int, include_token: bool = False) -> Optional[Identit def get_user_info(self, id: int, params: RequestParameters) -> Optional[IdentityDTO]: """ - Get user informations + Get user information Permission: SADMIN, GADMIN (own group), USER (own user) Args: id: user id params: request parameters - Returns: user informations and roles + Returns: user information and roles """ user = self.get_user(id, params) @@ -472,7 +472,7 @@ def authenticate(self, name: str, pwd: str) -> Optional[JWTUser]: Returns: jwt data with user information if auth success, None else. """ - intern = self.users.get_by_name(name) + intern: Optional[User] = self.users.get_by_name(name) if intern and intern.password.check(pwd): # type: ignore logger.info("successful login from intern user %s", name) return self.get_jwt(intern.id) @@ -510,9 +510,7 @@ def get_jwt(self, user_id: int) -> Optional[JWTUser]: logger.error("Can't claim JWT for user=%d", user_id) return None - def get_all_groups( - self, params: RequestParameters, details: Optional[bool] = False - ) -> List[Union[GroupDetailDTO, GroupDTO]]: + def get_all_groups(self, params: RequestParameters, details: bool = False) -> List[Union[GroupDetailDTO, GroupDTO]]: """ Get all groups. Permission: SADMIN @@ -529,7 +527,7 @@ def get_all_groups( if params.user.is_site_admin(): group_list = self.groups.get_all() else: - roles_by_user = self.roles.get_all_by_user(user=params.user.id) + roles_by_user = self.roles.get_all_by_user(params.user.id) for role in roles_by_user: if not details or role.type == RoleType.ADMIN: @@ -730,7 +728,7 @@ def delete_bot(self, id: int, params: RequestParameters) -> None: ) ): logger.info("bot %d deleted by user %s", id, params.get_user_id()) - for role in self.roles.get_all_by_user(user=id): + for role in self.roles.get_all_by_user(id): self.roles.delete(user=role.identity_id, group=role.group_id) return self.bots.delete(id) else: diff --git a/antarest/login/web.py b/antarest/login/web.py index ab63ec16b2..801325df7c 100644 --- a/antarest/login/web.py +++ b/antarest/login/web.py @@ -42,13 +42,13 @@ class UserCredentials(BaseModel): def create_login_api(service: LoginService, config: Config) -> APIRouter: """ Endpoints login implementation + Args: service: login facade service config: server config - jwt: jwt manager Returns: - + login endpoints """ bp = APIRouter(prefix="/v1") @@ -107,7 +107,7 @@ def refresh(jwt_manager: AuthJWT = Depends()) -> Any: response_model=List[Union[IdentityDTO, UserInfo]], ) def users_get_all( - details: Optional[bool] = False, + details: bool = False, current_user: JWTUser = Depends(auth.get_current_user), ) -> Any: logger.info("Fetching users list", extra={"user": current_user.id}) @@ -121,7 +121,7 @@ def users_get_all( ) def users_get_id( id: int, - details: Optional[bool] = False, + details: bool = False, current_user: JWTUser = Depends(auth.get_current_user), ) -> Any: logger.info(f"Fetching user info for {id}", extra={"user": current_user.id}) @@ -185,7 +185,7 @@ def roles_delete_by_user(id: int, current_user: JWTUser = Depends(auth.get_curre response_model=List[Union[GroupDetailDTO, GroupDTO]], ) def groups_get_all( - details: Optional[bool] = False, + details: bool = False, current_user: JWTUser = Depends(auth.get_current_user), ) -> Any: logger.info("Fetching groups list", extra={"user": current_user.id}) @@ -199,7 +199,7 @@ def groups_get_all( ) def groups_get_id( id: str, - details: Optional[bool] = False, + details: bool = False, current_user: JWTUser = Depends(auth.get_current_user), ) -> Any: logger.info(f"Fetching group {id} info", extra={"user": current_user.id}) diff --git a/antarest/matrixstore/service.py b/antarest/matrixstore/service.py index fbc5e0d8c3..4869ed11fa 100644 --- a/antarest/matrixstore/service.py +++ b/antarest/matrixstore/service.py @@ -10,6 +10,7 @@ from typing import List, Optional, Sequence, Tuple, Union import numpy as np +import py7zr from fastapi import UploadFile from numpy import typing as npt @@ -189,12 +190,22 @@ def create_by_importation(self, file: UploadFile, is_json: bool = False) -> List with contextlib.closing(f): buffer = io.BytesIO(f.read()) matrix_info: List[MatrixInfoDTO] = [] - with zipfile.ZipFile(buffer) as zf: - for info in zf.infolist(): - if info.is_dir() or info.filename in EXCLUDED_FILES: - continue - matrix_id = self._file_importation(zf.read(info.filename), is_json=is_json) - matrix_info.append(MatrixInfoDTO(id=matrix_id, name=info.filename)) + if file.filename.endswith("zip"): + with zipfile.ZipFile(buffer) as zf: + for info in zf.infolist(): + if info.is_dir() or info.filename in EXCLUDED_FILES: + continue + matrix_id = self._file_importation(zf.read(info.filename), is_json=is_json) + matrix_info.append(MatrixInfoDTO(id=matrix_id, name=info.filename)) + else: + with py7zr.SevenZipFile(buffer, "r") as szf: + for info in szf.list(): + if info.is_directory or info.filename in EXCLUDED_FILES: # type:ignore + continue + file_content = next(iter(szf.read(info.filename).values())) + matrix_id = self._file_importation(file_content.read(), is_json=is_json) + matrix_info.append(MatrixInfoDTO(id=matrix_id, name=info.filename)) + szf.reset() return matrix_info else: matrix_id = self._file_importation(f.read(), is_json=is_json) diff --git a/antarest/study/business/areas/renewable_management.py b/antarest/study/business/areas/renewable_management.py index 39eb204037..88870772e5 100644 --- a/antarest/study/business/areas/renewable_management.py +++ b/antarest/study/business/areas/renewable_management.py @@ -1,103 +1,271 @@ -from pathlib import PurePosixPath -from typing import Any, Dict, List, Optional +import json +import typing as t -from pydantic import Field +from pydantic import validator +from antarest.core.exceptions import ClusterConfigNotFound, ClusterNotFound from antarest.study.business.enum_ignore_case import EnumIgnoreCase -from antarest.study.business.utils import FieldInfo, FormFieldsBaseModel, execute_or_add_commands +from antarest.study.business.utils import AllOptionalMetaclass, camel_case_model, execute_or_add_commands from antarest.study.model import Study +from antarest.study.storage.rawstudy.model.filesystem.config.renewable import ( + RenewableConfig, + RenewableConfigType, + RenewableProperties, + create_renewable_config, +) +from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.storage_service import StudyStorageService +from antarest.study.storage.variantstudy.model.command.create_renewables_cluster import CreateRenewablesCluster +from antarest.study.storage.variantstudy.model.command.remove_renewables_cluster import RemoveRenewablesCluster from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig +__all__ = ( + "RenewableClusterInput", + "RenewableClusterCreation", + "RenewableClusterOutput", + "RenewableManager", +) + +_CLUSTER_PATH = "input/renewables/clusters/{area_id}/list/{cluster_id}" +_CLUSTERS_PATH = "input/renewables/clusters/{area_id}/list" + class TimeSeriesInterpretation(EnumIgnoreCase): POWER_GENERATION = "power-generation" PRODUCTION_FACTOR = "production-factor" -RENEWABLE_PATH = "input/renewables/clusters/{area}/list/{cluster}" +@camel_case_model +class RenewableClusterInput(RenewableProperties, metaclass=AllOptionalMetaclass): + """ + Model representing the data structure required to edit an existing renewable cluster. + """ + + class Config: + @staticmethod + def schema_extra(schema: t.MutableMapping[str, t.Any]) -> None: + schema["example"] = RenewableClusterInput( + group="Gas", + name="2 avail and must 1", + enabled=False, + unitCount=100, + nominalCapacity=1000.0, + tsInterpretation="power-generation", + ) + +class RenewableClusterCreation(RenewableClusterInput): + """ + Model representing the data structure required to create a new Renewable cluster within a study. + """ + + # noinspection Pydantic + @validator("name", pre=True) + def validate_name(cls, name: t.Optional[str]) -> str: + """ + Validator to check if the name is not empty. + """ + if not name: + raise ValueError("name must not be empty") + return name -class RenewableFormFields(FormFieldsBaseModel): + def to_config(self, study_version: t.Union[str, int]) -> RenewableConfigType: + values = self.dict(by_alias=False, exclude_none=True) + return create_renewable_config(study_version=study_version, **values) + + +@camel_case_model +class RenewableClusterOutput(RenewableConfig, metaclass=AllOptionalMetaclass): """ - Pydantic model representing renewable cluster configuration form fields. + Model representing the output data structure to display the details of a renewable cluster. """ - group: Optional[str] - name: Optional[str] - ts_interpretation: Optional[TimeSeriesInterpretation] - unit_count: Optional[int] = Field(description="Unit count", ge=1) - enabled: Optional[bool] = Field(description="Enable flag") - nominal_capacity: Optional[float] = Field(description="Nominal capacity (MW)", ge=0) - - -FIELDS_INFO: Dict[str, FieldInfo] = { - "group": { - "path": f"{RENEWABLE_PATH}/group", - "default_value": "", - }, - "name": { - "path": f"{RENEWABLE_PATH}/name", - "default_value": "", - }, - "ts_interpretation": { - "path": f"{RENEWABLE_PATH}/ts-interpretation", - "default_value": TimeSeriesInterpretation.POWER_GENERATION.value, - }, - "unit_count": { - "path": f"{RENEWABLE_PATH}/unitcount", - "default_value": 1, - }, - "enabled": { - "path": f"{RENEWABLE_PATH}/enabled", - "default_value": True, - }, - "nominal_capacity": { - "path": f"{RENEWABLE_PATH}/nominalcapacity", - "default_value": 0, - }, -} - - -def format_path(path: str, area_id: str, cluster_id: str) -> str: - return path.format(area=area_id, cluster=cluster_id) + class Config: + @staticmethod + def schema_extra(schema: t.MutableMapping[str, t.Any]) -> None: + schema["example"] = RenewableClusterOutput( + id="2 avail and must 1", + group="Gas", + name="2 avail and must 1", + enabled=False, + unitCount=100, + nominalCapacity=1000.0, + tsInterpretation="power-generation", + ) + + +def create_renewable_output( + study_version: t.Union[str, int], + cluster_id: str, + config: t.Mapping[str, t.Any], +) -> "RenewableClusterOutput": + obj = create_renewable_config(study_version=study_version, **config, id=cluster_id) + kwargs = obj.dict(by_alias=False) + return RenewableClusterOutput(**kwargs) class RenewableManager: + """ + A manager class responsible for handling operations related to renewable clusters within a study. + + Attributes: + storage_service (StudyStorageService): A service responsible for study data storage and retrieval. + """ + def __init__(self, storage_service: StudyStorageService): self.storage_service = storage_service - def get_field_values(self, study: Study, area_id: str, cluster_id: str) -> RenewableFormFields: + def _get_file_study(self, study: Study) -> FileStudy: + """ + Helper function to get raw study data. + """ + return self.storage_service.get_storage(study).get_raw(study) + + def get_clusters(self, study: Study, area_id: str) -> t.Sequence[RenewableClusterOutput]: + """ + Fetches all clusters related to a specific area in a study. + + Returns: + List of cluster output for all clusters. + + Raises: + ClusterConfigNotFound: If the clusters configuration for the specified area is not found. + """ + file_study = self._get_file_study(study) + path = _CLUSTERS_PATH.format(area_id=area_id) + + try: + clusters = file_study.tree.get(path.split("/"), depth=3) + except KeyError: + raise ClusterConfigNotFound(area_id) + + return [create_renewable_output(study.version, cluster_id, cluster) for cluster_id, cluster in clusters.items()] + + def create_cluster( + self, study: Study, area_id: str, cluster_data: RenewableClusterCreation + ) -> RenewableClusterOutput: + """ + Creates a new cluster within an area in the study. + + Args: + study: The study to search within. + area_id: The identifier of the area. + cluster_data: The data used to create the cluster configuration. + + Returns: + The newly created cluster. + """ + file_study = self._get_file_study(study) + study_version = study.version + cluster = cluster_data.to_config(study_version) + + command = CreateRenewablesCluster( + area_id=area_id, + cluster_name=cluster.id, + parameters=cluster.dict(by_alias=True, exclude={"id"}), + command_context=self.storage_service.variant_study_service.command_factory.command_context, + ) + execute_or_add_commands( + study, + file_study, + [command], + self.storage_service, + ) + + return self.get_cluster(study, area_id, cluster.id) + + def get_cluster(self, study: Study, area_id: str, cluster_id: str) -> RenewableClusterOutput: + """ + Retrieves a single cluster's data for a specific area in a study. + + Args: + study: The study to search within. + area_id: The identifier of the area. + cluster_id: The identifier of the cluster to retrieve. + + Returns: + The cluster output representation. + + Raises: + ClusterNotFound: If the specified cluster is not found within the area. + """ + file_study = self._get_file_study(study) + path = _CLUSTER_PATH.format(area_id=area_id, cluster_id=cluster_id) + try: + cluster = file_study.tree.get(path.split("/"), depth=1) + except KeyError: + raise ClusterNotFound(cluster_id) + return create_renewable_output(study.version, cluster_id, cluster) + + def update_cluster( + self, study: Study, area_id: str, cluster_id: str, cluster_data: RenewableClusterInput + ) -> RenewableClusterOutput: + """ + Updates the configuration of an existing cluster within an area in the study. + + Args: + study: The study where the cluster exists. + area_id: The identifier of the area where the cluster is located. + cluster_id: The identifier of the cluster to be updated. + cluster_data: The new data for updating the cluster configuration. + + Returns: + The updated cluster configuration. + + Raises: + ClusterNotFound: If the cluster to update is not found. + """ + + study_version = study.version + file_study = self._get_file_study(study) + path = _CLUSTER_PATH.format(area_id=area_id, cluster_id=cluster_id) + + try: + values = file_study.tree.get(path.split("/"), depth=1) + except KeyError: + raise ClusterNotFound(cluster_id) from None + + # merge old and new values + updated_values = { + **create_renewable_config(study_version, **values).dict(exclude={"id"}), + **cluster_data.dict(by_alias=False, exclude_none=True), + "id": cluster_id, + } + new_config = create_renewable_config(study_version, **updated_values) + new_data = json.loads(new_config.json(by_alias=True, exclude={"id"})) + + data = { + field.alias: new_data[field.alias] + for field_name, field in new_config.__fields__.items() + if field_name not in {"id"} + and (field_name in updated_values or getattr(new_config, field_name) != field.get_default()) + } + + command = UpdateConfig( + target=path, + data=data, + command_context=self.storage_service.variant_study_service.command_factory.command_context, + ) + file_study = self.storage_service.get_storage(study).get_raw(study) - renewable_config = file_study.tree.get(format_path(RENEWABLE_PATH, area_id, cluster_id).split("/")) - - def get_value(field_info: FieldInfo) -> Any: - target_name = PurePosixPath(field_info["path"]).name - return renewable_config.get(target_name, field_info["default_value"]) - - return RenewableFormFields.construct(**{name: get_value(info) for name, info in FIELDS_INFO.items()}) - - def set_field_values( - self, - study: Study, - area_id: str, - cluster_id: str, - field_values: RenewableFormFields, - ) -> None: - commands: List[UpdateConfig] = [] - - for field_name, value in field_values.__iter__(): - if value is not None: - info = FIELDS_INFO[field_name] - - commands.append( - UpdateConfig( - target=format_path(info["path"], area_id, cluster_id), - data=value, - command_context=self.storage_service.variant_study_service.command_factory.command_context, - ) - ) - - if commands: - file_study = self.storage_service.get_storage(study).get_raw(study) - execute_or_add_commands(study, file_study, commands, self.storage_service) + execute_or_add_commands(study, file_study, [command], self.storage_service) + return RenewableClusterOutput(**new_config.dict(by_alias=False)) + + def delete_clusters(self, study: Study, area_id: str, cluster_ids: t.Sequence[str]) -> None: + """ + Deletes multiple clusters from an area in the study. + + Args: + study: The study from which clusters will be deleted. + area_id: The identifier of the area where clusters will be deleted. + cluster_ids: A sequence of cluster identifiers to be deleted. + """ + file_study = self._get_file_study(study) + command_context = self.storage_service.variant_study_service.command_factory.command_context + + commands = [ + RemoveRenewablesCluster(area_id=area_id, cluster_id=cluster_id, command_context=command_context) + for cluster_id in cluster_ids + ] + + execute_or_add_commands(study, file_study, commands, self.storage_service) diff --git a/antarest/study/business/st_storage_manager.py b/antarest/study/business/areas/st_storage_management.py similarity index 88% rename from antarest/study/business/st_storage_manager.py rename to antarest/study/business/areas/st_storage_management.py index f16ff680d4..8d1edd59b1 100644 --- a/antarest/study/business/st_storage_manager.py +++ b/antarest/study/business/areas/st_storage_management.py @@ -18,17 +18,26 @@ STStorageConfig, STStorageGroup, STStorageProperties, + create_st_storage_config, ) +from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.command.create_st_storage import CreateSTStorage from antarest.study.storage.variantstudy.model.command.remove_st_storage import RemoveSTStorage from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig -_HOURS_IN_YEAR = 8760 +__all__ = ( + "STStorageManager", + "STStorageCreation", + "STStorageInput", + "STStorageOutput", + "STStorageMatrix", + "STStorageTimeSeries", +) @camel_case_model -class StorageInput(STStorageProperties, metaclass=AllOptionalMetaclass): +class STStorageInput(STStorageProperties, metaclass=AllOptionalMetaclass): """ Model representing the form used to EDIT an existing short-term storage. """ @@ -36,7 +45,7 @@ class StorageInput(STStorageProperties, metaclass=AllOptionalMetaclass): class Config: @staticmethod def schema_extra(schema: MutableMapping[str, Any]) -> None: - schema["example"] = StorageInput( + schema["example"] = STStorageInput( name="Siemens Battery", group=STStorageGroup.BATTERY, injection_nominal_capacity=150, @@ -48,7 +57,7 @@ def schema_extra(schema: MutableMapping[str, Any]) -> None: ) -class StorageCreation(StorageInput): +class STStorageCreation(STStorageInput): """ Model representing the form used to CREATE a new short-term storage. """ @@ -70,7 +79,7 @@ def to_config(self) -> STStorageConfig: @camel_case_model -class StorageOutput(STStorageConfig): +class STStorageOutput(STStorageConfig): """ Model representing the form used to display the details of a short-term storage entry. """ @@ -78,7 +87,7 @@ class StorageOutput(STStorageConfig): class Config: @staticmethod def schema_extra(schema: MutableMapping[str, Any]) -> None: - schema["example"] = StorageOutput( + schema["example"] = STStorageOutput( id="siemens_battery", name="Siemens Battery", group=STStorageGroup.BATTERY, @@ -90,7 +99,7 @@ def schema_extra(schema: MutableMapping[str, Any]) -> None: ) @classmethod - def from_config(cls, storage_id: str, config: Mapping[str, Any]) -> "StorageOutput": + def from_config(cls, storage_id: str, config: Mapping[str, Any]) -> "STStorageOutput": storage = STStorageConfig(**config, id=storage_id) values = storage.dict(by_alias=False) return cls(**values) @@ -133,8 +142,8 @@ def validate_time_series(cls, data: List[List[float]]) -> List[List[float]]: array = np.array(data) if array.size == 0: raise ValueError("time series must not be empty") - if array.shape != (_HOURS_IN_YEAR, 1): - raise ValueError(f"time series must have shape ({_HOURS_IN_YEAR}, 1)") + if array.shape != (8760, 1): + raise ValueError(f"time series must have shape ({8760}, 1)") if np.any(np.isnan(array)): raise ValueError("time series must not contain NaN values") return data @@ -222,12 +231,19 @@ class STStorageManager: def __init__(self, storage_service: StudyStorageService): self.storage_service = storage_service + def _get_file_study(self, study: Study) -> FileStudy: + """ + Helper function to get raw study data. + """ + + return self.storage_service.get_storage(study).get_raw(study) + def create_storage( self, study: Study, area_id: str, - form: StorageCreation, - ) -> StorageOutput: + form: STStorageCreation, + ) -> STStorageOutput: """ Create a new short-term storage configuration for the given `study`, `area_id`, and `form fields`. @@ -245,7 +261,7 @@ def create_storage( parameters=storage, command_context=self.storage_service.variant_study_service.command_factory.command_context, ) - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = self._get_file_study(study) execute_or_add_commands( study, file_study, @@ -259,7 +275,7 @@ def get_storages( self, study: Study, area_id: str, - ) -> Sequence[StorageOutput]: + ) -> Sequence[STStorageOutput]: """ Get the list of short-term storage configurations for the given `study`, and `area_id`. @@ -271,7 +287,7 @@ def get_storages( The list of forms used to display the short-term storages. """ - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = self._get_file_study(study) path = STORAGE_LIST_PATH.format(area_id=area_id, storage_id="")[:-1] try: config = file_study.tree.get(path.split("/"), depth=3) @@ -284,14 +300,14 @@ def get_storages( (STStorageConfig(id=storage_id, **options) for storage_id, options in config.items()), key=order_by, ) - return tuple(StorageOutput(**config.dict(by_alias=False)) for config in all_configs) + return tuple(STStorageOutput(**config.dict(by_alias=False)) for config in all_configs) def get_storage( self, study: Study, area_id: str, storage_id: str, - ) -> StorageOutput: + ) -> STStorageOutput: """ Get short-term storage configuration for the given `study`, `area_id`, and `storage_id`. @@ -304,21 +320,21 @@ def get_storage( Form used to display and edit a short-term storage. """ - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = self._get_file_study(study) path = STORAGE_LIST_PATH.format(area_id=area_id, storage_id=storage_id) try: config = file_study.tree.get(path.split("/"), depth=1) except KeyError: raise STStorageFieldsNotFoundError(storage_id) from None - return StorageOutput.from_config(storage_id, config) + return STStorageOutput.from_config(storage_id, config) def update_storage( self, study: Study, area_id: str, storage_id: str, - form: StorageInput, - ) -> StorageOutput: + form: STStorageInput, + ) -> STStorageOutput: """ Set short-term storage configuration for the given `study`, `area_id`, and `storage_id`. @@ -330,20 +346,21 @@ def update_storage( Returns: Updated form of short-term storage. """ - file_study = self.storage_service.get_storage(study).get_raw(study) + study_version = study.version + file_study = self._get_file_study(study) path = STORAGE_LIST_PATH.format(area_id=area_id, storage_id=storage_id) try: values = file_study.tree.get(path.split("/"), depth=1) except KeyError: raise STStorageFieldsNotFoundError(storage_id) from None else: - old_config = STStorageConfig(**values) + old_config = create_st_storage_config(study_version, **values) # use Python values to synchronize Config and Form values old_values = old_config.dict(exclude={"id"}) new_values = form.dict(by_alias=False, exclude_none=True) updated = {**old_values, **new_values} - new_config = STStorageConfig(**updated, id=storage_id) + new_config = create_st_storage_config(study_version, **updated, id=storage_id) new_data = json.loads(new_config.json(by_alias=True, exclude={"id"})) # create the dict containing the old values (excluding defaults), @@ -358,16 +375,13 @@ def update_storage( data[field.alias] = new_data[field.alias] # create the update config command with the modified data - command = UpdateConfig( - target=path, - data=data, - command_context=self.storage_service.variant_study_service.command_factory.command_context, - ) - file_study = self.storage_service.get_storage(study).get_raw(study) + command_context = self.storage_service.variant_study_service.command_factory.command_context + command = UpdateConfig(target=path, data=data, command_context=command_context) + file_study = self._get_file_study(study) execute_or_add_commands(study, file_study, [command], self.storage_service) values = new_config.dict(by_alias=False) - return StorageOutput(**values) + return STStorageOutput(**values) def delete_storages( self, @@ -390,7 +404,7 @@ def delete_storages( storage_id=storage_id, command_context=command_context, ) - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = self._get_file_study(study) execute_or_add_commands(study, file_study, [command], self.storage_service) def get_matrix( @@ -422,7 +436,7 @@ def _get_matrix_obj( storage_id: str, ts_name: STStorageTimeSeries, ) -> MutableMapping[str, Any]: - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = self._get_file_study(study) path = STORAGE_SERIES_PATH.format(area_id=area_id, storage_id=storage_id, ts_name=ts_name) try: matrix = file_study.tree.get(path.split("/"), depth=1) @@ -459,7 +473,7 @@ def _save_matrix_obj( ts_name: STStorageTimeSeries, matrix_obj: Dict[str, Any], ) -> None: - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = self._get_file_study(study) path = STORAGE_SERIES_PATH.format(area_id=area_id, storage_id=storage_id, ts_name=ts_name) try: file_study.tree.save(matrix_obj, path.split("/")) diff --git a/antarest/study/business/areas/thermal_management.py b/antarest/study/business/areas/thermal_management.py index 2287ea777d..9adf5f072e 100644 --- a/antarest/study/business/areas/thermal_management.py +++ b/antarest/study/business/areas/thermal_management.py @@ -1,259 +1,292 @@ -from pathlib import PurePosixPath -from typing import Any, Dict, List, Optional, cast +import json +import typing as t -from pydantic import StrictBool, StrictStr +from pydantic import validator -from antarest.study.business.enum_ignore_case import EnumIgnoreCase -from antarest.study.business.utils import FieldInfo, FormFieldsBaseModel, execute_or_add_commands +from antarest.core.exceptions import ClusterConfigNotFound, ClusterNotFound +from antarest.study.business.utils import AllOptionalMetaclass, camel_case_model, execute_or_add_commands from antarest.study.model import Study +from antarest.study.storage.rawstudy.model.filesystem.config.thermal import ( + Thermal860Config, + Thermal860Properties, + ThermalConfigType, + create_thermal_config, +) +from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.storage_service import StudyStorageService +from antarest.study.storage.variantstudy.model.command.create_cluster import CreateCluster +from antarest.study.storage.variantstudy.model.command.remove_cluster import RemoveCluster from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig +__all__ = ( + "ThermalClusterInput", + "ThermalClusterCreation", + "ThermalClusterOutput", + "ThermalManager", +) -class TimeSeriesGenerationOption(EnumIgnoreCase): - USE_GLOBAL_PARAMETER = "use global parameter" - FORCE_NO_GENERATION = "force no generation" - FORCE_GENERATION = "force generation" - - -class LawOption(EnumIgnoreCase): - UNIFORM = "uniform" - GEOMETRIC = "geometric" - - -THERMAL_PATH = "input/thermal/clusters/{area}/list/{cluster}" - - -class ThermalFormFields(FormFieldsBaseModel): - group: Optional[StrictStr] - name: Optional[StrictStr] - unit_count: Optional[int] - enabled: Optional[StrictBool] - nominal_capacity: Optional[int] - gen_ts: Optional[TimeSeriesGenerationOption] - min_stable_power: Optional[int] - min_up_time: Optional[int] - min_down_time: Optional[int] - must_run: Optional[StrictBool] - spinning: Optional[int] - volatility_forced: Optional[int] - volatility_planned: Optional[int] - law_forced: Optional[LawOption] - law_planned: Optional[LawOption] - marginal_cost: Optional[int] - spread_cost: Optional[int] - fixed_cost: Optional[int] - startup_cost: Optional[int] - market_bid_cost: Optional[int] - # Pollutants - co2: Optional[float] - so2: Optional[float] - nh3: Optional[float] - nox: Optional[float] - nmvoc: Optional[float] - pm25: Optional[float] - pm5: Optional[float] - pm10: Optional[float] - op1: Optional[float] - op2: Optional[float] - op3: Optional[float] - op4: Optional[float] - op5: Optional[float] - - -FIELDS_INFO: Dict[str, FieldInfo] = { - "group": { - "path": f"{THERMAL_PATH}/group", - "default_value": "", - }, - "name": { - "path": f"{THERMAL_PATH}/name", - "default_value": "", - }, - "unit_count": { - "path": f"{THERMAL_PATH}/unitcount", - "default_value": 0, - }, - "enabled": { - "path": f"{THERMAL_PATH}/enabled", - "default_value": True, - }, - "nominal_capacity": { - "path": f"{THERMAL_PATH}/nominalcapacity", - "default_value": 0, - }, - "gen_ts": { - "path": f"{THERMAL_PATH}/gen-ts", - "default_value": TimeSeriesGenerationOption.USE_GLOBAL_PARAMETER.value, - }, - "min_stable_power": { - "path": f"{THERMAL_PATH}/min-stable-power", - "default_value": 0, - }, - "min_up_time": { - "path": f"{THERMAL_PATH}/min-up-time", - "default_value": 1, - }, - "min_down_time": { - "path": f"{THERMAL_PATH}/min-down-time", - "default_value": 1, - }, - "must_run": { - "path": f"{THERMAL_PATH}/must-run", - "default_value": False, - }, - "spinning": { - "path": f"{THERMAL_PATH}/spinning", - "default_value": 0, - }, - "volatility_forced": { - "path": f"{THERMAL_PATH}/volatility.forced", - "default_value": 0, - }, - "volatility_planned": { - "path": f"{THERMAL_PATH}/volatility.planned", - "default_value": 0, - }, - "law_forced": { - "path": f"{THERMAL_PATH}/law.forced", - "default_value": LawOption.UNIFORM.value, - }, - "law_planned": { - "path": f"{THERMAL_PATH}/law.planned", - "default_value": LawOption.UNIFORM.value, - }, - "marginal_cost": { - "path": f"{THERMAL_PATH}/marginal-cost", - "default_value": 0, - }, - "spread_cost": { - "path": f"{THERMAL_PATH}/spread-cost", - "default_value": 0, - }, - "fixed_cost": { - "path": f"{THERMAL_PATH}/fixed-cost", - "default_value": 0, - }, - "startup_cost": { - "path": f"{THERMAL_PATH}/startup-cost", - "default_value": 0, - }, - "market_bid_cost": { - "path": f"{THERMAL_PATH}/market-bid-cost", - "default_value": 0, - }, - # Pollutants - "co2": { - "path": f"{THERMAL_PATH}/co2", - "default_value": 0.0, - }, - "so2": { - "path": f"{THERMAL_PATH}/so2", - "default_value": 0.0, - "start_version": 860, - }, - "nh3": { - "path": f"{THERMAL_PATH}/nh3", - "default_value": 0.0, - "start_version": 860, - }, - "nox": { - "path": f"{THERMAL_PATH}/nox", - "default_value": 0.0, - "start_version": 860, - }, - "nmvoc": { - "path": f"{THERMAL_PATH}/nmvoc", - "default_value": 0.0, - "start_version": 860, - }, - "pm25": { - "path": f"{THERMAL_PATH}/pm2_5", - "default_value": 0.0, - "start_version": 860, - }, - "pm5": { - "path": f"{THERMAL_PATH}/pm5", - "default_value": 0.0, - "start_version": 860, - }, - "pm10": { - "path": f"{THERMAL_PATH}/pm10", - "default_value": 0.0, - "start_version": 860, - }, - "op1": { - "path": f"{THERMAL_PATH}/op1", - "default_value": 0.0, - "start_version": 860, - }, - "op2": { - "path": f"{THERMAL_PATH}/op2", - "default_value": 0.0, - "start_version": 860, - }, - "op3": { - "path": f"{THERMAL_PATH}/op3", - "default_value": 0.0, - "start_version": 860, - }, - "op4": { - "path": f"{THERMAL_PATH}/op4", - "default_value": 0.0, - "start_version": 860, - }, - "op5": { - "path": f"{THERMAL_PATH}/op5", - "default_value": 0.0, - "start_version": 860, - }, -} - - -def format_path(path: str, area_id: str, cluster_id: str) -> str: - return path.format(area=area_id, cluster=cluster_id) +_CLUSTER_PATH = "input/thermal/clusters/{area_id}/list/{cluster_id}" +_CLUSTERS_PATH = "input/thermal/clusters/{area_id}/list" + + +@camel_case_model +class ThermalClusterInput(Thermal860Properties, metaclass=AllOptionalMetaclass): + """ + Model representing the data structure required to edit an existing thermal cluster within a study. + """ + + class Config: + @staticmethod + def schema_extra(schema: t.MutableMapping[str, t.Any]) -> None: + schema["example"] = ThermalClusterInput( + group="Gas", + name="2 avail and must 1", + enabled=False, + unitCount=100, + nominalCapacity=1000.0, + genTs="use global parameter", + co2=7.0, + ) + + +class ThermalClusterCreation(ThermalClusterInput): + """ + Model representing the data structure required to create a new thermal cluster within a study. + """ + + # noinspection Pydantic + @validator("name", pre=True) + def validate_name(cls, name: t.Optional[str]) -> str: + """ + Validator to check if the name is not empty. + """ + if not name: + raise ValueError("name must not be empty") + return name + + def to_config(self, study_version: t.Union[str, int]) -> ThermalConfigType: + values = self.dict(by_alias=False, exclude_none=True) + return create_thermal_config(study_version=study_version, **values) + + +@camel_case_model +class ThermalClusterOutput(Thermal860Config, metaclass=AllOptionalMetaclass): + """ + Model representing the output data structure to display the details of a thermal cluster within a study. + """ + + class Config: + @staticmethod + def schema_extra(schema: t.MutableMapping[str, t.Any]) -> None: + schema["example"] = ThermalClusterOutput( + id="2 avail and must 1", + group="Gas", + name="2 avail and must 1", + enabled=False, + unitCount=100, + nominalCapacity=1000.0, + genTs="use global parameter", + co2=7.0, + ) + + +def create_thermal_output( + study_version: t.Union[str, int], + cluster_id: str, + config: t.Mapping[str, t.Any], +) -> "ThermalClusterOutput": + obj = create_thermal_config(study_version=study_version, **config, id=cluster_id) + kwargs = obj.dict(by_alias=False) + return ThermalClusterOutput(**kwargs) class ThermalManager: + """ + Manager class implementing endpoints related to Thermal Clusters within a study. + Provides methods for creating, retrieving, updating, and deleting thermal clusters. + + Attributes: + storage_service: The service for accessing study storage. + """ + def __init__(self, storage_service: StudyStorageService): + """ + Initializes an instance with the service for accessing study storage. + """ + self.storage_service = storage_service - def get_field_values(self, study: Study, area_id: str, cluster_id: str) -> ThermalFormFields: - file_study = self.storage_service.get_storage(study).get_raw(study) - thermal_config = file_study.tree.get(format_path(THERMAL_PATH, area_id, cluster_id).split("/")) + def _get_file_study(self, study: Study) -> FileStudy: + """ + Helper function to get raw study data. + """ + + return self.storage_service.get_storage(study).get_raw(study) + + def get_cluster(self, study: Study, area_id: str, cluster_id: str) -> ThermalClusterOutput: + """ + Get a cluster by ID. - def get_value(field_info: FieldInfo) -> Any: - target_name = PurePosixPath(field_info["path"]).name - study_ver = file_study.config.version - start_ver = cast(int, field_info.get("start_version", 0)) - end_ver = cast(int, field_info.get("end_version", study_ver)) - is_in_version = start_ver <= study_ver <= end_ver + Args: + study: The study to get the cluster from. + area_id: The ID of the area where the cluster is located. + cluster_id: The ID of the cluster to retrieve. - return thermal_config.get(target_name, field_info["default_value"]) if is_in_version else None + Returns: + The cluster with the specified ID. - return ThermalFormFields.construct(**{name: get_value(info) for name, info in FIELDS_INFO.items()}) + Raises: + ClusterNotFound: If the specified cluster does not exist. + """ - def set_field_values( + file_study = self._get_file_study(study) + path = _CLUSTER_PATH.format(area_id=area_id, cluster_id=cluster_id) + try: + cluster = file_study.tree.get(path.split("/"), depth=1) + except KeyError: + raise ClusterNotFound(cluster_id) + study_version = study.version + return create_thermal_output(study_version, cluster_id, cluster) + + def get_clusters( + self, + study: Study, + area_id: str, + ) -> t.Sequence[ThermalClusterOutput]: + """ + Retrieve all thermal clusters from a specified area within a study. + + Args: + study: Study from which to retrieve the clusters. + area_id: ID of the area containing the clusters. + + Returns: + A list of thermal clusters within the specified area. + + Raises: + ClusterConfigNotFound: If no clusters are found in the specified area. + """ + + file_study = self._get_file_study(study) + path = _CLUSTERS_PATH.format(area_id=area_id) + try: + clusters = file_study.tree.get(path.split("/"), depth=3) + except KeyError: + raise ClusterConfigNotFound(area_id) + study_version = study.version + return [create_thermal_output(study_version, cluster_id, cluster) for cluster_id, cluster in clusters.items()] + + def create_cluster(self, study: Study, area_id: str, cluster_data: ThermalClusterCreation) -> ThermalClusterOutput: + """ + Create a new cluster. + + Args: + study: The study where the cluster will be created. + area_id: The ID of the area where the cluster will be created. + cluster_data: The data for the new cluster. + + Returns: + The created cluster. + """ + + file_study = self._get_file_study(study) + study_version = study.version + cluster = cluster_data.to_config(study_version) + # NOTE: currently, in the `CreateCluster` class, there is a confusion + # between the cluster name and the cluster ID (which is a section name). + command = CreateCluster( + area_id=area_id, + cluster_name=cluster.id, + parameters=cluster.dict(by_alias=True, exclude={"id"}), + command_context=self.storage_service.variant_study_service.command_factory.command_context, + ) + execute_or_add_commands( + study, + file_study, + [command], + self.storage_service, + ) + output = self.get_cluster(study, area_id, cluster.id) + return output + + def update_cluster( self, study: Study, area_id: str, cluster_id: str, - field_values: ThermalFormFields, - ) -> None: - commands: List[UpdateConfig] = [] - - for field_name, value in field_values.__iter__(): - if value is not None: - info = FIELDS_INFO[field_name] - - commands.append( - UpdateConfig( - target=format_path(info["path"], area_id, cluster_id), - data=value, - command_context=self.storage_service.variant_study_service.command_factory.command_context, - ) - ) - - if commands: - file_study = self.storage_service.get_storage(study).get_raw(study) - execute_or_add_commands(study, file_study, commands, self.storage_service) + cluster_data: ThermalClusterInput, + ) -> ThermalClusterOutput: + """ + Update a cluster with the given `cluster_id` in the given area of the given study + with the provided cluster data (form fields). + + Args: + study: The study containing the area and cluster to update. + area_id: The ID of the area containing the cluster to update. + cluster_id: The ID of the cluster to update. + cluster_data: The new data to update the cluster with. + + Returns: + The updated cluster. + + Raises: + ClusterNotFound: If the provided `cluster_id` does not match the ID of the cluster + in the provided cluster_data. + """ + + study_version = study.version + file_study = self._get_file_study(study) + path = _CLUSTER_PATH.format(area_id=area_id, cluster_id=cluster_id) + try: + values = file_study.tree.get(path.split("/"), depth=1) + except KeyError: + raise ClusterNotFound(cluster_id) from None + else: + old_config = create_thermal_config(study_version, **values) + + # Use Python values to synchronize Config and Form values + old_values = old_config.dict(exclude={"id"}) + new_values = cluster_data.dict(by_alias=False, exclude_none=True) + updated = {**old_values, **new_values} + new_config = create_thermal_config(study_version, **updated, id=cluster_id) + new_data = json.loads(new_config.json(by_alias=True, exclude={"id"})) + + # Create the dict containing the old values (excluding defaults), + # and the updated values (including defaults) + data: t.Dict[str, t.Any] = {} + for field_name, field in new_config.__fields__.items(): + if field_name in {"id"}: + continue + value = getattr(new_config, field_name) + if field_name in new_values or value != field.get_default(): + # use the JSON-converted value + data[field.alias] = new_data[field.alias] + + # create the update config command with the modified data + command_context = self.storage_service.variant_study_service.command_factory.command_context + command = UpdateConfig(target=path, data=data, command_context=command_context) + file_study = self.storage_service.get_storage(study).get_raw(study) + execute_or_add_commands(study, file_study, [command], self.storage_service) + + values = new_config.dict(by_alias=False) + return ThermalClusterOutput(**values) + + def delete_clusters(self, study: Study, area_id: str, cluster_ids: t.Sequence[str]) -> None: + """ + Delete the clusters with the given IDs in the given area of the given study. + + Args: + study: The study containing the area and clusters to delete. + area_id: The ID of the area containing the clusters to delete. + cluster_ids: The IDs of the clusters to delete. + """ + + file_study = self._get_file_study(study) + command_context = self.storage_service.variant_study_service.command_factory.command_context + + commands = [ + RemoveCluster(area_id=area_id, cluster_id=cluster_id, command_context=command_context) + for cluster_id in cluster_ids + ] + + execute_or_add_commands(study, file_study, commands, self.storage_service) diff --git a/antarest/study/business/table_mode_management.py b/antarest/study/business/table_mode_management.py index 0c769d9aa0..c124adf27d 100644 --- a/antarest/study/business/table_mode_management.py +++ b/antarest/study/business/table_mode_management.py @@ -5,12 +5,12 @@ from antarest.study.business.areas.properties_management import AdequacyPatchMode from antarest.study.business.areas.renewable_management import TimeSeriesInterpretation -from antarest.study.business.areas.thermal_management import LawOption, TimeSeriesGenerationOption from antarest.study.business.binding_constraint_management import BindingConstraintManager from antarest.study.business.enum_ignore_case import EnumIgnoreCase from antarest.study.business.utils import FormFieldsBaseModel, execute_or_add_commands from antarest.study.common.default_values import FilteringOptions, LinkProperties, NodalOptimization from antarest.study.model import RawStudy +from antarest.study.storage.rawstudy.model.filesystem.config.thermal import LawOption, TimeSeriesGenerationOption from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.command.icommand import ICommand @@ -399,16 +399,15 @@ def _get_glob_object(file_study: FileStudy, table_type: TableTemplateType) -> Di if area_id in info_map: info_map[area_id][field] = value return info_map - if table_type == TableTemplateType.LINK: - return file_study.tree.get(LINK_GLOB_PATH.format(area1="*").split("/")) - if table_type == TableTemplateType.CLUSTER: - return file_study.tree.get(CLUSTER_GLOB_PATH.format(area="*").split("/")) - if table_type == TableTemplateType.RENEWABLE: - return file_study.tree.get(RENEWABLE_GLOB_PATH.format(area="*").split("/")) - if table_type == TableTemplateType.BINDING_CONSTRAINT: - return file_study.tree.get(BINDING_CONSTRAINT_PATH.split("/")) - return {} + url = { + TableTemplateType.LINK: LINK_GLOB_PATH.format(area1="*").split("/"), + TableTemplateType.CLUSTER: CLUSTER_GLOB_PATH.format(area="*").split("/"), + TableTemplateType.RENEWABLE: RENEWABLE_GLOB_PATH.format(area="*").split("/"), + TableTemplateType.BINDING_CONSTRAINT: BINDING_CONSTRAINT_PATH.split("/"), + }[table_type] + + return file_study.tree.get(url) class TableModeManager: @@ -468,6 +467,7 @@ def set_table_data( ) -> None: commands: List[ICommand] = [] bindings_by_id = None + command_context = self.storage_service.variant_study_service.command_factory.command_context for key, columns in data.items(): path_vars = TableModeManager.__get_path_vars_from_key(table_type, key) @@ -491,7 +491,7 @@ def set_table_data( time_step=col_values.get("type", current_binding_dto.time_step), operator=col_values.get("operator", current_binding_dto.operator), coeffs=BindingConstraintManager.constraints_to_coeffs(current_binding_dto), - command_context=self.storage_service.variant_study_service.command_factory.command_context, + command_context=command_context, ) ) else: @@ -501,7 +501,7 @@ def set_table_data( UpdateConfig( target=TableModeManager.__get_column_path(table_type, path_vars, col), data=val, - command_context=self.storage_service.variant_study_service.command_factory.command_context, + command_context=command_context, ) ) diff --git a/antarest/study/business/utils.py b/antarest/study/business/utils.py index 33b62d766c..fbffbc310b 100644 --- a/antarest/study/business/utils.py +++ b/antarest/study/business/utils.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, Dict, MutableSequence, Optional, Sequence, Tuple, Type, TypedDict +from typing import Any, Callable, Dict, MutableSequence, Optional, Sequence, Tuple, Type, TypedDict, TypeVar import pydantic from pydantic import BaseModel, Extra @@ -110,7 +110,8 @@ def __new__( ) -> Any: annotations = namespaces.get("__annotations__", {}) for base in bases: - annotations.update(getattr(base, "__annotations__", {})) + for ancestor in reversed(base.__mro__): + annotations.update(getattr(ancestor, "__annotations__", {})) for field, field_type in annotations.items(): if not field.startswith("__"): # Optional fields are correctly handled @@ -119,7 +120,10 @@ def __new__( return super().__new__(cls, name, bases, namespaces) -def camel_case_model(model: Type[BaseModel]) -> Type[BaseModel]: +MODEL = TypeVar("MODEL", bound=Type[BaseModel]) + + +def camel_case_model(model: MODEL) -> MODEL: """ This decorator can be used to modify a model to use camel case aliases. diff --git a/antarest/study/business/xpansion_management.py b/antarest/study/business/xpansion_management.py index e8c6c79f98..91554bfe94 100644 --- a/antarest/study/business/xpansion_management.py +++ b/antarest/study/business/xpansion_management.py @@ -1,16 +1,16 @@ +import contextlib +import http +import io import logging import shutil -from http import HTTPStatus -from io import BytesIO -from typing import List, Optional, Union, cast -from zipfile import BadZipFile, ZipFile +import typing as t +import zipfile from fastapi import HTTPException, UploadFile from pydantic import BaseModel, Field, validator from antarest.core.exceptions import BadZipBinary from antarest.core.model import JSON -from antarest.core.utils.utils import suppress_exception from antarest.study.business.enum_ignore_case import EnumIgnoreCase from antarest.study.model import Study from antarest.study.storage.rawstudy.model.filesystem.bucket_node import BucketNode @@ -56,11 +56,16 @@ class MaxIteration(EnumIgnoreCase): class XpansionSensitivitySettingsDTO(BaseModel): - epsilon: float - projection: Optional[List[str]] + epsilon: float = 10000.0 + projection: t.List[str] = Field(default_factory=list) capex: bool = False + @validator("projection", pre=True) + def projection_validation(cls, v: t.Optional[t.Sequence[str]]) -> t.Sequence[str]: + return [] if v is None else v + +# noinspection SpellCheckingInspection class XpansionSettingsDTO(BaseModel): """ A data transfer object representing the general settings used for Xpansion. @@ -79,6 +84,7 @@ class XpansionSettingsDTO(BaseModel): ampl_solve_bounds_frequency: The frequency with which to solve bounds using AMPL. relative_gap: Tolerance on relative gap for the solution. batch_size: Amount of batches in the Benders decomposition. + separation_parameter: The separation parameter used in the Benders decomposition. solver: The solver used to solve the master and the sub-problems in the Benders decomposition. timelimit: The timelimit (in seconds) of the Benders step. log_level: The severity of the solver's log. @@ -89,28 +95,29 @@ class XpansionSettingsDTO(BaseModel): or a string ending with "%" and a valid float. """ - optimality_gap: Optional[float] = Field(default=1, ge=0) + optimality_gap: t.Optional[float] = Field(default=1, ge=0) - max_iteration: Optional[Union[int, MaxIteration]] = Field(default=MaxIteration.INF, ge=0) + max_iteration: t.Optional[t.Union[int, MaxIteration]] = Field(default=MaxIteration.INF, ge=0) uc_type: UcType = UcType.EXPANSION_FAST master: Master = Master.INTEGER - yearly_weights: Optional[str] = Field(None, alias="yearly-weights") - additional_constraints: Optional[str] = Field(None, alias="additional-constraints") - relaxed_optimality_gap: Optional[Union[float, str]] = Field(None, alias="relaxed-optimality-gap") - cut_type: Optional[CutType] = Field(None, alias="cut-type") - ampl_solver: Optional[str] = Field(None, alias="ampl.solver") - ampl_presolve: Optional[int] = Field(None, alias="ampl.presolve") - ampl_solve_bounds_frequency: Optional[int] = Field(None, alias="ampl.solve_bounds_frequency") - relative_gap: Optional[float] = Field(default=None, ge=0) - batch_size: Optional[int] = Field(default=0, ge=0) - solver: Optional[Solver] = None - timelimit: Optional[int] = 1000000000000 # 1e12 - log_level: Optional[int] = 0 - sensitivity_config: Optional[XpansionSensitivitySettingsDTO] = None + yearly_weights: t.Optional[str] = Field(None, alias="yearly-weights") + additional_constraints: t.Optional[str] = Field(None, alias="additional-constraints") + relaxed_optimality_gap: t.Optional[t.Union[float, str]] = Field(None, alias="relaxed-optimality-gap") + cut_type: t.Optional[CutType] = Field(None, alias="cut-type") + ampl_solver: t.Optional[str] = Field(None, alias="ampl.solver") + ampl_presolve: t.Optional[int] = Field(None, alias="ampl.presolve") + ampl_solve_bounds_frequency: t.Optional[int] = Field(None, alias="ampl.solve_bounds_frequency") + relative_gap: t.Optional[float] = Field(default=None, ge=0) + batch_size: t.Optional[int] = Field(default=0, ge=0) + separation_parameter: t.Optional[float] = Field(default=0.5, ge=0, le=1) + solver: t.Optional[Solver] = None + timelimit: t.Optional[int] = 1000000000000 # 1e12 + log_level: t.Optional[int] = 0 + sensitivity_config: t.Optional[XpansionSensitivitySettingsDTO] = None @validator("relaxed_optimality_gap") - def relaxed_optimality_gap_validation(cls, v: Optional[Union[float, str]]) -> Optional[Union[float, str]]: + def relaxed_optimality_gap_validation(cls, v: t.Optional[t.Union[float, str]]) -> t.Optional[t.Union[float, str]]: if isinstance(v, float): return v if isinstance(v, str): @@ -127,87 +134,87 @@ class XpansionCandidateDTO(BaseModel): name: str link: str annual_cost_per_mw: float = Field(alias="annual-cost-per-mw", ge=0) - unit_size: Optional[float] = Field(None, alias="unit-size", ge=0) - max_units: Optional[int] = Field(None, alias="max-units", ge=0) - max_investment: Optional[float] = Field(None, alias="max-investment", ge=0) - already_installed_capacity: Optional[int] = Field(None, alias="already-installed-capacity", ge=0) + unit_size: t.Optional[float] = Field(None, alias="unit-size", ge=0) + max_units: t.Optional[int] = Field(None, alias="max-units", ge=0) + max_investment: t.Optional[float] = Field(None, alias="max-investment", ge=0) + already_installed_capacity: t.Optional[int] = Field(None, alias="already-installed-capacity", ge=0) # this is obsolete (replaced by direct/indirect) - link_profile: Optional[str] = Field(None, alias="link-profile") + link_profile: t.Optional[str] = Field(None, alias="link-profile") # this is obsolete (replaced by direct/indirect) - already_installed_link_profile: Optional[str] = Field(None, alias="already-installed-link-profile") - direct_link_profile: Optional[str] = Field(None, alias="direct-link-profile") - indirect_link_profile: Optional[str] = Field(None, alias="indirect-link-profile") - already_installed_direct_link_profile: Optional[str] = Field(None, alias="already-installed-direct-link-profile") - already_installed_indirect_link_profile: Optional[str] = Field( + already_installed_link_profile: t.Optional[str] = Field(None, alias="already-installed-link-profile") + direct_link_profile: t.Optional[str] = Field(None, alias="direct-link-profile") + indirect_link_profile: t.Optional[str] = Field(None, alias="indirect-link-profile") + already_installed_direct_link_profile: t.Optional[str] = Field(None, alias="already-installed-direct-link-profile") + already_installed_indirect_link_profile: t.Optional[str] = Field( None, alias="already-installed-indirect-link-profile" ) class LinkNotFound(HTTPException): def __init__(self, message: str) -> None: - super().__init__(HTTPStatus.NOT_FOUND, message) + super().__init__(http.HTTPStatus.NOT_FOUND, message) class XpansionFileNotFoundError(HTTPException): def __init__(self, message: str) -> None: - super().__init__(HTTPStatus.NOT_FOUND, message) + super().__init__(http.HTTPStatus.NOT_FOUND, message) class IllegalCharacterInNameError(HTTPException): def __init__(self, message: str) -> None: - super().__init__(HTTPStatus.BAD_REQUEST, message) + super().__init__(http.HTTPStatus.BAD_REQUEST, message) class CandidateNameIsEmpty(HTTPException): def __init__(self) -> None: - super().__init__(HTTPStatus.BAD_REQUEST) + super().__init__(http.HTTPStatus.BAD_REQUEST) class WrongTypeFormat(HTTPException): def __init__(self, message: str) -> None: - super().__init__(HTTPStatus.BAD_REQUEST, message) + super().__init__(http.HTTPStatus.BAD_REQUEST, message) class WrongLinkFormatError(HTTPException): def __init__(self, message: str) -> None: - super().__init__(HTTPStatus.BAD_REQUEST, message) + super().__init__(http.HTTPStatus.BAD_REQUEST, message) class CandidateAlreadyExistsError(HTTPException): def __init__(self, message: str) -> None: - super().__init__(HTTPStatus.BAD_REQUEST, message) + super().__init__(http.HTTPStatus.BAD_REQUEST, message) class BadCandidateFormatError(HTTPException): def __init__(self, message: str) -> None: - super().__init__(HTTPStatus.BAD_REQUEST, message) + super().__init__(http.HTTPStatus.BAD_REQUEST, message) class CandidateNotFoundError(HTTPException): def __init__(self, message: str) -> None: - super().__init__(HTTPStatus.NOT_FOUND, message) + super().__init__(http.HTTPStatus.NOT_FOUND, message) class ConstraintsNotFoundError(HTTPException): def __init__(self, message: str) -> None: - super().__init__(HTTPStatus.NOT_FOUND, message) + super().__init__(http.HTTPStatus.NOT_FOUND, message) class FileCurrentlyUsedInSettings(HTTPException): def __init__(self, message: str) -> None: - super().__init__(HTTPStatus.CONFLICT, message) + super().__init__(http.HTTPStatus.CONFLICT, message) class FileAlreadyExistsError(HTTPException): def __init__(self, message: str) -> None: - super().__init__(HTTPStatus.CONFLICT, message) + super().__init__(http.HTTPStatus.CONFLICT, message) class XpansionManager: def __init__(self, study_storage_service: StudyStorageService): self.study_storage_service = study_storage_service - def create_xpansion_configuration(self, study: Study, zipped_config: Optional[UploadFile] = None) -> None: + def create_xpansion_configuration(self, study: Study, zipped_config: t.Optional[UploadFile] = None) -> None: logger.info(f"Initiating xpansion configuration for study '{study.id}'") file_study = self.study_storage_service.get_storage(study).get_raw(study) try: @@ -216,12 +223,12 @@ def create_xpansion_configuration(self, study: Study, zipped_config: Optional[Up except ChildNotFoundError: if zipped_config: try: - with ZipFile(BytesIO(zipped_config.file.read())) as zip_output: + with zipfile.ZipFile(io.BytesIO(zipped_config.file.read())) as zip_output: logger.info(f"Importing zipped xpansion configuration for study '{study.id}'") zip_output.extractall(path=file_study.config.path / "user" / "expansion") fix_study_root(file_study.config.path / "user" / "expansion") return - except BadZipFile: + except zipfile.BadZipFile: shutil.rmtree( file_study.config.path / "user" / "expansion", ignore_errors=True, @@ -249,6 +256,7 @@ def create_xpansion_configuration(self, study: Study, zipped_config: Optional[Up xpansion_settings["relative_gap"] = 1e-12 xpansion_settings["solver"] = Solver.CBC.value xpansion_settings["batch_size"] = 0 + xpansion_settings["separation_parameter"] = 0.5 xpansion_configuration_data = { "user": { @@ -273,15 +281,12 @@ def delete_xpansion_configuration(self, study: Study) -> None: def get_xpansion_settings(self, study: Study) -> XpansionSettingsDTO: logger.info(f"Getting xpansion settings for study '{study.id}'") file_study = self.study_storage_service.get_storage(study).get_raw(study) - json = file_study.tree.get(["user", "expansion", "settings"]) - json["sensitivity_config"] = ( - suppress_exception( - lambda: file_study.tree.get(["user", "expansion", "sensitivity", "sensitivity_in"]), - lambda e: logger.warning("Failed to read sensitivity config", exc_info=e), + settings_obj = file_study.tree.get(["user", "expansion", "settings"]) + with contextlib.suppress(KeyError): + settings_obj["sensitivity_config"] = file_study.tree.get( + ["user", "expansion", "sensitivity", "sensitivity_in"] ) - or None - ) - return XpansionSettingsDTO.parse_obj(json) + return XpansionSettingsDTO(**settings_obj) @staticmethod def _assert_xpansion_settings_additional_constraints_is_valid( @@ -413,9 +418,9 @@ def _assert_candidate_name_is_not_already_taken(candidates: JSON, xpansion_candi @staticmethod def _assert_investment_candidate_is_valid( - max_investment: Optional[float], - max_units: Optional[int], - unit_size: Optional[float], + max_investment: t.Optional[float], + max_units: t.Optional[int], + unit_size: t.Optional[float], ) -> None: bool_max_investment = max_investment is None bool_max_units = max_units is None @@ -480,7 +485,7 @@ def get_candidate(self, study: Study, candidate_name: str) -> XpansionCandidateD except StopIteration: raise CandidateNotFoundError(f"The candidate '{candidate_name}' does not exist") - def get_candidates(self, study: Study) -> List[XpansionCandidateDTO]: + def get_candidates(self, study: Study) -> t.List[XpansionCandidateDTO]: logger.info(f"Getting all candidates of study {study.id}") file_study = self.study_storage_service.get_storage(study).get_raw(study) candidates = file_study.tree.get(["user", "expansion", "candidates"]) @@ -519,13 +524,13 @@ def delete_candidate(self, study: Study, candidate_name: str) -> None: logger.info(f"Deleting candidate '{candidate_name}' from study '{study.id}'") file_study.tree.delete(["user", "expansion", "candidates", candidate_id]) - def update_xpansion_constraints_settings(self, study: Study, constraints_file_name: Optional[str]) -> None: + def update_xpansion_constraints_settings(self, study: Study, constraints_file_name: t.Optional[str]) -> None: self.update_xpansion_settings( study, XpansionSettingsDTO.parse_obj({"additional-constraints": constraints_file_name}), ) - def _raw_file_dir(self, raw_file_type: XpansionResourceFileType) -> List[str]: + def _raw_file_dir(self, raw_file_type: XpansionResourceFileType) -> t.List[str]: if raw_file_type == XpansionResourceFileType.CONSTRAINTS: return ["user", "expansion", "constraints"] elif raw_file_type == XpansionResourceFileType.CAPACITIES: @@ -537,7 +542,7 @@ def _raw_file_dir(self, raw_file_type: XpansionResourceFileType) -> List[str]: def _add_raw_files( self, file_study: FileStudy, - files: List[UploadFile], + files: t.List[UploadFile], raw_file_type: XpansionResourceFileType, ) -> None: keys = self._raw_file_dir(raw_file_type) @@ -571,7 +576,7 @@ def add_resource( self, study: Study, resource_type: XpansionResourceFileType, - files: List[UploadFile], + files: t.List[UploadFile], ) -> None: logger.info(f"Adding xpansion {resource_type} resource file list to study '{study.id}'") file_study = self.study_storage_service.get_storage(study).get_raw(study) @@ -608,12 +613,12 @@ def get_resource_content( study: Study, resource_type: XpansionResourceFileType, filename: str, - ) -> Union[JSON, bytes]: + ) -> t.Union[JSON, bytes]: logger.info(f"Getting xpansion {resource_type} resource file '{filename}' from study '{study.id}'") file_study = self.study_storage_service.get_storage(study).get_raw(study) return file_study.tree.get(self._raw_file_dir(resource_type) + [filename]) - def list_resources(self, study: Study, resource_type: XpansionResourceFileType) -> List[str]: + def list_resources(self, study: Study, resource_type: XpansionResourceFileType) -> t.List[str]: logger.info(f"Getting all xpansion {resource_type} files from study '{study.id}'") file_study = self.study_storage_service.get_storage(study).get_raw(study) try: @@ -621,54 +626,28 @@ def list_resources(self, study: Study, resource_type: XpansionResourceFileType) except ChildNotFoundError: return [] - def list_root_files(self, study: Study) -> List[str]: + def list_root_files(self, study: Study) -> t.List[str]: logger.info(f"Getting xpansion root resources file from study '{study.id}'") file_study = self.study_storage_service.get_storage(study).get_raw(study) registered_filenames = [registered_file.key for registered_file in Expansion.registered_files] root_files = [ key - for key, node in cast(FolderNode, file_study.tree.get_node(["user", "expansion"])).build().items() + for key, node in t.cast(FolderNode, file_study.tree.get_node(["user", "expansion"])).build().items() if key not in registered_filenames and type(node) != BucketNode ] return root_files @staticmethod - def _is_constraints_file_used(file_study: FileStudy, filename: str) -> bool: - try: - return ( - str( - file_study.tree.get( - [ - "user", - "expansion", - "settings", - "additional-constraints", - ] - ) - ) - == filename - ) - except KeyError: - return False + def _is_constraints_file_used(file_study: FileStudy, filename: str) -> bool: # type: ignore + with contextlib.suppress(KeyError): + constraints = file_study.tree.get(["user", "expansion", "settings", "additional-constraints"]) + return str(constraints) == filename @staticmethod - def _is_weights_file_used(file_study: FileStudy, filename: str) -> bool: - try: - return ( - str( - file_study.tree.get( - [ - "user", - "expansion", - "settings", - "yearly-weights", - ] - ) - ) - == filename - ) - except KeyError: - return False + def _is_weights_file_used(file_study: FileStudy, filename: str) -> bool: # type: ignore + with contextlib.suppress(KeyError): + weights = file_study.tree.get(["user", "expansion", "settings", "yearly-weights"]) + return str(weights) == filename @staticmethod def _is_capa_file_used(file_study: FileStudy, filename: str) -> bool: diff --git a/antarest/study/common/studystorage.py b/antarest/study/common/studystorage.py index 162266ebb2..c6a271a66f 100644 --- a/antarest/study/common/studystorage.py +++ b/antarest/study/common/studystorage.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from pathlib import Path -from typing import IO, Generic, List, Optional, Sequence, TypeVar, Union +from typing import BinaryIO, Generic, List, Optional, Sequence, TypeVar, Union from antarest.core.exceptions import StudyNotFoundError from antarest.core.model import JSON @@ -91,7 +91,7 @@ def patch_update_study_metadata(self, study: T, metadata: StudyMetadataPatchDTO) def import_output( self, study: T, - output: Union[IO[bytes], Path], + output: Union[BinaryIO, Path], output_name: Optional[str] = None, ) -> Optional[str]: """ diff --git a/antarest/study/service.py b/antarest/study/service.py index 5bf67200b7..a8da7fc60c 100644 --- a/antarest/study/service.py +++ b/antarest/study/service.py @@ -1,13 +1,14 @@ import base64 +import contextlib import io import json import logging import os from datetime import datetime, timedelta from http import HTTPStatus -from pathlib import Path +from pathlib import Path, PurePosixPath from time import time -from typing import IO, Any, Callable, Dict, List, Optional, Sequence, Tuple, Union, cast +from typing import Any, BinaryIO, Callable, Dict, List, Optional, Sequence, Tuple, Union, cast from uuid import uuid4 import numpy as np @@ -31,7 +32,7 @@ from antarest.core.filetransfer.service import FileTransferManager from antarest.core.interfaces.cache import CacheConstants, ICache from antarest.core.interfaces.eventbus import Event, EventType, IEventBus -from antarest.core.jwt import DEFAULT_ADMIN_USER, JWTUser +from antarest.core.jwt import DEFAULT_ADMIN_USER, JWTGroup, JWTUser from antarest.core.model import JSON, SUB_JSON, PermissionInfo, PublicMode, StudyPermissionType from antarest.core.requests import RequestParameters, UserHasNotPermissionError from antarest.core.roles import RoleType @@ -49,6 +50,7 @@ from antarest.study.business.areas.hydro_management import HydroManager from antarest.study.business.areas.properties_management import PropertiesManager from antarest.study.business.areas.renewable_management import RenewableManager +from antarest.study.business.areas.st_storage_management import STStorageManager from antarest.study.business.areas.thermal_management import ThermalManager from antarest.study.business.binding_constraint_management import BindingConstraintManager from antarest.study.business.config_management import ConfigManager @@ -59,7 +61,6 @@ from antarest.study.business.optimization_management import OptimizationManager from antarest.study.business.playlist_management import PlaylistManager from antarest.study.business.scenario_builder_management import ScenarioBuilderManager -from antarest.study.business.st_storage_manager import STStorageManager from antarest.study.business.table_mode_management import TableModeManager from antarest.study.business.thematic_trimming_management import ThematicTrimmingManager from antarest.study.business.timeseries_config_management import TimeSeriesConfigManager @@ -97,7 +98,12 @@ 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, upgrade_study +from antarest.study.storage.study_upgrader import ( + find_next_version, + get_current_version, + should_study_be_denormalized, + upgrade_study, +) from antarest.study.storage.utils import ( assert_permission, get_default_workspace_path, @@ -148,19 +154,23 @@ def _upgrade_study(self) -> None: """Run the task (lock the database).""" study_id: str = self._study_id target_version: str = self._target_version + is_study_denormalized = False with db(): # TODO We want to verify that a study doesn't have children and if it does do we upgrade all of them ? study_to_upgrade = self.repository.one(study_id) is_variant = isinstance(study_to_upgrade, VariantStudy) - if is_managed(study_to_upgrade) and not is_variant: - file_study = self.storage_service.get_storage(study_to_upgrade).get_raw(study_to_upgrade) - file_study.tree.denormalize() try: # sourcery skip: extract-method if is_variant: 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): + # 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) remove_from_cache(self.cache_service, study_to_upgrade.id) study_to_upgrade.version = target_version @@ -173,7 +183,7 @@ def _upgrade_study(self) -> None: ) ) finally: - if is_managed(study_to_upgrade) and not is_variant: + if is_study_denormalized: file_study = self.storage_service.get_storage(study_to_upgrade).get_raw(study_to_upgrade) file_study.tree.normalize() @@ -347,38 +357,33 @@ def save_logs( ) stopwatch.log_elapsed(lambda t: logger.info(f"Saved logs for job {job_id} in {t}s")) - def get_comments( - self, - uuid: str, - params: RequestParameters, - ) -> Union[str, JSON]: + def get_comments(self, study_id: str, params: RequestParameters) -> Union[str, JSON]: """ - Get study data inside filesystem + Get the comments of a study. + Args: - uuid: study uuid - params: request parameters + study_id: The ID of the study. + params: The parameters of the HTTP request containing the user information. - Returns: data study formatted in json + Returns: textual comments of the study. """ - study = self.get_study(uuid) + study = self.get_study(study_id) assert_permission(params.user, study, StudyPermissionType.READ) output: Union[str, JSON] + raw_study_service = self.storage_service.raw_study_service + variant_study_service = self.storage_service.variant_study_service if isinstance(study, RawStudy): - output = self.storage_service.get_storage(study).get(metadata=study, url="/settings/comments", depth=-1) + output = raw_study_service.get(metadata=study, url="/settings/comments") elif isinstance(study, VariantStudy): - patch = self.storage_service.raw_study_service.patch_service.get(study) - output = (patch.study or PatchStudy()).comments or self.storage_service.get_storage(study).get( - metadata=study, url="/settings/comments", depth=-1 - ) + patch = raw_study_service.patch_service.get(study) + patch_study = PatchStudy() if patch.study is None else patch.study + output = patch_study.comments or variant_study_service.get(metadata=study, url="/settings/comments") else: raise StudyTypeUnsupported(study.id, study.type) - try: - # try to decode string + with contextlib.suppress(AttributeError, UnicodeDecodeError): output = output.decode("utf-8") # type: ignore - except (AttributeError, UnicodeDecodeError): - pass return output @@ -534,15 +539,15 @@ def update_study_information( self._assert_study_unarchived(study) study_settings = self.storage_service.get_storage(study).get(study, study_settings_url) study_settings["horizon"] = metadata_patch.horizon - self._edit_study_using_command(study=study, url=study_settings_url, data=study_settings) + if metadata_patch.author: study_antares_url = "study/antares" self._assert_study_unarchived(study) study_antares = self.storage_service.get_storage(study).get(study, study_antares_url) study_antares["author"] = metadata_patch.author - self._edit_study_using_command(study=study, url=study_antares_url, data=study_antares) + study.additional_data = study.additional_data or StudyAdditionalData() if metadata_patch.name: study.name = metadata_patch.name @@ -658,19 +663,20 @@ def get_user_name(self, params: RequestParameters) -> str: def get_study_synthesis(self, study_id: str, params: RequestParameters) -> FileStudyTreeConfigDTO: """ - Return study synthesis + Get the synthesis of a study. + Args: - study_id: study id - params: request parameters + study_id: The ID of the study. + params: The parameters of the HTTP request containing the user information. Returns: study synthesis - """ study = self.get_study(study_id) assert_permission(params.user, study, StudyPermissionType.READ) study.last_access = datetime.utcnow() self.repository.save(study, update_in_listing=False) - return self.storage_service.get_storage(study).get_synthesis(study, params) + study_storage_service = self.storage_service.get_storage(study) + return study_storage_service.get_synthesis(study, params) def get_input_matrix_startdate(self, study_id: str, path: Optional[str], params: RequestParameters) -> MatrixIndex: study = self.get_study(study_id) @@ -692,7 +698,7 @@ def remove_duplicates(self) -> None: for study in self.repository.get_all(): if isinstance(study, RawStudy) and not study.archived: path = str(study.path) - if not path in study_paths: + if path not in study_paths: study_paths[path] = [] study_paths[path].append(study.id) @@ -1041,7 +1047,7 @@ def delete_study(self, uuid: str, children: bool, params: RequestParameters) -> """ study = self.get_study(uuid) - assert_permission(params.user, study, StudyPermissionType.DELETE) + assert_permission(params.user, study, StudyPermissionType.WRITE) study_info = study.to_json_summary() @@ -1272,20 +1278,23 @@ def set_sim_reference( def import_study( self, - stream: IO[bytes], + stream: BinaryIO, group_ids: List[str], params: RequestParameters, ) -> str: """ - Import zipped study. + Import a compressed study. Args: - stream: zip file + stream: binary content of the study compressed in ZIP or 7z format. group_ids: group to attach to study params: request parameters - Returns: new study uuid + Returns: + New study UUID. + Raises: + BadArchiveContent: If the archive is corrupted or in an unknown format. """ sid = str(uuid4()) path = str(get_default_workspace_path(self.config) / sid) @@ -1321,7 +1330,7 @@ def import_study( def import_output( self, uuid: str, - output: Union[IO[bytes], Path], + output: Union[BinaryIO, Path], params: RequestParameters, output_name_suffix: Optional[str] = None, auto_unzip: bool = True, @@ -1408,20 +1417,50 @@ def _create_edit_study_command( ) raise NotImplementedError() - def _edit_study_using_command(self, study: Study, url: str, data: SUB_JSON) -> ICommand: + def _edit_study_using_command( + self, + study: Study, + url: str, + data: SUB_JSON, + *, + create_missing: bool = False, + ) -> ICommand: """ - Replace data on disk with new, using ICommand + Replace data on disk with new, using variant commands. + + In addition to regular configuration changes, this function also allows the end user + to store files on disk, in the "user" directory of the study (without using variant commands). + Args: study: study url: data path to reach data: new data to replace + create_missing: Flag to indicate whether to create file or parent directories if missing. """ - study_service = self.storage_service.get_storage(study) file_study = study_service.get_raw(metadata=study) - tree_node = file_study.tree.get_node(url.split("/")) + + file_relpath = PurePosixPath(url.strip().strip("/")) + file_path = study_service.get_study_path(study).joinpath(file_relpath) + create_missing &= not file_path.exists() + if create_missing: + # IMPORTANT: We prohibit deep file system changes in private directories. + # - File and directory creation is only possible for the "user" directory, + # because the "input" and "output" directories are managed by Antares. + # - We also prohibit writing files in the "user/expansion" folder which currently + # contains the Xpansion tool configuration. + # This configuration should be moved to the "input/expansion" directory in the future. + if file_relpath and file_relpath.parts[0] == "user" and file_relpath.parts[1] != "expansion": + # In the case of variants, we must write the file directly in the study's snapshot folder, + # because the "user" folder is not managed by the command mechanism. + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.touch() + + # A 404 Not Found error is raised if the file does not exist. + tree_node = file_study.tree.get_node(file_relpath.parts) # type: ignore command = self._create_edit_study_command(tree_node=tree_node, url=url, data=data) + if isinstance(study_service, RawStudyService): res = command.apply(study_data=file_study) if not is_managed(study): @@ -1429,11 +1468,12 @@ def _edit_study_using_command(self, study: Study, url: str, data: SUB_JSON) -> I if not res.status: raise CommandApplicationError(res.message) - lastsave_url = "study/antares/lastsave" - lastsave_node = file_study.tree.get_node(lastsave_url.split("/")) - self._create_edit_study_command(tree_node=lastsave_node, url=lastsave_url, data=int(time())).apply( - file_study - ) + # noinspection SpellCheckingInspection + url = "study/antares/lastsave" + last_save_node = file_study.tree.get_node(url.split("/")) + cmd = self._create_edit_study_command(tree_node=last_save_node, url=url, data=int(time())) + cmd.apply(file_study) + self.storage_service.variant_study_service.invalidate_cache(study) elif isinstance(study_service, VariantStudyService): @@ -1442,8 +1482,10 @@ def _edit_study_using_command(self, study: Study, url: str, data: SUB_JSON) -> I command=command.to_dto(), params=RequestParameters(user=DEFAULT_ADMIN_USER), ) - else: - raise NotImplementedError() + + else: # pragma: no cover + raise TypeError(repr(type(study_service))) + return command # for testing purpose def apply_commands(self, uuid: str, commands: List[CommandDTO], params: RequestParameters) -> Optional[List[str]]: @@ -1483,6 +1525,8 @@ def edit_study( url: str, new: SUB_JSON, params: RequestParameters, + *, + create_missing: bool = False, ) -> JSON: """ Replace data inside study. @@ -1492,15 +1536,15 @@ def edit_study( url: path data target in study new: new data to replace params: request parameters + create_missing: Flag to indicate whether to create file or parent directories if missing. Returns: new data replaced - """ study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) - self._edit_study_using_command(study=study, url=url.strip().strip("/"), data=new) + self._edit_study_using_command(study=study, url=url.strip().strip("/"), data=new, create_missing=create_missing) self.event_bus.push( Event( @@ -1542,11 +1586,8 @@ def change_owner(self, study_id: str, owner_id: int, params: RequestParameters) ) ) - self._edit_study_using_command( - study=study, - url="study/antares/author", - data=new_owner.name if new_owner is not None else None, - ) + owner_name = None if new_owner is None else new_owner.name + self._edit_study_using_command(study=study, url="study/antares/author", data=owner_name) logger.info( "user %s change study %s owner to %d", @@ -1790,7 +1831,7 @@ def delete_link( def archive(self, uuid: str, params: RequestParameters) -> str: logger.info(f"Archiving study {uuid}") study = self.get_study(uuid) - assert_permission(params.user, study, StudyPermissionType.DELETE) + assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) @@ -1848,7 +1889,7 @@ def unarchive(self, uuid: str, params: RequestParameters) -> str: ): raise TaskAlreadyRunning() - assert_permission(params.user, study, StudyPermissionType.DELETE) + assert_permission(params.user, study, StudyPermissionType.WRITE) if not isinstance(study, RawStudy): raise StudyTypeUnsupported(study.id, study.type) @@ -1910,13 +1951,17 @@ def _save_study( study.content_status = content_status study.owner = self.user_service.get_user(owner.impersonator, params=RequestParameters(user=owner)) - groups = [] + + study.groups.clear() for gid in group_ids: - group = next(filter(lambda g: g.id == gid, owner.groups), None) - if group is None or not group.role.is_higher_or_equals(RoleType.WRITER) and not owner.is_site_admin(): + jwt_group: Optional[JWTGroup] = next(filter(lambda g: g.id == gid, owner.groups), None) # type: ignore + if ( + jwt_group is None + or jwt_group.role is None + or (jwt_group.role < RoleType.WRITER and not owner.is_site_admin()) + ): raise UserHasNotPermissionError(f"Permission denied for group ID: {gid}") - groups.append(Group(id=group.id, name=group.name)) - study.groups = groups + study.groups.append(Group(id=jwt_group.id, name=jwt_group.name)) self.repository.save(study) @@ -2179,7 +2224,7 @@ def unarchive_output( params: RequestParameters, ) -> Optional[str]: study = self.get_study(study_id) - assert_permission(params.user, study, StudyPermissionType.WRITE) + assert_permission(params.user, study, StudyPermissionType.READ) self._assert_study_unarchived(study) archive_task_names = StudyService._get_output_archive_task_names(study, output_id) diff --git a/antarest/study/storage/abstract_storage_service.py b/antarest/study/storage/abstract_storage_service.py index f056839ac6..60cd48b782 100644 --- a/antarest/study/storage/abstract_storage_service.py +++ b/antarest/study/storage/abstract_storage_service.py @@ -3,14 +3,14 @@ import tempfile from abc import ABC from pathlib import Path -from typing import IO, List, Optional, Union +from typing import BinaryIO, List, Optional, Union from uuid import uuid4 from antarest.core.config import Config from antarest.core.exceptions import BadOutputError, StudyOutputNotFoundError from antarest.core.interfaces.cache import CacheConstants, ICache from antarest.core.model import JSON -from antarest.core.utils.utils import StopWatch, assert_this, extract_zip, unzip, zip_dir +from antarest.core.utils.utils import StopWatch, extract_zip, unzip, zip_dir from antarest.study.common.studystorage import IStudyStorageService, T from antarest.study.common.utils import get_study_information from antarest.study.model import ( @@ -163,22 +163,27 @@ def get_study_sim_result( def import_output( self, metadata: T, - output: Union[IO[bytes], Path], + output: Union[BinaryIO, Path], output_name: Optional[str] = None, ) -> Optional[str]: """ - Import additional output in an existing study + Import additional output in an existing study. + Args: metadata: study output: new output (path or zipped data) output_name: optional suffix name to append to output name - Returns: output id + Returns: + Output identifier. + + Raises: + BadArchiveContent: If the output archive is corrupted or in an unknown format. """ path_output = Path(metadata.path) / "output" / f"imported_output_{str(uuid4())}" study_id = metadata.id path_output.mkdir(parents=True) - output_full_name: Optional[str] = None + output_full_name: Optional[str] is_zipped = False stopwatch = StopWatch() try: diff --git a/antarest/study/storage/rawstudy/model/filesystem/common/area_matrix_list.py b/antarest/study/storage/rawstudy/model/filesystem/common/area_matrix_list.py index b08c47ebf7..1d39a72a66 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/common/area_matrix_list.py +++ b/antarest/study/storage/rawstudy/model/filesystem/common/area_matrix_list.py @@ -104,9 +104,12 @@ def __init__( self.matrix_class = matrix_class def build(self) -> TREE: + # Note that cluster IDs are case-insensitive, but series IDs are in lower case. + # For instance, if your cluster ID is "Base", then the series ID will be "base". + series_ids = map(str.lower, self.config.get_thermal_ids(self.area)) children: TREE = { - thermal_cluster: self.matrix_class(self.context, self.config.next_file(f"{thermal_cluster}.txt")) - for thermal_cluster in self.config.get_thermal_names(self.area) + series_id: self.matrix_class(self.context, self.config.next_file(f"{series_id}.txt")) + for series_id in series_ids } return children diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/cluster.py b/antarest/study/storage/rawstudy/model/filesystem/config/cluster.py new file mode 100644 index 0000000000..2c7053e3ce --- /dev/null +++ b/antarest/study/storage/rawstudy/model/filesystem/config/cluster.py @@ -0,0 +1,92 @@ +""" +Common properties related to thermal and renewable clusters, and short-term storage. + +In the near future, this set of classes may be used for solar, wind and hydro clusters. +""" +import functools +import typing as t + +from pydantic import BaseModel, Extra, Field + +__all__ = ("ItemProperties", "ClusterProperties") + + +@functools.total_ordering +class ItemProperties( + BaseModel, + extra=Extra.forbid, + validate_assignment=True, + allow_population_by_field_name=True, +): + """ + Common properties related to thermal and renewable clusters, and short-term storage. + + Usage: + + >>> from antarest.study.storage.rawstudy.model.filesystem.config.cluster import ItemProperties + + >>> cl1 = ItemProperties(name="cluster-01", group="group-A") + >>> cl2 = ItemProperties(name="CLUSTER-01", group="Group-B") + >>> cl3 = ItemProperties(name="cluster-02", group="GROUP-A") + >>> l = [cl1, cl2, cl3] + >>> l.sort() + >>> [(c.group, c.name) for c in l] + [('group-A', 'cluster-01'), ('GROUP-A', 'cluster-02'), ('Group-B', 'CLUSTER-01')] + """ + + group: str = Field(default="", description="Cluster group") + + name: str = Field(description="Cluster name", regex=r"[a-zA-Z0-9_(),& -]+") + + def __lt__(self, other: t.Any) -> bool: + """ + Compare two clusters by group and name. + + This method may be used to sort and group clusters by `group` and `name`. + """ + if isinstance(other, ItemProperties): + return (self.group.upper(), self.name.upper()).__lt__((other.group.upper(), other.name.upper())) + return NotImplemented + + +class ClusterProperties(ItemProperties): + """ + Properties of a thermal or renewable cluster read from the configuration files. + + Usage: + + >>> from antarest.study.storage.rawstudy.model.filesystem.config.cluster import ClusterProperties + + >>> cl1 = ClusterProperties(name="cluster-01", group="group-A", enabled=True, unit_count=2, nominal_capacity=100) + >>> (cl1.installed_capacity, cl1.enabled_capacity) + (200.0, 200.0) + + >>> cl2 = ClusterProperties(name="cluster-01", group="group-A", enabled=False, unit_count=2, nominal_capacity=100) + >>> (cl2.installed_capacity, cl2.enabled_capacity) + (200.0, 0.0) + """ + + # Activity status: + # - True: the plant may generate. + # - False: not yet commissioned, moth-balled, etc. + enabled: bool = Field(default=True, description="Activity status") + + # noinspection SpellCheckingInspection + unit_count: int = Field(default=1, ge=1, description="Unit count", alias="unitcount") + + # noinspection SpellCheckingInspection + nominal_capacity: float = Field( + default=0.0, + ge=0, + description="Nominal capacity (MW per unit)", + alias="nominalcapacity", + ) + + @property + def installed_capacity(self) -> float: + """""" + return self.unit_count * self.nominal_capacity + + @property + def enabled_capacity(self) -> float: + return self.enabled * self.installed_capacity diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/files.py b/antarest/study/storage/rawstudy/model/filesystem/config/files.py index 8e174f3ea0..6f07b03adf 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/files.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/files.py @@ -20,14 +20,21 @@ ) from antarest.study.storage.rawstudy.model.filesystem.config.model import ( Area, - Cluster, DistrictSet, FileStudyTreeConfig, Link, Simulation, transform_name_to_id, ) -from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import STStorageConfig +from antarest.study.storage.rawstudy.model.filesystem.config.renewable import ( + RenewableConfigType, + create_renewable_config, +) +from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import ( + STStorageConfigType, + create_st_storage_config, +) +from antarest.study.storage.rawstudy.model.filesystem.config.thermal import ThermalConfigType, create_thermal_config from antarest.study.storage.rawstudy.model.filesystem.root.settings.generaldata import DUPLICATE_KEYS logger = logging.getLogger(__name__) @@ -333,56 +340,70 @@ def parse_area(root: Path, area: str) -> "Area": ) -def _parse_thermal(root: Path, area: str) -> List[Cluster]: - list_ini = _extract_data_from_file( +def _parse_thermal(root: Path, area: str) -> List[ThermalConfigType]: + """ + Parse the thermal INI file, return an empty list if missing. + """ + version = _parse_version(root) + relpath = Path(f"input/thermal/clusters/{area}/list.ini") + config_dict: Dict[str, Any] = _extract_data_from_file( + root=root, inside_root_path=relpath, file_type=FileType.SIMPLE_INI + ) + config_list = [] + for section, values in config_dict.items(): + try: + config_list.append(create_thermal_config(version, **values, id=section)) + except ValueError as exc: + config_path = root.joinpath(relpath) + logger.warning(f"Invalid thermal configuration: '{section}' in '{config_path}'", exc_info=exc) + return config_list + + +def _parse_renewables(root: Path, area: str) -> List[RenewableConfigType]: + """ + Parse the renewables INI file, return an empty list if missing. + """ + version = _parse_version(root) + relpath = Path(f"input/renewables/clusters/{area}/list.ini") + config_dict: Dict[str, Any] = _extract_data_from_file( root=root, - inside_root_path=Path(f"input/thermal/clusters/{area}/list.ini"), + inside_root_path=relpath, file_type=FileType.SIMPLE_INI, ) - return [ - Cluster( - id=transform_name_to_id(key), - enabled=list_ini.get(key, {}).get("enabled", True), - name=list_ini.get(key, {}).get("name", key), - ) - for key in list(list_ini.keys()) - ] + config_list = [] + for section, values in config_dict.items(): + try: + config_list.append(create_renewable_config(version, **values, id=section)) + except ValueError as exc: + config_path = root.joinpath(relpath) + logger.warning(f"Invalid renewable configuration: '{section}' in '{config_path}'", exc_info=exc) + return config_list -def _parse_st_storage(root: Path, area: str) -> List[STStorageConfig]: +def _parse_st_storage(root: Path, area: str) -> List[STStorageConfigType]: """ Parse the short-term storage INI file, return an empty list if missing. """ # st_storage feature exists only since 8.6 version - if _parse_version(root) < 860: + version = _parse_version(root) + if version < 860: return [] + relpath = Path(f"input/st-storage/clusters/{area}/list.ini") config_dict: Dict[str, Any] = _extract_data_from_file( root=root, - inside_root_path=Path(f"input/st-storage/clusters/{area}/list.ini"), + inside_root_path=relpath, file_type=FileType.SIMPLE_INI, ) - return [STStorageConfig(**values, id=storage_id) for storage_id, values in config_dict.items()] - - -def _parse_renewables(root: Path, area: str) -> List[Cluster]: - try: - list_ini = _extract_data_from_file( - root=root, - inside_root_path=Path(f"input/renewables/clusters/{area}/list.ini"), - file_type=FileType.SIMPLE_INI, - ) - return [ - Cluster( - id=transform_name_to_id(key), - enabled=list_ini.get(key, {}).get("enabled", True), - name=list_ini.get(key, {}).get("name", None), - ) - for key in list(list_ini.keys()) - ] - except Exception: - return [] + config_list = [] + for section, values in config_dict.items(): + try: + config_list.append(create_st_storage_config(version, **values, id=section)) + except ValueError as exc: + config_path = root.joinpath(relpath) + logger.warning(f"Invalid short-term storage configuration: '{section}' in '{config_path}'", exc_info=exc) + return config_list def _parse_links(root: Path, area: str) -> Dict[str, Link]: @@ -412,3 +433,15 @@ def _parse_filters_year(root: Path, area: str) -> List[str]: ) filters: str = optimization["filtering"]["filter-year-by-year"] return Link.split(filters) + + +def _check_build_on_solver_tests(test_dir: Path) -> None: + for antares_path in test_dir.rglob("study.antares"): + study_path = antares_path.parent + print(f"Checking '{study_path}'...") + build(study_path, "test") + + +if __name__ == "__main__": + TEST_DIR = Path("~/Projects/antarest_data/studies/Antares_Simulator_Tests_NR").expanduser() + _check_build_on_solver_tests(TEST_DIR) diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/identifier.py b/antarest/study/storage/rawstudy/model/filesystem/config/identifier.py new file mode 100644 index 0000000000..b4b1a3c3f3 --- /dev/null +++ b/antarest/study/storage/rawstudy/model/filesystem/config/identifier.py @@ -0,0 +1,83 @@ +import typing as t + +from pydantic import BaseModel, Extra, Field, root_validator + +__all__ = ("IgnoreCaseIdentifier", "LowerCaseIdentifier") + + +class IgnoreCaseIdentifier( + BaseModel, + extra=Extra.forbid, + validate_assignment=True, + allow_population_by_field_name=True, +): + """ + Base class for all configuration sections with an ID. + """ + + id: str = Field(description="ID (section name)", regex=r"[a-zA-Z0-9_(),& -]+") + + @classmethod + def generate_id(cls, name: str) -> str: + """ + Generate an ID from a name. + + Args: + name: Name of a section read from an INI file + + Returns: + The ID of the section. + """ + # Avoid circular imports + from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id + + return transform_name_to_id(name, lower=False) + + @root_validator(pre=True) + def validate_id(cls, values: t.MutableMapping[str, t.Any]) -> t.Mapping[str, t.Any]: + """ + Calculate an ID based on the name, if not provided. + + Args: + values: values used to construct the object. + + Returns: + The updated values. + """ + if storage_id := values.get("id"): + # If the ID is provided, it comes from a INI section name. + # In some legacy case, the ID was in lower case, so we need to convert it. + values["id"] = cls.generate_id(storage_id) + return values + if not values.get("name"): + return values + name = values["name"] + if storage_id := cls.generate_id(name): + values["id"] = storage_id + else: + raise ValueError(f"Invalid name '{name}'.") + return values + + +class LowerCaseIdentifier(IgnoreCaseIdentifier): + """ + Base class for all configuration sections with a lower case ID. + """ + + id: str = Field(description="ID (section name)", regex=r"[a-z0-9_(),& -]+") + + @classmethod + def generate_id(cls, name: str) -> str: + """ + Generate an ID from a name. + + Args: + name: Name of a section read from an INI file + + Returns: + The ID of the section. + """ + # Avoid circular imports + from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id + + return transform_name_to_id(name, lower=True) diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/model.py b/antarest/study/storage/rawstudy/model/filesystem/config/model.py index 774d225344..79400d8165 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/model.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/model.py @@ -10,7 +10,9 @@ from antarest.core.utils.utils import DTO from .binding_constraint import BindingConstraintDTO -from .st_storage import STStorageConfig +from .renewable import RenewableConfigType +from .st_storage import STStorageConfigType +from .thermal import ThermalConfigType class ENR_MODELLING(Enum): @@ -18,16 +20,6 @@ class ENR_MODELLING(Enum): CLUSTERS = "clusters" -class Cluster(BaseModel): - """ - Object linked to /input/thermal/clusters//list.ini - """ - - id: str - name: str - enabled: bool = True - - class Link(BaseModel): """ Object linked to /input/links//properties.ini information @@ -58,12 +50,12 @@ class Config: name: str links: Dict[str, Link] - thermals: List[Cluster] - renewables: List[Cluster] + thermals: List[ThermalConfigType] + renewables: List[RenewableConfigType] filters_synthesis: List[str] filters_year: List[str] # since v8.6 - st_storages: List[STStorageConfig] = [] + st_storages: List[STStorageConfigType] = [] class DistrictSet(BaseModel): @@ -87,7 +79,7 @@ def get_areas(self, all_areas: List[str]) -> List[str]: class Simulation(BaseModel): """ - Object linked to /output//about-the-study/** informations + Object linked to /output//about-the-study/** information """ name: str @@ -193,30 +185,23 @@ def set_names(self, only_output: bool = True) -> List[str]: [k for k, v in self.sets.items() if v.output or not only_output], ) - def get_thermal_names(self, area: str, only_enabled: bool = False) -> List[str]: - return self.cache.get( - f"%thermal%{area}%{only_enabled}%{area}", - [thermal.id for thermal in self.areas[area].thermals if not only_enabled or thermal.enabled], - ) + def get_thermal_ids(self, area: str) -> List[str]: + """ + Returns a list of thermal cluster IDs for a given area. + Note that IDs may not be in lower case (but series IDs are). + """ + return self.cache.get(f"%thermal%{area}%{area}", [th.id for th in self.areas[area].thermals]) + + def get_renewable_ids(self, area: str) -> List[str]: + """ + Returns a list of renewable cluster IDs for a given area. + Note that IDs may not be in lower case (but series IDs are). + """ + return self.cache.get(f"%renewable%{area}", [r.id for r in self.areas[area].renewables]) def get_st_storage_ids(self, area: str) -> List[str]: return self.cache.get(f"%st-storage%{area}", [s.id for s in self.areas[area].st_storages]) - def get_renewable_names( - self, - area: str, - only_enabled: bool = False, - section_name: bool = True, - ) -> List[str]: - return self.cache.get( - f"%renewable%{area}%{only_enabled}%{section_name}", - [ - renewable.id if section_name else renewable.name - for renewable in self.areas[area].renewables - if not only_enabled or renewable.enabled - ], - ) - def get_links(self, area: str) -> List[str]: return self.cache.get(f"%links%{area}", list(self.areas[area].links.keys())) diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/renewable.py b/antarest/study/storage/rawstudy/model/filesystem/config/renewable.py new file mode 100644 index 0000000000..3c53c23053 --- /dev/null +++ b/antarest/study/storage/rawstudy/model/filesystem/config/renewable.py @@ -0,0 +1,121 @@ +import typing as t + +from pydantic import Field + +from antarest.study.business.enum_ignore_case import EnumIgnoreCase +from antarest.study.storage.rawstudy.model.filesystem.config.cluster import ClusterProperties +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import IgnoreCaseIdentifier + +__all__ = ( + "TimeSeriesInterpretation", + "RenewableProperties", + "RenewableConfig", + "RenewableConfigType", + "create_renewable_config", +) + + +class TimeSeriesInterpretation(EnumIgnoreCase): + """ + Timeseries mode: + + - Power generation means that the unit of the timeseries is in MW, + - Production factor means that the unit of the timeseries is in p.u. + (between 0 and 1, 1 meaning the full installed capacity) + """ + + POWER_GENERATION = "power-generation" + PRODUCTION_FACTOR = "production-factor" + + +class RenewableClusterGroup(EnumIgnoreCase): + """ + Renewable cluster groups. + + The group can be any one of the following: + "Wind Onshore", "Wind Offshore", "Solar Thermal", "Solar PV", "Solar Rooftop", + "Other RES 1", "Other RES 2", "Other RES 3", or "Other RES 4". + If not specified, the renewable cluster will be part of the group "Other RES 1". + """ + + THERMAL_SOLAR = "Solar Thermal" + PV_SOLAR = "Solar PV" + ROOFTOP_SOLAR = "Solar Rooftop" + WIND_ON_SHORE = "Wind Onshore" + WIND_OFF_SHORE = "Wind Offshore" + OTHER1 = "Other RES 1" + OTHER2 = "Other RES 2" + OTHER3 = "Other RES 3" + OTHER4 = "Other RES 4" + + def __repr__(self) -> str: + return f"{self.__class__.__name__}.{self.name}" + + @classmethod + def _missing_(cls, value: object) -> t.Optional["RenewableClusterGroup"]: + """ + Retrieves the default group or the matched group when an unknown value is encountered. + """ + if isinstance(value, str): + # Check if any group value matches the input value ignoring case sensitivity. + # noinspection PyUnresolvedReferences + if any(value.upper() == group.value.upper() for group in cls): + return t.cast(RenewableClusterGroup, super()._missing_(value)) + # If a group is not found, return the default group ('OTHER1' by default). + return cls.OTHER1 + return t.cast(t.Optional["RenewableClusterGroup"], super()._missing_(value)) + + +class RenewableProperties(ClusterProperties): + """ + Properties of a renewable cluster read from the configuration files. + """ + + group: RenewableClusterGroup = Field( + default=RenewableClusterGroup.OTHER1, + description="Renewable Cluster Group", + ) + + ts_interpretation: TimeSeriesInterpretation = Field( + default=TimeSeriesInterpretation.POWER_GENERATION, + description="Time series interpretation", + alias="ts-interpretation", + ) + + +class RenewableConfig(RenewableProperties, IgnoreCaseIdentifier): + """ + Configuration of a renewable cluster. + + Usage: + + >>> from antarest.study.storage.rawstudy.model.filesystem.config.renewable import RenewableConfig + + >>> cfg = RenewableConfig(name="cluster-01") + >>> cfg.id + 'cluster-01' + >>> cfg.enabled + True + >>> cfg.ts_interpretation.value + 'power-generation' + """ + + +RenewableConfigType = RenewableConfig + + +def create_renewable_config(study_version: t.Union[str, int], **kwargs: t.Any) -> RenewableConfigType: + """ + Factory method to create a renewable configuration model. + + Args: + study_version: The version of the study. + **kwargs: The properties to be used to initialize the model. + + Returns: + The renewable configuration model. + + Raises: + ValueError: If the study version is not supported. + """ + return RenewableConfig(**kwargs) diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/st_storage.py b/antarest/study/storage/rawstudy/model/filesystem/config/st_storage.py index b82910a191..58efc0ceb8 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/st_storage.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/st_storage.py @@ -1,8 +1,18 @@ -from typing import Any, Dict +import typing as t -from pydantic import BaseModel, Extra, Field, root_validator +from pydantic import Field from antarest.study.business.enum_ignore_case import EnumIgnoreCase +from antarest.study.storage.rawstudy.model.filesystem.config.cluster import ItemProperties +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import LowerCaseIdentifier + +__all__ = ( + "STStorageGroup", + "STStorageProperties", + "STStorageConfig", + "STStorageConfigType", + "create_st_storage_config", +) class STStorageGroup(EnumIgnoreCase): @@ -30,22 +40,13 @@ class STStorageGroup(EnumIgnoreCase): # noinspection SpellCheckingInspection -class STStorageProperties( - BaseModel, - extra=Extra.forbid, - validate_assignment=True, - allow_population_by_field_name=True, -): +class STStorageProperties(ItemProperties): """ Properties of a short-term storage system read from the configuration files. All aliases match the name of the corresponding field in the INI files. """ - name: str = Field( - description="Short-term storage name", - regex=r"[a-zA-Z0-9_(),& -]+", - ) group: STStorageGroup = Field( STStorageGroup.OTHER1, description="Energy storage system group", @@ -70,14 +71,16 @@ class STStorageProperties( ) efficiency: float = Field( 1, - description="Efficiency of the storage system", + description="Efficiency of the storage system (%)", ge=0, le=1, ) + # The `initial_level` value must be between 0 and 1, but the default value is 0. initial_level: float = Field( - 0, - description="Initial level of the storage system", + 0.5, + description="Initial level of the storage system (%)", ge=0, + le=1, alias="initiallevel", ) initial_level_optim: bool = Field( @@ -88,38 +91,49 @@ class STStorageProperties( # noinspection SpellCheckingInspection -class STStorageConfig(STStorageProperties): +class STStorageConfig(STStorageProperties, LowerCaseIdentifier): """ Manage the configuration files in the context of Short-Term Storage. It provides a convenient way to read and write configuration data from/to an INI file format. + + Usage: + + >>> from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import STStorageConfig + + >>> st = STStorageConfig(name="Storage 1", group="battery", injection_nominal_capacity=1500) + >>> st.id + 'storage 1' + >>> st.group == STStorageGroup.BATTERY + True + >>> st.injection_nominal_capacity + 1500.0 + >>> st.injection_nominal_capacity = -897.32 + Traceback (most recent call last): + ... + pydantic.error_wrappers.ValidationError: 1 validation error for STStorageConfig + injection_nominal_capacity + ensure this value is greater than or equal to 0 (type=value_error.number.not_ge; limit_value=0) """ - # The `id` field is a calculated from the `name` if not provided. - # This value must be stored in the config cache. - id: str = Field( - description="Short-term storage ID", - regex=r"[a-zA-Z0-9_(),& -]+", - ) - @root_validator(pre=True) - def calculate_storage_id(cls, values: Dict[str, Any]) -> Dict[str, Any]: - """ - Calculate the short-term storage ID based on the storage name, if not provided. - - Args: - values: values used to construct the object. - - Returns: - The updated values. - """ - # Avoid circular imports - from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id - - if values.get("id") or not values.get("name"): - return values - storage_name = values["name"] - if storage_id := transform_name_to_id(storage_name): - values["id"] = storage_id - else: - raise ValueError(f"Invalid short term storage name '{storage_name}'.") - return values +STStorageConfigType = STStorageConfig + + +def create_st_storage_config(study_version: t.Union[str, int], **kwargs: t.Any) -> STStorageConfigType: + """ + Factory method to create a short-term storage configuration model. + + Args: + study_version: The version of the study. + **kwargs: The properties to be used to initialize the model. + + Returns: + The short-term storage configuration model. + + Raises: + ValueError: If the study version is not supported. + """ + version = int(study_version) + if version < 860: + raise ValueError(f"Unsupported study version: {version}") + return STStorageConfig(**kwargs) diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/thermal.py b/antarest/study/storage/rawstudy/model/filesystem/config/thermal.py new file mode 100644 index 0000000000..bfef19fe2d --- /dev/null +++ b/antarest/study/storage/rawstudy/model/filesystem/config/thermal.py @@ -0,0 +1,326 @@ +import typing as t + +from pydantic import Field + +from antarest.study.business.enum_ignore_case import EnumIgnoreCase +from antarest.study.storage.rawstudy.model.filesystem.config.cluster import ClusterProperties +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import IgnoreCaseIdentifier + +__all__ = ( + "TimeSeriesGenerationOption", + "LawOption", + "ThermalClusterGroup", + "ThermalProperties", + "Thermal860Properties", + "ThermalConfig", + "Thermal860Config", + "ThermalConfigType", + "create_thermal_config", +) + + +class TimeSeriesGenerationOption(EnumIgnoreCase): + """ + Options related to time series generation. + The option `USE_GLOBAL_PARAMETER` is used by default. + """ + + USE_GLOBAL_PARAMETER = "use global parameter" + FORCE_NO_GENERATION = "force no generation" + FORCE_GENERATION = "force generation" + + def __repr__(self) -> str: + return f"{self.__class__.__name__}.{self.name}" + + +class LawOption(EnumIgnoreCase): + """ + Law options used for series generation. + The UNIFORM `law` is used by default. + """ + + UNIFORM = "uniform" + GEOMETRIC = "geometric" + + def __repr__(self) -> str: + return f"{self.__class__.__name__}.{self.name}" + + +class ThermalClusterGroup(EnumIgnoreCase): + """ + Thermal cluster groups. + The group `OTHER1` is used by default. + """ + + NUCLEAR = "Nuclear" + LIGNITE = "Lignite" + HARD_COAL = "Hard Coal" + GAS = "Gas" + OIL = "Oil" + MIXED_FUEL = "Mixed Fuel" + OTHER1 = "Other 1" + OTHER2 = "Other 2" + OTHER3 = "Other 3" + OTHER4 = "Other 4" + + def __repr__(self) -> str: + return f"{self.__class__.__name__}.{self.name}" + + @classmethod + def _missing_(cls, value: object) -> t.Optional["ThermalClusterGroup"]: + """ + Retrieves the default group or the matched group when an unknown value is encountered. + """ + if isinstance(value, str): + # Check if any group value matches the input value ignoring case sensitivity. + # noinspection PyUnresolvedReferences + if any(value.upper() == group.value.upper() for group in cls): + return t.cast(ThermalClusterGroup, super()._missing_(value)) + # If a group is not found, return the default group ('OTHER1' by default). + # Note that 'OTHER' is an alias for 'OTHER1'. + return cls.OTHER1 + return t.cast(t.Optional["ThermalClusterGroup"], super()._missing_(value)) + + +class ThermalProperties(ClusterProperties): + """ + Thermal cluster configuration model. + This model describes the configuration parameters for a thermal cluster. + """ + + group: ThermalClusterGroup = Field( + default=ThermalClusterGroup.OTHER1, + description="Thermal Cluster Group", + ) + + gen_ts: TimeSeriesGenerationOption = Field( + default=TimeSeriesGenerationOption.USE_GLOBAL_PARAMETER, + description="Time Series Generation Option", + alias="gen-ts", + ) + min_stable_power: float = Field( + default=0.0, + description="Min. Stable Power (MW)", + alias="min-stable-power", + ) + min_up_time: int = Field( + default=1, + ge=1, + le=168, + description="Min. Up time (h)", + alias="min-up-time", + ) + min_down_time: int = Field( + default=1, + ge=1, + le=168, + description="Min. Down time (h)", + alias="min-down-time", + ) + must_run: bool = Field( + default=False, + description="Must run flag", + alias="must-run", + ) + spinning: float = Field( + default=0.0, + ge=0, + le=100, + description="Spinning (%)", + ) + volatility_forced: float = Field( + default=0.0, + ge=0, + le=1, + description="Forced Volatility", + alias="volatility.forced", + ) + volatility_planned: float = Field( + default=0.0, + ge=0, + le=1, + description="Planned volatility", + alias="volatility.planned", + ) + law_forced: LawOption = Field( + default=LawOption.UNIFORM, + description="Forced Law (ts-generator)", + alias="law.forced", + ) + law_planned: LawOption = Field( + default=LawOption.UNIFORM, + description="Planned Law (ts-generator)", + alias="law.planned", + ) + marginal_cost: float = Field( + default=0.0, + ge=0, + description="Marginal cost (euros/MWh)", + alias="marginal-cost", + ) + spread_cost: float = Field( + default=0.0, + ge=0, + description="Spread (euros/MWh)", + alias="spread-cost", + ) + fixed_cost: float = Field( + default=0.0, + ge=0, + description="Fixed cost (euros/hour)", + alias="fixed-cost", + ) + startup_cost: float = Field( + default=0.0, + ge=0, + description="Startup cost (euros/startup)", + alias="startup-cost", + ) + market_bid_cost: float = Field( + default=0.0, + ge=0, + description="Market bid cost (euros/MWh)", + alias="market-bid-cost", + ) + co2: float = Field( + default=0.0, + ge=0, + description="Emission rate of CO2 (t/MWh)", + ) + + +class Thermal860Properties(ThermalProperties): + """ + Thermal cluster configuration model for 860 study. + """ + + nh3: float = Field( + default=0.0, + ge=0, + description="Emission rate of NH3 (t/MWh)", + ) + so2: float = Field( + default=0.0, + ge=0, + description="Emission rate of SO2 (t/MWh)", + ) + nox: float = Field( + default=0.0, + ge=0, + description="Emission rate of NOX (t/MWh)", + ) + pm2_5: float = Field( + default=0.0, + ge=0, + description="Emission rate of PM 2.5 (t/MWh)", + alias="pm2_5", + ) + pm5: float = Field( + default=0.0, + ge=0, + description="Emission rate of PM 5 (t/MWh)", + ) + pm10: float = Field( + default=0.0, + ge=0, + description="Emission rate of PM 10 (t/MWh)", + ) + nmvoc: float = Field( + default=0.0, + ge=0, + description="Emission rate of NMVOC (t/MWh)", + ) + op1: float = Field( + default=0.0, + ge=0, + description="Emission rate of pollutant 1 (t/MWh)", + ) + op2: float = Field( + default=0.0, + ge=0, + description="Emission rate of pollutant 2 (t/MWh)", + ) + op3: float = Field( + default=0.0, + ge=0, + description="Emission rate of pollutant 3 (t/MWh)", + ) + op4: float = Field( + default=0.0, + ge=0, + description="Emission rate of pollutant 4 (t/MWh)", + ) + op5: float = Field( + default=0.0, + ge=0, + description="Emission rate of pollutant 5 (t/MWh)", + ) + + +class ThermalConfig(ThermalProperties, IgnoreCaseIdentifier): + """ + Thermal properties with section ID. + + Usage: + + >>> from antarest.study.storage.rawstudy.model.filesystem.config.thermal import ThermalConfig + + >>> cl = ThermalConfig(name="cluster 01!", group="Nuclear", co2=123) + >>> cl.id + 'cluster 01' + >>> cl.group == ThermalClusterGroup.NUCLEAR + True + >>> cl.co2 + 123.0 + >>> cl.nh3 + Traceback (most recent call last): + ... + AttributeError: 'ThermalConfig' object has no attribute 'nh3'""" + + +class Thermal860Config(Thermal860Properties, IgnoreCaseIdentifier): + """ + Thermal properties for study in version 8.6 or above. + + Usage: + + >>> from antarest.study.storage.rawstudy.model.filesystem.config.thermal import Thermal860Config + + >>> cl = Thermal860Config(name="cluster 01!", group="Nuclear", co2=123, nh3=456) + >>> cl.id + 'cluster 01' + >>> cl.group == ThermalClusterGroup.NUCLEAR + True + >>> cl.co2 + 123.0 + >>> cl.nh3 + 456.0 + >>> cl.op1 + 0.0 + """ + + +# NOTE: In the following Union, it is important to place the most specific type first, +# because the type matching generally occurs sequentially from left to right within the union. +ThermalConfigType = t.Union[Thermal860Config, ThermalConfig] + + +def create_thermal_config(study_version: t.Union[str, int], **kwargs: t.Any) -> ThermalConfigType: + """ + Factory method to create a thermal configuration model. + + Args: + study_version: The version of the study. + **kwargs: The properties to be used to initialize the model. + + Returns: + The thermal configuration model. + + Raises: + ValueError: If the study version is not supported. + """ + version = int(study_version) + if version >= 860: + return Thermal860Config(**kwargs) + else: + return ThermalConfig(**kwargs) diff --git a/antarest/study/storage/rawstudy/model/filesystem/factory.py b/antarest/study/storage/rawstudy/model/filesystem/factory.py index f37e9f371a..1899ec1bb4 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/factory.py +++ b/antarest/study/storage/rawstudy/model/filesystem/factory.py @@ -1,8 +1,11 @@ import logging +import os.path +import tempfile import time -from dataclasses import dataclass +import typing as t from pathlib import Path -from typing import Optional + +import filelock from antarest.core.interfaces.cache import CacheConstants, ICache from antarest.matrixstore.service import ISimpleMatrixService @@ -15,8 +18,15 @@ logger = logging.getLogger(__name__) -@dataclass -class FileStudy: +class FileStudy(t.NamedTuple): + """ + Antares study stored on the disk. + + Attributes: + config: Root object to handle all study parameters which impact tree structure. + tree: Top level node of antares tree structure. + """ + config: FileStudyTreeConfig tree: FileStudyTree @@ -34,12 +44,47 @@ def __init__( ) -> None: self.context = ContextServer(matrix=matrix, resolver=resolver) self.cache = cache + # It is better to store lock files in the temporary directory, + # because it is possible that there not deleted when the web application is stopped. + # Cleaning up lock files is thus easier. + self._lock_dir = tempfile.gettempdir() + self._lock_fmt = "{basename}.create_from_fs.lock" def create_from_fs( self, path: Path, study_id: str, - output_path: Optional[Path] = None, + output_path: t.Optional[Path] = None, + use_cache: bool = True, + ) -> FileStudy: + """ + Create a study from a path on the disk. + + `FileStudy` creation is done with a file lock to avoid that two studies are analyzed at the same time. + + Args: + path: full path of the study directory to parse. + study_id: ID of the study (if known). + output_path: full path of the "output" directory in the study directory. + use_cache: Whether to use cache or not. + + Returns: + Antares study stored on the disk. + """ + # This file lock is used to avoid that two studies are analyzed at the same time. + # This often happens when the user opens a study, because we display both + # the summary and the comments in the same time in the UI. + lock_basename = study_id if study_id else path.name + lock_file = os.path.join(self._lock_dir, self._lock_fmt.format(basename=lock_basename)) + with filelock.FileLock(lock_file): + logger.info(f"🏗 Creating a study by reading the configuration from the directory '{path}'...") + return self._create_from_fs_unsafe(path, study_id, output_path, use_cache) + + def _create_from_fs_unsafe( + self, + path: Path, + study_id: str, + output_path: t.Optional[Path] = None, use_cache: bool = True, ) -> FileStudy: cache_id = f"{CacheConstants.STUDY_FACTORY}/{study_id}" @@ -55,11 +100,11 @@ def create_from_fs( logger.info(f"Study {study_id} config built in {duration}s") result = FileStudy(config, FileStudyTree(self.context, config)) if study_id and use_cache: + logger.info(f"Cache new entry from StudyFactory (studyID: {study_id})") self.cache.put( cache_id, FileStudyTreeConfigDTO.from_build_config(config).dict(), ) - logger.info(f"Cache new entry from StudyFactory (studyID: {study_id})") return result def create_from_config(self, config: FileStudyTreeConfig) -> FileStudyTree: diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/input/renewables/clusters.py b/antarest/study/storage/rawstudy/model/filesystem/root/input/renewables/clusters.py index 7826c0d947..23592eb764 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/input/renewables/clusters.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/input/renewables/clusters.py @@ -20,7 +20,7 @@ def __init__( "nomialcapacity": 0, "ts-interpretation": str, } - types = {renewable: section for renewable in config.get_renewable_names(area)} + types = {cluster_id: section for cluster_id in config.get_renewable_ids(area)} IniFileNode.__init__(self, context, config, types) diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/input/renewables/series.py b/antarest/study/storage/rawstudy/model/filesystem/root/input/renewables/series.py index e6ee76d558..46b92ab5d2 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/input/renewables/series.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/input/renewables/series.py @@ -30,9 +30,11 @@ def __init__( self.area = area def build(self) -> TREE: + # Note that cluster IDs may not be in lower case, but series IDs are. + series_ids = map(str.lower, self.config.get_renewable_ids(self.area)) children: TREE = { - renewable: ClusteredRenewableSeries(self.context, self.config.next_file(renewable)) - for renewable in self.config.get_renewable_names(self.area) + series_id: ClusteredRenewableSeries(self.context, self.config.next_file(series_id)) + for series_id in series_ids } return children diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/cluster/area/list.py b/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/cluster/area/list.py index be1af98414..0349d79e35 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/cluster/area/list.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/cluster/area/list.py @@ -17,5 +17,5 @@ def __init__( "nominalcapacity": float, "market-bid-cost": float, } - types = {ther: section for ther in config.get_thermal_names(area)} + types = {th: section for th in config.get_thermal_ids(area)} super().__init__(context, config, types) diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/prepro/area/area.py b/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/prepro/area/area.py index c4164d7889..8bb88d01ac 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/prepro/area/area.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/prepro/area/area.py @@ -18,8 +18,11 @@ def __init__( self.area = area def build(self) -> TREE: + # Note that cluster IDs are case-insensitive, but series IDs are in lower case. + # For instance, if your cluster ID is "Base", then the series ID will be "base". + series_ids = map(str.lower, self.config.get_thermal_ids(self.area)) children: TREE = { - ther: InputThermalPreproAreaThermal(self.context, self.config.next_file(ther)) - for ther in self.config.get_thermal_names(self.area) + series_id: InputThermalPreproAreaThermal(self.context, self.config.next_file(series_id)) + for series_id in series_ids } return children diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/series/area/area.py b/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/series/area/area.py index 4fba9b77fd..31efc9a0b5 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/series/area/area.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/series/area/area.py @@ -18,8 +18,11 @@ def __init__( self.area = area def build(self) -> TREE: + # Note that cluster IDs are case-insensitive, but series IDs are in lower case. + # For instance, if your cluster ID is "Base", then the series ID will be "base". + series_ids = map(str.lower, self.config.get_thermal_ids(self.area)) children: TREE = { - ther: InputThermalSeriesAreaThermal(self.context, self.config.next_file(ther)) - for ther in self.config.get_thermal_names(self.area) + series_id: InputThermalSeriesAreaThermal(self.context, self.config.next_file(series_id)) + for series_id in series_ids } return children diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/output/simulation/mode/common/area.py b/antarest/study/storage/rawstudy/model/filesystem/root/output/simulation/mode/common/area.py index f59fc5a04a..d2b9541a22 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/output/simulation/mode/common/area.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/output/simulation/mode/common/area.py @@ -41,7 +41,7 @@ def build(self) -> TREE: self.area, ) - # has_thermal_clusters = len(self.config.get_thermal_names(self.area, only_enabled=True)) > 0 + # has_thermal_clusters = len(self.config.get_thermal_ids(self.area)) > 0 # todo get the config related to this output (now this may fail if input has changed since the launch) has_thermal_clusters = True @@ -54,7 +54,7 @@ def build(self) -> TREE: ) # has_enr_clusters = self.config.enr_modelling == ENR_MODELLING.CLUSTERS.value and - # len(self.config.get_renewable_names(self.area, only_enabled=True)) > 0 + # len(self.config.get_renewable_ids(self.area)) > 0 # todo get the config related to this output (now this may fail if input has changed since the launch) has_enr_clusters = True diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/settings/scenariobuilder.py b/antarest/study/storage/rawstudy/model/filesystem/root/settings/scenariobuilder.py index 4bfc19f263..16ac878209 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/settings/scenariobuilder.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/settings/scenariobuilder.py @@ -9,19 +9,21 @@ class ScenarioBuilder(IniFileNode): def __init__(self, context: ContextServer, config: FileStudyTreeConfig): self.config = config - rules: Dict[str, Type[int]] = dict() + rules: Dict[str, Type[int]] = {} for area in self.config.areas: for mode in ["l", "s", "w", "h"]: rules[f"{mode},{area},0"] = int self._add_thermal(area, rules) - IniFileNode.__init__( - self, + super().__init__( context=context, config=config, types={"Default Ruleset": rules}, ) def _add_thermal(self, area: str, rules: Dict[str, Type[int]]) -> None: - for thermal in self.config.get_thermal_names(area): - rules[f"t,{area},0,{thermal}"] = int + # Note that cluster IDs are case-insensitive, but series IDs are in lower case. + # For instance, if your cluster ID is "Base", then the series ID will be "base". + series_ids = map(str.lower, self.config.get_thermal_ids(area)) + for series_id in series_ids: + rules[f"t,{area},0,{series_id}"] = int diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/user/expansion/settings.py b/antarest/study/storage/rawstudy/model/filesystem/root/user/expansion/settings.py index 23534229f8..5eb2558fd4 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/user/expansion/settings.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/user/expansion/settings.py @@ -19,7 +19,7 @@ class ExpansionSettings(IniFileNode): - additional-constraints: str = filename. default = None version < 800 only: - - relaxed-optimality-gap: float = 1e6 + - relaxed-optimality-gap: float = 0.001 # relaxed-optimality-gap > 0 - cut-type: str = "average", "yearly" or "weekly". default="yearly" - ampl.solver: str = "cbc" - ampl.presolve: int = 0 @@ -27,8 +27,9 @@ class ExpansionSettings(IniFileNode): version >= 800 only: - relative_gap: float = 1e-12 - - solver: str = "Cbc" or "Coin". default="Cbc" + - solver: str = "Cbc", "Coin" or "Xpress". default="Cbc" - batch_size: int = 0 + - separation_parameter: float = 0.5 # 0 <= separation_parameter <= 1 """ def __init__(self, context: ContextServer, config: FileStudyTreeConfig): @@ -54,6 +55,7 @@ def __init__(self, context: ContextServer, config: FileStudyTreeConfig): "relative-gap": float, "solver": str, "batch_size": int, + "separation_parameter": float, **common_types, } super().__init__( diff --git a/antarest/study/storage/rawstudy/raw_study_service.py b/antarest/study/storage/rawstudy/raw_study_service.py index c6c73e63b5..fcde0db88e 100644 --- a/antarest/study/storage/rawstudy/raw_study_service.py +++ b/antarest/study/storage/rawstudy/raw_study_service.py @@ -1,11 +1,10 @@ -import io import logging import shutil import time from datetime import datetime from pathlib import Path from threading import Thread -from typing import IO, List, Optional, Sequence +from typing import BinaryIO, List, Optional, Sequence from uuid import uuid4 from zipfile import ZipFile @@ -259,7 +258,6 @@ def copy( study = self.study_factory.create_from_fs(dest_path, study_id=dest_study.id) update_antares_info(dest_study, study.tree, update_author=False) - del study.tree return dest_study def delete(self, metadata: RawStudy) -> None: @@ -298,15 +296,19 @@ def delete_output(self, metadata: RawStudy, output_name: str) -> None: output_path.unlink(missing_ok=True) remove_from_cache(self.cache, metadata.id) - def import_study(self, metadata: RawStudy, stream: IO[bytes]) -> Study: + def import_study(self, metadata: RawStudy, stream: BinaryIO) -> Study: """ - Import study + Import study in the directory of the study. + Args: - metadata: study information - stream: study content compressed in zip file + metadata: study information. + stream: binary content of the study compressed in ZIP or 7z format. - Returns: new study information. + Returns: + Updated study information. + Raises: + BadArchiveContent: If the archive is corrupted or in an unknown format. """ path_study = Path(metadata.path) path_study.mkdir() @@ -316,9 +318,9 @@ def import_study(self, metadata: RawStudy, stream: IO[bytes]) -> Study: fix_study_root(path_study) self.update_from_raw_meta(metadata) - except Exception as e: + except Exception: shutil.rmtree(path_study) - raise e + raise metadata.path = str(path_study) return metadata @@ -331,22 +333,22 @@ def export_study_flat( output_list_filter: Optional[List[str]] = None, denormalize: bool = True, ) -> None: - path_study = Path(metadata.path) - - if metadata.archived: - self.unarchive(metadata) try: + if metadata.archived: + self.unarchive(metadata) # may raise BadArchiveContent + export_study_flat( - path_study, + Path(metadata.path), dst_path, self.study_factory, outputs, output_list_filter, denormalize, ) + finally: if metadata.archived: - shutil.rmtree(metadata.path) + shutil.rmtree(metadata.path, ignore_errors=True) def check_errors( self, @@ -376,12 +378,19 @@ def archive(self, study: RawStudy) -> Path: self.cache.invalidate(study.id) return new_study_path + # noinspection SpellCheckingInspection def unarchive(self, study: RawStudy) -> None: - with open( - self.get_archive_path(study), - "rb", - ) as fh: - self.import_study(study, io.BytesIO(fh.read())) + """ + Extract the archive of a study. + + Args: + study: The study to be unarchived. + + Raises: + BadArchiveContent: If the archive is corrupted or in an unknown format. + """ + with open(self.get_archive_path(study), mode="rb") as fh: + self.import_study(study, fh) def get_archive_path(self, study: RawStudy) -> Path: return Path(self.config.storage.archive_dir / f"{study.id}.zip") diff --git a/antarest/study/storage/study_upgrader/__init__.py b/antarest/study/storage/study_upgrader/__init__.py index c9cbd3dcb6..6b96dc711b 100644 --- a/antarest/study/storage/study_upgrader/__init__.py +++ b/antarest/study/storage/study_upgrader/__init__.py @@ -214,7 +214,7 @@ def _copies_only_necessary_files(files_to_upgrade: List[Path], study_path: Path, files_to_retrieve.append(path) else: parent_path = path.parent - (tmp_path / parent_path).mkdir(parents=True) + (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 @@ -256,3 +256,19 @@ def _do_upgrade(study_path: Path, src_version: str, target_version: str) -> None 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 + # For now, the only upgrade that impacts study matrices is the upgrade from v8.1 to v8.2 + # In a near future, the upgrade from v8.6 to v8.7 will also require denormalization + return "820" in list_of_upgrades diff --git a/antarest/study/storage/study_upgrader/upgrader_810.py b/antarest/study/storage/study_upgrader/upgrader_810.py index 89711fe867..275bafb1e4 100644 --- a/antarest/study/storage/study_upgrader/upgrader_810.py +++ b/antarest/study/storage/study_upgrader/upgrader_810.py @@ -23,5 +23,5 @@ def upgrade_810(study_path: Path) -> None: 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) - study_path.joinpath("input", "renewables", "series").mkdir(parents=True) + 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 index 1740c03fb1..f5416d0180 100644 --- a/antarest/study/storage/study_upgrader/upgrader_820.py +++ b/antarest/study/storage/study_upgrader/upgrader_820.py @@ -24,7 +24,7 @@ def upgrade_820(study_path: Path) -> None: folder_path = Path(folder) all_txt = glob.glob(str(folder_path / "*.txt")) if len(all_txt) > 0: - (folder_path / "capacities").mkdir() + (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] diff --git a/antarest/study/storage/study_upgrader/upgrader_860.py b/antarest/study/storage/study_upgrader/upgrader_860.py index 10b15ff5ed..c74ee8d2c9 100644 --- a/antarest/study/storage/study_upgrader/upgrader_860.py +++ b/antarest/study/storage/study_upgrader/upgrader_860.py @@ -16,16 +16,17 @@ def upgrade_860(study_path: Path) -> None: writer = IniWriter(special_keys=DUPLICATE_KEYS) writer.write(data, study_path / GENERAL_DATA_PATH) - study_path.joinpath("input", "st-storage", "clusters").mkdir(parents=True) - study_path.joinpath("input", "st-storage", "series").mkdir(parents=True) + 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) + 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/utils.py b/antarest/study/storage/utils.py index f15264a782..8ec6499e50 100644 --- a/antarest/study/storage/utils.py +++ b/antarest/study/storage/utils.py @@ -390,12 +390,8 @@ def export_study_flat( stop_time = time.time() duration = "{:.3f}".format(stop_time - start_time) - logger.info( - { - False: f"Study '{study_dir}' exported (flat mode) in {duration}s", - True: f"Study '{study_dir}' exported with outputs (flat mode) in {duration}s", - }[bool(outputs)] - ) + with_outputs = "with outputs" if outputs else "without outputs" + logger.info(f"Study '{study_dir}' exported ({with_outputs}, flat mode) in {duration}s") study = study_factory.create_from_fs(dest, "", use_cache=False) if denormalize: study.tree.denormalize() diff --git a/antarest/study/storage/variantstudy/business/command_extractor.py b/antarest/study/storage/variantstudy/business/command_extractor.py index 83b88de09a..e0fd1d1e3c 100644 --- a/antarest/study/storage/variantstudy/business/command_extractor.py +++ b/antarest/study/storage/variantstudy/business/command_extractor.py @@ -194,33 +194,30 @@ def extract_link( def _extract_cluster(self, study: FileStudy, area_id: str, cluster_id: str, renewables: bool) -> List[ICommand]: study_tree = study.tree + if renewables: + cluster_type = "renewables" # with a final "s" + cluster_list = study.config.areas[area_id].renewables + create_cluster_command = CreateRenewablesCluster + else: + cluster_type = "thermal" # w/o a final "s" + cluster_list = study.config.areas[area_id].thermals # type: ignore + create_cluster_command = CreateCluster # type: ignore - cluster_type = "renewables" if renewables else "thermal" - cluster_list = ( - study.config.areas[area_id].thermals if not renewables else study.config.areas[area_id].renewables - ) - create_cluster_command = CreateCluster if not renewables else CreateRenewablesCluster - - cluster_name = next( - (cluster.name for cluster in cluster_list if cluster.id == cluster_id), - cluster_id, - ) + cluster = next(iter(c for c in cluster_list if c.id == cluster_id)) null_matrix_id = strip_matrix_protocol(self.generator_matrix_constants.get_null_matrix()) + # Note that cluster IDs are case-insensitive, but series IDs are in lower case. + series_id = cluster_id.lower() study_commands: List[ICommand] = [ create_cluster_command( area_id=area_id, - cluster_name=cluster_name, - parameters={}, + cluster_name=cluster.id, + parameters=cluster.dict(by_alias=True, exclude_defaults=True, exclude={"id"}), command_context=self.command_context, ), - self.generate_update_config( - study_tree, - ["input", cluster_type, "clusters", area_id, "list", cluster_name], - ), self.generate_replace_matrix( study_tree, - ["input", cluster_type, "series", area_id, cluster_id, "series"], + ["input", cluster_type, "series", area_id, series_id, "series"], null_matrix_id, ), ] @@ -229,12 +226,12 @@ def _extract_cluster(self, study: FileStudy, area_id: str, cluster_id: str, rene [ self.generate_replace_matrix( study_tree, - ["input", cluster_type, "prepro", area_id, cluster_id, "data"], + ["input", cluster_type, "prepro", area_id, series_id, "data"], null_matrix_id, ), self.generate_replace_matrix( study_tree, - ["input", cluster_type, "prepro", area_id, cluster_id, "modulation"], + ["input", cluster_type, "prepro", area_id, series_id, "modulation"], null_matrix_id, ), ] diff --git a/antarest/study/storage/variantstudy/business/utils.py b/antarest/study/storage/variantstudy/business/utils.py index 7bcc9ba8ef..6f04601ec5 100644 --- a/antarest/study/storage/variantstudy/business/utils.py +++ b/antarest/study/storage/variantstudy/business/utils.py @@ -34,12 +34,14 @@ def validate_matrix(matrix: Union[List[List[MatrixData]], str], values: Dict[str if isinstance(matrix, list): return MATRIX_PROTOCOL_PREFIX + matrix_service.create(data=matrix) elif isinstance(matrix, str): - if matrix_service.exists(matrix): + if not matrix: + raise ValueError("The matrix ID cannot be empty") + elif matrix_service.exists(matrix): return MATRIX_PROTOCOL_PREFIX + matrix else: - raise ValueError(f"Matrix with id {matrix} does not exist") + raise ValueError(f"Matrix with id '{matrix}' does not exist") else: - raise TypeError(f"The data {matrix} is neither a matrix nor a link to a matrix") + raise TypeError(f"The data '{matrix}' is neither a matrix nor a link to a matrix") def get_or_create_section(json_ini: JSON, section: str) -> JSON: diff --git a/antarest/study/storage/variantstudy/business/utils_binding_constraint.py b/antarest/study/storage/variantstudy/business/utils_binding_constraint.py index b2eded8a6a..db6ea4d1db 100644 --- a/antarest/study/storage/variantstudy/business/utils_binding_constraint.py +++ b/antarest/study/storage/variantstudy/business/utils_binding_constraint.py @@ -11,16 +11,6 @@ from antarest.study.storage.variantstudy.model.command.common import BindingConstraintOperator, CommandOutput -def cluster_does_not_exist(study_data: FileStudy, area: str, thermal_id: str) -> bool: - return area not in study_data.config.areas or thermal_id not in [ - thermal.id for thermal in study_data.config.areas[area].thermals - ] - - -def link_does_not_exist(study_data: FileStudy, area_1: str, area_2: str) -> bool: - return area_1 not in study_data.config.areas or area_2 not in study_data.config.areas[area_1].links - - def apply_binding_constraint( study_data: FileStudy, binding_constraints: JSON, @@ -51,28 +41,32 @@ def apply_binding_constraint( if comments is not None: binding_constraints[new_key]["comments"] = comments - for link_or_thermal in coeffs: - if "%" in link_or_thermal: - area_1, area_2 = link_or_thermal.split("%") - if link_does_not_exist(study_data, area_1, area_2): + for link_or_cluster in coeffs: + if "%" in link_or_cluster: + area_1, area_2 = link_or_cluster.split("%") + if area_1 not in study_data.config.areas or area_2 not in study_data.config.areas[area_1].links: return CommandOutput( status=False, - message=f"Link {link_or_thermal} does not exist", + message=f"Link '{link_or_cluster}' does not exist in binding constraint '{bd_id}'", ) - else: - area, thermal_id = link_or_thermal.split(".") - if cluster_does_not_exist(study_data, area, thermal_id): + elif "." in link_or_cluster: + # Cluster IDs are stored in lower case in the binding constraints file. + area, cluster_id = link_or_cluster.split(".") + thermal_ids = {thermal.id.lower() for thermal in study_data.config.areas[area].thermals} + if area not in study_data.config.areas or cluster_id not in thermal_ids: return CommandOutput( status=False, - message=f"Thermal cluster {link_or_thermal} does not exist", + message=f"Cluster '{link_or_cluster}' does not exist in binding constraint '{bd_id}'", ) + else: + raise NotImplementedError(f"Invalid link or thermal ID: {link_or_cluster}") # this is weird because Antares Simulator only accept int as offset - if len(coeffs[link_or_thermal]) == 2: - coeffs[link_or_thermal][1] = int(coeffs[link_or_thermal][1]) + if len(coeffs[link_or_cluster]) == 2: + coeffs[link_or_cluster][1] = int(coeffs[link_or_cluster][1]) - binding_constraints[new_key][link_or_thermal] = "%".join( - [str(coeff_val) for coeff_val in coeffs[link_or_thermal]] + binding_constraints[new_key][link_or_cluster] = "%".join( + [str(coeff_val) for coeff_val in coeffs[link_or_cluster]] ) parse_bindings_coeffs_and_save_into_config(bd_id, study_data.config, coeffs) study_data.tree.save( @@ -112,10 +106,13 @@ def parse_bindings_coeffs_and_save_into_config( def remove_area_cluster_from_binding_constraints( study_data_config: FileStudyTreeConfig, area_id: str, - cluster_id: Optional[str] = None, + cluster_id: str = "", ) -> None: - for binding in study_data_config.bindings: - if (cluster_id and f"{area_id}.{cluster_id}" in binding.clusters) or ( - not cluster_id and area_id in binding.areas - ): - study_data_config.bindings.remove(binding) + if cluster_id: + # Cluster IDs are stored in lower case in the binding constraints file. + cluster_id = cluster_id.lower() + selection = [b for b in study_data_config.bindings if f"{area_id}.{cluster_id}" in b.clusters] + else: + selection = [b for b in study_data_config.bindings if area_id in b.areas] + for binding in selection: + study_data_config.bindings.remove(binding) diff --git a/antarest/study/storage/variantstudy/model/command/create_area.py b/antarest/study/storage/variantstudy/model/command/create_area.py index a824f402e1..d2114c254e 100644 --- a/antarest/study/storage/variantstudy/model/command/create_area.py +++ b/antarest/study/storage/variantstudy/model/command/create_area.py @@ -1,5 +1,7 @@ from typing import Any, Dict, List, Tuple +from pydantic import Field + from antarest.core.model import JSON from antarest.study.common.default_values import FilteringOptions, NodalOptimization from antarest.study.storage.rawstudy.model.filesystem.config.model import ( @@ -31,14 +33,27 @@ def _generate_new_thermal_areas_ini( class CreateArea(ICommand): + """ + Command used to create a new area in the study. + """ + + # Overloaded metadata + # =================== + + command_name = CommandName.CREATE_AREA + version = 1 + + # Command parameters + # ================== + area_name: str - def __init__(self, **data: Any) -> None: - super().__init__( - command_name=CommandName.CREATE_AREA, - version=1, - **data, - ) + # The `metadata` attribute is added to ensure upward compatibility with previous versions. + # Ideally, this attribute should be of type `PatchArea`, but as it is not used, + # we choose to declare it as an empty dictionary. + # fixme: remove this attribute in the next version if it is not used by the "Script R" team, + # or if we don't want to support this feature. + metadata: Dict[str, str] = Field(default_factory=dict, description="Area metadata: country and tag list") def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, Dict[str, Any]]: if self.command_context.generator_matrix_constants is None: diff --git a/antarest/study/storage/variantstudy/model/command/create_binding_constraint.py b/antarest/study/storage/variantstudy/model/command/create_binding_constraint.py index ed53e5e31c..178c918a0c 100644 --- a/antarest/study/storage/variantstudy/model/command/create_binding_constraint.py +++ b/antarest/study/storage/variantstudy/model/command/create_binding_constraint.py @@ -98,7 +98,7 @@ class CreateBindingConstraint(AbstractBindingConstraintCommand): Command used to create a binding constraint. """ - command_name: CommandName = CommandName.CREATE_BINDING_CONSTRAINT + command_name = CommandName.CREATE_BINDING_CONSTRAINT version: int = 1 # Properties of the `CREATE_BINDING_CONSTRAINT` command: @@ -139,7 +139,7 @@ def _apply_config(self, study_data_config: FileStudyTreeConfig) -> Tuple[Command def _apply(self, study_data: FileStudy) -> CommandOutput: binding_constraints = study_data.tree.get(["input", "bindingconstraints", "bindingconstraints"]) - new_key = len(binding_constraints.keys()) + new_key = len(binding_constraints) bd_id = transform_name_to_id(self.name) return apply_binding_constraint( study_data, diff --git a/antarest/study/storage/variantstudy/model/command/create_cluster.py b/antarest/study/storage/variantstudy/model/command/create_cluster.py index 8097cad542..392abb23a9 100644 --- a/antarest/study/storage/variantstudy/model/command/create_cluster.py +++ b/antarest/study/storage/variantstudy/model/command/create_cluster.py @@ -1,15 +1,16 @@ from typing import Any, Dict, List, Optional, Tuple, Union, cast -from pydantic import validator +from pydantic import Extra, validator from antarest.core.model import JSON from antarest.core.utils.utils import assert_this from antarest.matrixstore.model import MatrixData from antarest.study.storage.rawstudy.model.filesystem.config.model import ( - Cluster, + Area, FileStudyTreeConfig, transform_name_to_id, ) +from antarest.study.storage.rawstudy.model.filesystem.config.thermal import create_thermal_config from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.business.utils import strip_matrix_protocol, validate_matrix from antarest.study.storage.variantstudy.model.command.common import CommandName, CommandOutput @@ -76,23 +77,28 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, ), {}, ) - cluster_id = transform_name_to_id(self.cluster_name) - for cluster in study_data.areas[self.area_id].thermals: - if cluster.id == cluster_id: - return ( - CommandOutput( - status=False, - message=f"Thermal cluster '{cluster_id}' already exists in the area '{self.area_id}'.", - ), - {}, - ) - study_data.areas[self.area_id].thermals.append(Cluster(id=cluster_id, name=self.cluster_name)) + area: Area = study_data.areas[self.area_id] + + # Check if the cluster already exists in the area + version = study_data.version + cluster = create_thermal_config(version, name=self.cluster_name) + if any(cl.id == cluster.id for cl in area.thermals): + return ( + CommandOutput( + status=False, + message=f"Thermal cluster '{cluster.id}' already exists in the area '{self.area_id}'.", + ), + {}, + ) + + area.thermals.append(cluster) + return ( CommandOutput( status=True, - message=f"Thermal cluster '{cluster_id}' added to area '{self.area_id}'.", + message=f"Thermal cluster '{cluster.id}' added to area '{self.area_id}'.", ), - {"cluster_id": cluster_id}, + {"cluster_id": cluster.id}, ) def _apply(self, study_data: FileStudy) -> CommandOutput: @@ -100,21 +106,22 @@ def _apply(self, study_data: FileStudy) -> CommandOutput: if not output.status: return output - cluster_id = data["cluster_id"] + # default values + self.parameters.setdefault("name", self.cluster_name) - cluster_list_config = study_data.tree.get(["input", "thermal", "clusters", self.area_id, "list"]) - # fixme: rigorously, the section name in the INI file is the cluster ID, not the cluster name - # cluster_list_config[transform_name_to_id(self.cluster_name)] = self.parameters - cluster_list_config[self.cluster_name] = self.parameters + cluster_id = data["cluster_id"] + config = study_data.tree.get(["input", "thermal", "clusters", self.area_id, "list"]) + config[cluster_id] = self.parameters - self.parameters["name"] = self.cluster_name + # Series identifiers are in lower case. + series_id = cluster_id.lower() new_cluster_data: JSON = { "input": { "thermal": { - "clusters": {self.area_id: {"list": cluster_list_config}}, + "clusters": {self.area_id: {"list": config}}, "prepro": { self.area_id: { - cluster_id: { + series_id: { "data": self.prepro, "modulation": self.modulation, } @@ -122,7 +129,7 @@ def _apply(self, study_data: FileStudy) -> CommandOutput: }, "series": { self.area_id: { - cluster_id: {"series": self.command_context.generator_matrix_constants.get_null_matrix()} + series_id: {"series": self.command_context.generator_matrix_constants.get_null_matrix()} } }, } @@ -171,12 +178,13 @@ def _create_diff(self, other: "ICommand") -> List["ICommand"]: from antarest.study.storage.variantstudy.model.command.replace_matrix import ReplaceMatrix from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig - cluster_id = transform_name_to_id(self.cluster_name) + # Series identifiers are in lower case. + series_id = transform_name_to_id(self.cluster_name, lower=True) commands: List[ICommand] = [] if self.prepro != other.prepro: commands.append( ReplaceMatrix( - target=f"input/thermal/prepro/{self.area_id}/{cluster_id}/data", + target=f"input/thermal/prepro/{self.area_id}/{series_id}/data", matrix=strip_matrix_protocol(other.prepro), command_context=self.command_context, ) @@ -184,7 +192,7 @@ def _create_diff(self, other: "ICommand") -> List["ICommand"]: if self.modulation != other.modulation: commands.append( ReplaceMatrix( - target=f"input/thermal/prepro/{self.area_id}/{cluster_id}/modulation", + target=f"input/thermal/prepro/{self.area_id}/{series_id}/modulation", matrix=strip_matrix_protocol(other.modulation), command_context=self.command_context, ) diff --git a/antarest/study/storage/variantstudy/model/command/create_district.py b/antarest/study/storage/variantstudy/model/command/create_district.py index 1f6a34ca6f..9311345db0 100644 --- a/antarest/study/storage/variantstudy/model/command/create_district.py +++ b/antarest/study/storage/variantstudy/model/command/create_district.py @@ -20,15 +20,25 @@ class DistrictBaseFilter(Enum): class CreateDistrict(ICommand): + """ + Command used to create a district in a study. + """ + + # Overloaded metadata + # =================== + + command_name = CommandName.CREATE_DISTRICT + version = 1 + + # Command parameters + # ================== + name: str base_filter: Optional[DistrictBaseFilter] = None filter_items: Optional[List[str]] = None output: bool = True comments: str = "" - def __init__(self, **data: Any) -> None: - super().__init__(command_name=CommandName.CREATE_DISTRICT, version=1, **data) - @validator("name") def validate_district_name(cls, val: str) -> str: valid_name = transform_name_to_id(val, lower=False) diff --git a/antarest/study/storage/variantstudy/model/command/create_link.py b/antarest/study/storage/variantstudy/model/command/create_link.py index 329806c166..b5b48eb64e 100644 --- a/antarest/study/storage/variantstudy/model/command/create_link.py +++ b/antarest/study/storage/variantstudy/model/command/create_link.py @@ -19,6 +19,19 @@ class LinkAlreadyExistError(Exception): class CreateLink(ICommand): + """ + Command used to create a link between two areas. + """ + + # Overloaded metadata + # =================== + + command_name = CommandName.CREATE_LINK + version = 1 + + # Command parameters + # ================== + area1: str area2: str parameters: Optional[Dict[str, str]] = None @@ -26,9 +39,6 @@ class CreateLink(ICommand): direct: Optional[Union[List[List[MatrixData]], str]] = None indirect: Optional[Union[List[List[MatrixData]], str]] = None - def __init__(self, **data: Any) -> None: - super().__init__(command_name=CommandName.CREATE_LINK, version=1, **data) - @validator("series", "direct", "indirect", always=True) def validate_series( cls, v: Optional[Union[List[List[MatrixData]], str]], values: Any diff --git a/antarest/study/storage/variantstudy/model/command/create_renewables_cluster.py b/antarest/study/storage/variantstudy/model/command/create_renewables_cluster.py index 59502439e6..0658de4076 100644 --- a/antarest/study/storage/variantstudy/model/command/create_renewables_cluster.py +++ b/antarest/study/storage/variantstudy/model/command/create_renewables_cluster.py @@ -1,14 +1,15 @@ from typing import Any, Dict, List, Tuple, cast -from pydantic import validator +from pydantic import Extra, validator from antarest.core.model import JSON from antarest.study.storage.rawstudy.model.filesystem.config.model import ( ENR_MODELLING, - Cluster, + Area, FileStudyTreeConfig, transform_name_to_id, ) +from antarest.study.storage.rawstudy.model.filesystem.config.renewable import create_renewable_config from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.model.command.common import CommandName, CommandOutput from antarest.study.storage.variantstudy.model.command.icommand import MATCH_SIGNATURE_SEPARATOR, ICommand @@ -42,6 +43,11 @@ def validate_cluster_name(cls, val: str) -> str: def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, Dict[str, Any]]: if study_data.enr_modelling != ENR_MODELLING.CLUSTERS.value: + # Since version 8.1 of the solver, we can use renewable clusters + # instead of "Load", "Wind" and "Solar" objects for modelling. + # When the "renewable-generation-modelling" parameter is set to "aggregated", + # it means that we want to ensure compatibility with previous versions. + # To use renewable clusters, the parameter must therefore be set to "clusters". message = ( f"Parameter 'renewable-generation-modelling'" f" must be set to '{ENR_MODELLING.CLUSTERS.value}'" @@ -58,25 +64,28 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, ), {}, ) + area: Area = study_data.areas[self.area_id] - cluster_id = transform_name_to_id(self.cluster_name) - for cluster in study_data.areas[self.area_id].renewables: - if cluster.id == cluster_id: - return ( - CommandOutput( - status=False, - message=f"Renewable cluster '{cluster_id}' already exists in the area '{self.area_id}'.", - ), - {}, - ) + # Check if the cluster already exists in the area + version = study_data.version + cluster = create_renewable_config(version, name=self.cluster_name) + if any(cl.id == cluster.id for cl in area.renewables): + return ( + CommandOutput( + status=False, + message=f"Renewable cluster '{cluster.id}' already exists in the area '{self.area_id}'.", + ), + {}, + ) + + area.renewables.append(cluster) - study_data.areas[self.area_id].renewables.append(Cluster(id=cluster_id, name=self.cluster_name)) return ( CommandOutput( status=True, - message=f"Renewable cluster '{cluster_id}' added to area '{self.area_id}'.", + message=f"Renewable cluster '{cluster.id}' added to area '{self.area_id}'.", ), - {"cluster_id": cluster_id}, + {"cluster_id": cluster.id}, ) def _apply(self, study_data: FileStudy) -> CommandOutput: @@ -84,24 +93,24 @@ def _apply(self, study_data: FileStudy) -> CommandOutput: if not output.status: return output - cluster_id = data["cluster_id"] - - cluster_list_config = study_data.tree.get(["input", "renewables", "clusters", self.area_id, "list"]) # default values if "ts-interpretation" not in self.parameters: self.parameters["ts-interpretation"] = "power-generation" - # fixme: rigorously, the section name in the INI file is the cluster ID, not the cluster name - # cluster_list_config[transform_name_to_id(self.cluster_name)] = self.parameters - cluster_list_config[self.cluster_name] = self.parameters + self.parameters.setdefault("name", self.cluster_name) + + cluster_id = data["cluster_id"] + config = study_data.tree.get(["input", "renewables", "clusters", self.area_id, "list"]) + config[cluster_id] = self.parameters - self.parameters["name"] = self.cluster_name + # Series identifiers are in lower case. + series_id = cluster_id.lower() new_cluster_data: JSON = { "input": { "renewables": { - "clusters": {self.area_id: {"list": cluster_list_config}}, + "clusters": {self.area_id: {"list": config}}, "series": { self.area_id: { - cluster_id: {"series": self.command_context.generator_matrix_constants.get_null_matrix()} + series_id: {"series": self.command_context.generator_matrix_constants.get_null_matrix()} } }, } diff --git a/antarest/study/storage/variantstudy/model/command/create_st_storage.py b/antarest/study/storage/variantstudy/model/command/create_st_storage.py index 026bbd5e1e..771c2dd4b0 100644 --- a/antarest/study/storage/variantstudy/model/command/create_st_storage.py +++ b/antarest/study/storage/variantstudy/model/command/create_st_storage.py @@ -2,13 +2,13 @@ from typing import Any, Dict, List, Optional, Tuple, Union, cast import numpy as np -from pydantic import Extra, Field, validator +from pydantic import Field, validator from pydantic.fields import ModelField from antarest.core.model import JSON from antarest.matrixstore.model import MatrixData from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, FileStudyTreeConfig -from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import STStorageConfig +from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import STStorageConfigType from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.business.matrix_constants_generator import GeneratorMatrixConstants from antarest.study.storage.variantstudy.business.utils import strip_matrix_protocol, validate_matrix @@ -37,11 +37,8 @@ class CreateSTStorage(ICommand): Command used to create a short-terme storage in an area. """ - class Config: - extra = Extra.forbid - - # Overloaded parameters - # ===================== + # Overloaded metadata + # =================== command_name = CommandName.CREATE_ST_STORAGE version = 1 @@ -50,7 +47,7 @@ class Config: # ================== area_id: str = Field(description="Area ID", regex=r"[a-z0-9_(),& -]+") - parameters: STStorageConfig + parameters: STStorageConfigType pmax_injection: Optional[Union[MatrixType, str]] = Field( None, description="Charge capacity (modulation)", @@ -165,7 +162,7 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, return ( CommandOutput( status=False, - message=(f"Invalid study version {version}, at least version {REQUIRED_VERSION} is required."), + message=f"Invalid study version {version}, at least version {REQUIRED_VERSION} is required.", ), {}, ) @@ -186,7 +183,7 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, return ( CommandOutput( status=False, - message=(f"Short-term storage '{self.storage_name}' already exists in the area '{self.area_id}'."), + message=f"Short-term storage '{self.storage_name}' already exists in the area '{self.area_id}'.", ), {}, ) @@ -197,7 +194,7 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, return ( CommandOutput( status=True, - message=(f"Short-term st_storage '{self.storage_name}' successfully added to area '{self.area_id}'."), + message=f"Short-term st_storage '{self.storage_name}' successfully added to area '{self.area_id}'.", ), {"storage_id": self.storage_id}, ) diff --git a/antarest/study/storage/variantstudy/model/command/icommand.py b/antarest/study/storage/variantstudy/model/command/icommand.py index 22916f2b81..6a17c34c10 100644 --- a/antarest/study/storage/variantstudy/model/command/icommand.py +++ b/antarest/study/storage/variantstudy/model/command/icommand.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any, Dict, List, Tuple -from pydantic import BaseModel +from pydantic import BaseModel, Extra from antarest.core.utils.utils import assert_this from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) -class ICommand(ABC, BaseModel): +class ICommand(ABC, BaseModel, extra=Extra.forbid, arbitrary_types_allowed=True): command_name: CommandName version: int command_context: CommandContext @@ -161,6 +161,3 @@ def get_command_extractor(self) -> "CommandExtractor": self.command_context.matrix_service, self.command_context.patch_service, ) - - class Config: - arbitrary_types_allowed = True diff --git a/antarest/study/storage/variantstudy/model/command/remove_area.py b/antarest/study/storage/variantstudy/model/command/remove_area.py index 03b287cf9f..a7914c627e 100644 --- a/antarest/study/storage/variantstudy/model/command/remove_area.py +++ b/antarest/study/storage/variantstudy/model/command/remove_area.py @@ -21,7 +21,7 @@ class RemoveArea(ICommand): Command used to remove an area. """ - command_name: CommandName = CommandName.REMOVE_AREA + command_name = CommandName.REMOVE_AREA version: int = 1 # Properties of the `REMOVE_AREA` command: diff --git a/antarest/study/storage/variantstudy/model/command/remove_binding_constraint.py b/antarest/study/storage/variantstudy/model/command/remove_binding_constraint.py index 97a27f5297..961e878443 100644 --- a/antarest/study/storage/variantstudy/model/command/remove_binding_constraint.py +++ b/antarest/study/storage/variantstudy/model/command/remove_binding_constraint.py @@ -13,7 +13,7 @@ class RemoveBindingConstraint(ICommand): Command used to remove a binding constraint. """ - command_name: CommandName = CommandName.REMOVE_BINDING_CONSTRAINT + command_name = CommandName.REMOVE_BINDING_CONSTRAINT version: int = 1 # Properties of the `REMOVE_BINDING_CONSTRAINT` command: diff --git a/antarest/study/storage/variantstudy/model/command/remove_cluster.py b/antarest/study/storage/variantstudy/model/command/remove_cluster.py index 8681794815..ff1fdd4a73 100644 --- a/antarest/study/storage/variantstudy/model/command/remove_cluster.py +++ b/antarest/study/storage/variantstudy/model/command/remove_cluster.py @@ -1,6 +1,6 @@ from typing import Any, Dict, List, Tuple -from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig +from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.business.utils_binding_constraint import ( remove_area_cluster_from_binding_constraints, @@ -11,121 +11,106 @@ class RemoveCluster(ICommand): + """ + Command used to remove a thermal cluster in an area. + """ + + # Overloaded metadata + # =================== + + command_name = CommandName.REMOVE_THERMAL_CLUSTER + version = 1 + + # Command parameters + # ================== + area_id: str cluster_id: str - def __init__(self, **data: Any) -> None: - super().__init__(command_name=CommandName.REMOVE_THERMAL_CLUSTER, version=1, **data) + def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, Dict[str, Any]]: + """ + Applies configuration changes to the study data: remove the thermal clusters from the storages list. + + Args: + study_data: The study data configuration. + + Returns: + A tuple containing the command output and a dictionary of extra data. + On success, the dictionary is empty. + """ + # Search the Area in the configuration + if self.area_id not in study_data.areas: + message = f"Area '{self.area_id}' does not exist in the study configuration." + return CommandOutput(status=False, message=message), {} + area: Area = study_data.areas[self.area_id] + + # Search the Thermal cluster in the area + thermal = next( + iter(thermal for thermal in area.thermals if thermal.id == self.cluster_id), + None, + ) + if thermal is None: + message = f"Thermal cluster '{self.cluster_id}' does not exist in the area '{self.area_id}'." + return CommandOutput(status=False, message=message), {} - def _remove_cluster(self, study_data: FileStudyTreeConfig) -> None: - study_data.areas[self.area_id].thermals = [ - cluster for cluster in study_data.areas[self.area_id].thermals if cluster.id != self.cluster_id.lower() - ] + for thermal in area.thermals: + if thermal.id == self.cluster_id: + break + else: + message = f"Thermal cluster '{self.cluster_id}' does not exist in the area '{self.area_id}'." + return CommandOutput(status=False, message=message), {} - def _apply_config(self, study_data_config: FileStudyTreeConfig) -> Tuple[CommandOutput, Dict[str, Any]]: - if self.area_id not in study_data_config.areas: - return ( - CommandOutput( - status=False, - message=f"Area '{self.area_id}' does not exist", - ), - dict(), - ) - - if ( - len( - [ - cluster - for cluster in study_data_config.areas[self.area_id].thermals - if cluster.id == self.cluster_id.lower() - ] - ) - == 0 - ): - return ( - CommandOutput( - status=False, - message=f"Cluster '{self.cluster_id}' does not exist", - ), - dict(), - ) - self._remove_cluster(study_data_config) - remove_area_cluster_from_binding_constraints(study_data_config, self.area_id, self.cluster_id.lower()) - - return ( - CommandOutput( - status=True, - message=f"Cluster '{self.cluster_id}' removed from area '{self.area_id}'", - ), - dict(), - ) + # Remove the Thermal cluster from the configuration + area.thermals.remove(thermal) + + remove_area_cluster_from_binding_constraints(study_data, self.area_id, self.cluster_id) + + message = f"Thermal cluster '{self.cluster_id}' removed from the area '{self.area_id}'." + return CommandOutput(status=True, message=message), {} def _apply(self, study_data: FileStudy) -> CommandOutput: + """ + Applies the study data to update thermal cluster configurations and saves the changes: + remove corresponding the configuration and remove the attached time series. + + Args: + study_data: The study data to be applied. + + Returns: + The output of the command execution. + """ + # Search the Area in the configuration if self.area_id not in study_data.config.areas: - return CommandOutput( - status=False, - message=f"Area '{self.area_id}' does not exist", - ) - - cluster_query_result = [ - cluster - for cluster in study_data.config.areas[self.area_id].thermals - if cluster.id == self.cluster_id.lower() + message = f"Area '{self.area_id}' does not exist in the study configuration." + return CommandOutput(status=False, message=message) + + # It is required to delete the files and folders that correspond to the thermal cluster + # BEFORE updating the configuration, as we need the configuration to do so. + # Specifically, deleting the time series uses the list of thermal clusters from the configuration. + + series_id = self.cluster_id.lower() + paths = [ + ["input", "thermal", "clusters", self.area_id, "list", self.cluster_id], + ["input", "thermal", "prepro", self.area_id, series_id], + ["input", "thermal", "series", self.area_id, series_id], ] + area: Area = study_data.config.areas[self.area_id] + if len(area.thermals) == 1: + paths.append(["input", "thermal", "prepro", self.area_id]) + paths.append(["input", "thermal", "series", self.area_id]) - if len(cluster_query_result) == 0: - return CommandOutput( - status=False, - message=f"Cluster '{self.cluster_id}' does not exist", - ) - cluster = cluster_query_result[0] - - cluster_list = study_data.tree.get(["input", "thermal", "clusters", self.area_id, "list"]) - - cluster_list_id = self.cluster_id - if cluster_list.get(cluster.name, None): - cluster_list_id = cluster.name - - study_data.tree.delete( - [ - "input", - "thermal", - "clusters", - self.area_id, - "list", - cluster_list_id, - ] - ) - study_data.tree.delete( - [ - "input", - "thermal", - "prepro", - self.area_id, - self.cluster_id.lower(), - ] - ) - study_data.tree.delete( - [ - "input", - "thermal", - "series", - self.area_id, - self.cluster_id.lower(), - ] - ) + for path in paths: + study_data.tree.delete(path) - self._remove_cluster(study_data.config) self._remove_cluster_from_binding_constraints(study_data) - return CommandOutput( - status=True, - message=f"Cluster '{self.cluster_id}' removed from area '{self.area_id}'", - ) + # Deleting the renewable cluster in the configuration must be done AFTER + # deleting the files and folders. + return self._apply_config(study_data.config)[0] def to_dto(self) -> CommandDTO: return CommandDTO( - action=CommandName.REMOVE_THERMAL_CLUSTER.value, + action=self.command_name.value, args={"area_id": self.area_id, "cluster_id": self.cluster_id}, ) @@ -149,30 +134,26 @@ def _create_diff(self, other: "ICommand") -> List["ICommand"]: def get_inner_matrices(self) -> List[str]: return [] + # noinspection SpellCheckingInspection def _remove_cluster_from_binding_constraints(self, study_data: FileStudy) -> None: - binding_constraints = study_data.tree.get(["input", "bindingconstraints", "bindingconstraints"]) - - id_to_remove = [] + config = study_data.tree.get(["input", "bindingconstraints", "bindingconstraints"]) - for id, bc in binding_constraints.items(): - if f"{self.area_id}.{self.cluster_id.lower()}" in bc.keys(): - id_to_remove.append(id) + # Binding constraints IDs to remove + ids_to_remove = set() - for id in id_to_remove: - study_data.tree.delete( - [ - "input", - "bindingconstraints", - binding_constraints[id]["id"], - ] - ) - study_data.config.bindings.remove( - next(iter([bind for bind in study_data.config.bindings if bind.id == binding_constraints[id]["id"]])) - ) + # Cluster IDs are stored in lower case in the binding contraints configuration file. + cluster_id = self.cluster_id.lower() + for bc_id, bc_props in config.items(): + if f"{self.area_id}.{cluster_id}" in bc_props.keys(): + ids_to_remove.add(bc_id) - del binding_constraints[id] + for bc_id in ids_to_remove: + study_data.tree.delete(["input", "bindingconstraints", config[bc_id]["id"]]) + bc = next(iter([bind for bind in study_data.config.bindings if bind.id == config[bc_id]["id"]])) + study_data.config.bindings.remove(bc) + del config[bc_id] study_data.tree.save( - binding_constraints, + config, ["input", "bindingconstraints", "bindingconstraints"], ) diff --git a/antarest/study/storage/variantstudy/model/command/remove_district.py b/antarest/study/storage/variantstudy/model/command/remove_district.py index c8d7094284..36995b5c11 100644 --- a/antarest/study/storage/variantstudy/model/command/remove_district.py +++ b/antarest/study/storage/variantstudy/model/command/remove_district.py @@ -8,10 +8,20 @@ class RemoveDistrict(ICommand): - id: str + """ + Command used to remove a district from the study. + """ + + # Overloaded metadata + # =================== + + command_name = CommandName.REMOVE_DISTRICT + version = 1 - def __init__(self, **data: Any) -> None: - super().__init__(command_name=CommandName.REMOVE_DISTRICT, version=1, **data) + # Command parameters + # ================== + + id: str def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, Dict[str, Any]]: del study_data.sets[self.id] diff --git a/antarest/study/storage/variantstudy/model/command/remove_link.py b/antarest/study/storage/variantstudy/model/command/remove_link.py index 5ec4c042cb..f70cecfb59 100644 --- a/antarest/study/storage/variantstudy/model/command/remove_link.py +++ b/antarest/study/storage/variantstudy/model/command/remove_link.py @@ -12,9 +12,15 @@ class RemoveLink(ICommand): Command used to remove a link. """ - command_name: CommandName = CommandName.REMOVE_LINK + # Overloaded metadata + # =================== + + command_name = CommandName.REMOVE_LINK version: int = 1 + # Command parameters + # ================== + # Properties of the `REMOVE_LINK` command: area1: str area2: str diff --git a/antarest/study/storage/variantstudy/model/command/remove_renewables_cluster.py b/antarest/study/storage/variantstudy/model/command/remove_renewables_cluster.py index ae647b11c7..46cfaaf0c8 100644 --- a/antarest/study/storage/variantstudy/model/command/remove_renewables_cluster.py +++ b/antarest/study/storage/variantstudy/model/command/remove_renewables_cluster.py @@ -1,27 +1,30 @@ -from typing import Any, Dict, List, Tuple +import typing as t from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy -from antarest.study.storage.variantstudy.business.utils_binding_constraint import ( - remove_area_cluster_from_binding_constraints, -) from antarest.study.storage.variantstudy.model.command.common import CommandName, CommandOutput from antarest.study.storage.variantstudy.model.command.icommand import MATCH_SIGNATURE_SEPARATOR, ICommand from antarest.study.storage.variantstudy.model.model import CommandDTO class RemoveRenewablesCluster(ICommand): + """ + Command used to remove a renewable cluster in an area. + """ + + # Overloaded metadata + # =================== + + command_name = CommandName.REMOVE_RENEWABLES_CLUSTER + version = 1 + + # Command parameters + # ================== + area_id: str cluster_id: str - def __init__(self, **data: Any) -> None: - super().__init__( - command_name=CommandName.REMOVE_RENEWABLES_CLUSTER, - version=1, - **data, - ) - - def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, Dict[str, Any]]: + def _apply_config(self, study_data: FileStudyTreeConfig) -> t.Tuple[CommandOutput, t.Dict[str, t.Any]]: """ Applies configuration changes to the study data: remove the renewable clusters from the storages list. @@ -57,8 +60,6 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, # Remove the Renewable cluster from the configuration area.renewables.remove(renewable) - remove_area_cluster_from_binding_constraints(study_data, self.area_id, self.cluster_id) - message = f"Renewable cluster '{self.cluster_id}' removed from the area '{self.area_id}'." return CommandOutput(status=True, message=message), {} @@ -73,13 +74,19 @@ def _apply(self, study_data: FileStudy) -> CommandOutput: Returns: The output of the command execution. """ + # Search the Area in the configuration + if self.area_id not in study_data.config.areas: + message = f"Area '{self.area_id}' does not exist in the study configuration." + return CommandOutput(status=False, message=message) + # It is required to delete the files and folders that correspond to the renewable cluster # BEFORE updating the configuration, as we need the configuration to do so. # Specifically, deleting the time series uses the list of renewable clusters from the configuration. + series_id = self.cluster_id.lower() paths = [ ["input", "renewables", "clusters", self.area_id, "list", self.cluster_id], - ["input", "renewables", "series", self.area_id, self.cluster_id], + ["input", "renewables", "series", self.area_id, series_id], ] area: Area = study_data.config.areas[self.area_id] if len(area.renewables) == 1: @@ -87,13 +94,14 @@ def _apply(self, study_data: FileStudy) -> CommandOutput: for path in paths: study_data.tree.delete(path) + # Deleting the renewable cluster in the configuration must be done AFTER # deleting the files and folders. return self._apply_config(study_data.config)[0] def to_dto(self) -> CommandDTO: return CommandDTO( - action=CommandName.REMOVE_RENEWABLES_CLUSTER.value, + action=self.command_name.value, args={"area_id": self.area_id, "cluster_id": self.cluster_id}, ) @@ -111,8 +119,8 @@ def match(self, other: ICommand, equal: bool = False) -> bool: return False return self.cluster_id == other.cluster_id and self.area_id == other.area_id - def _create_diff(self, other: "ICommand") -> List["ICommand"]: + def _create_diff(self, other: "ICommand") -> t.List["ICommand"]: return [] - def get_inner_matrices(self) -> List[str]: + def get_inner_matrices(self) -> t.List[str]: return [] diff --git a/antarest/study/storage/variantstudy/model/command/remove_st_storage.py b/antarest/study/storage/variantstudy/model/command/remove_st_storage.py index 09539efb42..0fd4bbb1de 100644 --- a/antarest/study/storage/variantstudy/model/command/remove_st_storage.py +++ b/antarest/study/storage/variantstudy/model/command/remove_st_storage.py @@ -17,14 +17,17 @@ class RemoveSTStorage(ICommand): Command used to remove a short-terme storage from an area. """ - area_id: str = Field(description="Area ID", regex=r"[a-z0-9_(),& -]+") - storage_id: str = Field( - description="Short term storage ID", - regex=r"[a-z0-9_(),& -]+", - ) + # Overloaded metadata + # =================== + + command_name = CommandName.REMOVE_ST_STORAGE + version = 1 - def __init__(self, **data: Any) -> None: - super().__init__(command_name=CommandName.REMOVE_ST_STORAGE, version=1, **data) + # Command parameters + # ================== + + area_id: str = Field(description="Area ID", regex=r"[a-z0-9_(),& -]+") + storage_id: str = Field(description="Short term storage ID", regex=r"[a-z0-9_(),& -]+") def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, Dict[str, Any]]: """ @@ -43,7 +46,7 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, return ( CommandOutput( status=False, - message=(f"Invalid study version {version}, at least version {REQUIRED_VERSION} is required."), + message=f"Invalid study version {version}, at least version {REQUIRED_VERSION} is required.", ), {}, ) @@ -53,7 +56,7 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, return ( CommandOutput( status=False, - message=(f"Area '{self.area_id}' does not exist in the study configuration."), + message=f"Area '{self.area_id}' does not exist in the study configuration.", ), {}, ) @@ -67,7 +70,7 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, return ( CommandOutput( status=False, - message=(f"Short term storage '{self.storage_id}' does not exist in the area '{self.area_id}'."), + message=f"Short term storage '{self.storage_id}' does not exist in the area '{self.area_id}'.", ), {}, ) @@ -78,7 +81,7 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, return ( CommandOutput( status=True, - message=(f"Short term storage '{self.storage_id}' removed from the area '{self.area_id}'."), + message=f"Short term storage '{self.storage_id}' removed from the area '{self.area_id}'.", ), {}, ) diff --git a/antarest/study/storage/variantstudy/model/command/replace_matrix.py b/antarest/study/storage/variantstudy/model/command/replace_matrix.py index b248cd4f05..4b66584c39 100644 --- a/antarest/study/storage/variantstudy/model/command/replace_matrix.py +++ b/antarest/study/storage/variantstudy/model/command/replace_matrix.py @@ -16,14 +16,24 @@ class ReplaceMatrix(ICommand): + """ + Command used to replace a matrice in an area. + """ + + # Overloaded metadata + # =================== + + command_name = CommandName.REPLACE_MATRIX + version = 1 + + # Command parameters + # ================== + target: str matrix: Union[List[List[MatrixData]], str] _validate_matrix = validator("matrix", each_item=True, always=True, allow_reuse=True)(validate_matrix) - def __init__(self, **data: Any) -> None: - super().__init__(command_name=CommandName.REPLACE_MATRIX, version=1, **data) - def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, Dict[str, Any]]: return ( CommandOutput( diff --git a/antarest/study/storage/variantstudy/model/command/update_binding_constraint.py b/antarest/study/storage/variantstudy/model/command/update_binding_constraint.py index 3ec874a375..ea52dca4c4 100644 --- a/antarest/study/storage/variantstudy/model/command/update_binding_constraint.py +++ b/antarest/study/storage/variantstudy/model/command/update_binding_constraint.py @@ -26,9 +26,15 @@ class UpdateBindingConstraint(AbstractBindingConstraintCommand): Command used to update a binding constraint. """ - command_name: CommandName = CommandName.UPDATE_BINDING_CONSTRAINT + # Overloaded metadata + # =================== + + command_name = CommandName.UPDATE_BINDING_CONSTRAINT version: int = 1 + # Command parameters + # ================== + # Properties of the `UPDATE_BINDING_CONSTRAINT` command: id: str diff --git a/antarest/study/storage/variantstudy/model/command/update_comments.py b/antarest/study/storage/variantstudy/model/command/update_comments.py index 69538aeb1a..028cbc5060 100644 --- a/antarest/study/storage/variantstudy/model/command/update_comments.py +++ b/antarest/study/storage/variantstudy/model/command/update_comments.py @@ -9,12 +9,20 @@ class UpdateComments(ICommand): - """Update the file contained at settings/comments.txt""" + """ + Command used to update the comments of the study located in `settings/comments.txt`. + """ - comments: str + # Overloaded metadata + # =================== + + command_name = CommandName.UPDATE_COMMENTS + version = 1 - def __init__(self, **data: Any) -> None: - super().__init__(command_name=CommandName.UPDATE_COMMENTS, version=1, **data) + # Command parameters + # ================== + + comments: str def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, Dict[str, Any]]: return ( diff --git a/antarest/study/storage/variantstudy/model/command/update_config.py b/antarest/study/storage/variantstudy/model/command/update_config.py index 61b04061e0..91caa6a738 100644 --- a/antarest/study/storage/variantstudy/model/command/update_config.py +++ b/antarest/study/storage/variantstudy/model/command/update_config.py @@ -10,12 +10,22 @@ class UpdateConfig(ICommand): + """ + Command used to create a thermal cluster in an area. + """ + + # Overloaded metadata + # =================== + + command_name = CommandName.UPDATE_CONFIG + version = 1 + + # Command parameters + # ================== + target: str data: Union[str, int, float, bool, JSON, None] - def __init__(self, **data: Any) -> None: - super().__init__(command_name=CommandName.UPDATE_CONFIG, version=1, **data) - def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, Dict[str, Any]]: return CommandOutput(status=True, message="ok"), dict() diff --git a/antarest/study/storage/variantstudy/model/command/update_district.py b/antarest/study/storage/variantstudy/model/command/update_district.py index 199806542a..1a14e37acd 100644 --- a/antarest/study/storage/variantstudy/model/command/update_district.py +++ b/antarest/study/storage/variantstudy/model/command/update_district.py @@ -9,15 +9,25 @@ class UpdateDistrict(ICommand): + """ + Command used to update a district in a study. + """ + + # Overloaded metadata + # =================== + + command_name = CommandName.UPDATE_DISTRICT + version = 1 + + # Command parameters + # ================== + id: str base_filter: Optional[DistrictBaseFilter] filter_items: Optional[List[str]] output: Optional[bool] comments: Optional[str] - def __init__(self, **data: Any) -> None: - super().__init__(command_name=CommandName.UPDATE_DISTRICT, version=1, **data) - def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, Dict[str, Any]]: base_set = study_data.sets[self.id] if self.id not in study_data.sets: diff --git a/antarest/study/storage/variantstudy/model/command/update_playlist.py b/antarest/study/storage/variantstudy/model/command/update_playlist.py index 6cd4216a93..c70dfebbb5 100644 --- a/antarest/study/storage/variantstudy/model/command/update_playlist.py +++ b/antarest/study/storage/variantstudy/model/command/update_playlist.py @@ -9,14 +9,24 @@ class UpdatePlaylist(ICommand): + """ + Command used to update a playlist. + """ + + # Overloaded metadata + # =================== + + command_name = CommandName.UPDATE_PLAYLIST + version = 1 + + # Command parameters + # ================== + active: bool items: Optional[List[int]] = None weights: Optional[Dict[int, float]] = None reverse: bool = False - def __init__(self, **data: Any) -> None: - super().__init__(command_name=CommandName.UPDATE_PLAYLIST, version=1, **data) - def _apply(self, study_data: FileStudy) -> CommandOutput: FileStudyHelpers.set_playlist( study_data, diff --git a/antarest/study/storage/variantstudy/model/command/update_raw_file.py b/antarest/study/storage/variantstudy/model/command/update_raw_file.py index e9d21c7dd4..c4b6cfb46b 100644 --- a/antarest/study/storage/variantstudy/model/command/update_raw_file.py +++ b/antarest/study/storage/variantstudy/model/command/update_raw_file.py @@ -10,12 +10,22 @@ class UpdateRawFile(ICommand): + """ + Command used to update a raw file. + """ + + # Overloaded metadata + # =================== + + command_name = CommandName.UPDATE_FILE + version = 1 + + # Command parameters + # ================== + target: str b64Data: str - def __init__(self, **data: Any) -> None: - super().__init__(command_name=CommandName.UPDATE_FILE, version=1, **data) - def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, Dict[str, Any]]: return CommandOutput(status=True, message="ok"), {} diff --git a/antarest/study/storage/variantstudy/model/command/update_scenario_builder.py b/antarest/study/storage/variantstudy/model/command/update_scenario_builder.py index cf1e7f0936..33463d034b 100644 --- a/antarest/study/storage/variantstudy/model/command/update_scenario_builder.py +++ b/antarest/study/storage/variantstudy/model/command/update_scenario_builder.py @@ -9,10 +9,20 @@ class UpdateScenarioBuilder(ICommand): - data: Dict[str, Any] + """ + Command used to update a scenario builder table. + """ + + # Overloaded metadata + # =================== + + command_name = CommandName.UPDATE_SCENARIO_BUILDER + version = 1 - def __init__(self, **data: Any) -> None: - super().__init__(command_name=CommandName.UPDATE_SCENARIO_BUILDER, version=1, **data) + # Command parameters + # ================== + + data: Dict[str, Any] def _apply(self, study_data: FileStudy) -> CommandOutput: def remove_rand_values(obj: Dict[str, Any]) -> Dict[str, Any]: diff --git a/antarest/study/web/raw_studies_blueprint.py b/antarest/study/web/raw_studies_blueprint.py index c25af6e8a4..b84a9bf107 100644 --- a/antarest/study/web/raw_studies_blueprint.py +++ b/antarest/study/web/raw_studies_blueprint.py @@ -3,10 +3,10 @@ import json import logging import pathlib -from typing import Any, List +import typing as t from fastapi import APIRouter, Body, Depends, File, HTTPException -from fastapi.params import Param +from fastapi.params import Param, Query from starlette.responses import JSONResponse, PlainTextResponse, Response, StreamingResponse from antarest.core.config import Config @@ -69,7 +69,6 @@ def create_raw_study_routes( "/studies/{uuid}/raw", tags=[APITag.study_raw_data], summary="Retrieve Raw Data from Study: JSON, Text, or File Attachment", - response_model=None, ) def get_study( uuid: str, @@ -77,9 +76,9 @@ def get_study( depth: int = 3, formatted: bool = True, current_user: JWTUser = Depends(auth.get_current_user), - ) -> Any: + ) -> t.Any: """ - Fetches raw data from a study identified by a UUID, and returns the data + Fetches raw data from a study, and returns the data in different formats based on the file type, or as a JSON response. Parameters: @@ -158,14 +157,24 @@ def get_study( status_code=http.HTTPStatus.NO_CONTENT, tags=[APITag.study_raw_data], summary="Update data by posting formatted data", - response_model=None, ) def edit_study( uuid: str, path: str = Param("/", examples=get_path_examples()), # type: ignore data: SUB_JSON = Body(default=""), current_user: JWTUser = Depends(auth.get_current_user), - ) -> Any: + ) -> None: + """ + Updates raw data for a study by posting formatted data. + + > NOTE: use the PUT endpoint to upload a file. + + Parameters: + - `uuid`: The UUID of the study. + - `path`: The path to the data to update. Defaults to "/". + - `data`: The formatted data to be posted. Defaults to an empty string. + The data could be a JSON object, or a simple string. + """ logger.info( f"Editing data at {path} for study {uuid}", extra={"user": current_user.id}, @@ -179,29 +188,55 @@ def edit_study( status_code=http.HTTPStatus.NO_CONTENT, tags=[APITag.study_raw_data], summary="Update data by posting a Raw file", - response_model=None, ) def replace_study_file( uuid: str, path: str = Param("/", examples=get_path_examples()), # type: ignore file: bytes = File(...), + create_missing: bool = Query( + False, + description="Create file or parent directories if missing.", + ), # type: ignore current_user: JWTUser = Depends(auth.get_current_user), - ) -> Any: + ) -> None: + """ + Update raw data for a study by posting a raw file. + + Parameters: + - `uuid`: The UUID of the study. + - `path`: The path to the data to update. Defaults to "/". + - `file`: The raw file to be posted (e.g. a CSV file opened in binary mode). + - `create_missing`: Flag to indicate whether to create file or parent directories if missing. + """ logger.info( f"Uploading new data file at {path} for study {uuid}", extra={"user": current_user.id}, ) path = sanitize_uuid(path) params = RequestParameters(user=current_user) - study_service.edit_study(uuid, path, file, params) + study_service.edit_study(uuid, path, file, params, create_missing=create_missing) @bp.get( "/studies/{uuid}/raw/validate", summary="Launch test validation on study", tags=[APITag.study_raw_data], - response_model=List[str], + response_model=t.List[str], ) - def validate(uuid: str, current_user: JWTUser = Depends(auth.get_current_user)) -> Any: + def validate( + uuid: str, + current_user: JWTUser = Depends(auth.get_current_user), + ) -> t.List[str]: + """ + Launches test validation on the raw data of a study. + The validation is done recursively on all the files in the study + + Parameters: + - `uuid`: The UUID of the study. + + Response: + - A list of strings indicating validation errors (if any) for the study's raw data. + The list is empty if no errors were found. + """ logger.info( f"Validating data for study {uuid}", extra={"user": current_user.id}, diff --git a/antarest/study/web/studies_blueprint.py b/antarest/study/web/studies_blueprint.py index 2548102b56..76b5e1a706 100644 --- a/antarest/study/web/studies_blueprint.py +++ b/antarest/study/web/studies_blueprint.py @@ -8,12 +8,13 @@ from markupsafe import escape from antarest.core.config import Config +from antarest.core.exceptions import BadZipBinary from antarest.core.filetransfer.model import FileDownloadTaskDTO from antarest.core.filetransfer.service import FileTransferManager from antarest.core.jwt import JWTUser from antarest.core.model import PublicMode from antarest.core.requests import RequestParameters -from antarest.core.utils.utils import sanitize_uuid +from antarest.core.utils.utils import BadArchiveContent, sanitize_uuid from antarest.core.utils.web import APITag from antarest.login.auth import Auth from antarest.study.model import ( @@ -113,14 +114,17 @@ def import_study( current_user: JWTUser = Depends(auth.get_current_user), ) -> str: """ - Upload and import a study from your computer to the Antares Web server. + Upload and import a compressed study from your computer to the Antares Web server. Args: - - `study`: The study file in ZIP format or its corresponding bytes. + - `study`: The binary content of the study file in ZIP or 7z format. - `groups`: The groups your study will belong to (Default: current user's groups). Returns: - The ID of the imported study. + + Raises: + - 415 error if the archive is corrupted or in an unknown format. """ logger.info("Importing new study", extra={"user": current_user.id}) zip_binary = io.BytesIO(study) @@ -129,7 +133,10 @@ def import_study( group_ids = groups.split(",") if groups else [group.id for group in current_user.groups] group_ids = [sanitize_uuid(gid) for gid in set(group_ids)] # sanitize and avoid duplicates - uuid = study_service.import_study(zip_binary, group_ids, params) + try: + uuid = study_service.import_study(zip_binary, group_ids, params) + except BadArchiveContent as e: + raise BadZipBinary(str(e)) return uuid diff --git a/antarest/study/web/study_data_blueprint.py b/antarest/study/web/study_data_blueprint.py index 610e3b7263..de9fbcecd1 100644 --- a/antarest/study/web/study_data_blueprint.py +++ b/antarest/study/web/study_data_blueprint.py @@ -4,6 +4,7 @@ from fastapi import APIRouter, Body, Depends from fastapi.params import Body, Query +from starlette.responses import RedirectResponse from antarest.core.config import Config from antarest.core.jwt import JWTUser @@ -18,8 +19,13 @@ from antarest.study.business.area_management import AreaCreationDTO, AreaInfoDTO, AreaType, AreaUI, LayerInfoDTO from antarest.study.business.areas.hydro_management import ManagementOptionsFormFields from antarest.study.business.areas.properties_management import PropertiesFormFields -from antarest.study.business.areas.renewable_management import RenewableFormFields -from antarest.study.business.areas.thermal_management import ThermalFormFields +from antarest.study.business.areas.renewable_management import ( + RenewableClusterCreation, + RenewableClusterInput, + RenewableClusterOutput, +) +from antarest.study.business.areas.st_storage_management import * +from antarest.study.business.areas.thermal_management import * from antarest.study.business.binding_constraint_management import ConstraintTermDTO, UpdateBindingConstProps from antarest.study.business.correlation_management import CorrelationFormFields, CorrelationManager, CorrelationMatrix from antarest.study.business.district_manager import DistrictCreationDTO, DistrictInfoDTO, DistrictUpdateDTO @@ -27,13 +33,6 @@ from antarest.study.business.link_management import LinkInfoDTO from antarest.study.business.optimization_management import OptimizationFormFields from antarest.study.business.playlist_management import PlaylistColumns -from antarest.study.business.st_storage_manager import ( - StorageCreation, - StorageInput, - StorageOutput, - STStorageMatrix, - STStorageTimeSeries, -) from antarest.study.business.table_mode_management import ColumnsModelTypes, TableTemplateType from antarest.study.business.thematic_trimming_management import ThematicTrimmingFormFields from antarest.study.business.timeseries_config_management import TSFormFields @@ -100,7 +99,7 @@ def get_links( @bp.post( "/studies/{uuid}/areas", tags=[APITag.study_data], - summary="Create a new area/cluster", + summary="Create a new area", response_model=AreaInfoDTO, ) def create_area( @@ -296,7 +295,7 @@ def remove_layer( extra={"user": current_user.id}, ) params = RequestParameters(user=current_user) - study = study_service.check_study_access(uuid, StudyPermissionType.DELETE, params) + study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) study_service.areas.remove_layer(study, layer_id) @bp.get( @@ -431,8 +430,8 @@ def edit_matrix( Args: - `uuid`: The UUID of the study. - - `path`: The path of the matrix to update. - - `matrix_edit_instructions`: A list of edit instructions to be applied to the matrix. + - `path`: the path of the matrix to update. + - `matrix_edit_instructions`: a list of edit instructions to be applied to the matrix. Permissions: - User must have WRITE permission on the study. @@ -929,7 +928,7 @@ def get_allocation_matrix( Get the hydraulic allocation matrix for all areas. Parameters: - - `uuid`: the study UUID. + - `uuid`: The study UUID. Returns the data frame matrix, where: - the rows are the areas, @@ -959,7 +958,7 @@ def get_allocation_form_fields( Get the form fields used for the allocation form. Parameters: - - `uuid`: the study UUID, + - `uuid`: The study UUID, - `area_id`: the area ID. Returns the allocation form fields. @@ -997,7 +996,7 @@ def set_allocation_form_fields( Update the hydraulic allocation of a given area. Parameters: - - `uuid`: the study UUID, + - `uuid`: The study UUID, - `area_id`: the area ID. Returns the updated allocation form fields. @@ -1042,15 +1041,15 @@ def get_correlation_matrix( Parameters: - `uuid`: The UUID of the study. - - `columns`: A filter on the area identifiers: + - `columns`: a filter on the area identifiers: - Use no parameter to select all areas. - Use an area identifier to select a single area. - Use a comma-separated list of areas to select those areas. Returns the hydraulic/load/solar/wind correlation matrix with the following attributes: - - `index`: A list of all study areas. - - `columns`: A list of selected production areas. - - `data`: A 2D-array matrix of correlation coefficients with values in the range of -1 to 1. + - `index`: a list of all study areas. + - `columns`: a list of selected production areas. + - `data`: a 2D-array matrix of correlation coefficients with values in the range of -1 to 1. """ params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) @@ -1094,9 +1093,9 @@ def set_correlation_matrix( Parameters: - `uuid`: The UUID of the study. - - `index`: A list of all study areas. - - `columns`: A list of selected production areas. - - `data`: A 2D-array matrix of correlation coefficients with values in the range of -1 to 1. + - `index`: a list of all study areas. + - `columns`: a list of selected production areas. + - `data`: a 2D-array matrix of correlation coefficients with values in the range of -1 to 1. Returns the hydraulic/load/solar/wind correlation matrix updated """ @@ -1284,127 +1283,375 @@ def set_properties_form_values( study_service.properties_manager.set_field_values(study, area_id, form_fields) @bp.get( - path="/studies/{uuid}/areas/{area_id}/clusters/renewable/{cluster_id}/form", + path="/studies/{uuid}/areas/{area_id}/clusters/renewable", tags=[APITag.study_data], - summary="Get renewable options for a given cluster", - response_model=RenewableFormFields, - response_model_exclude_none=True, + summary="Get all renewable clusters", + response_model=Sequence[RenewableClusterOutput], ) - def get_renewable_form_values( + def get_renewable_clusters( + uuid: str, + area_id: str, + current_user: JWTUser = Depends(auth.get_current_user), + ) -> Sequence[RenewableClusterOutput]: + logger.info( + "Getting renewable clusters for study %s and area %s", + uuid, + area_id, + extra={"user": current_user.id}, + ) + params = RequestParameters(user=current_user) + study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) + return study_service.renewable_manager.get_clusters(study, area_id) + + @bp.get( + path="/studies/{uuid}/areas/{area_id}/clusters/renewable/{cluster_id}", + tags=[APITag.study_data], + summary="Get a single renewable cluster", + response_model=RenewableClusterOutput, + ) + def get_renewable_cluster( uuid: str, area_id: str, cluster_id: str, current_user: JWTUser = Depends(auth.get_current_user), - ) -> RenewableFormFields: + ) -> RenewableClusterOutput: logger.info( - "Getting renewable form values for study %s and cluster %s", + "Getting renewable cluster values for study %s and cluster %s", uuid, cluster_id, extra={"user": current_user.id}, ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.renewable_manager.get_field_values(study, area_id, cluster_id) + return study_service.renewable_manager.get_cluster(study, area_id, cluster_id) + + @bp.get( + path="/studies/{uuid}/areas/{area_id}/clusters/renewable/{cluster_id}/form", + tags=[APITag.study_data], + summary="Get renewable configuration for a given cluster (deprecated)", + response_class=RedirectResponse, + deprecated=True, + ) + def redirect_get_renewable_cluster( + uuid: str, + area_id: str, + cluster_id: str, + ) -> str: + return f"/v1/studies/{uuid}/areas/{area_id}/clusters/renewable/{cluster_id}" + + @bp.post( + path="/studies/{uuid}/areas/{area_id}/clusters/renewable", + tags=[APITag.study_data], + summary="Create a new renewable cluster", + response_model=RenewableClusterOutput, + ) + def create_renewable_cluster( + uuid: str, + area_id: str, + cluster_data: RenewableClusterCreation, + current_user: JWTUser = Depends(auth.get_current_user), + ) -> RenewableClusterOutput: + """ + Create a new renewable cluster. + + Args: + - `uuid`: The UUID of the study. + - `area_id`: the area ID. + - `cluster_data`: the properties used for creation: + "name" and "group". + + Returns: The properties of the newly-created renewable clusters. + """ + logger.info( + f"Creating renewable cluster for study '{uuid}' and area '{area_id}'", + extra={"user": current_user.id}, + ) + request_params = RequestParameters(user=current_user) + study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, request_params) + return study_service.renewable_manager.create_cluster(study, area_id, cluster_data) + + @bp.patch( + path="/studies/{uuid}/areas/{area_id}/clusters/renewable/{cluster_id}", + tags=[APITag.study_data], + summary="Update a renewable cluster", + response_model=RenewableClusterOutput, + ) + def update_renewable_cluster( + uuid: str, + area_id: str, + cluster_id: str, + cluster_data: RenewableClusterInput, + current_user: JWTUser = Depends(auth.get_current_user), + ) -> RenewableClusterOutput: + logger.info( + f"Updating renewable cluster for study '{uuid}' and cluster '{cluster_id}'", + extra={"user": current_user.id}, + ) + request_params = RequestParameters(user=current_user) + study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, request_params) + return study_service.renewable_manager.update_cluster(study, area_id, cluster_id, cluster_data) @bp.put( path="/studies/{uuid}/areas/{area_id}/clusters/renewable/{cluster_id}/form", tags=[APITag.study_data], - summary="Set renewable form values for a given cluster", + summary="Get renewable configuration for a given cluster (deprecated)", + response_model=RenewableClusterOutput, + deprecated=True, ) - def set_renewable_form_values( + def redirect_update_renewable_cluster( uuid: str, area_id: str, cluster_id: str, - form_fields: RenewableFormFields, + cluster_data: RenewableClusterInput, + current_user: JWTUser = Depends(auth.get_current_user), + ) -> RenewableClusterOutput: + # We cannot perform redirection, because we have a PUT, where a PATCH is required. + return update_renewable_cluster(uuid, area_id, cluster_id, cluster_data, current_user=current_user) + + @bp.delete( + path="/studies/{uuid}/areas/{area_id}/clusters/renewable", + tags=[APITag.study_data], + summary="Remove renewable clusters", + status_code=HTTPStatus.NO_CONTENT, + response_model=None, + ) + def delete_renewable_clusters( + uuid: str, + area_id: str, + cluster_ids: Sequence[str], current_user: JWTUser = Depends(auth.get_current_user), ) -> None: + """ + Remove one or several renewable cluster(s) and it's time series. + + Args: + - `uuid`: The UUID of the study. + - `area_id`: the area ID. + - `cluster_ids`: list of IDs to remove. + """ logger.info( - "Setting renewable form values for study %s and cluster %s", - uuid, - cluster_id, + f"Deleting renewable clusters {cluster_ids!r} for study '{uuid}' and area '{area_id}'", extra={"user": current_user.id}, ) request_params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, request_params) + study_service.renewable_manager.delete_clusters(study, area_id, cluster_ids) + + @bp.get( + path="/studies/{uuid}/areas/{area_id}/clusters/thermal", + tags=[APITag.study_data], + summary="Get thermal clusters for a given area", + response_model=Sequence[ThermalClusterOutput], + ) + def get_thermal_clusters( + uuid: str, + area_id: str, + current_user: JWTUser = Depends(auth.get_current_user), + ) -> Sequence[ThermalClusterOutput]: + """ + Retrieve the list of thermal clusters for a specified area. + + Args: + - `uuid`: The UUID of the study. + - `area_id`: the area ID. - study_service.renewable_manager.set_field_values(study, area_id, cluster_id, form_fields) + Returns: The list thermal clusters. + """ + logger.info( + "Getting thermal clusters for study %s and area %s", + uuid, + area_id, + extra={"user": current_user.id}, + ) + params = RequestParameters(user=current_user) + study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) + return study_service.thermal_manager.get_clusters(study, area_id) @bp.get( - path="/studies/{uuid}/areas/{area_id}/clusters/thermal/{cluster_id}/form", + path="/studies/{uuid}/areas/{area_id}/clusters/thermal/{cluster_id}", tags=[APITag.study_data], - summary="Get thermal options for a given cluster", - response_model=ThermalFormFields, - response_model_exclude_none=True, + summary="Get thermal configuration for a given cluster", + response_model=ThermalClusterOutput, ) - def get_thermal_form_values( + def get_thermal_cluster( uuid: str, area_id: str, cluster_id: str, current_user: JWTUser = Depends(auth.get_current_user), - ) -> ThermalFormFields: + ) -> ThermalClusterOutput: + """ + Retrieve the thermal clusters for a specified area. + + Args: + - `uuid`: The UUID of the study. + - `area_id`: the area ID. + - `cluster_id`: the cluster ID. + + Returns: The properties of the thermal clusters. + """ logger.info( - "Getting thermal form values for study %s and cluster %s", + "Getting thermal cluster values for study %s and cluster %s", uuid, cluster_id, extra={"user": current_user.id}, ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.thermal_manager.get_field_values(study, area_id, cluster_id) + return study_service.thermal_manager.get_cluster(study, area_id, cluster_id) + + @bp.get( + path="/studies/{uuid}/areas/{area_id}/clusters/thermal/{cluster_id}/form", + tags=[APITag.study_data], + summary="Get thermal configuration for a given cluster (deprecated)", + response_class=RedirectResponse, + deprecated=True, + ) + def redirect_get_thermal_cluster( + uuid: str, + area_id: str, + cluster_id: str, + ) -> str: + return f"/v1/studies/{uuid}/areas/{area_id}/clusters/thermal/{cluster_id}" + + @bp.post( + path="/studies/{uuid}/areas/{area_id}/clusters/thermal", + tags=[APITag.study_data], + summary="Create a new thermal cluster for a given area", + response_model=ThermalClusterOutput, + ) + def create_thermal_cluster( + uuid: str, + area_id: str, + cluster_data: ThermalClusterCreation, + current_user: JWTUser = Depends(auth.get_current_user), + ) -> ThermalClusterOutput: + """ + Create a new thermal cluster for a specified area. + + Args: + - `uuid`: The UUID of the study. + - `area_id`: the area ID. + - `cluster_data`: the properties used for creation: + "name" and "group". + + Returns: The properties of the newly-created thermal clusters. + """ + logger.info( + f"Creating thermal cluster for study '{uuid}' and area '{area_id}'", + extra={"user": current_user.id}, + ) + request_params = RequestParameters(user=current_user) + study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, request_params) + return study_service.thermal_manager.create_cluster(study, area_id, cluster_data) + + @bp.patch( + path="/studies/{uuid}/areas/{area_id}/clusters/thermal/{cluster_id}", + tags=[APITag.study_data], + summary="Update thermal cluster for a given area", + response_model=ThermalClusterOutput, + ) + def update_thermal_cluster( + uuid: str, + area_id: str, + cluster_id: str, + cluster_data: ThermalClusterInput, + current_user: JWTUser = Depends(auth.get_current_user), + ) -> ThermalClusterOutput: + """ + Update the properties of a thermal cluster for a specified area. + + Args: + - `uuid`: The UUID of the study. + - `area_id`: the area ID. + - `cluster_data`: the properties used for updating. + + Returns: The properties of the updated thermal clusters. + """ + logger.info( + f"Updating thermal cluster for study '{uuid}' and cluster '{cluster_id}'", + extra={"user": current_user.id}, + ) + request_params = RequestParameters(user=current_user) + study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, request_params) + return study_service.thermal_manager.update_cluster(study, area_id, cluster_id, cluster_data) @bp.put( path="/studies/{uuid}/areas/{area_id}/clusters/thermal/{cluster_id}/form", tags=[APITag.study_data], - summary="Set thermal form values for a given cluster", + summary="Get thermal configuration for a given cluster (deprecated)", + response_model=ThermalClusterOutput, + deprecated=True, ) - def set_thermal_form_values( + def redirect_update_thermal_cluster( uuid: str, area_id: str, cluster_id: str, - form_fields: ThermalFormFields, + cluster_data: ThermalClusterInput, + current_user: JWTUser = Depends(auth.get_current_user), + ) -> ThermalClusterOutput: + # We cannot perform redirection, because we have a PUT, where a PATCH is required. + return update_thermal_cluster(uuid, area_id, cluster_id, cluster_data, current_user=current_user) + + @bp.delete( + path="/studies/{uuid}/areas/{area_id}/clusters/thermal", + tags=[APITag.study_data], + summary="Remove thermal clusters for a given area", + status_code=HTTPStatus.NO_CONTENT, + response_model=None, + ) + def delete_thermal_clusters( + uuid: str, + area_id: str, + cluster_ids: Sequence[str], current_user: JWTUser = Depends(auth.get_current_user), ) -> None: + """ + Remove one or several thermal cluster(s) from a specified area. + This endpoint removes the properties and time series of each thermal clusters. + + Args: + - `uuid`: The UUID of the study. + - `area_id`: the area ID. + - `cluster_ids`: list of thermal cluster IDs to remove. + """ logger.info( - "Setting thermal form values for study %s and cluster %s", - uuid, - cluster_id, + f"Deleting thermal clusters {cluster_ids!r} for study '{uuid}' and area '{area_id}'", extra={"user": current_user.id}, ) request_params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, request_params) - - study_service.thermal_manager.set_field_values(study, area_id, cluster_id, form_fields) + study_service.thermal_manager.delete_clusters(study, area_id, cluster_ids) @bp.get( path="/studies/{uuid}/areas/{area_id}/storages/{storage_id}", tags=[APITag.study_data], summary="Get the short-term storage properties", - response_model=StorageOutput, + response_model=STStorageOutput, ) def get_st_storage( uuid: str, area_id: str, storage_id: str, current_user: JWTUser = Depends(auth.get_current_user), - ) -> StorageOutput: + ) -> STStorageOutput: """ Retrieve the storages by given uuid and area id of a study. Args: - `uuid`: The UUID of the study. - - `area_id`: The area ID of the study. - - `storage_id`: The storage ID of the study. + - `area_id`: the area ID. + - `storage_id`: the storage ID of the study. Returns: One storage with the following attributes: - - `id`: The storage ID of the study. - - `name`: The name of the storage. - - `group`: The group of the storage. - - `injectionNominalCapacity`: The injection Nominal Capacity of the storage. - - `withdrawalNominalCapacity`: The withdrawal Nominal Capacity of the storage. - - `reservoirCapacity`: The reservoir capacity of the storage. - - `efficiency`: The efficiency of the storage. - - `initialLevel`: The initial Level of the storage. - - `initialLevelOptim`: The initial Level Optim of the storage. + - `id`: the storage ID of the study. + - `name`: the name of the storage. + - `group`: the group of the storage. + - `injectionNominalCapacity`: the injection Nominal Capacity of the storage. + - `withdrawalNominalCapacity`: the withdrawal Nominal Capacity of the storage. + - `reservoirCapacity`: the reservoir capacity of the storage. + - `efficiency`: the efficiency of the storage. + - `initialLevel`: the initial Level of the storage. + - `initialLevelOptim`: the initial Level Optim of the storage. Permissions: The user must have READ permission on the study. @@ -1421,30 +1668,30 @@ def get_st_storage( path="/studies/{uuid}/areas/{area_id}/storages", tags=[APITag.study_data], summary="Get the list of short-term storage properties", - response_model=Sequence[StorageOutput], + response_model=Sequence[STStorageOutput], ) def get_st_storages( uuid: str, area_id: str, current_user: JWTUser = Depends(auth.get_current_user), - ) -> Sequence[StorageOutput]: + ) -> Sequence[STStorageOutput]: """ Retrieve the short-term storages by given uuid and area ID of a study. Args: - `uuid`: The UUID of the study. - - `area_id`: The area ID. + - `area_id`: the area ID. Returns: A list of storages with the following attributes: - - `id`: The storage ID of the study. - - `name`: The name of the storage. - - `group`: The group of the storage. - - `injectionNominalCapacity`: The injection Nominal Capacity of the storage. - - `withdrawalNominalCapacity`: The withdrawal Nominal Capacity of the storage. - - `reservoirCapacity`: The reservoir capacity of the storage. - - `efficiency`: The efficiency of the storage. - - `initialLevel`: The initial Level of the storage. - - `initialLevelOptim`: The initial Level Optim of the storage. + - `id`: the storage ID of the study. + - `name`: the name of the storage. + - `group`: the group of the storage. + - `injectionNominalCapacity`: the injection Nominal Capacity of the storage. + - `withdrawalNominalCapacity`: the withdrawal Nominal Capacity of the storage. + - `reservoirCapacity`: the reservoir capacity of the storage. + - `efficiency`: the efficiency of the storage. + - `initialLevel`: the initial Level of the storage. + - `initialLevelOptim`: the initial Level Optim of the storage. Permissions: The user must have READ permission on the study. @@ -1475,14 +1722,14 @@ def get_st_storage_matrix( Args: - `uuid`: The UUID of the study. - - `area_id`: The area ID. - - `storage_id`: The ID of the short-term storage. - - `ts_name`: The name of the time series to retrieve. + - `area_id`: the area ID. + - `storage_id`: the ID of the short-term storage. + - `ts_name`: the name of the time series to retrieve. Returns: The time series matrix with the following attributes: - - `index`: A list of 0-indexed time series lines (8760 lines). - - `columns`: A list of 0-indexed time series columns (1 column). - - `data`: A 2D-array matrix representing the time series. + - `index`: a list of 0-indexed time series lines (8760 lines). + - `columns`: a list of 0-indexed time series columns (1 column). + - `data`: a 2D-array matrix representing the time series. Permissions: - User must have READ permission on the study. @@ -1513,10 +1760,10 @@ def update_st_storage_matrix( Args: - `uuid`: The UUID of the study. - - `area_id`: The area ID. - - `storage_id`: The ID of the short-term storage. - - `ts_name`: The name of the time series to retrieve. - - `ts`: The time series matrix to update. + - `area_id`: the area ID. + - `storage_id`: the ID of the short-term storage. + - `ts_name`: the name of the time series to retrieve. + - `ts`: the time series matrix to update. Permissions: - User must have WRITE permission on the study. @@ -1545,8 +1792,8 @@ def validate_st_storage_matrices( Args: - `uuid`: The UUID of the study. - - `area_id`: The area ID. - - `storage_id`: The ID of the short-term storage. + - `area_id`: the area ID. + - `storage_id`: the ID of the short-term storage. Permissions: - User must have READ permission on the study. @@ -1563,14 +1810,14 @@ def validate_st_storage_matrices( path="/studies/{uuid}/areas/{area_id}/storages", tags=[APITag.study_data], summary="Create a new short-term storage in an area", - response_model=StorageOutput, + response_model=STStorageOutput, ) def create_st_storage( uuid: str, area_id: str, - form: StorageCreation, + form: STStorageCreation, current_user: JWTUser = Depends(auth.get_current_user), - ) -> StorageOutput: + ) -> STStorageOutput: """ Create a new short-term storage in an area. @@ -1588,15 +1835,15 @@ def create_st_storage( - `initialLevelOptim`: The initial Level Optim of the updated storage Returns: New storage with the following attributes: - - `id`: The storage ID of the study. - - `name`: The name of the storage. - - `group`: The group of the storage. - - `injectionNominalCapacity`: The injection Nominal Capacity of the storage. - - `withdrawalNominalCapacity`: The withdrawal Nominal Capacity of the storage. - - `reservoirCapacity`: The reservoir capacity of the storage. - - `efficiency`: The efficiency of the storage. - - `initialLevel`: The initial Level of the storage. - - `initialLevelOptim`: The initial Level Optim of the storage. + - `id`: the storage ID of the study. + - `name`: the name of the storage. + - `group`: the group of the storage. + - `injectionNominalCapacity`: the injection Nominal Capacity of the storage. + - `withdrawalNominalCapacity`: the withdrawal Nominal Capacity of the storage. + - `reservoirCapacity`: the reservoir capacity of the storage. + - `efficiency`: the efficiency of the storage. + - `initialLevel`: the initial Level of the storage. + - `initialLevelOptim`: the initial Level Optim of the storage. Permissions: - User must have READ/WRITE permission on the study. @@ -1619,36 +1866,36 @@ def update_st_storage( uuid: str, area_id: str, storage_id: str, - form: StorageInput, + form: STStorageInput, current_user: JWTUser = Depends(auth.get_current_user), - ) -> StorageOutput: + ) -> STStorageOutput: """ Update short-term storage of a study. Args: - `uuid`: The UUID of the study. - - `area_id`: The area ID. - - `storage_id`: The storage id of the study that we want to update. - - `form`: The characteristic of the storage that we can update: - - `name`: The name of the updated storage. - - `group`: The group of the updated storage. - - `injectionNominalCapacity`: The injection Nominal Capacity of the updated storage. - - `withdrawalNominalCapacity`: The withdrawal Nominal Capacity of the updated storage. + - `area_id`: the area ID. + - `storage_id`: the storage id of the study that we want to update. + - `form`: the characteristic of the storage that we can update: + - `name`: the name of the updated storage. + - `group`: the group of the updated storage. + - `injectionNominalCapacity`: the injection Nominal Capacity of the updated storage. + - `withdrawalNominalCapacity`: the withdrawal Nominal Capacity of the updated storage. - `reservoirCapacity`: The reservoir capacity of the updated storage. - - `efficiency`: The efficiency of the updated storage - - `initialLevel`: The initial Level of the updated storage - - `initialLevelOptim`: The initial Level Optim of the updated storage + - `efficiency`: the efficiency of the updated storage + - `initialLevel`: the initial Level of the updated storage + - `initialLevelOptim`: the initial Level Optim of the updated storage Returns: The updated storage with the following attributes: - - `name`: The name of the updated storage. - - `group`: The group of the updated storage. - - `injectionNominalCapacity`: The injection Nominal Capacity of the updated storage. - - `withdrawalNominalCapacity`: The withdrawal Nominal Capacity of the updated storage. + - `name`: the name of the updated storage. + - `group`: the group of the updated storage. + - `injectionNominalCapacity`: the injection Nominal Capacity of the updated storage. + - `withdrawalNominalCapacity`: the withdrawal Nominal Capacity of the updated storage. - `reservoirCapacity`: The reservoir capacity of the updated storage. - - `efficiency`: The efficiency of the updated storage - - `initialLevel`: The initial Level of the updated storage - - `initialLevelOptim`: The initial Level Optim of the updated storage - - `id`: The storage ID of the study that we want to update. + - `efficiency`: the efficiency of the updated storage + - `initialLevel`: the initial Level of the updated storage + - `initialLevelOptim`: the initial Level Optim of the updated storage + - `id`: the storage ID of the study that we want to update. Permissions: - User must have READ/WRITE permission on the study. @@ -1667,7 +1914,6 @@ def update_st_storage( tags=[APITag.study_data], summary="Remove short-term storages from an area", status_code=HTTPStatus.NO_CONTENT, - response_model=None, ) def delete_st_storages( uuid: str, @@ -1680,8 +1926,8 @@ def delete_st_storages( Args: - `uuid`: The UUID of the study. - - `area_id`: The area ID. - - `storage_ids`: List of IDs of the storages to remove from the area. + - `area_id`: the area ID. + - `storage_ids`: ist of IDs of the storages to remove from the area. Permissions: - User must have DELETED permission on the study. @@ -1691,7 +1937,7 @@ def delete_st_storages( extra={"user": current_user.id}, ) params = RequestParameters(user=current_user) - study = study_service.check_study_access(uuid, StudyPermissionType.DELETE, params) + study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) study_service.st_storage_manager.delete_storages(study, area_id, storage_ids) return bp diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 4e75de7010..f48e0941ca 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,172 @@ Antares Web Changelog ===================== +v2.16.0 (2023-11-30) +-------------------- + +### Features + +* **api:** add renewable clusters to API endpoints [`#1798`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1798) +* **api:** add endpoint get_nb_cores [`#1727`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1727) +* **api-raw:** add the `create_missing` flag to allow creating missing files when uploading a file [`#1817`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1817) +* **api-storage:** update initial level field default value [`#1836`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1836) +* **api-thermal:** add clusters management endpoints [`2fbfcfc`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/2fbfcfc83b7b4df3ad59f602ab36336b8d3848fa) +* **api-thermal:** delay the import of `transform_name_to_id` [`1d80ecf`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/1d80ecfa844bf6f20fe91977764a529858b2c5ce) +* **api-thermal:** implement the `__repr__` method in enum classes for easier debugging [`7e595a9`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/7e595a90555750b72401c99a21eaac2e88419015) +* **api-thermal:** improve the `ThermalClusterGroup` enum to allow "Other 1" and "Other" values for the `OTHER1` option [`c2d6bc1`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/c2d6bc1be72ad722aea1dccc61779a5d25c52786) +* **binding-constraint:** add the binding constraint series in the matrix constants generator [`c1b4667`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/c1b4667dcb2ebcce0a4225030888efa392d6b109) +* **common:** add dynamic size for GroupedDataTable [`116c0a5`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/116c0a5743d434aa40975c8c023ab8c545ad9d49) +* **job-result-dto:** the `JobResultDTO` returns the owner (ID and name) instead of owner ID [`a53d1b2`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/a53d1b21a024b9fc3ba0cfbde8f9396917165b18) +* **launcher:** add information about which user launched a study [`#1761`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1761) +* **launcher:** unzipping task raised by the user who launched the study [`f6fe27f`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/f6fe27fdabf052a70c55c357b235e7692ac83064) +* **launcher:** allow users with `Permission.READ` or above to unzip a study [`4568b91`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/4568b9154cc174de1a430805b6378234bc2c3584) +* **model:** handling binding constraints frequency in study configuration parsing [`#1702`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1702) +* **model:** add a one-to-many relationship between `Identity` and `JobResult` [`e9a10b1`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/e9a10b1e1c45e248ba4968d243cf0ea561084924) +* **model:** handling binding constraints frequency in study configuration parsing (#1702) [`02b6ba7`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/02b6ba7a8c069ce4e3f7a765aa7aa074a4277ba5) +* **permission:** update permission types, replaced DELETE with WRITE for improved study control [`#1775`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1775) +* **requirements:** add py7zr to project requirements [`e10622e`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/e10622ef2d8b75b3107c2494128c92223dda5f6d) +* **simulation-ui:** use API to get launcher number of cores [`#1776`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1776) +* **st-storage:** allow all parameters in endpoint for short term storage creation [`#1736`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1736) +* **tests:** add integration tests for outputs import [`a8db0b4`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/a8db0b42db3a7e334125ec591b193acaae04c37b) +* **thermal:** refactor thermal view [`#1733`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1733) +* **ui:** reduce studies header height [`#1819`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1819) +* **ui:** update LauncherDialog [`#1789`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1789) +* **ui-areas:** add version check for storage tab [`#1825`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1825) +* **ui-common:** add missing areaId param to routes [`4c67f3d`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/4c67f3d9b84e38ebe5e560892319f779806d5af9) +* **ui-common:** add missing areaId param to routes [`3f09111`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/3f09111541c8a55bb5e36fccb6cc547a14601ad5) +* **ui-launcher:** block launch button if cores info cannot be get [`4267028`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/4267028ae0ba719b349c30007273108ec4a1407e) +* **ui-model:** add validation rules for thermal renewable and storage forms [`86fe2ed`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/86fe2ed30f3c0494306c38713a6f96e856eb003a) +* **ui-model:** check integer fields [`da314aa`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/da314aa31707f33359a9a495588d376d3f6824ce) +* **ui-npm:** update packages [`7e08826`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/7e08826bf1fa62c07445905fd7ba978fa7d4cba6) +* **ui-renewables:** add new list view [`#1803`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1803) +* **ui-storages:** add storages list view and form [`#1791`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1791) +* **ui-studies:** rework study launch dialog [`#1784`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1784) +* **ui-studies:** update launcher dialog xpansion field [`011fbc6`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/011fbc689f45c06c381fbb4efb6f7bd0dedb1da6) +* **ui-thermal:** update thermal clusters list view and form labels [`#1807`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1807) +* **ui-thermal:** minor user experience improvements [`5258d14`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/5258d14484feca50e452a69dd265860141804888) +* **ui-thermal:** update thermal cluster view with new component [`eb93f72`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/eb93f72301c3823b2afffff3543d8008abc8962f) +* **ui-thermal:** minor improvements [`4434d7c`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/4434d7c0cc73cf9e86350fc3da4ea3df6d059054) +* **ui-thermal:** add type for cluster capacity [`10aaea3`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/10aaea3009500a8a69e6d6f3ba3f496c4fdd47d3) +* **ui-thermal:** add error display on thermal clusters list [`92e3132`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/92e3132c30e216d4b075d6af028c1e38031dde9f) +* **upgrade:** denormalize study only when needed [`931fdae`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/931fdae3b2b05abad035c62b61edcb831171a5e5) +* **utils:** add 7z support for matrices import [`d53a99c`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/d53a99c27056fd9785b2659ab8805232b8d546fe) +* **utils:** support 7z import for studies, outputs and matrices [`a84d2aa`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/a84d2aa29f0346c68226b58a3b97e1df7a1b1db9) +* **utils:** add integration test for 7z study import [`1f2da96`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/1f2da96cd3ef16f7b7d5546a68d4e807a63866a4) +* **utils:** add support for .7z [`0779d88`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/0779d8876137db432194d5eb3280bbf9fd495538) + + +### Performance + +* **api-study:** optimize study overview display in Antares Web [`52cf0ca`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/52cf0ca0b54886b87d336facc52409224894b17b) +* **role-repository:** optimize the query used in `get_all_by_user` to fetch roles and groups at once [`dcde8a3`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/dcde8a30d4c4f96a33d33a4b0156519eb4fa4647) +* **study-factory:** `FileStudy` creation is done with a file lock to avoid that two studies are analyzed at the same time [`0bd9d5f`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/0bd9d5f7ae47f4ecc871fb0a34bde433f3659856) +* **variant:** correct slow variant study snapshot generation [`#1828`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1828) + + +### Bug Fixes + +* **matrix:** matrix ID validator now raise an exception if the ID is empty [`d483198`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/d483198ea5891081e524d10242a2330e4c9c0116) +* **raw-study:** correct matrix list generation for thermal clusters to handle non-lowercase IDs [`db86384`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/db863844fc6d1af6d61ff8401fbeccef9645b799) +* **role-type:** correct implementation of the `__ge__` operator [`379d4ae`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/379d4aee4685096cbf86879050c2fd1d87fdc5d1) +* **service:** allow unzipping with permission read [`0fa59a6`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/0fa59a6da29e6524b819b5949780e48d0dd9df48) +* **st-storage:** the `initial_level` value must be between 0 and 1 [`#1815`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1815) +* **tests:** fix unit test and refactor integration one [`6bbbffc`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/6bbbffc5213e996098239174c7611be979d0cb45) +* **ui:** update dynamic areaId segment URL path on area change [`#1811`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1811) +* **ui:** i18next API change [`e8a8503`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/e8a8503cb24ddd543196cd7bddc6ec03b140bc1c) +* **ui:** TS issues [`ff8e635`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/ff8e6357d25d02d07a28bafd8e8223f41a33ce84) +* **ui:** immer import change [`d45d274`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/d45d274a316f990d60c3d6a6b54b2a8827618be3) +* **ui:** correct merge conflict imports [`77fd1ad`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/77fd1ad68a1337bef05cb8e094321f693162f193) +* **ui-common:** add case check for name in GroupedDataTable [`586580b`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/586580b320a3cfb0a6e48ae3e0b2d5d1db6c68aa) +* **ui-lancher:** cores limit cannot be break [`d3f4b79`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/d3f4b792780a7662330fb3c6869dc2812e1c249e) +* **ui-launcher:** add API values for min max number of cores [`ea188b1`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/ea188b1fc8934769f314a43a8a27a5b3f9601f0f) +* **ui-study:** prevent study tasks list to contain all studies tasks [`#1820`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1820) +* **ui-tasks:** filter deleted studies jobs [`#1816`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1816) +* **ui-thermal:** update regex to prevent commas in id [`b7ab9a5`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/b7ab9a5a1bfb004a369a7df33e35eac5729c4056) +* **ui-thermal:** update regex to avoid ReDoS vulnerabilities [`c837474`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/c837474f882559cd0a283e300db364603806c002) +* **xpansion:** add missing `separation_parameter` field in the settings [`#1831`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1831) + + +### Documentation + +* **api-thermal:** add the documentation of the thermal cluster endpoints [`c84c6cd`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/c84c6cd5db7412c55d0947eaec374c808dc78bb8) +* **api-thermal:** improve the documentation of the thermal manager [`099e4e0`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/099e4e0535b7491edd1d8d0fef268137dec9e704) +* **api-thermal:** correct thermal manager doctrings [`3d3f8c3`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/3d3f8c343f7192d904a9f225c4cf9e836c4ee8e2) +* **how-to:** add a "How to import a compressed study?" topic in the documentation [`28c654f`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/28c654f7493db54beccfbf640b95b0aeda874e31) +* **import-api:** improve the documentation of the import API [`f1f2112`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/f1f2112e92d946002bab16c26e7cbe6a0d6945c5) +* **rtd:** correct the configuration for ReadTheDocs [`#1833`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1833) +* **study-service:** improve documentation of the `StudyService` class [`3ece436`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/3ece4368eb14f77027a575ed8b2b2e9ffc2bbcb5) +* **upgrade:** add little documentation [`7075ed8`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/7075ed83b0aa298c91bfc736b23a3e83901a2f24) + + +### Tests + +* correct `test_commands_service`: ensure the admin user is in database [`#1782`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1782) +* add the `DbStatementRecorder` class to record SQL statements in memory to diagnose the queries performed by the application [`c02a839`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/c02a839d9f16eb757f1bac7fdce3e4572bf102d2) +* add unit tests for `antarest.core.utils.utils.extract_zip` [`4878878`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/48788783dc24519926b1b6517ed45be3fc1deee9) +* add the `compare_elements` function to compare two XML and find differences [`15753e3`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/15753e3cb6fc9d66993c00227e02d9f3d10cc89e) +* **api:** added end-to-end test to ensure seamless functionality of API endpoints with the "bot" token [`#1770`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1770) +* **api-user:** correct API endpoint `user_save` [`483d639`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/483d63937b1325a3b98ca4fcad4b6f75c18cc15e) +* **apidoc:** add unit test for apidoc page [`#1797`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1797) +* **integration:** simplify the `app` fixture used in integration tests and add comments [`e48ce6d`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/e48ce6d9c8dfd30d117f5af853d0179e49418c85) +* **login-service:** refactor unit tests to use fixture instead of mocks [`02f20f0`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/02f20f001c3612969b680c904bf6d22d5a9786da) +* **matrix-dataset:** correct unit test `test_watcher` [`d829033`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/d82903375b8d83b8dde30e6d8637825602708b7c) +* **matrix-dataset:** correct unit test `test_dataset_lifecycle` [`da6f84b`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/da6f84be183902428db61ad227fc3c98df29d8bb) +* **model:** correct and improve the `TestLdapService` unit tests [`84c0c05`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/84c0c05c96e9a73ca45e004d1f5cc5f4fbb58218) +* **model:** don't decorate model classes with `@dataclass` [`82d565b`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/82d565b4423281bf41782201cceef34b3d5b29dc) +* **study-comments:** add integration tests for study synthesis endpoint [`f56a4ae`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/f56a4ae1b565843d1a58db60d10c945ea0c6e657) +* **study-comments:** add integration tests for study comments endpoints [`3eb1491`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/3eb1491aca9b84a6beb2007a8e0977679a560e4c) +* **study-comments:** remove deprecated integration tests [`7651438`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/7651438021ba580f5710f301b27ae5c0fc7a7674) +* **thermal-manager:** add unit tests for the `TestThermalManager` class [`d7f83b8`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/d7f83b8249af25172d25e8e7287be063d8aa9077) +* **watcher:** correct unit test `test_partial_scan` [`cf07093`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/cf070939309b6e4f772542046c7bfb2727f450c6) + + +### Refactoring + +* **cluster:** improve the implementation of the thermal, renewable and the short terme storage [`48c3352`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/48c3352a7e1429cdbc74518f4163d93e97262a9e) +* **commands:** refactor the variant commands to use thermal / renewable configurations [`996fb20`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/996fb20d5359ab7f6184ac02945de200f20a294b) +* **commands:** simplify implementation of study variant commands and improve constraints [`4ce9bd0`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/4ce9bd039ec31441f0f17860ca0896ddc951f513) +* **commands:** refactor the variant commands to use thermal / renewable configurations [`ff3ccdf`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/ff3ccdf4f7721aff8bf1d89e3d265bb4b001f884) +* **config:** create base classes use to configure thermal and renewable clusters and short term storage [`e7e7198`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/e7e719869e27cdea7e1b5a87875692af7d4d1977) +* **config:** use `create_st_storage_config` during config parsing [`0d1bac8`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/0d1bac80dcead8dc5fe1d0f8276e7905456ae1cb) +* **launcher-service:** rename long variable name `user_who_launched_the_study` [`4ec7cb0`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/4ec7cb013be776489f43ccf0c7940d36018cbd7e) +* **login-model:** drop superfluous `__eq__` operator [`73bc168`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/73bc168327594e533ced27c404f67c494bfa0908) +* **raw-study:** the `create_from_fs` method is changed to return a `NamedTuple` (perf) [`191fb98`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/191fb98c0723299d6c6c531cf2a61140c3fd4c1a) +* **role-type:** replace `is_higher_or_equals` by `__ge__` operator [`fc10814`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/fc108147ddc5505c8fcf5833891ade4ec765b940) +* **st-storage:** implement `STStorageProperties` using `BaseClusterProperties` class [`0cc639e`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/0cc639e1cf3c1aee57d70ff137acc0c479335e82) +* **table-mode:** simplify implementation of the `TableModeManager` class [`52fcb80`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/52fcb80e0e1a8ee7afccc8a79285925b7fbb6fd0) +* **thermal-api:** replace the `DELETE` permission with `WRITE` in the `delete_thermal_clusters` endpoint [`37f99ac`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/37f99ac62a711fd72877799e2aafd0919d960e58) +* **thermal-manager:** correct the `update_cluster` method to update only required and changed properties [`9715add`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/9715addf45c412e8b3017852d49e890080c2591b) +* **utils:** replace HTTP exception with `BadArchiveContent` in `extract_zip` function, and update documentation accordingly [`2143dd7`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/2143dd7cc2fa20b5f6ecdb7395a4f9c2d08e696d) + + +### Styles + +* **api-thermal:** sort imports and reformat source code. [`451c27c`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/451c27cc39a2323dd665297eaf05034b5fd5da4b) +* **model:** simplify typing imports [`2d77d65`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/2d77d65f9ac89c788366045fd544202f8dd4c5d3) +* **typing:** replace `IO[bytes]` by `typing.BinaryIO` in function signatures [`4e2e5f9`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/4e2e5f92c66f0792d0ea4bea3a1d5e14eb686a03) +* **ui:** update file with new Prettier version [`dfd79a7`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/dfd79a7dd73a5e769a20eaff7b9234e829ab8cf4) +* **ui:** issue with Prettier [`b204b6a`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/b204b6acafe5bd03036b078bbc72233366d46b1d) + + +### Chore + +* fix typo in the documentation of shell scripts [`#1793`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1793) +* correct the typing of the `camel_case_model` decorator [`4784a74`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/4784a744c84067beba9e1c11d0c33938821524ef) +* correct bad imports in unit tests [`786de74`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/786de74c3606455465fb6204aaf3f310f092de5c) +* fix typing in `CommandExtractor` class [`0da8f3e`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/0da8f3e12e5e2c7f1eb2e3bdd63257d3cf3d74b0) +* handle all ancestor classes in `AllOptionalMetaclass` [`7153292`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/7153292a74aad0f70a4f1a09d85f95fb440acabf) +* correct indentation in docstring [`6d19ebb`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/6d19ebb101dc0cbe0d3b2a602e668d562328288c) +* **api:** modify `ThermalConfig` in API example to avoid `ValidationError` [`#1795`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1795) +* **api-thermal:** improve typing of functions [`36ea466`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/36ea466ba75b9c06477cbc4d9a31ddb51ecb168f) +* **doc:** correct image names and titles in the documentation [`b976713`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/b9767132389f2ad09b3a6cd57a0870efb9f0eb38) + + +### Reverts + +* change `requirements.txt` to restore `PyYAML~=5.4.1` [`cf47556`](https://github.com/AntaresSimulatorTeam/AntaREST/commit/cf47556e8161bf7f184cac5bc08904954f04b1f6) + + + v2.15.6 (2023-11-24) -------------------- diff --git a/docs/assets/media/how-to/studies-import-drop-file-dialog.png b/docs/assets/media/how-to/studies-import-drop-file-dialog.png new file mode 100644 index 0000000000..9710e46e00 Binary files /dev/null and b/docs/assets/media/how-to/studies-import-drop-file-dialog.png differ diff --git a/docs/assets/media/how-to/studies-import-main-view.png b/docs/assets/media/how-to/studies-import-main-view.png new file mode 100644 index 0000000000..82ba4a8965 Binary files /dev/null and b/docs/assets/media/how-to/studies-import-main-view.png differ diff --git a/docs/assets/media/how-to/studies-import-studies-list.png b/docs/assets/media/how-to/studies-import-studies-list.png new file mode 100644 index 0000000000..a1f42b9ea4 Binary files /dev/null and b/docs/assets/media/how-to/studies-import-studies-list.png differ diff --git a/docs/assets/media/how-to/sudies-upgrade-menu_version.png b/docs/assets/media/how-to/studies-upgrade-menu_version.png similarity index 100% rename from docs/assets/media/how-to/sudies-upgrade-menu_version.png rename to docs/assets/media/how-to/studies-upgrade-menu_version.png diff --git a/docs/how-to/studies-import.md b/docs/how-to/studies-import.md new file mode 100644 index 0000000000..4434131437 --- /dev/null +++ b/docs/how-to/studies-import.md @@ -0,0 +1,123 @@ +--- +title: How to Import a Compressed Study? +author: Laurent LAPORTE +date: 2023-10-25 +tags: + + - import + - zip + - 7z + +--- + +# How to Import a Compressed Study? + +## Introduction + +Antares Web can import a study from a compressed file (ZIP or 7z). The compressed file must contain the following files: + +- `study.antares`: metadata of the study (see below) +- `layers/`: configuration of the layers +- `settings/`: study settings (`generaldata.ini`, etc.) +- `input/`: study inputs (areas, thermal clusters, binding constraints, etc.) + +The compressed file can contain additional files: + +- `Desktop.ini`: the Windows desktop settings. +- `users/`: the users' data, including xpansion settings. +- `output/`: the output of the study +- etc. + +The `study.antares` file is a text file in the INI format, containing the metadata of the study. Example: + +```ini +[antares] +version = 860 +caption = 000 Free Data Sample +created = 1525354818 +lastsave = 1696057404 +author = John DOE +``` + +After import, the study is available as a managed study in the `default` workspace. +The metadata (version, caption, and author) are preserved, and the creation date is updated to the current date. + +## Importing a Study in Antares Web + +To import a study into Antares Web, follow these steps from the "Studies" view: + +Click the "Import" button in the "Studies" view: + +![studies-import-main-view.png](../assets/media/how-to/studies-import-main-view.png) + +The import dialog box will appear. Click the "Browse" button to select the compressed file to import: + +![studies-import-drop-file-dialog.png](../assets/media/how-to/studies-import-drop-file-dialog.png) + +You can also drag and drop the compressed file into the dialog box. + +Once imported, you can see the study in the list of studies. Select the "default" workspace to view the imported study. You can also search for the study by name using the search input. + +![studies-import-studies-list.png](../assets/media/how-to/studies-import-studies-list.png) + +## Importing a Study Using the API Endpoint + +The following Python script demonstrates how to import a study using the API endpoint `POST /v1/studies/_import`: + +You need to provide the following parameters: + +- `study`: binary data of the compressed file to import +- `groups`: list of groups to which the study will be assigned (optional) + +Make sure you have the correct API URL and a valid authentication token. + +```python +import httpx # or requests + +URL = "https://antares-web/api" +TOKEN = "" + +with open("perso/new_study.zip", mode="rb") as fd: + with httpx.Client(verify=False) as client: + res = client.post( + f"{URL}/v1/studies/_import", + headers={"Authorization": f"Bearer {TOKEN}"}, + files={"study": fd}, + params={"groups": "foo,bar"}, + ) + +res.raise_for_status() +study_uuid = res.json() +``` + +The script above imports the compressed file `perso/new_study.zip` and assigns the study to the groups `foo` and `bar`. + +Here's a breakdown of what each part of the code does: + +1. `import httpx`: This line imports the `httpx` library, which is used for making HTTP requests in Python. + Alternatively, the `requests` library can be used instead of `httpx` for the same purpose. + +2. `URL = "https://antares-web/api"`: This line sets the URL to which the POST request will be made. + You need to provide the right URL according to your own Antares Web server. + +3. `TOKEN = ""`: This line sets the authentication token that will be used in the request. + You should replace `` with your actual authentication token. + +4. The `with open("perso/new_study.zip", mode="rb") as fd:` block opens the specified compressed file in binary mode. + +5. The `with httpx.Client(verify=False) as client:` block creates an HTTP client. + The `verify=False` argument is used to disable SSL certificate verification. + +6. `res = client.post(...)` makes a POST request to the specified URL with the provided parameters. + It sends the file contents, sets the headers with the authentication token, and adds query parameters. + +7. `res.raise_for_status()` checks if the response from the server indicates an error. + If an error is detected, it raises an exception. + You may have the HTTP error 415 if the file is not a valid ZIP of 7z file. + +8. `study_uuid = res.json()` parses the response from the server, assuming it is in JSON format, + and assigns it to the variable `study_uuid`. + +See also: + +- ["User account & api tokens"](../user-guide/1-interface.md#user-account-and-api-tokens) in the user guide. diff --git a/docs/how-to/studies-upgrade.md b/docs/how-to/studies-upgrade.md index 6c538fc59b..fe48723539 100644 --- a/docs/how-to/studies-upgrade.md +++ b/docs/how-to/studies-upgrade.md @@ -1,5 +1,5 @@ --- -title: How to upgrade a study? +title: How to Upgrade a Study? author: Laurent LAPORTE date: 2023-03-10 tags: @@ -9,7 +9,9 @@ tags: --- -# Introduction +# How to Upgrade a Study? + +## Introduction Upgrading versioned studies is an important step to ensure compatibility of your studies with the latest versions of Antares Web and Antares Simulator. This upgrade is necessary because some earlier versions may be deprecated and no @@ -27,13 +29,13 @@ We strongly recommend upgrading your studies to the latest version to take advan improvements in Antares Web and Antares Simulator. If you encounter any difficulties during the upgrade, please do not hesitate to contact our support team for assistance. -# Upgrading +## Upgrading To upgrade your study to the latest version of Antares Web and Antares Simulator, you can follow these steps: On the main page of the study, you can find the version number at the top of the menu bar: -![](../assets/media/how-to/sudies-upgrade-menu_version.png) +![studies-upgrade-menu_version.png](../assets/media/how-to/studies-upgrade-menu_version.png) To upgrade the study, click on the nemu and select "Upgrade Study". @@ -54,7 +56,7 @@ When the upgrade is done, you can see the version number updated: Once the upgrade is complete, you can open your study and perform the manual upgrade in the configuration. -# See also +## See also - Create a new study in the latest version - Run a study in the latest version diff --git a/docs/install/0-INSTALL.md b/docs/install/0-INSTALL.md index ccb86e9b63..851ea0b799 100644 --- a/docs/install/0-INSTALL.md +++ b/docs/install/0-INSTALL.md @@ -39,7 +39,7 @@ Then perform the following steps: ```shell cd webapp - npm install + npm install --legacy-peer-deps npm run build cd .. ``` diff --git a/docs/user-guide/1-interface.md b/docs/user-guide/1-interface.md index 3cc95db3b1..3d42c95f78 100644 --- a/docs/user-guide/1-interface.md +++ b/docs/user-guide/1-interface.md @@ -124,7 +124,7 @@ The data which can be uploaded are either a single tsv file, or a zipped list of ![](../assets/media/img/userguide_dataset_creation.png) -## User account & api tokens +## User account and api tokens For normal user, the account section allows the creation of "api token". These token can be used in scripts that will use the [API](#api-documentation). diff --git a/mkdocs.yml b/mkdocs.yml index fa50daadc6..48558e8f0e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -34,6 +34,7 @@ nav: - 'User interface': 'user-guide/1-interface.md' - 'Variant manager': 'user-guide/2-variant_manager.md' - 'How to': + - 'Import a study': 'how-to/studies-import.md' - 'Upgrade a study': 'how-to/studies-upgrade.md' - 'Build': - 'Introduction': 'install/0-INSTALL.md' diff --git a/requirements.txt b/requirements.txt index bd0455b56f..70ded33e8c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,6 +16,7 @@ numpy~=1.22.1 pandas~=1.4.0 plyer~=2.0.0 psycopg2-binary==2.9.4 +py7zr~=0.20.6 pydantic~=1.9.0 PyQt5~=5.15.6 python-json-logger~=2.0.7 diff --git a/setup.py b/setup.py index fda4c522cd..37074c1e33 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="AntaREST", - version="2.15.6", + version="2.16.0", description="Antares Server", long_description=Path("README.md").read_text(encoding="utf-8"), long_description_content_type="text/markdown", diff --git a/sonar-project.properties b/sonar-project.properties index 291006420b..e19a4a82dc 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -6,5 +6,5 @@ sonar.exclusions=antarest/gui.py,antarest/main.py sonar.python.coverage.reportPaths=coverage.xml sonar.python.version=3.8 sonar.javascript.lcov.reportPaths=webapp/coverage/lcov.info -sonar.projectVersion=2.15.6 +sonar.projectVersion=2.16.0 sonar.coverage.exclusions=antarest/gui.py,antarest/main.py,antarest/singleton_services.py,antarest/worker/archive_worker_service.py,webapp/**/* \ No newline at end of file diff --git a/tests/core/test_utils.py b/tests/core/test_utils.py index 366b97788b..78808aafab 100644 --- a/tests/core/test_utils.py +++ b/tests/core/test_utils.py @@ -1,5 +1,5 @@ +import zipfile from pathlib import Path -from zipfile import ZIP_DEFLATED, ZipFile import pytest @@ -7,7 +7,7 @@ from antarest.core.utils.utils import concat_files, concat_files_to_str, read_in_zip, retry, suppress_exception -def test_retry(): +def test_retry() -> None: def func_failure() -> str: raise ShouldNotHappenException() @@ -15,7 +15,7 @@ def func_failure() -> str: retry(func_failure, 2) -def test_concat_files(tmp_path: Path): +def test_concat_files(tmp_path: Path) -> None: f1 = tmp_path / "f1.txt" f2 = tmp_path / "f2.txt" f3 = tmp_path / "f3.txt" @@ -27,7 +27,7 @@ def test_concat_files(tmp_path: Path): assert f_target.read_text(encoding="utf-8") == "hello world !\nDone." -def test_concat_files_to_str(tmp_path: Path): +def test_concat_files_to_str(tmp_path: Path) -> None: f1 = tmp_path / "f1.txt" f2 = tmp_path / "f2.txt" f3 = tmp_path / "f3.txt" @@ -37,9 +37,9 @@ def test_concat_files_to_str(tmp_path: Path): assert concat_files_to_str([f1, f2, f3]) == "hello world !\nDone." -def test_read_in_zip(tmp_path: Path): +def test_read_in_zip(tmp_path: Path) -> None: zip_file = tmp_path / "test.zip" - with ZipFile(zip_file, "w", ZIP_DEFLATED) as output_data: + with zipfile.ZipFile(zip_file, "w", zipfile.ZIP_DEFLATED) as output_data: output_data.writestr("matrix.txt", "0\n1") output_data.writestr("sub/matrix2.txt", "0\n2") @@ -56,10 +56,10 @@ def test_read_in_zip(tmp_path: Path): assert expected_results[2] is None -def test_suppress_exception(): +def test_suppress_exception() -> None: def func_failure() -> str: raise ShouldNotHappenException() - catched_exc = [] - suppress_exception(func_failure, lambda ex: catched_exc.append(ex)) - assert len(catched_exc) == 1 + caught_exc = [] + suppress_exception(func_failure, lambda ex: caught_exc.append(ex)) + assert len(caught_exc) == 1 diff --git a/tests/core/utils/__init__.py b/tests/core/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/core/utils/test_extract_zip.py b/tests/core/utils/test_extract_zip.py new file mode 100644 index 0000000000..11c3c11ff3 --- /dev/null +++ b/tests/core/utils/test_extract_zip.py @@ -0,0 +1,64 @@ +import io +import zipfile +from pathlib import Path + +import py7zr +import pytest + +from antarest.core.utils.utils import BadArchiveContent, extract_zip + + +class TestExtractZip: + """ + Test the `extract_zip` function. + """ + + def test_extract_zip__with_zip(self, tmp_path: Path): + # First, create a small ZIP file + zip_path = tmp_path / "test.zip" + with zipfile.ZipFile(zip_path, mode="w", compression=zipfile.ZIP_DEFLATED) as zipf: + zipf.writestr(zinfo_or_arcname="test.txt", data="Hello world!") + + # Then, call the function + with open(zip_path, mode="rb") as stream: + extract_zip(stream, tmp_path) + + # Finally, check the result + assert (tmp_path / "test.txt").read_text() == "Hello world!" + + def test_extract_zip__with_7z(self, tmp_path: Path): + # First, create a small ZIP file + zip_path = tmp_path / "test.7z" + with py7zr.SevenZipFile(zip_path, mode="w") as zipf: + zipf.writestr(data="Hello world!", arcname="test.txt") + + # Then, call the function + with open(zip_path, mode="rb") as stream: + extract_zip(stream, tmp_path) + + # Finally, check the result + assert (tmp_path / "test.txt").read_text() == "Hello world!" + + def test_extract_zip__empty_file(self): + stream = io.BytesIO(b"") + + with pytest.raises(BadArchiveContent): + extract_zip(stream, Path("dummy/path")) + + def test_extract_zip__corrupted_zip(self): + stream = io.BytesIO(b"PK\x03\x04 BLURP") + + with pytest.raises(BadArchiveContent): + extract_zip(stream, Path("dummy/path")) + + def test_extract_zip__corrupted_7z(self): + stream = io.BytesIO(b"7z BLURP") + + with pytest.raises(BadArchiveContent): + extract_zip(stream, Path("dummy/path")) + + def test_extract_zip__unknown_format(self): + stream = io.BytesIO(b"ZORRO") + + with pytest.raises(BadArchiveContent): + extract_zip(stream, Path("dummy/path")) diff --git a/tests/db_statement_recorder.py b/tests/db_statement_recorder.py new file mode 100644 index 0000000000..c6a1264e03 --- /dev/null +++ b/tests/db_statement_recorder.py @@ -0,0 +1,66 @@ +""" +Record SQL statements in memory to diagnose the queries performed by the application. +""" +import contextlib +import types +import typing as t + +from sqlalchemy import event # type: ignore +from sqlalchemy.engine import Connection, Engine # type: ignore + + +class DBStatementRecorder(contextlib.AbstractContextManager): # type: ignore + """ + Record SQL statements in memory to diagnose the queries performed by the application. + + Usage:: + + from tests.db_statement_logger import DBStatementRecorder + + db_session = ... + with DBStatementRecorder(db_session.bind) as db_recorder: + # Perform some SQL queries + ... + + # Get the SQL statements + sql_statements = db_recorder.sql_statements + + See [SqlAlchemy Events](https://docs.sqlalchemy.org/en/14/orm/session_events.html) for more details. + """ + + def __init__(self, db_engine: Engine) -> None: + self.db_engine: Engine = db_engine + self.sql_statements: t.List[str] = [] + + def __enter__(self) -> "DBStatementRecorder": + event.listen(self.db_engine, "before_cursor_execute", self.before_cursor_execute) + return self + + def __exit__( + self, + exc_type: t.Optional[t.Type[BaseException]], + exc_val: t.Optional[BaseException], + exc_tb: t.Optional[types.TracebackType], + ) -> t.Optional[bool]: + event.remove(self.db_engine, "before_cursor_execute", self.before_cursor_execute) + return None if exc_type is None else False # propagate exceptions if any. + + def before_cursor_execute( + self, + conn: Connection, + cursor: t.Any, + statement: str, + parameters: t.Any, + context: t.Any, + executemany: bool, + ) -> None: + # note: add a breakpoint here to debug the SQL statements. + self.sql_statements.append(statement) + + def __str__(self) -> str: + """ + Return a string representation the SQL statements. + """ + if self.sql_statements: + return "Recorded SQL statements:\n" + "\n-------\n".join(self.sql_statements) + return "No SQL statements recorded." diff --git a/tests/integration/assets/STA-mini.7z b/tests/integration/assets/STA-mini.7z new file mode 100644 index 0000000000..6f462f7700 Binary files /dev/null and b/tests/integration/assets/STA-mini.7z differ diff --git a/tests/integration/assets/base_study.zip b/tests/integration/assets/base_study.zip index 712833942c..8a79496138 100644 Binary files a/tests/integration/assets/base_study.zip and b/tests/integration/assets/base_study.zip differ diff --git a/tests/integration/assets/launcher_mock.bat b/tests/integration/assets/launcher_mock.bat new file mode 100755 index 0000000000..bbf3480229 --- /dev/null +++ b/tests/integration/assets/launcher_mock.bat @@ -0,0 +1,6 @@ +@echo off + +echo %* +set "exit_status=%errorlevel%" +echo exit %exit_status% +exit /B %exit_status% diff --git a/tests/integration/assets/launcher_mock.sh b/tests/integration/assets/launcher_mock.sh index 8f8830d31e..e028f272f5 100755 --- a/tests/integration/assets/launcher_mock.sh +++ b/tests/integration/assets/launcher_mock.sh @@ -1,7 +1,5 @@ #!/bin/bash -CUR_DIR=$(cd `dirname $0` && pwd) - echo "$@" exit_status=$? echo "exit ${exit_status}" diff --git a/tests/integration/assets/matrices.7z b/tests/integration/assets/matrices.7z new file mode 100644 index 0000000000..2d4a69a4d6 Binary files /dev/null and b/tests/integration/assets/matrices.7z differ diff --git a/tests/integration/assets/matrices.zip b/tests/integration/assets/matrices.zip new file mode 100644 index 0000000000..98ac501644 Binary files /dev/null and b/tests/integration/assets/matrices.zip differ diff --git a/tests/integration/assets/output_adq.7z b/tests/integration/assets/output_adq.7z new file mode 100644 index 0000000000..3faa4da9d9 Binary files /dev/null and b/tests/integration/assets/output_adq.7z differ diff --git a/tests/integration/assets/output_adq.zip b/tests/integration/assets/output_adq.zip new file mode 100644 index 0000000000..68f8dc512b Binary files /dev/null and b/tests/integration/assets/output_adq.zip differ diff --git a/tests/integration/assets/variant_study.zip b/tests/integration/assets/variant_study.zip index 4e08926cda..e19151bb07 100644 Binary files a/tests/integration/assets/variant_study.zip and b/tests/integration/assets/variant_study.zip differ diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 5083910145..a88dc4f9e8 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,6 +1,7 @@ +import os +import typing as t +import zipfile from pathlib import Path -from typing import cast -from zipfile import ZipFile import jinja2 import pytest @@ -8,7 +9,6 @@ from sqlalchemy import create_engine from starlette.testclient import TestClient -from antarest.core.utils.fastapi_sqlalchemy import DBSessionMiddleware from antarest.dbmodel import Base from antarest.main import fastapi_app from antarest.study.storage.rawstudy.watcher import Watcher @@ -18,21 +18,25 @@ PROJECT_DIR = next(iter(p for p in HERE.parents if p.joinpath("antarest").exists())) RESOURCES_DIR = PROJECT_DIR.joinpath("resources") +RUN_ON_WINDOWS = os.name == "nt" + @pytest.fixture(name="app") -def app_fixture(tmp_path: Path): - # First, create a database and apply migrations +def app_fixture(tmp_path: Path) -> FastAPI: + # Currently, it is impossible to use a SQLite database in memory (with "sqlite:///:memory:") + # because the database is created by the FastAPI application during each integration test, + # which doesn't apply the migrations (migrations are done by Alembic). + # An alternative is to use a SQLite database stored on disk, because migrations can be persisted. db_path = tmp_path / "db.sqlite" db_url = f"sqlite:///{db_path}" + # ATTENTION: when setting up integration tests, be aware that creating the database + # tables requires a dedicated DB engine (the `engine` below). + # This is crucial as the FastAPI application initializes its own engine (a global object), + # and the DB engine used in integration tests is not the same. engine = create_engine(db_url, echo=False) Base.metadata.create_all(engine) - # noinspection SpellCheckingInspection - DBSessionMiddleware( - None, - custom_engine=engine, - session_args={"autocommit": False, "autoflush": False}, - ) + del engine # This object won't be used anymore. # Prepare the directories used by the repos matrix_dir = tmp_path / "matrix_store" @@ -49,7 +53,7 @@ def app_fixture(tmp_path: Path): # Extract the sample study sta_mini_zip_path = ASSETS_DIR.joinpath("STA-mini.zip") - with ZipFile(sta_mini_zip_path) as zip_output: + with zipfile.ZipFile(sta_mini_zip_path) as zip_output: zip_output.extractall(path=ext_workspace_path) # Generate a "config.yml" file for the app @@ -58,6 +62,7 @@ def app_fixture(tmp_path: Path): template = template_env.get_template("config.template.yml") config_path = tmp_path / "config.yml" + launcher_name = "launcher_mock.bat" if RUN_ON_WINDOWS else "launcher_mock.sh" with open(config_path, "w") as fh: fh.write( template.render( @@ -67,13 +72,13 @@ def app_fixture(tmp_path: Path): matrix_dir=str(matrix_dir), archive_dir=str(archive_dir), tmp_dir=str(tmp_dir), - launcher_mock=ASSETS_DIR / "launcher_mock.sh", + launcher_mock=ASSETS_DIR / launcher_name, ) ) app, services = fastapi_app(config_path, RESOURCES_DIR, mount_front=False) yield app - cast(Watcher, services["watcher"]).stop() + t.cast(Watcher, services["watcher"]).stop() @pytest.fixture(name="client") diff --git a/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py b/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py index 3d0177b1f2..8229fa543a 100644 --- a/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py +++ b/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py @@ -1,4 +1,5 @@ import http +import io import itertools import json import pathlib @@ -57,10 +58,10 @@ def test_get_study( rel_path = file_path.relative_to(study_dir).as_posix() res = client.get( f"/v1/studies/{study_id}/raw", - params={"path": f"/{rel_path}", "depth": 1}, + params={"path": rel_path, "depth": 1}, headers=headers, ) - res.raise_for_status() + assert res.status_code == 200, res.json() if file_path.suffix == ".json": # special case for JSON files actual = res.json() @@ -86,19 +87,74 @@ def test_get_study( params={"path": f"/{rel_path.as_posix()}", "depth": 1}, headers=headers, ) - res.raise_for_status() + assert res.status_code == 200, res.json() actual = res.content expected = file_path.read_bytes() assert actual == expected + # If you try to retrieve a file that doesn't exist, we should have a 404 error + res = client.get( + f"/v1/studies/{study_id}/raw", + params={"path": "user/somewhere/something.txt"}, + headers=headers, + ) + assert res.status_code == 404, res.json() + assert res.json() == { + "description": "'somewhere' not a child of User", + "exception": "ChildNotFoundError", + } + + # If you want to update an existing resource, you can use PUT method. + # But, if the resource doesn't exist, you should have a 404 Not Found error. + res = client.put( + f"/v1/studies/{study_id}/raw", + params={"path": "user/somewhere/something.txt"}, + headers=headers, + files={"file": io.BytesIO(b"Goodbye World!")}, + ) + assert res.status_code == 404, res.json() + assert res.json() == { + "description": "'somewhere' not a child of User", + "exception": "ChildNotFoundError", + } + + # To create a resource, you can use PUT method and the `create_missing` flag. + # The expected status code should be 204 No Content. + res = client.put( + f"/v1/studies/{study_id}/raw", + params={"path": "user/somewhere/something.txt", "create_missing": True}, + headers=headers, + files={"file": io.BytesIO(b"Goodbye Cruel World!")}, + ) + assert res.status_code == 204, res.json() + + # To update a resource, you can use PUT method, with or without the `create_missing` flag. + # The expected status code should be 204 No Content. + res = client.put( + f"/v1/studies/{study_id}/raw", + params={"path": "user/somewhere/something.txt", "create_missing": True}, + headers=headers, + files={"file": io.BytesIO(b"This is the end!")}, + ) + assert res.status_code == 204, res.json() + + # You can check that the resource has been created or updated. + res = client.get( + f"/v1/studies/{study_id}/raw", + params={"path": "user/somewhere/something.txt"}, + headers=headers, + ) + assert res.status_code == 200, res.json() + assert res.content == b"This is the end!" + # If we ask for properties, we should have a JSON content rel_path = "/input/links/de/properties/fr" res = client.get( f"/v1/studies/{study_id}/raw", - params={"path": f"/{rel_path}", "depth": 2}, + params={"path": rel_path, "depth": 2}, headers=headers, ) - res.raise_for_status() + assert res.status_code == 200, res.json() actual = res.json() assert actual == { "asset-type": "ac", @@ -120,10 +176,10 @@ def test_get_study( rel_path = "/input/links/de/fr" res = client.get( f"/v1/studies/{study_id}/raw", - params={"path": f"/{rel_path}", "formatted": True}, + params={"path": rel_path, "formatted": True}, headers=headers, ) - res.raise_for_status() + assert res.status_code == 200, res.json() actual = res.json() assert actual == {"index": ANY, "columns": ANY, "data": ANY} @@ -131,14 +187,32 @@ def test_get_study( rel_path = "/input/links/de/fr" res = client.get( f"/v1/studies/{study_id}/raw", - params={"path": f"/{rel_path}", "formatted": False}, + params={"path": rel_path, "formatted": False}, headers=headers, ) - res.raise_for_status() + assert res.status_code == 200, res.json() actual = res.text actual_lines = actual.splitlines() first_row = [float(x) for x in actual_lines[0].split("\t")] - assert first_row == [100000, 100000, 0.010000, 0.010000, 0, 0, 0, 0] + assert first_row == [100000, 100000, 0.01, 0.01, 0, 0, 0, 0] + + # If ask for an empty matrix, we should have an empty binary content + res = client.get( + f"/v1/studies/{study_id}/raw", + params={"path": "input/thermal/prepro/de/01_solar/data", "formatted": False}, + headers=headers, + ) + assert res.status_code == 200, res.json() + assert res.content == b"" + + # But, if we use formatted = True, we should have a JSON objet representing and empty matrix + res = client.get( + f"/v1/studies/{study_id}/raw", + params={"path": "input/thermal/prepro/de/01_solar/data", "formatted": True}, + headers=headers, + ) + assert res.status_code == 200, res.json() + assert res.json() == {"index": [0], "columns": [], "data": []} # Some files can be corrupted user_folder_dir = study_dir.joinpath("user/bad") @@ -158,12 +232,13 @@ def test_get_study( params={"path": "/input/areas/list", "depth": 1}, headers=headers, ) - res.raise_for_status() + assert res.status_code == 200, res.json() assert res.json() == ["DE", "ES", "FR", "IT"] # asserts that the GET /raw endpoint is able to read matrix containing NaN values res = client.get( - f"/v1/studies/{study_id}/raw?path=output/20201014-1427eco/economy/mc-all/areas/de/id-monthly", + f"/v1/studies/{study_id}/raw", + params={"path": "output/20201014-1427eco/economy/mc-all/areas/de/id-monthly"}, headers=headers, ) assert res.status_code == 200 diff --git a/tests/integration/studies_blueprint/__init__.py b/tests/integration/studies_blueprint/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/studies_blueprint/assets/__init__.py b/tests/integration/studies_blueprint/assets/__init__.py new file mode 100644 index 0000000000..773f16ec60 --- /dev/null +++ b/tests/integration/studies_blueprint/assets/__init__.py @@ -0,0 +1,3 @@ +from pathlib import Path + +ASSETS_DIR = Path(__file__).parent.resolve() diff --git a/tests/integration/studies_blueprint/assets/test_comments/raw_study.comments.xml b/tests/integration/studies_blueprint/assets/test_comments/raw_study.comments.xml new file mode 100644 index 0000000000..fb5fa497a0 --- /dev/null +++ b/tests/integration/studies_blueprint/assets/test_comments/raw_study.comments.xml @@ -0,0 +1,186 @@ + + + + + 02/12/2016 + + + Création du modèle TYNDP 2018 avec la version ANTARES V5.0.4 + + + Tous les paramètres sont les paramètres par défaut d'ANTARES + + + + + + NŒUDS + + + Création des 54 nœuds représentant les pays. + + + + + + NŒUDS FICTIFS POMPAGE TURBINAGE + + + Création des 4 nœuds fictifs: 0_PUMP_Daily / 0_TURB_Daily + / 1_PUMP_Weekly / 1_TURB_Weekly + + + + Création des liens entre les 4 nœuds + fictifs et les pays concernés sur la base des capacités existantes dans le package de données + livré le 05 décembre 2016. + + + + Création d'une consommation de 80 000 MW pour + les 2 noeuds de pompage. + + + + Pour chaque nœud pompage et turbinage, création + d'un cluster de 80 000 MW dans Thermal (coût nul pour les nœuds de pompage, coût à + 0.01 €/MWh pour les nœuds de turbinage). + + + + Pour hydro storage, paramètre intra-daily + modulation par défaut à 24 mis à 2. + + + + + + + BINDING CONSTRAINT + + + "Création manuelle des " + 34 + binding constraint + 34 + " pour l'hydraulique (0.75)" + + + + + + LIENS + + + Création des liens entre pays sur la base des liens existants + dans l'onglet new BTC - 2020 (MAF&TYNDP) du fichier 2020_2025 BTC-PROPOSAL.xlsx + + + + "Pour le lien ES-GB-FR: création d'un nœud fictif " + + 34 + y-multiterminal + 34 + " et des liens vers ES/GB/FR" + + + "Création d'une " + 34 + binding constraint + 34 + " pour limitation des imports/exports sur la + Pologne" + + + + + + + ECONOMIC OPT. + + + Unsupplied Energy Cost (average): valeur mise à 3 000 pour + tous les nœuds + + + + + + + DECEMBRE 2016 + + + MISE A JOUR AVEC LES DONNEES POUR SUSTAINABLE TRANSITION 2030 + + + HYDRO: construction des inputs à partir du script R hydro4. + + + THERMAL: mise à jour des prix biofuels pour DKe/DKw/EE/IE/FI/PL/SK. + + + OTHER Non-RES avec price band: DKe/DKw/NL. + + + + + + JANVIER 2017 + + + MISE A JOUR AVEC LES DONNEES POUR SUSTAINABLE TRANSITION 2030 DU 13/01/2017 + + + INTEGRATION DES DSR + + + + + + FEVRIER 2017 + + + MISE A JOUR AVEC LES DONNEES POUR SUSTAINABLE TRANSITION 2030 DU 03/02/2017 + + + CORRECTION DES PRIX BIOFUELS (prix générique biofuel: BE/HU/NL/SE/SI/TR/Hard coal PL) + + + "CORRECTION " + "0_PUMP_Daily / 0_TURB_Daily " + " LUv" + + + ETUDE AVEC LES 34 ANNEES CLIMATIQUES (SELECTION ANNEE CLIMATIQUE 1982/1984/2007 SUITE A DECISION MST + TYNDP DU 27/01/2017) + + + + + + + MARS 2017 + + + MISE A JOUR AVEC LES DONNEES POUR SUSTAINABLE TRANSITION 2030 DU 17/03/2017 (Hors PECD + 2.1) + + + + MISE A JOUR HURDLE COST: 0.01 €/MWh + + + + + + AVRIL 2017 + + + MISE A JOUR AVEC PECD + + + \ No newline at end of file diff --git a/tests/integration/studies_blueprint/assets/test_synthesis/raw_study.synthesis.json b/tests/integration/studies_blueprint/assets/test_synthesis/raw_study.synthesis.json new file mode 100644 index 0000000000..a8b094394f --- /dev/null +++ b/tests/integration/studies_blueprint/assets/test_synthesis/raw_study.synthesis.json @@ -0,0 +1,1491 @@ +{ + "study_path": "DUMMY_VALUE", + "path": "DUMMY_VALUE", + "study_id": "DUMMY_VALUE", + "version": 700, + "output_path": "DUMMY_VALUE", + "areas": { + "de": { + "name": "DE", + "links": { + "fr": { + "filters_synthesis": [], + "filters_year": [ + "hourly" + ] + } + }, + "thermals": [ + { + "id": "01_solar", + "group": "Other 1", + "name": "01_solar", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 10.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 10.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "02_wind_on", + "group": "Other 1", + "name": "02_wind_on", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 20.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 20.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "03_wind_off", + "group": "Other 1", + "name": "03_wind_off", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 30.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 30.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "04_res", + "group": "Other 1", + "name": "04_res", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 40.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 40.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "05_nuclear", + "group": "Other 1", + "name": "05_nuclear", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 50.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 50.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "06_coal", + "group": "Other 1", + "name": "06_coal", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 60.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 60.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "07_gas", + "group": "Other 1", + "name": "07_gas", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 70.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 70.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "08_non-res", + "group": "Other 1", + "name": "08_non-res", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 80.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 80.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "09_hydro_pump", + "group": "Other 1", + "name": "09_hydro_pump", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 90.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 90.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + } + ], + "renewables": [], + "filters_synthesis": [ + "daily", + "monthly" + ], + "filters_year": [ + "hourly", + "weekly", + "annual" + ], + "st_storages": [] + }, + "es": { + "name": "ES", + "links": { + "fr": { + "filters_synthesis": [], + "filters_year": [ + "hourly" + ] + } + }, + "thermals": [ + { + "id": "01_solar", + "group": "Other 1", + "name": "01_solar", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 10.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 10.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "02_wind_on", + "group": "Other 1", + "name": "02_wind_on", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 20.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 20.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "03_wind_off", + "group": "Other 1", + "name": "03_wind_off", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 30.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 30.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "04_res", + "group": "Other 1", + "name": "04_res", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 40.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 40.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "05_nuclear", + "group": "Other 1", + "name": "05_nuclear", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 50.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 50.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "06_coal", + "group": "Other 1", + "name": "06_coal", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 60.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 60.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "07_gas", + "group": "Other 1", + "name": "07_gas", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 70.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 70.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "08_non-res", + "group": "Other 1", + "name": "08_non-res", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 80.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 80.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "09_hydro_pump", + "group": "Other 1", + "name": "09_hydro_pump", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 90.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 90.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + } + ], + "renewables": [], + "filters_synthesis": [ + "daily", + "monthly" + ], + "filters_year": [ + "hourly", + "weekly", + "annual" + ], + "st_storages": [] + }, + "fr": { + "name": "FR", + "links": { + "it": { + "filters_synthesis": [], + "filters_year": [ + "hourly" + ] + } + }, + "thermals": [ + { + "id": "01_solar", + "group": "Other 1", + "name": "01_solar", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 10.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 10.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "02_wind_on", + "group": "Other 1", + "name": "02_wind_on", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 20.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 20.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "03_wind_off", + "group": "Other 1", + "name": "03_wind_off", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 30.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 30.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "04_res", + "group": "Other 1", + "name": "04_res", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 40.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 40.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "05_nuclear", + "group": "Other 1", + "name": "05_nuclear", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 50.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 50.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "06_coal", + "group": "Other 1", + "name": "06_coal", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 60.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 60.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "07_gas", + "group": "Other 1", + "name": "07_gas", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 70.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 70.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "08_non-res", + "group": "Other 1", + "name": "08_non-res", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 80.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 80.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "09_hydro_pump", + "group": "Other 1", + "name": "09_hydro_pump", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 90.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 90.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + } + ], + "renewables": [], + "filters_synthesis": [], + "filters_year": [ + "hourly" + ], + "st_storages": [] + }, + "it": { + "name": "IT", + "links": {}, + "thermals": [ + { + "id": "01_solar", + "group": "Other 1", + "name": "01_solar", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 10.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 10.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "02_wind_on", + "group": "Other 1", + "name": "02_wind_on", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 20.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 20.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "03_wind_off", + "group": "Other 1", + "name": "03_wind_off", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 30.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 30.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "04_res", + "group": "Other 1", + "name": "04_res", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 40.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 40.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "05_nuclear", + "group": "Other 1", + "name": "05_nuclear", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 50.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 50.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "06_coal", + "group": "Other 1", + "name": "06_coal", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 60.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 60.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "07_gas", + "group": "Other 1", + "name": "07_gas", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 70.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 70.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "08_non-res", + "group": "Other 1", + "name": "08_non-res", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 80.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 80.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "09_hydro_pump", + "group": "Other 1", + "name": "09_hydro_pump", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 90.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 90.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + } + ], + "renewables": [], + "filters_synthesis": [], + "filters_year": [ + "hourly" + ], + "st_storages": [] + } + }, + "sets": { + "all areas": { + "name": "All areas", + "inverted_set": true, + "areas": null, + "output": false, + "filters_synthesis": [ + "hourly", + "daily", + "weekly", + "monthly", + "annual" + ], + "filters_year": [ + "hourly", + "daily", + "weekly", + "monthly", + "annual" + ], + "ALL": [ + "hourly", + "daily", + "weekly", + "monthly", + "annual" + ] + } + }, + "outputs": { + "20201014-1422eco-hello": { + "name": "hello", + "date": "20201014-1422", + "mode": "economy", + "nbyears": 1, + "synthesis": true, + "by_year": true, + "error": false, + "playlist": [ + 1 + ], + "archived": false, + "xpansion": "" + }, + "20201014-1425eco-goodbye": { + "name": "goodbye", + "date": "20201014-1425", + "mode": "economy", + "nbyears": 2, + "synthesis": true, + "by_year": true, + "error": false, + "playlist": [ + 1, + 2 + ], + "archived": false, + "xpansion": "" + }, + "20201014-1427eco": { + "name": "", + "date": "20201014-1427", + "mode": "economy", + "nbyears": 1, + "synthesis": true, + "by_year": false, + "error": false, + "playlist": [ + 1 + ], + "archived": false, + "xpansion": "" + }, + "20201014-1430adq": { + "name": "", + "date": "20201014-1430", + "mode": "adequacy", + "nbyears": 1, + "synthesis": true, + "by_year": false, + "error": false, + "playlist": [ + 1 + ], + "archived": false, + "xpansion": "" + }, + "20201014-1430adq-2": { + "name": "2", + "date": "20201014-1430", + "mode": "adequacy", + "nbyears": 1, + "synthesis": true, + "by_year": false, + "error": false, + "playlist": [ + 1 + ], + "archived": true, + "xpansion": "" + } + }, + "bindings": [], + "store_new_set": true, + "archive_input_series": [], + "enr_modelling": "aggregated", + "zip_path": null +} \ No newline at end of file diff --git a/tests/integration/studies_blueprint/assets/test_synthesis/variant_study.synthesis.json b/tests/integration/studies_blueprint/assets/test_synthesis/variant_study.synthesis.json new file mode 100644 index 0000000000..c6fc6c1344 --- /dev/null +++ b/tests/integration/studies_blueprint/assets/test_synthesis/variant_study.synthesis.json @@ -0,0 +1,1419 @@ +{ + "study_path": "DUMMY_VALUE", + "path": "DUMMY_VALUE", + "study_id": "DUMMY_VALUE", + "version": 700, + "output_path": "DUMMY_VALUE", + "areas": { + "de": { + "name": "DE", + "links": { + "fr": { + "filters_synthesis": [], + "filters_year": [ + "hourly" + ] + } + }, + "thermals": [ + { + "id": "01_solar", + "group": "Other 1", + "name": "01_solar", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 10.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 10.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "02_wind_on", + "group": "Other 1", + "name": "02_wind_on", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 20.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 20.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "03_wind_off", + "group": "Other 1", + "name": "03_wind_off", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 30.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 30.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "04_res", + "group": "Other 1", + "name": "04_res", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 40.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 40.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "05_nuclear", + "group": "Other 1", + "name": "05_nuclear", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 50.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 50.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "06_coal", + "group": "Other 1", + "name": "06_coal", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 60.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 60.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "07_gas", + "group": "Other 1", + "name": "07_gas", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 70.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 70.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "08_non-res", + "group": "Other 1", + "name": "08_non-res", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 80.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 80.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "09_hydro_pump", + "group": "Other 1", + "name": "09_hydro_pump", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 90.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 90.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + } + ], + "renewables": [], + "filters_synthesis": [ + "daily", + "monthly" + ], + "filters_year": [ + "hourly", + "weekly", + "annual" + ], + "st_storages": [] + }, + "es": { + "name": "ES", + "links": { + "fr": { + "filters_synthesis": [], + "filters_year": [ + "hourly" + ] + } + }, + "thermals": [ + { + "id": "01_solar", + "group": "Other 1", + "name": "01_solar", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 10.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 10.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "02_wind_on", + "group": "Other 1", + "name": "02_wind_on", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 20.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 20.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "03_wind_off", + "group": "Other 1", + "name": "03_wind_off", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 30.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 30.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "04_res", + "group": "Other 1", + "name": "04_res", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 40.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 40.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "05_nuclear", + "group": "Other 1", + "name": "05_nuclear", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 50.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 50.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "06_coal", + "group": "Other 1", + "name": "06_coal", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 60.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 60.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "07_gas", + "group": "Other 1", + "name": "07_gas", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 70.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 70.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "08_non-res", + "group": "Other 1", + "name": "08_non-res", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 80.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 80.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "09_hydro_pump", + "group": "Other 1", + "name": "09_hydro_pump", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 90.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 90.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + } + ], + "renewables": [], + "filters_synthesis": [ + "daily", + "monthly" + ], + "filters_year": [ + "hourly", + "weekly", + "annual" + ], + "st_storages": [] + }, + "fr": { + "name": "FR", + "links": { + "it": { + "filters_synthesis": [], + "filters_year": [ + "hourly" + ] + } + }, + "thermals": [ + { + "id": "01_solar", + "group": "Other 1", + "name": "01_solar", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 10.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 10.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "02_wind_on", + "group": "Other 1", + "name": "02_wind_on", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 20.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 20.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "03_wind_off", + "group": "Other 1", + "name": "03_wind_off", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 30.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 30.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "04_res", + "group": "Other 1", + "name": "04_res", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 40.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 40.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "05_nuclear", + "group": "Other 1", + "name": "05_nuclear", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 50.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 50.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "06_coal", + "group": "Other 1", + "name": "06_coal", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 60.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 60.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "07_gas", + "group": "Other 1", + "name": "07_gas", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 70.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 70.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "08_non-res", + "group": "Other 1", + "name": "08_non-res", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 80.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 80.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "09_hydro_pump", + "group": "Other 1", + "name": "09_hydro_pump", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 90.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 90.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + } + ], + "renewables": [], + "filters_synthesis": [], + "filters_year": [ + "hourly" + ], + "st_storages": [] + }, + "it": { + "name": "IT", + "links": {}, + "thermals": [ + { + "id": "01_solar", + "group": "Other 1", + "name": "01_solar", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 10.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 10.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "02_wind_on", + "group": "Other 1", + "name": "02_wind_on", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 20.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 20.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "03_wind_off", + "group": "Other 1", + "name": "03_wind_off", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 30.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 30.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "04_res", + "group": "Other 1", + "name": "04_res", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 40.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 40.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "05_nuclear", + "group": "Other 1", + "name": "05_nuclear", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 50.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 50.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "06_coal", + "group": "Other 1", + "name": "06_coal", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 60.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 60.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "07_gas", + "group": "Other 1", + "name": "07_gas", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 70.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 70.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "08_non-res", + "group": "Other 1", + "name": "08_non-res", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 80.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 80.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + }, + { + "id": "09_hydro_pump", + "group": "Other 1", + "name": "09_hydro_pump", + "enabled": true, + "unitcount": 1, + "nominalcapacity": 1000000.0, + "gen-ts": "use global parameter", + "min-stable-power": 0.0, + "min-up-time": 1, + "min-down-time": 1, + "must-run": false, + "spinning": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 90.0, + "spread-cost": 0.0, + "fixed-cost": 0.0, + "startup-cost": 0.0, + "market-bid-cost": 90.0, + "co2": 0.0, + "nh3": 0.0, + "so2": 0.0, + "nox": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "pm10": 0.0, + "nmvoc": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0 + } + ], + "renewables": [], + "filters_synthesis": [], + "filters_year": [ + "hourly" + ], + "st_storages": [] + } + }, + "sets": { + "all areas": { + "name": "All areas", + "inverted_set": true, + "areas": null, + "output": false, + "filters_synthesis": [ + "hourly", + "daily", + "weekly", + "monthly", + "annual" + ], + "filters_year": [ + "hourly", + "daily", + "weekly", + "monthly", + "annual" + ], + "ALL": [ + "hourly", + "daily", + "weekly", + "monthly", + "annual" + ] + } + }, + "outputs": {}, + "bindings": [], + "store_new_set": true, + "archive_input_series": [], + "enr_modelling": "aggregated", + "zip_path": null +} \ No newline at end of file diff --git a/tests/integration/studies_blueprint/test_comments.py b/tests/integration/studies_blueprint/test_comments.py new file mode 100644 index 0000000000..ca6d746443 --- /dev/null +++ b/tests/integration/studies_blueprint/test_comments.py @@ -0,0 +1,132 @@ +import io +import time +from xml.etree import ElementTree + +from starlette.testclient import TestClient + +from tests.integration.studies_blueprint.assets import ASSETS_DIR +from tests.xml_compare import compare_elements + + +class TestStudyComments: + """ + This class contains tests related to the following endpoints: + + - GET /v1/studies/{study_id}/comments + - PUT /v1/studies/{study_id}/comments + """ + + def test_raw_study( + self, + client: TestClient, + user_access_token: str, + study_id: str, + ) -> None: + """ + This test verifies that we can retrieve and modify the comments of a study. + It also performs performance measurements and analyzes. + """ + + # Get the comments of the study and compare with the expected file + res = client.get( + f"/v1/studies/{study_id}/comments", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + actual = res.json() + actual_xml = ElementTree.parse(io.StringIO(actual)).getroot() + expected_xml = ElementTree.parse(ASSETS_DIR.joinpath("test_comments/raw_study.comments.xml")).getroot() + assert compare_elements(actual_xml, expected_xml) == "" + + # Ensure the duration is relatively short + start = time.time() + res = client.get( + f"/v1/studies/{study_id}/comments", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + duration = time.time() - start + assert 0 <= duration <= 0.1, f"Duration is {duration} seconds" + + # Update the comments of the study + res = client.put( + f"/v1/studies/{study_id}/comments", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={"comments": "Ceci est un commentaire en français."}, + ) + assert res.status_code == 204, res.json() + + # Get the comments of the study and compare with the expected file + res = client.get( + f"/v1/studies/{study_id}/comments", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + assert res.json() == "Ceci est un commentaire en français." + + def test_variant_study( + self, + client: TestClient, + user_access_token: str, + study_id: str, + ) -> None: + """ + This test verifies that we can retrieve and modify the comments of a VARIANT study. + It also performs performance measurements and analyzes. + """ + # First, we create a copy of the study, and we convert it to a managed study. + res = client.post( + f"/v1/studies/{study_id}/copy", + headers={"Authorization": f"Bearer {user_access_token}"}, + params={"dest": "default", "with_outputs": False, "use_task": False}, # type: ignore + ) + assert res.status_code == 201, res.json() + base_study_id = res.json() + assert base_study_id is not None + + # Then, we create a new variant of the base study + res = client.post( + f"/v1/studies/{base_study_id}/variants", + headers={"Authorization": f"Bearer {user_access_token}"}, + params={"name": f"Variant XYZ"}, + ) + assert res.status_code == 200, res.json() # should be CREATED + variant_id = res.json() + assert variant_id is not None + + # Get the comments of the study and compare with the expected file + res = client.get( + f"/v1/studies/{variant_id}/comments", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + actual = res.json() + actual_xml = ElementTree.parse(io.StringIO(actual)).getroot() + expected_xml = ElementTree.parse(ASSETS_DIR.joinpath("test_comments/raw_study.comments.xml")).getroot() + assert compare_elements(actual_xml, expected_xml) == "" + + # Ensure the duration is relatively short + start = time.time() + res = client.get( + f"/v1/studies/{variant_id}/comments", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + duration = time.time() - start + assert 0 <= duration <= 0.1, f"Duration is {duration} seconds" + + # Update the comments of the study + res = client.put( + f"/v1/studies/{variant_id}/comments", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={"comments": "Ceci est un commentaire en français."}, + ) + assert res.status_code == 204, res.json() + + # Get the comments of the study and compare with the expected file + res = client.get( + f"/v1/studies/{variant_id}/comments", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + assert res.json() == "Ceci est un commentaire en français." diff --git a/tests/integration/studies_blueprint/test_synthesis.py b/tests/integration/studies_blueprint/test_synthesis.py new file mode 100644 index 0000000000..70f5f0c907 --- /dev/null +++ b/tests/integration/studies_blueprint/test_synthesis.py @@ -0,0 +1,111 @@ +import json +import sys +import time + +from starlette.testclient import TestClient + +from tests.integration.studies_blueprint.assets import ASSETS_DIR + + +def _compare_resource_file(actual, res_path): + # note: private data are masked in the resource file + masked = dict.fromkeys(["study_path", "path", "output_path", "study_id"], "DUMMY_VALUE") + actual.update(masked) + if res_path.exists(): + # Compare the actual synthesis with the expected one + expected = json.loads(res_path.read_text()) + assert actual == expected + else: + # Update the resource file with the actual synthesis (a git commit is required) + res_path.parent.mkdir(parents=True, exist_ok=True) + res_path.write_text(json.dumps(actual, indent=2, ensure_ascii=False)) + print(f"Resource file '{res_path}' must be committed.", file=sys.stderr) + + +class TestStudySynthesis: + """ + This class contains tests related to the following endpoints: + + - GET /v1/studies/{study_id}/synthesis + """ + + def test_raw_study( + self, + client: TestClient, + user_access_token: str, + study_id: str, + ) -> None: + """ + This test verifies that we can retrieve the synthesis of a study. + It also performs performance measurements and analyzes. + """ + + # Get the synthesis of the study and compare with the expected file + res = client.get( + f"/v1/studies/{study_id}/synthesis", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + actual = res.json() + res_path = ASSETS_DIR.joinpath("test_synthesis/raw_study.synthesis.json") + _compare_resource_file(actual, res_path) + + # Ensure the duration is relatively short + start = time.time() + res = client.get( + f"/v1/studies/{study_id}/synthesis", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + duration = time.time() - start + assert 0 <= duration <= 0.1, f"Duration is {duration} seconds" + + def test_variant_study( + self, + client: TestClient, + user_access_token: str, + study_id: str, + ) -> None: + """ + This test verifies that we can retrieve and modify the synthesis of a VARIANT study. + It also performs performance measurements and analyzes. + """ + # First, we create a copy of the study, and we convert it to a managed study. + res = client.post( + f"/v1/studies/{study_id}/copy", + headers={"Authorization": f"Bearer {user_access_token}"}, + params={"dest": "default", "with_outputs": False, "use_task": False}, # type: ignore + ) + assert res.status_code == 201, res.json() + base_study_id = res.json() + assert base_study_id is not None + + # Then, we create a new variant of the base study + res = client.post( + f"/v1/studies/{base_study_id}/variants", + headers={"Authorization": f"Bearer {user_access_token}"}, + params={"name": f"Variant XYZ"}, + ) + assert res.status_code == 200, res.json() # should be CREATED + variant_id = res.json() + assert variant_id is not None + + # Get the synthesis of the study and compare with the expected file + res = client.get( + f"/v1/studies/{variant_id}/synthesis", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + actual = res.json() + res_path = ASSETS_DIR.joinpath("test_synthesis/variant_study.synthesis.json") + _compare_resource_file(actual, res_path) + + # Ensure the duration is relatively short + start = time.time() + res = client.get( + f"/v1/studies/{variant_id}/synthesis", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + duration = time.time() - start + assert 0 <= duration <= 0.1, f"Duration is {duration} seconds" diff --git a/tests/integration/study_data_blueprint/test_renewable.py b/tests/integration/study_data_blueprint/test_renewable.py new file mode 100644 index 0000000000..c3bb7eaa79 --- /dev/null +++ b/tests/integration/study_data_blueprint/test_renewable.py @@ -0,0 +1,424 @@ +""" +## End-to-end test of the renewable cluster management. + +We should consider the following scenario parameters : +* study type: `["raw", "variant"]` + - user/bot can manage properties/matrices indifferently for raw or variant studies. +* token: `["user_token", "bot_token"]` (bot = application token) + - an authenticated user with the right permission (WRITE) can manage clusters, + - we can use a bot token to manage clusters. +* study permission: + - `StudyPermissionType.READ`: user/bot can only read properties/matrices, + - `StudyPermissionType.RUN`: user/bot has no permission to manage clusters, + - `StudyPermissionType.WRITE`: user/bot can manage cluster properties/matrices, + - `StudyPermissionType.MANAGE_PERMISSIONS`: user/bot has no permission to manage clusters. + +We should test the following end poins: +* create a cluster (with only a name/with all properties) +* read the properties of a cluster +* read the matrices of a cluster +* read the list of clusters +* update a cluster (all the properties/a single property) +* update the matrices of a cluster +* delete a cluster (or several clusters) +* validate the consistency of the matrices (and properties) +""" +import json +import re + +import pytest +from starlette.testclient import TestClient + +from antarest.core.tasks.model import TaskStatus +from antarest.core.utils.string import to_camel_case +from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.renewable import RenewableProperties +from tests.integration.utils import wait_task_completion + +DEFAULT_PROPERTIES = json.loads(RenewableProperties(name="Dummy").json()) +DEFAULT_PROPERTIES = {to_camel_case(k): v for k, v in DEFAULT_PROPERTIES.items() if k != "name"} + +# noinspection SpellCheckingInspection +EXISTING_CLUSTERS = [] + + +@pytest.mark.unit_test +class TestRenewable: + def test_lifecycle( + self, + client: TestClient, + user_access_token: str, + study_id: str, + ) -> None: + # Upgrade study to version 810 + res = client.put( + f"/v1/studies/{study_id}/upgrade", + headers={"Authorization": f"Bearer {user_access_token}"}, + params={"target_version": 810}, + ) + res.raise_for_status() + task_id = res.json() + task = wait_task_completion(client, user_access_token, task_id) + assert task.status == TaskStatus.COMPLETED, task + + # ===================== + # General Data Update + # ===================== + + # Parameter 'renewable-generation-modelling' must be set to 'clusters' instead of 'aggregated'. + # The `enr_modelling` value must be set to "clusters" instead of "aggregated" + args = { + "target": "settings/generaldata/other preferences", + "data": {"renewable-generation-modelling": "clusters"}, + } + res = client.post( + f"/v1/studies/{study_id}/commands", + headers={"Authorization": f"Bearer {user_access_token}"}, + json=[{"action": "update_config", "args": args}], + ) + assert res.status_code == 200, res.json() + + # ============================= + # RENEWABLE CLUSTER CREATION + # ============================= + + area_id = transform_name_to_id("FR") + fr_solar_pv = "FR Solar PV" + + # Un attempt to create a renewable cluster without name + # should raise a validation error (other properties are optional). + # Un attempt to create a renewable cluster with an empty name + # or an invalid name should also raise a validation error. + attempts = [{}, {"name": ""}, {"name": "!??"}] + for attempt in attempts: + res = client.post( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/renewable", + headers={"Authorization": f"Bearer {user_access_token}"}, + json=attempt, + ) + assert res.status_code == 422, res.json() + assert res.json()["exception"] in {"ValidationError", "RequestValidationError"}, res.json() + + # We can create a renewable cluster with the following properties: + fr_solar_pv_props = { + **DEFAULT_PROPERTIES, + "name": fr_solar_pv, + "group": "Solar PV", + "nominalCapacity": 5001, + "unitCount": 1, + "tsInterpretation": "production-factor", + } + res = client.post( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/renewable", + headers={"Authorization": f"Bearer {user_access_token}"}, + json=fr_solar_pv_props, + ) + assert res.status_code == 200, res.json() + fr_solar_pv_id = res.json()["id"] + assert fr_solar_pv_id == transform_name_to_id(fr_solar_pv, lower=False) + # noinspection SpellCheckingInspection + fr_solar_pv_cfg = {"id": fr_solar_pv_id, **fr_solar_pv_props} + assert res.json() == fr_solar_pv_cfg + + # reading the properties of a renewable cluster + res = client.get( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/renewable/{fr_solar_pv_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + assert res.json() == fr_solar_pv_cfg + + # ============================= + # RENEWABLE CLUSTER MATRICES + # ============================= + + # TODO: add unit tests for renewable cluster matrices + + # ================================== + # RENEWABLE CLUSTER LIST / GROUPS + # ================================== + + # Reading the list of renewable clusters + res = client.get( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/renewable", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + assert res.json() == EXISTING_CLUSTERS + [fr_solar_pv_cfg] + + # updating properties + res = client.patch( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/renewable/{fr_solar_pv_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={ + "name": "FR Solar pv old 1", + "nominalCapacity": 5132, + }, + ) + assert res.status_code == 200, res.json() + fr_solar_pv_cfg = { + **fr_solar_pv_cfg, + "name": "FR Solar pv old 1", + "nominalCapacity": 5132, + } + assert res.json() == fr_solar_pv_cfg + + res = client.get( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/renewable/{fr_solar_pv_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + assert res.json() == fr_solar_pv_cfg + + # =========================== + # RENEWABLE CLUSTER UPDATE + # =========================== + + # updating properties + res = client.patch( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/renewable/{fr_solar_pv_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={ + "nominalCapacity": 2260, + "tsInterpretation": "power-generation", + }, + ) + fr_solar_pv_cfg = { + **fr_solar_pv_cfg, + "nominalCapacity": 2260, + "tsInterpretation": "power-generation", + } + assert res.status_code == 200, res.json() + assert res.json() == fr_solar_pv_cfg + + # An attempt to update the `unitCount` property with an invalid value + # should raise a validation error. + # The `unitCount` property must be an integer greater than 0. + bad_properties = {"unitCount": 0} + res = client.patch( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/renewable/{fr_solar_pv_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + json=bad_properties, + ) + assert res.status_code == 422, res.json() + assert res.json()["exception"] == "ValidationError", res.json() + + # The renewable cluster properties should not have been updated. + res = client.get( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/renewable/{fr_solar_pv_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + assert res.json() == fr_solar_pv_cfg + + # ============================= + # RENEWABLE CLUSTER DELETION + # ============================= + + # To delete a renewable cluster, we need to provide its ID. + res = client.request( + "DELETE", + f"/v1/studies/{study_id}/areas/{area_id}/clusters/renewable", + headers={"Authorization": f"Bearer {user_access_token}"}, + json=[fr_solar_pv_id], + ) + assert res.status_code == 204, res.json() + assert res.text in {"", "null"} # Old FastAPI versions return 'null'. + + # If the renewable cluster list is empty, the deletion should be a no-op. + res = client.request( + "DELETE", + f"/v1/studies/{study_id}/areas/{area_id}/clusters/renewable", + headers={"Authorization": f"Bearer {user_access_token}"}, + json=[], + ) + assert res.status_code == 204, res.json() + assert res.text in {"", "null"} # Old FastAPI versions return 'null'. + + # It's possible to delete multiple renewable clusters at once. + # Create two clusters + res = client.post( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/renewable", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={"name": "Other Cluster 1"}, + ) + assert res.status_code == 200, res.json() + other_cluster_id1 = res.json()["id"] + + res = client.post( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/renewable", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={"name": "Other Cluster 2"}, + ) + assert res.status_code == 200, res.json() + other_cluster_id2 = res.json()["id"] + + # We can delete the two renewable clusters at once. + res = client.request( + "DELETE", + f"/v1/studies/{study_id}/areas/{area_id}/clusters/renewable", + headers={"Authorization": f"Bearer {user_access_token}"}, + json=[other_cluster_id1, other_cluster_id2], + ) + assert res.status_code == 204, res.json() + assert res.text in {"", "null"} # Old FastAPI versions return 'null'. + + # The list of renewable clusters should be empty. + res = client.get( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/renewable", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + expected = [ + c + for c in EXISTING_CLUSTERS + if transform_name_to_id(c["name"], lower=False) not in [other_cluster_id1, other_cluster_id2] + ] + assert res.json() == expected + + # =========================== + # RENEWABLE CLUSTER ERRORS + # =========================== + + # Check DELETE with the wrong value of `area_id` + bad_area_id = "bad_area" + res = client.request( + "DELETE", + f"/v1/studies/{study_id}/areas/{bad_area_id}/clusters/renewable", + headers={"Authorization": f"Bearer {user_access_token}"}, + json=[fr_solar_pv_id], + ) + assert res.status_code == 500, res.json() + obj = res.json() + description = obj["description"] + assert bad_area_id in description + assert re.search( + re.escape("Area 'bad_area' does not exist"), + description, + flags=re.IGNORECASE, + ) + + # Check DELETE with the wrong value of `study_id` + bad_study_id = "bad_study" + res = client.request( + "DELETE", + f"/v1/studies/{bad_study_id}/areas/{area_id}/clusters/renewable", + headers={"Authorization": f"Bearer {user_access_token}"}, + json=[fr_solar_pv_id], + ) + obj = res.json() + description = obj["description"] + assert res.status_code == 404, res.json() + assert bad_study_id in description + + # Check GET with wrong `area_id` + res = client.get( + f"/v1/studies/{study_id}/areas/{bad_area_id}/clusters/renewable/{fr_solar_pv_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + obj = res.json() + description = obj["description"] + assert bad_area_id in description + assert res.status_code == 404, res.json() + + # Check GET with wrong `study_id` + res = client.get( + f"/v1/studies/{bad_study_id}/areas/{area_id}/clusters/renewable/{fr_solar_pv_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + obj = res.json() + description = obj["description"] + assert res.status_code == 404, res.json() + assert bad_study_id in description + + # Check POST with wrong `study_id` + res = client.post( + f"/v1/studies/{bad_study_id}/areas/{area_id}/clusters/renewable", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={"name": fr_solar_pv, "group": "Battery"}, + ) + obj = res.json() + description = obj["description"] + assert res.status_code == 404, res.json() + assert bad_study_id in description + + # Check POST with wrong `area_id` + res = client.post( + f"/v1/studies/{study_id}/areas/{bad_area_id}/clusters/renewable", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={ + "name": fr_solar_pv, + "group": "Wind Onshore", + "nominalCapacity": 617, + "unitCount": 2, + "tsInterpretation": "production-factor", + }, + ) + assert res.status_code == 500, res.json() + obj = res.json() + description = obj["description"] + assert bad_area_id in description + assert re.search(r"Area ", description, flags=re.IGNORECASE) + assert re.search(r"does not exist ", description, flags=re.IGNORECASE) + + # Check POST with wrong `group` + res = client.post( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/renewable", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={"name": fr_solar_pv, "group": "GroupFoo"}, + ) + assert res.status_code == 200, res.json() + obj = res.json() + # If a group is not found, return the default group ("Other RES 1" by default). + assert obj["group"] == "Other RES 1" + + # Check PATCH with the wrong `area_id` + res = client.patch( + f"/v1/studies/{study_id}/areas/{bad_area_id}/clusters/renewable/{fr_solar_pv_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={ + "group": "Wind Onshore", + "nominalCapacity": 617, + "unitCount": 2, + "tsInterpretation": "production-factor", + }, + ) + assert res.status_code == 404, res.json() + obj = res.json() + description = obj["description"] + assert bad_area_id in description + assert re.search(r"not a child of ", description, flags=re.IGNORECASE) + + # Check PATCH with the wrong `cluster_id` + bad_cluster_id = "bad_cluster" + res = client.patch( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/renewable/{bad_cluster_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={ + "group": "Wind Onshore", + "nominalCapacity": 617, + "unitCount": 2, + "tsInterpretation": "production-factor", + }, + ) + assert res.status_code == 404, res.json() + obj = res.json() + description = obj["description"] + assert bad_cluster_id in description + assert re.search(re.escape("'bad_cluster' not found"), description, flags=re.IGNORECASE) + + # Check PATCH with the wrong `study_id` + res = client.patch( + f"/v1/studies/{bad_study_id}/areas/{area_id}/clusters/renewable/{fr_solar_pv_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={ + "group": "Wind Onshore", + "nominalCapacity": 617, + "unitCount": 2, + "tsInterpretation": "production-factor", + }, + ) + assert res.status_code == 404, res.json() + obj = res.json() + description = obj["description"] + assert bad_study_id in description diff --git a/tests/integration/study_data_blueprint/test_st_storage.py b/tests/integration/study_data_blueprint/test_st_storage.py index 2d1035af3e..ed3a45a360 100644 --- a/tests/integration/study_data_blueprint/test_st_storage.py +++ b/tests/integration/study_data_blueprint/test_st_storage.py @@ -201,15 +201,16 @@ def test_lifecycle__nominal( f"/v1/studies/{study_id}/areas/{area_id}/storages/{siemens_battery_id}", headers={"Authorization": f"Bearer {user_access_token}"}, json={ - "initialLevel": 5900, + "initialLevel": 0.59, "reservoirCapacity": 0, }, ) siemens_config = { **siemens_config, - "initialLevel": 5900, + "initialLevel": 0.59, "reservoirCapacity": 0, } + assert res.status_code == 200, res.json() assert res.json() == siemens_config # An attempt to update the `efficiency` property with an invalid value @@ -265,7 +266,7 @@ def test_lifecycle__nominal( "withdrawalNominalCapacity": 1350, "reservoirCapacity": 1500, "efficiency": 0.90, - "initialLevel": 200, + "initialLevel": 0.2, "initialLevelOptim": False, } res = client.post( @@ -285,7 +286,7 @@ def test_lifecycle__nominal( "withdrawalNominalCapacity": 1800, "reservoirCapacity": 20000, "efficiency": 0.78, - "initialLevel": 10000, + "initialLevel": 1, } res = client.post( f"/v1/studies/{study_id}/areas/{area_id}/storages", @@ -414,7 +415,7 @@ def test_lifecycle__nominal( # Check POST with wrong `group` res = client.post( - f"/v1/studies/{study_id}/areas/{bad_area_id}/storages", + f"/v1/studies/{study_id}/areas/{area_id}/storages", headers={"Authorization": f"Bearer {user_access_token}"}, json={"name": siemens_battery, "group": "GroupFoo"}, ) @@ -443,9 +444,10 @@ def test_lifecycle__nominal( assert bad_area_id in description assert re.search(r"not a child of ", description, flags=re.IGNORECASE) - # Check PATCH with the wrong `siemens_battery_id` + # Check PATCH with the wrong `storage_id` + bad_storage_id = "bad_storage" res = client.patch( - f"/v1/studies/{study_id}/areas/{area_id}/storages/{siemens_battery_id}", + f"/v1/studies/{study_id}/areas/{area_id}/storages/{bad_storage_id}", headers={"Authorization": f"Bearer {user_access_token}"}, json={ "efficiency": 1.0, @@ -460,7 +462,7 @@ def test_lifecycle__nominal( assert res.status_code == 404, res.json() obj = res.json() description = obj["description"] - assert siemens_battery_id in description + assert bad_storage_id in description assert re.search(r"fields of storage", description, flags=re.IGNORECASE) assert re.search(r"not found", description, flags=re.IGNORECASE) diff --git a/tests/integration/study_data_blueprint/test_thermal.py b/tests/integration/study_data_blueprint/test_thermal.py new file mode 100644 index 0000000000..dd95aafdd3 --- /dev/null +++ b/tests/integration/study_data_blueprint/test_thermal.py @@ -0,0 +1,750 @@ +""" +## End-to-end test of the thermal cluster management. + +We should consider the following scenario parameters : +* study version: `[860, 800]`: + - `860`: user/bot can read/update all properties **including** new pollutant values. + - `800`: user/bot can read/update all properties **excluding** new pollutant values: + - an attempt to create or update a study with new pollutant values must raise an error. +* study type: `["raw", "variant"]` + - user/bot can manage properties/matrices indifferently for raw or variant studies. +* token: `["user_token", "bot_token"]` (bot = application token) + - an authenticated user with the right permission (WRITE) can manage clusters, + - we can use a bot token to manage clusters. +* study permission: + - `StudyPermissionType.READ`: user/bot can only read properties/matrices, + - `StudyPermissionType.RUN`: user/bot has no permission to manage clusters, + - `StudyPermissionType.WRITE`: user/bot can manage cluster properties/matrices, + - `StudyPermissionType.MANAGE_PERMISSIONS`: user/bot has no permission to manage clusters. + +We should test the following end poins: +* create a cluster (with only a name/with all properties) +* read the properties of a cluster +* read the matrices of a cluster +* read the list of clusters +* update a cluster (all the properties/a single property) +* update the matrices of a cluster +* delete a cluster (or several clusters) +* validate the consistency of the matrices (and properties) +""" +import json +import re + +import pytest +from starlette.testclient import TestClient + +from antarest.core.utils.string import to_camel_case +from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.thermal import Thermal860Properties, ThermalProperties + +DEFAULT_PROPERTIES = json.loads(ThermalProperties(name="Dummy").json()) +DEFAULT_PROPERTIES = {to_camel_case(k): v for k, v in DEFAULT_PROPERTIES.items() if k != "name"} + +DEFAULT_860_PROPERTIES = json.loads(Thermal860Properties(name="Dummy").json()) +DEFAULT_860_PROPERTIES = {to_camel_case(k): v for k, v in DEFAULT_860_PROPERTIES.items() if k != "name"} + +# noinspection SpellCheckingInspection +EXISTING_CLUSTERS = [ + { + "co2": 0.0, + "enabled": True, + "fixedCost": 0.0, + "genTs": "use global parameter", + "group": "Other 1", + "id": "01_solar", + "lawForced": "uniform", + "lawPlanned": "uniform", + "marginalCost": 10.0, + "marketBidCost": 10.0, + "minDownTime": 1, + "minStablePower": 0.0, + "minUpTime": 1, + "mustRun": False, + "name": "01_solar", + "nh3": None, + "nmvoc": None, + "nominalCapacity": 1000000.0, + "nox": None, + "op1": None, + "op2": None, + "op3": None, + "op4": None, + "op5": None, + "pm10": None, + "pm25": None, + "pm5": None, + "so2": None, + "spinning": 0.0, + "spreadCost": 0.0, + "startupCost": 0.0, + "unitCount": 1, + "volatilityForced": 0.0, + "volatilityPlanned": 0.0, + }, + { + "co2": 0.0, + "enabled": True, + "fixedCost": 0.0, + "genTs": "use global parameter", + "group": "Other 1", + "id": "02_wind_on", + "lawForced": "uniform", + "lawPlanned": "uniform", + "marginalCost": 20.0, + "marketBidCost": 20.0, + "minDownTime": 1, + "minStablePower": 0.0, + "minUpTime": 1, + "mustRun": False, + "name": "02_wind_on", + "nh3": None, + "nmvoc": None, + "nominalCapacity": 1000000.0, + "nox": None, + "op1": None, + "op2": None, + "op3": None, + "op4": None, + "op5": None, + "pm10": None, + "pm25": None, + "pm5": None, + "so2": None, + "spinning": 0.0, + "spreadCost": 0.0, + "startupCost": 0.0, + "unitCount": 1, + "volatilityForced": 0.0, + "volatilityPlanned": 0.0, + }, + { + "co2": 0.0, + "enabled": True, + "fixedCost": 0.0, + "genTs": "use global parameter", + "group": "Other 1", + "id": "03_wind_off", + "lawForced": "uniform", + "lawPlanned": "uniform", + "marginalCost": 30.0, + "marketBidCost": 30.0, + "minDownTime": 1, + "minStablePower": 0.0, + "minUpTime": 1, + "mustRun": False, + "name": "03_wind_off", + "nh3": None, + "nmvoc": None, + "nominalCapacity": 1000000.0, + "nox": None, + "op1": None, + "op2": None, + "op3": None, + "op4": None, + "op5": None, + "pm10": None, + "pm25": None, + "pm5": None, + "so2": None, + "spinning": 0.0, + "spreadCost": 0.0, + "startupCost": 0.0, + "unitCount": 1, + "volatilityForced": 0.0, + "volatilityPlanned": 0.0, + }, + { + "co2": 0.0, + "enabled": True, + "fixedCost": 0.0, + "genTs": "use global parameter", + "group": "Other 1", + "id": "04_res", + "lawForced": "uniform", + "lawPlanned": "uniform", + "marginalCost": 40.0, + "marketBidCost": 40.0, + "minDownTime": 1, + "minStablePower": 0.0, + "minUpTime": 1, + "mustRun": False, + "name": "04_res", + "nh3": None, + "nmvoc": None, + "nominalCapacity": 1000000.0, + "nox": None, + "op1": None, + "op2": None, + "op3": None, + "op4": None, + "op5": None, + "pm10": None, + "pm25": None, + "pm5": None, + "so2": None, + "spinning": 0.0, + "spreadCost": 0.0, + "startupCost": 0.0, + "unitCount": 1, + "volatilityForced": 0.0, + "volatilityPlanned": 0.0, + }, + { + "co2": 0.0, + "enabled": True, + "fixedCost": 0.0, + "genTs": "use global parameter", + "group": "Other 1", + "id": "05_nuclear", + "lawForced": "uniform", + "lawPlanned": "uniform", + "marginalCost": 50.0, + "marketBidCost": 50.0, + "minDownTime": 1, + "minStablePower": 0.0, + "minUpTime": 1, + "mustRun": False, + "name": "05_nuclear", + "nh3": None, + "nmvoc": None, + "nominalCapacity": 1000000.0, + "nox": None, + "op1": None, + "op2": None, + "op3": None, + "op4": None, + "op5": None, + "pm10": None, + "pm25": None, + "pm5": None, + "so2": None, + "spinning": 0.0, + "spreadCost": 0.0, + "startupCost": 0.0, + "unitCount": 1, + "volatilityForced": 0.0, + "volatilityPlanned": 0.0, + }, + { + "co2": 0.0, + "enabled": True, + "fixedCost": 0.0, + "genTs": "use global parameter", + "group": "Other 1", + "id": "06_coal", + "lawForced": "uniform", + "lawPlanned": "uniform", + "marginalCost": 60.0, + "marketBidCost": 60.0, + "minDownTime": 1, + "minStablePower": 0.0, + "minUpTime": 1, + "mustRun": False, + "name": "06_coal", + "nh3": None, + "nmvoc": None, + "nominalCapacity": 1000000.0, + "nox": None, + "op1": None, + "op2": None, + "op3": None, + "op4": None, + "op5": None, + "pm10": None, + "pm25": None, + "pm5": None, + "so2": None, + "spinning": 0.0, + "spreadCost": 0.0, + "startupCost": 0.0, + "unitCount": 1, + "volatilityForced": 0.0, + "volatilityPlanned": 0.0, + }, + { + "co2": 0.0, + "enabled": True, + "fixedCost": 0.0, + "genTs": "use global parameter", + "group": "Other 1", + "id": "07_gas", + "lawForced": "uniform", + "lawPlanned": "uniform", + "marginalCost": 70.0, + "marketBidCost": 70.0, + "minDownTime": 1, + "minStablePower": 0.0, + "minUpTime": 1, + "mustRun": False, + "name": "07_gas", + "nh3": None, + "nmvoc": None, + "nominalCapacity": 1000000.0, + "nox": None, + "op1": None, + "op2": None, + "op3": None, + "op4": None, + "op5": None, + "pm10": None, + "pm25": None, + "pm5": None, + "so2": None, + "spinning": 0.0, + "spreadCost": 0.0, + "startupCost": 0.0, + "unitCount": 1, + "volatilityForced": 0.0, + "volatilityPlanned": 0.0, + }, + { + "co2": 0.0, + "enabled": True, + "fixedCost": 0.0, + "genTs": "use global parameter", + "group": "Other 1", + "id": "08_non-res", + "lawForced": "uniform", + "lawPlanned": "uniform", + "marginalCost": 80.0, + "marketBidCost": 80.0, + "minDownTime": 1, + "minStablePower": 0.0, + "minUpTime": 1, + "mustRun": False, + "name": "08_non-res", + "nh3": None, + "nmvoc": None, + "nominalCapacity": 1000000.0, + "nox": None, + "op1": None, + "op2": None, + "op3": None, + "op4": None, + "op5": None, + "pm10": None, + "pm25": None, + "pm5": None, + "so2": None, + "spinning": 0.0, + "spreadCost": 0.0, + "startupCost": 0.0, + "unitCount": 1, + "volatilityForced": 0.0, + "volatilityPlanned": 0.0, + }, + { + "co2": 0.0, + "enabled": True, + "fixedCost": 0.0, + "genTs": "use global parameter", + "group": "Other 1", + "id": "09_hydro_pump", + "lawForced": "uniform", + "lawPlanned": "uniform", + "marginalCost": 90.0, + "marketBidCost": 90.0, + "minDownTime": 1, + "minStablePower": 0.0, + "minUpTime": 1, + "mustRun": False, + "name": "09_hydro_pump", + "nh3": None, + "nmvoc": None, + "nominalCapacity": 1000000.0, + "nox": None, + "op1": None, + "op2": None, + "op3": None, + "op4": None, + "op5": None, + "pm10": None, + "pm25": None, + "pm5": None, + "so2": None, + "spinning": 0.0, + "spreadCost": 0.0, + "startupCost": 0.0, + "unitCount": 1, + "volatilityForced": 0.0, + "volatilityPlanned": 0.0, + }, +] + + +@pytest.mark.unit_test +class TestThermal: + def test_lifecycle( + self, + client: TestClient, + user_access_token: str, + study_id: str, + ) -> None: + # ============================= + # THERMAL CLUSTER CREATION + # ============================= + + area_id = transform_name_to_id("FR") + fr_gas_conventional = "FR_Gas conventional" + + # Un attempt to create a thermal cluster without name + # should raise a validation error (other properties are optional). + # Un attempt to create a thermal cluster with an empty name + # or an invalid name should also raise a validation error. + attempts = [{}, {"name": ""}, {"name": "!??"}] + for attempt in attempts: + res = client.post( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/thermal", + headers={"Authorization": f"Bearer {user_access_token}"}, + json=attempt, + ) + assert res.status_code == 422, res.json() + assert res.json()["exception"] in {"ValidationError", "RequestValidationError"}, res.json() + + # We can create a thermal cluster with the following properties: + fr_gas_conventional_props = { + **DEFAULT_PROPERTIES, + "name": fr_gas_conventional, + "group": "Gas", + "unitCount": 15, + "nominalCapacity": 31.6, + "minStablePower": 5.4984, + "minUpTime": 5, + "minDownTime": 5, + "co2": 0.57, + "marginalCost": 181.267, + "startupCost": 6035.6, + "marketBidCost": 181.267, + } + res = client.post( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/thermal", + headers={"Authorization": f"Bearer {user_access_token}"}, + json=fr_gas_conventional_props, + ) + assert res.status_code == 200, res.json() + fr_gas_conventional_id = res.json()["id"] + assert fr_gas_conventional_id == transform_name_to_id(fr_gas_conventional, lower=False) + # noinspection SpellCheckingInspection + fr_gas_conventional_cfg = { + **fr_gas_conventional_props, + "id": fr_gas_conventional_id, + "nh3": None, + "nmvoc": None, + "nox": None, + "op1": None, + "op2": None, + "op3": None, + "op4": None, + "op5": None, + "pm10": None, + "pm25": None, + "pm5": None, + "so2": None, + } + assert res.json() == fr_gas_conventional_cfg + + # reading the properties of a thermal cluster + res = client.get( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/thermal/{fr_gas_conventional_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + assert res.json() == fr_gas_conventional_cfg + + # ============================= + # THERMAL CLUSTER MATRICES + # ============================= + + # TODO: add unit tests for thermal cluster matrices + + # ================================== + # THERMAL CLUSTER LIST / GROUPS + # ================================== + + # Reading the list of thermal clusters + res = client.get( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/thermal", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + assert res.json() == EXISTING_CLUSTERS + [fr_gas_conventional_cfg] + + # updating properties + res = client.patch( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/thermal/{fr_gas_conventional_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={ + "name": "FR_Gas conventional old 1", + "nominalCapacity": 32.1, + }, + ) + assert res.status_code == 200, res.json() + fr_gas_conventional_cfg = { + **fr_gas_conventional_cfg, + "name": "FR_Gas conventional old 1", + "nominalCapacity": 32.1, + } + assert res.json() == fr_gas_conventional_cfg + + res = client.get( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/thermal/{fr_gas_conventional_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + assert res.json() == fr_gas_conventional_cfg + + # =========================== + # THERMAL CLUSTER UPDATE + # =========================== + + # updating properties + res = client.patch( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/thermal/{fr_gas_conventional_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={ + "marginalCost": 182.456, + "startupCost": 6140.8, + "marketBidCost": 182.456, + }, + ) + fr_gas_conventional_cfg = { + **fr_gas_conventional_cfg, + "marginalCost": 182.456, + "startupCost": 6140.8, + "marketBidCost": 182.456, + } + assert res.status_code == 200, res.json() + assert res.json() == fr_gas_conventional_cfg + + # An attempt to update the `unitCount` property with an invalid value + # should raise a validation error. + # The `unitCount` property must be an integer greater than 0. + bad_properties = {"unitCount": 0} + res = client.patch( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/thermal/{fr_gas_conventional_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + json=bad_properties, + ) + assert res.status_code == 422, res.json() + assert res.json()["exception"] == "ValidationError", res.json() + + # The thermal cluster properties should not have been updated. + res = client.get( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/thermal/{fr_gas_conventional_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + assert res.json() == fr_gas_conventional_cfg + + # ============================= + # THERMAL CLUSTER DELETION + # ============================= + + # To delete a thermal cluster, we need to provide its ID. + res = client.request( + "DELETE", + f"/v1/studies/{study_id}/areas/{area_id}/clusters/thermal", + headers={"Authorization": f"Bearer {user_access_token}"}, + json=[fr_gas_conventional_id], + ) + assert res.status_code == 204, res.json() + assert res.text in {"", "null"} # Old FastAPI versions return 'null'. + + # If the thermal cluster list is empty, the deletion should be a no-op. + res = client.request( + "DELETE", + f"/v1/studies/{study_id}/areas/{area_id}/clusters/thermal", + headers={"Authorization": f"Bearer {user_access_token}"}, + json=[], + ) + assert res.status_code == 204, res.json() + assert res.text in {"", "null"} # Old FastAPI versions return 'null'. + + # It's possible to delete multiple thermal clusters at once. + # We can delete the two thermal clusters at once. + other_cluster_id1 = "01_solar" + other_cluster_id2 = "02_wind_on" + res = client.request( + "DELETE", + f"/v1/studies/{study_id}/areas/{area_id}/clusters/thermal", + headers={"Authorization": f"Bearer {user_access_token}"}, + json=[other_cluster_id1, other_cluster_id2], + ) + assert res.status_code == 204, res.json() + assert res.text in {"", "null"} # Old FastAPI versions return 'null'. + + # The list of thermal clusters should be empty. + res = client.get( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/thermal", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + assert res.status_code == 200, res.json() + expected = [ + c + for c in EXISTING_CLUSTERS + if transform_name_to_id(c["name"], lower=False) not in [other_cluster_id1, other_cluster_id2] + ] + assert res.json() == expected + + # =========================== + # THERMAL CLUSTER ERRORS + # =========================== + + # Check DELETE with the wrong value of `area_id` + bad_area_id = "bad_area" + res = client.request( + "DELETE", + f"/v1/studies/{study_id}/areas/{bad_area_id}/clusters/thermal", + headers={"Authorization": f"Bearer {user_access_token}"}, + json=[fr_gas_conventional_id], + ) + assert res.status_code == 500, res.json() + obj = res.json() + description = obj["description"] + assert bad_area_id in description + assert re.search( + re.escape("Area 'bad_area' does not exist"), + description, + flags=re.IGNORECASE, + ) + + # Check DELETE with the wrong value of `study_id` + bad_study_id = "bad_study" + res = client.request( + "DELETE", + f"/v1/studies/{bad_study_id}/areas/{area_id}/clusters/thermal", + headers={"Authorization": f"Bearer {user_access_token}"}, + json=[fr_gas_conventional_id], + ) + obj = res.json() + description = obj["description"] + assert res.status_code == 404, res.json() + assert bad_study_id in description + + # Check GET with wrong `area_id` + res = client.get( + f"/v1/studies/{study_id}/areas/{bad_area_id}/clusters/thermal/{fr_gas_conventional_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + obj = res.json() + description = obj["description"] + assert bad_area_id in description + assert res.status_code == 404, res.json() + + # Check GET with wrong `study_id` + res = client.get( + f"/v1/studies/{bad_study_id}/areas/{area_id}/clusters/thermal/{fr_gas_conventional_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + ) + obj = res.json() + description = obj["description"] + assert res.status_code == 404, res.json() + assert bad_study_id in description + + # Check POST with wrong `study_id` + res = client.post( + f"/v1/studies/{bad_study_id}/areas/{area_id}/clusters/thermal", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={"name": fr_gas_conventional, "group": "Battery"}, + ) + obj = res.json() + description = obj["description"] + assert res.status_code == 404, res.json() + assert bad_study_id in description + + # Check POST with wrong `area_id` + res = client.post( + f"/v1/studies/{study_id}/areas/{bad_area_id}/clusters/thermal", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={ + "name": fr_gas_conventional, + "group": "Oil", + "unitCount": 1, + "nominalCapacity": 120.0, + "minStablePower": 60.0, + "co2": 0.77, + "marginalCost": 186.664, + "startupCost": 4800.0, + "marketBidCost": 186.664, + }, + ) + assert res.status_code == 500, res.json() + obj = res.json() + description = obj["description"] + assert bad_area_id in description + assert re.search(r"Area ", description, flags=re.IGNORECASE) + assert re.search(r"does not exist ", description, flags=re.IGNORECASE) + + # Check POST with wrong `group` + res = client.post( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/thermal", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={"name": fr_gas_conventional, "group": "GroupFoo"}, + ) + assert res.status_code == 200, res.json() + obj = res.json() + # If a group is not found, return the default group ('OTHER1' by default). + assert obj["group"] == "Other 1" + + # Check PATCH with the wrong `area_id` + res = client.patch( + f"/v1/studies/{study_id}/areas/{bad_area_id}/clusters/thermal/{fr_gas_conventional_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={ + "group": "Oil", + "unitCount": 1, + "nominalCapacity": 120.0, + "minStablePower": 60.0, + "co2": 0.77, + "marginalCost": 186.664, + "startupCost": 4800.0, + "marketBidCost": 186.664, + }, + ) + assert res.status_code == 404, res.json() + obj = res.json() + description = obj["description"] + assert bad_area_id in description + assert re.search(r"not a child of ", description, flags=re.IGNORECASE) + + # Check PATCH with the wrong `cluster_id` + bad_cluster_id = "bad_cluster" + res = client.patch( + f"/v1/studies/{study_id}/areas/{area_id}/clusters/thermal/{bad_cluster_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={ + "group": "Oil", + "unitCount": 1, + "nominalCapacity": 120.0, + "minStablePower": 60.0, + "co2": 0.77, + "marginalCost": 186.664, + "startupCost": 4800.0, + "marketBidCost": 186.664, + }, + ) + assert res.status_code == 404, res.json() + obj = res.json() + description = obj["description"] + assert bad_cluster_id in description + assert re.search(re.escape("'bad_cluster' not found"), description, flags=re.IGNORECASE) + + # Check PATCH with the wrong `study_id` + res = client.patch( + f"/v1/studies/{bad_study_id}/areas/{area_id}/clusters/thermal/{fr_gas_conventional_id}", + headers={"Authorization": f"Bearer {user_access_token}"}, + json={ + "group": "Oil", + "unitCount": 1, + "nominalCapacity": 120.0, + "minStablePower": 60.0, + "co2": 0.77, + "marginalCost": 186.664, + "startupCost": 4800.0, + "marketBidCost": 186.664, + }, + ) + assert res.status_code == 404, res.json() + obj = res.json() + description = obj["description"] + assert bad_study_id in description diff --git a/tests/integration/test_apidoc.py b/tests/integration/test_apidoc.py new file mode 100644 index 0000000000..562f7ccb8e --- /dev/null +++ b/tests/integration/test_apidoc.py @@ -0,0 +1,11 @@ +from fastapi.openapi.utils import get_flat_models_from_routes +from fastapi.utils import get_model_definitions +from pydantic.schema import get_model_name_map +from starlette.testclient import TestClient + + +def test_apidoc(client: TestClient) -> None: + # Asserts that the apidoc can be loaded + flat_models = get_flat_models_from_routes(client.app.routes) + model_name_map = get_model_name_map(flat_models) + get_model_definitions(flat_models=flat_models, model_name_map=model_name_map) diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index b2cec0cae6..0938557239 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -10,7 +10,6 @@ from antarest.study.business.area_management import AreaType, LayerInfoDTO from antarest.study.business.areas.properties_management import AdequacyPatchMode from antarest.study.business.areas.renewable_management import TimeSeriesInterpretation -from antarest.study.business.areas.thermal_management import LawOption, TimeSeriesGenerationOption from antarest.study.business.general_management import Mode from antarest.study.business.optimization_management import ( SimplexOptimizationRange, @@ -26,6 +25,8 @@ TransmissionCapacity, ) from antarest.study.model import MatrixIndex, StudyDownloadLevelDTO +from antarest.study.storage.rawstudy.model.filesystem.config.renewable import RenewableClusterGroup +from antarest.study.storage.rawstudy.model.filesystem.config.thermal import LawOption, TimeSeriesGenerationOption from antarest.study.storage.variantstudy.model.command.common import CommandName from tests.integration.assets import ASSETS_DIR from tests.integration.utils import wait_for @@ -107,22 +108,6 @@ def test_main(client: TestClient, admin_access_token: str, study_id: str) -> Non assert res.status_code == 417 assert res.json()["description"] == "Not a year by year simulation" - # Set new comments - res = client.put( - f"/v1/studies/{study_id}/comments", - headers={"Authorization": f'Bearer {george_credentials["access_token"]}'}, - json={"comments": comments}, - ) - assert res.status_code == 204, res.json() - - # Get comments - res = client.get( - f"/v1/studies/{study_id}/comments", - headers={"Authorization": f'Bearer {george_credentials["access_token"]}'}, - ) - assert res.status_code == 200, res.json() - assert res.json() == comments - # study synthesis res = client.get( f"/v1/studies/{study_id}/synthesis", @@ -349,8 +334,20 @@ def test_main(client: TestClient, admin_access_token: str, study_id: str) -> Non headers={"Authorization": f'Bearer {fred_credentials["access_token"]}'}, ) job_info = res.json()[0] - assert job_info["id"] == job_id - assert job_info["owner_id"] == fred_id + assert job_info == { + "id": job_id, + "study_id": study_id, + "launcher": "local", + "launcher_params": ANY, + "status": "pending", + "creation_date": ANY, + "completion_date": None, + "msg": None, + "output_id": None, + "exit_code": None, + "solver_stats": None, + "owner": {"id": fred_id, "name": "Fred"}, + } # update metadata res = client.put( @@ -1749,97 +1746,53 @@ def test_area_management(client: TestClient, admin_access_token: str, study_id: # Renewable form - res_renewable_config = client.put( + res = client.put( f"/v1/studies/{study_id}/areas/area 1/clusters/renewable/cluster renewable 1/form", headers=admin_headers, json={ "name": "cluster renewable 1 renamed", - "tsInterpretation": TimeSeriesInterpretation.PRODUCTION_FACTOR.value, + "tsInterpretation": TimeSeriesInterpretation.PRODUCTION_FACTOR, "unitCount": 9, "enabled": False, "nominalCapacity": 3, }, ) - assert res_renewable_config.status_code == 200 + assert res.status_code == 200, res.json() - res_renewable_config = client.get( - f"/v1/studies/{study_id}/areas/area 1/clusters/renewable/cluster renewable 1/form", headers=admin_headers + res = client.get( + f"/v1/studies/{study_id}/areas/area 1/clusters/renewable/cluster renewable 1/form", + headers=admin_headers, ) - res_renewable_config_json = res_renewable_config.json() - - assert res_renewable_config_json == { - "group": "", - "tsInterpretation": TimeSeriesInterpretation.PRODUCTION_FACTOR.value, + expected = { + "enabled": False, + "group": RenewableClusterGroup.OTHER1, # Default group used when not specified. + "id": "cluster renewable 1", "name": "cluster renewable 1 renamed", + "nominalCapacity": 3.0, + "tsInterpretation": TimeSeriesInterpretation.PRODUCTION_FACTOR, "unitCount": 9, - "enabled": False, - "nominalCapacity": 3, } + assert res.status_code == 200, res.json() + assert res.json() == expected # Thermal form - res_thermal_config = client.put( - f"/v1/studies/{study_id}/areas/area 1/clusters/thermal/cluster 1/form", - headers=admin_headers, - json={ - "group": "Lignite", - "name": "cluster 1 renamed", - "unitCount": 3, - "enabled": False, - "nominalCapacity": 3, - "genTs": "use global parameter", - "minStablePower": 3, - "minUpTime": 3, - "minDownTime": 3, - "mustRun": False, - "spinning": 3, - "volatilityForced": 3, - "volatilityPlanned": 3, - "lawForced": "uniform", - "lawPlanned": "uniform", - "marginalCost": 3, - "spreadCost": 3, - "fixedCost": 3, - "startupCost": 3, - "marketBidCost": 3, - "co2": 3, - "so2": 2, - "nh3": 2, - "nox": 4, - "nmvoc": 5, - "pm25": 11.3, - "pm5": 7, - "pm10": 9, - "op1": 0.5, - "op2": 39, - "op3": 3, - "op4": 2.4, - "op5": 0, - }, - ) - assert res_thermal_config.status_code == 200 - - res_thermal_config = client.get( - f"/v1/studies/{study_id}/areas/area 1/clusters/thermal/cluster 1/form", headers=admin_headers - ) - res_thermal_config_json = res_thermal_config.json() - - assert res_thermal_config_json == { + obj = { "group": "Lignite", "name": "cluster 1 renamed", "unitCount": 3, "enabled": False, "nominalCapacity": 3, - "genTs": TimeSeriesGenerationOption.USE_GLOBAL_PARAMETER.value, + "genTs": "use global parameter", "minStablePower": 3, "minUpTime": 3, "minDownTime": 3, "mustRun": False, "spinning": 3, - "volatilityForced": 3, - "volatilityPlanned": 3, - "lawForced": LawOption.UNIFORM.value, - "lawPlanned": LawOption.UNIFORM.value, + "volatilityForced": 0.3, + "volatilityPlanned": 0.3, + "lawForced": "uniform", + "lawPlanned": "uniform", "marginalCost": 3, "spreadCost": 3, "fixedCost": 3, @@ -1859,6 +1812,21 @@ def test_area_management(client: TestClient, admin_access_token: str, study_id: "op4": 2.4, "op5": 0, } + res = client.put( + # This URL is deprecated, but we must check it for backward compatibility. + f"/v1/studies/{study_id}/areas/area 1/clusters/thermal/cluster 1/form", + headers=admin_headers, + json=obj, + ) + assert res.status_code == 200, res.json() + + res = client.get( + # This URL is deprecated, but we must check it for backward compatibility. + f"/v1/studies/{study_id}/areas/area 1/clusters/thermal/cluster 1/form", + headers=admin_headers, + ) + assert res.status_code == 200, res.json() + assert res.json() == {"id": "cluster 1", **obj} # Links @@ -2228,12 +2196,13 @@ def test_binding_constraint_manager(client: TestClient, admin_access_token: str, def test_import(client: TestClient, admin_access_token: str, study_id: str) -> None: admin_headers = {"Authorization": f"Bearer {admin_access_token}"} - study_path = ASSETS_DIR / "STA-mini.zip" + zip_path = ASSETS_DIR / "STA-mini.zip" + seven_zip_path = ASSETS_DIR / "STA-mini.7z" # Admin who belongs to a group imports a study uuid = client.post( "/v1/studies/_import", - files={"study": io.BytesIO(study_path.read_bytes())}, + files={"study": io.BytesIO(zip_path.read_bytes())}, headers=admin_headers, ).json() res = client.get(f"v1/studies/{uuid}", headers=admin_headers).json() @@ -2253,13 +2222,69 @@ def test_import(client: TestClient, admin_access_token: str, study_id: str) -> N georges_headers = {"Authorization": f'Bearer {george_credentials["access_token"]}'} uuid = client.post( "/v1/studies/_import", - files={"study": io.BytesIO(study_path.read_bytes())}, + files={"study": io.BytesIO(zip_path.read_bytes())}, headers=georges_headers, ).json() res = client.get(f"v1/studies/{uuid}", headers=georges_headers).json() assert res["groups"] == [] assert res["public_mode"] == PublicMode.READ + # Study importer works for 7z files + res = client.post( + "/v1/studies/_import", + files={"study": io.BytesIO(seven_zip_path.read_bytes())}, + headers=admin_headers, + ) + assert res.status_code == 201 + + # tests outputs import for .zip + output_path_zip = ASSETS_DIR / "output_adq.zip" + client.post( + f"/v1/studies/{study_id}/output", + headers={"Authorization": f'Bearer {george_credentials["access_token"]}'}, + files={"output": io.BytesIO(output_path_zip.read_bytes())}, + ) + res = client.get( + f"/v1/studies/{study_id}/outputs", + headers={"Authorization": f'Bearer {george_credentials["access_token"]}'}, + ) + assert len(res.json()) == 6 + + # tests outputs import for .7z + output_path_seven_zip = ASSETS_DIR / "output_adq.7z" + client.post( + f"/v1/studies/{study_id}/output", + headers={"Authorization": f'Bearer {george_credentials["access_token"]}'}, + files={"output": io.BytesIO(output_path_seven_zip.read_bytes())}, + ) + res = client.get( + f"/v1/studies/{study_id}/outputs", + headers={"Authorization": f'Bearer {george_credentials["access_token"]}'}, + ) + assert len(res.json()) == 7 + + # test matrices import for .zip and .7z files + matrices_zip_path = ASSETS_DIR / "matrices.zip" + res_zip = client.post( + "/v1/matrix/_import", + headers={"Authorization": f'Bearer {george_credentials["access_token"]}'}, + files={"file": (matrices_zip_path.name, io.BytesIO(matrices_zip_path.read_bytes()), "application/zip")}, + ) + matrices_seven_zip_path = ASSETS_DIR / "matrices.7z" + res_seven_zip = client.post( + "/v1/matrix/_import", + headers={"Authorization": f'Bearer {george_credentials["access_token"]}'}, + files={ + "file": (matrices_seven_zip_path.name, io.BytesIO(matrices_seven_zip_path.read_bytes()), "application/zip") + }, + ) + for res in [res_zip, res_seven_zip]: + assert res.status_code == 200 + result = res.json() + assert len(result) == 2 + assert result[0]["name"] == "fr.txt" + assert result[1]["name"] == "it.txt" + def test_copy(client: TestClient, admin_access_token: str, study_id: str) -> None: admin_headers = {"Authorization": f"Bearer {admin_access_token}"} diff --git a/tests/integration/test_integration_token_end_to_end.py b/tests/integration/test_integration_token_end_to_end.py new file mode 100644 index 0000000000..bdab6a036d --- /dev/null +++ b/tests/integration/test_integration_token_end_to_end.py @@ -0,0 +1,219 @@ +import io +import typing as t +from unittest.mock import ANY + +import numpy as np +from starlette.testclient import TestClient + +from antarest.core.tasks.model import TaskDTO, TaskStatus +from antarest.launcher.model import JobResultDTO, JobStatus +from tests.integration.assets import ASSETS_DIR +from tests.integration.utils import wait_for + + +class CommandDict(t.TypedDict): + action: str + args: t.Dict[str, t.Any] + + +def test_nominal_case_of_an_api_user(client: TestClient, admin_access_token: str) -> None: + """ + Test the nominal case for an API user, which includes creating a **bot**, importing a study, + creating a variant, editing study configurations, creating thermal clusters, generating a variant, + running a simulation, and performing various other operations. + The test checks the success and status codes of the API endpoints. + """ + study_path = ASSETS_DIR / "STA-mini.zip" + + # create a bot + res = client.post( + "/v1/bots", + headers={"Authorization": f"Bearer {admin_access_token}"}, + json={"name": "admin_bot", "roles": [{"group": "admin", "role": 40}], "is_author": False}, + ) + bot_headers = {"Authorization": f"Bearer {res.json()}"} + + # import a study + res = client.post( + "/v1/studies/_import", + files={"study": io.BytesIO(study_path.read_bytes())}, + headers=bot_headers, + ) + study_id = res.json() + + # create a variant from it + res = client.post(f"/v1/studies/{study_id}/variants", headers=bot_headers, params={"name": "foo"}) + variant_id = res.json() + + # get the first area id of the study + res = client.get(f"/v1/studies/{variant_id}/areas", headers=bot_headers) + area_id = res.json()[0]["id"] + + # edit an area (for instance its geographic trimming attribute) + res = client.put( + f"/v1/studies/{variant_id}/config/general/form", + headers=bot_headers, + json={"geographicTrimming": True}, + ) + assert res.status_code == 200 + commands: t.List[CommandDict] + commands = [ + { + "action": "update_config", + "args": { + "target": f"input/areas/{area_id}/optimization/filtering/filter_synthesis", + "data": "annual", + }, + } + ] + res = client.post(f"/v1/studies/{variant_id}/commands", headers=bot_headers, json=commands) + assert res.status_code == 200 + + # modify its playlist (to do so, set its mcYears to more than the biggest year of the playlist) + res = client.put(f"/v1/studies/{variant_id}/config/general/form", headers=bot_headers, json={"nbYears": 10}) + assert res.status_code == 200 + res = client.put(f"/v1/studies/{variant_id}/config/playlist", headers=bot_headers, json={"playlist": [1, 4, 7]}) + assert res.status_code == 200 + + # create a first simple thermal cluster + commands = [ + { + "action": "create_cluster", + "args": { + "area_id": area_id, + "cluster_name": "mycluster", + "parameters": { + "group": "Gas", + "unitCount": 1, + "marginal_cost": 50, + }, + }, + } + ] + res = client.post(f"/v1/studies/{variant_id}/commands", headers=bot_headers, json=commands) + assert res.status_code == 200 + + # create a second thermal cluster with a lot of arguments + cluster_id = "new_cluster" + commands = [ + { + "action": "create_cluster", + "args": { + "area_id": area_id, + "cluster_name": cluster_id, + "parameters": { + "group": "Gas", + "marginal-cost": 98, + "unitCount": 1, + "nominalCapacity": 250, + "minStablePower": 0.0, + "minUpTime": 2, + "minDownTime": 2, + "spinning": 5, + "spreadCost": 0.0, + "startupCost": 2500, + "marketBidCost": 85, + "co2": 0.3, + }, + }, + } + ] + res = client.post(f"/v1/studies/{variant_id}/commands", headers=bot_headers, json=commands) + assert res.status_code == 200 + # add time_series matrix + command_matrix = [ + { + "action": "replace_matrix", + "args": { + "target": f"input/thermal/series/{area_id}/{cluster_id}/series", + "matrix": np.zeros((8760, 3), dtype=np.float64).tolist(), + }, + } + ] + res = client.post(f"/v1/studies/{variant_id}/commands", headers=bot_headers, json=command_matrix) + assert res.status_code == 200 + # add prepro data matrix + command_matrix[0]["args"]["target"] = f"input/thermal/prepro/{area_id}/{cluster_id}/data" + data_matrix = np.zeros((365, 6), dtype=np.float64) + data_matrix[:, 2:6] = 1 + command_matrix[0]["args"]["matrix"] = data_matrix.tolist() + res = client.post(f"/v1/studies/{variant_id}/commands", headers=bot_headers, json=command_matrix) + assert res.status_code == 200 + # add prepro modulation matrix + command_matrix[0]["args"]["target"] = f"input/thermal/prepro/{area_id}/{cluster_id}/modulation" + modulation_matrix = np.ones((8760, 4), dtype=np.float64) + modulation_matrix[:, 3] = 0 + command_matrix[0]["args"]["matrix"] = modulation_matrix.tolist() + res = client.post(f"/v1/studies/{variant_id}/commands", headers=bot_headers, json=command_matrix) + assert res.status_code == 200 + + # edit existing cluster with only one argument + # noinspection SpellCheckingInspection + commands = [ + { + "action": "update_config", + "args": { + "target": f"input/thermal/clusters/{area_id}/list/{cluster_id}/nominalcapacity", + "data": 300, + }, + } + ] + res = client.post(f"/v1/studies/{variant_id}/commands", headers=bot_headers, json=commands) + assert res.status_code == 200 + + # generate variant before running a simulation + res = client.put(f"/v1/studies/{variant_id}/generate", headers=bot_headers) + assert res.status_code == 200 + res = client.get( + f"/v1/tasks/{res.json()}", + headers=bot_headers, + params={"wait_for_completion": True}, + ) + assert res.status_code == 200 + task_result = TaskDTO(**res.json()) + assert task_result.status == TaskStatus.COMPLETED + assert task_result.result is not None + assert task_result.result.success + + # run the simulation + launcher_options = {"nb_cpu": 18, "auto_unzip": True, "output_suffix": "launched_by_bot"} + res = client.post(f"/v1/launcher/run/{variant_id}", json=launcher_options, headers=bot_headers) + job_id = res.json()["job_id"] + + # note: this list is used to collect job result for debugging purposes + job_results: t.List[JobResultDTO] = [] + + def wait_unit_finished() -> bool: + res_ = client.get(f"/v1/launcher/jobs/{job_id}", headers=bot_headers) + job_result_ = JobResultDTO(**res_.json()) + job_results.append(job_result_) + return job_result_.status in {JobStatus.SUCCESS, JobStatus.FAILED} + + wait_for(wait_unit_finished) + + # The launching is simulated, and no output is generated, so we expect to have a failure + # since the job will not be able to get the outputs. + assert job_results[-1].status == JobStatus.FAILED + + # read a result + res = client.get(f"/v1/studies/{study_id}/outputs", headers=bot_headers) + assert len(res.json()) == 5 + first_output_name = res.json()[0]["name"] + res = client.get( + f"/v1/studies/{study_id}/raw", + headers=bot_headers, + params={ + "path": f"output/{first_output_name}/economy/mc-all/areas/{area_id}/details-monthly", + "depth": 3, + }, + ) + assert res.json() == {"index": ANY, "columns": ANY, "data": ANY} + + # remove output + client.delete(f"/v1/studies/{study_id}/outputs/{first_output_name}", headers=bot_headers) + res = client.get(f"/v1/studies/{study_id}/outputs", headers=bot_headers) + assert len(res.json()) == 4 + + # delete variant + res = client.delete(f"/v1/studies/{variant_id}", headers=bot_headers) + assert res.status_code == 200 diff --git a/tests/integration/test_integration_variantmanager_tool.py b/tests/integration/test_integration_variantmanager_tool.py index a247d81c1d..0c53a03ffb 100644 --- a/tests/integration/test_integration_variantmanager_tool.py +++ b/tests/integration/test_integration_variantmanager_tool.py @@ -9,7 +9,7 @@ from fastapi import FastAPI from starlette.testclient import TestClient -from antarest.study.storage.rawstudy.io.reader import IniReader +from antarest.study.storage.rawstudy.io.reader import IniReader, MultipleSameKeysIniReader from antarest.study.storage.variantstudy.model.command.common import CommandName from antarest.study.storage.variantstudy.model.model import CommandDTO, GenerationResultInfoDTO from antarest.tools.lib import ( @@ -177,6 +177,10 @@ def test_parse_commands(tmp_path: str, app: FastAPI) -> None: assert (generated_study_path / item_relpath).read_text() == fixed_4_columns_empty_data elif item_relpath in fixed_8_cols_empty_items: assert (generated_study_path / item_relpath).read_text() == fixed_8_columns_empty_data + elif file_path.suffix == ".ini": + actual = MultipleSameKeysIniReader().read(study_path / item_relpath) + expected = MultipleSameKeysIniReader().read(generated_study_path / item_relpath) + assert actual == expected, f"Invalid configuration: '{item_relpath}'" else: actual = (study_path / item_relpath).read_text() expected = (generated_study_path / item_relpath).read_text() @@ -214,6 +218,11 @@ def test_diff_local(tmp_path: Path) -> None: if file_path.is_dir() or file_path.name in ["comments.txt", "study.antares", "Desktop.ini", "study.ico"]: continue item_relpath = file_path.relative_to(variant_study_path).as_posix() - actual = (variant_study_path / item_relpath).read_text() - expected = (output_study_path / item_relpath).read_text() - assert actual.strip() == expected.strip() + if file_path.suffix == ".ini": + actual = MultipleSameKeysIniReader().read(variant_study_path / item_relpath) + expected = MultipleSameKeysIniReader().read(output_study_path / item_relpath) + assert actual == expected, f"Invalid configuration: '{item_relpath}'" + else: + actual = (variant_study_path / item_relpath).read_text() + expected = (output_study_path / item_relpath).read_text() + assert actual.strip() == expected.strip() diff --git a/tests/integration/test_integration_xpansion.py b/tests/integration/test_integration_xpansion.py index 0aa0579734..fa4de2e0af 100644 --- a/tests/integration/test_integration_xpansion.py +++ b/tests/integration/test_integration_xpansion.py @@ -85,19 +85,20 @@ def test_integration_xpansion(client: TestClient, tmp_path: Path, admin_access_t "ampl.presolve": None, "ampl.solve_bounds_frequency": None, "ampl.solver": None, + "batch_size": 0, "cut-type": None, + "log_level": 0, "master": "integer", "max_iteration": "+Inf", "optimality_gap": 1.0, "relative_gap": 1e-12, "relaxed-optimality-gap": None, + "sensitivity_config": {"capex": False, "epsilon": 10000.0, "projection": []}, + "separation_parameter": 0.5, "solver": "Cbc", - "batch_size": 0, + "timelimit": 1e12, "uc_type": "expansion_fast", "yearly-weights": None, - "timelimit": 1e12, - "log_level": 0, - "sensitivity_config": None, } res = client.put( @@ -111,21 +112,21 @@ def test_integration_xpansion(client: TestClient, tmp_path: Path, admin_access_t "ampl.presolve": None, "ampl.solve_bounds_frequency": None, "ampl.solver": None, + "batch_size": 0, "cut-type": None, + "log_level": 0, "master": "integer", "max_iteration": "+Inf", "optimality_gap": 42.0, "relative_gap": None, "relaxed-optimality-gap": None, + "sensitivity_config": None, + "separation_parameter": 0.5, "solver": None, - "batch_size": 0, + "timelimit": 1000000000000, "uc_type": "expansion_fast", "yearly-weights": None, - "timelimit": 1e12, - "log_level": 0, - "sensitivity_config": None, } - res = client.put( f"{xpansion_base_url}/settings", headers=headers, diff --git a/tests/integration/variant_blueprint/test_renewable_cluster.py b/tests/integration/variant_blueprint/test_renewable_cluster.py index 6cae45424a..bdefbb402f 100644 --- a/tests/integration/variant_blueprint/test_renewable_cluster.py +++ b/tests/integration/variant_blueprint/test_renewable_cluster.py @@ -46,7 +46,7 @@ def test_lifecycle( area_fr_id = transform_name_to_id("FR") cluster_fr1 = "Oleron" - cluster_fr1_id = transform_name_to_id(cluster_fr1) + cluster_fr1_id = transform_name_to_id(cluster_fr1, lower=False) args = { "area_id": area_fr_id, "cluster_name": cluster_fr1_id, @@ -65,7 +65,7 @@ def test_lifecycle( res.raise_for_status() cluster_fr2 = "La_Rochelle" - cluster_fr2_id = transform_name_to_id(cluster_fr2) + cluster_fr2_id = transform_name_to_id(cluster_fr2, lower=False) args = { "area_id": area_fr_id, "cluster_name": cluster_fr2_id, @@ -87,15 +87,16 @@ def test_lifecycle( # Check the properties of the renewable clusters in the "FR" area res = client.get( - f"/v1/studies/{study_id}/areas/{area_fr_id}/clusters/renewable/{cluster_fr1_id}/form", + f"/v1/studies/{study_id}/areas/{area_fr_id}/clusters/renewable/{cluster_fr1_id}", headers={"Authorization": f"Bearer {user_access_token}"}, ) res.raise_for_status() properties = res.json() expected = { "enabled": True, - "group": "wind offshore", - "name": cluster_fr1_id, # known bug: should be `cluster_fr1` + "group": "Wind Offshore", + "id": "Oleron", + "name": cluster_fr1, "nominalCapacity": 2500.0, "tsInterpretation": "power-generation", "unitCount": 1, @@ -103,15 +104,16 @@ def test_lifecycle( assert properties == expected res = client.get( - f"/v1/studies/{study_id}/areas/{area_fr_id}/clusters/renewable/{cluster_fr2_id}/form", + f"/v1/studies/{study_id}/areas/{area_fr_id}/clusters/renewable/{cluster_fr2_id}", headers={"Authorization": f"Bearer {user_access_token}"}, ) res.raise_for_status() properties = res.json() expected = { "enabled": False, - "group": "solar pv", - "name": cluster_fr2_id, # known bug: should be `cluster_fr2` + "group": "Solar PV", + "id": "La_Rochelle", + "name": cluster_fr2, "nominalCapacity": 3500.0, "tsInterpretation": "power-generation", "unitCount": 4, @@ -124,13 +126,15 @@ def test_lifecycle( # Then, it is possible to update a time series. values_fr1 = np.random.randint(0, 1001, size=(8760, 1)) + series_fr1_id = cluster_fr1_id.lower() # Series IDs are in lower case + series_fr2_id = cluster_fr2_id.lower() # Series IDs are in lower case args_fr1 = { - "target": f"input/renewables/series/{area_fr_id}/{cluster_fr1_id}/series", + "target": f"input/renewables/series/{area_fr_id}/{series_fr1_id}/series", "matrix": values_fr1.tolist(), } values_fr2 = np.random.randint(0, 1001, size=(8760, 1)) args_fr2 = { - "target": f"input/renewables/series/{area_fr_id}/{cluster_fr2_id}/series", + "target": f"input/renewables/series/{area_fr_id}/{series_fr2_id}/series", "matrix": values_fr2.tolist(), } res = client.post( @@ -145,7 +149,7 @@ def test_lifecycle( # Check the matrices of the renewable clusters in the "FR" area res = client.get( - f"/v1/studies/{study_id}/raw?path=input/renewables/series/{area_fr_id}/{cluster_fr1_id}/series", + f"/v1/studies/{study_id}/raw?path=input/renewables/series/{area_fr_id}/{series_fr1_id}/series", headers={"Authorization": f"Bearer {user_access_token}"}, ) res.raise_for_status() @@ -153,7 +157,7 @@ def test_lifecycle( assert np.array(matrix_fr1["data"], dtype=np.float64).all() == values_fr1.all() res = client.get( - f"/v1/studies/{study_id}/raw?path=input/renewables/series/{area_fr_id}/{cluster_fr2_id}/series", + f"/v1/studies/{study_id}/raw?path=input/renewables/series/{area_fr_id}/{series_fr2_id}/series", headers={"Authorization": f"Bearer {user_access_token}"}, ) res.raise_for_status() @@ -167,7 +171,7 @@ def test_lifecycle( area_it_id = transform_name_to_id("IT") cluster_it1 = "Oléron" - cluster_it1_id = transform_name_to_id(cluster_it1) + cluster_it1_id = transform_name_to_id(cluster_it1, lower=False) args = { "area_id": area_it_id, "cluster_name": cluster_it1_id, @@ -187,15 +191,16 @@ def test_lifecycle( res.raise_for_status() res = client.get( - f"/v1/studies/{study_id}/areas/{area_it_id}/clusters/renewable/{cluster_it1_id}/form", + f"/v1/studies/{study_id}/areas/{area_it_id}/clusters/renewable/{cluster_it1_id}", headers={"Authorization": f"Bearer {user_access_token}"}, ) res.raise_for_status() properties = res.json() expected = { "enabled": True, - "group": "wind offshore", - "name": cluster_it1_id, # known bug: should be `cluster_it1` + "group": "Wind Offshore", + "id": "Ol ron", + "name": cluster_it1, "nominalCapacity": 1000.0, "tsInterpretation": "production-factor", "unitCount": 1, @@ -203,8 +208,9 @@ def test_lifecycle( assert properties == expected # Check the matrices of the renewable clusters in the "IT" area + series_it1_id = cluster_it1_id.lower() # Series IDs are in lower case res = client.get( - f"/v1/studies/{study_id}/raw?path=input/renewables/series/{area_it_id}/{cluster_it1_id}/series", + f"/v1/studies/{study_id}/raw?path=input/renewables/series/{area_it_id}/{series_it1_id}/series", headers={"Authorization": f"Bearer {user_access_token}"}, ) res.raise_for_status() @@ -238,7 +244,7 @@ def test_lifecycle( "list": { cluster_fr1_id: { "group": "wind offshore", - "name": cluster_fr1_id, + "name": cluster_fr1, "nominalcapacity": 2500, "ts-interpretation": "power-generation", }, @@ -248,7 +254,7 @@ def test_lifecycle( "list": { cluster_it1_id: { "group": "wind offshore", - "name": cluster_it1_id, + "name": cluster_it1, "nominalcapacity": 1000, "ts-interpretation": "production-factor", "unitcount": 1, @@ -281,7 +287,7 @@ def test_lifecycle( "list": { cluster_it1_id: { "group": "wind offshore", - "name": cluster_it1_id, + "name": cluster_it1, "nominalcapacity": 1000, "ts-interpretation": "production-factor", "unitcount": 1, diff --git a/tests/integration/variant_blueprint/test_st_storage.py b/tests/integration/variant_blueprint/test_st_storage.py index 722415de2a..c0f531ddb4 100644 --- a/tests/integration/variant_blueprint/test_st_storage.py +++ b/tests/integration/variant_blueprint/test_st_storage.py @@ -159,7 +159,7 @@ def test_lifecycle( "withdrawalnominalcapacity": 1800, "reservoircapacity": 20000, "efficiency": 0.78, - "initiallevel": 10000, + "initiallevel": 0.91, }, "pmax_injection": pmax_injection.tolist(), "pmax_withdrawal": pmax_withdrawal.tolist(), @@ -207,7 +207,7 @@ def test_lifecycle( "withdrawal_nominal_capacity": 1500, "reservoir_capacity": 20000, "efficiency": 0.78, - "initial_level": 10000, + "initial_level": 0.91, "initial_level_optim": "BlurBool", # Oops! }, "pmax_injection": pmax_injection.tolist(), diff --git a/tests/launcher/test_model.py b/tests/launcher/test_model.py index 3f05dcc314..fe7ccead1e 100644 --- a/tests/launcher/test_model.py +++ b/tests/launcher/test_model.py @@ -1,4 +1,5 @@ import re +import typing as t import uuid from sqlalchemy.orm.session import Session # type: ignore @@ -43,7 +44,7 @@ def test_create(self, db_session: Session) -> None: db.commit() with db_session as db: - jr = db.query(JobResult).one() + jr: JobResult = db.query(JobResult).one() assert jr.id == job_result_id assert jr.study_id == study_id assert jr.launcher is None @@ -76,9 +77,16 @@ def test_create_with_owner(self, db_session: Session) -> None: job_result_id = job_result.id with db_session as db: - jr = db.get(JobResult, job_result_id) + jr: t.Optional[JobResult] = db.get(JobResult, job_result_id) + assert jr is not None assert jr.owner_id == owner_id + # Check the relationships between `JobResult` and `Identity` + all_users: t.Sequence[Identity] = db.query(Identity).all() + assert len(all_users) == 1 + assert all_users[0].job_results == [jr] + assert jr.owner == all_users[0] + def test_update_with_owner(self, db_session: Session) -> None: """ Test the update of a `JobResult` instance with an owner in the database. @@ -99,12 +107,14 @@ def test_update_with_owner(self, db_session: Session) -> None: with db_session as db: # Update the job result with the owner + user: t.Optional[Identity] = db.get(Identity, owner_id) job_result = db.get(JobResult, job_result_id) - job_result.owner_id = owner_id + job_result.owner = user db.commit() with db_session as db: - jr = db.get(JobResult, job_result_id) + jr: t.Optional[JobResult] = db.get(JobResult, job_result_id) + assert jr is not None assert jr.owner_id == owner_id def test_delete_with_owner(self, db_session: Session) -> None: @@ -112,24 +122,23 @@ def test_delete_with_owner(self, db_session: Session) -> None: Test the deletion of an owner and check if the associated `JobResult`'s `owner_id` is set to None. """ with db_session as db: - identity = Identity() - db.add(identity) - db.commit() - owner_id = identity.id - - job_result = JobResult(id=str(uuid.uuid4()), owner_id=owner_id) + owner = Identity() + job_result = JobResult(id=str(uuid.uuid4()), owner=owner) db.add(job_result) db.commit() + owner_id = owner.id job_result_id = job_result.id with db_session as db: - identity = db.get(Identity, owner_id) + identity: t.Optional[Identity] = db.get(Identity, owner_id) + assert identity is not None db.delete(identity) db.commit() with db_session as db: # check `ondelete="SET NULL"` - jr = db.get(JobResult, job_result_id) + jr: t.Optional[JobResult] = db.get(JobResult, job_result_id) + assert jr is not None assert jr.owner_id is None @@ -161,7 +170,7 @@ def test_create(self, db_session: Session) -> None: db.commit() with db_session as db: - jl = db.query(JobLog).one() + jl: JobLog = db.query(JobLog).one() assert jl.id == 1 assert jl.message == "Log message" assert jl.job_id == job_result_id @@ -197,11 +206,12 @@ def test_delete_job_result(self, db_session: Session) -> None: job_log_id = job_log.id with db_session as db: - jr = db.get(JobResult, job_result_id) + jr: t.Optional[JobResult] = db.get(JobResult, job_result_id) + assert jr is not None db.delete(jr) db.commit() with db_session as db: # check `cascade="all, delete, delete-orphan"` - jl = db.get(JobLog, job_log_id) + jl: t.Optional[JobLog] = db.get(JobLog, job_log_id) assert jl is None diff --git a/tests/login/conftest.py b/tests/login/conftest.py new file mode 100644 index 0000000000..7b7935d6d3 --- /dev/null +++ b/tests/login/conftest.py @@ -0,0 +1,90 @@ +import pytest + +from antarest.core.config import Config +from antarest.core.interfaces.eventbus import IEventBus +from antarest.core.utils.fastapi_sqlalchemy import DBSessionMiddleware +from antarest.login.ldap import LdapService +from antarest.login.repository import BotRepository, GroupRepository, RoleRepository, UserLdapRepository, UserRepository +from antarest.login.service import LoginService + + +# noinspection PyUnusedLocal +@pytest.fixture(name="group_repo") +def group_repo_fixture(db_middleware: DBSessionMiddleware) -> GroupRepository: + """Fixture that creates a GroupRepository instance.""" + # note: `DBSessionMiddleware` is required to instantiate a thread-local db session. + # important: the `GroupRepository` insert an admin group in the database if it does not exist: + # >>> Group(id="admin", name="admin") + return GroupRepository() + + +# noinspection PyUnusedLocal +@pytest.fixture(name="user_repo") +def user_repo_fixture(core_config: Config, db_middleware: DBSessionMiddleware) -> UserRepository: + """Fixture that creates a UserRepository instance.""" + # note: `DBSessionMiddleware` is required to instantiate a thread-local db session. + # important: the `UserRepository` insert an admin user in the database if it does not exist. + # >>> User(id=1, name="admin", password=Password(config.security.admin_pwd)) + return UserRepository(config=core_config) + + +# noinspection PyUnusedLocal +@pytest.fixture(name="user_ldap_repo") +def user_ldap_repo_fixture(db_middleware: DBSessionMiddleware) -> UserLdapRepository: + """Fixture that creates a UserLdapRepository instance.""" + # note: `DBSessionMiddleware` is required to instantiate a thread-local db session. + return UserLdapRepository() + + +# noinspection PyUnusedLocal +@pytest.fixture(name="bot_repo") +def bot_repo_fixture(db_middleware: DBSessionMiddleware) -> BotRepository: + """Fixture that creates a BotRepository instance.""" + # note: `DBSessionMiddleware` is required to instantiate a thread-local db session. + return BotRepository() + + +# noinspection PyUnusedLocal +@pytest.fixture(name="role_repo") +def role_repo_fixture(db_middleware: DBSessionMiddleware) -> RoleRepository: + """Fixture that creates a RoleRepository instance.""" + # note: `DBSessionMiddleware` is required to instantiate a thread-local db session. + # important: the `RoleRepository` insert an admin role in the database if it does not exist. + # >>> Role(type=RoleType.ADMIN, identity=User(id=1), group=Group(id="admin")) + return RoleRepository() + + +@pytest.fixture(name="ldap_service") +def ldap_service_fixture( + core_config: Config, + user_ldap_repo: UserLdapRepository, + group_repo: GroupRepository, + role_repo: RoleRepository, +) -> LdapService: + """Fixture that creates a LdapService instance.""" + return LdapService( + config=core_config, + users=user_ldap_repo, + groups=group_repo, + roles=role_repo, + ) + + +@pytest.fixture(name="login_service") +def login_service_fixture( + user_repo: UserRepository, + bot_repo: BotRepository, + group_repo: GroupRepository, + role_repo: RoleRepository, + ldap_service: LdapService, + event_bus: IEventBus, +) -> LoginService: + """Fixture that creates a LoginService instance.""" + return LoginService( + user_repo=user_repo, + bot_repo=bot_repo, + group_repo=group_repo, + role_repo=role_repo, + ldap=ldap_service, + event_bus=event_bus, + ) diff --git a/tests/login/test_ldap.py b/tests/login/test_ldap.py index e32d8c35c1..bb23cd86c2 100644 --- a/tests/login/test_ldap.py +++ b/tests/login/test_ldap.py @@ -1,110 +1,144 @@ import json import threading +import typing as t from http.server import BaseHTTPRequestHandler, HTTPServer -from unittest.mock import Mock, call + +from sqlalchemy import create_engine from antarest.core.config import Config, ExternalAuthConfig, SecurityConfig from antarest.core.roles import RoleType +from antarest.core.utils.fastapi_sqlalchemy import DBSessionMiddleware, db +from antarest.dbmodel import Base from antarest.login.ldap import AuthDTO, ExternalUser, LdapService -from antarest.login.model import Group, Role, UserLdap +from antarest.login.model import UserLdap +from antarest.login.repository import GroupRepository, RoleRepository, UserLdapRepository + +class MockHTTPRequestHandler(BaseHTTPRequestHandler): + """ + This HTTP request handler simulates an LDAP server. + """ -class MockServerHandler(BaseHTTPRequestHandler): + def log_request(self, code="-", size="-"): + """Override the log_request method to suppress access logs""" + + # noinspection PyPep8Naming def do_POST(self): + content_length = int(self.headers["Content-Length"]) + data = self.rfile.read(content_length) + if "/auth" in self.path: - content_length = int(self.headers["Content-Length"]) - data = self.rfile.read(content_length) create = AuthDTO.from_json(json.loads(data)) - antares = ExternalUser( - external_id=create.user, - first_name="John", - last_name="Smith", - groups={ - "groupA": "some group name", - "groupB": "some other group name", - "groupC": "isGroupD", - }, - ) - res = json.dumps(antares.to_json()) + if create.user == "ext_id": + # Simulate a known user + ext_user = ExternalUser( + external_id=create.user, + first_name="John", + last_name="Smith", + groups={ + "groupA": "some group name", + "groupB": "some other group name", + "groupC": "isGroupD", + }, + ) + res = json.dumps(ext_user.to_json(), ensure_ascii=False).encode("utf-8") + self.send_response(200) + + else: + # Simulate an unknown user + res = "null".encode("utf-8") + # response code is 401 (unauthorized) to simulate a wrong password + self.send_response(401) - self.send_response(200) + self.send_header("Content-type", "application/json; charset=utf-8") self.send_header("Content-Length", f"{len(res)}") self.end_headers() - self.wfile.write(res.encode()) - - -def test_ldap(): - config = Config( - security=SecurityConfig( - external_auth=ExternalAuthConfig( - url="http://localhost:8869", - default_group_role=RoleType.WRITER, - add_ext_groups=True, - group_mapping={"groupC": "D"}, - ) - ) - ) - repo = Mock() - repo.get_by_external_id.return_value = None - repo.save.side_effect = lambda x: x - group_repo = Mock() - role_repo = Mock() - ldap = LdapService(config=config, users=repo, groups=group_repo, roles=role_repo) - - # Start server - httpd = HTTPServer(("localhost", 8869), MockServerHandler) - server = threading.Thread(None, httpd.handle_request) - server.start() - - role_repo.get_all_by_user.return_value = [Role(group_id="groupA")] - group_repo.save.side_effect = lambda x: x - group_repo.get.side_effect = lambda x: Group(id="D", name="groupD") if x == "D" else None - role_repo.save.side_effect = lambda x: x - - res = ldap.login(name="extid", password="pwd") - - assert res - assert "John Smith" == res.name - assert "extid" == res.external_id - repo.save.assert_called_once_with( - UserLdap( - name="John Smith", - external_id="extid", - firstname="John", - lastname="Smith", + self.wfile.write(res) + + else: + res = "Not found: {self.path}".encode("utf-8") + self.send_response(404) + self.send_header("Content-type", "text/plain; charset=utf-8") + self.send_header("Content-Length", f"{len(res)}") + self.end_headers() + self.wfile.write(res) + + +class TestLdapService: + """ + Test the LDAP service + """ + + def test_login(self): + # Create an in-memory database for this test. + engine = create_engine("sqlite:///:memory:", echo=False) + Base.metadata.create_all(engine) + # noinspection SpellCheckingInspection + DBSessionMiddleware( + None, + custom_engine=engine, + session_args={"autocommit": False, "autoflush": False}, ) - ) - group_repo.save.assert_has_calls( - [ - call(Group(id="groupB", name="some other group name")), - call(Group(id="D", name="groupD")), - ] - ) - role_repo.save.assert_has_calls( - [ - call( - Role( - identity=UserLdap( - name="John Smith", - external_id="extid", - firstname="John", - lastname="Smith", - ), - group=Group(id="groupB", name="some other group name"), - type=RoleType.WRITER, - ) - ), - call( - Role( - identity=UserLdap( - name="John Smith", - external_id="extid", - firstname="John", - lastname="Smith", - ), - group=Group(id="D", name="groupD"), - type=RoleType.WRITER, + + # Start a mocked LDAP server in a dedicated thread. + server_address = ("", 8869) # port 8869 is the default port for LDAP + httpd = HTTPServer(server_address, MockHTTPRequestHandler) + server = threading.Thread(None, httpd.serve_forever, daemon=True) + server.start() + + try: + with db(): + ldap_repo = UserLdapRepository() + group_repo = GroupRepository() + role_repo = RoleRepository() + + config = Config( + security=SecurityConfig( + external_auth=ExternalAuthConfig( + url="http://localhost:8869", + default_group_role=RoleType.WRITER, + add_ext_groups=True, + group_mapping={"groupC": "D"}, + ) + ) ) - ), - ] - ) + + ldap_service = LdapService(config=config, users=ldap_repo, groups=group_repo, roles=role_repo) + + # An unknown user cannot log in + user: t.Optional[UserLdap] = ldap_service.login(name="unknown", password="pwd") + assert user is None + + # A known user can log in + user: t.Optional[UserLdap] = ldap_service.login(name="ext_id", password="pwd") + assert user + assert user.name == "John Smith" + assert user.external_id == "ext_id" + assert user.firstname == "John" + assert user.lastname == "Smith" + assert user.id + + # The user can be retrieved from the database + assert ldap_repo.get(user.id) == user + + # Check that groups have been created + roles = role_repo.get_all_by_user(user.id) + assert len(roles) == 3 + + # all roles have the same user and role type + assert all(r.identity == user for r in roles) + assert all(r.type == RoleType.WRITER for r in roles) + + # the groups are the ones defined in the LDAP server + groups = {r.group.id: r.group.name for r in roles} + assert groups == { + "groupA": "some group name", + "groupB": "some other group name", + "D": "isGroupD", # use the configured mapping + } + + finally: + # Stop the mocked LDAP server when the test is finished. + httpd.shutdown() + httpd.server_close() + server.join() diff --git a/tests/login/test_login_service.py b/tests/login/test_login_service.py new file mode 100644 index 0000000000..a56f256003 --- /dev/null +++ b/tests/login/test_login_service.py @@ -0,0 +1,997 @@ +import typing as t +from unittest.mock import Mock + +import pytest +from fastapi import HTTPException + +from antarest.core.jwt import JWTGroup, JWTUser +from antarest.core.requests import RequestParameters, UserHasNotPermissionError +from antarest.core.roles import RoleType +from antarest.login.model import ( + Bot, + BotCreateDTO, + BotRoleCreateDTO, + Group, + Password, + Role, + RoleCreationDTO, + User, + UserCreateDTO, + UserLdap, +) +from antarest.login.service import LoginService +from tests.helpers import with_db_context + +SITE_ADMIN = RequestParameters( + user=JWTUser( + id=0, + impersonator=0, + type="users", + groups=[JWTGroup(id="admin", name="admin", role=RoleType.ADMIN)], + ) +) + +GROUP_ADMIN = RequestParameters( + user=JWTUser( + id=1, + impersonator=1, + type="users", + groups=[JWTGroup(id="group", name="group", role=RoleType.ADMIN)], + ) +) + +USER3 = RequestParameters( + user=JWTUser( + id=3, + impersonator=3, + type="users", + groups=[JWTGroup(id="group", name="group", role=RoleType.READER)], + ) +) + +BAD_PARAM = RequestParameters(user=None) + + +class TestLoginService: + """ + Test login service. + + important: + + - the `GroupRepository` insert an admin group in the database if it does not exist: + `Group(id="admin", name="admin")` + + - the `UserRepository` insert an admin user in the database if it does not exist. + `User(id=1, name="admin", password=Password(config.security.admin_pwd))` + + - the `RoleRepository` insert an admin role in the database if it does not exist. + `Role(type=RoleType.ADMIN, identity=User(id=1), group=Group(id="admin"))` + """ + + @with_db_context + @pytest.mark.parametrize( + "param, can_create", [(SITE_ADMIN, True), (GROUP_ADMIN, True), (USER3, False), (BAD_PARAM, False)] + ) + def test_save_group(self, login_service: LoginService, param: RequestParameters, can_create: bool) -> None: + group = Group(id="group", name="group") + + # Only site admin and group admin can update a group + if can_create: + actual = login_service.save_group(group, param) + assert actual == group + else: + with pytest.raises(UserHasNotPermissionError): + login_service.save_group(group, param) + actual = login_service.groups.get(group.id) + assert actual is None + + # Users can't create a duplicate group + with pytest.raises(HTTPException): + login_service.save_group(group, param) + + @with_db_context + @pytest.mark.parametrize( + "param, can_create", [(SITE_ADMIN, True), (GROUP_ADMIN, False), (USER3, False), (BAD_PARAM, False)] + ) + def test_create_user(self, login_service: LoginService, param: RequestParameters, can_create: bool) -> None: + create = UserCreateDTO(name="hello", password="world") + + # Only site admin can create a user + if can_create: + actual = login_service.create_user(create, param) + assert actual.name == create.name + else: + with pytest.raises(UserHasNotPermissionError): + login_service.create_user(create, param) + actual = login_service.users.get_by_name(create.name) + assert actual is None + + # Users can't create a duplicate user + with pytest.raises(HTTPException): + login_service.create_user(create, param) + + @with_db_context + @pytest.mark.parametrize( + "param, can_save", [(SITE_ADMIN, True), (GROUP_ADMIN, False), (USER3, False), (BAD_PARAM, False)] + ) + def test_save_user(self, login_service: LoginService, param: RequestParameters, can_save: bool) -> None: + create = UserCreateDTO(name="Laurent", password="S3cr3t") + user = login_service.create_user(create, SITE_ADMIN) + user.name = "Roland" + + # Only site admin can update a user + if can_save: + login_service.save_user(user, param) + actual = login_service.users.get_by_name(user.name) + assert actual == user + else: + with pytest.raises(UserHasNotPermissionError): + login_service.save_user(user, param) + actual = login_service.users.get_by_name(user.name) + assert actual != user + + @with_db_context + def test_save_user__themselves(self, login_service: LoginService) -> None: + user_create = UserCreateDTO(name="Laurent", password="S3cr3t") + user = login_service.create_user(user_create, SITE_ADMIN) + + # users can update themselves + param = RequestParameters( + user=JWTUser( + id=user.id, + impersonator=user.id, + type="users", + groups=[JWTGroup(id="group", name="group", role=RoleType.READER)], + ) + ) + user.name = "Roland" + actual = login_service.save_user(user, param) + assert actual == user + + @with_db_context + def test_save_bot(self, login_service: LoginService) -> None: + # Prepare the user3 in the db + assert USER3.user is not None + user3 = User(id=USER3.user.id, name="Scoobydoo") + login_service.users.save(user3) + + # Prepare the user group and role + for jwt_group in USER3.user.groups: + group = Group(id=jwt_group.id, name=jwt_group.name) + login_service.groups.save(group) + role = Role(type=jwt_group.role, identity=user3, group=group) + login_service.roles.save(role) + + # Request parameters must reference a user + with pytest.raises(HTTPException): + login_service.save_bot(BotCreateDTO(name="bot", roles=[]), BAD_PARAM) + + # The user USER3 is a reader in the group "group" and can crate a bot with the same role + assert all(jwt_group.role == RoleType.READER for jwt_group in USER3.user.groups) + bot_create = BotCreateDTO(name="bot", roles=[BotRoleCreateDTO(group="group", role=RoleType.READER.value)]) + bot = login_service.save_bot(bot_create, USER3) + + assert bot.name == bot_create.name + assert bot.owner == USER3.user.id + assert bot.is_author is True + + # The user can't create a bot with an empty name + bot_create = BotCreateDTO(name="", roles=[BotRoleCreateDTO(group="group", role=RoleType.READER.value)]) + with pytest.raises(HTTPException): + login_service.save_bot(bot_create, USER3) + + # The user can't create a bot with a higher role than his own + for role_type in set(RoleType) - {RoleType.READER}: + bot_create = BotCreateDTO(name="bot", roles=[BotRoleCreateDTO(group="group", role=role_type.value)]) + with pytest.raises(UserHasNotPermissionError): + login_service.save_bot(bot_create, USER3) + + # The user can't create a bot that already exists + bot_create = BotCreateDTO(name="bot", roles=[BotRoleCreateDTO(group="group", role=RoleType.READER.value)]) + with pytest.raises(HTTPException): + login_service.save_bot(bot_create, USER3) + + # The user can't create a bot with a group that does not exist + bot_create = BotCreateDTO(name="bot", roles=[BotRoleCreateDTO(group="unknown", role=RoleType.READER.value)]) + with pytest.raises(HTTPException): + login_service.save_bot(bot_create, USER3) + + @with_db_context + @pytest.mark.parametrize( + "param, can_save", [(SITE_ADMIN, True), (GROUP_ADMIN, True), (USER3, False), (BAD_PARAM, False)] + ) + def test_save_role(self, login_service: LoginService, param: RequestParameters, can_save: bool) -> None: + # Prepare the site admin in the db + assert SITE_ADMIN.user is not None + admin = User(id=SITE_ADMIN.user.id, name="Superman") + login_service.users.save(admin) + + # Prepare the group "group" in the db + # noinspection SpellCheckingInspection + group = Group(id="group", name="Kryptonians") + login_service.groups.save(group) + + # Only site admin and group admin can update a role + role = RoleCreationDTO(type=RoleType.ADMIN, identity_id=0, group_id="group") + if can_save: + actual = login_service.save_role(role, param) + assert actual.type == RoleType.ADMIN + assert actual.identity == admin + else: + with pytest.raises(UserHasNotPermissionError): + login_service.save_role(role, param) + actual = login_service.roles.get_all_by_group(group.id) + assert len(actual) == 0 + + @with_db_context + @pytest.mark.parametrize( + "param, can_get", [(SITE_ADMIN, True), (GROUP_ADMIN, True), (USER3, True), (BAD_PARAM, False)] + ) + def test_get_group(self, login_service: LoginService, param: RequestParameters, can_get: bool) -> None: + # Prepare the group "group" in the db + # noinspection SpellCheckingInspection + group = Group(id="group", name="Vulcans") + login_service.groups.save(group) + + # Anybody except anonymous can get a group + if can_get: + actual = login_service.get_group("group", param) + assert actual == group + else: + with pytest.raises(UserHasNotPermissionError): + login_service.get_group(group.id, param) + + # noinspection SpellCheckingInspection + @with_db_context + @pytest.mark.parametrize( + "param, expected", + [ + ( + SITE_ADMIN, + { + "id": "group", + "name": "Vulcans", + "users": [ + {"id": 3, "name": "Spock", "role": RoleType.RUNNER}, + {"id": 4, "name": "Saavik", "role": RoleType.RUNNER}, + ], + }, + ), + ( + GROUP_ADMIN, + { + "id": "group", + "name": "Vulcans", + "users": [ + {"id": 3, "name": "Spock", "role": RoleType.RUNNER}, + {"id": 4, "name": "Saavik", "role": RoleType.RUNNER}, + ], + }, + ), + (USER3, {}), + (BAD_PARAM, {}), + ], + ) + def test_get_group_info( + self, + login_service: LoginService, + param: RequestParameters, + expected: t.Mapping[str, t.Any], + ) -> None: + # Prepare the group "group" in the db + # noinspection SpellCheckingInspection + group = Group(id="group", name="Vulcans") + login_service.groups.save(group) + + # Prepare the user3 in the db + assert USER3.user is not None + user3 = User(id=USER3.user.id, name="Spock") + login_service.users.save(user3) + + # Prepare an LDAP user named "Jane" with id=4 + user4 = UserLdap(id=4, name="Saavik") + login_service.users.save(user4) + + # Spock and Saavik are vulcans and can run simulations + role = Role(type=RoleType.RUNNER, identity=user3, group=group) + login_service.roles.save(role) + role = Role(type=RoleType.RUNNER, identity=user4, group=group) + login_service.roles.save(role) + + # Only site admin and group admin can get a group info + if expected: + actual = login_service.get_group_info("group", param) + assert actual.dict() == expected + else: + with pytest.raises(UserHasNotPermissionError): + login_service.get_group_info(group.id, param) + + @with_db_context + @pytest.mark.parametrize( + "param, can_get", [(SITE_ADMIN, True), (GROUP_ADMIN, True), (USER3, True), (BAD_PARAM, False)] + ) + def test_get_user(self, login_service: LoginService, param: RequestParameters, can_get: bool) -> None: + # Prepare a group of readers + group = Group(id="group", name="readers") + login_service.groups.save(group) + + # The user3 is a reader in the group "group" + user3 = User(id=USER3.user.id, name="Batman") + login_service.users.save(user3) + role = Role(type=RoleType.READER, identity=user3, group=group) + login_service.roles.save(role) + + # Anybody except anonymous can get the user3 + if can_get: + actual = login_service.get_user(user3.id, param) + assert actual == user3 + else: + # This function doesn't raise an exception if the user does not exist + actual = login_service.get_user(user3.id, param) + assert actual is None + + @with_db_context + def test_get_identity(self, login_service: LoginService) -> None: + # important: id=1 is the admin user + user = login_service.users.save(User(id=2, name="John")) + user_ldap = login_service.users.save(UserLdap(id=3, name="Jane")) + bot = login_service.users.save(Bot(id=4, name="my-app", owner=3, is_author=False)) + + assert login_service.get_identity(2, include_token=False) == user + assert login_service.get_identity(3, include_token=False) == user_ldap + assert login_service.get_identity(4, include_token=False) is None + + assert login_service.get_identity(2, include_token=True) == user + assert login_service.get_identity(3, include_token=True) == user_ldap + assert login_service.get_identity(4, include_token=True) == bot + + @with_db_context + @pytest.mark.parametrize( + "param, expected", + [ + ( + SITE_ADMIN, + { + "id": 3, + "name": "Batman", + "roles": [ + { + "group_id": "group", + "group_name": "readers", + "identity_id": 3, + "type": RoleType.READER, + } + ], + }, + ), + ( + GROUP_ADMIN, + { + "id": 3, + "name": "Batman", + "roles": [ + { + "group_id": "group", + "group_name": "readers", + "identity_id": 3, + "type": RoleType.READER, + } + ], + }, + ), + ( + USER3, + { + "id": 3, + "name": "Batman", + "roles": [ + { + "group_id": "group", + "group_name": "readers", + "identity_id": 3, + "type": RoleType.READER, + } + ], + }, + ), + (BAD_PARAM, {}), + ], + ) + def test_get_user_info( + self, + login_service: LoginService, + param: RequestParameters, + expected: t.Mapping[str, t.Any], + ) -> None: + # Prepare a group of readers + group = Group(id="group", name="readers") + login_service.groups.save(group) + + # The user3 is a reader in the group "group" + user3 = User(id=USER3.user.id, name="Batman") + login_service.users.save(user3) + role = Role(type=RoleType.READER, identity=user3, group=group) + login_service.roles.save(role) + + # Anybody except anonymous can get the user3 + if expected: + actual = login_service.get_user_info(user3.id, param) + assert actual.dict() == expected + else: + # This function doesn't raise an exception if the user does not exist + actual = login_service.get_user_info(user3.id, param) + assert actual is None + + @with_db_context + @pytest.mark.parametrize( + "param, can_get", [(SITE_ADMIN, True), (GROUP_ADMIN, False), (USER3, True), (BAD_PARAM, False)] + ) + def test_get_bot(self, login_service: LoginService, param: RequestParameters, can_get: bool) -> None: + # Prepare a user in the db, with id=4 + clark = User(id=3, name="Clark") + login_service.users.save(clark) + + # Prepare a bot in the db (using the ID or user3) + bot = Bot(id=4, name="Maria", owner=clark.id, is_author=True) + login_service.users.save(bot) + + # Only site admin and the owner can get a bot + if can_get: + actual = login_service.get_bot(bot.id, param) + assert actual == bot + else: + with pytest.raises(UserHasNotPermissionError): + login_service.get_bot(bot.id, param) + + @with_db_context + @pytest.mark.parametrize( + "param, expected", + [ + ( + SITE_ADMIN, + { + "id": 4, + "isAuthor": True, + "name": "Maria", + "roles": [ + { + "group_id": "Metropolis", + "group_name": "watchers", + "identity_id": 4, + "type": RoleType.READER, + } + ], + }, + ), + (GROUP_ADMIN, {}), + ( + USER3, + { + "id": 4, + "isAuthor": True, + "name": "Maria", + "roles": [ + { + "group_id": "Metropolis", + "group_name": "watchers", + "identity_id": 4, + "type": RoleType.READER, + } + ], + }, + ), + (BAD_PARAM, {}), + ], + ) + def test_get_bot_info( + self, + login_service: LoginService, + param: RequestParameters, + expected: t.Mapping[str, t.Any], + ) -> None: + # Prepare a user in the db, with id=4 + clark = User(id=3, name="Clark") + login_service.users.save(clark) + + # Prepare a bot in the db (using the ID or user3) + bot = Bot(id=4, name="Maria", owner=clark.id, is_author=True) + login_service.users.save(bot) + + # Prepare a group of readers + group = Group(id="Metropolis", name="watchers") + login_service.groups.save(group) + + # The user3 is a reader in the group "group" + role = Role(type=RoleType.READER, identity=bot, group=group) + login_service.roles.save(role) + + # Only site admin and the owner can get a bot + if expected: + actual = login_service.get_bot_info(bot.id, param) + assert actual is not None + assert actual.dict() == expected + else: + with pytest.raises(UserHasNotPermissionError): + login_service.get_bot_info(bot.id, param) + + @with_db_context + @pytest.mark.parametrize("param, expected", [(SITE_ADMIN, [5]), (GROUP_ADMIN, []), (USER3, [5]), (BAD_PARAM, [])]) + def test_get_all_bots_by_owner( + self, + login_service: LoginService, + param: RequestParameters, + expected: t.Mapping[str, t.Any], + ) -> None: + # add a user, an LDAP user and a bot in the db + user = User(id=3, name="John") + login_service.users.save(user) + user_ldap = UserLdap(id=4, name="Jane") + login_service.users.save(user_ldap) + bot = Bot(id=5, name="my-app", owner=3, is_author=False) + login_service.users.save(bot) + + if expected: + actual = login_service.get_all_bots_by_owner(3, param) + assert [b.id for b in actual] == expected + else: + with pytest.raises(UserHasNotPermissionError): + login_service.get_all_bots_by_owner(3, param) + + @with_db_context + def test_exists_bot(self, login_service: LoginService) -> None: + # Prepare the user3 in the db + assert USER3.user is not None + user3 = User(id=USER3.user.id, name="Clark") + login_service.users.save(user3) + + # Prepare a bot in the db (using the ID or user3) + bot = Bot(id=4, name="Maria", owner=user3.id, is_author=True) + login_service.users.save(bot) + + # Everybody can check the existence of a bot + assert login_service.exists_bot(4) + assert not login_service.exists_bot(5) # unknown ID + assert not login_service.exists_bot(3) # user ID, not bot ID + + @with_db_context + def test_authenticate__unknown_user(self, login_service: LoginService) -> None: + # An unknown user cannot log in + user = login_service.authenticate(name="unknown", pwd="S3cr3t") + assert user is None + + @with_db_context + def test_authenticate__known_user(self, login_service: LoginService) -> None: + # Create a user named "Tarzan" in the group "Adventure" + group = Group(id="adventure", name="Adventure") + login_service.groups.save(group) + user = User(id=3, name="Tarzan", password=Password("S3cr3t")) + login_service.users.save(user) + role = Role(type=RoleType.READER, identity=user, group=group) + login_service.roles.save(role) + + # A known user can log in + jwt_user = login_service.authenticate(name="Tarzan", pwd="S3cr3t") + assert jwt_user is not None + assert jwt_user.id == user.id + assert jwt_user.impersonator == user.id + assert jwt_user.type == "users" + assert jwt_user.groups == [JWTGroup(id="adventure", name="Adventure", role=RoleType.READER)] + + @with_db_context + def test_authenticate__external_user(self, login_service: LoginService) -> None: + # Create a user named "Tarzan" + user_ldap = UserLdap(id=3, name="Tarzan", external_id="tarzan", firstname="Tarzan", lastname="Jungle") + login_service.users.save(user_ldap) + + # Mock the LDAP service + login_service.ldap.login = Mock(return_value=user_ldap) # type: ignore + login_service.ldap.get = Mock(return_value=user_ldap) # type: ignore + + # A known user can log in + jwt_user = login_service.authenticate(name="Tarzan", pwd="S3cr3t") + assert jwt_user is not None + assert jwt_user.id == user_ldap.id + assert jwt_user.impersonator == user_ldap.id + assert jwt_user.type == "users_ldap" + assert jwt_user.groups == [] + + @with_db_context + def test_get_jwt(self, login_service: LoginService) -> None: + # Prepare the user3 in the db + assert USER3.user is not None + user3 = User(id=USER3.user.id, name="Clark") + login_service.users.save(user3) + + # Attach a group to the user + group = Group(id="group", name="readers") + login_service.groups.save(group) + role = Role(type=RoleType.READER, identity=user3, group=group) + login_service.roles.save(role) + + # Prepare an LDAP user in the db + user_ldap = UserLdap(id=4, name="Jane") + login_service.users.save(user_ldap) + + # Prepare a bot in the db (using the ID or user3) + bot = Bot(id=5, name="Maria", owner=user3.id, is_author=True) + login_service.users.save(bot) + + # We can get a JWT for a user, an LDAP user, but not a bot + jwt_user = login_service.get_jwt(user3.id) + assert jwt_user is not None + assert jwt_user.id == user3.id + assert jwt_user.impersonator == user3.id + assert jwt_user.type == "users" + assert jwt_user.groups == [JWTGroup(id="group", name="readers", role=RoleType.READER)] + + jwt_user = login_service.get_jwt(user_ldap.id) + assert jwt_user is not None + assert jwt_user.id == user_ldap.id + assert jwt_user.impersonator == user_ldap.id + assert jwt_user.type == "users_ldap" + assert jwt_user.groups == [] + + jwt_user = login_service.get_jwt(bot.id) + assert jwt_user is None + + @with_db_context + @pytest.mark.parametrize( + "param, expected", + [ + ( + SITE_ADMIN, + [ + {"id": "admin", "name": "admin"}, + {"id": "gr1", "name": "Adventure"}, + {"id": "gr2", "name": "Comedy"}, + ], + ), + ( + GROUP_ADMIN, + [ + {"id": "admin", "name": "admin"}, + {"id": "gr2", "name": "Comedy"}, + ], + ), + ( + USER3, + [ + {"id": "gr1", "name": "Adventure"}, + ], + ), + (BAD_PARAM, []), + ], + ) + def test_get_all_groups( + self, + login_service: LoginService, + param: RequestParameters, + expected: t.Sequence[t.Mapping[str, str]], + ) -> None: + # Prepare some groups in the db + group1 = Group(id="gr1", name="Adventure") + login_service.groups.save(group1) + group2 = Group(id="gr2", name="Comedy") + login_service.groups.save(group2) + + # The group admin is a reader in the group "gr2" + assert GROUP_ADMIN.user is not None + robin_hood = User(id=GROUP_ADMIN.user.id, name="Robin") + login_service.users.save(robin_hood) + role = Role(type=RoleType.READER, identity=robin_hood, group=group2) + login_service.roles.save(role) + + # The user3 is a reader in the group "gr1" + assert USER3.user is not None + indiana_johns = User(id=USER3.user.id, name="Indiana") + login_service.users.save(indiana_johns) + role = Role(type=RoleType.READER, identity=indiana_johns, group=group1) + login_service.roles.save(role) + + # Anybody except anonymous can get the list of groups + if expected: + # The site admin can see all groups + actual = login_service.get_all_groups(param) + assert [g.dict() for g in actual] == expected + else: + with pytest.raises(UserHasNotPermissionError): + login_service.get_all_groups(param) + + @with_db_context + @pytest.mark.parametrize( + "param, expected", + [ + ( + SITE_ADMIN, + [ + {"id": 0, "name": "Superman"}, + {"id": 1, "name": "John"}, + {"id": 2, "name": "Jane"}, + {"id": 3, "name": "Tarzan"}, + ], + ), + ( + GROUP_ADMIN, + [ + {"id": 1, "name": "John"}, + ], + ), + ( + USER3, + [ + {"id": 3, "name": "Tarzan"}, + ], + ), + (BAD_PARAM, []), + ], + ) + def test_get_all_users( + self, + login_service: LoginService, + param: RequestParameters, + expected: t.Sequence[t.Mapping[str, t.Union[int, str]]], + ) -> None: + # Insert some users in the db + user0 = User(id=0, name="Superman") + login_service.users.save(user0) + user1 = User(id=1, name="John") + login_service.users.save(user1) + user2 = User(id=2, name="Jane") + login_service.users.save(user2) + user3 = User(id=3, name="Tarzan") + login_service.users.save(user3) + + # user3 is a reader in the group "group" + group = Group(id="group", name="readers") + login_service.groups.save(group) + role = Role(type=RoleType.READER, identity=user3, group=group) + login_service.roles.save(role) + + # Anybody except anonymous can get the list of users + if expected: + actual = login_service.get_all_users(param) + assert [u.dict() for u in actual] == expected + else: + with pytest.raises(UserHasNotPermissionError): + login_service.get_all_users(param) + + @with_db_context + @pytest.mark.parametrize( + "param, expected", + [ + (SITE_ADMIN, [5]), + (GROUP_ADMIN, []), + (USER3, []), + (BAD_PARAM, []), + ], + ) + def test_get_all_bots( + self, + login_service: LoginService, + param: RequestParameters, + expected: t.Sequence[int], + ) -> None: + # add a user, an LDAP user and a bot in the db + user = User(id=3, name="John") + login_service.users.save(user) + user_ldap = UserLdap(id=4, name="Jane") + login_service.users.save(user_ldap) + bot = Bot(id=5, name="my-app", owner=3, is_author=False) + login_service.users.save(bot) + + if expected: + actual = login_service.get_all_bots(param) + assert [b.id for b in actual] == expected + else: + with pytest.raises(UserHasNotPermissionError): + login_service.get_all_bots(param) + + @with_db_context + @pytest.mark.parametrize( + "param, expected", + [ + (SITE_ADMIN, [(3, "group")]), + (GROUP_ADMIN, [(3, "group")]), + (USER3, []), + (BAD_PARAM, []), + ], + ) + def test_get_all_roles_in_group( + self, + login_service: LoginService, + param: RequestParameters, + expected: t.Sequence[t.Tuple[int, str]], + ) -> None: + # Insert some users in the db + user0 = User(id=0, name="Superman") + login_service.users.save(user0) + user1 = User(id=1, name="John") + login_service.users.save(user1) + user2 = User(id=2, name="Jane") + login_service.users.save(user2) + user3 = User(id=3, name="Tarzan") + login_service.users.save(user3) + + # user3 is a reader in the group "group" + group = Group(id="group", name="readers") + login_service.groups.save(group) + role = Role(type=RoleType.READER, identity=user3, group=group) + login_service.roles.save(role) + + # The site admin and the group admin can get the list of roles in a group + if expected: + actual = login_service.get_all_roles_in_group("group", param) + assert [(r.identity_id, r.group_id) for r in actual] == expected + else: + with pytest.raises(UserHasNotPermissionError): + login_service.get_all_roles_in_group("group", param) + + @with_db_context + @pytest.mark.parametrize( + "param, can_delete", + [ + (SITE_ADMIN, True), + (GROUP_ADMIN, True), + (USER3, False), + (BAD_PARAM, False), + ], + ) + def test_delete_group(self, login_service: LoginService, param: RequestParameters, can_delete: bool) -> None: + # Insert a group in the db + group = Group(id="group", name="readers") + login_service.groups.save(group) + + # The site admin and the group admin can delete a group + if can_delete: + login_service.delete_group("group", param) + actual = login_service.groups.get("group") + assert actual is None + else: + with pytest.raises(UserHasNotPermissionError): + login_service.delete_group("group", param) + actual = login_service.groups.get("group") + assert actual is not None + + @with_db_context + @pytest.mark.parametrize( + "param, can_delete", + [ + (SITE_ADMIN, True), + (GROUP_ADMIN, False), + (USER3, False), + (BAD_PARAM, False), + ], + ) + def test_delete_user(self, login_service: LoginService, param: RequestParameters, can_delete: bool) -> None: + # Insert a user in the db which is an owner of a bot + user = User(id=3, name="John") + login_service.users.save(user) + bot = Bot(id=4, name="my-app", owner=3, is_author=False) + login_service.users.save(bot) + + # The site admin can delete the user + if can_delete: + login_service.delete_user(3, param) + actual = login_service.users.get(3) + assert actual is None + else: + with pytest.raises(UserHasNotPermissionError): + login_service.delete_user(3, param) + actual = login_service.users.get(3) + assert actual is not None + + @with_db_context + @pytest.mark.parametrize( + "param, can_delete", + [ + (SITE_ADMIN, True), + (GROUP_ADMIN, False), + (USER3, True), + (BAD_PARAM, False), + ], + ) + def test_delete_bot(self, login_service: LoginService, param: RequestParameters, can_delete: bool) -> None: + # Insert a user in the db which is an owner of a bot + user = User(id=3, name="John") + login_service.users.save(user) + bot = Bot(id=4, name="my-app", owner=3, is_author=False) + login_service.users.save(bot) + + # The site admin and the current owner can delete the bot + if can_delete: + login_service.delete_bot(4, param) + actual = login_service.bots.get(4) + assert actual is None + else: + with pytest.raises(UserHasNotPermissionError): + login_service.delete_bot(4, param) + actual = login_service.bots.get(4) + assert actual is not None + + @with_db_context + @pytest.mark.parametrize( + "param, can_delete", + [ + (SITE_ADMIN, True), + (GROUP_ADMIN, True), + (USER3, False), + (BAD_PARAM, False), + ], + ) + def test_delete_role(self, login_service: LoginService, param: RequestParameters, can_delete: bool) -> None: + # Insert the user3 in the db + user = User(id=3, name="Tarzan") + login_service.users.save(user) + + # Insert a group in the db + group = Group(id="group", name="readers") + login_service.groups.save(group) + + # Insert a role in the db + role = Role(type=RoleType.READER, identity=user, group=group) + login_service.roles.save(role) + + # The site admin and the group admin can delete a role + if can_delete: + login_service.delete_role(3, "group", param) + actual = login_service.roles.get_all_by_group("group") + assert len(actual) == 0 + else: + with pytest.raises(UserHasNotPermissionError): + login_service.delete_role(3, "group", param) + actual = login_service.roles.get_all_by_group("group") + assert len(actual) == 1 + + @with_db_context + @pytest.mark.parametrize( + "param, can_delete", + [ + (SITE_ADMIN, True), + (GROUP_ADMIN, True), + (USER3, False), + (BAD_PARAM, False), + ], + ) + def test_delete_all_roles_from_user( + self, login_service: LoginService, param: RequestParameters, can_delete: bool + ) -> None: + # Insert the user3 in the db + assert USER3.user is not None + user = User(id=USER3.user.id, name="Tarzan") + login_service.users.save(user) + + # Insert a group in the db + group = Group(id="group", name="readers") + login_service.groups.save(group) + + # Insert a role in the db + role = Role(type=RoleType.READER, identity=user, group=group) + login_service.roles.save(role) + + # Insert the group admin in the db + assert GROUP_ADMIN.user is not None + group_admin = User(id=GROUP_ADMIN.user.id, name="John") + login_service.users.save(group_admin) + + # Insert another group in the db + group2 = Group(id="group2", name="readers") + login_service.groups.save(group2) + + # Insert a role in the db + role2 = Role(type=RoleType.READER, identity=group_admin, group=group2) + login_service.roles.save(role2) + + # The site admin and the group admin can delete a role + if can_delete: + login_service.delete_all_roles_from_user(3, param) + actual = login_service.roles.get_all_by_group("group") + assert len(actual) == 0 + actual = login_service.roles.get_all_by_group("group2") + assert len(actual) == 1 + else: + with pytest.raises(UserHasNotPermissionError): + login_service.delete_all_roles_from_user(3, param) + actual = login_service.roles.get_all_by_group("group") + assert len(actual) == 1 + actual = login_service.roles.get_all_by_group("group2") + assert len(actual) == 1 diff --git a/tests/login/test_repository.py b/tests/login/test_repository.py index 3599ec437c..6669747507 100644 --- a/tests/login/test_repository.py +++ b/tests/login/test_repository.py @@ -142,7 +142,7 @@ def test_roles(): a = repo.save(a) assert a == repo.get(user=0, group="group") - assert [a] == repo.get_all_by_user(user=0) + assert [a] == repo.get_all_by_user(0) assert [a] == repo.get_all_by_group(group="group") repo.delete(user=0, group="group") diff --git a/tests/login/test_service.py b/tests/login/test_service.py deleted file mode 100644 index 7cd26cbe16..0000000000 --- a/tests/login/test_service.py +++ /dev/null @@ -1,734 +0,0 @@ -from typing import Any, Callable, List, Tuple -from unittest.mock import Mock - -import pytest -from fastapi import HTTPException - -from antarest.core.jwt import JWTGroup, JWTUser -from antarest.core.requests import RequestParameters, UserHasNotPermissionError -from antarest.login.ldap import LdapService -from antarest.login.model import ( - Bot, - BotCreateDTO, - BotRoleCreateDTO, - Group, - Identity, - Password, - Role, - RoleCreationDTO, - RoleType, - User, - UserCreateDTO, - UserLdap, -) -from antarest.login.service import LoginService, UserNotFoundError - -SADMIN = RequestParameters( - user=JWTUser( - id=0, - impersonator=0, - type="users", - groups=[JWTGroup(id="admin", name="admin", role=RoleType.ADMIN)], - ) -) - -GADMIN = RequestParameters( - user=JWTUser( - id=1, - impersonator=1, - type="users", - groups=[JWTGroup(id="group", name="group", role=RoleType.ADMIN)], - ) -) - -USER3 = RequestParameters( - user=JWTUser( - id=3, - impersonator=3, - type="users", - groups=[JWTGroup(id="group", name="group", role=RoleType.READER)], - ) -) - - -def assert_permission( - test: Callable[[RequestParameters], Any], - values: List[Tuple[RequestParameters, bool]], - error=UserHasNotPermissionError, -): - for params, res in values: - if res: - assert test(params) - else: - with pytest.raises(error): - test(params) - - -def test_save_group(): - groups = Mock() - groups.get_by_name.return_value = None - groups.save.return_value = Group(id="group", name="group") - - service = LoginService( - user_repo=Mock(), - bot_repo=Mock(), - group_repo=groups, - role_repo=Mock(), - ldap=Mock(), - event_bus=Mock(), - ) - # Case 1 : Group name doesn't exist - group = Group(id="group", name="group") - assert_permission( - test=lambda x: service.save_group(group, x), - values=[(SADMIN, True), (GADMIN, True), (USER3, False)], - ) - - # Case 2 : Group name already exists - groups.get_by_name.return_value = group - assert_permission( - test=lambda x: service.save_group(group, x), - values=[(SADMIN, False), (GADMIN, False), (USER3, False)], - error=HTTPException, - ) - - -def test_create_user(): - create = UserCreateDTO(name="hello", password="world") - ldap = Mock() - ldap.save.return_value = None - - users = Mock() - users.save.return_value = User(id=3, name="hello") - users.get_by_name.return_value = None - - service = LoginService( - user_repo=users, - bot_repo=Mock(), - group_repo=Mock(), - role_repo=Mock(), - ldap=ldap, - event_bus=Mock(), - ) - - service.create_user(create, param=SADMIN) - users.save.assert_called_once_with(User(name="hello", password=Password("world"))) - - -def test_save_user(): - user = User(id=3) - users = Mock() - users.save.return_value = user - - service = LoginService( - user_repo=users, - bot_repo=Mock(), - group_repo=Mock(), - role_repo=Mock(), - ldap=Mock(), - event_bus=Mock(), - ) - - assert_permission( - test=lambda x: service.save_user(user, x), - values=[(SADMIN, True), (GADMIN, False), (USER3, True)], - ) - - -def test_get_identity(): - user = User(id=1) - ldap = UserLdap(id=2, name="Jane") - bot = Bot(id=3, name="bot", owner=3, is_author=False) - user_repo = Mock() - ldap_repo = Mock(spec=LdapService) - bot_repo = Mock() - service = LoginService( - user_repo=user_repo, - bot_repo=bot_repo, - group_repo=Mock(), - role_repo=Mock(), - ldap=ldap_repo, - event_bus=Mock(), - ) - user_repo.get.return_value = user - ldap_repo.get.return_value = None - bot_repo.get.return_value = None - assert service.get_identity(1) == user - - user_repo.get.return_value = None - ldap_repo.get.return_value = ldap - assert service.get_identity(2) == ldap - - ldap_repo.get.return_value = None - assert service.get_identity(3) is None - - bot_repo.get.return_value = bot - assert service.get_identity(3, True) == bot - - -def test_save_bot(): - bot_create = BotCreateDTO( - name="bot", - group="group", - roles=[BotRoleCreateDTO(group="group", role=10)], - ) - bots = Mock() - bots.save.side_effect = lambda b: Bot( - name=b.name, - id=2, - is_author=b.is_author, - owner=b.owner, - ) - bots.get_by_name_and_owner.return_value = None - - roles = Mock() - roles.get.return_value = Role(identity=Identity(id=3), group=Group(id="group"), type=RoleType.WRITER) - - service = LoginService( - user_repo=Mock(), - bot_repo=bots, - group_repo=Mock(), - role_repo=roles, - ldap=Mock(), - event_bus=Mock(), - ) - - res = service.save_bot(bot_create, USER3) - assert res == Bot(name="bot", id=2, is_author=True, owner=3) - - -def test_save_bot_wrong_role(): - bot_create = BotCreateDTO( - name="bot", - group="group", - roles=[BotRoleCreateDTO(group="group", role=40)], - ) - bots = Mock() - bots.save.side_effect = lambda b: b - - roles = Mock() - roles.get.return_value = Role(identity=Identity(id=3), group=Group(id="group"), type=RoleType.READER) - - service = LoginService( - user_repo=Mock(), - bot_repo=bots, - group_repo=Mock(), - role_repo=roles, - ldap=Mock(), - event_bus=Mock(), - ) - - assert_permission( - test=lambda x: service.save_bot(bot_create, x), - values=[(USER3, False)], - ) - - -def test_save_role(): - role = RoleCreationDTO(type=RoleType.ADMIN, identity_id=0, group_id="group") - users = Mock() - users.get.return_value = User(id=0, name="admin") - groups = Mock() - groups.get.return_value = Group(id="group", name="some group") - roles = Mock() - roles.save.return_value = role - - service = LoginService( - user_repo=users, - bot_repo=Mock(), - group_repo=groups, - role_repo=roles, - ldap=Mock(), - event_bus=Mock(), - ) - - assert_permission( - test=lambda x: service.save_role(role, x), - values=[(SADMIN, True), (GADMIN, True), (USER3, False)], - ) - - -def test_get_group(): - group = Group(id="group", name="group") - groups = Mock() - groups.get.return_value = group - - service = LoginService( - user_repo=Mock(), - bot_repo=Mock(), - group_repo=groups, - role_repo=Mock(), - ldap=Mock(), - event_bus=Mock(), - ) - - assert_permission( - test=lambda x: service.get_group("group", x), - values=[(SADMIN, True), (GADMIN, True), (USER3, True)], - ) - - -def test_get_group_info(): - groups = Mock() - group = Group(id="group", name="group") - groups.get.return_value = group - - users = Mock() - user = User(id=3, name="John") - users.get.return_value = user - - ldap = Mock(spec=LdapService) - ldap.get.return_value = UserLdap(id=4, name="Jane") - - roles = Mock() - roles.get_all_by_group.return_value = [Role(group=group, identity=user, type=RoleType.RUNNER)] - roles.get_all_by_user.return_value = [Role(group=group, identity=user, type=RoleType.RUNNER)] - - service = LoginService( - user_repo=users, - bot_repo=Mock(), - group_repo=groups, - role_repo=roles, - ldap=ldap, - event_bus=Mock(), - ) - - assert_permission( - test=lambda x: service.get_group_info("group", x), - values=[(SADMIN, True), (GADMIN, True), (USER3, False)], - ) - - -def test_get_user(): - user = User(id=3) - users = Mock() - users.get.return_value = user - - ldap = Mock() - ldap.get.return_value = None - - role = Role(type=RoleType.READER, identity=user, group=Group(id="group")) - roles = Mock() - roles.get_all_by_user.return_value = [role] - - service = LoginService( - user_repo=users, - bot_repo=Mock(), - group_repo=Mock(), - role_repo=roles, - ldap=ldap, - event_bus=Mock(), - ) - - assert_permission( - test=lambda x: service.get_user(3, x), - values=[(SADMIN, True), (GADMIN, True), (USER3, True)], - error=UserNotFoundError, - ) - - -def test_get_user_info(): - users = Mock() - user_ok = User(id=3, name="user") - user_nok = User(id=2, name="user") - - ldap = Mock() - ldap.get.return_value = None - - roles = Mock() - group_ok = Group(id="group", name="group") - group_nok = Group(id="other_group", name="group") - - service = LoginService( - user_repo=users, - bot_repo=Mock(), - group_repo=Mock(), - role_repo=roles, - ldap=ldap, - event_bus=Mock(), - ) - - user_id = 3 - # When GADMIN ok, USER3 is himself - users.get.return_value = user_ok - roles.get_all_by_user.return_value = [Role(type=RoleType.ADMIN, group=group_ok, identity=user_ok)] - assert_permission( - test=lambda x: service.get_user_info(user_id, x), - values=[(SADMIN, True), (GADMIN, True), (USER3, True)], - ) - - # When GADMIN not ok, USER3 is not himself - # TODO Now it doesn't throw an error but just return None - # users.get.return_value = user_nok - # roles.get_all_by_user.return_value = [ - # Role(type=RoleType.ADMIN, group=group_nok, identity=user_nok) - # ] - # assert_permission( - # test=lambda x: service.get_user_info(user_id, x), - # values=[(SADMIN, True), (GADMIN, False), (USER3, False)], - # error=UserNotFoundError, - # ) - - -def test_get_bot(): - bots = Mock() - bots.get.return_value = Bot(owner=3) - - service = LoginService( - user_repo=Mock(), - bot_repo=bots, - group_repo=Mock(), - role_repo=Mock(), - ldap=Mock(), - event_bus=Mock(), - ) - - assert_permission( - test=lambda x: service.get_bot(3, x), - values=[(SADMIN, True), (USER3, True), (GADMIN, False)], - error=UserHasNotPermissionError, - ) - - -def test_get_bot_info(): - bots = Mock() - bot = Bot(id=4, name="bot", owner=3, is_author=False) - bots.get.return_value = bot - - roles = Mock() - group = Group(id="group", name="group") - - service = LoginService( - user_repo=Mock(), - bot_repo=bots, - group_repo=Mock(), - role_repo=roles, - ldap=Mock(), - event_bus=Mock(), - ) - - bot_id = 3 - # When USER3 is himself - bots.get.return_value = bot - roles.get_all_by_user.return_value = [Role(type=RoleType.ADMIN, group=group, identity=bot)] - assert_permission( - test=lambda x: service.get_bot_info(bot_id, x), - values=[(SADMIN, True), (GADMIN, False), (USER3, True)], - ) - - -def test_authentication_wrong_user(): - users = Mock() - users.get_by_name.return_value = None - - ldap = Mock() - ldap.login.return_value = None - ldap.get_by_name.return_value = None - - service = LoginService( - user_repo=users, - bot_repo=Mock(), - group_repo=Mock(), - role_repo=Mock(), - ldap=ldap, - event_bus=Mock(), - ) - assert not service.authenticate("dupond", "pwd") - users.get_by_name.assert_called_once_with("dupond") - - -def test_authenticate(): - users = Mock() - users.get_by_name.return_value = User(id=0, password=Password("pwd")) - users.get.return_value = User(id=0, name="linus") - - ldap = Mock() - ldap.login.return_value = None - ldap.get.return_value = None - - roles = Mock() - roles.get_all_by_user.return_value = [Role(type=RoleType.READER, group=Group(id="group", name="group"))] - - exp = JWTUser( - id=0, - impersonator=0, - type="users", - groups=[JWTGroup(id="group", name="group", role=RoleType.READER)], - ) - - service = LoginService( - user_repo=users, - bot_repo=Mock(), - group_repo=Mock(), - role_repo=roles, - ldap=ldap, - event_bus=Mock(), - ) - assert exp == service.authenticate("dupond", "pwd") - - users.get_by_name.assert_called_once_with("dupond") - roles.get_all_by_user.assert_called_once_with(0) - - -def test_authentication_ldap_user(): - users = Mock() - users.get_by_name.return_value = None - - roles = Mock() - roles.get_all_by_user.return_value = [] - - ldap = Mock() - user = UserLdap(id=10, name="ExtUser") - ldap.login.return_value = user - ldap.get.return_value = user - - exp = JWTUser( - id=10, - impersonator=10, - type="users_ldap", - ) - - service = LoginService( - user_repo=users, - bot_repo=Mock(), - group_repo=Mock(), - role_repo=roles, - ldap=ldap, - event_bus=Mock(), - ) - assert exp == service.authenticate("dupond", "pwd") - ldap.get.assert_called_once_with(10) - - -def test_get_all_groups(): - group = Group(id="my-group", name="my-group") - groups = Mock() - groups.get_all.return_value = [group] - - user = User(id=3, name="name") - role = Role(group=group, identity=user) - roles = Mock() - roles.get_all_by_user.return_value = [role] - - service = LoginService( - user_repo=Mock(), - bot_repo=Mock(), - group_repo=groups, - role_repo=roles, - ldap=Mock(), - event_bus=Mock(), - ) - - assert_permission( - test=lambda x: service.get_all_groups(x), - values=[(SADMIN, True), (GADMIN, True), (USER3, True)], - ) - - -def test_get_all_users(): - users = Mock() - users.get_all.return_value = [User(id=0, name="alice")] - - ldap = Mock() - ldap.get_all.return_value = [] - - user = User(id=3, name="user") - role_gadmin_ok = Role(group=Group(id="group"), type=RoleType.ADMIN, identity=user) - - role_repo = Mock() - role_repo.get_all_by_user.return_value = [role_gadmin_ok] - role_repo.get_all_by_group.return_value = [role_gadmin_ok] - - service = LoginService( - user_repo=users, - bot_repo=Mock(), - group_repo=Mock(), - role_repo=role_repo, - ldap=ldap, - event_bus=Mock(), - ) - - assert_permission( - test=lambda x: service.get_all_users(x), - values=[(SADMIN, True), (GADMIN, True), (USER3, True)], - ) - - -def test_get_all_bots(): - bots = Mock() - bots.get_all.return_value = [Bot(id=0, name="alice")] - - service = LoginService( - user_repo=Mock(), - bot_repo=bots, - group_repo=Mock(), - role_repo=Mock(), - ldap=Mock(), - event_bus=Mock(), - ) - - assert_permission( - test=lambda x: service.get_all_bots(x), - values=[(SADMIN, True), (GADMIN, False), (USER3, False)], - ) - - -def test_get_all_bots_by_owner(): - bots = Mock() - bots.get_all_by_owner.return_value = [Bot(id=0, name="alice")] - - service = LoginService( - user_repo=Mock(), - bot_repo=bots, - group_repo=Mock(), - role_repo=Mock(), - ldap=Mock(), - event_bus=Mock(), - ) - - assert_permission( - test=lambda x: service.get_all_bots_by_owner(3, x), - values=[(SADMIN, True), (GADMIN, False), (USER3, True)], - ) - - -def test_get_all_roles_in_group(): - roles = Mock() - roles.get_all_by_group.return_value = [Role()] - - service = LoginService( - user_repo=Mock(), - bot_repo=Mock(), - group_repo=Mock(), - role_repo=roles, - ldap=Mock(), - event_bus=Mock(), - ) - - assert_permission( - test=lambda x: service.get_all_roles_in_group("group", x), - values=[(SADMIN, True), (GADMIN, True), (USER3, False)], - ) - - -def test_delete_group(): - groups = Mock() - groups.delete.return_value = Group() - - roles = Mock() - roles.get_all_by_group.return_value = [] - - service = LoginService( - user_repo=Mock(), - bot_repo=Mock(), - group_repo=groups, - role_repo=roles, - ldap=Mock(), - event_bus=Mock(), - ) - - assert_permission( - test=lambda x: service.delete_group("group", x), - values=[(SADMIN, True), (GADMIN, True), (USER3, False)], - ) - - -def test_delete_user(): - users = Mock() - users.delete.return_value = User() - - bots = Mock() - bots.get_all_by_owner.return_value = [Bot(id=4, owner=3)] - bots.get.return_value = Bot(id=4, owner=3) - - roles = Mock() - roles.get_all_by_user.return_value = [] - - service = LoginService( - user_repo=users, - bot_repo=bots, - group_repo=Mock(), - role_repo=roles, - ldap=Mock(), - event_bus=Mock(), - ) - - assert_permission( - test=lambda x: service.delete_user(3, x), - values=[(SADMIN, True), (GADMIN, False), (USER3, False)], - ) - - users.delete.assert_called_with(3) - bots.delete.assert_called_with(4) - - -def test_delete_bot(): - bots = Mock() - bots.delete.return_value = Bot() - bots.get.return_value = Bot(id=4, owner=3) - - roles = Mock() - roles.get_all_by_user.return_value = [] - - service = LoginService( - user_repo=Mock(), - bot_repo=bots, - group_repo=Mock(), - role_repo=roles, - ldap=Mock(), - event_bus=Mock(), - ) - - assert_permission( - test=lambda x: service.delete_bot(3, x), - values=[(SADMIN, True), (GADMIN, False), (USER3, True)], - ) - - -def test_delete_role(): - roles = Mock() - roles.delete.return_value = Role() - - service = LoginService( - user_repo=Mock(), - bot_repo=Mock(), - group_repo=Mock(), - role_repo=roles, - ldap=Mock(), - event_bus=Mock(), - ) - - assert_permission( - test=lambda x: service.delete_role(3, "group", x), - values=[(SADMIN, True), (GADMIN, True), (USER3, False)], - ) - - -def test_delete_all_roles(): - roles = Mock() - user = User(id=3, name="user") - role_gadmin_ok = Role(group=Group(id="group"), type=RoleType.READER, identity=user) - role_gadmin_nok = Role(group=Group(id="other-group"), type=RoleType.READER, identity=user) - - service = LoginService( - user_repo=Mock(), - bot_repo=Mock(), - group_repo=Mock(), - role_repo=roles, - ldap=Mock(), - event_bus=Mock(), - ) - - user_id = 3 - # GADMIN OK - roles.get_all_by_user.return_value = [role_gadmin_ok] - assert_permission( - test=lambda x: service.delete_all_roles_from_user(user_id, x), - values=[(SADMIN, True), (GADMIN, True), (USER3, False)], - ) - # GADMIN NOK - roles.get_all_by_user.return_value = [role_gadmin_nok] - assert_permission( - test=lambda x: service.delete_all_roles_from_user(user_id, x), - values=[(SADMIN, True), (GADMIN, False), (USER3, False)], - ) diff --git a/tests/login/test_web.py b/tests/login/test_web.py index bea5327772..0f7175fc54 100644 --- a/tests/login/test_web.py +++ b/tests/login/test_web.py @@ -232,15 +232,20 @@ def test_user_save() -> None: app = create_app(service) client = TestClient(app) + user_obj = user.to_dto().dict() res = client.put( "/v1/users/0", headers=create_auth_token(app), - json=user.to_dto().dict(), + json=user_obj, ) assert res.status_code == 200 - service.save_user.assert_called_once_with(user, PARAMS) - assert res.json() == user.to_dto().dict() + assert res.json() == user_obj + + assert service.save_user.call_count == 1 + call = service.save_user.call_args_list[0] + assert call[0][0].to_dto().dict() == user_obj + assert call[0][1] == PARAMS @pytest.mark.unit_test diff --git a/tests/matrixstore/test_service.py b/tests/matrixstore/test_service.py index db26e6403a..5c6eb837c7 100644 --- a/tests/matrixstore/test_service.py +++ b/tests/matrixstore/test_service.py @@ -365,7 +365,19 @@ def test_dataset_lifecycle() -> None: ], ) service.create_dataset(dataset_info, matrices, params=userA) - dataset_repo.save.assert_called_with(expected) + assert dataset_repo.save.call_count == 1 + call = dataset_repo.save.call_args_list[0] + assert call[0][0].name == "datasetA" + assert call[0][0].public is True + assert call[0][0].owner_id == userA.user.id + groups = call[0][0].groups + assert len(groups) == 1 + assert groups[0].id == "groupA" + assert groups[0].name == "groupA" + assert call[0][0].matrices == [ + MatrixDataSetRelation(name="A", matrix_id="m1"), + MatrixDataSetRelation(name="B", matrix_id="m2"), + ] somedate = datetime.datetime.now() dataset_repo.query.return_value = [ @@ -466,16 +478,8 @@ def test_dataset_lifecycle() -> None: ), botA, ) + user_service.get_group.assert_called_with("groupB", botA) - dataset_repo.save.assert_called_with( - MatrixDataSet( - id="some id", - name="datasetA bis", - public=False, - groups=[Group(id="groupB", name="groupB")], - updated_at=ANY, - ) - ) service.delete_dataset("dataset", userA) dataset_repo.delete.assert_called_once() diff --git a/tests/storage/business/test_arealink_manager.py b/tests/storage/business/test_arealink_manager.py index 36ea403c06..9f8e0be884 100644 --- a/tests/storage/business/test_arealink_manager.py +++ b/tests/storage/business/test_arealink_manager.py @@ -16,13 +16,8 @@ from antarest.study.repository import StudyMetadataRepository from antarest.study.storage.patch_service import PatchService from antarest.study.storage.rawstudy.model.filesystem.config.files import build -from antarest.study.storage.rawstudy.model.filesystem.config.model import ( - Area, - Cluster, - DistrictSet, - FileStudyTreeConfig, - Link, -) +from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, DistrictSet, FileStudyTreeConfig, Link +from antarest.study.storage.rawstudy.model.filesystem.config.thermal import ThermalConfig from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.rawstudy.model.filesystem.root.filestudytree import FileStudyTree from antarest.study.storage.rawstudy.raw_study_service import RawStudyService @@ -259,7 +254,7 @@ def test_get_all_area(): "a2": Link(filters_synthesis=[], filters_year=[]), "a3": Link(filters_synthesis=[], filters_year=[]), }, - thermals=[Cluster(id="a", name="a", enabled=True)], + thermals=[ThermalConfig(id="a", name="a", enabled=True)], renewables=[], filters_synthesis=[], filters_year=[], @@ -470,7 +465,7 @@ def test_update_clusters(): "a1": Area( name="a1", links={}, - thermals=[Cluster(id="a", name="a", enabled=True)], + thermals=[ThermalConfig(id="a", name="a", enabled=True)], renewables=[], filters_synthesis=[], filters_year=[], diff --git a/tests/storage/business/test_import.py b/tests/storage/business/test_import.py index d319034b72..2cd08eeba9 100644 --- a/tests/storage/business/test_import.py +++ b/tests/storage/business/test_import.py @@ -6,7 +6,8 @@ import pytest -from antarest.core.exceptions import BadZipBinary, StudyValidationError +from antarest.core.exceptions import StudyValidationError +from antarest.core.utils.utils import BadArchiveContent from antarest.study.model import DEFAULT_WORKSPACE_NAME, RawStudy, StudyAdditionalData from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.rawstudy.raw_study_service import RawStudyService @@ -65,7 +66,7 @@ def test_import_study(tmp_path: Path) -> None: assert md.groups == ["fake_group_1", "fake_group_2"] shutil.rmtree(tmp_path / "other-study") - with pytest.raises(BadZipBinary): + with pytest.raises(BadArchiveContent, match="Unsupported archive format"): study_service.import_study(md, io.BytesIO(b"")) diff --git a/tests/storage/business/test_watcher.py b/tests/storage/business/test_watcher.py index adb14ae32c..6faeeabfe5 100644 --- a/tests/storage/business/test_watcher.py +++ b/tests/storage/business/test_watcher.py @@ -10,8 +10,7 @@ from antarest.core.exceptions import CannotScanInternalWorkspace from antarest.core.persistence import Base from antarest.core.utils.fastapi_sqlalchemy import DBSessionMiddleware -from antarest.login.model import Group -from antarest.study.model import DEFAULT_WORKSPACE_NAME, StudyFolder +from antarest.study.model import DEFAULT_WORKSPACE_NAME from antarest.study.storage.rawstudy.watcher import Watcher from tests.storage.conftest import SimpleSyncTaskService @@ -88,12 +87,16 @@ def test_scan(tmp_path: Path): watcher.scan() - service.sync_studies_on_disk.assert_called_once_with( - [ - StudyFolder(c, "diese", [Group(id="tata", name="tata")]), - ], - None, - ) + assert service.sync_studies_on_disk.call_count == 1 + call = service.sync_studies_on_disk.call_args_list[0] + assert len(call.args[0]) == 1 + assert call.args[0][0].path == c + assert call.args[0][0].workspace == "diese" + groups = call.args[0][0].groups + assert len(groups) == 1 + assert groups[0].id == "tata" + assert groups[0].name == "tata" + assert call.args[1] is None @pytest.mark.unit_test @@ -129,12 +132,16 @@ def test_partial_scan(tmp_path: Path): watcher.scan(workspace_name="test", workspace_directory_path=default) - service.sync_studies_on_disk.assert_called_once_with( - [ - StudyFolder(a, "test", [Group(id="toto", name="toto")]), - ], - default, - ) + assert service.sync_studies_on_disk.call_count == 1 + call = service.sync_studies_on_disk.call_args_list[0] + assert len(call.args[0]) == 1 + assert call.args[0][0].path == a + assert call.args[0][0].workspace == "test" + groups = call.args[0][0].groups + assert len(groups) == 1 + assert groups[0].id == "toto" + assert groups[0].name == "toto" + assert call.args[1] == tmp_path / "test" def process(x: int) -> bool: diff --git a/tests/storage/business/test_xpansion_manager.py b/tests/storage/business/test_xpansion_manager.py index 2289f8767b..1dc88ff31e 100644 --- a/tests/storage/business/test_xpansion_manager.py +++ b/tests/storage/business/test_xpansion_manager.py @@ -1,9 +1,10 @@ +import io import os +import typing as t import uuid -from io import StringIO +import zipfile from pathlib import Path from unittest.mock import Mock -from zipfile import ZipFile import pytest from fastapi import UploadFile @@ -11,8 +12,13 @@ from antarest.core.model import JSON from antarest.study.business.xpansion_management import ( + CutType, FileCurrentlyUsedInSettings, LinkNotFound, + Master, + MaxIteration, + Solver, + UcType, XpansionCandidateDTO, XpansionFileNotFoundError, XpansionManager, @@ -31,19 +37,19 @@ from antarest.study.storage.variantstudy.model.command.create_link import CreateLink from antarest.study.storage.variantstudy.model.command_context import CommandContext from antarest.study.storage.variantstudy.variant_study_service import VariantStudyService +from tests.storage.business.assets import ASSETS_DIR def make_empty_study(tmpdir: Path, version: int) -> FileStudy: - cur_dir: Path = Path(__file__).parent study_path = Path(tmpdir / str(uuid.uuid4())) os.mkdir(study_path) - with ZipFile(cur_dir / "assets" / f"empty_study_{version}.zip") as zip_output: + with zipfile.ZipFile(ASSETS_DIR / f"empty_study_{version}.zip") as zip_output: zip_output.extractall(path=study_path) config = build(study_path, "1") return FileStudy(config, FileStudyTree(Mock(), config)) -def make_xpansion_manager(empty_study): +def make_xpansion_manager(empty_study: FileStudy) -> XpansionManager: raw_study_service = Mock(spec=RawStudyService) variant_study_service = Mock(spec=VariantStudyService) xpansion_manager = XpansionManager( @@ -54,7 +60,7 @@ def make_xpansion_manager(empty_study): return xpansion_manager -def make_areas(empty_study): +def make_areas(empty_study: FileStudy) -> None: CreateArea( area_name="area1", command_context=Mock(spec=CommandContext, generator_matrix_constants=Mock()), @@ -65,7 +71,7 @@ def make_areas(empty_study): )._apply_config(empty_study.config) -def make_link(empty_study): +def make_link(empty_study: FileStudy) -> None: CreateLink( area1="area1", area2="area2", @@ -73,7 +79,7 @@ def make_link(empty_study): )._apply_config(empty_study.config) -def make_link_and_areas(empty_study): +def make_link_and_areas(empty_study: FileStudy) -> None: make_areas(empty_study) make_link(empty_study) @@ -114,6 +120,7 @@ def make_link_and_areas(empty_study): "relative_gap": 1e-12, "solver": "Cbc", "batch_size": 0, + "separation_parameter": 0.5, }, "sensitivity": {"sensitivity_in": {}}, "candidates": {}, @@ -124,7 +131,7 @@ def make_link_and_areas(empty_study): ), ], ) -def test_create_configuration(tmp_path: Path, version: int, expected_output: JSON): +def test_create_configuration(tmp_path: Path, version: int, expected_output: JSON) -> None: """ Test the creation of a configuration. """ @@ -141,7 +148,7 @@ def test_create_configuration(tmp_path: Path, version: int, expected_output: JSO @pytest.mark.unit_test -def test_delete_xpansion_configuration(tmp_path: Path): +def test_delete_xpansion_configuration(tmp_path: Path) -> None: """ Test the deletion of a configuration. """ @@ -164,53 +171,58 @@ def test_delete_xpansion_configuration(tmp_path: Path): @pytest.mark.unit_test @pytest.mark.parametrize( - "version,expected_output", + "version, expected_output", [ ( 720, - XpansionSettingsDTO.parse_obj( - { - "optimality_gap": 1, - "max_iteration": "+Inf", - "uc_type": "expansion_fast", - "master": "integer", - "yearly_weight": None, - "additional-constraints": None, - "relaxed-optimality-gap": 1000000.0, - "cut-type": "yearly", - "ampl.solver": "cbc", - "ampl.presolve": 0, - "ampl.solve_bounds_frequency": 1000000, - "relative_gap": None, - "solver": None, - } - ), + { + "additional-constraints": None, + "ampl.presolve": 0, + "ampl.solve_bounds_frequency": 1000000, + "ampl.solver": "cbc", + "batch_size": 0, + "cut-type": CutType.YEARLY, + "log_level": 0, + "master": Master.INTEGER, + "max_iteration": MaxIteration.INF, + "optimality_gap": 1.0, + "relative_gap": None, + "relaxed-optimality-gap": 1000000.0, + "sensitivity_config": {"capex": False, "epsilon": 10000.0, "projection": []}, + "separation_parameter": 0.5, + "solver": None, + "timelimit": 1000000000000, + "uc_type": UcType.EXPANSION_FAST, + "yearly-weights": None, + }, ), ( 810, - XpansionSettingsDTO.parse_obj( - { - "optimality_gap": 1, - "max_iteration": "+Inf", - "uc_type": "expansion_fast", - "master": "integer", - "yearly_weight": None, - "additional-constraints": None, - "relaxed-optimality-gap": None, - "cut-type": None, - "ampl.solver": None, - "ampl.presolve": None, - "ampl.solve_bounds_frequency": None, - "relative_gap": 1e-12, - "solver": "Cbc", - "batch_size": 0, - } - ), + { + "additional-constraints": None, + "ampl.presolve": None, + "ampl.solve_bounds_frequency": None, + "ampl.solver": None, + "batch_size": 0, + "cut-type": None, + "log_level": 0, + "master": Master.INTEGER, + "max_iteration": MaxIteration.INF, + "optimality_gap": 1.0, + "relative_gap": 1e-12, + "relaxed-optimality-gap": None, + "sensitivity_config": {"capex": False, "epsilon": 10000.0, "projection": []}, + "separation_parameter": 0.5, + "solver": Solver.CBC, + "timelimit": 1000000000000, + "uc_type": UcType.EXPANSION_FAST, + "yearly-weights": None, + }, ), ], ) @pytest.mark.unit_test -def test_get_xpansion_settings(tmp_path: Path, version: int, expected_output: JSON): +def test_get_xpansion_settings(tmp_path: Path, version: int, expected_output: JSON) -> None: """ Test the retrieval of the xpansion settings. """ @@ -221,11 +233,12 @@ def test_get_xpansion_settings(tmp_path: Path, version: int, expected_output: JS xpansion_manager.create_xpansion_configuration(study) - assert xpansion_manager.get_xpansion_settings(study) == expected_output + actual = xpansion_manager.get_xpansion_settings(study) + assert actual.dict(by_alias=True) == expected_output @pytest.mark.unit_test -def test_xpansion_sensitivity_settings(tmp_path: Path): +def test_xpansion_sensitivity_settings(tmp_path: Path) -> None: """ Test that attribute projection in sensitivity_config is optional """ @@ -258,7 +271,7 @@ def test_xpansion_sensitivity_settings(tmp_path: Path): @pytest.mark.unit_test -def test_update_xpansion_settings(tmp_path: Path): +def test_update_xpansion_settings(tmp_path: Path) -> None: """ Test the retrieval of the xpansion settings. """ @@ -269,32 +282,37 @@ def test_update_xpansion_settings(tmp_path: Path): xpansion_manager.create_xpansion_configuration(study) - new_settings = XpansionSettingsDTO.parse_obj( - { - "optimality_gap": 4, - "max_iteration": 123, - "uc_type": "expansion_fast", - "master": "integer", - "yearly_weight": None, - "additional-constraints": None, - "relaxed-optimality-gap": "1.2%", - "cut-type": None, - "ampl.solver": None, - "ampl.presolve": None, - "ampl.solve_bounds_frequency": None, - "relative_gap": 1e-12, - "solver": "Cbc", - "batch_size": 4, - } - ) + expected = { + "optimality_gap": 4.0, + "max_iteration": 123, + "uc_type": UcType.EXPANSION_FAST, + "master": Master.INTEGER, + "yearly-weights": None, + "additional-constraints": None, + "relaxed-optimality-gap": "1.2%", + "cut-type": None, + "ampl.solver": None, + "ampl.presolve": None, + "ampl.solve_bounds_frequency": None, + "relative_gap": 1e-12, + "batch_size": 4, + "separation_parameter": 0.5, + "solver": Solver.CBC, + "timelimit": 1000000000000, + "log_level": 0, + "sensitivity_config": {"epsilon": 10000.0, "projection": [], "capex": False}, + } + + new_settings = XpansionSettingsDTO(**expected) xpansion_manager.update_xpansion_settings(study, new_settings) - assert xpansion_manager.get_xpansion_settings(study) == new_settings + actual = xpansion_manager.get_xpansion_settings(study) + assert actual.dict(by_alias=True) == expected @pytest.mark.unit_test -def test_add_candidate(tmp_path: Path): +def test_add_candidate(tmp_path: Path) -> None: empty_study = make_empty_study(tmp_path, 810) study = RawStudy(id="1", path=empty_study.config.study_path, version=810) xpansion_manager = make_xpansion_manager(empty_study) @@ -343,7 +361,7 @@ def test_add_candidate(tmp_path: Path): @pytest.mark.unit_test -def test_get_candidate(tmp_path: Path): +def test_get_candidate(tmp_path: Path) -> None: empty_study = make_empty_study(tmp_path, 810) study = RawStudy(id="1", path=empty_study.config.study_path, version=810) xpansion_manager = make_xpansion_manager(empty_study) @@ -379,7 +397,7 @@ def test_get_candidate(tmp_path: Path): @pytest.mark.unit_test -def test_get_candidates(tmp_path: Path): +def test_get_candidates(tmp_path: Path) -> None: empty_study = make_empty_study(tmp_path, 810) study = RawStudy(id="1", path=empty_study.config.study_path, version=810) xpansion_manager = make_xpansion_manager(empty_study) @@ -417,7 +435,7 @@ def test_get_candidates(tmp_path: Path): @pytest.mark.unit_test -def test_update_candidates(tmp_path: Path): +def test_update_candidates(tmp_path: Path) -> None: empty_study = make_empty_study(tmp_path, 810) study = RawStudy(id="1", path=empty_study.config.study_path, version=810) xpansion_manager = make_xpansion_manager(empty_study) @@ -451,7 +469,7 @@ def test_update_candidates(tmp_path: Path): @pytest.mark.unit_test -def test_delete_candidate(tmp_path: Path): +def test_delete_candidate(tmp_path: Path) -> None: empty_study = make_empty_study(tmp_path, 810) study = RawStudy(id="1", path=empty_study.config.study_path, version=810) xpansion_manager = make_xpansion_manager(empty_study) @@ -487,7 +505,7 @@ def test_delete_candidate(tmp_path: Path): @pytest.mark.unit_test -def test_update_constraints(tmp_path: Path): +def test_update_constraints(tmp_path: Path) -> None: empty_study = make_empty_study(tmp_path, 810) study = RawStudy(id="1", path=empty_study.config.study_path, version=810) xpansion_manager = make_xpansion_manager(empty_study) @@ -510,7 +528,7 @@ def test_update_constraints(tmp_path: Path): @pytest.mark.unit_test -def test_add_resources(tmp_path: Path): +def test_add_resources(tmp_path: Path) -> None: empty_study = make_empty_study(tmp_path, 810) study = RawStudy(id="1", path=empty_study.config.study_path, version=810) xpansion_manager = make_xpansion_manager(empty_study) @@ -524,8 +542,8 @@ def test_add_resources(tmp_path: Path): content3 = "2" upload_file_list = [ - UploadFile(filename=filename1, file=StringIO(content1)), - UploadFile(filename=filename2, file=StringIO(content2)), + UploadFile(filename=filename1, file=io.StringIO(content1)), + UploadFile(filename=filename2, file=io.StringIO(content2)), ] xpansion_manager.add_resource(study, XpansionResourceFileType.CONSTRAINTS, upload_file_list) @@ -533,14 +551,16 @@ def test_add_resources(tmp_path: Path): xpansion_manager.add_resource( study, XpansionResourceFileType.WEIGHTS, - [UploadFile(filename=filename3, file=StringIO(content3))], + [UploadFile(filename=filename3, file=io.StringIO(content3))], ) assert filename1 in empty_study.tree.get(["user", "expansion", "constraints"]) - assert content1.encode() == empty_study.tree.get(["user", "expansion", "constraints", filename1]) + expected1 = empty_study.tree.get(["user", "expansion", "constraints", filename1]) + assert content1.encode() == t.cast(bytes, expected1) assert filename2 in empty_study.tree.get(["user", "expansion", "constraints"]) - assert content2.encode() == empty_study.tree.get(["user", "expansion", "constraints", filename2]) + expected2 = empty_study.tree.get(["user", "expansion", "constraints", filename2]) + assert content2.encode() == t.cast(bytes, expected2) assert filename3 in empty_study.tree.get(["user", "expansion", "weights"]) assert { @@ -560,7 +580,7 @@ def test_add_resources(tmp_path: Path): @pytest.mark.unit_test -def test_list_root_resources(tmp_path: Path): +def test_list_root_resources(tmp_path: Path) -> None: empty_study = make_empty_study(tmp_path, 810) study = RawStudy(id="1", path=empty_study.config.study_path, version=810) xpansion_manager = make_xpansion_manager(empty_study) @@ -573,7 +593,7 @@ def test_list_root_resources(tmp_path: Path): @pytest.mark.unit_test -def test_get_single_constraints(tmp_path: Path): +def test_get_single_constraints(tmp_path: Path) -> None: empty_study = make_empty_study(tmp_path, 810) study = RawStudy(id="1", path=empty_study.config.study_path, version=810) xpansion_manager = make_xpansion_manager(empty_study) @@ -591,7 +611,7 @@ def test_get_single_constraints(tmp_path: Path): @pytest.mark.unit_test -def test_get_all_constraints(tmp_path: Path): +def test_get_all_constraints(tmp_path: Path) -> None: empty_study = make_empty_study(tmp_path, 810) study = RawStudy(id="1", path=empty_study.config.study_path, version=810) xpansion_manager = make_xpansion_manager(empty_study) @@ -603,8 +623,8 @@ def test_get_all_constraints(tmp_path: Path): content2 = "1" upload_file_list = [ - UploadFile(filename=filename1, file=StringIO(content1)), - UploadFile(filename=filename2, file=StringIO(content2)), + UploadFile(filename=filename1, file=io.StringIO(content1)), + UploadFile(filename=filename2, file=io.StringIO(content2)), ] xpansion_manager.add_resource(study, XpansionResourceFileType.CONSTRAINTS, upload_file_list) @@ -616,7 +636,7 @@ def test_get_all_constraints(tmp_path: Path): @pytest.mark.unit_test -def test_add_capa(tmp_path: Path): +def test_add_capa(tmp_path: Path) -> None: empty_study = make_empty_study(tmp_path, 810) study = RawStudy(id="1", path=empty_study.config.study_path, version=810) xpansion_manager = make_xpansion_manager(empty_study) @@ -628,8 +648,8 @@ def test_add_capa(tmp_path: Path): content2 = "1" upload_file_list = [ - UploadFile(filename=filename1, file=StringIO(content1)), - UploadFile(filename=filename2, file=StringIO(content2)), + UploadFile(filename=filename1, file=io.StringIO(content1)), + UploadFile(filename=filename2, file=io.StringIO(content2)), ] xpansion_manager.add_resource(study, XpansionResourceFileType.CAPACITIES, upload_file_list) @@ -650,7 +670,7 @@ def test_add_capa(tmp_path: Path): @pytest.mark.unit_test -def test_delete_capa(tmp_path: Path): +def test_delete_capa(tmp_path: Path) -> None: empty_study = make_empty_study(tmp_path, 810) study = RawStudy(id="1", path=empty_study.config.study_path, version=810) xpansion_manager = make_xpansion_manager(empty_study) @@ -662,8 +682,8 @@ def test_delete_capa(tmp_path: Path): content2 = "1" upload_file_list = [ - UploadFile(filename=filename1, file=StringIO(content1)), - UploadFile(filename=filename2, file=StringIO(content2)), + UploadFile(filename=filename1, file=io.StringIO(content1)), + UploadFile(filename=filename2, file=io.StringIO(content2)), ] xpansion_manager.add_resource(study, XpansionResourceFileType.CAPACITIES, upload_file_list) @@ -676,7 +696,7 @@ def test_delete_capa(tmp_path: Path): @pytest.mark.unit_test -def test_get_single_capa(tmp_path: Path): +def test_get_single_capa(tmp_path: Path) -> None: empty_study = make_empty_study(tmp_path, 810) study = RawStudy(id="1", path=empty_study.config.study_path, version=810) xpansion_manager = make_xpansion_manager(empty_study) @@ -688,8 +708,8 @@ def test_get_single_capa(tmp_path: Path): content2 = "3\nbc\td" upload_file_list = [ - UploadFile(filename=filename1, file=StringIO(content1)), - UploadFile(filename=filename2, file=StringIO(content2)), + UploadFile(filename=filename1, file=io.StringIO(content1)), + UploadFile(filename=filename2, file=io.StringIO(content2)), ] xpansion_manager.add_resource(study, XpansionResourceFileType.CAPACITIES, upload_file_list) @@ -704,7 +724,7 @@ def test_get_single_capa(tmp_path: Path): @pytest.mark.unit_test -def test_get_all_capa(tmp_path: Path): +def test_get_all_capa(tmp_path: Path) -> None: empty_study = make_empty_study(tmp_path, 810) study = RawStudy(id="1", path=empty_study.config.study_path, version=810) xpansion_manager = make_xpansion_manager(empty_study) @@ -716,8 +736,8 @@ def test_get_all_capa(tmp_path: Path): content2 = "1" upload_file_list = [ - UploadFile(filename=filename1, file=StringIO(content1)), - UploadFile(filename=filename2, file=StringIO(content2)), + UploadFile(filename=filename1, file=io.StringIO(content1)), + UploadFile(filename=filename2, file=io.StringIO(content2)), ] xpansion_manager.add_resource(study, XpansionResourceFileType.CAPACITIES, upload_file_list) diff --git a/tests/storage/rawstudies/test_factory.py b/tests/storage/rawstudies/test_factory.py index 823d26cf79..b3fd356f4f 100644 --- a/tests/storage/rawstudies/test_factory.py +++ b/tests/storage/rawstudies/test_factory.py @@ -13,7 +13,7 @@ def test_renewable_subtree(): path = Path(__file__).parent / "samples/v810/sample1" context: ContextServer = Mock(specs=ContextServer) config = build(path, "") - assert config.get_renewable_names("area") == ["la_rochelle", "oleron"] + assert config.get_renewable_ids("area") == ["la_rochelle", "oleron"] tree = FileStudyTree(context, config) json_tree = tree.get([], depth=-1) diff --git a/tests/storage/repository/filesystem/config/test_config_files.py b/tests/storage/repository/filesystem/config/test_config_files.py index e0e0ccb45f..1b87692900 100644 --- a/tests/storage/repository/filesystem/config/test_config_files.py +++ b/tests/storage/repository/filesystem/config/test_config_files.py @@ -1,13 +1,18 @@ +import logging from pathlib import Path from typing import Any, Dict from zipfile import ZipFile import pytest -from antarest.study.storage.rawstudy.model.filesystem.config.binding_constraint import BindingConstraintFrequency +from antarest.study.storage.rawstudy.model.filesystem.config.binding_constraint import ( + BindingConstraintDTO, + BindingConstraintFrequency, +) from antarest.study.storage.rawstudy.model.filesystem.config.files import ( _parse_links, _parse_outputs, + _parse_renewables, _parse_sets, _parse_st_storage, _parse_thermal, @@ -15,14 +20,14 @@ ) from antarest.study.storage.rawstudy.model.filesystem.config.model import ( Area, - BindingConstraintDTO, - Cluster, DistrictSet, FileStudyTreeConfig, Link, Simulation, ) +from antarest.study.storage.rawstudy.model.filesystem.config.renewable import RenewableConfig from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import STStorageConfig, STStorageGroup +from antarest.study.storage.rawstudy.model.filesystem.config.thermal import Thermal860Config, ThermalConfig from tests.storage.business.assets import ASSETS_DIR @@ -265,28 +270,121 @@ def test_parse_area(tmp_path: Path) -> None: assert build(study_path, "id") == config +# noinspection SpellCheckingInspection +THERMAL_LIST_INI = """\ +[t1] +name = t1 + +[t2] +name = UPPER2 +enabled = false + +[UPPER3] +name = UPPER3 +enabled = true +nominalcapacity = 456.5 +""" + + def test_parse_thermal(tmp_path: Path) -> None: study_path = build_empty_files(tmp_path) - (study_path / "input/thermal/clusters/fr").mkdir(parents=True) - content = """ - [t1] - name = t1 - - [t2] - name = t2 - enabled = false + study_path.joinpath("study.antares").write_text("[antares] \n version = 700") + ini_path = study_path.joinpath("input/thermal/clusters/fr/list.ini") + + # Error case: `input/thermal/clusters/fr` directory is missing. + assert not ini_path.parent.exists() + actual = _parse_thermal(study_path, "fr") + assert actual == [] + + # Error case: `list.ini` is missing. + ini_path.parent.mkdir(parents=True) + actual = _parse_thermal(study_path, "fr") + assert actual == [] + + # Nominal case: `list.ini` is found. + ini_path.write_text(THERMAL_LIST_INI) + actual = _parse_thermal(study_path, "fr") + expected = [ + ThermalConfig(id="t1", name="t1", enabled=True), + ThermalConfig(id="t2", name="UPPER2", enabled=False), + ThermalConfig(id="UPPER3", name="UPPER3", enabled=True, nominal_capacity=456.5), + ] + assert actual == expected + + +# noinspection SpellCheckingInspection +THERMAL_860_LIST_INI = """\ +[t1] +name = t1 + +[t2] +name = t2 +co2 = 156 +nh3 = 456 +""" + + +@pytest.mark.parametrize("version", [850, 860, 870]) +def test_parse_thermal_860(tmp_path: Path, version, caplog) -> None: + study_path = build_empty_files(tmp_path) + study_path.joinpath("study.antares").write_text(f"[antares] \n version = {version}") + ini_path = study_path.joinpath("input/thermal/clusters/fr/list.ini") + ini_path.parent.mkdir(parents=True) + ini_path.write_text(THERMAL_860_LIST_INI) + with caplog.at_level(logging.WARNING): + actual = _parse_thermal(study_path, "fr") + if version >= 860: + expected = [ + Thermal860Config(id="t1", name="t1"), + Thermal860Config(id="t2", name="t2", co2=156, nh3=456), + ] + assert not caplog.text + else: + expected = [ThermalConfig(id="t1", name="t1")] + assert "extra fields not permitted" in caplog.text + assert actual == expected - [t3] - name = t3 - enabled = true - """ - (study_path / "input/thermal/clusters/fr/list.ini").write_text(content) - assert _parse_thermal(study_path, "fr") == [ - Cluster(id="t1", name="t1", enabled=True), - Cluster(id="t2", name="t2", enabled=False), - Cluster(id="t3", name="t3", enabled=True), +# noinspection SpellCheckingInspection +REWABLES_LIST_INI = """\ +[t1] +name = t1 + +[t2] +name = UPPER2 +enabled = false + +[UPPER3] +name = UPPER3 +enabled = true +nominalcapacity = 456.5 +""" + + +def test_parse_renewables(tmp_path: Path) -> None: + study_path = build_empty_files(tmp_path) + study_path.joinpath("study.antares").write_text("[antares] \n version = 700") + ini_path = study_path.joinpath("input/renewables/clusters/fr/list.ini") + + # Error case: `input/renewables/clusters/fr` directory is missing. + assert not ini_path.parent.exists() + actual = _parse_renewables(study_path, "fr") + assert actual == [] + + # Error case: `list.ini` is missing. + ini_path.parent.mkdir(parents=True) + actual = _parse_renewables(study_path, "fr") + assert actual == [] + + # Nominal case: `list.ini` is found. + ini_path.write_text(REWABLES_LIST_INI) + actual = _parse_renewables(study_path, "fr") + expected = [ + RenewableConfig(id="t1", name="t1", enabled=True), + RenewableConfig(id="t2", name="UPPER2", enabled=False), + RenewableConfig(id="UPPER3", name="UPPER3", enabled=True, nominal_capacity=456.5), ] + assert actual == expected # noinspection SpellCheckingInspection @@ -308,7 +406,7 @@ def test_parse_thermal(tmp_path: Path) -> None: withdrawalnominalcapacity = 1800.0 reservoircapacity = 20000.0 efficiency = 0.78 -initiallevel = 10000.0 +initiallevel = 0.91 initialleveloptim = False """ @@ -340,7 +438,7 @@ def test_parse_st_storage(tmp_path: Path) -> None: withdrawal_nominal_capacity=1800.0, reservoir_capacity=20000.0, efficiency=0.78, - initial_level=10000.0, + initial_level=0.91, initial_level_optim=False, ), ] diff --git a/tests/storage/repository/filesystem/test_scenariobuilder.py b/tests/storage/repository/filesystem/test_scenariobuilder.py index 8ee9ed7e1b..bbce3c29c7 100644 --- a/tests/storage/repository/filesystem/test_scenariobuilder.py +++ b/tests/storage/repository/filesystem/test_scenariobuilder.py @@ -1,7 +1,8 @@ from pathlib import Path from unittest.mock import Mock -from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, Cluster, FileStudyTreeConfig +from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, FileStudyTreeConfig +from antarest.study.storage.rawstudy.model.filesystem.config.thermal import ThermalConfig from antarest.study.storage.rawstudy.model.filesystem.root.settings.scenariobuilder import ScenarioBuilder content = """ @@ -66,15 +67,15 @@ def test_get(tmp_path: Path): path.write_text(content) thermals = [ - Cluster(id="01_solar", name="01_solar", enabled=True), - Cluster(id="02_wind_on", name="02_wind_on", enabled=True), - Cluster(id="03_wind_off", name="03_wind_off", enabled=True), - Cluster(id="04_res", name="04_res", enabled=True), - Cluster(id="05_nuclear", name="05_nuclear", enabled=True), - Cluster(id="06_coal", name="06_coal", enabled=True), - Cluster(id="07_gas", name="07_gas", enabled=True), - Cluster(id="08_non-res", name="08_non-res", enabled=True), - Cluster(id="09_hydro_pump", name="09_hydro_pump", enabled=True), + ThermalConfig(id="01_solar", name="01_solar", enabled=True), + ThermalConfig(id="02_wind_on", name="02_wind_on", enabled=True), + ThermalConfig(id="03_wind_off", name="03_wind_off", enabled=True), + ThermalConfig(id="04_res", name="04_res", enabled=True), + ThermalConfig(id="05_nuclear", name="05_nuclear", enabled=True), + ThermalConfig(id="06_coal", name="06_coal", enabled=True), + ThermalConfig(id="07_gas", name="07_gas", enabled=True), + ThermalConfig(id="08_non-res", name="08_non-res", enabled=True), + ThermalConfig(id="09_hydro_pump", name="09_hydro_pump", enabled=True), ] areas = { diff --git a/tests/storage/test_service.py b/tests/storage/test_service.py index bc5dac87d1..60b35e2b5e 100644 --- a/tests/storage/test_service.py +++ b/tests/storage/test_service.py @@ -863,7 +863,6 @@ def test_assert_permission() -> None: # when study found in workspace without group study = Study(id=uuid, public_mode=PublicMode.FULL) assert not assert_permission(jwt, study, StudyPermissionType.MANAGE_PERMISSIONS, raising=False) - assert assert_permission(jwt, study, StudyPermissionType.DELETE) assert assert_permission(jwt, study, StudyPermissionType.READ) assert assert_permission(jwt, study, StudyPermissionType.WRITE) assert assert_permission(jwt, study, StudyPermissionType.RUN) @@ -1575,6 +1574,9 @@ def test_upgrade_study__raw_study__nominal( study_id = str(uuid.uuid4()) study_name = "my_study" target_version = "800" + current_version = "720" + (tmp_path / "study.antares").touch() + (tmp_path / "study.antares").write_text(f"version = {current_version}") # Prepare a RAW study # noinspection PyArgumentList @@ -1585,7 +1587,7 @@ def test_upgrade_study__raw_study__nominal( path=str(tmp_path), created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), - version="720", + version=current_version, additional_data=StudyAdditionalData(), archived=False, owner=None, @@ -1752,6 +1754,8 @@ def test_upgrade_study__raw_study__failed(upgrade_study_mock: Mock, tmp_path: Pa 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}") # Prepare a RAW study # noinspection PyArgumentList diff --git a/tests/study/business/areas/__init__.py b/tests/study/business/areas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/study/business/areas/assets/__init__.py b/tests/study/business/areas/assets/__init__.py new file mode 100644 index 0000000000..773f16ec60 --- /dev/null +++ b/tests/study/business/areas/assets/__init__.py @@ -0,0 +1,3 @@ +from pathlib import Path + +ASSETS_DIR = Path(__file__).parent.resolve() diff --git a/tests/study/business/areas/assets/thermal_management/study_legacy.zip b/tests/study/business/areas/assets/thermal_management/study_legacy.zip new file mode 100644 index 0000000000..d3a1c05205 Binary files /dev/null and b/tests/study/business/areas/assets/thermal_management/study_legacy.zip differ diff --git a/tests/study/business/test_st_storage_manager.py b/tests/study/business/areas/test_st_storage_management.py similarity index 98% rename from tests/study/business/test_st_storage_manager.py rename to tests/study/business/areas/test_st_storage_management.py index 8c9ad36b31..87a98d1eaf 100644 --- a/tests/study/business/test_st_storage_manager.py +++ b/tests/study/business/areas/test_st_storage_management.py @@ -17,7 +17,7 @@ ) from antarest.core.model import PublicMode from antarest.login.model import Group, User -from antarest.study.business.st_storage_manager import STStorageManager +from antarest.study.business.areas.st_storage_management import STStorageManager from antarest.study.model import RawStudy, Study, StudyContentStatus from antarest.study.storage.rawstudy.io.reader import IniReader from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import STStorageGroup @@ -47,7 +47,7 @@ withdrawalnominalcapacity = 1500 reservoircapacity = 20000 efficiency = 0.78 -initiallevel = 10000 +initiallevel = 0.5 [storage3] name = Storage3 @@ -56,7 +56,7 @@ withdrawalnominalcapacity = 1500 reservoircapacity = 21000 efficiency = 0.72 -initiallevel = 20000 +initiallevel = 1 """ LIST_CFG = IniReader().read(io.StringIO(LIST_INI)) @@ -137,7 +137,7 @@ def test_get_st_storages__nominal_case( "efficiency": 0.94, "group": STStorageGroup.BATTERY, "id": "storage1", - "initialLevel": 0.0, + "initialLevel": 0.5, "initialLevelOptim": True, "injectionNominalCapacity": 1500.0, "name": "Storage1", @@ -148,7 +148,7 @@ def test_get_st_storages__nominal_case( "efficiency": 0.78, "group": STStorageGroup.PSP_CLOSED, "id": "storage2", - "initialLevel": 10000.0, + "initialLevel": 0.5, "initialLevelOptim": False, "injectionNominalCapacity": 2000.0, "name": "Storage2", @@ -159,7 +159,7 @@ def test_get_st_storages__nominal_case( "efficiency": 0.72, "group": STStorageGroup.PSP_CLOSED, "id": "storage3", - "initialLevel": 20000.0, + "initialLevel": 1, "initialLevelOptim": False, "injectionNominalCapacity": 1500.0, "name": "Storage3", @@ -245,7 +245,7 @@ def test_get_st_storage__nominal_case( "efficiency": 0.94, "group": STStorageGroup.BATTERY, "id": "storage1", - "initialLevel": 0.0, + "initialLevel": 0.5, "initialLevelOptim": True, "injectionNominalCapacity": 1500.0, "name": "Storage1", diff --git a/tests/study/business/areas/test_thermal_management.py b/tests/study/business/areas/test_thermal_management.py new file mode 100644 index 0000000000..da451759c3 --- /dev/null +++ b/tests/study/business/areas/test_thermal_management.py @@ -0,0 +1,436 @@ +import datetime +import re +import shutil +import uuid +import zipfile +from pathlib import Path + +import pytest +from sqlalchemy.orm.session import Session # type: ignore + +from antarest.core.exceptions import CommandApplicationError +from antarest.core.model import PublicMode +from antarest.core.utils.fastapi_sqlalchemy import db +from antarest.login.model import Group, User +from antarest.study.business.areas.thermal_management import ThermalClusterCreation, ThermalClusterInput, ThermalManager +from antarest.study.model import RawStudy, Study, StudyAdditionalData, StudyContentStatus +from antarest.study.storage.rawstudy.model.filesystem.config.thermal import ( + LawOption, + ThermalClusterGroup, + TimeSeriesGenerationOption, +) +from antarest.study.storage.storage_service import StudyStorageService +from tests.study.business.areas.assets import ASSETS_DIR + + +@pytest.fixture(name="zip_legacy_path") +def zip_legacy_path_fixture(tmp_path: Path) -> Path: + target_dir = tmp_path.joinpath("resources") + target_dir.mkdir() + resource_zip = ASSETS_DIR.joinpath("thermal_management/study_legacy.zip") + shutil.copy2(resource_zip, target_dir) + return target_dir.joinpath(resource_zip.name) + + +@pytest.fixture(name="metadata_legacy") +def metadata_legacy_fixture(tmp_path: Path, zip_legacy_path: Path) -> RawStudy: + with zipfile.ZipFile(zip_legacy_path, mode="r") as zf: + content = zf.read("study.antares").decode("utf-8") + config = dict(re.findall(r"^(\w+)\s*=\s*(.*?)$", content, flags=re.I | re.M)) + + workspace_dir = tmp_path.joinpath("studies") + workspace_dir.mkdir() + + # noinspection PyArgumentList,SpellCheckingInspection + metadata = RawStudy( + id=str(uuid.uuid4()), + name=config["caption"], + version=config["version"], + author=config["author"], + created_at=datetime.datetime.fromtimestamp(int(config["created"]), datetime.timezone.utc), + updated_at=datetime.datetime.fromtimestamp(int(config["lastsave"]), datetime.timezone.utc), + public_mode=PublicMode.FULL, + workspace="default", + path=str(workspace_dir.joinpath(config["caption"])), + content_status=StudyContentStatus.VALID, + additional_data=StudyAdditionalData(author=config["author"]), + ) + + return metadata + + +# noinspection PyArgumentList +@pytest.fixture(name="study_legacy_uuid") +def study_legacy_uuid_fixture( + zip_legacy_path: Path, + metadata_legacy: RawStudy, + study_storage_service: StudyStorageService, + db_session: Session, +) -> str: + study_id = metadata_legacy.id + metadata_legacy.user = User(id=1, name="admin") + metadata_legacy.groups = [Group(id="my-group", name="group")] + db_session.add(metadata_legacy) + db_session.commit() + + with db_session: + metadata = db_session.query(Study).get(study_id) + with open(zip_legacy_path, mode="rb") as fd: + study_storage_service.raw_study_service.import_study(metadata, fd) + + return study_id + + +class TestThermalManager: + def test_get_cluster__study_legacy( + self, + db_session: Session, + study_storage_service: StudyStorageService, + study_legacy_uuid: str, + ): + """ + Given a legacy study with a thermal cluster, + When we get the cluster, + Then we should get the cluster properties with the correct name and ID. + Every property related to version 860 or above should be None. + """ + # The study must be fetched from the database + study: RawStudy = db_session.query(Study).get(study_legacy_uuid) + + # Given the following arguments + manager = ThermalManager(study_storage_service) + + # Run the method being tested + form = manager.get_cluster(study, area_id="north", cluster_id="2 avail and must 1") + + # Assert that the returned fields match the expected fields + actual = form.dict(by_alias=True) + expected = { + "id": "2 avail and must 1", + "group": ThermalClusterGroup.GAS, + "name": "2 avail and must 1", + "enabled": False, + "unitCount": 100, + "nominalCapacity": 1000.0, + "genTs": TimeSeriesGenerationOption.USE_GLOBAL_PARAMETER, + "minStablePower": 0.0, + "minUpTime": 1, + "minDownTime": 1, + "mustRun": True, + "spinning": 0.0, + "volatilityForced": 0.0, + "volatilityPlanned": 0.0, + "lawForced": LawOption.UNIFORM, + "lawPlanned": LawOption.UNIFORM, + "marginalCost": 0.0, + "spreadCost": 0.0, + "fixedCost": 0.0, + "startupCost": 0.0, + "marketBidCost": 0.0, + "co2": 7.0, + # Pollutant values are `None` because they are not defined before version 8.6. + "nh3": None, + "so2": None, + "nox": None, + "pm25": None, + "pm5": None, + "pm10": None, + "nmvoc": None, + "op1": None, + "op2": None, + "op3": None, + "op4": None, + "op5": None, + } + assert actual == expected + + def test_get_clusters__study_legacy( + self, + db_session: Session, + study_storage_service: StudyStorageService, + study_legacy_uuid: str, + ): + """ + Given a legacy study with thermal clusters, + When we get the clusters, + Then we should get all cluster properties with the correct names and IDs. + Every property related to version 860 or above should be None. + """ + # The study must be fetched from the database + study: RawStudy = db_session.query(Study).get(study_legacy_uuid) + + # Given the following arguments + manager = ThermalManager(study_storage_service) + + # Run the method being tested + groups = manager.get_clusters(study, area_id="north") + + # Assert that the returned fields match the expected fields + actual = [form.dict(by_alias=True) for form in groups] + expected = [ + { + "id": "2 avail and must 1", + "group": ThermalClusterGroup.GAS, + "name": "2 avail and must 1", + "enabled": False, + "unitCount": 100, + "nominalCapacity": 1000.0, + "genTs": TimeSeriesGenerationOption.USE_GLOBAL_PARAMETER, + "minStablePower": 0.0, + "minUpTime": 1, + "minDownTime": 1, + "mustRun": True, + "spinning": 0.0, + "volatilityForced": 0.0, + "volatilityPlanned": 0.0, + "lawForced": LawOption.UNIFORM, + "lawPlanned": LawOption.UNIFORM, + "marginalCost": 0.0, + "spreadCost": 0.0, + "fixedCost": 0.0, + "startupCost": 0.0, + "marketBidCost": 0.0, + "co2": 7.0, + "nh3": None, + "so2": None, + "nox": None, + "pm25": None, + "pm5": None, + "pm10": None, + "nmvoc": None, + "op1": None, + "op2": None, + "op3": None, + "op4": None, + "op5": None, + }, + { + "id": "on and must 2", + "group": ThermalClusterGroup.HARD_COAL, + "name": "on and must 2", + "enabled": True, + "unitCount": 100, + "nominalCapacity": 2300.0, + "genTs": TimeSeriesGenerationOption.USE_GLOBAL_PARAMETER, + "minStablePower": 0.0, + "minUpTime": 1, + "minDownTime": 1, + "mustRun": True, + "spinning": 0.0, + "volatilityForced": 0.0, + "volatilityPlanned": 0.0, + "lawForced": LawOption.UNIFORM, + "lawPlanned": LawOption.UNIFORM, + "marginalCost": 0.0, + "spreadCost": 0.0, + "fixedCost": 0.0, + "startupCost": 0.0, + "marketBidCost": 0.0, + "co2": 0.0, + "nh3": None, + "so2": None, + "nox": None, + "pm25": None, + "pm5": None, + "pm10": None, + "nmvoc": None, + "op1": None, + "op2": None, + "op3": None, + "op4": None, + "op5": None, + }, + { + "id": "2 avail and must 2", + "group": ThermalClusterGroup.GAS, + "name": "2 avail and must 2", + "enabled": False, + "unitCount": 100, + "nominalCapacity": 1500.0, + "genTs": TimeSeriesGenerationOption.USE_GLOBAL_PARAMETER, + "minStablePower": 0.0, + "minUpTime": 1, + "minDownTime": 1, + "mustRun": True, + "spinning": 0.0, + "volatilityForced": 0.0, + "volatilityPlanned": 0.0, + "lawForced": LawOption.UNIFORM, + "lawPlanned": LawOption.UNIFORM, + "marginalCost": 0.0, + "spreadCost": 0.0, + "fixedCost": 0.0, + "startupCost": 0.0, + "marketBidCost": 0.0, + "co2": 0.0, + "nh3": None, + "so2": None, + "nox": None, + "pm25": None, + "pm5": None, + "pm10": None, + "nmvoc": None, + "op1": None, + "op2": None, + "op3": None, + "op4": None, + "op5": None, + }, + ] + assert actual == expected + + def test_create_cluster__study_legacy( + self, + study_storage_service: StudyStorageService, + study_legacy_uuid: str, + ): + """ + Given a legacy study, + When we create a new thermal cluster, + Then we should get the cluster properties with the correct name and ID. + Every property related to version 860 or above should be None. + """ + with db(): + # The study must be fetched from the database + study: RawStudy = db.session.query(Study).get(study_legacy_uuid) + + # Given the following arguments + manager = ThermalManager(study_storage_service) + + props = dict( + name="New Cluster", + group=ThermalClusterGroup.NUCLEAR, + enabled=True, + unitCount=350, + nominalCapacity=1000, + genTs=TimeSeriesGenerationOption.USE_GLOBAL_PARAMETER, + minStablePower=0, + minUpTime=15, + minDownTime=20, + co2=12.59, + ) + cluster_data = ThermalClusterCreation(**props) + form = manager.create_cluster(study, area_id="north", cluster_data=cluster_data) + + # Assert that the returned fields match the expected fields + actual = form.dict(by_alias=True) + expected = { + "co2": 12.59, + "enabled": True, + "fixedCost": 0.0, + "genTs": TimeSeriesGenerationOption.USE_GLOBAL_PARAMETER, + "group": ThermalClusterGroup.NUCLEAR, + "id": "New Cluster", + "lawForced": LawOption.UNIFORM, + "lawPlanned": LawOption.UNIFORM, + "marginalCost": 0.0, + "marketBidCost": 0.0, + "minDownTime": 20, + "minStablePower": 0.0, + "minUpTime": 15, + "mustRun": False, + "name": "New Cluster", + "nh3": None, + "nmvoc": None, + "nominalCapacity": 1000.0, + "nox": None, + "op1": None, + "op2": None, + "op3": None, + "op4": None, + "op5": None, + "pm10": None, + "pm25": None, + "pm5": None, + "so2": None, + "spinning": 0.0, + "spreadCost": 0.0, + "startupCost": 0.0, + "unitCount": 350, + "volatilityForced": 0.0, + "volatilityPlanned": 0.0, + } + assert actual == expected + + def test_update_cluster( + self, + study_storage_service: StudyStorageService, + study_legacy_uuid: str, + ): + with db(): + # The study must be fetched from the database + study: RawStudy = db.session.query(Study).get(study_legacy_uuid) + + # Given the following arguments + manager = ThermalManager(study_storage_service) + + # When some properties of the cluster are updated + cluster_data = ThermalClusterInput(name="New name", nominalCapacity=2000) + manager.update_cluster(study, area_id="north", cluster_id="2 avail and must 1", cluster_data=cluster_data) + + # Assert that the returned fields match the expected fields + form = manager.get_cluster(study, area_id="north", cluster_id="2 avail and must 1") + actual = form.dict(by_alias=True) + expected = { + "id": "2 avail and must 1", + "group": ThermalClusterGroup.GAS, + "name": "New name", + "enabled": False, + "unitCount": 100, + "nominalCapacity": 2000.0, + "genTs": TimeSeriesGenerationOption.USE_GLOBAL_PARAMETER, + "minStablePower": 0.0, + "minUpTime": 1, + "minDownTime": 1, + "mustRun": True, + "spinning": 0.0, + "volatilityForced": 0.0, + "volatilityPlanned": 0.0, + "lawForced": LawOption.UNIFORM, + "lawPlanned": LawOption.UNIFORM, + "marginalCost": 0.0, + "spreadCost": 0.0, + "fixedCost": 0.0, + "startupCost": 0.0, + "marketBidCost": 0.0, + "co2": 7.0, + # Pollutant values are `None` because they are not defined before version 8.6. + "nh3": None, + "so2": None, + "nox": None, + "pm25": None, + "pm5": None, + "pm10": None, + "nmvoc": None, + "op1": None, + "op2": None, + "op3": None, + "op4": None, + "op5": None, + } + assert actual == expected + + def test_delete_clusters( + self, + study_storage_service: StudyStorageService, + study_legacy_uuid: str, + ): + with db(): + # The study must be fetched from the database + study: RawStudy = db.session.query(Study).get(study_legacy_uuid) + + # Given the following arguments + manager = ThermalManager(study_storage_service) + + # When the clusters are deleted + manager.delete_clusters(study, area_id="north", cluster_ids=["2 avail and must 1", "on and must 2"]) + + # Assert that the returned fields match the expected fields + groups = manager.get_clusters(study, area_id="north") + actual = [form.id for form in groups] + expected = ["2 avail and must 2"] + assert actual == expected + + # A second attempt should raise an error + with pytest.raises(CommandApplicationError): + manager.delete_clusters(study, area_id="north", cluster_ids=["2 avail and must 1"]) diff --git a/tests/variantstudy/model/command/test_create_st_storage.py b/tests/variantstudy/model/command/test_create_st_storage.py index 6647235319..c430b92eeb 100644 --- a/tests/variantstudy/model/command/test_create_st_storage.py +++ b/tests/variantstudy/model/command/test_create_st_storage.py @@ -5,16 +5,13 @@ from pydantic import ValidationError 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.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 -from antarest.study.storage.variantstudy.model.command.create_st_storage import ( - REQUIRED_VERSION, - CreateSTStorage, - STStorageConfig, -) +from antarest.study.storage.variantstudy.model.command.create_st_storage import REQUIRED_VERSION, CreateSTStorage from antarest.study.storage.variantstudy.model.command.replace_matrix import ReplaceMatrix from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig from antarest.study.storage.variantstudy.model.command_context import CommandContext @@ -106,7 +103,7 @@ def test_init__invalid_storage_name(self, recent_study: FileStudy, command_conte assert ctx.value.errors() == [ { "loc": ("__root__",), - "msg": "Invalid short term storage name '?%$$'.", + "msg": "Invalid name '?%$$'.", "type": "value_error", } ] @@ -366,8 +363,8 @@ def test_to_dto(self, command_context: CommandContext): actual = cmd.to_dto() expected_parameters = PARAMETERS.copy() - # `initiallevel` = 0 because `initialleveloptim` is True - expected_parameters["initiallevel"] = 0 + # `initiallevel` = 0.5 (the default value) because `initialleveloptim` is True + expected_parameters["initiallevel"] = 0.5 constants = command_context.generator_matrix_constants assert actual == CommandDTO( diff --git a/tests/variantstudy/model/command/test_remove_cluster.py b/tests/variantstudy/model/command/test_remove_cluster.py index e4525fbc36..99333d811a 100644 --- a/tests/variantstudy/model/command/test_remove_cluster.py +++ b/tests/variantstudy/model/command/test_remove_cluster.py @@ -13,21 +13,13 @@ class TestRemoveCluster: - def test_validation(self, empty_study: FileStudy): - pass - def test_apply(self, empty_study: FileStudy, command_context: CommandContext): area_name = "Area_name" area_id = transform_name_to_id(area_name) - cluster_name = "cluster_name" - cluster_id = transform_name_to_id(cluster_name) + cluster_name = "Cluster Name" + cluster_id = transform_name_to_id(cluster_name, lower=False) - CreateArea.parse_obj( - { - "area_name": area_name, - "command_context": command_context, - } - ).apply(empty_study) + CreateArea(area_name=area_name, command_context=command_context).apply(empty_study) ################################################################################################ hash_before_cluster = dirhash(empty_study.config.study_path, "md5") @@ -52,7 +44,7 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext): time_step=BindingConstraintFrequency.HOURLY, operator=BindingConstraintOperator.LESS, coeffs={ - f"{area_id}.{cluster_id}": [800, 30], + f"{area_id}.{cluster_id.lower()}": [800, 30], }, comments="Hello", command_context=command_context, diff --git a/tests/variantstudy/model/command/test_remove_renewables_cluster.py b/tests/variantstudy/model/command/test_remove_renewables_cluster.py index b8cda91601..26eaa52837 100644 --- a/tests/variantstudy/model/command/test_remove_renewables_cluster.py +++ b/tests/variantstudy/model/command/test_remove_renewables_cluster.py @@ -15,23 +15,18 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext): empty_study.config.version = 810 area_name = "Area_name" area_id = transform_name_to_id(area_name) - cluster_name = "cluster_name" - cluster_id = transform_name_to_id(cluster_name) + cluster_name = "Cluster Name" + cluster_id = transform_name_to_id(cluster_name, lower=False) - CreateArea.parse_obj( - { - "area_name": area_name, - "command_context": command_context, - } - ).apply(empty_study) + CreateArea(area_name=area_name, command_context=command_context).apply(empty_study) hash_before_cluster = dirhash(empty_study.config.study_path, "md5") CreateRenewablesCluster( area_id=area_id, - cluster_name=cluster_name, + cluster_name=cluster_id, parameters={ - "name": "name", + "name": cluster_name, "ts-interpretation": "power-generation", }, command_context=command_context, diff --git a/tests/variantstudy/test_command_factory.py b/tests/variantstudy/test_command_factory.py index 47e3c57811..aac2be6c59 100644 --- a/tests/variantstudy/test_command_factory.py +++ b/tests/variantstudy/test_command_factory.py @@ -409,7 +409,7 @@ def test_command_factory(self, command_dto: CommandDTO): ) commands = command_factory.to_command(command_dto=command_dto) - if isinstance(args := command_dto.args, dict): + if isinstance(command_dto.args, dict): exp_action_args_list = [(command_dto.action, command_dto.args)] else: exp_action_args_list = [(command_dto.action, args) for args in command_dto.args] diff --git a/tests/xml_compare.py b/tests/xml_compare.py new file mode 100644 index 0000000000..19e36b4a68 --- /dev/null +++ b/tests/xml_compare.py @@ -0,0 +1,32 @@ +import re +import typing as t +from xml.etree.ElementTree import Element + + +def compare_elements(elem1: Element, elem2: Element, parents: t.Tuple[str, ...] = ()) -> str: + # Compare tags + xpath = "".join(f"/{p}" for p in parents) + if elem1.tag != elem2.tag: + return f"Tag mismatch: {xpath} '{elem1.tag}' != '{elem2.tag}'" + + # Compare attributes + if elem1.attrib != elem2.attrib: + return f"Attrib mismatch: {xpath} {elem1.attrib!r} != {elem2.attrib!r}" + + # Compare text content (normalize whitespace) + text1 = re.sub(r"\s+", " ", elem1.text.strip()) if elem1.text else "" + text2 = re.sub(r"\s+", " ", elem2.text.strip()) if elem2.text else "" + if text1 != text2: + return f"Text mismatch: {xpath} {elem1.text!r} != {elem2.text!r}" + + # Compare children + if len(elem1) != len(elem2): + return f"Children mismatch: {xpath} {len(elem1)} != {len(elem2)}" + + parent: str = re.sub(r"^\{[^}]+}", "", elem1.tag) + for child1, child2 in zip(elem1, elem2): + if err_msg := compare_elements(child1, child2, parents=parents + (parent,)): + return err_msg + + # no error + return "" diff --git a/webapp/config-overrides.js b/webapp/config-overrides.js index dc56364b73..d79a2b2c1c 100644 --- a/webapp/config-overrides.js +++ b/webapp/config-overrides.js @@ -19,7 +19,7 @@ module.exports = function override(config, env) { new webpack.ProvidePlugin({ process: "process/browser", Buffer: ["buffer", "Buffer"], - }) + }), ); // eslint-disable-next-line no-param-reassign config.ignoreWarnings = [/Failed to parse source map/]; diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 754f0f27bb..19ba7c4221 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -1,117 +1,117 @@ { "name": "antares-web", - "version": "2.15.6", + "version": "2.16.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "antares-web", - "version": "2.15.6", - "dependencies": { - "@emotion/react": "11.10.6", - "@emotion/styled": "11.10.6", - "@handsontable/react": "12.3.1", - "@mui/icons-material": "5.11.9", - "@mui/lab": "5.0.0-alpha.120", - "@mui/material": "5.11.10", - "@reduxjs/toolkit": "1.9.3", - "@types/d3": "5.16.4", - "@types/draft-convert": "2.1.4", - "@types/draft-js": "0.11.10", - "@types/draftjs-to-html": "0.8.1", - "@types/node": "16.11.20", - "@types/react": "18.0.28", - "@types/react-d3-graph": "2.6.3", - "@types/react-dom": "18.0.11", + "version": "2.16.0", + "dependencies": { + "@emotion/react": "11.11.1", + "@emotion/styled": "11.11.0", + "@handsontable/react": "13.1.0", + "@mui/icons-material": "5.14.11", + "@mui/lab": "5.0.0-alpha.146", + "@mui/material": "5.14.11", + "@reduxjs/toolkit": "1.9.6", + "@types/d3": "5.16.0", + "@types/draft-convert": "2.1.5", + "@types/draft-js": "0.11.13", + "@types/draftjs-to-html": "0.8.2", + "@types/node": "18.16.1", + "@types/react": "18.2.24", + "@types/react-d3-graph": "2.6.5", + "@types/react-dom": "18.2.8", "@types/xml-js": "1.0.0", - "assert": "2.0.0", - "axios": "1.3.3", + "assert": "2.1.0", + "axios": "1.5.1", "buffer": "6.0.3", - "clsx": "1.2.1", + "clsx": "2.0.0", "crypto-browserify": "3.12.0", "d3": "5.16.0", "debug": "4.3.4", "draft-convert": "2.1.13", "draft-js": "0.11.7", "draftjs-to-html": "0.9.1", - "handsontable": "12.3.1", + "handsontable": "13.1.0", "hoist-non-react-statics": "3.3.2", "https-browserify": "1.0.0", - "i18next": "22.4.10", - "i18next-browser-languagedetector": "7.0.1", + "i18next": "23.5.1", + "i18next-browser-languagedetector": "7.1.0", "i18next-xhr-backend": "3.2.2", - "immer": "9.0.19", - "js-cookie": "3.0.1", + "immer": "10.0.3", + "js-cookie": "3.0.5", "jwt-decode": "3.1.2", "lodash": "4.17.21", - "material-react-table": "1.14.0", + "material-react-table": "1.15.0", "moment": "2.29.4", - "notistack": "2.0.8", + "notistack": "3.0.1", "os": "0.1.2", "os-browserify": "0.3.0", - "plotly.js": "2.18.2", - "ramda": "0.28.0", - "ramda-adjunct": "3.4.0", + "plotly.js": "2.26.1", + "ramda": "0.29.0", + "ramda-adjunct": "4.1.1", "react": "18.2.0", "react-beautiful-dnd": "13.1.1", "react-color": "2.19.3", "react-d3-graph": "2.6.0", "react-dom": "18.2.0", "react-dropzone": "14.2.3", - "react-hook-form": "7.43.9", - "react-i18next": "12.1.5", + "react-hook-form": "7.47.0", + "react-i18next": "13.2.2", "react-json-view": "1.21.3", "react-plotly.js": "2.6.0", - "react-redux": "8.0.5", + "react-redux": "8.1.3", "react-router": "6.3.0", "react-router-dom": "6.3.0", "react-scripts": "5.0.1", "react-split": "2.0.14", "react-use": "17.4.0", - "react-virtualized-auto-sizer": "1.0.7", - "react-window": "1.8.8", + "react-virtualized-auto-sizer": "1.0.20", + "react-window": "1.8.9", "redux": "4.2.1", "redux-thunk": "2.4.2", "stream-http": "3.2.0", - "swagger-ui-react": "4.15.5", + "swagger-ui-react": "5.9.0", "ts-toolbelt": "9.6.0", - "url": "0.11.0", + "url": "0.11.3", "use-undo": "1.1.1", - "uuid": "9.0.0", + "uuid": "9.0.1", "xml-js": "1.6.11" }, "devDependencies": { - "@total-typescript/ts-reset": "0.4.2", - "@types/debug": "4.1.7", - "@types/js-cookie": "3.0.3", - "@types/lodash": "4.14.191", - "@types/ramda": "0.28.23", - "@types/react-beautiful-dnd": "13.1.3", - "@types/react-color": "3.0.6", - "@types/react-plotly.js": "2.6.0", + "@total-typescript/ts-reset": "0.5.1", + "@types/debug": "4.1.9", + "@types/js-cookie": "3.0.4", + "@types/lodash": "4.14.199", + "@types/ramda": "0.29.5", + "@types/react-beautiful-dnd": "13.1.5", + "@types/react-color": "3.0.7", + "@types/react-plotly.js": "2.6.1", "@types/react-virtualized-auto-sizer": "1.0.1", - "@types/react-window": "1.8.5", - "@types/redux-logger": "3.0.9", - "@types/swagger-ui-react": "4.11.0", - "@types/uuid": "9.0.0", - "@typescript-eslint/eslint-plugin": "5.53.0", - "@typescript-eslint/parser": "5.53.0", + "@types/react-window": "1.8.6", + "@types/redux-logger": "3.0.10", + "@types/swagger-ui-react": "4.18.1", + "@types/uuid": "9.0.4", + "@typescript-eslint/eslint-plugin": "6.7.3", + "@typescript-eslint/parser": "6.7.3", "cross-env": "7.0.3", - "eslint": "8.34.0", + "eslint": "8.50.0", "eslint-config-airbnb": "19.0.4", - "eslint-config-prettier": "8.6.0", - "eslint-plugin-import": "2.27.5", + "eslint-config-prettier": "9.0.0", + "eslint-plugin-import": "2.28.1", "eslint-plugin-jsx-a11y": "6.7.1", - "eslint-plugin-prettier": "4.2.1", - "eslint-plugin-react": "7.32.2", + "eslint-plugin-prettier": "5.0.0", + "eslint-plugin-react": "7.33.2", "eslint-plugin-react-hooks": "4.6.0", "husky": "8.0.3", "jest-sonar-reporter": "2.0.0", - "prettier": "2.8.4", + "prettier": "3.0.3", "process": "0.11.10", "react-app-rewired": "2.2.1", "stream-browserify": "3.0.0", - "typescript": "4.9.5" + "typescript": "5.2.2" }, "engines": { "node": "18.16.1" @@ -165,11 +165,11 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz", - "integrity": "sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/highlight": "^7.22.10", + "@babel/highlight": "^7.22.13", "chalk": "^2.4.2" }, "engines": { @@ -241,32 +241,32 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", - "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", + "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.10.tgz", - "integrity": "sha512-fTmqbbUBAwCcre6zPzNngvsI0aNrPZe77AeqvDxWM9Nm+04RrJ3CAmGHA9f7lJQY6ZMhRztNemy4uslDxTX4Qw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.0.tgz", + "integrity": "sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ==", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.10", - "@babel/generator": "^7.22.10", - "@babel/helper-compilation-targets": "^7.22.10", - "@babel/helper-module-transforms": "^7.22.9", - "@babel/helpers": "^7.22.10", - "@babel/parser": "^7.22.10", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.10", - "@babel/types": "^7.22.10", - "convert-source-map": "^1.7.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helpers": "^7.23.0", + "@babel/parser": "^7.23.0", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.0", + "@babel/types": "^7.23.0", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", + "json5": "^2.2.3", "semver": "^6.3.1" }, "engines": { @@ -277,6 +277,11 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -286,9 +291,9 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.22.10.tgz", - "integrity": "sha512-0J8DNPRXQRLeR9rPaUMM3fA+RbixjnVLe/MRMYCkp3hzgsSuxCHQ8NN8xQG1wIHKJ4a1DTROTvFJdW+B5/eOsg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.22.15.tgz", + "integrity": "sha512-yc8OOBIQk1EcRrpizuARSQS0TWAcOMpEJ1aafhNznaeYkeL+OhqnDObGFylB8ka8VFF/sZc+S4RzHyO+3LjQxg==", "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", "eslint-visitor-keys": "^2.1.0", @@ -319,11 +324,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.10.tgz", - "integrity": "sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dependencies": { - "@babel/types": "^7.22.10", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -344,23 +349,23 @@ } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.10.tgz", - "integrity": "sha512-Av0qubwDQxC56DoUReVDeLfMEjYYSN1nZrTUrWkXd7hpU73ymRANkbuDm3yni9npkn+RXy9nNbEJZEzXr7xrfQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", "dependencies": { - "@babel/types": "^7.22.10" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz", - "integrity": "sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", "dependencies": { "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", "browserslist": "^4.21.9", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -378,14 +383,14 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.10.tgz", - "integrity": "sha512-5IBb77txKYQPpOEdUdIhBx8VrZyDCQ+H82H0+5dX1TmuscP5vJKEE3cKurjtIw/vFwzbVH48VweE78kVDBrqjA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", + "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-replace-supers": "^7.22.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", @@ -408,9 +413,9 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz", - "integrity": "sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "regexpu-core": "^5.3.1", @@ -447,20 +452,20 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -478,37 +483,37 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", - "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", + "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", - "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", - "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", + "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-simple-access": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.5" + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -537,13 +542,13 @@ } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz", - "integrity": "sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-wrap-function": "^7.22.9" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -553,12 +558,12 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz", - "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", + "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { @@ -610,53 +615,53 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", - "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.10.tgz", - "integrity": "sha512-OnMhjWjuGYtdoO3FmsEFWvBStBAe2QOgwOLsLNDjN+aaiMD8InJk1/O3HSD8lkqTjCgg5YI34Tz15KNNA3p+nQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", + "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", "dependencies": { "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.10" + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.19" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.10.tgz", - "integrity": "sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw==", + "version": "7.23.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.1.tgz", + "integrity": "sha512-chNpneuK18yW5Oxsr+t553UZzzAs3aZnFm4bxhebsNTeshrC95yA7l5yl7GBAG+JG1rF0F7zzD2EixK9mWSDoA==", "dependencies": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.10", - "@babel/types": "^7.22.10" + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.0", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.10.tgz", - "integrity": "sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, @@ -729,9 +734,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.10.tgz", - "integrity": "sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "bin": { "parser": "bin/babel-parser.js" }, @@ -740,9 +745,9 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz", - "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", + "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -754,13 +759,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", - "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", + "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.5" + "@babel/plugin-transform-optional-chaining": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -773,6 +778,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -785,13 +791,13 @@ } }, "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.22.10.tgz", - "integrity": "sha512-KxN6TqZzcFi4uD3UifqXElBTBNLAEH1l3vzMQj6JwJZbL2sZlThxSViOKCYY+4Ah4V4JhQ95IVB7s/Y6SJSlMQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.23.0.tgz", + "integrity": "sha512-kYsT+f5ARWF6AdFmqoEEp+hpqxEB8vGmRWfw2aj78M2vTwS2uHW91EF58iFm1Z9U8Y/RrLu2XKJn46P9ca1b0w==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.10", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-split-export-declaration": "^7.22.6", "@babel/plugin-syntax-decorators": "^7.22.10" }, @@ -806,6 +812,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" @@ -821,6 +828,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/plugin-syntax-numeric-separator": "^7.10.4" @@ -836,6 +844,7 @@ "version": "7.21.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", "dependencies": { "@babel/helper-plugin-utils": "^7.20.2", "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", @@ -852,6 +861,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -1173,9 +1183,9 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.10.tgz", - "integrity": "sha512-eueE8lvKVzq5wIObKK/7dvoeKJ+xc6TvRn6aysIjS6pSCeLy7S/eVi7pEQknZqyqvzaNKdDtem8nUNTBgDVR2g==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz", + "integrity": "sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==", "dependencies": { "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5", @@ -1220,9 +1230,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.10.tgz", - "integrity": "sha512-1+kVpGAOOI1Albt6Vse7c8pHzcZQdQKW+wJH+g8mCaszOdDVwRXa/slHPqIw+oJAJANTKDMuM2cBdV0Dg618Vg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz", + "integrity": "sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1249,11 +1259,11 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz", - "integrity": "sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", + "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.11", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, @@ -1265,17 +1275,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz", - "integrity": "sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", + "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-function-name": "^7.22.5", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", "@babel/helper-split-export-declaration": "^7.22.6", "globals": "^11.1.0" }, @@ -1310,9 +1320,9 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.10.tgz", - "integrity": "sha512-dPJrL0VOyxqLM9sritNbMSGx/teueHF/htMKrPT7DNxccXxRDPYqlgPFFdr8u+F+qUZOkZoXue/6rL5O5GduEw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz", + "integrity": "sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1353,9 +1363,9 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz", - "integrity": "sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", + "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3" @@ -1383,9 +1393,9 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz", - "integrity": "sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", + "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" @@ -1413,9 +1423,9 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz", - "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", + "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1443,9 +1453,9 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz", - "integrity": "sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", + "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-json-strings": "^7.8.3" @@ -1472,9 +1482,9 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz", - "integrity": "sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", + "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" @@ -1501,11 +1511,11 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", - "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz", + "integrity": "sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==", "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1516,11 +1526,11 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", - "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz", + "integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==", "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-simple-access": "^7.22.5" }, @@ -1532,14 +1542,14 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz", - "integrity": "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz", + "integrity": "sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==", "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5" + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -1593,9 +1603,9 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz", - "integrity": "sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", + "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" @@ -1608,9 +1618,9 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz", - "integrity": "sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", + "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-numeric-separator": "^7.10.4" @@ -1623,15 +1633,15 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz", - "integrity": "sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", + "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", "dependencies": { - "@babel/compat-data": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", + "@babel/compat-data": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.22.5" + "@babel/plugin-transform-parameters": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -1656,9 +1666,9 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz", - "integrity": "sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", + "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" @@ -1671,9 +1681,9 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.10.tgz", - "integrity": "sha512-MMkQqZAZ+MGj+jGTG3OTuhKeBpNcO+0oCEbrGNEaOmiEn+1MzRyQlYsruGiU8RTK3zV6XwrVJTmwiDOyYK6J9g==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz", + "integrity": "sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", @@ -1687,9 +1697,9 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz", - "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", + "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1716,12 +1726,12 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz", - "integrity": "sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==", + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", + "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.11", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, @@ -1775,15 +1785,15 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.5.tgz", - "integrity": "sha512-rog5gZaVbUip5iWDMTYbVM15XQq+RkUKhET/IHR6oizR+JEoN6CAfTTuHcK4vwUyzca30qqHqEpzBOnaRMWYMA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.15.tgz", + "integrity": "sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-jsx": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -1851,11 +1861,11 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.22.10.tgz", - "integrity": "sha512-RchI7HePu1eu0CYNKHHHQdfenZcM4nz8rew5B1VWqeRKdcwW5aQ5HeG9eTUbWiAS1UrmHVLmoxTWHt3iLD/NhA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.22.15.tgz", + "integrity": "sha512-tEVLhk8NRZSmwQ0DJtxxhTrCht1HVo8VaMzYT4w6lwyKBuHsgoioAUA7/6eT2fRfc5/23fuGdlwIxXhRVgWr4g==", "dependencies": { - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "babel-plugin-polyfill-corejs2": "^0.4.5", "babel-plugin-polyfill-corejs3": "^0.8.3", @@ -1949,12 +1959,12 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.10.tgz", - "integrity": "sha512-7++c8I/ymsDo4QQBAgbraXLzIM6jmfao11KgIBEYZRReWzNWH9NtNgJcyrZiXsOPh523FQm6LfpLyy/U5fn46A==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.15.tgz", + "integrity": "sha512-1uirS0TnijxvQLnlv5wQBwOX3E1wCFX7ITv+9pBV2wKEk4K+M5tqDaoNXnTH8tjEIYHLO98MwiTWO04Ggz4XuA==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.10", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-typescript": "^7.22.5" }, @@ -2025,16 +2035,16 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.10.tgz", - "integrity": "sha512-riHpLb1drNkpLlocmSyEg4oYJIQFeXAK/d7rI6mbD0XsvoTOOweXDmQPG/ErxsEhWk3rl3Q/3F6RFQlVFS8m0A==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.20.tgz", + "integrity": "sha512-11MY04gGC4kSzlPHRfvVkNAZhUxOvm7DCJ37hPDnUENwe06npjIRAfInEMTGSb4LZK5ZgDFkv5hw0lGebHeTyg==", "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-compilation-targets": "^7.22.10", + "@babel/compat-data": "^7.22.20", + "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", @@ -2055,41 +2065,41 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.22.5", - "@babel/plugin-transform-async-generator-functions": "^7.22.10", + "@babel/plugin-transform-async-generator-functions": "^7.22.15", "@babel/plugin-transform-async-to-generator": "^7.22.5", "@babel/plugin-transform-block-scoped-functions": "^7.22.5", - "@babel/plugin-transform-block-scoping": "^7.22.10", + "@babel/plugin-transform-block-scoping": "^7.22.15", "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-class-static-block": "^7.22.5", - "@babel/plugin-transform-classes": "^7.22.6", + "@babel/plugin-transform-class-static-block": "^7.22.11", + "@babel/plugin-transform-classes": "^7.22.15", "@babel/plugin-transform-computed-properties": "^7.22.5", - "@babel/plugin-transform-destructuring": "^7.22.10", + "@babel/plugin-transform-destructuring": "^7.22.15", "@babel/plugin-transform-dotall-regex": "^7.22.5", "@babel/plugin-transform-duplicate-keys": "^7.22.5", - "@babel/plugin-transform-dynamic-import": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.11", "@babel/plugin-transform-exponentiation-operator": "^7.22.5", - "@babel/plugin-transform-export-namespace-from": "^7.22.5", - "@babel/plugin-transform-for-of": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.11", + "@babel/plugin-transform-for-of": "^7.22.15", "@babel/plugin-transform-function-name": "^7.22.5", - "@babel/plugin-transform-json-strings": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.11", "@babel/plugin-transform-literals": "^7.22.5", - "@babel/plugin-transform-logical-assignment-operators": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", "@babel/plugin-transform-member-expression-literals": "^7.22.5", "@babel/plugin-transform-modules-amd": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.22.5", - "@babel/plugin-transform-modules-systemjs": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.15", + "@babel/plugin-transform-modules-systemjs": "^7.22.11", "@babel/plugin-transform-modules-umd": "^7.22.5", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", "@babel/plugin-transform-new-target": "^7.22.5", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5", - "@babel/plugin-transform-numeric-separator": "^7.22.5", - "@babel/plugin-transform-object-rest-spread": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", + "@babel/plugin-transform-numeric-separator": "^7.22.11", + "@babel/plugin-transform-object-rest-spread": "^7.22.15", "@babel/plugin-transform-object-super": "^7.22.5", - "@babel/plugin-transform-optional-catch-binding": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.10", - "@babel/plugin-transform-parameters": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.11", + "@babel/plugin-transform-optional-chaining": "^7.22.15", + "@babel/plugin-transform-parameters": "^7.22.15", "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/plugin-transform-private-property-in-object": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.11", "@babel/plugin-transform-property-literals": "^7.22.5", "@babel/plugin-transform-regenerator": "^7.22.10", "@babel/plugin-transform-reserved-words": "^7.22.5", @@ -2103,7 +2113,7 @@ "@babel/plugin-transform-unicode-regex": "^7.22.5", "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", "@babel/preset-modules": "0.1.6-no-external-plugins", - "@babel/types": "^7.22.10", + "@babel/types": "^7.22.19", "babel-plugin-polyfill-corejs2": "^0.4.5", "babel-plugin-polyfill-corejs3": "^0.8.3", "babel-plugin-polyfill-regenerator": "^0.5.2", @@ -2139,14 +2149,14 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.22.5.tgz", - "integrity": "sha512-M+Is3WikOpEJHgR385HbuCITPTaPRaNkibTEa9oiofmJvIsrceb4yp9RL9Kb+TE8LznmeyZqpP+Lopwcx59xPQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.22.15.tgz", + "integrity": "sha512-Csy1IJ2uEh/PecCBXXoZGAZBeCATTuePzCSB7dLYWS0vOEj6CNpjxIhW4duWwZodBNueH7QO14WbGn8YyeuN9w==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", "@babel/plugin-transform-react-display-name": "^7.22.5", - "@babel/plugin-transform-react-jsx": "^7.22.5", + "@babel/plugin-transform-react-jsx": "^7.22.15", "@babel/plugin-transform-react-jsx-development": "^7.22.5", "@babel/plugin-transform-react-pure-annotations": "^7.22.5" }, @@ -2158,15 +2168,15 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.22.5.tgz", - "integrity": "sha512-YbPaal9LxztSGhmndR46FmAbkJ/1fAsw293tSU+I5E5h+cnJ3d4GTwyUgGYmOXJYdGA+uNePle4qbaRzj2NISQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.0.tgz", + "integrity": "sha512-6P6VVa/NM/VlAYj5s2Aq/gdVg8FSENCg3wlZ6Qau9AcPaoF5LbN1nyGlR9DTRIw9PpxI94e+ReydsJHcjwAweg==", "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", "@babel/plugin-syntax-jsx": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.22.5", - "@babel/plugin-transform-typescript": "^7.22.5" + "@babel/plugin-transform-modules-commonjs": "^7.23.0", + "@babel/plugin-transform-typescript": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -2181,9 +2191,9 @@ "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" }, "node_modules/@babel/runtime": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz", - "integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==", + "version": "7.23.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", + "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -2192,9 +2202,9 @@ } }, "node_modules/@babel/runtime-corejs3": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.22.10.tgz", - "integrity": "sha512-IcixfV2Jl3UrqZX4c81+7lVg5++2ufYJyAFW3Aux/ZTvY6LVYYhJ9rMgnbX0zGVq6eqfVpnoatTjZdVki/GmWA==", + "version": "7.23.1", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.23.1.tgz", + "integrity": "sha512-OKKfytwoc0tr7cDHwQm0RLVR3y+hDGFz3EPuvLNU/0fOeXJeKNIHj7ffNVFnncWt3sC58uyUCRSzf8nBQbyF6A==", "dependencies": { "core-js-pure": "^3.30.2", "regenerator-runtime": "^0.14.0" @@ -2204,31 +2214,31 @@ } }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.10.tgz", - "integrity": "sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig==", - "dependencies": { - "@babel/code-frame": "^7.22.10", - "@babel/generator": "^7.22.10", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.0.tgz", + "integrity": "sha512-t/QaEvyIoIkwzpiZ7aoSKK8kObQYeF7T2v+dazAYCb8SXtp58zEVkWW7zAnju8FNKNdr4ScAOEDmMItbyOmEYw==", + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.10", - "@babel/types": "^7.22.10", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -2245,12 +2255,12 @@ } }, "node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dependencies": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -2263,9 +2273,9 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" }, "node_modules/@braintree/sanitize-url": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.0.tgz", - "integrity": "sha512-mgmE7XBYY/21erpzhexk4Cj1cyTQ9LzvnTxtzM17BJ7ERMNE6W72mQRo0I1Ud8eFJ+RVVIcBNhLFZ3GX4XFz5w==" + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", + "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==" }, "node_modules/@choojs/findup": { "version": "0.2.1", @@ -2597,17 +2607,17 @@ "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, "node_modules/@emotion/react": { - "version": "11.10.6", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.6.tgz", - "integrity": "sha512-6HT8jBmcSkfzO7mc+N1L9uwvOnlcGoix8Zn7srt+9ga0MjREo6lRpuVX0kzo6Jp6oTqDhREOFsygN6Ew4fEQbw==", + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz", + "integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==", "dependencies": { "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.10.6", - "@emotion/cache": "^11.10.5", - "@emotion/serialize": "^1.1.1", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@emotion/utils": "^1.2.0", - "@emotion/weak-memoize": "^0.3.0", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { @@ -2637,16 +2647,16 @@ "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" }, "node_modules/@emotion/styled": { - "version": "11.10.6", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.10.6.tgz", - "integrity": "sha512-OXtBzOmDSJo5Q0AFemHCfl+bUueT8BIcPSxu0EGTpGk6DmI5dnhSzQANm1e1ze0YZL7TDyAyy6s/b/zmGOS3Og==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz", + "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==", "dependencies": { "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.10.6", - "@emotion/is-prop-valid": "^1.2.0", - "@emotion/serialize": "^1.1.1", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@emotion/utils": "^1.2.0" + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.1", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" }, "peerDependencies": { "@emotion/react": "^11.0.0-rc.0", @@ -2695,14 +2705,22 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, + "node_modules/@eslint-community/regexpp": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", + "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", - "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.4.0", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -2717,18 +2735,68 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/js": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", + "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", + "integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz", + "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==", + "dependencies": { + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", + "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", + "dependencies": { + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.2.tgz", + "integrity": "sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==", + "dependencies": { + "@floating-ui/dom": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", + "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" + }, "node_modules/@handsontable/react": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/@handsontable/react/-/react-12.3.1.tgz", - "integrity": "sha512-BwumRAQqHeuxaRF9xShWO6ivlNYZ9ZoU/WabKfjtnAv/Ke4g9/VBxe4UZ+x/rElpz06f0yzBm4cG0A6AS4Hyug==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@handsontable/react/-/react-13.1.0.tgz", + "integrity": "sha512-dl5r1VHw2A8UTIprigfIc/DaXXrKovpMcn8s9GDHpdpqBPwOCDsp5ETdqiSeZ2y+TiNydUeJhb/z+N2T+rsP+g==", "peerDependencies": { - "handsontable": ">=12.0.0" + "handsontable": ">=13.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", @@ -3256,18 +3324,17 @@ } }, "node_modules/@mui/base": { - "version": "5.0.0-alpha.118", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.118.tgz", - "integrity": "sha512-GAEpqhnuHjRaAZLdxFNuOf2GDTp9sUawM46oHZV4VnYPFjXJDkIYFWfIQLONb0nga92OiqS5DD/scGzVKCL0Mw==", + "version": "5.0.0-beta.17", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.17.tgz", + "integrity": "sha512-xNbk7iOXrglNdIxFBN0k3ySsPIFLWCnFxqsAYl7CIcDkD9low4kJ7IUuy6ctwx/HAy2fenrT3KXHr1sGjAMgpQ==", "dependencies": { - "@babel/runtime": "^7.20.13", - "@emotion/is-prop-valid": "^1.2.0", - "@mui/types": "^7.2.3", - "@mui/utils": "^5.11.9", - "@popperjs/core": "^2.11.6", - "clsx": "^1.2.1", - "prop-types": "^15.8.1", - "react-is": "^18.2.0" + "@babel/runtime": "^7.22.15", + "@floating-ui/react-dom": "^2.0.2", + "@mui/types": "^7.2.4", + "@mui/utils": "^5.14.11", + "@popperjs/core": "^2.11.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1" }, "engines": { "node": ">=12.0.0" @@ -3288,20 +3355,20 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "5.14.4", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.4.tgz", - "integrity": "sha512-pW2XghSi3hpYKX57Wu0SCWMTSpzvXZmmucj3TcOJWaCiFt4xr05w2gcwBZi36dAp9uvd9//9N51qbblmnD+GPg==", + "version": "5.14.11", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.11.tgz", + "integrity": "sha512-uY8FLQURhXe3f3O4dS5OSGML9KDm9+IE226cBu78jarVIzdQGPlXwGIlSI9VJR8MvZDA6C0+6XfWDhWCHruC5Q==", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui" } }, "node_modules/@mui/icons-material": { - "version": "5.11.9", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.11.9.tgz", - "integrity": "sha512-SPANMk6K757Q1x48nCwPGdSNb8B71d+2hPMJ0V12VWerpSsbjZtvAPi5FAn13l2O5mwWkvI0Kne+0tCgnNxMNw==", + "version": "5.14.11", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.14.11.tgz", + "integrity": "sha512-aHReLasBuS/+hhPzbZCgZ0eTcZ2QRnoC2WNK7XvdAf3l+LjC1flzjh6GWw1tZJ5NHnZ+bivdwtLFQ8XTR96JkA==", "dependencies": { - "@babel/runtime": "^7.20.13" + "@babel/runtime": "^7.22.15" }, "engines": { "node": ">=12.0.0" @@ -3322,18 +3389,18 @@ } }, "node_modules/@mui/lab": { - "version": "5.0.0-alpha.120", - "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.120.tgz", - "integrity": "sha512-vjlF2jTKSZnNxtUO0xxHEDfpL5cG0LLNRsfKv8TYOiPs0Q1bbqO3YfqJsqxv8yh+wx7EFZc8lwJ4NSAQdenW3A==", + "version": "5.0.0-alpha.146", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.146.tgz", + "integrity": "sha512-azkSNz/F4VAzXdXG1Yu/pdWiMQY8dRpwHycLCQCK7oql5AOVh1pVEmw5+nMT161oc5bOzxBkIsNGPCBwXIZ7Ww==", "dependencies": { - "@babel/runtime": "^7.20.13", - "@mui/base": "5.0.0-alpha.118", - "@mui/system": "^5.11.9", - "@mui/types": "^7.2.3", - "@mui/utils": "^5.11.9", - "clsx": "^1.2.1", - "prop-types": "^15.8.1", - "react-is": "^18.2.0" + "@babel/runtime": "^7.22.15", + "@mui/base": "5.0.0-beta.17", + "@mui/system": "^5.14.11", + "@mui/types": "^7.2.4", + "@mui/utils": "^5.14.11", + "@mui/x-tree-view": "6.0.0-alpha.1", + "clsx": "^2.0.0", + "prop-types": "^15.8.1" }, "engines": { "node": ">=12.0.0" @@ -3363,19 +3430,19 @@ } }, "node_modules/@mui/material": { - "version": "5.11.10", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.11.10.tgz", - "integrity": "sha512-hs1WErbiedqlJIZsljgoil908x4NMp8Lfk8di+5c7o809roqKcFTg2+k3z5ucKvs29AXcsdXrDB/kn2K6dGYIw==", - "dependencies": { - "@babel/runtime": "^7.20.13", - "@mui/base": "5.0.0-alpha.118", - "@mui/core-downloads-tracker": "^5.11.9", - "@mui/system": "^5.11.9", - "@mui/types": "^7.2.3", - "@mui/utils": "^5.11.9", - "@types/react-transition-group": "^4.4.5", - "clsx": "^1.2.1", - "csstype": "^3.1.1", + "version": "5.14.11", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.11.tgz", + "integrity": "sha512-DnSdJzcR7lwG12JA5L2t8JF+RDzMygu5rCNW+logWb/KW2/TRzwLyVWO+CorHTBjBRd38DBxnwOCDiYkDd+N3A==", + "dependencies": { + "@babel/runtime": "^7.22.15", + "@mui/base": "5.0.0-beta.17", + "@mui/core-downloads-tracker": "^5.14.11", + "@mui/system": "^5.14.11", + "@mui/types": "^7.2.4", + "@mui/utils": "^5.14.11", + "@types/react-transition-group": "^4.4.6", + "clsx": "^2.0.0", + "csstype": "^3.1.2", "prop-types": "^15.8.1", "react-is": "^18.2.0", "react-transition-group": "^4.4.5" @@ -3407,12 +3474,12 @@ } }, "node_modules/@mui/private-theming": { - "version": "5.14.4", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.4.tgz", - "integrity": "sha512-ISXsHDiQ3z1XA4IuKn+iXDWvDjcz/UcQBiFZqtdoIsEBt8CB7wgdQf3LwcwqO81dl5ofg/vNQBEnXuKfZHrnYA==", + "version": "5.14.11", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.11.tgz", + "integrity": "sha512-MSnNNzTu9pfKLCKs1ZAKwOTgE4bz+fQA0fNr8Jm7NDmuWmw0CaN9Vq2/MHsatE7+S0A25IAKby46Uv1u53rKVQ==", "dependencies": { - "@babel/runtime": "^7.22.6", - "@mui/utils": "^5.14.4", + "@babel/runtime": "^7.22.15", + "@mui/utils": "^5.14.11", "prop-types": "^15.8.1" }, "engines": { @@ -3433,11 +3500,11 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.13.2", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.13.2.tgz", - "integrity": "sha512-VCYCU6xVtXOrIN8lcbuPmoG+u7FYuOERG++fpY74hPpEWkyFQG97F+/XfTQVYzlR2m7nPjnwVUgATcTCMEaMvw==", + "version": "5.14.11", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.11.tgz", + "integrity": "sha512-jdUlqRgTYQ8RMtPX4MbRZqar6W2OiIb6J5KEFbIu4FqvPrk44Each4ppg/LAqp1qNlBYq5i+7Q10MYLMpDxX9A==", "dependencies": { - "@babel/runtime": "^7.21.0", + "@babel/runtime": "^7.22.15", "@emotion/cache": "^11.11.0", "csstype": "^3.1.2", "prop-types": "^15.8.1" @@ -3464,15 +3531,15 @@ } }, "node_modules/@mui/system": { - "version": "5.14.4", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.4.tgz", - "integrity": "sha512-oPgfWS97QNfHcDBapdkZIs4G5i85BJt69Hp6wbXF6s7vi3Evcmhdk8AbCRW6n0sX4vTj8oe0mh0RIm1G2A1KDA==", + "version": "5.14.11", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.11.tgz", + "integrity": "sha512-yl8xV+y0k7j6dzBsHabKwoShmjqLa8kTxrhUI3JpqLG358VRVMJRW/ES0HhvfcCi4IVXde+Tc2P3K1akGL8zoA==", "dependencies": { - "@babel/runtime": "^7.22.6", - "@mui/private-theming": "^5.14.4", - "@mui/styled-engine": "^5.13.2", + "@babel/runtime": "^7.22.15", + "@mui/private-theming": "^5.14.11", + "@mui/styled-engine": "^5.14.11", "@mui/types": "^7.2.4", - "@mui/utils": "^5.14.4", + "@mui/utils": "^5.14.11", "clsx": "^2.0.0", "csstype": "^3.1.2", "prop-types": "^15.8.1" @@ -3502,14 +3569,6 @@ } } }, - "node_modules/@mui/system/node_modules/clsx": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", - "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", - "engines": { - "node": ">=6" - } - }, "node_modules/@mui/types": { "version": "7.2.4", "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz", @@ -3524,13 +3583,12 @@ } }, "node_modules/@mui/utils": { - "version": "5.14.4", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.4.tgz", - "integrity": "sha512-4ANV0txPD3x0IcTCSEHKDWnsutg1K3m6Vz5IckkbLXVYu17oOZCVUdOKsb/txUmaCd0v0PmSRe5PW+Mlvns5dQ==", + "version": "5.14.11", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.11.tgz", + "integrity": "sha512-fmkIiCPKyDssYrJ5qk+dime1nlO3dmWfCtaPY/uVBqCRMBZ11JhddB9m8sjI2mgqQQwRJG5bq3biaosNdU/s4Q==", "dependencies": { - "@babel/runtime": "^7.22.6", + "@babel/runtime": "^7.22.15", "@types/prop-types": "^15.7.5", - "@types/react-is": "^18.2.1", "prop-types": "^15.8.1", "react-is": "^18.2.0" }, @@ -3542,7 +3600,42 @@ "url": "https://opencollective.com/mui" }, "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-tree-view": { + "version": "6.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-6.0.0-alpha.1.tgz", + "integrity": "sha512-JUG3HmBrmGEALbCFg1b+i7h726e1dWYZs4db3syO1j+Q++E3nbvE4Lehp5yGTFm+8esH0Tny50tuJaa4WX6VSA==", + "dependencies": { + "@babel/runtime": "^7.22.6", + "@mui/utils": "^5.14.3", + "@types/react-transition-group": "^4.4.6", + "clsx": "^2.0.0", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/base": "^5.0.0-alpha.87", + "@mui/material": "^5.8.6", + "@mui/system": "^5.8.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" } }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { @@ -3553,6 +3646,26 @@ "eslint-scope": "5.1.1" } }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3585,6 +3698,56 @@ "node": ">= 8" } }, + "node_modules/@pkgr/utils": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", + "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "fast-glob": "^3.3.0", + "is-glob": "^4.0.3", + "open": "^9.1.0", + "picocolors": "^1.0.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@pkgr/utils/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@pkgr/utils/node_modules/open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dev": true, + "dependencies": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@plotly/d3": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/@plotly/d3/-/d3-3.8.1.tgz", @@ -3629,9 +3792,9 @@ } }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz", - "integrity": "sha512-j0Ya0hCFZPd4x40qLzbhGsh9TMtdb+CJQiso+WxLOPNasohq9cc5SNUcwsZaRH6++Xh91Xkm/xHCkuIiIu0LUA==", + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.11.tgz", + "integrity": "sha512-7j/6vdTym0+qZ6u4XbSAxrWBGYSdCfTzySkj7WAFgDLmSyWlOrWvpyzxlFh5jtw9dn0oL/jtW+06XfFiisN3JQ==", "dependencies": { "ansi-html-community": "^0.0.8", "common-path-prefix": "^3.0.0", @@ -3650,7 +3813,7 @@ "@types/webpack": "4.x || 5.x", "react-refresh": ">=0.10.0 <1.0.0", "sockjs-client": "^1.4.0", - "type-fest": ">=0.17.0 <4.0.0", + "type-fest": ">=0.17.0 <5.0.0", "webpack": ">=4.43.0 <6.0.0", "webpack-dev-server": "3.x || 4.x", "webpack-hot-middleware": "2.x", @@ -3695,14 +3858,14 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.3.tgz", - "integrity": "sha512-GU2TNBQVofL09VGmuSioNPQIu6Ml0YLf4EJhgj0AvBadRlCGzUWet8372LjvO4fqKZF2vH1xU0htAa7BrK9pZg==", + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.6.tgz", + "integrity": "sha512-Gc4ikl90ORF4viIdAkY06JNUnODjKfGxZRwATM30EdHq8hLSVoSrwXne5dd739yenP5bJxAX7tLuOWK5RPGtrw==", "dependencies": { - "immer": "^9.0.16", - "redux": "^4.2.0", + "immer": "^9.0.21", + "redux": "^4.2.1", "redux-thunk": "^2.4.2", - "reselect": "^4.1.7" + "reselect": "^4.1.8" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18", @@ -3717,6 +3880,15 @@ } } }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3792,9 +3964,9 @@ "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" }, "node_modules/@rushstack/eslint-patch": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz", - "integrity": "sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw==" + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.5.1.tgz", + "integrity": "sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA==" }, "node_modules/@sinclair/typebox": { "version": "0.24.51", @@ -4036,1034 +4208,354 @@ } }, "node_modules/@swagger-api/apidom-ast": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ast/-/apidom-ast-0.74.1.tgz", - "integrity": "sha512-EoHyaRBeZmNYFNlDNZGeI45zRLfcVW0o4uZ8Fs/+HN1UIyDoZdr+ObElj5PEkCmdDx7ADlNmoGK4B+4AQA2LeA==", + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ast/-/apidom-ast-0.76.2.tgz", + "integrity": "sha512-yLSeI3KtfpR7tI/misqTeasFonssj9GGhCOJfSHBuRAZkrPCJf0eU8vh3pL7YPa8lqFWcPT+z/arZoMcC9VLnQ==", "dependencies": { "@babel/runtime-corejs3": "^7.20.7", + "@swagger-api/apidom-error": "^0.76.2", "@types/ramda": "~0.29.3", "ramda": "~0.29.0", - "ramda-adjunct": "^4.0.0", + "ramda-adjunct": "^4.1.1", "stampit": "^4.3.2", "unraw": "^3.0.0" } }, - "node_modules/@swagger-api/apidom-ast/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", - "dependencies": { - "types-ramda": "^0.29.4" - } - }, - "node_modules/@swagger-api/apidom-ast/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, - "node_modules/@swagger-api/apidom-ast/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" - } - }, "node_modules/@swagger-api/apidom-core": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-core/-/apidom-core-0.74.1.tgz", - "integrity": "sha512-y70oo/CrNMSi7TtUkATXkSWd+Q/4BjchwCuLpWbhSJuIpJM+W9yGyzWOFTFLZQpDbwK0yzocMk8iPClq/rWNPw==", + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-core/-/apidom-core-0.76.2.tgz", + "integrity": "sha512-366dJJM7DFONlO3nUQfQRMJpJzZjPpWZldbHJZCcvy+aCyrNYI3Waauas7fm29UXRliPirGrd9e/ZsnW3Jimag==", "dependencies": { "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-ast": "^0.74.1", + "@swagger-api/apidom-ast": "^0.76.2", + "@swagger-api/apidom-error": "^0.76.2", "@types/ramda": "~0.29.3", "minim": "~0.23.8", "ramda": "~0.29.0", - "ramda-adjunct": "^4.0.0", - "short-unique-id": "^4.4.4", + "ramda-adjunct": "^4.1.1", + "short-unique-id": "^5.0.2", "stampit": "^4.3.2" } }, - "node_modules/@swagger-api/apidom-core/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", + "node_modules/@swagger-api/apidom-error": { + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-error/-/apidom-error-0.76.2.tgz", + "integrity": "sha512-QxoWL+qGzwftqXSJaYLZ1Nrdtro+U1zX5Q4OLK+Ggg8Hi6Kn1SGXcHhn4JZ9J1rwrP85XCabilL3z9mhdebqWg==", "dependencies": { - "types-ramda": "^0.29.4" - } - }, - "node_modules/@swagger-api/apidom-core/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, - "node_modules/@swagger-api/apidom-core/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" + "@babel/runtime-corejs3": "^7.20.7", + "@types/ramda": "~0.29.3", + "ramda": "~0.29.0", + "ramda-adjunct": "^4.0.0" } }, "node_modules/@swagger-api/apidom-json-pointer": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-json-pointer/-/apidom-json-pointer-0.74.1.tgz", - "integrity": "sha512-UusZdVY2AbYSyMK0aPSNvCiCtgn6NcGnS9fbAPVFsV+ALEtWYdMs/ZjfqYhbuzd+nRY34J9GCF7m+kVysZ9EWw==", + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-json-pointer/-/apidom-json-pointer-0.76.2.tgz", + "integrity": "sha512-2XCgA4bn8vB1VMDbSiP+6SHUTiBxx1EVLW2pgqFolhLPMdiI/QBVmoW+jEkvTPo4d5gwj/vP5WDs5QnnC9VwEA==", "dependencies": { "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^0.74.1", + "@swagger-api/apidom-core": "^0.76.2", + "@swagger-api/apidom-error": "^0.76.2", "@types/ramda": "~0.29.3", "ramda": "~0.29.0", "ramda-adjunct": "^4.0.0" } }, - "node_modules/@swagger-api/apidom-json-pointer/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", - "dependencies": { - "types-ramda": "^0.29.4" - } - }, - "node_modules/@swagger-api/apidom-json-pointer/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, - "node_modules/@swagger-api/apidom-json-pointer/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" - } - }, "node_modules/@swagger-api/apidom-ns-api-design-systems": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-api-design-systems/-/apidom-ns-api-design-systems-0.74.1.tgz", - "integrity": "sha512-eJxd3B4lQbVCi+g9ZXSM0IeCbqPEH5o7WdLdfrSowFLQqc7jQur/29UhbAh2PDvPSI/l7oaNzwgPTp4Zm8SaTw==", + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-api-design-systems/-/apidom-ns-api-design-systems-0.76.2.tgz", + "integrity": "sha512-ct83R5Pvc08jeOuGShO4N0ty7VO8f46WedTDCbzT4edMRhd9Xdr5UFxkwWDuliy4uLzl9ZayHygSxfnyZKQb8g==", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^0.74.1", - "@swagger-api/apidom-ns-openapi-3-1": "^0.74.1", + "@swagger-api/apidom-core": "^0.76.2", + "@swagger-api/apidom-error": "^0.76.2", + "@swagger-api/apidom-ns-openapi-3-1": "^0.76.2", "@types/ramda": "~0.29.3", "ramda": "~0.29.0", - "ramda-adjunct": "^4.0.0", + "ramda-adjunct": "^4.1.1", "stampit": "^4.3.2" } }, - "node_modules/@swagger-api/apidom-ns-api-design-systems/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", - "optional": true, - "dependencies": { - "types-ramda": "^0.29.4" - } - }, - "node_modules/@swagger-api/apidom-ns-api-design-systems/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, - "node_modules/@swagger-api/apidom-ns-api-design-systems/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", - "optional": true, - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" - } - }, "node_modules/@swagger-api/apidom-ns-asyncapi-2": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-asyncapi-2/-/apidom-ns-asyncapi-2-0.74.1.tgz", - "integrity": "sha512-xH6ilO8jJpZOWzWwbse3xi8zIbe3Iho+AMwwMFtkCnjUqmv81TGhlA6VPXpLCKgFsnZqJVyCKn/VaTW8N6379w==", + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-asyncapi-2/-/apidom-ns-asyncapi-2-0.76.2.tgz", + "integrity": "sha512-ffV2AhF7jTBbYl2vX0nYSDufs70CmC/kNMWHkgwR2Vq86lgadUc6S/NK/djpWY8+oAU3EYmHwTqu07hpSOUb4A==", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^0.74.1", - "@swagger-api/apidom-ns-json-schema-draft-7": "^0.74.1", + "@swagger-api/apidom-core": "^0.76.2", + "@swagger-api/apidom-ns-json-schema-draft-7": "^0.76.2", "@types/ramda": "~0.29.3", "ramda": "~0.29.0", - "ramda-adjunct": "^4.0.0", + "ramda-adjunct": "^4.1.1", "stampit": "^4.3.2" } }, - "node_modules/@swagger-api/apidom-ns-asyncapi-2/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", - "optional": true, - "dependencies": { - "types-ramda": "^0.29.4" - } - }, - "node_modules/@swagger-api/apidom-ns-asyncapi-2/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, - "node_modules/@swagger-api/apidom-ns-asyncapi-2/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", - "optional": true, - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" - } - }, "node_modules/@swagger-api/apidom-ns-json-schema-draft-4": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-4/-/apidom-ns-json-schema-draft-4-0.74.1.tgz", - "integrity": "sha512-zUQvrxoRQpvdYymHko1nxNeVWwqdGDYNYWUFW/EGZbP0sigKmuSZkh6LdseB9Pxt1WQD/6MkW3zN4JMXt/qFUA==", + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-4/-/apidom-ns-json-schema-draft-4-0.76.2.tgz", + "integrity": "sha512-0Y32CQE6tIt4IPsoCzWAUskZSyGkfw87IIsH5Bcm3D1qIlAhPAokQbe1212MmZoLVUvqrXDqZHXnOxxMaHZvYw==", "dependencies": { "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-ast": "^0.74.1", - "@swagger-api/apidom-core": "^0.74.1", + "@swagger-api/apidom-ast": "^0.76.2", + "@swagger-api/apidom-core": "^0.76.2", "@types/ramda": "~0.29.3", "ramda": "~0.29.0", - "ramda-adjunct": "^4.0.0", + "ramda-adjunct": "^4.1.1", "stampit": "^4.3.2" } }, - "node_modules/@swagger-api/apidom-ns-json-schema-draft-4/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", - "dependencies": { - "types-ramda": "^0.29.4" - } - }, - "node_modules/@swagger-api/apidom-ns-json-schema-draft-4/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, - "node_modules/@swagger-api/apidom-ns-json-schema-draft-4/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" - } - }, "node_modules/@swagger-api/apidom-ns-json-schema-draft-6": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-6/-/apidom-ns-json-schema-draft-6-0.74.1.tgz", - "integrity": "sha512-8GFH6bR5ERyuS+4u7CnLirBPYkYWostk31WDj7YeY5b0BRNtI3omH4rV24KECu99ZAg/unZY688VwmN25Dut/A==", - "optional": true, - "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^0.74.1", - "@swagger-api/apidom-ns-json-schema-draft-4": "^0.74.1", - "@types/ramda": "~0.29.3", - "ramda": "~0.29.0", - "ramda-adjunct": "^4.0.0", - "stampit": "^4.3.2" - } - }, - "node_modules/@swagger-api/apidom-ns-json-schema-draft-6/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", - "optional": true, - "dependencies": { - "types-ramda": "^0.29.4" - } - }, - "node_modules/@swagger-api/apidom-ns-json-schema-draft-6/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, - "node_modules/@swagger-api/apidom-ns-json-schema-draft-6/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", - "optional": true, - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" - } - }, - "node_modules/@swagger-api/apidom-ns-json-schema-draft-7": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-7/-/apidom-ns-json-schema-draft-7-0.74.1.tgz", - "integrity": "sha512-4ttxnBuRcegp1ooKtwoOqXDUNCWH4GuQlMBOUlHfKPR35qbMf0LCYU+ROvTk05ycoVkc2x6+AJQ4He684EXwfw==", - "optional": true, - "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^0.74.1", - "@swagger-api/apidom-ns-json-schema-draft-6": "^0.74.1", - "@types/ramda": "~0.29.3", - "ramda": "~0.29.0", - "ramda-adjunct": "^4.0.0", - "stampit": "^4.3.2" - } - }, - "node_modules/@swagger-api/apidom-ns-json-schema-draft-7/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", - "optional": true, - "dependencies": { - "types-ramda": "^0.29.4" - } - }, - "node_modules/@swagger-api/apidom-ns-json-schema-draft-7/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, - "node_modules/@swagger-api/apidom-ns-json-schema-draft-7/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", - "optional": true, - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" - } - }, - "node_modules/@swagger-api/apidom-ns-openapi-3-0": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-0/-/apidom-ns-openapi-3-0-0.74.1.tgz", - "integrity": "sha512-n5jccxnbiNjHiID0uTV1UXdt47WxyduQRKK9ILo7N2yXqkwI1ygqQNBVEUC/YZnHT4ZvFsifYAqbT0hO1h54ig==", - "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^0.74.1", - "@swagger-api/apidom-ns-json-schema-draft-4": "^0.74.1", - "@types/ramda": "~0.29.3", - "ramda": "~0.29.0", - "ramda-adjunct": "^4.0.0", - "stampit": "^4.3.2" - } - }, - "node_modules/@swagger-api/apidom-ns-openapi-3-0/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", - "dependencies": { - "types-ramda": "^0.29.4" - } - }, - "node_modules/@swagger-api/apidom-ns-openapi-3-0/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, - "node_modules/@swagger-api/apidom-ns-openapi-3-0/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" - } - }, - "node_modules/@swagger-api/apidom-ns-openapi-3-1": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-1/-/apidom-ns-openapi-3-1-0.74.1.tgz", - "integrity": "sha512-8ZqQBjMfiCEwePUbwdKIAStl7nIPIiyKGrON4Sy+PWTwvCQiam3haKeT5r6TDiTFyrS3idSplfXijuWfZF//Ag==", - "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-ast": "^0.74.1", - "@swagger-api/apidom-core": "^0.74.1", - "@swagger-api/apidom-ns-openapi-3-0": "^0.74.1", - "@types/ramda": "~0.29.3", - "ramda": "~0.29.0", - "ramda-adjunct": "^4.0.0", - "stampit": "^4.3.2" - } - }, - "node_modules/@swagger-api/apidom-ns-openapi-3-1/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", - "dependencies": { - "types-ramda": "^0.29.4" - } - }, - "node_modules/@swagger-api/apidom-ns-openapi-3-1/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, - "node_modules/@swagger-api/apidom-ns-openapi-3-1/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-json": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-api-design-systems-json/-/apidom-parser-adapter-api-design-systems-json-0.74.1.tgz", - "integrity": "sha512-RFwnL2u3OzKVkE4jQ4zGNHA83BnXM3EjpTNRbCzcmsP78RGr7H9HebPaiRPpLMyC3GuzBwPXe8WbOdYsReuFww==", - "optional": true, - "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^0.74.1", - "@swagger-api/apidom-ns-api-design-systems": "^0.74.1", - "@swagger-api/apidom-parser-adapter-json": "^0.74.1", - "@types/ramda": "~0.29.3", - "ramda": "~0.29.0", - "ramda-adjunct": "^4.0.0" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-json/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", - "optional": true, - "dependencies": { - "types-ramda": "^0.29.4" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-json/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-json/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", - "optional": true, - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-yaml": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-api-design-systems-yaml/-/apidom-parser-adapter-api-design-systems-yaml-0.74.1.tgz", - "integrity": "sha512-3r5lxhP/glOhQVFRVRf/Ps2F5V2oMowG6+YBkajV2jCW9XPIrIuVef+KcjbQQlm06J3QnD+Tg/ZiLXcxziAvoQ==", - "optional": true, - "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^0.74.1", - "@swagger-api/apidom-ns-api-design-systems": "^0.74.1", - "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.74.1", - "@types/ramda": "~0.29.3", - "ramda": "~0.29.0", - "ramda-adjunct": "^4.0.0" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-yaml/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", - "optional": true, - "dependencies": { - "types-ramda": "^0.29.4" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-yaml/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-yaml/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", - "optional": true, - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-json-2": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-asyncapi-json-2/-/apidom-parser-adapter-asyncapi-json-2-0.74.1.tgz", - "integrity": "sha512-jPp5n0aKtqZrQrz+Lh1B5LNocuMliA3OvNWGGTD14T54qNDJ+a2B6a31SXZqzjqfseWr7SeE2Z/RM5ljqviLWA==", - "optional": true, - "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^0.74.1", - "@swagger-api/apidom-ns-asyncapi-2": "^0.74.1", - "@swagger-api/apidom-parser-adapter-json": "^0.74.1", - "@types/ramda": "~0.29.3", - "ramda": "~0.29.0", - "ramda-adjunct": "^4.0.0" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-json-2/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", - "optional": true, - "dependencies": { - "types-ramda": "^0.29.4" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-json-2/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-json-2/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", - "optional": true, - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2/-/apidom-parser-adapter-asyncapi-yaml-2-0.74.1.tgz", - "integrity": "sha512-em8o7bu0XEMac6cJvSi9WjMpTEny39gn+1UrANnICpvsMoiRjlfE5yEG4eueewV1nsukO4qTiUjTf32BGNgHYg==", + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-6/-/apidom-ns-json-schema-draft-6-0.76.2.tgz", + "integrity": "sha512-i6nZtj3ie6SP1LhRtBeZNJuBppWkuC/+AsVfUzXkH5pM+3B7Puklc77hHdLtmvUTpd/iRBdlfsklvBVXJYPtUA==", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^0.74.1", - "@swagger-api/apidom-ns-asyncapi-2": "^0.74.1", - "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.74.1", + "@swagger-api/apidom-core": "^0.76.2", + "@swagger-api/apidom-error": "^0.76.2", + "@swagger-api/apidom-ns-json-schema-draft-4": "^0.76.2", "@types/ramda": "~0.29.3", - "ramda": "~0.29.0", - "ramda-adjunct": "^4.0.0" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", - "optional": true, - "dependencies": { - "types-ramda": "^0.29.4" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", - "optional": true, - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-json": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-json/-/apidom-parser-adapter-json-0.74.1.tgz", - "integrity": "sha512-CtJxt/o0ZyW/GkvETuTUUlCjTJ/wH0S7jr3CBnZR/vVVVlVfIYkGw2fEo8HUBAr+EnJNFfWOzOAjXQHul71wUw==", - "optional": true, - "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-ast": "^0.74.1", - "@swagger-api/apidom-core": "^0.74.1", - "@types/ramda": "~0.29.3", - "ramda": "~0.29.0", - "ramda-adjunct": "^4.0.0", - "stampit": "^4.3.2", - "tree-sitter": "=0.20.4", - "tree-sitter-json": "=0.20.0", - "web-tree-sitter": "=0.20.3" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-json/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", - "optional": true, - "dependencies": { - "types-ramda": "^0.29.4" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-json/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-json/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", - "optional": true, - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-0": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-3-0/-/apidom-parser-adapter-openapi-json-3-0-0.74.1.tgz", - "integrity": "sha512-k8zOeb2aCyEVUdW1sUUBmawyqHmx7C7WB9eXFM1yEzwy3Y589cVygiy6AG1yOaPU8WWzR80+xPEqHw0VmqkBRg==", - "optional": true, - "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^0.74.1", - "@swagger-api/apidom-ns-openapi-3-0": "^0.74.1", - "@swagger-api/apidom-parser-adapter-json": "^0.74.1", - "@types/ramda": "~0.29.3", - "ramda": "~0.29.0", - "ramda-adjunct": "^4.0.0" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-0/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", - "optional": true, - "dependencies": { - "types-ramda": "^0.29.4" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-0/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-0/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", - "optional": true, - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" + "ramda": "~0.29.0", + "ramda-adjunct": "^4.1.1", + "stampit": "^4.3.2" } }, - "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-1": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-3-1/-/apidom-parser-adapter-openapi-json-3-1-0.74.1.tgz", - "integrity": "sha512-x70fOeBiavi9siSq2Hr07cBcIXdTEDpi87OpaQIGTk5tjN8wQfnQF1MWxdHpe4p/cJN7LiYw5Dx6uIAhp/RuGg==", + "node_modules/@swagger-api/apidom-ns-json-schema-draft-7": { + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-7/-/apidom-ns-json-schema-draft-7-0.76.2.tgz", + "integrity": "sha512-Klyfi/1XkJVUZa1nJP87HPMjklmB3IxE+TSD27aZIEi7GKASu96euan0gflZaegexUBA9hsAngk98USbdpHpgQ==", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^0.74.1", - "@swagger-api/apidom-ns-openapi-3-1": "^0.74.1", - "@swagger-api/apidom-parser-adapter-json": "^0.74.1", + "@swagger-api/apidom-core": "^0.76.2", + "@swagger-api/apidom-error": "^0.76.2", + "@swagger-api/apidom-ns-json-schema-draft-6": "^0.76.2", "@types/ramda": "~0.29.3", "ramda": "~0.29.0", - "ramda-adjunct": "^4.0.0" + "ramda-adjunct": "^4.1.1", + "stampit": "^4.3.2" } }, - "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-1/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", - "optional": true, + "node_modules/@swagger-api/apidom-ns-openapi-3-0": { + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-0/-/apidom-ns-openapi-3-0-0.76.2.tgz", + "integrity": "sha512-tV7dfbAZjX4HHul6JzmWsipMIVHCX5fAsBwLTltq8qmF9X9m6kZwg7fb4pD+cGK2KVlZl/ucDDDIQLDRWpOAog==", "dependencies": { - "types-ramda": "^0.29.4" + "@babel/runtime-corejs3": "^7.20.7", + "@swagger-api/apidom-core": "^0.76.2", + "@swagger-api/apidom-ns-json-schema-draft-4": "^0.76.2", + "@types/ramda": "~0.29.3", + "ramda": "~0.29.0", + "ramda-adjunct": "^4.1.1", + "stampit": "^4.3.2" } }, - "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-1/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" + "node_modules/@swagger-api/apidom-ns-openapi-3-1": { + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-1/-/apidom-ns-openapi-3-1-0.76.2.tgz", + "integrity": "sha512-Mb9VhVacoWvQcBqxO4j0eweyM6PGupAOt7XcOL5CzID0dOU+P4BbAv6kHD++0bTqRgXk1O31HkS/yPJmPaTCrw==", + "dependencies": { + "@babel/runtime-corejs3": "^7.20.7", + "@swagger-api/apidom-ast": "^0.76.2", + "@swagger-api/apidom-core": "^0.76.2", + "@swagger-api/apidom-ns-openapi-3-0": "^0.76.2", + "@types/ramda": "~0.29.3", + "ramda": "~0.29.0", + "ramda-adjunct": "^4.1.1", + "stampit": "^4.3.2" } }, - "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-1/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", + "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-json": { + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-api-design-systems-json/-/apidom-parser-adapter-api-design-systems-json-0.76.2.tgz", + "integrity": "sha512-mJ4HLVIR9YHgWu0SiHykFQ9Sz1f3eV5Wqhrff8sH2Qll+4QSSdOOs0tW4Gp56F0HIcrU66uvrrTy1tpkO943aw==", "optional": true, - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" + "dependencies": { + "@babel/runtime-corejs3": "^7.20.7", + "@swagger-api/apidom-core": "^0.76.2", + "@swagger-api/apidom-ns-api-design-systems": "^0.76.2", + "@swagger-api/apidom-parser-adapter-json": "^0.76.2", + "@types/ramda": "~0.29.3", + "ramda": "~0.29.0", + "ramda-adjunct": "^4.0.0" } }, - "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0/-/apidom-parser-adapter-openapi-yaml-3-0-0.74.1.tgz", - "integrity": "sha512-MdZrzR+9AbunoP9OyETqZabhCllUiu5lu59uG7exo7jR1GfC28A4wVolNhi0C01wOcS+55t+1qvzi+i+9Kz3ew==", + "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-yaml": { + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-api-design-systems-yaml/-/apidom-parser-adapter-api-design-systems-yaml-0.76.2.tgz", + "integrity": "sha512-ot0F8Pw9/oWce6daDK+3srhNad/Iva/OlkVtN0S9cR58Zcn8p1F3s6RcN7ZG97i8EdBuyQj6Bm0jzXnOX+lvtQ==", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^0.74.1", - "@swagger-api/apidom-ns-openapi-3-0": "^0.74.1", - "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.74.1", + "@swagger-api/apidom-core": "^0.76.2", + "@swagger-api/apidom-ns-api-design-systems": "^0.76.2", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.76.2", "@types/ramda": "~0.29.3", "ramda": "~0.29.0", "ramda-adjunct": "^4.0.0" } }, - "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", + "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-json-2": { + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-asyncapi-json-2/-/apidom-parser-adapter-asyncapi-json-2-0.76.2.tgz", + "integrity": "sha512-FK06pb4w5E8RQ65Nh1FHHM8aWzPL7fHr2HeuXZkbSeKu4j0xyzwYkxZVGwZJOT6YPJR0Yrkb/2rD89CNXsLctA==", "optional": true, "dependencies": { - "types-ramda": "^0.29.4" + "@babel/runtime-corejs3": "^7.20.7", + "@swagger-api/apidom-core": "^0.76.2", + "@swagger-api/apidom-ns-asyncapi-2": "^0.76.2", + "@swagger-api/apidom-parser-adapter-json": "^0.76.2", + "@types/ramda": "~0.29.3", + "ramda": "~0.29.0", + "ramda-adjunct": "^4.0.0" } }, - "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", + "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": { + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2/-/apidom-parser-adapter-asyncapi-yaml-2-0.76.2.tgz", + "integrity": "sha512-7TGhZgHZ9nmBJnFA7YhDWbNDbKoUOGVkBqx563ExHr2FewaohiQ/wagXAhKZzOK+HS+KHvob09uROtqOWGdIew==", "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" + "dependencies": { + "@babel/runtime-corejs3": "^7.20.7", + "@swagger-api/apidom-core": "^0.76.2", + "@swagger-api/apidom-ns-asyncapi-2": "^0.76.2", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.76.2", + "@types/ramda": "~0.29.3", + "ramda": "~0.29.0", + "ramda-adjunct": "^4.0.0" } }, - "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", + "node_modules/@swagger-api/apidom-parser-adapter-json": { + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-json/-/apidom-parser-adapter-json-0.76.2.tgz", + "integrity": "sha512-vbH7EcldZ/gSK9FnGUW1cpibM5+hiJPQcoyLmzLZe8YBxX73qzd2WAd77v+uI56eO9Z0G4KMCRCF9PDZT/tz5Q==", "optional": true, - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" + "dependencies": { + "@babel/runtime-corejs3": "^7.20.7", + "@swagger-api/apidom-ast": "^0.76.2", + "@swagger-api/apidom-core": "^0.76.2", + "@swagger-api/apidom-error": "^0.76.2", + "@types/ramda": "~0.29.3", + "ramda": "~0.29.0", + "ramda-adjunct": "^4.1.1", + "stampit": "^4.3.2", + "tree-sitter": "=0.20.4", + "tree-sitter-json": "=0.20.0", + "web-tree-sitter": "=0.20.3" } }, - "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1/-/apidom-parser-adapter-openapi-yaml-3-1-0.74.1.tgz", - "integrity": "sha512-OaDAhZm38chXyc0P0yHQSD4fCmUmEUWTTLgHntJDmvAZ7nSkV4NddDP7cgZ07z8dLEwMokI//9u+I/s0G0BO0Q==", + "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-0": { + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-3-0/-/apidom-parser-adapter-openapi-json-3-0-0.76.2.tgz", + "integrity": "sha512-Kqcq5QUgz1TcCuPaL+zU+wmdAEo7YM0LR5jyWQo3FAT3BhAsmeVv2wRZMiz9RMDrPyxzHzbJhjMZxCqL8r2G0g==", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^0.74.1", - "@swagger-api/apidom-ns-openapi-3-1": "^0.74.1", - "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.74.1", + "@swagger-api/apidom-core": "^0.76.2", + "@swagger-api/apidom-ns-openapi-3-0": "^0.76.2", + "@swagger-api/apidom-parser-adapter-json": "^0.76.2", "@types/ramda": "~0.29.3", "ramda": "~0.29.0", "ramda-adjunct": "^4.0.0" } }, - "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", + "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-1": { + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-3-1/-/apidom-parser-adapter-openapi-json-3-1-0.76.2.tgz", + "integrity": "sha512-kfZ4BBxww5afiIIeFT6l0/Kuob72dnYAP+Qnmp2zQB3GQUTilKqv+ddj4blCF19n8RGNERVv2RDHLTZhjg+1AA==", "optional": true, "dependencies": { - "types-ramda": "^0.29.4" + "@babel/runtime-corejs3": "^7.20.7", + "@swagger-api/apidom-core": "^0.76.2", + "@swagger-api/apidom-ns-openapi-3-1": "^0.76.2", + "@swagger-api/apidom-parser-adapter-json": "^0.76.2", + "@types/ramda": "~0.29.3", + "ramda": "~0.29.0", + "ramda-adjunct": "^4.0.0" } }, - "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", + "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": { + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0/-/apidom-parser-adapter-openapi-yaml-3-0-0.76.2.tgz", + "integrity": "sha512-spXabhd0sgX87QaYUDou22KduSL5GHCmLNuPDpPykYelB/zZnE8aPsrjBMIgK9CPZoQCDoWYYmtRTPfJjKwf3Q==", "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" + "dependencies": { + "@babel/runtime-corejs3": "^7.20.7", + "@swagger-api/apidom-core": "^0.76.2", + "@swagger-api/apidom-ns-openapi-3-0": "^0.76.2", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.76.2", + "@types/ramda": "~0.29.3", + "ramda": "~0.29.0", + "ramda-adjunct": "^4.0.0" } }, - "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", + "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": { + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1/-/apidom-parser-adapter-openapi-yaml-3-1-0.76.2.tgz", + "integrity": "sha512-KIEg9QWeiMMKQ9VtftK+1Rc7irKQjj0VTsoEtraun9N2MWLVt7g+xZKqbqtQ4/ovv5J8JBHE+hFGLdm2qZalsg==", "optional": true, - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" + "dependencies": { + "@babel/runtime-corejs3": "^7.20.7", + "@swagger-api/apidom-core": "^0.76.2", + "@swagger-api/apidom-ns-openapi-3-1": "^0.76.2", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.76.2", + "@types/ramda": "~0.29.3", + "ramda": "~0.29.0", + "ramda-adjunct": "^4.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-yaml-1-2/-/apidom-parser-adapter-yaml-1-2-0.74.1.tgz", - "integrity": "sha512-QHxx3ZJ12FAF8yserAR1qL863/eOdi78HgdDFqVeg5tOfUUDXLnvEYbtCWejIjudBFD6s88ctffzN7+DEDFOPg==", + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-yaml-1-2/-/apidom-parser-adapter-yaml-1-2-0.76.2.tgz", + "integrity": "sha512-nmEDYOfqeB8yCHbQ5yEQkJ09zIDOeX61KXTUktP4yErm96WVjIUk5YTTAkO7QbAEND9JHE+BAnS25cBC8BxFFA==", "optional": true, "dependencies": { "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-ast": "^0.74.1", - "@swagger-api/apidom-core": "^0.74.1", + "@swagger-api/apidom-ast": "^0.76.2", + "@swagger-api/apidom-core": "^0.76.2", + "@swagger-api/apidom-error": "^0.76.2", "@types/ramda": "~0.29.3", "ramda": "~0.29.0", - "ramda-adjunct": "^4.0.0", + "ramda-adjunct": "^4.1.1", "stampit": "^4.3.2", "tree-sitter": "=0.20.4", "tree-sitter-yaml": "=0.5.0", "web-tree-sitter": "=0.20.3" } }, - "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", - "optional": true, - "dependencies": { - "types-ramda": "^0.29.4" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, - "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", - "optional": true, - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" - } - }, "node_modules/@swagger-api/apidom-reference": { - "version": "0.74.1", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-0.74.1.tgz", - "integrity": "sha512-DwMGmTA2VkiPf8CLDnhhR4PObqzrGGOKydxd3uWWFFI0/itU8mZcBZssMHseW1dV2fC9hvkva672Gt2W/wSJng==", + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-0.76.2.tgz", + "integrity": "sha512-O1qX6Tql+B18Em/ERyqCzuhcvOG3JeRq4QIHfebzS3lNxpxX6si/z0DrL5K1azBldmnXx7UGqt/fvwq8GQJmIA==", "dependencies": { "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^0.74.1", + "@swagger-api/apidom-core": "^0.76.2", "@types/ramda": "~0.29.3", "axios": "^1.4.0", "minimatch": "^7.4.3", "process": "^0.11.10", "ramda": "~0.29.0", - "ramda-adjunct": "^4.0.0", + "ramda-adjunct": "^4.1.1", "stampit": "^4.3.2" }, "optionalDependencies": { - "@swagger-api/apidom-json-pointer": "^0.74.1", - "@swagger-api/apidom-ns-asyncapi-2": "^0.74.1", - "@swagger-api/apidom-ns-openapi-3-0": "^0.74.1", - "@swagger-api/apidom-ns-openapi-3-1": "^0.74.1", - "@swagger-api/apidom-parser-adapter-api-design-systems-json": "^0.74.1", - "@swagger-api/apidom-parser-adapter-api-design-systems-yaml": "^0.74.1", - "@swagger-api/apidom-parser-adapter-asyncapi-json-2": "^0.74.1", - "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": "^0.74.1", - "@swagger-api/apidom-parser-adapter-json": "^0.74.1", - "@swagger-api/apidom-parser-adapter-openapi-json-3-0": "^0.74.1", - "@swagger-api/apidom-parser-adapter-openapi-json-3-1": "^0.74.1", - "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "^0.74.1", - "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "^0.74.1", - "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.74.1" - } - }, - "node_modules/@swagger-api/apidom-reference/node_modules/@types/ramda": { - "version": "0.29.3", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.3.tgz", - "integrity": "sha512-Yh/RHkjN0ru6LVhSQtTkCRo6HXkfL9trot/2elzM/yXLJmbLm2v6kJc8yftTnwv1zvUob6TEtqI2cYjdqG3U0Q==", - "dependencies": { - "types-ramda": "^0.29.4" - } - }, - "node_modules/@swagger-api/apidom-reference/node_modules/axios": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", - "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "@swagger-api/apidom-error": "^0.76.2", + "@swagger-api/apidom-json-pointer": "^0.76.2", + "@swagger-api/apidom-ns-asyncapi-2": "^0.76.2", + "@swagger-api/apidom-ns-openapi-3-0": "^0.76.2", + "@swagger-api/apidom-ns-openapi-3-1": "^0.76.2", + "@swagger-api/apidom-parser-adapter-api-design-systems-json": "^0.76.2", + "@swagger-api/apidom-parser-adapter-api-design-systems-yaml": "^0.76.2", + "@swagger-api/apidom-parser-adapter-asyncapi-json-2": "^0.76.2", + "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": "^0.76.2", + "@swagger-api/apidom-parser-adapter-json": "^0.76.2", + "@swagger-api/apidom-parser-adapter-openapi-json-3-0": "^0.76.2", + "@swagger-api/apidom-parser-adapter-openapi-json-3-1": "^0.76.2", + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "^0.76.2", + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "^0.76.2", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.76.2" } }, "node_modules/@swagger-api/apidom-reference/node_modules/brace-expansion": { @@ -5088,30 +4580,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@swagger-api/apidom-reference/node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, - "node_modules/@swagger-api/apidom-reference/node_modules/ramda-adjunct": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.0.0.tgz", - "integrity": "sha512-W/NiJAlZdwZ/iUkWEQQgRdH5Szqqet1WoVH9cdqDVjFbVaZHuJfJRvsxqHhvq6tZse+yVbFatLDLdVa30wBlGQ==", - "engines": { - "node": ">=0.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda-adjunct" - }, - "peerDependencies": { - "ramda": ">= 0.29.0" - } - }, "node_modules/@tanstack/match-sorter-utils": { "version": "8.8.4", "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.8.4.tgz", @@ -5128,11 +4596,11 @@ } }, "node_modules/@tanstack/react-table": { - "version": "8.9.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.9.2.tgz", - "integrity": "sha512-Irvw4wqVF9hhuYzmNrlae4IKdlmgSyoRWnApSLebvYzqHoi5tEsYzBj6YPd0hX78aB/L+4w/jgK2eBQVpGfThQ==", + "version": "8.10.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.10.3.tgz", + "integrity": "sha512-Qya1cJ+91arAlW7IRDWksRDnYw28O446jJ/ljkRSc663EaftJoBCAU10M+VV1K6MpCBLrXq1BD5IQc1zj/ZEjA==", "dependencies": { - "@tanstack/table-core": "8.9.2" + "@tanstack/table-core": "8.10.3" }, "engines": { "node": ">=12" @@ -5147,11 +4615,11 @@ } }, "node_modules/@tanstack/react-virtual": { - "version": "3.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.0-beta.54.tgz", - "integrity": "sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ==", + "version": "3.0.0-beta.60", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.0-beta.60.tgz", + "integrity": "sha512-F0wL9+byp7lf/tH6U5LW0ZjBqs+hrMXJrj5xcIGcklI0pggvjzMNW9DdIBcyltPNr6hmHQ0wt8FDGe1n1ZAThA==", "dependencies": { - "@tanstack/virtual-core": "3.0.0-beta.54" + "@tanstack/virtual-core": "3.0.0-beta.60" }, "funding": { "type": "github", @@ -5162,9 +4630,9 @@ } }, "node_modules/@tanstack/table-core": { - "version": "8.9.2", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.9.2.tgz", - "integrity": "sha512-ajc0OF+karBAdaSz7OK09rCoAHB1XI1+wEhu+tDNMPc+XcO+dTlXXN/Vc0a8vym4kElvEjXEDd9c8Zfgt4bekA==", + "version": "8.10.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.10.3.tgz", + "integrity": "sha512-hJ55YfJlWbfzRROfcyA/kC1aZr/shsLA8XNAwN8jXylhYWGLnPmiJJISrUfj4dMMWRiFi0xBlnlC7MLH+zSrcw==", "engines": { "node": ">=12" }, @@ -5174,9 +4642,9 @@ } }, "node_modules/@tanstack/virtual-core": { - "version": "3.0.0-beta.54", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0-beta.54.tgz", - "integrity": "sha512-jtkwqdP2rY2iCCDVAFuaNBH3fiEi29aTn2RhtIoky8DTTiCdc48plpHHreLwmv1PICJ4AJUUESaq3xa8fZH8+g==", + "version": "3.0.0-beta.60", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0-beta.60.tgz", + "integrity": "sha512-QlCdhsV1+JIf0c0U6ge6SQmpwsyAT0oQaOSZk50AtEeAyQl9tQrd6qCHAslxQpgphrfe945abvKG8uYvw3hIGA==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -5191,9 +4659,9 @@ } }, "node_modules/@total-typescript/ts-reset": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@total-typescript/ts-reset/-/ts-reset-0.4.2.tgz", - "integrity": "sha512-vqd7ZUDSrXFVT1n8b2kc3LnklncDQFPvR58yUS1kEP23/nHPAO9l1lMjUfnPrXYYk4Hj54rrLKMW5ipwk7k09A==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@total-typescript/ts-reset/-/ts-reset-0.5.1.tgz", + "integrity": "sha512-AqlrT8YA1o7Ff5wPfMOL0pvL+1X+sw60NN6CcOCqs658emD6RfiXhF7Gu9QcfKBH7ELY2nInLhKSCWVoNL70MQ==", "dev": true }, "node_modules/@trysound/sax": { @@ -5260,9 +4728,9 @@ } }, "node_modules/@types/babel__core": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", - "integrity": "sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==", + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.2.tgz", + "integrity": "sha512-pNpr1T1xLUc2l3xJKuPtsEky3ybxN3m4fJkknfIpTCTfIZCDW57oAg+EfCgIIp2rvCe0Wn++/FfodDS4YXxBwA==", "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -5272,68 +4740,68 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.5.tgz", + "integrity": "sha512-h9yIuWbJKdOPLJTbmSpPzkF67e659PbQDba7ifWm5BJ8xTv+sDmS7rFmywkWOvXedGTivCdeGSIIX8WLcRTz8w==", "dependencies": { "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.2.tgz", + "integrity": "sha512-/AVzPICMhMOMYoSx9MoKpGDKdBRsIXMNByh1PXSZoa+v6ZoLa8xxtsT/uLQ/NJm0XVAWl/BvId4MlDeXJaeIZQ==", "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__traverse": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.1.tgz", - "integrity": "sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg==", + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.2.tgz", + "integrity": "sha512-ojlGK1Hsfce93J0+kn3H5R73elidKUaZonirN33GSmgTUMpzI/MIFfSpF3haANe3G1bEBS9/9/QEqwTzwqFsKw==", "dependencies": { "@babel/types": "^7.20.7" } }, "node_modules/@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz", + "integrity": "sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==", "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "node_modules/@types/bonjour": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", - "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "version": "3.5.11", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.11.tgz", + "integrity": "sha512-isGhjmBtLIxdHBDl2xGwUzEM8AOyOvWsADWq7rqirdi/ZQoHnLWErHvsThcEzTX8juDRiZtzp2Qkv5bgNh6mAg==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "version": "3.4.36", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", + "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", - "integrity": "sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.1.tgz", + "integrity": "sha512-iaQslNbARe8fctL5Lk+DsmgWOM83lM+7FzP0eQUJs1jd3kBE8NWqBTIT2S8SqQOJjxvt2eyIjpOuYeRXq2AdMw==", "dependencies": { "@types/express-serve-static-core": "*", "@types/node": "*" } }, "node_modules/@types/d3": { - "version": "5.16.4", - "resolved": "https://registry.npmjs.org/@types/d3/-/d3-5.16.4.tgz", - "integrity": "sha512-2u0O9iP1MubFiQ+AhR1id4Egs+07BLtvRATG6IL2Gs9+KzdrfaxCKNq5hxEyw1kxwsqB/lCgr108XuHcKtb/5w==", + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-5.16.0.tgz", + "integrity": "sha512-BPe6m763fJet428zPK3/fLBzgGlh204kEEisUcZGsUqgQHh0V+fOHhuGV3pyJtT2QLe+E0y5oqxNYix32OgmHA==", "dependencies": { "@types/d3-array": "^1", "@types/d3-axis": "^1", @@ -5344,92 +4812,92 @@ "@types/d3-contour": "^1", "@types/d3-dispatch": "^1", "@types/d3-drag": "^1", - "@types/d3-dsv": "^1", + "@types/d3-dsv": "*", "@types/d3-ease": "^1", - "@types/d3-fetch": "^1", + "@types/d3-fetch": "*", "@types/d3-force": "^1", "@types/d3-format": "^1", "@types/d3-geo": "^1", "@types/d3-hierarchy": "^1", - "@types/d3-interpolate": "^1", + "@types/d3-interpolate": "*", "@types/d3-path": "^1", "@types/d3-polygon": "^1", "@types/d3-quadtree": "^1", "@types/d3-random": "^1", - "@types/d3-scale": "^2", + "@types/d3-scale": "*", "@types/d3-scale-chromatic": "^1", "@types/d3-selection": "^1", "@types/d3-shape": "^1", "@types/d3-time": "^1", - "@types/d3-time-format": "^2", + "@types/d3-time-format": "*", "@types/d3-timer": "^1", - "@types/d3-transition": "^1", + "@types/d3-transition": "*", "@types/d3-voronoi": "*", - "@types/d3-zoom": "^1" + "@types/d3-zoom": "*" } }, "node_modules/@types/d3-array": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-1.2.9.tgz", - "integrity": "sha512-E/7RgPr2ylT5dWG0CswMi9NpFcjIEDqLcUSBgNHe/EMahfqYaTx4zhcggG3khqoEB/leY4Vl6nTSbwLUPjXceA==" + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-1.2.10.tgz", + "integrity": "sha512-b47UQ8RWEDdWdpxTdeppAZ1pyy64PMiLawItciimtvqBS1+FqUi3tk7iG0UT/6vQKMhuHpsMVVOadj71Q7vUcQ==" }, "node_modules/@types/d3-axis": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-1.0.16.tgz", - "integrity": "sha512-p7085weOmo4W+DzlRRVC/7OI/jugaKbVa6WMQGCQscaMylcbuaVEGk7abJLNyGVFLeCBNrHTdDiqRGnzvL0nXQ==", + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-1.0.17.tgz", + "integrity": "sha512-sQEfX7/mokx3ncUs6mpuicw+XFc6Drt/H9Axwc73KcCAmUdrdnexvBZGeZiyTjYw7RnA0DpOwrUwWfz8OfiS5Q==", "dependencies": { "@types/d3-selection": "^1" } }, "node_modules/@types/d3-brush": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-1.1.5.tgz", - "integrity": "sha512-4zGkBafJf5zCsBtLtvDj/pNMo5X9+Ii/1hUz0GvQ+wEwelUBm2AbIDAzJnp2hLDFF307o0fhxmmocHclhXC+tw==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-1.1.6.tgz", + "integrity": "sha512-eAqaEzE6zA1JbslrEHvDXMjADV5LyrIfK00YkgmxVKodvrPiw6JxVBseySO3YE3UNIZ/jBplE9NDIlpY7t5pwQ==", "dependencies": { "@types/d3-selection": "^1" } }, "node_modules/@types/d3-chord": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-1.0.11.tgz", - "integrity": "sha512-0DdfJ//bxyW3G9Nefwq/LDgazSKNN8NU0lBT3Cza6uVuInC2awMNsAcv1oKyRFLn9z7kXClH5XjwpveZjuz2eg==" + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-1.0.12.tgz", + "integrity": "sha512-JVjVlm+XxkScLWTogtELXPX/to9G9UOs6NIs9mNZI9e4AHIpA3K3vNCESoZd049iBQEGF+wAf1wGnByyv/kXVw==" }, "node_modules/@types/d3-collection": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-collection/-/d3-collection-1.0.10.tgz", - "integrity": "sha512-54Fdv8u5JbuXymtmXm2SYzi1x/Svt+jfWBU5junkhrCewL92VjqtCBDn97coBRVwVFmYNnVTNDyV8gQyPYfm+A==" + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-collection/-/d3-collection-1.0.11.tgz", + "integrity": "sha512-PN9XeRw8FyadFGrmK1f6VDo95sbJ1cKqGy9nyUzdC2xUdYSYmvJGLBcg/DUfS2a1Zh4tTqgE10HebuN/r8qSpw==" }, "node_modules/@types/d3-color": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-1.4.2.tgz", - "integrity": "sha512-fYtiVLBYy7VQX+Kx7wU/uOIkGQn8aAEY8oWMoyja3N4dLd8Yf6XgSIR/4yWvMuveNOH5VShnqCgRqqh/UNanBA==" + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-1.4.3.tgz", + "integrity": "sha512-jcHMwBcuuQ1LPt43jdbOhdOFczfDfhzvAZ1+1L0KiXPv4VqGsWAltxfxUDvtSuIMsvTZ2eeua+tOtxI6qqxYUg==" }, "node_modules/@types/d3-contour": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-1.3.3.tgz", - "integrity": "sha512-LxwmGIfVJIc1cKs7ZFRQ1FbtXpfH7QTXYRdMIJsFP71uCMdF6jJ0XZakYDX6Hn4yZkLf+7V8FgD34yCcok+5Ww==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-1.3.4.tgz", + "integrity": "sha512-tiPQPWBCwu4Xg3jZSwBn4GM17ZYbFCsVyHgmwHmLaGpxFYfdScEX5Sjgj33roNlhQGrd2g14tLACyvZJ0dtXxA==", "dependencies": { "@types/d3-array": "^1", "@types/geojson": "*" } }, "node_modules/@types/d3-dispatch": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-1.0.9.tgz", - "integrity": "sha512-zJ44YgjqALmyps+II7b1mZLhrtfV/FOxw9owT87mrweGWcg+WK5oiJX2M3SYJ0XUAExBduarysfgbR11YxzojQ==" + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-1.0.10.tgz", + "integrity": "sha512-QDjKymeWL+SNmHVlLO7e9/zgR59I1uKC+FockA7EifxfpzmkBnqapzOUGDgi5bt8WBUg10mhTzWAyqruuixSGQ==" }, "node_modules/@types/d3-drag": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-1.2.5.tgz", - "integrity": "sha512-7NeTnfolst1Js3Vs7myctBkmJWu6DMI3k597AaHUX98saHjHWJ6vouT83UrpE+xfbSceHV+8A0JgxuwgqgmqWw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-1.2.6.tgz", + "integrity": "sha512-vG4mVNCKKYee3+C0p/Qk4q0W0zBU4tG9ub1DltjZ2edLK/5SKssu3f1IqzuDSPnAMs5oFYLsI6yd4phUZ1KAlg==", "dependencies": { "@types/d3-selection": "^1" } }, "node_modules/@types/d3-dsv": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-1.2.2.tgz", - "integrity": "sha512-GRnz9z8ypqb7OsQ/xw/BmFAp0/k3pgM1s19FTZZSlCMY0EvyVTkU8xzZKKDXzytGXPpTNC4R5pGl9oxEvVSnHQ==" + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.4.tgz", + "integrity": "sha512-YxfUVJ55HxR8oq88136w09mBMPNhgH7PZjteq72onWXWOohGif/cLQnQv8V4A5lEGjXF04LhwSTpmzpY9wyVyA==" }, "node_modules/@types/d3-ease": { "version": "1.0.11", @@ -5437,42 +4905,42 @@ "integrity": "sha512-wUigPL0kleGZ9u3RhzBP07lxxkMcUjL5IODP42mN/05UNL+JJCDnpEPpFbJiPvLcTeRKGIRpBBJyP/1BNwYsVA==" }, "node_modules/@types/d3-fetch": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-1.2.2.tgz", - "integrity": "sha512-rtFs92GugtV/NpiJQd0WsmGLcg52tIL0uF0bKbbJg231pR9JEb6HT4AUwrtuLq3lOeKdLBhsjV14qb0pMmd0Aw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.4.tgz", + "integrity": "sha512-RleYajubALkGjrvatxWhlygfvB1KNF0Uzz9guRUeeA+M/2B7l8rxObYdktaX9zU1st04lMCHjZWe4vbl+msH2Q==", "dependencies": { - "@types/d3-dsv": "^1" + "@types/d3-dsv": "*" } }, "node_modules/@types/d3-force": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-1.2.4.tgz", - "integrity": "sha512-fkorLTKvt6AQbFBQwn4aq7h9rJ4c7ZVcPMGB8X6eFFveAyMZcv7t7m6wgF4Eg93rkPgPORU7sAho1QSHNcZu6w==" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-1.2.5.tgz", + "integrity": "sha512-1TB2IqtkPXsr7zUgPORayl2xsl28X4WMwlpaw2BLKTQpJ5ePO1t6TkM4spbTwoqc6dWipVTwg0gdOVrbzGQPNQ==" }, "node_modules/@types/d3-format": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.2.tgz", - "integrity": "sha512-WeGCHAs7PHdZYq6lwl/+jsl+Nfc1J2W1kNcMeIMYzQsT6mtBDBgtJ/rcdjZ0k0rVIvqEZqhhuD5TK/v3P2gFHQ==" + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.3.tgz", + "integrity": "sha512-Rp3dUYGqPSn4RY+GDW1GfY++JoFvnXU2E+5pU0/4iYLVgdwt029lRlAsAeHk9lJvq3UXl10l09Cmmj2G1wnNlA==" }, "node_modules/@types/d3-geo": { - "version": "1.12.4", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-1.12.4.tgz", - "integrity": "sha512-lNDaAuOaML6w2d1XE0Txr5YOXLBQSF1q2IU6eXh/u1TTPQSm2Ah+TMIub1+CIMq8J/7DOzi5Cr8/yHqjNvqLKA==", + "version": "1.12.5", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-1.12.5.tgz", + "integrity": "sha512-YRqBbphH5XZuNtsVsjWKDZZYparpwxDlEiQSdROePXJYZ+Ibi4UwCkSA3hIH484c/kvUv8Dpx5ViefKTLsC1Ow==", "dependencies": { "@types/geojson": "*" } }, "node_modules/@types/d3-hierarchy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz", - "integrity": "sha512-AbStKxNyWiMDQPGDguG2Kuhlq1Sv539pZSxYbx4UZeYkutpPwXCcgyiRrlV4YH64nIOsKx7XVnOMy9O7rJsXkg==" + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-OmR+pfqnz0qd+gaDJUoJBBzhvZQOwtNAjhXSztBbBDtyUXkzGsPlEv4KSGJ2zm5lNPtxG7v8Zifixk0jpFPlCQ==" }, "node_modules/@types/d3-interpolate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-1.4.2.tgz", - "integrity": "sha512-ylycts6llFf8yAEs1tXzx2loxxzDZHseuhPokrqKprTQSTcD3JbJI1omZP1rphsELZO3Q+of3ff0ZS7+O6yVzg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.2.tgz", + "integrity": "sha512-zAbCj9lTqW9J9PlF4FwnvEjXZUy75NQqPm7DMHZXuxCFTpuTrdK2NMYGQekf4hlasL78fCYOLu4EE3/tXElwow==", "dependencies": { - "@types/d3-color": "^1" + "@types/d3-color": "*" } }, "node_modules/@types/d3-path": { @@ -5486,9 +4954,9 @@ "integrity": "sha512-1TOJPXCBJC9V3+K3tGbTqD/CsqLyv/YkTXAcwdsZzxqw5cvpdnCuDl42M4Dvi8XzMxZNCT9pL4ibrK2n4VmAcw==" }, "node_modules/@types/d3-quadtree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-1.0.9.tgz", - "integrity": "sha512-5E0OJJn2QVavITFEc1AQlI8gLcIoDZcTKOD3feKFckQVmFV4CXhqRFt83tYNVNIN4ZzRkjlAMavJa1ldMhf5rA==" + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-1.0.10.tgz", + "integrity": "sha512-mtSpFuOo7ZzOpZlGok8jyaPIqLYm3yKwipnXWKp3g0Oq6locFNQEDgkWytIrALWuoZezTAER31jHDbee6V5XJg==" }, "node_modules/@types/d3-random": { "version": "1.1.3", @@ -5496,11 +4964,11 @@ "integrity": "sha512-XXR+ZbFCoOd4peXSMYJzwk0/elP37WWAzS/DG+90eilzVbUSsgKhBcWqylGWe+lA2ubgr7afWAOBaBxRgMUrBQ==" }, "node_modules/@types/d3-scale": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-2.2.6.tgz", - "integrity": "sha512-CHu34T5bGrJOeuhGxyiz9Xvaa9PlsIaQoOqjDg7zqeGj2x0rwPhGquiy03unigvcMxmvY0hEaAouT0LOFTLpIw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.5.tgz", + "integrity": "sha512-w/C++3W394MHzcLKO2kdsIn5KKNTOqeQVzyPSGPLzQbkPw/jpeaGtSRlakcKevGgGsjJxGsbqS0fPrVFDbHrDA==", "dependencies": { - "@types/d3-time": "^1" + "@types/d3-time": "*" } }, "node_modules/@types/d3-scale-chromatic": { @@ -5509,27 +4977,27 @@ "integrity": "sha512-7FtJYrmXTEWLykShjYhoGuDNR/Bda0+tstZMkFj4RRxUEryv16AGh3be21tqg84B6KfEwiZyEpBcTyPyU+GWjg==" }, "node_modules/@types/d3-selection": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-1.4.3.tgz", - "integrity": "sha512-GjKQWVZO6Sa96HiKO6R93VBE8DUW+DDkFpIMf9vpY5S78qZTlRRSNUsHr/afDpF7TvLDV7VxrUFOWW7vdIlYkA==" + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-1.4.4.tgz", + "integrity": "sha512-nbt9x1vP2C1Wz0JxZ2aSYFvJQIukc1QdL1zGHe5O989bDHpgrVz1mgmA/8n+vapb7g3mjUPe2YoLrqEalmtxKA==" }, "node_modules/@types/d3-shape": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.8.tgz", - "integrity": "sha512-gqfnMz6Fd5H6GOLYixOZP/xlrMtJms9BaS+6oWxTKHNqPGZ93BkWWupQSCYm6YHqx6h9wjRupuJb90bun6ZaYg==", + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.9.tgz", + "integrity": "sha512-NX8FSlYqN4MPjiOwJAu5a3y6iEj7lS8nb8zP5dQpHOWh24vMJLTXno7c7wm72SfTFNAalfvZVsatMUrEa686gg==", "dependencies": { "@types/d3-path": "^1" } }, "node_modules/@types/d3-time": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-1.1.1.tgz", - "integrity": "sha512-ULX7LoqXTCYtM+tLYOaeAJK7IwCT+4Gxlm2MaH0ErKLi07R5lh8NHCAyWcDkCCmx1AfRcBEV6H9QE9R25uP7jw==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-1.1.2.tgz", + "integrity": "sha512-CHxXBqjSFt/w7OiGIy84L2nbzNwWhBaK6xyeAAWqKLgKlHY38vg33BMFMTCT8YOYeinNIQIQPjGvZLO8Z9M8WA==" }, "node_modules/@types/d3-time-format": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.1.tgz", - "integrity": "sha512-fck0Z9RGfIQn3GJIEKVrp15h9m6Vlg0d5XXeiE/6+CQiBmMDZxfR21XtjEPuDeg7gC3bBM0SdieA5XF3GW1wKA==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.1.tgz", + "integrity": "sha512-Br6EFeu9B1Zrem7KaYbr800xCmEDyq8uE60kEU8rWhC/XpFYX6ocGMZuRJDQfFCq6SyakQxNHFqIfJbFLf4x6Q==" }, "node_modules/@types/d3-timer": { "version": "1.0.10", @@ -5537,89 +5005,89 @@ "integrity": "sha512-ZnAbquVqy+4ZjdW0cY6URp+qF/AzTVNda2jYyOzpR2cPT35FTXl78s15Bomph9+ckOiI1TtkljnWkwbIGAb6rg==" }, "node_modules/@types/d3-transition": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-1.3.2.tgz", - "integrity": "sha512-J+a3SuF/E7wXbOSN19p8ZieQSFIm5hU2Egqtndbc54LXaAEOpLfDx4sBu/PKAKzHOdgKK1wkMhINKqNh4aoZAg==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.5.tgz", + "integrity": "sha512-dcfjP6prFxj3ziFOJrnt4W2P0oXNj/sGxsJXH8286sHtVZ4qWGbjuZj+RRCYx4YZ4C0izpeE8OqXVCtoWEtzYg==", "dependencies": { - "@types/d3-selection": "^1" + "@types/d3-selection": "*" } }, "node_modules/@types/d3-voronoi": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@types/d3-voronoi/-/d3-voronoi-1.1.9.tgz", - "integrity": "sha512-DExNQkaHd1F3dFPvGA/Aw2NGyjMln6E9QzsiqOcBgnE+VInYnFBHBBySbZQts6z6xD+5jTfKCP7M4OqMyVjdwQ==" + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@types/d3-voronoi/-/d3-voronoi-1.1.10.tgz", + "integrity": "sha512-OOnVvmmth88o/MM3hCrGGBPtwKuU5vj0XLdL5GXtda1JgzyORb43nAUw/tA0ifkijvrXS/vy4Faw8Iy6dpmbWg==" }, "node_modules/@types/d3-zoom": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-1.8.4.tgz", - "integrity": "sha512-K+6jCM9llyC5U4WvkmiXbCoOIuUX03Wi72C/L9PMPVxymWDaxTHzDgHD/HYlEyDRGiVp7D77m7XPcD/m/TRDrw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.5.tgz", + "integrity": "sha512-mIefdTLtxuWUWTbBupCUXPAXVPmi8/Uwrq41gQpRh0rD25GMU1ku+oTELqNY2NuuiI0F3wXC5e1liBQi7YS7XQ==", "dependencies": { - "@types/d3-interpolate": "^1", - "@types/d3-selection": "^1" + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" } }, "node_modules/@types/debug": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", - "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.9.tgz", + "integrity": "sha512-8Hz50m2eoS56ldRlepxSBa6PWEVCtzUo/92HgLc2qTMnotJNIm7xP+UZhyWoYsyOdd5dxZ+NZLb24rsKyFs2ow==", "dev": true, "dependencies": { "@types/ms": "*" } }, "node_modules/@types/draft-convert": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@types/draft-convert/-/draft-convert-2.1.4.tgz", - "integrity": "sha512-NbhE0ijzENK6ZQa3zO0KK6cNS80XESnQ03Bg7DEzE/qBg6YvpIEni6vzruYCYPkm1Sc6z0K7XbtJu4p53elGdA==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/draft-convert/-/draft-convert-2.1.5.tgz", + "integrity": "sha512-gRJphK0P4yl/Px5iVZXJFB5ojq4aF91QL77vR0lBth9w66dnceOFn5lj3WoKdr5fXIaPKQoA4JRxRtdNoAr1TQ==", "dependencies": { "@types/draft-js": "*", "@types/react": "*" } }, "node_modules/@types/draft-js": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@types/draft-js/-/draft-js-0.11.10.tgz", - "integrity": "sha512-o/DST8x0wNerwhRiaE577UHFIXb6HCywaZOZfj9TF2vU3CONvsCGoQmdOsKqERdXp+3ZNlSvFUH0B8lEEYOT4A==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@types/draft-js/-/draft-js-0.11.13.tgz", + "integrity": "sha512-4cMlNWoKwcRwkA+GtCD53u5Dj78n9Z8hScB5rZYKw4Q3UZxh0h1Fm4ezLgGjAHkhsdrviWQm3MPuUi/jLOuq8A==", "dependencies": { "@types/react": "*", "immutable": "~3.7.4" } }, "node_modules/@types/draftjs-to-html": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@types/draftjs-to-html/-/draftjs-to-html-0.8.1.tgz", - "integrity": "sha512-NBkphQs+qZ/sAz/j1pCUaxkPAOx00LTsE88aMSSfcvK+UfCpjHJDqIMCkm6wKotuJvY5w0BtdRazQ0sAaXzPdg==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@types/draftjs-to-html/-/draftjs-to-html-0.8.2.tgz", + "integrity": "sha512-bjxcYNJJ7RTvYFx+P557aHs8TIVqqzJsl1kkxt4r9r+StxIdsTcrHZ2fhGKJ+jZRLsD5o+LReDFGeqzNghrGHQ==", "dependencies": { "@types/draft-js": "*" } }, "node_modules/@types/eslint": { - "version": "8.44.2", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", - "integrity": "sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==", + "version": "8.44.3", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz", + "integrity": "sha512-iM/WfkwAhwmPff3wZuPLYiHX18HI24jU8k1ZSH7P8FHwxTjZ2P6CoX2wnF43oprR+YXJM6UUxATkNvyv/JHd+g==", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "node_modules/@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.5.tgz", + "integrity": "sha512-JNvhIEyxVW6EoMIFIvj93ZOywYFatlpu9deeH6eSx6PE3WHYvHaQtmHmQeNw7aA81bYGBPPQqdtBm6b1SsQMmA==", "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "node_modules/@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.2.tgz", + "integrity": "sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==" }, "node_modules/@types/express": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", - "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "version": "4.17.18", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.18.tgz", + "integrity": "sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -5628,9 +5096,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.35", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz", - "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==", + "version": "4.17.37", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.37.tgz", + "integrity": "sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==", "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -5639,30 +5107,30 @@ } }, "node_modules/@types/geojson": { - "version": "7946.0.10", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", - "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" + "version": "7946.0.11", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.11.tgz", + "integrity": "sha512-L7A0AINMXQpVwxHJ4jxD6/XjZ4NDufaRlUJHjNIFKYUFBH1SvOW+neaqb0VTRSLW5suSrSu19ObFEFnfNcr+qg==" }, "node_modules/@types/graceful-fs": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", - "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.7.tgz", + "integrity": "sha512-MhzcwU8aUygZroVwL2jeYk6JisJrPl/oov/gsgGCue9mkgl9wjGbzReYQClxiUgFDnib9FuHqTndccKeZKxTRw==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/hast": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.5.tgz", - "integrity": "sha512-SvQi0L/lNpThgPoleH53cdjB3y9zpLlVjRbqB3rH8hx1jiRSBGAhyjV3H+URFjNVRqt2EdYNrbZE5IsGlNfpRg==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.6.tgz", + "integrity": "sha512-47rJE80oqPmFdVDCD7IheXBrVdwuBgsYwoczFvKmwfo2Mzsnt+V9OONsYauFmICb6lQPpCuXYJWejBNs4pDJRg==", "dependencies": { "@types/unist": "^2" } }, "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", - "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-YIQtIg4PKr7ZyqNPZObpxfHsHEmuB8dXCxd6qVcGuQVDK2bpsF7bYNnBJ4Nn7giuACZg+WewExgrtAJ3XnA4Xw==", "dependencies": { "@types/react": "*", "hoist-non-react-statics": "^3.3.0" @@ -5674,14 +5142,14 @@ "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==" }, "node_modules/@types/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==" + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz", + "integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==" }, "node_modules/@types/http-proxy": { - "version": "1.17.11", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz", - "integrity": "sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==", + "version": "1.17.12", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.12.tgz", + "integrity": "sha512-kQtujO08dVtQ2wXAuSFfk9ASy3sug4+ogFR8Kd8UgP8PEuc1/G/8yjYRmp//PcDNJEUKOza/MrQu15bouEUCiw==", "dependencies": { "@types/node": "*" } @@ -5692,31 +5160,31 @@ "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==" }, "node_modules/@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-gPQuzaPR5h/djlAv2apEG1HVOyj1IUs7GpfMZixU0/0KXT3pm64ylHuMUI1/Akh+sq/iikxg6Z2j+fcMDXaaTQ==", "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "node_modules/@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-kv43F9eb3Lhj+lr/Hn6OcLCs/sSM8bt+fIaP11rCYngfV6NVjzWXJ17owQtDQTL9tQ8WSLUrGsSJ6rJz0F1w1A==", "dependencies": { "@types/istanbul-lib-report": "*" } }, "node_modules/@types/js-cookie": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.3.tgz", - "integrity": "sha512-Xe7IImK09HP1sv2M/aI+48a20VX+TdRJucfq4vfRVy6nWN8PYPOEnlMRSgxJAgYQIXJVL8dZ4/ilAM7dWNaOww==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.4.tgz", + "integrity": "sha512-vMMnFF+H5KYqdd/myCzq6wLDlPpteJK+jGFgBus3Da7lw+YsDmx2C8feGTzY2M3Fo823yON+HC2CL240j4OV+w==", "dev": true }, "node_modules/@types/json-schema": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", - "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==" + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", + "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==" }, "node_modules/@types/json5": { "version": "0.0.29", @@ -5724,26 +5192,26 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, "node_modules/@types/lodash": { - "version": "4.14.191", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz", - "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==", + "version": "4.14.199", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.199.tgz", + "integrity": "sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg==", "dev": true }, "node_modules/@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", + "integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==" }, "node_modules/@types/ms": { - "version": "0.7.31", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", - "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "version": "0.7.32", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.32.tgz", + "integrity": "sha512-xPSg0jm4mqgEkNhowKgZFBNtwoEwF6gJ4Dhww+GFpm3IgtNseHQZ5IqdNwnquZEoANxyDAKDRAdVo4Z72VvD/g==", "dev": true }, "node_modules/@types/node": { - "version": "16.11.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.20.tgz", - "integrity": "sha512-lAKaZ0Lc1Umwd0AqLr6iy5U8u/1DpK7/JzNgQn9cMMUk2mFR8bbhEP8BQrI9Cm5CU0bOVCaWbkGBvgqKMOJHsw==" + "version": "18.16.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.1.tgz", + "integrity": "sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA==" }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -5759,9 +5227,9 @@ } }, "node_modules/@types/plotly.js": { - "version": "2.12.26", - "resolved": "https://registry.npmjs.org/@types/plotly.js/-/plotly.js-2.12.26.tgz", - "integrity": "sha512-vP1iaVL4HHYSbugv49pwtLL6D9CSqOnQLjiRRdRYjVMEDbjIWhMgxc49BJAxSUShupiJHDp35e0WJS9SwIB2WA==", + "version": "2.12.27", + "resolved": "https://registry.npmjs.org/@types/plotly.js/-/plotly.js-2.12.27.tgz", + "integrity": "sha512-Ah7XuePFNxu2XAHG79GeKN/Ky8dZ0k6hzy49da6AeZFrTqO5wDbtJovp3co3C+iRitp8tA6rIxkltiJ3cjsQWw==", "dev": true }, "node_modules/@types/prettier": { @@ -5770,44 +5238,37 @@ "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==" }, "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + "version": "15.7.8", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.8.tgz", + "integrity": "sha512-kMpQpfZKSCBqltAJwskgePRaYRFukDkm1oItcAbC3gNELR20XIBcN9VRgg4+m8DKsTfkWeA4m4Imp4DDuWy7FQ==" }, "node_modules/@types/q": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz", - "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==" + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.6.tgz", + "integrity": "sha512-IKjZ8RjTSwD4/YG+2gtj7BPFRB/lNbWKTiSj3M7U/TD2B7HfYCxvp2Zz6xA2WIY7pAuL1QOUPw8gQRbUrrq4fQ==" }, "node_modules/@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + "version": "6.9.8", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz", + "integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==" }, "node_modules/@types/ramda": { - "version": "0.28.23", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.28.23.tgz", - "integrity": "sha512-9TYWiwkew+mCMsL7jZ+kkzy6QXn8PL5/SKmBPmjgUlTpkokZWTBr+OhiIUDztpAEbslWyt24NNfEmZUBFmnXig==", - "dev": true, + "version": "0.29.5", + "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.29.5.tgz", + "integrity": "sha512-oBBdRfoZoCl/aBIpBbct/uUHAbJ5i7vSOHK83SvH2Qr9ermYITRNKnEYgGJlnkagUY2cu8L2//Jq7o1355Go5A==", "dependencies": { - "ts-toolbelt": "^6.15.1" + "types-ramda": "^0.29.4" } }, - "node_modules/@types/ramda/node_modules/ts-toolbelt": { - "version": "6.15.5", - "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-6.15.5.tgz", - "integrity": "sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==", - "dev": true - }, "node_modules/@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz", + "integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==" }, "node_modules/@types/react": { - "version": "18.0.28", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz", - "integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==", + "version": "18.2.24", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.24.tgz", + "integrity": "sha512-Ee0Jt4sbJxMu1iDcetZEIKQr99J1Zfb6D4F3qfUWoR1JpInkY1Wdg4WwCyBjL257D0+jGqSl1twBjV8iCaC0Aw==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -5815,18 +5276,18 @@ } }, "node_modules/@types/react-beautiful-dnd": { - "version": "13.1.3", - "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.3.tgz", - "integrity": "sha512-BNdmvONKtsrZq3AGrujECQrIn8cDT+fZsxBLXuX3YWY/nHfZinUFx4W88eS0rkcXzuLbXpKOsu/1WCMPMLEpPg==", + "version": "13.1.5", + "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.5.tgz", + "integrity": "sha512-mzohmMtV48b0bXF2dP8188LzUv9HAGHKucOORYsd5Sqq830pJ4gseFyDDAH0TR4TeD1ceG9DxTQ0FOFbtCSy4Q==", "dev": true, "dependencies": { "@types/react": "*" } }, "node_modules/@types/react-color": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.6.tgz", - "integrity": "sha512-OzPIO5AyRmLA7PlOyISlgabpYUa3En74LP8mTMa0veCA719SvYQov4WLMsHvCgXP+L+KI9yGhYnqZafVGG0P4w==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.7.tgz", + "integrity": "sha512-IGZA7e8Oia0+Sb3/1KP0qTThGelZ9DRspfeLrFWQWv5vXHiYlJJQMC2kgQr75CtP4uL8/kvT8qBgrOVlxVoNTw==", "dev": true, "dependencies": { "@types/react": "*", @@ -5834,33 +5295,25 @@ } }, "node_modules/@types/react-d3-graph": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/@types/react-d3-graph/-/react-d3-graph-2.6.3.tgz", - "integrity": "sha512-OYM7eO/4U6ARP8E3izk5VrkHLqO/0YI3QGFymLGt/AR/ETbc4Spx0Ljpa/3hJ/0wLmncLTyUV6gegcNooDAEww==", + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@types/react-d3-graph/-/react-d3-graph-2.6.5.tgz", + "integrity": "sha512-bao1+Zu1qhuFyE7K/Nk9HdmDlz39YdDB2z6TpKhujip4IjoIfE3smL2cIe4pFirL772rVxaJdAARhwok1cB/sA==", "dependencies": { "@types/react": "*" } }, "node_modules/@types/react-dom": { - "version": "18.0.11", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz", - "integrity": "sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/react-is": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-18.2.1.tgz", - "integrity": "sha512-wyUkmaaSZEzFZivD8F2ftSyAfk6L+DfFliVj/mYdOXbVjRcS87fQJLTnhk6dRZPuJjI+9g6RZJO4PNCngUrmyw==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.8.tgz", + "integrity": "sha512-bAIvO5lN/U8sPGvs1Xm61rlRHHaq5rp5N3kp9C+NJ/Q41P8iqjkXSu0+/qu8POsjH9pNWb0OYabFez7taP7omw==", "dependencies": { "@types/react": "*" } }, "node_modules/@types/react-plotly.js": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@types/react-plotly.js/-/react-plotly.js-2.6.0.tgz", - "integrity": "sha512-nJJ57U0/CNDAO+F3dpnMgM8PtjLE/O1I3O6gq4+5Q13uKqrPnHGYOttfdzQJ4D7KYgF609miVzEYakUS2zds8w==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@types/react-plotly.js/-/react-plotly.js-2.6.1.tgz", + "integrity": "sha512-vFJZRCC2Pav0NdrFm0grPMm9+67ejGZZglDBWqo+J6VFbB4CAatjoNiowfardznuujaaoDNoZ4MSCFwYyVk4aA==", "dev": true, "dependencies": { "@types/plotly.js": "*", @@ -5868,9 +5321,9 @@ } }, "node_modules/@types/react-redux": { - "version": "7.1.25", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz", - "integrity": "sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==", + "version": "7.1.27", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.27.tgz", + "integrity": "sha512-xj7d9z32p1K/eBmO+OEy+qfaWXtcPlN8f1Xk3Ne0p/ZRQ867RI5bQ/bpBtxbqU1AHNhKJSgGvld/P2myU2uYkg==", "dependencies": { "@types/hoist-non-react-statics": "^3.3.0", "@types/react": "*", @@ -5879,9 +5332,9 @@ } }, "node_modules/@types/react-transition-group": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", - "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==", + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.7.tgz", + "integrity": "sha512-ICCyBl5mvyqYp8Qeq9B5G/fyBSRC0zx3XM3sCC6KkcMsNeAHqXBKkmat4GqdJET5jtYUpZXrxI5flve5qhi2Eg==", "dependencies": { "@types/react": "*" } @@ -5896,27 +5349,27 @@ } }, "node_modules/@types/react-window": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz", - "integrity": "sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==", + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.6.tgz", + "integrity": "sha512-AVJr3A5rIO9dQQu5TwTN0lP2c1RtuqyyZGCt7PGP8e5gUpn1PuQRMJb/u3UpdbwTHh4wbEi33UMW5NI0IXt1Mg==", "dev": true, "dependencies": { "@types/react": "*" } }, "node_modules/@types/reactcss": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.6.tgz", - "integrity": "sha512-qaIzpCuXNWomGR1Xq8SCFTtF4v8V27Y6f+b9+bzHiv087MylI/nTCqqdChNeWS7tslgROmYB7yeiruWX7WnqNg==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.7.tgz", + "integrity": "sha512-MYPuVierMjIo0EDQnNauvBA94IOeB9lfjC619g+26u7ilsTtoFv6X7eQvaw79Fqqpi0yzoSMz0nUazeJlQUZnA==", "dev": true, "dependencies": { "@types/react": "*" } }, "node_modules/@types/redux-logger": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/redux-logger/-/redux-logger-3.0.9.tgz", - "integrity": "sha512-cwYhVbYNgH01aepeMwhd0ABX6fhVB2rcQ9m80u8Fl50ZODhsZ8RhQArnLTkE7/Zrfq4Sz/taNoF7DQy9pCZSKg==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/redux-logger/-/redux-logger-3.0.10.tgz", + "integrity": "sha512-wjcuptRVZQoWjbwKdLrEnit9A7TvW0HD6ZYNQGWRRCUBBux2RT8JxqwYemaS2fTQT3ebOvsszt2L/Hrylxh5PA==", "dev": true, "dependencies": { "redux": "^4.0.0" @@ -5936,36 +5389,36 @@ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.4.tgz", + "integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==" }, "node_modules/@types/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==" + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==" }, "node_modules/@types/send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz", - "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.2.tgz", + "integrity": "sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==", "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "node_modules/@types/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-asaEIoc6J+DbBKXtO7p2shWUpKacZOoMBEGBgPG91P8xhO53ohzHWGCs4ScZo5pQMf5ukQzVT9fhX1WzpHihig==", "dependencies": { "@types/express": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.2.tgz", - "integrity": "sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.3.tgz", + "integrity": "sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==", "dependencies": { "@types/http-errors": "*", "@types/mime": "*", @@ -5973,9 +5426,9 @@ } }, "node_modules/@types/sockjs": { - "version": "0.3.33", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", - "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "version": "0.3.34", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.34.tgz", + "integrity": "sha512-R+n7qBFnm/6jinlteC9DBL5dGiDGjWAvjo4viUanpnc/dG1y7uDoacXPIQ/PQEg1fI912SMHIa014ZjRpvDw4g==", "dependencies": { "@types/node": "*" } @@ -5986,23 +5439,23 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" }, "node_modules/@types/swagger-ui-react": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@types/swagger-ui-react/-/swagger-ui-react-4.11.0.tgz", - "integrity": "sha512-WPMR+GWbLd7zvY/uOTGzGzP44zK2rIZSnU00+pDR2YANnEV6/qj0kqYfdSY1Vk6qdvI7dR0Tx8JEMgUUrUQDjw==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-react/-/swagger-ui-react-4.18.1.tgz", + "integrity": "sha512-nYhNi+cyN78vve1/QY5PNKYzHYlDKETtXj+gQAhuoCRB+GxGT3MVJUj8WCdwYj4vF0s1j68qkLv/66DGe5ZlnA==", "dev": true, "dependencies": { "@types/react": "*" } }, "node_modules/@types/trusted-types": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz", - "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.4.tgz", + "integrity": "sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==" }, "node_modules/@types/unist": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.7.tgz", - "integrity": "sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g==" + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.8.tgz", + "integrity": "sha512-d0XxK3YTObnWVp6rZuev3c49+j4Lo8g4L1ZRm9z5L0xpoZycUPshHgczK5gsUMaZOstjVYYi09p5gYvUtfChYw==" }, "node_modules/@types/use-sync-external-store": { "version": "0.0.3", @@ -6010,15 +5463,15 @@ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" }, "node_modules/@types/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.4.tgz", + "integrity": "sha512-zAuJWQflfx6dYJM62vna+Sn5aeSWhh3OB+wfUEACNcqUSc0AGc5JKl+ycL1vrH7frGTXhJchYjE1Hak8L819dA==", "dev": true }, "node_modules/@types/ws": { - "version": "8.5.5", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", - "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.6.tgz", + "integrity": "sha512-8B5EO9jLVCy+B58PLHvLDuOD8DRVMgQzq8d55SjLCOn9kqGyqOvy27exVaTio1q1nX5zLu8/6N0n2ThSxOM6tg==", "dependencies": { "@types/node": "*" } @@ -6033,44 +5486,46 @@ } }, "node_modules/@types/yargs": { - "version": "16.0.5", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.5.tgz", - "integrity": "sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ==", + "version": "16.0.6", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.6.tgz", + "integrity": "sha512-oTP7/Q13GSPrgcwEwdlnkoZSQ1Hg9THe644qq8PG6hhJzjZ3qj1JjEFPIwWV/IXVs5XGIVqtkNOS9kh63WIJ+A==", "dependencies": { "@types/yargs-parser": "*" } }, "node_modules/@types/yargs-parser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.1.tgz", + "integrity": "sha512-axdPBuLuEJt0c4yI5OZssC19K2Mq1uKdrfZBzuxLvaztgqUtFYZUNw7lETExPYJR9jdEoIg4mb7RQKRQzOkeGQ==" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.53.0.tgz", - "integrity": "sha512-alFpFWNucPLdUOySmXCJpzr6HKC3bu7XooShWM+3w/EL6J2HIoB2PFxpLnq4JauWVk6DiVeNKzQlFEaE+X9sGw==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.3.tgz", + "integrity": "sha512-vntq452UHNltxsaaN+L9WyuMch8bMd9CqJ3zhzTPXXidwbf5mqqKCVXEuvRZUqLJSTLeWE65lQwyXsRGnXkCTA==", + "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.53.0", - "@typescript-eslint/type-utils": "5.53.0", - "@typescript-eslint/utils": "5.53.0", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.7.3", + "@typescript-eslint/type-utils": "6.7.3", + "@typescript-eslint/utils": "6.7.3", + "@typescript-eslint/visitor-keys": "6.7.3", "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "regexpp": "^3.2.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -6191,25 +5646,47 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/@typescript-eslint/parser": { - "version": "5.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.53.0.tgz", - "integrity": "sha512-MKBw9i0DLYlmdOb3Oq/526+al20AJZpANdT6Ct9ffxcV8nKCHz63t/S0IhlTFNsBIHJv+GY5SFJ0XfqVeydQrQ==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.3.tgz", + "integrity": "sha512-TlutE+iep2o7R8Lf+yoer3zU6/0EAUc8QIBB3GYBc1KGz4c4TRm83xwXUZVPlZ6YCLss4r77jbu6j3sendJoiQ==", + "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.53.0", - "@typescript-eslint/types": "5.53.0", - "@typescript-eslint/typescript-estree": "5.53.0", + "@typescript-eslint/scope-manager": "6.7.3", + "@typescript-eslint/types": "6.7.3", + "@typescript-eslint/typescript-estree": "6.7.3", + "@typescript-eslint/visitor-keys": "6.7.3", "debug": "^4.3.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -6218,15 +5695,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.53.0.tgz", - "integrity": "sha512-Opy3dqNsp/9kBBeCPhkCNR7fmdSQqA+47r21hr9a14Bx0xnkElEQmhoHga+VoaoQ6uDHjDKmQPIYcUcKJifS7w==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.3.tgz", + "integrity": "sha512-wOlo0QnEou9cHO2TdkJmzF7DFGvAKEnB82PuPNHpT8ZKKaZu6Bm63ugOTn9fXNJtvuDPanBc78lGUGGytJoVzQ==", + "dev": true, "dependencies": { - "@typescript-eslint/types": "5.53.0", - "@typescript-eslint/visitor-keys": "5.53.0" + "@typescript-eslint/types": "6.7.3", + "@typescript-eslint/visitor-keys": "6.7.3" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -6234,24 +5712,25 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.53.0.tgz", - "integrity": "sha512-HO2hh0fmtqNLzTAme/KnND5uFNwbsdYhCZghK2SoxGp3Ifn2emv+hi0PBUjzzSh0dstUIFqOj3bp0AwQlK4OWw==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.3.tgz", + "integrity": "sha512-Fc68K0aTDrKIBvLnKTZ5Pf3MXK495YErrbHb1R6aTpfK5OdSFj0rVN7ib6Tx6ePrZ2gsjLqr0s98NG7l96KSQw==", + "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.53.0", - "@typescript-eslint/utils": "5.53.0", + "@typescript-eslint/typescript-estree": "6.7.3", + "@typescript-eslint/utils": "6.7.3", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -6260,11 +5739,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.53.0.tgz", - "integrity": "sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.3.tgz", + "integrity": "sha512-4g+de6roB2NFcfkZb439tigpAMnvEIg3rIjWQ+EM7IBaYt/CdJt6em9BJ4h4UpdgaBWdmx2iWsafHTrqmgIPNw==", + "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -6272,20 +5752,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.53.0.tgz", - "integrity": "sha512-eKmipH7QyScpHSkhbptBBYh9v8FxtngLquq292YTEQ1pxVs39yFBlLC1xeIZcPPz1RWGqb7YgERJRGkjw8ZV7w==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.3.tgz", + "integrity": "sha512-YLQ3tJoS4VxLFYHTw21oe1/vIZPRqAO91z6Uv0Ss2BKm/Ag7/RVQBcXTGcXhgJMdA4U+HrKuY5gWlJlvoaKZ5g==", + "dev": true, "dependencies": { - "@typescript-eslint/types": "5.53.0", - "@typescript-eslint/visitor-keys": "5.53.0", + "@typescript-eslint/types": "6.7.3", + "@typescript-eslint/visitor-keys": "6.7.3", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -6298,40 +5779,41 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.53.0.tgz", - "integrity": "sha512-VUOOtPv27UNWLxFwQK/8+7kvxVC+hPHNsJjzlJyotlaHjLSIgOCKj9I0DBUjwOOA64qjBwx5afAPjksqOxMO0g==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.3.tgz", + "integrity": "sha512-vzLkVder21GpWRrmSR9JxGZ5+ibIUSudXlW52qeKpzUEQhRSmyZiVDDj3crAth7+5tmN1ulvgKaCU2f/bPRCzg==", + "dev": true, "dependencies": { - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.53.0", - "@typescript-eslint/types": "5.53.0", - "@typescript-eslint/typescript-estree": "5.53.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.7.3", + "@typescript-eslint/types": "6.7.3", + "@typescript-eslint/typescript-estree": "6.7.3", + "semver": "^7.5.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.53.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.53.0.tgz", - "integrity": "sha512-JqNLnX3leaHFZEN0gCh81sIvgrp/2GOACZNgO4+Tkf64u51kTpAyWFOY8XHx8XuXr3N2C9zgPPHtcpMg6z1g0w==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.3.tgz", + "integrity": "sha512-HEVXkU9IB+nk9o63CeICMHxFWbHWr3E1mpilIQBe9+7L/lH97rleFLVtYsfnWB+JVMaiFnEaxvknvmIzX+CqVg==", + "dev": true, "dependencies": { - "@typescript-eslint/types": "5.53.0", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "6.7.3", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -6785,14 +6267,14 @@ "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==" }, "node_modules/array-includes": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", - "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", "is-string": "^1.0.7" }, "engines": { @@ -6828,14 +6310,32 @@ "node": ">=8" } }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.flat": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", - "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -6846,13 +6346,13 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", - "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -6863,13 +6363,13 @@ } }, "node_modules/array.prototype.reduce": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz", - "integrity": "sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.6.tgz", + "integrity": "sha512-UW+Mz8LG/sPSU8jRDCjVr6J/ZKAGpHfwrZ6kWTG5qCxIEiXdVshqGnu5vEZA8S1y6X4aCSbQZ0/EEsfvEvBiSg==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-array-method-boxes-properly": "^1.0.0", "is-string": "^1.0.7" }, @@ -6881,25 +6381,26 @@ } }, "node_modules/array.prototype.tosorted": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", - "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz", + "integrity": "sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.1.3" + "get-intrinsic": "^1.2.1" } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz", - "integrity": "sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.2", "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "get-intrinsic": "^1.2.1", "is-array-buffer": "^3.0.2", "is-shared-array-buffer": "^1.0.2" @@ -6933,14 +6434,15 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, "node_modules/assert": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.0.0.tgz", - "integrity": "sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", "dependencies": { - "es6-object-assign": "^1.1.0", - "is-nan": "^1.2.1", - "object-is": "^1.0.1", - "util": "^0.12.0" + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" } }, "node_modules/ast-types-flow": { @@ -6953,6 +6455,14 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, + "node_modules/asynciterator.prototype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", + "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", + "dependencies": { + "has-symbols": "^1.0.3" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -6983,9 +6493,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.14", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", - "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", + "version": "10.4.16", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", + "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", "funding": [ { "type": "opencollective", @@ -6994,12 +6504,16 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "browserslist": "^4.21.5", - "caniuse-lite": "^1.0.30001464", - "fraction.js": "^4.2.0", + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001538", + "fraction.js": "^4.3.6", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", "postcss-value-parser": "^4.2.0" @@ -7026,17 +6540,17 @@ } }, "node_modules/axe-core": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.2.tgz", - "integrity": "sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==", + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.8.2.tgz", + "integrity": "sha512-/dlp0fxyM3R8YW7MFzaHWXrf4zzbr0vaYb23VBFCl83R7nWNPg/yaQw2Dc8jzCMmDVLhSdzH8MjrsuIUuvX+6g==", "engines": { "node": ">=4" } }, "node_modules/axios": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.3.tgz", - "integrity": "sha512-eYq77dYIFS77AQlhzEL937yUBSepBfPIe8FcgEDN35vMNZKMrs81pgnyrQpwfy4NF4b4XWX1Zgx7yX+25w8QJA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", + "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -7180,12 +6694,12 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz", - "integrity": "sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA==", + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.4.tgz", + "integrity": "sha512-9l//BZZsPR+5XjyJMPtZSK4jv0BsTO1zDac2GC6ygx9WLGlcsnRd1Co0B2zT5fF5Ic6BZy+9m3HNZ3QcOeDKfg==", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.4.2", - "core-js-compat": "^3.31.0" + "core-js-compat": "^3.32.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -7302,19 +6816,29 @@ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==" }, "node_modules/bfj": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.0.2.tgz", - "integrity": "sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.1.0.tgz", + "integrity": "sha512-I6MMLkn+anzNdCUp9hMRyui1HaNEUCco50lxbvNS4+EyXg8lN3nJ48PjPWtbH8UVS9CuMoaKE9U2V3l29DaRQw==", "dependencies": { - "bluebird": "^3.5.5", - "check-types": "^11.1.1", + "bluebird": "^3.7.2", + "check-types": "^11.2.3", "hoopy": "^0.1.4", + "jsonpath": "^1.1.1", "tryer": "^1.0.1" }, "engines": { "node": ">= 8.0.0" } }, + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -7479,6 +7003,18 @@ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" }, + "node_modules/bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "dev": true, + "dependencies": { + "big-integer": "^1.6.44" + }, + "engines": { + "node": ">= 5.10.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -7569,9 +7105,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.10", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", - "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", "funding": [ { "type": "opencollective", @@ -7587,10 +7123,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001517", - "electron-to-chromium": "^1.4.477", + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.11" + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -7656,6 +7192,21 @@ "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==" }, + "node_modules/bundle-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "dev": true, + "dependencies": { + "run-applescript": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -7724,9 +7275,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001519", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz", - "integrity": "sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==", + "version": "1.0.30001542", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001542.tgz", + "integrity": "sha512-UrtAXVcj1mvPBFQ4sKd38daP8dEcXXr5sQe6QNNinaPd0iA/cxg9/l3VrSdL73jgw5sKyuQ6jNgiKO12W3SsVA==", "funding": [ { "type": "opencollective", @@ -7809,9 +7360,9 @@ } }, "node_modules/check-types": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.2.tgz", - "integrity": "sha512-HBiYvXvn9Z70Z88XKjz3AEKd4HJhBXsa3j7xFnITAzoS8+q6eIGi8qDB8FKPBAjtuxjI/zFpwuiCb8oDtKOYrA==" + "version": "11.2.3", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", + "integrity": "sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==" }, "node_modules/chevrotain": { "version": "6.5.0", @@ -7941,9 +7492,9 @@ } }, "node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", "engines": { "node": ">=6" } @@ -8319,9 +7870,9 @@ } }, "node_modules/core-js": { - "version": "3.32.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.32.0.tgz", - "integrity": "sha512-rd4rYZNlF3WuoYuRIDEmbR/ga9CeuWX9U05umAvgrrZoHY4Z++cp/xwPQMvUpBB4Ag6J8KfD80G0zwCyaSxDww==", + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.0.tgz", + "integrity": "sha512-HoZr92+ZjFEKar5HS6MC776gYslNOKHt75mEBKWKnPeFDpZ6nH5OeF3S6HFT1mUAUZKrzkez05VboaX8myjSuw==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -8329,11 +7880,11 @@ } }, "node_modules/core-js-compat": { - "version": "3.32.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.0.tgz", - "integrity": "sha512-7a9a3D1k4UCVKnLhrgALyFcP7YCsLOQIxPd0dKjf/6GuPcgyiGP70ewWdCGrSK7evyhymi0qO4EqCmSJofDeYw==", + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.0.tgz", + "integrity": "sha512-0w4LcLXsVEuNkIqwjjf9rjCoPhK8uqA4tMRh4Ge26vfLtUutshn+aRJU21I9LCJlh2QQHfisNToLjw1XEJLTWw==", "dependencies": { - "browserslist": "^4.21.9" + "browserslist": "^4.22.1" }, "funding": { "type": "opencollective", @@ -8341,9 +7892,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.32.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.32.0.tgz", - "integrity": "sha512-qsev1H+dTNYpDUEURRuOXMvpdtAnNEvQWS/FMJ2Vb5AY8ZP4rAPQldkE27joykZPJTe0+IVgHZYh1P5Xu1/i1g==", + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.33.0.tgz", + "integrity": "sha512-FKSIDtJnds/YFIEaZ4HszRX7hkxGpNKM7FC9aJ9WLJbSd3lD4vOltFuVIBLR8asSx9frkTSqL0dw90SKQxgKrg==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -8784,9 +8335,9 @@ "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==" }, "node_modules/cssdb": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.7.0.tgz", - "integrity": "sha512-1hN+I3r4VqSNQ+OmMXxYexnumbOONkSil0TWMebVXHtzYW4tRRPovUNHPHj2d4nrgOuYJ8Vs3XwvywsuwwXNNA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.7.2.tgz", + "integrity": "sha512-pQPYP7/kch4QlkTcLuUNiNL2v/E+O+VIdotT+ug62/+2B2/jkzs5fMM6RHCzGCZ9C82pODEMSIzRRUzJOrl78g==", "funding": [ { "type": "opencollective", @@ -9299,30 +8850,174 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==" - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "node_modules/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==" + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "dev": true, + "dependencies": { + "bundle-name": "^3.0.0", + "default-browser-id": "^3.0.0", + "execa": "^7.1.1", + "titleize": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", + "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "dev": true, + "dependencies": { + "bplist-parser": "^0.2.0", + "untildify": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/default-browser/node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/default-browser/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/default-gateway": { @@ -9336,6 +9031,19 @@ "node": ">= 10" } }, + "node_modules/define-data-property": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", + "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -9345,10 +9053,11 @@ } }, "node_modules/define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -9515,9 +9224,9 @@ "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==" }, "node_modules/dns-packet": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.0.tgz", - "integrity": "sha512-rza3UH1LwdHh9qyPXp8lkwpjSNk/AMD3dPytUoRoqnypDUhY0xvbdmVhWOfxO68frEfV9BU8V12Ez7ZsHGZpCQ==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" }, @@ -9787,9 +9496,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.490", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.490.tgz", - "integrity": "sha512-6s7NVJz+sATdYnIwhdshx/N/9O6rvMxmhVoDSDFdj6iA45gHR8EQje70+RYsF4GeB+k0IeNSBnP7yG9ZXJFr7A==" + "version": "1.4.537", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.537.tgz", + "integrity": "sha512-W1+g9qs9hviII0HAwOdehGYkr+zt7KKdmCcJcjH0mYg6oL8+ioT3Skjmt7BLoAQqXhjf40AXd+HlR4oAWMlXjA==" }, "node_modules/element-size": { "version": "1.1.1", @@ -9900,17 +9609,17 @@ } }, "node_modules/es-abstract": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", - "integrity": "sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz", + "integrity": "sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==", "dependencies": { "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.2", "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.5", + "function.prototype.name": "^1.1.6", "get-intrinsic": "^1.2.1", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", @@ -9926,23 +9635,23 @@ "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", + "is-typed-array": "^1.1.12", "is-weakref": "^1.0.2", "object-inspect": "^1.12.3", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.0", - "safe-array-concat": "^1.0.0", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.7", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", "typed-array-buffer": "^1.0.0", "typed-array-byte-length": "^1.0.0", "typed-array-byte-offset": "^1.0.0", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.10" + "which-typed-array": "^1.1.11" }, "engines": { "node": ">= 0.4" @@ -9956,10 +9665,31 @@ "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" }, + "node_modules/es-iterator-helpers": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", + "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", + "dependencies": { + "asynciterator.prototype": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.1", + "es-set-tostringtag": "^2.0.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.0.1" + } + }, "node_modules/es-module-lexer": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", - "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==" + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", + "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==" }, "node_modules/es-set-tostringtag": { "version": "2.0.1", @@ -10022,11 +9752,6 @@ "es6-symbol": "^3.1.1" } }, - "node_modules/es6-object-assign": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", - "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==" - }, "node_modules/es6-symbol": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", @@ -10101,48 +9826,46 @@ } }, "node_modules/eslint": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.34.0.tgz", - "integrity": "sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", + "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", "dependencies": { - "@eslint/eslintrc": "^1.4.1", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.50.0", + "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "regexpp": "^3.2.0", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -10152,95 +9875,295 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-airbnb": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", + "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", + "dev": true, + "dependencies": { + "eslint-config-airbnb-base": "^15.0.0", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5" + }, + "engines": { + "node": "^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-hooks": "^4.3.0" + } + }, + "node_modules/eslint-config-airbnb-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", + "dev": true, + "dependencies": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.2" + } + }, + "node_modules/eslint-config-airbnb-base/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", + "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-config-react-app": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", + "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==", + "dependencies": { + "@babel/core": "^7.16.0", + "@babel/eslint-parser": "^7.16.3", + "@rushstack/eslint-patch": "^1.1.0", + "@typescript-eslint/eslint-plugin": "^5.5.0", + "@typescript-eslint/parser": "^5.5.0", + "babel-preset-react-app": "^10.0.1", + "confusing-browser-globals": "^1.0.11", + "eslint-plugin-flowtype": "^8.0.3", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jest": "^25.3.0", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.27.1", + "eslint-plugin-react-hooks": "^4.3.0", + "eslint-plugin-testing-library": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "eslint": "^8.0.0" + } + }, + "node_modules/eslint-config-react-app/node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-react-app/node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-react-app/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-react-app/node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-react-app/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-react-app/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/eslint-config-airbnb": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", - "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", - "dev": true, + "node_modules/eslint-config-react-app/node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", "dependencies": { - "eslint-config-airbnb-base": "^15.0.0", - "object.assign": "^4.1.2", - "object.entries": "^1.1.5" + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" }, "engines": { - "node": "^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.32.0 || ^8.2.0", - "eslint-plugin-import": "^2.25.3", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-react": "^7.28.0", - "eslint-plugin-react-hooks": "^4.3.0" + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/eslint-config-airbnb-base": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", - "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", - "dev": true, + "node_modules/eslint-config-react-app/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", "dependencies": { - "confusing-browser-globals": "^1.0.10", - "object.assign": "^4.1.2", - "object.entries": "^1.1.5", - "semver": "^6.3.0" + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "peerDependencies": { - "eslint": "^7.32.0 || ^8.2.0", - "eslint-plugin-import": "^2.25.2" - } - }, - "node_modules/eslint-config-airbnb-base/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/eslint-config-prettier": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz", - "integrity": "sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==", - "dev": true, - "bin": { - "eslint-config-prettier": "bin/cli.js" + "node_modules/eslint-config-react-app/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" }, - "peerDependencies": { - "eslint": ">=7.0.0" + "engines": { + "node": ">=8.0.0" } }, - "node_modules/eslint-config-react-app": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", - "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==", - "dependencies": { - "@babel/core": "^7.16.0", - "@babel/eslint-parser": "^7.16.3", - "@rushstack/eslint-patch": "^1.1.0", - "@typescript-eslint/eslint-plugin": "^5.5.0", - "@typescript-eslint/parser": "^5.5.0", - "babel-preset-react-app": "^10.0.1", - "confusing-browser-globals": "^1.0.11", - "eslint-plugin-flowtype": "^8.0.3", - "eslint-plugin-import": "^2.25.3", - "eslint-plugin-jest": "^25.3.0", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-react": "^7.27.1", - "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-testing-library": "^5.0.1" - }, + "node_modules/eslint-config-react-app/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "eslint": "^8.0.0" + "node": ">=4.0" } }, "node_modules/eslint-import-resolver-node": { @@ -10303,25 +10226,27 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.27.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", - "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz", + "integrity": "sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==", "dependencies": { "array-includes": "^3.1.6", + "array.prototype.findlastindex": "^1.2.2", "array.prototype.flat": "^1.3.1", "array.prototype.flatmap": "^1.3.1", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.7", - "eslint-module-utils": "^2.7.4", + "eslint-module-utils": "^2.8.0", "has": "^1.0.3", - "is-core-module": "^2.11.0", + "is-core-module": "^2.13.0", "is-glob": "^4.0.3", "minimatch": "^3.1.2", + "object.fromentries": "^2.0.6", + "object.groupby": "^1.0.0", "object.values": "^1.1.6", - "resolve": "^1.22.1", - "semver": "^6.3.0", - "tsconfig-paths": "^3.14.1" + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" }, "engines": { "node": ">=4" @@ -10418,35 +10343,44 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", - "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz", + "integrity": "sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==", "dev": true, "dependencies": { - "prettier-linter-helpers": "^1.0.0" + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.5" }, "engines": { - "node": ">=12.0.0" + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/prettier" }, "peerDependencies": { - "eslint": ">=7.28.0", - "prettier": ">=2.0.0" + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "prettier": ">=3.0.0" }, "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, "eslint-config-prettier": { "optional": true } } }, "node_modules/eslint-plugin-react": { - "version": "7.32.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", - "integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==", + "version": "7.33.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", + "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", "dependencies": { "array-includes": "^3.1.6", "array.prototype.flatmap": "^1.3.1", "array.prototype.tosorted": "^1.1.1", "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.12", "estraverse": "^5.3.0", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", @@ -10456,7 +10390,7 @@ "object.values": "^1.1.6", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.4", - "semver": "^6.3.0", + "semver": "^6.3.1", "string.prototype.matchall": "^4.0.8" }, "engines": { @@ -10622,7 +10556,7 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/eslint-scope": { + "node_modules/eslint-plugin-testing-library/node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", @@ -10634,7 +10568,7 @@ "node": ">=8.0.0" } }, - "node_modules/eslint-scope/node_modules/estraverse": { + "node_modules/eslint-plugin-testing-library/node_modules/estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", @@ -10642,35 +10576,25 @@ "node": ">=4.0" } }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dependencies": { - "eslint-visitor-keys": "^2.0.0" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "engines": { - "node": ">=10" + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz", - "integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -10777,21 +10701,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -11192,9 +11101,9 @@ } }, "node_modules/fbemitter/node_modules/ua-parser-js": { - "version": "1.0.35", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz", - "integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==", + "version": "1.0.36", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.36.tgz", + "integrity": "sha512-znuyCIXzl8ciS3+y3fHJI/2OhQIXbXw9MWC/o3qwyR+RGppjZHrM27CGFSKCJXi2Kctiz537iOu2KnXs1lMQhw==", "funding": [ { "type": "opencollective", @@ -11203,6 +11112,10 @@ { "type": "paypal", "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" } ], "engines": { @@ -11391,21 +11304,22 @@ } }, "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", + "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", "dependencies": { - "flatted": "^3.1.0", + "flatted": "^3.2.7", + "keyv": "^4.5.3", "rimraf": "^3.0.2" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=12.0.0" } }, "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" }, "node_modules/flatten-vertex-data": { "version": "1.0.2", @@ -11442,9 +11356,9 @@ } }, "node_modules/flux/node_modules/ua-parser-js": { - "version": "1.0.35", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz", - "integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==", + "version": "1.0.36", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.36.tgz", + "integrity": "sha512-znuyCIXzl8ciS3+y3fHJI/2OhQIXbXw9MWC/o3qwyR+RGppjZHrM27CGFSKCJXi2Kctiz537iOu2KnXs1lMQhw==", "funding": [ { "type": "opencollective", @@ -11453,6 +11367,10 @@ { "type": "paypal", "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" } ], "engines": { @@ -11460,9 +11378,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", "funding": [ { "type": "individual", @@ -11607,11 +11525,6 @@ "node": ">= 6" } }, - "node_modules/form-data-encoder": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.9.0.tgz", - "integrity": "sha512-rahaRMkN8P8d/tgK/BLPX+WBVM27NbvdXBxqQujBtkDAIFspaRqN7Od7lfdGQA6KAD+f82fYCLBq1ipvcu8qLw==" - }, "node_modules/format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -11620,18 +11533,6 @@ "node": ">=0.4.x" } }, - "node_modules/formdata-node": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", - "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", - "dependencies": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.3" - }, - "engines": { - "node": ">= 12.20" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -11641,15 +11542,15 @@ } }, "node_modules/fraction.js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", - "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz", + "integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==", "engines": { "node": "*" }, "funding": { "type": "patreon", - "url": "https://www.patreon.com/infusion" + "url": "https://github.com/sponsors/rawify" } }, "node_modules/fresh": { @@ -11721,9 +11622,9 @@ } }, "node_modules/fs-monkey": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.4.tgz", - "integrity": "sha512-INM/fWAxMICjttnD0DX1rBvinKskj5G1w+oy/pnm9u/tSlnBrzFonJMcalKJ30P8RRsPzKcCG7Q8l0jx5Fh9YQ==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", + "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==" }, "node_modules/fs.realpath": { "version": "1.0.0", @@ -11731,9 +11632,9 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, "optional": true, "os": [ @@ -11749,14 +11650,14 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" }, "engines": { "node": ">= 0.4" @@ -11977,9 +11878,9 @@ } }, "node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.22.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", + "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", "dependencies": { "type-fest": "^0.20.2" }, @@ -12204,6 +12105,14 @@ "resolve": "^1.0.0" } }, + "node_modules/goober": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.13.tgz", + "integrity": "sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -12220,10 +12129,10 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, - "node_modules/grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, "node_modules/grid-index": { "version": "1.1.0", @@ -12250,19 +12159,19 @@ "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==" }, "node_modules/handsontable": { - "version": "12.3.1", - "resolved": "https://registry.npmjs.org/handsontable/-/handsontable-12.3.1.tgz", - "integrity": "sha512-gm6dUoOwWo4ppwBnWFWn3VKSD2SkM00rwnI8JCU4TpsuDebkmqQqBcXZVPLqoOnB2USm/5t64/R4kXqJPaMB2g==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/handsontable/-/handsontable-13.1.0.tgz", + "integrity": "sha512-KqJtS3rJeOWsFWCffnDlM8fcLMTqmW+Vbc7OCVM4X7dYDpfefgFerjNLIxLfT9xohcQoUOS1jcvXwV8dwSppVQ==", "dependencies": { "@types/pikaday": "1.7.4", - "core-js": "^3.0.0", + "core-js": "^3.31.1", "dompurify": "^2.1.1", "moment": "2.29.4", "numbro": "2.1.2", "pikaday": "1.8.2" }, "optionalDependencies": { - "hyperformula": "^2.0.0" + "hyperformula": "^2.4.0" } }, "node_modules/harmony-reflect": { @@ -12752,9 +12661,9 @@ } }, "node_modules/hyperformula": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/hyperformula/-/hyperformula-2.5.0.tgz", - "integrity": "sha512-HkP7JZAmG7EQFF5XAhB3aGtTHvafblSRITTMYUsVoT9czIvYY7CvMQFfK1JNHJUVS844t8bnJpKEOqwcgBcHZg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/hyperformula/-/hyperformula-2.6.0.tgz", + "integrity": "sha512-jnEwxG8vFoxVBh7ahx2G6IpUvjH4yB3RNj6C9FX1C3XwQ4Km28dPp6obnRCXk/cZuh6tc/V4pSD9+jwv7r0Xgg==", "optional": true, "dependencies": { "chevrotain": "^6.5.0", @@ -12768,9 +12677,9 @@ "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" }, "node_modules/i18next": { - "version": "22.4.10", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.10.tgz", - "integrity": "sha512-3EqgGK6fAJRjnGgfkNSStl4mYLCjUoJID338yVyLMj5APT67HUtWoqSayZewiiC5elzMUB1VEUwcmSCoeQcNEA==", + "version": "23.5.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.5.1.tgz", + "integrity": "sha512-JelYzcaCoFDaa+Ysbfz2JsGAKkrHiMG6S61+HLBUEIPaF40WMwW9hCPymlQGrP+wWawKxKPuSuD71WZscCsWHg==", "funding": [ { "type": "individual", @@ -12786,13 +12695,13 @@ } ], "dependencies": { - "@babel/runtime": "^7.20.6" + "@babel/runtime": "^7.22.5" } }, "node_modules/i18next-browser-languagedetector": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.1.tgz", - "integrity": "sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.1.0.tgz", + "integrity": "sha512-cr2k7u1XJJ4HTOjM9GyOMtbOA47RtUoWRAtt52z43r3AoMs2StYKyjS3URPhzHaf+mn10hY9dZWamga5WPQjhA==", "dependencies": { "@babel/runtime": "^7.19.4" } @@ -12872,9 +12781,9 @@ } }, "node_modules/immer": { - "version": "9.0.19", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.19.tgz", - "integrity": "sha512-eY+Y0qcsB4TZKwgQzLaE/lqYMlKhv5J9dyd2RhhtGhNo2njPXDqU9XPfcNfa3MIDsdtZt5KlkIsirlo4dHsWdQ==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz", + "integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==", "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -13041,6 +12950,20 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", @@ -13094,22 +13017,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dependencies": { - "ci-info": "^2.0.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/is-ci/node_modules/ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" - }, "node_modules/is-core-module": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", @@ -13166,6 +13073,17 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-finite": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", @@ -13226,27 +13144,68 @@ "node": ">=0.10.0" } }, - "node_modules/is-hexadecimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-iexplorer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-iexplorer/-/is-iexplorer-1.0.0.tgz", + "integrity": "sha512-YeLzceuwg3K6O0MLM3UyUUjKAlyULetwryFp1mHy1I5PfArK0AEqlfa+MR4gkJjcbuJXoDJCvXbyqZVf5CR2Sg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-iexplorer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-iexplorer/-/is-iexplorer-1.0.0.tgz", - "integrity": "sha512-YeLzceuwg3K6O0MLM3UyUUjKAlyULetwryFp1mHy1I5PfArK0AEqlfa+MR4gkJjcbuJXoDJCvXbyqZVf5CR2Sg==", - "engines": { - "node": ">=0.10.0" + "node_modules/is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-mobile": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-3.1.1.tgz", - "integrity": "sha512-RRoXXR2HNFxNkUnxtaBdGBXtFlUMFa06S0NUKf/LCF+MuGLu13gi9iBCkoEmc6+rpXuwi5Mso5V8Zf7mNynMBQ==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-4.0.0.tgz", + "integrity": "sha512-mlcHZA84t1qLSuWkt2v0I2l61PYdyQDt4aG1mLIXF5FDMm4+haBCxCPYSr/uwqQNRk1MiTizn0ypEuRAOLRAew==" }, "node_modules/is-module": { "version": "1.0.0", @@ -13369,6 +13328,14 @@ "node": ">=6" } }, + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-shared-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", @@ -13448,6 +13415,14 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -13459,6 +13434,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -13571,6 +13558,18 @@ "node": ">=8" } }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, "node_modules/jake": { "version": "10.8.7", "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", @@ -14202,9 +14201,9 @@ } }, "node_modules/jest-watch-typeahead/node_modules/@types/yargs": { - "version": "17.0.24", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", - "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "version": "17.0.26", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.26.tgz", + "integrity": "sha512-Y3vDy2X6zw/ZCumcwLpdhM5L7jmyGpmBCTYMHDLqT2IKVMYRRLdv6ZakA+wxhra6Z/3bwhNbNl9bDGXaFU+6rw==", "dependencies": { "@types/yargs-parser": "*" } @@ -14441,19 +14440,19 @@ } }, "node_modules/jiti": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.19.1.tgz", - "integrity": "sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz", + "integrity": "sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==", "bin": { "jiti": "bin/jiti.js" } }, "node_modules/js-cookie": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz", - "integrity": "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", "engines": { - "node": ">=12" + "node": ">=14" } }, "node_modules/js-file-download": { @@ -14461,15 +14460,6 @@ "resolved": "https://registry.npmjs.org/js-file-download/-/js-file-download-0.4.12.tgz", "integrity": "sha512-rML+NkoD08p5Dllpjo0ffy4jRHeY6Zsapvr/W86N7E0yuzAO6qa5X9+xog6zQNlH102J7IXljNY2FtS6Lj3ucg==" }, - "node_modules/js-sdsl": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.2.tgz", - "integrity": "sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -14579,6 +14569,11 @@ "node": ">=4" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -14594,6 +14589,17 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "node_modules/json-stable-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.2.tgz", + "integrity": "sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g==", + "dependencies": { + "jsonify": "^0.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -14621,6 +14627,141 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/jsonpath": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", + "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", + "dependencies": { + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.12.1" + } + }, + "node_modules/jsonpath/node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/jsonpath/node_modules/escodegen/node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jsonpath/node_modules/esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/jsonpath/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/jsonpath/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/jsonpath/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/jsonpath/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/jsonpath/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsonpath/node_modules/static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "dependencies": { + "escodegen": "^1.8.1" + } + }, + "node_modules/jsonpath/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/jsonpointer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", @@ -14653,6 +14794,14 @@ "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==" }, + "node_modules/keyv": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -14953,13 +15102,13 @@ "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" }, "node_modules/material-react-table": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/material-react-table/-/material-react-table-1.14.0.tgz", - "integrity": "sha512-DMajcZwsduHfT/pfp9BiSX4qSXvj6I4fGlg6UiD7x3Ubj1EY004UK83AQIU+s8ZIC+uXObHX3OL/MiMw+qjNJg==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/material-react-table/-/material-react-table-1.15.0.tgz", + "integrity": "sha512-f59XPZ+jFErRAs3ym3cHsK6kBLCrYJGX6GoF473V1/gCpsNbkWEEdmCVMpB8ycOUNDEXtnRDMZzk3LjTMd6wpg==", "dependencies": { "@tanstack/match-sorter-utils": "8.8.4", - "@tanstack/react-table": "8.9.2", - "@tanstack/react-virtual": "3.0.0-beta.54", + "@tanstack/react-table": "8.10.3", + "@tanstack/react-virtual": "3.0.0-beta.60", "highlight-words": "1.2.2" }, "engines": { @@ -15332,9 +15481,9 @@ } }, "node_modules/nan": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", - "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", "optional": true }, "node_modules/nano-css": { @@ -15436,11 +15585,6 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" - }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -15451,9 +15595,9 @@ } }, "node_modules/node-abi": { - "version": "3.45.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz", - "integrity": "sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==", + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.47.0.tgz", + "integrity": "sha512-2s6B2CWZM//kPgwnuI0KrYwNjfdByE25zvAaEpq9IH4zcNsarH8Ihu/UuX6XMPEogDAxkuUFeZn60pXNHAqn3A==", "optional": true, "dependencies": { "semver": "^7.3.5" @@ -15462,6 +15606,11 @@ "node": ">=10" } }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -15481,9 +15630,9 @@ } }, "node_modules/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -15499,6 +15648,22 @@ } } }, + "node_modules/node-fetch-commonjs": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch-commonjs/-/node-fetch-commonjs-3.3.2.tgz", + "integrity": "sha512-VBlAiynj3VMLrotgwOS3OyECFxas5y7ltLcK4t41lMUZeaK15Ym4QRkqN0EQKAFL42q9i21EPKjzLUPfltR72A==", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -15550,31 +15715,32 @@ } }, "node_modules/notistack": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/notistack/-/notistack-2.0.8.tgz", - "integrity": "sha512-/IY14wkFp5qjPgKNvAdfL5Jp6q90+MjgKTPh4c81r/lW70KeuX6b9pE/4f8L4FG31cNudbN9siiFS5ql1aSLRw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/notistack/-/notistack-3.0.1.tgz", + "integrity": "sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA==", "dependencies": { "clsx": "^1.1.0", - "hoist-non-react-statics": "^3.3.0" + "goober": "^2.0.33" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/notistack" }, "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "@mui/material": "^5.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } + } + }, + "node_modules/notistack/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" } }, "node_modules/npm-run-path": { @@ -15691,26 +15857,26 @@ } }, "node_modules/object.entries": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", - "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", + "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" } }, "node_modules/object.fromentries": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", - "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -15720,14 +15886,14 @@ } }, "node_modules/object.getownpropertydescriptors": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.6.tgz", - "integrity": "sha512-lq+61g26E/BgHv0ZTFgRvi7NMEPuAxLkFU7rukXjc/AlwH4Am5xXVnIXy3un1bg/JPbXHrixRkK1itUzzPiIjQ==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.7.tgz", + "integrity": "sha512-PrJz0C2xJ58FNn11XV2lr4Jt5Gzl94qpy9Lu0JlfEj14z88sqbSBJCBEzdlNUCzY2gburhbrwOZ5BHCmuNUy0g==", "dependencies": { - "array.prototype.reduce": "^1.0.5", + "array.prototype.reduce": "^1.0.6", "call-bind": "^1.0.2", "define-properties": "^1.2.0", - "es-abstract": "^1.21.2", + "es-abstract": "^1.22.1", "safe-array-concat": "^1.0.0" }, "engines": { @@ -15737,26 +15903,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, "node_modules/object.hasown": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", - "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", + "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==", "dependencies": { - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object.values": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -16021,46 +16198,32 @@ } }, "node_modules/patch-package": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-6.5.1.tgz", - "integrity": "sha512-I/4Zsalfhc6bphmJTlrLoOcAF87jcxko4q0qsv4bGcurbr8IskEOtdnt9iCmsQVGL1B+iUhSQqweyTLJfCF9rA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", + "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==", "dependencies": { "@yarnpkg/lockfile": "^1.1.0", "chalk": "^4.1.2", - "cross-spawn": "^6.0.5", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", "find-yarn-workspace-root": "^2.0.0", "fs-extra": "^9.0.0", - "is-ci": "^2.0.0", + "json-stable-stringify": "^1.0.2", "klaw-sync": "^6.0.0", "minimist": "^1.2.6", "open": "^7.4.2", "rimraf": "^2.6.3", - "semver": "^5.6.0", + "semver": "^7.5.3", "slash": "^2.0.0", "tmp": "^0.0.33", - "yaml": "^1.10.2" + "yaml": "^2.2.2" }, "bin": { "patch-package": "index.js" }, "engines": { - "node": ">=10", - "npm": ">5" - } - }, - "node_modules/patch-package/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" + "node": ">=14", + "npm": ">5" } }, "node_modules/patch-package/node_modules/fs-extra": { @@ -16092,14 +16255,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/patch-package/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "engines": { - "node": ">=4" - } - }, "node_modules/patch-package/node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -16111,33 +16266,6 @@ "rimraf": "bin.js" } }, - "node_modules/patch-package/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/patch-package/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/patch-package/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/patch-package/node_modules/slash": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", @@ -16146,15 +16274,12 @@ "node": ">=6" } }, - "node_modules/patch-package/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" + "node_modules/patch-package/node_modules/yaml": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", + "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", + "engines": { + "node": ">= 14" } }, "node_modules/path-exists": { @@ -16400,9 +16525,9 @@ } }, "node_modules/plotly.js": { - "version": "2.18.2", - "resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-2.18.2.tgz", - "integrity": "sha512-Z8ZgWNfjeIEYxt/PCfpKZoWMxWylGYiuz28W2frUwEeTEcnnspi+JveC3IWYGmlq6dS3AWlJiZOJWJgdrJjCmA==", + "version": "2.26.1", + "resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-2.26.1.tgz", + "integrity": "sha512-aZgY5NEbuwgsnbTNmMy3BXPkx/QK+wuSnnEWvEeeUnhEZK+fTKazx6zCsbImLPinOvQtTdOXH4LhqrTYcYd69g==", "dependencies": { "@plotly/d3": "3.8.1", "@plotly/d3-sankey": "0.7.2", @@ -16430,7 +16555,7 @@ "glslify": "^7.1.1", "has-hover": "^1.0.1", "has-passive-events": "^1.0.0", - "is-mobile": "^3.1.1", + "is-mobile": "^4.0.0", "mapbox-gl": "1.10.1", "mouse-change": "^1.4.0", "mouse-event-offset": "^3.0.2", @@ -16443,7 +16568,7 @@ "regl": "npm:@plotly/regl@^2.1.2", "regl-error2d": "^2.0.12", "regl-line2d": "^3.1.2", - "regl-scatter2d": "^3.2.8", + "regl-scatter2d": "^3.2.9", "regl-splom": "^1.0.14", "strongly-connected-components": "^1.0.1", "superscript-text": "^1.0.0", @@ -16477,9 +16602,9 @@ "integrity": "sha512-mKjR5nolISvF+q2BtC1fi/llpxBPTQ3wLWN8+ldzdw2Hocpc8C72ZqnamCM4Z6z+68GVVjkeM01WJegQmZ8MEQ==" }, "node_modules/postcss": { - "version": "8.4.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", - "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "funding": [ { "type": "opencollective", @@ -16961,9 +17086,9 @@ } }, "node_modules/postcss-load-config/node_modules/yaml": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", - "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", + "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", "engines": { "node": ">= 14" } @@ -17670,15 +17795,15 @@ } }, "node_modules/prettier": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", - "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", "dev": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" @@ -17922,15 +18047,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "engines": { - "node": ">=0.4.x" - } - }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -17974,18 +18090,18 @@ "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" }, "node_modules/ramda": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.28.0.tgz", - "integrity": "sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==", + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", + "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", "funding": { "type": "opencollective", "url": "https://opencollective.com/ramda" } }, "node_modules/ramda-adjunct": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-3.4.0.tgz", - "integrity": "sha512-qKRgqwZzJUZmPJfGK8/uLVxQXkiftKhW6FW9NUCUlQrzsBUZBvFAZUxwH7nTRwDMg+ChRU69rVVuS/4EUgtuIg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-4.1.1.tgz", + "integrity": "sha512-BnCGsZybQZMDGram9y7RiryoRHS5uwx8YeGuUeDKuZuvK38XO6JJfmK85BwRWAKFA6pZ5nZBO/HBFtExVaf31w==", "engines": { "node": ">=0.10.3" }, @@ -17994,7 +18110,7 @@ "url": "https://opencollective.com/ramda-adjunct" }, "peerDependencies": { - "ramda": ">= 0.28.0 <= 0.28.0" + "ramda": ">= 0.29.0" } }, "node_modules/randexp": { @@ -18289,6 +18405,15 @@ "node": ">=14" } }, + "node_modules/react-dev-utils/node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/react-dev-utils/node_modules/loader-utils": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", @@ -18331,9 +18456,9 @@ "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, "node_modules/react-hook-form": { - "version": "7.43.9", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.43.9.tgz", - "integrity": "sha512-AUDN3Pz2NSeoxQ7Hs6OhQhDr6gtF9YRuutGDwPQqhSUAHJSgGl2VeY3qN19MG0SucpjgDiuMJ4iC5T5uB+eaNQ==", + "version": "7.47.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.47.0.tgz", + "integrity": "sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg==", "engines": { "node": ">=12.22.0" }, @@ -18346,15 +18471,15 @@ } }, "node_modules/react-i18next": { - "version": "12.1.5", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.1.5.tgz", - "integrity": "sha512-7PQAv6DA0TcStG96fle+8RfTwxVbHVlZZJPoEszwUNvDuWpGldJmNWa3ZPesEsZQZGF6GkzwvEh6p57qpFD2gQ==", + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-13.2.2.tgz", + "integrity": "sha512-+nFUkbRByFwnrfDcYqvzBuaeZb+nACHx+fAWN/pZMddWOCJH5hoc21+Sa/N/Lqi6ne6/9wC/qRGOoQhJa6IkEQ==", "dependencies": { - "@babel/runtime": "^7.20.6", + "@babel/runtime": "^7.22.5", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { - "i18next": ">= 19.0.0", + "i18next": ">= 23.2.3", "react": ">= 16.8.0" }, "peerDependenciesMeta": { @@ -18433,9 +18558,9 @@ } }, "node_modules/react-redux": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz", - "integrity": "sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", + "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", "dependencies": { "@babel/runtime": "^7.12.1", "@types/hoist-non-react-statics": "^3.3.1", @@ -18450,7 +18575,7 @@ "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0", "react-native": ">=0.59", - "redux": "^4" + "redux": "^4 || ^5.0.0-beta.0" }, "peerDependenciesMeta": { "@types/react": { @@ -18602,9 +18727,9 @@ } }, "node_modules/react-textarea-autosize": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.2.tgz", - "integrity": "sha512-uOkyjkEl0ByEK21eCJMHDGBAAd/BoFQBawYK5XItjAmCTeSbjxghd8qnt7nzsLYzidjnoObu6M26xts0YGKsGg==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz", + "integrity": "sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==", "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", @@ -18677,21 +18802,18 @@ "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" }, "node_modules/react-virtualized-auto-sizer": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.7.tgz", - "integrity": "sha512-Mxi6lwOmjwIjC1X4gABXMJcKHsOo0xWl3E3ugOgufB8GJU+MqrtY35aBuvCYv/razQ1Vbp7h1gWJjGjoNN5pmA==", - "engines": { - "node": ">8.0.0" - }, + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.20.tgz", + "integrity": "sha512-OdIyHwj4S4wyhbKHOKM1wLSj/UDXm839Z3Cvfg2a9j+He6yDa6i5p0qQvEiCnyQlGO/HyfSnigQwuxvYalaAXA==", "peerDependencies": { "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc", "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc" } }, "node_modules/react-window": { - "version": "1.8.8", - "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.8.tgz", - "integrity": "sha512-D4IiBeRtGXziZ1n0XklnFGu7h9gU684zepqyKzgPNzrsrk7xOCxni+TCckjg2Nr/DiaEEGVVmnhYSlT2rB47dQ==", + "version": "1.8.9", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.9.tgz", + "integrity": "sha512-+Eqx/fj1Aa5WnhRfj9dJg4VYATGwIUP2ItwItiJ6zboKWA6EX3lYDAXfGF2hyNqplEprhbtjbipiADEcwQ823Q==", "dependencies": { "@babel/runtime": "^7.0.0", "memoize-one": ">=3.1.1 <6" @@ -18779,6 +18901,25 @@ "redux": "^4" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", + "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/refractor": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", @@ -18807,9 +18948,9 @@ "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" }, "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", "dependencies": { "regenerate": "^1.4.2" }, @@ -18842,13 +18983,13 @@ "optional": true }, "node_modules/regexp.prototype.flags": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", - "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", - "functions-have-names": "^1.2.3" + "set-function-name": "^2.0.0" }, "engines": { "node": ">= 0.4" @@ -18857,17 +18998,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, "node_modules/regexpu-core": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", @@ -19067,9 +19197,9 @@ "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" }, "node_modules/resolve": { - "version": "1.22.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", - "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "version": "1.22.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", + "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -19292,6 +19422,21 @@ "@babel/runtime": "^7.1.2" } }, + "node_modules/run-applescript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -19320,12 +19465,12 @@ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" }, "node_modules/safe-array-concat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", - "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", + "get-intrinsic": "^1.2.1", "has-symbols": "^1.0.3", "isarray": "^2.0.5" }, @@ -19416,9 +19561,9 @@ } }, "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" }, "node_modules/saxes": { "version": "5.0.1", @@ -19660,6 +19805,19 @@ "node": ">= 0.8.0" } }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/set-harmonic-interval": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz", @@ -19723,9 +19881,9 @@ } }, "node_modules/short-unique-id": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/short-unique-id/-/short-unique-id-4.4.4.tgz", - "integrity": "sha512-oLF1NCmtbiTWl2SqdXZQbo5KM1b7axdp0RgQLq8qCBBLoq+o3A5wmLrNM6bZIh54/a8BJ3l69kTXuxwZ+XCYuw==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/short-unique-id/-/short-unique-id-5.0.3.tgz", + "integrity": "sha512-yhniEILouC0s4lpH0h7rJsfylZdca10W9mDJRAFh3EpcSUanCHGb0R7kcFOIUCZYSAPo0PUD5ZxWQdW0T4xaug==", "bin": { "short-unique-id": "bin/short-unique-id", "suid": "bin/short-unique-id" @@ -20230,17 +20388,18 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/string.prototype.matchall": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", - "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", + "integrity": "sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.4.3", + "internal-slot": "^1.0.5", + "regexp.prototype.flags": "^1.5.0", + "set-function-name": "^2.0.0", "side-channel": "^1.0.4" }, "funding": { @@ -20248,13 +20407,13 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", - "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -20264,26 +20423,26 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -20694,6 +20853,11 @@ "boolbase": "~1.0.0" } }, + "node_modules/svgo/node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, "node_modules/svgo/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -20714,47 +20878,45 @@ } }, "node_modules/swagger-client": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/swagger-client/-/swagger-client-3.20.0.tgz", - "integrity": "sha512-5RLge2NIE1UppIT/AjUPEceT05hcBAzjiQkrXJYjpxsbFV/UDH3pp+fsrWbAeuZtgRdhNB9KDo+szLoUpzkydQ==", - "dependencies": { - "@babel/runtime-corejs3": "^7.20.13", - "@swagger-api/apidom-core": ">=0.74.1 <1.0.0", - "@swagger-api/apidom-json-pointer": ">=0.74.1 <1.0.0", - "@swagger-api/apidom-ns-openapi-3-1": ">=0.74.1 <1.0.0", - "@swagger-api/apidom-reference": ">=0.74.1 <1.0.0", + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/swagger-client/-/swagger-client-3.22.3.tgz", + "integrity": "sha512-9I3BGD/6LItBzvJoKaRZ+QQ7IcEKq+iVlvvvcfZz65WgnXkORM1uj5+M+Oa5d8Tu5qABuOXd1UnlClBPuTITBA==", + "dependencies": { + "@babel/runtime-corejs3": "^7.22.15", + "@swagger-api/apidom-core": ">=0.76.2 <1.0.0", + "@swagger-api/apidom-json-pointer": ">=0.76.2 <1.0.0", + "@swagger-api/apidom-ns-openapi-3-1": ">=0.76.2 <1.0.0", + "@swagger-api/apidom-reference": ">=0.76.2 <1.0.0", "cookie": "~0.5.0", - "cross-fetch": "^3.1.5", "deepmerge": "~4.3.0", "fast-json-patch": "^3.0.0-1", - "form-data-encoder": "^1.4.3", - "formdata-node": "^4.0.0", "is-plain-object": "^5.0.0", "js-yaml": "^4.1.0", - "lodash": "^4.17.21", + "node-abort-controller": "^3.1.1", + "node-fetch-commonjs": "^3.3.1", "qs": "^6.10.2", "traverse": "~0.6.6", - "url": "~0.11.0" + "undici": "^5.24.0" } }, "node_modules/swagger-ui-react": { - "version": "4.15.5", - "resolved": "https://registry.npmjs.org/swagger-ui-react/-/swagger-ui-react-4.15.5.tgz", - "integrity": "sha512-jt2g6cDt3wOsc+1YQv4D86V4K659Xs1/pbhjYWlgNfjZB0TSN601MASWxbP+65U0iPpsJTpF7EmRzAunTOVs8Q==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/swagger-ui-react/-/swagger-ui-react-5.9.0.tgz", + "integrity": "sha512-j45ceuGHMRmI8nhOaG71VeQwrPutFHDq6QhgrxOmf4BRMOdOQgVY1POQY9ksnXZtskbD9J2NHURs4BLEDIs8gA==", "dependencies": { - "@babel/runtime-corejs3": "^7.18.9", - "@braintree/sanitize-url": "=6.0.0", + "@babel/runtime-corejs3": "^7.23.1", + "@braintree/sanitize-url": "=6.0.4", "base64-js": "^1.5.1", "classnames": "^2.3.1", "css.escape": "1.5.1", "deep-extend": "0.6.0", - "dompurify": "=2.3.10", + "dompurify": "=3.0.6", "ieee754": "^1.2.1", "immutable": "^3.x.x", "js-file-download": "^0.4.12", "js-yaml": "=4.1.0", "lodash": "^4.17.21", - "patch-package": "^6.5.0", + "patch-package": "^8.0.0", "prop-types": "^15.8.1", "randexp": "^0.5.3", "randombytes": "^2.1.0", @@ -20763,16 +20925,16 @@ "react-immutable-proptypes": "2.2.0", "react-immutable-pure-component": "^2.2.0", "react-inspector": "^6.0.1", - "react-redux": "^7.2.4", + "react-redux": "^8.1.2", "react-syntax-highlighter": "^15.5.0", "redux": "^4.1.2", "redux-immutable": "^4.0.0", "remarkable": "^2.0.1", - "reselect": "^4.1.5", + "reselect": "^4.1.8", "serialize-error": "^8.1.0", "sha.js": "^2.4.11", - "swagger-client": "^3.18.5", - "url-parse": "^1.5.8", + "swagger-client": "^3.22.3", + "url-parse": "^1.5.10", "xml": "=1.0.1", "xml-but-prettier": "^1.0.1", "zenscroll": "^4.0.2" @@ -20783,44 +20945,31 @@ } }, "node_modules/swagger-ui-react/node_modules/dompurify": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.10.tgz", - "integrity": "sha512-o7Fg/AgC7p/XpKjf/+RC3Ok6k4St5F7Q6q6+Nnm3p2zGWioAY6dh0CbbuwOhH2UcSzKsdniE/YnE2/92JcsA+g==" + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.6.tgz", + "integrity": "sha512-ilkD8YEnnGh1zJ240uJsW7AzE+2qpbOUYjacomn3AvJ6J4JhKGSZ2nh4wUIXPZrEPppaCLx5jFe8T89Rk8tQ7w==" }, - "node_modules/swagger-ui-react/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, - "node_modules/swagger-ui-react/node_modules/react-redux": { - "version": "7.2.9", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", - "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "node_modules/synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" }, - "peerDependencies": { - "react": "^16.8.3 || ^17 || ^18" + "engines": { + "node": "^14.18.0 || >=16.0.0" }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } + "funding": { + "url": "https://opencollective.com/unts" } }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" - }, "node_modules/tailwindcss": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", @@ -20980,9 +21129,9 @@ } }, "node_modules/terser": { - "version": "5.19.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.2.tgz", - "integrity": "sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.20.0.tgz", + "integrity": "sha512-e56ETryaQDyebBwJIWYB2TT6f2EZ0fL0sW/JRXNMN26zZdKi2u/E/5my5lG6jNxym6qsrVXfFRmOdV42zlAgLQ==", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -21146,6 +21295,18 @@ "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" }, + "node_modules/titleize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -21291,6 +21452,18 @@ "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-easing": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", @@ -21337,9 +21510,9 @@ } }, "node_modules/tslib": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", - "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -21511,22 +21684,22 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/ua-parser-js": { - "version": "0.7.35", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz", - "integrity": "sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==", + "version": "0.7.36", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.36.tgz", + "integrity": "sha512-CPPLoCts2p7D8VbybttE3P2ylv0OBZEAy7a12DsulIEcAiMtWJy+PBgMXgWDI80D5UwqE8oQPHYnk13tm38M2Q==", "funding": [ { "type": "opencollective", @@ -21535,6 +21708,10 @@ { "type": "paypal", "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" } ], "engines": { @@ -21555,6 +21732,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" + }, + "node_modules/undici": { + "version": "5.25.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.25.3.tgz", + "integrity": "sha512-7lmhlz3K1+IKB6IUjkdzV2l0jKY8/0KguEMdEpzzXCug5pEGIp3DxUg0DEN65DrVoxHiRKpPORC/qzX+UglSkQ==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -21637,6 +21830,15 @@ "resolved": "https://registry.npmjs.org/unraw/-/unraw-3.0.0.tgz", "integrity": "sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg==" }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/upath": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", @@ -21647,9 +21849,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "funding": [ { "type": "opencollective", @@ -21689,12 +21891,12 @@ } }, "node_modules/url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", + "integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==", "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" + "punycode": "^1.4.1", + "qs": "^6.11.2" } }, "node_modules/url-parse": { @@ -21707,9 +21909,9 @@ } }, "node_modules/url/node_modules/punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" }, "node_modules/use-composed-ref": { "version": "1.3.0", @@ -21818,9 +22020,13 @@ } }, "node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } @@ -21926,11 +22132,11 @@ "integrity": "sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw==" }, "node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", - "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", "engines": { - "node": ">= 14" + "node": ">= 8" } }, "node_modules/web-tree-sitter": { @@ -22180,9 +22386,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", "engines": { "node": ">=10.0.0" }, @@ -22242,6 +22448,26 @@ "node": ">=10.13.0" } }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -22272,9 +22498,9 @@ } }, "node_modules/whatwg-fetch": { - "version": "3.6.17", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.17.tgz", - "integrity": "sha512-c4ghIvG6th0eudYwKZY5keb81wtFz9/WeAHAoy8+r18kcWlitUIrmGFQ2rWEl4UCKUilD3zCLHOIPheHx5ypRQ==" + "version": "3.6.19", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.19.tgz", + "integrity": "sha512-d67JP4dHSbm2TrpFj8AbO8DnL1JXL5J9u0Kq2xW6d0TFDbCA3Muhdt8orXC22utleTVj7Prqt82baN6RBvnEgw==" }, "node_modules/whatwg-mimetype": { "version": "2.3.0", @@ -22324,6 +22550,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/which-typed-array": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", diff --git a/webapp/package.json b/webapp/package.json index a7d3e779aa..1bc82aae3e 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -1,81 +1,81 @@ { "name": "antares-web", - "version": "2.15.6", + "version": "2.16.0", "private": true, "engines": { "node": "18.16.1" }, "dependencies": { - "@emotion/react": "11.10.6", - "@emotion/styled": "11.10.6", - "@handsontable/react": "12.3.1", - "@mui/icons-material": "5.11.9", - "@mui/lab": "5.0.0-alpha.120", - "@mui/material": "5.11.10", - "@reduxjs/toolkit": "1.9.3", - "@types/d3": "5.16.4", - "@types/draft-convert": "2.1.4", - "@types/draft-js": "0.11.10", - "@types/draftjs-to-html": "0.8.1", - "@types/node": "16.11.20", - "@types/react": "18.0.28", - "@types/react-d3-graph": "2.6.3", - "@types/react-dom": "18.0.11", + "@emotion/react": "11.11.1", + "@emotion/styled": "11.11.0", + "@handsontable/react": "13.1.0", + "@mui/icons-material": "5.14.11", + "@mui/lab": "5.0.0-alpha.146", + "@mui/material": "5.14.11", + "@reduxjs/toolkit": "1.9.6", + "@types/d3": "5.16.0", + "@types/draft-convert": "2.1.5", + "@types/draft-js": "0.11.13", + "@types/draftjs-to-html": "0.8.2", + "@types/node": "18.16.1", + "@types/react": "18.2.24", + "@types/react-d3-graph": "2.6.5", + "@types/react-dom": "18.2.8", "@types/xml-js": "1.0.0", - "assert": "2.0.0", - "axios": "1.3.3", + "assert": "2.1.0", + "axios": "1.5.1", "buffer": "6.0.3", - "clsx": "1.2.1", + "clsx": "2.0.0", "crypto-browserify": "3.12.0", "d3": "5.16.0", "debug": "4.3.4", "draft-convert": "2.1.13", "draft-js": "0.11.7", "draftjs-to-html": "0.9.1", - "handsontable": "12.3.1", + "handsontable": "13.1.0", "hoist-non-react-statics": "3.3.2", "https-browserify": "1.0.0", - "i18next": "22.4.10", - "i18next-browser-languagedetector": "7.0.1", + "i18next": "23.5.1", + "i18next-browser-languagedetector": "7.1.0", "i18next-xhr-backend": "3.2.2", - "immer": "9.0.19", - "js-cookie": "3.0.1", + "immer": "10.0.3", + "js-cookie": "3.0.5", "jwt-decode": "3.1.2", "lodash": "4.17.21", - "material-react-table": "1.14.0", + "material-react-table": "1.15.0", "moment": "2.29.4", - "notistack": "2.0.8", + "notistack": "3.0.1", "os": "0.1.2", "os-browserify": "0.3.0", - "plotly.js": "2.18.2", - "ramda": "0.28.0", - "ramda-adjunct": "3.4.0", + "plotly.js": "2.26.1", + "ramda": "0.29.0", + "ramda-adjunct": "4.1.1", "react": "18.2.0", "react-beautiful-dnd": "13.1.1", "react-color": "2.19.3", "react-d3-graph": "2.6.0", "react-dom": "18.2.0", "react-dropzone": "14.2.3", - "react-hook-form": "7.43.9", - "react-i18next": "12.1.5", + "react-hook-form": "7.47.0", + "react-i18next": "13.2.2", "react-json-view": "1.21.3", "react-plotly.js": "2.6.0", - "react-redux": "8.0.5", + "react-redux": "8.1.3", "react-router": "6.3.0", "react-router-dom": "6.3.0", "react-scripts": "5.0.1", "react-split": "2.0.14", "react-use": "17.4.0", - "react-virtualized-auto-sizer": "1.0.7", - "react-window": "1.8.8", + "react-virtualized-auto-sizer": "1.0.20", + "react-window": "1.8.9", "redux": "4.2.1", "redux-thunk": "2.4.2", "stream-http": "3.2.0", - "swagger-ui-react": "4.15.5", + "swagger-ui-react": "5.9.0", "ts-toolbelt": "9.6.0", - "url": "0.11.0", + "url": "0.11.3", "use-undo": "1.1.1", - "uuid": "9.0.0", + "uuid": "9.0.1", "xml-js": "1.6.11" }, "scripts": { @@ -101,36 +101,36 @@ "proxy": "http://localhost:8080", "homepage": "/", "devDependencies": { - "@total-typescript/ts-reset": "0.4.2", - "@types/debug": "4.1.7", - "@types/js-cookie": "3.0.3", - "@types/lodash": "4.14.191", - "@types/ramda": "0.28.23", - "@types/react-beautiful-dnd": "13.1.3", - "@types/react-color": "3.0.6", - "@types/react-plotly.js": "2.6.0", + "@total-typescript/ts-reset": "0.5.1", + "@types/debug": "4.1.9", + "@types/js-cookie": "3.0.4", + "@types/lodash": "4.14.199", + "@types/ramda": "0.29.5", + "@types/react-beautiful-dnd": "13.1.5", + "@types/react-color": "3.0.7", + "@types/react-plotly.js": "2.6.1", "@types/react-virtualized-auto-sizer": "1.0.1", - "@types/react-window": "1.8.5", - "@types/redux-logger": "3.0.9", - "@types/swagger-ui-react": "4.11.0", - "@types/uuid": "9.0.0", - "@typescript-eslint/eslint-plugin": "5.53.0", - "@typescript-eslint/parser": "5.53.0", + "@types/react-window": "1.8.6", + "@types/redux-logger": "3.0.10", + "@types/swagger-ui-react": "4.18.1", + "@types/uuid": "9.0.4", + "@typescript-eslint/eslint-plugin": "6.7.3", + "@typescript-eslint/parser": "6.7.3", "cross-env": "7.0.3", - "eslint": "8.34.0", + "eslint": "8.50.0", "eslint-config-airbnb": "19.0.4", - "eslint-config-prettier": "8.6.0", - "eslint-plugin-import": "2.27.5", + "eslint-config-prettier": "9.0.0", + "eslint-plugin-import": "2.28.1", "eslint-plugin-jsx-a11y": "6.7.1", - "eslint-plugin-prettier": "4.2.1", - "eslint-plugin-react": "7.32.2", + "eslint-plugin-prettier": "5.0.0", + "eslint-plugin-react": "7.33.2", "eslint-plugin-react-hooks": "4.6.0", "husky": "8.0.3", "jest-sonar-reporter": "2.0.0", - "prettier": "2.8.4", + "prettier": "3.0.3", "process": "0.11.10", "react-app-rewired": "2.2.1", "stream-browserify": "3.0.0", - "typescript": "4.9.5" + "typescript": "5.2.2" } } diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index 72087cc152..5c72efdac2 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -16,6 +16,7 @@ "global.save": "Save", "global.cancel": "Cancel", "global.copy": "Copy", + "global.duplicate": "Duplicate", "global.copyId": "Copy ID", "global.data": "Data", "global.loading": "Loading", @@ -116,6 +117,7 @@ "form.field.minValue": "The minimum value is {{0}}", "form.field.maxValue": "The maximum value is {{0}}", "form.field.notAllowedValue": "Not allowed value", + "form.field.specialChars": "Special characters allowed: {{0}}", "matrix.graphSelector": "Columns", "matrix.message.importHint": "Click or drag and drop a matrix here", "matrix.importNewMatrix": "Import a new matrix", @@ -186,17 +188,15 @@ "settings.error.updateMaintenance": "Maintenance mode not updated", "launcher.additionalModes": "Additional modes", "launcher.autoUnzip": "Automatically unzip", - "launcher.xpress": "Use Xpress Solver (>= 8.3)", + "launcher.xpress": "Xpress (>= 8.3)", "launcher.xpansion.sensitivityMode": "Sensitivity mode", "launcher.xpansion.versionCpp": "Version C++", "launcher.xpansion.versionR": "Version R (<= 7.x)", "study.runStudy": "Study launch", "study.otherOptions": "Other options", - "study.xpansionMode": "Xpansion mode", "study.archiveOutputMode": "Archive mode", "study.postProcessing": "Post processing", - "study.timeLimit": "Time limit", - "study.timeLimitHelper": "Time limit in hours (max: {{max}}h)", + "study.timeLimit": "Time limit (h)", "study.nbCpu": "Number of cores", "study.clusterLoad": "Cluster load", "study.synthesis": "Synthesis", @@ -405,15 +405,32 @@ "study.modelization.map.districts.edit": "Edit districts", "study.modelization.map.districts.delete.confirm": "Are you sure you want to delete '{{0}}' district?", "study.modelization.load": "Load", - "study.modelization.thermal": "Thermal Clus.", + "study.modelization.thermal": "Thermal", "study.modelization.hydro": "Hydro", "study.modelization.hydro.correlation.viewMatrix": "View all correlations", "study.modelization.hydro.correlation.coefficient": "Coeff. (%)", "study.modelization.hydro.allocation.viewMatrix": "View all allocations", "study.modelization.hydro.allocation.error.field.delete": "Error when deleting the allocation", + "study.modelization.storages": "Storages", + "study.modelization.storages.capacities": "Injection / withdrawal capacities", + "study.modelization.storages.ruleCurves": "Rule Curves", + "study.modelization.storages.inflows": "Inflows", + "study.modelization.storages.chargeCapacity": "Withdrawal capacity", + "study.modelization.storages.dischargeCapacity": "Injection capacity", + "study.modelization.storages.lowerRuleCurve": "Lower rule curve", + "study.modelization.storages.upperRuleCurve": "Upper rule curve", + "study.modelization.storages.injectionNominalCapacity": "Withdrawal (MW)", + "study.modelization.storages.injectionNominalCapacity.info": "Withdrawal capacity from the network (MW)", + "study.modelization.storages.withdrawalNominalCapacity": "Injection (MW)", + "study.modelization.storages.withdrawalNominalCapacity.info": "Injection capacity from stock to the network (MW)", + "study.modelization.storages.reservoirCapacity": "Stock (MWh)", + "study.modelization.storages.reservoirCapacity.info": "Stock (MWh)", + "study.modelization.storages.efficiency": "Efficiency (%)", + "study.modelization.storages.initialLevel": "Initial level", + "study.modelization.storages.initialLevelOptim": "Initial level optimized", "study.modelization.wind": "Wind", "study.modelization.solar": "Solar", - "study.modelization.renewables": "Renewables Clus.", + "study.modelization.renewables": "Renewables", "study.modelization.reserves": "Reserves", "study.modelization.miscGen": "Misc. Gen.", "study.modelization.clusters.byGroups": "Clusters by groups", @@ -424,7 +441,7 @@ "study.modelization.clusters.thermal.pollutants": "Pollutant emission rates", "study.modelization.clusters.unitcount": "Unit", "study.modelization.clusters.enabled": "Enabled", - "study.modelization.clusters.nominalCapacity": "Nominal capacity", + "study.modelization.clusters.nominalCapacity": "Nominal capacity (MW)", "study.modelization.clusters.mustRun": "Must run", "study.modelization.clusters.minStablePower": "Min stable power (MW)", "study.modelization.clusters.minUpTime": "Min uptime (h)", @@ -444,11 +461,11 @@ "study.modelization.clusters.thermal.op4": "Other pollutant 4 (t/MWh)", "study.modelization.clusters.thermal.op5": "Other pollutant 5 (t/MWh)", "study.modelization.clusters.operatingCosts": "Operating costs", - "study.modelization.clusters.marginalCost": "Marginal cost", - "study.modelization.clusters.fixedCost": "Fixed cost", - "study.modelization.clusters.startupCost": "Startup cost", - "study.modelization.clusters.marketBidCost": "Market bid", - "study.modelization.clusters.spreadCost": "Spread cost", + "study.modelization.clusters.marginalCost": "Marginal cost (€/MWh)", + "study.modelization.clusters.fixedCost": "Fixed costs (€/h)", + "study.modelization.clusters.startupCost": "Startup cost (€)", + "study.modelization.clusters.marketBidCost": "Market bid cost (€/MWh)", + "study.modelization.clusters.spreadCost": "Spread cost (€/MWh)", "study.modelization.clusters.timeSeriesGen": "Time-Series generation", "study.modelization.clusters.genTs": "Generate Time-Series", "study.modelization.clusters.volatilityForced": "Volatility forced", @@ -507,6 +524,7 @@ "study.error.exportOutput": "Failed to export the output", "study.error.listOutputs": "Failed to retrieve output list", "study.error.launcherVersions": "Failed to retrieve launcher versions", + "study.error.launcherCores": "Failed to retrieve launcher number of cores", "study.error.fetchComments": "Failed to fetch comments", "study.error.commentsNotSaved": "Comments not saved", "study.error.studyIdCopy": "Failed to copy study ID", @@ -647,6 +665,7 @@ "xpansion.solver": "Solver", "xpansion.timeLimit": "Time limit (in hours)", "xpansion.logLevel": "Log level", + "xpansion.separationParameter": "Separation parameter", "xpansion.constraints": "Constraints", "xpansion.capacities": "Capacities", "xpansion.weights": "Weights", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index bcb2cefcf8..05c8e0a786 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -16,6 +16,7 @@ "global.save": "Sauvegarder", "global.cancel": "Annuler", "global.copy": "Copier", + "global.duplicate": "Dupliquer", "global.copyId": "Copier l'ID", "global.data": "Données", "global.loading": "Chargement", @@ -116,6 +117,7 @@ "form.field.minValue": "La valeur minimum est {{0}}", "form.field.maxValue": "La valeur maximum est {{0}}", "form.field.notAllowedValue": "Valeur non autorisée", + "form.field.specialChars": "Caractères spéciaux autorisés: {{0}}", "matrix.graphSelector": "Colonnes", "matrix.message.importHint": "Cliquer ou glisser une matrice ici", "matrix.importNewMatrix": "Import d'une nouvelle matrice", @@ -186,17 +188,16 @@ "settings.error.updateMaintenance": "Erreur lors du changement du status de maintenance", "launcher.additionalModes": "Mode additionnels", "launcher.autoUnzip": "Dézippage automatique", - "launcher.xpress": "Utiliser le solver Xpress (>= 8.3)", + "launcher.xpress": "Xpress (>= 8.3)", "launcher.xpansion.sensitivityMode": "Analyse de sensibilité", "launcher.xpansion.versionCpp": "Version C++", "launcher.xpansion.versionR": "Version R (<= 7.x)", "study.runStudy": "Lancement d'étude", "study.otherOptions": "Autres options", - "study.xpansionMode": "Mode Xpansion", "study.archiveOutputMode": "Mode archivé", "study.postProcessing": "Post-traitement", - "study.timeLimit": "Limite de temps", - "study.timeLimitHelper": "Limite de temps en heures (max: {{max}}h)", + "study.timeLimit": "Limite de temps (h)", + "study.timeLimitHelper": "(heures) max: {{max}}h", "study.nbCpu": "Nombre de coeurs", "study.clusterLoad": "Charge du cluster", "study.synthesis": "Synthèse", @@ -405,15 +406,32 @@ "study.modelization.map.districts.edit": "Modifier un district", "study.modelization.map.districts.delete.confirm": "Êtes-vous sûr de vouloir supprimer le district '{{0}}' ?", "study.modelization.load": "Conso", - "study.modelization.thermal": "Clus. Thermiques", + "study.modelization.thermal": "Thermiques", "study.modelization.hydro": "Hydro", "study.modelization.hydro.correlation.viewMatrix": "Voir les correlations", "study.modelization.hydro.correlation.coefficient": "Coeff. (%)", "study.modelization.hydro.allocation.viewMatrix": "Voir les allocations", "study.modelization.hydro.allocation.error.field.delete": "Erreur lors de la suppression de l'allocation", + "study.modelization.storages": "Stockages", + "study.modelization.storages.capacities": "Capacités d'injection / soutirage", + "study.modelization.storages.ruleCurves": "Courbe guides", + "study.modelization.storages.inflows": "Apports", + "study.modelization.storages.chargeCapacity": "Capacité de soutirage", + "study.modelization.storages.dischargeCapacity": "Capacité d'injection", + "study.modelization.storages.lowerRuleCurve": "Courbe guide inférieure", + "study.modelization.storages.upperRuleCurve": "Courbe guide supérieure", + "study.modelization.storages.injectionNominalCapacity": "Soutirage (MW)", + "study.modelization.storages.injectionNominalCapacity.info": "Capacité de soutirage du stock depuis le réseau (MW)", + "study.modelization.storages.withdrawalNominalCapacity": "Injection (MW)", + "study.modelization.storages.withdrawalNominalCapacity.info": "Capacité d'injection du stock vers le réseau (MW)", + "study.modelization.storages.reservoirCapacity": "Stock (MWh)", + "study.modelization.storages.reservoirCapacity.info": "Stock (MWh)", + "study.modelization.storages.efficiency": "Efficacité (%)", + "study.modelization.storages.initialLevel": "Niveau initial (%)", + "study.modelization.storages.initialLevelOptim": "Niveau initial optimisé", "study.modelization.wind": "Éolien", "study.modelization.solar": "Solaire", - "study.modelization.renewables": "Clus. Renouvelables", + "study.modelization.renewables": "Renouvelables", "study.modelization.reserves": "Réserves", "study.modelization.miscGen": "Divers Gen.", "study.modelization.clusters.byGroups": "Clusters par groupes", @@ -424,11 +442,11 @@ "study.modelization.clusters.thermal.pollutants": "Taux d’émissions de polluants", "study.modelization.clusters.unitcount": "Nombre d’unités", "study.modelization.clusters.enabled": "Activé", - "study.modelization.clusters.nominalCapacity": "Capacité nominale", + "study.modelization.clusters.nominalCapacity": "Capacité nominale (MW)", "study.modelization.clusters.mustRun": "Must run", "study.modelization.clusters.minStablePower": "Puissance stable min (MW)", - "study.modelization.clusters.minUpTime": "Temps de dispo. min (h)", - "study.modelization.clusters.minDownTime": "Temps d'arrêt min (h)", + "study.modelization.clusters.minUpTime": "Durée min de marche (h)", + "study.modelization.clusters.minDownTime": "Durée min d’arrêt (h)", "study.modelization.clusters.spinning": "Spinning (%)", "study.modelization.clusters.thermal.co2": "CO\u2082 (t/MWh)", "study.modelization.clusters.thermal.so2": "SO\u2082 (t/MWh)", @@ -444,11 +462,11 @@ "study.modelization.clusters.thermal.op4": "Autre polluant 4 (t/MWh)", "study.modelization.clusters.thermal.op5": "Autre polluant 5 (t/MWh)", "study.modelization.clusters.operatingCosts": "Coûts d'exploitation", - "study.modelization.clusters.marginalCost": "Coûts marginaux", - "study.modelization.clusters.fixedCost": "Coûts fixe", - "study.modelization.clusters.startupCost": "Coûts de démarrage", - "study.modelization.clusters.marketBidCost": "Offre de marché", - "study.modelization.clusters.spreadCost": "Coûts de répartition", + "study.modelization.clusters.marginalCost": "Coûts marginaux (€/MWh)", + "study.modelization.clusters.fixedCost": "Coûts fixes (€/h)", + "study.modelization.clusters.startupCost": "Coûts de démarrage (€)", + "study.modelization.clusters.marketBidCost": "Offre de marché (€/MWh)", + "study.modelization.clusters.spreadCost": "Spread (€/MWh)", "study.modelization.clusters.timeSeriesGen": "Génération des Séries temporelles", "study.modelization.clusters.genTs": "Générer des Séries temporelles", "study.modelization.clusters.volatilityForced": "Volatilité forcée", @@ -507,6 +525,7 @@ "study.error.exportOutput": "Échec lors de l'export de la sortie", "study.error.listOutputs": "Échec de la récupération des sorties", "study.error.launcherVersions": "Échec lors de la récupération des versions du launcher", + "study.error.launcherCores": "Échec lors de la récupération du nombre de cœurs du launcher", "study.error.fetchComments": "Échec lors de la récupération des commentaires", "study.error.commentsNotSaved": "Erreur lors de l'enregistrement des commentaires", "study.error.studyIdCopy": "Erreur lors de la copie de l'identifiant de l'étude", @@ -647,6 +666,7 @@ "xpansion.solver": "Solveur", "xpansion.timeLimit": "Limite de temps (en heures)", "xpansion.logLevel": "Niveau de log", + "xpansion.separationParameter": "Paramètre de séparation", "xpansion.constraints": "Contraintes", "xpansion.capacities": "Capacités", "xpansion.weights": "Poids", diff --git a/webapp/src/components/App/Data/DataPropsView.tsx b/webapp/src/components/App/Data/DataPropsView.tsx index 3621377611..6bc40ad646 100644 --- a/webapp/src/components/App/Data/DataPropsView.tsx +++ b/webapp/src/components/App/Data/DataPropsView.tsx @@ -20,8 +20,8 @@ function DataPropsView(props: PropTypes) { (item) => item.name.search(input) >= 0 || !!item.matrices.find( - (matrix: MatrixInfoDTO) => matrix.id.search(input) >= 0 - ) + (matrix: MatrixInfoDTO) => matrix.id.search(input) >= 0, + ), ); }; diff --git a/webapp/src/components/App/Data/DatasetCreationDialog.tsx b/webapp/src/components/App/Data/DatasetCreationDialog.tsx index a4760a6fe3..6b08f34006 100644 --- a/webapp/src/components/App/Data/DatasetCreationDialog.tsx +++ b/webapp/src/components/App/Data/DatasetCreationDialog.tsx @@ -47,7 +47,7 @@ function DatasetCreationDialog(props: PropTypes) { const { open, onNewDataUpdate, onClose, data } = props; const [groupList, setGroupList] = useState>([]); const [selectedGroupList, setSelectedGroupList] = useState>( - [] + [], ); const [name, setName] = useState(""); const [isJson, setIsJson] = useState(false); @@ -68,7 +68,7 @@ function DatasetCreationDialog(props: PropTypes) { currentFile, data, isJson, - setUploadProgress + setUploadProgress, ); enqueueSnackbar(t(msg), { variant: "success" }); } catch (e) { @@ -105,7 +105,7 @@ function DatasetCreationDialog(props: PropTypes) { setSelectedGroupList(selectedGroupList.concat([item])); } else { setSelectedGroupList( - selectedGroupList.filter((elm) => item.id !== elm.id) + selectedGroupList.filter((elm) => item.id !== elm.id), ); } }; @@ -277,7 +277,7 @@ function DatasetCreationDialog(props: PropTypes) { > {groupList.map((item) => { const index = selectedGroupList.findIndex( - (elm) => item.id === elm.id + (elm) => item.id === elm.id, ); if (index >= 0) { return ( diff --git a/webapp/src/components/App/Data/index.tsx b/webapp/src/components/App/Data/index.tsx index f9ec511f9e..921f61ffda 100644 --- a/webapp/src/components/App/Data/index.tsx +++ b/webapp/src/components/App/Data/index.tsx @@ -177,10 +177,9 @@ function Data() { lineHeight: 1.334, }} > - {`Matrices - ${ - dataList.find((item) => item.id === selectedItem) - ?.name - }`} + {`Matrices - ${dataList.find( + (item) => item.id === selectedItem, + )?.name}`} @@ -227,10 +226,9 @@ function Data() { alignItems: "center", }} > - {`Matrices - ${ - dataList.find((item) => item.id === selectedItem) - ?.name - }`} + {`Matrices - ${dataList.find( + (item) => item.id === selectedItem, + )?.name}`} ) } diff --git a/webapp/src/components/App/Data/utils.tsx b/webapp/src/components/App/Data/utils.tsx index 427373703a..8e98c70368 100644 --- a/webapp/src/components/App/Data/utils.tsx +++ b/webapp/src/components/App/Data/utils.tsx @@ -15,7 +15,7 @@ const updateMatrix = async ( name: string, publicStatus: boolean, selectedGroupList: Array, - onNewDataUpdate: (newData: MatrixDataSetDTO) => void + onNewDataUpdate: (newData: MatrixDataSetDTO) => void, // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise => { const matrixMetadata: MatrixDataSetUpdateDTO = { @@ -37,7 +37,7 @@ const createMatrix = async ( publicStatus: boolean, selectedGroupList: Array, matrices: Array, - onNewDataUpdate: (newData: MatrixDataSetDTO) => void + onNewDataUpdate: (newData: MatrixDataSetDTO) => void, // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise => { const matrixMetadata: MatrixDataSetUpdateDTO = { @@ -58,7 +58,7 @@ export const saveMatrix = async ( file?: File, data?: MatrixDataSetDTO, json?: boolean, - onProgress?: (progress: number) => void + onProgress?: (progress: number) => void, ): Promise => { if (!name.replace(/\s/g, "")) { throw Error("global.error.emptyName"); @@ -69,14 +69,14 @@ export const saveMatrix = async ( const matrixInfos = await createMatrixByImportation( file, !!json, - onProgress + onProgress, ); await createMatrix( name, publicStatus, selectedGroupList, matrixInfos, - onNewDataUpdate + onNewDataUpdate, ); } else { throw Error("data.error.fileNotUploaded"); @@ -87,7 +87,7 @@ export const saveMatrix = async ( name, publicStatus, selectedGroupList, - onNewDataUpdate + onNewDataUpdate, ); } diff --git a/webapp/src/components/App/Settings/Groups/dialog/CreateGroupDialog.tsx b/webapp/src/components/App/Settings/Groups/dialog/CreateGroupDialog.tsx index 05a6369e30..dd99768fce 100644 --- a/webapp/src/components/App/Settings/Groups/dialog/CreateGroupDialog.tsx +++ b/webapp/src/components/App/Settings/Groups/dialog/CreateGroupDialog.tsx @@ -46,11 +46,17 @@ function CreateGroupDialog(props: Props) { try { newGroup = await mounted(createGroup(name)); - enqueueSnackbar(t("settings.success.groupCreation", [newGroup.name]), { - variant: "success", - }); + enqueueSnackbar( + t("settings.success.groupCreation", { 0: newGroup.name }), + { + variant: "success", + }, + ); } catch (e) { - enqueueErrorSnackbar(t("settings.error.groupSave", [name]), e as Error); + enqueueErrorSnackbar( + t("settings.error.groupSave", { 0: name }), + e as Error, + ); throw e; } @@ -64,7 +70,7 @@ function CreateGroupDialog(props: Props) { group_id: newGroup.id, type: perm.type, identity_id: perm.user.id, - }) + }), ); const res: RoleDetailsDTO[] = await mounted(Promise.all(promises)); @@ -82,8 +88,8 @@ function CreateGroupDialog(props: Props) { reloadFetchGroups(); enqueueErrorSnackbar( - t("settings.error.userRolesSave", [newGroup.name]), - e as Error + t("settings.error.userRolesSave", { 0: newGroup.name }), + e as Error, ); } diff --git a/webapp/src/components/App/Settings/Groups/dialog/GroupFormDialog/GroupForm.tsx b/webapp/src/components/App/Settings/Groups/dialog/GroupFormDialog/GroupForm.tsx index 69c3e849e9..6a360e2d5e 100644 --- a/webapp/src/components/App/Settings/Groups/dialog/GroupFormDialog/GroupForm.tsx +++ b/webapp/src/components/App/Settings/Groups/dialog/GroupFormDialog/GroupForm.tsx @@ -56,7 +56,7 @@ function GroupForm(props: UseFormReturnPlus) { const allowToAddPermission = selectedUser && !getValues("permissions").some( - ({ user }: { user: UserDTO }) => user.id === selectedUser.id + ({ user }: { user: UserDTO }) => user.id === selectedUser.id, ); const filteredAndSortedUsers = useMemo(() => { @@ -66,8 +66,8 @@ function GroupForm(props: UseFormReturnPlus) { return sortByName( users.filter( (user) => - !RESERVED_USER_NAMES.includes(user.name) && user.id !== authUser?.id - ) + !RESERVED_USER_NAMES.includes(user.name) && user.id !== authUser?.id, + ), ); }, [users, authUser]); diff --git a/webapp/src/components/App/Settings/Groups/dialog/UpdateGroupDialog.tsx b/webapp/src/components/App/Settings/Groups/dialog/UpdateGroupDialog.tsx index 8a131cb445..2a85371138 100644 --- a/webapp/src/components/App/Settings/Groups/dialog/UpdateGroupDialog.tsx +++ b/webapp/src/components/App/Settings/Groups/dialog/UpdateGroupDialog.tsx @@ -54,7 +54,7 @@ function UpdateGroupDialog(props: Props) { type: user.role, })), }), - [group] + [group], ); //////////////////////////////////////////////////////////////// @@ -67,9 +67,9 @@ function UpdateGroupDialog(props: Props) { const groupName = name || group.name; const notifySuccess = R.once(() => - enqueueSnackbar(t("settings.success.groupUpdate", [groupName]), { + enqueueSnackbar(t("settings.success.groupUpdate", { 0: groupName }), { variant: "success", - }) + }), ); if (groupName !== group.name) { @@ -79,8 +79,8 @@ function UpdateGroupDialog(props: Props) { notifySuccess(); } catch (e) { enqueueErrorSnackbar( - t("settings.error.groupSave", [groupName]), - e as Error + t("settings.error.groupSave", { 0: groupName }), + e as Error, ); throw e; } @@ -117,7 +117,7 @@ function UpdateGroupDialog(props: Props) { identity_id: role.user.id, type: role.type, group_id: group.id, - }) + }), ), ]; @@ -128,7 +128,7 @@ function UpdateGroupDialog(props: Props) { id: identity.id, name: identity.name, role: type, - }) + }), ); editGroup({ id: group.id, name: groupName, users: newUserRoles }); @@ -139,8 +139,8 @@ function UpdateGroupDialog(props: Props) { reloadFetchUsers(); enqueueErrorSnackbar( - t("settings.error.groupRolesSave", [groupName]), - e as Error + t("settings.error.groupRolesSave", { 0: groupName }), + e as Error, ); } } diff --git a/webapp/src/components/App/Settings/Groups/index.tsx b/webapp/src/components/App/Settings/Groups/index.tsx index a8786b9f0b..bbabb41bb6 100644 --- a/webapp/src/components/App/Settings/Groups/index.tsx +++ b/webapp/src/components/App/Settings/Groups/index.tsx @@ -10,7 +10,7 @@ import { Skeleton, Typography, } from "@mui/material"; -import produce from "immer"; +import { produce } from "immer"; import { ReactNode, useMemo, useReducer, useState } from "react"; import { useTranslation } from "react-i18next"; import { usePromise as usePromiseWrapper, useUpdateEffect } from "react-use"; @@ -164,14 +164,14 @@ function Groups() { mounted(deleteGroup(group.id)) .then(() => { dispatch({ type: GroupActionKind.DELETE, payload: group.id }); - enqueueSnackbar(t("settings.success.groupDelete", [group.name]), { + enqueueSnackbar(t("settings.success.groupDelete", { 0: group.name }), { variant: "success", }); }) .catch((err) => { enqueueErrorSnackbar( - t("settings.error.groupDelete", [group.name]), - err + t("settings.error.groupDelete", { 0: group.name }), + err, ); }) .finally(() => { @@ -264,7 +264,7 @@ function Groups() { alert="warning" open > - {t("settings.question.deleteGroup", [groupToDelete.name])} + {t("settings.question.deleteGroup", { 0: groupToDelete.name })} )} {groupToEdit && ( diff --git a/webapp/src/components/App/Settings/Maintenance/index.tsx b/webapp/src/components/App/Settings/Maintenance/index.tsx index c1ecaec92a..817c7ca690 100644 --- a/webapp/src/components/App/Settings/Maintenance/index.tsx +++ b/webapp/src/components/App/Settings/Maintenance/index.tsx @@ -47,7 +47,7 @@ function Maintenance() { mode: await getMaintenanceMode(), message: await getMessageInfo(), }), - { errorMessage: t("maintenance.error.maintenanceError") } + { errorMessage: t("maintenance.error.maintenanceError") }, ); useUpdateEffect(() => { diff --git a/webapp/src/components/App/Settings/Tokens/dialog/CreateTokenDialog.tsx b/webapp/src/components/App/Settings/Tokens/dialog/CreateTokenDialog.tsx index 23f62bec61..44e0ebbf33 100644 --- a/webapp/src/components/App/Settings/Tokens/dialog/CreateTokenDialog.tsx +++ b/webapp/src/components/App/Settings/Tokens/dialog/CreateTokenDialog.tsx @@ -46,20 +46,23 @@ function CreateTokenDialog(props: Props) { (perm: { group: GroupDTO; type: RoleType }) => ({ group: perm.group.id, role: perm.type, - }) + }), ) as BotCreateDTO["roles"]; const tokenValue = await mounted( - createBot({ name, is_author: false, roles }) + createBot({ name, is_author: false, roles }), ); setTokenValueToDisplay(tokenValue); - enqueueSnackbar(t("settings.success.tokenCreation", [name]), { + enqueueSnackbar(t("settings.success.tokenCreation", { 0: name }), { variant: "success", }); } catch (e) { - enqueueErrorSnackbar(t("settings.error.tokenSave", [name]), e as Error); + enqueueErrorSnackbar( + t("settings.error.tokenSave", { 0: name }), + e as Error, + ); closeDialog(); } finally { reloadFetchTokens(); diff --git a/webapp/src/components/App/Settings/Tokens/dialog/TokenFormDialog/TokenForm.tsx b/webapp/src/components/App/Settings/Tokens/dialog/TokenFormDialog/TokenForm.tsx index f426ebf87b..128d963bde 100644 --- a/webapp/src/components/App/Settings/Tokens/dialog/TokenFormDialog/TokenForm.tsx +++ b/webapp/src/components/App/Settings/Tokens/dialog/TokenFormDialog/TokenForm.tsx @@ -63,7 +63,7 @@ function TokenForm(props: Props) { const allowToAddPermission = selectedGroup && !getValues("permissions").some( - ({ group }: { group: GroupDTO }) => group.id === selectedGroup.id + ({ group }: { group: GroupDTO }) => group.id === selectedGroup.id, ); const filteredAndSortedGroups = useMemo(() => { @@ -71,7 +71,7 @@ function TokenForm(props: Props) { return []; } return sortByName( - groups.filter((group) => !RESERVED_GROUP_NAMES.includes(group.name)) + groups.filter((group) => !RESERVED_GROUP_NAMES.includes(group.name)), ); }, [groups]); @@ -187,7 +187,7 @@ function TokenForm(props: Props) { render={({ field }) => (