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
devlake_mcp/config.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DevLake 配置管理
|
|
3
|
+
|
|
4
|
+
统一管理 DevLake 的所有配置:
|
|
5
|
+
- MCP 服务器配置(DevLakeConfig)
|
|
6
|
+
- Hooks 环境配置(init_hooks_env)
|
|
7
|
+
- Git 信息缓存机制
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Optional, Dict
|
|
14
|
+
|
|
15
|
+
# 导入常量配置
|
|
16
|
+
from .constants import (
|
|
17
|
+
DEFAULT_API_BASE_URL,
|
|
18
|
+
API_REQUEST_TIMEOUT,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class DevLakeConfig:
|
|
24
|
+
"""DevLake 配置类(用于 MCP 服务器和 Hooks)"""
|
|
25
|
+
|
|
26
|
+
# API 基础 URL
|
|
27
|
+
base_url: str
|
|
28
|
+
|
|
29
|
+
# API Token(如果需要认证)
|
|
30
|
+
api_token: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
# 超时设置(秒)
|
|
33
|
+
timeout: int = 30
|
|
34
|
+
|
|
35
|
+
# 是否启用 SSL 验证
|
|
36
|
+
verify_ssl: bool = True
|
|
37
|
+
|
|
38
|
+
# Git 配置(用于 Hooks)
|
|
39
|
+
git_repo_path: Optional[str] = None
|
|
40
|
+
git_email: Optional[str] = None
|
|
41
|
+
git_author: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_env(cls, include_git: bool = False) -> "DevLakeConfig":
|
|
45
|
+
"""
|
|
46
|
+
从环境变量加载配置
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
include_git: 是否包含 Git 配置(Hooks 使用)
|
|
50
|
+
|
|
51
|
+
环境变量:
|
|
52
|
+
- DEVLAKE_BASE_URL: DevLake API 地址(默认:http://devlake.test.chinawayltd.com)
|
|
53
|
+
- DEVLAKE_API_TOKEN: API Token(可选)
|
|
54
|
+
- DEVLAKE_TIMEOUT: 请求超时时间(默认:5 秒)
|
|
55
|
+
- DEVLAKE_VERIFY_SSL: 是否验证 SSL(默认:true)
|
|
56
|
+
|
|
57
|
+
Git 环境变量(include_git=True 时):
|
|
58
|
+
- GIT_REPO_PATH: Git仓库路径
|
|
59
|
+
- GIT_EMAIL: Git邮箱
|
|
60
|
+
- GIT_AUTHOR: Git用户名
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
DevLakeConfig: 配置实例
|
|
64
|
+
"""
|
|
65
|
+
# API 配置
|
|
66
|
+
base_url = os.getenv('DEVLAKE_BASE_URL', DEFAULT_API_BASE_URL)
|
|
67
|
+
api_token = os.getenv('DEVLAKE_API_TOKEN')
|
|
68
|
+
timeout = int(os.getenv('DEVLAKE_TIMEOUT', str(API_REQUEST_TIMEOUT)))
|
|
69
|
+
verify_ssl = os.getenv('DEVLAKE_VERIFY_SSL', "true").lower() == "true"
|
|
70
|
+
|
|
71
|
+
config = cls(
|
|
72
|
+
base_url=base_url.rstrip("/"),
|
|
73
|
+
api_token=api_token,
|
|
74
|
+
timeout=timeout,
|
|
75
|
+
verify_ssl=verify_ssl
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# 加载 Git 配置(Hooks 专用)
|
|
79
|
+
if include_git:
|
|
80
|
+
config._load_git_config()
|
|
81
|
+
|
|
82
|
+
return config
|
|
83
|
+
|
|
84
|
+
def _load_git_config(self) -> None:
|
|
85
|
+
"""加载 Git 配置并同步到环境变量(内部方法)"""
|
|
86
|
+
self.git_repo_path = os.getenv('GIT_REPO_PATH')
|
|
87
|
+
self.git_email = os.getenv('GIT_EMAIL')
|
|
88
|
+
self.git_author = os.getenv('GIT_AUTHOR')
|
|
89
|
+
|
|
90
|
+
# 如果环境变量未设置,尝试从 Git 配置读取
|
|
91
|
+
if not self.git_repo_path or not self.git_email:
|
|
92
|
+
try:
|
|
93
|
+
from .git_utils import get_git_repo_path, get_git_info
|
|
94
|
+
|
|
95
|
+
cwd = os.getcwd()
|
|
96
|
+
|
|
97
|
+
if not self.git_repo_path:
|
|
98
|
+
self.git_repo_path = get_git_repo_path(cwd)
|
|
99
|
+
os.environ['GIT_REPO_PATH'] = self.git_repo_path
|
|
100
|
+
|
|
101
|
+
if not self.git_email or not self.git_author:
|
|
102
|
+
git_info = get_git_info(cwd, include_user_info=True)
|
|
103
|
+
if not self.git_email:
|
|
104
|
+
self.git_email = git_info.get('git_email', 'unknown')
|
|
105
|
+
os.environ['GIT_EMAIL'] = self.git_email
|
|
106
|
+
if not self.git_author:
|
|
107
|
+
self.git_author = git_info.get('git_author', 'unknown')
|
|
108
|
+
os.environ['GIT_AUTHOR'] = self.git_author
|
|
109
|
+
|
|
110
|
+
except Exception as e:
|
|
111
|
+
print(f"\n⚠️ 警告:获取 Git 配置失败:{str(e)}", file=sys.stderr)
|
|
112
|
+
if not self.git_repo_path:
|
|
113
|
+
self.git_repo_path = 'unknown'
|
|
114
|
+
os.environ['GIT_REPO_PATH'] = 'unknown'
|
|
115
|
+
if not self.git_email:
|
|
116
|
+
self.git_email = 'unknown'
|
|
117
|
+
os.environ['GIT_EMAIL'] = 'unknown'
|
|
118
|
+
if not self.git_author:
|
|
119
|
+
self.git_author = 'unknown'
|
|
120
|
+
os.environ['GIT_AUTHOR'] = 'unknown'
|
|
121
|
+
|
|
122
|
+
# 验证必需的 Git 配置
|
|
123
|
+
self._validate_git_config()
|
|
124
|
+
|
|
125
|
+
def _validate_git_config(self):
|
|
126
|
+
"""验证 Git 配置(内部方法)"""
|
|
127
|
+
missing_configs = []
|
|
128
|
+
|
|
129
|
+
if not self.git_repo_path or self.git_repo_path == 'unknown' or self.git_repo_path.startswith('local/'):
|
|
130
|
+
missing_configs.append('remote.origin.url')
|
|
131
|
+
|
|
132
|
+
if not self.git_email or self.git_email == 'unknown':
|
|
133
|
+
missing_configs.append('user.email')
|
|
134
|
+
|
|
135
|
+
if missing_configs:
|
|
136
|
+
print("\n" + "="*60, file=sys.stderr)
|
|
137
|
+
print("❌ 错误:缺少必需的 Git 配置", file=sys.stderr)
|
|
138
|
+
print("="*60, file=sys.stderr)
|
|
139
|
+
print("\n📝 请按照以下步骤配置 Git:\n", file=sys.stderr)
|
|
140
|
+
|
|
141
|
+
if 'remote.origin.url' in missing_configs:
|
|
142
|
+
print("1️⃣ 配置 Git 远程仓库:", file=sys.stderr)
|
|
143
|
+
print(" git remote add origin <repository-url>", file=sys.stderr)
|
|
144
|
+
print("", file=sys.stderr)
|
|
145
|
+
|
|
146
|
+
if 'user.email' in missing_configs:
|
|
147
|
+
print("2️⃣ 配置 Git 用户邮箱:", file=sys.stderr)
|
|
148
|
+
print(" git config user.email 'your-email@example.com'", file=sys.stderr)
|
|
149
|
+
print("", file=sys.stderr)
|
|
150
|
+
|
|
151
|
+
print("💡 提示:配置完成后,请重新运行命令。", file=sys.stderr)
|
|
152
|
+
print("="*60 + "\n", file=sys.stderr)
|
|
153
|
+
sys.exit(2)
|
|
154
|
+
|
|
155
|
+
def get_headers(self) -> Dict[str, str]:
|
|
156
|
+
"""
|
|
157
|
+
获取请求头
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
dict: 请求头字典
|
|
161
|
+
"""
|
|
162
|
+
headers = {
|
|
163
|
+
"Content-Type": "application/json",
|
|
164
|
+
"Accept": "application/json"
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if self.api_token:
|
|
168
|
+
headers["Authorization"] = f"Bearer {self.api_token}"
|
|
169
|
+
|
|
170
|
+
return headers
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def initialize_for_hooks(cls) -> "DevLakeConfig":
|
|
174
|
+
"""
|
|
175
|
+
为 Hooks 环境初始化配置
|
|
176
|
+
|
|
177
|
+
这个方法会:
|
|
178
|
+
1. 从环境变量加载 API 配置
|
|
179
|
+
2. 获取静态 Git 信息(author, email, repo_path)并缓存到环境变量
|
|
180
|
+
3. 验证配置完整性
|
|
181
|
+
|
|
182
|
+
注意:
|
|
183
|
+
- 只缓存静态信息(author, email, repo_path)
|
|
184
|
+
- 动态信息(branch, commit)每次都重新获取,确保最新值
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
DevLakeConfig: 已初始化的配置实例
|
|
188
|
+
|
|
189
|
+
使用示例:
|
|
190
|
+
# 在 hooks 启动脚本中调用一次
|
|
191
|
+
config = DevLakeConfig.initialize_for_hooks()
|
|
192
|
+
|
|
193
|
+
# 静态信息从环境变量读取(已缓存)
|
|
194
|
+
git_author = os.getenv('GIT_AUTHOR')
|
|
195
|
+
git_email = os.getenv('GIT_EMAIL')
|
|
196
|
+
|
|
197
|
+
# 动态信息每次获取最新值
|
|
198
|
+
git_info = get_git_info(cwd, include_user_info=False)
|
|
199
|
+
git_branch = git_info.get('git_branch')
|
|
200
|
+
"""
|
|
201
|
+
# 加载配置并获取静态 Git 信息(会自动缓存到环境变量)
|
|
202
|
+
config = cls.from_env(include_git=True)
|
|
203
|
+
|
|
204
|
+
return config
|
devlake_mcp/constants.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
DevLake MCP 常量配置
|
|
5
|
+
|
|
6
|
+
集中管理所有魔法值,提高代码可维护性。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Set
|
|
10
|
+
|
|
11
|
+
# ============================================================================
|
|
12
|
+
# Git 配置
|
|
13
|
+
# ============================================================================
|
|
14
|
+
|
|
15
|
+
# Git 命令超时时间(秒)
|
|
16
|
+
GIT_COMMAND_TIMEOUT: int = 1
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ============================================================================
|
|
20
|
+
# 文件过滤配置
|
|
21
|
+
# ============================================================================
|
|
22
|
+
|
|
23
|
+
# 敏感文件模式(包含这些关键词的文件不采集)
|
|
24
|
+
SENSITIVE_FILE_PATTERNS: list[str] = [
|
|
25
|
+
'.env',
|
|
26
|
+
'.env.', # .env.local, .env.production 等
|
|
27
|
+
'.secret',
|
|
28
|
+
'.secrets',
|
|
29
|
+
'.key',
|
|
30
|
+
'.pem',
|
|
31
|
+
'.crt',
|
|
32
|
+
'.p12',
|
|
33
|
+
'.pfx',
|
|
34
|
+
'credentials',
|
|
35
|
+
'password',
|
|
36
|
+
'.npmrc', # npm 配置
|
|
37
|
+
'.pypirc', # PyPI 配置
|
|
38
|
+
'id_rsa', # SSH 私钥
|
|
39
|
+
'id_dsa',
|
|
40
|
+
'id_ecdsa',
|
|
41
|
+
'id_ed25519',
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# 敏感目录(这些目录下的文件不采集)
|
|
45
|
+
SENSITIVE_DIRS: list[str] = [
|
|
46
|
+
'.ssh',
|
|
47
|
+
'.gnupg',
|
|
48
|
+
'.aws',
|
|
49
|
+
'.azure',
|
|
50
|
+
'.config',
|
|
51
|
+
'node_modules', # 前端依赖
|
|
52
|
+
'.venv', # Python 虚拟环境
|
|
53
|
+
'venv',
|
|
54
|
+
'__pycache__',
|
|
55
|
+
'.git', # Git 元数据
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
# 二进制文件扩展名(这些文件不采集)
|
|
59
|
+
BINARY_FILE_EXTENSIONS: Set[str] = {
|
|
60
|
+
# 图片
|
|
61
|
+
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', '.webp',
|
|
62
|
+
# 压缩包
|
|
63
|
+
'.zip', '.tar', '.gz', '.bz2', '.rar', '.7z', '.xz',
|
|
64
|
+
# 文档
|
|
65
|
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
66
|
+
# 可执行文件
|
|
67
|
+
'.exe', '.dll', '.so', '.dylib', '.app', '.dmg',
|
|
68
|
+
# 编译产物
|
|
69
|
+
'.class', '.pyc', '.pyo', '.o', '.a', '.jar',
|
|
70
|
+
# 音视频
|
|
71
|
+
'.mp3', '.mp4', '.avi', '.mov', '.wav', '.flac',
|
|
72
|
+
# 字体
|
|
73
|
+
'.ttf', '.otf', '.woff', '.woff2', '.eot',
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ============================================================================
|
|
78
|
+
# API 配置
|
|
79
|
+
# ============================================================================
|
|
80
|
+
|
|
81
|
+
# API 请求超时时间(秒)
|
|
82
|
+
API_REQUEST_TIMEOUT: int = 5
|
|
83
|
+
|
|
84
|
+
# API 默认基础 URL
|
|
85
|
+
DEFAULT_API_BASE_URL: str = "http://devlake.test.chinawayltd.com"
|
|
86
|
+
|
|
87
|
+
# 最大内容大小(字节)- 10MB
|
|
88
|
+
MAX_CONTENT_SIZE: int = 10 * 1024 * 1024
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ============================================================================
|
|
92
|
+
# 临时文件配置
|
|
93
|
+
# ============================================================================
|
|
94
|
+
|
|
95
|
+
# 临时文件最大保留时间(小时)
|
|
96
|
+
TEMP_FILE_MAX_AGE_HOURS: int = 24
|
|
97
|
+
|
|
98
|
+
# 临时目录默认名称
|
|
99
|
+
TEMP_DIR_NAME: str = 'devlake_mcp'
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ============================================================================
|
|
103
|
+
# Hooks 配置
|
|
104
|
+
# ============================================================================
|
|
105
|
+
|
|
106
|
+
# Hook 执行超时时间(秒)
|
|
107
|
+
HOOK_EXECUTION_TIMEOUT: int = 5
|
|
108
|
+
|
|
109
|
+
# Hook 日志目录(默认为项目目录)
|
|
110
|
+
HOOK_LOG_DIR: str = '.claude/logs' # Claude Code hooks 日志目录(项目)
|
|
111
|
+
CURSOR_HOOK_LOG_DIR: str = '.cursor/logs' # Cursor hooks 日志目录(项目)
|
|
112
|
+
|
|
113
|
+
# 全局日志目录(当使用全局配置时)
|
|
114
|
+
GLOBAL_HOOK_LOG_DIR: str = None # 运行时动态设置为 ~/.claude/logs
|
|
115
|
+
GLOBAL_CURSOR_HOOK_LOG_DIR: str = None # 运行时动态设置为 ~/.cursor/logs
|
|
116
|
+
|
|
117
|
+
# 默认 IDE 类型
|
|
118
|
+
DEFAULT_IDE_TYPE: str = 'claude_code'
|
|
119
|
+
|
|
120
|
+
# 默认模型名称
|
|
121
|
+
DEFAULT_MODEL_NAME: str = 'claude-sonnet-4-5'
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ============================================================================
|
|
125
|
+
# Generation 配置
|
|
126
|
+
# ============================================================================
|
|
127
|
+
|
|
128
|
+
# Generation 状态文件名
|
|
129
|
+
GENERATION_STATE_FILE_NAME: str = 'generation_state.json'
|
|
130
|
+
|
|
131
|
+
# Generation 状态最大保留时间(小时)- 超过此时间的状态可以被清理
|
|
132
|
+
GENERATION_STATE_MAX_AGE_HOURS: int = 24
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ============================================================================
|
|
136
|
+
# Transcript 压缩配置
|
|
137
|
+
# ============================================================================
|
|
138
|
+
|
|
139
|
+
# Transcript 压缩阈值(字节)- 超过此大小才进行压缩
|
|
140
|
+
# 1MB = 1 * 1024 * 1024 bytes * 0.5
|
|
141
|
+
TRANSCRIPT_COMPRESSION_THRESHOLD: int = 1 * 1024 * 1024 * 0.5
|
|
142
|
+
|
|
143
|
+
# Transcript 压缩算法
|
|
144
|
+
TRANSCRIPT_COMPRESSION_ALGORITHM: str = 'gzip'
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ============================================================================
|
|
148
|
+
# 日志配置
|
|
149
|
+
# ============================================================================
|
|
150
|
+
|
|
151
|
+
# 日志级别映射(字符串 -> logging 常量)
|
|
152
|
+
VALID_LOG_LEVELS: dict[str, int] = {
|
|
153
|
+
'DEBUG': 10, # logging.DEBUG
|
|
154
|
+
'INFO': 20, # logging.INFO
|
|
155
|
+
'WARNING': 30, # logging.WARNING
|
|
156
|
+
'ERROR': 40, # logging.ERROR
|
|
157
|
+
'CRITICAL': 50, # logging.CRITICAL
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# 默认日志级别
|
|
161
|
+
DEFAULT_LOG_LEVEL: str = 'INFO'
|
devlake_mcp/enums.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
枚举类型定义模块
|
|
5
|
+
|
|
6
|
+
提供项目中使用的所有枚举类型
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class IDEType(str, Enum):
|
|
13
|
+
"""
|
|
14
|
+
支持的 IDE 类型枚举
|
|
15
|
+
|
|
16
|
+
继承 str 以便:
|
|
17
|
+
1. 直接用于字符串比较和拼接
|
|
18
|
+
2. JSON 序列化时自动转换为字符串
|
|
19
|
+
3. 向后兼容现有字符串参数
|
|
20
|
+
|
|
21
|
+
使用示例:
|
|
22
|
+
# 作为参数
|
|
23
|
+
start_generation(session_id, ide_type=IDEType.CLAUDE_CODE)
|
|
24
|
+
|
|
25
|
+
# 字符串比较
|
|
26
|
+
if ide_type == IDEType.CLAUDE_CODE:
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
# 获取字符串值
|
|
30
|
+
ide_type.value # 'claude_code'
|
|
31
|
+
|
|
32
|
+
# 从字符串创建
|
|
33
|
+
IDEType('claude_code') # IDEType.CLAUDE_CODE
|
|
34
|
+
"""
|
|
35
|
+
CLAUDE_CODE = 'claude_code' # Anthropic Claude Code
|
|
36
|
+
CURSOR = 'cursor' # Cursor AI IDE
|
|
37
|
+
QODER = 'qoder' # Qoder IDE (未来支持)
|
|
38
|
+
UNKNOWN = 'unknown' # 未知或不支持的 IDE
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_string(cls, value: str) -> 'IDEType':
|
|
42
|
+
"""
|
|
43
|
+
从字符串创建枚举(安全转换)
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
value: IDE 类型字符串
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
对应的 IDEType 枚举值,无效值返回 UNKNOWN
|
|
50
|
+
|
|
51
|
+
示例:
|
|
52
|
+
IDEType.from_string('claude_code') # IDEType.CLAUDE_CODE
|
|
53
|
+
IDEType.from_string('invalid') # IDEType.UNKNOWN
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
return cls(value.lower())
|
|
57
|
+
except (ValueError, AttributeError):
|
|
58
|
+
return cls.UNKNOWN
|