newsdataapi 0.3.1__tar.gz → 0.3.3__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.1 → newsdataapi-0.3.3}/.github/workflows/ci.yml +1 -1
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/PKG-INFO +7 -5
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/README.md +2 -2
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/pyproject.toml +9 -4
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/src/newsdataapi/__init__.py +1 -1
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/src/newsdataapi/client.py +18 -2
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/src/newsdataapi/websocket.py +21 -4
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/tests/conftest.py +4 -0
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/tests/test_unit.py +103 -1
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/uv.lock +639 -33
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/.github/workflows/publish.yml +0 -0
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/.gitignore +0 -0
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/.python-version +0 -0
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/LICENSE +0 -0
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/examples/basic_usage.py +0 -0
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/examples/websocket_usage.py +0 -0
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/newsdata-logo.png +0 -0
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/src/newsdataapi/constants.py +0 -0
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/src/newsdataapi/csv_export.py +0 -0
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/src/newsdataapi/exceptions.py +0 -0
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/src/newsdataapi/py.typed +0 -0
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/tests/__init__.py +0 -0
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/tests/test_integration.py +0 -0
- {newsdataapi-0.3.1 → newsdataapi-0.3.3}/tests/test_websocket.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: newsdataapi
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
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
|
|
@@ -35,6 +35,8 @@ Classifier: Intended Audience :: Developers
|
|
|
35
35
|
Classifier: License :: OSI Approved :: MIT License
|
|
36
36
|
Classifier: Operating System :: OS Independent
|
|
37
37
|
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
38
40
|
Classifier: Programming Language :: Python :: 3.10
|
|
39
41
|
Classifier: Programming Language :: Python :: 3.11
|
|
40
42
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -43,9 +45,9 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
43
45
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
44
46
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
45
47
|
Classifier: Typing :: Typed
|
|
46
|
-
Requires-Python: >=3.
|
|
48
|
+
Requires-Python: >=3.8
|
|
47
49
|
Requires-Dist: requests<3,>=2.25
|
|
48
|
-
Requires-Dist: websockets<17,>=16
|
|
50
|
+
Requires-Dist: websockets<17,>=16; python_version >= '3.10'
|
|
49
51
|
Description-Content-Type: text/markdown
|
|
50
52
|
|
|
51
53
|
<div align="center">
|
|
@@ -77,7 +79,7 @@ If you use [uv](https://github.com/astral-sh/uv):
|
|
|
77
79
|
uv add newsdataapi
|
|
78
80
|
```
|
|
79
81
|
|
|
80
|
-
Supports Python 3.
|
|
82
|
+
Supports Python 3.8 through 3.14. The runtime dependencies are `requests` (REST) and `websockets` (real-time streaming). `NewsDataApiWebSocket` requires Python 3.10 or newer, so `websockets` is only installed there; on 3.8 and 3.9 the REST client works as usual.
|
|
81
83
|
|
|
82
84
|
## Quickstart
|
|
83
85
|
|
|
@@ -139,7 +141,7 @@ for page in client.latest_api(q="news", paginate=True, max_pages=5):
|
|
|
139
141
|
|
|
140
142
|
## Real-time news (WebSocket)
|
|
141
143
|
|
|
142
|
-
Register a query first — the returned `registration_id` identifies it from then on:
|
|
144
|
+
Requires Python 3.10 or newer. Register a query first — the returned `registration_id` identifies it from then on:
|
|
143
145
|
|
|
144
146
|
```python
|
|
145
147
|
from newsdataapi import NewsDataApiClient, NewsDataApiWebSocket
|
|
@@ -27,7 +27,7 @@ If you use [uv](https://github.com/astral-sh/uv):
|
|
|
27
27
|
uv add newsdataapi
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
Supports Python 3.
|
|
30
|
+
Supports Python 3.8 through 3.14. The runtime dependencies are `requests` (REST) and `websockets` (real-time streaming). `NewsDataApiWebSocket` requires Python 3.10 or newer, so `websockets` is only installed there; on 3.8 and 3.9 the REST client works as usual.
|
|
31
31
|
|
|
32
32
|
## Quickstart
|
|
33
33
|
|
|
@@ -89,7 +89,7 @@ for page in client.latest_api(q="news", paginate=True, max_pages=5):
|
|
|
89
89
|
|
|
90
90
|
## Real-time news (WebSocket)
|
|
91
91
|
|
|
92
|
-
Register a query first — the returned `registration_id` identifies it from then on:
|
|
92
|
+
Requires Python 3.10 or newer. Register a query first — the returned `registration_id` identifies it from then on:
|
|
93
93
|
|
|
94
94
|
```python
|
|
95
95
|
from newsdataapi import NewsDataApiClient, NewsDataApiWebSocket
|
|
@@ -5,11 +5,13 @@ dynamic = ["version"]
|
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { file = "LICENSE" }
|
|
7
7
|
authors = [{ name = "NewsData.io", email = "contact@newsdata.io" }]
|
|
8
|
-
requires-python = ">=3.
|
|
8
|
+
requires-python = ">=3.8"
|
|
9
9
|
|
|
10
10
|
dependencies = [
|
|
11
11
|
"requests>=2.25,<3",
|
|
12
|
-
|
|
12
|
+
# websockets 16 needs Python 3.10+. On 3.8/3.9 it is not installed: the
|
|
13
|
+
# REST client works there, and NewsDataApiWebSocket raises on construction.
|
|
14
|
+
"websockets>=16,<17; python_version >= '3.10'",
|
|
13
15
|
]
|
|
14
16
|
|
|
15
17
|
keywords = [
|
|
@@ -37,6 +39,8 @@ classifiers = [
|
|
|
37
39
|
"License :: OSI Approved :: MIT License",
|
|
38
40
|
"Operating System :: OS Independent",
|
|
39
41
|
"Programming Language :: Python :: 3",
|
|
42
|
+
"Programming Language :: Python :: 3.8",
|
|
43
|
+
"Programming Language :: Python :: 3.9",
|
|
40
44
|
"Programming Language :: Python :: 3.10",
|
|
41
45
|
"Programming Language :: Python :: 3.11",
|
|
42
46
|
"Programming Language :: Python :: 3.12",
|
|
@@ -90,7 +94,7 @@ addopts = [
|
|
|
90
94
|
|
|
91
95
|
[tool.ruff]
|
|
92
96
|
line-length = 100
|
|
93
|
-
target-version = "
|
|
97
|
+
target-version = "py38"
|
|
94
98
|
|
|
95
99
|
[tool.ruff.lint]
|
|
96
100
|
select = [
|
|
@@ -107,6 +111,7 @@ select = [
|
|
|
107
111
|
]
|
|
108
112
|
|
|
109
113
|
[tool.mypy]
|
|
110
|
-
|
|
114
|
+
# mypy no longer accepts 3.8 as a target; the 3.8 CI job covers it at runtime.
|
|
115
|
+
python_version = "3.9"
|
|
111
116
|
strict = true
|
|
112
117
|
files = ["src/newsdataapi"]
|
|
@@ -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,7 +82,7 @@ _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
|
|
|
87
88
|
_QUOTA_EXHAUSTED_CODES = frozenset({"ApiKeyLimitExceeded", "ApiLimitExceeded"})
|
|
@@ -103,7 +104,8 @@ def _validate_params(user_params: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
103
104
|
* Server-side mutex groups (``q``/``qInTitle``/``qInMeta``,
|
|
104
105
|
``country``/``excludecountry``, ``category``/``excludecategory``,
|
|
105
106
|
``language``/``excludelanguage``, and
|
|
106
|
-
``domain``/``domainurl``/``excludedomain``) are
|
|
107
|
+
``domain``/``domainurl``/``excludedomain``/``excludedomainurl``) are
|
|
108
|
+
enforced client-side;
|
|
107
109
|
setting more than one from any group raises
|
|
108
110
|
``NewsdataValidationError`` before the request leaves.
|
|
109
111
|
* ``sentiment_score`` requires ``sentiment`` to be set; passing
|
|
@@ -480,6 +482,7 @@ class NewsDataApiClient:
|
|
|
480
482
|
domain: str | list[str] | None = None,
|
|
481
483
|
domainurl: str | list[str] | None = None,
|
|
482
484
|
excludedomain: str | list[str] | None = None,
|
|
485
|
+
excludedomainurl: str | list[str] | None = None,
|
|
483
486
|
prioritydomain: str | None = None,
|
|
484
487
|
timeframe: int | str | None = None,
|
|
485
488
|
timezone: str | None = None,
|
|
@@ -528,6 +531,7 @@ class NewsDataApiClient:
|
|
|
528
531
|
"domain": domain,
|
|
529
532
|
"domainurl": domainurl,
|
|
530
533
|
"excludedomain": excludedomain,
|
|
534
|
+
"excludedomainurl": excludedomainurl,
|
|
531
535
|
"prioritydomain": prioritydomain,
|
|
532
536
|
"timeframe": str(timeframe) if timeframe is not None else None,
|
|
533
537
|
"timezone": timezone,
|
|
@@ -571,6 +575,7 @@ class NewsDataApiClient:
|
|
|
571
575
|
domain: str | list[str] | None = None,
|
|
572
576
|
domainurl: str | list[str] | None = None,
|
|
573
577
|
excludedomain: str | list[str] | None = None,
|
|
578
|
+
excludedomainurl: str | list[str] | None = None,
|
|
574
579
|
prioritydomain: str | None = None,
|
|
575
580
|
timezone: str | None = None,
|
|
576
581
|
size: int | None = None,
|
|
@@ -617,6 +622,7 @@ class NewsDataApiClient:
|
|
|
617
622
|
"domain": domain,
|
|
618
623
|
"domainurl": domainurl,
|
|
619
624
|
"excludedomain": excludedomain,
|
|
625
|
+
"excludedomainurl": excludedomainurl,
|
|
620
626
|
"prioritydomain": prioritydomain,
|
|
621
627
|
"timezone": timezone,
|
|
622
628
|
"size": size,
|
|
@@ -684,6 +690,7 @@ class NewsDataApiClient:
|
|
|
684
690
|
domain: str | list[str] | None = None,
|
|
685
691
|
domainurl: str | list[str] | None = None,
|
|
686
692
|
excludedomain: str | list[str] | None = None,
|
|
693
|
+
excludedomainurl: str | list[str] | None = None,
|
|
687
694
|
prioritydomain: str | None = None,
|
|
688
695
|
timeframe: int | str | None = None,
|
|
689
696
|
timezone: str | None = None,
|
|
@@ -723,6 +730,7 @@ class NewsDataApiClient:
|
|
|
723
730
|
"domain": domain,
|
|
724
731
|
"domainurl": domainurl,
|
|
725
732
|
"excludedomain": excludedomain,
|
|
733
|
+
"excludedomainurl": excludedomainurl,
|
|
726
734
|
"prioritydomain": prioritydomain,
|
|
727
735
|
"timeframe": str(timeframe) if timeframe is not None else None,
|
|
728
736
|
"timezone": timezone,
|
|
@@ -762,6 +770,7 @@ class NewsDataApiClient:
|
|
|
762
770
|
domain: str | list[str] | None = None,
|
|
763
771
|
domainurl: str | list[str] | None = None,
|
|
764
772
|
excludedomain: str | list[str] | None = None,
|
|
773
|
+
excludedomainurl: str | list[str] | None = None,
|
|
765
774
|
language: str | list[str] | None = None,
|
|
766
775
|
excludelanguage: str | list[str] | None = None,
|
|
767
776
|
prioritydomain: str | None = None,
|
|
@@ -807,6 +816,7 @@ class NewsDataApiClient:
|
|
|
807
816
|
"domain": domain,
|
|
808
817
|
"domainurl": domainurl,
|
|
809
818
|
"excludedomain": excludedomain,
|
|
819
|
+
"excludedomainurl": excludedomainurl,
|
|
810
820
|
"language": language,
|
|
811
821
|
"excludelanguage": excludelanguage,
|
|
812
822
|
"prioritydomain": prioritydomain,
|
|
@@ -854,6 +864,7 @@ class NewsDataApiClient:
|
|
|
854
864
|
domain: str | list[str] | None = None,
|
|
855
865
|
domainurl: str | list[str] | None = None,
|
|
856
866
|
excludedomain: str | list[str] | None = None,
|
|
867
|
+
excludedomainurl: str | list[str] | None = None,
|
|
857
868
|
full_content: bool | None = None,
|
|
858
869
|
image: bool | None = None,
|
|
859
870
|
video: bool | None = None,
|
|
@@ -901,6 +912,7 @@ class NewsDataApiClient:
|
|
|
901
912
|
"domain": domain,
|
|
902
913
|
"domainurl": domainurl,
|
|
903
914
|
"excludedomain": excludedomain,
|
|
915
|
+
"excludedomainurl": excludedomainurl,
|
|
904
916
|
"full_content": full_content,
|
|
905
917
|
"image": image,
|
|
906
918
|
"video": video,
|
|
@@ -940,6 +952,7 @@ class NewsDataApiClient:
|
|
|
940
952
|
domain: str | list[str] | None = None,
|
|
941
953
|
domainurl: str | list[str] | None = None,
|
|
942
954
|
excludedomain: str | list[str] | None = None,
|
|
955
|
+
excludedomainurl: str | list[str] | None = None,
|
|
943
956
|
full_content: bool | None = None,
|
|
944
957
|
image: bool | None = None,
|
|
945
958
|
video: bool | None = None,
|
|
@@ -978,6 +991,7 @@ class NewsDataApiClient:
|
|
|
978
991
|
"domain": domain,
|
|
979
992
|
"domainurl": domainurl,
|
|
980
993
|
"excludedomain": excludedomain,
|
|
994
|
+
"excludedomainurl": excludedomainurl,
|
|
981
995
|
"full_content": full_content,
|
|
982
996
|
"image": image,
|
|
983
997
|
"video": video,
|
|
@@ -1011,6 +1025,7 @@ class NewsDataApiClient:
|
|
|
1011
1025
|
domain: str | list[str] | None = None,
|
|
1012
1026
|
domainurl: str | list[str] | None = None,
|
|
1013
1027
|
excludedomain: str | list[str] | None = None,
|
|
1028
|
+
excludedomainurl: str | list[str] | None = None,
|
|
1014
1029
|
language: str | list[str] | None = None,
|
|
1015
1030
|
excludelanguage: str | list[str] | None = None,
|
|
1016
1031
|
full_content: bool | None = None,
|
|
@@ -1055,6 +1070,7 @@ class NewsDataApiClient:
|
|
|
1055
1070
|
"domain": domain,
|
|
1056
1071
|
"domainurl": domainurl,
|
|
1057
1072
|
"excludedomain": excludedomain,
|
|
1073
|
+
"excludedomainurl": excludedomainurl,
|
|
1058
1074
|
"language": language,
|
|
1059
1075
|
"excludelanguage": excludelanguage,
|
|
1060
1076
|
"full_content": full_content,
|
|
@@ -11,14 +11,11 @@ from __future__ import annotations
|
|
|
11
11
|
import asyncio
|
|
12
12
|
import json
|
|
13
13
|
import logging
|
|
14
|
+
import sys
|
|
14
15
|
import time
|
|
15
16
|
from collections.abc import AsyncIterator, Iterator
|
|
16
17
|
from typing import Any
|
|
17
18
|
|
|
18
|
-
from websockets.asyncio import client as _asyncio_client
|
|
19
|
-
from websockets.exceptions import ConnectionClosedError, InvalidStatus
|
|
20
|
-
from websockets.sync import client as _sync_client
|
|
21
|
-
|
|
22
19
|
from . import constants
|
|
23
20
|
from .client import NewsDataApiClient, _redact_url, _validate_params
|
|
24
21
|
from .exceptions import (
|
|
@@ -27,6 +24,15 @@ from .exceptions import (
|
|
|
27
24
|
NewsdataWebSocketError,
|
|
28
25
|
)
|
|
29
26
|
|
|
27
|
+
# websockets requires Python 3.10+ and is not installed on older versions,
|
|
28
|
+
# where only the REST client is available.
|
|
29
|
+
_WEBSOCKET_SUPPORTED = sys.version_info >= (3, 10)
|
|
30
|
+
|
|
31
|
+
if _WEBSOCKET_SUPPORTED:
|
|
32
|
+
from websockets.asyncio import client as _asyncio_client
|
|
33
|
+
from websockets.exceptions import ConnectionClosedError, InvalidStatus
|
|
34
|
+
from websockets.sync import client as _sync_client
|
|
35
|
+
|
|
30
36
|
logger = logging.getLogger(__name__)
|
|
31
37
|
|
|
32
38
|
|
|
@@ -109,6 +115,9 @@ class NewsDataApiWebSocket:
|
|
|
109
115
|
always raises :class:`~newsdataapi.NewsdataWebSocketAuthError` and is
|
|
110
116
|
never retried.
|
|
111
117
|
|
|
118
|
+
Requires Python 3.10 or newer; on older versions the constructor raises
|
|
119
|
+
:class:`~newsdataapi.NewsdataWebSocketError`.
|
|
120
|
+
|
|
112
121
|
Args:
|
|
113
122
|
api_client: The :class:`~newsdataapi.NewsDataApiClient` that supplies
|
|
114
123
|
the API key and performs the management HTTP calls. Not closed by
|
|
@@ -147,6 +156,12 @@ class NewsDataApiWebSocket:
|
|
|
147
156
|
additional_headers: dict[str, str] | None = None,
|
|
148
157
|
proxy: str | None = None,
|
|
149
158
|
) -> None:
|
|
159
|
+
if not _WEBSOCKET_SUPPORTED:
|
|
160
|
+
raise NewsdataWebSocketError(
|
|
161
|
+
"NewsDataApiWebSocket requires Python 3.10 or newer (this is Python "
|
|
162
|
+
f"{sys.version_info.major}.{sys.version_info.minor}); "
|
|
163
|
+
"NewsDataApiClient works on Python 3.8+"
|
|
164
|
+
)
|
|
150
165
|
self.api_client = api_client
|
|
151
166
|
self._base_url = base_url
|
|
152
167
|
self._reconnect = reconnect
|
|
@@ -177,6 +192,7 @@ class NewsDataApiWebSocket:
|
|
|
177
192
|
domain: str | list[str] | None = None,
|
|
178
193
|
domainurl: str | list[str] | None = None,
|
|
179
194
|
excludedomain: str | list[str] | None = None,
|
|
195
|
+
excludedomainurl: str | list[str] | None = None,
|
|
180
196
|
prioritydomain: str | None = None,
|
|
181
197
|
timezone: str | None = None,
|
|
182
198
|
full_content: bool | None = None,
|
|
@@ -212,6 +228,7 @@ class NewsDataApiWebSocket:
|
|
|
212
228
|
"domain": domain,
|
|
213
229
|
"domainurl": domainurl,
|
|
214
230
|
"excludedomain": excludedomain,
|
|
231
|
+
"excludedomainurl": excludedomainurl,
|
|
215
232
|
"prioritydomain": prioritydomain,
|
|
216
233
|
"timezone": timezone,
|
|
217
234
|
"full_content": full_content,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import sys
|
|
5
6
|
from collections.abc import Iterator
|
|
6
7
|
|
|
7
8
|
import pytest
|
|
@@ -9,6 +10,9 @@ import responses
|
|
|
9
10
|
|
|
10
11
|
from newsdataapi import NewsDataApiClient
|
|
11
12
|
|
|
13
|
+
# test_websocket.py imports websockets, which is only installed on Python 3.10+.
|
|
14
|
+
collect_ignore = ["test_websocket.py"] if sys.version_info < (3, 10) else []
|
|
15
|
+
|
|
12
16
|
|
|
13
17
|
@pytest.fixture
|
|
14
18
|
def mocked_responses() -> Iterator[responses.RequestsMock]:
|
|
@@ -7,6 +7,7 @@ API key.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import sys
|
|
10
11
|
from datetime import datetime, timedelta, timezone
|
|
11
12
|
from email.utils import format_datetime
|
|
12
13
|
from pathlib import Path
|
|
@@ -25,6 +26,7 @@ from newsdataapi import (
|
|
|
25
26
|
NewsdataRateLimitError,
|
|
26
27
|
NewsdataServerError,
|
|
27
28
|
NewsdataValidationError,
|
|
29
|
+
NewsdataWebSocketError,
|
|
28
30
|
save_to_csv,
|
|
29
31
|
)
|
|
30
32
|
from newsdataapi.client import (
|
|
@@ -148,13 +150,23 @@ def test_validate_size_negative_rejected() -> None:
|
|
|
148
150
|
({"country": "us", "excludecountry": "gb"}, "country"),
|
|
149
151
|
({"category": "business", "excludecategory": "sports"}, "category"),
|
|
150
152
|
({"language": "en", "excludelanguage": "fr"}, "language"),
|
|
151
|
-
# Domain
|
|
153
|
+
# Domain 4-way mutex (any two of
|
|
154
|
+
# domain/domainurl/excludedomain/excludedomainurl)
|
|
152
155
|
({"domain": "cnn.com", "domainurl": "https://bbc.com"}, "domain"),
|
|
153
156
|
({"domain": "cnn.com", "excludedomain": "fox.com"}, "domain"),
|
|
154
157
|
(
|
|
155
158
|
{"domainurl": "https://bbc.com", "excludedomain": "fox.com"},
|
|
156
159
|
"domainurl",
|
|
157
160
|
),
|
|
161
|
+
({"domain": "cnn.com", "excludedomainurl": "fox.com"}, "domain"),
|
|
162
|
+
(
|
|
163
|
+
{"domainurl": "https://bbc.com", "excludedomainurl": "fox.com"},
|
|
164
|
+
"domainurl",
|
|
165
|
+
),
|
|
166
|
+
(
|
|
167
|
+
{"excludedomain": "cnn.com", "excludedomainurl": "fox.com"},
|
|
168
|
+
"excludedomain",
|
|
169
|
+
),
|
|
158
170
|
(
|
|
159
171
|
{
|
|
160
172
|
"domain": "cnn.com",
|
|
@@ -1285,6 +1297,51 @@ def test_count_endpoint_url_resolves(
|
|
|
1285
1297
|
assert len(mocked_responses.calls) == 1
|
|
1286
1298
|
|
|
1287
1299
|
|
|
1300
|
+
@pytest.mark.parametrize(
|
|
1301
|
+
("method_name", "endpoint_path", "args"),
|
|
1302
|
+
[
|
|
1303
|
+
("latest_api", "latest", ()),
|
|
1304
|
+
("archive_api", "archive", ()),
|
|
1305
|
+
("crypto_api", "crypto", ()),
|
|
1306
|
+
("market_api", "market", ()),
|
|
1307
|
+
("count_api", "count", ("2024-01-01", "2024-01-31")),
|
|
1308
|
+
("crypto_count_api", "crypto/count", ("2024-01-01", "2024-01-31")),
|
|
1309
|
+
("market_count_api", "market/count", ("2024-01-01", "2024-01-31")),
|
|
1310
|
+
],
|
|
1311
|
+
)
|
|
1312
|
+
def test_excludedomainurl_sent_as_csv(
|
|
1313
|
+
client: NewsDataApiClient,
|
|
1314
|
+
mocked_responses: responses.RequestsMock,
|
|
1315
|
+
method_name: str,
|
|
1316
|
+
endpoint_path: str,
|
|
1317
|
+
args: tuple[str, ...],
|
|
1318
|
+
) -> None:
|
|
1319
|
+
mocked_responses.get(
|
|
1320
|
+
f"https://newsdata.io/api/1/{endpoint_path}",
|
|
1321
|
+
json={"status": "success", "results": []},
|
|
1322
|
+
status=200,
|
|
1323
|
+
)
|
|
1324
|
+
getattr(client, method_name)(*args, excludedomainurl=["bbc.com", "cnn.com"])
|
|
1325
|
+
sent_url = mocked_responses.calls[0].request.url
|
|
1326
|
+
assert sent_url is not None
|
|
1327
|
+
assert "excludedomainurl=bbc.com%2Ccnn.com" in sent_url
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
def test_raw_query_accepts_excludedomainurl(
|
|
1331
|
+
client: NewsDataApiClient,
|
|
1332
|
+
mocked_responses: responses.RequestsMock,
|
|
1333
|
+
) -> None:
|
|
1334
|
+
mocked_responses.get(
|
|
1335
|
+
LATEST_URL,
|
|
1336
|
+
json={"status": "success", "results": []},
|
|
1337
|
+
status=200,
|
|
1338
|
+
)
|
|
1339
|
+
client.latest_api(raw_query="excludedomainurl=bbc.com")
|
|
1340
|
+
sent_url = mocked_responses.calls[0].request.url
|
|
1341
|
+
assert sent_url is not None
|
|
1342
|
+
assert "excludedomainurl=bbc.com" in sent_url
|
|
1343
|
+
|
|
1344
|
+
|
|
1288
1345
|
# ===========================================================================
|
|
1289
1346
|
# WebSocket query management (register / fetch / delete)
|
|
1290
1347
|
# ===========================================================================
|
|
@@ -1293,7 +1350,23 @@ WS_REGISTER_URL = "https://newsdata.io/api/1/websocket/register"
|
|
|
1293
1350
|
WS_FETCH_URL = "https://newsdata.io/api/1/websocket/fetch"
|
|
1294
1351
|
WS_DELETE_URL = "https://newsdata.io/api/1/websocket/delete"
|
|
1295
1352
|
|
|
1353
|
+
# NewsDataApiWebSocket can't be constructed below Python 3.10.
|
|
1354
|
+
requires_websocket = pytest.mark.skipif(
|
|
1355
|
+
sys.version_info < (3, 10), reason="WebSocket support requires Python 3.10+"
|
|
1356
|
+
)
|
|
1357
|
+
|
|
1358
|
+
|
|
1359
|
+
def test_websocket_requires_python_310(
|
|
1360
|
+
client: NewsDataApiClient,
|
|
1361
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1362
|
+
) -> None:
|
|
1363
|
+
"""Below Python 3.10, construction fails up front with a clear error."""
|
|
1364
|
+
monkeypatch.setattr("newsdataapi.websocket._WEBSOCKET_SUPPORTED", False)
|
|
1365
|
+
with pytest.raises(NewsdataWebSocketError, match="requires Python 3.10"):
|
|
1366
|
+
NewsDataApiWebSocket(client)
|
|
1367
|
+
|
|
1296
1368
|
|
|
1369
|
+
@requires_websocket
|
|
1297
1370
|
def test_websocket_register_posts_query_params(
|
|
1298
1371
|
client: NewsDataApiClient,
|
|
1299
1372
|
mocked_responses: responses.RequestsMock,
|
|
@@ -1320,6 +1393,7 @@ def test_websocket_register_posts_query_params(
|
|
|
1320
1393
|
assert not sent.body # params travel in the query string, not a body
|
|
1321
1394
|
|
|
1322
1395
|
|
|
1396
|
+
@requires_websocket
|
|
1323
1397
|
def test_websocket_register_always_sends_news_type_latest(
|
|
1324
1398
|
client: NewsDataApiClient,
|
|
1325
1399
|
mocked_responses: responses.RequestsMock,
|
|
@@ -1335,6 +1409,7 @@ def test_websocket_register_always_sends_news_type_latest(
|
|
|
1335
1409
|
assert "news_type=latest" in sent_url
|
|
1336
1410
|
|
|
1337
1411
|
|
|
1412
|
+
@requires_websocket
|
|
1338
1413
|
def test_websocket_register_mutex_validation_applies(
|
|
1339
1414
|
client: NewsDataApiClient,
|
|
1340
1415
|
) -> None:
|
|
@@ -1343,6 +1418,7 @@ def test_websocket_register_mutex_validation_applies(
|
|
|
1343
1418
|
NewsDataApiWebSocket(client).websocket_register(q="a", qInTitle="b")
|
|
1344
1419
|
|
|
1345
1420
|
|
|
1421
|
+
@requires_websocket
|
|
1346
1422
|
def test_websocket_register_raw_query(
|
|
1347
1423
|
client: NewsDataApiClient,
|
|
1348
1424
|
mocked_responses: responses.RequestsMock,
|
|
@@ -1359,6 +1435,7 @@ def test_websocket_register_raw_query(
|
|
|
1359
1435
|
assert "news_type=latest" in sent_url
|
|
1360
1436
|
|
|
1361
1437
|
|
|
1438
|
+
@requires_websocket
|
|
1362
1439
|
def test_websocket_register_raw_query_rejects_unknown_param(
|
|
1363
1440
|
client: NewsDataApiClient,
|
|
1364
1441
|
) -> None:
|
|
@@ -1367,6 +1444,25 @@ def test_websocket_register_raw_query_rejects_unknown_param(
|
|
|
1367
1444
|
NewsDataApiWebSocket(client).websocket_register(raw_query="q=pizza&size=10")
|
|
1368
1445
|
|
|
1369
1446
|
|
|
1447
|
+
@requires_websocket
|
|
1448
|
+
def test_websocket_register_sends_excludedomainurl(
|
|
1449
|
+
client: NewsDataApiClient,
|
|
1450
|
+
mocked_responses: responses.RequestsMock,
|
|
1451
|
+
) -> None:
|
|
1452
|
+
mocked_responses.post(
|
|
1453
|
+
WS_REGISTER_URL,
|
|
1454
|
+
json={"status": "success", "results": {"registration_id": "abc"}},
|
|
1455
|
+
status=200,
|
|
1456
|
+
)
|
|
1457
|
+
NewsDataApiWebSocket(client).websocket_register(
|
|
1458
|
+
q="pizza", excludedomainurl=["bbc.com", "cnn.com"]
|
|
1459
|
+
)
|
|
1460
|
+
sent_url = mocked_responses.calls[0].request.url
|
|
1461
|
+
assert sent_url is not None
|
|
1462
|
+
assert "excludedomainurl=bbc.com%2Ccnn.com" in sent_url
|
|
1463
|
+
|
|
1464
|
+
|
|
1465
|
+
@requires_websocket
|
|
1370
1466
|
def test_websocket_register_duplicate_409(
|
|
1371
1467
|
client: NewsDataApiClient,
|
|
1372
1468
|
mocked_responses: responses.RequestsMock,
|
|
@@ -1394,6 +1490,7 @@ def test_websocket_register_duplicate_409(
|
|
|
1394
1490
|
assert len(mocked_responses.calls) == 1 # 4xx is never retried
|
|
1395
1491
|
|
|
1396
1492
|
|
|
1493
|
+
@requires_websocket
|
|
1397
1494
|
def test_websocket_register_403_raises_auth(
|
|
1398
1495
|
client: NewsDataApiClient,
|
|
1399
1496
|
mocked_responses: responses.RequestsMock,
|
|
@@ -1404,6 +1501,7 @@ def test_websocket_register_403_raises_auth(
|
|
|
1404
1501
|
NewsDataApiWebSocket(client).websocket_register(q="pizza")
|
|
1405
1502
|
|
|
1406
1503
|
|
|
1504
|
+
@requires_websocket
|
|
1407
1505
|
def test_websocket_register_429_exhausts_to_rate_limit(
|
|
1408
1506
|
client: NewsDataApiClient,
|
|
1409
1507
|
mocked_responses: responses.RequestsMock,
|
|
@@ -1416,6 +1514,7 @@ def test_websocket_register_429_exhausts_to_rate_limit(
|
|
|
1416
1514
|
NewsDataApiWebSocket(client).websocket_register(q="pizza")
|
|
1417
1515
|
|
|
1418
1516
|
|
|
1517
|
+
@requires_websocket
|
|
1419
1518
|
def test_websocket_fetch_returns_queries(
|
|
1420
1519
|
client: NewsDataApiClient,
|
|
1421
1520
|
mocked_responses: responses.RequestsMock,
|
|
@@ -1446,6 +1545,7 @@ def test_websocket_fetch_returns_queries(
|
|
|
1446
1545
|
assert "apikey=test_key_xxx" in sent_url
|
|
1447
1546
|
|
|
1448
1547
|
|
|
1548
|
+
@requires_websocket
|
|
1449
1549
|
def test_websocket_delete_sends_registration_id(
|
|
1450
1550
|
client: NewsDataApiClient,
|
|
1451
1551
|
mocked_responses: responses.RequestsMock,
|
|
@@ -1465,6 +1565,7 @@ def test_websocket_delete_sends_registration_id(
|
|
|
1465
1565
|
assert "registration_id=abc123" in sent_url
|
|
1466
1566
|
|
|
1467
1567
|
|
|
1568
|
+
@requires_websocket
|
|
1468
1569
|
def test_websocket_delete_unknown_id_404(
|
|
1469
1570
|
client: NewsDataApiClient,
|
|
1470
1571
|
mocked_responses: responses.RequestsMock,
|
|
@@ -1475,6 +1576,7 @@ def test_websocket_delete_unknown_id_404(
|
|
|
1475
1576
|
assert exc_info.value.status_code == 404
|
|
1476
1577
|
|
|
1477
1578
|
|
|
1579
|
+
@requires_websocket
|
|
1478
1580
|
@pytest.mark.parametrize("bad_id", ["", None, 123])
|
|
1479
1581
|
def test_websocket_delete_rejects_bad_registration_id(
|
|
1480
1582
|
client: NewsDataApiClient,
|