99class CurrentsAPIError (Exception ):
1010 """Raised when the Currents API returns an error response."""
1111
12- def __init__ (self , response ):
12+ def __init__ (self , response , http_status = None ):
13+ if not isinstance (response , dict ):
14+ response = {}
1315 self .response = response
14- self ._status = response . get ( "status" )
16+ self ._http_status = http_status
1517 self ._code = response .get ("code" )
1618 self ._message = response .get ("message" ) or response .get ("msg" )
17- super ().__init__ (self ._message or str (response ))
19+ super ().__init__ (self ._message or ( str (response ) if response else "Unknown API error" ))
1820
1921 @property
2022 def status (self ):
23+ """HTTP status code as int; payload status used only as last resort."""
24+ if self ._http_status is not None :
25+ return self ._http_status
26+ payload_status = self .response .get ("status" )
2127 try :
22- return int (self . _status )
28+ return int (payload_status )
2329 except (TypeError , ValueError ):
24- return self . _status
30+ return payload_status
2531
2632 @property
2733 def code (self ):
@@ -58,17 +64,36 @@ def _get(self, endpoint, params=None):
5864 params = params or {},
5965 )
6066 if r .status_code != requests .codes .ok :
61- raise CurrentsAPIError (r .json ())
62- return r .json ()
67+ raise self ._error_from_response (r )
68+ try :
69+ payload = r .json ()
70+ except ValueError :
71+ raise CurrentsAPIError (
72+ {"message" : "Response body is not valid JSON" },
73+ http_status = r .status_code ,
74+ )
75+ return payload
76+
77+ @staticmethod
78+ def _error_from_response (r ):
79+ try :
80+ payload = r .json ()
81+ except ValueError :
82+ payload = {}
83+ if not isinstance (payload , dict ):
84+ payload = {
85+ "message" : "API returned a non-object error payload" ,
86+ "details" : payload ,
87+ }
88+ return CurrentsAPIError (payload , http_status = r .status_code )
6389
6490 def latest_news (self , language = None ):
6591 params = {}
66- if language :
92+ if language is not None :
6793 if not isinstance (language , str ):
6894 raise ValueError ("language must be a string" )
6995 params ["language" ] = language
7096 return self ._get (self .latest_endpoint , params )
71-
7297 def search (
7398 self ,
7499 language = None ,
@@ -80,32 +105,38 @@ def search(
80105 ):
81106 params = {}
82107
83- if keywords :
108+ if keywords is not None :
84109 if not isinstance (keywords , str ):
85110 raise ValueError ("keywords must be a string" )
111+ if not keywords .strip ():
112+ raise ValueError ("keywords must not be empty" )
86113 params ["keywords" ] = keywords
87114
88- if country :
115+ if country is not None :
89116 if not isinstance (country , str ):
90117 raise ValueError ("country must be a string" )
91118 params ["country" ] = country
92119
93- if language :
120+ if language is not None :
94121 if not isinstance (language , str ):
95122 raise ValueError ("language must be a string" )
96123 params ["language" ] = language
97124
98- if category :
125+ if category is not None :
99126 if not isinstance (category , str ):
100127 raise ValueError ("category must be a string" )
101128 params ["category" ] = category
102129
103- if start_date :
104- date = self ._normalize_date (self ._parse_date (start_date , "start_date" ))
130+ if start_date is not None :
131+ date = self ._normalize_date (
132+ self ._parse_date (start_date , "start_date" ), "start_date"
133+ )
105134 params ["start_date" ] = date .strftime ("%Y-%m-%dT%H:%M:%SZ" )
106135
107- if end_date :
108- date = self ._normalize_date (self ._parse_date (end_date , "end_date" ))
136+ if end_date is not None :
137+ date = self ._normalize_date (
138+ self ._parse_date (end_date , "end_date" ), "end_date"
139+ )
109140 params ["end_date" ] = date .strftime ("%Y-%m-%dT%H:%M:%SZ" )
110141
111142 return self ._get (self .search_endpoint , params )
@@ -124,7 +155,7 @@ def _parse_date(date_value, param_name):
124155 if isinstance (date_value , str ):
125156 try :
126157 return parser .parse (date_value )
127- except (parser . ParserError , OverflowError , ValueError ) as exc :
158+ except (OverflowError , ValueError ) as exc :
128159 raise ValueError (
129160 "{} is not a parsable date: {}" .format (param_name , exc )
130161 ) from exc
@@ -138,7 +169,18 @@ def _parse_date(date_value, param_name):
138169 )
139170
140171 @staticmethod
141- def _normalize_date (date_value ):
142- if isinstance (date_value , datetime .datetime ) and date_value .tzinfo is not None :
172+ def _normalize_date (date_value , param_name ):
173+ if isinstance (date_value , datetime .datetime ):
174+ if date_value .tzinfo is None :
175+ raise ValueError (
176+ "{} datetime must be timezone-aware; attach a tzinfo "
177+ "(naive datetimes are ambiguous and are NOT assumed to be "
178+ "UTC)" .format (param_name )
179+ )
143180 return date_value .astimezone (datetime .timezone .utc )
181+ if isinstance (date_value , datetime .date ):
182+ return datetime .datetime (
183+ date_value .year , date_value .month , date_value .day ,
184+ tzinfo = datetime .timezone .utc ,
185+ )
144186 return date_value
0 commit comments