From 793eea95ef0ff25f942c615643cea0392e9dd87f Mon Sep 17 00:00:00 2001 From: himanshu Date: Mon, 28 Sep 2026 15:10:53 +0530 Subject: [PATCH] Add excludedomainurl filter; bump version to 0.3.2 --- src/newsdataapi/__init__.py | 2 +- src/newsdataapi/client.py | 20 +++++++++- src/newsdataapi/websocket.py | 2 + tests/test_unit.py | 74 +++++++++++++++++++++++++++++++++++- 4 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/newsdataapi/__init__.py b/src/newsdataapi/__init__.py index 047e27a..2c15fe2 100644 --- a/src/newsdataapi/__init__.py +++ b/src/newsdataapi/__init__.py @@ -15,7 +15,7 @@ ) from .websocket import NewsDataApiWebSocket -__version__ = "0.3.1" +__version__ = "0.3.2" __all__ = [ "NewsDataApiClient", diff --git a/src/newsdataapi/client.py b/src/newsdataapi/client.py index 677ba16..d021c62 100644 --- a/src/newsdataapi/client.py +++ b/src/newsdataapi/client.py @@ -47,6 +47,7 @@ "domain", "domainurl", "excludedomain", + "excludedomainurl", "timezone", "from_date", "to_date", @@ -81,7 +82,7 @@ ("country", "excludecountry"), ("category", "excludecategory"), ("language", "excludelanguage"), - ("domain", "domainurl", "excludedomain"), + ("domain", "domainurl", "excludedomain", "excludedomainurl"), ) _QUOTA_EXHAUSTED_CODES = frozenset({"ApiKeyLimitExceeded", "ApiLimitExceeded"}) @@ -103,7 +104,8 @@ def _validate_params(user_params: Mapping[str, Any]) -> dict[str, Any]: * Server-side mutex groups (``q``/``qInTitle``/``qInMeta``, ``country``/``excludecountry``, ``category``/``excludecategory``, ``language``/``excludelanguage``, and - ``domain``/``domainurl``/``excludedomain``) are enforced client-side; + ``domain``/``domainurl``/``excludedomain``/``excludedomainurl``) are + enforced client-side; setting more than one from any group raises ``NewsdataValidationError`` before the request leaves. * ``sentiment_score`` requires ``sentiment`` to be set; passing @@ -480,6 +482,7 @@ def latest_api( domain: str | list[str] | None = None, domainurl: str | list[str] | None = None, excludedomain: str | list[str] | None = None, + excludedomainurl: str | list[str] | None = None, prioritydomain: str | None = None, timeframe: int | str | None = None, timezone: str | None = None, @@ -528,6 +531,7 @@ def latest_api( "domain": domain, "domainurl": domainurl, "excludedomain": excludedomain, + "excludedomainurl": excludedomainurl, "prioritydomain": prioritydomain, "timeframe": str(timeframe) if timeframe is not None else None, "timezone": timezone, @@ -571,6 +575,7 @@ def archive_api( domain: str | list[str] | None = None, domainurl: str | list[str] | None = None, excludedomain: str | list[str] | None = None, + excludedomainurl: str | list[str] | None = None, prioritydomain: str | None = None, timezone: str | None = None, size: int | None = None, @@ -617,6 +622,7 @@ def archive_api( "domain": domain, "domainurl": domainurl, "excludedomain": excludedomain, + "excludedomainurl": excludedomainurl, "prioritydomain": prioritydomain, "timezone": timezone, "size": size, @@ -684,6 +690,7 @@ def crypto_api( domain: str | list[str] | None = None, domainurl: str | list[str] | None = None, excludedomain: str | list[str] | None = None, + excludedomainurl: str | list[str] | None = None, prioritydomain: str | None = None, timeframe: int | str | None = None, timezone: str | None = None, @@ -723,6 +730,7 @@ def crypto_api( "domain": domain, "domainurl": domainurl, "excludedomain": excludedomain, + "excludedomainurl": excludedomainurl, "prioritydomain": prioritydomain, "timeframe": str(timeframe) if timeframe is not None else None, "timezone": timezone, @@ -762,6 +770,7 @@ def market_api( domain: str | list[str] | None = None, domainurl: str | list[str] | None = None, excludedomain: str | list[str] | None = None, + excludedomainurl: str | list[str] | None = None, language: str | list[str] | None = None, excludelanguage: str | list[str] | None = None, prioritydomain: str | None = None, @@ -807,6 +816,7 @@ def market_api( "domain": domain, "domainurl": domainurl, "excludedomain": excludedomain, + "excludedomainurl": excludedomainurl, "language": language, "excludelanguage": excludelanguage, "prioritydomain": prioritydomain, @@ -854,6 +864,7 @@ def count_api( domain: str | list[str] | None = None, domainurl: str | list[str] | None = None, excludedomain: str | list[str] | None = None, + excludedomainurl: str | list[str] | None = None, full_content: bool | None = None, image: bool | None = None, video: bool | None = None, @@ -901,6 +912,7 @@ def count_api( "domain": domain, "domainurl": domainurl, "excludedomain": excludedomain, + "excludedomainurl": excludedomainurl, "full_content": full_content, "image": image, "video": video, @@ -940,6 +952,7 @@ def crypto_count_api( domain: str | list[str] | None = None, domainurl: str | list[str] | None = None, excludedomain: str | list[str] | None = None, + excludedomainurl: str | list[str] | None = None, full_content: bool | None = None, image: bool | None = None, video: bool | None = None, @@ -978,6 +991,7 @@ def crypto_count_api( "domain": domain, "domainurl": domainurl, "excludedomain": excludedomain, + "excludedomainurl": excludedomainurl, "full_content": full_content, "image": image, "video": video, @@ -1011,6 +1025,7 @@ def market_count_api( domain: str | list[str] | None = None, domainurl: str | list[str] | None = None, excludedomain: str | list[str] | None = None, + excludedomainurl: str | list[str] | None = None, language: str | list[str] | None = None, excludelanguage: str | list[str] | None = None, full_content: bool | None = None, @@ -1055,6 +1070,7 @@ def market_count_api( "domain": domain, "domainurl": domainurl, "excludedomain": excludedomain, + "excludedomainurl": excludedomainurl, "language": language, "excludelanguage": excludelanguage, "full_content": full_content, diff --git a/src/newsdataapi/websocket.py b/src/newsdataapi/websocket.py index 83cf195..297d13b 100644 --- a/src/newsdataapi/websocket.py +++ b/src/newsdataapi/websocket.py @@ -177,6 +177,7 @@ def websocket_register( domain: str | list[str] | None = None, domainurl: str | list[str] | None = None, excludedomain: str | list[str] | None = None, + excludedomainurl: str | list[str] | None = None, prioritydomain: str | None = None, timezone: str | None = None, full_content: bool | None = None, @@ -212,6 +213,7 @@ def websocket_register( "domain": domain, "domainurl": domainurl, "excludedomain": excludedomain, + "excludedomainurl": excludedomainurl, "prioritydomain": prioritydomain, "timezone": timezone, "full_content": full_content, diff --git a/tests/test_unit.py b/tests/test_unit.py index 418a497..c616c6f 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -148,13 +148,23 @@ def test_validate_size_negative_rejected() -> None: ({"country": "us", "excludecountry": "gb"}, "country"), ({"category": "business", "excludecategory": "sports"}, "category"), ({"language": "en", "excludelanguage": "fr"}, "language"), - # Domain 3-way mutex (any two of domain/domainurl/excludedomain) + # Domain 4-way mutex (any two of + # domain/domainurl/excludedomain/excludedomainurl) ({"domain": "cnn.com", "domainurl": "https://bbc.com"}, "domain"), ({"domain": "cnn.com", "excludedomain": "fox.com"}, "domain"), ( {"domainurl": "https://bbc.com", "excludedomain": "fox.com"}, "domainurl", ), + ({"domain": "cnn.com", "excludedomainurl": "fox.com"}, "domain"), + ( + {"domainurl": "https://bbc.com", "excludedomainurl": "fox.com"}, + "domainurl", + ), + ( + {"excludedomain": "cnn.com", "excludedomainurl": "fox.com"}, + "excludedomain", + ), ( { "domain": "cnn.com", @@ -1285,6 +1295,51 @@ def test_count_endpoint_url_resolves( assert len(mocked_responses.calls) == 1 +@pytest.mark.parametrize( + ("method_name", "endpoint_path", "args"), + [ + ("latest_api", "latest", ()), + ("archive_api", "archive", ()), + ("crypto_api", "crypto", ()), + ("market_api", "market", ()), + ("count_api", "count", ("2024-01-01", "2024-01-31")), + ("crypto_count_api", "crypto/count", ("2024-01-01", "2024-01-31")), + ("market_count_api", "market/count", ("2024-01-01", "2024-01-31")), + ], +) +def test_excludedomainurl_sent_as_csv( + client: NewsDataApiClient, + mocked_responses: responses.RequestsMock, + method_name: str, + endpoint_path: str, + args: tuple[str, ...], +) -> None: + mocked_responses.get( + f"https://newsdata.io/api/1/{endpoint_path}", + json={"status": "success", "results": []}, + status=200, + ) + getattr(client, method_name)(*args, excludedomainurl=["bbc.com", "cnn.com"]) + sent_url = mocked_responses.calls[0].request.url + assert sent_url is not None + assert "excludedomainurl=bbc.com%2Ccnn.com" in sent_url + + +def test_raw_query_accepts_excludedomainurl( + client: NewsDataApiClient, + mocked_responses: responses.RequestsMock, +) -> None: + mocked_responses.get( + LATEST_URL, + json={"status": "success", "results": []}, + status=200, + ) + client.latest_api(raw_query="excludedomainurl=bbc.com") + sent_url = mocked_responses.calls[0].request.url + assert sent_url is not None + assert "excludedomainurl=bbc.com" in sent_url + + # =========================================================================== # WebSocket query management (register / fetch / delete) # =========================================================================== @@ -1367,6 +1422,23 @@ def test_websocket_register_raw_query_rejects_unknown_param( NewsDataApiWebSocket(client).websocket_register(raw_query="q=pizza&size=10") +def test_websocket_register_sends_excludedomainurl( + client: NewsDataApiClient, + mocked_responses: responses.RequestsMock, +) -> None: + mocked_responses.post( + WS_REGISTER_URL, + json={"status": "success", "results": {"registration_id": "abc"}}, + status=200, + ) + NewsDataApiWebSocket(client).websocket_register( + q="pizza", excludedomainurl=["bbc.com", "cnn.com"] + ) + sent_url = mocked_responses.calls[0].request.url + assert sent_url is not None + assert "excludedomainurl=bbc.com%2Ccnn.com" in sent_url + + def test_websocket_register_duplicate_409( client: NewsDataApiClient, mocked_responses: responses.RequestsMock,