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,412 @@
|
|
|
1
|
+
"""Production middleware pipeline for request/response handling."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
9
|
+
from typing import Any, Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from langchain_openapi.utils import sanitize_request_log
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
NextCallable = Callable[[httpx.Request], Awaitable[httpx.Response]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@runtime_checkable
|
|
21
|
+
class Middleware(Protocol):
|
|
22
|
+
"""Protocol for HTTP request/response middleware components."""
|
|
23
|
+
|
|
24
|
+
async def dispatch(
|
|
25
|
+
self,
|
|
26
|
+
request: httpx.Request,
|
|
27
|
+
call_next: NextCallable,
|
|
28
|
+
) -> httpx.Response: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MiddlewarePipeline:
|
|
32
|
+
"""Orchestrates sequential execution of a list of Middleware instances."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, middlewares: Sequence[Middleware] | None = None) -> None:
|
|
35
|
+
self.middlewares = list(middlewares) if middlewares else []
|
|
36
|
+
|
|
37
|
+
async def execute(
|
|
38
|
+
self,
|
|
39
|
+
request: httpx.Request,
|
|
40
|
+
transport_call: NextCallable,
|
|
41
|
+
) -> httpx.Response:
|
|
42
|
+
index = 0
|
|
43
|
+
|
|
44
|
+
async def call_next(req: httpx.Request) -> httpx.Response:
|
|
45
|
+
nonlocal index
|
|
46
|
+
if index < len(self.middlewares):
|
|
47
|
+
mw = self.middlewares[index]
|
|
48
|
+
index += 1
|
|
49
|
+
return await mw.dispatch(req, call_next)
|
|
50
|
+
return await transport_call(req)
|
|
51
|
+
|
|
52
|
+
return await call_next(request)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class RetryMiddleware(Middleware):
|
|
56
|
+
"""Middleware for retrying failed HTTP requests with configurable backoff."""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
retries: int = 3,
|
|
61
|
+
backoff: str = "exponential",
|
|
62
|
+
backoff_factor: float = 0.5,
|
|
63
|
+
retry_status_codes: Sequence[int] = (429, 500, 502, 503, 504),
|
|
64
|
+
) -> None:
|
|
65
|
+
self.retries = retries
|
|
66
|
+
self.backoff = backoff
|
|
67
|
+
self.backoff_factor = backoff_factor
|
|
68
|
+
self.retry_status_codes = set(retry_status_codes)
|
|
69
|
+
|
|
70
|
+
async def dispatch(
|
|
71
|
+
self,
|
|
72
|
+
request: httpx.Request,
|
|
73
|
+
call_next: NextCallable,
|
|
74
|
+
) -> httpx.Response:
|
|
75
|
+
attempt = 0
|
|
76
|
+
last_exception: Exception | None = None
|
|
77
|
+
|
|
78
|
+
while attempt <= self.retries:
|
|
79
|
+
try:
|
|
80
|
+
response = await call_next(request)
|
|
81
|
+
if (
|
|
82
|
+
response.status_code in self.retry_status_codes
|
|
83
|
+
and attempt < self.retries
|
|
84
|
+
):
|
|
85
|
+
attempt += 1
|
|
86
|
+
delay = self._calculate_delay(attempt)
|
|
87
|
+
logger.warning(
|
|
88
|
+
"Retrying %s (attempt %d/%d) after HTTP %d (delay %.2fs)",
|
|
89
|
+
request.url,
|
|
90
|
+
attempt,
|
|
91
|
+
self.retries,
|
|
92
|
+
response.status_code,
|
|
93
|
+
delay,
|
|
94
|
+
)
|
|
95
|
+
await asyncio.sleep(delay)
|
|
96
|
+
continue
|
|
97
|
+
return response
|
|
98
|
+
except (httpx.TransportError, httpx.TimeoutException) as exc:
|
|
99
|
+
last_exception = exc
|
|
100
|
+
if attempt < self.retries:
|
|
101
|
+
attempt += 1
|
|
102
|
+
delay = self._calculate_delay(attempt)
|
|
103
|
+
logger.warning(
|
|
104
|
+
"Retrying %s (attempt %d/%d) after error: %s (delay %.2fs)",
|
|
105
|
+
request.url,
|
|
106
|
+
attempt,
|
|
107
|
+
self.retries,
|
|
108
|
+
exc,
|
|
109
|
+
delay,
|
|
110
|
+
)
|
|
111
|
+
await asyncio.sleep(delay)
|
|
112
|
+
continue
|
|
113
|
+
raise
|
|
114
|
+
|
|
115
|
+
if last_exception:
|
|
116
|
+
raise last_exception
|
|
117
|
+
return response
|
|
118
|
+
|
|
119
|
+
def _calculate_delay(self, attempt: int) -> float:
|
|
120
|
+
if self.backoff == "fixed":
|
|
121
|
+
return float(self.backoff_factor)
|
|
122
|
+
return float(self.backoff_factor * (2 ** (attempt - 1)))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class RateLimitMiddleware(Middleware):
|
|
126
|
+
"""Middleware for throttling HTTP requests using a token bucket algorithm."""
|
|
127
|
+
|
|
128
|
+
def __init__(self, requests_per_second: float = 5.0) -> None:
|
|
129
|
+
if requests_per_second <= 0:
|
|
130
|
+
raise ValueError("requests_per_second must be greater than zero.")
|
|
131
|
+
|
|
132
|
+
self.rate = float(requests_per_second)
|
|
133
|
+
self.capacity = float(requests_per_second)
|
|
134
|
+
self.tokens = float(requests_per_second)
|
|
135
|
+
self.last_update = time.monotonic()
|
|
136
|
+
self._lock = asyncio.Lock()
|
|
137
|
+
|
|
138
|
+
async def dispatch(
|
|
139
|
+
self,
|
|
140
|
+
request: httpx.Request,
|
|
141
|
+
call_next: NextCallable,
|
|
142
|
+
) -> httpx.Response:
|
|
143
|
+
await self._acquire_token()
|
|
144
|
+
return await call_next(request)
|
|
145
|
+
|
|
146
|
+
async def _acquire_token(self) -> None:
|
|
147
|
+
async with self._lock:
|
|
148
|
+
while True:
|
|
149
|
+
now = time.monotonic()
|
|
150
|
+
elapsed = now - self.last_update
|
|
151
|
+
self.last_update = now
|
|
152
|
+
|
|
153
|
+
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
|
|
154
|
+
|
|
155
|
+
if self.tokens >= 1.0:
|
|
156
|
+
self.tokens -= 1.0
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
needed_tokens = 1.0 - self.tokens
|
|
160
|
+
wait_time = needed_tokens / self.rate
|
|
161
|
+
await asyncio.sleep(wait_time)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@runtime_checkable
|
|
165
|
+
class CacheBackend(Protocol):
|
|
166
|
+
"""Protocol interface for cache storage backends."""
|
|
167
|
+
|
|
168
|
+
async def get(self, key: str) -> httpx.Response | None: ...
|
|
169
|
+
|
|
170
|
+
async def set(self, key: str, response: httpx.Response, ttl: float) -> None: ...
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class InMemoryCacheBackend(CacheBackend):
|
|
174
|
+
"""Default in-memory cache backend with TTL expiration."""
|
|
175
|
+
|
|
176
|
+
def __init__(self) -> None:
|
|
177
|
+
self._cache: dict[str, tuple[httpx.Response, float]] = {}
|
|
178
|
+
|
|
179
|
+
async def get(self, key: str) -> httpx.Response | None:
|
|
180
|
+
if key not in self._cache:
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
response, expire_at = self._cache[key]
|
|
184
|
+
if time.monotonic() > expire_at:
|
|
185
|
+
del self._cache[key]
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
return httpx.Response(
|
|
189
|
+
status_code=response.status_code,
|
|
190
|
+
headers=response.headers,
|
|
191
|
+
content=response.content,
|
|
192
|
+
request=response.request,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
async def set(self, key: str, response: httpx.Response, ttl: float) -> None:
|
|
196
|
+
expire_at = time.monotonic() + ttl
|
|
197
|
+
content = (
|
|
198
|
+
await response.aread() if hasattr(response, "aread") else response.content
|
|
199
|
+
)
|
|
200
|
+
cached_resp = httpx.Response(
|
|
201
|
+
status_code=response.status_code,
|
|
202
|
+
headers=response.headers,
|
|
203
|
+
content=content,
|
|
204
|
+
request=response.request,
|
|
205
|
+
)
|
|
206
|
+
self._cache[key] = (cached_resp, expire_at)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class CacheMiddleware(Middleware):
|
|
210
|
+
"""Middleware for caching HTTP GET responses."""
|
|
211
|
+
|
|
212
|
+
def __init__(
|
|
213
|
+
self,
|
|
214
|
+
ttl: float = 300.0,
|
|
215
|
+
backend: CacheBackend | None = None,
|
|
216
|
+
cache_authenticated: bool = False,
|
|
217
|
+
) -> None:
|
|
218
|
+
self.ttl = ttl
|
|
219
|
+
self.backend = backend or InMemoryCacheBackend()
|
|
220
|
+
self.cache_authenticated = cache_authenticated
|
|
221
|
+
|
|
222
|
+
async def dispatch(
|
|
223
|
+
self,
|
|
224
|
+
request: httpx.Request,
|
|
225
|
+
call_next: NextCallable,
|
|
226
|
+
) -> httpx.Response:
|
|
227
|
+
if request.method.upper() != "GET":
|
|
228
|
+
return await call_next(request)
|
|
229
|
+
|
|
230
|
+
if not self.cache_authenticated and self._is_authenticated(request):
|
|
231
|
+
return await call_next(request)
|
|
232
|
+
|
|
233
|
+
key = self._generate_cache_key(request)
|
|
234
|
+
|
|
235
|
+
cached = await self.backend.get(key)
|
|
236
|
+
if cached is not None:
|
|
237
|
+
logger.info("Cache hit for %s", request.url)
|
|
238
|
+
return cached
|
|
239
|
+
|
|
240
|
+
logger.info("Cache miss for %s", request.url)
|
|
241
|
+
response = await call_next(request)
|
|
242
|
+
|
|
243
|
+
if response.status_code == 200:
|
|
244
|
+
await self.backend.set(key, response, self.ttl)
|
|
245
|
+
|
|
246
|
+
return response
|
|
247
|
+
|
|
248
|
+
def _is_authenticated(self, request: httpx.Request) -> bool:
|
|
249
|
+
auth_headers = {"authorization", "cookie", "x-api-key"}
|
|
250
|
+
return any(h.lower() in auth_headers for h in request.headers)
|
|
251
|
+
|
|
252
|
+
def _generate_cache_key(self, request: httpx.Request) -> str:
|
|
253
|
+
key_raw = f"{request.method.upper()}:{request.url}"
|
|
254
|
+
return hashlib.sha256(key_raw.encode("utf-8")).hexdigest()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class PaginationMiddleware(Middleware):
|
|
258
|
+
"""Middleware for automated multi-page result aggregation."""
|
|
259
|
+
|
|
260
|
+
def __init__(
|
|
261
|
+
self,
|
|
262
|
+
max_pages: int = 10,
|
|
263
|
+
max_items: int = 1000,
|
|
264
|
+
) -> None:
|
|
265
|
+
self.max_pages = max_pages
|
|
266
|
+
self.max_items = max_items
|
|
267
|
+
|
|
268
|
+
async def dispatch(
|
|
269
|
+
self,
|
|
270
|
+
request: httpx.Request,
|
|
271
|
+
call_next: NextCallable,
|
|
272
|
+
) -> httpx.Response:
|
|
273
|
+
params = dict(request.url.params)
|
|
274
|
+
is_paginate_flag = (
|
|
275
|
+
request.headers.get("X-LangChain-Paginate", "").lower() == "true"
|
|
276
|
+
or params.pop("__paginate__", "").lower() == "true"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
if not is_paginate_flag:
|
|
280
|
+
return await call_next(request)
|
|
281
|
+
|
|
282
|
+
if "X-LangChain-Paginate" in request.headers:
|
|
283
|
+
del request.headers["X-LangChain-Paginate"]
|
|
284
|
+
request.url = request.url.copy_with(params=params)
|
|
285
|
+
|
|
286
|
+
response = await call_next(request)
|
|
287
|
+
if response.status_code != 200:
|
|
288
|
+
return response
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
initial_data = json.loads(response.text)
|
|
292
|
+
except Exception:
|
|
293
|
+
return response
|
|
294
|
+
|
|
295
|
+
aggregated_items, data_key = self._extract_items(initial_data)
|
|
296
|
+
if aggregated_items is None:
|
|
297
|
+
return response
|
|
298
|
+
|
|
299
|
+
base_payload = initial_data
|
|
300
|
+
current_page = 1
|
|
301
|
+
page_param_name, _current_val = self._detect_pagination_param(params)
|
|
302
|
+
|
|
303
|
+
while current_page < self.max_pages and len(aggregated_items) < self.max_items:
|
|
304
|
+
current_page += 1
|
|
305
|
+
next_params = dict(params)
|
|
306
|
+
|
|
307
|
+
if page_param_name == "page":
|
|
308
|
+
next_params["page"] = str(current_page)
|
|
309
|
+
elif page_param_name == "offset":
|
|
310
|
+
limit = int(params.get("limit", len(aggregated_items)))
|
|
311
|
+
next_params["offset"] = str((current_page - 1) * limit)
|
|
312
|
+
elif page_param_name == "cursor" and isinstance(initial_data, dict):
|
|
313
|
+
cursor_val = initial_data.get("next_cursor") or initial_data.get(
|
|
314
|
+
"cursor"
|
|
315
|
+
)
|
|
316
|
+
if not cursor_val:
|
|
317
|
+
break
|
|
318
|
+
next_params["cursor"] = str(cursor_val)
|
|
319
|
+
else:
|
|
320
|
+
next_params["page"] = str(current_page)
|
|
321
|
+
|
|
322
|
+
next_req = httpx.Request(
|
|
323
|
+
method=request.method,
|
|
324
|
+
url=request.url.copy_with(params=next_params),
|
|
325
|
+
headers=request.headers,
|
|
326
|
+
content=request.content,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
page_resp = await call_next(next_req)
|
|
330
|
+
if page_resp.status_code != 200:
|
|
331
|
+
break
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
page_data = json.loads(page_resp.text)
|
|
335
|
+
except Exception:
|
|
336
|
+
break
|
|
337
|
+
|
|
338
|
+
page_items, _ = self._extract_items(page_data)
|
|
339
|
+
if not page_items:
|
|
340
|
+
break
|
|
341
|
+
|
|
342
|
+
aggregated_items.extend(page_items)
|
|
343
|
+
initial_data = page_data
|
|
344
|
+
|
|
345
|
+
if len(aggregated_items) >= self.max_items:
|
|
346
|
+
aggregated_items = aggregated_items[: self.max_items]
|
|
347
|
+
break
|
|
348
|
+
|
|
349
|
+
final_payload: Any
|
|
350
|
+
if data_key is not None and isinstance(base_payload, dict):
|
|
351
|
+
base_payload[data_key] = aggregated_items
|
|
352
|
+
final_payload = base_payload
|
|
353
|
+
else:
|
|
354
|
+
final_payload = aggregated_items
|
|
355
|
+
|
|
356
|
+
return httpx.Response(
|
|
357
|
+
status_code=200,
|
|
358
|
+
headers=response.headers,
|
|
359
|
+
content=json.dumps(final_payload).encode("utf-8"),
|
|
360
|
+
request=request,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def _extract_items(self, data: Any) -> tuple[list[Any] | None, str | None]:
|
|
364
|
+
if isinstance(data, list):
|
|
365
|
+
return list(data), None
|
|
366
|
+
if isinstance(data, dict):
|
|
367
|
+
for key in ("items", "data", "results", "records"):
|
|
368
|
+
if key in data and isinstance(data[key], list):
|
|
369
|
+
return list(data[key]), key
|
|
370
|
+
return None, None
|
|
371
|
+
|
|
372
|
+
def _detect_pagination_param(self, params: dict[str, str]) -> tuple[str, str]:
|
|
373
|
+
for p in ("page", "offset", "cursor"):
|
|
374
|
+
if p in params:
|
|
375
|
+
return p, params[p]
|
|
376
|
+
return "page", "1"
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class LoggingMiddleware(Middleware):
|
|
380
|
+
"""Middleware for logging HTTP request/response metrics."""
|
|
381
|
+
|
|
382
|
+
def __init__(self, logger_instance: logging.Logger | None = None) -> None:
|
|
383
|
+
self.logger = logger_instance or logger
|
|
384
|
+
|
|
385
|
+
async def dispatch(
|
|
386
|
+
self,
|
|
387
|
+
request: httpx.Request,
|
|
388
|
+
call_next: NextCallable,
|
|
389
|
+
) -> httpx.Response:
|
|
390
|
+
sanitized = sanitize_request_log(request.method, str(request.url))
|
|
391
|
+
start_time = time.monotonic()
|
|
392
|
+
self.logger.info("HTTP Request: %s", sanitized)
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
response = await call_next(request)
|
|
396
|
+
elapsed = time.monotonic() - start_time
|
|
397
|
+
self.logger.info(
|
|
398
|
+
"HTTP Response: %s -> Status %d (%.3fs)",
|
|
399
|
+
sanitized,
|
|
400
|
+
response.status_code,
|
|
401
|
+
elapsed,
|
|
402
|
+
)
|
|
403
|
+
return response
|
|
404
|
+
except Exception as exc:
|
|
405
|
+
elapsed = time.monotonic() - start_time
|
|
406
|
+
self.logger.error(
|
|
407
|
+
"HTTP Exception: %s failed after %.3fs: %s",
|
|
408
|
+
sanitized,
|
|
409
|
+
elapsed,
|
|
410
|
+
exc,
|
|
411
|
+
)
|
|
412
|
+
raise
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Internal normalized models for OpenAPI specification entities.
|
|
2
|
+
|
|
3
|
+
Note on Limitations:
|
|
4
|
+
Current parser models skip polymorphic constructs (oneOf, anyOf, allOf,
|
|
5
|
+
discriminator), callbacks, links, webhooks, and complex example inheritance
|
|
6
|
+
patterns. These features will be evaluated in future milestones.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from langchain_openapi.enums import DataType, HTTPMethod, ParameterLocation
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Schema:
|
|
17
|
+
"""Represents a data type schema definition.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
type: The data type (e.g. 'string', 'integer', 'object', 'array').
|
|
21
|
+
format: Optional format modifier (e.g. 'date-time', 'email', 'uuid').
|
|
22
|
+
properties: Dictionary of property schemas if type is 'object'.
|
|
23
|
+
items: Schema of array elements if type is 'array'.
|
|
24
|
+
required: List of required property names if type is 'object'.
|
|
25
|
+
enum: List of allowed values for enum types.
|
|
26
|
+
default: Default value for the schema.
|
|
27
|
+
nullable: Whether null values are permitted.
|
|
28
|
+
description: Description of the schema element.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
type: DataType | str | list[DataType | str] | None = None
|
|
32
|
+
format: str | None = None
|
|
33
|
+
properties: dict[str, "Schema"] | None = None
|
|
34
|
+
items: "Schema | None" = None
|
|
35
|
+
required: list[str] | None = None
|
|
36
|
+
enum: list[Any] | None = None
|
|
37
|
+
default: Any = None
|
|
38
|
+
nullable: bool = False
|
|
39
|
+
description: str | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class MediaType:
|
|
44
|
+
"""Represents a media type definition (e.g., 'application/json').
|
|
45
|
+
|
|
46
|
+
Attributes:
|
|
47
|
+
content_type: MIME type string (e.g., 'application/json').
|
|
48
|
+
schema: Optional Schema instance defining the payload structure.
|
|
49
|
+
example: Single example value for this media type.
|
|
50
|
+
examples: Dictionary of named example objects.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
content_type: str
|
|
54
|
+
schema: Schema | None = None
|
|
55
|
+
example: Any = None
|
|
56
|
+
examples: dict[str, Any] | None = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class Parameter:
|
|
61
|
+
"""Represents an operation parameter (path, query, header, or cookie).
|
|
62
|
+
|
|
63
|
+
Attributes:
|
|
64
|
+
name: Parameter name.
|
|
65
|
+
location: Parameter location (path, query, header, cookie).
|
|
66
|
+
required: Whether the parameter is required.
|
|
67
|
+
description: Parameter description.
|
|
68
|
+
schema: Optional Schema defining the parameter value type.
|
|
69
|
+
default: Default value for the parameter.
|
|
70
|
+
example: Example value for the parameter.
|
|
71
|
+
style: Parameter serialization style modifier.
|
|
72
|
+
explode: Whether parameter array/object items explode into separate values.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
name: str
|
|
76
|
+
location: ParameterLocation
|
|
77
|
+
required: bool = False
|
|
78
|
+
description: str | None = None
|
|
79
|
+
schema: Schema | None = None
|
|
80
|
+
default: Any = None
|
|
81
|
+
example: Any = None
|
|
82
|
+
style: str | None = None
|
|
83
|
+
explode: bool | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class RequestBody:
|
|
88
|
+
"""Represents an operation request payload definition.
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
required: Whether a request body is mandatory.
|
|
92
|
+
description: Request body description.
|
|
93
|
+
content: Map of MIME content types to MediaType instances.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
required: bool = False
|
|
97
|
+
description: str | None = None
|
|
98
|
+
content: dict[str, MediaType] = field(default_factory=dict)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class Response:
|
|
103
|
+
"""Represents an HTTP response status definition.
|
|
104
|
+
|
|
105
|
+
Attributes:
|
|
106
|
+
status_code: HTTP status code string (e.g., '200', '404', 'default').
|
|
107
|
+
description: Response description.
|
|
108
|
+
content: Map of MIME content types to MediaType instances.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
status_code: str
|
|
112
|
+
description: str
|
|
113
|
+
content: dict[str, MediaType] = field(default_factory=dict)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class Operation:
|
|
118
|
+
"""Represents a single API operation corresponding to an HTTP method on a path.
|
|
119
|
+
|
|
120
|
+
Attributes:
|
|
121
|
+
name: Resolved operation identifier (or generated fallback name).
|
|
122
|
+
summary: Short summary of the operation.
|
|
123
|
+
description: Detailed description of the operation.
|
|
124
|
+
method: HTTP method (GET, POST, etc.).
|
|
125
|
+
path: Path template (e.g., '/users/{id}').
|
|
126
|
+
operation_id: Explicit operationId if declared in spec.
|
|
127
|
+
tags: Tags associated with the operation.
|
|
128
|
+
parameters: List of Parameter objects for path, query, headers, cookies.
|
|
129
|
+
request_body: Optional RequestBody instance for request payloads.
|
|
130
|
+
responses: Map of HTTP status code strings to Response objects.
|
|
131
|
+
deprecated: Whether the operation is deprecated.
|
|
132
|
+
security: List of security requirement maps for this operation.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
name: str
|
|
136
|
+
method: HTTPMethod
|
|
137
|
+
path: str
|
|
138
|
+
summary: str | None = None
|
|
139
|
+
description: str | None = None
|
|
140
|
+
operation_id: str | None = None
|
|
141
|
+
tags: list[str] = field(default_factory=list)
|
|
142
|
+
parameters: list[Parameter] = field(default_factory=list)
|
|
143
|
+
request_body: RequestBody | None = None
|
|
144
|
+
responses: dict[str, Response] = field(default_factory=dict)
|
|
145
|
+
deprecated: bool = False
|
|
146
|
+
security: list[dict[str, list[str]]] = field(default_factory=list)
|