newsdataapi 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/PKG-INFO +1 -1
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/src/newsdataapi/__init__.py +1 -1
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/src/newsdataapi/client.py +35 -3
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/src/newsdataapi/websocket.py +2 -0
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/tests/test_unit.py +89 -1
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/.github/workflows/ci.yml +0 -0
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/.github/workflows/publish.yml +0 -0
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/.gitignore +0 -0
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/.python-version +0 -0
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/LICENSE +0 -0
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/README.md +0 -0
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/examples/basic_usage.py +0 -0
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/examples/websocket_usage.py +0 -0
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/newsdata-logo.png +0 -0
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/pyproject.toml +0 -0
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/src/newsdataapi/constants.py +0 -0
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/src/newsdataapi/csv_export.py +0 -0
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/src/newsdataapi/exceptions.py +0 -0
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/src/newsdataapi/py.typed +0 -0
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/tests/__init__.py +0 -0
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/tests/conftest.py +0 -0
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/tests/test_integration.py +0 -0
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/tests/test_websocket.py +0 -0
- {newsdataapi-0.3.0 → newsdataapi-0.3.2}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: newsdataapi
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Official Python client (SDK) for the Newsdata.io News API — fetch real-time, historical, crypto, and stock-market news via REST, with validation, retries, pagination, and typed exceptions.
|
|
5
5
|
Project-URL: Homepage, https://newsdata.io
|
|
6
6
|
Project-URL: Documentation, https://newsdata.io/documentation
|
|
@@ -47,6 +47,7 @@ _STRING_PARAMS = frozenset(
|
|
|
47
47
|
"domain",
|
|
48
48
|
"domainurl",
|
|
49
49
|
"excludedomain",
|
|
50
|
+
"excludedomainurl",
|
|
50
51
|
"timezone",
|
|
51
52
|
"from_date",
|
|
52
53
|
"to_date",
|
|
@@ -81,9 +82,11 @@ _MUTEX_GROUPS: tuple[tuple[str, ...], ...] = (
|
|
|
81
82
|
("country", "excludecountry"),
|
|
82
83
|
("category", "excludecategory"),
|
|
83
84
|
("language", "excludelanguage"),
|
|
84
|
-
("domain", "domainurl", "excludedomain"),
|
|
85
|
+
("domain", "domainurl", "excludedomain", "excludedomainurl"),
|
|
85
86
|
)
|
|
86
87
|
|
|
88
|
+
_QUOTA_EXHAUSTED_CODES = frozenset({"ApiKeyLimitExceeded", "ApiLimitExceeded"})
|
|
89
|
+
|
|
87
90
|
|
|
88
91
|
def _validate_params(user_params: Mapping[str, Any]) -> dict[str, Any]:
|
|
89
92
|
"""Validate and normalize user-provided endpoint parameters.
|
|
@@ -101,7 +104,8 @@ def _validate_params(user_params: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
101
104
|
* Server-side mutex groups (``q``/``qInTitle``/``qInMeta``,
|
|
102
105
|
``country``/``excludecountry``, ``category``/``excludecategory``,
|
|
103
106
|
``language``/``excludelanguage``, and
|
|
104
|
-
``domain``/``domainurl``/``excludedomain``) are
|
|
107
|
+
``domain``/``domainurl``/``excludedomain``/``excludedomainurl``) are
|
|
108
|
+
enforced client-side;
|
|
105
109
|
setting more than one from any group raises
|
|
106
110
|
``NewsdataValidationError`` before the request leaves.
|
|
107
111
|
* ``sentiment_score`` requires ``sentiment`` to be set; passing
|
|
@@ -298,6 +302,17 @@ def _parse_retry_after(value: str | None) -> int | None:
|
|
|
298
302
|
return None
|
|
299
303
|
|
|
300
304
|
|
|
305
|
+
def _extract_error_code(body: Any) -> str | None:
|
|
306
|
+
"""Return the API error code from an error response body, if present."""
|
|
307
|
+
if isinstance(body, dict):
|
|
308
|
+
results = body.get("results")
|
|
309
|
+
if isinstance(results, dict):
|
|
310
|
+
code = results.get("code")
|
|
311
|
+
if isinstance(code, str):
|
|
312
|
+
return code
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
|
|
301
316
|
def _redact_url(url: str, param: str = "apikey") -> str:
|
|
302
317
|
"""Return ``url`` with the value of ``param`` replaced by ``REDACTED``.
|
|
303
318
|
|
|
@@ -467,6 +482,7 @@ class NewsDataApiClient:
|
|
|
467
482
|
domain: str | list[str] | None = None,
|
|
468
483
|
domainurl: str | list[str] | None = None,
|
|
469
484
|
excludedomain: str | list[str] | None = None,
|
|
485
|
+
excludedomainurl: str | list[str] | None = None,
|
|
470
486
|
prioritydomain: str | None = None,
|
|
471
487
|
timeframe: int | str | None = None,
|
|
472
488
|
timezone: str | None = None,
|
|
@@ -515,6 +531,7 @@ class NewsDataApiClient:
|
|
|
515
531
|
"domain": domain,
|
|
516
532
|
"domainurl": domainurl,
|
|
517
533
|
"excludedomain": excludedomain,
|
|
534
|
+
"excludedomainurl": excludedomainurl,
|
|
518
535
|
"prioritydomain": prioritydomain,
|
|
519
536
|
"timeframe": str(timeframe) if timeframe is not None else None,
|
|
520
537
|
"timezone": timezone,
|
|
@@ -558,6 +575,7 @@ class NewsDataApiClient:
|
|
|
558
575
|
domain: str | list[str] | None = None,
|
|
559
576
|
domainurl: str | list[str] | None = None,
|
|
560
577
|
excludedomain: str | list[str] | None = None,
|
|
578
|
+
excludedomainurl: str | list[str] | None = None,
|
|
561
579
|
prioritydomain: str | None = None,
|
|
562
580
|
timezone: str | None = None,
|
|
563
581
|
size: int | None = None,
|
|
@@ -604,6 +622,7 @@ class NewsDataApiClient:
|
|
|
604
622
|
"domain": domain,
|
|
605
623
|
"domainurl": domainurl,
|
|
606
624
|
"excludedomain": excludedomain,
|
|
625
|
+
"excludedomainurl": excludedomainurl,
|
|
607
626
|
"prioritydomain": prioritydomain,
|
|
608
627
|
"timezone": timezone,
|
|
609
628
|
"size": size,
|
|
@@ -671,6 +690,7 @@ class NewsDataApiClient:
|
|
|
671
690
|
domain: str | list[str] | None = None,
|
|
672
691
|
domainurl: str | list[str] | None = None,
|
|
673
692
|
excludedomain: str | list[str] | None = None,
|
|
693
|
+
excludedomainurl: str | list[str] | None = None,
|
|
674
694
|
prioritydomain: str | None = None,
|
|
675
695
|
timeframe: int | str | None = None,
|
|
676
696
|
timezone: str | None = None,
|
|
@@ -710,6 +730,7 @@ class NewsDataApiClient:
|
|
|
710
730
|
"domain": domain,
|
|
711
731
|
"domainurl": domainurl,
|
|
712
732
|
"excludedomain": excludedomain,
|
|
733
|
+
"excludedomainurl": excludedomainurl,
|
|
713
734
|
"prioritydomain": prioritydomain,
|
|
714
735
|
"timeframe": str(timeframe) if timeframe is not None else None,
|
|
715
736
|
"timezone": timezone,
|
|
@@ -749,6 +770,7 @@ class NewsDataApiClient:
|
|
|
749
770
|
domain: str | list[str] | None = None,
|
|
750
771
|
domainurl: str | list[str] | None = None,
|
|
751
772
|
excludedomain: str | list[str] | None = None,
|
|
773
|
+
excludedomainurl: str | list[str] | None = None,
|
|
752
774
|
language: str | list[str] | None = None,
|
|
753
775
|
excludelanguage: str | list[str] | None = None,
|
|
754
776
|
prioritydomain: str | None = None,
|
|
@@ -794,6 +816,7 @@ class NewsDataApiClient:
|
|
|
794
816
|
"domain": domain,
|
|
795
817
|
"domainurl": domainurl,
|
|
796
818
|
"excludedomain": excludedomain,
|
|
819
|
+
"excludedomainurl": excludedomainurl,
|
|
797
820
|
"language": language,
|
|
798
821
|
"excludelanguage": excludelanguage,
|
|
799
822
|
"prioritydomain": prioritydomain,
|
|
@@ -841,6 +864,7 @@ class NewsDataApiClient:
|
|
|
841
864
|
domain: str | list[str] | None = None,
|
|
842
865
|
domainurl: str | list[str] | None = None,
|
|
843
866
|
excludedomain: str | list[str] | None = None,
|
|
867
|
+
excludedomainurl: str | list[str] | None = None,
|
|
844
868
|
full_content: bool | None = None,
|
|
845
869
|
image: bool | None = None,
|
|
846
870
|
video: bool | None = None,
|
|
@@ -888,6 +912,7 @@ class NewsDataApiClient:
|
|
|
888
912
|
"domain": domain,
|
|
889
913
|
"domainurl": domainurl,
|
|
890
914
|
"excludedomain": excludedomain,
|
|
915
|
+
"excludedomainurl": excludedomainurl,
|
|
891
916
|
"full_content": full_content,
|
|
892
917
|
"image": image,
|
|
893
918
|
"video": video,
|
|
@@ -927,6 +952,7 @@ class NewsDataApiClient:
|
|
|
927
952
|
domain: str | list[str] | None = None,
|
|
928
953
|
domainurl: str | list[str] | None = None,
|
|
929
954
|
excludedomain: str | list[str] | None = None,
|
|
955
|
+
excludedomainurl: str | list[str] | None = None,
|
|
930
956
|
full_content: bool | None = None,
|
|
931
957
|
image: bool | None = None,
|
|
932
958
|
video: bool | None = None,
|
|
@@ -965,6 +991,7 @@ class NewsDataApiClient:
|
|
|
965
991
|
"domain": domain,
|
|
966
992
|
"domainurl": domainurl,
|
|
967
993
|
"excludedomain": excludedomain,
|
|
994
|
+
"excludedomainurl": excludedomainurl,
|
|
968
995
|
"full_content": full_content,
|
|
969
996
|
"image": image,
|
|
970
997
|
"video": video,
|
|
@@ -998,6 +1025,7 @@ class NewsDataApiClient:
|
|
|
998
1025
|
domain: str | list[str] | None = None,
|
|
999
1026
|
domainurl: str | list[str] | None = None,
|
|
1000
1027
|
excludedomain: str | list[str] | None = None,
|
|
1028
|
+
excludedomainurl: str | list[str] | None = None,
|
|
1001
1029
|
language: str | list[str] | None = None,
|
|
1002
1030
|
excludelanguage: str | list[str] | None = None,
|
|
1003
1031
|
full_content: bool | None = None,
|
|
@@ -1042,6 +1070,7 @@ class NewsDataApiClient:
|
|
|
1042
1070
|
"domain": domain,
|
|
1043
1071
|
"domainurl": domainurl,
|
|
1044
1072
|
"excludedomain": excludedomain,
|
|
1073
|
+
"excludedomainurl": excludedomainurl,
|
|
1045
1074
|
"language": language,
|
|
1046
1075
|
"excludelanguage": excludelanguage,
|
|
1047
1076
|
"full_content": full_content,
|
|
@@ -1201,7 +1230,10 @@ class NewsDataApiClient:
|
|
|
1201
1230
|
# Rate limit.
|
|
1202
1231
|
if response.status_code == 429:
|
|
1203
1232
|
retry_after = _parse_retry_after(response.headers.get("Retry-After"))
|
|
1204
|
-
if
|
|
1233
|
+
if (
|
|
1234
|
+
_extract_error_code(body) in _QUOTA_EXHAUSTED_CODES
|
|
1235
|
+
or attempt >= self.max_retries
|
|
1236
|
+
):
|
|
1205
1237
|
raise NewsdataRateLimitError(
|
|
1206
1238
|
body,
|
|
1207
1239
|
status_code=429,
|
|
@@ -177,6 +177,7 @@ class NewsDataApiWebSocket:
|
|
|
177
177
|
domain: str | list[str] | None = None,
|
|
178
178
|
domainurl: str | list[str] | None = None,
|
|
179
179
|
excludedomain: str | list[str] | None = None,
|
|
180
|
+
excludedomainurl: str | list[str] | None = None,
|
|
180
181
|
prioritydomain: str | None = None,
|
|
181
182
|
timezone: str | None = None,
|
|
182
183
|
full_content: bool | None = None,
|
|
@@ -212,6 +213,7 @@ class NewsDataApiWebSocket:
|
|
|
212
213
|
"domain": domain,
|
|
213
214
|
"domainurl": domainurl,
|
|
214
215
|
"excludedomain": excludedomain,
|
|
216
|
+
"excludedomainurl": excludedomainurl,
|
|
215
217
|
"prioritydomain": prioritydomain,
|
|
216
218
|
"timezone": timezone,
|
|
217
219
|
"full_content": full_content,
|
|
@@ -148,13 +148,23 @@ def test_validate_size_negative_rejected() -> None:
|
|
|
148
148
|
({"country": "us", "excludecountry": "gb"}, "country"),
|
|
149
149
|
({"category": "business", "excludecategory": "sports"}, "category"),
|
|
150
150
|
({"language": "en", "excludelanguage": "fr"}, "language"),
|
|
151
|
-
# Domain
|
|
151
|
+
# Domain 4-way mutex (any two of
|
|
152
|
+
# domain/domainurl/excludedomain/excludedomainurl)
|
|
152
153
|
({"domain": "cnn.com", "domainurl": "https://bbc.com"}, "domain"),
|
|
153
154
|
({"domain": "cnn.com", "excludedomain": "fox.com"}, "domain"),
|
|
154
155
|
(
|
|
155
156
|
{"domainurl": "https://bbc.com", "excludedomain": "fox.com"},
|
|
156
157
|
"domainurl",
|
|
157
158
|
),
|
|
159
|
+
({"domain": "cnn.com", "excludedomainurl": "fox.com"}, "domain"),
|
|
160
|
+
(
|
|
161
|
+
{"domainurl": "https://bbc.com", "excludedomainurl": "fox.com"},
|
|
162
|
+
"domainurl",
|
|
163
|
+
),
|
|
164
|
+
(
|
|
165
|
+
{"excludedomain": "cnn.com", "excludedomainurl": "fox.com"},
|
|
166
|
+
"excludedomain",
|
|
167
|
+
),
|
|
158
168
|
(
|
|
159
169
|
{
|
|
160
170
|
"domain": "cnn.com",
|
|
@@ -823,6 +833,22 @@ def test_429_exhausted_raises_rate_limit_with_retry_after(
|
|
|
823
833
|
assert exc_info.value.retry_after == 5
|
|
824
834
|
|
|
825
835
|
|
|
836
|
+
@pytest.mark.parametrize("code", ["ApiKeyLimitExceeded", "ApiLimitExceeded"])
|
|
837
|
+
def test_429_quota_exhausted_raises_without_retry(
|
|
838
|
+
code: str,
|
|
839
|
+
client: NewsDataApiClient,
|
|
840
|
+
mocked_responses: responses.RequestsMock,
|
|
841
|
+
no_sleep: None,
|
|
842
|
+
) -> None:
|
|
843
|
+
"""A 429 whose error code means exhausted credits is never retried."""
|
|
844
|
+
body = {"status": "error", "results": {"message": "limit", "code": code}}
|
|
845
|
+
mocked_responses.get(LATEST_URL, json=body, status=429)
|
|
846
|
+
with pytest.raises(NewsdataRateLimitError) as exc_info:
|
|
847
|
+
client.latest_api()
|
|
848
|
+
assert len(mocked_responses.calls) == 1
|
|
849
|
+
assert exc_info.value.response_body == body
|
|
850
|
+
|
|
851
|
+
|
|
826
852
|
def test_500_then_200_succeeds(
|
|
827
853
|
client: NewsDataApiClient,
|
|
828
854
|
mocked_responses: responses.RequestsMock,
|
|
@@ -1269,6 +1295,51 @@ def test_count_endpoint_url_resolves(
|
|
|
1269
1295
|
assert len(mocked_responses.calls) == 1
|
|
1270
1296
|
|
|
1271
1297
|
|
|
1298
|
+
@pytest.mark.parametrize(
|
|
1299
|
+
("method_name", "endpoint_path", "args"),
|
|
1300
|
+
[
|
|
1301
|
+
("latest_api", "latest", ()),
|
|
1302
|
+
("archive_api", "archive", ()),
|
|
1303
|
+
("crypto_api", "crypto", ()),
|
|
1304
|
+
("market_api", "market", ()),
|
|
1305
|
+
("count_api", "count", ("2024-01-01", "2024-01-31")),
|
|
1306
|
+
("crypto_count_api", "crypto/count", ("2024-01-01", "2024-01-31")),
|
|
1307
|
+
("market_count_api", "market/count", ("2024-01-01", "2024-01-31")),
|
|
1308
|
+
],
|
|
1309
|
+
)
|
|
1310
|
+
def test_excludedomainurl_sent_as_csv(
|
|
1311
|
+
client: NewsDataApiClient,
|
|
1312
|
+
mocked_responses: responses.RequestsMock,
|
|
1313
|
+
method_name: str,
|
|
1314
|
+
endpoint_path: str,
|
|
1315
|
+
args: tuple[str, ...],
|
|
1316
|
+
) -> None:
|
|
1317
|
+
mocked_responses.get(
|
|
1318
|
+
f"https://newsdata.io/api/1/{endpoint_path}",
|
|
1319
|
+
json={"status": "success", "results": []},
|
|
1320
|
+
status=200,
|
|
1321
|
+
)
|
|
1322
|
+
getattr(client, method_name)(*args, excludedomainurl=["bbc.com", "cnn.com"])
|
|
1323
|
+
sent_url = mocked_responses.calls[0].request.url
|
|
1324
|
+
assert sent_url is not None
|
|
1325
|
+
assert "excludedomainurl=bbc.com%2Ccnn.com" in sent_url
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
def test_raw_query_accepts_excludedomainurl(
|
|
1329
|
+
client: NewsDataApiClient,
|
|
1330
|
+
mocked_responses: responses.RequestsMock,
|
|
1331
|
+
) -> None:
|
|
1332
|
+
mocked_responses.get(
|
|
1333
|
+
LATEST_URL,
|
|
1334
|
+
json={"status": "success", "results": []},
|
|
1335
|
+
status=200,
|
|
1336
|
+
)
|
|
1337
|
+
client.latest_api(raw_query="excludedomainurl=bbc.com")
|
|
1338
|
+
sent_url = mocked_responses.calls[0].request.url
|
|
1339
|
+
assert sent_url is not None
|
|
1340
|
+
assert "excludedomainurl=bbc.com" in sent_url
|
|
1341
|
+
|
|
1342
|
+
|
|
1272
1343
|
# ===========================================================================
|
|
1273
1344
|
# WebSocket query management (register / fetch / delete)
|
|
1274
1345
|
# ===========================================================================
|
|
@@ -1351,6 +1422,23 @@ def test_websocket_register_raw_query_rejects_unknown_param(
|
|
|
1351
1422
|
NewsDataApiWebSocket(client).websocket_register(raw_query="q=pizza&size=10")
|
|
1352
1423
|
|
|
1353
1424
|
|
|
1425
|
+
def test_websocket_register_sends_excludedomainurl(
|
|
1426
|
+
client: NewsDataApiClient,
|
|
1427
|
+
mocked_responses: responses.RequestsMock,
|
|
1428
|
+
) -> None:
|
|
1429
|
+
mocked_responses.post(
|
|
1430
|
+
WS_REGISTER_URL,
|
|
1431
|
+
json={"status": "success", "results": {"registration_id": "abc"}},
|
|
1432
|
+
status=200,
|
|
1433
|
+
)
|
|
1434
|
+
NewsDataApiWebSocket(client).websocket_register(
|
|
1435
|
+
q="pizza", excludedomainurl=["bbc.com", "cnn.com"]
|
|
1436
|
+
)
|
|
1437
|
+
sent_url = mocked_responses.calls[0].request.url
|
|
1438
|
+
assert sent_url is not None
|
|
1439
|
+
assert "excludedomainurl=bbc.com%2Ccnn.com" in sent_url
|
|
1440
|
+
|
|
1441
|
+
|
|
1354
1442
|
def test_websocket_register_duplicate_409(
|
|
1355
1443
|
client: NewsDataApiClient,
|
|
1356
1444
|
mocked_responses: responses.RequestsMock,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|