mcp-proxy-adapter 3.1.6__py3-none-any.whl → 4.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.
- mcp_proxy_adapter/api/app.py +65 -27
- mcp_proxy_adapter/api/handlers.py +1 -1
- mcp_proxy_adapter/api/middleware/error_handling.py +11 -10
- mcp_proxy_adapter/api/tool_integration.py +5 -2
- mcp_proxy_adapter/api/tools.py +3 -3
- mcp_proxy_adapter/commands/base.py +19 -1
- mcp_proxy_adapter/commands/command_registry.py +254 -8
- mcp_proxy_adapter/commands/hooks.py +260 -0
- mcp_proxy_adapter/commands/reload_command.py +211 -0
- mcp_proxy_adapter/commands/reload_settings_command.py +125 -0
- mcp_proxy_adapter/commands/settings_command.py +189 -0
- mcp_proxy_adapter/config.py +16 -1
- mcp_proxy_adapter/core/__init__.py +44 -0
- mcp_proxy_adapter/core/logging.py +87 -34
- mcp_proxy_adapter/core/settings.py +376 -0
- mcp_proxy_adapter/core/utils.py +2 -2
- mcp_proxy_adapter/custom_openapi.py +81 -2
- mcp_proxy_adapter/examples/README.md +124 -0
- mcp_proxy_adapter/examples/__init__.py +7 -0
- mcp_proxy_adapter/examples/basic_server/README.md +60 -0
- mcp_proxy_adapter/examples/basic_server/__init__.py +7 -0
- mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +39 -0
- mcp_proxy_adapter/examples/basic_server/config.json +35 -0
- mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +238 -0
- mcp_proxy_adapter/examples/basic_server/server.py +98 -0
- mcp_proxy_adapter/examples/custom_commands/README.md +127 -0
- mcp_proxy_adapter/examples/custom_commands/__init__.py +27 -0
- mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +250 -0
- mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +6 -0
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +103 -0
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +111 -0
- mcp_proxy_adapter/examples/custom_commands/config.json +62 -0
- mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +169 -0
- mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +215 -0
- mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +76 -0
- mcp_proxy_adapter/examples/custom_commands/custom_settings.json +96 -0
- mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +241 -0
- mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +135 -0
- mcp_proxy_adapter/examples/custom_commands/echo_command.py +122 -0
- mcp_proxy_adapter/examples/custom_commands/hooks.py +230 -0
- mcp_proxy_adapter/examples/custom_commands/intercept_command.py +123 -0
- mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +103 -0
- mcp_proxy_adapter/examples/custom_commands/server.py +223 -0
- mcp_proxy_adapter/examples/custom_commands/test_hooks.py +176 -0
- mcp_proxy_adapter/examples/deployment/README.md +49 -0
- mcp_proxy_adapter/examples/deployment/__init__.py +7 -0
- mcp_proxy_adapter/examples/deployment/config.development.json +8 -0
- {examples/basic_example → mcp_proxy_adapter/examples/deployment}/config.json +11 -7
- mcp_proxy_adapter/examples/deployment/config.production.json +12 -0
- mcp_proxy_adapter/examples/deployment/config.staging.json +11 -0
- mcp_proxy_adapter/examples/deployment/docker-compose.yml +31 -0
- mcp_proxy_adapter/examples/deployment/run.sh +43 -0
- mcp_proxy_adapter/examples/deployment/run_docker.sh +84 -0
- mcp_proxy_adapter/openapi.py +3 -2
- mcp_proxy_adapter/tests/api/test_custom_openapi.py +617 -0
- mcp_proxy_adapter/tests/api/test_handlers.py +522 -0
- mcp_proxy_adapter/tests/api/test_schemas.py +546 -0
- mcp_proxy_adapter/tests/api/test_tool_integration.py +531 -0
- mcp_proxy_adapter/tests/unit/test_base_command.py +391 -85
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.1.0.dist-info}/METADATA +3 -3
- mcp_proxy_adapter-4.1.0.dist-info/RECORD +110 -0
- {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.1.0.dist-info}/WHEEL +1 -1
- {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.1.0.dist-info}/top_level.txt +0 -1
- examples/__init__.py +0 -19
- examples/anti_patterns/README.md +0 -51
- examples/anti_patterns/__init__.py +0 -9
- examples/anti_patterns/bad_design/README.md +0 -72
- examples/anti_patterns/bad_design/global_state.py +0 -170
- examples/anti_patterns/bad_design/monolithic_command.py +0 -272
- examples/basic_example/README.md +0 -245
- examples/basic_example/__init__.py +0 -8
- examples/basic_example/commands/__init__.py +0 -5
- examples/basic_example/commands/echo_command.py +0 -95
- examples/basic_example/commands/math_command.py +0 -151
- examples/basic_example/commands/time_command.py +0 -152
- examples/basic_example/docs/EN/README.md +0 -177
- examples/basic_example/docs/RU/README.md +0 -177
- examples/basic_example/server.py +0 -151
- examples/basic_example/tests/conftest.py +0 -243
- examples/check_vstl_schema.py +0 -106
- examples/commands/echo_command.py +0 -52
- examples/commands/echo_command_di.py +0 -152
- examples/commands/echo_result.py +0 -65
- examples/commands/get_date_command.py +0 -98
- examples/commands/new_uuid4_command.py +0 -91
- examples/complete_example/Dockerfile +0 -24
- examples/complete_example/README.md +0 -92
- examples/complete_example/__init__.py +0 -8
- examples/complete_example/commands/__init__.py +0 -5
- examples/complete_example/commands/system_command.py +0 -328
- examples/complete_example/config.json +0 -41
- examples/complete_example/configs/config.dev.yaml +0 -40
- examples/complete_example/configs/config.docker.yaml +0 -40
- examples/complete_example/docker-compose.yml +0 -35
- examples/complete_example/requirements.txt +0 -20
- examples/complete_example/server.py +0 -113
- examples/di_example/.pytest_cache/README.md +0 -8
- examples/di_example/server.py +0 -249
- examples/fix_vstl_help.py +0 -123
- examples/minimal_example/README.md +0 -65
- examples/minimal_example/__init__.py +0 -8
- examples/minimal_example/config.json +0 -14
- examples/minimal_example/main.py +0 -136
- examples/minimal_example/simple_server.py +0 -163
- examples/minimal_example/tests/conftest.py +0 -171
- examples/minimal_example/tests/test_hello_command.py +0 -111
- examples/minimal_example/tests/test_integration.py +0 -181
- examples/patch_vstl_service.py +0 -105
- examples/patch_vstl_service_mcp.py +0 -108
- examples/server.py +0 -69
- examples/simple_server.py +0 -128
- examples/test_package_3.1.4.py +0 -177
- examples/test_server.py +0 -134
- examples/tool_description_example.py +0 -82
- mcp_proxy_adapter/py.typed +0 -0
- mcp_proxy_adapter-3.1.6.dist-info/RECORD +0 -118
- {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,65 +0,0 @@
|
|
1
|
-
# Минимальный пример MCP Proxy Adapter
|
2
|
-
|
3
|
-
Данный пример демонстрирует минимальную конфигурацию для запуска микросервиса с одной простой командой.
|
4
|
-
|
5
|
-
## Структура примера
|
6
|
-
|
7
|
-
```
|
8
|
-
minimal_example/
|
9
|
-
├── config.json # Файл конфигурации в JSON формате
|
10
|
-
├── README.md # Документация
|
11
|
-
├── simple_server.py # Пример сервера с одной командой
|
12
|
-
└── tests/ # Директория с интеграционными тестами
|
13
|
-
├── conftest.py # Настройка тестов
|
14
|
-
└── test_integration.py # Интеграционные тесты
|
15
|
-
```
|
16
|
-
|
17
|
-
## Запуск примера
|
18
|
-
|
19
|
-
```bash
|
20
|
-
# Перейти в директорию проекта
|
21
|
-
cd examples/minimal_example
|
22
|
-
|
23
|
-
# Запустить сервер
|
24
|
-
python simple_server.py
|
25
|
-
```
|
26
|
-
|
27
|
-
После запуска сервер будет доступен по адресу [http://localhost:8000](http://localhost:8000).
|
28
|
-
|
29
|
-
## Тестирование API
|
30
|
-
|
31
|
-
### Через веб-интерфейс
|
32
|
-
|
33
|
-
Откройте в браузере [http://localhost:8000/docs](http://localhost:8000/docs) для доступа к интерактивной документации Swagger UI.
|
34
|
-
|
35
|
-
### Через командную строку
|
36
|
-
|
37
|
-
```bash
|
38
|
-
# Вызов команды hello через JSON-RPC
|
39
|
-
curl -X POST "http://localhost:8000/api/jsonrpc" \
|
40
|
-
-H "Content-Type: application/json" \
|
41
|
-
-d '{"jsonrpc": "2.0", "method": "hello", "params": {"name": "User"}, "id": 1}'
|
42
|
-
|
43
|
-
# Вызов команды hello через упрощенный эндпоинт
|
44
|
-
curl -X POST "http://localhost:8000/cmd" \
|
45
|
-
-H "Content-Type: application/json" \
|
46
|
-
-d '{"command": "hello", "params": {"name": "User"}}'
|
47
|
-
```
|
48
|
-
|
49
|
-
### Запуск интеграционных тестов
|
50
|
-
|
51
|
-
```bash
|
52
|
-
# Запуск всех тестов
|
53
|
-
pytest tests/test_integration.py -v
|
54
|
-
|
55
|
-
# Запуск конкретного теста
|
56
|
-
pytest tests/test_integration.py::TestHelloCommandIntegration::test_jsonrpc_hello_default -v
|
57
|
-
```
|
58
|
-
|
59
|
-
## Что демонстрирует этот пример
|
60
|
-
|
61
|
-
1. Создание минимального сервиса с MCP Proxy Adapter
|
62
|
-
2. Определение простой команды с метаданными
|
63
|
-
3. Основные эндпоинты API
|
64
|
-
4. Работа с JSON-RPC
|
65
|
-
5. Тестирование API через интеграционные тесты
|
examples/minimal_example/main.py
DELETED
@@ -1,136 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Минимальный пример приложения MCP Proxy Adapter.
|
3
|
-
"""
|
4
|
-
|
5
|
-
import os
|
6
|
-
import sys
|
7
|
-
import uvicorn
|
8
|
-
import logging
|
9
|
-
import json
|
10
|
-
from pathlib import Path
|
11
|
-
|
12
|
-
from mcp_proxy_adapter import create_app, Command, SuccessResult
|
13
|
-
from mcp_proxy_adapter.commands.command_registry import registry
|
14
|
-
from mcp_proxy_adapter.core.logging import logger
|
15
|
-
from mcp_proxy_adapter.config import config
|
16
|
-
|
17
|
-
|
18
|
-
class HelloCommand(Command):
|
19
|
-
"""Простая команда, возвращающая приветствие."""
|
20
|
-
|
21
|
-
name = "hello"
|
22
|
-
|
23
|
-
async def execute(self, name: str = "World") -> SuccessResult:
|
24
|
-
"""Выполняет команду с переданным именем."""
|
25
|
-
logger.info(f"Executing hello command with name: {name}")
|
26
|
-
return SuccessResult({"message": f"Hello, {name}!"})
|
27
|
-
|
28
|
-
|
29
|
-
def ensure_config():
|
30
|
-
"""Проверяет наличие конфигурационного файла и создает его при необходимости."""
|
31
|
-
config_path = Path("config.json")
|
32
|
-
|
33
|
-
if not config_path.exists():
|
34
|
-
config_data = {
|
35
|
-
"server": {
|
36
|
-
"host": "0.0.0.0",
|
37
|
-
"port": 8000,
|
38
|
-
"debug": True,
|
39
|
-
"log_level": "INFO"
|
40
|
-
},
|
41
|
-
"logging": {
|
42
|
-
"level": "info",
|
43
|
-
"file": "logs/minimal_example.log",
|
44
|
-
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
45
|
-
},
|
46
|
-
"debug": True
|
47
|
-
}
|
48
|
-
|
49
|
-
# Создаем директорию для логов
|
50
|
-
logs_dir = Path("logs")
|
51
|
-
logs_dir.mkdir(exist_ok=True)
|
52
|
-
|
53
|
-
# Записываем конфигурацию
|
54
|
-
with open(config_path, "w") as f:
|
55
|
-
json.dump(config_data, f, indent=2)
|
56
|
-
|
57
|
-
logger.info(f"Created default configuration at {config_path}")
|
58
|
-
|
59
|
-
|
60
|
-
def setup_application():
|
61
|
-
"""
|
62
|
-
Настраивает и возвращает FastAPI приложение.
|
63
|
-
"""
|
64
|
-
# Проверяем наличие конфигурационного файла
|
65
|
-
ensure_config()
|
66
|
-
|
67
|
-
# Определяем путь к конфигурационному файлу
|
68
|
-
config_path = Path("config.json").absolute()
|
69
|
-
|
70
|
-
# Загружаем конфигурацию из файла
|
71
|
-
if config_path.exists():
|
72
|
-
config.load_from_file(str(config_path))
|
73
|
-
logger.info(f"Loaded configuration from {config_path}")
|
74
|
-
|
75
|
-
# Создаем приложение
|
76
|
-
app = create_app()
|
77
|
-
|
78
|
-
# Корректно регистрируем команду
|
79
|
-
try:
|
80
|
-
# Проверяем, есть ли уже команда в реестре
|
81
|
-
if registry.command_exists(HelloCommand.name):
|
82
|
-
# Если команда существует, удаляем ее
|
83
|
-
logger.info(f"Command '{HelloCommand.name}' already exists, re-registering it")
|
84
|
-
registry.unregister(HelloCommand.name)
|
85
|
-
|
86
|
-
# Регистрируем команду
|
87
|
-
registry.register(HelloCommand)
|
88
|
-
logger.info(f"Command '{HelloCommand.name}' successfully registered")
|
89
|
-
|
90
|
-
# Выводим список всех зарегистрированных команд
|
91
|
-
commands = registry.get_all_commands()
|
92
|
-
logger.info(f"Total registered commands: {len(commands)}")
|
93
|
-
except Exception as e:
|
94
|
-
logger.error(f"Error registering command: {e}")
|
95
|
-
|
96
|
-
return app
|
97
|
-
|
98
|
-
|
99
|
-
def main():
|
100
|
-
"""
|
101
|
-
Основная функция для запуска приложения.
|
102
|
-
"""
|
103
|
-
try:
|
104
|
-
# Настраиваем приложение
|
105
|
-
app = setup_application()
|
106
|
-
|
107
|
-
# Получаем настройки сервера из конфигурации
|
108
|
-
host = config.get("server.host", "0.0.0.0")
|
109
|
-
port = config.get("server.port", 8000)
|
110
|
-
debug = config.get("server.debug", False)
|
111
|
-
|
112
|
-
logger.info(f"Starting server on {host}:{port} (debug: {debug})")
|
113
|
-
|
114
|
-
if debug:
|
115
|
-
# В режиме отладки запускаем с перезагрузкой, используя строку импорта
|
116
|
-
uvicorn.run(
|
117
|
-
"main:setup_application",
|
118
|
-
host=host,
|
119
|
-
port=port,
|
120
|
-
reload=True,
|
121
|
-
factory=True
|
122
|
-
)
|
123
|
-
else:
|
124
|
-
# В обычном режиме запускаем экземпляр приложения
|
125
|
-
uvicorn.run(
|
126
|
-
app,
|
127
|
-
host=host,
|
128
|
-
port=port
|
129
|
-
)
|
130
|
-
except Exception as e:
|
131
|
-
logger.exception(f"Error during application startup: {e}")
|
132
|
-
sys.exit(1)
|
133
|
-
|
134
|
-
|
135
|
-
if __name__ == "__main__":
|
136
|
-
main()
|
@@ -1,163 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Минимальный пример использования MCP Proxy Adapter.
|
3
|
-
|
4
|
-
Этот пример демонстрирует минимальную конфигурацию для запуска микросервиса
|
5
|
-
с одной простой командой.
|
6
|
-
"""
|
7
|
-
|
8
|
-
import os
|
9
|
-
import uvicorn
|
10
|
-
from typing import Dict, Any, List
|
11
|
-
|
12
|
-
from mcp_proxy_adapter import Command, SuccessResult, create_app, registry
|
13
|
-
from mcp_proxy_adapter.core.logging import setup_logging
|
14
|
-
from mcp_proxy_adapter.config import config
|
15
|
-
|
16
|
-
|
17
|
-
class HelloResult(SuccessResult):
|
18
|
-
"""
|
19
|
-
Результат выполнения команды hello.
|
20
|
-
|
21
|
-
Атрибуты:
|
22
|
-
message (str): Приветственное сообщение
|
23
|
-
"""
|
24
|
-
|
25
|
-
def __init__(self, message: str):
|
26
|
-
"""
|
27
|
-
Инициализация результата команды.
|
28
|
-
|
29
|
-
Args:
|
30
|
-
message: Приветственное сообщение
|
31
|
-
"""
|
32
|
-
self.message = message
|
33
|
-
|
34
|
-
def to_dict(self) -> Dict[str, Any]:
|
35
|
-
"""
|
36
|
-
Преобразование результата в словарь.
|
37
|
-
|
38
|
-
Returns:
|
39
|
-
Словарь с результатом выполнения команды
|
40
|
-
"""
|
41
|
-
return {"message": self.message}
|
42
|
-
|
43
|
-
@classmethod
|
44
|
-
def get_schema(cls) -> Dict[str, Any]:
|
45
|
-
"""
|
46
|
-
Получение JSON-схемы для результата.
|
47
|
-
|
48
|
-
Returns:
|
49
|
-
JSON-схема результата
|
50
|
-
"""
|
51
|
-
return {
|
52
|
-
"type": "object",
|
53
|
-
"properties": {
|
54
|
-
"message": {"type": "string", "description": "Приветственное сообщение"}
|
55
|
-
},
|
56
|
-
"required": ["message"]
|
57
|
-
}
|
58
|
-
|
59
|
-
|
60
|
-
class HelloCommand(Command):
|
61
|
-
"""
|
62
|
-
Команда, возвращающая приветственное сообщение.
|
63
|
-
|
64
|
-
Эта команда принимает имя пользователя и возвращает приветственное сообщение.
|
65
|
-
"""
|
66
|
-
|
67
|
-
# Имя команды для регистрации
|
68
|
-
name = "hello"
|
69
|
-
# Класс результата команды
|
70
|
-
result_class = HelloResult
|
71
|
-
|
72
|
-
async def execute(self, name: str = "World") -> HelloResult:
|
73
|
-
"""
|
74
|
-
Выполнение команды.
|
75
|
-
|
76
|
-
Args:
|
77
|
-
name: Имя для приветствия (по умолчанию "World")
|
78
|
-
|
79
|
-
Returns:
|
80
|
-
Результат выполнения команды с приветственным сообщением
|
81
|
-
"""
|
82
|
-
return HelloResult(f"Hello, {name}!")
|
83
|
-
|
84
|
-
@classmethod
|
85
|
-
def get_schema(cls) -> Dict[str, Any]:
|
86
|
-
"""
|
87
|
-
Получение JSON-схемы для параметров команды.
|
88
|
-
|
89
|
-
Returns:
|
90
|
-
JSON-схема параметров команды
|
91
|
-
"""
|
92
|
-
return {
|
93
|
-
"type": "object",
|
94
|
-
"properties": {
|
95
|
-
"name": {
|
96
|
-
"type": "string",
|
97
|
-
"description": "Имя для приветствия"
|
98
|
-
}
|
99
|
-
},
|
100
|
-
"additionalProperties": False
|
101
|
-
}
|
102
|
-
|
103
|
-
@classmethod
|
104
|
-
def _generate_examples(cls, params: Dict[str, Dict[str, Any]]) -> List[Dict[str, Any]]:
|
105
|
-
"""
|
106
|
-
Генерирует примеры использования команды.
|
107
|
-
|
108
|
-
Args:
|
109
|
-
params: Информация о параметрах команды
|
110
|
-
|
111
|
-
Returns:
|
112
|
-
Список примеров
|
113
|
-
"""
|
114
|
-
return [
|
115
|
-
{
|
116
|
-
"command": "hello",
|
117
|
-
"description": "Приветствие по умолчанию"
|
118
|
-
},
|
119
|
-
{
|
120
|
-
"command": "hello",
|
121
|
-
"params": {"name": "User"},
|
122
|
-
"description": "Приветствие с указанным именем"
|
123
|
-
}
|
124
|
-
]
|
125
|
-
|
126
|
-
|
127
|
-
def main():
|
128
|
-
"""Запуск микросервиса."""
|
129
|
-
# Определяем путь к конфигурационному файлу
|
130
|
-
current_dir = os.path.dirname(os.path.abspath(__file__))
|
131
|
-
config_path = os.path.join(current_dir, "config.json")
|
132
|
-
|
133
|
-
# Загружаем конфигурацию, если файл существует
|
134
|
-
if os.path.exists(config_path):
|
135
|
-
config.load_from_file(config_path)
|
136
|
-
|
137
|
-
# Настраиваем логирование
|
138
|
-
setup_logging("INFO")
|
139
|
-
|
140
|
-
# Создаем приложение FastAPI
|
141
|
-
app = create_app()
|
142
|
-
app.title = "Minimal Example"
|
143
|
-
app.description = "Минимальный пример использования MCP Proxy Adapter"
|
144
|
-
app.version = "1.0.0"
|
145
|
-
|
146
|
-
# Регистрируем команду
|
147
|
-
registry.register(HelloCommand)
|
148
|
-
|
149
|
-
# Определяем порт из переменной окружения или используем 8000 по умолчанию
|
150
|
-
port = int(os.environ.get("TEST_SERVER_PORT", 8000))
|
151
|
-
|
152
|
-
# Запускаем сервер
|
153
|
-
uvicorn.run(
|
154
|
-
app,
|
155
|
-
host="0.0.0.0",
|
156
|
-
port=port,
|
157
|
-
reload=False, # Отключаем автоперезагрузку для тестов
|
158
|
-
log_level="info"
|
159
|
-
)
|
160
|
-
|
161
|
-
|
162
|
-
if __name__ == "__main__":
|
163
|
-
main()
|
@@ -1,171 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Pytest configuration and fixtures for minimal example tests.
|
3
|
-
"""
|
4
|
-
|
5
|
-
import os
|
6
|
-
import sys
|
7
|
-
import time
|
8
|
-
import socket
|
9
|
-
import asyncio
|
10
|
-
import threading
|
11
|
-
import multiprocessing
|
12
|
-
from typing import Callable, Tuple, Dict, Any
|
13
|
-
|
14
|
-
import pytest
|
15
|
-
import requests
|
16
|
-
|
17
|
-
# Add parent directory to path for imports
|
18
|
-
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
19
|
-
|
20
|
-
# Import the server module from the parent directory
|
21
|
-
import simple_server
|
22
|
-
|
23
|
-
|
24
|
-
def find_free_port() -> int:
|
25
|
-
"""
|
26
|
-
Find a free port on localhost.
|
27
|
-
|
28
|
-
Returns:
|
29
|
-
Free port number
|
30
|
-
"""
|
31
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
32
|
-
sock.bind(('localhost', 0))
|
33
|
-
return sock.getsockname()[1]
|
34
|
-
|
35
|
-
|
36
|
-
class ServerProcess:
|
37
|
-
"""Helper class to manage server process."""
|
38
|
-
|
39
|
-
def __init__(self, port: int):
|
40
|
-
"""
|
41
|
-
Initialize server process with a specified port.
|
42
|
-
|
43
|
-
Args:
|
44
|
-
port: Port number to use
|
45
|
-
"""
|
46
|
-
self.port = port
|
47
|
-
self.process = None
|
48
|
-
|
49
|
-
def start(self) -> None:
|
50
|
-
"""Start the server in a separate process."""
|
51
|
-
def run_server():
|
52
|
-
# Set the test port via environment variable
|
53
|
-
os.environ["TEST_SERVER_PORT"] = str(self.port)
|
54
|
-
simple_server.main()
|
55
|
-
|
56
|
-
self.process = multiprocessing.Process(target=run_server)
|
57
|
-
self.process.daemon = True
|
58
|
-
self.process.start()
|
59
|
-
|
60
|
-
# Wait for server to start
|
61
|
-
self._wait_for_server()
|
62
|
-
|
63
|
-
def stop(self) -> None:
|
64
|
-
"""Stop the server process."""
|
65
|
-
if self.process and self.process.is_alive():
|
66
|
-
self.process.terminate()
|
67
|
-
self.process.join(timeout=2)
|
68
|
-
|
69
|
-
def _wait_for_server(self, max_attempts: int = 10) -> None:
|
70
|
-
"""
|
71
|
-
Wait for the server to become available.
|
72
|
-
|
73
|
-
Args:
|
74
|
-
max_attempts: Maximum number of connection attempts
|
75
|
-
"""
|
76
|
-
for i in range(max_attempts):
|
77
|
-
try:
|
78
|
-
response = requests.get(f"http://localhost:{self.port}/health")
|
79
|
-
if response.status_code == 200:
|
80
|
-
return
|
81
|
-
except requests.ConnectionError:
|
82
|
-
pass
|
83
|
-
|
84
|
-
time.sleep(0.5)
|
85
|
-
|
86
|
-
raise TimeoutError(f"Server did not start within {max_attempts * 0.5} seconds")
|
87
|
-
|
88
|
-
|
89
|
-
@pytest.fixture
|
90
|
-
def server_port() -> int:
|
91
|
-
"""
|
92
|
-
Fixture that provides a free port for the test server.
|
93
|
-
|
94
|
-
Returns:
|
95
|
-
Port number
|
96
|
-
"""
|
97
|
-
return find_free_port()
|
98
|
-
|
99
|
-
|
100
|
-
@pytest.fixture
|
101
|
-
def server(server_port: int) -> ServerProcess:
|
102
|
-
"""
|
103
|
-
Fixture that provides a running server instance.
|
104
|
-
|
105
|
-
Args:
|
106
|
-
server_port: Port to run the server on
|
107
|
-
|
108
|
-
Returns:
|
109
|
-
Server process object
|
110
|
-
"""
|
111
|
-
server_process = ServerProcess(server_port)
|
112
|
-
server_process.start()
|
113
|
-
|
114
|
-
yield server_process
|
115
|
-
|
116
|
-
server_process.stop()
|
117
|
-
|
118
|
-
|
119
|
-
@pytest.fixture
|
120
|
-
def api_url(server_port: int) -> str:
|
121
|
-
"""
|
122
|
-
Fixture that provides the base API URL.
|
123
|
-
|
124
|
-
Args:
|
125
|
-
server_port: Server port
|
126
|
-
|
127
|
-
Returns:
|
128
|
-
Base API URL
|
129
|
-
"""
|
130
|
-
return f"http://localhost:{server_port}"
|
131
|
-
|
132
|
-
|
133
|
-
@pytest.fixture
|
134
|
-
def jsonrpc_client(api_url: str) -> Callable:
|
135
|
-
"""
|
136
|
-
Fixture that provides a JSON-RPC client function.
|
137
|
-
|
138
|
-
Args:
|
139
|
-
api_url: Base API URL
|
140
|
-
|
141
|
-
Returns:
|
142
|
-
Function to make JSON-RPC requests
|
143
|
-
"""
|
144
|
-
def make_request(method: str, params: Dict[str, Any], request_id: int = 1) -> Dict[str, Any]:
|
145
|
-
"""
|
146
|
-
Make a JSON-RPC request.
|
147
|
-
|
148
|
-
Args:
|
149
|
-
method: Method name
|
150
|
-
params: Method parameters
|
151
|
-
request_id: Request ID
|
152
|
-
|
153
|
-
Returns:
|
154
|
-
JSON-RPC response
|
155
|
-
"""
|
156
|
-
payload = {
|
157
|
-
"jsonrpc": "2.0",
|
158
|
-
"method": method,
|
159
|
-
"params": params,
|
160
|
-
"id": request_id
|
161
|
-
}
|
162
|
-
|
163
|
-
response = requests.post(
|
164
|
-
f"{api_url}/api/jsonrpc",
|
165
|
-
json=payload,
|
166
|
-
headers={"Content-Type": "application/json"}
|
167
|
-
)
|
168
|
-
|
169
|
-
return response.json()
|
170
|
-
|
171
|
-
return make_request
|
@@ -1,111 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Unit tests for the HelloCommand in the minimal example.
|
3
|
-
"""
|
4
|
-
|
5
|
-
import pytest
|
6
|
-
|
7
|
-
from simple_server import HelloCommand, HelloResult
|
8
|
-
|
9
|
-
|
10
|
-
class TestHelloCommand:
|
11
|
-
"""Tests for HelloCommand class."""
|
12
|
-
|
13
|
-
@pytest.mark.asyncio
|
14
|
-
async def test_execute_with_default_name(self):
|
15
|
-
"""Test HelloCommand.execute with default name parameter."""
|
16
|
-
# Create command instance
|
17
|
-
command = HelloCommand()
|
18
|
-
|
19
|
-
# Execute command with default parameter
|
20
|
-
result = await command.execute()
|
21
|
-
|
22
|
-
# Check result type
|
23
|
-
assert isinstance(result, HelloResult)
|
24
|
-
|
25
|
-
# Check message content
|
26
|
-
assert result.message == "Hello, World!"
|
27
|
-
|
28
|
-
# Check serialization
|
29
|
-
assert result.to_dict() == {"message": "Hello, World!"}
|
30
|
-
|
31
|
-
@pytest.mark.asyncio
|
32
|
-
async def test_execute_with_custom_name(self):
|
33
|
-
"""Test HelloCommand.execute with custom name parameter."""
|
34
|
-
# Create command instance
|
35
|
-
command = HelloCommand()
|
36
|
-
|
37
|
-
# Execute command with custom parameter
|
38
|
-
result = await command.execute(name="Test")
|
39
|
-
|
40
|
-
# Check result
|
41
|
-
assert isinstance(result, HelloResult)
|
42
|
-
assert result.message == "Hello, Test!"
|
43
|
-
|
44
|
-
# Check serialization
|
45
|
-
assert result.to_dict() == {"message": "Hello, Test!"}
|
46
|
-
|
47
|
-
@pytest.mark.asyncio
|
48
|
-
async def test_execute_with_empty_name(self):
|
49
|
-
"""Test HelloCommand.execute with empty name parameter."""
|
50
|
-
# Create command instance
|
51
|
-
command = HelloCommand()
|
52
|
-
|
53
|
-
# Execute command with empty parameter
|
54
|
-
result = await command.execute(name="")
|
55
|
-
|
56
|
-
# Check result
|
57
|
-
assert isinstance(result, HelloResult)
|
58
|
-
assert result.message == "Hello, !"
|
59
|
-
|
60
|
-
# Check serialization
|
61
|
-
assert result.to_dict() == {"message": "Hello, !"}
|
62
|
-
|
63
|
-
|
64
|
-
class TestHelloResult:
|
65
|
-
"""Tests for HelloResult class."""
|
66
|
-
|
67
|
-
def test_init(self):
|
68
|
-
"""Test HelloResult initialization."""
|
69
|
-
# Create result instance
|
70
|
-
result = HelloResult("Hello, Test!")
|
71
|
-
|
72
|
-
# Check attributes
|
73
|
-
assert result.message == "Hello, Test!"
|
74
|
-
|
75
|
-
def test_to_dict(self):
|
76
|
-
"""Test HelloResult.to_dict method."""
|
77
|
-
# Create result instance
|
78
|
-
result = HelloResult("Hello, Test!")
|
79
|
-
|
80
|
-
# Check serialization
|
81
|
-
assert result.to_dict() == {"message": "Hello, Test!"}
|
82
|
-
|
83
|
-
def test_get_schema(self):
|
84
|
-
"""Test HelloResult.get_schema method."""
|
85
|
-
# Get schema
|
86
|
-
schema = HelloResult.get_schema()
|
87
|
-
|
88
|
-
# Check schema structure
|
89
|
-
assert schema["type"] == "object"
|
90
|
-
assert "properties" in schema
|
91
|
-
assert "message" in schema["properties"]
|
92
|
-
assert schema["properties"]["message"]["type"] == "string"
|
93
|
-
assert "required" in schema
|
94
|
-
assert "message" in schema["required"]
|
95
|
-
|
96
|
-
|
97
|
-
class TestHelloCommandSchema:
|
98
|
-
"""Tests for HelloCommand schema."""
|
99
|
-
|
100
|
-
def test_get_schema(self):
|
101
|
-
"""Test HelloCommand.get_schema method."""
|
102
|
-
# Get schema
|
103
|
-
schema = HelloCommand.get_schema()
|
104
|
-
|
105
|
-
# Check schema structure
|
106
|
-
assert schema["type"] == "object"
|
107
|
-
assert "properties" in schema
|
108
|
-
assert "name" in schema["properties"]
|
109
|
-
assert schema["properties"]["name"]["type"] == "string"
|
110
|
-
assert "additionalProperties" in schema
|
111
|
-
assert schema["additionalProperties"] is False
|