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