Skip to content

Commit c5e0161

Browse files
author
himanshu
committed
Tighten parameter validation
- size: enforce lower bound (must be in [1, 50]) - raw_query: reject keys with empty values - sentiment_score requires sentiment
1 parent 24598db commit c5e0161

4 files changed

Lines changed: 85 additions & 10 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,6 @@ temp.py
2121
# Claude Code
2222
CLAUDE.md
2323
.claude/
24+
25+
# IDE
26+
.vscode/

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ finally:
5858
| Method | Endpoint | Notes |
5959
|--------|----------|-------|
6060
| `latest_api()` | `/latest` | Real-time news |
61-
| `archive_api()` | `/archive` | Historical news (pass a date range) |
62-
| `sources_api()` | `/sources` | Available news sources (single page) |
61+
| `archive_api()` | `/archive` | Historical news |
62+
| `sources_api()` | `/sources` | Available news sources |
6363
| `crypto_api()` | `/crypto` | Cryptocurrency news |
6464
| `market_api()` | `/market` | Market / financial news |
6565
| `count_api(from_date, to_date)` | `/count` | Aggregate counts |

src/newsdataapi/client.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ def _validate_params(user_params: Mapping[str, Any]) -> dict[str, Any]:
104104
``domain``/``domainurl``/``excludedomain``) are enforced client-side;
105105
setting more than one from any group raises
106106
``NewsdataValidationError`` before the request leaves.
107+
* ``sentiment_score`` requires ``sentiment`` to be set; passing
108+
``sentiment_score`` alone raises ``NewsdataValidationError``.
107109
108110
Raises:
109111
NewsdataValidationError: On any type mismatch, unknown raw_query
@@ -132,6 +134,15 @@ def _validate_params(user_params: Mapping[str, Any]) -> dict[str, Any]:
132134
param=set_in_group[0],
133135
)
134136

137+
if (
138+
user_params.get("sentiment_score") is not None
139+
and user_params.get("sentiment") is None
140+
):
141+
raise NewsdataValidationError(
142+
"sentiment_score requires sentiment to be set",
143+
param="sentiment_score",
144+
)
145+
135146
validated: dict[str, Any] = {}
136147
for param, value in user_params.items():
137148
if value is None or param == "raw_query":
@@ -187,9 +198,9 @@ def _check_int_param(param: str, value: Any) -> None:
187198
f"{param!r} must be an int, got {type(value).__name__}",
188199
param=param,
189200
)
190-
if param == "size" and value > 50:
201+
if param == "size" and (value > 50 or value < 1):
191202
raise NewsdataValidationError(
192-
f"size must be 50 or less (got {value})",
203+
f"size must be between 1 and 50 (got {value})",
193204
param="size",
194205
)
195206

@@ -242,7 +253,12 @@ def _parse_raw_query(raw_query: Any, *, allowed_keys: set[str]) -> dict[str, Any
242253
f"Unknown parameter in raw_query: {key!r}",
243254
param=key,
244255
)
245-
result[normalized] = values[0] if values else ""
256+
if not values or not values[0]:
257+
raise NewsdataValidationError(
258+
f"Parameter {key!r} must have a value",
259+
param=key,
260+
)
261+
result[normalized] = values[0]
246262
return result
247263

248264

tests/test_unit.py

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,23 @@ def test_validate_size_well_above_cap_rejected() -> None:
113113
_validate_params({"size": 1000})
114114

115115

