From 61a41b60c872ccfa09560a08ace849cc7e2a12f2 Mon Sep 17 00:00:00 2001 From: zack-rma Date: Thu, 20 Aug 2026 17:26:13 -0700 Subject: [PATCH 01/11] Initial implementation of water user, water contract, and lock endpoints --- cwms/locks/locks.py | 196 ++++++++++++++ cwms/projects/water_supply/water_contracts.py | 250 ++++++++++++++++++ cwms/projects/water_supply/water_users.py | 209 +++++++++++++++ 3 files changed, 655 insertions(+) create mode 100644 cwms/locks/locks.py create mode 100644 cwms/projects/water_supply/water_contracts.py create mode 100644 cwms/projects/water_supply/water_users.py diff --git a/cwms/locks/locks.py b/cwms/locks/locks.py new file mode 100644 index 00000000..e397740f --- /dev/null +++ b/cwms/locks/locks.py @@ -0,0 +1,196 @@ +# 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} + + return api.get(endpoint, params, api_version=1) + + +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} + + return api.get(endpoint, params, api_version=1) + + +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..c18919ae --- /dev/null +++ b/cwms/projects/water_supply/water_users.py @@ -0,0 +1,209 @@ +# 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, water_user]): + 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} + + return 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}" + + response = api.delete(endpoint, api_version=1) + return Data(response) + + +def update_water_user( + office_id: str, project_id: str, water_user: str, data: JSON, name: str +) -> Data: + """ + 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 + ------- + 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, 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} + + response = api.patch(endpoint, data, params, api_version=1) + return Data(response) From 90f2c68cc1cbdfd9be4c5742c3ba2620ea7b751c Mon Sep 17 00:00:00 2001 From: zack-rma Date: Fri, 21 Aug 2026 10:36:24 -0700 Subject: [PATCH 02/11] Added integration tests. Added new endpoints to API. --- cwms/__init__.py | 3 + tests/cda/locks/locks_test.py | 9 + .../projects/water_supply/accounting_test.py | 257 ++++++++++++++++++ .../water_supply/water_contract_test.py | 227 ++++++++++++++++ .../projects/water_supply/water_user_test.py | 141 ++++++++++ 5 files changed, 637 insertions(+) create mode 100644 tests/cda/locks/locks_test.py create mode 100644 tests/cda/projects/water_supply/accounting_test.py create mode 100644 tests/cda/projects/water_supply/water_contract_test.py create mode 100644 tests/cda/projects/water_supply/water_user_test.py diff --git a/cwms/__init__.py b/cwms/__init__.py index 7879399d..fb967a9a 100644 --- a/cwms/__init__.py +++ b/cwms/__init__.py @@ -12,6 +12,7 @@ from cwms.locations.location_groups import * from cwms.locations.lookups 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 * @@ -20,6 +21,8 @@ from cwms.projects.projects import * from cwms.projects.water_supply.accounting import * from cwms.properties.properties import * +from cwms.projects.water_supply.water_contracts import * +from cwms.projects.water_supply.water_users import * from cwms.ratings.ratings import * from cwms.ratings.ratings_spec import * from cwms.ratings.ratings_template import * 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/accounting_test.py b/tests/cda/projects/water_supply/accounting_test.py new file mode 100644 index 00000000..86d5bebb --- /dev/null +++ b/tests/cda/projects/water_supply/accounting_test.py @@ -0,0 +1,257 @@ +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, +} + +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) + + +@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) + 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..d6625482 --- /dev/null +++ b/tests/cda/projects/water_supply/water_contract_test.py @@ -0,0 +1,227 @@ +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, +} + +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) + + +@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) + + +@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..97110588 --- /dev/null +++ b/tests/cda/projects/water_supply/water_user_test.py @@ -0,0 +1,141 @@ +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.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" + + +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) + + +@pytest.fixture(scope="module", autouse=True) +def setup_data(): + _cleanup() + + 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 From bb69d62a43c80d60469b9ef6508b7e7f5e5b62b7 Mon Sep 17 00:00:00 2001 From: zack-rma Date: Fri, 21 Aug 2026 11:14:44 -0700 Subject: [PATCH 03/11] Bug fixes for booleans, endpoint responses --- cwms/locks/locks.py | 6 ++++-- cwms/projects/water_supply/water_users.py | 8 +++----- tests/cda/projects/water_supply/accounting_test.py | 6 +++--- tests/cda/projects/water_supply/water_contract_test.py | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cwms/locks/locks.py b/cwms/locks/locks.py index e397740f..bfeee67e 100644 --- a/cwms/locks/locks.py +++ b/cwms/locks/locks.py @@ -45,7 +45,8 @@ def get_lock(name: str, office_id: str, unit: Optional[str] = "SI") -> Data: params = {"office": office_id, "unit": unit} - return api.get(endpoint, params, api_version=1) + response = api.get(endpoint, params, api_version=1) + return Data(response) def get_locks(office_id: str, project_id: str) -> Data: @@ -82,7 +83,8 @@ def get_locks(office_id: str, project_id: str) -> Data: params = {"office": office_id, "project": project_id} - return api.get(endpoint, params, api_version=1) + response = api.get(endpoint, params, api_version=1) + return Data(response) def create_lock(data: JSON, fail_if_exists: bool = True) -> None: diff --git a/cwms/projects/water_supply/water_users.py b/cwms/projects/water_supply/water_users.py index c18919ae..9e54d744 100644 --- a/cwms/projects/water_supply/water_users.py +++ b/cwms/projects/water_supply/water_users.py @@ -72,7 +72,7 @@ def get_water_users(office_id: str, project_id: str) -> Data: ServerError If a 500-level error occurs. """ - if not all([office_id, project_id, water_user]): + 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" @@ -158,8 +158,7 @@ def delete_water_user(office_id: str, project_id: str, water_user: str) -> None: endpoint = f"projects/{office_id}/{project_id}/water-user/{water_user}" - response = api.delete(endpoint, api_version=1) - return Data(response) + api.delete(endpoint, api_version=1) def update_water_user( @@ -205,5 +204,4 @@ def update_water_user( endpoint = f"projects/{office_id}/{project_id}/water-user/{water_user}" params = {"name": name} - response = api.patch(endpoint, data, params, api_version=1) - return Data(response) + api.patch(endpoint, data, params, api_version=1) diff --git a/tests/cda/projects/water_supply/accounting_test.py b/tests/cda/projects/water_supply/accounting_test.py index 86d5bebb..08ab808b 100644 --- a/tests/cda/projects/water_supply/accounting_test.py +++ b/tests/cda/projects/water_supply/accounting_test.py @@ -36,7 +36,7 @@ "name": PUMP_LOCATION_ID2, "latitude": 0, "longitude": 0, - "active": true, + "active": True, "public-name": PUBLIC_NAME, "long-name": LONG_NAME, "description": DESCRIPTION, @@ -62,7 +62,7 @@ "name": PUMP_LOCATION_ID3, "latitude": 0, "longitude": 0, - "active": true, + "active": True, "public-name": PUBLIC_NAME, "long-name": LONG_NAME, "description": DESCRIPTION, @@ -88,7 +88,7 @@ "name": PUMP_LOCATION_ID, "latitude": 0, "longitude": 0, - "active": true, + "active": True, "public-name": PUBLIC_NAME, "long-name": LONG_NAME, "description": DESCRIPTION, diff --git a/tests/cda/projects/water_supply/water_contract_test.py b/tests/cda/projects/water_supply/water_contract_test.py index d6625482..4841589a 100644 --- a/tests/cda/projects/water_supply/water_contract_test.py +++ b/tests/cda/projects/water_supply/water_contract_test.py @@ -35,7 +35,7 @@ "name": PUMP_LOCATION_ID2, "latitude": 0, "longitude": 0, - "active": true, + "active": True, "public-name": PUBLIC_NAME, "long-name": LONG_NAME, "description": DESCRIPTION, @@ -61,7 +61,7 @@ "name": PUMP_LOCATION_ID3, "latitude": 0, "longitude": 0, - "active": true, + "active": True, "public-name": PUBLIC_NAME, "long-name": LONG_NAME, "description": DESCRIPTION, @@ -87,7 +87,7 @@ "name": PUMP_LOCATION_ID, "latitude": 0, "longitude": 0, - "active": true, + "active": True, "public-name": PUBLIC_NAME, "long-name": LONG_NAME, "description": DESCRIPTION, From 06e8d3ddeb16d7db47270e256e23bdcdfaa7e7ec Mon Sep 17 00:00:00 2001 From: zack-rma Date: Fri, 21 Aug 2026 13:22:52 -0700 Subject: [PATCH 04/11] Added missing location, fixed returned data. --- cwms/projects/water_supply/water_users.py | 7 ++--- ...unting_test.py => pump_accounting_test.py} | 28 +++++++++++++++++ .../water_supply/water_contract_test.py | 28 +++++++++++++++++ .../projects/water_supply/water_user_test.py | 31 +++++++++++++++++++ 4 files changed, 90 insertions(+), 4 deletions(-) rename tests/cda/projects/water_supply/{accounting_test.py => pump_accounting_test.py} (90%) diff --git a/cwms/projects/water_supply/water_users.py b/cwms/projects/water_supply/water_users.py index 9e54d744..5b87c25e 100644 --- a/cwms/projects/water_supply/water_users.py +++ b/cwms/projects/water_supply/water_users.py @@ -122,7 +122,7 @@ def create_water_user( endpoint = f"projects/{office_id}/{project_id}/water-user" params = {"fail-if-exists": fail_if_exists} - return api.post(endpoint, data, params, api_version=1) + api.post(endpoint, data, params, api_version=1) def delete_water_user(office_id: str, project_id: str, water_user: str) -> None: @@ -163,7 +163,7 @@ def delete_water_user(office_id: str, project_id: str, water_user: str) -> None: def update_water_user( office_id: str, project_id: str, water_user: str, data: JSON, name: str -) -> Data: +) -> None: """ Updates a water user in CWMS. @@ -182,8 +182,7 @@ def update_water_user( Returns ------- - Data - The JSON response from CWMS Data API wrapped in a Data object. + None Raises ------ diff --git a/tests/cda/projects/water_supply/accounting_test.py b/tests/cda/projects/water_supply/pump_accounting_test.py similarity index 90% rename from tests/cda/projects/water_supply/accounting_test.py rename to tests/cda/projects/water_supply/pump_accounting_test.py index 08ab808b..8422041b 100644 --- a/tests/cda/projects/water_supply/accounting_test.py +++ b/tests/cda/projects/water_supply/pump_accounting_test.py @@ -31,6 +31,32 @@ "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, @@ -222,6 +248,7 @@ def _cleanup(): 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) @@ -232,6 +259,7 @@ def setup_data(): 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 ) diff --git a/tests/cda/projects/water_supply/water_contract_test.py b/tests/cda/projects/water_supply/water_contract_test.py index 4841589a..49fd229a 100644 --- a/tests/cda/projects/water_supply/water_contract_test.py +++ b/tests/cda/projects/water_supply/water_contract_test.py @@ -30,6 +30,32 @@ "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, @@ -137,12 +163,14 @@ def _cleanup(): 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) diff --git a/tests/cda/projects/water_supply/water_user_test.py b/tests/cda/projects/water_supply/water_user_test.py index 97110588..4e5e64b8 100644 --- a/tests/cda/projects/water_supply/water_user_test.py +++ b/tests/cda/projects/water_supply/water_user_test.py @@ -6,6 +6,7 @@ import pytest import cwms +import cwms.locations.physical_locations as pl import cwms.projects.water_supply.water_users as wu TEST_OFFICE = "SPK" @@ -18,16 +19,46 @@ TEST_ENTITY_NAME4 = "Test User 4" TEST_ENTITY_NAME5 = "Test User 5" +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}, From aa73e630fd972c56abe1d97c0b9dab29dd6fb4ce Mon Sep 17 00:00:00 2001 From: zack-rma Date: Fri, 21 Aug 2026 14:07:16 -0700 Subject: [PATCH 05/11] Added missing variables --- tests/cda/projects/water_supply/water_user_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/cda/projects/water_supply/water_user_test.py b/tests/cda/projects/water_supply/water_user_test.py index 4e5e64b8..d7282ded 100644 --- a/tests/cda/projects/water_supply/water_user_test.py +++ b/tests/cda/projects/water_supply/water_user_test.py @@ -18,6 +18,11 @@ 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, From db5f2ba534543457761e8a61d52b830a221824d4 Mon Sep 17 00:00:00 2001 From: zack-rma Date: Tue, 25 Aug 2026 17:09:01 -0700 Subject: [PATCH 06/11] Added lock tests. Updated cleanup handling for other tests --- tests/cda/locks/locks_test.py | 152 ++++++++++++++++++ .../water_supply/pump_accounting_test.py | 5 +- .../water_supply/water_contract_test.py | 5 +- .../projects/water_supply/water_user_test.py | 5 +- 4 files changed, 164 insertions(+), 3 deletions(-) diff --git a/tests/cda/locks/locks_test.py b/tests/cda/locks/locks_test.py index 4bcda24e..a24deb7f 100644 --- a/tests/cda/locks/locks_test.py +++ b/tests/cda/locks/locks_test.py @@ -6,4 +6,156 @@ import pytest import cwms +import cwms.locations.physical_locations as pl import cwms.locks.locks as lk + +TEST_OFFICE = "SPK" +TEST_PROJECT_ID = "BIGH" +LOCK_ID = "pytest-lock-123" + +TEST_LOCK_LOCATION = { + "office-id": TEST_OFFICE, + "name": LOCK_ID, + "latitude": 38.5, + "longitude": -121.7, + "active": True, + "public-name": LOCK_ID, + "long-name": "TEST_LOCATION", + "description": "for testing", + "timezone-name": "UTC", + "location-type": "SITE", + "location-kind": "LOCK", + "nation": "US", + "state-initial": "CA", + "county-name": "Sacramento", + "horizontal-datum": "NGVD29", + "published-longitude": 38.5, + "published-latitude": -121.7, + "elevation": 10.0, + "elevation-units": "m", + "bounding-office-id": TEST_OFFICE, + "nearest-city": "Davis", +} + +TEST_PROJECT_LOCATION = { + "name": TEST_PROJECT_ID, + "latitude": 40.0, + "longitude": -105.0, + "elevation": 1000.0, + "horizontal-datum": "NAD83", + "vertical-datum": "NAVD88", + "office-id": TEST_OFFICE, + "location-type": "TESTING", + "location-kind": "PROJECT", + "public-name": "Test Location", + "long-name": "A pytest-generated location", + "timezone-name": "America/Chicago", + "nation": "US", +} + +TEST_LOCK = { + "project-id": {"office-id": TEST_OFFICE, "name": "PROJECT"}, + "location": TEST_LOCK_LOCATION, + "chamber-type": { + "display-value": "Single Chamber", + "tooltip": "A lock gate system with a single chamber", + "active": True, + "office-id": "CWMS", + }, + "lock-width": 50.0, + "lock-length": 50.0, + "normal-lock-lift": 10.0, + "volume-per-lockage": 10.0, + "minimum-draft": 25.5, + "maximum-lock-lift": 25.6, + "length-units": "ft", + "volume-units": "ft3", + "elevation-units": "ft", + "high-water-upper-pool-location-level": { + "level-value": 15.96, + "level-link": "/locks/TEST_LOCATION2.Elev-Closure.Inst.0.High Water Upper Pool?office=SPK", + }, + "high-water-lower-pool-location-level": { + "level-value": 22.7, + "level-link": "/locks/TEST_LOCATION2.Elev-Closure.Inst.0.High Water Lower Pool?office=SPK", + }, + "low-water-upper-pool-location-level": { + "level-value": 18.0, + "level-link": "/locks/TEST_LOCATION2.Elev-Closure.Inst.0.Low Water Upper Pool?office=SPK", + }, + "low-water-lower-pool-location-level": { + "level-value": 55.0, + "level-link": "/locks/TEST_LOCATION2.Elev-Closure.Inst.0.Low Water Lower Pool?office=SPK", + }, + "high-water-upper-pool-warning-level": 2.0, + "high-water-lower-pool-warning-level": 2.0, +} + + +def _cleanup(): + lk.delete_lock(LOCK_ID, TEST_OFFICE) + pl.delete_location(TEST_PROJECT_ID, TEST_OFFICE) + + +@pytest.fixture(scope="module", autouse=True) +def setup_data(): + try: + _cleanup() + except Exception: + pass + pl.store_location(TEST_PROJECT_LOCATION, False) + lk.create_lock(TEST_LOCK, False) + + +@pytest.fixture(autouse=True) +def init_session(): + print("Initializing CWMS API session for water supply accounting tests...") + + +def test_get_locks(): + locks = lk.get_locks() + assert locks is not None + + +def test_get_lock(): + lock = lk.get_lock(LOCK_ID, TEST_OFFICE) + assert lock is not None + assert lock.json == TEST_LOCK + + +def test_create_lock(): + test_lock2 = TEST_LOCK + new_loc = "pytest-lock-879" + test_lock2["location"]["name"] = new_loc + lk.create_lock(test_lock2, False) + lock = lk.get_lock(new_loc, TEST_OFFICE) + assert lock is not None + assert lock.json == test_lock2 + + +def test_delete_lock(): + test_lock2 = TEST_LOCK + new_loc = "pytest-lock-456" + test_lock2["location"]["name"] = new_loc + lk.create_lock(test_lock2, False) + lock = lk.get_lock(new_loc, TEST_OFFICE) + assert lock is not None + assert lock.json == test_lock2 + lk.delete_lock(new_loc, TEST_OFFICE) + lock = lk.get_lock(new_loc, TEST_OFFICE) + assert lock is None + + +def test_update_lock(): + test_lock2 = TEST_LOCK + new_loc = "pytest-lock-881" + test_lock2["location"]["name"] = new_loc + test_lock2["location"]["description"] = "pytest-lock-description" + lk.create_lock(test_lock2, False) + lock = lk.get_lock(new_loc, TEST_OFFICE) + assert lock is not None + assert lock.json == test_lock2 + lk.update_lock(LOCK_ID, TEST_OFFICE, new_loc) + lock = lk.get_lock(LOCK_ID, TEST_OFFICE) + assert lock is not None + assert lock.json == test_lock2 diff --git a/tests/cda/projects/water_supply/pump_accounting_test.py b/tests/cda/projects/water_supply/pump_accounting_test.py index 8422041b..5a985068 100644 --- a/tests/cda/projects/water_supply/pump_accounting_test.py +++ b/tests/cda/projects/water_supply/pump_accounting_test.py @@ -253,7 +253,10 @@ def _cleanup(): @pytest.fixture(scope="module", autouse=True) def setup_data(): - _cleanup() + try: + _cleanup() + except Exception: + pass wu.create_water_user(WATER_USER, TEST_OFFICE, TEST_PROJECT_ID, False) pl.store_location(PUMP_LOCATION1, False) diff --git a/tests/cda/projects/water_supply/water_contract_test.py b/tests/cda/projects/water_supply/water_contract_test.py index 49fd229a..05760d65 100644 --- a/tests/cda/projects/water_supply/water_contract_test.py +++ b/tests/cda/projects/water_supply/water_contract_test.py @@ -168,7 +168,10 @@ def _cleanup(): @pytest.fixture(scope="module", autouse=True) def setup_data(): - _cleanup() + try: + _cleanup() + except Exception: + pass pl.store_location(PROJECT_LOCATION, False) wu.create_water_user(WATER_USER, TEST_OFFICE, TEST_PROJECT_ID, False) diff --git a/tests/cda/projects/water_supply/water_user_test.py b/tests/cda/projects/water_supply/water_user_test.py index d7282ded..12bb8b7e 100644 --- a/tests/cda/projects/water_supply/water_user_test.py +++ b/tests/cda/projects/water_supply/water_user_test.py @@ -60,7 +60,10 @@ def _cleanup(): @pytest.fixture(scope="module", autouse=True) def setup_data(): - _cleanup() + try: + _cleanup() + except Exception: + pass pl.store_location(PROJECT_LOCATION, False) From 570f237a5664636a6b38aeba601b16ae1daf77c7 Mon Sep 17 00:00:00 2001 From: zack-rma Date: Wed, 26 Aug 2026 11:24:53 -0700 Subject: [PATCH 07/11] Fixed api versioning for projects endpoints. Updated tests for water user and contracts. --- cwms/projects/projects.py | 8 +- cwms/projects/water_supply/water_contracts.py | 13 +- .../water_supply/pump_accounting_test.py | 4 +- .../water_supply/water_contract_test.py | 50 ++++-- .../projects/water_supply/water_user_test.py | 151 ++++++++++++++++-- 5 files changed, 185 insertions(+), 41 deletions(-) diff --git a/cwms/projects/projects.py b/cwms/projects/projects.py index ef47cdb8..2937143e 100644 --- a/cwms/projects/projects.py +++ b/cwms/projects/projects.py @@ -87,7 +87,7 @@ def get_projects( "page": page, "page-size": page_size, } - response = api.get(endpoint, params) + response = api.get(endpoint, params, api_version=1) return Data(response) @@ -208,7 +208,7 @@ def rename_project(office_id: str, old_name: str, new_name: str) -> None: endpoint = f"projects/{old_name}" params = {"office": office_id, "name": new_name} - api.patch(endpoint=endpoint, params=params) + api.patch(endpoint=endpoint, params=params, api_version=1) def store_project(data: JSON, fail_if_exists: Optional[bool] = True) -> None: @@ -243,7 +243,7 @@ def store_project(data: JSON, fail_if_exists: Optional[bool] = True) -> None: endpoint = "projects" params = {"fail-if-exists": fail_if_exists} - api.post(endpoint, data, params) + api.post(endpoint, data, params, api_version=1) def status_update( @@ -306,4 +306,4 @@ def status_update( "begin": (begin.isoformat() if begin else None), "end": (end.isoformat() if end else None), } - api.post(endpoint, None, params) + api.post(endpoint, None, params, api_version=1) diff --git a/cwms/projects/water_supply/water_contracts.py b/cwms/projects/water_supply/water_contracts.py index 6d660701..311ab7fb 100644 --- a/cwms/projects/water_supply/water_contracts.py +++ b/cwms/projects/water_supply/water_contracts.py @@ -90,8 +90,6 @@ def get_water_contracts(office_id: str, project_id: str, water_user: str) -> Dat def create_water_contract( - office_id: str, - project_id: str, water_user: str, data: JSON, fail_if_exists: bool = True, @@ -102,10 +100,6 @@ def create_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 @@ -132,12 +126,13 @@ def create_water_contract( 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 water_user: + raise ValueError("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" + # Note the office ID and project ID are not used by CDA + endpoint = f"projects/{water_user}/{water_user}/water-user/{water_user}/contracts" params = { "fail-if-exists": fail_if_exists, "ignore-nulls": ignore_nulls, diff --git a/tests/cda/projects/water_supply/pump_accounting_test.py b/tests/cda/projects/water_supply/pump_accounting_test.py index 5a985068..167dc97e 100644 --- a/tests/cda/projects/water_supply/pump_accounting_test.py +++ b/tests/cda/projects/water_supply/pump_accounting_test.py @@ -263,9 +263,7 @@ def setup_data(): 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 - ) + wc.create_water_contract(TEST_ENTITY_NAME, WATER_CONTRACT, False) @pytest.fixture(autouse=True) diff --git a/tests/cda/projects/water_supply/water_contract_test.py b/tests/cda/projects/water_supply/water_contract_test.py index 05760d65..1dabc43f 100644 --- a/tests/cda/projects/water_supply/water_contract_test.py +++ b/tests/cda/projects/water_supply/water_contract_test.py @@ -7,6 +7,7 @@ import cwms import cwms.locations.physical_locations as pl +import cwms.projects.projects as proj import cwms.projects.water_supply.water_contracts as wc import cwms.projects.water_supply.water_users as wu @@ -56,6 +57,39 @@ "elevation-units": "m", } +PROJECT = { + "location": { + "office-id": TEST_OFFICE, + "name": TEST_PROJECT_ID, + "timezone-name": "UTC", + }, + "federal-cost": 100.0, + "non-federal-cost": 50.0, + "cost-year": 1717282800000, + "cost-unit": "$", + "federal-o-and-m-cost": 10.0, + "non-federal-o-and-m-cost": 5.0, + "authorizing-law": "Authorizing Law", + "project-owner": "Project Owner", + "hydropower-desc": "Hydropower Description", + "sedimentation-desc": "Sedimentation Description", + "downstream-urban-desc": "Downstream Urban Description", + "bank-full-capacity-desc": "Bank Full Capacity Description", + "pump-back-location": { + "office-id": "SPK", + "name": PUMP_LOCATION_ID, + "timezone-name": "UTC", + }, + "near-gage-location": { + "office-id": "SPK", + "name": PUMP_LOCATION_ID2, + "timezone-name": "UTC", + }, + "yield-time-frame-start": 1717282800000, + "yield-time-frame-end": 1717308000000, + "project-remarks": "Remarks", +} + PUMP_LOCATION1 = { "office-id": TEST_OFFICE, "name": PUMP_LOCATION_ID2, @@ -160,6 +194,7 @@ def _cleanup(): wu.delete_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME) + proj.delete_project(TEST_PROJECT_ID, TEST_OFFICE) pl.delete_location(PUMP_LOCATION_ID, TEST_OFFICE) pl.delete_location(PUMP_LOCATION_ID2, TEST_OFFICE) pl.delete_location(PUMP_LOCATION_ID3, TEST_OFFICE) @@ -174,10 +209,11 @@ def setup_data(): pass 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) + proj.store_project(PROJECT, False) + wu.create_water_user(WATER_USER, TEST_OFFICE, TEST_PROJECT_ID, False) @pytest.fixture(autouse=True) @@ -186,9 +222,7 @@ def init_session(): def test_store_water_contract(): - wc.create_water_contract( - TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME, WATER_CONTRACT, False - ) + wc.create_water_contract(TEST_ENTITY_NAME, WATER_CONTRACT, False) data = wc.get_water_contract( TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME, TEST_CONTRACT_ID ) @@ -206,9 +240,7 @@ 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 - ) + wc.create_water_contract(TEST_ENTITY_NAME, WATER_CONTRACT2, False) data = wc.get_water_contract( TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME, new_contract_name ) @@ -225,9 +257,7 @@ def test_delete_water_contract(): 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 - ) + wc.create_water_contract(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: diff --git a/tests/cda/projects/water_supply/water_user_test.py b/tests/cda/projects/water_supply/water_user_test.py index 12bb8b7e..30c93acd 100644 --- a/tests/cda/projects/water_supply/water_user_test.py +++ b/tests/cda/projects/water_supply/water_user_test.py @@ -7,6 +7,7 @@ import cwms import cwms.locations.physical_locations as pl +import cwms.projects.projects as proj import cwms.projects.water_supply.water_users as wu TEST_OFFICE = "SPK" @@ -23,6 +24,9 @@ LOCATION_TYPE = "Test Location Type" DESCRIPTION = "Test Description" MAP_LABEL = "Test Map Label" +PUMP_LOCATION_ID = "Sac River-Pump 1" +PUMP_LOCATION_ID2 = "Sac River-Pump 2" +PUMP_LOCATION_ID3 = "Sac River-Pump 3" PROJECT_LOCATION = { "office-id": TEST_OFFICE, @@ -50,11 +54,122 @@ "elevation-units": "m", } +PROJECT = { + "location": { + "office-id": TEST_OFFICE, + "name": TEST_PROJECT_ID, + "timezone-name": "UTC", + }, + "federal-cost": 100.0, + "non-federal-cost": 50.0, + "cost-year": 1717282800000, + "cost-unit": "$", + "federal-o-and-m-cost": 10.0, + "non-federal-o-and-m-cost": 5.0, + "authorizing-law": "Authorizing Law", + "project-owner": "Project Owner", + "hydropower-desc": "Hydropower Description", + "sedimentation-desc": "Sedimentation Description", + "downstream-urban-desc": "Downstream Urban Description", + "bank-full-capacity-desc": "Bank Full Capacity Description", + "pump-back-location": { + "office-id": "SPK", + "name": PUMP_LOCATION_ID, + "timezone-name": "UTC", + }, + "near-gage-location": { + "office-id": "SPK", + "name": PUMP_LOCATION_ID2, + "timezone-name": "UTC", + }, + "yield-time-frame-start": 1717282800000, + "yield-time-frame-end": 1717308000000, + "project-remarks": "Remarks", +} + +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", +} + 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) - + proj.delete_project(TEST_PROJECT_ID, TEST_OFFICE) pl.delete_location(TEST_PROJECT_ID, TEST_OFFICE) @@ -66,6 +181,7 @@ def setup_data(): pass pl.store_location(PROJECT_LOCATION, False) + proj.store_project(PROJECT, False) water_user = { "entity-name": TEST_ENTITY_NAME, @@ -97,17 +213,19 @@ def test_store_water_user(): 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) + data = data.json 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["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) + data = data.json 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["project-id"]["name"] == TEST_PROJECT_ID + assert data["project-id"]["office-id"] == TEST_OFFICE assert data["water-right"] == TEST_WATER_RIGHT @@ -120,9 +238,10 @@ def test_delete_water_user(): 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) + data = data.json 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["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) @@ -134,14 +253,14 @@ def test_get_water_users(): assert len(data) >= 2 found_first = False found_second = False - for value in data: - assert value["project-id.name"] == TEST_PROJECT_ID + for value in data.json: + 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 + 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 + assert value["project-id"]["office-id"] == TEST_OFFICE2 found_second = True assert found_first assert found_second @@ -156,9 +275,10 @@ def test_update_water_user(): 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) + data = data.json 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["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" @@ -174,7 +294,8 @@ def test_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) + data = data.json assert data["entity-name"] == new_name - assert data["project-id.name"] == TEST_PROJECT_ID - assert data["project-id.office-id"] == TEST_OFFICE + assert data["project-id"]["name"] == TEST_PROJECT_ID + assert data["project-id"]["office-id"] == TEST_OFFICE assert data["water-right"] == water_rights From 3aa070144f88922a439db18ad4e7e03f47791c9b Mon Sep 17 00:00:00 2001 From: zack-rma Date: Wed, 26 Aug 2026 13:34:21 -0700 Subject: [PATCH 08/11] Water user bug fixes --- cwms/projects/water_supply/water_users.py | 2 +- .../cda/projects/water_supply/water_user_test.py | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/cwms/projects/water_supply/water_users.py b/cwms/projects/water_supply/water_users.py index 5b87c25e..650aea24 100644 --- a/cwms/projects/water_supply/water_users.py +++ b/cwms/projects/water_supply/water_users.py @@ -75,7 +75,7 @@ def get_water_users(office_id: str, project_id: str) -> Data: 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" + endpoint = f"projects/{office_id}/{project_id}/water-user" response = api.get(endpoint, api_version=1) return Data(response) diff --git a/tests/cda/projects/water_supply/water_user_test.py b/tests/cda/projects/water_supply/water_user_test.py index 30c93acd..03015407 100644 --- a/tests/cda/projects/water_supply/water_user_test.py +++ b/tests/cda/projects/water_supply/water_user_test.py @@ -11,7 +11,6 @@ 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" @@ -168,7 +167,7 @@ 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) + wu.delete_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME3) proj.delete_project(TEST_PROJECT_ID, TEST_OFFICE) pl.delete_location(TEST_PROJECT_ID, TEST_OFFICE) @@ -191,12 +190,12 @@ def setup_data(): water_user2 = { "entity-name": TEST_ENTITY_NAME3, - "project-id": {"office-id": TEST_OFFICE2, "name": TEST_PROJECT_ID}, + "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) - wu.create_water_user(water_user2, TEST_OFFICE2, TEST_PROJECT_ID, False) + wu.create_water_user(water_user2, TEST_OFFICE, TEST_PROJECT_ID, False) @pytest.fixture(autouse=True) @@ -244,23 +243,21 @@ def test_delete_water_user(): 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 + with pytest.raises(cwms.ApiError): + wu.get_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME4) def test_get_water_users(): data = wu.get_water_users(TEST_OFFICE, TEST_PROJECT_ID) - assert len(data) >= 2 + assert len(data.json) >= 2 found_first = False found_second = False for value in data.json: 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 From ff50b54cbf0234d8e8b2013d0b115f07f2b24ff9 Mon Sep 17 00:00:00 2001 From: zack-rma Date: Mon, 31 Aug 2026 10:34:30 -0700 Subject: [PATCH 09/11] Add lookup handling to contract test. Remove unnecessary parameter from lookup deletion. --- cwms/locations/lookups.py | 10 ++++----- tests/cda/locations/lookup_test.py | 2 +- .../water_supply/water_contract_test.py | 21 ++++++++++++------- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/cwms/locations/lookups.py b/cwms/locations/lookups.py index 405f291c..53298662 100644 --- a/cwms/locations/lookups.py +++ b/cwms/locations/lookups.py @@ -123,14 +123,12 @@ def update_lookup(data: JSON, category: str, prefix: str) -> None: api.patch(endpoint, data, params, api_version=1) -def delete_lookup(name: str, category: str, prefix: str, office_id: str) -> None: +def delete_lookup(category: str, prefix: str, office_id: str) -> None: """ Deletes a specified lookup entry. Parameters ---------- - name : str - Specifies the location type to delete. category : str Specifies the category id of the lookup type to be deleted. prefix : str @@ -153,9 +151,9 @@ def delete_lookup(name: str, category: str, prefix: str, office_id: str) -> None ServerError If a 500 range error code response is returned from the server. """ - if not all([name, category, prefix, office_id]): - raise ValueError("Name, Category, Prefix, and Office ID must be specified") + if not all([category, prefix, office_id]): + raise ValueError("Category, Prefix, and Office ID must be specified") - endpoint = f"{ENDPOINT}/{name}" + endpoint = f"{ENDPOINT}/{category}" params = {"category": category, "prefix": prefix, "office": office_id} api.delete(endpoint, params, api_version=1) diff --git a/tests/cda/locations/lookup_test.py b/tests/cda/locations/lookup_test.py index 40ba6b7c..7481e452 100644 --- a/tests/cda/locations/lookup_test.py +++ b/tests/cda/locations/lookup_test.py @@ -110,7 +110,7 @@ def test_delete_lookup(): } lookups.create_lookup(data, CATEGORY, PREFIX) - lookups.delete_lookup(DISPLAY_VALUE, CATEGORY, PREFIX, office) + lookups.delete_lookup(CATEGORY, PREFIX, office) result = lookups.get_all_lookups(CATEGORY, PREFIX, office) found = False diff --git a/tests/cda/projects/water_supply/water_contract_test.py b/tests/cda/projects/water_supply/water_contract_test.py index 1dabc43f..f8a90721 100644 --- a/tests/cda/projects/water_supply/water_contract_test.py +++ b/tests/cda/projects/water_supply/water_contract_test.py @@ -6,6 +6,7 @@ import pytest import cwms +import cwms.locations.lookups as lk import cwms.locations.physical_locations as pl import cwms.projects.projects as proj import cwms.projects.water_supply.water_contracts as wc @@ -24,6 +25,8 @@ LOCATION_TYPE = "Test Location Type" DESCRIPTION = "Test Description" MAP_LABEL = "Test Map Label" +LOOKUP_CATEGORY = "AT_WS_CONTRACT_TYPE" +LOOKUP_PREFIX = "WS_CONTRACT_TYPE" WATER_USER = { "entity-name": TEST_ENTITY_NAME, @@ -168,16 +171,18 @@ "elevation-units": "m", } +CONTRACT_LOOKUP = { + "office-id": TEST_OFFICE, + "display-value": "Test Display Value", + "tooltip": "Test Tooltip", + "active": True, +} + 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-type": CONTRACT_LOOKUP, "contract-effective-date": 158000, "contract-expiration-date": 167000, "contracted-storage": 200000.5, @@ -199,6 +204,7 @@ def _cleanup(): pl.delete_location(PUMP_LOCATION_ID2, TEST_OFFICE) pl.delete_location(PUMP_LOCATION_ID3, TEST_OFFICE) pl.delete_location(TEST_PROJECT_ID, TEST_OFFICE) + lk.delete_lookup(CONTRACT_LOOKUP, LOOKUP_CATEGORY, LOOKUP_PREFIX, TEST_OFFICE) @pytest.fixture(scope="module", autouse=True) @@ -214,6 +220,7 @@ def setup_data(): pl.store_location(PUMP_LOCATION3, False) proj.store_project(PROJECT, False) wu.create_water_user(WATER_USER, TEST_OFFICE, TEST_PROJECT_ID, False) + lk.create_lookup(CONTRACT_LOOKUP, LOOKUP_CATEGORY, LOOKUP_PREFIX) @pytest.fixture(autouse=True) @@ -266,7 +273,7 @@ def test_get_water_contracts(): elif item["contract-id"]["name"] == "Addendum Contract": assert item == WATER_CONTRACT2 else: - assert False, "Unexpected contract found in list" + pytest.fail("Unexpected contract found in list") def test_update_water_contract(): From 8b3a2ce3a642d666aca755f1cb2228d7588d7208 Mon Sep 17 00:00:00 2001 From: zack-rma Date: Mon, 31 Aug 2026 12:06:19 -0700 Subject: [PATCH 10/11] Updated locks and lock tests. Fixed imports. --- cwms/__init__.py | 4 +- cwms/locks/locks.py | 2 +- tests/cda/locks/locks_test.py | 169 +++++++++++++++++++++++++++------- 3 files changed, 139 insertions(+), 36 deletions(-) diff --git a/cwms/__init__.py b/cwms/__init__.py index fb967a9a..7c480b9f 100644 --- a/cwms/__init__.py +++ b/cwms/__init__.py @@ -12,7 +12,7 @@ from cwms.locations.location_groups import * from cwms.locations.lookups import * from cwms.locations.physical_locations import * -from cwms.locks import * +from cwms.locks.locks import * from cwms.measurements.measurements import * from cwms.outlets.outlets import * from cwms.outlets.virtual_outlets import * @@ -20,9 +20,9 @@ from cwms.projects.project_locks import * from cwms.projects.projects import * from cwms.projects.water_supply.accounting import * -from cwms.properties.properties 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 * from cwms.ratings.ratings_template import * diff --git a/cwms/locks/locks.py b/cwms/locks/locks.py index bfeee67e..e66e1318 100644 --- a/cwms/locks/locks.py +++ b/cwms/locks/locks.py @@ -81,7 +81,7 @@ def get_locks(office_id: str, project_id: str) -> Data: endpoint = "projects/locks" - params = {"office": office_id, "project": project_id} + params = {"office": office_id, "project-id": project_id} response = api.get(endpoint, params, api_version=1) return Data(response) diff --git a/tests/cda/locks/locks_test.py b/tests/cda/locks/locks_test.py index a24deb7f..2bca5ea8 100644 --- a/tests/cda/locks/locks_test.py +++ b/tests/cda/locks/locks_test.py @@ -8,10 +8,18 @@ import cwms import cwms.locations.physical_locations as pl import cwms.locks.locks as lk +import cwms.projects.projects as proj TEST_OFFICE = "SPK" TEST_PROJECT_ID = "BIGH" -LOCK_ID = "pytest-lock-123" +LOCK_ID = "pytest-lock123" +PUMP_LOCATION_ID = "Sac River-Pump 1" +PUMP_LOCATION_ID2 = "Sac River-Pump 2" +PUBLIC_NAME = "Test Public Pump Name" +LONG_NAME = "Test Long Name" +LOCATION_TYPE = "Test Location Type" +DESCRIPTION = "Test Description" +MAP_LABEL = "Test Map Label" TEST_LOCK_LOCATION = { "office-id": TEST_OFFICE, @@ -54,7 +62,7 @@ } TEST_LOCK = { - "project-id": {"office-id": TEST_OFFICE, "name": "PROJECT"}, + "project-id": {"office-id": TEST_OFFICE, "name": TEST_PROJECT_ID}, "location": TEST_LOCK_LOCATION, "chamber-type": { "display-value": "Single Chamber", @@ -68,33 +76,105 @@ "volume-per-lockage": 10.0, "minimum-draft": 25.5, "maximum-lock-lift": 25.6, - "length-units": "ft", - "volume-units": "ft3", - "elevation-units": "ft", - "high-water-upper-pool-location-level": { - "level-value": 15.96, - "level-link": "/locks/TEST_LOCATION2.Elev-Closure.Inst.0.High Water Upper Pool?office=SPK", - }, - "high-water-lower-pool-location-level": { - "level-value": 22.7, - "level-link": "/locks/TEST_LOCATION2.Elev-Closure.Inst.0.High Water Lower Pool?office=SPK", + "length-units": "m", + "volume-units": "m3", + "elevation-units": "m", + "high-water-upper-pool-warning-level": 2.0, + "high-water-lower-pool-warning-level": 2.0, +} + +PUMP_LOCATION1 = { + "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", +} + +PUMP_LOCATION2 = { + "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", +} + +PROJECT = { + "location": { + "office-id": TEST_OFFICE, + "name": TEST_PROJECT_ID, + "timezone-name": "UTC", }, - "low-water-upper-pool-location-level": { - "level-value": 18.0, - "level-link": "/locks/TEST_LOCATION2.Elev-Closure.Inst.0.Low Water Upper Pool?office=SPK", + "federal-cost": 100.0, + "non-federal-cost": 50.0, + "cost-year": 1717282800000, + "cost-unit": "$", + "federal-o-and-m-cost": 10.0, + "non-federal-o-and-m-cost": 5.0, + "authorizing-law": "Authorizing Law", + "project-owner": "Project Owner", + "hydropower-desc": "Hydropower Description", + "sedimentation-desc": "Sedimentation Description", + "downstream-urban-desc": "Downstream Urban Description", + "bank-full-capacity-desc": "Bank Full Capacity Description", + "pump-back-location": { + "office-id": TEST_OFFICE, + "name": PUMP_LOCATION_ID, + "timezone-name": "UTC", }, - "low-water-lower-pool-location-level": { - "level-value": 55.0, - "level-link": "/locks/TEST_LOCATION2.Elev-Closure.Inst.0.Low Water Lower Pool?office=SPK", + "near-gage-location": { + "office-id": TEST_OFFICE, + "name": PUMP_LOCATION_ID2, + "timezone-name": "UTC", }, - "high-water-upper-pool-warning-level": 2.0, - "high-water-lower-pool-warning-level": 2.0, + "yield-time-frame-start": 1717282800000, + "yield-time-frame-end": 1717308000000, + "project-remarks": "Remarks", } def _cleanup(): lk.delete_lock(LOCK_ID, TEST_OFFICE) + pl.delete_location(PUMP_LOCATION_ID, TEST_OFFICE) + pl.delete_location(PUMP_LOCATION_ID2, TEST_OFFICE) pl.delete_location(TEST_PROJECT_ID, TEST_OFFICE) + pl.delete_location(LOCK_ID, TEST_OFFICE) @pytest.fixture(scope="module", autouse=True) @@ -103,7 +183,11 @@ def setup_data(): _cleanup() except Exception: pass + pl.store_location(PUMP_LOCATION1, False) + pl.store_location(PUMP_LOCATION2, False) + pl.store_location(TEST_LOCK_LOCATION, False) pl.store_location(TEST_PROJECT_LOCATION, False) + proj.store_project(PROJECT, False) lk.create_lock(TEST_LOCK, False) @@ -113,49 +197,68 @@ def init_session(): def test_get_locks(): - locks = lk.get_locks() + locks = lk.get_locks(TEST_OFFICE, TEST_PROJECT_ID) assert locks is not None def test_get_lock(): lock = lk.get_lock(LOCK_ID, TEST_OFFICE) assert lock is not None - assert lock.json == TEST_LOCK + lock = lock.json + assert lock["lock-width"] == TEST_LOCK["lock-width"] + assert lock["location"]["name"] == TEST_LOCK["location"]["name"] + assert lock["project-id"] == TEST_LOCK["project-id"] def test_create_lock(): test_lock2 = TEST_LOCK - new_loc = "pytest-lock-879" + new_loc = "pytest-lock879" test_lock2["location"]["name"] = new_loc lk.create_lock(test_lock2, False) lock = lk.get_lock(new_loc, TEST_OFFICE) assert lock is not None - assert lock.json == test_lock2 + lock = lock.json + assert lock["lock-width"] == test_lock2["lock-width"] + assert lock["location"]["name"] == test_lock2["location"]["name"] + assert lock["project-id"] == test_lock2["project-id"] def test_delete_lock(): test_lock2 = TEST_LOCK - new_loc = "pytest-lock-456" + new_loc = "pytest-lock456" test_lock2["location"]["name"] = new_loc lk.create_lock(test_lock2, False) lock = lk.get_lock(new_loc, TEST_OFFICE) assert lock is not None - assert lock.json == test_lock2 + lock = lock.json + assert lock["lock-width"] == test_lock2["lock-width"] + assert lock["location"]["name"] == test_lock2["location"]["name"] + assert lock["project-id"] == test_lock2["project-id"] lk.delete_lock(new_loc, TEST_OFFICE) - lock = lk.get_lock(new_loc, TEST_OFFICE) - assert lock is None + try: + lock = lk.get_lock(new_loc, TEST_OFFICE) + except Exception: + found = False + assert not found def test_update_lock(): test_lock2 = TEST_LOCK - new_loc = "pytest-lock-881" + new_loc = "pytest-lock881" test_lock2["location"]["name"] = new_loc test_lock2["location"]["description"] = "pytest-lock-description" lk.create_lock(test_lock2, False) lock = lk.get_lock(new_loc, TEST_OFFICE) assert lock is not None - assert lock.json == test_lock2 - lk.update_lock(LOCK_ID, TEST_OFFICE, new_loc) - lock = lk.get_lock(LOCK_ID, TEST_OFFICE) + lock = lock.json + assert lock["lock-width"] == test_lock2["lock-width"] + assert lock["location"]["name"] == test_lock2["location"]["name"] + assert lock["project-id"] == test_lock2["project-id"] + updated_loc = "pytest-lock996" + lk.update_lock(LOCK_ID, TEST_OFFICE, updated_loc) + lock = lk.get_lock(updated_loc, TEST_OFFICE) assert lock is not None - assert lock.json == test_lock2 + lock = lock.json + assert lock["lock-width"] == test_lock2["lock-width"] + assert updated_loc == test_lock2["location"]["name"] + assert lock["project-id"] == test_lock2["project-id"] From 21f492a86faa7716f2bbc4cd0b217485861bd795 Mon Sep 17 00:00:00 2001 From: zack-rma Date: Mon, 31 Aug 2026 14:03:26 -0700 Subject: [PATCH 11/11] Updated water user test --- .../projects/water_supply/water_user_test.py | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/tests/cda/projects/water_supply/water_user_test.py b/tests/cda/projects/water_supply/water_user_test.py index 03015407..ed3ff48f 100644 --- a/tests/cda/projects/water_supply/water_user_test.py +++ b/tests/cda/projects/water_supply/water_user_test.py @@ -18,6 +18,7 @@ TEST_ENTITY_NAME3 = "Test User 3" TEST_ENTITY_NAME4 = "Test User 4" TEST_ENTITY_NAME5 = "Test User 5" +TEST_ENTITY_NAME5 = "California DWR" PUBLIC_NAME = "Test Public Pump Name" LONG_NAME = "Test Long Name" LOCATION_TYPE = "Test Location Type" @@ -166,10 +167,38 @@ def _cleanup(): - wu.delete_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME) - wu.delete_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME3) - proj.delete_project(TEST_PROJECT_ID, TEST_OFFICE) - pl.delete_location(TEST_PROJECT_ID, TEST_OFFICE) + try: + wu.delete_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME) + except Exception: + pass + try: + wu.delete_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME3) + except Exception: + pass + try: + wu.delete_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME2) + except Exception: + pass + try: + wu.delete_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME4) + except Exception: + pass + try: + wu.delete_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME5) + except Exception: + pass + try: + wu.delete_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME6) + except Exception: + pass + try: + proj.delete_project(TEST_PROJECT_ID, TEST_OFFICE) + except Exception: + pass + try: + pl.delete_location(TEST_PROJECT_ID, TEST_OFFICE) + except Exception: + pass @pytest.fixture(scope="module", autouse=True) @@ -278,21 +307,20 @@ def test_update_water_user(): 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, + "entity-name": TEST_ENTITY_NAME6, "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 + TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME5, updated_user, TEST_ENTITY_NAME6 ) - data = wu.get_water_user(TEST_OFFICE, TEST_PROJECT_ID, new_name) + data = wu.get_water_user(TEST_OFFICE, TEST_PROJECT_ID, TEST_ENTITY_NAME6) data = data.json - assert data["entity-name"] == new_name + assert data["entity-name"] == TEST_ENTITY_NAME6 assert data["project-id"]["name"] == TEST_PROJECT_ID assert data["project-id"]["office-id"] == TEST_OFFICE assert data["water-right"] == water_rights