commonground-api-common 1.12.2__py3-none-any.whl → 2.4.1__py3-none-any.whl

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 (67) hide show
  1. commonground_api_common-1.12.2.data/scripts/patch_content_types → commonground_api_common-2.4.1.data/scripts/generate_schema +2 -4
  2. {commonground_api_common-1.12.2.dist-info → commonground_api_common-2.4.1.dist-info}/METADATA +47 -40
  3. {commonground_api_common-1.12.2.dist-info → commonground_api_common-2.4.1.dist-info}/RECORD +47 -52
  4. {commonground_api_common-1.12.2.dist-info → commonground_api_common-2.4.1.dist-info}/WHEEL +1 -1
  5. vng_api_common/__init__.py +1 -1
  6. vng_api_common/admin.py +1 -20
  7. vng_api_common/api/views.py +1 -0
  8. vng_api_common/apps.py +44 -26
  9. vng_api_common/audittrails/utils.py +44 -0
  10. vng_api_common/authorizations/admin.py +1 -1
  11. vng_api_common/authorizations/middleware.py +244 -0
  12. vng_api_common/authorizations/migrations/0016_remove_authorizationsconfig_api_root_and_more.py +76 -0
  13. vng_api_common/authorizations/models.py +62 -3
  14. vng_api_common/authorizations/utils.py +17 -0
  15. vng_api_common/authorizations/validators.py +5 -11
  16. vng_api_common/caching/etags.py +2 -1
  17. vng_api_common/client.py +61 -29
  18. vng_api_common/conf/api.py +33 -48
  19. vng_api_common/contrib/setup_configuration/models.py +32 -0
  20. vng_api_common/contrib/setup_configuration/steps.py +46 -0
  21. vng_api_common/extensions/file.py +26 -0
  22. vng_api_common/extensions/gegevensgroep.py +16 -0
  23. vng_api_common/extensions/geojson.py +270 -0
  24. vng_api_common/extensions/hyperlink.py +37 -0
  25. vng_api_common/extensions/polymorphic.py +68 -0
  26. vng_api_common/extensions/query.py +20 -0
  27. vng_api_common/filters.py +0 -1
  28. vng_api_common/generators.py +12 -113
  29. vng_api_common/middleware.py +1 -227
  30. vng_api_common/migrations/0006_delete_apicredential.py +120 -0
  31. vng_api_common/mocks.py +4 -1
  32. vng_api_common/models.py +10 -111
  33. vng_api_common/notifications/api/views.py +8 -8
  34. vng_api_common/notifications/handlers.py +8 -3
  35. vng_api_common/notifications/migrations/0011_remove_subscription_config_and_more.py +23 -0
  36. vng_api_common/oas.py +6 -10
  37. vng_api_common/pagination.py +10 -0
  38. vng_api_common/routers.py +3 -3
  39. vng_api_common/schema.py +414 -158
  40. vng_api_common/tests/schema.py +13 -0
  41. vng_api_common/utils.py +0 -22
  42. vng_api_common/validators.py +111 -113
  43. vng_api_common/views.py +35 -20
  44. commonground_api_common-1.12.2.data/scripts/generate_schema +0 -39
  45. commonground_api_common-1.12.2.data/scripts/use_external_components +0 -16
  46. vng_api_common/inspectors/cache.py +0 -57
  47. vng_api_common/inspectors/fields.py +0 -126
  48. vng_api_common/inspectors/files.py +0 -121
  49. vng_api_common/inspectors/geojson.py +0 -360
  50. vng_api_common/inspectors/polymorphic.py +0 -72
  51. vng_api_common/inspectors/query.py +0 -96
  52. vng_api_common/inspectors/utils.py +0 -40
  53. vng_api_common/inspectors/view.py +0 -547
  54. vng_api_common/management/commands/generate_autorisaties.py +0 -43
  55. vng_api_common/management/commands/generate_notificaties.py +0 -40
  56. vng_api_common/management/commands/generate_swagger.py +0 -197
  57. vng_api_common/management/commands/patch_error_contenttypes.py +0 -61
  58. vng_api_common/management/commands/use_external_components.py +0 -94
  59. vng_api_common/notifications/constants.py +0 -3
  60. vng_api_common/notifications/models.py +0 -97
  61. vng_api_common/templates/vng_api_common/api_schema_to_markdown_table.md +0 -16
  62. vng_api_common/templates/vng_api_common/autorisaties.md +0 -15
  63. vng_api_common/templates/vng_api_common/notificaties.md +0 -24
  64. {commonground_api_common-1.12.2.dist-info → commonground_api_common-2.4.1.dist-info}/top_level.txt +0 -0
  65. /vng_api_common/{inspectors → contrib}/__init__.py +0 -0
  66. /vng_api_common/{management → contrib/setup_configuration}/__init__.py +0 -0
  67. /vng_api_common/{management/commands → extensions}/__init__.py +0 -0
