Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions asknews_sdk/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from asknews_sdk.api.analytics import AnalyticsAPI, AsyncAnalyticsAPI
from asknews_sdk.api.byok import AsyncByokAPI, ByokAPI
from asknews_sdk.api.chat import AsyncChatAPI, ChatAPI
from asknews_sdk.api.distribution import AsyncDistributionAPI, DistributionAPI
from asknews_sdk.api.news import AsyncNewsAPI, NewsAPI
from asknews_sdk.api.stories import AsyncStoriesAPI, StoriesAPI
from asknews_sdk.api.wiki import AsyncWikiAPI, WikiAPI
Expand All @@ -17,6 +18,8 @@
"AsyncNewsAPI",
"ChatAPI",
"AsyncChatAPI",
"DistributionAPI",
"AsyncDistributionAPI",
"WikiAPI",
"AsyncWikiAPI",
)
101 changes: 101 additions & 0 deletions asknews_sdk/api/distribution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from typing import Dict, List, Optional

from asknews_sdk.api.base import BaseAPI
from asknews_sdk.client import APIClient, AsyncAPIClient
from asknews_sdk.dto.distribution import DomainMetricsResponse, DomainMetricsTimeWindowResponse


class DistributionAPI(BaseAPI[APIClient]):
"""Distribution API."""

def get_domain_metrics(
self,
domain_names: List[str],
start_date: Optional[int] = None,
end_date: Optional[int] = None,
*,
http_headers: Optional[Dict] = None,
) -> DomainMetricsResponse:
"""Get raw publisher metric event counts for domains."""
response = self.client.request(
method="GET",
endpoint="/v1/distribution/stats/metrics",
query={
"domain_names": domain_names,
"start_date": start_date,
"end_date": end_date,
},
headers=http_headers,
accept=[(DomainMetricsResponse.__content_type__, 1.0)],
)
return DomainMetricsResponse.model_validate(response.content)

def get_domain_metrics_timeseries(
self,
domain_names: List[str],
start_date: Optional[int] = None,
end_date: Optional[int] = None,
*,
http_headers: Optional[Dict] = None,
) -> DomainMetricsTimeWindowResponse:
"""Get raw publisher metric event counts per day for domains."""
response = self.client.request(
method="GET",
endpoint="/v1/distribution/stats/metrics_timeseries",
query={
"domain_names": domain_names,
"start_date": start_date,
"end_date": end_date,
},
headers=http_headers,
accept=[(DomainMetricsTimeWindowResponse.__content_type__, 1.0)],
)
return DomainMetricsTimeWindowResponse.model_validate(response.content)


class AsyncDistributionAPI(BaseAPI[AsyncAPIClient]):
"""Distribution API (async)."""

async def get_domain_metrics(
self,
domain_names: List[str],
start_date: Optional[int] = None,
end_date: Optional[int] = None,
*,
http_headers: Optional[Dict] = None,
) -> DomainMetricsResponse:
"""Get raw publisher metric event counts for domains."""
response = await self.client.request(
method="GET",
endpoint="/v1/distribution/stats/metrics",
query={
"domain_names": domain_names,
"start_date": start_date,
"end_date": end_date,
},
headers=http_headers,
accept=[(DomainMetricsResponse.__content_type__, 1.0)],
)
return DomainMetricsResponse.model_validate(response.content)

