mcp-proxy-adapter 2.0.2__py3-none-any.whl → 2.1.1__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.
- docs/README.md +172 -0
- docs/README_ru.md +172 -0
- docs/architecture.md +251 -0
- docs/architecture_ru.md +343 -0
- docs/command_development.md +250 -0
- docs/command_development_ru.md +593 -0
- docs/deployment.md +251 -0
- docs/deployment_ru.md +1298 -0
- docs/examples.md +254 -0
- docs/examples_ru.md +401 -0
- docs/mcp_proxy_adapter.md +251 -0
- docs/mcp_proxy_adapter_ru.md +405 -0
- docs/quickstart.md +251 -0
- docs/quickstart_ru.md +397 -0
- docs/testing.md +255 -0
- docs/testing_ru.md +469 -0
- docs/validation_ru.md +287 -0
- examples/analyze_config.py +141 -0
- examples/basic_integration.py +161 -0
- examples/docstring_and_schema_example.py +60 -0
- examples/extension_example.py +60 -0
- examples/help_best_practices.py +67 -0
- examples/help_usage.py +64 -0
- examples/mcp_proxy_client.py +131 -0
- examples/mcp_proxy_config.json +175 -0
- examples/openapi_server.py +369 -0
- examples/project_structure_example.py +47 -0
- examples/testing_example.py +53 -0
- mcp_proxy_adapter/__init__.py +1 -1
- mcp_proxy_adapter/models.py +19 -19
- {mcp_proxy_adapter-2.0.2.dist-info → mcp_proxy_adapter-2.1.1.dist-info}/METADATA +47 -13
- mcp_proxy_adapter-2.1.1.dist-info/RECORD +61 -0
- {mcp_proxy_adapter-2.0.2.dist-info → mcp_proxy_adapter-2.1.1.dist-info}/WHEEL +1 -1
- mcp_proxy_adapter-2.1.1.dist-info/top_level.txt +5 -0
- scripts/code_analyzer/code_analyzer.py +328 -0
- scripts/code_analyzer/register_commands.py +446 -0
- scripts/publish.py +85 -0
- tests/conftest.py +12 -0
- tests/test_adapter.py +529 -0
- tests/test_adapter_coverage.py +274 -0
- tests/test_basic_dispatcher.py +169 -0
- tests/test_command_registry.py +328 -0
- tests/test_examples.py +32 -0
- tests/test_mcp_proxy_adapter.py +568 -0
- tests/test_mcp_proxy_adapter_basic.py +262 -0
- tests/test_part1.py +348 -0
- tests/test_part2.py +524 -0
- tests/test_schema.py +358 -0
- tests/test_simple_adapter.py +251 -0
- mcp_proxy_adapter/adapters/__init__.py +0 -16
- mcp_proxy_adapter/cli/__init__.py +0 -12
- mcp_proxy_adapter/cli/__main__.py +0 -79
- mcp_proxy_adapter/cli/command_runner.py +0 -233
- mcp_proxy_adapter/generators/__init__.py +0 -14
- mcp_proxy_adapter/generators/endpoint_generator.py +0 -172
- mcp_proxy_adapter/generators/openapi_generator.py +0 -254
- mcp_proxy_adapter/generators/rest_api_generator.py +0 -207
- mcp_proxy_adapter/openapi_schema/__init__.py +0 -38
- mcp_proxy_adapter/openapi_schema/command_registry.py +0 -312
- mcp_proxy_adapter/openapi_schema/rest_schema.py +0 -510
- mcp_proxy_adapter/openapi_schema/rpc_generator.py +0 -307
- mcp_proxy_adapter/openapi_schema/rpc_schema.py +0 -416
- mcp_proxy_adapter/validators/__init__.py +0 -14
- mcp_proxy_adapter/validators/base_validator.py +0 -23
- mcp_proxy_adapter-2.0.2.dist-info/RECORD +0 -33
- mcp_proxy_adapter-2.0.2.dist-info/top_level.txt +0 -1
- {mcp_proxy_adapter-2.0.2.dist-info → mcp_proxy_adapter-2.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,593 @@
|
|
1
|
+
# Руководство по разработке команд
|
2
|
+
|
3
|
+
В этом руководстве описаны лучшие практики и рекомендации по разработке команд для системы Command Registry.
|
4
|
+
|
5
|
+
## Основные принципы
|
6
|
+
|
7
|
+
При разработке команд для Command Registry придерживайтесь следующих принципов:
|
8
|
+
|
9
|
+
1. **Единственная ответственность**: команда должна выполнять одну задачу и делать это хорошо
|
10
|
+
2. **Декларативность**: команда должна быть самодокументированной через типизацию и docstrings
|
11
|
+
3. **Независимость**: команда не должна зависеть от других команд
|
12
|
+
4. **Идемпотентность**: повторное выполнение команды с одинаковыми параметрами должно давать идентичный результат
|
13
|
+
5. **Валидация**: команда должна проверять входные данные перед выполнением
|
14
|
+
|
15
|
+
## Структура команды
|
16
|
+
|
17
|
+
Рекомендуемая структура команды:
|
18
|
+
|
19
|
+
```python
|
20
|
+
from typing import Dict, List, Optional, Any, Union, Tuple
|
21
|
+
import logging
|
22
|
+
|
23
|
+
logger = logging.getLogger(__name__)
|
24
|
+
|
25
|
+
def command_name(
|
26
|
+
required_param: str,
|
27
|
+
optional_param: Optional[int] = None,
|
28
|
+
*,
|
29
|
+
keyword_only_param: bool = False
|
30
|
+
) -> Dict[str, Any]:
|
31
|
+
"""
|
32
|
+
Краткое описание команды (одно предложение).
|
33
|
+
|
34
|
+
Подробное описание команды, объясняющее её назначение,
|
35
|
+
особенности работы и возможные побочные эффекты.
|
36
|
+
|
37
|
+
Args:
|
38
|
+
required_param: Описание обязательного параметра
|
39
|
+
optional_param: Описание необязательного параметра
|
40
|
+
keyword_only_param: Описание параметра, который можно передать только по имени
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
Описание возвращаемого значения
|
44
|
+
|
45
|
+
Raises:
|
46
|
+
ValueError: Когда возникает ошибка валидации
|
47
|
+
RuntimeError: Когда возникает ошибка выполнения
|
48
|
+
|
49
|
+
Examples:
|
50
|
+
>>> command_name("value", 42, keyword_only_param=True)
|
51
|
+
{'status': 'success', 'result': 'value_processed'}
|
52
|
+
"""
|
53
|
+
# Логирование начала выполнения
|
54
|
+
logger.debug(f"Executing command_name with params: {required_param}, {optional_param}, {keyword_only_param}")
|
55
|
+
|
56
|
+
# Валидация параметров
|
57
|
+
if not required_param:
|
58
|
+
raise ValueError("required_param cannot be empty")
|
59
|
+
|
60
|
+
if optional_param is not None and optional_param < 0:
|
61
|
+
raise ValueError("optional_param must be non-negative")
|
62
|
+
|
63
|
+
# Выполнение основной логики
|
64
|
+
try:
|
65
|
+
# Основная логика команды
|
66
|
+
result = process_data(required_param, optional_param, keyword_only_param)
|
67
|
+
|
68
|
+
# Логирование успешного выполнения
|
69
|
+
logger.info(f"Command command_name executed successfully with result: {result}")
|
70
|
+
|
71
|
+
return {
|
72
|
+
"status": "success",
|
73
|
+
"result": result
|
74
|
+
}
|
75
|
+
except Exception as e:
|
76
|
+
# Логирование ошибки
|
77
|
+
logger.error(f"Error executing command command_name: {str(e)}", exc_info=True)
|
78
|
+
|
79
|
+
# Проброс исключения для обработки выше
|
80
|
+
raise RuntimeError(f"Failed to execute command: {str(e)}") from e
|
81
|
+
```
|
82
|
+
|
83
|
+
## Лучшие практики
|
84
|
+
|
85
|
+
### Типизация
|
86
|
+
|
87
|
+
1. **Используйте типизацию для всех параметров и возвращаемого значения**:
|
88
|
+
|
89
|
+
```python
|
90
|
+
def process_data(data: List[Dict[str, Any]], limit: Optional[int] = None) -> Tuple[List[Dict[str, Any]], int]:
|
91
|
+
# ...
|
92
|
+
```
|
93
|
+
|
94
|
+
2. **Используйте сложные типы из модуля `typing`**:
|
95
|
+
|
96
|
+
```python
|
97
|
+
from typing import Dict, List, Optional, Union, Callable, TypeVar, Generic
|
98
|
+
|
99
|
+
T = TypeVar('T')
|
100
|
+
|
101
|
+
def filter_items(
|
102
|
+
items: List[T],
|
103
|
+
predicate: Callable[[T], bool]
|
104
|
+
) -> List[T]:
|
105
|
+
# ...
|
106
|
+
```
|
107
|
+
|
108
|
+
3. **Определяйте свои типы для сложных структур**:
|
109
|
+
|
110
|
+
```python
|
111
|
+
from typing import Dict, List, TypedDict, Optional
|
112
|
+
|
113
|
+
class UserData(TypedDict):
|
114
|
+
id: str
|
115
|
+
name: str
|
116
|
+
email: str
|
117
|
+
roles: List[str]
|
118
|
+
profile: Optional[Dict[str, str]]
|
119
|
+
|
120
|
+
def get_user(user_id: str) -> UserData:
|
121
|
+
# ...
|
122
|
+
```
|
123
|
+
|
124
|
+
### Документация
|
125
|
+
|
126
|
+
1. **Используйте docstrings для описания команды, её параметров и возвращаемого значения**:
|
127
|
+
|
128
|
+
```python
|
129
|
+
def calculate_total(prices: List[float], discount: float = 0.0) -> float:
|
130
|
+
"""
|
131
|
+
Рассчитывает общую стоимость с учетом скидки.
|
132
|
+
|
133
|
+
Args:
|
134
|
+
prices: Список цен товаров
|
135
|
+
discount: Скидка в процентах (от 0.0 до 1.0)
|
136
|
+
|
137
|
+
Returns:
|
138
|
+
Общая стоимость со скидкой
|
139
|
+
|
140
|
+
Raises:
|
141
|
+
ValueError: Если скидка не в диапазоне [0, 1]
|
142
|
+
"""
|
143
|
+
```
|
144
|
+
|
145
|
+
2. **Добавляйте примеры использования в docstring**:
|
146
|
+
|
147
|
+
```python
|
148
|
+
def calculate_total(prices: List[float], discount: float = 0.0) -> float:
|
149
|
+
"""
|
150
|
+
...
|
151
|
+
|
152
|
+
Examples:
|
153
|
+
>>> calculate_total([10.0, 20.0, 30.0])
|
154
|
+
60.0
|
155
|
+
>>> calculate_total([10.0, 20.0, 30.0], discount=0.1)
|
156
|
+
54.0
|
157
|
+
"""
|
158
|
+
```
|
159
|
+
|
160
|
+
### Валидация параметров
|
161
|
+
|
162
|
+
1. **Проверяйте корректность параметров в начале функции**:
|
163
|
+
|
164
|
+
```python
|
165
|
+
def update_user(user_id: str, name: Optional[str] = None, email: Optional[str] = None) -> Dict[str, Any]:
|
166
|
+
"""Обновляет информацию о пользователе."""
|
167
|
+
if not user_id:
|
168
|
+
raise ValueError("user_id cannot be empty")
|
169
|
+
|
170
|
+
if email is not None and "@" not in email:
|
171
|
+
raise ValueError("Invalid email format")
|
172
|
+
|
173
|
+
# Основная логика...
|
174
|
+
```
|
175
|
+
|
176
|
+
2. **Используйте библиотеки для валидации сложных данных**:
|
177
|
+
|
178
|
+
```python
|
179
|
+
from pydantic import BaseModel, EmailStr, validator
|
180
|
+
|
181
|
+
class UserUpdateParams(BaseModel):
|
182
|
+
user_id: str
|
183
|
+
name: Optional[str] = None
|
184
|
+
email: Optional[EmailStr] = None
|
185
|
+
|
186
|
+
@validator('user_id')
|
187
|
+
def user_id_must_not_be_empty(cls, v):
|
188
|
+
if not v:
|
189
|
+
raise ValueError('user_id cannot be empty')
|
190
|
+
return v
|
191
|
+
|
192
|
+
def update_user(params: UserUpdateParams) -> Dict[str, Any]:
|
193
|
+
# Pydantic уже выполнил валидацию
|
194
|
+
# Основная логика...
|
195
|
+
```
|
196
|
+
|
197
|
+
### Обработка ошибок
|
198
|
+
|
199
|
+
1. **Используйте специфичные типы исключений**:
|
200
|
+
|
201
|
+
```python
|
202
|
+
def get_user(user_id: str) -> Dict[str, Any]:
|
203
|
+
if not user_id:
|
204
|
+
raise ValueError("user_id cannot be empty")
|
205
|
+
|
206
|
+
user = db.find_user(user_id)
|
207
|
+
if user is None:
|
208
|
+
raise UserNotFoundError(f"User with id {user_id} not found")
|
209
|
+
|
210
|
+
return user
|
211
|
+
```
|
212
|
+
|
213
|
+
2. **Сохраняйте контекст ошибки с помощью цепочки исключений**:
|
214
|
+
|
215
|
+
```python
|
216
|
+
def process_order(order_id: str) -> Dict[str, Any]:
|
217
|
+
try:
|
218
|
+
order = get_order(order_id)
|
219
|
+
# Обработка заказа...
|
220
|
+
except OrderNotFoundError as e:
|
221
|
+
raise ProcessingError(f"Failed to process order {order_id}") from e
|
222
|
+
except Exception as e:
|
223
|
+
raise ProcessingError(f"Unexpected error while processing order {order_id}") from e
|
224
|
+
```
|
225
|
+
|
226
|
+
### Логирование
|
227
|
+
|
228
|
+
1. **Используйте логирование для отслеживания хода выполнения команды**:
|
229
|
+
|
230
|
+
```python
|
231
|
+
import logging
|
232
|
+
|
233
|
+
logger = logging.getLogger(__name__)
|
234
|
+
|
235
|
+
def complex_operation(data: Dict[str, Any]) -> Dict[str, Any]:
|
236
|
+
logger.info(f"Starting complex operation with data: {data}")
|
237
|
+
|
238
|
+
try:
|
239
|
+
# Шаг 1
|
240
|
+
logger.debug("Performing step 1")
|
241
|
+
result_1 = step_1(data)
|
242
|
+
|
243
|
+
# Шаг 2
|
244
|
+
logger.debug("Performing step 2")
|
245
|
+
result_2 = step_2(result_1)
|
246
|
+
|
247
|
+
# Финальный шаг
|
248
|
+
logger.debug("Performing final step")
|
249
|
+
final_result = final_step(result_2)
|
250
|
+
|
251
|
+
logger.info(f"Complex operation completed successfully with result: {final_result}")
|
252
|
+
return final_result
|
253
|
+
except Exception as e:
|
254
|
+
logger.error(f"Error during complex operation: {str(e)}", exc_info=True)
|
255
|
+
raise
|
256
|
+
```
|
257
|
+
|
258
|
+
### Тестирование команд
|
259
|
+
|
260
|
+
1. **Пишите юнит-тесты для команд**:
|
261
|
+
|
262
|
+
```python
|
263
|
+
import pytest
|
264
|
+
from myapp.commands import calculate_total
|
265
|
+
|
266
|
+
def test_calculate_total_basic():
|
267
|
+
result = calculate_total([10.0, 20.0, 30.0])
|
268
|
+
assert result == 60.0
|
269
|
+
|
270
|
+
def test_calculate_total_with_discount():
|
271
|
+
result = calculate_total([10.0, 20.0, 30.0], discount=0.1)
|
272
|
+
assert result == 54.0
|
273
|
+
|
274
|
+
def test_calculate_total_empty_list():
|
275
|
+
result = calculate_total([])
|
276
|
+
assert result == 0.0
|
277
|
+
|
278
|
+
def test_calculate_total_invalid_discount():
|
279
|
+
with pytest.raises(ValueError):
|
280
|
+
calculate_total([10.0, 20.0], discount=1.5)
|
281
|
+
```
|
282
|
+
|
283
|
+
2. **Используйте параметризированные тесты**:
|
284
|
+
|
285
|
+
```python
|
286
|
+
import pytest
|
287
|
+
from myapp.commands import calculate_total
|
288
|
+
|
289
|
+
@pytest.mark.parametrize("prices, discount, expected", [
|
290
|
+
([10.0, 20.0, 30.0], 0.0, 60.0),
|
291
|
+
([10.0, 20.0, 30.0], 0.1, 54.0),
|
292
|
+
([10.0], 0.5, 5.0),
|
293
|
+
([], 0.0, 0.0),
|
294
|
+
])
|
295
|
+
def test_calculate_total(prices, discount, expected):
|
296
|
+
result = calculate_total(prices, discount)
|
297
|
+
assert result == expected
|
298
|
+
|
299
|
+
@pytest.mark.parametrize("invalid_discount", [-0.1, 1.1, 2.0])
|
300
|
+
def test_calculate_total_invalid_discount(invalid_discount):
|
301
|
+
with pytest.raises(ValueError):
|
302
|
+
calculate_total([10.0], discount=invalid_discount)
|
303
|
+
```
|
304
|
+
|
305
|
+
## Организация команд
|
306
|
+
|
307
|
+
### Группировка по модулям
|
308
|
+
|
309
|
+
Для больших проектов рекомендуется группировать команды по функциональности:
|
310
|
+
|
311
|
+
```
|
312
|
+
commands/
|
313
|
+
__init__.py
|
314
|
+
users/
|
315
|
+
__init__.py
|
316
|
+
create.py
|
317
|
+
update.py
|
318
|
+
delete.py
|
319
|
+
orders/
|
320
|
+
__init__.py
|
321
|
+
create.py
|
322
|
+
process.py
|
323
|
+
cancel.py
|
324
|
+
products/
|
325
|
+
__init__.py
|
326
|
+
...
|
327
|
+
```
|
328
|
+
|
329
|
+
В файле `commands/__init__.py` можно экспортировать все команды:
|
330
|
+
|
331
|
+
```python
|
332
|
+
# commands/__init__.py
|
333
|
+
from .users.create import create_user
|
334
|
+
from .users.update import update_user
|
335
|
+
from .users.delete import delete_user
|
336
|
+
from .orders.create import create_order
|
337
|
+
from .orders.process import process_order
|
338
|
+
from .orders.cancel import cancel_order
|
339
|
+
# ...
|
340
|
+
|
341
|
+
__all__ = [
|
342
|
+
'create_user',
|
343
|
+
'update_user',
|
344
|
+
'delete_user',
|
345
|
+
'create_order',
|
346
|
+
'process_order',
|
347
|
+
'cancel_order',
|
348
|
+
# ...
|
349
|
+
]
|
350
|
+
```
|
351
|
+
|
352
|
+
### Регистрация команд
|
353
|
+
|
354
|
+
При большом количестве команд рекомендуется использовать автоматическую регистрацию:
|
355
|
+
|
356
|
+
```python
|
357
|
+
from command_registry import CommandRegistry
|
358
|
+
from command_registry.dispatchers import CommandDispatcher
|
359
|
+
import commands
|
360
|
+
|
361
|
+
dispatcher = CommandDispatcher()
|
362
|
+
registry = CommandRegistry(dispatcher)
|
363
|
+
|
364
|
+
# Регистрация всех команд из модуля
|
365
|
+
registry.scan_module(commands, recursive=True)
|
366
|
+
```
|
367
|
+
|
368
|
+
## Примеры команд
|
369
|
+
|
370
|
+
### Простая команда
|
371
|
+
|
372
|
+
```python
|
373
|
+
def add_numbers(a: int, b: int) -> int:
|
374
|
+
"""
|
375
|
+
Складывает два числа.
|
376
|
+
|
377
|
+
Args:
|
378
|
+
a: Первое число
|
379
|
+
b: Второе число
|
380
|
+
|
381
|
+
Returns:
|
382
|
+
Сумма чисел
|
383
|
+
"""
|
384
|
+
return a + b
|
385
|
+
```
|
386
|
+
|
387
|
+
### Команда с валидацией
|
388
|
+
|
389
|
+
```python
|
390
|
+
def calculate_discount(price: float, percentage: float) -> float:
|
391
|
+
"""
|
392
|
+
Рассчитывает сумму скидки.
|
393
|
+
|
394
|
+
Args:
|
395
|
+
price: Исходная цена (должна быть положительной)
|
396
|
+
percentage: Процент скидки (от 0 до 100)
|
397
|
+
|
398
|
+
Returns:
|
399
|
+
Сумма скидки
|
400
|
+
|
401
|
+
Raises:
|
402
|
+
ValueError: Если цена отрицательная или процент скидки вне диапазона [0, 100]
|
403
|
+
"""
|
404
|
+
if price < 0:
|
405
|
+
raise ValueError("Price cannot be negative")
|
406
|
+
|
407
|
+
if not 0 <= percentage <= 100:
|
408
|
+
raise ValueError("Percentage must be between 0 and 100")
|
409
|
+
|
410
|
+
return price * (percentage / 100)
|
411
|
+
```
|
412
|
+
|
413
|
+
### Команда с внешними зависимостями
|
414
|
+
|
415
|
+
```python
|
416
|
+
import logging
|
417
|
+
from typing import Dict, Any, Optional
|
418
|
+
from database import get_database_connection
|
419
|
+
|
420
|
+
logger = logging.getLogger(__name__)
|
421
|
+
|
422
|
+
def get_user_profile(user_id: str, include_private: bool = False) -> Dict[str, Any]:
|
423
|
+
"""
|
424
|
+
Получает профиль пользователя из базы данных.
|
425
|
+
|
426
|
+
Args:
|
427
|
+
user_id: Идентификатор пользователя
|
428
|
+
include_private: Включать ли приватные данные
|
429
|
+
|
430
|
+
Returns:
|
431
|
+
Профиль пользователя
|
432
|
+
|
433
|
+
Raises:
|
434
|
+
ValueError: Если идентификатор пользователя пустой
|
435
|
+
UserNotFoundError: Если пользователь не найден
|
436
|
+
DatabaseError: При ошибке работы с базой данных
|
437
|
+
"""
|
438
|
+
if not user_id:
|
439
|
+
raise ValueError("user_id cannot be empty")
|
440
|
+
|
441
|
+
logger.info(f"Retrieving user profile for user_id={user_id}")
|
442
|
+
|
443
|
+
try:
|
444
|
+
db = get_database_connection()
|
445
|
+
user = db.users.find_one({"_id": user_id})
|
446
|
+
|
447
|
+
if user is None:
|
448
|
+
raise UserNotFoundError(f"User with id {user_id} not found")
|
449
|
+
|
450
|
+
# Формирование результата
|
451
|
+
profile = {
|
452
|
+
"id": user["_id"],
|
453
|
+
"username": user["username"],
|
454
|
+
"name": user["name"],
|
455
|
+
"created_at": user["created_at"].isoformat()
|
456
|
+
}
|
457
|
+
|
458
|
+
# Добавление приватных данных, если требуется
|
459
|
+
if include_private:
|
460
|
+
profile.update({
|
461
|
+
"email": user["email"],
|
462
|
+
"phone": user["phone"],
|
463
|
+
"last_login": user.get("last_login", {}).get("timestamp", "").isoformat()
|
464
|
+
})
|
465
|
+
|
466
|
+
logger.info(f"Successfully retrieved user profile for user_id={user_id}")
|
467
|
+
return profile
|
468
|
+
|
469
|
+
except UserNotFoundError:
|
470
|
+
logger.warning(f"User with id {user_id} not found")
|
471
|
+
raise
|
472
|
+
except Exception as e:
|
473
|
+
logger.error(f"Error retrieving user profile for user_id={user_id}: {str(e)}", exc_info=True)
|
474
|
+
raise DatabaseError(f"Failed to retrieve user profile: {str(e)}") from e
|
475
|
+
```
|
476
|
+
|
477
|
+
### Асинхронная команда
|
478
|
+
|
479
|
+
```python
|
480
|
+
import logging
|
481
|
+
import asyncio
|
482
|
+
from typing import Dict, Any, List
|
483
|
+
from database import get_async_database_connection
|
484
|
+
|
485
|
+
logger = logging.getLogger(__name__)
|
486
|
+
|
487
|
+
async def search_products(
|
488
|
+
query: str,
|
489
|
+
categories: Optional[List[str]] = None,
|
490
|
+
min_price: Optional[float] = None,
|
491
|
+
max_price: Optional[float] = None,
|
492
|
+
limit: int = 20,
|
493
|
+
offset: int = 0
|
494
|
+
) -> Dict[str, Any]:
|
495
|
+
"""
|
496
|
+
Выполняет поиск товаров по различным критериям.
|
497
|
+
|
498
|
+
Args:
|
499
|
+
query: Поисковый запрос
|
500
|
+
categories: Список категорий для фильтрации
|
501
|
+
min_price: Минимальная цена
|
502
|
+
max_price: Максимальная цена
|
503
|
+
limit: Максимальное количество результатов
|
504
|
+
offset: Смещение результатов для пагинации
|
505
|
+
|
506
|
+
Returns:
|
507
|
+
Словарь с результатами поиска и метаданными
|
508
|
+
"""
|
509
|
+
logger.info(f"Searching products with query: {query}, categories: {categories}, "
|
510
|
+
f"price range: {min_price}-{max_price}, limit: {limit}, offset: {offset}")
|
511
|
+
|
512
|
+
# Формирование фильтра
|
513
|
+
filter_query = {"$text": {"$search": query}}
|
514
|
+
|
515
|
+
if categories:
|
516
|
+
filter_query["category"] = {"$in": categories}
|
517
|
+
|
518
|
+
price_filter = {}
|
519
|
+
if min_price is not None:
|
520
|
+
price_filter["$gte"] = min_price
|
521
|
+
if max_price is not None:
|
522
|
+
price_filter["$lte"] = max_price
|
523
|
+
|
524
|
+
if price_filter:
|
525
|
+
filter_query["price"] = price_filter
|
526
|
+
|
527
|
+
try:
|
528
|
+
# Получение соединения с базой данных
|
529
|
+
db = await get_async_database_connection()
|
530
|
+
|
531
|
+
# Выполнение запроса
|
532
|
+
cursor = db.products.find(filter_query).sort("relevance", -1).skip(offset).limit(limit)
|
533
|
+
products = await cursor.to_list(length=limit)
|
534
|
+
|
535
|
+
# Получение общего количества результатов
|
536
|
+
total_count = await db.products.count_documents(filter_query)
|
537
|
+
|
538
|
+
# Формирование результата
|
539
|
+
result = {
|
540
|
+
"items": products,
|
541
|
+
"total": total_count,
|
542
|
+
"limit": limit,
|
543
|
+
"offset": offset,
|
544
|
+
"has_more": offset + len(products) < total_count
|
545
|
+
}
|
546
|
+
|
547
|
+
logger.info(f"Found {total_count} products for query: {query}")
|
548
|
+
return result
|
549
|
+
|
550
|
+
except Exception as e:
|
551
|
+
logger.error(f"Error searching products: {str(e)}", exc_info=True)
|
552
|
+
raise SearchError(f"Failed to search products: {str(e)}") from e
|
553
|
+
```
|
554
|
+
|
555
|
+
## Рекомендации по именованию команд
|
556
|
+
|
557
|
+
1. **Используйте глаголы для обозначения действий**:
|
558
|
+
- `create_user`
|
559
|
+
- `update_profile`
|
560
|
+
- `delete_item`
|
561
|
+
- `process_payment`
|
562
|
+
|
563
|
+
2. **Будьте консистентны в именовании**:
|
564
|
+
- `get_user`, `get_order`, `get_product` (не `fetch_user`, `retrieve_order`)
|
565
|
+
- `create_user`, `create_order`, `create_product` (не `add_user`, `new_order`)
|
566
|
+
|
567
|
+
3. **Избегайте сокращений и аббревиатур**:
|
568
|
+
- `calculate_total` вместо `calc_total`
|
569
|
+
- `verify_email` вместо `ver_email`
|
570
|
+
|
571
|
+
4. **Используйте snake_case для имен команд**:
|
572
|
+
- `process_payment` вместо `processPayment`
|
573
|
+
- `update_user_profile` вместо `updateUserProfile`
|
574
|
+
|
575
|
+
5. **Включайте в название объект, над которым выполняется действие**:
|
576
|
+
- `create_user` вместо просто `create`
|
577
|
+
- `send_notification` вместо просто `send`
|
578
|
+
|
579
|
+
## Совместимость команд
|
580
|
+
|
581
|
+
При разработке и изменении команд соблюдайте правила совместимости:
|
582
|
+
|
583
|
+
1. **Не меняйте тип возвращаемого значения**
|
584
|
+
2. **Не удаляйте существующие параметры**
|
585
|
+
3. **Добавляйте только необязательные параметры**
|
586
|
+
4. **Не меняйте семантику параметров**
|
587
|
+
5. **Не изменяйте логику работы команды без изменения её названия**
|
588
|
+
|
589
|
+
Если требуется несовместимое изменение:
|
590
|
+
|
591
|
+
1. Создайте новую команду с другим именем
|
592
|
+
2. Пометьте старую команду как устаревшую (deprecated)
|
593
|
+
3. В документации укажите рекомендацию по миграции
|