116+
def test_validate_size_at_min_accepted() -> None:
117+
"""size=1 (the minimum) is accepted."""
118+
assert _validate_params({"size": 1}) == {"size": 1}
119+
120+
121+
def test_validate_size_below_min_rejected() -> None:
122+
"""size < 1 is rejected client-side."""
123+
with pytest.raises(NewsdataValidationError) as exc_info:
124+
_validate_params({"size": 0})
125+
assert exc_info.value.param == "size"
126+
127+
128+
def test_validate_size_negative_rejected() -> None:
129+
with pytest.raises(NewsdataValidationError):
130+
_validate_params({"size": -5})
131+
132+
116133
@pytest.mark.parametrize(
117134
("params", "first_conflict"),
118135
[
@@ -176,24 +193,49 @@ def test_validate_no_mutex_when_one_per_group_set() -> None:
176193

177194

178195
def test_validate_float_param_accepts_float() -> None:
179-
assert _validate_params({"sentiment_score": 44.5}) == {"sentiment_score": 44.5}
196+
out = _validate_params({"sentiment": "positive", "sentiment_score": 44.5})
197+
assert out == {"sentiment": "positive", "sentiment_score": 44.5}
180198

181199

182200
def test_validate_float_param_accepts_int() -> None:
183201
"""int is accepted alongside float — urlencode handles the conversion."""
184-
assert _validate_params({"sentiment_score": 50}) == {"sentiment_score": 50}
202+
out = _validate_params({"sentiment": "positive", "sentiment_score": 50})
203+
assert out == {"sentiment": "positive", "sentiment_score": 50}
185204

186205

187206
def test_validate_float_param_rejects_bool() -> None:
188207
"""``bool`` is an ``int`` subclass — must still be rejected for sentiment_score."""
189208
with pytest.raises(NewsdataValidationError) as exc_info:
190-
_validate_params({"sentiment_score": True})
209+
_validate_params({"sentiment": "positive", "sentiment_score": True})
191210
assert exc_info.value.param == "sentiment_score"
211+
assert "must be a number" in str(exc_info.value)
192212

193213

194214
def test_validate_float_param_rejects_str() -> None:
195-
with pytest.raises(NewsdataValidationError):
196-
_validate_params({"sentiment_score": "44.5"})
215+
with pytest.raises(NewsdataValidationError) as exc_info:
216+
_validate_params({"sentiment": "positive", "sentiment_score": "44.5"})
217+
assert exc_info.value.param == "sentiment_score"
218+
assert "must be a number" in str(exc_info.value)
219+
220+
221+
def test_sentiment_score_requires_sentiment() -> None:
222+
"""sentiment_score alone (without sentiment) is rejected."""
223+
with pytest.raises(NewsdataValidationError) as exc_info:
224+
_validate_params({"sentiment_score": 44.5})
225+
assert exc_info.value.param == "sentiment_score"
226+
assert "sentiment" in str(exc_info.value).lower()
227+
228+
229+
def test_sentiment_score_with_sentiment_passes() -> None:
230+
"""sentiment_score paired with sentiment passes through."""
231+
out = _validate_params({"sentiment": "positive", "sentiment_score": 44.5})
232+
assert out == {"sentiment": "positive", "sentiment_score": 44.5}
233+
234+
235+
def test_sentiment_alone_passes() -> None:
236+
"""sentiment without sentiment_score is allowed (regression)."""
237+
out = _validate_params({"sentiment": "positive"})
238+
assert out == {"sentiment": "positive"}
197239

198240

199241
def test_validate_unknown_param_passes_through() -> None:
@@ -312,6 +354,20 @@ def test_raw_query_apikey_case_insensitive_drop() -> None:
312354
assert out == {"q": "news"}
313355

314356

357+
def test_raw_query_rejects_key_with_empty_value() -> None:
358+
"""A key with an empty value (e.g., q=) is rejected."""
359+
with pytest.raises(NewsdataValidationError) as exc_info:
360+
_parse_raw_query("q=", allowed_keys={"q", "raw_query"})
361+
assert exc_info.value.param == "q"
362+
363+
364+
def test_raw_query_rejects_key_with_no_value() -> None:
365+
"""A bare key with no '=' (e.g., q&country=us) is rejected."""
366+
with pytest.raises(NewsdataValidationError) as exc_info:
367+
_parse_raw_query("q&country=us", allowed_keys={"q", "country", "raw_query"})
368+
assert exc_info.value.param == "q"
369+
370+
315371
# ===========================================================================
316372
# Retry-After parsing
317373
# ===========================================================================

0 commit comments

Comments
 (0)