axmp-openapi-helper 0.1.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.
- axmp_openapi_helper/__init__.py +14 -0
- axmp_openapi_helper/multi_openapi_helper.py +629 -0
- axmp_openapi_helper/openapi/axmp_api_models.py +873 -0
- axmp_openapi_helper/openapi/fastapi/openapi_models.py +400 -0
- axmp_openapi_helper/openapi/multi_openapi_spec.py +186 -0
- axmp_openapi_helper/openapi/operation.py +48 -0
- axmp_openapi_helper/wrapper/api_wrapper.py +250 -0
- axmp_openapi_helper-0.1.0.dist-info/METADATA +299 -0
- axmp_openapi_helper-0.1.0.dist-info/RECORD +11 -0
- axmp_openapi_helper-0.1.0.dist-info/WHEEL +4 -0
- axmp_openapi_helper-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""This module provides a helper for working with OpenAPI specifications."""
|
|
2
|
+
|
|
3
|
+
from .multi_openapi_helper import MultiOpenAPIHelper
|
|
4
|
+
from .openapi.multi_openapi_spec import MultiOpenAPISpecConfig
|
|
5
|
+
from .openapi.operation import AxmpAPIOperation
|
|
6
|
+
from .wrapper.api_wrapper import AuthenticationType, AxmpAPIWrapper
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AxmpAPIOperation",
|
|
10
|
+
"AuthenticationType",
|
|
11
|
+
"MultiOpenAPISpecConfig",
|
|
12
|
+
"AxmpAPIWrapper",
|
|
13
|
+
"MultiOpenAPIHelper",
|
|
14
|
+
]
|
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
"""This module provides a helper for the OpenAPI specification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from axmp_openapi_helper.openapi.axmp_api_models import SUPPORTED_METHODS, AxmpOpenAPI
|
|
9
|
+
from axmp_openapi_helper.openapi.fastapi.openapi_models import Operation
|
|
10
|
+
from axmp_openapi_helper.openapi.multi_openapi_spec import (
|
|
11
|
+
APIServerConfig,
|
|
12
|
+
AuthenticationType,
|
|
13
|
+
MethodSpec,
|
|
14
|
+
MultiOpenAPISpecConfig,
|
|
15
|
+
)
|
|
16
|
+
from axmp_openapi_helper.openapi.operation import AxmpAPIOperation
|
|
17
|
+
from axmp_openapi_helper.wrapper.api_wrapper import AxmpAPIWrapper
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MultiOpenAPIHelper:
|
|
23
|
+
"""MultiOpenAPIHelper for ZMP ApiWrapper."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, multi_openapi_spec_config: MultiOpenAPISpecConfig):
|
|
26
|
+
"""Initialize the OpenAPIHelper."""
|
|
27
|
+
self.multi_openapi_spec_config = multi_openapi_spec_config
|
|
28
|
+
self._validate_multi_openapi_spec_config()
|
|
29
|
+
|
|
30
|
+
self._openapi_servers: dict[str, APIServerConfig] = (
|
|
31
|
+
self._initialize_openapi_servers()
|
|
32
|
+
)
|
|
33
|
+
self._all_operations: list[AxmpAPIOperation] = self._initialize_all_operations()
|
|
34
|
+
self._clients: dict[str, AxmpAPIWrapper] = self._initialize_clients()
|
|
35
|
+
|
|
36
|
+
def _validate_multi_openapi_spec_config(self):
|
|
37
|
+
"""Validate the multi-server API specification configuration."""
|
|
38
|
+
path_method_set = set()
|
|
39
|
+
for backend in self.multi_openapi_spec_config.backends:
|
|
40
|
+
# check the spec file path and zmp open api
|
|
41
|
+
if not backend.spec_file_path and not backend.open_api_spec:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"Spec file path or ZMP open api is required for backend: {backend.server_name}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# check auth config
|
|
47
|
+
if backend.auth_config:
|
|
48
|
+
if backend.auth_config.type not in [
|
|
49
|
+
AuthenticationType.BASIC,
|
|
50
|
+
AuthenticationType.BEARER,
|
|
51
|
+
AuthenticationType.API_KEY,
|
|
52
|
+
AuthenticationType.NONE,
|
|
53
|
+
]:
|
|
54
|
+
raise ValueError(f"Invalid auth type: {backend.auth_config.type}")
|
|
55
|
+
|
|
56
|
+
if backend.auth_config.type == AuthenticationType.BASIC and (
|
|
57
|
+
not backend.auth_config.username or not backend.auth_config.password
|
|
58
|
+
):
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"Username and password are required for basic auth: {backend.server_name}"
|
|
61
|
+
)
|
|
62
|
+
if backend.auth_config.type == AuthenticationType.API_KEY and (
|
|
63
|
+
not backend.auth_config.api_key_name
|
|
64
|
+
or not backend.auth_config.api_key_value
|
|
65
|
+
):
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"API key name and value are required for api key auth: {backend.server_name}"
|
|
68
|
+
)
|
|
69
|
+
if (
|
|
70
|
+
backend.auth_config.type == AuthenticationType.BEARER
|
|
71
|
+
and not backend.auth_config.bearer_token
|
|
72
|
+
):
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"Bearer token is required for bearer auth: {backend.server_name}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# check tool config
|
|
78
|
+
if not backend.tool_config:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"Tool config is required for backend: {backend.server_name}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if not backend.tool_config.api_maps and not backend.tool_config.route_maps:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
f"API maps or route maps are required for backend: {backend.server_name}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# check api maps
|
|
89
|
+
if backend.tool_config.api_maps:
|
|
90
|
+
for api_map in backend.tool_config.api_maps:
|
|
91
|
+
# check path
|
|
92
|
+
if not api_map.path or not api_map.path.startswith("/"):
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"API map path is required and must start with /: {api_map.path}"
|
|
95
|
+
)
|
|
96
|
+
if api_map.path.endswith("/"):
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"API map path must not end with /: {api_map.path}"
|
|
99
|
+
)
|
|
100
|
+
if backend.base_path and not api_map.path.startswith(
|
|
101
|
+
backend.base_path
|
|
102
|
+
):
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"API map path must start with base path: {api_map.path} and base path is {backend.base_path}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# check methods
|
|
108
|
+
for method in api_map.methods:
|
|
109
|
+
method_name = None
|
|
110
|
+
if isinstance(method, str):
|
|
111
|
+
method_name = method.lower()
|
|
112
|
+
if method_name not in SUPPORTED_METHODS:
|
|
113
|
+
raise ValueError(f"Invalid method name: {method_name}")
|
|
114
|
+
elif isinstance(method, MethodSpec):
|
|
115
|
+
method_name = method.method.lower()
|
|
116
|
+
if method_name not in SUPPORTED_METHODS:
|
|
117
|
+
raise ValueError(f"Invalid method name: {method_name}")
|
|
118
|
+
if not method.tool_name and not method.description:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"Tool name and description are required for method: {method_name}"
|
|
121
|
+
)
|
|
122
|
+
else:
|
|
123
|
+
raise ValueError(f"Invalid method type: {type(method)}")
|
|
124
|
+
|
|
125
|
+
path_method = (
|
|
126
|
+
f"[{backend.server_name}:{api_map.path}:{method_name}]"
|
|
127
|
+
)
|
|
128
|
+
if path_method in path_method_set:
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"Duplicate operation found: {path_method}"
|
|
131
|
+
)
|
|
132
|
+
path_method_set.add(path_method)
|
|
133
|
+
|
|
134
|
+
# check route maps
|
|
135
|
+
if backend.tool_config.route_maps:
|
|
136
|
+
for route_map in backend.tool_config.route_maps:
|
|
137
|
+
if (
|
|
138
|
+
not route_map.pattern
|
|
139
|
+
and not route_map.methods
|
|
140
|
+
and not route_map.tags
|
|
141
|
+
):
|
|
142
|
+
raise ValueError(
|
|
143
|
+
f"At least one of pattern, methods, and tags is required for route map: {route_map}"
|
|
144
|
+
)
|
|
145
|
+
# check pattern whether it is valid regex
|
|
146
|
+
try:
|
|
147
|
+
re.compile(route_map.pattern)
|
|
148
|
+
except re.error:
|
|
149
|
+
raise ValueError(f"Invalid regex pattern: {route_map.pattern}")
|
|
150
|
+
|
|
151
|
+
# check methods whether it is valid and in supported methods
|
|
152
|
+
for method in route_map.methods:
|
|
153
|
+
if method not in SUPPORTED_METHODS:
|
|
154
|
+
raise ValueError(f"Invalid method name: {method}")
|
|
155
|
+
|
|
156
|
+
# TODO: check tags whether it is valid and in supported tags
|
|
157
|
+
|
|
158
|
+
def _initialize_openapi_servers(self) -> dict[str, APIServerConfig]:
|
|
159
|
+
"""Initialize the openapi servers."""
|
|
160
|
+
return {
|
|
161
|
+
backend.server_name: backend
|
|
162
|
+
for backend in self.multi_openapi_spec_config.backends
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
def _initialize_all_operations(self) -> list[AxmpAPIOperation]:
|
|
166
|
+
"""Initialize the all operations."""
|
|
167
|
+
operations: list[AxmpAPIOperation] = []
|
|
168
|
+
|
|
169
|
+
for backend in self.multi_openapi_spec_config.backends:
|
|
170
|
+
axmp_open_api: AxmpOpenAPI = None
|
|
171
|
+
|
|
172
|
+
if backend.spec_file_path:
|
|
173
|
+
axmp_open_api = AxmpOpenAPI.from_spec_file(backend.spec_file_path)
|
|
174
|
+
elif backend.open_api_spec:
|
|
175
|
+
axmp_open_api = AxmpOpenAPI.from_openapi(backend.open_api_spec)
|
|
176
|
+
else:
|
|
177
|
+
raise ValueError(
|
|
178
|
+
f"Spec file path or open API spec is required for backend: {backend.server_name}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# generate operations from api maps
|
|
182
|
+
if backend.tool_config.api_maps:
|
|
183
|
+
operations.extend(
|
|
184
|
+
self._get_api_operation_from_api_maps(
|
|
185
|
+
backend=backend, axmp_open_api=axmp_open_api
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# generate operations from route maps
|
|
190
|
+
if backend.tool_config.route_maps:
|
|
191
|
+
_route_map_operations = self._get_api_operation_from_route_maps(
|
|
192
|
+
backend=backend, axmp_open_api=axmp_open_api
|
|
193
|
+
)
|
|
194
|
+
# check the duplicate operations by path and method of the _route_map_operations in the operations
|
|
195
|
+
for _operation in _route_map_operations:
|
|
196
|
+
if _operation.name not in [op.name for op in operations]:
|
|
197
|
+
operations.append(_operation)
|
|
198
|
+
|
|
199
|
+
return operations
|
|
200
|
+
|
|
201
|
+
def _get_api_operation_from_route_maps(
|
|
202
|
+
self, *, backend: APIServerConfig, axmp_open_api: AxmpOpenAPI
|
|
203
|
+
) -> list[AxmpAPIOperation]:
|
|
204
|
+
"""Get API operations from route maps."""
|
|
205
|
+
common_operations: list[tuple[str, str, Operation]] = []
|
|
206
|
+
|
|
207
|
+
for route_map in backend.tool_config.route_maps:
|
|
208
|
+
pattern_matched_operations: list[tuple[str, str, Operation]] = []
|
|
209
|
+
tag_matched_operations: list[tuple[str, str, Operation]] = []
|
|
210
|
+
method_matched_operations: list[tuple[str, str, Operation]] = []
|
|
211
|
+
|
|
212
|
+
if route_map.pattern:
|
|
213
|
+
pattern_matched_operations = (
|
|
214
|
+
axmp_open_api.get_operations_by_path_pattern(
|
|
215
|
+
regex=route_map.pattern
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if route_map.tags and len(route_map.tags) > 0:
|
|
220
|
+
for tag in route_map.tags:
|
|
221
|
+
tag_matched_operations.extend(
|
|
222
|
+
axmp_open_api.get_operations_by_tag(tag=tag)
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if route_map.methods and len(route_map.methods) > 0:
|
|
226
|
+
for method in route_map.methods:
|
|
227
|
+
method_matched_operations.extend(
|
|
228
|
+
axmp_open_api.get_operations_by_method(method=method)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# extract the common operations from pattern_matched_operations, tag_matched_operations, method_matched_operations
|
|
232
|
+
for path, method, operation in pattern_matched_operations:
|
|
233
|
+
if (path, method) in [
|
|
234
|
+
(path, method) for path, method, _ in tag_matched_operations
|
|
235
|
+
] and (path, method) in [
|
|
236
|
+
(path, method) for path, method, _ in method_matched_operations
|
|
237
|
+
]:
|
|
238
|
+
# check the duplicate operations by path and method of the common_operations
|
|
239
|
+
if (path, method, operation) not in common_operations:
|
|
240
|
+
common_operations.append((path, method, operation))
|
|
241
|
+
|
|
242
|
+
return self._convert_operations_to_api_operations(
|
|
243
|
+
backend=backend,
|
|
244
|
+
axmp_open_api=axmp_open_api,
|
|
245
|
+
operations=common_operations,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def _convert_operations_to_api_operations(
|
|
249
|
+
self,
|
|
250
|
+
*,
|
|
251
|
+
backend: APIServerConfig,
|
|
252
|
+
axmp_open_api: AxmpOpenAPI,
|
|
253
|
+
operations: list[tuple[str, str, Operation]],
|
|
254
|
+
) -> list[AxmpAPIOperation]:
|
|
255
|
+
"""Convert operations to API operations."""
|
|
256
|
+
api_operations: list[AxmpAPIOperation] = []
|
|
257
|
+
for path, method, operation in operations:
|
|
258
|
+
tool_name = self._generate_name_from_path(
|
|
259
|
+
path=path,
|
|
260
|
+
method=method,
|
|
261
|
+
base_path=backend.base_path,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
description = None
|
|
265
|
+
if operation.description:
|
|
266
|
+
description = operation.description
|
|
267
|
+
else:
|
|
268
|
+
description = ""
|
|
269
|
+
|
|
270
|
+
query_params, path_params, request_body = (
|
|
271
|
+
axmp_open_api.generate_models_by_path_and_method(
|
|
272
|
+
path=path, method=method
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
api_operations.append(
|
|
277
|
+
AxmpAPIOperation(
|
|
278
|
+
server_name=backend.server_name,
|
|
279
|
+
name=tool_name,
|
|
280
|
+
description=description,
|
|
281
|
+
path=path,
|
|
282
|
+
method=method,
|
|
283
|
+
query_params=query_params,
|
|
284
|
+
path_params=path_params,
|
|
285
|
+
request_body=request_body,
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
return api_operations
|
|
290
|
+
|
|
291
|
+
def _get_api_operation_from_api_maps(
|
|
292
|
+
self, *, backend: APIServerConfig, axmp_open_api: AxmpOpenAPI
|
|
293
|
+
) -> list[AxmpAPIOperation]:
|
|
294
|
+
"""Get API operations from API maps."""
|
|
295
|
+
operations: list[AxmpAPIOperation] = []
|
|
296
|
+
|
|
297
|
+
for api_map in backend.tool_config.api_maps: # type: ignore
|
|
298
|
+
for method in api_map.methods:
|
|
299
|
+
method_name = None
|
|
300
|
+
tool_name = None
|
|
301
|
+
description = None
|
|
302
|
+
|
|
303
|
+
if isinstance(method, str):
|
|
304
|
+
method_name = method
|
|
305
|
+
elif isinstance(method, MethodSpec):
|
|
306
|
+
method_name = method.method
|
|
307
|
+
description = method.description
|
|
308
|
+
tool_name = method.tool_name
|
|
309
|
+
else:
|
|
310
|
+
raise ValueError(f"Invalid method type: {type(method)}")
|
|
311
|
+
|
|
312
|
+
operation: Operation = axmp_open_api.get_operation_by_path_method(
|
|
313
|
+
path=api_map.path,
|
|
314
|
+
method=method_name,
|
|
315
|
+
)
|
|
316
|
+
query_params, path_params, request_body = (
|
|
317
|
+
axmp_open_api.generate_models_by_path_and_method(
|
|
318
|
+
path=api_map.path, method=method_name
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# if tool_name is not provided, generate it from the path and method
|
|
323
|
+
if not tool_name:
|
|
324
|
+
tool_name = self._generate_name_from_path(
|
|
325
|
+
path=api_map.path,
|
|
326
|
+
method=method_name,
|
|
327
|
+
base_path=backend.base_path,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# duplicate check the tool_name in the operations
|
|
331
|
+
# because the tool_name should be unique in the operations for the mcp server
|
|
332
|
+
for op in operations:
|
|
333
|
+
if op.name == tool_name:
|
|
334
|
+
# NOTE: if the tool_name is duplicate, we should add a number to the tool_name
|
|
335
|
+
# to make it unique
|
|
336
|
+
tool_name = self._generate_unique_tool_name(
|
|
337
|
+
tool_name=tool_name, operations=operations
|
|
338
|
+
)
|
|
339
|
+
break
|
|
340
|
+
|
|
341
|
+
# if description is not provided, generate it from the operation
|
|
342
|
+
if not description:
|
|
343
|
+
if operation.description:
|
|
344
|
+
description = operation.description
|
|
345
|
+
else:
|
|
346
|
+
description = ""
|
|
347
|
+
|
|
348
|
+
operations.append(
|
|
349
|
+
AxmpAPIOperation(
|
|
350
|
+
server_name=backend.server_name,
|
|
351
|
+
name=tool_name,
|
|
352
|
+
description=description,
|
|
353
|
+
path=api_map.path,
|
|
354
|
+
method=method_name,
|
|
355
|
+
query_params=query_params,
|
|
356
|
+
path_params=path_params,
|
|
357
|
+
request_body=request_body,
|
|
358
|
+
)
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
return operations
|
|
362
|
+
|
|
363
|
+
def _generate_unique_tool_name(
|
|
364
|
+
self, *, tool_name: str, operations: list[AxmpAPIOperation]
|
|
365
|
+
) -> str:
|
|
366
|
+
"""Generate the unique tool name."""
|
|
367
|
+
if tool_name in [op.name for op in operations]:
|
|
368
|
+
tool_name_index = tool_name.split("_")[-1]
|
|
369
|
+
if tool_name_index.isdigit():
|
|
370
|
+
tool_name = f"{tool_name.split('_')[:-1]}_{int(tool_name_index) + 1}"
|
|
371
|
+
else:
|
|
372
|
+
tool_name = f"{tool_name}_1"
|
|
373
|
+
return tool_name
|
|
374
|
+
|
|
375
|
+
def _initialize_clients(self) -> dict[str, AxmpAPIWrapper]:
|
|
376
|
+
"""Initialize the clients."""
|
|
377
|
+
clients = {}
|
|
378
|
+
for server_name, openapi_server in self._openapi_servers.items():
|
|
379
|
+
clients[server_name] = AxmpAPIWrapper(
|
|
380
|
+
openapi_server.endpoint,
|
|
381
|
+
auth_type=openapi_server.auth_config.type,
|
|
382
|
+
username=openapi_server.auth_config.username,
|
|
383
|
+
password=openapi_server.auth_config.password,
|
|
384
|
+
bearer_token=openapi_server.auth_config.bearer_token,
|
|
385
|
+
api_key_name=openapi_server.auth_config.api_key_name,
|
|
386
|
+
api_key_value=openapi_server.auth_config.api_key_value,
|
|
387
|
+
tls_verify=openapi_server.tls_verify,
|
|
388
|
+
timeout=openapi_server.timeout,
|
|
389
|
+
)
|
|
390
|
+
return clients
|
|
391
|
+
|
|
392
|
+
@property
|
|
393
|
+
def openapi_servers(self) -> dict[str, APIServerConfig]:
|
|
394
|
+
"""Get the openapi servers."""
|
|
395
|
+
return self._openapi_servers
|
|
396
|
+
|
|
397
|
+
@property
|
|
398
|
+
def all_operations(self) -> list[AxmpAPIOperation]:
|
|
399
|
+
"""Generate the operations from the multi-server API specification configuration."""
|
|
400
|
+
if not self._all_operations:
|
|
401
|
+
self._initialize_all_operations()
|
|
402
|
+
|
|
403
|
+
return self._all_operations
|
|
404
|
+
|
|
405
|
+
def get_operations_by_server_name(
|
|
406
|
+
self, *, server_name: str
|
|
407
|
+
) -> list[AxmpAPIOperation]:
|
|
408
|
+
"""Get the operations by server name."""
|
|
409
|
+
return [op for op in self.all_operations if op.server_name == server_name]
|
|
410
|
+
|
|
411
|
+
def _generate_name_from_path(
|
|
412
|
+
self, *, path: str, method: str, base_path: str | None = None
|
|
413
|
+
) -> str:
|
|
414
|
+
"""Generate the operation name from the path."""
|
|
415
|
+
if base_path:
|
|
416
|
+
if path.startswith(base_path):
|
|
417
|
+
path = path.replace(base_path, "")
|
|
418
|
+
else:
|
|
419
|
+
# NOTE: if the path does not start with the base_path, it means the path is not in the base_path
|
|
420
|
+
# e.g. /healthz is not in the base_path /api/alert/v1
|
|
421
|
+
# raise ValueError(f"Path {path} does not start with prefix {base_path}")
|
|
422
|
+
pass
|
|
423
|
+
|
|
424
|
+
replaced_path = re.sub(r"[{}]", "", path) # remove path params brackets
|
|
425
|
+
replaced_path = re.sub(r"[/]", "_", replaced_path) # replace / with _
|
|
426
|
+
|
|
427
|
+
return f"{method.lower()}{replaced_path}"
|
|
428
|
+
|
|
429
|
+
async def run(self, *, name: str, args: dict | None = None) -> str:
|
|
430
|
+
"""Run the operation by name and args."""
|
|
431
|
+
logger.debug(f"name: {name}")
|
|
432
|
+
logger.debug(f"args: {args}")
|
|
433
|
+
|
|
434
|
+
operation = next((op for op in self.all_operations if op.name == name), None)
|
|
435
|
+
if not operation:
|
|
436
|
+
raise ValueError(f"Operation {name} not found")
|
|
437
|
+
|
|
438
|
+
if args is None:
|
|
439
|
+
args = {}
|
|
440
|
+
|
|
441
|
+
operation.path_params = (
|
|
442
|
+
operation.path_params(**args) if operation.path_params else None
|
|
443
|
+
)
|
|
444
|
+
operation.query_params = (
|
|
445
|
+
operation.query_params(**args) if operation.query_params else None
|
|
446
|
+
)
|
|
447
|
+
operation.request_body = (
|
|
448
|
+
operation.request_body(**args) if operation.request_body else None
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
logger.debug(f"path_params: {operation.path_params}")
|
|
452
|
+
logger.debug(f"query_params: {operation.query_params}")
|
|
453
|
+
logger.debug(f"request_body: {operation.request_body}")
|
|
454
|
+
|
|
455
|
+
return await self.run_operation(operation=operation)
|
|
456
|
+
|
|
457
|
+
async def run_operation(self, *, operation: AxmpAPIOperation) -> str:
|
|
458
|
+
"""Run the operation."""
|
|
459
|
+
logger.debug(f"operation: {operation}")
|
|
460
|
+
|
|
461
|
+
client = self._clients[operation.server_name]
|
|
462
|
+
|
|
463
|
+
logger.debug(f"client: {client}")
|
|
464
|
+
|
|
465
|
+
return await client.run(
|
|
466
|
+
operation.method,
|
|
467
|
+
operation.path,
|
|
468
|
+
path_params=operation.path_params,
|
|
469
|
+
query_params=operation.query_params,
|
|
470
|
+
request_body=operation.request_body,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
def get_all_tags(self) -> list[str]:
|
|
474
|
+
"""Get all tags of the operations."""
|
|
475
|
+
tags = []
|
|
476
|
+
|
|
477
|
+
for backend in self.multi_openapi_spec_config.backends:
|
|
478
|
+
axmp_open_api: AxmpOpenAPI = None
|
|
479
|
+
|
|
480
|
+
if backend.spec_file_path:
|
|
481
|
+
axmp_open_api = AxmpOpenAPI.from_spec_file(backend.spec_file_path)
|
|
482
|
+
elif backend.open_api_spec:
|
|
483
|
+
axmp_open_api = AxmpOpenAPI.from_openapi(backend.open_api_spec)
|
|
484
|
+
else:
|
|
485
|
+
raise ValueError(
|
|
486
|
+
f"Spec file path or open API spec is required for backend: {backend.server_name}"
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
tags.extend(axmp_open_api.get_tags())
|
|
490
|
+
|
|
491
|
+
return list(set(tags))
|
|
492
|
+
|
|
493
|
+
def get_tags(self, *, server_name: str) -> list[str]:
|
|
494
|
+
"""Get all tags of the operations by server name."""
|
|
495
|
+
open_api_server = self._openapi_servers[server_name]
|
|
496
|
+
axmp_open_api: AxmpOpenAPI = None
|
|
497
|
+
|
|
498
|
+
if open_api_server.spec_file_path:
|
|
499
|
+
axmp_open_api = AxmpOpenAPI.from_spec_file(open_api_server.spec_file_path)
|
|
500
|
+
elif open_api_server.open_api_spec:
|
|
501
|
+
axmp_open_api = AxmpOpenAPI.from_openapi(open_api_server.open_api_spec)
|
|
502
|
+
|
|
503
|
+
if not axmp_open_api:
|
|
504
|
+
raise ValueError(f"OpenAPI spec is required for server: {server_name}")
|
|
505
|
+
|
|
506
|
+
return axmp_open_api.get_tags()
|
|
507
|
+
|
|
508
|
+
# get all operations by tag
|
|
509
|
+
def get_all_operations_by_tag(
|
|
510
|
+
self, *, tag: str
|
|
511
|
+
) -> list[tuple[str, str, Operation]]:
|
|
512
|
+
"""Get operations by tag."""
|
|
513
|
+
operations: list[tuple[str, str, Operation]] = []
|
|
514
|
+
for backend in self.multi_openapi_spec_config.backends:
|
|
515
|
+
axmp_open_api: AxmpOpenAPI = None
|
|
516
|
+
|
|
517
|
+
if backend.spec_file_path:
|
|
518
|
+
axmp_open_api = AxmpOpenAPI.from_spec_file(backend.spec_file_path)
|
|
519
|
+
elif backend.open_api_spec:
|
|
520
|
+
axmp_open_api = AxmpOpenAPI.from_openapi(backend.open_api_spec)
|
|
521
|
+
else:
|
|
522
|
+
raise ValueError(
|
|
523
|
+
f"Spec file path or open API spec is required for backend: {backend.server_name}"
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
operations.extend(axmp_open_api.get_operations_by_tag(tag=tag))
|
|
527
|
+
|
|
528
|
+
return operations
|
|
529
|
+
|
|
530
|
+
def get_operations_by_tag(
|
|
531
|
+
self, *, server_name: str, tag: str
|
|
532
|
+
) -> list[tuple[str, str, Operation]]:
|
|
533
|
+
"""Get operations by tag."""
|
|
534
|
+
open_api_server = self._openapi_servers[server_name]
|
|
535
|
+
axmp_open_api: AxmpOpenAPI = None
|
|
536
|
+
|
|
537
|
+
if open_api_server.spec_file_path:
|
|
538
|
+
axmp_open_api = AxmpOpenAPI.from_spec_file(open_api_server.spec_file_path)
|
|
539
|
+
elif open_api_server.open_api_spec:
|
|
540
|
+
axmp_open_api = AxmpOpenAPI.from_openapi(open_api_server.open_api_spec)
|
|
541
|
+
else:
|
|
542
|
+
raise ValueError(
|
|
543
|
+
f"Spec file path or open API spec is required for server: {server_name}"
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
return axmp_open_api.get_operations_by_tag(tag=tag)
|
|
547
|
+
|
|
548
|
+
def get_all_operations_by_path_pattern(
|
|
549
|
+
self, *, regex: str
|
|
550
|
+
) -> list[tuple[str, str, Operation]]:
|
|
551
|
+
"""Get operations by path pattern."""
|
|
552
|
+
operations: list[tuple[str, str, Operation]] = []
|
|
553
|
+
for backend in self.multi_openapi_spec_config.backends:
|
|
554
|
+
axmp_open_api: AxmpOpenAPI = None
|
|
555
|
+
|
|
556
|
+
if backend.spec_file_path:
|
|
557
|
+
axmp_open_api = AxmpOpenAPI.from_spec_file(backend.spec_file_path)
|
|
558
|
+
elif backend.open_api_spec:
|
|
559
|
+
axmp_open_api = AxmpOpenAPI.from_openapi(backend.open_api_spec)
|
|
560
|
+
else:
|
|
561
|
+
raise ValueError(
|
|
562
|
+
f"Spec file path or open API spec is required for backend: {backend.server_name}"
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
operations.extend(axmp_open_api.get_operations_by_path_pattern(regex=regex))
|
|
566
|
+
|
|
567
|
+
return operations
|
|
568
|
+
|
|
569
|
+
def get_operations_by_path_pattern(
|
|
570
|
+
self, *, server_name: str, regex: str
|
|
571
|
+
) -> list[tuple[str, str, Operation]]:
|
|
572
|
+
"""Get operations by path pattern."""
|
|
573
|
+
open_api_server = self._openapi_servers[server_name]
|
|
574
|
+
axmp_open_api: AxmpOpenAPI = None
|
|
575
|
+
|
|
576
|
+
if open_api_server.spec_file_path:
|
|
577
|
+
axmp_open_api = AxmpOpenAPI.from_spec_file(open_api_server.spec_file_path)
|
|
578
|
+
elif open_api_server.open_api_spec:
|
|
579
|
+
axmp_open_api = AxmpOpenAPI.from_openapi(open_api_server.open_api_spec)
|
|
580
|
+
else:
|
|
581
|
+
raise ValueError(
|
|
582
|
+
f"Spec file path or open API spec is required for server: {server_name}"
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
return axmp_open_api.get_operations_by_path_pattern(regex=regex)
|
|
586
|
+
|
|
587
|
+
def get_all_operations_by_method(
|
|
588
|
+
self, *, method: str
|
|
589
|
+
) -> list[tuple[str, str, Operation]]:
|
|
590
|
+
"""Get operations by method."""
|
|
591
|
+
operations: list[tuple[str, str, Operation]] = []
|
|
592
|
+
for backend in self.multi_openapi_spec_config.backends:
|
|
593
|
+
axmp_open_api: AxmpOpenAPI = None
|
|
594
|
+
|
|
595
|
+
if backend.spec_file_path:
|
|
596
|
+
axmp_open_api = AxmpOpenAPI.from_spec_file(backend.spec_file_path)
|
|
597
|
+
elif backend.open_api_spec:
|
|
598
|
+
axmp_open_api = AxmpOpenAPI.from_openapi(backend.open_api_spec)
|
|
599
|
+
else:
|
|
600
|
+
raise ValueError(
|
|
601
|
+
f"Spec file path or open API spec is required for backend: {backend.server_name}"
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
operations.extend(axmp_open_api.get_operations_by_method(method=method))
|
|
605
|
+
|
|
606
|
+
return operations
|
|
607
|
+
|
|
608
|
+
def get_operations_by_method(
|
|
609
|
+
self, *, server_name: str, method: str
|
|
610
|
+
) -> list[tuple[str, str, Operation]]:
|
|
611
|
+
"""Get operations by method."""
|
|
612
|
+
open_api_server = self._openapi_servers[server_name]
|
|
613
|
+
axmp_open_api: AxmpOpenAPI = None
|
|
614
|
+
|
|
615
|
+
if open_api_server.spec_file_path:
|
|
616
|
+
axmp_open_api = AxmpOpenAPI.from_spec_file(open_api_server.spec_file_path)
|
|
617
|
+
elif open_api_server.open_api_spec:
|
|
618
|
+
axmp_open_api = AxmpOpenAPI.from_openapi(open_api_server.open_api_spec)
|
|
619
|
+
else:
|
|
620
|
+
raise ValueError(
|
|
621
|
+
f"Spec file path or open API spec is required for server: {server_name}"
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
return axmp_open_api.get_operations_by_method(method=method)
|
|
625
|
+
|
|
626
|
+
async def close(self) -> None:
|
|
627
|
+
"""Close the clients."""
|
|
628
|
+
for client in self._clients.values():
|
|
629
|
+
await client.close()
|