langchain/tests/unit_tests/tools/openapi/test_api_models.py
William FH f240651bd8
Add Request body (#2507)
This still doesn't handle the following

- non-JSON media types
- anyOf, allOf, oneOf's

And doesn't emit the typescript definitions for referred types yet, but
that can be saved for a separate PR.

Also, we could have better support for Swagger 2.0 specs and OpenAPI
3.0.3 (can use the same lib for the latter) recommend offline conversion
for now.
2023-04-06 13:02:42 -07:00

197 lines
5.9 KiB
Python

"""Test the APIOperation class."""
import json
import os
from pathlib import Path
from typing import Iterable, List, Tuple
import pytest
import yaml
from openapi_schema_pydantic import (
Components,
Info,
MediaType,
Reference,
RequestBody,
Schema,
)
from langchain.tools.openapi.utils.api_models import (
APIOperation,
APIRequestBody,
APIRequestBodyProperty,
)
from langchain.tools.openapi.utils.openapi_utils import HTTPVerb, OpenAPISpec
_DIR = Path(__file__).parent
def _get_test_specs() -> Iterable[Path]:
"""Walk the test_specs directory and collect all files with the name 'apispec'
in them.
"""
test_specs_dir = _DIR / "test_specs"
return (
Path(root) / file
for root, _, files in os.walk(test_specs_dir)
for file in files
if file.startswith("apispec")
)
def _get_paths_and_methods_from_spec_dictionary(
spec: dict,
) -> Iterable[Tuple[str, str]]:
"""Return a tuple (paths, methods) for every path in spec."""
valid_methods = [verb.value for verb in HTTPVerb]
for path_name, path_item in spec["paths"].items():
for method in valid_methods:
if method in path_item:
yield (path_name, method)
def http_paths_and_methods() -> List[Tuple[str, OpenAPISpec, str, str]]:
"""Return a args for every method in cached OpenAPI spec in test_specs."""
http_paths_and_methods = []
for test_spec in _get_test_specs():
spec_name = test_spec.parent.name
if test_spec.suffix == ".json":
with test_spec.open("r") as f:
spec = json.load(f)
else:
with test_spec.open("r") as f:
spec = yaml.safe_load(f.read())
parsed_spec = OpenAPISpec.from_file(test_spec)
for path, method in _get_paths_and_methods_from_spec_dictionary(spec):
http_paths_and_methods.append(
(
spec_name,
parsed_spec,
path,
method,
)
)
return http_paths_and_methods
@pytest.mark.parametrize(
"spec_name, spec, path, method",
http_paths_and_methods(),
)
def test_parse_api_operations(
spec_name: str, spec: OpenAPISpec, path: str, method: str
) -> None:
"""Test the APIOperation class."""
try:
APIOperation.from_openapi_spec(spec, path, method)
except Exception as e:
raise AssertionError(f"Error processong {spec_name}: {e} ") from e
@pytest.fixture
def raw_spec() -> OpenAPISpec:
"""Return a raw OpenAPI spec."""
return OpenAPISpec(
info=Info(title="Test API", version="1.0.0"),
)
def test_api_request_body_from_request_body_with_ref(raw_spec: OpenAPISpec) -> None:
"""Test instantiating APIRequestBody from RequestBody with a reference."""
raw_spec.components = Components(
schemas={
"Foo": Schema(
type="object",
properties={
"foo": Schema(type="string"),
"bar": Schema(type="number"),
},
required=["foo"],
)
}
)
media_type = MediaType(
schema=Reference(
ref="#/components/schemas/Foo",
)
)
request_body = RequestBody(content={"application/json": media_type})
api_request_body = APIRequestBody.from_request_body(request_body, raw_spec)
assert api_request_body.description is None
assert len(api_request_body.properties) == 2
foo_prop = api_request_body.properties[0]
assert foo_prop.name == "foo"
assert foo_prop.required is True
bar_prop = api_request_body.properties[1]
assert bar_prop.name == "bar"
assert bar_prop.required is False
assert api_request_body.media_type == "application/json"
def test_api_request_body_from_request_body_with_schema(raw_spec: OpenAPISpec) -> None:
"""Test instantiating APIRequestBody from RequestBody with a schema."""
request_body = RequestBody(
content={
"application/json": MediaType(
schema=Schema(type="object", properties={"foo": Schema(type="string")})
)
}
)
api_request_body = APIRequestBody.from_request_body(request_body, raw_spec)
assert api_request_body.properties == [
APIRequestBodyProperty(
name="foo",
required=False,
type="string",
default=None,
description=None,
properties=[],
references_used=[],
)
]
assert api_request_body.media_type == "application/json"
def test_api_request_body_property_from_schema(raw_spec: OpenAPISpec) -> None:
raw_spec.components = Components(
schemas={
"Bar": Schema(
type="number",
)
}
)
schema = Schema(
type="object",
properties={
"foo": Schema(type="string"),
"bar": Reference(ref="#/components/schemas/Bar"),
},
required=["bar"],
)
api_request_body_property = APIRequestBodyProperty.from_schema(
schema, "test", required=True, spec=raw_spec
)
expected_sub_properties = [
APIRequestBodyProperty(
name="foo",
required=False,
type="string",
default=None,
description=None,
properties=[],
references_used=[],
),
APIRequestBodyProperty(
name="bar",
required=True,
type="number",
default=None,
description=None,
properties=[],
references_used=["Bar"],
),
]
assert api_request_body_property.properties[0] == expected_sub_properties[0]
assert api_request_body_property.properties[1] == expected_sub_properties[1]
assert api_request_body_property.type == "object"
assert api_request_body_property.properties[1].references_used == ["Bar"]