Skip to content

Commit 28a3d08

Browse files
authored
Merge branch 'main' into 283-missing-data-is-converted-to-nan-and-cant-be-re-inserted
2 parents ecab560 + 4cc357c commit 28a3d08

5 files changed

Lines changed: 181 additions & 6 deletions

File tree

cwms/catalog/catalog.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ def get_locations_catalog(
6767
"location-kind-like": location_kind_like,
6868
}
6969

70-
response = api.get(endpoint=endpoint, params=params, api_version=2)
70+
response = api.get_with_paging(
71+
endpoint=endpoint, selector="entries", params=params, api_version=2
72+
)
7173
return Data(response, selector="entries")
7274

7375

@@ -131,7 +133,9 @@ def get_timeseries_catalog(
131133
"include-extents": include_extents,
132134
}
133135

134-
response = api.get(endpoint=endpoint, params=params, api_version=2)
136+
response = api.get_with_paging(
137+
endpoint=endpoint, selector="entries", params=params, api_version=2
138+
)
135139
return Data(response, selector="entries")
136140

137141

cwms/users/users.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,66 @@ def get_user_profile() -> dict[str, Any]:
3535
return dict(response)
3636

3737

38+
def filter_users_by_office(data: dict[str, Any], office: str) -> dict[str, Any]:
39+
"""
40+
Filter users JSON to only include users that have roles for the specified office.
41+
Each user's roles dict will only contain the entry for that office.
42+
43+
Args:
44+
data: The full users JSON as a Python dict.
45+
office: The office key to filter by (e.g., 'MVP', 'LRL').
46+
47+
Returns:
48+
A new dict with the same structure, filtered to the specified office.
49+
"""
50+
filtered_users = []
51+
52+
for user in data.get("users", []):
53+
roles = user.get("roles", {})
54+
55+
if office in roles:
56+
# Build a copy of the user with only the target office's roles
57+
filtered_user = {k: v for k, v in user.items() if k != "roles"}
58+
filtered_user["roles"] = {office: roles[office]}
59+
filtered_users.append(filtered_user)
60+
61+
return {
62+
"page": data.get("page"),
63+
"page-size": data.get("page-size"),
64+
"total": len(filtered_users),
65+
"users": filtered_users,
66+
}
67+
68+
3869
def get_users(
3970
office_id: Optional[str] = None,
71+
username_like: Optional[str] = None,
72+
include_roles: Optional[bool] = None,
4073
page: Optional[str] = None,
41-
page_size: Optional[int] = None,
74+
page_size: Optional[int] = 5000,
4275
) -> Data:
4376
"""Retrieve users with optional office and paging filters."""
4477

45-
params = {"office": office_id, "page": page, "page-size": page_size}
78+
endpoint = "users"
79+
params = {
80+
"office": office_id,
81+
"username-like": username_like,
82+
"include-roles": include_roles,
83+
"page": page,
84+
"page-size": page_size,
85+
}
4686
try:
47-
response = api.get("users", params=params, api_version=1)
87+
response = api.get_with_paging(
88+
endpoint=endpoint, selector="users", params=params, api_version=1
89+
)
4890
except api.ApiError as error:
4991
_raise_user_management_error(error, "User list lookup")
92+
93+
# filter by office if office_id is provided since the API does not
94+
# currently support filtering by office on the backend. This is a
95+
# temporary workaround until the API supports office filtering.
96+
if office_id:
97+
response = filter_users_by_office(response, office_id)
5098
return Data(response, selector="users")
5199