vng_api_common/schema.py CHANGED
@@ -1,177 +1,433 @@
1
- import json
2
1
  import logging
3
- import os
4
- from urllib.parse import urlsplit
2
+ from typing import Dict, List, Optional, Type
5
3
 
6
4
  from django.conf import settings
7
- from django.urls import get_script_prefix
8
-
9
- from drf_yasg import openapi
10
- from drf_yasg.app_settings import swagger_settings
11
- from drf_yasg.codecs import yaml_sane_dump, yaml_sane_load
12
- from drf_yasg.generators import OpenAPISchemaGenerator as _OpenAPISchemaGenerator
13
- from drf_yasg.renderers import SwaggerJSONRenderer, SwaggerYAMLRenderer, _SpecRenderer
14
- from drf_yasg.utils import get_consumes, get_produces
15
- from drf_yasg.views import get_schema_view
16
- from rest_framework import exceptions, permissions
17
- from rest_framework.response import Response
18
- from rest_framework.settings import api_settings
19
-
20
- logger = logging.getLogger(__name__)
5
+ from django.utils.translation import gettext_lazy as _
21
6
 
7
+ from drf_spectacular import openapi
8
+ from drf_spectacular.utils import OpenApiExample, OpenApiParameter, OpenApiTypes
9
+ from rest_framework import exceptions, serializers, status
22
10
 
23
- class OpenAPISchemaGenerator(_OpenAPISchemaGenerator):
24
- def get_schema(self, request=None, public=False):
25
- """
26
- Rewrite parent class to add 'responses' in components
27
- """
28
- endpoints = self.get_endpoints(request)
29
- components = self.reference_resolver_class(
30
- openapi.SCHEMA_DEFINITIONS, "responses", force_init=True
31
- )
32
- self.consumes = get_consumes(api_settings.DEFAULT_PARSER_CLASSES)
33
- self.produces = get_produces(api_settings.DEFAULT_RENDERER_CLASSES)
34
- paths, prefix = self.get_paths(endpoints, components, request, public)
11
+ from .audittrails.utils import _view_supports_audittrail
12
+ from .caching.introspection import has_cache_header
13
+ from .constants import HEADER_AUDIT, HEADER_LOGRECORD_ID, VERSION_HEADER
14
+ from .exceptions import Conflict, Gone, PreconditionFailed
15
+ from .geo import DEFAULT_CRS, HEADER_ACCEPT, HEADER_CONTENT, GeoMixin
16
+ from .permissions import BaseAuthRequired, get_required_scopes
17
+ from .serializers import FoutSerializer, ValidatieFoutSerializer
18
+ from .views import ERROR_CONTENT_TYPE
35
19
 
