Skip to content

Commit 127b857

Browse files
authored
Http Error Hint Additions + Blob Test Fixes (#246)
* Add 401 and ensure status code and hint are always returned for errors * Patch bump * Forgot to install black precommit locally on this system * Make 401 clear and give response text * Correct for empty response/ 400s * Handle excel response process * Ensure correct mime_type is sent even if OS does not have it in guess_types * Correct issue for tests with static json response in hint in favor of getattr
1 parent 22ce790 commit 127b857

File tree

3 files changed

+67
-19
lines changed

3 files changed

+67
-19
lines changed

cwms/api.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import base64
3030
import json
3131
import logging
32+
from http import HTTPStatus
3233
from json import JSONDecodeError
3334
from typing import Any, Optional, cast
3435

@@ -72,9 +73,9 @@ class InvalidVersion(Exception):
7273
class ApiError(Exception):
7374
"""CWMS Data Api Error.
7475
75-
This class is a light wrapper around a `requests.Response` object. Its primary purpose
76-
is to generate an error message that includes the request URL and provide additional
77-
information to the user to help them resolve the error.
76+
Light wrapper around a response-like object (e.g., requests.Response or a
77+
test stub with url, status_code, reason, and content attributes). Produces
78+
a concise, single-line error message with an optional hint.
7879
"""
7980

8081
def __init__(self, response: Response):
@@ -91,23 +92,37 @@ def __str__(self) -> str:
9192
message += "."
9293

9394
# Add additional context to help the user resolve the issue.
94-
if hint := self.hint():
95+
hint = self.hint()
96+
if hint:
9597
message += f" {hint}"
9698

97-
if content := self.response.content:
98-
message += f" {content.decode('utf8')}"
99+
# Optional content (decoded if bytes)
100+
content = getattr(self.response, "content", None)
101+
if content:
102+
if isinstance(content, bytes):
103+
try:
104+
text = content.decode("utf-8", errors="replace")
105+
except Exception:
106+
text = repr(content)
107+
else:
108+
text = str(content)
109+
message += f" {text}"
99110

100111
return message
101112

102113
def hint(self) -> str:
103-
"""Return a message with additional information on how to resolve the error."""
114+
"""Return a short hint based on HTTP status code."""
115+
status = getattr(self.response, "status_code", None)
104116

105-
if self.response.status_code == 400:
117+
if status == 429:
118+
return "Too many requests made."
119+
if status == 400:
106120
return "Check that your parameters are correct."
107-
elif self.response.status_code == 404:
121+
if status == 404:
108122
return "May be the result of an empty query."
109-
else:
110-
return ""
123+
124+
# No hint for other codes
125+
return ""
111126

112127

113128
def init_session(
@@ -227,6 +242,12 @@ def _process_response(response: Response) -> Any:
227242
return response.text
228243
if content_type.startswith("image/"):
229244
return base64.b64encode(response.content).decode("utf-8")
245+
# Handle excel content types
246+
if content_type in [
247+
"application/vnd.ms-excel",
248+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
249+
]:
250+
return response.content
230251
# Fallback for remaining content types
231252
return response.content.decode("utf-8")
232253
except JSONDecodeError as error:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "cwms-python"
33
repository = "https://github.com/HydrologicEngineeringCenter/cwms-python"
44

5-
version = "1.0.0"
5+
version = "1.0.1"
66

77

88
packages = [

tests/cda/blobs/blob_CDA_test.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,20 @@ def _find_blob_row(office: str, blob_id: str) -> Optional[pd.Series]:
5959

6060

6161
def test_store_blob_excel():
62+
# Create an empty file with the excel extension
6263
excel_file_path = Path(__file__).parent.parent / "resources" / "blob_test.xlsx"
6364
with open(excel_file_path, "rb") as f:
6465
file_data = f.read()
65-
mime_type, _ = mimetypes.guess_type(excel_file_path)
66+
67+
# Get the file extension and decide which type to use if xlsx or xlx
68+
ext = excel_file_path.suffix.lower()
69+
mime_type = mimetypes.guess_type(excel_file_path.name)[0]
70+
# Some linux systems may not have the excel mimetypes registered
71+
if ext == ".xlsx":
72+
mime_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
73+
elif ext == ".xls":
74+
mime_type = "application/vnd.ms-excel"
75+
6676
excel_blob_id = "TEST_BLOB_EXCEL"
6777
payload = {
6878
"office-id": TEST_OFFICE,
@@ -72,12 +82,16 @@ def test_store_blob_excel():
7282
"value": base64.b64encode(file_data).decode("utf-8"),
7383
}
7484
blobs.store_blobs(data=payload)
75-
try:
76-
row = _find_blob_row(TEST_OFFICE, excel_blob_id)
77-
assert row is not None, "Stored blob not found in listing"
78-
finally:
79-
# Cleanup excel
80-
blobs.delete_blob(blob_id=excel_blob_id, office_id=TEST_OFFICE)
85+
row = _find_blob_row(TEST_OFFICE, excel_blob_id)
86+
assert row is not None, "Stored blob not found in listing"
87+
88+
89+
def test_get_excel_blob():
90+
# Retrieve the excel blob stored in the previous test
91+
excel_blob_id = "TEST_BLOB_EXCEL"
92+
content = blobs.get_blob(office_id=TEST_OFFICE, blob_id=excel_blob_id)
93+
assert content is not None, "Failed to retrieve excel blob"
94+
assert len(content) > 0, "Excel blob content is empty"
8195

8296

8397
def test_store_blob():
@@ -133,3 +147,16 @@ def test_update_blob():
133147
# Verify new content
134148
content = blobs.get_blob(office_id=TEST_OFFICE, blob_id=TEST_BLOB_UPDATED_ID)
135149
assert TEST_TEXT_UPDATED in content
150+
151+
152+
def test_delete_blobs():
153+
# Delete the test blob
154+
blobs.delete_blob(office_id=TEST_OFFICE, blob_id=TEST_BLOB_ID)
155+
blobs.delete_blob(office_id=TEST_OFFICE, blob_id="TEST_BLOB_EXCEL")
156+
157+
# Confirm deletion via listing
158+
row = _find_blob_row(TEST_OFFICE, TEST_BLOB_ID)
159+
assert row is None, "Blob still found after deletion"
160+
161+
row = _find_blob_row(TEST_OFFICE, "TEST_BLOB_EXCEL")
162+
assert row is None, "Excel blob still found after deletion"

0 commit comments

Comments
 (0)