52100

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"id": "d9b66ebe",
7+
"metadata": {
8+
"vscode": {
9+
"languageId": "plaintext"
10+
}
11+
},
12+
"outputs": [],
13+
"source": [
14+
"import pandas as pd\n",
15+
"import cwms\n",
16+
"import os\n",
17+
"import logging\n",
18+
"\n",
19+
"\n",
20+
"TS_NAME = \"KEYS.Elev.Inst.1Hour.0.Ccp-Rev\"\n",
21+
"OFFICE = os.getenv(\"OFFICE\", \"SWT\")\n",
22+
"API_KEY_DEV = os.getenv(\"CDA_API_KEY_DEV\")\n",
23+
"API_ROOT_DEV = os.getenv(\"CDA_API_ROOT_DEV\")\n",
24+
"\n",
25+
"logging.basicConfig(\n",
26+
" level=logging.DEBUG, format=\"%(asctime)s - %(levelname)s - %(message)s\"\n",
27+
")\n",
28+
"\n",
29+
"\n",
30+
"if not API_ROOT_DEV:\n",
31+
" logging.error(\n",
32+
" \"CDA_API_KEY environment variable is not set. Please set it to your API key.\"\n",
33+
" )\n",
34+
"\n",
35+
"if not API_KEY_DEV:\n",
36+
" logging.error(\n",
37+
" \"CDA_API_ROOT environment variable is not set. Please set it to your API root URL.\"\n",
38+
" )\n",
39+
"\n",
40+
"logging.debug(\"Starting Script!\")\n",
41+
"\n",
42+
"\n",
43+
"logging.info(f\"Getting timeseries data for {TS_NAME} from office {OFFICE}\")\n",
44+
"\n",
45+
"cwms.init_session()\n",
46+
"ts = cwms.get_timeseries(ts_id=TS_NAME, office_id=OFFICE)\n",
47+
"shifted_df = ts.df.copy()\n",
48+
"\n",
49+
"if isinstance(shifted_df.index, pd.DatetimeIndex):\n",
50+
" shifted_df.index = shifted_df.index + pd.Timedelta(hours=10)\n",
51+
"else:\n",
52+
" shifted_df[\"date-time\"] = pd.to_datetime(shifted_df[\"date-time\"]) + pd.Timedelta(\n",
53+
" hours=10\n",
54+
" )\n",
55+
"\n",
56+
"ts_json = cwms.timeseries_df_to_json(\n",
57+
" data=shifted_df,\n",
58+
" office_id=OFFICE,\n",
59+
" ts_id=TS_NAME,\n",
60+
" units=ts.json.get(\"units\"),\n",
61+
")\n",
62+
"cwms.init_session(api_root=API_ROOT_DEV, api_key=API_KEY_DEV)\n",
63+
"cwms.store_timeseries(data=ts_json)\n",
64+
"\n",
65+
"\n",
66+
"logging.debug(\"Script Done\")\n"
67+
]
68+
}
69+
],
70+
"metadata": {
71+
"language_info": {
72+
"name": "python"
73+
}
74+
},
75+
"nbformat": 4,
76+
"nbformat_minor": 5
77+
}

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ repository = "https://github.com/HydrologicEngineeringCenter/cwms-python"
44

55
version = "1.0.6"
66

7-
87
packages = [
98
{ include = "cwms" },
109
]

tests/cda/users/users_CDA_test.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,53 @@ def test_get_users():
107107
)
108108

109109

110+
def test_get_users_by_office():
111+
# /users?office={office_id}
112+
# Covers filtering the user list by office and verifies the known test account appears in the
113+
# returned page when filtering by the test user's office.
114+
user_name = str(TEST_USER_NAME)
115+
office_id = TEST_OFFICE_ID
116+
users = cwms.get_users(office_id=office_id, page_size=200).json
117+
users_list = users.get("users", [])
118+
assert isinstance(users_list, list)
119+
assert any(
120+
str(u.get("user-name", "")).lower() == user_name.lower() for u in users_list
121+
)
122+
for user in users["users"]:
123+
roles = user["roles"]
124+
assert set(roles.keys()) == {
125+
office_id
126+
}, f"{user['user-name']} has unexpected office keys: {set(roles.keys())}"
127+
128+
129+
def test_get_users_by_office_not_present_in_other_office():
130+
# /users?office={office_id}
131+
# Covers filtering the user list by office and verifies the known test account does not appear in the
132+
# returned page when filtering by a different office.
133+
user_name = str(TEST_USER_NAME)
134+
office_id = "MVP" if TEST_OFFICE_ID != "MVP" else "SPK"
135+
users = cwms.get_users(office_id=office_id, page_size=200)
136+
users_list = users.json.get("users", [])
137+
assert isinstance(users_list, list)
138+
assert not any(
139+
str(u.get("user-name", "")).lower() == user_name.lower() for u in users_list
140+
)
141+
142+
143+
def test_get_users_with_paging():
144+
# /users with paging parameters
145+
# Covers requesting a specific page and page size and verifies the response contains the expected pagination metadata.
146+
page_size = 2
147+
users_with_page = cwms.get_users(page_size=page_size)
148+
users_list = users_with_page.json.get("users", [])
149+
users_without_page = cwms.get_users()
150+
all_users_list = users_without_page.json.get("users", [])
151+
assert isinstance(users_list, list)
152+
assert isinstance(all_users_list, list)
153+
assert len(users_list) >= page_size
154+
assert len(all_users_list) == len(users_list)
155+
156+
110157
def test_store_update_delete_user_roles_roundtrip():
111158
# Round Trip: Covers the role-management lifecycle for an existing user in one office:
112159
# 1. add a role the user does not currently have

0 commit comments

Comments
 (0)