-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(load): add arrow endpoints #2200
base: dev
Are you sure you want to change the base?
Changes from 37 commits
ad63883
62f1b1d
cc01641
d73c8de
998c04e
f4206fe
f8b0f8a
ebd2df4
9a3591a
fddf3b8
582aed0
72ec467
43a5e40
4e74852
c99cdb1
0a4e6f5
1b6dc2b
cae2930
1b86d9a
f45f065
6506459
213cbaf
7b5e0a0
37b7108
1ef1ff1
4a234e6
c0a82de
e91dc71
997803a
255b104
aff9c83
d63cb8e
a522a22
8fe2863
9d55b36
5df3960
09bc218
7e3b03a
6277ae0
5fd6880
feb957f
cb3d3f4
fe6acbd
3218843
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# 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. | ||
from io import BytesIO | ||
from typing import cast | ||
|
||
import pandas as pd | ||
from starlette.responses import Response | ||
|
||
from antarest.study.business.model.load_model import LoadDTO | ||
from antarest.study.model import Study | ||
from antarest.study.storage.rawstudy.model.filesystem.matrix.input_series_matrix import InputSeriesMatrix | ||
from antarest.study.storage.storage_service import StudyStorageService | ||
|
||
LOAD_PATH = "input/load/series/load_{area_id}" | ||
|
||
|
||
class LoadManager: | ||
def __init__(self, storage_service: StudyStorageService) -> None: | ||
self.storage_service = storage_service | ||
|
||
def get_load_matrix(self, study: Study, area_id: str) -> Response: | ||
file_study = self.storage_service.get_storage(study).get_raw(study) | ||
|
||
load_path = LOAD_PATH.format(area_id=area_id).split("/") | ||
node = file_study.tree.get_node(load_path) | ||
|
||
if not isinstance(node, InputSeriesMatrix): | ||
return Response(content="Invalid node type", status_code=400) | ||
|
||
matrix_data = InputSeriesMatrix.parse(node, return_dataframe=True) | ||
|
||
matrix_df = cast(pd.DataFrame, matrix_data) | ||
matrix_df.columns = matrix_df.columns.map(str) | ||
buffer = BytesIO() | ||
matrix_df.to_feather(buffer, compression="uncompressed") | ||
return Response(content=buffer.getvalue(), media_type="application/vnd.apache.arrow.file") | ||
|
||
def update_load_matrix(self, study: Study, area_id: str, load_dto: LoadDTO) -> LoadDTO: | ||
load_properties = load_dto.to_properties() | ||
load_path = LOAD_PATH.format(area_id=area_id).split("/") | ||
|
||
file_study = self.storage_service.get_storage(study).get_raw(study) | ||
|
||
df = pd.read_feather(BytesIO(load_properties.matrix)) | ||
|
||
if df.shape[1] != 2: | ||
pass | ||
|
||
file_study.tree.save(load_properties.matrix, load_path) | ||
|
||
return load_dto |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# 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. | ||
from pydantic import ConfigDict | ||
|
||
from antarest.core.serialization import AntaresBaseModel | ||
from antarest.core.utils.string import to_camel_case, to_kebab_case | ||
|
||
|
||
class LoadDTO(AntaresBaseModel): | ||
matrix: bytes | ||
|
||
model_config = ConfigDict(alias_generator=to_camel_case, extra="forbid") | ||
|
||
def to_properties(self) -> "LoadProperties": | ||
return LoadProperties(matrix=self.matrix) | ||
|
||
|
||
class LoadProperties(AntaresBaseModel): | ||
matrix: bytes | ||
|
||
model_config = ConfigDict(alias_generator=to_kebab_case, extra="forbid") | ||
|
||
def to_dto(self) -> LoadDTO: | ||
return LoadDTO(matrix=self.matrix) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -87,6 +87,7 @@ | |
from antarest.study.business.district_manager import DistrictManager | ||
from antarest.study.business.general_management import GeneralManager | ||
from antarest.study.business.link_management import LinkInfoDTO, LinkManager | ||
from antarest.study.business.load_management import LoadManager | ||
from antarest.study.business.matrix_management import MatrixManager, MatrixManagerError | ||
from antarest.study.business.optimization_management import OptimizationManager | ||
from antarest.study.business.playlist_management import PlaylistManager | ||
|
@@ -366,6 +367,7 @@ def __init__( | |
self.adequacy_patch_manager = AdequacyPatchManager(self.storage_service) | ||
self.advanced_parameters_manager = AdvancedParamsManager(self.storage_service) | ||
self.hydro_manager = HydroManager(self.storage_service) | ||
self.load_manager = LoadManager(self.storage_service) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generally speaking for the other matrices, what is your vision here ? For hydro for instance, we want to add the code inside the HydroManager or do we want to create a specific manager on the side ? I personally prefer the 1st option |
||
self.allocation_manager = AllocationManager(self.storage_service) | ||
self.properties_manager = PropertiesManager(self.storage_service) | ||
self.renewable_manager = RenewableManager(self.storage_service) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,7 +17,7 @@ | |
|
||
import typing_extensions as te | ||
from fastapi import APIRouter, Body, Depends, Query | ||
from starlette.responses import RedirectResponse | ||
from starlette.responses import RedirectResponse, Response | ||
|
||
from antarest.core.config import Config | ||
from antarest.core.jwt import JWTUser | ||
|
@@ -69,13 +69,14 @@ | |
from antarest.study.business.district_manager import DistrictCreationDTO, DistrictInfoDTO, DistrictUpdateDTO | ||
from antarest.study.business.general_management import GeneralFormFields | ||
from antarest.study.business.link_management import LinkInfoDTO | ||
from antarest.study.business.model.load_model import LoadDTO | ||
from antarest.study.business.optimization_management import OptimizationFormFields | ||
from antarest.study.business.playlist_management import PlaylistColumns | ||
from antarest.study.business.scenario_builder_management import Rulesets, ScenarioType | ||
from antarest.study.business.table_mode_management import TableDataDTO, TableModeType | ||
from antarest.study.business.thematic_trimming_field_infos import ThematicTrimmingFormFields | ||
from antarest.study.business.timeseries_config_management import TSFormFields | ||
from antarest.study.model import PatchArea, PatchCluster | ||
from antarest.study.model import MatrixFormat, PatchArea, PatchCluster | ||
from antarest.study.service import StudyService | ||
from antarest.study.storage.rawstudy.model.filesystem.config.binding_constraint import ( | ||
BindingConstraintFrequency, | ||
|
@@ -524,6 +525,41 @@ def update_inflow_structure( | |
study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) | ||
study_service.hydro_manager.update_inflow_structure(study, area_id, values) | ||
|
||
@bp.get( | ||
"/studies/{uuid}/{area_id}/load/series", | ||
tags=[APITag.study_data], | ||
summary="Get load series data", | ||
) | ||
def get_load_series( | ||
uuid: str, | ||
area_id: str, | ||
current_user: JWTUser = Depends(auth.get_current_user), | ||
) -> Response: | ||
"""Return the load matrix in ARROW format.""" | ||
logger.info( | ||
msg=f"Getting load series data for area {area_id} of study {uuid}", | ||
extra={"user": current_user.id}, | ||
) | ||
params = RequestParameters(user=current_user) | ||
study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) | ||
return study_service.load_manager.get_load_matrix(study, area_id) | ||
|
||
@bp.put( | ||
"/studies/{uuid}/{area_id}/load/series", | ||
tags=[APITag.study_data], | ||
summary="Update load series data", | ||
) | ||
def update_load_series( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we want to support arrow input, I don't think we will be able to use a pydantic model (LoadDTO), because they expect JSON, which cannot contain binary data. So: if we want the same URL to support at the same time JSON and arrow, we will need to separate it in 2 functions I think, for example discriminated by the "accept-content" header. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Eventually, we decided to only support Arrow format. I'll check to see what is best to handle binary inputs |
||
uuid: str, | ||
area_id: str, | ||
load_dto: LoadDTO, | ||
current_user: JWTUser = Depends(auth.get_current_user), | ||
) -> LoadDTO: | ||
"""Replace the existing load matrix.""" | ||
params = RequestParameters(user=current_user) | ||
study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) | ||
return study_service.load_manager.update_load_matrix(study, area_id, load_dto) | ||
|
||
@bp.put( | ||
"/studies/{uuid}/matrix", | ||
tags=[APITag.study_data], | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# 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. | ||
from io import BytesIO | ||
|
||
import pandas as pd | ||
import pytest | ||
from starlette.testclient import TestClient | ||
|
||
from tests.integration.prepare_proxy import PreparerProxy | ||
|
||
|
||
@pytest.mark.unit_test | ||
class TestLink: | ||
@pytest.mark.parametrize("study_type", ["raw", "variant"]) | ||
def test_load(self, client: TestClient, user_access_token: str, study_type: str) -> None: | ||
client.headers = {"Authorization": f"Bearer {user_access_token}"} # type: ignore | ||
|
||
preparer = PreparerProxy(client, user_access_token) | ||
study_id = preparer.create_study("foo", version=880) | ||
|
||
if study_type == "variant": | ||
study_id = preparer.create_variant(study_id, name="Variant 1") | ||
|
||
area1_id = preparer.create_area(study_id, name="Area 1")["id"] | ||
|
||
# Test simple get ARROW | ||
|
||
res = client.get(f"/v1/studies/{study_id}/{area1_id}/load/series?matrix_format=arrow") | ||
assert res.status_code == 200 | ||
assert res.headers["content-type"] == "application/vnd.apache.arrow.file" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This "business" layer should be independent from web considerations, it should return a dataframe instead.
Formatting to arrow should be done in the web layer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done