mcp-proxy-adapter 2.1.0__py3-none-any.whl → 2.1.2__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 (78) hide show
  1. docs/README.md +172 -0
  2. docs/README_ru.md +172 -0
  3. docs/architecture.md +251 -0
  4. docs/architecture_ru.md +343 -0
  5. docs/command_development.md +250 -0
  6. docs/command_development_ru.md +593 -0
  7. docs/deployment.md +251 -0
  8. docs/deployment_ru.md +1298 -0
  9. docs/examples.md +254 -0
  10. docs/examples_ru.md +401 -0
  11. docs/mcp_proxy_adapter.md +251 -0
  12. docs/mcp_proxy_adapter_ru.md +405 -0
  13. docs/quickstart.md +251 -0
  14. docs/quickstart_ru.md +397 -0
  15. docs/testing.md +255 -0
  16. docs/testing_ru.md +469 -0
  17. docs/validation_ru.md +287 -0
  18. examples/analyze_config.py +141 -0
  19. examples/basic_integration.py +161 -0
  20. examples/docstring_and_schema_example.py +60 -0
  21. examples/extension_example.py +60 -0
  22. examples/help_best_practices.py +67 -0
  23. examples/help_usage.py +64 -0
  24. examples/mcp_proxy_client.py +131 -0
  25. examples/mcp_proxy_config.json +175 -0
  26. examples/openapi_server.py +369 -0
  27. examples/project_structure_example.py +47 -0
  28. examples/testing_example.py +53 -0
  29. mcp_proxy_adapter/__init__.py +17 -0
  30. mcp_proxy_adapter/adapter.py +697 -0
  31. mcp_proxy_adapter/models.py +47 -0
  32. mcp_proxy_adapter/registry.py +439 -0
  33. mcp_proxy_adapter/schema.py +257 -0
  34. {mcp_proxy_adapter-2.1.0.dist-info → mcp_proxy_adapter-2.1.2.dist-info}/METADATA +2 -2
  35. mcp_proxy_adapter-2.1.2.dist-info/RECORD +61 -0
  36. mcp_proxy_adapter-2.1.2.dist-info/top_level.txt +5 -0
  37. scripts/code_analyzer/code_analyzer.py +328 -0
  38. scripts/code_analyzer/register_commands.py +446 -0
  39. scripts/publish.py +85 -0
  40. tests/conftest.py +12 -0
  41. tests/test_adapter.py +529 -0
  42. tests/test_adapter_coverage.py +274 -0
  43. tests/test_basic_dispatcher.py +169 -0
  44. tests/test_command_registry.py +328 -0
  45. tests/test_examples.py +32 -0
  46. tests/test_mcp_proxy_adapter.py +568 -0
  47. tests/test_mcp_proxy_adapter_basic.py +262 -0
  48. tests/test_part1.py +348 -0
  49. tests/test_part2.py +524 -0
  50. tests/test_schema.py +358 -0
  51. tests/test_simple_adapter.py +251 -0
  52. adapters/__init__.py +0 -16
  53. cli/__init__.py +0 -12
  54. cli/__main__.py +0 -79
  55. cli/command_runner.py +0 -233
  56. generators/__init__.py +0 -14
  57. generators/endpoint_generator.py +0 -172
  58. generators/openapi_generator.py +0 -254
  59. generators/rest_api_generator.py +0 -207
  60. mcp_proxy_adapter-2.1.0.dist-info/RECORD +0 -28
  61. mcp_proxy_adapter-2.1.0.dist-info/top_level.txt +0 -7
  62. openapi_schema/__init__.py +0 -38
  63. openapi_schema/command_registry.py +0 -312
  64. openapi_schema/rest_schema.py +0 -510
  65. openapi_schema/rpc_generator.py +0 -307
  66. openapi_schema/rpc_schema.py +0 -416
  67. validators/__init__.py +0 -14
  68. validators/base_validator.py +0 -23
  69. {analyzers → mcp_proxy_adapter/analyzers}/__init__.py +0 -0
  70. {analyzers → mcp_proxy_adapter/analyzers}/docstring_analyzer.py +0 -0
  71. {analyzers → mcp_proxy_adapter/analyzers}/type_analyzer.py +0 -0
  72. {dispatchers → mcp_proxy_adapter/dispatchers}/__init__.py +0 -0
  73. {dispatchers → mcp_proxy_adapter/dispatchers}/base_dispatcher.py +0 -0
  74. {dispatchers → mcp_proxy_adapter/dispatchers}/json_rpc_dispatcher.py +0 -0
  75. {validators → mcp_proxy_adapter/validators}/docstring_validator.py +0 -0
  76. {validators → mcp_proxy_adapter/validators}/metadata_validator.py +0 -0
  77. {mcp_proxy_adapter-2.1.0.dist-info → mcp_proxy_adapter-2.1.2.dist-info}/WHEEL +0 -0
  78. {mcp_proxy_adapter-2.1.0.dist-info → mcp_proxy_adapter-2.1.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,257 @@
1
+ """
2
+ Module for optimizing OpenAPI schema for MCP Proxy.
3
+ """
4
+ from typing import Dict, Any, List, Optional
5
+
6
+ class SchemaOptimizer:
7
+ """
8
+ OpenAPI schema optimizer for use with MCP Proxy.
9
+
10
+ This class transforms a standard OpenAPI schema into a format
11
+ more suitable for use with MCP Proxy and AI models.
12
+ """
13
+
14
+ def optimize(
15
+ self,
16
+ schema: Dict[str, Any],
17
+ cmd_endpoint: str,
18
+ commands_info: Dict[str, Dict[str, Any]]
19
+ ) -> Dict[str, Any]:
20
+ """
21
+ Optimizes OpenAPI schema for MCP Proxy.
22
+
23
+ Args:
24
+ schema: Original OpenAPI schema
25
+ cmd_endpoint: Path for universal JSON-RPC endpoint
26
+ commands_info: Information about registered commands
27
+
28
+ Returns:
29
+ Dict[str, Any]: Optimized schema
30
+ """
31
+ # Create a new schema in a format compatible with the proxy
32
+ optimized = {
33
+ "openapi": "3.0.2",
34
+ "info": {
35
+ "title": "Command Registry API",
36
+ "description": "API for executing commands through MCPProxy",
37
+ "version": "1.0.0"
38
+ },
39
+ "paths": {
40
+ cmd_endpoint: {
41
+ "post": {
42
+ "summary": "Execute command",
43
+ "description": "Universal endpoint for executing various commands",
44
+ "operationId": "execute_command",
45
+ "requestBody": {
46
+ "content": {
47
+ "application/json": {
48
+ "schema": {
49
+ "$ref": "#/components/schemas/CommandRequest"
50
+ },
51
+ "examples": {}
52
+ }
53
+ },
54
+ "required": True
55
+ },
56
+ "responses": {
57
+ "200": {
58
+ "description": "Command executed successfully",
59
+ "content": {
60
+ "application/json": {
61
+ "schema": {
62
+ "$ref": "#/components/schemas/CommandResponse"
63
+ }
64
+ }
65
+ }
66
+ },
67
+ "400": {
68
+ "description": "Error in request or during command execution",
69
+ "content": {
70
+ "application/json": {
71
+ "schema": {
72
+ "type": "object",
73
+ "properties": {
74
+ "detail": {
75
+ "type": "string",
76
+ "description": "Error description"
77
+ }
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
86
+ },
87
+ "components": {
88
+ "schemas": {
89
+ "CommandRequest": {
90
+ "title": "CommandRequest",
91
+ "description": "Command execution request",
92
+ "type": "object",
93
+ "required": ["command"],
94
+ "properties": {
95
+ "command": {
96
+ "title": "Command",
97
+ "description": "Command to execute",
98
+ "type": "string",
99
+ "enum": list(commands_info.keys())
100
+ },
101
+ "params": {
102
+ "title": "Parameters",
103
+ "description": "Command parameters, depend on command type",
104
+ "oneOf": []
105
+ }
106
+ }
107
+ },
108
+ "CommandResponse": {
109
+ "title": "CommandResponse",
110
+ "description": "Command execution response",
111
+ "type": "object",
112
+ "required": ["result"],
113
+ "properties": {
114
+ "result": {
115
+ "title": "Result",
116
+ "description": "Command execution result"
117
+ }
118
+ }
119
+ }
120
+ },
121
+ "examples": {}
122
+ }
123
+ }
124
+
125
+ # Add parameter schemas and examples for each command
126
+ for cmd_name, cmd_info in commands_info.items():
127
+ param_schema_name = f"{cmd_name.capitalize()}Params"
128
+ params = cmd_info.get("params", {})
129
+ has_params = bool(params)
130
+
131
+ # Define parameter schema (даже если params пустой, схема будет пустым объектом)
132
+ param_schema = {
133
+ "title": param_schema_name,
134
+ "description": f"Parameters for command {cmd_name}",
135
+ "type": "object",
136
+ "properties": {},
137
+ }
138
+ required_params = []
139
+ example_params = {}
140
+
141
+ for param_name, param_info in params.items():
142
+ param_property = {
143
+ "title": param_name.capitalize(),
144
+ "type": param_info.get("type", "string"),
145
+ "description": param_info.get("description", "")
146
+ }
147
+ if "default" in param_info:
148
+ param_property["default"] = param_info["default"]
149
+ if "enum" in param_info:
150
+ param_property["enum"] = param_info["enum"]
151
+ param_schema["properties"][param_name] = param_property
152
+ if param_info.get("required", False):
153
+ required_params.append(param_name)
154
+ if "example" in param_info:
155
+ example_params[param_name] = param_info["example"]
156
+ elif "default" in param_info:
157
+ example_params[param_name] = param_info["default"]
158
+ elif param_info.get("type") == "string":
159
+ example_params[param_name] = "example_value"
160
+ elif param_info.get("type") == "integer":
161
+ example_params[param_name] = 1
162
+ elif param_info.get("type") == "boolean":
163
+ example_params[param_name] = False
164
+
165
+ if required_params:
166
+ param_schema["required"] = required_params
167
+
168
+ # Добавляем схему параметров всегда, даже если она пустая
169
+ optimized["components"]["schemas"][param_schema_name] = param_schema
170
+
171
+ # Добавляем $ref на схему параметров в oneOf всегда
172
+ optimized["components"]["schemas"]["CommandRequest"]["properties"]["params"]["oneOf"].append({
173
+ "$ref": f"#/components/schemas/{param_schema_name}"
174
+ })
175
+
176
+ # Пример использования команды
177
+ example_id = f"{cmd_name}_example"
178
+ example = {
179
+ "summary": f"Example of using command {cmd_name}",
180
+ "value": {
181
+ "command": cmd_name
182
+ }
183
+ }
184
+ if has_params:
185
+ example["value"]["params"] = example_params
186
+ optimized["components"]["examples"][example_id] = example
187
+ optimized["paths"][cmd_endpoint]["post"]["requestBody"]["content"]["application/json"]["examples"][example_id] = {
188
+ "$ref": f"#/components/examples/{example_id}"
189
+ }
190
+
191
+ # Для команд без параметров добавляем type: null в oneOf
192
+ optimized_oneof = optimized["components"]["schemas"]["CommandRequest"]["properties"]["params"]["oneOf"]
193
+ optimized_oneof.append({"type": "null"})
194
+
195
+ # Add tool descriptions to schema for AI models
196
+ self._add_tool_descriptions(optimized, commands_info)
197
+ return optimized
198
+
199
+ def _add_tool_descriptions(
200
+ self,
201
+ schema: Dict[str, Any],
202
+ commands_info: Dict[str, Dict[str, Any]]
203
+ ) -> None:
204
+ """
205
+ Adds AI tool descriptions to the schema.
206
+
207
+ This method enhances the OpenAPI schema with special descriptions
208
+ for better integration with AI models and MCPProxy.
209
+
210
+ Args:
211
+ schema: OpenAPI schema to enhance
212
+ commands_info: Information about registered commands
213
+ """
214
+ # Add AI tool descriptions to x-mcp-tools
215
+ schema["x-mcp-tools"] = []
216
+
217
+ for cmd_name, cmd_info in commands_info.items():
218
+ # Create tool description
219
+ tool = {
220
+ "name": f"mcp_{cmd_name}", # Add mcp_ prefix to command name
221
+ "description": cmd_info.get("description", "") or cmd_info.get("summary", ""),
222
+ "parameters": {
223
+ "type": "object",
224
+ "properties": {},
225
+ "required": []
226
+ }
227
+ }
228
+
229
+ # Add parameters
230
+ for param_name, param_info in cmd_info.get("params", {}).items():
231
+ # Convert parameter to JSON Schema format
232
+ param_schema = {}
233
+
234
+ # Parameter type
235
+ param_schema["type"] = param_info.get("type", "string")
236
+
237
+ # Description
238
+ if "description" in param_info:
239
+ param_schema["description"] = param_info["description"]
240
+
241
+ # Default value
242
+ if "default" in param_info:
243
+ param_schema["default"] = param_info["default"]
244
+
245
+ # Possible values
246
+ if "enum" in param_info:
247
+ param_schema["enum"] = param_info["enum"]
248
+
249
+ # Add parameter to schema
250
+ tool["parameters"]["properties"][param_name] = param_schema
251
+
252
+ # If parameter is required, add to required list
253
+ if param_info.get("required", False):
254
+ tool["parameters"]["required"].append(param_name)
255
+
256
+ # Add tool to list
257
+ schema["x-mcp-tools"].append(tool)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-proxy-adapter
3
- Version: 2.1.0
3
+ Version: 2.1.2
4
4
  Summary: Adapter for exposing Command Registry commands as tools for AI models via MCP Proxy.
5
5
  Home-page: https://github.com/vasilyvz/mcp-proxy-adapter
6
6
  Author: Vasiliy VZ
@@ -21,7 +21,7 @@ Requires-Python: >=3.9, <4
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  Requires-Dist: fastapi<1.0.0,>=0.95.0
24
- Requires-Dist: pydantic<2.0.0,>=1.10.0
24
+ Requires-Dist: pydantic>=2.0.0
25
25
  Requires-Dist: uvicorn<1.0.0,>=0.22.0
26
26
  Requires-Dist: docstring-parser<1.0.0,>=0.15
27
27
  Requires-Dist: typing-extensions<5.0.0,>=4.5.0
@@ -0,0 +1,61 @@
1
+ docs/README.md,sha256=XpZqqKcf7GZ_uqtaadmIZc4dE9oyKv7biKj41kDtQo4,4633
2
+ docs/README_ru.md,sha256=Ln8oDVOurhBVurQaEFDweVvq_bZZbdFSSTvWTrpBgMs,6326
3
+ docs/architecture.md,sha256=wF5-RMQI-QssY4jA2lSKjjq6iHunda-n2PqI5ELAj84,10430
4
+ docs/architecture_ru.md,sha256=Ato3CoAmhNBukMO6uje12e5-OlpDYVrwGIcN1CKqTsw,15529
5
+ docs/command_development.md,sha256=-CKfEtLO-4tB2WKZNbIT1Ao4juiKFBcKS7As4_1ja74,6645
6
+ docs/command_development_ru.md,sha256=kzvYpnEfsXfZxZY1wswwDChzBtMtEY9l74M7XkWxtHk,19968
7
+ docs/deployment.md,sha256=eOOCq8V6Gz_SDo9hReXKXXNtaS8WmrxR-KXjJjUE1pw,4988
8
+ docs/deployment_ru.md,sha256=0eNxKqiqc-sIht4brdZVEcxZkbrUP4y95PhfpD_0Qyg,40107
9
+ docs/examples.md,sha256=85kfd73rgqJ2DUD3PrRfjViCXgv5bM_9Uqizm_ET7NE,6348
10
+ docs/examples_ru.md,sha256=mxQqwKXMX6OcY6tyfuRgmYqAQXYnrzTGOJS_u5ECUcs,11866
11
+ docs/mcp_proxy_adapter.md,sha256=Y-dcFKwYbYJe5_zDrvlDPuSz-S1czL7Gh848EzclS7k,8210
12
+ docs/mcp_proxy_adapter_ru.md,sha256=aJ1KAyoyzcRPSn7YGralpUGO-zmUQMtDKoQ2zrOUVTc,17489
13
+ docs/quickstart.md,sha256=YH0KspJcbe59ynpRf9kne-QsLGcUo4kKtNiEZ48nrus,5419
14
+ docs/quickstart_ru.md,sha256=biipkQ4_XGVzqxLeGptzfWxyr6J9TN1ggdhCf2pDWKA,11884
15
+ docs/testing.md,sha256=Z1hdqairo14QMoPxeBX1_YAbbS3Uedy4uaxfvu8aG5k,7572
16
+ docs/testing_ru.md,sha256=z2-zGIXuFx8GpLYwydP9Oe3N8l_zVzheLBYvxgI-f6k,18219
17
+ docs/validation_ru.md,sha256=iYtM3ZoFGe3_EaCG0738LkGJ5FPayrEr2z_KPhNcs0Q,15886
18
+ examples/analyze_config.py,sha256=vog7TNHDw5ZoYhQLbAvZvEoufmQwH54KJzQBJrSq5w4,4283
19
+ examples/basic_integration.py,sha256=w_oA777YiQt36gzI113KPQ6k45caXbMCqW9hD8sy8zo,4657
20
+ examples/docstring_and_schema_example.py,sha256=c96L4KF_7yWzffmvd4hyeQuXSdYyYkv7Uvuy0QxgMcQ,1929
21
+ examples/extension_example.py,sha256=vnatnFdNTapMpPcQ79Ugitk92ZiUfpLTs7Dvsodf1og,2277
22
+ examples/help_best_practices.py,sha256=wUtZRnAktnpfAc9vAvqSxUquHEr5ewaPDPyc6BoCqdQ,2637
23
+ examples/help_usage.py,sha256=UOd3HJeYlQpQkAyceGNm66jXX_h-T05pjIGD-b7-Pfg,2568
24
+ examples/mcp_proxy_client.py,sha256=z4IzFlGigVTQSb8TpcrQ_a0migsmC58LnNwc8wZmTfw,3811
25
+ examples/mcp_proxy_config.json,sha256=39aeV7NNEZstX9RAOdsr5tUZnvCuo7bR38Xc1sGGCJI,3655
26
+ examples/openapi_server.py,sha256=hueoG9KcimOc4W27euWksMYjjG8Jz-li9hEKi3ZCDBk,12515
27
+ examples/project_structure_example.py,sha256=sswTo6FZb1F5juHa0FYG3cgvrh3wfgGfJu2bBy5tCm4,1460
28
+ examples/testing_example.py,sha256=AB13c4C1bjs1145O-yriwyreeVXtMOlQLzs2BCGmprk,1719
29
+ mcp_proxy_adapter/__init__.py,sha256=_6D-TfANWp9zc550M5LUeGPvioFqG1bAl3tZj-gNmJU,463
30
+ mcp_proxy_adapter/adapter.py,sha256=76dkVeDuqLsJ5AhuftzLlwy2M6yr_PfNbmNfo9dXVhc,28844
31
+ mcp_proxy_adapter/models.py,sha256=acqVQBYAojHXeJ1MJyvpMyT6-J6aMxWuZMszn_-RsOU,2338
32
+ mcp_proxy_adapter/registry.py,sha256=jgC4TKaPbMbAsoxvGp2ToaOE4drD-VfZug7WJbm4IW4,15853
33
+ mcp_proxy_adapter/schema.py,sha256=HZM0TTQTSi8ha1TEeVevdCyGZOUPoT1soB7Nex0hV50,10947
34
+ mcp_proxy_adapter/analyzers/__init__.py,sha256=2rcYZDP-bXq078MQpxP32lAwYYyRhOwAQGBcefBfBzY,368
35
+ mcp_proxy_adapter/analyzers/docstring_analyzer.py,sha256=T3FLJEo_uChShfiEKRl8GpVoHvh5HiudZkxnj4KixfA,7541
36
+ mcp_proxy_adapter/analyzers/type_analyzer.py,sha256=6Wac7osKwF03waFSwQ8ZM0Wqn_zAP2D-I4WMEpR0hQM,5230
37
+ mcp_proxy_adapter/dispatchers/__init__.py,sha256=FWgimgInGphIjCEnvA3-ZExiapUzYAVis2H9C5IWivU,365
38
+ mcp_proxy_adapter/dispatchers/base_dispatcher.py,sha256=S5_Xri058jAmOWeit1tedB_GMZQ9RLcNcYabA83ZF6k,2288
39
+ mcp_proxy_adapter/dispatchers/json_rpc_dispatcher.py,sha256=ffu1M32E1AdC7IB44mlbV2L56eJQMsp-7fYi_r4rmHc,6331
40
+ mcp_proxy_adapter/validators/docstring_validator.py,sha256=Onpq2iNJ1qF4ejkJJIlBkLROuSNIVALHVmXIgkCpaFI,2934
41
+ mcp_proxy_adapter/validators/metadata_validator.py,sha256=uCrn38-VYYn89l6f5CC_GoTAHAweaOW2Z6Esro1rtGw,3155
42
+ mcp_proxy_adapter-2.1.2.dist-info/licenses/LICENSE,sha256=OkApFEwdgMCt_mbvUI-eIwKMSTe38K3XnU2DT5ub-wI,1072
43
+ scripts/publish.py,sha256=MMKksSAzWoZOlxxHOm9-o6juEJYj5AMS4OG5pDIeChw,2627
44
+ scripts/code_analyzer/code_analyzer.py,sha256=FmWctoxttImvIPJoyNMLyUt7pEg3mQihqt6T9vMBKVQ,12766
45
+ scripts/code_analyzer/register_commands.py,sha256=GpQxdFsA0BWHONZpDjl18zkoqlbV6pmkabLMwIR_1JE,20545
46
+ tests/conftest.py,sha256=gkusSolrEp6-AHLjMKDUVxEcmfboa2P5SbeyiMugP-E,495
47
+ tests/test_adapter.py,sha256=ohLX1wGFmHnv5DXJx2segjT2EEg9-3ZU9FKwS74D7KI,18247
48
+ tests/test_adapter_coverage.py,sha256=k5oCvT9EQy_8JYam4Os88Npnn-Hgr0pm_ChHyRwk4Vc,8813
49
+ tests/test_basic_dispatcher.py,sha256=wB30AM3UPI_EklAMrGIwQHDTqbfY80I5spi6arpdxCc,5472
50
+ tests/test_command_registry.py,sha256=Ze4qwi2fV-EOuJ0su2l-jclLRCB9ZdZNUY54GFtde9E,12000
51
+ tests/test_examples.py,sha256=dYiRgV3Z74MiOJq4I1soPE4xmJNSM8L-IqxdV4vXCWI,1615
52
+ tests/test_mcp_proxy_adapter.py,sha256=WDYkLpz9Kd4OW0XbfhSanmhca7m7DIGZNC1L6CAoNpA,24890
53
+ tests/test_mcp_proxy_adapter_basic.py,sha256=gKIcqYzZVEdJcp3PKKJPKNxtJ05l80Wj4ig1Vd2dagM,10624
54
+ tests/test_part1.py,sha256=gb7lQ-lKjqYMMLGMt_7aSuScXSqy80tP8vBq2TBSiTs,14191
55
+ tests/test_part2.py,sha256=842aTEHCwqS6UBs3sE0DRbXRdeiZ8n0w4rJTAWschnY,18220
56
+ tests/test_schema.py,sha256=-X_323kncA0l3o1GKIKYf9OjhIj1hOJBPCH71682pk8,13704
57
+ tests/test_simple_adapter.py,sha256=IFLbVphiRMl1w5LoT1AihYOt0b66GNnt8uDafwokVCI,8374
58
+ mcp_proxy_adapter-2.1.2.dist-info/METADATA,sha256=RWCuMTdFD78eQjOZ_r33iBT8ChwqBJcKu_0T4MDQr6E,8150
59
+ mcp_proxy_adapter-2.1.2.dist-info/WHEEL,sha256=wXxTzcEDnjrTwFYjLPcsW_7_XihufBwmpiBeiXNBGEA,91
60
+ mcp_proxy_adapter-2.1.2.dist-info/top_level.txt,sha256=FkpPEr9rFZcEWxGfy6kRIUkCiLMxHpTD3--73ThTnPo,46
61
+ mcp_proxy_adapter-2.1.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ docs
2
+ examples
3
+ mcp_proxy_adapter
4
+ scripts
5
+ tests
@@ -0,0 +1,328 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Code Analyzer Tool
4
+
5
+ This script performs three main functions:
6
+ 1. Lists Python files with more than 350 lines of code
7
+ 2. Creates a call graph showing which function calls which other function
8
+ 3. Creates an index of all functions with their file paths
9
+ """
10
+
11
+ import os
12
+ import ast
13
+ import sys
14
+ from pathlib import Path
15
+ import networkx as nx
16
+ import matplotlib.pyplot as plt
17
+ from collections import defaultdict
18
+
19
+ # Директории, которые следует исключить из анализа
20
+ EXCLUDED_DIRS = [
21
+ '.venv',
22
+ 'site-packages',
23
+ 'dist-packages',
24
+ '__pycache__',
25
+ 'node_modules',
26
+ '.git'
27
+ ]
28
+
29
+ def should_skip_dir(path):
30
+ """Проверяет, нужно ли пропустить директорию"""
31
+ for excluded in EXCLUDED_DIRS:
32
+ if excluded in path:
33
+ return True
34
+ return False
35
+
36
+ class FunctionCallVisitor(ast.NodeVisitor):
37
+ """AST visitor to collect function calls within each function definition"""
38
+
39
+ def __init__(self):
40
+ self.current_function = None
41
+ self.call_graph = defaultdict(set)
42
+ self.functions = {} # Map function names to their full names with class if applicable
43
+ self.current_class = None
44
+
45
+ def visit_ClassDef(self, node):
46
+ old_class = self.current_class
47
+ self.current_class = node.name
48
+ # Visit all children in the class
49
+ self.generic_visit(node)
50
+ self.current_class = old_class
51
+
52
+ def visit_FunctionDef(self, node):
53
+ # Save the current function
54
+ old_function = self.current_function
55
+
56
+ # Create fully qualified function name
57
+ if self.current_class:
58
+ self.current_function = f"{self.current_class}.{node.name}"
59
+ else:
60
+ self.current_function = node.name
61
+
62
+ # Map the simple name to the fully qualified name
63
+ self.functions[node.name] = self.current_function
64
+
65
+ # Visit all children in the function
66
+ self.generic_visit(node)
67
+
68
+ # Restore the parent function
69
+ self.current_function = old_function
70
+
71
+ def visit_AsyncFunctionDef(self, node):
72
+ # Handle async functions just like regular functions
73
+ self.visit_FunctionDef(node)
74
+
75
+ def visit_Call(self, node):
76
+ # Check if we're inside a function
77
+ if self.current_function:
78
+ func_name = None
79
+
80
+ # Get the name of the called function
81
+ if isinstance(node.func, ast.Name):
82
+ # Direct function call like "foo()"
83
+ func_name = node.func.id
84
+ elif isinstance(node.func, ast.Attribute):
85
+ # Method call like "obj.foo()"
86
+ func_name = node.func.attr
87
+
88
+ if func_name:
89
+ # Add to the call graph
90
+ self.call_graph[self.current_function].add(func_name)
91
+
92
+ # Continue visiting children
93
+ self.generic_visit(node)
94
+
95
+ def count_lines(file_path):
96
+ """Count the number of non-empty, non-comment lines in a file"""
97
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
98
+ lines = f.readlines()
99
+
100
+ # Count non-empty, non-comment lines
101
+ count = 0
102
+ for line in lines:
103
+ line = line.strip()
104
+ if line and not line.startswith('#'):
105
+ count += 1
106
+
107
+ return count
108
+
109
+ def find_large_files(root_dir, min_lines=350):
110
+ """Find Python files with more than min_lines lines of code"""
111
+ large_files = []
112
+
113
+ for root, dirs, files in os.walk(root_dir):
114
+ # Пропускаем исключенные директории
115
+ dirs[:] = [d for d in dirs if not should_skip_dir(os.path.join(root, d))]
116
+
117
+ for file in files:
118
+ if file.endswith('.py'):
119
+ file_path = os.path.join(root, file)
120
+
121
+ # Пропускаем файлы в исключенных директориях
122
+ if should_skip_dir(file_path):
123
+ continue
124
+
125
+ line_count = count_lines(file_path)
126
+
127
+ if line_count > min_lines:
128
+ relative_path = os.path.relpath(file_path, root_dir)
129
+ large_files.append((relative_path, line_count))
130
+
131
+ return large_files
132
+
133
+ def create_function_index(root_dir, output_file='function_index.txt'):
134
+ """Create an index of all functions with their file paths"""
135
+ function_index = []
136
+ modules_analyzed = 0
137
+
138
+ # Process each Python file
139
+ for root, dirs, files in os.walk(root_dir):
140
+ # Пропускаем исключенные директории
141
+ dirs[:] = [d for d in dirs if not should_skip_dir(os.path.join(root, d))]
142
+
143
+ for file in files:
144
+ if file.endswith('.py'):
145
+ file_path = os.path.join(root, file)
146
+
147
+ # Пропускаем файлы в исключенных директориях
148
+ if should_skip_dir(file_path):
149
+ continue
150
+
151
+ try:
152
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
153
+ module_content = f.read()
154
+
155
+ # Parse the file
156
+ module = ast.parse(module_content, filename=file_path)
157
+
158
+ # Extract the module name from the file path
159
+ rel_path = os.path.relpath(file_path, root_dir)
160
+ module_name = os.path.splitext(rel_path)[0].replace(os.path.sep, '.')
161
+
162
+ # Find functions and classes
163
+ for node in ast.walk(module):
164
+ if isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef):
165
+ function_name = node.name
166
+ function_index.append((function_name, rel_path))
167
+ elif isinstance(node, ast.ClassDef):
168
+ class_name = node.name
169
+ function_index.append((f"class {class_name}", rel_path))
170
+ # Find methods in the class
171
+ for class_node in node.body:
172
+ if isinstance(class_node, ast.FunctionDef) or isinstance(class_node, ast.AsyncFunctionDef):
173
+ method_name = class_node.name
174
+ function_index.append((f"{class_name}.{method_name}", rel_path))
175
+
176
+ modules_analyzed += 1
177
+
178
+ except (SyntaxError, UnicodeDecodeError) as e:
179
+ print(f"Error analyzing {file_path}: {e}")
180
+
181
+ # Save function index to file
182
+ with open(output_file, 'w', encoding='utf-8') as f:
183
+ f.write(f"# Function Index (analyzed {modules_analyzed} modules)\n\n")
184
+
185
+ for function_name, file_path in sorted(function_index, key=lambda x: x[0].lower()):
186
+ f.write(f"{function_name}: {file_path}\n")
187
+
188
+ print(f"Function index saved to {output_file}")
189
+
190
+ return function_index
191
+
192
+ def create_call_graph(root_dir, output_file='call_graph.txt'):
193
+ """Create a graph of function calls and save it to a file"""
194
+ call_graph = defaultdict(set)
195
+ modules_analyzed = 0
196
+
197
+ # Process each Python file
198
+ for root, dirs, files in os.walk(root_dir):
199
+ # Пропускаем исключенные директории
200
+ dirs[:] = [d for d in dirs if not should_skip_dir(os.path.join(root, d))]
201
+
202
+ for file in files:
203
+ if file.endswith('.py'):
204
+ file_path = os.path.join(root, file)
205
+
206
+ # Пропускаем файлы в исключенных директориях
207
+ if should_skip_dir(file_path):
208
+ continue
209
+
210
+ try:
211
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
212
+ module_content = f.read()
213
+
214
+ # Parse the file
215
+ module = ast.parse(module_content, filename=file_path)
216
+
217
+ # Extract the module name from the file path
218
+ rel_path = os.path.relpath(file_path, root_dir)
219
+ module_name = os.path.splitext(rel_path)[0].replace(os.path.sep, '.')
220
+
221
+ # Find function calls
222
+ visitor = FunctionCallVisitor()
223
+ visitor.visit(module)
224
+
225
+ # Add to the global call graph with module name prefix
226
+ for caller, callees in visitor.call_graph.items():
227
+ full_caller = f"{module_name}::{caller}"
228
+ for callee in callees:
229
+ # Try to get the fully qualified name if available
230
+ if callee in visitor.functions:
231
+ full_callee = f"{module_name}::{visitor.functions[callee]}"
232
+ else:
233
+ full_callee = callee
234
+
235
+ call_graph[full_caller].add(full_callee)
236
+
237
+ modules_analyzed += 1
238
+
239
+ except (SyntaxError, UnicodeDecodeError) as e:
240
+ print(f"Error analyzing {file_path}: {e}")
241
+
242
+ # Save call graph to file
243
+ with open(output_file, 'w', encoding='utf-8') as f:
244
+ f.write(f"# Function Call Graph (analyzed {modules_analyzed} modules)\n\n")
245
+
246
+ for caller, callees in sorted(call_graph.items()):
247
+ f.write(f"{caller}:\n")
248
+ for callee in sorted(callees):
249
+ f.write(f" - {callee}\n")
250
+ f.write("\n")
251
+
252
+ print(f"Call graph saved to {output_file}")
253
+
254
+ return call_graph
255
+
256
+ def visualize_call_graph(call_graph, output_file='call_graph.png'):
257
+ """Create a visual representation of the call graph using NetworkX"""
258
+ G = nx.DiGraph()
259
+
260
+ # Add nodes and edges
261
+ for caller, callees in call_graph.items():
262
+ G.add_node(caller)
263
+ for callee in callees:
264
+ G.add_node(callee)
265
+ G.add_edge(caller, callee)
266
+
267
+ # Check if the graph is too large to visualize effectively
268
+ if len(G.nodes) > 100:
269
+ print(f"Warning: Graph is very large ({len(G.nodes)} nodes), visualization may be cluttered")
270
+ print("Consider filtering the graph or using a specialized tool like pyan for better visualization")
271
+
272
+ try:
273
+ # Create the plot
274
+ plt.figure(figsize=(20, 20))
275
+ pos = nx.spring_layout(G, k=0.3, iterations=50)
276
+ nx.draw(G, pos, with_labels=True, node_size=100, node_color="skyblue",
277
+ font_size=8, font_weight="bold", arrows=True,
278
+ connectionstyle='arc3, rad=0.1', arrowsize=10)
279
+
280
+ # Save the figure
281
+ plt.tight_layout()
282
+ plt.savefig(output_file, dpi=300, bbox_inches='tight')
283
+ plt.close()
284
+
285
+ print(f"Call graph visualization saved to {output_file}")
286
+ except Exception as e:
287
+ print(f"Error creating visualization: {e}")
288
+ print("Text-based call graph is still available")
289
+
290
+ def main():
291
+ if len(sys.argv) > 1:
292
+ root_dir = sys.argv[1]
293
+ else:
294
+ root_dir = os.getcwd() # Default to current directory
295
+
296
+ print(f"Analyzing code in {root_dir} (excluding library files)")
297
+
298
+ # Find large files
299
+ print("\n=== Files with more than 350 lines ===")
300
+ large_files = find_large_files(root_dir)
301
+
302
+ if large_files:
303
+ with open('large_files.txt', 'w', encoding='utf-8') as f:
304
+ f.write("# Files with more than 350 lines of code\n\n")
305
+ for file_path, line_count in sorted(large_files, key=lambda x: x[1], reverse=True):
306
+ info = f"{file_path}: {line_count} lines"
307
+ print(info)
308
+ f.write(f"{info}\n")
309
+ print(f"\nList of large files saved to large_files.txt")
310
+ else:
311
+ print("No files with more than 350 lines found.")
312
+
313
+ # Create function index
314
+ print("\n=== Creating function index ===")
315
+ create_function_index(root_dir)
316
+
317
+ # Create call graph
318
+ print("\n=== Creating function call graph ===")
319
+ call_graph = create_call_graph(root_dir)
320
+
321
+ # Try to visualize the graph if matplotlib is available
322
+ try:
323
+ visualize_call_graph(call_graph)
324
+ except Exception as e:
325
+ print(f"Could not create visual graph: {e}")
326
+
327
+ if __name__ == "__main__":
328
+ main()