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.
@@ -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