Example #1
0
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")
                                  ]))
Example #2
0
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"
                                               ]))
Example #3
0
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")
Example #4
0
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")}))
    }
Example #5
0
 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)
Example #6
0
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")
Example #7
0
 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)
Example #8
0
    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))}
Example #9
0
 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
Example #10
0
 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
Example #11
0
 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
Example #12
0
 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)
Example #13
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"))
Example #14
0
def test_openapi_types_uuid():
    type_converter = OpenApiTypeConverter()
    openapi_type = type_converter.get_openapi_type(UUID)
    assert openapi_type == Schema(type="string", format="uuid")
Example #15
0
def test_openapi_types_bytes():
    type_converter = OpenApiTypeConverter()
    openapi_type = type_converter.get_openapi_type(bytes)
    assert openapi_type == Schema(type="string", format="binary")
Example #16
0
def test_openapi_types_float():
    type_converter = OpenApiTypeConverter()
    openapi_type = type_converter.get_openapi_type(float)
    assert openapi_type == Schema(type="number")
Example #17
0
def test_openapi_types_string():
    type_converter = OpenApiTypeConverter()
    openapi_type = type_converter.get_openapi_type(str)
    assert openapi_type == Schema(type="string")
Example #18
0
def test_openapi_types_int():
    type_converter = OpenApiTypeConverter()
    openapi_type = type_converter.get_openapi_type(int)
    assert openapi_type == Schema(type="integer")
Example #19
0
def test_openapi_types_bool():
    type_converter = OpenApiTypeConverter()
    openapi_type = type_converter.get_openapi_type(bool)
    assert openapi_type == Schema(type="boolean")
Example #20
0
 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]))
Example #21
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")
Example #22
0
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"))
Example #23
0
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)
Example #24
0
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")])
Example #25
0
 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]))
Example #26
0
 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)
Example #27
0
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"))
Example #28
0
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
Example #29
0
def test_openapi_types_tuple():
    type_converter = OpenApiTypeConverter()
    openapi_type = type_converter.get_openapi_type(tuple)
    assert openapi_type == Schema(type="array", items=Schema())