diff --git a/public_api/urls.py b/public_api/urls.py index ca1b3870f..649c3a2e3 100644 --- a/public_api/urls.py +++ b/public_api/urls.py @@ -16,7 +16,10 @@ TopicDetailViewV2, TopicListViewV2, ) -from public_api.version_02.views.timeseries_viewset import APITimeSeriesViewSetV2 +from public_api.version_02.views.timeseries_viewset import ( + APITimeSeriesViewSetV2, + APITimeSeriesViewSetV3, +) from public_api.views import ( GeographyDetailView, GeographyListView, @@ -50,6 +53,7 @@ def construct_url_patterns_for_public_api( urls = [] urls.extend(_construct_version_one_urls(prefix=prefix)) urls.extend(_construct_version_two_urls(prefix=prefix)) + urls.extend(_construct_version_three_urls(prefix=prefix)) if MetricsPublicAPIInterface.is_auth_enabled(): urls.append( @@ -222,3 +226,28 @@ def _construct_version_two_urls( name="timeseries-list-v2", ), ] + + +def _construct_version_three_urls( + *, + prefix: str, +) -> list[resolvers.URLResolver]: + """Returns a list of URLResolvers for the public_api version 3 + + Args: + prefix: The prefix to add to the start of the url paths + + Returns: + List of `URLResolver` objects each representing a + set of versioned URLS. + """ + + return [ + path( + f"{prefix}v3/themes/", + APITimeSeriesViewSetV3.as_view( + {"get": "list"}, name=APITimeSeriesViewSetV3.name + ), + name="timeseries-list-v3", + ), + ] diff --git a/public_api/version_02/views/timeseries_viewset.py b/public_api/version_02/views/timeseries_viewset.py index 2579207c9..c1f3d28a7 100644 --- a/public_api/version_02/views/timeseries_viewset.py +++ b/public_api/version_02/views/timeseries_viewset.py @@ -1,3 +1,4 @@ +import django_filters from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.utils import extend_schema from rest_framework import pagination, viewsets @@ -101,3 +102,98 @@ def get_queryset(self): metric=self.kwargs["metric"], restrict_to_public=True, ) + + +class APITimeSeriesFilterSetv3(django_filters.FilterSet): + geography = django_filters.CharFilter(field_name="geography", lookup_expr="iexact") + geography_type = django_filters.CharFilter( + field_name="geography_type", lookup_expr="iexact" + ) + + class Meta: + model = MetricsPublicAPIInterface.get_api_timeseries_model() + fields = [ + "sub_theme", + "topic", + "geography_type", + "geography", + "metric", + "stratum", + "sex", + "age", + "year", + "epiweek", + "date", + "in_reporting_delay_period", + ] + + +@extend_schema(tags=[PUBLIC_API_TAG]) +class APITimeSeriesViewSetV3(viewsets.ReadOnlyModelViewSet): + """ + This endpoint will provide the full timeseries of a slice of data. + + There is one mandatory URL parameter and other optional query parameters: + + Note that by default, results are paginated by a page size of 5 + + This page size can be changed using the *page_size* parameter. + The maximum supported page size is **365**. + + --- + + The mandatory URL parameter is: + + - `theme` - The largest topical subgroup e.g. **infectious_disease** + + --- + + The optional query parameters are as follows in order from first to last: + + - `sub_theme` - A topical subgroup e.g. **respiratory** + + - `topic` - The name of the topic e.g. **COVID-19** + + - `geography_type` - The type of the geography type e.g. **Nation** + + - `geography` - The name of the geography associated with metric e.g. **London** + + - `metric` - The name of the metric being queried for e.g. **COVID-19_deaths_ONSByDay** + + - `stratum` - Smallest subgroup a metric can be broken down into e.g. ethnicity, testing pillar + + - `age` - Smallest subgroup a metric can be broken down into e.g. **15_44** for the age group of 15-44 years + + - `sex` - Patient gender e.g. **f** for Female or **all** for all genders + + - `year` - Epi year of the metrics value (important for annual metrics) e.g. **2020** + + - `month` - Epi month of the metric value (important for monthly metrics) e.g. **12** + + - `epiweek` - Epi week of the metric value (important for weekly metrics) e.g. **30** + + - `date` - The date which this metric value was recorded in the format **YYYY-MM-DD** e.g. **2020-07-20** + + - `in_reporting_delay_period` - A boolean indicating whether the data point is considered to be subject + to retrospective updates. + + """ + + permission_classes = [] + name = "API Time Series Version 3" + queryset = ( + MetricsPublicAPIInterface.get_api_timeseries_model() + .objects.all() + .order_by("date") + ) + serializer_class = APITimeSeriesListSerializerv2 + pagination_class = APITimeSeriesPaginationv2 + filter_backends = [DjangoFilterBackend] + filterset_class = APITimeSeriesFilterSetv3 + + def get_queryset(self): + queryset = super().get_queryset() + + return queryset.filter( + theme=self.kwargs["theme"], + ) diff --git a/tests/unit/public_api/test_timeseries_viewset_v3.py b/tests/unit/public_api/test_timeseries_viewset_v3.py new file mode 100644 index 000000000..9238f7dc8 --- /dev/null +++ b/tests/unit/public_api/test_timeseries_viewset_v3.py @@ -0,0 +1,43 @@ +import pytest +from rest_framework.test import APIRequestFactory + +from public_api.version_02.views.timeseries_viewset import APITimeSeriesViewSetV3 +from tests.factories.metrics.api_models.time_series import APITimeSeriesFactory + + +@pytest.mark.django_db +class TestAPITimeSeriesViewSetV3GetQueryset: + def test_filters_queryset_by_theme_kwarg(self) -> None: + """ + Ensure `get_queryset` applies the `theme` URL kwarg as a filter. + """ + # Given: three records with different themes + infectious_record_1 = APITimeSeriesFactory.create_record( + theme_name="infectious_disease", + date="2023-01-01", + ) + infectious_record_2 = APITimeSeriesFactory.create_record( + theme_name="infectious_disease", + date="2023-01-02", + ) + APITimeSeriesFactory.create_record( + theme_name="extreme_event", + ) + + # And a view instance with the theme URL kwarg set + view = APITimeSeriesViewSetV3() + view.kwargs = {"theme": "infectious_disease"} + + # DRF's GenericAPIView expects a `request` attribute, even though our + # overridden `get_queryset` does not use it directly. + view.request = APIRequestFactory().get("//v3/themes/infectious_disease") + + # When + queryset = view.get_queryset() + + # Then: only records matching the URL theme are returned + themes = {instance.theme for instance in queryset} + assert themes == {"infectious_disease"} + assert queryset.count() == 2 + assert queryset.first() == infectious_record_1 + assert queryset.last() == infectious_record_2