Skip to content

Commit aa8a7ae

Browse files
authored
Merge pull request #9 from currentslab/agent/0.1.1-error-handling
fix(client): error handling, tz-aware dates, date validation (0.1.1)
2 parents 14649d3 + cfc75c9 commit aa8a7ae

4 files changed

Lines changed: 87 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## 0.1.1
4+
5+
### Fixed
6+
- `CurrentsAPIError.message` now falls back to the API's `msg` key (401
7+
responses carry `msg`, not `message`), and `str(exception)` returns the
8+
human-readable message instead of a raw dict repr.
9+
- `CurrentsAPIError.status` now returns an `int` when the payload carries a
10+
numeric status string, so `e.status == 401` works as expected.
11+
- Timezone-aware `datetime` inputs to `search()` are converted to UTC before
12+
formatting, instead of silently losing their offset while the string gains a
13+
misleading `Z` suffix.
14+
- Unparsable date strings (e.g. `"2026-13-45"`) now raise `ValueError` with a
15+
clear message instead of leaking `dateutil.parser.ParserError`.
16+
317
## 0.1.0
418

519
### Added

currentsapi/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import sys
22

33
__project__ = "currentsapi"
4-
__version__ = "0.1.0"
4+
__version__ = "0.1.1"
55
__repo__ = "https://github.com/currentslab/currentsapi-python"
66

77
from .client import CurrentsAPI

currentsapi/client.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,25 @@ class CurrentsAPIError(Exception):
1111

1212
def __init__(self, response):
1313
self.response = response
14-
super().__init__(str(response))
14+
self._status = response.get("status")
15+
self._code = response.get("code")
16+
self._message = response.get("message") or response.get("msg")
17+
super().__init__(self._message or str(response))
1518

1619
@property
1720
def status(self):
18-
return self.response.get("status")
21+
try:
22+
return int(self._status)
23+
except (TypeError, ValueError):
24+
return self._status
1925

2026
@property
2127
def code(self):
22-
return self.response.get("code")
28+
return self._code
2329

2430
@property
2531
def message(self):
26-
return self.response.get("message")
32+
return self._message
2733

2834

2935
class CurrentsAPI:
@@ -95,11 +101,11 @@ def search(
95101
params["category"] = category
96102

97103
if start_date:
98-
date = self._parse_date(start_date, "start_date")
104+
date = self._normalize_date(self._parse_date(start_date, "start_date"))
99105
params["start_date"] = date.strftime("%Y-%m-%dT%H:%M:%SZ")
100106

101107
if end_date:
102-
date = self._parse_date(end_date, "end_date")
108+
date = self._normalize_date(self._parse_date(end_date, "end_date"))
103109
params["end_date"] = date.strftime("%Y-%m-%dT%H:%M:%SZ")
104110

105111
return self._get(self.search_endpoint, params)
@@ -116,7 +122,12 @@ def available_category(self):
116122
@staticmethod
117123
def _parse_date(date_value, param_name):
118124
if isinstance(date_value, str):
119-
return parser.parse(date_value)
125+
try:
126+
return parser.parse(date_value)
127+
except (parser.ParserError, OverflowError, ValueError) as exc:
128+
raise ValueError(
129+
"{} is not a parsable date: {}".format(param_name, exc)
130+
) from exc
120131
elif isinstance(date_value, datetime.date):
121132
return date_value
122133
else:
@@ -125,3 +136,9 @@ def _parse_date(date_value, param_name):
125136
param_name
126137
)
127138
)
139+
140+
@staticmethod
141+
def _normalize_date(date_value):
142+
if isinstance(date_value, datetime.datetime) and date_value.tzinfo is not None:
143+
return date_value.astimezone(datetime.timezone.utc)
144+
return date_value

tests/test_client.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,54 @@ def test_invalid_start_date(self):
160160
with self.assertRaises(ValueError):
161161
api.search(start_date=123)
162162

163+
@patch("currentsapi.client.requests.get")
164+
def test_api_error_uses_msg_key(self, mock_get):
165+
mock_get.return_value = Mock(
166+
status_code=401,
167+
json=Mock(return_value={"status": "401", "msg": "Invalid token"}),
168+
)
169+
api = CurrentsAPI("key")
170+
with self.assertRaises(CurrentsAPIError) as ctx:
171+
api.latest_news()
172+
self.assertEqual(ctx.exception.status, 401)
173+
self.assertIsNone(ctx.exception.code)
174+
self.assertEqual(ctx.exception.message, "Invalid token")
175+
self.assertEqual(str(ctx.exception), "Invalid token")
176+
177+
@patch("currentsapi.client.requests.get")
178+
def test_api_error_prefers_message_over_msg(self, mock_get):
179+
mock_get.return_value = Mock(
180+
status_code=400,
181+
json=Mock(
182+
return_value={
183+
"status": "400",
184+
"msg": "Bad request",
185+
"code": "INVALID_QUERY",
186+
"message": "Invalid parameters",
187+
}
188+
),
189+
)
190+
api = CurrentsAPI("key")
191+
with self.assertRaises(CurrentsAPIError) as ctx:
192+
api.search(keywords="x", category="nope")
193+
self.assertEqual(ctx.exception.status, 400)
194+
self.assertEqual(ctx.exception.code, "INVALID_QUERY")
195+
self.assertEqual(ctx.exception.message, "Invalid parameters")
196+
self.assertEqual(str(ctx.exception), "Invalid parameters")
197+
198+
@patch("currentsapi.client.requests.get")
199+
def test_tz_aware_datetime_converted_to_utc(self, mock_get):
200+
mock_get.return_value = Mock(status_code=200, json=Mock(return_value={"status": "ok"}))
201+
api = CurrentsAPI("key")
202+
tz = datetime.timezone(datetime.timedelta(hours=8))
203+
api.search(start_date=datetime.datetime(2024, 6, 1, 12, 0, tzinfo=tz))
204+
kwargs = mock_get.call_args.kwargs
205+
self.assertEqual(kwargs["params"]["start_date"], "2024-06-01T04:00:00Z")
206+
207+
def test_impossible_date_string_raises_valueerror(self):
208+
api = CurrentsAPI("key")
209+
with self.assertRaises(ValueError):
210+
api.search(start_date="2026-13-45")
163211

164212
if __name__ == "__main__":
165213
unittest.main()

0 commit comments

Comments
 (0)