36
- security_definitions = self.get_security_definitions()
37
- if security_definitions:
38
- security_requirements = self.get_security_requirements(security_definitions)
39
- else:
40
- security_requirements = None
41
-
42
- url = self.url
43
- if url is None and request is not None:
44
- url = request.build_absolute_uri()
45
-
46
- return openapi.Swagger(
47
- info=self.info,
48
- paths=paths,
49
- consumes=self.consumes or None,
50
- produces=self.produces or None,
51
- security_definitions=security_definitions,
52
- security=security_requirements,
53
- _url=url,
54
- _prefix=prefix,
55
- _version=self.version,
56
- **dict(components),
57
- )
58
-
59
- def get_path_parameters(self, path, view_cls):
60
- """Return a list of Parameter instances corresponding to any templated path variables.
20
+ logger = logging.getLogger(__name__)
61
21
 
62
- :param str path: templated request path
63
- :param type view_cls: the view class associated with the path
64
- :return: path parameters
65
- :rtype: list[openapi.Parameter]
22
+ COMMON_ERRORS = [
23
+ exceptions.AuthenticationFailed,
24
+ exceptions.NotAuthenticated,
25
+ exceptions.PermissionDenied,
26
+ exceptions.NotAcceptable,
27
+ Conflict,
28
+ Gone,
29
+ exceptions.UnsupportedMediaType,
30
+ exceptions.Throttled,
31
+ exceptions.APIException,
32
+ ]
33
+
34
+ DEFAULT_ACTION_ERRORS = {
35
+ "create": COMMON_ERRORS + [exceptions.ParseError, exceptions.ValidationError],
36
+ "list": COMMON_ERRORS,
37
+ "retrieve": COMMON_ERRORS + [exceptions.NotFound],
38
+ "update": COMMON_ERRORS
39
+ + [exceptions.ParseError, exceptions.ValidationError, exceptions.NotFound],
40
+ "partial_update": COMMON_ERRORS
41
+ + [exceptions.ParseError, exceptions.ValidationError, exceptions.NotFound],
42
+ "destroy": COMMON_ERRORS + [exceptions.NotFound],
43
+ }
44
+
45
+ HTTP_STATUS_CODE_TITLES = {
46
+ status.HTTP_100_CONTINUE: "Continue",
47
+ status.HTTP_101_SWITCHING_PROTOCOLS: "Switching protocols",
48
+ status.HTTP_200_OK: "OK",
49
+ status.HTTP_201_CREATED: "Created",
50
+ status.HTTP_202_ACCEPTED: "Accepted",
51
+ status.HTTP_203_NON_AUTHORITATIVE_INFORMATION: "Non authoritative information",
52
+ status.HTTP_204_NO_CONTENT: "No content",
53
+ status.HTTP_205_RESET_CONTENT: "Reset content",
54
+ status.HTTP_206_PARTIAL_CONTENT: "Partial content",
55
+ status.HTTP_207_MULTI_STATUS: "Multi status",
56
+ status.HTTP_300_MULTIPLE_CHOICES: "Multiple choices",
57
+ status.HTTP_301_MOVED_PERMANENTLY: "Moved permanently",
58
+ status.HTTP_302_FOUND: "Found",
59
+ status.HTTP_303_SEE_OTHER: "See other",
60
+ status.HTTP_304_NOT_MODIFIED: "Not modified",
61
+ status.HTTP_305_USE_PROXY: "Use proxy",
62
+ status.HTTP_306_RESERVED: "Reserved",
63
+ status.HTTP_307_TEMPORARY_REDIRECT: "Temporary redirect",
64
+ status.HTTP_400_BAD_REQUEST: "Bad request",
65
+ status.HTTP_401_UNAUTHORIZED: "Unauthorized",
66
+ status.HTTP_402_PAYMENT_REQUIRED: "Payment required",
67
+ status.HTTP_403_FORBIDDEN: "Forbidden",
68
+ status.HTTP_404_NOT_FOUND: "Not found",
69
+ status.HTTP_405_METHOD_NOT_ALLOWED: "Method not allowed",
70
+ status.HTTP_406_NOT_ACCEPTABLE: "Not acceptable",
71
+ status.HTTP_407_PROXY_AUTHENTICATION_REQUIRED: "Proxy authentication required",
72
+ status.HTTP_408_REQUEST_TIMEOUT: "Request timeout",
73
+ status.HTTP_409_CONFLICT: "Conflict",
74
+ status.HTTP_410_GONE: "Gone",
75
+ status.HTTP_411_LENGTH_REQUIRED: "Length required",
76
+ status.HTTP_412_PRECONDITION_FAILED: "Precondition failed",
77
+ status.HTTP_413_REQUEST_ENTITY_TOO_LARGE: "Request entity too large",
78
+ status.HTTP_414_REQUEST_URI_TOO_LONG: "Request uri too long",
79
+ status.HTTP_415_UNSUPPORTED_MEDIA_TYPE: "Unsupported media type",
80
+ status.HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE: "Requested range not satisfiable",
81
+ status.HTTP_417_EXPECTATION_FAILED: "Expectation failed",
82
+ status.HTTP_422_UNPROCESSABLE_ENTITY: "Unprocessable entity",
83
+ status.HTTP_423_LOCKED: "Locked",
84
+ status.HTTP_424_FAILED_DEPENDENCY: "Failed dependency",
85
+ status.HTTP_428_PRECONDITION_REQUIRED: "Precondition required",
86
+ status.HTTP_429_TOO_MANY_REQUESTS: "Too many requests",
87
+ status.HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE: "Request header fields too large",
88
+ status.HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS: "Unavailable for legal reasons",
89
+ status.HTTP_500_INTERNAL_SERVER_ERROR: "Internal server error",
90
+ status.HTTP_501_NOT_IMPLEMENTED: "Not implemented",
91
+ status.HTTP_502_BAD_GATEWAY: "Bad gateway",
92
+ status.HTTP_503_SERVICE_UNAVAILABLE: "Service unavailable",
93
+ status.HTTP_504_GATEWAY_TIMEOUT: "Gateway timeout",
94
+ status.HTTP_505_HTTP_VERSION_NOT_SUPPORTED: "HTTP version not supported",
95
+ status.HTTP_507_INSUFFICIENT_STORAGE: "Insufficient storage",
96
+ status.HTTP_511_NETWORK_AUTHENTICATION_REQUIRED: "Network authentication required",
97
+ }
98
+
99
+
100
+ class AutoSchema(openapi.AutoSchema):
101
+ method_mapping = dict(
102
+ **openapi.AutoSchema.method_mapping,
103
+ head="headers",
104
+ )
105
+
106
+ def get_auth(self) -> List[Dict[str, List[str]]]:
66
107
  """
