newsdataapi 0.2.3__tar.gz → 0.3.0__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.2.3 → newsdataapi-0.3.0}/.github/workflows/ci.yml +1 -1
- {newsdataapi-0.2.3 → newsdataapi-0.3.0}/PKG-INFO +89 -11
- {newsdataapi-0.2.3 → newsdataapi-0.3.0}/README.md +84 -5
- newsdataapi-0.3.0/examples/websocket_usage.py +98 -0
- {newsdataapi-0.2.3 → newsdataapi-0.3.0}/pyproject.toml +8 -5
- {newsdataapi-0.2.3 → newsdataapi-0.3.0}/src/newsdataapi/__init__.py +7 -1
- {newsdataapi-0.2.3 → newsdataapi-0.3.0}/src/newsdataapi/client.py +6 -3
- {newsdataapi-0.2.3 → newsdataapi-0.3.0}/src/newsdataapi/constants.py +10 -0
- {newsdataapi-0.2.3 → newsdataapi-0.3.0}/src/newsdataapi/exceptions.py +11 -0
- newsdataapi-0.3.0/src/newsdataapi/websocket.py +361 -0
- {newsdataapi-0.2.3 → newsdataapi-0.3.0}/tests/test_unit.py +201 -0
- newsdataapi-0.3.0/tests/test_websocket.py +544 -0
- {newsdataapi-0.2.3 → newsdataapi-0.3.0}/uv.lock +104 -638
- {newsdataapi-0.2.3 → newsdataapi-0.3.0}/.github/workflows/publish.yml +0 -0
- {newsdataapi-0.2.3 → newsdataapi-0.3.0}/.gitignore +0 -0
- {newsdataapi-0.2.3 → newsdataapi-0.3.0}/.python-version +0 -0
- {newsdataapi-0.2.3 → newsdataapi-0.3.0}/LICENSE +0 -0
- {newsdataapi-0.2.3 → newsdataapi-0.3.0}/examples/basic_usage.py +0 -0
- {newsdataapi-0.2.3 → newsdataapi-0.3.0}/newsdata-logo.png +0 -0
- {newsdataapi-0.2.3 → newsdataapi-0.3.0}/src/newsdataapi/csv_export.py +0 -0
- {newsdataapi-0.2.3 → newsdataapi-0.3.0}/src/newsdataapi/py.typed +0 -0
- {newsdataapi-0.2.3 → newsdataapi-0.3.0}/tests/__init__.py +0 -0
- {newsdataapi-0.2.3 → newsdataapi-0.3.0}/tests/conftest.py +0 -0
- {newsdataapi-0.2.3 → newsdataapi-0.3.0}/tests/test_integration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: newsdataapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -29,14 +29,12 @@ License: MIT License
|
|
|
29
29
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
30
|
SOFTWARE.
|
|
31
31
|
License-File: LICENSE
|
|
32
|
-
Keywords: api-client,crypto-news,financial-news,market-news,news,news-aggregator,news-api,newsdata,newsdata-io,python-sdk,rest-api,sdk
|
|
32
|
+
Keywords: api-client,crypto-news,financial-news,market-news,news,news-aggregator,news-api,news-stream,newsdata,newsdata-io,python-sdk,realtime-news,rest-api,sdk,websocket,websockets
|
|
33
33
|
Classifier: Development Status :: 4 - Beta
|
|
34
34
|
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
|
|
40
38
|
Classifier: Programming Language :: Python :: 3.10
|
|
41
39
|
Classifier: Programming Language :: Python :: 3.11
|
|
42
40
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -45,8 +43,9 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
45
43
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
46
44
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
47
45
|
Classifier: Typing :: Typed
|
|
48
|
-
Requires-Python: >=3.
|
|
46
|
+
Requires-Python: >=3.10
|
|
49
47
|
Requires-Dist: requests<3,>=2.25
|
|
48
|
+
Requires-Dist: websockets<17,>=16
|
|
50
49
|
Description-Content-Type: text/markdown
|
|
51
50
|
|
|
52
51
|
<div align="center">
|
|
@@ -64,7 +63,7 @@ Description-Content-Type: text/markdown
|
|
|
64
63
|
|
|
65
64
|
</div>
|
|
66
65
|
|
|
67
|
-
`newsdataapi` is the official Python SDK for the [NewsData.io](https://newsdata.io) REST API. It wraps every endpoint (`latest`, `archive`, `sources`, `crypto`, `market`, `count`, `crypto/count`, `market/count`) with consistent retry, pagination, and error handling.
|
|
66
|
+
`newsdataapi` is the official Python SDK for the [NewsData.io](https://newsdata.io) REST API. It wraps every endpoint (`latest`, `archive`, `sources`, `crypto`, `market`, `count`, `crypto/count`, `market/count`) with consistent retry, pagination, and error handling. It also covers the real-time WebSocket service end to end with `NewsDataApiWebSocket`: register, list, and delete queries, and stream the matching news as it is published (sync or asyncio).
|
|
68
67
|
|
|
69
68
|
## Installation
|
|
70
69
|
|
|
@@ -78,7 +77,7 @@ If you use [uv](https://github.com/astral-sh/uv):
|
|
|
78
77
|
uv add newsdataapi
|
|
79
78
|
```
|
|
80
79
|
|
|
81
|
-
Supports Python 3.
|
|
80
|
+
Supports Python 3.10 through 3.14. The runtime dependencies are `requests` (REST) and `websockets` (real-time streaming).
|
|
82
81
|
|
|
83
82
|
## Quickstart
|
|
84
83
|
|
|
@@ -118,7 +117,7 @@ finally:
|
|
|
118
117
|
| `crypto_count_api(from_date, to_date)` | `/crypto/count` | Aggregate crypto counts |
|
|
119
118
|
| `market_count_api(from_date, to_date)` | `/market/count` | Aggregate market counts |
|
|
120
119
|
|
|
121
|
-
All endpoint parameters are keyword-only (except the required `from_date` / `to_date` on the count endpoints). Most accept either a single string or a `list[str]`; lists are comma-joined for the API.
|
|
120
|
+
All endpoint parameters are keyword-only (except the required `from_date` / `to_date` on the count endpoints). Most accept either a single string or a `list[str]`; lists are comma-joined for the API. The real-time WebSocket endpoints are covered by `NewsDataApiWebSocket` (see below).
|
|
122
121
|
|
|
123
122
|
See the [NewsData.io documentation](https://newsdata.io/documentation) — or the [OpenAPI 3.1 spec](https://newsdata.io/openapi.json) — for the full parameter reference.
|
|
124
123
|
|
|
@@ -138,6 +137,83 @@ for page in client.latest_api(q="news", paginate=True, max_pages=5):
|
|
|
138
137
|
|
|
139
138
|
`scroll` and `paginate` are mutually exclusive. `scroll=True` truncates strictly to `max_result`; `paginate=True` stops at `max_pages` or when the API returns no `nextPage`.
|
|
140
139
|
|
|
140
|
+
## Real-time news (WebSocket)
|
|
141
|
+
|
|
142
|
+
Register a query first — the returned `registration_id` identifies it from then on:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from newsdataapi import NewsDataApiClient, NewsDataApiWebSocket
|
|
146
|
+
|
|
147
|
+
client = NewsDataApiClient("YOUR_API_KEY")
|
|
148
|
+
ws = NewsDataApiWebSocket(client)
|
|
149
|
+
response = ws.websocket_register(q="bitcoin", language="en")
|
|
150
|
+
registration_id = response["results"]["registration_id"]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`websocket_register` accepts the familiar filter parameters (`q`, `country`, `language`, `domain`, …). Registering an identical query twice raises `NewsdataAPIError` with `status_code=409` — the existing id is in `e.response_body["results"]["registration_id"]`. `websocket_fetch()` lists every registered query, and `websocket_delete(registration_id)` removes one.
|
|
154
|
+
|
|
155
|
+
Then stream — each yielded response has the familiar `status` / `totalResults` / `results` shape:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
for response in ws.stream(registration_id):
|
|
159
|
+
for article in response["results"]:
|
|
160
|
+
print(article["title"], "-", article["link"])
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Use it as a context manager to close the connection promptly when you stop early (otherwise it closes when iteration ends):
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
with NewsDataApiWebSocket(client) as ws:
|
|
167
|
+
for response in ws.stream(registration_id):
|
|
168
|
+
print(response["totalResults"])
|
|
169
|
+
break
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Inside asyncio applications use `stream_async()` — the same class, same behavior, awaited iteration:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
import asyncio
|
|
176
|
+
|
|
177
|
+
async def main():
|
|
178
|
+
async with NewsDataApiWebSocket(client) as ws:
|
|
179
|
+
async for response in ws.stream_async(registration_id):
|
|
180
|
+
for article in response["results"]:
|
|
181
|
+
print(article["title"], "-", article["link"])
|
|
182
|
+
|
|
183
|
+
asyncio.run(main())
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Transient drops (network errors, server restarts, abnormal closes) are reconnected automatically with a capped exponential backoff. Pass `reconnect=False` to stop on the first disconnect instead. A permanent rejection — bad API key, missing WebSocket entitlement, unknown `registration_id`, device limit reached, or exhausted quota — raises `NewsdataWebSocketAuthError` and is **not** retried:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from newsdataapi import NewsdataWebSocketAuthError, NewsdataWebSocketError
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
for response in NewsDataApiWebSocket(client).stream(registration_id):
|
|
193
|
+
...
|
|
194
|
+
except NewsdataWebSocketAuthError as e:
|
|
195
|
+
print(f"rejected: {e}")
|
|
196
|
+
except NewsdataWebSocketError as e:
|
|
197
|
+
print(f"stream error: {e}")
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
All connection options are keyword-only:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
ws = NewsDataApiWebSocket(
|
|
204
|
+
client,
|
|
205
|
+
base_url="wss://ws.newsdata.io/ws/event", # override for staging / self-hosted / proxied
|
|
206
|
+
reconnect=True, # auto-reconnect on transient drops; default True
|
|
207
|
+
reconnect_delay=1.0, # seconds before first reconnect (doubles each retry)
|
|
208
|
+
reconnect_delay_max=30.0, # cap on the reconnect delay
|
|
209
|
+
open_timeout=10.0, # handshake timeout (None disables)
|
|
210
|
+
ping_interval=20.0, # keepalive ping interval (None disables)
|
|
211
|
+
ping_timeout=20.0, # wait for ping reply before dropping (None disables)
|
|
212
|
+
additional_headers={"X-Trace": "abc"}, # extra handshake headers
|
|
213
|
+
proxy="http://host:port", # proxy URL
|
|
214
|
+
)
|
|
215
|
+
```
|
|
216
|
+
|
|
141
217
|
## Error handling
|
|
142
218
|
|
|
143
219
|
```python
|
|
@@ -169,7 +245,9 @@ NewsdataException
|
|
|
169
245
|
│ ├── NewsdataAuthError (401 / 403)
|
|
170
246
|
│ ├── NewsdataRateLimitError (429; carries .retry_after)
|
|
171
247
|
│ └── NewsdataServerError (5xx)
|
|
172
|
-
|
|
248
|
+
├── NewsdataNetworkError (carries .original)
|
|
249
|
+
└── NewsdataWebSocketError (real-time stream)
|
|
250
|
+
└── NewsdataWebSocketAuthError (handshake 401 / 403, or policy-violation close 1008)
|
|
173
251
|
```
|
|
174
252
|
|
|
175
253
|
`NewsdataException` is always a valid catch-all.
|
|
@@ -205,7 +283,7 @@ client = NewsDataApiClient(
|
|
|
205
283
|
pagination_delay=1.0, # seconds between pages; default 1.0
|
|
206
284
|
max_result=None, # cap on merged results in scroll mode; default None (no cap)
|
|
207
285
|
max_pages=None, # cap on pages yielded in paginate mode; default None (no cap)
|
|
208
|
-
proxies={"https": "..."}, # passed
|
|
286
|
+
proxies={"https": "..."}, # passed with every request
|
|
209
287
|
accept_language="en", # Accept-Language header
|
|
210
288
|
include_headers=False, # if True, returned dicts include response_headers
|
|
211
289
|
base_url="...", # override for staging / proxied environments
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
|
-
`newsdataapi` is the official Python SDK for the [NewsData.io](https://newsdata.io) REST API. It wraps every endpoint (`latest`, `archive`, `sources`, `crypto`, `market`, `count`, `crypto/count`, `market/count`) with consistent retry, pagination, and error handling.
|
|
16
|
+
`newsdataapi` is the official Python SDK for the [NewsData.io](https://newsdata.io) REST API. It wraps every endpoint (`latest`, `archive`, `sources`, `crypto`, `market`, `count`, `crypto/count`, `market/count`) with consistent retry, pagination, and error handling. It also covers the real-time WebSocket service end to end with `NewsDataApiWebSocket`: register, list, and delete queries, and stream the matching news as it is published (sync or asyncio).
|
|
17
17
|
|
|
18
18
|
## Installation
|
|
19
19
|
|
|
@@ -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.10 through 3.14. The runtime dependencies are `requests` (REST) and `websockets` (real-time streaming).
|
|
31
31
|
|
|
32
32
|
## Quickstart
|
|
33
33
|
|
|
@@ -67,7 +67,7 @@ finally:
|
|
|
67
67
|
| `crypto_count_api(from_date, to_date)` | `/crypto/count` | Aggregate crypto counts |
|
|
68
68
|
| `market_count_api(from_date, to_date)` | `/market/count` | Aggregate market counts |
|
|
69
69
|
|
|
70
|
-
All endpoint parameters are keyword-only (except the required `from_date` / `to_date` on the count endpoints). Most accept either a single string or a `list[str]`; lists are comma-joined for the API.
|
|
70
|
+
All endpoint parameters are keyword-only (except the required `from_date` / `to_date` on the count endpoints). Most accept either a single string or a `list[str]`; lists are comma-joined for the API. The real-time WebSocket endpoints are covered by `NewsDataApiWebSocket` (see below).
|
|
71
71
|
|
|
72
72
|
See the [NewsData.io documentation](https://newsdata.io/documentation) — or the [OpenAPI 3.1 spec](https://newsdata.io/openapi.json) — for the full parameter reference.
|
|
73
73
|
|
|
@@ -87,6 +87,83 @@ for page in client.latest_api(q="news", paginate=True, max_pages=5):
|
|
|
87
87
|
|
|
88
88
|
`scroll` and `paginate` are mutually exclusive. `scroll=True` truncates strictly to `max_result`; `paginate=True` stops at `max_pages` or when the API returns no `nextPage`.
|
|
89
89
|
|
|
90
|
+
## Real-time news (WebSocket)
|
|
91
|
+
|
|
92
|
+
Register a query first — the returned `registration_id` identifies it from then on:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from newsdataapi import NewsDataApiClient, NewsDataApiWebSocket
|
|
96
|
+
|
|
97
|
+
client = NewsDataApiClient("YOUR_API_KEY")
|
|
98
|
+
ws = NewsDataApiWebSocket(client)
|
|
99
|
+
response = ws.websocket_register(q="bitcoin", language="en")
|
|
100
|
+
registration_id = response["results"]["registration_id"]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`websocket_register` accepts the familiar filter parameters (`q`, `country`, `language`, `domain`, …). Registering an identical query twice raises `NewsdataAPIError` with `status_code=409` — the existing id is in `e.response_body["results"]["registration_id"]`. `websocket_fetch()` lists every registered query, and `websocket_delete(registration_id)` removes one.
|
|
104
|
+
|
|
105
|
+
Then stream — each yielded response has the familiar `status` / `totalResults` / `results` shape:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
for response in ws.stream(registration_id):
|
|
109
|
+
for article in response["results"]:
|
|
110
|
+
print(article["title"], "-", article["link"])
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Use it as a context manager to close the connection promptly when you stop early (otherwise it closes when iteration ends):
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
with NewsDataApiWebSocket(client) as ws:
|
|
117
|
+
for response in ws.stream(registration_id):
|
|
118
|
+
print(response["totalResults"])
|
|
119
|
+
break
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Inside asyncio applications use `stream_async()` — the same class, same behavior, awaited iteration:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
import asyncio
|
|
126
|
+
|
|
127
|
+
async def main():
|
|
128
|
+
async with NewsDataApiWebSocket(client) as ws:
|
|
129
|
+
async for response in ws.stream_async(registration_id):
|
|
130
|
+
for article in response["results"]:
|
|
131
|
+
print(article["title"], "-", article["link"])
|
|
132
|
+
|
|
133
|
+
asyncio.run(main())
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Transient drops (network errors, server restarts, abnormal closes) are reconnected automatically with a capped exponential backoff. Pass `reconnect=False` to stop on the first disconnect instead. A permanent rejection — bad API key, missing WebSocket entitlement, unknown `registration_id`, device limit reached, or exhausted quota — raises `NewsdataWebSocketAuthError` and is **not** retried:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from newsdataapi import NewsdataWebSocketAuthError, NewsdataWebSocketError
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
for response in NewsDataApiWebSocket(client).stream(registration_id):
|
|
143
|
+
...
|
|
144
|
+
except NewsdataWebSocketAuthError as e:
|
|
145
|
+
print(f"rejected: {e}")
|
|
146
|
+
except NewsdataWebSocketError as e:
|
|
147
|
+
print(f"stream error: {e}")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
All connection options are keyword-only:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
ws = NewsDataApiWebSocket(
|
|
154
|
+
client,
|
|
155
|
+
base_url="wss://ws.newsdata.io/ws/event", # override for staging / self-hosted / proxied
|
|
156
|
+
reconnect=True, # auto-reconnect on transient drops; default True
|
|
157
|
+
reconnect_delay=1.0, # seconds before first reconnect (doubles each retry)
|
|
158
|
+
reconnect_delay_max=30.0, # cap on the reconnect delay
|
|
159
|
+
open_timeout=10.0, # handshake timeout (None disables)
|
|
160
|
+
ping_interval=20.0, # keepalive ping interval (None disables)
|
|
161
|
+
ping_timeout=20.0, # wait for ping reply before dropping (None disables)
|
|
162
|
+
additional_headers={"X-Trace": "abc"}, # extra handshake headers
|
|
163
|
+
proxy="http://host:port", # proxy URL
|
|
164
|
+
)
|
|
165
|
+
```
|
|
166
|
+
|
|
90
167
|
## Error handling
|
|
91
168
|
|
|
92
169
|
```python
|
|
@@ -118,7 +195,9 @@ NewsdataException
|
|
|
118
195
|
│ ├── NewsdataAuthError (401 / 403)
|
|
119
196
|
│ ├── NewsdataRateLimitError (429; carries .retry_after)
|
|
120
197
|
│ └── NewsdataServerError (5xx)
|
|
121
|
-
|
|
198
|
+
├── NewsdataNetworkError (carries .original)
|
|
199
|
+
└── NewsdataWebSocketError (real-time stream)
|
|
200
|
+
└── NewsdataWebSocketAuthError (handshake 401 / 403, or policy-violation close 1008)
|
|
122
201
|
```
|
|
123
202
|
|
|
124
203
|
`NewsdataException` is always a valid catch-all.
|
|
@@ -154,7 +233,7 @@ client = NewsDataApiClient(
|
|
|
154
233
|
pagination_delay=1.0, # seconds between pages; default 1.0
|
|
155
234
|
max_result=None, # cap on merged results in scroll mode; default None (no cap)
|
|
156
235
|
max_pages=None, # cap on pages yielded in paginate mode; default None (no cap)
|
|
157
|
-
proxies={"https": "..."}, # passed
|
|
236
|
+
proxies={"https": "..."}, # passed with every request
|
|
158
237
|
accept_language="en", # Accept-Language header
|
|
159
238
|
include_headers=False, # if True, returned dicts include response_headers
|
|
160
239
|
base_url="...", # override for staging / proxied environments
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Real-time news streaming example for the newsdataapi SDK.
|
|
2
|
+
|
|
3
|
+
Run with::
|
|
4
|
+
|
|
5
|
+
NEWSDATA_API_KEY=<key> python examples/websocket_usage.py
|
|
6
|
+
|
|
7
|
+
Set NEWSDATA_WS_MODE=async to run the asyncio example instead of the sync one.
|
|
8
|
+
|
|
9
|
+
Articles are matched by a registered query. If NEWSDATA_REGISTRATION_ID is
|
|
10
|
+
set, that query is streamed directly; otherwise the example registers a demo
|
|
11
|
+
query (``q="pizza"``) first and prints the resulting ``registration_id`` so
|
|
12
|
+
you can reuse it on the next run or remove it later with
|
|
13
|
+
``NewsDataApiWebSocket(client).websocket_delete(registration_id)``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
from newsdataapi import (
|
|
22
|
+
NewsDataApiClient,
|
|
23
|
+
NewsdataAPIError,
|
|
24
|
+
NewsDataApiWebSocket,
|
|
25
|
+
NewsdataWebSocketAuthError,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _client_and_registration() -> tuple[NewsDataApiClient, str]:
|
|
30
|
+
apikey = os.environ.get("NEWSDATA_API_KEY")
|
|
31
|
+
if not apikey:
|
|
32
|
+
raise SystemExit(
|
|
33
|
+
"Set NEWSDATA_API_KEY in your environment before running this example."
|
|
34
|
+
)
|
|
35
|
+
client = NewsDataApiClient(apikey)
|
|
36
|
+
registration_id = os.environ.get("NEWSDATA_REGISTRATION_ID")
|
|
37
|
+
if not registration_id:
|
|
38
|
+
registration_id = _register_demo_query(client)
|
|
39
|
+
return client, registration_id
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _register_demo_query(client: NewsDataApiClient) -> str:
|
|
43
|
+
"""Register a demo query and return its ``registration_id``.
|
|
44
|
+
|
|
45
|
+
Registering an identical query again answers HTTP 409 with the existing
|
|
46
|
+
id in the response body — reuse it instead of failing.
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
response = NewsDataApiWebSocket(client).websocket_register(q="pizza")
|
|
50
|
+
except NewsdataAPIError as exc:
|
|
51
|
+
if exc.status_code == 409 and exc.response_body:
|
|
52
|
+
registration_id: str = exc.response_body["results"]["registration_id"]
|
|
53
|
+
print(f"query already registered; reusing {registration_id}")
|
|
54
|
+
return registration_id
|
|
55
|
+
raise
|
|
56
|
+
registration_id = response["results"]["registration_id"]
|
|
57
|
+
print(f'registered demo query q="pizza" -> {registration_id}')
|
|
58
|
+
return registration_id
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def sync_example() -> None:
|
|
62
|
+
client, registration_id = _client_and_registration()
|
|
63
|
+
|
|
64
|
+
# Iterate to receive matched news as it is published. Transient drops are
|
|
65
|
+
# reconnected automatically; press Ctrl+C to stop. The context manager is
|
|
66
|
+
# optional — it just closes the socket promptly when the block exits.
|
|
67
|
+
with NewsDataApiWebSocket(client) as ws:
|
|
68
|
+
for response in ws.stream(registration_id):
|
|
69
|
+
for article in response["results"]:
|
|
70
|
+
print(article.get("title"), "-", article.get("link"))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def async_example() -> None:
|
|
74
|
+
client, registration_id = _client_and_registration()
|
|
75
|
+
|
|
76
|
+
# The async counterpart — same behavior, awaited iteration.
|
|
77
|
+
async with NewsDataApiWebSocket(client) as ws:
|
|
78
|
+
async for response in ws.stream_async(registration_id):
|
|
79
|
+
for article in response["results"]:
|
|
80
|
+
print(article.get("title"), "-", article.get("link"))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def main() -> None:
|
|
84
|
+
try:
|
|
85
|
+
if os.environ.get("NEWSDATA_WS_MODE") == "async":
|
|
86
|
+
asyncio.run(async_example())
|
|
87
|
+
else:
|
|
88
|
+
sync_example()
|
|
89
|
+
except NewsdataWebSocketAuthError as exc:
|
|
90
|
+
# Permanent rejection: bad key, no WebSocket entitlement, unknown
|
|
91
|
+
# registration_id, device limit, or exhausted quota.
|
|
92
|
+
print(f"connection rejected: {exc}")
|
|
93
|
+
except KeyboardInterrupt:
|
|
94
|
+
print("\nstopped.")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
main()
|
|
@@ -5,10 +5,11 @@ 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.10"
|
|
9
9
|
|
|
10
10
|
dependencies = [
|
|
11
11
|
"requests>=2.25,<3",
|
|
12
|
+
"websockets>=16,<17",
|
|
12
13
|
]
|
|
13
14
|
|
|
14
15
|
keywords = [
|
|
@@ -17,6 +18,8 @@ keywords = [
|
|
|
17
18
|
"newsdata-io",
|
|
18
19
|
"news-api",
|
|
19
20
|
"rest-api",
|
|
21
|
+
"websocket",
|
|
22
|
+
"websockets",
|
|
20
23
|
"api-client",
|
|
21
24
|
"python-sdk",
|
|
22
25
|
"sdk",
|
|
@@ -24,6 +27,8 @@ keywords = [
|
|
|
24
27
|
"financial-news",
|
|
25
28
|
"market-news",
|
|
26
29
|
"news-aggregator",
|
|
30
|
+
"realtime-news",
|
|
31
|
+
"news-stream",
|
|
27
32
|
]
|
|
28
33
|
|
|
29
34
|
classifiers = [
|
|
@@ -32,8 +37,6 @@ classifiers = [
|
|
|
32
37
|
"License :: OSI Approved :: MIT License",
|
|
33
38
|
"Operating System :: OS Independent",
|
|
34
39
|
"Programming Language :: Python :: 3",
|
|
35
|
-
"Programming Language :: Python :: 3.8",
|
|
36
|
-
"Programming Language :: Python :: 3.9",
|
|
37
40
|
"Programming Language :: Python :: 3.10",
|
|
38
41
|
"Programming Language :: Python :: 3.11",
|
|
39
42
|
"Programming Language :: Python :: 3.12",
|
|
@@ -87,7 +90,7 @@ addopts = [
|
|
|
87
90
|
|
|
88
91
|
[tool.ruff]
|
|
89
92
|
line-length = 100
|
|
90
|
-
target-version = "
|
|
93
|
+
target-version = "py310"
|
|
91
94
|
|
|
92
95
|
[tool.ruff.lint]
|
|
93
96
|
select = [
|
|
@@ -104,6 +107,6 @@ select = [
|
|
|
104
107
|
]
|
|
105
108
|
|
|
106
109
|
[tool.mypy]
|
|
107
|
-
python_version = "3.
|
|
110
|
+
python_version = "3.10"
|
|
108
111
|
strict = true
|
|
109
112
|
files = ["src/newsdataapi"]
|
|
@@ -10,12 +10,16 @@ from .exceptions import (
|
|
|
10
10
|
NewsdataRateLimitError,
|
|
11
11
|
NewsdataServerError,
|
|
12
12
|
NewsdataValidationError,
|
|
13
|
+
NewsdataWebSocketAuthError,
|
|
14
|
+
NewsdataWebSocketError,
|
|
13
15
|
)
|
|
16
|
+
from .websocket import NewsDataApiWebSocket
|
|
14
17
|
|
|
15
|
-
__version__ = "0.
|
|
18
|
+
__version__ = "0.3.0"
|
|
16
19
|
|
|
17
20
|
__all__ = [
|
|
18
21
|
"NewsDataApiClient",
|
|
22
|
+
"NewsDataApiWebSocket",
|
|
19
23
|
"NewsdataAPIError",
|
|
20
24
|
"NewsdataAuthError",
|
|
21
25
|
"NewsdataException",
|
|
@@ -23,6 +27,8 @@ __all__ = [
|
|
|
23
27
|
"NewsdataRateLimitError",
|
|
24
28
|
"NewsdataServerError",
|
|
25
29
|
"NewsdataValidationError",
|
|
30
|
+
"NewsdataWebSocketAuthError",
|
|
31
|
+
"NewsdataWebSocketError",
|
|
26
32
|
"__version__",
|
|
27
33
|
"save_to_csv",
|
|
28
34
|
]
|
|
@@ -1120,8 +1120,10 @@ class NewsDataApiClient:
|
|
|
1120
1120
|
self,
|
|
1121
1121
|
endpoint: str,
|
|
1122
1122
|
params: Mapping[str, Any],
|
|
1123
|
+
*,
|
|
1124
|
+
method: str = "GET",
|
|
1123
1125
|
) -> dict[str, Any]:
|
|
1124
|
-
"""Execute a single GET
|
|
1126
|
+
"""Execute a single request (GET by default), with retries.
|
|
1125
1127
|
|
|
1126
1128
|
Returns the parsed JSON body on success. Raises a
|
|
1127
1129
|
:class:`NewsdataException` subclass on permanent failure.
|
|
@@ -1135,8 +1137,9 @@ class NewsDataApiClient:
|
|
|
1135
1137
|
|
|
1136
1138
|
t0 = time.perf_counter()
|
|
1137
1139
|
try:
|
|
1138
|
-
logger.info("
|
|
1139
|
-
response = self._session.
|
|
1140
|
+
logger.info("%s %s", method, _redact_url(full_url))
|
|
1141
|
+
response = self._session.request(
|
|
1142
|
+
method,
|
|
1140
1143
|
full_url,
|
|
1141
1144
|
proxies=self.proxies,
|
|
1142
1145
|
timeout=self.request_timeout,
|
|
@@ -15,6 +15,9 @@ MARKET_ENDPOINT = "market"
|
|
|
15
15
|
COUNT_ENDPOINT = "count"
|
|
16
16
|
CRYPTO_COUNT_ENDPOINT = "crypto/count"
|
|
17
17
|
MARKET_COUNT_ENDPOINT = "market/count"
|
|
18
|
+
WEBSOCKET_REGISTER_ENDPOINT = "websocket/register"
|
|
19
|
+
WEBSOCKET_FETCH_ENDPOINT = "websocket/fetch"
|
|
20
|
+
WEBSOCKET_DELETE_ENDPOINT = "websocket/delete"
|
|
18
21
|
|
|
19
22
|
# HTTP defaults.
|
|
20
23
|
DEFAULT_REQUEST_TIMEOUT = 30 # seconds
|
|
@@ -22,3 +25,10 @@ DEFAULT_MAX_RETRIES = 5
|
|
|
22
25
|
DEFAULT_RETRY_BACKOFF = 2.0 # base seconds; doubles each attempt
|
|
23
26
|
DEFAULT_RETRY_BACKOFF_MAX = 60.0 # cap on any single retry sleep
|
|
24
27
|
PAGINATION_DELAY = 1.0 # seconds slept between pages
|
|
28
|
+
|
|
29
|
+
# Real-time WebSocket defaults (newsdataapi.NewsDataApiWebSocket).
|
|
30
|
+
WS_BASE_URL = "wss://ws.newsdata.io/ws/event"
|
|
31
|
+
WS_NEWS_TYPE = "latest" # feed a registered query matches against
|
|
32
|
+
WS_POLICY_VIOLATION = 1008 # close code for a permanent connection rejection
|
|
33
|
+
WS_RECONNECT_DELAY = 1.0 # seconds before the first reconnect; doubles each retry
|
|
34
|
+
WS_RECONNECT_DELAY_MAX = 30.0 # cap on the reconnect delay
|
|
@@ -96,3 +96,14 @@ class NewsdataNetworkError(NewsdataException):
|
|
|
96
96
|
) -> None:
|
|
97
97
|
super().__init__(message)
|
|
98
98
|
self.original = original
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class NewsdataWebSocketError(NewsdataException):
|
|
102
|
+
"""A real-time WebSocket consumer error
|
|
103
|
+
(:class:`newsdataapi.NewsDataApiWebSocket`)."""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class NewsdataWebSocketAuthError(NewsdataWebSocketError):
|
|
107
|
+
"""The server rejected the WebSocket connection — bad API key, missing
|
|
108
|
+
WebSocket entitlement, unknown ``registration_id``, device limit reached,
|
|
109
|
+
or exhausted quota. Not retried."""
|