newsdataapi 0.3.2__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.
Files changed (24) hide show
  1. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/.github/workflows/ci.yml +1 -1
  2. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/PKG-INFO +7 -5
  3. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/README.md +2 -2
  4. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/pyproject.toml +9 -4
  5. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/src/newsdataapi/__init__.py +1 -1
  6. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/src/newsdataapi/websocket.py +19 -4
  7. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/tests/conftest.py +4 -0
  8. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/tests/test_unit.py +30 -0
  9. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/uv.lock +639 -33
  10. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/.github/workflows/publish.yml +0 -0
  11. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/.gitignore +0 -0
  12. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/.python-version +0 -0
  13. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/LICENSE +0 -0
  14. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/examples/basic_usage.py +0 -0
  15. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/examples/websocket_usage.py +0 -0
  16. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/newsdata-logo.png +0 -0
  17. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/src/newsdataapi/client.py +0 -0
  18. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/src/newsdataapi/constants.py +0 -0
  19. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/src/newsdataapi/csv_export.py +0 -0
  20. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/src/newsdataapi/exceptions.py +0 -0
  21. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/src/newsdataapi/py.typed +0 -0
  22. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/tests/__init__.py +0 -0
  23. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/tests/test_integration.py +0 -0
  24. {newsdataapi-0.3.2 → newsdataapi-0.3.3}/tests/test_websocket.py +0 -0
@@ -21,7 +21,7 @@ jobs:
21
21
  strategy:
22
22
  fail-fast: false
23
23
  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"]
25
25
  steps:
26
26
  - uses: actions/checkout@v4
27
27
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: newsdataapi
3
- Version: 0.3.2
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.10
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.10 through 3.14. The runtime dependencies are `requests` (REST) and `websockets` (real-time streaming).
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.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.
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.10"
8
+ requires-python = ">=3.8"
9
9
 