67
- parameters = super().get_path_parameters(path, view_cls)
68
-
69
- # see if we can specify UUID a bit more
70
- for parameter in parameters:
71
- # the most pragmatic of checks
72
- if not parameter.name.endswith("_uuid"):
73
- continue
74
- parameter.format = openapi.FORMAT_UUID
75
- parameter.description = "Unieke resource identifier (UUID4)"
76
- return parameters
77
-
78
-
79
- DefaultSchemaView = get_schema_view(
80
- # validators=['flex', 'ssv'],
81
- public=True,
82
- permission_classes=(permissions.AllowAny,),
83
- )
84
-
85
-
86
- class OpenAPIV3RendererMixin:
87
- def render(self, data, media_type=None, renderer_context=None):
88
- if "openapi" in data or "swagger" in data:
89
- if self.format == ".yaml":
90
- return yaml_sane_dump(data, False)
91
- elif self.format == ".json":
92
- return json.dumps(data)
93
-
94
- return super().render(
95
- data, media_type=media_type, renderer_context=renderer_context
96
- )
97
-
98
-
99
- SPEC_RENDERERS = (
100
- type("SwaggerYAMLRenderer", (OpenAPIV3RendererMixin, SwaggerYAMLRenderer), {}),
101
- type("SwaggerJSONRenderer", (OpenAPIV3RendererMixin, SwaggerJSONRenderer), {}),
102
- )
103
-
108
+ Return a list of security requirements for this operation.
104
109
 
