devlake-mcp 0.4.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.
devlake_mcp/client.py ADDED
@@ -0,0 +1,474 @@
1
+ """
2
+ DevLake API 客户端
3
+
4
+ 提供与 DevLake REST API 交互的基础功能。
5
+
6
+ 改进:
7
+ - 使用 requests 库替代 urllib(更简洁、更强大)
8
+ - 完善的错误处理(针对不同HTTP状态码和业务错误)
9
+ - 完整的类型注解
10
+ - 自动连接池管理
11
+
12
+ 错误处理:
13
+ - HTTP 层错误:网络连接、超时、HTTP 4xx/5xx
14
+ - 业务层错误:HTTP 200 但 success=false
15
+ """
16
+
17
+ import requests
18
+ from typing import Any, Dict, List, Optional
19
+ from requests.exceptions import RequestException, Timeout, ConnectionError as ReqConnectionError
20
+
21
+ from .config import DevLakeConfig
22
+
23
+
24
+ # ============================================================================
25
+ # 异常类定义
26
+ # ============================================================================
27
+
28
+ class DevLakeAPIError(Exception):
29
+ """DevLake API 错误基类"""
30
+ pass
31
+
32
+
33
+ class DevLakeConnectionError(DevLakeAPIError):
34
+ """连接错误(网络不可达、DNS解析失败等)"""
35
+ pass
36
+
37
+
38
+ class DevLakeTimeoutError(DevLakeAPIError):
39
+ """请求超时错误"""
40
+ pass
41
+
42
+
43
+ class DevLakeAuthError(DevLakeAPIError):
44
+ """认证失败错误(401/403)"""
45
+ pass
46
+
47
+
48
+ class DevLakeNotFoundError(DevLakeAPIError):
49
+ """资源不存在错误(404)"""
50
+ pass
51
+
52
+
53
+ class DevLakeValidationError(DevLakeAPIError):
54
+ """请求参数验证错误(400)"""
55
+ pass
56
+
57
+
58
+ class DevLakeServerError(DevLakeAPIError):
59
+ """服务器错误(5xx)"""
60
+ pass
61
+
62
+
63
+ class DevLakeBusinessError(DevLakeAPIError):
64
+ """业务逻辑错误(HTTP 200 但业务状态异常)"""
65
+ def __init__(self, message: str, code: Optional[int] = None, response_data: Optional[Dict[str, Any]] = None):
66
+ super().__init__(message)
67
+ self.code = code
68
+ self.response_data = response_data
69
+
70
+
71
+ class DevLakeClient:
72
+ """
73
+ DevLake API 客户端
74
+
75
+ 提供与 DevLake REST API 交互的基础功能,包括:
76
+ - GET/POST/PUT/PATCH/DELETE 请求
77
+ - 自动错误处理(HTTP层 + 业务层)
78
+ - 响应解析
79
+ - 自动连接池管理
80
+
81
+ 改进:
82
+ - 使用 requests.Session 实现连接池复用
83
+ - 更精细的错误分类和处理(区分HTTP错误和业务错误)
84
+ - 完整的类型注解
85
+
86
+ 错误层次:
87
+ 1. HTTP 层错误 (4xx/5xx): 网络传输层面的错误
88
+ - DevLakeConnectionError: 连接失败
89
+ - DevLakeTimeoutError: 请求超时
90
+ - DevLakeAuthError: 认证失败 (401/403)
91
+ - DevLakeNotFoundError: 资源不存在 (404)
92
+ - DevLakeValidationError: 参数错误 (400)
93
+ - DevLakeServerError: 服务器错误 (5xx)
94
+
95
+ 2. 业务层错误 (HTTP 200 但 success=false): 应用逻辑层面的错误
96
+ - DevLakeBusinessError: 业务逻辑错误,如字段缺失、数据验证失败等
97
+ """
98
+
99
+ def __init__(self, config: Optional[DevLakeConfig] = None):
100
+ """
101
+ 初始化客户端
102
+
103
+ Args:
104
+ config: DevLake 配置,如果为 None 则从环境变量加载
105
+
106
+ 注意:
107
+ 推荐使用 context manager 来确保资源正确释放:
108
+
109
+ with DevLakeClient() as client:
110
+ client.post('/api/sessions', data)
111
+ """
112
+ self.config = config or DevLakeConfig.from_env()
113
+
114
+ # 创建 requests Session(连接池复用)
115
+ self.session = requests.Session()
116
+ self.session.headers.update(self.config.get_headers())
117
+ self._closed = False
118
+
119
+ def _make_request(
120
+ self,
121
+ method: str,
122
+ path: str,
123
+ data: Optional[Dict[str, Any]] = None,
124
+ params: Optional[Dict[str, Any]] = None
125
+ ) -> Dict[str, Any]:
126
+ """
127
+ 发起 HTTP 请求(使用 requests 库)
128
+
129
+ Args:
130
+ method: HTTP 方法(GET, POST, PUT, PATCH, DELETE)
131
+ path: API 路径(如 /api/connections)
132
+ data: 请求体数据(自动转换为JSON)
133
+ params: URL 查询参数
134
+
135
+ Returns:
136
+ Dict[str, Any]: 响应 JSON 数据
137
+
138
+ Raises:
139
+ DevLakeConnectionError: 连接失败
140
+ DevLakeTimeoutError: 请求超时
141
+ DevLakeAuthError: 认证失败
142
+ DevLakeNotFoundError: 资源不存在
143
+ DevLakeValidationError: 验证失败
144
+ DevLakeServerError: 服务器错误
145
+ DevLakeBusinessError: 业务逻辑错误(HTTP 200 但业务状态异常)
146
+ DevLakeAPIError: 其他 API 错误
147
+ """
148
+ # 构建完整 URL
149
+ url = f"{self.config.base_url}{path}"
150
+
151
+ try:
152
+ # 发起请求
153
+ response = self.session.request(
154
+ method=method,
155
+ url=url,
156
+ json=data, # requests 会自动序列化为 JSON
157
+ params=params,
158
+ timeout=self.config.timeout,
159
+ verify=self.config.verify_ssl
160
+ )
161
+
162
+ # 检查 HTTP 状态码
163
+ response.raise_for_status()
164
+
165
+ # 解析响应
166
+ if not response.text:
167
+ return {}
168
+
169
+ result = response.json()
170
+
171
+ # 检查业务层错误(HTTP 200 但业务状态异常)
172
+ # DevLake API 统一响应格式: {"success": true/false, "error": "...", "data": {...}}
173
+ if isinstance(result, dict) and result.get('success') is False:
174
+ error_msg = result.get('error', '业务逻辑错误')
175
+ error_code = result.get('code') # 可选的错误码
176
+ raise DevLakeBusinessError(
177
+ f"业务错误: {error_msg}",
178
+ code=error_code,
179
+ response_data=result
180
+ )
181
+
182
+ return result
183
+
184
+ except Timeout as e:
185
+ raise DevLakeTimeoutError(
186
+ f"请求超时({self.config.timeout}秒): {url}"
187
+ ) from e
188
+
189
+ except ReqConnectionError as e:
190
+ raise DevLakeConnectionError(
191
+ f"连接失败: {url}\n原因: {str(e)}"
192
+ ) from e
193
+
194
+ except requests.HTTPError as e:
195
+ # 获取错误详情
196
+ error_body = ""
197
+ try:
198
+ error_body = e.response.text if e.response else ""
199
+ except Exception:
200
+ error_body = "<无法读取错误详情>"
201
+
202
+ # 优先从 response 对象获取状态码
203
+ status_code = e.response.status_code if e.response else 0
204
+
205
+ # 如果 response 为 None,尝试从错误消息中提取状态码
206
+ # 例如: "400 Client Error: BAD REQUEST"
207
+ if status_code == 0:
208
+ import re
209
+ error_str = str(e)
210
+ match = re.match(r'^(\d{3})\s', error_str)
211
+ if match:
212
+ status_code = int(match.group(1))
213
+
214
+ # 根据状态码抛出不同的异常
215
+ if status_code == 0:
216
+ # 没有收到响应(response 为 None),说明请求根本没发出去或者被中断
217
+ raise DevLakeAPIError(
218
+ f"HTTP 请求失败,未收到响应\n"
219
+ f"URL: {url}\n"
220
+ f"方法: {method}\n"
221
+ f"原始错误: {str(e)}"
222
+ ) from e
223
+ elif status_code == 400:
224
+ # 添加请求数据到错误信息中,帮助调试
225
+ import json
226
+ request_data = json.dumps(data, ensure_ascii=False, indent=2) if data else "无"
227
+ raise DevLakeValidationError(
228
+ f"请求参数错误(400 Bad Request)\n"
229
+ f"URL: {url}\n"
230
+ f"请求数据: {request_data}\n"
231
+ f"服务器响应: {error_body}"
232
+ ) from e
233
+ elif status_code == 401:
234
+ raise DevLakeAuthError(
235
+ f"认证失败,请检查 API Token"
236
+ ) from e
237
+ elif status_code == 403:
238
+ raise DevLakeAuthError(
239
+ f"权限不足,无法访问资源: {path}"
240
+ ) from e
241
+ elif status_code == 404:
242
+ raise DevLakeNotFoundError(
243
+ f"资源不存在: {path}"
244
+ ) from e
245
+ elif status_code >= 500:
246
+ raise DevLakeServerError(
247
+ f"服务器错误({status_code}): {error_body}"
248
+ ) from e
249
+ else:
250
+ raise DevLakeAPIError(
251
+ f"API错误({status_code}): {error_body}"
252
+ ) from e
253
+
254
+ except RequestException as e:
255
+ raise DevLakeAPIError(
256
+ f"请求失败: {str(e)}"
257
+ ) from e
258
+
259
+ def close(self):
260
+ """
261
+ 关闭 session 释放连接池资源
262
+
263
+ 在不使用 context manager 时,建议手动调用此方法释放资源。
264
+ """
265
+ if not self._closed:
266
+ self.session.close()
267
+ self._closed = True
268
+
269
+ def __enter__(self):
270
+ """Context manager 进入"""
271
+ return self
272
+
273
+ def __exit__(self, exc_type, exc_val, exc_tb):
274
+ """Context manager 退出,确保资源释放"""
275
+ self.close()
276
+ return False
277
+
278
+ def __del__(self):
279
+ """析构函数,确保资源最终被释放"""
280
+ try:
281
+ self.close()
282
+ except Exception:
283
+ # 析构时忽略所有异常
284
+ pass
285
+
286
+ def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
287
+ """
288
+ 发起 GET 请求
289
+
290
+ Args:
291
+ path: API 路径
292
+ params: URL 查询参数
293
+
294
+ Returns:
295
+ Dict[str, Any]: 响应数据
296
+ """
297
+ return self._make_request("GET", path, params=params)
298
+
299
+ def post(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]:
300
+ """
301
+ 发起 POST 请求
302
+
303
+ Args:
304
+ path: API 路径
305
+ data: 请求体数据
306
+
307
+ Returns:
308
+ Dict[str, Any]: 响应数据
309
+ """
310
+ return self._make_request("POST", path, data=data)
311
+
312
+ def put(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]:
313
+ """
314
+ 发起 PUT 请求
315
+
316
+ Args:
317
+ path: API 路径
318
+ data: 请求体数据
319
+
320
+ Returns:
321
+ Dict[str, Any]: 响应数据
322
+ """
323
+ return self._make_request("PUT", path, data=data)
324
+
325
+ def patch(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]:
326
+ """
327
+ 发起 PATCH 请求
328
+
329
+ Args:
330
+ path: API 路径
331
+ data: 请求体数据
332
+
333
+ Returns:
334
+ Dict[str, Any]: 响应数据
335
+ """
336
+ return self._make_request("PATCH", path, data=data)
337
+
338
+ def delete(self, path: str) -> Dict[str, Any]:
339
+ """
340
+ 发起 DELETE 请求
341
+
342
+ Args:
343
+ path: API 路径
344
+
345
+ Returns:
346
+ Dict[str, Any]: 响应数据
347
+ """
348
+ return self._make_request("DELETE", path)
349
+
350
+ def health_check(self) -> Dict[str, Any]:
351
+ """
352
+ 健康检查
353
+
354
+ Returns:
355
+ Dict[str, Any]: 健康状态信息
356
+ """
357
+ try:
358
+ # DevLake 的健康检查端点
359
+ response = self.get("/api/ping")
360
+ return {
361
+ "status": "healthy",
362
+ "message": "DevLake API is accessible",
363
+ "base_url": self.config.base_url,
364
+ "response": response
365
+ }
366
+ except Exception as e:
367
+ return {
368
+ "status": "unhealthy",
369
+ "message": str(e),
370
+ "base_url": self.config.base_url
371
+ }
372
+
373
+ # ========================================================================
374
+ # AI Coding API 便捷方法(用于 Hooks)
375
+ # ========================================================================
376
+
377
+ def create_session(self, session_data: Dict[str, Any]) -> Dict[str, Any]:
378
+ """
379
+ 创建 AI 编码会话
380
+
381
+ Args:
382
+ session_data: 会话数据(包含 session_id, user_name, git_repo_path 等)
383
+
384
+ Returns:
385
+ Dict[str, Any]: 创建的会话数据
386
+ """
387
+ return self.post("/api/ai-coding/sessions", session_data)
388
+
389
+ def update_session(self, session_id: str, update_data: Dict[str, Any]) -> Dict[str, Any]:
390
+ """
391
+ 更新 AI 编码会话
392
+
393
+ Args:
394
+ session_id: 会话 ID
395
+ update_data: 更新数据
396
+
397
+ Returns:
398
+ Dict[str, Any]: 更新后的会话数据
399
+ """
400
+ return self.patch(f"/api/ai-coding/sessions/{session_id}", update_data)
401
+
402
+ def increment_session_rounds(self, session_id: str) -> Dict[str, Any]:
403
+ """
404
+ 增加会话的对话轮数(conversation_rounds)
405
+
406
+ Args:
407
+ session_id: 会话 ID
408
+
409
+ Returns:
410
+ Dict[str, Any]: 更新后的会话数据
411
+ """
412
+ return self.patch(f"/api/ai-coding/sessions/{session_id}/increment-rounds", {})
413
+
414
+
415
+ def create_prompt(self, prompt_data: Dict[str, Any]) -> Dict[str, Any]:
416
+ """
417
+ 创建 Prompt 记录
418
+
419
+ Args:
420
+ prompt_data: Prompt 数据(包含 session_id, prompt_uuid, prompt_content 等)
421
+
422
+ Returns:
423
+ Dict[str, Any]: 创建的 Prompt 数据
424
+ """
425
+ return self.post("/api/ai-coding/prompts", prompt_data)
426
+
427
+ def get_prompt(self, prompt_uuid: str) -> Dict[str, Any]:
428
+ """
429
+ 获取 Prompt 记录
430
+
431
+ Args:
432
+ prompt_uuid: Prompt UUID(使用 generation_id)
433
+
434
+ Returns:
435
+ Dict[str, Any]: Prompt 数据
436
+ """
437
+ return self.get(f"/api/ai-coding/prompts/{prompt_uuid}")
438
+
439
+ def update_prompt(self, prompt_uuid: str, update_data: Dict[str, Any]) -> Dict[str, Any]:
440
+ """
441
+ 更新 Prompt 记录
442
+
443
+ Args:
444
+ prompt_uuid: Prompt UUID(使用 generation_id)
445
+ update_data: 更新数据(如 response_content, status, loop_count 等)
446
+
447
+ Returns:
448
+ Dict[str, Any]: 更新后的 Prompt 数据
449
+ """
450
+ return self.patch(f"/api/ai-coding/prompts/{prompt_uuid}", update_data)
451
+
452
+ def create_file_changes(self, changes: List[Dict[str, Any]]) -> Dict[str, Any]:
453
+ """
454
+ 批量创建文件变更记录
455
+
456
+ Args:
457
+ changes: 文件变更列表
458
+
459
+ Returns:
460
+ Dict[str, Any]: 创建结果
461
+ """
462
+ return self.post("/api/ai-coding/file-changes", {"changes": changes})
463
+
464
+ def create_transcript(self, transcript_data: Dict[str, Any]) -> Dict[str, Any]:
465
+ """
466
+ 创建 Transcript 记录
467
+
468
+ Args:
469
+ transcript_data: Transcript 数据(包含 session_id, transcript_content 等)
470
+
471
+ Returns:
472
+ Dict[str, Any]: 创建的 Transcript 数据
473
+ """
474
+ return self.post("/api/ai-coding/transcripts", transcript_data)
devlake_mcp/compat.py ADDED
@@ -0,0 +1,165 @@
1
+ """
2
+ 兼容性检测模块
3
+
4
+ 提供 Python 版本检测和功能可用性检查,实现渐进式功能支持:
5
+ - Python 3.9:仅支持 Hooks 模式
6
+ - Python 3.10+:完整支持(Hooks + MCP Server)
7
+ """
8
+
9
+ import sys
10
+ import warnings
11
+ from typing import Optional, Any
12
+
13
+ # 检测 Python 版本
14
+ PYTHON_VERSION = sys.version_info
15
+ HAS_MCP_SUPPORT = PYTHON_VERSION >= (3, 10)
16
+
17
+ # 尝试导入 fastmcp(仅在 Python 3.10+ 时)
18
+ MCP_AVAILABLE = False
19
+ FastMCP: Optional[Any] = None
20
+
21
+ if HAS_MCP_SUPPORT:
22
+ try:
23
+ from fastmcp import FastMCP
24
+ MCP_AVAILABLE = True
25
+ except ImportError:
26
+ MCP_AVAILABLE = False
27
+ warnings.warn(
28
+ "fastmcp 未安装。MCP 功能已禁用。\n"
29
+ "安装方式: pip install 'devlake-mcp[mcp]'",
30
+ ImportWarning,
31
+ stacklevel=2
32
+ )
33
+
34
+
35
+ def get_version_info() -> dict:
36
+ """
37
+ 获取版本和功能支持信息
38
+
39
+ Returns:
40
+ dict: {
41
+ "python_version": "3.10.19",
42
+ "python_version_tuple": (3, 10, 19),
43
+ "mcp_supported": True, # Python 版本是否支持 MCP
44
+ "mcp_available": True, # fastmcp 是否已安装
45
+ "fastmcp_version": "2.13.0.2", # fastmcp 版本(如果已安装)
46
+ "features": {
47
+ "hooks": True, # Hooks 模式(所有版本都支持)
48
+ "mcp_server": True # MCP Server 模式
49
+ },
50
+ "recommended_action": "..." # 推荐操作
51
+ }
52
+ """
53
+ python_version_str = f"{PYTHON_VERSION.major}.{PYTHON_VERSION.minor}.{PYTHON_VERSION.micro}"
54
+
55
+ # 获取 fastmcp 版本(如果可用)
56
+ fastmcp_version = None
57
+ if MCP_AVAILABLE:
58
+ try:
59
+ import fastmcp
60
+ fastmcp_version = getattr(fastmcp, '__version__', 'unknown')
61
+ except Exception:
62
+ pass
63
+
64
+ # 判断推荐操作
65
+ if MCP_AVAILABLE:
66
+ recommended_action = "✓ 所有功能可用"
67
+ elif not HAS_MCP_SUPPORT:
68
+ recommended_action = "升级到 Python 3.10+ 以使用 MCP 功能"
69
+ else:
70
+ recommended_action = "安装完整功能: pip install 'devlake-mcp[mcp]'"
71
+
72
+ return {
73
+ "python_version": python_version_str,
74
+ "python_version_tuple": (PYTHON_VERSION.major, PYTHON_VERSION.minor, PYTHON_VERSION.micro),
75
+ "mcp_supported": HAS_MCP_SUPPORT,
76
+ "mcp_available": MCP_AVAILABLE,
77
+ "fastmcp_version": fastmcp_version,
78
+ "features": {
79
+ "hooks": True, # Hooks 模式所有版本都支持
80
+ "mcp_server": MCP_AVAILABLE, # MCP Server 需要 fastmcp
81
+ },
82
+ "recommended_action": recommended_action
83
+ }
84
+
85
+
86
+ def check_mcp_available() -> bool:
87
+ """
88
+ 检查 MCP 功能是否可用
89
+
90
+ Returns:
91
+ bool: MCP 功能是否可用(fastmcp 已安装且 Python >= 3.10)
92
+ """
93
+ return MCP_AVAILABLE
94
+
95
+
96
+ def get_compatibility_warnings() -> list[str]:
97
+ """
98
+ 获取兼容性警告信息
99
+
100
+ Returns:
101
+ list[str]: 警告信息列表
102
+ """
103
+ warnings_list = []
104
+
105
+ if not HAS_MCP_SUPPORT:
106
+ warnings_list.append(
107
+ f"⚠ Python {PYTHON_VERSION.major}.{PYTHON_VERSION.minor} detected. "
108
+ f"MCP 功能需要 Python 3.10+"
109
+ )
110
+ warnings_list.append(
111
+ "ℹ Hooks 模式可用于 Python 3.9"
112
+ )
113
+
114
+ elif not MCP_AVAILABLE:
115
+ warnings_list.append(
116
+ "⚠ fastmcp 未安装。MCP Server 功能不可用。"
117
+ )
118
+ warnings_list.append(
119
+ "ℹ 安装完整功能: pip install 'devlake-mcp[mcp]'"
120
+ )
121
+
122
+ return warnings_list
123
+
124
+
125
+ def print_compatibility_info(verbose: bool = False):
126
+ """
127
+ 打印兼容性信息
128
+
129
+ Args:
130
+ verbose: 是否显示详细信息
131
+ """
132
+ info = get_version_info()
133
+
134
+ print("=" * 60)
135
+ print("DevLake MCP - 版本信息")
136
+ print("=" * 60)
137
+ print(f"Python 版本: {info['python_version']}")
138
+
139
+ if info['mcp_available']:
140
+ print(f"✓ MCP Server: 已启用 (FastMCP {info['fastmcp_version']})")
141
+ elif info['mcp_supported']:
142
+ print("✗ MCP Server: 未安装 (需要 fastmcp)")
143
+ else:
144
+ print("✗ MCP Server: 不支持 (需要 Python 3.10+)")
145
+
146
+ print(f"✓ Hooks 模式: 可用")
147
+ print()
148
+
149
+ # 显示警告
150
+ warnings_list = get_compatibility_warnings()
151
+ if warnings_list:
152
+ print("注意事项:")
153
+ for warning in warnings_list:
154
+ print(f" {warning}")
155
+ print()
156
+
157
+ # 显示推荐操作
158
+ print(f"建议: {info['recommended_action']}")
159
+ print("=" * 60)
160
+
161
+ if verbose:
162
+ print("\n功能详情:")
163
+ print(f" - Hooks 模式: {'✓' if info['features']['hooks'] else '✗'}")
164
+ print(f" - MCP Server: {'✓' if info['features']['mcp_server'] else '✗'}")
165
+ print()