10
10
  dependencies = [
11
11
  "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'",
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 = "py310"
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
- 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"
111
116
  strict = true
112
117
  files = ["src/newsdataapi"]
@@ -15,7 +15,7 @@ from .exceptions import (
15
15
  )
16
16
  from .websocket import NewsDataApiWebSocket
17
17
 
18
- __version__ = "0.3.2"
18
+ __version__ = "0.3.3"
19
19
 
20
20
  __all__ = [
21
21
  "NewsDataApiClient",
@@ -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
@@ -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 (
@@ -1348,7 +1350,23 @@ WS_REGISTER_URL = "https://newsdata.io/api/1/websocket/register"
1348
1350
  WS_FETCH_URL = "https://newsdata.io/api/1/websocket/fetch"
1349
1351
  WS_DELETE_URL = "https://newsdata.io/api/1/websocket/delete"
1350
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
+
1351
1368
 
1369
+ @requires_websocket
1352
1370
  def test_websocket_register_posts_query_params(
1353
1371
  client: NewsDataApiClient,
1354
1372
  mocked_responses: responses.RequestsMock,
@@ -1375,6 +1393,7 @@ def test_websocket_register_posts_query_params(
1375
1393
  assert not sent.body # params travel in the query string, not a body
1376
1394
 
1377
1395
 
1396
+ @requires_websocket
1378
1397
  def test_websocket_register_always_sends_news_type_latest(
1379
1398
  client: NewsDataApiClient,
1380
1399
  mocked_responses: responses.RequestsMock,
@@ -1390,6 +1409,7 @@ def test_websocket_register_always_sends_news_type_latest(
1390
1409
  assert "news_type=latest" in sent_url
1391
1410
 
1392
1411
 
1412
+ @requires_websocket
1393
1413
  def test_websocket_register_mutex_validation_applies(
1394
1414
  client: NewsDataApiClient,
1395
1415
  ) -> None:
@@ -1398,6 +1418,7 @@ def test_websocket_register_mutex_validation_applies(
1398
1418
  NewsDataApiWebSocket(client).websocket_register(q="a", qInTitle="b")
1399
1419
 
1400
1420
 
1421
+ @requires_websocket
1401
1422
  def test_websocket_register_raw_query(
1402
1423
  client: NewsDataApiClient,
1403
1424
  mocked_responses: responses.RequestsMock,
@@ -1414,6 +1435,7 @@ def test_websocket_register_raw_query(
1414
1435
  assert "news_type=latest" in sent_url
1415
1436
 
1416
1437
 
1438
+ @requires_websocket
1417
1439
  def test_websocket_register_raw_query_rejects_unknown_param(
1418
1440
  client: NewsDataApiClient,
1419
1441
  ) -> None:
@@ -1422,6 +1444,7 @@ def test_websocket_register_raw_query_rejects_unknown_param(
1422
1444
  NewsDataApiWebSocket(client).websocket_register(raw_query="q=pizza&size=10")
1423
1445
 
1424
1446
 
1447
+ @requires_websocket
1425
1448
  def test_websocket_register_sends_excludedomainurl(
1426
1449
  client: NewsDataApiClient,
1427
1450
  mocked_responses: responses.RequestsMock,
@@ -1439,6 +1462,7 @@ def test_websocket_register_sends_excludedomainurl(
1439
1462
  assert "excludedomainurl=bbc.com%2Ccnn.com" in sent_url
1440
1463
 
1441
1464
 
1465
+ @requires_websocket
1442
1466
  def test_websocket_register_duplicate_409(
1443
1467
  client: NewsDataApiClient,
1444
1468
  mocked_responses: responses.RequestsMock,
@@ -1466,6 +1490,7 @@ def test_websocket_register_duplicate_409(
1466
1490
  assert len(mocked_responses.calls) == 1 # 4xx is never retried
1467
1491
 
1468
1492
 
1493
+ @requires_websocket
1469
1494
  def test_websocket_register_403_raises_auth(
1470
1495
  client: NewsDataApiClient,
1471
1496
  mocked_responses: responses.RequestsMock,
@@ -1476,6 +1501,7 @@ def test_websocket_register_403_raises_auth(
1476
1501
  NewsDataApiWebSocket(client).websocket_register(q="pizza")
1477
1502
 
1478
1503
 
1504
+ @requires_websocket
1479
1505
  def test_websocket_register_429_exhausts_to_rate_limit(
1480
1506
  client: NewsDataApiClient,
1481
1507
  mocked_responses: responses.RequestsMock,
@@ -1488,6 +1514,7 @@ def test_websocket_register_429_exhausts_to_rate_limit(
1488
1514
  NewsDataApiWebSocket(client).websocket_register(q="pizza")
1489
1515
 
1490
1516
 
1517
+ @requires_websocket
1491
1518
  def test_websocket_fetch_returns_queries(
1492
1519
  client: NewsDataApiClient,
1493
1520
  mocked_responses: responses.RequestsMock,
@@ -1518,6 +1545,7 @@ def test_websocket_fetch_returns_queries(
1518
1545
  assert "apikey=test_key_xxx" in sent_url
1519
1546
 
1520
1547
 
1548
+ @requires_websocket
1521
1549
  def test_websocket_delete_sends_registration_id(
1522
1550
  client: NewsDataApiClient,
1523
1551
  mocked_responses: responses.RequestsMock,
@@ -1537,6 +1565,7 @@ def test_websocket_delete_sends_registration_id(
1537
1565
  assert "registration_id=abc123" in sent_url
1538
1566
 
1539
1567
 
1568
+ @requires_websocket
1540
1569
  def test_websocket_delete_unknown_id_404(
1541
1570
  client: NewsDataApiClient,
1542
1571
  mocked_responses: responses.RequestsMock,
@@ -1547,6 +1576,7 @@ def test_websocket_delete_unknown_id_404(
1547
1576
  assert exc_info.value.status_code == 404
1548
1577
 
1549
1578
 
1579
+ @requires_websocket
1550
1580
  @pytest.mark.parametrize("bad_id", ["", None, 123])
1551
1581
  def test_websocket_delete_rejects_bad_registration_id(
1552
1582
  client: NewsDataApiClient,