diff --git a/cwms/__init__.py b/cwms/__init__.py index d743cc21..772f27a4 100644 --- a/cwms/__init__.py +++ b/cwms/__init__.py @@ -11,6 +11,7 @@ from cwms.locations.gate_changes import * from cwms.locations.location_groups import * from cwms.locations.physical_locations import * +from cwms.locks import * from cwms.measurements.measurements import * from cwms.outlets.outlets import * from cwms.outlets.virtual_outlets import * @@ -18,6 +19,8 @@ from cwms.projects.project_locks import * from cwms.projects.projects import * from cwms.projects.water_supply.accounting import * +from cwms.projects.water_supply.water_contracts import * +from cwms.projects.water_supply.water_users import * from cwms.properties.properties import * from cwms.ratings.ratings import * from cwms.ratings.ratings_spec import * diff --git a/cwms/locks/locks.py b/cwms/locks/locks.py new file mode 100644 index 00000000..bfeee67e --- /dev/null +++ b/cwms/locks/locks.py @@ -0,0 +1,198 @@ +# Copyright (c) 2026 +# United States Army Corps of Engineers - Hydrologic Engineering Center (USACE/HEC) +# All Rights Reserved. USACE PROPRIETARY/CONFIDENTIAL. +# Source may not be released without written approval from HEC + +from typing import Optional + +import cwms.api as api +from cwms.cwms_types import JSON, Data, DeleteMethod + + +def get_lock(name: str, office_id: str, unit: Optional[str] = "SI") -> Data: + """ + Get a specified lock with the given office and name. + + Parameters + ---------- + office_id : str + The office ID of the lock to retrieve. (Query) + name : str + The name of the lock to retrieve. (Query) + unit : str + The unit system to use for the response. Defaults to "SI". (Query) + + Returns + ------- + Data + The JSON response from CWMS Data API wrapped in a Data object. + + Raises + ------ + ValueError + If any required path parameters are None. + ClientError + If a 400-level error occurs. + NoDataFoundError + If a 404-level error occurs. + ServerError + If a 500-level error occurs. + """ + if not all([office_id, name]): + raise ValueError("Office and Name must be provided.") + + endpoint = f"projects/locks/{name}" + + params = {"office": office_id, "unit": unit} + + response = api.get(endpoint, params, api_version=1) + return Data(response) + + +def get_locks(office_id: str, project_id: str) -> Data: + """ + Get all locks for the given office and project. + + Parameters + ---------- + office_id : str + The office ID of the locks to retrieve. (Query) + project_id : str + The project ID of the locks to retrieve. (Query) + + Returns + ------- + Data + The JSON response from CWMS Data API wrapped in a Data object. + + Raises + ------ + ValueError + If any required path parameters are None. + ClientError + If a 400-level error occurs. + NoDataFoundError + If a 404-level error occurs. + ServerError + If a 500-level error occurs. + """ + if not all([office_id, project_id]): + raise ValueError("Office and Project ID must be provided.") + + endpoint = "projects/locks" + + params = {"office": office_id, "project": project_id} + + response = api.get(endpoint, params, api_version=1) + return Data(response) + + +def create_lock(data: JSON, fail_if_exists: bool = True) -> None: + """ + Create CWMS Lock. + + Parameters + ---------- + data : JSON + Lock successfully stored to CWMS. (Body) + fail_if_exists : bool, optional + Create will fail if provided ID already exists. Default: True. (Query) + + Returns + ------- + None + + Raises + ------ + ValueError + If any required parameters are None. + ClientError + If a 400-level error occurs. + NoDataFoundError + If a 404-level error occurs. + ServerError + If a 500-level error occurs. + """ + if not data: + raise ValueError("Data must be provided and cannot be empty.") + + endpoint = "projects/locks" + params = {"fail-if-exists": fail_if_exists} + + api.post(endpoint, data, params, api_version=1) + + +def delete_lock( + name: str, office_id: str, method: DeleteMethod = DeleteMethod.DELETE_KEY +) -> None: + """ + Delete CWMS Lock + + Parameters + ---------- + name : str + Specifies the name of the lock to be deleted. (Path) + office_id : str + Specifies the owning office of the lock to be deleted. (Query) + method : DeleteMethod, optional + Specifies the delete method used. Defaults to "DELETE_KEY". (Query) + + Returns + ------- + None + + Raises + ------ + ValueError + If any required parameters are None. + ClientError + If a 400-level error occurs. + NoDataFoundError + If a 404-level error occurs. + ServerError + If a 500-level error occurs. + """ + if not all([name, office_id]): + raise ValueError("Name and Office ID must be provided.") + + endpoint = f"projects/locks/{name}" + params = {"office": office_id, "method": method.name} + + api.delete(endpoint, params, api_version=1) + + +def update_lock(name: str, office_id: str, new_name: str) -> None: + """ + Rename CWMS Lock + + Parameters + ---------- + name : str + Specifies the name of the lock to be renamed. (Path) + office_id : str + Specifies the owning office of the lock to be renamed. (Query) + new_name : str + Specifies the new lock name. (Query) + + Returns + ------- + None + + Raises + ------ + ValueError + If any required parameters are None. + ClientError + If a 400-level error occurs. + NoDataFoundError + If a 404-level error occurs. + ServerError + If a 500-level error occurs. + """ + if not all([name, office_id, new_name]): + raise ValueError("Name, Office ID, and New Name must be provided.") + + endpoint = f"projects/locks/{name}" + params = {"office": office_id, "name": new_name} + + api.patch(endpoint, None, params, api_version=1) diff --git a/cwms/projects/water_supply/water_contracts.py b/cwms/projects/water_supply/water_contracts.py new file mode 100644 index 00000000..6d660701 --- /dev/null +++ b/cwms/projects/water_supply/water_contracts.py @@ -0,0 +1,250 @@ +# Copyright (c) 2026 +# United States Army Corps of Engineers - Hydrologic Engineering Center (USACE/HEC) +# All Rights Reserved. USACE PROPRIETARY/CONFIDENTIAL. +# Source may not be released without written approval from HEC + +import cwms.api as api +from cwms.cwms_types import JSON, Data, DeleteMethod + + +def get_water_contract( + office_id: str, project_id: str, water_user: str, contract_name: str +) -> Data: + """ + Return a specified water contract + + Parameters + ---------- + office_id : str + The office Id the contract is associated with. (Path) + project_id : str + The project Id the contract is associated with. (Path) + water_user : str + The water user the contract is associated with. (Path) + contract_name : str + The name of the contract to retrieve. (Path) + + Returns + ------- + Data + The JSON response from CWMS Data API wrapped in a Data object. + + Raises + ------ + ValueError + If any required path parameters are None. + ClientError + If a 400-level error occurs. + NoDataFoundError + If a 404-level error occurs. + ServerError + If a 500-level error occurs. + """ + if not all([office_id, project_id, water_user, contract_name]): + raise ValueError( + "Office, project_id, water_user, and contract_name must be provided." + ) + + endpoint = f"projects/{office_id}/{project_id}/water-users/{water_user}/contracts/{contract_name}" + + response = api.get(endpoint, api_version=1) + return Data(response) + + +def get_water_contracts(office_id: str, project_id: str, water_user: str) -> Data: + """ + Return all water contracts for the specified water user, project, and office + + Parameters + ---------- + office_id : str + The office Id the contract is associated with. (Path) + project_id : str + The project Id the contract is associated with. (Path) + water_user : str + The water user the contract is associated with. (Path) + + Returns + ------- + Data + The JSON response from CWMS Data API wrapped in a Data object. + + Raises + ------ + ValueError + If any required path parameters are None. + ClientError + If a 400-level error occurs. + NoDataFoundError + If a 404-level error occurs. + ServerError + If a 500-level error occurs. + """ + if not all([office_id, project_id, water_user]): + raise ValueError("Office, project_id, and water_user must be provided.") + + endpoint = f"projects/{office_id}/{project_id}/water-users/{water_user}/contracts" + + response = api.get(endpoint, api_version=1) + return Data(response) + + +def create_water_contract( + office_id: str, + project_id: str, + water_user: str, + data: JSON, + fail_if_exists: bool = True, + ignore_nulls: bool = False, +) -> None: + """ + Create a new water contract + + Parameters + ---------- + office_id : str + The office Id the contract is associated with. (Path) + project_id : str + The project Id the contract is associated with. (Path) + water_user : str + The water user the contract is associated with. (Path) + data : JSON + Water contract successfully stored to CWMS. (Body) + fail_if_exists : bool, optional + If true, the contract will not be stored if it already exists. + Default: True (Query) + ignore_nulls : bool, optional + If true, null fields will be ignored when storing the contract. + Default: False (Query) + + Returns + ------- + None + + Raises + ------ + ValueError + If any required path parameters are None. + ClientError + If a 400-level error occurs. + NoDataFoundError + If a 404-level error occurs. + ServerError + If a 500-level error occurs. + """ + if not all([office_id, project_id, water_user]): + raise ValueError("Office, project_id, and water_user must be provided.") + if not data: + raise ValueError("Data must be provided and cannot be empty.") + + endpoint = f"projects/{office_id}/{project_id}/water-user/{water_user}/contracts" + params = { + "fail-if-exists": fail_if_exists, + "ignore-nulls": ignore_nulls, + } + + api.post(endpoint, data, params, api_version=1) + + +def delete_water_contract( + office_id: str, + project_id: str, + water_user: str, + contract_name: str, + method: DeleteMethod = DeleteMethod.DELETE_KEY, +) -> None: + """ + Delete a specified water contract + + Parameters + ---------- + office_id : str + The office Id the contract is associated with. (Path) + project_id : str + The project Id the contract is associated with. (Path) + water_user : str + The water user the contract is associated with. (Path) + contract_name : str + The name of the contract to be deleted. (Path) + method : DeleteMethod, optional + Specifies the delete method used. Defaults to DELETE_KEY. (Query) + + Returns + ------- + None + + Raises + ------ + ValueError + If any required path parameters are None. + ClientError + If a 400-level error occurs. + NoDataFoundError + If a 404-level error occurs. + ServerError + If a 500-level error occurs. + """ + if not all([office_id, project_id, water_user, contract_name]): + raise ValueError( + "Office, Project ID, Water User, and Contract Name must be provided." + ) + + endpoint = f"projects/{office_id}/{project_id}/water-users/{water_user}/contracts/{contract_name}" + params = {"method": method.name} + + api.delete(endpoint, params, api_version=1) + + +def update_water_contract( + office_id: str, + project_id: str, + water_user: str, + contract_name: str, + new_contract_name: str, + data: JSON, +) -> None: + """ + Updates a water contract in CWMS. + + Parameters + ---------- + office_id : str + The office Id the contract is associated with. (Path) + project_id : str + The project Id the contract is associated with. (Path) + water_user : str + The water user the contract is associated with. (Path) + contract_name : str + The name of the contract to be updated. (Path) + new_contract_name : str + The new name of the contract. (Query) + data : JSON + Water contract successfully updated in CWMS. (Body) + + Returns + ------- + None + + Raises + ------ + ValueError + If any required path parameters are None. + ClientError + If a 400-level error occurs. + NoDataFoundError + If a 404-level error occurs. + ServerError + If a 500-level error occurs. + """ + if not all([office_id, project_id, water_user, contract_name, new_contract_name]): + raise ValueError( + "Office, Project ID, Contract Name, New Contract Name, and Water User must be provided." + ) + if not data: + raise ValueError("Data must be provided and cannot be empty.") + + endpoint = f"projects/{office_id}/{project_id}/water-user/{water_user}/contracts/{contract_name}" + + params = {"contract-name": new_contract_name} + + api.patch(endpoint, data, params, api_version=1) diff --git a/cwms/projects/water_supply/water_users.py b/cwms/projects/water_supply/water_users.py new file mode 100644 index 00000000..5b87c25e --- /dev/null +++ b/cwms/projects/water_supply/water_users.py @@ -0,0 +1,206 @@ +# Copyright (c) 2026 +# United States Army Corps of Engineers - Hydrologic Engineering Center (USACE/HEC) +# All Rights Reserved. USACE PROPRIETARY/CONFIDENTIAL. +# Source may not be released without written approval from HEC + +import cwms.api as api +from cwms.cwms_types import JSON, Data + + +def get_water_user(office_id: str, project_id: str, water_user: str) -> Data: + """ + Gets a specified water user. + + Parameters + ---------- + office_id : str + The office Id the contract is associated with. (Path) + project_id : str + The project Id the contract is associated with. (Path) + water_user : str + The water user the contract is associated with. (Path) + + Returns + ------- + Data + The JSON response from CWMS Data API wrapped in a Data object. + + Raises + ------ + ValueError + If any required path parameters are None. + ClientError + If a 400-level error occurs. + NoDataFoundError + If a 404-level error occurs. + ServerError + If a 500-level error occurs. + """ + if not all([office_id, project_id, water_user]): + raise ValueError("Office, project_id, and water_user must be provided.") + + endpoint = f"projects/{office_id}/{project_id}/water-user/{water_user}" + + response = api.get(endpoint, api_version=1) + return Data(response) + + +def get_water_users(office_id: str, project_id: str) -> Data: + """ + Gets a specified water user. + + Parameters + ---------- + office_id : str + The office Id the contract is associated with. (Path) + project_id : str + The project Id the contract is associated with. (Path) + + Returns + ------- + Data + The JSON response from CWMS Data API wrapped in a Data object. + + Raises + ------ + ValueError + If any required path parameters are None. + ClientError + If a 400-level error occurs. + NoDataFoundError + If a 404-level error occurs. + ServerError + If a 500-level error occurs. + """ + if not all([office_id, project_id]): + raise ValueError("Office and Project ID must be provided.") + + endpoint = f"projects/{office_id}/{project_id}/water-users" + + response = api.get(endpoint, api_version=1) + return Data(response) + + +def create_water_user( + data: JSON, office_id: str, project_id: str, fail_if_exists: bool = True +) -> None: + """ + Stores a water user to CWMS. + + Parameters + ---------- + office_id : str + The office Id the contract is associated with. (Path) + project_id : str + The project Id the contract is associated with. (Path) + data : JSON + Water user successfully stored to CWMS. (Body) + fail_if_exists : bool, optional + If true, the operation will fail if the water user already exists. + Default: true (Query) + + Returns + ------- + None + + Raises + ------ + ValueError + If any required path parameters are None. + ClientError + If a 400-level error occurs. + NoDataFoundError + If a 404-level error occurs. + ServerError + If a 500-level error occurs. + """ + if not all([office_id, project_id]): + raise ValueError("Office and project_id must be provided.") + if not data: + raise ValueError("Data must be provided and cannot be empty.") + + endpoint = f"projects/{office_id}/{project_id}/water-user" + params = {"fail-if-exists": fail_if_exists} + + api.post(endpoint, data, params, api_version=1) + + +def delete_water_user(office_id: str, project_id: str, water_user: str) -> None: + """ + Deletes a specified water user. + + Parameters + ---------- + office_id : str + The office Id the contract is associated with. (Path) + project_id : str + The project Id the contract is associated with. (Path) + water_user : str + The water user the contract is associated with. (Path) + + Returns + ------- + None + + Raises + ------ + ValueError + If any required path parameters are None. + ClientError + If a 400-level error occurs. + NoDataFoundError + If a 404-level error occurs. + ServerError + If a 500-level error occurs. + """ + if not all([office_id, project_id, water_user]): + raise ValueError("Office, Project ID, and Water User must be provided.") + + endpoint = f"projects/{office_id}/{project_id}/water-user/{water_user}" + + api.delete(endpoint, api_version=1) + + +def update_water_user( + office_id: str, project_id: str, water_user: str, data: JSON, name: str +) -> None: + """ + Updates a water user in CWMS. + + Parameters + ---------- + office_id : str + The office Id the contract is associated with. (Path) + project_id : str + The project Id the contract is associated with. (Path) + water_user : str + The water user the contract is associated with. (Path) + data : JSON + Water user entity data in JSON format. (Body) + name : str + Specifies the new name of the water user entity. (Query) + + Returns + ------- + None + + Raises + ------ + ValueError + If any required path parameters are None. + ClientError + If a 400-level error occurs. + NoDataFoundError + If a 404-level error occurs. + ServerError + If a 500-level error occurs. + """ + if not all([office_id, project_id, water_user, name]): + raise ValueError("Office, Project ID, Water User, and Name must be provided.") + if not data: + raise ValueError("Data must be provided and cannot be empty.") + + endpoint = f"projects/{office_id}/{project_id}/water-user/{water_user}" + params = {"name": name} + + api.patch(endpoint, data, params, api_version=1) diff --git a/tests/cda/locks/locks_test.py b/tests/cda/locks/locks_test.py new file mode 100644 index 00000000..4bcda24e --- /dev/null +++ b/tests/cda/locks/locks_test.py @@ -0,0 +1,9 @@ +from datetime import datetime, timedelta, timezone +from unittest.mock import patch + +import pandas as pd +import pandas.testing as pdt +import pytest + +import cwms +import cwms.locks.locks as lk diff --git a/tests/cda/projects/water_supply/pump_accounting_test.py b/tests/cda/projects/water_supply/pump_accounting_test.py new file mode 100644 index 00000000..8422041b --- /dev/null +++ b/tests/cda/projects/water_supply/pump_accounting_test.py @@ -0,0 +1,285 @@ +from datetime import datetime, timedelta, timezone +from unittest.mock import patch + +import pandas as pd +import pandas.testing as pdt +import pytest + +import cwms +import cwms.locations.physical_locations as pl +import cwms.projects.water_supply.accounting as ac +import cwms.projects.water_supply.water_contracts as wc +import cwms.projects.water_supply.water_users as wu + +TEST_OFFICE = "SPK" +TEST_CONTRACT_ID = "Sac River Pumps" +TEST_PROJECT_ID = "Sacramento Delta" +TEST_ENTITY_NAME = "California DWR" +TEST_WATER_RIGHT = "CA Water Rights Permit #12345" +PUMP_LOCATION_ID = "Sac River-Pump 1" +PUMP_LOCATION_ID2 = "Sac River-Pump 2" +PUMP_LOCATION_ID3 = "Sac River-Pump 3" +PUBLIC_NAME = "Test Public Pump Name" +LONG_NAME = "Test Long Name" +LOCATION_TYPE = "Test Location Type" +DESCRIPTION = "Test Description" +MAP_LABEL = "Test Map Label" + +WATER_USER = { + "entity-name": TEST_ENTITY_NAME, + "project-id": {"office-id": TEST_OFFICE, "name": TEST_PROJECT_ID}, + "water-right": TEST_WATER_RIGHT, +} + +PROJECT_LOCATION = { + "office-id": TEST_OFFICE, + "name": TEST_PROJECT_ID, + "latitude": 0, + "longitude": 0, + "active": True, + "public-name": PUBLIC_NAME, + "long-name": LONG_NAME, + "description": DESCRIPTION, + "timezone-name": "UTC", + "location-type": LOCATION_TYPE, + "location-kind": "PROJECT", + "nation": "US", + "state-initial": "NV", + "county-name": "Clark", + "nearest-city": "Sparks", + "horizontal-datum": "WGS84", + "published-longitude": 0, + "published-latitude": 0, + "vertical-datum": "NGVD29", + "elevation": 150, + "map-label": MAP_LABEL, + "bounding-office-id": TEST_OFFICE, + "elevation-units": "m", +} + +PUMP_LOCATION1 = { + "office-id": TEST_OFFICE, + "name": PUMP_LOCATION_ID2, + "latitude": 0, + "longitude": 0, + "active": True, + "public-name": PUBLIC_NAME, + "long-name": LONG_NAME, + "description": DESCRIPTION, + "timezone-name": "UTC", + "location-type": LOCATION_TYPE, + "location-kind": "PUMP", + "nation": "US", + "state-initial": "NV", + "county-name": "Clark", + "nearest-city": "Sparks", + "horizontal-datum": "WGS84", + "published-longitude": 0, + "published-latitude": 0, + "vertical-datum": "NGVD29", + "elevation": 150, + "map-label": MAP_LABEL, + "bounding-office-id": TEST_OFFICE, + "elevation-units": "m", +} + +PUMP_LOCATION2 = { + "office-id": TEST_OFFICE, + "name": PUMP_LOCATION_ID3, + "latitude": 0, + "longitude": 0, + "active": True, + "public-name": PUBLIC_NAME, + "long-name": LONG_NAME, + "description": DESCRIPTION, + "timezone-name": "UTC", + "location-type": LOCATION_TYPE, + "location-kind": "PUMP", + "nation": "US", + "state-initial": "NV", + "county-name": "Clark", + "nearest-city": "Sparks", + "horizontal-datum": "WGS84", + "published-longitude": 0, + "published-latitude": 0, + "vertical-datum": "NGVD29", + "elevation": 150, + "map-label": MAP_LABEL, + "bounding-office-id": TEST_OFFICE, + "elevation-units": "m", +} + +PUMP_LOCATION3 = { + "office-id": TEST_OFFICE, + "name": PUMP_LOCATION_ID, + "latitude": 0, + "longitude": 0, + "active": True, + "public-name": PUBLIC_NAME, + "long-name": LONG_NAME, + "description": DESCRIPTION, + "timezone-name": "UTC", + "location-type": LOCATION_TYPE, + "location-kind": "PUMP", + "nation": "US", + "state-initial": "NV", + "county-name": "Clark", + "nearest-city": "Sparks", + "horizontal-datum": "WGS84", + "published-longitude": 0, + "published-latitude": 0, + "vertical-datum": "NGVD29", + "elevation": 150, + "map-label": MAP_LABEL, + "bounding-office-id": TEST_OFFICE, + "elevation-units": "m", +} + +WATER_CONTRACT = { + "office-id": TEST_OFFICE, + "water-user": WATER_USER, + "contract-id": {"office-id": TEST_OFFICE, "name": TEST_CONTRACT_ID}, + "contract-type": { + "office-id": TEST_OFFICE, + "display-value": "Test Display Value", + "tooltip": "Test Tooltip", + "active": True, + }, + "contract-effective-date": 158000, + "contract-expiration-date": 167000, + "contracted-storage": 200000.5, + "initial-use-allocation": 15600, + "future-use-allocation": 27800.5, + "storage-units-id": "m3", + "future-use-percent-activated": 15.6, + "total-alloc-percent-activated": 65.2, + "pump-out-location": {"pump-location": PUMP_LOCATION1, "pump-type": "OUT"}, + "pump-out-below-location": {"pump-location": PUMP_LOCATION2, "pump-type": "BELOW"}, + "pump-in-location": {"pump-location": PUMP_LOCATION3, "pump-type": "IN"}, +} + +PUMP_ACCOUNTING = { + "contract-name": TEST_CONTRACT_ID, + "water-user": WATER_USER, + "pump-locations": { + "pump-in": {"office-id": TEST_OFFICE, "name": PUMP_LOCATION_ID}, + "pump-out": {"office-id": TEST_OFFICE, "name": PUMP_LOCATION_ID2}, + "pump-below": {"office-id": TEST_OFFICE, "name": PUMP_LOCATION_ID3}, + }, + "pump-accounting": { + "2022-11-20T21:17:28Z": [ + { + "pump-type": "IN", + "transfer-type-display": "Temporary Inlet", + "flow": 1.0, + "flow-unit": "cms", + "comment": "Added water to the system", + }, + { + "pump-type": "OUT", + "transfer-type-display": "Pipeline", + "flow": 2.0, + "flow-unit": "cms", + "comment": "Removed excess water", + }, + { + "pump-type": "BELOW", + "transfer-type-display": "Pipeline", + "flow": 3.0, + "flow-unit": "cms", + "comment": "Daily water release", + }, + ], + "2023-11-21T21:17:28Z": [ + { + "pump-type": "IN", + "transfer-type-display": "Pipeline", + "flow": 4.0, + "flow-unit": "cms", + "comment": "Pump transfer for the day", + }, + { + "pump-type": "OUT", + "transfer-type-display": "Pipeline", + "flow": 5.0, + "flow-unit": "cms", + "comment": "Excess water transfer", + }, + { + "pump-type": "BELOW", + "transfer-type-display": "Pipeline", + "flow": 6.0, + "flow-unit": "cms", + "comment": "Water returned to the river", + }, + ], + "2024-11-22T21:17:28Z": [ + { + "pump-type": "IN", + "transfer-type-display": "Pipeline", + "flow": 7.0, + "flow-unit": "cms", + "comment": "Pump transfer for the day", + }, + { + "pump-type": "OUT", + "transfer-type-display": "Pipeline", + "flow": 8.0, + "flow-unit": "cms", + "comment": "Excess water transfer", + }, + { + "pump-type": "BELOW", + "transfer-type-display": "Pipeline", + "flow": 9.0, + "flow-unit": "cms", + "comment": "Water returned to the river", + }, + ], + }, +} + + +def _cleanup(): + wc.delete_water_contract( + TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME, TEST_CONTRACT_ID + ) + wu.delete_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME) + pl.delete_location(PUMP_LOCATION_ID, TEST_OFFICE) + pl.delete_location(PUMP_LOCATION_ID2, TEST_OFFICE) + pl.delete_location(PUMP_LOCATION_ID3, TEST_OFFICE) + pl.delete_location(TEST_PROJECT_ID, TEST_OFFICE) + + +@pytest.fixture(scope="module", autouse=True) +def setup_data(): + _cleanup() + + wu.create_water_user(WATER_USER, TEST_OFFICE, TEST_PROJECT_ID, False) + pl.store_location(PUMP_LOCATION1, False) + pl.store_location(PUMP_LOCATION2, False) + pl.store_location(PUMP_LOCATION3, False) + pl.store_location(PROJECT_LOCATION, False) + wc.create_water_contract( + TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME, WATER_CONTRACT, False + ) + + +@pytest.fixture(autouse=True) +def init_session(): + print("Initializing CWMS API session for water supply accounting tests...") + + +def test_create_get_accounting(): + ac.store_pump_accounting( + TEST_OFFICE, TEST_PROJECT_ID, TEST_CONTRACT_ID, PUMP_ACCOUNTING + ) + data = ac.get_pump_accounting( + TEST_OFFICE, + TEST_PROJECT_ID, + TEST_ENTITY_NAME, + TEST_CONTRACT_ID, + "2022-11-19T00:00:00Z", + "2022-11-22T00:00:00Z", + ) + assert data == PUMP_ACCOUNTING diff --git a/tests/cda/projects/water_supply/water_contract_test.py b/tests/cda/projects/water_supply/water_contract_test.py new file mode 100644 index 00000000..49fd229a --- /dev/null +++ b/tests/cda/projects/water_supply/water_contract_test.py @@ -0,0 +1,255 @@ +from datetime import datetime, timedelta, timezone +from unittest.mock import patch + +import pandas as pd +import pandas.testing as pdt +import pytest + +import cwms +import cwms.locations.physical_locations as pl +import cwms.projects.water_supply.water_contracts as wc +import cwms.projects.water_supply.water_users as wu + +TEST_OFFICE = "SPK" +PUMP_LOCATION_ID = "Sac River-Pump 1" +PUMP_LOCATION_ID2 = "Sac River-Pump 2" +PUMP_LOCATION_ID3 = "Sac River-Pump 3" +TEST_CONTRACT_ID = "Sac River Pumps" +TEST_PROJECT_ID = "Sacramento Delta" +TEST_ENTITY_NAME = "California DWR" +TEST_WATER_RIGHT = "CA Water Rights Permit #12345" +PUBLIC_NAME = "Test Public Pump Name" +LONG_NAME = "Test Long Name" +LOCATION_TYPE = "Test Location Type" +DESCRIPTION = "Test Description" +MAP_LABEL = "Test Map Label" + +WATER_USER = { + "entity-name": TEST_ENTITY_NAME, + "project-id": {"office-id": TEST_OFFICE, "name": TEST_PROJECT_ID}, + "water-right": TEST_WATER_RIGHT, +} + +PROJECT_LOCATION = { + "office-id": TEST_OFFICE, + "name": TEST_PROJECT_ID, + "latitude": 0, + "longitude": 0, + "active": True, + "public-name": PUBLIC_NAME, + "long-name": LONG_NAME, + "description": DESCRIPTION, + "timezone-name": "UTC", + "location-type": LOCATION_TYPE, + "location-kind": "PROJECT", + "nation": "US", + "state-initial": "NV", + "county-name": "Clark", + "nearest-city": "Sparks", + "horizontal-datum": "WGS84", + "published-longitude": 0, + "published-latitude": 0, + "vertical-datum": "NGVD29", + "elevation": 150, + "map-label": MAP_LABEL, + "bounding-office-id": TEST_OFFICE, + "elevation-units": "m", +} + +PUMP_LOCATION1 = { + "office-id": TEST_OFFICE, + "name": PUMP_LOCATION_ID2, + "latitude": 0, + "longitude": 0, + "active": True, + "public-name": PUBLIC_NAME, + "long-name": LONG_NAME, + "description": DESCRIPTION, + "timezone-name": "UTC", + "location-type": LOCATION_TYPE, + "location-kind": "PUMP", + "nation": "US", + "state-initial": "NV", + "county-name": "Clark", + "nearest-city": "Sparks", + "horizontal-datum": "WGS84", + "published-longitude": 0, + "published-latitude": 0, + "vertical-datum": "NGVD29", + "elevation": 150, + "map-label": MAP_LABEL, + "bounding-office-id": TEST_OFFICE, + "elevation-units": "m", +} + +PUMP_LOCATION2 = { + "office-id": TEST_OFFICE, + "name": PUMP_LOCATION_ID3, + "latitude": 0, + "longitude": 0, + "active": True, + "public-name": PUBLIC_NAME, + "long-name": LONG_NAME, + "description": DESCRIPTION, + "timezone-name": "UTC", + "location-type": LOCATION_TYPE, + "location-kind": "PUMP", + "nation": "US", + "state-initial": "NV", + "county-name": "Clark", + "nearest-city": "Sparks", + "horizontal-datum": "WGS84", + "published-longitude": 0, + "published-latitude": 0, + "vertical-datum": "NGVD29", + "elevation": 150, + "map-label": MAP_LABEL, + "bounding-office-id": TEST_OFFICE, + "elevation-units": "m", +} + +PUMP_LOCATION3 = { + "office-id": TEST_OFFICE, + "name": PUMP_LOCATION_ID, + "latitude": 0, + "longitude": 0, + "active": True, + "public-name": PUBLIC_NAME, + "long-name": LONG_NAME, + "description": DESCRIPTION, + "timezone-name": "UTC", + "location-type": LOCATION_TYPE, + "location-kind": "PUMP", + "nation": "US", + "state-initial": "NV", + "county-name": "Clark", + "nearest-city": "Sparks", + "horizontal-datum": "WGS84", + "published-longitude": 0, + "published-latitude": 0, + "vertical-datum": "NGVD29", + "elevation": 150, + "map-label": MAP_LABEL, + "bounding-office-id": TEST_OFFICE, + "elevation-units": "m", +} + +WATER_CONTRACT = { + "office-id": TEST_OFFICE, + "water-user": WATER_USER, + "contract-id": {"office-id": TEST_OFFICE, "name": TEST_CONTRACT_ID}, + "contract-type": { + "office-id": TEST_OFFICE, + "display-value": "Test Display Value", + "tooltip": "Test Tooltip", + "active": True, + }, + "contract-effective-date": 158000, + "contract-expiration-date": 167000, + "contracted-storage": 200000.5, + "initial-use-allocation": 15600, + "future-use-allocation": 27800.5, + "storage-units-id": "m3", + "future-use-percent-activated": 15.6, + "total-alloc-percent-activated": 65.2, + "pump-out-location": {"pump-location": PUMP_LOCATION1, "pump-type": "OUT"}, + "pump-out-below-location": {"pump-location": PUMP_LOCATION2, "pump-type": "BELOW"}, + "pump-in-location": {"pump-location": PUMP_LOCATION3, "pump-type": "IN"}, +} + + +def _cleanup(): + wu.delete_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME) + pl.delete_location(PUMP_LOCATION_ID, TEST_OFFICE) + pl.delete_location(PUMP_LOCATION_ID2, TEST_OFFICE) + pl.delete_location(PUMP_LOCATION_ID3, TEST_OFFICE) + pl.delete_location(TEST_PROJECT_ID, TEST_OFFICE) + + +@pytest.fixture(scope="module", autouse=True) +def setup_data(): + _cleanup() + + pl.store_location(PROJECT_LOCATION, False) + wu.create_water_user(WATER_USER, TEST_OFFICE, TEST_PROJECT_ID, False) + pl.store_location(PUMP_LOCATION1, False) + pl.store_location(PUMP_LOCATION2, False) + pl.store_location(PUMP_LOCATION3, False) + + +@pytest.fixture(autouse=True) +def init_session(): + print("Initializing CWMS API session for water contract tests...") + + +def test_store_water_contract(): + wc.create_water_contract( + TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME, WATER_CONTRACT, False + ) + data = wc.get_water_contract( + TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME, TEST_CONTRACT_ID + ) + assert data == WATER_CONTRACT + + +def test_get_water_contract(): + data = wc.get_water_contract( + TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME, TEST_CONTRACT_ID + ) + assert data == WATER_CONTRACT + + +def test_delete_water_contract(): + WATER_CONTRACT2 = WATER_CONTRACT + new_contract_name = "Temporary Contract" + WATER_CONTRACT2["contract-id"]["name"] = new_contract_name + wc.create_water_contract( + TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME, WATER_CONTRACT2, False + ) + data = wc.get_water_contract( + TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME, new_contract_name + ) + assert data == WATER_CONTRACT2 + wc.delete_water_contract( + TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME, new_contract_name + ) + data = wc.get_water_contract( + TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME, new_contract_name + ) + assert data is None + + +def test_get_water_contracts(): + WATER_CONTRACT2 = WATER_CONTRACT + WATER_CONTRACT2["contract-id"]["name"] = "Addendum Contract" + wc.create_water_contract( + TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME, WATER_CONTRACT2, False + ) + data = wc.get_water_contracts(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME) + assert len(data) == 2 + for item in data: + if item["contract-id"]["name"] == TEST_CONTRACT_ID: + assert item == WATER_CONTRACT + elif item["contract-id"]["name"] == "Addendum Contract": + assert item == WATER_CONTRACT2 + else: + assert False, "Unexpected contract found in list" + + +def test_update_water_contract(): + WATER_CONTRACT2 = WATER_CONTRACT + new_contract_name = "Additional Contract" + WATER_CONTRACT2["contract-id"]["name"] = new_contract_name + WATER_CONTRACT2["future-use-percent-activated"] = 225.6 + wc.update_water_contract( + TEST_OFFICE, + TEST_PROJECT_ID, + TEST_ENTITY_NAME, + TEST_CONTRACT_ID, + new_contract_name, + WATER_CONTRACT2, + ) + data = wc.get_water_contract( + TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME, new_contract_name + ) + assert data == WATER_CONTRACT2 diff --git a/tests/cda/projects/water_supply/water_user_test.py b/tests/cda/projects/water_supply/water_user_test.py new file mode 100644 index 00000000..d7282ded --- /dev/null +++ b/tests/cda/projects/water_supply/water_user_test.py @@ -0,0 +1,177 @@ +from datetime import datetime, timedelta, timezone +from unittest.mock import patch + +import pandas as pd +import pandas.testing as pdt +import pytest + +import cwms +import cwms.locations.physical_locations as pl +import cwms.projects.water_supply.water_users as wu + +TEST_OFFICE = "SPK" +TEST_OFFICE2 = "LRL" +TEST_PROJECT_ID = "pytest_wu" +TEST_ENTITY_NAME = "Test User" +TEST_WATER_RIGHT = "Test Water Right" +TEST_ENTITY_NAME2 = "Test User 2" +TEST_ENTITY_NAME3 = "Test User 3" +TEST_ENTITY_NAME4 = "Test User 4" +TEST_ENTITY_NAME5 = "Test User 5" +PUBLIC_NAME = "Test Public Pump Name" +LONG_NAME = "Test Long Name" +LOCATION_TYPE = "Test Location Type" +DESCRIPTION = "Test Description" +MAP_LABEL = "Test Map Label" + +PROJECT_LOCATION = { + "office-id": TEST_OFFICE, + "name": TEST_PROJECT_ID, + "latitude": 0, + "longitude": 0, + "active": True, + "public-name": PUBLIC_NAME, + "long-name": LONG_NAME, + "description": DESCRIPTION, + "timezone-name": "UTC", + "location-type": LOCATION_TYPE, + "location-kind": "PROJECT", + "nation": "US", + "state-initial": "NV", + "county-name": "Clark", + "nearest-city": "Sparks", + "horizontal-datum": "WGS84", + "published-longitude": 0, + "published-latitude": 0, + "vertical-datum": "NGVD29", + "elevation": 150, + "map-label": MAP_LABEL, + "bounding-office-id": TEST_OFFICE, + "elevation-units": "m", +} + + +def _cleanup(): + wu.delete_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME) + wu.delete_water_user(TEST_OFFICE2, TEST_PROJECT_ID, TEST_ENTITY_NAME3) + + pl.delete_location(TEST_PROJECT_ID, TEST_OFFICE) + + +@pytest.fixture(scope="module", autouse=True) +def setup_data(): + _cleanup() + + pl.store_location(PROJECT_LOCATION, False) + + water_user = { + "entity-name": TEST_ENTITY_NAME, + "project-id": {"office-id": TEST_OFFICE, "name": TEST_PROJECT_ID}, + "water-right": TEST_WATER_RIGHT, + } + + water_user2 = { + "entity-name": TEST_ENTITY_NAME3, + "project-id": {"office-id": TEST_OFFICE2, "name": TEST_PROJECT_ID}, + "water-right": TEST_WATER_RIGHT, + } + + wu.create_water_user(water_user, TEST_OFFICE, TEST_PROJECT_ID, False) + wu.create_water_user(water_user2, TEST_OFFICE2, TEST_PROJECT_ID, False) + + +@pytest.fixture(autouse=True) +def init_session(): + print("Initializing CWMS API session for water user tests...") + + +def test_store_water_user(): + water_user = { + "entity-name": TEST_ENTITY_NAME2, + "project-id": {"office-id": TEST_OFFICE, "name": TEST_PROJECT_ID}, + "water-right": TEST_WATER_RIGHT, + } + + wu.create_water_user(water_user, TEST_OFFICE, TEST_PROJECT_ID, False) + data = wu.get_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME2) + assert data["entity-name"] == TEST_ENTITY_NAME2 + assert data["project-id.name"] == TEST_PROJECT_ID + assert data["project-id.office-id"] == TEST_OFFICE + assert data["water-right"] == TEST_WATER_RIGHT + + +def test_get_water_user(): + data = wu.get_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME) + assert data["entity-name"] == TEST_ENTITY_NAME + assert data["project-id.name"] == TEST_PROJECT_ID + assert data["project-id.office-id"] == TEST_OFFICE + assert data["water-right"] == TEST_WATER_RIGHT + + +def test_delete_water_user(): + water_user = { + "entity-name": TEST_ENTITY_NAME4, + "project-id": {"office-id": TEST_OFFICE, "name": TEST_PROJECT_ID}, + "water-right": TEST_WATER_RIGHT, + } + + wu.create_water_user(water_user, TEST_OFFICE, TEST_PROJECT_ID, False) + data = wu.get_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME4) + assert data["entity-name"] == TEST_ENTITY_NAME4 + assert data["project-id.name"] == TEST_PROJECT_ID + assert data["project-id.office-id"] == TEST_OFFICE + assert data["water-right"] == TEST_WATER_RIGHT + wu.delete_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME4) + data = wu.get_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME4) + assert data is None + + +def test_get_water_users(): + data = wu.get_water_users(TEST_OFFICE, TEST_PROJECT_ID) + assert len(data) >= 2 + found_first = False + found_second = False + for value in data: + assert value["project-id.name"] == TEST_PROJECT_ID + assert value["water-right"] == TEST_WATER_RIGHT + if value["entity-name"] == TEST_ENTITY_NAME: + assert value["project-id.office-id"] == TEST_OFFICE + found_first = True + if value["entity-name"] == TEST_ENTITY_NAME3: + assert value["project-id.office-id"] == TEST_OFFICE2 + found_second = True + assert found_first + assert found_second + + +def test_update_water_user(): + water_user = { + "entity-name": TEST_ENTITY_NAME5, + "project-id": {"office-id": TEST_OFFICE, "name": TEST_PROJECT_ID}, + "water-right": TEST_WATER_RIGHT, + } + + wu.create_water_user(water_user, TEST_OFFICE, TEST_PROJECT_ID, False) + data = wu.get_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME5) + assert data["entity-name"] == TEST_ENTITY_NAME5 + assert data["project-id.name"] == TEST_PROJECT_ID + assert data["project-id.office-id"] == TEST_OFFICE + assert data["water-right"] == TEST_WATER_RIGHT + + new_name = "New Water User" + water_rights = "Restricted Water Rights" + + updated_user = { + "entity-name": new_name, + "project-id": {"office-id": TEST_OFFICE, "name": TEST_PROJECT_ID}, + "water-right": water_rights, + } + + wu.update_water_user( + TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME5, updated_user, new_name + ) + data = wu.get_water_user(TEST_OFFICE, TEST_PROJECT_ID, new_name) + assert data["entity-name"] == new_name + assert data["project-id.name"] == TEST_PROJECT_ID + assert data["project-id.office-id"] == TEST_OFFICE + assert data["water-right"] == water_rights