async def get_domain_metrics_timeseries(
self,
domain_names: List[str],
start_date: Optional[int] = None,
end_date: Optional[int] = None,
*,
http_headers: Optional[Dict] = None,
) -> DomainMetricsTimeWindowResponse:
"""Get raw publisher metric event counts per day for domains."""
response = await self.client.request(
method="GET",
endpoint="/v1/distribution/stats/metrics_timeseries",
query={
"domain_names": domain_names,
"start_date": start_date,
"end_date": end_date,
},
headers=http_headers,
accept=[(DomainMetricsTimeWindowResponse.__content_type__, 1.0)],
)
return DomainMetricsTimeWindowResponse.model_validate(response.content)
8 changes: 8 additions & 0 deletions asknews_sdk/dto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
)
from asknews_sdk.dto.byok import ApiKeyResponse, Provider, UpsertApiKeyRequest
from asknews_sdk.dto.common import FilterParams
from asknews_sdk.dto.distribution import (
DomainMetricsDayItem,
DomainMetricsResponse,
DomainMetricsTimeWindowResponse,
)
from asknews_sdk.dto.error import APIErrorModel, HTTPValidationError, ValidationError
from asknews_sdk.dto.news import (
ReferralItem,
Expand Down Expand Up @@ -47,6 +52,9 @@
"WebhookAction",
"WebhookParams",
"FilterParams",
"DomainMetricsDayItem",
"DomainMetricsResponse",
"DomainMetricsTimeWindowResponse",
"APIErrorModel",
"ValidationError",
"HTTPValidationError",
Expand Down
25 changes: 25 additions & 0 deletions asknews_sdk/dto/distribution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from typing import List

from pydantic import BaseModel

from asknews_sdk.dto.base import BaseSchema


class DomainMetricsDayItem(BaseModel):
day: str
surfaces: int
citations: int
full_text: int


class DomainMetricsResponse(BaseSchema):
surfaces: int
citations: int
full_text: int


class DomainMetricsTimeWindowResponse(BaseSchema):
data: List[DomainMetricsDayItem]
total_surfaces: int
total_citations: int
total_full_text: int
4 changes: 4 additions & 0 deletions asknews_sdk/sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
AsyncAnalyticsAPI,
AsyncByokAPI,
AsyncChatAPI,
AsyncDistributionAPI,
AsyncNewsAPI,
AsyncStoriesAPI,
AsyncWikiAPI,
ByokAPI,
ChatAPI,
DistributionAPI,
NewsAPI,
StoriesAPI,
WikiAPI,
Expand Down Expand Up @@ -121,6 +123,7 @@ def __init__(
self.chat = ChatAPI(self.client)
self.wiki = WikiAPI(self.client)
self.byok = ByokAPI(self.client)
self.distribution = DistributionAPI(self.client)

def __enter__(self) -> AskNewsSDK:
return self
Expand Down Expand Up @@ -232,6 +235,7 @@ def __init__(
self.chat = AsyncChatAPI(self.client)
self.wiki = AsyncWikiAPI(self.client)
self.byok = AsyncByokAPI(self.client)
self.distribution = AsyncDistributionAPI(self.client)

async def __aenter__(self) -> AsyncAskNewsSDK:
return self
Expand Down
121 changes: 121 additions & 0 deletions tests/api/test_distribution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from urllib.parse import parse_qs

import pytest
from respx import MockRouter

from asknews_sdk.api.distribution import AsyncDistributionAPI, DistributionAPI
from asknews_sdk.client import APIClient, AsyncAPIClient
from asknews_sdk.dto.distribution import DomainMetricsResponse, DomainMetricsTimeWindowResponse
from asknews_sdk.sdk import AskNewsSDK, AsyncAskNewsSDK


DOMAIN_NAMES = ["example.com", "example.org"]
START_DATE = 1_700_000_000
END_DATE = 1_700_086_400


@pytest.fixture
def sync_distribution_api(sync_api_client: APIClient):
return DistributionAPI(sync_api_client)


@pytest.fixture
def async_distribution_api(async_api_client: AsyncAPIClient):
return AsyncDistributionAPI(async_api_client)


def test_sync_sdk_exposes_distribution_api():
with AskNewsSDK(auth=None) as sdk:
assert isinstance(sdk.distribution, DistributionAPI)


@pytest.mark.asyncio
async def test_async_sdk_exposes_distribution_api():
async with AsyncAskNewsSDK(auth=None) as sdk:
assert isinstance(sdk.distribution, AsyncDistributionAPI)


def test_sync_get_domain_metrics(sync_distribution_api: DistributionAPI, response_mock: MockRouter):
payload = {"surfaces": 12, "citations": 7, "full_text": 3}
mock_route = response_mock.get("/v1/distribution/stats/metrics").respond(json=payload)

response = sync_distribution_api.get_domain_metrics(
DOMAIN_NAMES,
start_date=START_DATE,
end_date=END_DATE,
http_headers={"custom-header": "custom-value"},
)

assert response == DomainMetricsResponse(**payload)
request = mock_route.calls.last.request
assert request.method == "GET"
assert request.headers["accept"] == DomainMetricsResponse.__content_type__
assert request.headers["custom-header"] == "custom-value"
assert parse_qs(request.url.query.decode()) == {
"domain_names": DOMAIN_NAMES,
"start_date": [str(START_DATE)],
"end_date": [str(END_DATE)],
}


def test_sync_get_domain_metrics_timeseries(
sync_distribution_api: DistributionAPI, response_mock: MockRouter
):
payload = {
"data": [{"day": "2026-08-01", "surfaces": 5, "citations": 3, "full_text": 1}],
"total_surfaces": 5,
"total_citations": 3,
"total_full_text": 1,
}
mock_route = response_mock.get("/v1/distribution/stats/metrics_timeseries").respond(
json=payload
)

response = sync_distribution_api.get_domain_metrics_timeseries(DOMAIN_NAMES)

assert response == DomainMetricsTimeWindowResponse(**payload)
request = mock_route.calls.last.request
assert request.method == "GET"
assert request.headers["accept"] == DomainMetricsTimeWindowResponse.__content_type__
assert parse_qs(request.url.query.decode()) == {"domain_names": DOMAIN_NAMES}


@pytest.mark.asyncio
async def test_async_get_domain_metrics(
async_distribution_api: AsyncDistributionAPI, response_mock: MockRouter
):
payload = {"surfaces": 12, "citations": 7, "full_text": 3}
mock_route = response_mock.get("/v1/distribution/stats/metrics").respond(json=payload)

response = await async_distribution_api.get_domain_metrics(
DOMAIN_NAMES, start_date=START_DATE, end_date=END_DATE
)

assert response == DomainMetricsResponse(**payload)
assert parse_qs(mock_route.calls.last.request.url.query.decode()) == {
"domain_names": DOMAIN_NAMES,
"start_date": [str(START_DATE)],
"end_date": [str(END_DATE)],
}


@pytest.mark.asyncio
async def test_async_get_domain_metrics_timeseries(
async_distribution_api: AsyncDistributionAPI, response_mock: MockRouter
):
payload = {
"data": [{"day": "2026-08-01", "surfaces": 5, "citations": 3, "full_text": 1}],
"total_surfaces": 5,
"total_citations": 3,
"total_full_text": 1,
}
mock_route = response_mock.get("/v1/distribution/stats/metrics_timeseries").respond(
json=payload
)

response = await async_distribution_api.get_domain_metrics_timeseries(DOMAIN_NAMES)

assert response == DomainMetricsTimeWindowResponse(**payload)
assert parse_qs(mock_route.calls.last.request.url.query.decode()) == {
"domain_names": DOMAIN_NAMES
}
Loading