Skip to content

Commit

Permalink
Merge pull request #3507 from open-formulieren/refactor/3489-more-api…
Browse files Browse the repository at this point in the history
…-clients

API client refactors/clean ups
  • Loading branch information
sergei-maertens authored Sep 27, 2023
2 parents cd521c6 + 792b7de commit a0a969b
Show file tree
Hide file tree
Showing 28 changed files with 365 additions and 244 deletions.
44 changes: 42 additions & 2 deletions src/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2902,6 +2902,46 @@ paths:
$ref: '#/components/headers/X-Is-Form-Designer'
Content-Language:
$ref: '#/components/headers/Content-Language'
/api/v2/geo/address-autocomplete:
get:
operationId: geo_address_autocomplete_retrieve
description: |-
Get the street name and city for a given postal code and house number.
**NOTE** the `/api/v2/location/get-street-name-and-city/` endpoint will be removed in v3. Use `/api/v2/geo/address-autocomplete/` instead.
summary: Get a street name and city
parameters:
- in: query
name: house_number
schema:
type: number
description: House number of the address
required: true
- in: query
name: postcode
schema:
type: string
description: Postal code of the address
required: true
tags:
- geo
deprecated: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/GetStreetNameAndCityViewResult'
description: ''
headers:
X-Session-Expires-In:
$ref: '#/components/headers/X-Session-Expires-In'
X-CSRFToken:
$ref: '#/components/headers/X-CSRFToken'
X-Is-Form-Designer:
$ref: '#/components/headers/X-Is-Form-Designer'
Content-Language:
$ref: '#/components/headers/Content-Language'
/api/v2/geo/address-search:
get:
operationId: geo_address_search_list
Expand Down Expand Up @@ -3208,11 +3248,11 @@ paths:
$ref: '#/components/headers/Content-Language'
/api/v2/location/get-street-name-and-city:
get:
operationId: get_street_name_and_city_list
operationId: location_get_street_name_and_city_retrieve
description: |-
Get the street name and city for a given postal code and house number.
**NOTE** this endpoint will redirect to `/api/v3/geo/address-autocomplete/` in v3.
**NOTE** the `/api/v2/location/get-street-name-and-city/` endpoint will be removed in v3. Use `/api/v2/geo/address-autocomplete/` instead.
summary: Get a street name and city
parameters:
- in: query
Expand Down
9 changes: 8 additions & 1 deletion src/openforms/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from rest_framework import routers
from rest_framework_nested.routers import NestedSimpleRouter

