hammad-python 0.0.29__py3-none-any.whl → 0.0.31__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.
- ham/__init__.py +10 -0
- {hammad_python-0.0.29.dist-info → hammad_python-0.0.31.dist-info}/METADATA +6 -32
- hammad_python-0.0.31.dist-info/RECORD +6 -0
- hammad/__init__.py +0 -84
- hammad/_internal.py +0 -256
- hammad/_main.py +0 -226
- hammad/cache/__init__.py +0 -40
- hammad/cache/base_cache.py +0 -181
- hammad/cache/cache.py +0 -169
- hammad/cache/decorators.py +0 -261
- hammad/cache/file_cache.py +0 -80
- hammad/cache/ttl_cache.py +0 -74
- hammad/cli/__init__.py +0 -33
- hammad/cli/animations.py +0 -573
- hammad/cli/plugins.py +0 -867
- hammad/cli/styles/__init__.py +0 -55
- hammad/cli/styles/settings.py +0 -139
- hammad/cli/styles/types.py +0 -358
- hammad/cli/styles/utils.py +0 -634
- hammad/data/__init__.py +0 -90
- hammad/data/collections/__init__.py +0 -49
- hammad/data/collections/collection.py +0 -326
- hammad/data/collections/indexes/__init__.py +0 -37
- hammad/data/collections/indexes/qdrant/__init__.py +0 -1
- hammad/data/collections/indexes/qdrant/index.py +0 -723
- hammad/data/collections/indexes/qdrant/settings.py +0 -94
- hammad/data/collections/indexes/qdrant/utils.py +0 -210
- hammad/data/collections/indexes/tantivy/__init__.py +0 -1
- hammad/data/collections/indexes/tantivy/index.py +0 -426
- hammad/data/collections/indexes/tantivy/settings.py +0 -40
- hammad/data/collections/indexes/tantivy/utils.py +0 -176
- hammad/data/configurations/__init__.py +0 -35
- hammad/data/configurations/configuration.py +0 -564
- hammad/data/models/__init__.py +0 -50
- hammad/data/models/extensions/__init__.py +0 -4
- hammad/data/models/extensions/pydantic/__init__.py +0 -42
- hammad/data/models/extensions/pydantic/converters.py +0 -759
- hammad/data/models/fields.py +0 -546
- hammad/data/models/model.py +0 -1078
- hammad/data/models/utils.py +0 -280
- hammad/data/sql/__init__.py +0 -24
- hammad/data/sql/database.py +0 -576
- hammad/data/sql/types.py +0 -127
- hammad/data/types/__init__.py +0 -75
- hammad/data/types/file.py +0 -431
- hammad/data/types/multimodal/__init__.py +0 -36
- hammad/data/types/multimodal/audio.py +0 -200
- hammad/data/types/multimodal/image.py +0 -182
- hammad/data/types/text.py +0 -1308
- hammad/formatting/__init__.py +0 -33
- hammad/formatting/json/__init__.py +0 -27
- hammad/formatting/json/converters.py +0 -158
- hammad/formatting/text/__init__.py +0 -63
- hammad/formatting/text/converters.py +0 -723
- hammad/formatting/text/markdown.py +0 -131
- hammad/formatting/yaml/__init__.py +0 -26
- hammad/formatting/yaml/converters.py +0 -5
- hammad/genai/__init__.py +0 -217
- hammad/genai/a2a/__init__.py +0 -32
- hammad/genai/a2a/workers.py +0 -552
- hammad/genai/agents/__init__.py +0 -59
- hammad/genai/agents/agent.py +0 -1973
- hammad/genai/agents/run.py +0 -1024
- hammad/genai/agents/types/__init__.py +0 -42
- hammad/genai/agents/types/agent_context.py +0 -13
- hammad/genai/agents/types/agent_event.py +0 -128
- hammad/genai/agents/types/agent_hooks.py +0 -220
- hammad/genai/agents/types/agent_messages.py +0 -31
- hammad/genai/agents/types/agent_response.py +0 -125
- hammad/genai/agents/types/agent_stream.py +0 -327
- hammad/genai/graphs/__init__.py +0 -125
- hammad/genai/graphs/_utils.py +0 -190
- hammad/genai/graphs/base.py +0 -1828
- hammad/genai/graphs/plugins.py +0 -316
- hammad/genai/graphs/types.py +0 -638
- hammad/genai/models/__init__.py +0 -1
- hammad/genai/models/embeddings/__init__.py +0 -43
- hammad/genai/models/embeddings/model.py +0 -226
- hammad/genai/models/embeddings/run.py +0 -163
- hammad/genai/models/embeddings/types/__init__.py +0 -37
- hammad/genai/models/embeddings/types/embedding_model_name.py +0 -75
- hammad/genai/models/embeddings/types/embedding_model_response.py +0 -76
- hammad/genai/models/embeddings/types/embedding_model_run_params.py +0 -66
- hammad/genai/models/embeddings/types/embedding_model_settings.py +0 -47
- hammad/genai/models/language/__init__.py +0 -57
- hammad/genai/models/language/model.py +0 -1098
- hammad/genai/models/language/run.py +0 -878
- hammad/genai/models/language/types/__init__.py +0 -40
- hammad/genai/models/language/types/language_model_instructor_mode.py +0 -47
- hammad/genai/models/language/types/language_model_messages.py +0 -28
- hammad/genai/models/language/types/language_model_name.py +0 -239
- hammad/genai/models/language/types/language_model_request.py +0 -127
- hammad/genai/models/language/types/language_model_response.py +0 -217
- hammad/genai/models/language/types/language_model_response_chunk.py +0 -56
- hammad/genai/models/language/types/language_model_settings.py +0 -89
- hammad/genai/models/language/types/language_model_stream.py +0 -600
- hammad/genai/models/language/utils/__init__.py +0 -28
- hammad/genai/models/language/utils/requests.py +0 -421
- hammad/genai/models/language/utils/structured_outputs.py +0 -135
- hammad/genai/models/model_provider.py +0 -4
- hammad/genai/models/multimodal.py +0 -47
- hammad/genai/models/reranking.py +0 -26
- hammad/genai/types/__init__.py +0 -1
- hammad/genai/types/base.py +0 -215
- hammad/genai/types/history.py +0 -290
- hammad/genai/types/tools.py +0 -507
- hammad/logging/__init__.py +0 -35
- hammad/logging/decorators.py +0 -834
- hammad/logging/logger.py +0 -1018
- hammad/mcp/__init__.py +0 -53
- hammad/mcp/client/__init__.py +0 -35
- hammad/mcp/client/client.py +0 -624
- hammad/mcp/client/client_service.py +0 -400
- hammad/mcp/client/settings.py +0 -178
- hammad/mcp/servers/__init__.py +0 -26
- hammad/mcp/servers/launcher.py +0 -1161
- hammad/runtime/__init__.py +0 -32
- hammad/runtime/decorators.py +0 -142
- hammad/runtime/run.py +0 -299
- hammad/service/__init__.py +0 -49
- hammad/service/create.py +0 -527
- hammad/service/decorators.py +0 -283
- hammad/types.py +0 -288
- hammad/typing/__init__.py +0 -435
- hammad/web/__init__.py +0 -43
- hammad/web/http/__init__.py +0 -1
- hammad/web/http/client.py +0 -944
- hammad/web/models.py +0 -275
- hammad/web/openapi/__init__.py +0 -1
- hammad/web/openapi/client.py +0 -740
- hammad/web/search/__init__.py +0 -1
- hammad/web/search/client.py +0 -1023
- hammad/web/utils.py +0 -472
- hammad_python-0.0.29.dist-info/RECORD +0 -135
- {hammad → ham}/py.typed +0 -0
- {hammad_python-0.0.29.dist-info → hammad_python-0.0.31.dist-info}/WHEEL +0 -0
- {hammad_python-0.0.29.dist-info → hammad_python-0.0.31.dist-info}/licenses/LICENSE +0 -0
hammad/web/openapi/client.py
DELETED
@@ -1,740 +0,0 @@
|
|
1
|
-
"""hammad.web.openapi.client"""
|
2
|
-
|
3
|
-
from __future__ import annotations
|
4
|
-
|
5
|
-
import asyncio
|
6
|
-
import json
|
7
|
-
import re
|
8
|
-
from typing import Any, Dict, List, Literal, Optional, Union, overload
|
9
|
-
|
10
|
-
from msgspec import yaml
|
11
|
-
from pydantic import BaseModel, Field, field_validator
|
12
|
-
|
13
|
-
from ..http.client import AsyncHttpClient, HttpRequest, HttpResponse, HttpError
|
14
|
-
|
15
|
-
__all__ = (
|
16
|
-
"OpenAPIError",
|
17
|
-
"ParameterInfo",
|
18
|
-
"RequestBodyInfo",
|
19
|
-
"ResponseInfo",
|
20
|
-
"OpenAPIOperation",
|
21
|
-
"OpenAPISpec",
|
22
|
-
"OpenAPIClient",
|
23
|
-
"AsyncOpenAPIClient",
|
24
|
-
"create_openapi_client",
|
25
|
-
)
|
26
|
-
|
27
|
-
|
28
|
-
class OpenAPIError(HttpError):
|
29
|
-
"""Custom exception for OpenAPI toolkit errors with semantic feedback."""
|
30
|
-
|
31
|
-
def __init__(
|
32
|
-
self,
|
33
|
-
message: str,
|
34
|
-
suggestion: str = "",
|
35
|
-
context: Optional[Dict[str, Any]] = None,
|
36
|
-
schema_path: Optional[str] = None,
|
37
|
-
operation_id: Optional[str] = None,
|
38
|
-
):
|
39
|
-
super().__init__(message, suggestion, context)
|
40
|
-
self.schema_path = schema_path
|
41
|
-
self.operation_id = operation_id
|
42
|
-
|
43
|
-
def get_full_error(self) -> str:
|
44
|
-
"""Get the full error message with OpenAPI-specific context."""
|
45
|
-
error_msg = f"OPENAPI ERROR: {self.message}"
|
46
|
-
if self.operation_id:
|
47
|
-
error_msg += f" (Operation: {self.operation_id})"
|
48
|
-
if self.schema_path:
|
49
|
-
error_msg += f" (Path: {self.schema_path})"
|
50
|
-
if self.suggestion:
|
51
|
-
error_msg += f"\nSUGGESTION: {self.suggestion}"
|
52
|
-
if self.context:
|
53
|
-
error_msg += f"\nCONTEXT: {self.context}"
|
54
|
-
return error_msg
|
55
|
-
|
56
|
-
def __str__(self) -> str:
|
57
|
-
"""Return the full error message when converting to string."""
|
58
|
-
return self.get_full_error()
|
59
|
-
|
60
|
-
|
61
|
-
class ParameterInfo(BaseModel):
|
62
|
-
"""Represents a single parameter for an HTTP operation."""
|
63
|
-
|
64
|
-
name: str
|
65
|
-
location: Literal["path", "query", "header", "cookie"]
|
66
|
-
required: bool = False
|
67
|
-
schema_: Dict[str, Any] = Field(default_factory=dict, alias="schema")
|
68
|
-
description: Optional[str] = None
|
69
|
-
|
70
|
-
|
71
|
-
class RequestBodyInfo(BaseModel):
|
72
|
-
"""Represents the request body for an HTTP operation."""
|
73
|
-
|
74
|
-
required: bool = False
|
75
|
-
content_schema: Dict[str, Dict[str, Any]] = Field(
|
76
|
-
default_factory=dict
|
77
|
-
) # Key: media type
|
78
|
-
description: Optional[str] = None
|
79
|
-
|
80
|
-
|
81
|
-
class ResponseInfo(BaseModel):
|
82
|
-
"""Represents response information."""
|
83
|
-
|
84
|
-
description: Optional[str] = None
|
85
|
-
content_schema: Dict[str, Dict[str, Any]] = Field(
|
86
|
-
default_factory=dict
|
87
|
-
) # Key: media type
|
88
|
-
|
89
|
-
|
90
|
-
class OpenAPIOperation(BaseModel):
|
91
|
-
"""Represents a single OpenAPI operation."""
|
92
|
-
|
93
|
-
path: str
|
94
|
-
method: Literal["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]
|
95
|
-
operation_id: Optional[str] = None
|
96
|
-
summary: Optional[str] = None
|
97
|
-
description: Optional[str] = None
|
98
|
-
tags: List[str] = Field(default_factory=list)
|
99
|
-
parameters: List[ParameterInfo] = Field(default_factory=list)
|
100
|
-
request_body: Optional[RequestBodyInfo] = None
|
101
|
-
responses: Dict[str, ResponseInfo] = Field(default_factory=dict)
|
102
|
-
|
103
|
-
@field_validator("method", mode="before")
|
104
|
-
@classmethod
|
105
|
-
def validate_method(cls, v):
|
106
|
-
"""Validate HTTP method."""
|
107
|
-
return v.upper()
|
108
|
-
|
109
|
-
|
110
|
-
class OpenAPISpec(BaseModel):
|
111
|
-
"""Represents a parsed OpenAPI specification."""
|
112
|
-
|
113
|
-
openapi: str
|
114
|
-
info: Dict[str, Any]
|
115
|
-
servers: List[Dict[str, Any]] = Field(default_factory=list)
|
116
|
-
operations: List[OpenAPIOperation] = Field(default_factory=list)
|
117
|
-
components: Dict[str, Any] = Field(default_factory=dict)
|
118
|
-
|
119
|
-
@property
|
120
|
-
def base_url(self) -> Optional[str]:
|
121
|
-
"""Get the base URL from servers."""
|
122
|
-
if self.servers:
|
123
|
-
return self.servers[0].get("url")
|
124
|
-
return None
|
125
|
-
|
126
|
-
|
127
|
-
class AsyncOpenAPIClient(AsyncHttpClient):
|
128
|
-
"""
|
129
|
-
OpenAPI toolkit that extends HttpToolkit with OpenAPI schema parsing and operation execution.
|
130
|
-
|
131
|
-
This class parses OpenAPI specifications and provides methods to execute operations
|
132
|
-
with proper parameter validation and semantic error handling.
|
133
|
-
"""
|
134
|
-
|
135
|
-
def __init__(
|
136
|
-
self,
|
137
|
-
openapi_spec: Union[str, Dict[str, Any]],
|
138
|
-
base_url: Optional[str] = None,
|
139
|
-
default_headers: Optional[Dict[str, str]] = None,
|
140
|
-
timeout: float = 30.0,
|
141
|
-
follow_redirects: bool = True,
|
142
|
-
verify_ssl: bool = True,
|
143
|
-
# Semantic authentication parameters
|
144
|
-
api_key: Optional[str] = None,
|
145
|
-
api_key_header: str = "X-API-Key",
|
146
|
-
bearer_token: Optional[str] = None,
|
147
|
-
basic_auth: Optional[tuple[str, str]] = None,
|
148
|
-
user_agent: Optional[str] = None,
|
149
|
-
):
|
150
|
-
"""
|
151
|
-
Initialize the OpenAPI toolkit.
|
152
|
-
|
153
|
-
Args:
|
154
|
-
openapi_spec: OpenAPI specification as dict, JSON string, or YAML string
|
155
|
-
base_url: Base URL override (uses spec servers if not provided)
|
156
|
-
default_headers: Default headers to include in all requests
|
157
|
-
timeout: Default timeout in seconds
|
158
|
-
follow_redirects: Whether to follow redirects by default
|
159
|
-
verify_ssl: Whether to verify SSL certificates
|
160
|
-
api_key: API key for authentication
|
161
|
-
api_key_header: Header name for API key (default: X-API-Key)
|
162
|
-
bearer_token: Bearer token for Authorization header
|
163
|
-
basic_auth: Tuple of (username, password) for basic auth
|
164
|
-
user_agent: User-Agent header value
|
165
|
-
"""
|
166
|
-
# Parse the OpenAPI spec
|
167
|
-
self.spec = self._parse_openapi_spec(openapi_spec)
|
168
|
-
|
169
|
-
# Use base_url override or get from spec
|
170
|
-
resolved_base_url = base_url or self.spec.base_url
|
171
|
-
|
172
|
-
# Initialize the parent HttpToolkit
|
173
|
-
super().__init__(
|
174
|
-
base_url=resolved_base_url,
|
175
|
-
default_headers=default_headers,
|
176
|
-
timeout=timeout,
|
177
|
-
follow_redirects=follow_redirects,
|
178
|
-
verify_ssl=verify_ssl,
|
179
|
-
api_key=api_key,
|
180
|
-
api_key_header=api_key_header,
|
181
|
-
bearer_token=bearer_token,
|
182
|
-
basic_auth=basic_auth,
|
183
|
-
user_agent=user_agent,
|
184
|
-
)
|
185
|
-
|
186
|
-
def _parse_openapi_spec(self, spec: Union[str, Dict[str, Any]]) -> OpenAPISpec:
|
187
|
-
"""Parse OpenAPI specification from various formats."""
|
188
|
-
try:
|
189
|
-
# Handle different input types
|
190
|
-
if isinstance(spec, str):
|
191
|
-
# Try to parse as JSON first
|
192
|
-
try:
|
193
|
-
spec_dict = json.loads(spec)
|
194
|
-
except json.JSONDecodeError:
|
195
|
-
# Try to parse as YAML
|
196
|
-
try:
|
197
|
-
spec_dict = yaml.decode(
|
198
|
-
spec.encode() if isinstance(spec, str) else spec
|
199
|
-
)
|
200
|
-
except Exception as e:
|
201
|
-
raise OpenAPIError(
|
202
|
-
message="Failed to parse OpenAPI specification",
|
203
|
-
suggestion="Ensure the specification is valid JSON or YAML",
|
204
|
-
context={"parsing_error": str(e)},
|
205
|
-
)
|
206
|
-
elif isinstance(spec, dict):
|
207
|
-
spec_dict = spec
|
208
|
-
else:
|
209
|
-
raise OpenAPIError(
|
210
|
-
message="Invalid OpenAPI specification format",
|
211
|
-
suggestion="Provide specification as dict, JSON string, or YAML string",
|
212
|
-
context={"provided_type": type(spec).__name__},
|
213
|
-
)
|
214
|
-
|
215
|
-
# Validate required fields
|
216
|
-
if "openapi" not in spec_dict:
|
217
|
-
raise OpenAPIError(
|
218
|
-
message="OpenAPI version not specified",
|
219
|
-
suggestion="Include 'openapi' field with version (e.g., '3.0.0')",
|
220
|
-
context={"spec_keys": list(spec_dict.keys())},
|
221
|
-
)
|
222
|
-
|
223
|
-
if "info" not in spec_dict:
|
224
|
-
raise OpenAPIError(
|
225
|
-
message="OpenAPI info section missing",
|
226
|
-
suggestion="Include 'info' section with title and version",
|
227
|
-
context={"spec_keys": list(spec_dict.keys())},
|
228
|
-
)
|
229
|
-
|
230
|
-
# Parse operations from paths
|
231
|
-
operations = []
|
232
|
-
paths = spec_dict.get("paths", {})
|
233
|
-
|
234
|
-
for path_str, path_item in paths.items():
|
235
|
-
if not isinstance(path_item, dict):
|
236
|
-
continue
|
237
|
-
|
238
|
-
# Extract operations for each HTTP method
|
239
|
-
for method in [
|
240
|
-
"get",
|
241
|
-
"post",
|
242
|
-
"put",
|
243
|
-
"patch",
|
244
|
-
"delete",
|
245
|
-
"head",
|
246
|
-
"options",
|
247
|
-
]:
|
248
|
-
operation_data = path_item.get(method)
|
249
|
-
if not operation_data:
|
250
|
-
continue
|
251
|
-
|
252
|
-
try:
|
253
|
-
operation = self._parse_operation(
|
254
|
-
path_str, method, operation_data
|
255
|
-
)
|
256
|
-
operations.append(operation)
|
257
|
-
except Exception as e:
|
258
|
-
# Log the error but continue processing other operations
|
259
|
-
print(
|
260
|
-
f"Warning: Failed to parse operation {method.upper()} {path_str}: {e}"
|
261
|
-
)
|
262
|
-
|
263
|
-
return OpenAPISpec(
|
264
|
-
openapi=spec_dict["openapi"],
|
265
|
-
info=spec_dict["info"],
|
266
|
-
servers=spec_dict.get("servers", []),
|
267
|
-
operations=operations,
|
268
|
-
components=spec_dict.get("components", {}),
|
269
|
-
)
|
270
|
-
|
271
|
-
except OpenAPIError:
|
272
|
-
raise
|
273
|
-
except Exception as e:
|
274
|
-
raise OpenAPIError(
|
275
|
-
message=f"Failed to parse OpenAPI specification: {str(e)}",
|
276
|
-
suggestion="Check that the specification is valid and well-formed",
|
277
|
-
context={"error_type": type(e).__name__},
|
278
|
-
)
|
279
|
-
|
280
|
-
def _parse_operation(
|
281
|
-
self, path: str, method: str, operation_data: Dict[str, Any]
|
282
|
-
) -> OpenAPIOperation:
|
283
|
-
"""Parse a single OpenAPI operation."""
|
284
|
-
# Extract parameters
|
285
|
-
parameters = []
|
286
|
-
for param_data in operation_data.get("parameters", []):
|
287
|
-
if "$ref" in param_data:
|
288
|
-
# For simplicity, skip references for now
|
289
|
-
continue
|
290
|
-
|
291
|
-
param = ParameterInfo(
|
292
|
-
name=param_data["name"],
|
293
|
-
location=param_data["in"],
|
294
|
-
required=param_data.get("required", False),
|
295
|
-
schema_=param_data.get("schema", {}),
|
296
|
-
description=param_data.get("description"),
|
297
|
-
)
|
298
|
-
parameters.append(param)
|
299
|
-
|
300
|
-
# Extract request body
|
301
|
-
request_body = None
|
302
|
-
request_body_data = operation_data.get("requestBody")
|
303
|
-
if request_body_data and "$ref" not in request_body_data:
|
304
|
-
content_schema = {}
|
305
|
-
content = request_body_data.get("content", {})
|
306
|
-
for media_type, media_data in content.items():
|
307
|
-
if "schema" in media_data:
|
308
|
-
content_schema[media_type] = media_data["schema"]
|
309
|
-
|
310
|
-
request_body = RequestBodyInfo(
|
311
|
-
required=request_body_data.get("required", False),
|
312
|
-
content_schema=content_schema,
|
313
|
-
description=request_body_data.get("description"),
|
314
|
-
)
|
315
|
-
|
316
|
-
# Extract responses
|
317
|
-
responses = {}
|
318
|
-
for status_code, response_data in operation_data.get("responses", {}).items():
|
319
|
-
if "$ref" in response_data:
|
320
|
-
# For simplicity, skip references for now
|
321
|
-
continue
|
322
|
-
|
323
|
-
content_schema = {}
|
324
|
-
content = response_data.get("content", {})
|
325
|
-
for media_type, media_data in content.items():
|
326
|
-
if "schema" in media_data:
|
327
|
-
content_schema[media_type] = media_data["schema"]
|
328
|
-
|
329
|
-
responses[status_code] = ResponseInfo(
|
330
|
-
description=response_data.get("description"),
|
331
|
-
content_schema=content_schema,
|
332
|
-
)
|
333
|
-
|
334
|
-
return OpenAPIOperation(
|
335
|
-
path=path,
|
336
|
-
method=method.upper(),
|
337
|
-
operation_id=operation_data.get("operationId"),
|
338
|
-
summary=operation_data.get("summary"),
|
339
|
-
description=operation_data.get("description"),
|
340
|
-
tags=operation_data.get("tags", []),
|
341
|
-
parameters=parameters,
|
342
|
-
request_body=request_body,
|
343
|
-
responses=responses,
|
344
|
-
)
|
345
|
-
|
346
|
-
def get_operations(self) -> List[OpenAPIOperation]:
|
347
|
-
"""Get all available operations."""
|
348
|
-
return self.spec.operations
|
349
|
-
|
350
|
-
def get_operation(self, operation_id: str) -> Optional[OpenAPIOperation]:
|
351
|
-
"""Get operation by operation ID."""
|
352
|
-
for operation in self.spec.operations:
|
353
|
-
if operation.operation_id == operation_id:
|
354
|
-
return operation
|
355
|
-
return None
|
356
|
-
|
357
|
-
def get_operations_by_tag(self, tag: str) -> List[OpenAPIOperation]:
|
358
|
-
"""Get operations by tag."""
|
359
|
-
return [op for op in self.spec.operations if tag in op.tags]
|
360
|
-
|
361
|
-
def find_operations(
|
362
|
-
self, path: Optional[str] = None, method: Optional[str] = None
|
363
|
-
) -> List[OpenAPIOperation]:
|
364
|
-
"""Find operations by path and/or method."""
|
365
|
-
operations = self.spec.operations
|
366
|
-
|
367
|
-
if path:
|
368
|
-
# Support partial path matching
|
369
|
-
operations = [op for op in operations if path in op.path]
|
370
|
-
|
371
|
-
if method:
|
372
|
-
method = method.upper()
|
373
|
-
operations = [op for op in operations if op.method == method]
|
374
|
-
|
375
|
-
return operations
|
376
|
-
|
377
|
-
async def execute_operation(
|
378
|
-
self,
|
379
|
-
operation_id: str,
|
380
|
-
parameters: Optional[Dict[str, Any]] = None,
|
381
|
-
request_body: Optional[Dict[str, Any]] = None,
|
382
|
-
headers: Optional[Dict[str, str]] = None,
|
383
|
-
) -> HttpResponse:
|
384
|
-
"""
|
385
|
-
Execute an OpenAPI operation by operation ID.
|
386
|
-
|
387
|
-
Args:
|
388
|
-
operation_id: The operation ID to execute
|
389
|
-
parameters: Parameters for the operation (path, query, header params)
|
390
|
-
request_body: Request body data for POST/PUT/PATCH operations
|
391
|
-
headers: Additional headers
|
392
|
-
|
393
|
-
Returns:
|
394
|
-
HttpResponse object with response data
|
395
|
-
|
396
|
-
Raises:
|
397
|
-
OpenAPIError: On operation execution failures
|
398
|
-
"""
|
399
|
-
# Find the operation
|
400
|
-
operation = self.get_operation(operation_id)
|
401
|
-
if not operation:
|
402
|
-
available_operations = [
|
403
|
-
op.operation_id for op in self.spec.operations if op.operation_id
|
404
|
-
]
|
405
|
-
raise OpenAPIError(
|
406
|
-
message=f"Operation '{operation_id}' not found",
|
407
|
-
suggestion=f"Use one of the available operations: {available_operations}",
|
408
|
-
context={
|
409
|
-
"requested_operation": operation_id,
|
410
|
-
"available_operations": available_operations,
|
411
|
-
},
|
412
|
-
operation_id=operation_id,
|
413
|
-
)
|
414
|
-
|
415
|
-
return await self._execute_operation_obj(
|
416
|
-
operation, parameters, request_body, headers
|
417
|
-
)
|
418
|
-
|
419
|
-
async def _execute_operation_obj(
|
420
|
-
self,
|
421
|
-
operation: OpenAPIOperation,
|
422
|
-
parameters: Optional[Dict[str, Any]] = None,
|
423
|
-
request_body: Optional[Dict[str, Any]] = None,
|
424
|
-
headers: Optional[Dict[str, str]] = None,
|
425
|
-
) -> HttpResponse:
|
426
|
-
"""Execute an OpenAPI operation object."""
|
427
|
-
parameters = parameters or {}
|
428
|
-
|
429
|
-
try:
|
430
|
-
# Build the URL with path parameters
|
431
|
-
url = operation.path
|
432
|
-
path_params = {}
|
433
|
-
query_params = {}
|
434
|
-
header_params = {}
|
435
|
-
|
436
|
-
# Separate parameters by location
|
437
|
-
for param in operation.parameters:
|
438
|
-
if param.name in parameters:
|
439
|
-
value = parameters[param.name]
|
440
|
-
|
441
|
-
if param.location == "path":
|
442
|
-
path_params[param.name] = value
|
443
|
-
elif param.location == "query":
|
444
|
-
query_params[param.name] = value
|
445
|
-
elif param.location == "header":
|
446
|
-
header_params[param.name] = str(value)
|
447
|
-
elif param.required:
|
448
|
-
raise OpenAPIError(
|
449
|
-
message=f"Required parameter '{param.name}' not provided",
|
450
|
-
suggestion=f"Provide the required {param.location} parameter '{param.name}'",
|
451
|
-
context={
|
452
|
-
"parameter_name": param.name,
|
453
|
-
"location": param.location,
|
454
|
-
},
|
455
|
-
operation_id=operation.operation_id,
|
456
|
-
schema_path=operation.path,
|
457
|
-
)
|
458
|
-
|
459
|
-
# Replace path parameters in URL
|
460
|
-
for param_name, param_value in path_params.items():
|
461
|
-
url = url.replace(f"{{{param_name}}}", str(param_value))
|
462
|
-
|
463
|
-
# Check if there are unresolved path parameters
|
464
|
-
unresolved_params = re.findall(r"\{([^}]+)\}", url)
|
465
|
-
if unresolved_params:
|
466
|
-
raise OpenAPIError(
|
467
|
-
message=f"Path parameters not provided: {unresolved_params}",
|
468
|
-
suggestion=f"Provide values for path parameters: {', '.join(unresolved_params)}",
|
469
|
-
context={
|
470
|
-
"unresolved_params": unresolved_params,
|
471
|
-
"path": operation.path,
|
472
|
-
},
|
473
|
-
operation_id=operation.operation_id,
|
474
|
-
schema_path=operation.path,
|
475
|
-
)
|
476
|
-
|
477
|
-
# Combine headers
|
478
|
-
combined_headers = headers or {}
|
479
|
-
combined_headers.update(header_params)
|
480
|
-
|
481
|
-
# Build the full URL using the parent class method
|
482
|
-
full_url = self._build_url(url)
|
483
|
-
|
484
|
-
# Create the request
|
485
|
-
request = HttpRequest(
|
486
|
-
method=operation.method,
|
487
|
-
url=full_url,
|
488
|
-
headers=combined_headers,
|
489
|
-
params=query_params,
|
490
|
-
json_data=request_body,
|
491
|
-
)
|
492
|
-
|
493
|
-
# Execute the request
|
494
|
-
return await self.request(request)
|
495
|
-
|
496
|
-
except OpenAPIError:
|
497
|
-
raise
|
498
|
-
except Exception as e:
|
499
|
-
raise OpenAPIError(
|
500
|
-
message=f"Failed to execute operation: {str(e)}",
|
501
|
-
suggestion="Check your parameters and request body format",
|
502
|
-
context={"error_type": type(e).__name__, "parameters": parameters},
|
503
|
-
operation_id=operation.operation_id,
|
504
|
-
schema_path=operation.path,
|
505
|
-
)
|
506
|
-
|
507
|
-
def generate_example_request(self, operation_id: str) -> Dict[str, Any]:
|
508
|
-
"""
|
509
|
-
Generate an example request for an operation.
|
510
|
-
|
511
|
-
Args:
|
512
|
-
operation_id: The operation ID
|
513
|
-
|
514
|
-
Returns:
|
515
|
-
Dictionary with example parameters and request body
|
516
|
-
"""
|
517
|
-
operation = self.get_operation(operation_id)
|
518
|
-
if not operation:
|
519
|
-
raise OpenAPIError(
|
520
|
-
message=f"Operation '{operation_id}' not found",
|
521
|
-
suggestion="Use a valid operation ID from the specification",
|
522
|
-
operation_id=operation_id,
|
523
|
-
)
|
524
|
-
|
525
|
-
example = {"parameters": {}, "request_body": None}
|
526
|
-
|
527
|
-
# Generate example parameters
|
528
|
-
for param in operation.parameters:
|
529
|
-
example_value = self._generate_example_value(param.schema_)
|
530
|
-
example["parameters"][param.name] = example_value
|
531
|
-
|
532
|
-
# Generate example request body
|
533
|
-
if operation.request_body and operation.request_body.content_schema:
|
534
|
-
# Use the first available content type
|
535
|
-
content_type = next(iter(operation.request_body.content_schema))
|
536
|
-
schema = operation.request_body.content_schema[content_type]
|
537
|
-
example["request_body"] = self._generate_example_value(schema)
|
538
|
-
|
539
|
-
return example
|
540
|
-
|
541
|
-
def _generate_example_value(self, schema: Dict[str, Any]) -> Any:
|
542
|
-
"""Generate an example value from a JSON schema."""
|
543
|
-
if not schema:
|
544
|
-
return "example"
|
545
|
-
|
546
|
-
schema_type = schema.get("type", "string")
|
547
|
-
|
548
|
-
# Use provided examples or defaults
|
549
|
-
if "example" in schema:
|
550
|
-
return schema["example"]
|
551
|
-
if "default" in schema:
|
552
|
-
return schema["default"]
|
553
|
-
if "enum" in schema and schema["enum"]:
|
554
|
-
return schema["enum"][0]
|
555
|
-
|
556
|
-
# Generate based on type
|
557
|
-
if schema_type == "string":
|
558
|
-
format_type = schema.get("format", "")
|
559
|
-
if format_type == "date-time":
|
560
|
-
return "2024-01-01T12:00:00Z"
|
561
|
-
elif format_type == "date":
|
562
|
-
return "2024-01-01"
|
563
|
-
elif format_type == "email":
|
564
|
-
return "user@example.com"
|
565
|
-
elif format_type == "uuid":
|
566
|
-
return "123e4567-e89b-12d3-a456-426614174000"
|
567
|
-
else:
|
568
|
-
return "example_string"
|
569
|
-
elif schema_type == "integer":
|
570
|
-
return 42
|
571
|
-
elif schema_type == "number":
|
572
|
-
return 3.14
|
573
|
-
elif schema_type == "boolean":
|
574
|
-
return True
|
575
|
-
elif schema_type == "array":
|
576
|
-
items_schema = schema.get("items", {})
|
577
|
-
return [self._generate_example_value(items_schema)]
|
578
|
-
elif schema_type == "object":
|
579
|
-
result = {}
|
580
|
-
properties = schema.get("properties", {})
|
581
|
-
required = schema.get("required", [])
|
582
|
-
|
583
|
-
# Generate for required properties first
|
584
|
-
for prop_name in required:
|
585
|
-
if prop_name in properties:
|
586
|
-
result[prop_name] = self._generate_example_value(
|
587
|
-
properties[prop_name]
|
588
|
-
)
|
589
|
-
|
590
|
-
# Add a few optional properties for completeness
|
591
|
-
for prop_name, prop_schema in list(properties.items())[:3]:
|
592
|
-
if prop_name not in result:
|
593
|
-
result[prop_name] = self._generate_example_value(prop_schema)
|
594
|
-
|
595
|
-
return result
|
596
|
-
|
597
|
-
return "example"
|
598
|
-
|
599
|
-
|
600
|
-
class OpenAPIClient(AsyncOpenAPIClient):
|
601
|
-
"""
|
602
|
-
OpenAPI toolkit that extends HttpToolkit with OpenAPI schema parsing and operation execution.
|
603
|
-
|
604
|
-
This class parses OpenAPI specifications and provides methods to execute operations
|
605
|
-
with proper parameter validation and semantic error handling.
|
606
|
-
"""
|
607
|
-
|
608
|
-
def execute_operation(
|
609
|
-
self,
|
610
|
-
operation_id: str,
|
611
|
-
parameters: Optional[Dict[str, Any]] = None,
|
612
|
-
request_body: Optional[Dict[str, Any]] = None,
|
613
|
-
headers: Optional[Dict[str, str]] = None,
|
614
|
-
) -> HttpResponse:
|
615
|
-
"""
|
616
|
-
Execute an OpenAPI operation by operation ID (synchronous version).
|
617
|
-
|
618
|
-
Args:
|
619
|
-
operation_id: The operation ID to execute
|
620
|
-
parameters: Parameters for the operation (path, query, header params)
|
621
|
-
request_body: Request body data for POST/PUT/PATCH operations
|
622
|
-
headers: Additional headers
|
623
|
-
|
624
|
-
Returns:
|
625
|
-
HttpResponse object with response data
|
626
|
-
|
627
|
-
Raises:
|
628
|
-
OpenAPIError: On operation execution failures
|
629
|
-
"""
|
630
|
-
return asyncio.run(
|
631
|
-
self.async_execute_operation(
|
632
|
-
operation_id, parameters, request_body, headers
|
633
|
-
)
|
634
|
-
)
|
635
|
-
|
636
|
-
async def async_execute_operation(
|
637
|
-
self,
|
638
|
-
operation_id: str,
|
639
|
-
parameters: Optional[Dict[str, Any]] = None,
|
640
|
-
request_body: Optional[Dict[str, Any]] = None,
|
641
|
-
headers: Optional[Dict[str, str]] = None,
|
642
|
-
) -> HttpResponse:
|
643
|
-
"""
|
644
|
-
Execute an OpenAPI operation by operation ID (async version).
|
645
|
-
|
646
|
-
Args:
|
647
|
-
operation_id: The operation ID to execute
|
648
|
-
parameters: Parameters for the operation (path, query, header params)
|
649
|
-
request_body: Request body data for POST/PUT/PATCH operations
|
650
|
-
headers: Additional headers
|
651
|
-
|
652
|
-
Returns:
|
653
|
-
HttpResponse object with response data
|
654
|
-
|
655
|
-
Raises:
|
656
|
-
OpenAPIError: On operation execution failures
|
657
|
-
"""
|
658
|
-
return await super().execute_operation(
|
659
|
-
operation_id, parameters, request_body, headers
|
660
|
-
)
|
661
|
-
|
662
|
-
|
663
|
-
@overload
|
664
|
-
def create_openapi_client(
|
665
|
-
spec_url_or_path: str,
|
666
|
-
base_url: Optional[str] = None,
|
667
|
-
default_headers: Optional[Dict[str, str]] = None,
|
668
|
-
timeout: float = 30.0,
|
669
|
-
follow_redirects: bool = True,
|
670
|
-
verify_ssl: bool = True,
|
671
|
-
# Semantic authentication parameters
|
672
|
-
api_key: Optional[str] = None,
|
673
|
-
api_key_header: str = "X-API-Key",
|
674
|
-
bearer_token: Optional[str] = None,
|
675
|
-
basic_auth: Optional[tuple[str, str]] = None,
|
676
|
-
user_agent: Optional[str] = None,
|
677
|
-
async_client: Literal[True] = ...,
|
678
|
-
) -> AsyncOpenAPIClient: ...
|
679
|
-
|
680
|
-
|
681
|
-
@overload
|
682
|
-
def create_openapi_client(
|
683
|
-
spec_url_or_path: str,
|
684
|
-
base_url: Optional[str] = None,
|
685
|
-
default_headers: Optional[Dict[str, str]] = None,
|
686
|
-
timeout: float = 30.0,
|
687
|
-
follow_redirects: bool = True,
|
688
|
-
verify_ssl: bool = True,
|
689
|
-
# Semantic authentication parameters
|
690
|
-
api_key: Optional[str] = None,
|
691
|
-
api_key_header: str = "X-API-Key",
|
692
|
-
bearer_token: Optional[str] = None,
|
693
|
-
basic_auth: Optional[tuple[str, str]] = None,
|
694
|
-
user_agent: Optional[str] = None,
|
695
|
-
async_client: Literal[False] = ...,
|
696
|
-
) -> OpenAPIClient: ...
|
697
|
-
|
698
|
-
|
699
|
-
def create_openapi_client(
|
700
|
-
spec_url_or_path: str,
|
701
|
-
base_url: Optional[str] = None,
|
702
|
-
default_headers: Optional[Dict[str, str]] = None,
|
703
|
-
timeout: float = 30.0,
|
704
|
-
follow_redirects: bool = True,
|
705
|
-
verify_ssl: bool = True,
|
706
|
-
# Semantic authentication parameters
|
707
|
-
api_key: Optional[str] = None,
|
708
|
-
api_key_header: str = "X-API-Key",
|
709
|
-
bearer_token: Optional[str] = None,
|
710
|
-
basic_auth: Optional[tuple[str, str]] = None,
|
711
|
-
user_agent: Optional[str] = None,
|
712
|
-
async_client: bool = False,
|
713
|
-
) -> Union[OpenAPIClient, AsyncOpenAPIClient]:
|
714
|
-
"""
|
715
|
-
Create a new OpenAPIClient instance.
|
716
|
-
|
717
|
-
Args:
|
718
|
-
spec_url_or_path: URL or path to OpenAPI specification
|
719
|
-
base_url: Base URL for all requests (optional)
|
720
|
-
default_headers: Default headers to include in all requests
|
721
|
-
timeout: Default timeout in seconds
|
722
|
-
follow_redirects: Whether to follow redirects by default
|
723
|
-
verify_ssl: Whether to verify SSL certificates
|
724
|
-
api_key: API key for authentication
|
725
|
-
api_key_header: Header name for API key (default: X-API-Key)
|
726
|
-
bearer_token: Bearer token for Authorization header
|
727
|
-
basic_auth: Tuple of (username, password) for basic auth
|
728
|
-
user_agent: User-Agent header value
|
729
|
-
async_client: Whether to return an async client instance
|
730
|
-
|
731
|
-
Returns:
|
732
|
-
OpenAPIClient or AsyncOpenAPIClient instance based on async_client parameter
|
733
|
-
"""
|
734
|
-
params = locals()
|
735
|
-
del params["async_client"]
|
736
|
-
|
737
|
-
if async_client:
|
738
|
-
return AsyncOpenAPIClient(**params)
|
739
|
-
else:
|
740
|
-
return OpenAPIClient(**params)
|