def test_openapi_types_list(): type_converter = OpenApiTypeConverter() openapi_type = type_converter.get_openapi_type(List[Union[int, UUID]]) assert openapi_type == Schema(type="array", items=Schema(anyOf=[ Schema(type="integer"), Schema(type="string", format="uuid") ]))
def test_openapi_types_enum(): type_converter = OpenApiTypeConverter() openapi_type = type_converter.get_openapi_type(List[ResourceAction]) assert openapi_type == Schema(type="array", items=Schema(type="string", enum=[ "store", "push", "pull", "deploy", "dryrun", "getfact", "other" ]))
def test_openapi_types_anyurl(): type_converter = OpenApiTypeConverter() openapi_type = type_converter.get_openapi_type(AnyUrl) assert openapi_type == Schema(type="string", format="uri") openapi_type = type_converter.get_openapi_type(AnyHttpUrl) assert openapi_type == Schema(type="string", format="uri") openapi_type = type_converter.get_openapi_type(PostgresDsn) assert openapi_type == Schema(type="string", format="uri")
def test_return_value(api_methods_fixture): operation_handler = OperationHandler( OpenApiTypeConverter(), ArgOptionHandler(OpenApiTypeConverter())) json_response_content = operation_handler._build_return_value_wrapper( MethodProperties.methods["post_method"][0]) assert json_response_content == { "application/json": MediaType(schema=Schema(type="object", properties={"data": Schema(type="object")})) }
def _handle_pydantic_model(self, type_annotation: Type, by_alias: bool = True) -> Schema: # JsonSchema stores the model (and sub-model) definitions at #/definitions, # but OpenAPI requires them to be placed at "#/components/schemas/" # The ref_prefix changes the references, but the actual schemas are still at #/definitions schema = model_schema(type_annotation, by_alias=by_alias, ref_prefix=self.ref_prefix) if "definitions" in schema.keys(): definitions: Dict[str, Dict[str, object]] = schema.pop("definitions") if self.components.schemas is not None: for key, definition in definitions.items(): self.components.schemas[key] = Schema(**definition) return Schema(**schema)
def test_get_openapi_types(): type_converter = OpenApiTypeConverter() openapi_type = type_converter.get_openapi_type_of_parameter( inspect.Parameter("param", kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=UUID) ) assert openapi_type == Schema(type="string", format="uuid") openapi_type = type_converter.get_openapi_type_of_parameter( inspect.Parameter("param", kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=int) ) assert openapi_type == Schema(type="integer")
def _handle_union_type(self, type_annotation: Type) -> Schema: # An Optional is always a Union type_args = typing_inspect.get_args(type_annotation, evaluate=True) openapi_types = [self.get_openapi_type(type_arg) for type_arg in type_args if not self._is_none_type(type_arg)] none_type_in_type_args = len(openapi_types) < len(type_args) if none_type_in_type_args: if len(openapi_types) == 1: openapi_type = openapi_types[0].copy(deep=True) # An Optional in the OpenAPI Schema is nullable openapi_type.nullable = True return openapi_type return Schema(anyOf=openapi_types, nullable=True) # A Union type can be expressed as a schema that matches any of the type arguments return Schema(anyOf=openapi_types)
def _build_return_value_wrapper(self, url_method_properties: MethodProperties) -> Optional[Dict[str, MediaType]]: return_type = inspect.signature(url_method_properties.function).return_annotation if return_type is None or return_type == inspect.Signature.empty: return None return_properties: Optional[Dict[str, Schema]] = None if return_type == ReturnValue or is_generic_type(return_type) and get_origin(return_type) == ReturnValue: # Dealing with the special case of ReturnValue[...] links_type = self.type_converter.get_openapi_type(Dict[str, str]) links_type.title = "Links" links_type.nullable = True warnings_type = self.type_converter.get_openapi_type(List[str]) warnings_type.title = "Warnings" return_properties = { "links": links_type, "metadata": Schema( title="Metadata", nullable=True, type="object", properties={ "warnings": warnings_type, }, ), } type_args = get_args(return_type, evaluate=True) if not type_args or len(type_args) != 1: raise RuntimeError( "ReturnValue definition should take one type Argument, e.g. ReturnValue[None]. " f"Got this instead: {type_args}" ) if not url_method_properties.envelope: raise RuntimeError("Methods returning a ReturnValue object should always have an envelope") if type_args[0] != NoneType: return_properties[url_method_properties.envelope_key] = self.type_converter.get_openapi_type(type_args[0]) else: openapi_return_type = self.type_converter.get_openapi_type(return_type) if url_method_properties.envelope: return_properties = {url_method_properties.envelope_key: openapi_return_type} return {"application/json": MediaType(schema=Schema(type="object", properties=return_properties))}
def _build_json_request_body(self, properties: Dict) -> RequestBody: request_body = RequestBody( required=True, content={"application/json": MediaType(schema=Schema(type="object", properties=properties))}, description=self._get_request_body_description(), ) return request_body
def extract_response_headers_from_arg_options( self, arg_options: Dict[str, ArgOption] ) -> Optional[Dict[str, Union[Header, Reference]]]: headers: Dict[str, Union[Header, Reference]] = {} for option_name, option in arg_options.items(): if option.header and option.reply_header: headers[option.header] = Header(description=option.header, schema=Schema(type="string")) return headers if headers else None
def _build_return_value_wrapper(self, url_method_properties: MethodProperties) -> Optional[Dict[str, MediaType]]: return_type = inspect.signature(url_method_properties.function).return_annotation if return_type is not None and return_type != inspect.Signature.empty: return_properties: Optional[Dict[str, SchemaBase]] = None openapi_return_type = self.type_converter.get_openapi_type(return_type) if url_method_properties.envelope: return_properties = {url_method_properties.envelope_key: openapi_return_type} return {"application/json": MediaType(schema=Schema(type="object", properties=return_properties))} return None
def _handle_pydantic_model(self, type_annotation: Type) -> Schema: # JsonSchema stores the model (and sub-model) definitions at #/definitions, # but OpenAPI requires them to be placed at "#/components/schemas/" # The ref_prefix changes the references, but the actual schemas are still at #/definitions schema = model_schema(type_annotation, by_alias=True, ref_prefix="#/components/schemas/") if "definitions" in schema.keys(): definitions = schema.pop("definitions") if self.components.schemas is not None: self.components.schemas.update(definitions) return Schema(**schema)
def get_openapi_type(self, type_annotation: Type) -> Schema: type_origin = typing_inspect.get_origin(type_annotation) if typing_inspect.is_union_type(type_annotation): return self._handle_union_type(type_annotation) elif inspect.isclass(type_annotation) and issubclass(type_annotation, BaseModel): return self._handle_pydantic_model(type_annotation) elif inspect.isclass(type_annotation) and issubclass(type_annotation, Enum): return self._handle_enums(type_annotation) elif inspect.isclass(type_origin) and issubclass(type_origin, typing.Mapping): return self._handle_dictionary(type_annotation) elif inspect.isclass(type_origin) and issubclass(type_origin, typing.Sequence): return self._handle_list(type_annotation) # Fallback to primitive types return self.python_to_openapi_types.get(type_annotation, Schema(type="object"))
def test_openapi_types_uuid(): type_converter = OpenApiTypeConverter() openapi_type = type_converter.get_openapi_type(UUID) assert openapi_type == Schema(type="string", format="uuid")
def test_openapi_types_bytes(): type_converter = OpenApiTypeConverter() openapi_type = type_converter.get_openapi_type(bytes) assert openapi_type == Schema(type="string", format="binary")
def test_openapi_types_float(): type_converter = OpenApiTypeConverter() openapi_type = type_converter.get_openapi_type(float) assert openapi_type == Schema(type="number")
def test_openapi_types_string(): type_converter = OpenApiTypeConverter() openapi_type = type_converter.get_openapi_type(str) assert openapi_type == Schema(type="string")
def test_openapi_types_int(): type_converter = OpenApiTypeConverter() openapi_type = type_converter.get_openapi_type(int) assert openapi_type == Schema(type="integer")
def test_openapi_types_bool(): type_converter = OpenApiTypeConverter() openapi_type = type_converter.get_openapi_type(bool) assert openapi_type == Schema(type="boolean")
def _handle_list(self, type_annotation: Type) -> Schema: # Type argument is always present, see protocol.common.MethodProperties._validate_type_arg() list_member_type = typing_inspect.get_args(type_annotation, evaluate=True) return Schema(type="array", items=self.get_openapi_type(list_member_type[0]))
def test_openapi_types_datetime(): type_converter = OpenApiTypeConverter() openapi_type = type_converter.get_openapi_type(datetime) assert openapi_type == Schema(type="string", format="date-time")
def test_openapi_types_dict(): type_converter = OpenApiTypeConverter() openapi_type = type_converter.get_openapi_type(Dict[str, UUID]) assert openapi_type == Schema(type="object", additionalProperties=Schema(type="string", format="uuid"))
def test_openapi_types_optional(): type_converter = OpenApiTypeConverter() openapi_type = type_converter.get_openapi_type(Optional[str]) assert openapi_type == Schema(type="string", nullable=True)
def test_openapi_types_union(): type_converter = OpenApiTypeConverter() openapi_type = type_converter.get_openapi_type(Union[str, bytes]) assert openapi_type == Schema(anyOf=[Schema(type="string"), Schema(type="string", format="binary")])
def _handle_dictionary(self, type_annotation: Type) -> Schema: type_args = typing_inspect.get_args(type_annotation, evaluate=True) return Schema(type="object", additionalProperties=self.get_openapi_type(type_args[1]))
def _handle_enums(self, type_annotation: Type) -> Schema: enum_keys = [name for name in type_annotation.__members__.keys()] return Schema(type="string", enum=enum_keys)
class OpenApiTypeConverter: """ Lookup for OpenAPI types corresponding to python types """ components = Components(schemas={}) python_to_openapi_types = { bool: Schema(type="boolean"), int: Schema(type="integer"), str: Schema(type="string"), tuple: Schema(type="array", items=Schema()), list: Schema(type="array", items=Schema()), dict: Schema(type="object"), float: Schema(type="number", format="float"), bytes: Schema(type="string", format="binary"), datetime: Schema(type="string", format="date-time"), uuid.UUID: Schema(type="string", format="uuid"), typing.Any: Schema(), types.StrictNonIntBool: Schema(type="boolean"), } def get_openapi_type_of_parameter(self, parameter_type: inspect.Parameter) -> Schema: type_annotation = parameter_type.annotation return self.get_openapi_type(type_annotation) def _is_none_type(self, type_annotation: Type) -> bool: return inspect.isclass(type_annotation) and issubclass(type_annotation, type(None)) def _handle_union_type(self, type_annotation: Type) -> Schema: # An Optional is always a Union type_args = typing_inspect.get_args(type_annotation, evaluate=True) openapi_types = [self.get_openapi_type(type_arg) for type_arg in type_args if not self._is_none_type(type_arg)] none_type_in_type_args = len(openapi_types) < len(type_args) if none_type_in_type_args: if len(openapi_types) == 1: openapi_type = openapi_types[0].copy(deep=True) # An Optional in the OpenAPI Schema is nullable openapi_type.nullable = True return openapi_type return Schema(anyOf=openapi_types, nullable=True) # A Union type can be expressed as a schema that matches any of the type arguments return Schema(anyOf=openapi_types) def _handle_dictionary(self, type_annotation: Type) -> Schema: type_args = typing_inspect.get_args(type_annotation, evaluate=True) return Schema(type="object", additionalProperties=self.get_openapi_type(type_args[1])) def _handle_pydantic_model(self, type_annotation: Type) -> Schema: # JsonSchema stores the model (and sub-model) definitions at #/definitions, # but OpenAPI requires them to be placed at "#/components/schemas/" # The ref_prefix changes the references, but the actual schemas are still at #/definitions schema = model_schema(type_annotation, by_alias=True, ref_prefix="#/components/schemas/") if "definitions" in schema.keys(): definitions = schema.pop("definitions") if self.components.schemas is not None: self.components.schemas.update(definitions) return Schema(**schema) def _handle_enums(self, type_annotation: Type) -> Schema: enum_keys = [name for name in type_annotation.__members__.keys()] return Schema(type="string", enum=enum_keys) def _handle_list(self, type_annotation: Type) -> Schema: # Type argument is always present, see protocol.common.MethodProperties._validate_type_arg() list_member_type = typing_inspect.get_args(type_annotation, evaluate=True) return Schema(type="array", items=self.get_openapi_type(list_member_type[0])) def get_openapi_type(self, type_annotation: Type) -> Schema: type_origin = typing_inspect.get_origin(type_annotation) if typing_inspect.is_union_type(type_annotation): return self._handle_union_type(type_annotation) elif inspect.isclass(type_annotation) and issubclass(type_annotation, BaseModel): return self._handle_pydantic_model(type_annotation) elif inspect.isclass(type_annotation) and issubclass(type_annotation, Enum): return self._handle_enums(type_annotation) elif inspect.isclass(type_origin) and issubclass(type_origin, typing.Mapping): return self._handle_dictionary(type_annotation) elif inspect.isclass(type_origin) and issubclass(type_origin, typing.Sequence): return self._handle_list(type_annotation) # Fallback to primitive types return self.python_to_openapi_types.get(type_annotation, Schema(type="object"))
def test_openapi_schema() -> None: ref_prefix = "#/" schemas = { "person": Schema( **{ "title": "Person", "properties": { "address": { "$ref": ref_prefix + "address", }, "age": { "title": "Age", "type": "integer", }, }, } ), "address": Schema( **{ "title": "Address", "properties": { "street": { "title": "Street", "type": "string", }, "number": { "title": "Number", "type": "integer", }, "city": { "title": "City", "type": "string", }, }, } ), } assert schemas["person"] == Schema( title="Person", properties={ "address": Schema(**{"$ref": ref_prefix + "address"}), "age": Schema(title="Age", type="integer"), }, ) assert schemas["address"] == Schema( title="Address", properties={ "street": Schema(title="Street", type="string"), "number": Schema(title="Number", type="integer"), "city": Schema(title="City", type="string"), }, ) assert Schema(**{"$ref": ref_prefix + "person"}).resolve(ref_prefix, schemas) == schemas["person"] assert Schema(**{"$ref": ref_prefix + "address"}).resolve(ref_prefix, schemas) == schemas["address"] person_schema = schemas["person"].copy(deep=True) assert not Schema(**{"$ref": ref_prefix + "person"}).recursive_resolve(ref_prefix, schemas, update={}) == person_schema person_schema.properties["address"] = schemas["address"] assert Schema(**{"$ref": ref_prefix + "person"}).recursive_resolve(ref_prefix, schemas, update={}) == person_schema
def test_openapi_types_tuple(): type_converter = OpenApiTypeConverter() openapi_type = type_converter.get_openapi_type(tuple) assert openapi_type == Schema(type="array", items=Schema())