Skip to content

Commit 4a1a933

Browse files
authored
Merge pull request #30 from newsdataapi/new_updates
Restore Python 3.8 support for REST client; bump version to 0.3.3
2 parents 06fdbd4 + a1d3bfd commit 4a1a933

8 files changed

Lines changed: 705 additions & 45 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
strategy:
2222
fail-fast: false
2323
matrix:
24-
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
24+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
2525
steps:
2626
- uses: actions/checkout@v4
2727

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ If you use [uv](https://github.com/astral-sh/uv):
2727
uv add newsdataapi
2828
```
2929

30-
Supports Python 3.10 through 3.14. The runtime dependencies are `requests` (REST) and `websockets` (real-time streaming).
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.
3131

3232
## Quickstart
3333

@@ -89,7 +89,7 @@ for page in client.latest_api(q="news", paginate=True, max_pages=5):
8989

9090
## Real-time news (WebSocket)
9191

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:
9393

9494
```python
9595
from newsdataapi import NewsDataApiClient, NewsDataApiWebSocket

‎pyproject.toml‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ dynamic = ["version"]
55
readme = "README.md"
66
license = { file = "LICENSE" }
77
authors = [{ name = "NewsData.io", email = "contact@newsdata.io" }]
8-
requires-python = ">=3.10"
8+
requires-python = ">=3.8"
99

1010
dependencies = [
1111
"requests>=2.25,<3",
12-
"websockets>=16,<17",
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'",
1315
]
1416

1517
keywords = [
@@ -37,6 +39,8 @@ classifiers = [
3739
"License :: OSI Approved :: MIT License",
3840
"Operating System :: OS Independent",
3941
"Programming Language :: Python :: 3",
42+
"Programming Language :: Python :: 3.8",
43+
"Programming Language :: Python :: 3.9",
4044
"Programming Language :: Python :: 3.10",
4145
"Programming Language :: Python :: 3.11",
4246
"Programming Language :: Python :: 3.12",
@@ -90,7 +94,7 @@ addopts = [
9094

9195
[tool.ruff]
9296
line-length = 100
93-
target-version = "py310"
97+
target-version = "py38"
9498

9599
[tool.ruff.lint]
96100
select = [
@@ -107,6 +111,7 @@ select = [
107111
]
108112

109113
[tool.mypy]
110-
python_version = "3.10"
114+
# mypy no longer accepts 3.8 as a target; the 3.8 CI job covers it at runtime.
115+
python_version = "3.9"
111116
strict = true
112117
files = ["src/newsdataapi"]

‎src/newsdataapi/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
)
1616
from .websocket import NewsDataApiWebSocket
1717

18-
__version__ = "0.3.2"
18+
__version__ = "0.3.3"
1919

2020
__all__ = [
2121
"NewsDataApiClient",

‎src/newsdataapi/websocket.py‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,11 @@
1111
import asyncio
1212
import json
1313
import logging
14+
import sys
1415
import time
1516
from collections.abc import AsyncIterator, Iterator
1617
from typing import Any
1718

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-
2219
from . import constants
2320
from .client import NewsDataApiClient, _redact_url, _validate_params
2421
from .exceptions import (
@@ -27,6 +24,15 @@
2724
NewsdataWebSocketError,
2825
)
2926

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+
3036
logger = logging.getLogger(__name__)
3137

3238

@@ -109,6 +115,9 @@ class NewsDataApiWebSocket:
109115
always raises :class:`~newsdataapi.NewsdataWebSocketAuthError` and is
110116
never retried.
111117
118+
Requires Python 3.10 or newer; on older versions the constructor raises
119+
:class:`~newsdataapi.NewsdataWebSocketError`.
120+
112121
Args:
113122
api_client: The :class:`~newsdataapi.NewsDataApiClient` that supplies
114123
the API key and performs the management HTTP calls. Not closed by
@@ -147,6 +156,12 @@ def __init__(
147156
additional_headers: dict[str, str] | None = None,
148157
proxy: str | None = None,
149158
) -> 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+
)
150165
self.api_client = api_client
151166
self._base_url = base_url
152167
self._reconnect = reconnect

‎tests/conftest.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22

33
from __future__ import annotations
44

5+
import sys
56
from collections.abc import Iterator
67

78
import pytest
89
import responses
910

1011
from newsdataapi import NewsDataApiClient
1112

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+
1216

1317
@pytest.fixture
1418
def mocked_responses() -> Iterator[responses.RequestsMock]:

‎tests/test_unit.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from __future__ import annotations
99

10+
import sys
1011
from datetime import datetime, timedelta, timezone
1112
from email.utils import format_datetime
1213
from pathlib import Path
@@ -25,6 +26,7 @@
2526
NewsdataRateLimitError,
2627
NewsdataServerError,
2728
NewsdataValidationError,
29+
NewsdataWebSocketError,
2830
save_to_csv,
2931
)
3032
from newsdataapi.client import (
@@ -1348,7 +1350,23 @@ def test_raw_query_accepts_excludedomainurl(
13481350
WS_FETCH_URL = "https://newsdata.io/api/1/websocket/fetch"
13491351
WS_DELETE_URL = "https://newsdata.io/api/1/websocket/delete"
13501352

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+
13511368

1369+
@requires_websocket
13521370
def test_websocket_register_posts_query_params(
13531371
client: NewsDataApiClient,
13541372
mocked_responses: responses.RequestsMock,
@@ -1375,6 +1393,7 @@ def test_websocket_register_posts_query_params(
13751393
assert not sent.body # params travel in the query string, not a body
13761394

13771395

1396+
@requires_websocket
13781397
def test_websocket_register_always_sends_news_type_latest(
13791398
client: NewsDataApiClient,
13801399
mocked_responses: responses.RequestsMock,
@@ -1390,6 +1409,7 @@ def test_websocket_register_always_sends_news_type_latest(
13901409
assert "news_type=latest" in sent_url
13911410

13921411

1412+
@requires_websocket
13931413
def test_websocket_register_mutex_validation_applies(
13941414
client: NewsDataApiClient,
13951415
) -> None:
@@ -1398,6 +1418,7 @@ def test_websocket_register_mutex_validation_applies(
13981418
NewsDataApiWebSocket(client).websocket_register(q="a", qInTitle="b")
13991419

14001420

1421+
@requires_websocket
14011422
def test_websocket_register_raw_query(
14021423
client: NewsDataApiClient,
14031424
mocked_responses: responses.RequestsMock,
@@ -1414,6 +1435,7 @@ def test_websocket_register_raw_query(
14141435
assert "news_type=latest" in sent_url
14151436

14161437

1438+
@requires_websocket
14171439
def test_websocket_register_raw_query_rejects_unknown_param(
14181440
client: NewsDataApiClient,
14191441
) -> None:
@@ -1422,6 +1444,7 @@ def test_websocket_register_raw_query_rejects_unknown_param(
14221444
NewsDataApiWebSocket(client).websocket_register(raw_query="q=pizza&size=10")
14231445

14241446

1447+
@requires_websocket
14251448
def test_websocket_register_sends_excludedomainurl(
14261449
client: NewsDataApiClient,
14271450
mocked_responses: responses.RequestsMock,
@@ -1439,6 +1462,7 @@ def test_websocket_register_sends_excludedomainurl(
14391462
assert "excludedomainurl=bbc.com%2Ccnn.com" in sent_url
14401463

14411464

1465+
@requires_websocket
14421466
def test_websocket_register_duplicate_409(
14431467
client: NewsDataApiClient,
14441468
mocked_responses: responses.RequestsMock,
@@ -1466,6 +1490,7 @@ def test_websocket_register_duplicate_409(
14661490
assert len(mocked_responses.calls) == 1 # 4xx is never retried
14671491

14681492

1493+
@requires_websocket
14691494
def test_websocket_register_403_raises_auth(
14701495
client: NewsDataApiClient,
14711496
mocked_responses: responses.RequestsMock,
@@ -1476,6 +1501,7 @@ def test_websocket_register_403_raises_auth(
14761501
NewsDataApiWebSocket(client).websocket_register(q="pizza")
14771502

14781503

1504+
@requires_websocket
14791505
def test_websocket_register_429_exhausts_to_rate_limit(
14801506
client: NewsDataApiClient,
14811507
mocked_responses: responses.RequestsMock,
@@ -1488,6 +1514,7 @@ def test_websocket_register_429_exhausts_to_rate_limit(
14881514
NewsDataApiWebSocket(client).websocket_register(q="pizza")
14891515

14901516

1517+
@requires_websocket
14911518
def test_websocket_fetch_returns_queries(
14921519
client: NewsDataApiClient,
14931520
mocked_responses: responses.RequestsMock,
@@ -1518,6 +1545,7 @@ def test_websocket_fetch_returns_queries(
15181545
assert "apikey=test_key_xxx" in sent_url
15191546

15201547

1548+
@requires_websocket
15211549
def test_websocket_delete_sends_registration_id(
15221550
client: NewsDataApiClient,
15231551
mocked_responses: responses.RequestsMock,
@@ -1537,6 +1565,7 @@ def test_websocket_delete_sends_registration_id(
15371565
assert "registration_id=abc123" in sent_url
15381566

15391567

1568+
@requires_websocket
15401569
def test_websocket_delete_unknown_id_404(
15411570
client: NewsDataApiClient,
15421571
mocked_responses: responses.RequestsMock,
@@ -1547,6 +1576,7 @@ def test_websocket_delete_unknown_id_404(
15471576
assert exc_info.value.status_code == 404
15481577

15491578

1579+
@requires_websocket
15501580
@pytest.mark.parametrize("bad_id", ["", None, 123])
15511581
def test_websocket_delete_rejects_bad_registration_id(
15521582
client: NewsDataApiClient,

0 commit comments

Comments
 (0)