from openforms.contrib.kadaster.api.views import AddressAutocompleteView
from openforms.forms.api.viewsets import (
CategoryViewSet,
FormDefinitionViewSet,
Expand Down Expand Up @@ -97,7 +98,13 @@
path("forms-import", FormsImportAPIView.as_view(), name="forms-import"),
path("prefill/", include("openforms.prefill.api.urls")),
path("validation/", include("openforms.validations.api.urls")),
path("location/", include("openforms.locations.api.urls")),
# TODO: in Open Forms v3, this must become a simple RedirectView to the
# endpoint for openforms.contrib.kadaster.api.views.AddressAutocomplete
path(
"location/get-street-name-and-city",
AddressAutocompleteView.as_view(),
name="get-street-name-and-city-list",
),
path(
"logic/description",
GenerateLogicDescriptionView.as_view(),
Expand Down
20 changes: 18 additions & 2 deletions src/openforms/contrib/kadaster/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@
from rest_framework import serializers


class GetStreetNameAndCityViewInputSerializer(serializers.Serializer):
postcode = serializers.CharField(
label=_("postal code"), help_text=_("Postal code to use in search")
)
house_number = serializers.CharField(
label=_("house number"), help_text=_("House number to use in search")
)


class GetStreetNameAndCityViewResultSerializer(serializers.Serializer):
street_name = serializers.CharField(
label=_("street name"), help_text=_("Found street name")
)
city = serializers.CharField(label=_("city"), help_text=_("Found city"))


class LatitudeLongitudeSerializer(serializers.Serializer):
lat = serializers.FloatField(label=_("Latitude"))
lng = serializers.FloatField(label=_("Longitude"))
Expand All @@ -15,7 +31,7 @@ class RijksDriehoekSerializer(serializers.Serializer):


class AddressSearchResultSerializer(serializers.Serializer):
label = serializers.CharField(label=_("Location name"))
label = serializers.CharField(label=_("Location name")) # type: ignore
lat_lng = LatitudeLongitudeSerializer(
label=_("Latitude/longitude"),
help_text=_("Latitude and longitude in the WGS 84 coordinate system."),
Expand Down Expand Up @@ -47,4 +63,4 @@ class LatLngSearchInputSerializer(serializers.Serializer):


class LatLngSearchResultSerializer(serializers.Serializer):
label = serializers.CharField(label=_("Closest address"))
label = serializers.CharField(label=_("Closest address")) # type: ignore
7 changes: 6 additions & 1 deletion src/openforms/contrib/kadaster/api/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
from django.urls import path

from .views import AddressSearchView, LatLngSearchView
from .views import AddressAutocompleteView, AddressSearchView, LatLngSearchView

app_name = "geo"

urlpatterns = [
path(
"address-autocomplete",
AddressAutocompleteView.as_view(),
name="address-autocomplete",
),
path(
"address-search",
AddressSearchView.as_view(),
Expand Down
80 changes: 75 additions & 5 deletions src/openforms/contrib/kadaster/api/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import logging
from functools import partial

from django.core.cache import cache
from django.utils.translation import gettext_lazy as _

from drf_spectacular.types import OpenApiTypes
Expand All @@ -11,23 +13,91 @@

from openforms.api.serializers import ExceptionSerializer, ValidationErrorSerializer
from openforms.api.views.mixins import ListMixin
from openforms.contrib.kadaster.clients.bag import AddressResult
from openforms.submissions.api.permissions import AnyActiveSubmissionPermission

from ..clients import get_locatieserver_client
from ..clients import get_bag_client, get_locatieserver_client
from .serializers import (
AddressSearchResultSerializer,
GetStreetNameAndCityViewInputSerializer,
GetStreetNameAndCityViewResultSerializer,
LatLngSearchInputSerializer,
LatLngSearchResultSerializer,
)

logger = logging.getLogger(__name__)


ADDRESS_AUTOCOMPLETE_CACHE_TIMEOUT = (
60 * 60 * 24
) # 24 hours - address data does NOT update frequently


def lookup_address(postcode: str, number: str) -> AddressResult | None:
with get_bag_client() as client:
return client.get_address(postcode, number)


class AddressAutocompleteView(APIView):
"""
Get the street name and city when given a postcode and house number.
"""

authentication_classes = ()
permission_classes = [AnyActiveSubmissionPermission]

@extend_schema(
summary=_("Get a street name and city"), # type: ignore
description=_(
"Get the street name and city for a given postal code and house number.\n\n"
"**NOTE** the `/api/v2/location/get-street-name-and-city/` endpoint will "
"be removed in v3. Use `/api/v2/geo/address-autocomplete/` instead."
), # type: ignore
responses=GetStreetNameAndCityViewResultSerializer,
parameters=[
OpenApiParameter(
"postcode",
OpenApiTypes.STR,
OpenApiParameter.QUERY,
description=_("Postal code of the address"), # type: ignore
required=True,
),
OpenApiParameter(
"house_number",
OpenApiTypes.NUMBER,
OpenApiParameter.QUERY,
description=_("House number of the address"), # type: ignore
required=True,
),
],
deprecated=True,
)
def get(self, request, *args, **kwargs):
serializer = GetStreetNameAndCityViewInputSerializer(data=request.query_params)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
postcode, number = data["postcode"], data["house_number"]

# check the cache so we avoid hitting the remote API too often (and risk
# of being throttled, see #1832)
address_data = cache.get_or_set(
key=f"BAG|get_address|{postcode}|{number}",
default=partial(lookup_address, postcode, number),
timeout=ADDRESS_AUTOCOMPLETE_CACHE_TIMEOUT,
)

if not address_data:
# If address is not found just return an empty response
return Response({})

return Response(GetStreetNameAndCityViewResultSerializer(address_data).data)


@extend_schema(
summary=_("Get an adress based on coordinates"),
summary=_("Get an adress based on coordinates"), # type: ignore
description=_(
"Get the closest address name based on the given longitude and latitude."
),
), # type: ignore
parameters=[
OpenApiParameter(
"lat",
Expand Down Expand Up @@ -77,13 +147,13 @@ def get(self, request: Request):


@extend_schema(
summary=_("List address suggestions with coordinates."),
summary=_("List address suggestions with coordinates."), # type: ignore
description=_(
"Get a list of addresses, ordered by relevance/match score of the input "
"query. Note that only results having latitude/longitude data are returned.\n\n"
"The results are retrieved from the configured geo search service, defaulting "
"to the Kadaster location server."
),
), # type: ignore
parameters=[
OpenApiParameter(
"q",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@

import requests_mock

from openforms.contrib.kadaster.clients.bag import AddressResult
from openforms.contrib.kadaster.models import KadasterApiConfig
from openforms.submissions.tests.factories import SubmissionFactory
from openforms.submissions.tests.mixins import SubmissionsMixin
from zgw_consumers_ext.tests.factories import ServiceFactory

from ..clients.bag import AddressResult
from ..models import KadasterApiConfig

CACHES = settings.CACHES.copy()
CACHES["default"] = {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}

Expand All @@ -31,7 +32,7 @@ def setUp(self):
self.addCleanup(caches["default"].clear)
self._add_submission_to_session(self.submission)

@patch("openforms.locations.api.views.lookup_address")
@patch("openforms.contrib.kadaster.api.views.lookup_address")
def test_getting_street_name_and_city(self, m_lookup_address):
m_lookup_address.return_value = AddressResult(
street_name="Keizersgracht", city="Amsterdam"
Expand Down Expand Up @@ -111,7 +112,7 @@ def test_getting_street_name_and_city_without_house_number_returns_error(
},
)

@patch("openforms.locations.api.views.lookup_address")
@patch("openforms.contrib.kadaster.api.views.lookup_address")
def test_getting_street_name_and_city_with_extra_query_params_ignores_extra_param(
self, m_lookup_address
):
Expand All @@ -129,7 +130,7 @@ def test_getting_street_name_and_city_with_extra_query_params_ignores_extra_para
self.assertEqual(response.json()["streetName"], "Keizersgracht")
self.assertEqual(response.json()["city"], "Amsterdam")

@patch("openforms.locations.api.views.lookup_address")
@patch("openforms.contrib.kadaster.api.views.lookup_address")
def test_address_not_found_returns_empty_200_response(self, m_lookup_address):
m_lookup_address.return_value = None

Expand All @@ -142,7 +143,7 @@ def test_address_not_found_returns_empty_200_response(self, m_lookup_address):
self.assertEqual(response.json(), {})

@tag("gh-1832")
@patch("openforms.locations.api.views.lookup_address")
@patch("openforms.contrib.kadaster.api.views.lookup_address")
def test_endpoint_uses_caching(self, m_lookup_address):
m_lookup_address.return_value = AddressResult(
street_name="Keizersgracht", city="Amsterdam"
Expand Down
15 changes: 12 additions & 3 deletions src/openforms/contrib/microsoft/client.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
"""
Microsoft (graph) API client.
This client implementation wraps around the API client/integration implemented in the
O365 package. While it uses requests-oauth2client under the hood, we opt to *not*
use our own :module:`api_client` implementation here, as the typical Dutch API/service
requirements such as mTLS are not relevant. The service model definition also does not
allow configuring any of those aspects.
"""
import json
import os
from io import BytesIO
Expand All @@ -6,9 +15,9 @@

from O365 import Account

from openforms.contrib.microsoft.constants import ConflictHandling
from openforms.contrib.microsoft.exceptions import MSAuthenticationError
from openforms.contrib.microsoft.models import MSGraphService
from .constants import ConflictHandling
from .exceptions import MSAuthenticationError
from .models import MSGraphService


class MSGraphClient:
Expand Down
2 changes: 1 addition & 1 deletion src/openforms/contrib/microsoft/tests/factories.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import factory

from openforms.contrib.microsoft.models import MSGraphService
from ..models import MSGraphService


class MSGraphServiceFactory(factory.django.DjangoModelFactory):
Expand Down
13 changes: 3 additions & 10 deletions src/openforms/formio/components/np_family_members/stuf_bg.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
from typing import List, Tuple

from stuf.stuf_bg.models import StufBGConfig
from stuf.stuf_bg.client import get_client


def get_np_children_stuf_bg(bsn: str) -> List[Tuple[str, str]]:
config = StufBGConfig.get_solo()
assert isinstance(config, StufBGConfig)
client = config.get_client()

attributes = [
"inp.heeftAlsKinderen",
]

data = client.get_values(bsn, attributes)
with get_client() as client:
data = client.get_values(bsn, ["inp.heeftAlsKinderen"])

# Kids
child_choices = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,7 @@ def test_get_children_haal_centraal(self, mock_brp_config_get_solo):
self.assertEqual(("456789123", "Bolly van Doe"), kids_choices[0])
self.assertEqual(("789123456", "Billy van Doe"), kids_choices[1])

@patch(
"openforms.formio.components.np_family_members.stuf_bg.StufBGConfig.get_solo"
)
@patch("stuf.stuf_bg.client.StufBGConfig.get_solo")
def test_get_children_stuf_bg(self, mock_stufbg_config_get_solo):
stuf_bg_service = StufServiceFactory.build()
mock_stufbg_config_get_solo.return_value = StufBGConfig(service=stuf_bg_service)
Expand Down
Empty file.
Empty file.
19 changes: 0 additions & 19 deletions src/openforms/locations/api/serializers.py

This file was deleted.

Empty file.
Loading

0 comments on commit a0a969b

Please sign in to comment.