def _verify_parameters( path: str, parameters: Sequence[OpenAPIParameter], ): """Verifies matching of parameters to the placeholders used in an URL-Template This works both ways, ensuring that no parameter is supplied which is then not used and that each template-variable in the URL-template has a corresponding parameter supplied, either globally or locally. Args: path: The URL-Template, for eample: '/user/{username}' parameters: A list of parameters. A parameter can either be a string referencing a globally defined parameter by name, or a dict containing a full parameter. Examples: In case of success, this function will return nothing. >>> _verify_parameters('/foo/{bar}', [{'name': 'bar', 'in': 'path'}]) Yet, when problems are found, ValueErrors are raised. >>> _verify_parameters('/foo', [{'name': 'foo', 'in': 'path'}]) Traceback (most recent call last): ... ValueError: Param 'foo', which is specified as 'path', not used in path. Found params: [] >>> _verify_parameters('/foo/{bar}', []) Traceback (most recent call last): ... ValueError: Param 'bar', which is used in the HTTP path, was not specified in [] Returns: Nothing. Raises: ValueError in case of a mismatch. """ param_names = _names_of(parameters) path_params = path_parameters(path) for path_param in path_params: if path_param not in param_names: raise ValueError( f"Param {path_param!r}, which is used in the HTTP path, was not specified in " f"{parameters!r}") for param in parameters: if isinstance( param, dict ) and param['in'] == 'path' and param['name'] not in path_params: raise ValueError( f"Param {repr(param['name'])}, which is specified as 'path', not used in path. " f"Found params: {path_params}") find_all_parameters(parameters, errors='raise')
def __call__(self, func): """This is the real decorator. Returns: A wrapped function. The wrapper does input and output validation. """ wrapped = wrap_with_validation(func, self.request_schema, self.response_schema) self.func = func self.operation_id = func.__module__ + "." + func.__name__ ENDPOINT_REGISTRY.add_endpoint( self, find_all_parameters(_reduce_to_primitives(self.parameters)), ) if not self.output_empty and self.response_schema is None: raise ValueError( f"{self.operation_id}: 'response_schema' required when output will be sent!" ) if self.output_empty and self.response_schema: raise ValueError( f"{self.operation_id}: On empty output 'output_schema' may not be used." ) return wrapped
def _add_api_spec(func): module_obj = import_string(func.__module__) module_name = module_obj.__name__ operation_id = func.__module__ + "." + func.__name__ ENDPOINT_REGISTRY.add_endpoint( module_name, name, method, path, find_all_parameters(primitive_parameters), ) if not output_empty and response_schema is None: raise ValueError(f"{operation_id}: 'response_schema' required when " f"output will be sent!") if output_empty and response_schema: raise ValueError(f"{operation_id}: On empty output 'output_schema' may not be used.") headers: Dict[str, PrimitiveParameter] = {} if etag in ('output', 'both'): headers.update(ETAG_HEADER_PARAM.header_dict()) responses: ResponseType = {} # We don't(!) support any endpoint without an output schema. # Just define one! if response_schema is not None: responses['200'] = { 'content': { content_type: { 'schema': response_schema }, }, 'description': apispec.utils.dedent(response_schema.__doc__ or ''), 'headers': headers, } if will_do_redirects: responses['302'] = { 'description': ('Either the resource has moved or has not yet completed. Please see this ' 'resource for further information.') } # Actually, iff you don't want to give out anything, then we don't need a schema. if output_empty: responses['204'] = { 'description': 'Operation done successfully. No further output.', 'headers': headers, } tag_obj: OpenAPITag = { 'name': module_name, } docstring_name = _docstring_name(module_obj.__doc__) if docstring_name: tag_obj['x-displayName'] = docstring_name docstring_desc = _docstring_description(module_obj.__doc__) if docstring_desc: tag_obj['description'] = docstring_desc _add_tag(tag_obj, tag_group='Endpoints') operation_spec: OperationSpecType = { 'operationId': operation_id, 'tags': [module_name], 'description': '', 'responses': { 'default': { 'description': 'Any unsuccessful or unexpected result.', 'content': { 'application/problem+json': { 'schema': error_schema, } } } }, 'parameters': primitive_parameters, } if etag in ('input', 'both'): operation_spec['parameters'].append(ETAG_IF_MATCH_HEADER.to_dict()) operation_spec['responses'].update(responses) if request_schema is not None: tag = _tag_from_schema(request_schema) _add_tag(tag, tag_group='Request Schemas') operation_spec['requestBody'] = { 'required': request_body_required, 'content': { 'application/json': { 'schema': request_schema, } } } operation_spec['x-codeSamples'] = code_samples(path, method, request_schema, operation_spec) # If we don't have any parameters we remove the empty list, so the spec will not have it. if not operation_spec['parameters']: del operation_spec['parameters'] docstring_name = _docstring_name(func.__doc__) if docstring_name: operation_spec['summary'] = docstring_name docstring_desc = _docstring_description(func.__doc__) if docstring_desc: operation_spec['description'] = docstring_desc apispec.utils.deepupdate(operation_spec, options) if func not in _SEEN_ENDPOINTS: # NOTE: # Only add the endpoint to the spec if not already done. We don't want # to add it multiple times. While this shouldn't be happening, doctest # sometimes complains that it would be happening. Not sure why. Anyways, # it's not a big deal as long as the SPEC is written correctly. SPEC.path(path=path, operations={method.lower(): operation_spec}) _SEEN_ENDPOINTS.add(func) return wrap_with_validation(func, request_schema, response_schema)