langchain-openapi-tools 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- langchain_openapi/__init__.py +143 -0
- langchain_openapi/enums.py +35 -0
- langchain_openapi/exceptions.py +41 -0
- langchain_openapi/executor.py +266 -0
- langchain_openapi/loader.py +148 -0
- langchain_openapi/middleware.py +412 -0
- langchain_openapi/models.py +146 -0
- langchain_openapi/parser.py +434 -0
- langchain_openapi/providers.py +164 -0
- langchain_openapi/py.typed +1 -0
- langchain_openapi/schema_converter.py +251 -0
- langchain_openapi/toolkit.py +325 -0
- langchain_openapi/utils.py +108 -0
- langchain_openapi_tools-1.0.0.dist-info/METADATA +279 -0
- langchain_openapi_tools-1.0.0.dist-info/RECORD +17 -0
- langchain_openapi_tools-1.0.0.dist-info/WHEEL +4 -0
- langchain_openapi_tools-1.0.0.dist-info/licenses/LICENSE +19 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""LangChain OpenAPI package.
|
|
2
|
+
|
|
3
|
+
Provides utilities for loading, parsing, converting OpenAPI specifications,
|
|
4
|
+
executing HTTP requests asynchronously, configuring request providers,
|
|
5
|
+
applying production middleware, and exporting dynamic LangChain tools.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from langchain_openapi.enums import DataType, HTTPMethod, ParameterLocation
|
|
9
|
+
from langchain_openapi.exceptions import (
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
ExecutionTimeoutError,
|
|
12
|
+
HTTPExecutionError,
|
|
13
|
+
InvalidSpecError,
|
|
14
|
+
OpenAPIError,
|
|
15
|
+
ProviderError,
|
|
16
|
+
RequestValidationError,
|
|
17
|
+
ResponseParsingError,
|
|
18
|
+
SpecLoadError,
|
|
19
|
+
UnsupportedVersionError,
|
|
20
|
+
)
|
|
21
|
+
from langchain_openapi.executor import (
|
|
22
|
+
AsyncHTTPExecutor,
|
|
23
|
+
BuiltRequest,
|
|
24
|
+
RequestBuilder,
|
|
25
|
+
ResponseData,
|
|
26
|
+
ResponseParser,
|
|
27
|
+
create_async_client,
|
|
28
|
+
)
|
|
29
|
+
from langchain_openapi.loader import OpenAPILoader
|
|
30
|
+
from langchain_openapi.middleware import (
|
|
31
|
+
CacheBackend,
|
|
32
|
+
CacheMiddleware,
|
|
33
|
+
InMemoryCacheBackend,
|
|
34
|
+
LoggingMiddleware,
|
|
35
|
+
Middleware,
|
|
36
|
+
MiddlewarePipeline,
|
|
37
|
+
NextCallable,
|
|
38
|
+
PaginationMiddleware,
|
|
39
|
+
RateLimitMiddleware,
|
|
40
|
+
RetryMiddleware,
|
|
41
|
+
)
|
|
42
|
+
from langchain_openapi.models import (
|
|
43
|
+
MediaType,
|
|
44
|
+
Operation,
|
|
45
|
+
Parameter,
|
|
46
|
+
RequestBody,
|
|
47
|
+
Response,
|
|
48
|
+
Schema,
|
|
49
|
+
)
|
|
50
|
+
from langchain_openapi.parser import (
|
|
51
|
+
OpenAPIParser,
|
|
52
|
+
OpenAPISpec,
|
|
53
|
+
ReferenceResolver,
|
|
54
|
+
generate_fallback_operation_name,
|
|
55
|
+
)
|
|
56
|
+
from langchain_openapi.providers import (
|
|
57
|
+
APIKeyHeaderProvider,
|
|
58
|
+
APIKeyQueryProvider,
|
|
59
|
+
BasicAuthProvider,
|
|
60
|
+
BearerAuthProvider,
|
|
61
|
+
CompositeProvider,
|
|
62
|
+
CookiesProvider,
|
|
63
|
+
NoAuthProvider,
|
|
64
|
+
RequestProvider,
|
|
65
|
+
StaticHeadersProvider,
|
|
66
|
+
)
|
|
67
|
+
from langchain_openapi.schema_converter import (
|
|
68
|
+
PydanticFactory,
|
|
69
|
+
SchemaConverter,
|
|
70
|
+
map_schema_type_to_python,
|
|
71
|
+
)
|
|
72
|
+
from langchain_openapi.toolkit import (
|
|
73
|
+
LangChainToolFactory,
|
|
74
|
+
OpenAPIToolCallbackHandler,
|
|
75
|
+
OpenAPIToolkit,
|
|
76
|
+
build_tool_description,
|
|
77
|
+
format_tool_name,
|
|
78
|
+
)
|
|
79
|
+
from langchain_openapi.utils import sanitize_request_log
|
|
80
|
+
|
|
81
|
+
__version__ = "1.0.0"
|
|
82
|
+
|
|
83
|
+
__all__ = [
|
|
84
|
+
"APIKeyHeaderProvider",
|
|
85
|
+
"APIKeyQueryProvider",
|
|
86
|
+
"AsyncHTTPExecutor",
|
|
87
|
+
"AuthenticationError",
|
|
88
|
+
"BasicAuthProvider",
|
|
89
|
+
"BearerAuthProvider",
|
|
90
|
+
"BuiltRequest",
|
|
91
|
+
"CacheBackend",
|
|
92
|
+
"CacheMiddleware",
|
|
93
|
+
"CompositeProvider",
|
|
94
|
+
"CookiesProvider",
|
|
95
|
+
"DataType",
|
|
96
|
+
"ExecutionTimeoutError",
|
|
97
|
+
"HTTPExecutionError",
|
|
98
|
+
"HTTPMethod",
|
|
99
|
+
"InMemoryCacheBackend",
|
|
100
|
+
"InvalidSpecError",
|
|
101
|
+
"LangChainToolFactory",
|
|
102
|
+
"LoggingMiddleware",
|
|
103
|
+
"MediaType",
|
|
104
|
+
"Middleware",
|
|
105
|
+
"MiddlewarePipeline",
|
|
106
|
+
"NextCallable",
|
|
107
|
+
"NoAuthProvider",
|
|
108
|
+
"OpenAPIError",
|
|
109
|
+
"OpenAPILoader",
|
|
110
|
+
"OpenAPIParser",
|
|
111
|
+
"OpenAPISpec",
|
|
112
|
+
"OpenAPIToolCallbackHandler",
|
|
113
|
+
"OpenAPIToolkit",
|
|
114
|
+
"Operation",
|
|
115
|
+
"PaginationMiddleware",
|
|
116
|
+
"Parameter",
|
|
117
|
+
"ParameterLocation",
|
|
118
|
+
"ProviderError",
|
|
119
|
+
"PydanticFactory",
|
|
120
|
+
"RateLimitMiddleware",
|
|
121
|
+
"ReferenceResolver",
|
|
122
|
+
"RequestBody",
|
|
123
|
+
"RequestBuilder",
|
|
124
|
+
"RequestProvider",
|
|
125
|
+
"RequestValidationError",
|
|
126
|
+
"Response",
|
|
127
|
+
"ResponseData",
|
|
128
|
+
"ResponseParser",
|
|
129
|
+
"ResponseParsingError",
|
|
130
|
+
"RetryMiddleware",
|
|
131
|
+
"Schema",
|
|
132
|
+
"SchemaConverter",
|
|
133
|
+
"SpecLoadError",
|
|
134
|
+
"StaticHeadersProvider",
|
|
135
|
+
"UnsupportedVersionError",
|
|
136
|
+
"__version__",
|
|
137
|
+
"build_tool_description",
|
|
138
|
+
"create_async_client",
|
|
139
|
+
"format_tool_name",
|
|
140
|
+
"generate_fallback_operation_name",
|
|
141
|
+
"map_schema_type_to_python",
|
|
142
|
+
"sanitize_request_log",
|
|
143
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Enumerations for OpenAPI specification entities."""
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HTTPMethod(StrEnum):
|
|
7
|
+
"""Supported HTTP request methods."""
|
|
8
|
+
|
|
9
|
+
GET = "GET"
|
|
10
|
+
POST = "POST"
|
|
11
|
+
PUT = "PUT"
|
|
12
|
+
PATCH = "PATCH"
|
|
13
|
+
DELETE = "DELETE"
|
|
14
|
+
OPTIONS = "OPTIONS"
|
|
15
|
+
HEAD = "HEAD"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ParameterLocation(StrEnum):
|
|
19
|
+
"""Locations where parameters can be supplied in an HTTP request."""
|
|
20
|
+
|
|
21
|
+
PATH = "path"
|
|
22
|
+
QUERY = "query"
|
|
23
|
+
HEADER = "header"
|
|
24
|
+
COOKIE = "cookie"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DataType(StrEnum):
|
|
28
|
+
"""Core data types supported in OpenAPI schemas."""
|
|
29
|
+
|
|
30
|
+
STRING = "string"
|
|
31
|
+
INTEGER = "integer"
|
|
32
|
+
NUMBER = "number"
|
|
33
|
+
BOOLEAN = "boolean"
|
|
34
|
+
ARRAY = "array"
|
|
35
|
+
OBJECT = "object"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Custom exceptions for langchain-openapi."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class OpenAPIError(Exception):
|
|
5
|
+
"""Base exception for all langchain-openapi errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SpecLoadError(OpenAPIError):
|
|
9
|
+
"""Raised when an OpenAPI specification cannot be loaded from a source."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InvalidSpecError(OpenAPIError):
|
|
13
|
+
"""Raised when a loaded specification fails validation requirements."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UnsupportedVersionError(OpenAPIError):
|
|
17
|
+
"""Raised when the specification uses an unsupported OpenAPI or Swagger version."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HTTPExecutionError(OpenAPIError):
|
|
21
|
+
"""Base exception for HTTP request execution errors."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuthenticationError(HTTPExecutionError):
|
|
25
|
+
"""Raised when authentication credentials fail or cannot be applied."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ProviderError(HTTPExecutionError):
|
|
29
|
+
"""Raised when a request provider fails during request mutation."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RequestValidationError(HTTPExecutionError):
|
|
33
|
+
"""Raised when request arguments are invalid or missing required parameters."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ResponseParsingError(HTTPExecutionError):
|
|
37
|
+
"""Raised when an HTTP response payload cannot be parsed."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ExecutionTimeoutError(HTTPExecutionError):
|
|
41
|
+
"""Raised when an HTTP request times out."""
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Asynchronous HTTP execution engine for OpenAPI Operations."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from langchain_openapi.enums import ParameterLocation
|
|
12
|
+
from langchain_openapi.exceptions import (
|
|
13
|
+
ExecutionTimeoutError,
|
|
14
|
+
HTTPExecutionError,
|
|
15
|
+
RequestValidationError,
|
|
16
|
+
ResponseParsingError,
|
|
17
|
+
)
|
|
18
|
+
from langchain_openapi.middleware import Middleware, MiddlewarePipeline
|
|
19
|
+
from langchain_openapi.models import Operation
|
|
20
|
+
from langchain_openapi.providers import NoAuthProvider, RequestProvider
|
|
21
|
+
from langchain_openapi.utils import sanitize_request_log
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class BuiltRequest:
|
|
28
|
+
"""Container for constructed request metadata ready for execution."""
|
|
29
|
+
|
|
30
|
+
method: str
|
|
31
|
+
url: str
|
|
32
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
33
|
+
params: dict[str, Any] = field(default_factory=dict)
|
|
34
|
+
json_body: Any = None
|
|
35
|
+
cookies: dict[str, str] = field(default_factory=dict)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class ResponseData:
|
|
40
|
+
"""Normalized response payload."""
|
|
41
|
+
|
|
42
|
+
status_code: int
|
|
43
|
+
headers: dict[str, str]
|
|
44
|
+
body: Any
|
|
45
|
+
raw: httpx.Response
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def create_async_client(
|
|
49
|
+
timeout: float = 30.0,
|
|
50
|
+
headers: dict[str, str] | None = None,
|
|
51
|
+
cookies: dict[str, str] | None = None,
|
|
52
|
+
**kwargs: Any,
|
|
53
|
+
) -> httpx.AsyncClient:
|
|
54
|
+
"""Create a configured httpx.AsyncClient instance."""
|
|
55
|
+
return httpx.AsyncClient(
|
|
56
|
+
timeout=timeout,
|
|
57
|
+
headers=headers,
|
|
58
|
+
cookies=cookies,
|
|
59
|
+
**kwargs,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RequestBuilder:
|
|
64
|
+
"""Translates Operation models into BuiltRequest instances."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, base_url: str | None = None) -> None:
|
|
67
|
+
self.base_url = base_url.rstrip("/") if base_url else ""
|
|
68
|
+
|
|
69
|
+
def build(
|
|
70
|
+
self,
|
|
71
|
+
operation: Operation,
|
|
72
|
+
arguments: dict[str, Any],
|
|
73
|
+
base_url_override: str | None = None,
|
|
74
|
+
) -> BuiltRequest:
|
|
75
|
+
method = operation.method.value.upper()
|
|
76
|
+
base_url = base_url_override.rstrip("/") if base_url_override else self.base_url
|
|
77
|
+
|
|
78
|
+
path_template = operation.path
|
|
79
|
+
query_params: dict[str, Any] = {}
|
|
80
|
+
headers: dict[str, str] = {}
|
|
81
|
+
cookies: dict[str, str] = {}
|
|
82
|
+
consumed_args: set[str] = set()
|
|
83
|
+
|
|
84
|
+
if arguments.get("paginate") or arguments.get("__paginate__"):
|
|
85
|
+
headers["X-LangChain-Paginate"] = "true"
|
|
86
|
+
consumed_args.add("paginate")
|
|
87
|
+
consumed_args.add("__paginate__")
|
|
88
|
+
|
|
89
|
+
for param in operation.parameters:
|
|
90
|
+
name = param.name
|
|
91
|
+
val = arguments.get(name)
|
|
92
|
+
|
|
93
|
+
if param.location == ParameterLocation.PATH:
|
|
94
|
+
if val is None:
|
|
95
|
+
raise RequestValidationError(
|
|
96
|
+
f"Missing required path parameter '{name}' "
|
|
97
|
+
f"for operation '{operation.name}'."
|
|
98
|
+
)
|
|
99
|
+
path_template = path_template.replace(f"{{{name}}}", str(val))
|
|
100
|
+
consumed_args.add(name)
|
|
101
|
+
|
|
102
|
+
elif param.location == ParameterLocation.QUERY:
|
|
103
|
+
if val is not None:
|
|
104
|
+
query_params[name] = val
|
|
105
|
+
consumed_args.add(name)
|
|
106
|
+
|
|
107
|
+
elif param.location == ParameterLocation.HEADER:
|
|
108
|
+
if val is not None:
|
|
109
|
+
headers[name] = str(val)
|
|
110
|
+
consumed_args.add(name)
|
|
111
|
+
|
|
112
|
+
elif param.location == ParameterLocation.COOKIE:
|
|
113
|
+
if val is not None:
|
|
114
|
+
cookies[name] = str(val)
|
|
115
|
+
consumed_args.add(name)
|
|
116
|
+
|
|
117
|
+
full_url = (
|
|
118
|
+
f"{base_url}/{path_template.lstrip('/')}" if base_url else path_template
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
json_body: Any = None
|
|
122
|
+
if operation.request_body:
|
|
123
|
+
if "body" in arguments:
|
|
124
|
+
json_body = arguments["body"]
|
|
125
|
+
else:
|
|
126
|
+
unconsumed = {
|
|
127
|
+
k: v for k, v in arguments.items() if k not in consumed_args
|
|
128
|
+
}
|
|
129
|
+
if unconsumed:
|
|
130
|
+
json_body = unconsumed
|
|
131
|
+
|
|
132
|
+
if json_body is not None and "Content-Type" not in headers:
|
|
133
|
+
headers["Content-Type"] = "application/json"
|
|
134
|
+
|
|
135
|
+
return BuiltRequest(
|
|
136
|
+
method=method,
|
|
137
|
+
url=full_url,
|
|
138
|
+
headers=headers,
|
|
139
|
+
params=query_params,
|
|
140
|
+
json_body=json_body,
|
|
141
|
+
cookies=cookies,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class ResponseParser:
|
|
146
|
+
"""Attempts JSON decoding and standardizes HTTP responses."""
|
|
147
|
+
|
|
148
|
+
def parse(self, response: httpx.Response) -> ResponseData:
|
|
149
|
+
try:
|
|
150
|
+
status_code = response.status_code
|
|
151
|
+
headers = dict(response.headers)
|
|
152
|
+
|
|
153
|
+
if response.content:
|
|
154
|
+
try:
|
|
155
|
+
body: Any = response.json()
|
|
156
|
+
except Exception:
|
|
157
|
+
body = response.text
|
|
158
|
+
else:
|
|
159
|
+
body = None
|
|
160
|
+
|
|
161
|
+
return ResponseData(
|
|
162
|
+
status_code=status_code,
|
|
163
|
+
headers=headers,
|
|
164
|
+
body=body,
|
|
165
|
+
raw=response,
|
|
166
|
+
)
|
|
167
|
+
except Exception as exc:
|
|
168
|
+
raise ResponseParsingError(
|
|
169
|
+
f"Failed to parse HTTP response from '{response.url}': {exc}"
|
|
170
|
+
) from exc
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class AsyncHTTPExecutor:
|
|
174
|
+
"""Generic asynchronous HTTP executor for executing OpenAPI operations."""
|
|
175
|
+
|
|
176
|
+
def __init__(
|
|
177
|
+
self,
|
|
178
|
+
base_url: str | None = None,
|
|
179
|
+
client: httpx.AsyncClient | None = None,
|
|
180
|
+
provider: RequestProvider | None = None,
|
|
181
|
+
middleware: Sequence[Middleware] | None = None,
|
|
182
|
+
timeout: float = 30.0,
|
|
183
|
+
) -> None:
|
|
184
|
+
self.base_url = base_url
|
|
185
|
+
self._client = client
|
|
186
|
+
self.provider = provider if provider is not None else NoAuthProvider()
|
|
187
|
+
self.pipeline = MiddlewarePipeline(middleware)
|
|
188
|
+
self.timeout = timeout
|
|
189
|
+
self._builder = RequestBuilder(base_url=base_url)
|
|
190
|
+
self._parser = ResponseParser()
|
|
191
|
+
|
|
192
|
+
async def execute(
|
|
193
|
+
self,
|
|
194
|
+
operation: Operation,
|
|
195
|
+
arguments: dict[str, Any],
|
|
196
|
+
base_url_override: str | None = None,
|
|
197
|
+
) -> ResponseData:
|
|
198
|
+
built_req = self._builder.build(
|
|
199
|
+
operation=operation,
|
|
200
|
+
arguments=arguments,
|
|
201
|
+
base_url_override=base_url_override,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
start_time = time.monotonic()
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
if self._client is not None:
|
|
208
|
+
response = await self._send_request(self._client, built_req)
|
|
209
|
+
else:
|
|
210
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
211
|
+
response = await self._send_request(client, built_req)
|
|
212
|
+
except httpx.TimeoutException as exc:
|
|
213
|
+
logger.error(
|
|
214
|
+
"Request timed out for %s after %.1fs",
|
|
215
|
+
sanitize_request_log(built_req.method, built_req.url),
|
|
216
|
+
self.timeout,
|
|
217
|
+
)
|
|
218
|
+
raise ExecutionTimeoutError(
|
|
219
|
+
f"HTTP request to '{built_req.url}' timed out after {self.timeout}s."
|
|
220
|
+
) from exc
|
|
221
|
+
except (RequestValidationError, ResponseParsingError):
|
|
222
|
+
raise
|
|
223
|
+
except httpx.HTTPError as exc:
|
|
224
|
+
logger.error(
|
|
225
|
+
"HTTP execution failed for %s: %s",
|
|
226
|
+
sanitize_request_log(built_req.method, built_req.url),
|
|
227
|
+
exc,
|
|
228
|
+
)
|
|
229
|
+
raise HTTPExecutionError(
|
|
230
|
+
f"HTTP request execution failed for '{built_req.url}': {exc}"
|
|
231
|
+
) from exc
|
|
232
|
+
|
|
233
|
+
elapsed = time.monotonic() - start_time
|
|
234
|
+
logger.info(
|
|
235
|
+
"Executed HTTP %s -> Status %d (%.3fs)",
|
|
236
|
+
sanitize_request_log(built_req.method, built_req.url),
|
|
237
|
+
response.status_code,
|
|
238
|
+
elapsed,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return self._parser.parse(response)
|
|
242
|
+
|
|
243
|
+
async def _send_request(
|
|
244
|
+
self, client: httpx.AsyncClient, req_data: BuiltRequest
|
|
245
|
+
) -> httpx.Response:
|
|
246
|
+
kwargs: dict[str, Any] = {
|
|
247
|
+
"method": req_data.method,
|
|
248
|
+
"url": req_data.url,
|
|
249
|
+
"headers": req_data.headers,
|
|
250
|
+
"params": req_data.params,
|
|
251
|
+
"json": req_data.json_body,
|
|
252
|
+
"timeout": self.timeout,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
request = client.build_request(**kwargs)
|
|
256
|
+
|
|
257
|
+
if req_data.cookies:
|
|
258
|
+
cookie_hdr = "; ".join(f"{k}={v}" for k, v in req_data.cookies.items())
|
|
259
|
+
request.headers["Cookie"] = cookie_hdr
|
|
260
|
+
|
|
261
|
+
request = await self.provider.apply(request)
|
|
262
|
+
|
|
263
|
+
async def transport_call(req: httpx.Request) -> httpx.Response:
|
|
264
|
+
return await client.send(req)
|
|
265
|
+
|
|
266
|
+
return await self.pipeline.execute(request, transport_call)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""OpenAPI Specification Loader."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from langchain_openapi.exceptions import SpecLoadError
|
|
11
|
+
from langchain_openapi.parser import OpenAPISpec
|
|
12
|
+
from langchain_openapi.utils import parse_json_or_yaml, validate_raw_spec
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OpenAPILoader:
|
|
18
|
+
"""Loader for obtaining normalized OpenAPI specifications from various sources.
|
|
19
|
+
|
|
20
|
+
Supported sources include local JSON/YAML files, remote URLs, and in-memory dicts.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
load_fn: Callable[[], dict[str, Any]],
|
|
26
|
+
source_info: str,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Initialize loader with a fetch callable and a source description string.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
load_fn: A zero-argument callable that returns a raw spec dictionary.
|
|
32
|
+
source_info: Human-readable description of the source for logging.
|
|
33
|
+
"""
|
|
34
|
+
self._load_fn = load_fn
|
|
35
|
+
self._source_info = source_info
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_url(
|
|
39
|
+
cls,
|
|
40
|
+
url: str,
|
|
41
|
+
timeout: float = 30.0,
|
|
42
|
+
headers: dict[str, str] | None = None,
|
|
43
|
+
) -> "OpenAPILoader":
|
|
44
|
+
"""Create an OpenAPILoader for a remote URL source.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
url: HTTP or HTTPS URL of the OpenAPI specification.
|
|
48
|
+
timeout: Request timeout in seconds (default: 30.0).
|
|
49
|
+
headers: Optional dictionary of HTTP headers for the request.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
An OpenAPILoader instance configured for URL fetching.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def _fetch_from_url() -> dict[str, Any]:
|
|
56
|
+
logger.info("Loading OpenAPI specification from URL: %s", url)
|
|
57
|
+
req_headers = {"User-Agent": "langchain-openapi"}
|
|
58
|
+
if headers:
|
|
59
|
+
req_headers.update(headers)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
with httpx.Client(timeout=timeout, follow_redirects=True) as client:
|
|
63
|
+
response = client.get(url, headers=req_headers)
|
|
64
|
+
response.raise_for_status()
|
|
65
|
+
content = response.text
|
|
66
|
+
except httpx.HTTPError as exc:
|
|
67
|
+
logger.error("Failed to fetch OpenAPI spec from URL '%s': %s", url, exc)
|
|
68
|
+
raise SpecLoadError(
|
|
69
|
+
f"Failed to fetch specification from URL '{url}': {exc}"
|
|
70
|
+
) from exc
|
|
71
|
+
|
|
72
|
+
return parse_json_or_yaml(content)
|
|
73
|
+
|
|
74
|
+
return cls(load_fn=_fetch_from_url, source_info=f"URL '{url}'")
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_file(cls, file_path: str | Path) -> "OpenAPILoader":
|
|
78
|
+
"""Create an OpenAPILoader for a local JSON or YAML file source.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
file_path: Path to the local OpenAPI specification file.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
An OpenAPILoader instance configured for file loading.
|
|
85
|
+
"""
|
|
86
|
+
path = Path(file_path)
|
|
87
|
+
|
|
88
|
+
def _read_from_file() -> dict[str, Any]:
|
|
89
|
+
logger.info("Loading OpenAPI specification from file: %s", path)
|
|
90
|
+
if not path.exists():
|
|
91
|
+
logger.error("Specification file does not exist: %s", path)
|
|
92
|
+
raise SpecLoadError(f"Specification file not found: '{path}'")
|
|
93
|
+
|
|
94
|
+
if not path.is_file():
|
|
95
|
+
logger.error("Path is not a regular file: %s", path)
|
|
96
|
+
raise SpecLoadError(f"Path is not a regular file: '{path}'")
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
content = path.read_text(encoding="utf-8")
|
|
100
|
+
except OSError as exc:
|
|
101
|
+
logger.error("Error reading specification file '%s': %s", path, exc)
|
|
102
|
+
raise SpecLoadError(
|
|
103
|
+
f"Error reading specification file '{path}': {exc}"
|
|
104
|
+
) from exc
|
|
105
|
+
|
|
106
|
+
return parse_json_or_yaml(content)
|
|
107
|
+
|
|
108
|
+
return cls(load_fn=_read_from_file, source_info=f"file '{path}'")
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def from_dict(cls, spec_dict: dict[str, Any]) -> "OpenAPILoader":
|
|
112
|
+
"""Create an OpenAPILoader from an in-memory dictionary.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
spec_dict: Raw Python dictionary containing the OpenAPI specification.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
An OpenAPILoader instance configured for dictionary loading.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def _get_from_dict() -> dict[str, Any]:
|
|
122
|
+
logger.info("Loading OpenAPI specification from in-memory dictionary")
|
|
123
|
+
return spec_dict
|
|
124
|
+
|
|
125
|
+
return cls(load_fn=_get_from_dict, source_info="in-memory dictionary")
|
|
126
|
+
|
|
127
|
+
def load(self) -> OpenAPISpec:
|
|
128
|
+
"""Load, validate, and normalize the OpenAPI specification.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
An OpenAPISpec instance containing the normalized specification data.
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
SpecLoadError: If loading from source or parsing JSON/YAML fails.
|
|
135
|
+
InvalidSpecError: If required fields ('openapi', 'paths') are missing.
|
|
136
|
+
UnsupportedVersionError: If the OpenAPI version is unsupported.
|
|
137
|
+
"""
|
|
138
|
+
raw_data = self._load_fn()
|
|
139
|
+
validate_raw_spec(raw_data)
|
|
140
|
+
spec = OpenAPISpec.from_dict(raw_data)
|
|
141
|
+
|
|
142
|
+
logger.info(
|
|
143
|
+
"Successfully loaded OpenAPI spec '%s' (OpenAPI version %s) from %s",
|
|
144
|
+
spec.title,
|
|
145
|
+
spec.version,
|
|
146
|
+
self._source_info,
|
|
147
|
+
)
|
|
148
|
+
return spec
|