def add_path(self, path=None, operations=None, **kwargs): """Add a new path object to the spec. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#pathsObject :param str|None path: URL path component :param dict|None operations: describes the http methods and options for `path` :param dict kwargs: parameters used by any path helpers see :meth:`register_path_helper` """ def normalize_path(path): if path and 'basePath' in self.options: pattern = '^{0}'.format(re.escape(self.options['basePath'])) path = re.sub(pattern, '', path) return path path = normalize_path(path) operations = operations or OrderedDict() # Execute path helpers for plugin in self.plugins: try: ret = plugin.path_helper(path=path, operations=operations, **kwargs) except PluginMethodNotImplementedError: continue if ret is not None: path = normalize_path(ret) if not path: raise APISpecError('Path template is not specified') # Execute operation helpers for plugin in self.plugins: try: plugin.operation_helper(path=path, operations=operations, **kwargs) except PluginMethodNotImplementedError: continue # Execute response helpers # TODO: cache response helpers output for each (method, status_code) couple for method, operation in iteritems(operations): if method in VALID_METHODS and 'responses' in operation: for status_code, response in iteritems(operation['responses']): for plugin in self.plugins: try: response.update( plugin.response_helper(method, status_code, ** kwargs) or {}) except PluginMethodNotImplementedError: continue clean_operations(operations, self.openapi_version.major) self._paths.setdefault(path, operations).update(operations)
def path_from_view(spec, app, view, **kwargs): """Path helper that allows passing a Chalice view function.""" kwarg_ops = kwargs.get('operations') kwarg_ops = set() if not kwarg_ops else set(kwarg_ops) uri, methods = _route_for_view(app, view, path=kwargs.get('path', Path()), operations=kwarg_ops) operations = load_operations_from_docstring(view.__doc__) if not operations: operations = {} # check that the operations in the docstring match those of the actual route decorator path = Path(path=uri, operations={ method: op for method, op in iteritems(operations) if method in methods }) # add methods from route decorator that were not in docstring for op in methods: path.operations.setdefault(op, {}) return path
def fields2jsonschema(self, fields, ordered=False, partial=None): """Return the JSON Schema Object given a mapping between field names and :class:`Field <marshmallow.Field>` objects. :param dict fields: A dictionary of field name field object pairs :param bool ordered: Whether to preserve the order in which fields were declared :param bool|tuple partial: Whether to override a field's required flag. If `True` no fields will be set as required. If an iterable fields in the iterable will not be marked as required. :rtype: dict, a JSON Schema Object """ jsonschema = {"type": "object", "properties": OrderedDict() if ordered else {}} for field_name, field_obj in iteritems(fields): observed_field_name = self._observed_name(field_obj, field_name) property = self.field2property(field_obj) jsonschema["properties"][observed_field_name] = property if field_obj.required: if not partial or ( is_collection(partial) and field_name not in partial ): jsonschema.setdefault("required", []).append(observed_field_name) if "required" in jsonschema: jsonschema["required"].sort() return jsonschema
def metadata2properties(self, field): """Return a dictionary of properties extracted from field Metadata Will include field metadata that are valid properties of `OpenAPI schema objects <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject>`_ (e.g. “description”, “enum”, “example”). In addition, `specification extensions <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#specification-extensions>`_ are supported. Prefix `x_` to the desired extension when passing the keyword argument to the field constructor. apispec will convert `x_` to `x-` to comply with OpenAPI. :param Field field: A marshmallow field. :rtype: dict """ # Dasherize metadata that starts with x_ metadata = { key.replace("_", "-") if key.startswith("x_") else key: value for key, value in iteritems(field.metadata) } # Avoid validation error with "Additional properties not allowed" ret = { key: value for key, value in metadata.items() if key in _VALID_PROPERTIES or key.startswith(_VALID_PREFIX) } return ret
def clean_operations(operations, openapi_major_version): """Ensure that all parameters with "in" equal to "path" are also required as required by the OpenAPI specification, as well as normalizing any references to global parameters. Also checks for invalid HTTP methods. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject. :param dict operations: Dict mapping status codes to operations :param int openapi_major_version: The major version of the OpenAPI standard to use. Supported values are 2 and 3. """ invalid = { key for key in set(iterkeys(operations)) - set(VALID_METHODS[openapi_major_version]) if not key.startswith("x-") } if invalid: raise APISpecError("One or more HTTP methods are invalid: {}".format( ", ".join(invalid))) def get_ref(obj_type, obj, openapi_major_version): """Return object or rererence If obj is a dict, it is assumed to be a complete description and it is returned as is. Otherwise, it is assumed to be a reference name as string and the corresponding $ref string is returned. :param str obj_type: "parameter" or "response" :param dict|str obj: parameter or response in dict form or as ref_id string :param int openapi_major_version: The major version of the OpenAPI standard """ if isinstance(obj, dict): return obj return build_reference(obj_type, openapi_major_version, obj) for operation in (operations or {}).values(): if "parameters" in operation: parameters = operation["parameters"] for parameter in parameters: if (isinstance(parameter, dict) and "in" in parameter and parameter["in"] == "path"): parameter["required"] = True operation["parameters"] = [ get_ref("parameter", p, openapi_major_version) for p in parameters ] if "responses" in operation: responses = OrderedDict() for code, response in iteritems(operation["responses"]): try: code = int(code) # handles IntEnums like http.HTTPStatus except (TypeError, ValueError): if openapi_major_version < 3: warnings.warn( "Non-integer code not allowed in OpenAPI < 3") responses[str(code)] = get_ref("response", response, openapi_major_version) operation["responses"] = responses
def load_operations_from_docstring(docstring): """Return a dictionary of OpenAPI operations parsed from a a docstring. """ doc_data = load_yaml_from_docstring(docstring) return { key: val for key, val in iteritems(doc_data) if key in PATH_KEYS or key.startswith('x-') }
def to_dict(self): subsections = { "schema": self._schemas, "parameter": self._parameters, "response": self._responses, "security_scheme": self._security_schemes, } return { COMPONENT_SUBSECTIONS[self.openapi_version.major][k]: v for k, v in iteritems(subsections) if v != {} }
def views(): ret = ['<ul>'] view_funcs = APP.view_functions for ep, view_func in iteritems(view_funcs): ret.append('<li>View Function = ' + view_func.__name__ + ', ep = ' + ep + '<ul>') for rule in APP.url_map._rules_by_endpoint[ep]: ret.append('<li>' + rule.rule + '</li>') ret.append('</ul></li>') ret.append('</ul>') return "".join(ret)
def _route_for_view(current_app, view, path=Path(), operations=set()): view_funcs = current_app.routes for uri, endpoint in iteritems(view_funcs): methods = set() for method, route_entry in iteritems(endpoint): method = method.lower() if route_entry.view_function == view and (not operations or method in operations): if path.path and not path.path == uri: break else: methods.add(method) else: if methods: return uri, methods raise APISpecError( 'Could not find endpoint for view {0} and path {1}'.format( view, getattr(path, 'path', None)))
def load_operations_from_docstring(docstring): """Return a dictionary of Swagger operations parsed from a a docstring. """ doc_data = load_yaml_from_docstring(docstring) if doc_data: return { key: val for key, val in iteritems(doc_data) if key in PATH_KEYS } else: return None
def _rule_for_view(view): view_funcs = current_app.view_functions endpoint = None for ep, view_func in iteritems(view_funcs): if view_func == view: endpoint = ep if not endpoint: raise APISpecError('Could not find endpoint for view {0}'.format(view)) # WARNING: Assume 1 rule per view function for now rule = current_app.url_map._rules_by_endpoint[endpoint][0] return rule
def _rule_for_view(view, app=None): if app is None: app = current_app view_funcs = app.view_functions endpoint = None for ept, view_func in iteritems(view_funcs): if view_func == view: endpoint = ept if not endpoint: raise APISpecError( "Could not find endpoint for view {}".format(view)) # WARNING: Assume 1 rule per view function for now rule = app.url_map._rules_by_endpoint[endpoint][0] return rule
def fields2parameters(self, fields, default_in="body"): """Return an array of OpenAPI parameters given a mapping between field names and :class:`Field <marshmallow.Field>` objects. If `default_in` is "body", then return an array of a single parameter; else return an array of a parameter for each included field in the :class:`Schema <marshmallow.Schema>`. In OpenAPI3, only "query", "header", "path" or "cookie" are allowed for the location of parameters. In OpenAPI 3, "requestBody" is used when fields are in the body. This function always returns a list, with a parameter for each included field in the :class:`Schema <marshmallow.Schema>`. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject """ parameters = [] body_param = None for field_name, field_obj in iteritems(fields): if field_obj.dump_only: continue param = self.field2parameter( field_obj, name=self._observed_name(field_obj, field_name), default_in=default_in, ) if ( self.openapi_version.major < 3 and param["in"] == "body" and body_param is not None ): body_param["schema"]["properties"].update(param["schema"]["properties"]) required_fields = param["schema"].get("required", []) if required_fields: body_param["schema"].setdefault("required", []).extend( required_fields ) else: if self.openapi_version.major < 3 and param["in"] == "body": body_param = param parameters.append(param) return parameters
def clean_operations(operations, openapi_major_version): """Ensure that all parameters with "in" equal to "path" are also required as required by the OpenAPI specification, as well as normalizing any references to global parameters. Also checks for invalid HTTP methods. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject. :param dict operations: Dict mapping status codes to operations :param int openapi_major_version: The major version of the OpenAPI standard to use. Supported values are 2 and 3. """ invalid = { key for key in set(iterkeys(operations)) - set(VALID_METHODS[openapi_major_version]) if not key.startswith("x-") } if invalid: raise APISpecError("One or more HTTP methods are invalid: {}".format( ", ".join(invalid))) for operation in (operations or {}).values(): if "parameters" in operation: operation["parameters"] = clean_parameters(operation["parameters"], openapi_major_version) if "responses" in operation: responses = OrderedDict() for code, response in iteritems(operation["responses"]): try: code = int(code) # handles IntEnums like http.HTTPStatus except (TypeError, ValueError): if openapi_major_version < 3: warnings.warn( "Non-integer code not allowed in OpenAPI < 3") responses[str(code)] = get_ref("response", response, openapi_major_version) operation["responses"] = responses
def add_path(self, path=None, operations=None, **kwargs): """Add a new path object to the spec. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#pathsObject :param str|Path|None path: URL Path component or Path instance :param dict|None operations: describes the http methods and options for `path` :param dict kwargs: parameters used by any path helpers see :meth:`register_path_helper` """ def normalize_path(path): if path and 'basePath' in self.options: pattern = '^{0}'.format(re.escape(self.options['basePath'])) path = re.sub(pattern, '', path) return path if isinstance(path, Path): path.path = normalize_path(path.path) if operations: path.operations.update(operations) else: path = Path( path=normalize_path(path), operations=operations, openapi_version=self.openapi_version, ) # Execute path helpers for plugin in self.plugins: try: ret = plugin.path_helper(path=path, operations=path.operations, **kwargs) except PluginMethodNotImplementedError: continue if isinstance(ret, Path): ret.path = normalize_path(ret.path) path.update(ret) # Deprecated interface for func in self._path_helpers: try: ret = func(self, path=path, operations=path.operations, **kwargs) except TypeError: continue if isinstance(ret, Path): ret.path = normalize_path(ret.path) path.update(ret) if not path.path: raise APISpecError('Path template is not specified') # Execute operation helpers for plugin in self.plugins: try: plugin.operation_helper(path=path, operations=path.operations, **kwargs) except PluginMethodNotImplementedError: continue # Deprecated interface for func in self._operation_helpers: func(self, path=path, operations=path.operations, **kwargs) # Execute response helpers # TODO: cache response helpers output for each (method, status_code) couple for method, operation in iteritems(path.operations): if method in VALID_METHODS and 'responses' in operation: for status_code, response in iteritems(operation['responses']): for plugin in self.plugins: try: response.update( plugin.response_helper(method, status_code, ** kwargs) or {}) except PluginMethodNotImplementedError: continue # Deprecated interface # Rule is that method + http status exist in both operations and helpers methods = set(iterkeys(path.operations)) & set( iterkeys(self._response_helpers)) for method in methods: responses = path.operations[method]['responses'] statuses = set(iterkeys(responses)) & set( iterkeys(self._response_helpers[method])) for status_code in statuses: for func in self._response_helpers[method][status_code]: responses[status_code].update(func(self, **kwargs), ) self._paths.setdefault(path.path, path.operations).update(path.operations)
def clean_operations(operations, openapi_major_version): """Ensure that all parameters with "in" equal to "path" are also required as required by the OpenAPI specification, as well as normalizing any references to global parameters. Also checks for invalid HTTP methods. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#parameterObject. :param dict operations: Dict mapping status codes to operations :param int openapi_major_version: The major version of the OpenAPI standard to use. Supported values are 2 and 3. """ invalid = { key for key in set(iterkeys(operations)) - set(VALID_METHODS) if not key.startswith('x-') } if invalid: raise APISpecError( 'One or more HTTP methods are invalid: {0}'.format( ', '.join(invalid)), ) def get_ref(obj_type, obj, openapi_major_version): """Return object or rererence If obj is a dict, it is assumed to be a complete description and it is returned as is. Otherwise, it is assumed to be a reference name as string and the corresponding $ref string is returned. :param str obj_type: 'parameter' or 'response' :param dict|str obj: parameter or response in dict form or as ref_id string :param int openapi_major_version: The major version of the OpenAPI standard """ if isinstance(obj, dict): return obj ref_paths = { 'parameter': { 2: 'parameters', 3: 'components/parameters', }, 'response': { 2: 'responses', 3: 'components/responses', }, } ref_path = ref_paths[obj_type][openapi_major_version] return {'$ref': '#/{0}/{1}'.format(ref_path, obj)} for operation in (operations or {}).values(): if 'parameters' in operation: parameters = operation['parameters'] for parameter in parameters: if (isinstance(parameter, dict) and 'in' in parameter and parameter['in'] == 'path'): parameter['required'] = True operation['parameters'] = [ get_ref('parameter', p, openapi_major_version) for p in parameters ] if 'responses' in operation: for code, response in iteritems(operation['responses']): operation['responses'][code] = get_ref('response', response, openapi_major_version)
def clean_operations(operations, openapi_major_version): """Ensure that all parameters with "in" equal to "path" are also required as required by the OpenAPI specification, as well as normalizing any references to global parameters. Also checks for invalid HTTP methods. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject. :param dict operations: Dict mapping status codes to operations :param int openapi_major_version: The major version of the OpenAPI standard to use. Supported values are 2 and 3. """ invalid = { key for key in set(iterkeys(operations)) - set(VALID_METHODS[openapi_major_version]) if not key.startswith("x-") } if invalid: raise APISpecError("One or more HTTP methods are invalid: {}".format( ", ".join(invalid))) def get_ref(obj_type, obj, openapi_major_version): """Return object or rererence If obj is a dict, it is assumed to be a complete description and it is returned as is. Otherwise, it is assumed to be a reference name as string and the corresponding $ref string is returned. :param str obj_type: 'parameter' or 'response' :param dict|str obj: parameter or response in dict form or as ref_id string :param int openapi_major_version: The major version of the OpenAPI standard """ if isinstance(obj, dict): return obj ref_paths = { "parameter": { 2: "parameters", 3: "components/parameters" }, "response": { 2: "responses", 3: "components/responses" }, } ref_path = ref_paths[obj_type][openapi_major_version] return {"$ref": "#/{}/{}".format(ref_path, obj)} for operation in (operations or {}).values(): if "parameters" in operation: parameters = operation["parameters"] for parameter in parameters: if (isinstance(parameter, dict) and "in" in parameter and parameter["in"] == "path"): parameter["required"] = True operation["parameters"] = [ get_ref("parameter", p, openapi_major_version) for p in parameters ] if "responses" in operation: for code, response in iteritems(operation["responses"]): operation["responses"][code] = get_ref("response", response, openapi_major_version)