diff --git a/.gitignore b/.gitignore index cf40b51c..47964111 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,31 @@ -.vscode +# Some categories and items copied from https://github.com/github/gitignore/blob/main/Python.gitignore + +# Byte-compiled / optimized / DLL files __pycache__ -**.ipynb -.idea -venv -.env +# Distribution / packaging +.Python +build/ +dist/ +sdist/ +*.egg-info/ +site/ + +# Unit test / coverage reports .coverage coverage.xml -AntaresWebDesktop +.tox/ + +# Environments +.env +.venv +env/ +venv/ + +# Jupyter notebooks +**.ipynb + +# Other +.vscode +.idea +AntaresWebDesktop/ diff --git a/README.md b/README.md index bf4656b6..d3b49718 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,46 @@ -Antares Craft python library is currently under construction. When completed it will allow to create, update and read antares studies. +# antares_craft +[![github ci](https://github.com/AntaresSimulatorTeam/antares_craft/actions/workflows/ci.yml/badge.svg)](https://github.com/AntaresSimulatorTeam/antares_craft/actions/workflows/ci.yml) + +## about + +Antares Craft python library is currently under construction. When completed it will allow to create, update and read +antares studies. This project only supports antares studies with a version v8.8 or higher. -To reformat your code, use this command line : `ruff check src/ tests/ --fix && ruff format src/ tests/` +## developers +### install dev requirements + +Install dev requirements with `pip install -r requirements-dev.txt` + +### linting and formatting + +To reformat your code, use this command line: `ruff check src/ tests/ --fix && ruff format src/ tests/` -To launch integration tests you'll need an AntaresWebDesktop instance on your local env (at least the v.2.17.3, **currently running in 2.17.5**). -To install it, download it from the last [Antares Web release](https://github.com/AntaresSimulatorTeam/AntaREST/releases) (inside the assets list). +### typechecking + +To typecheck your code, use this command line: `mypy` + +### integration testing + +To launch integration tests you'll need an AntaresWebDesktop instance on your local env (at least the v.2.17.3, +**currently running in 2.17.5**). +To install it, download it from the last [Antares Web release](https://github.com/AntaresSimulatorTeam/AntaREST/releases) +(inside the assets list). Then, unzip it at the root of this repository and rename the folder `AntaresWebDesktop`. *NB*: The expected folder structure is the following: `antares_craft/AntaresWebDesktop/config.yaml` + +### tox +To use [tox](https://tox.wiki/) to run unit tests in multiple python versions at the same time as linting and formatting +with ruff and typing with mypy: +1) As the dev requirements include [uv](https://docs.astral.sh/uv/) and `tox-uv` there is no need to install python +versions, `uv` will do this for you. +2) Use `tox -p` to run the environments in parallel to save time, this will create virtual environment with the +necessary python versions the first time you run tox. + +### mkdocs +Smallest beginning of `mkdocs` included more as proof of concept than anything, theme and logo copied from [Antares +Simulator](https://github.com/AntaresSimulatorTeam/Antares_Simulator). +1) To preview the docs on your local machine run `mkdocs serve`. +2) To build the static site for publishing for example on [Read the Docs](https://readthedocs.io) use `mkdocs build`. +3) To flesh out the documentation see [mkdoc guides](https://www.mkdocs.org/user-guide/). diff --git a/docs/assets/Icone.png b/docs/assets/Icone.png new file mode 100644 index 00000000..597698ea Binary files /dev/null and b/docs/assets/Icone.png differ diff --git a/docs/assets/antares.png b/docs/assets/antares.png new file mode 100644 index 00000000..0115f089 Binary files /dev/null and b/docs/assets/antares.png differ diff --git a/docs/assets/logo.png b/docs/assets/logo.png new file mode 100644 index 00000000..1bd09417 Binary files /dev/null and b/docs/assets/logo.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..d3b49718 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,46 @@ +# antares_craft +[![github ci](https://github.com/AntaresSimulatorTeam/antares_craft/actions/workflows/ci.yml/badge.svg)](https://github.com/AntaresSimulatorTeam/antares_craft/actions/workflows/ci.yml) + +## about + +Antares Craft python library is currently under construction. When completed it will allow to create, update and read +antares studies. + +This project only supports antares studies with a version v8.8 or higher. + +## developers +### install dev requirements + +Install dev requirements with `pip install -r requirements-dev.txt` + +### linting and formatting + +To reformat your code, use this command line: `ruff check src/ tests/ --fix && ruff format src/ tests/` + +### typechecking + +To typecheck your code, use this command line: `mypy` + +### integration testing + +To launch integration tests you'll need an AntaresWebDesktop instance on your local env (at least the v.2.17.3, +**currently running in 2.17.5**). +To install it, download it from the last [Antares Web release](https://github.com/AntaresSimulatorTeam/AntaREST/releases) +(inside the assets list). +Then, unzip it at the root of this repository and rename the folder `AntaresWebDesktop`. +*NB*: The expected folder structure is the following: `antares_craft/AntaresWebDesktop/config.yaml` + +### tox +To use [tox](https://tox.wiki/) to run unit tests in multiple python versions at the same time as linting and formatting +with ruff and typing with mypy: +1) As the dev requirements include [uv](https://docs.astral.sh/uv/) and `tox-uv` there is no need to install python +versions, `uv` will do this for you. +2) Use `tox -p` to run the environments in parallel to save time, this will create virtual environment with the +necessary python versions the first time you run tox. + +### mkdocs +Smallest beginning of `mkdocs` included more as proof of concept than anything, theme and logo copied from [Antares +Simulator](https://github.com/AntaresSimulatorTeam/Antares_Simulator). +1) To preview the docs on your local machine run `mkdocs serve`. +2) To build the static site for publishing for example on [Read the Docs](https://readthedocs.io) use `mkdocs build`. +3) To flesh out the documentation see [mkdoc guides](https://www.mkdocs.org/user-guide/). diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 00000000..9b302519 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,10 @@ +# Reference + +To start using Antares Craft create a study using either the API or local modes, you can then build on the created object to add study elements as you go: + +Exemple: + + study = create_study_local("test_study", 880, {"local_path": "test_study", "study_name": "test_study"}) + + +::: src.antares.model.study diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..5acc9b1f --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,34 @@ +site_name: Antares Craft +repo_url: https://github.com/AntaresSimulatorTeam/antares_craft +nav: + - Home: index.md + - Quickstart: quickstart.md +theme: + name: material + logo: assets/logo.png + favicon: assets/Icone.png + prev_next_buttons_location: none +# custom_dir: docs/overrides + features: + - navigation.instant + - navigation.top + - content.tabs.link + # - navigation.expand + # - navigation.sections + # - header.autohide + # - toc.separate + palette: + - media: "(prefers-color-scheme: light)" + scheme: antares + toggle: + icon: material/toggle-switch-off-outline + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/toggle-switch + name: Switch to light mode + +plugins: +- search +- mkdocstrings diff --git a/pyproject.toml b/pyproject.toml index d2f25a6a..4b611495 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,13 +5,35 @@ build-backend = "setuptools.build_meta" [project] name = "antares_craft" version = "0.0.1" +description = """Antares Craft python library is currently under construction. When completed it will allow to \ +create, update and read antares studies.""" +readme = "README.md" +license = {file = "LICENSE"} +authors = [ + {name="Sylvain Leclerc"}, + {name="Tatiana Vargas"}, + {name="Martin Behlthle"}, + {name="Sigurd Borge"} +] +requires-python = ">=3.9" dependencies = [ ] +classifiers = [ +# Classifiers here: https://pypi.org/classifiers/ + "Development Status :: 2 - Pre-Alpha", + "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +[project.urls] +Repository = "https://github.com/AntaresSimulatorTeam/antares_craft" +"Bug Tracker" = "https://github.com/AntaresSimulatorTeam/antares_craft/issues" + +# Setuptools [tool.setuptools.packages.find] -# All the following settings are optional: +## All the following settings are optional: where = ["src"] - -[tool.ruff] -line-length = 120 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 8c6696a4..dde65207 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ absl-py~=1.4.0 +click~=8.1.7 +configparser~=5.0.2 numpy~=1.26.4 -requests~=2.31.0 pandas~=2.2.2 pandas-stubs~=2.2.2 +pydantic~=2.7.1 pytest~=7.2.1 python-dateutil~=2.9.0 -pydantic~=2.7.1 -configparser~=5.0.2 -click~=8.1.7 +requests~=2.31.0 diff --git a/src/antares/api_conf/request_wrapper.py b/src/antares/api_conf/request_wrapper.py index 0f86f41d..21924a95 100644 --- a/src/antares/api_conf/request_wrapper.py +++ b/src/antares/api_conf/request_wrapper.py @@ -12,13 +12,20 @@ import json -from typing import Any, Dict, Optional, Union +from typing import Any, Iterable, Mapping, Optional, Union import requests from antares.exceptions.exceptions import APIError -DATA_TYPE = Union[str, bytes, Dict[str, Any], None] +DATA_TYPE = Union[ + Iterable[bytes], + str, + bytes, + list[tuple[Any, Any]], + tuple[tuple[Any, Any], ...], + Mapping[Any, Any], +] def _handle_exceptions(response: requests.Response) -> requests.Response: diff --git a/src/antares/service/api_services/renewable_api.py b/src/antares/service/api_services/renewable_api.py index 35892b50..4bc42039 100644 --- a/src/antares/service/api_services/renewable_api.py +++ b/src/antares/service/api_services/renewable_api.py @@ -16,11 +16,7 @@ from antares.api_conf.api_conf import APIconf from antares.api_conf.request_wrapper import RequestWrapper -from antares.exceptions.exceptions import ( - APIError, - RenewableMatrixDownloadError, - RenewablePropertiesUpdateError, -) +from antares.exceptions.exceptions import APIError, RenewableMatrixDownloadError, RenewablePropertiesUpdateError from antares.model.renewable import RenewableCluster, RenewableClusterProperties from antares.service.api_services.utils import get_matrix from antares.service.base_services import BaseRenewableService @@ -64,9 +60,6 @@ def get_renewable_matrix(self, renewable: RenewableCluster) -> pd.DataFrame: / f"{renewable.name}" / "series" ) - return get_matrix( - f"{self._base_url}/studies/{self.study_id}/raw?path={path}", - self._wrapper, - ) + return get_matrix(f"{self._base_url}/studies/{self.study_id}/raw?path={path}", self._wrapper) except APIError as e: raise RenewableMatrixDownloadError(renewable.area_id, renewable.name, e.message) from e diff --git a/src/antares/service/base_services.py b/src/antares/service/base_services.py index 428cf206..4a1b02ad 100644 --- a/src/antares/service/base_services.py +++ b/src/antares/service/base_services.py @@ -137,10 +137,7 @@ def create_load(self, area: Area, series: Optional[pd.DataFrame]) -> Load: @abstractmethod def create_st_storage( - self, - area_id: str, - st_storage_name: str, - properties: Optional[STStorageProperties] = None, + self, area_id: str, st_storage_name: str, properties: Optional[STStorageProperties] = None ) -> STStorage: """ Args: diff --git a/tests/antares/delete/test_delete_api.py b/tests/antares/delete/test_delete_api.py index 24e423cb..f222c7ba 100644 --- a/tests/antares/delete/test_delete_api.py +++ b/tests/antares/delete/test_delete_api.py @@ -30,9 +30,7 @@ from antares.model.st_storage import STStorage from antares.model.thermal import ThermalCluster from antares.service.api_services.area_api import AreaApiService -from antares.service.api_services.binding_constraint_api import ( - BindingConstraintApiService, -) +from antares.service.api_services.binding_constraint_api import BindingConstraintApiService from antares.service.api_services.link_api import LinkApiService from antares.service.api_services.renewable_api import RenewableApiService from antares.service.api_services.st_storage_api import ShortTermStorageApiService @@ -63,11 +61,7 @@ def test_delete_area_success(self): def test_delete_area_fails(self): with requests_mock.Mocker() as mocker: url = f"https://antares.com/api/v1/studies/{self.study_id}/areas/{self.area_fr.id}" - mocker.delete( - url, - json={"description": self.antares_web_description_msg}, - status_code=404, - ) + mocker.delete(url, json={"description": self.antares_web_description_msg}, status_code=404) with pytest.raises( AreaDeletionError, match=f"Could not delete the area {self.area_fr.id}: {self.antares_web_description_msg}", @@ -85,11 +79,7 @@ def test_delete_link_fails(self): with requests_mock.Mocker() as mocker: link = Link(self.area_fr, self.area_be, self.link_service) url = f"https://antares.com/api/v1/studies/{self.study_id}/links/{self.area_fr.id}/{self.area_be.id}" - mocker.delete( - url, - json={"description": self.antares_web_description_msg}, - status_code=404, - ) + mocker.delete(url, json={"description": self.antares_web_description_msg}, status_code=404) with pytest.raises( LinkDeletionError, match=f"Could not delete the link {self.area_fr.id} / {self.area_be.id}: {self.antares_web_description_msg}", @@ -108,11 +98,7 @@ def test_delete_thermal_fails(self): cluster1 = ThermalCluster(self.thermal_service, self.area_fr.id, "gaz_cluster") cluster2 = ThermalCluster(self.thermal_service, self.area_fr.id, "gaz_cluster_2") url = f"https://antares.com/api/v1/studies/{self.study_id}/areas/{self.area_fr.id}/clusters/thermal" - mocker.delete( - url, - json={"description": self.antares_web_description_msg}, - status_code=404, - ) + mocker.delete(url, json={"description": self.antares_web_description_msg}, status_code=404) with pytest.raises( ThermalDeletionError, match=f"Could not delete the following thermal clusters: gaz_cluster, gaz_cluster_2 inside area fr: {self.antares_web_description_msg}", @@ -130,11 +116,7 @@ def test_delete_renewable_fails(self): with requests_mock.Mocker() as mocker: cluster = RenewableCluster(self.renewable_service, self.area_fr.id, "gaz_cluster") url = f"https://antares.com/api/v1/studies/{self.study_id}/areas/{self.area_fr.id}/clusters/renewable" - mocker.delete( - url, - json={"description": self.antares_web_description_msg}, - status_code=404, - ) + mocker.delete(url, json={"description": self.antares_web_description_msg}, status_code=404) with pytest.raises( RenewableDeletionError, match=f"Could not delete the following renewable clusters: gaz_cluster inside area fr: {self.antares_web_description_msg}", @@ -152,11 +134,7 @@ def test_delete_st_storage_fails(self): with requests_mock.Mocker() as mocker: storage = STStorage(self.st_storage_service, self.area_fr.id, "battery_fr") url = f"https://antares.com/api/v1/studies/{self.study_id}/areas/{self.area_fr.id}/storages" - mocker.delete( - url, - json={"description": self.antares_web_description_msg}, - status_code=404, - ) + mocker.delete(url, json={"description": self.antares_web_description_msg}, status_code=404) with pytest.raises( STStorageDeletionError, match=f"Could not delete the following short term storages: battery_fr inside area fr: {self.antares_web_description_msg}", @@ -176,11 +154,7 @@ def test_delete_binding_constraint_fails(self): constraint_id = "bc_1" constraint = BindingConstraint(constraint_id, self.constraint_service) url = f"https://antares.com/api/v1/studies/{self.study_id}/bindingconstraints/{constraint_id}" - mocker.delete( - url, - json={"description": self.antares_web_description_msg}, - status_code=404, - ) + mocker.delete(url, json={"description": self.antares_web_description_msg}, status_code=404) with pytest.raises( BindingConstraintDeletionError, match=f"Could not delete the binding constraint {constraint_id}: {self.antares_web_description_msg}", @@ -204,11 +178,7 @@ def test_delete_constraint_terms_fails(self): url = ( f"https://antares.com/api/v1/studies/{self.study_id}/bindingconstraints/{constraint_id}/term/{term_id}" ) - mocker.delete( - url, - json={"description": self.antares_web_description_msg}, - status_code=404, - ) + mocker.delete(url, json={"description": self.antares_web_description_msg}, status_code=404) with pytest.raises( ConstraintTermDeletionError, match=f"Could not delete the term {term_id} of the binding constraint {constraint_id}: {self.antares_web_description_msg}", diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 00000000..befa122e --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + diff --git a/tests/integration/antares_web_desktop.py b/tests/integration/antares_web_desktop.py index 47373e2c..81f5fa49 100644 --- a/tests/integration/antares_web_desktop.py +++ b/tests/integration/antares_web_desktop.py @@ -33,13 +33,7 @@ def __init__(self): executable_path = antares_web_desktop_path / "AntaresWeb" / "AntaresWebServer" else: executable_path = antares_web_desktop_path / "AntaresWeb" / "AntaresWebServer.exe" - args = [ - str(executable_path), - "-c", - str(config_path), - "--auto-upgrade-db", - "--no-front", - ] + args = [str(executable_path), "-c", str(config_path), "--auto-upgrade-db", "--no-front"] self.desktop_path = antares_web_desktop_path self.host = "127.0.0.1" self.port = 8080 diff --git a/tests/integration/test_web_client.py b/tests/integration/test_web_client.py index e04e496c..f42620c0 100644 --- a/tests/integration/test_web_client.py +++ b/tests/integration/test_web_client.py @@ -23,33 +23,17 @@ STStorageMatrixUploadError, ) from antares.model.area import AdequacyPatchMode, AreaProperties, AreaUi, FilterOption -from antares.model.binding_constraint import ( - BindingConstraintProperties, - ClusterData, - ConstraintTerm, - LinkData, -) +from antares.model.binding_constraint import BindingConstraintProperties, ClusterData, ConstraintTerm, LinkData from antares.model.link import LinkProperties, LinkStyle, LinkUi -from antares.model.renewable import ( - RenewableClusterGroup, - RenewableClusterProperties, - TimeSeriesInterpretation, -) +from antares.model.renewable import RenewableClusterGroup, RenewableClusterProperties, TimeSeriesInterpretation from antares.model.settings import GeneralProperties, PlaylistData, StudySettings -from antares.model.settings.advanced_parameters import ( - AdvancedProperties, - UnitCommitmentMode, -) +from antares.model.settings.advanced_parameters import AdvancedProperties, UnitCommitmentMode from antares.model.settings.general import Mode -from antares.model.st_storage import ( - STStorageGroup, - STStorageMatrixName, - STStorageProperties, -) +from antares.model.st_storage import STStorageGroup, STStorageMatrixName, STStorageProperties from antares.model.study import create_study_api from antares.model.thermal import ThermalClusterGroup, ThermalClusterProperties -from tests.integration.antares_web_desktop import AntaresWebDesktop +from integration.antares_web_desktop import AntaresWebDesktop # todo add integration tests for matrices @@ -99,19 +83,12 @@ def test_creation_lifecycle(self): properties = AreaProperties() properties.energy_cost_spilled = 100 properties.adequacy_patch_mode = AdequacyPatchMode.INSIDE - properties.filter_synthesis = [ - FilterOption.HOURLY, - FilterOption.DAILY, - FilterOption.HOURLY, - ] + properties.filter_synthesis = [FilterOption.HOURLY, FilterOption.DAILY, FilterOption.HOURLY] area_name = "DE" area_de = study.create_area(area_name, properties=properties) assert area_de.properties.energy_cost_spilled == 100 assert area_de.properties.adequacy_patch_mode == AdequacyPatchMode.INSIDE - assert area_de.properties.filter_synthesis == { - FilterOption.HOURLY, - FilterOption.DAILY, - } + assert area_de.properties.filter_synthesis == {FilterOption.HOURLY, FilterOption.DAILY} # tests link creation with default values link_de_fr = study.create_link(area_from=area_de, area_to=area_fr) @@ -123,26 +100,14 @@ def test_creation_lifecycle(self): link_ui = LinkUi(colorr=44) link_properties = LinkProperties(hurdles_cost=True) link_properties.filter_year_by_year = [FilterOption.HOURLY] - link_be_fr = study.create_link( - area_from=area_be, - area_to=area_fr, - ui=link_ui, - properties=link_properties, - ) + link_be_fr = study.create_link(area_from=area_be, area_to=area_fr, ui=link_ui, properties=link_properties) assert link_be_fr.ui.colorr == 44 assert link_be_fr.properties.hurdles_cost assert link_be_fr.properties.filter_year_by_year == {FilterOption.HOURLY} # asserts study contains all links and areas - assert study.get_areas() == { - area_be.id: area_be, - area_fr.id: area_fr, - area_de.id: area_de, - } - assert study.get_links() == { - link_be_fr.name: link_be_fr, - link_de_fr.name: link_de_fr, - } + assert study.get_areas() == {area_be.id: area_be, area_fr.id: area_fr, area_de.id: area_de} + assert study.get_links() == {link_be_fr.name: link_be_fr, link_de_fr.name: link_de_fr} # test thermal cluster creation with default values thermal_name = "Cluster_test %?" @@ -242,20 +207,11 @@ def test_creation_lifecycle(self): # asserts areas contains the clusters + short term storages assert area_be.get_thermals() == {thermal_be.id: thermal_be} - assert area_fr.get_thermals() == { - thermal_fr.id: thermal_fr, - thermal_value_be.id: thermal_value_be, - } + assert area_fr.get_thermals() == {thermal_fr.id: thermal_fr, thermal_value_be.id: thermal_value_be} assert area_be.get_renewables() == {} - assert area_fr.get_renewables() == { - renewable_onshore.id: renewable_onshore, - renewable_fr.id: renewable_fr, - } + assert area_fr.get_renewables() == {renewable_onshore.id: renewable_onshore, renewable_fr.id: renewable_fr} assert area_be.get_st_storages() == {} - assert area_fr.get_st_storages() == { - battery_fr.id: battery_fr, - storage_fr.id: storage_fr, - } + assert area_fr.get_st_storages() == {battery_fr.id: battery_fr, storage_fr.id: storage_fr} # test binding constraint creation without terms properties = BindingConstraintProperties(enabled=False) @@ -274,10 +230,7 @@ def test_creation_lifecycle(self): terms = [link_term_2, cluster_term] constraint_2 = study.create_binding_constraint(name="bc_2", terms=terms) assert constraint_2.name == "bc_2" - assert constraint_2.get_terms() == { - link_term_2.id: link_term_2, - cluster_term.id: cluster_term, - } + assert constraint_2.get_terms() == {link_term_2.id: link_term_2, cluster_term.id: cluster_term} # test constraint creation with matrices # Case that fails @@ -316,10 +269,7 @@ def test_creation_lifecycle(self): cluster_term = ConstraintTerm(data=cluster_data, weight=100) terms = [link_term_1, cluster_term] constraint_1.add_terms(terms) - assert constraint_1.get_terms() == { - link_term_1.id: link_term_1, - cluster_term.id: cluster_term, - } + assert constraint_1.get_terms() == {link_term_1.id: link_term_1, cluster_term.id: cluster_term} # asserts study contains the constraints assert study.get_binding_constraints() == {