hammad-python 0.0.30__py3-none-any.whl → 0.0.32__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.
Files changed (137) hide show
  1. ham/__init__.py +200 -0
  2. {hammad_python-0.0.30.dist-info → hammad_python-0.0.32.dist-info}/METADATA +6 -32
  3. hammad_python-0.0.32.dist-info/RECORD +6 -0
  4. hammad/__init__.py +0 -84
  5. hammad/_internal.py +0 -256
  6. hammad/_main.py +0 -226
  7. hammad/cache/__init__.py +0 -40
  8. hammad/cache/base_cache.py +0 -181
  9. hammad/cache/cache.py +0 -169
  10. hammad/cache/decorators.py +0 -261
  11. hammad/cache/file_cache.py +0 -80
  12. hammad/cache/ttl_cache.py +0 -74
  13. hammad/cli/__init__.py +0 -33
  14. hammad/cli/animations.py +0 -573
  15. hammad/cli/plugins.py +0 -867
  16. hammad/cli/styles/__init__.py +0 -55
  17. hammad/cli/styles/settings.py +0 -139
  18. hammad/cli/styles/types.py +0 -358
  19. hammad/cli/styles/utils.py +0 -634
  20. hammad/data/__init__.py +0 -90
  21. hammad/data/collections/__init__.py +0 -49
  22. hammad/data/collections/collection.py +0 -326
  23. hammad/data/collections/indexes/__init__.py +0 -37
  24. hammad/data/collections/indexes/qdrant/__init__.py +0 -1
  25. hammad/data/collections/indexes/qdrant/index.py +0 -723
  26. hammad/data/collections/indexes/qdrant/settings.py +0 -94
  27. hammad/data/collections/indexes/qdrant/utils.py +0 -210
  28. hammad/data/collections/indexes/tantivy/__init__.py +0 -1
  29. hammad/data/collections/indexes/tantivy/index.py +0 -426
  30. hammad/data/collections/indexes/tantivy/settings.py +0 -40
  31. hammad/data/collections/indexes/tantivy/utils.py +0 -176
  32. hammad/data/configurations/__init__.py +0 -35
  33. hammad/data/configurations/configuration.py +0 -564
  34. hammad/data/models/__init__.py +0 -50
  35. hammad/data/models/extensions/__init__.py +0 -4
  36. hammad/data/models/extensions/pydantic/__init__.py +0 -42
  37. hammad/data/models/extensions/pydantic/converters.py +0 -759
  38. hammad/data/models/fields.py +0 -546
  39. hammad/data/models/model.py +0 -1078
  40. hammad/data/models/utils.py +0 -280
  41. hammad/data/sql/__init__.py +0 -24
  42. hammad/data/sql/database.py +0 -576
  43. hammad/data/sql/types.py +0 -127
  44. hammad/data/types/__init__.py +0 -75
  45. hammad/data/types/file.py +0 -431
  46. hammad/data/types/multimodal/__init__.py +0 -36
  47. hammad/data/types/multimodal/audio.py +0 -200
  48. hammad/data/types/multimodal/image.py +0 -182
  49. hammad/data/types/text.py +0 -1308
  50. hammad/formatting/__init__.py +0 -33
  51. hammad/formatting/json/__init__.py +0 -27
  52. hammad/formatting/json/converters.py +0 -158
  53. hammad/formatting/text/__init__.py +0 -63
  54. hammad/formatting/text/converters.py +0 -723
  55. hammad/formatting/text/markdown.py +0 -131
  56. hammad/formatting/yaml/__init__.py +0 -26
  57. hammad/formatting/yaml/converters.py +0 -5
  58. hammad/genai/__init__.py +0 -217
  59. hammad/genai/a2a/__init__.py +0 -32
  60. hammad/genai/a2a/workers.py +0 -552
  61. hammad/genai/agents/__init__.py +0 -59
  62. hammad/genai/agents/agent.py +0 -1973
  63. hammad/genai/agents/run.py +0 -1024
  64. hammad/genai/agents/types/__init__.py +0 -42
  65. hammad/genai/agents/types/agent_context.py +0 -13
  66. hammad/genai/agents/types/agent_event.py +0 -128
  67. hammad/genai/agents/types/agent_hooks.py +0 -220
  68. hammad/genai/agents/types/agent_messages.py +0 -31
  69. hammad/genai/agents/types/agent_response.py +0 -125
  70. hammad/genai/agents/types/agent_stream.py +0 -327
  71. hammad/genai/graphs/__init__.py +0 -125
  72. hammad/genai/graphs/_utils.py +0 -190
  73. hammad/genai/graphs/base.py +0 -1828
  74. hammad/genai/graphs/plugins.py +0 -316
  75. hammad/genai/graphs/types.py +0 -638
  76. hammad/genai/models/__init__.py +0 -1
  77. hammad/genai/models/embeddings/__init__.py +0 -43
  78. hammad/genai/models/embeddings/model.py +0 -226
  79. hammad/genai/models/embeddings/run.py +0 -163
  80. hammad/genai/models/embeddings/types/__init__.py +0 -37
  81. hammad/genai/models/embeddings/types/embedding_model_name.py +0 -75
  82. hammad/genai/models/embeddings/types/embedding_model_response.py +0 -76
  83. hammad/genai/models/embeddings/types/embedding_model_run_params.py +0 -66
  84. hammad/genai/models/embeddings/types/embedding_model_settings.py +0 -47
  85. hammad/genai/models/language/__init__.py +0 -57
  86. hammad/genai/models/language/model.py +0 -1098
  87. hammad/genai/models/language/run.py +0 -878
  88. hammad/genai/models/language/types/__init__.py +0 -40
  89. hammad/genai/models/language/types/language_model_instructor_mode.py +0 -47
  90. hammad/genai/models/language/types/language_model_messages.py +0 -28
  91. hammad/genai/models/language/types/language_model_name.py +0 -239
  92. hammad/genai/models/language/types/language_model_request.py +0 -127
  93. hammad/genai/models/language/types/language_model_response.py +0 -217
  94. hammad/genai/models/language/types/language_model_response_chunk.py +0 -56
  95. hammad/genai/models/language/types/language_model_settings.py +0 -89
  96. hammad/genai/models/language/types/language_model_stream.py +0 -600
  97. hammad/genai/models/language/utils/__init__.py +0 -28
  98. hammad/genai/models/language/utils/requests.py +0 -421
  99. hammad/genai/models/language/utils/structured_outputs.py +0 -135
  100. hammad/genai/models/model_provider.py +0 -4
  101. hammad/genai/models/multimodal.py +0 -47
  102. hammad/genai/models/reranking.py +0 -26
  103. hammad/genai/types/__init__.py +0 -1
  104. hammad/genai/types/base.py +0 -215
  105. hammad/genai/types/history.py +0 -290
  106. hammad/genai/types/tools.py +0 -507
  107. hammad/logging/__init__.py +0 -35
  108. hammad/logging/decorators.py +0 -834
  109. hammad/logging/logger.py +0 -1018
  110. hammad/mcp/__init__.py +0 -53
  111. hammad/mcp/client/__init__.py +0 -35
  112. hammad/mcp/client/client.py +0 -624
  113. hammad/mcp/client/client_service.py +0 -400
  114. hammad/mcp/client/settings.py +0 -178
  115. hammad/mcp/servers/__init__.py +0 -26
  116. hammad/mcp/servers/launcher.py +0 -1161
  117. hammad/runtime/__init__.py +0 -32
  118. hammad/runtime/decorators.py +0 -142
  119. hammad/runtime/run.py +0 -299
  120. hammad/service/__init__.py +0 -49
  121. hammad/service/create.py +0 -527
  122. hammad/service/decorators.py +0 -283
  123. hammad/types.py +0 -288
  124. hammad/typing/__init__.py +0 -435
  125. hammad/web/__init__.py +0 -43
  126. hammad/web/http/__init__.py +0 -1
  127. hammad/web/http/client.py +0 -944
  128. hammad/web/models.py +0 -275
  129. hammad/web/openapi/__init__.py +0 -1
  130. hammad/web/openapi/client.py +0 -740
  131. hammad/web/search/__init__.py +0 -1
  132. hammad/web/search/client.py +0 -1023
  133. hammad/web/utils.py +0 -472
  134. hammad_python-0.0.30.dist-info/RECORD +0 -135
  135. {hammad → ham}/py.typed +0 -0
  136. {hammad_python-0.0.30.dist-info → hammad_python-0.0.32.dist-info}/WHEEL +0 -0
  137. {hammad_python-0.0.30.dist-info → hammad_python-0.0.32.dist-info}/licenses/LICENSE +0 -0
@@ -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)