105
- class SchemaMixin:
106
- """
107
- Always serve the v3 version, which is kept in version control.
110
+ `OpenApiAuthenticationExtension` can't be used here since it's tightly coupled
111
+ with DRF authentication classes
112
+ """
113
+ permissions = self.view.get_permissions()
114
+ scope_permissions = [
115
+ perm for perm in permissions if isinstance(perm, BaseAuthRequired)
116
+ ]
108
117
 
109
- .. warn:: there is a risk of the generated schema not being in sync with
110
- the code. Unfortunately, that's the tradeoff we have. We could set up
111
- CI to check for outdated schemas.
112
- """
118
+ if not scope_permissions:
119
+ return super().get_auth()
113
120
 
114
- schema_path = None
121
+ scopes = get_required_scopes(self.view.request, self.view)
122
+ if not scopes:
123
+ return []
115
124
 
116
- @property
117
- def _is_openapi_v2(self) -> bool:
118
- default = "3" if "format" in self.kwargs else "2"
119
- version = self.request.GET.get("v", default)
120
- return version.startswith("2")
125
+ return [{settings.SECURITY_DEFINITION_NAME: [str(scopes)]}]
121
126
 
122
- def get_renderers(self):
123
- if self._is_openapi_v2:
124
- return super().get_renderers()
125
- return [renderer() for renderer in SPEC_RENDERERS]
127
+ def get_operation_id(self):
128
+ """
129
+ Use view basename as a base for operation_id
130
+ """
131
+ if hasattr(self.view, "basename"):
132
+ basename = self.view.basename
133
+ action = "head" if self.method == "HEAD" else self.view.action
134
+ # make compatible with old OAS
135
+ if action == "destroy":
136
+ action = "delete"
137
+ elif action == "retrieve":
138
+ action = "read"
139
+
140
+ return f"{basename}_{action}"
141
+ return super().get_operation_id()
142
+
143
+ def get_error_responses(self) -> Dict[int, Type[serializers.Serializer]]:
144
+ """
145
+ return dictionary of error codes and correspondent error serializers
146
+ - define status codes based on exceptions for each endpoint
147
+ - define error serializers based on status code
148
+ """
126
149
 
127
- def get_schema_path(self) -> str:
128
- return self.schema_path or os.path.join(
129
- settings.BASE_DIR, "src", "openapi.yaml"
150
+ # only supports viewsets
151
+ action = getattr(self.view, "action", None)
152
+ if not action:
153
+ return {}
154
+
155
+ # define status codes for the action based on potential exceptions
156
+ # general errors
157
+ general_klasses = DEFAULT_ACTION_ERRORS.get(action)
158
+ if general_klasses is None:
159
+ logger.debug("Unknown action %s, no default error responses added")
160
+ return {}
161
+
162
+ exception_klasses = general_klasses[:]
163
+ # add geo and validation errors
164
+ has_validation_errors = action == "list" or any(
165
+ issubclass(klass, exceptions.ValidationError) for klass in exception_klasses
130
166
  )
131
-
132
- def get(self, request, version="", *args, **kwargs):
133
- if self._is_openapi_v2:
134
- version = request.version or version or ""
135
- if isinstance(request.accepted_renderer, _SpecRenderer):
136
- generator = self.generator_class(
137
- getattr(self, "info", swagger_settings.DEFAULT_INFO),
138
- version,
139
- None,
140
- None,
141
- None,
142
- )
167
+ if has_validation_errors:
168
+ exception_klasses.append(exceptions.ValidationError)
169
+
170
+ if isinstance(self.view, GeoMixin):
171
+ exception_klasses.append(PreconditionFailed)
172
+
173
+ status_codes = sorted({e.status_code for e in exception_klasses})
174
+
175
+ # choose serializer based on the status code
176
+ responses = {}
177
+ for status_code in status_codes:
178
+ error_serializer = (
179
+ ValidatieFoutSerializer
180
+ if status_code == exceptions.ValidationError.status_code
181
+ else FoutSerializer
182
+ )
183
+ responses[status_code] = error_serializer
184
+
185
+ return responses
186
+
187
+ def get_response_serializers(
188
+ self,
189
+ ) -> Dict[int, Optional[Type[serializers.Serializer]]]:
190
+ """append error serializers"""
191
+ response_serializers = super().get_response_serializers()
192
+
193
+ if self.method == "HEAD":
194
+ return {200: None}
195
+
196
+ if self.method == "DELETE":
197
+ status_code = 204
198
+ serializer = None
199
+ elif self._is_create_operation():
200
+ status_code = 201
201
+ serializer = response_serializers
202
+ else:
203
+ status_code = 200
204
+ serializer = response_serializers
205
+
206
+ responses = {
207
+ status_code: serializer,
208
+ **self.get_error_responses(),
209
+ }
210
+ return responses
211
+
212
+ def _get_response_for_code(
213
+ self, serializer, status_code, media_types=None, direction="response"
214
+ ):
215
+ """
216
+ choose media types and set descriptions
217
+ add custom response for expand
218
+ """
219
+ if not media_types:
220
+ if int(status_code) >= 400:
221
+ media_types = [ERROR_CONTENT_TYPE]
143
222
  else:
144
- generator = self.generator_class(
145
- getattr(self, "info", swagger_settings.DEFAULT_INFO),
146
- version,
147
- None,
148
- patterns=[],
149
- )
150
-
151
- schema = generator.get_schema(request, self.public)
152
- if schema is None:
153
- raise exceptions.PermissionDenied() # pragma: no cover
154
- return Response(schema)
223
+ media_types = ["application/json"]
155
224
 
156
- # serve the staticically included V3 schema
157
- SCHEMA_PATH = self.get_schema_path()
158
- with open(SCHEMA_PATH, "r") as infile:
159
- schema = yaml_sane_load(infile)
160
-
161
- # fix the servers
162
- for server in schema["servers"]:
163
- split_url = urlsplit(server["url"])
164
- if split_url.netloc:
165
- continue
166
-
167
- prefix = get_script_prefix()
168
- if prefix.endswith("/"):
169
- prefix = prefix[:-1]
170
- server_path = f"{prefix}{server['url']}"
171
- server["url"] = request.build_absolute_uri(server_path)
172
-
173
- return Response(data=schema, headers={"X-OAS-Version": schema["openapi"]})
225
+ response = super()._get_response_for_code(
226
+ serializer, status_code, media_types, direction
227
+ )
174
228
 
229
+ # add description based on the status code
230
+ if not response.get("description"):
231
+ response["description"] = HTTP_STATUS_CODE_TITLES.get(int(status_code), "")
232
+ return response
233
+
234
+ def get_override_parameters(self):
235
+ """Add request and response headers"""
236
+ version_headers = self.get_version_headers()
237
+ content_type_headers = self.get_content_type_headers()
238
+ cache_headers = self.get_cache_headers()
239
+ log_headers = self.get_log_headers()
240
+ location_headers = self.get_location_headers()
241
+ geo_headers = self.get_geo_headers()
242
+ return (
243
+ version_headers
244
+ + content_type_headers
245
+ + cache_headers
246
+ + log_headers
247
+ + location_headers
248
+ + geo_headers
249
+ )
175
250
 
176
- class SchemaView(SchemaMixin, DefaultSchemaView):
177
- pass
251
+ def get_version_headers(self) -> List[OpenApiParameter]:
252
+ return [
253
+ OpenApiParameter(
254
+ name=VERSION_HEADER,
255
+ type=str,
256
+ location=OpenApiParameter.HEADER,
257
+ description=_(
258
+ "Geeft een specifieke API-versie aan in de context van "
259
+ "een specifieke aanroep. Voorbeeld: 1.2.1."
260
+ ),
261
+ response=True,
262
+ )
263
+ ]
264
+
265
+ def get_content_type_headers(self) -> List[OpenApiParameter]:
266
+ if self.method not in ["POST", "PUT", "PATCH"]:
267
+ return []
268
+
269
+ mime_type_enum = [
270
+ cls.media_type
271
+ for cls in self.view.parser_classes
272
+ if hasattr(cls, "media_type")
273
+ ]
274
+
275
+ return [
276
+ OpenApiParameter(
277
+ name="Content-Type",
278
+ type=str,
279
+ location=OpenApiParameter.HEADER,
280
+ description=_("Content type of the request body."),
281
+ enum=mime_type_enum,
282
+ required=True,
283
+ )
284
+ ]
285
+
286
+ def get_cache_headers(self) -> List[OpenApiParameter]:
287
+ """
288
+ support ETag headers
289
+ """
290
+ if not has_cache_header(self.view):
291
+ return []
292
+
293
+ return [
294
+ OpenApiParameter(
295
+ name="If-None-Match",
296
+ type=str,
297
+ location=OpenApiParameter.HEADER,
298
+ required=False,
299
+ description=_(
300
+ "Perform conditional requests. This header should contain one or "
301
+ "multiple ETag values of resources the client has cached. If the "
302
+ "current resource ETag value is in this set, then an HTTP 304 "
303
+ "empty body will be returned. See "
304
+ "[MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) "
305
+ "for details."
306
+ ),
307
+ examples=[
308
+ OpenApiExample(
309
+ name="oneValue",
310
+ summary=_("One ETag value"),
311
+ value='"79054025255fb1a26e4bc422aef54eb4"',
312
+ ),
313
+ OpenApiExample(
314
+ name="multipleValues",
315
+ summary=_("Multiple ETag values"),
316
+ value='"79054025255fb1a26e4bc422aef54eb4", "e4d909c290d0fb1ca068ffaddf22cbd0"',
317
+ ),
318
+ ],
319
+ ),
320
+ OpenApiParameter(
321
+ name="ETag",
322
+ type=str,
323
+ location=OpenApiParameter.HEADER,
324
+ response=[200],
325
+ description=_(
326
+ "De ETag berekend op de response body JSON. "
327
+ "Indien twee resources exact dezelfde ETag hebben, dan zijn "
328
+ "deze resources identiek aan elkaar. Je kan de ETag gebruiken "
329
+ "om caching te implementeren."
330
+ ),
331
+ ),
332
+ ]
333
+
334
+ def get_location_headers(self) -> List[OpenApiParameter]:
335
+ return [
336
+ OpenApiParameter(
337
+ name="Location",
338
+ type=OpenApiTypes.URI,
339
+ location=OpenApiParameter.HEADER,
340
+ description=_("URL waar de resource leeft."),
341
+ response=[201],
342
+ ),
343
+ ]
344
+
345
+ def get_geo_headers(self) -> List[OpenApiParameter]:
346
+ if not isinstance(self.view, GeoMixin):
347
+ return []
348
+
349
+ request_headers = []
350
+ if self.method != "DELETE":
351
+ request_headers.append(
352
+ OpenApiParameter(
353
+ name=HEADER_ACCEPT,
354
+ type=str,
355
+ location=OpenApiParameter.HEADER,
356
+ required=False,
357
+ description=_(
358
+ "The desired 'Coordinate Reference System' (CRS) of the response data. "
359
+ "According to the GeoJSON spec, WGS84 is the default (EPSG: 4326 "
360
+ "is the same as WGS84)."
361
+ ),
362
+ enum=[DEFAULT_CRS],
363
+ )
364
+ )
365
+
366
+ if self.method in ("POST", "PUT", "PATCH"):
367
+ request_headers.append(
368
+ OpenApiParameter(
369
+ name=HEADER_CONTENT,
370
+ type=str,
371
+ location=OpenApiParameter.HEADER,
372
+ required=True,
373
+ description=_(
374
+ "The 'Coordinate Reference System' (CRS) of the request data. "
375
+ "According to the GeoJSON spec, WGS84 is the default (EPSG: 4326 "
376
+ "is the same as WGS84)."
377
+ ),
378
+ enum=[DEFAULT_CRS],
379
+ ),
380
+ )
381
+
382
+ response_headers = [
383
+ OpenApiParameter(
384
+ name=HEADER_CONTENT,
385
+ type=str,
386
+ location=OpenApiParameter.HEADER,
387
+ required=True,
388
+ description=_(
389
+ "The 'Coordinate Reference System' (CRS) of the request data. "
390
+ "According to the GeoJSON spec, WGS84 is the default (EPSG: 4326 "
391
+ "is the same as WGS84)."
392
+ ),
393
+ enum=[DEFAULT_CRS],
394
+ response=[200, 201],
395
+ )
396
+ ]
397
+
398
+ return request_headers + response_headers
399
+
400
+ def get_log_headers(self) -> List[OpenApiParameter]:
401
+ if not _view_supports_audittrail(self.view):
402
+ return []
403
+
404
+ return [
405
+ OpenApiParameter(
406
+ name=HEADER_LOGRECORD_ID,
407
+ type=str,
408
+ location=OpenApiParameter.HEADER,
409
+ required=False,
410
+ description=_(
411
+ "Identifier of the request, traceable throughout the network"
412
+ ),
413
+ ),
414
+ OpenApiParameter(
415
+ name=HEADER_AUDIT,
416
+ type=str,
417
+ location=OpenApiParameter.HEADER,
418
+ required=False,
419
+ description=_("Explanation why the request is done"),
420
+ ),
421
+ ]
422
+
423
+ def get_summary(self):
424
+ if self.method == "HEAD":
425
+ return _("De headers voor een specifiek(e) %(model)s opvragen ") % {
426
+ "model": self.view.queryset.model._meta.verbose_name.upper()
427
+ }
428
+ return super().get_summary()
429
+
430
+ def get_description(self):
431
+ if self.method == "HEAD":
432
+ return _("Vraag de headers op die je bij een GET request zou krijgen.")
433
+ return super().get_description()
@@ -5,6 +5,7 @@ from urllib.parse import urlparse
5
5
  from django.conf import settings
6
6
 
7
7
  import yaml
8
+ from requests_mock import Mocker
8
9
 
9
10
  DEFAULT_PATH_PARAMETERS = {"version": "1"}
10
11
 
@@ -69,3 +70,15 @@ def get_validation_errors(response, field, index=0):
69
70
  return error
70
71
 
71
72
  i += 1
73
+
74
+
75
+ def mock_service_oas_get(
76
+ mock: Mocker, url: str, service: str, oas_url: str = ""
77
+ ) -> None:
78
+ from zgw_consumers_oas.schema_loading import read_schema
79
+
80
+ if not oas_url:
81
+ oas_url = f"{url}schema/openapi.yaml?v=3"
82
+
83
+ content = read_schema(service)
84
+ mock.get(oas_url, content=content)
vng_api_common/utils.py CHANGED
@@ -13,9 +13,6 @@ from django.utils.encoding import smart_str
13
13
  from django.utils.module_loading import import_string
14
14
 
15
15
  from rest_framework.utils import formatting
16
- from zds_client.client import ClientError
17
-
18
- from .client import get_client
19
16
 
20
17
  try:
21
18
  from djangorestframework_camel_case.util import (
@@ -186,25 +183,6 @@ def get_uuid_from_path(path: str) -> str:
186
183
  return uuid_str
187
184
 
188
185
 
189
- def request_object_attribute(
190
- url: str, attribute: str, resource: Union[str, None] = None
191
- ) -> str:
192
- client = get_client(url)
193
-
194
- try:
195
- result = client.retrieve(resource, url=url)[attribute]
196
- except (ClientError, KeyError) as exc:
197
- logger.warning(
198
- "%s was retrieved from %s with the %s: %s",
199
- attribute,
200
- url,
201
- exc.__class__.__name__,
202
- exc,
203
- )
204
- result = ""
205
- return result
206
-
207
-
208
186
  def generate_unique_identification(instance: models.Model, date_field_name: str):
209
187
  model = type(instance)
210
188
  model_name = getattr(model, "IDENTIFICATIE_PREFIX", model._meta.model_name.upper())