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.
Files changed (24) hide show
  1. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/.github/workflows/ci.yml +1 -1
  2. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/PKG-INFO +89 -11
  3. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/README.md +84 -5
  4. newsdataapi-0.3.0/examples/websocket_usage.py +98 -0
  5. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/pyproject.toml +8 -5
  6. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/src/newsdataapi/__init__.py +7 -1
  7. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/src/newsdataapi/client.py +6 -3
  8. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/src/newsdataapi/constants.py +10 -0
  9. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/src/newsdataapi/exceptions.py +11 -0
  10. newsdataapi-0.3.0/src/newsdataapi/websocket.py +361 -0
  11. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/tests/test_unit.py +201 -0
  12. newsdataapi-0.3.0/tests/test_websocket.py +544 -0
  13. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/uv.lock +104 -638
  14. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/.github/workflows/publish.yml +0 -0
  15. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/.gitignore +0 -0
  16. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/.python-version +0 -0
  17. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/LICENSE +0 -0
  18. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/examples/basic_usage.py +0 -0
  19. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/newsdata-logo.png +0 -0
  20. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/src/newsdataapi/csv_export.py +0 -0
  21. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/src/newsdataapi/py.typed +0 -0
  22. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/tests/__init__.py +0 -0
  23. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/tests/conftest.py +0 -0
  24. {newsdataapi-0.2.3 → newsdataapi-0.3.0}/tests/test_integration.py +0 -0
@@ -21,7 +21,7 @@ jobs:
21
21
  strategy:
22
22
  fail-fast: false
23
23
  matrix:
24
- python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
24
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
25
25
  steps:
26
26
  - uses: actions/checkout@v4
27
27
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: newsdataapi
3
- Version: 0.2.3
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.8
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.8 through 3.14. The only runtime dependency is `requests`.
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
- └── NewsdataNetworkError (carries .original)
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 to requests.Session.get
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.8 through 3.14. The only runtime dependency is `requests`.
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
- └── NewsdataNetworkError (carries .original)
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 to requests.Session.get
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"
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 = "py38"
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.9"
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.2.3"
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 request, with retries.
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("GET %s", _redact_url(full_url))
1139
- response = self._session.get(
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."""