commonground-api-common 2.1.2__py3-none-any.whl → 2.3.0__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.
- commonground_api_common-2.1.2.data/scripts/patch_content_types → commonground_api_common-2.3.0.data/scripts/generate_schema +2 -4
- {commonground_api_common-2.1.2.dist-info → commonground_api_common-2.3.0.dist-info}/METADATA +12 -3
- {commonground_api_common-2.1.2.dist-info → commonground_api_common-2.3.0.dist-info}/RECORD +27 -36
- {commonground_api_common-2.1.2.dist-info → commonground_api_common-2.3.0.dist-info}/WHEEL +1 -1
- vng_api_common/__init__.py +1 -1
- vng_api_common/api/views.py +1 -0
- vng_api_common/apps.py +43 -26
- vng_api_common/audittrails/utils.py +44 -0
- vng_api_common/conf/api.py +33 -45
- vng_api_common/contrib/setup_configuration/models.py +32 -0
- vng_api_common/contrib/setup_configuration/steps.py +46 -0
- vng_api_common/extensions/file.py +26 -0
- vng_api_common/extensions/gegevensgroep.py +16 -0
- vng_api_common/extensions/geojson.py +270 -0
- vng_api_common/extensions/hyperlink.py +37 -0
- vng_api_common/extensions/polymorphic.py +68 -0
- vng_api_common/extensions/query.py +20 -0
- vng_api_common/filters.py +0 -1
- vng_api_common/generators.py +12 -113
- vng_api_common/notifications/api/views.py +3 -3
- vng_api_common/oas.py +6 -7
- vng_api_common/schema.py +414 -158
- vng_api_common/views.py +1 -1
- commonground_api_common-2.1.2.data/scripts/generate_schema +0 -39
- commonground_api_common-2.1.2.data/scripts/use_external_components +0 -16
- vng_api_common/inspectors/cache.py +0 -57
- vng_api_common/inspectors/fields.py +0 -126
- vng_api_common/inspectors/files.py +0 -121
- vng_api_common/inspectors/geojson.py +0 -360
- vng_api_common/inspectors/polymorphic.py +0 -72
- vng_api_common/inspectors/query.py +0 -91
- vng_api_common/inspectors/utils.py +0 -40
- vng_api_common/inspectors/view.py +0 -547
- vng_api_common/management/commands/generate_autorisaties.py +0 -43
- vng_api_common/management/commands/generate_notificaties.py +0 -40
- vng_api_common/management/commands/generate_swagger.py +0 -197
- vng_api_common/management/commands/patch_error_contenttypes.py +0 -61
- vng_api_common/management/commands/use_external_components.py +0 -94
- vng_api_common/templates/vng_api_common/api_schema_to_markdown_table.md +0 -16
- vng_api_common/templates/vng_api_common/autorisaties.md +0 -15
- vng_api_common/templates/vng_api_common/notificaties.md +0 -24
- {commonground_api_common-2.1.2.dist-info → commonground_api_common-2.3.0.dist-info}/top_level.txt +0 -0
- /vng_api_common/{inspectors → contrib}/__init__.py +0 -0
- /vng_api_common/{management → contrib/setup_configuration}/__init__.py +0 -0
- /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
|
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.
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
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
|
-
|
106
|
-
|
107
|
-
|
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
|
-
|
110
|
-
|
111
|
-
CI to check for outdated schemas.
|
112
|
-
"""
|
118
|
+
if not scope_permissions:
|
119
|
+
return super().get_auth()
|
113
120
|
|
114
|
-
|
121
|
+
scopes = get_required_scopes(self.view.request, self.view)
|
122
|
+
if not scopes:
|
123
|
+
return []
|
115
124
|
|
116
|
-
|
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
|
123
|
-
|
124
|
-
|
125
|
-
|
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
|
-
|
128
|
-
|
129
|
-
|
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
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
-
|
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
|
-
|
157
|
-
|
158
|
-
|
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
|
-
|
177
|
-
|
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()
|
vng_api_common/views.py
CHANGED
@@ -15,7 +15,7 @@ from rest_framework import exceptions as drf_exceptions, status
|
|
15
15
|
from rest_framework.response import Response
|
16
16
|
from rest_framework.views import exception_handler as drf_exception_handler
|
17
17
|
|
18
|
-
from vng_api_common.client import Client
|
18
|
+
from vng_api_common.client import Client
|
19
19
|
|
20
20
|
from . import exceptions
|
21
21
|
from .compat import sentry_client
|
@@ -1,39 +0,0 @@
|
|
1
|
-
#!/bin/bash
|
2
|
-
|
3
|
-
# Run this script from the root of the repository
|
4
|
-
|
5
|
-
set -e
|
6
|
-
|
7
|
-
if [[ -z "$VIRTUAL_ENV" ]]; then
|
8
|
-
echo "You need to activate your virtual env before running this script"
|
9
|
-
exit 1
|
10
|
-
fi
|
11
|
-
|
12
|
-
echo "Generating Swagger schema"
|
13
|
-
src/manage.py generate_swagger \
|
14
|
-
./src/swagger2.0.json \
|
15
|
-
--overwrite \
|
16
|
-
--format=json \
|
17
|
-
--mock-request \
|
18
|
-
--url https://example.com/api/v1
|
19
|
-
|
20
|
-
echo "Converting Swagger to OpenAPI 3.0..."
|
21
|
-
npm run convert
|
22
|
-
patch_content_types
|
23
|
-
|
24
|
-
echo "Generating unresolved OpenAPI 3.0 schema"
|
25
|
-
use_external_components
|
26
|
-
|
27
|
-
echo "Generating resources document"
|
28
|
-
src/manage.py generate_swagger \
|
29
|
-
./src/resources.md \
|
30
|
-
--overwrite \
|
31
|
-
--mock-request \
|
32
|
-
--url https://example.com/api/v1 \
|
33
|
-
--to-markdown-table
|
34
|
-
|
35
|
-
echo "Generating autorisaties.md"
|
36
|
-
src/manage.py generate_autorisaties --output-file ./src/autorisaties.md
|
37
|
-
|
38
|
-
echo "Generating notificaties.md"
|
39
|
-
src/manage.py generate_notificaties --output-file ./src/notificaties.md
|
@@ -1,16 +0,0 @@
|
|
1
|
-
#!/bin/bash
|
2
|
-
|
3
|
-
# Run this script from the root of the repository
|
4
|
-
|
5
|
-
set -e
|
6
|
-
|
7
|
-
if [[ -z "$VIRTUAL_ENV" ]]; then
|
8
|
-
echo "You need to activate your virtual env before running this script"
|
9
|
-
exit 1
|
10
|
-
fi
|
11
|
-
|
12
|
-
source_file=${1:-./src/openapi.yaml}
|
13
|
-
output=${2:-./src/openapi_unresolved.yaml}
|
14
|
-
manage=${MANAGE:-src/manage.py}
|
15
|
-
|
16
|
-
python $manage use_external_components $source_file $output
|