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/__init__.py +7 -0
- devlake_mcp/__main__.py +10 -0
- devlake_mcp/cli.py +794 -0
- devlake_mcp/client.py +474 -0
- devlake_mcp/compat.py +165 -0
- devlake_mcp/config.py +204 -0
- devlake_mcp/constants.py +161 -0
- devlake_mcp/enums.py +58 -0
- devlake_mcp/generation_manager.py +296 -0
- devlake_mcp/git_utils.py +489 -0
- devlake_mcp/hooks/__init__.py +49 -0
- devlake_mcp/hooks/hook_utils.py +246 -0
- devlake_mcp/hooks/post_tool_use.py +325 -0
- devlake_mcp/hooks/pre_tool_use.py +110 -0
- devlake_mcp/hooks/record_session.py +183 -0
- devlake_mcp/hooks/session_start.py +81 -0
- devlake_mcp/hooks/stop.py +275 -0
- devlake_mcp/hooks/transcript_utils.py +547 -0
- devlake_mcp/hooks/user_prompt_submit.py +204 -0
- devlake_mcp/logging_config.py +202 -0
- devlake_mcp/retry_queue.py +556 -0
- devlake_mcp/server.py +664 -0
- devlake_mcp/session_manager.py +444 -0
- devlake_mcp/utils.py +225 -0
- devlake_mcp/version_utils.py +174 -0
- devlake_mcp-0.4.1.dist-info/METADATA +541 -0
- devlake_mcp-0.4.1.dist-info/RECORD +31 -0
- devlake_mcp-0.4.1.dist-info/WHEEL +5 -0
- devlake_mcp-0.4.1.dist-info/entry_points.txt +3 -0
- devlake_mcp-0.4.1.dist-info/licenses/LICENSE +21 -0
- devlake_mcp-0.4.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
用户提示词提交时记录会话信息(UserPromptSubmit Hook)
|
|
5
|
+
|
|
6
|
+
触发时机: 用户点击发送按钮后、发起后端请求之前
|
|
7
|
+
|
|
8
|
+
Claude Code 输入格式:
|
|
9
|
+
{
|
|
10
|
+
"session_id": "abc123",
|
|
11
|
+
"transcript_path": "/Users/.../.claude/projects/.../xxx.jsonl",
|
|
12
|
+
"cwd": "/Users/...",
|
|
13
|
+
"permission_mode": "default",
|
|
14
|
+
"hook_event_name": "UserPromptSubmit",
|
|
15
|
+
"prompt": "Write a function to calculate the factorial of a number"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
功能:
|
|
19
|
+
1. 调用 session_manager.check_and_switch_session() 自动处理会话生命周期
|
|
20
|
+
- 首次会话:创建 session
|
|
21
|
+
- 会话延续:什么都不做
|
|
22
|
+
- 会话切换:结束旧的,创建新的
|
|
23
|
+
2. 上传用户的 prompt 内容(记录用户输入)
|
|
24
|
+
3. 静默退出,不阻塞用户操作
|
|
25
|
+
|
|
26
|
+
数据流:
|
|
27
|
+
- Session: 由 session_manager 自动管理
|
|
28
|
+
- Prompt: 每次用户输入 → POST /api/ai-coding/prompts
|
|
29
|
+
|
|
30
|
+
注意:
|
|
31
|
+
- 所有会话管理逻辑已集中到 session_manager 模块
|
|
32
|
+
- API 调用使用 try-except 确保不阻塞用户
|
|
33
|
+
- 异步执行,立即返回
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import sys
|
|
37
|
+
import os
|
|
38
|
+
import json
|
|
39
|
+
import logging
|
|
40
|
+
from datetime import datetime
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
# 导入公共工具
|
|
44
|
+
from devlake_mcp.hooks.hook_utils import run_async
|
|
45
|
+
from devlake_mcp.client import DevLakeClient
|
|
46
|
+
from devlake_mcp.git_utils import get_git_info, get_git_repo_path
|
|
47
|
+
from devlake_mcp.retry_queue import save_failed_upload
|
|
48
|
+
from devlake_mcp.session_manager import check_and_switch_session
|
|
49
|
+
from devlake_mcp.generation_manager import start_generation
|
|
50
|
+
from devlake_mcp.logging_config import configure_logging, get_log_dir
|
|
51
|
+
from devlake_mcp.constants import HOOK_LOG_DIR
|
|
52
|
+
from devlake_mcp.enums import IDEType
|
|
53
|
+
|
|
54
|
+
# 配置日志(启动时调用一次)
|
|
55
|
+
configure_logging(log_dir=get_log_dir(HOOK_LOG_DIR), log_file='user_prompt_submit.log')
|
|
56
|
+
logger = logging.getLogger(__name__)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def upload_prompt(
|
|
60
|
+
session_id: str,
|
|
61
|
+
prompt_content: str,
|
|
62
|
+
cwd: str,
|
|
63
|
+
transcript_path: str = None,
|
|
64
|
+
permission_mode: str = 'default'
|
|
65
|
+
):
|
|
66
|
+
"""
|
|
67
|
+
上传 Prompt 记录到 DevLake API
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
session_id: Session ID
|
|
71
|
+
prompt_content: 用户输入的 prompt 文本
|
|
72
|
+
cwd: 当前工作目录
|
|
73
|
+
transcript_path: 转录文件路径(可选)
|
|
74
|
+
"""
|
|
75
|
+
prompt_data = None # 初始化,确保 except 块可访问
|
|
76
|
+
try:
|
|
77
|
+
# 1. 获取 Git 信息(动态 + 静态)
|
|
78
|
+
git_info = get_git_info(cwd, timeout=1, include_user_info=True)
|
|
79
|
+
git_author = git_info.get('git_author', 'unknown')
|
|
80
|
+
|
|
81
|
+
# 2. 获取 Git 仓库路径
|
|
82
|
+
git_repo_path = get_git_repo_path(cwd)
|
|
83
|
+
|
|
84
|
+
# 3. 从 git_repo_path 提取 project_name
|
|
85
|
+
project_name = git_repo_path.split('/')[-1] if '/' in git_repo_path else git_repo_path
|
|
86
|
+
|
|
87
|
+
# 4. 生成 prompt_uuid(使用 generation_id)
|
|
88
|
+
prompt_uuid = start_generation(session_id, ide_type=IDEType.CLAUDE_CODE)
|
|
89
|
+
logger.debug(f'生成 generation_id: {prompt_uuid}')
|
|
90
|
+
|
|
91
|
+
# 5. 获取 prompt_sequence(必填字段)
|
|
92
|
+
with DevLakeClient() as client:
|
|
93
|
+
# 先获取下一个序号
|
|
94
|
+
next_seq_response = client.get('/api/ai-coding/prompts/next-sequence', params={'session_id': session_id})
|
|
95
|
+
prompt_sequence = next_seq_response.get('next_sequence', 1)
|
|
96
|
+
logger.debug(f'获取 prompt_sequence: {prompt_sequence}')
|
|
97
|
+
|
|
98
|
+
# 6. 构造 prompt 数据
|
|
99
|
+
prompt_data = {
|
|
100
|
+
'session_id': session_id,
|
|
101
|
+
'prompt_uuid': prompt_uuid,
|
|
102
|
+
'prompt_sequence': prompt_sequence, # 必填字段
|
|
103
|
+
'prompt_content': prompt_content,
|
|
104
|
+
'prompt_submit_time': datetime.now().isoformat(), # API 使用 prompt_submit_time
|
|
105
|
+
'cwd': cwd, # 当前工作目录
|
|
106
|
+
'permission_mode': permission_mode # 权限模式
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# 添加 transcript_path(如果有)
|
|
110
|
+
if transcript_path:
|
|
111
|
+
prompt_data['transcript_path'] = transcript_path
|
|
112
|
+
|
|
113
|
+
logger.info(f'准备上传 Prompt: {session_id}, prompt_uuid: {prompt_uuid}, sequence: {prompt_sequence}, content: {prompt_content[:50]}...')
|
|
114
|
+
|
|
115
|
+
# 7. 调用 DevLake API 创建 prompt
|
|
116
|
+
with DevLakeClient() as client:
|
|
117
|
+
client.create_prompt(prompt_data)
|
|
118
|
+
|
|
119
|
+
logger.info(f'成功上传 Prompt: {prompt_uuid}')
|
|
120
|
+
|
|
121
|
+
except Exception as e:
|
|
122
|
+
# API 调用失败,记录错误但不阻塞
|
|
123
|
+
logger.error(
|
|
124
|
+
f'上传 Prompt 失败 ({session_id}): '
|
|
125
|
+
f'异常类型={type(e).__name__}, '
|
|
126
|
+
f'错误信息={str(e)}',
|
|
127
|
+
exc_info=True # 记录完整堆栈信息
|
|
128
|
+
)
|
|
129
|
+
# 保存到本地队列(支持自动重试)
|
|
130
|
+
if prompt_data:
|
|
131
|
+
save_failed_upload(
|
|
132
|
+
queue_type='prompt',
|
|
133
|
+
data=prompt_data,
|
|
134
|
+
error=str(e)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@run_async
|
|
139
|
+
def main():
|
|
140
|
+
"""
|
|
141
|
+
UserPromptSubmit Hook 主逻辑
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
# 1. 从 stdin 读取 hook 输入
|
|
145
|
+
input_data = json.load(sys.stdin)
|
|
146
|
+
|
|
147
|
+
# 2. 获取关键字段
|
|
148
|
+
session_id = input_data.get('session_id')
|
|
149
|
+
prompt_content = input_data.get('prompt', '')
|
|
150
|
+
transcript_path = input_data.get('transcript_path')
|
|
151
|
+
permission_mode = input_data.get('permission_mode', 'default')
|
|
152
|
+
|
|
153
|
+
# 注意:如果 cwd 是空字符串,也应该使用 os.getcwd()
|
|
154
|
+
raw_cwd = input_data.get('cwd')
|
|
155
|
+
logger.debug(f'input_data 中的 cwd 原始值: {repr(raw_cwd)}')
|
|
156
|
+
|
|
157
|
+
cwd = raw_cwd or os.getcwd()
|
|
158
|
+
logger.debug(f'最终使用的 cwd: {cwd}')
|
|
159
|
+
|
|
160
|
+
if not session_id:
|
|
161
|
+
logger.error('未获取到 session_id,跳过处理')
|
|
162
|
+
sys.exit(0)
|
|
163
|
+
return # 确保退出(测试时 sys.exit 被 mock)
|
|
164
|
+
|
|
165
|
+
if not prompt_content:
|
|
166
|
+
logger.debug('未获取到 prompt 内容,跳过上传')
|
|
167
|
+
sys.exit(0)
|
|
168
|
+
return # 确保退出(测试时 sys.exit 被 mock)
|
|
169
|
+
|
|
170
|
+
logger.debug(f'UserPromptSubmit 触发 - session_id: {session_id}, prompt: {prompt_content[:50]}...')
|
|
171
|
+
|
|
172
|
+
# 3. 会话管理(自动处理首次会话、会话切换、会话延续)
|
|
173
|
+
try:
|
|
174
|
+
check_and_switch_session(
|
|
175
|
+
new_session_id=session_id,
|
|
176
|
+
cwd=cwd,
|
|
177
|
+
ide_type=IDEType.CLAUDE_CODE
|
|
178
|
+
)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.error(f'会话管理失败: {e}')
|
|
181
|
+
|
|
182
|
+
# 4. 上传 prompt(记录用户输入)
|
|
183
|
+
try:
|
|
184
|
+
upload_prompt(
|
|
185
|
+
session_id=session_id,
|
|
186
|
+
prompt_content=prompt_content,
|
|
187
|
+
cwd=cwd,
|
|
188
|
+
transcript_path=transcript_path,
|
|
189
|
+
permission_mode=permission_mode
|
|
190
|
+
)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.error(f'上传 prompt 失败: {e}')
|
|
193
|
+
|
|
194
|
+
# 成功,正常退出
|
|
195
|
+
sys.exit(0)
|
|
196
|
+
|
|
197
|
+
except Exception as e:
|
|
198
|
+
# 任何异常都静默失败(不阻塞用户)
|
|
199
|
+
logger.error(f'UserPromptSubmit Hook 执行失败: {e}', exc_info=True)
|
|
200
|
+
sys.exit(0)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == '__main__':
|
|
204
|
+
main()
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
日志配置模块(简化版)
|
|
5
|
+
|
|
6
|
+
提供符合 Python logging 最佳实践的配置函数。
|
|
7
|
+
|
|
8
|
+
使用方法:
|
|
9
|
+
# 在应用/hook 启动时调用一次
|
|
10
|
+
from devlake_mcp.logging_config import configure_logging, get_log_dir
|
|
11
|
+
configure_logging(log_dir=get_log_dir('.claude/logs'), log_file='hook.log')
|
|
12
|
+
|
|
13
|
+
# 之后各个模块直接用标准方式
|
|
14
|
+
import logging
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
logger.info('message')
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import logging
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
from .constants import VALID_LOG_LEVELS, DEFAULT_LOG_LEVEL
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_log_dir(default_dir: str) -> str:
|
|
28
|
+
"""
|
|
29
|
+
获取日志目录路径
|
|
30
|
+
|
|
31
|
+
根据配置文件的位置和内容,自动选择项目目录或全局目录。
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
default_dir: 默认日志目录(相对于项目根目录),如 '.claude/logs' 或 '.cursor/logs'
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
str: 日志目录路径
|
|
38
|
+
|
|
39
|
+
优先级逻辑:
|
|
40
|
+
1. 优先检查项目配置(./.claude/settings.json 或 ./.cursor/hooks.json)
|
|
41
|
+
- Claude Code: 检查 settings.json 中是否有 "hooks" 配置
|
|
42
|
+
- Cursor: 检查 hooks.json 是否存在且有效
|
|
43
|
+
2. 如果项目配置存在且有效,使用项目日志目录
|
|
44
|
+
3. 否则检查全局配置(~/.claude/settings.json 或 ~/.cursor/hooks.json)
|
|
45
|
+
4. 如果全局配置存在且有效,使用全局日志目录
|
|
46
|
+
5. 最后使用项目日志目录作为默认值
|
|
47
|
+
"""
|
|
48
|
+
import json
|
|
49
|
+
|
|
50
|
+
home = Path.home()
|
|
51
|
+
cwd = Path.cwd()
|
|
52
|
+
|
|
53
|
+
# 根据 default_dir 判断是 Claude Code 还是 Cursor
|
|
54
|
+
if '.claude' in default_dir:
|
|
55
|
+
project_config = cwd / ".claude" / "settings.json"
|
|
56
|
+
global_config = home / ".claude" / "settings.json"
|
|
57
|
+
project_log_dir = str(cwd / ".claude" / "logs")
|
|
58
|
+
global_log_dir = str(home / ".claude" / "logs")
|
|
59
|
+
|
|
60
|
+
# 1. 优先检查项目配置
|
|
61
|
+
if project_config.exists():
|
|
62
|
+
try:
|
|
63
|
+
with open(project_config, 'r', encoding='utf-8') as f:
|
|
64
|
+
config = json.load(f)
|
|
65
|
+
# 检查是否有 hooks 配置
|
|
66
|
+
if 'hooks' in config and config['hooks']:
|
|
67
|
+
return project_log_dir
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
# 2. 检查全局配置
|
|
72
|
+
if global_config.exists():
|
|
73
|
+
try:
|
|
74
|
+
with open(global_config, 'r', encoding='utf-8') as f:
|
|
75
|
+
config = json.load(f)
|
|
76
|
+
# 检查是否有 hooks 配置
|
|
77
|
+
if 'hooks' in config and config['hooks']:
|
|
78
|
+
return global_log_dir
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
elif '.cursor' in default_dir:
|
|
83
|
+
project_config = cwd / ".cursor" / "hooks.json"
|
|
84
|
+
global_config = home / ".cursor" / "hooks.json"
|
|
85
|
+
project_log_dir = str(cwd / ".cursor" / "logs")
|
|
86
|
+
global_log_dir = str(home / ".cursor" / "logs")
|
|
87
|
+
|
|
88
|
+
# 1. 优先检查项目配置
|
|
89
|
+
if project_config.exists():
|
|
90
|
+
try:
|
|
91
|
+
with open(project_config, 'r', encoding='utf-8') as f:
|
|
92
|
+
config = json.load(f)
|
|
93
|
+
# 检查是否有有效的 hooks 配置
|
|
94
|
+
if config.get('version') and 'hooks' in config and config['hooks']:
|
|
95
|
+
return project_log_dir
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
# 2. 检查全局配置
|
|
100
|
+
if global_config.exists():
|
|
101
|
+
try:
|
|
102
|
+
with open(global_config, 'r', encoding='utf-8') as f:
|
|
103
|
+
config = json.load(f)
|
|
104
|
+
# 检查是否有有效的 hooks 配置
|
|
105
|
+
if config.get('version') and 'hooks' in config and config['hooks']:
|
|
106
|
+
return global_log_dir
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
# 3. 默认使用项目日志目录
|
|
111
|
+
return default_dir
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def configure_logging(
|
|
115
|
+
log_dir: Optional[str] = None,
|
|
116
|
+
log_file: Optional[str] = None
|
|
117
|
+
):
|
|
118
|
+
"""
|
|
119
|
+
配置全局 logging(在应用启动时调用一次)
|
|
120
|
+
|
|
121
|
+
根据环境变量配置日志行为:
|
|
122
|
+
- DEVLAKE_MCP_LOGGING_ENABLED: 是否启用(默认 true)
|
|
123
|
+
- DEVLAKE_MCP_LOG_LEVEL: 日志级别(默认 INFO)
|
|
124
|
+
- DEVLAKE_MCP_CONSOLE_LOG: 是否输出到控制台(默认 false,仅在开发调试时启用)
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
log_dir: 日志文件目录(可选)
|
|
128
|
+
log_file: 日志文件名(可选)
|
|
129
|
+
|
|
130
|
+
示例:
|
|
131
|
+
>>> from devlake_mcp.logging_config import configure_logging
|
|
132
|
+
>>> configure_logging(log_dir='.claude/logs', log_file='hook.log')
|
|
133
|
+
>>>
|
|
134
|
+
>>> import logging
|
|
135
|
+
>>> logger = logging.getLogger(__name__)
|
|
136
|
+
>>> logger.info('Hello')
|
|
137
|
+
|
|
138
|
+
注意:
|
|
139
|
+
- 默认情况下,日志只写入文件,不输出到控制台
|
|
140
|
+
- 控制台输出会在 IDE hook 界面显示为 Error Output(stderr)
|
|
141
|
+
- 如需调试,可设置环境变量 DEVLAKE_MCP_CONSOLE_LOG=true 启用控制台输出
|
|
142
|
+
"""
|
|
143
|
+
# 读取环境变量
|
|
144
|
+
enabled = os.getenv('DEVLAKE_MCP_LOGGING_ENABLED', 'true').lower() in ('true', '1', 'yes')
|
|
145
|
+
level_str = os.getenv('DEVLAKE_MCP_LOG_LEVEL', DEFAULT_LOG_LEVEL).upper()
|
|
146
|
+
|
|
147
|
+
# 获取日志级别
|
|
148
|
+
level = VALID_LOG_LEVELS.get(level_str, VALID_LOG_LEVELS[DEFAULT_LOG_LEVEL])
|
|
149
|
+
|
|
150
|
+
# 如果禁用,使用 NullHandler
|
|
151
|
+
if not enabled:
|
|
152
|
+
logging.basicConfig(
|
|
153
|
+
level=level,
|
|
154
|
+
handlers=[logging.NullHandler()]
|
|
155
|
+
)
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
# 准备 handlers
|
|
159
|
+
handlers = []
|
|
160
|
+
formatter = logging.Formatter(
|
|
161
|
+
'%(asctime)s [%(levelname)s] %(name)s: %(message)s',
|
|
162
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# 文件 handler(如果提供了 log_dir 和 log_file)
|
|
166
|
+
if log_dir and log_file:
|
|
167
|
+
try:
|
|
168
|
+
Path(log_dir).mkdir(parents=True, exist_ok=True)
|
|
169
|
+
file_handler = logging.FileHandler(
|
|
170
|
+
Path(log_dir) / log_file,
|
|
171
|
+
encoding='utf-8'
|
|
172
|
+
)
|
|
173
|
+
file_handler.setFormatter(formatter)
|
|
174
|
+
file_handler.setLevel(level)
|
|
175
|
+
handlers.append(file_handler)
|
|
176
|
+
except Exception as e:
|
|
177
|
+
# 创建文件 handler 失败,只用控制台
|
|
178
|
+
print(f"警告:无法创建日志文件 {log_dir}/{log_file}: {e}")
|
|
179
|
+
|
|
180
|
+
# 控制台 handler(仅在开发调试时启用)
|
|
181
|
+
# 通过环境变量 DEVLAKE_MCP_CONSOLE_LOG=true 启用
|
|
182
|
+
if os.getenv('DEVLAKE_MCP_CONSOLE_LOG', 'false').lower() in ('true', '1', 'yes'):
|
|
183
|
+
import sys
|
|
184
|
+
console_handler = logging.StreamHandler(sys.stdout) # 使用 stdout 而不是 stderr
|
|
185
|
+
console_handler.setFormatter(formatter)
|
|
186
|
+
console_handler.setLevel(level)
|
|
187
|
+
handlers.append(console_handler)
|
|
188
|
+
|
|
189
|
+
# 如果没有任何 handler,添加 NullHandler(避免 logging 警告)
|
|
190
|
+
if not handlers:
|
|
191
|
+
handlers.append(logging.NullHandler())
|
|
192
|
+
|
|
193
|
+
# 配置全局 logging
|
|
194
|
+
logging.basicConfig(
|
|
195
|
+
level=level,
|
|
196
|
+
handlers=handlers,
|
|
197
|
+
force=True # 覆盖已有配置
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# 抑制第三方库的 DEBUG 日志
|
|
201
|
+
for lib in ['urllib3', 'urllib3.connectionpool', 'requests']:
|
|
202
|
+
logging.getLogger(lib).setLevel(logging.WARNING)
|