devmate 1.0.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.
- devmate/__init__.py +6 -0
- devmate/__main__.py +5 -0
- devmate/app/__init__.py +1 -0
- devmate/app/cli.py +1663 -0
- devmate/app/tui.py +291 -0
- devmate/app/web/__init__.py +95 -0
- devmate/bus/__init__.py +1 -0
- devmate/bus/bus.py +151 -0
- devmate/bus/command.py +84 -0
- devmate/core/__init__.py +1 -0
- devmate/core/config.py +115 -0
- devmate/core/logging.py +54 -0
- devmate/core/security.py +61 -0
- devmate/events/__init__.py +1 -0
- devmate/events/bus.py +97 -0
- devmate/events/domain_event.py +37 -0
- devmate/module/__init__.py +1 -0
- devmate/module/registry.py +74 -0
- devmate/security/audit.py +96 -0
- devmate/security/vault.py +221 -0
- devmate-1.0.0.dist-info/METADATA +116 -0
- devmate-1.0.0.dist-info/RECORD +37 -0
- devmate-1.0.0.dist-info/WHEEL +5 -0
- devmate-1.0.0.dist-info/entry_points.txt +18 -0
- devmate-1.0.0.dist-info/top_level.txt +13 -0
- devmate_agent/__init__.py +440 -0
- devmate_apidev/__init__.py +271 -0
- devmate_apihub/__init__.py +313 -0
- devmate_dbadmin/__init__.py +263 -0
- devmate_gitflow/__init__.py +247 -0
- devmate_monitor/__init__.py +112 -0
- devmate_notekeeper/__init__.py +280 -0
- devmate_regexlab/__init__.py +212 -0
- devmate_reporter/__init__.py +170 -0
- devmate_scaffold/__init__.py +181 -0
- devmate_sshman/__init__.py +330 -0
- devmate_toolkit/__init__.py +223 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
toolkit — 文件瑞士军刀
|
|
3
|
+
|
|
4
|
+
包含文件整理(organize)、目录大小排行(size)、缓存清理(cleanup)等实用功能。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from devmate.bus.command import CommandResult
|
|
11
|
+
|
|
12
|
+
# ── 文件类型分类规则 ────────────────────────────────
|
|
13
|
+
|
|
14
|
+
CATEGORY_RULES: dict[str, list[str]] = {
|
|
15
|
+
"图片": [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp", ".ico"],
|
|
16
|
+
"文档": [".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".txt", ".md", ".csv"],
|
|
17
|
+
"代码": [".py", ".js", ".ts", ".java", ".go", ".rs", ".c", ".cpp", ".h", ".hpp",
|
|
18
|
+
".css", ".scss", ".html", ".json", ".xml", ".yaml", ".yml", ".toml"],
|
|
19
|
+
"压缩包": [".zip", ".tar", ".gz", ".bz2", ".7z", ".rar"],
|
|
20
|
+
"视频": [".mp4", ".avi", ".mkv", ".mov", ".flv", ".wmv"],
|
|
21
|
+
"音频": [".mp3", ".wav", ".flac", ".aac", ".ogg"],
|
|
22
|
+
"安装包": [".dmg", ".pkg", ".exe", ".msi", ".deb", ".rpm", ".AppImage"],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_category(suffix: str) -> str:
|
|
27
|
+
"""根据文件后缀返回分类名"""
|
|
28
|
+
for category, extensions in CATEGORY_RULES.items():
|
|
29
|
+
if suffix.lower() in extensions:
|
|
30
|
+
return category
|
|
31
|
+
return "其他"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _size_str(bytes_val: int) -> str:
|
|
35
|
+
"""字节数转人类可读大小"""
|
|
36
|
+
for unit in ("B", "KB", "MB", "GB", "TB"):
|
|
37
|
+
if bytes_val < 1024:
|
|
38
|
+
return f"{bytes_val:.1f}{unit}"
|
|
39
|
+
bytes_val /= 1024
|
|
40
|
+
return f"{bytes_val:.1f}PB"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ── organize:文件整理 ──────────────────────────────
|
|
44
|
+
|
|
45
|
+
def organize(source: str, dry_run: bool = False, by: str = "type") -> CommandResult:
|
|
46
|
+
"""按类别整理文件
|
|
47
|
+
|
|
48
|
+
按文件类型(by=type)或按修改日期(by=date)将文件分类到子目录。
|
|
49
|
+
"""
|
|
50
|
+
src = Path(source).expanduser().resolve()
|
|
51
|
+
if not src.is_dir():
|
|
52
|
+
return CommandResult(success=False, error=f"目录不存在: {source}")
|
|
53
|
+
|
|
54
|
+
stats = {"已整理": 0, "跳过": 0, "错误": 0}
|
|
55
|
+
details: list[dict[str, Any]] = []
|
|
56
|
+
|
|
57
|
+
for item in src.iterdir():
|
|
58
|
+
if not item.is_file():
|
|
59
|
+
stats["跳过"] += 1
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
if by == "type":
|
|
63
|
+
category = _get_category(item.suffix)
|
|
64
|
+
target_dir = src / category
|
|
65
|
+
action_label = category
|
|
66
|
+
elif by == "date":
|
|
67
|
+
mtime = item.stat().st_mtime
|
|
68
|
+
from datetime import datetime
|
|
69
|
+
date_str = datetime.fromtimestamp(mtime).strftime("%Y-%m")
|
|
70
|
+
target_dir = src / date_str
|
|
71
|
+
action_label = date_str
|
|
72
|
+
else:
|
|
73
|
+
return CommandResult(success=False, error=f"不支持的整理方式: {by}")
|
|
74
|
+
|
|
75
|
+
target = target_dir / item.name
|
|
76
|
+
|
|
77
|
+
if target.exists():
|
|
78
|
+
stats["跳过"] += 1
|
|
79
|
+
details.append({"file": item.name, "action": "跳过(已存在)"})
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
if not dry_run:
|
|
83
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
try:
|
|
85
|
+
item.rename(target)
|
|
86
|
+
except OSError:
|
|
87
|
+
stats["错误"] += 1
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
stats["已整理"] += 1
|
|
91
|
+
details.append({"file": item.name, "action": f"→ {action_label}"})
|
|
92
|
+
|
|
93
|
+
msg = f"整理完成: {stats['已整理']} 个文件已整理"
|
|
94
|
+
if stats["跳过"]:
|
|
95
|
+
msg += f", {stats['跳过']} 个跳过"
|
|
96
|
+
if stats["错误"]:
|
|
97
|
+
msg += f", {stats['错误']} 个错误"
|
|
98
|
+
if dry_run:
|
|
99
|
+
msg = f"[预览] 将整理 {stats['已整理']} 个文件" + (
|
|
100
|
+
f",{stats['跳过']} 个跳过" if stats["跳过"] else ""
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return CommandResult(success=True, data={
|
|
104
|
+
"stats": stats,
|
|
105
|
+
"details": details,
|
|
106
|
+
"message": msg,
|
|
107
|
+
"source": str(src),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ── size:目录大小排行 ─────────────────────────────
|
|
112
|
+
|
|
113
|
+
def size(path: str, top: int = 10) -> CommandResult:
|
|
114
|
+
"""统计目录大小排行"""
|
|
115
|
+
target = Path(path).expanduser().resolve()
|
|
116
|
+
if not target.is_dir():
|
|
117
|
+
return CommandResult(success=False, error=f"目录不存在: {path}")
|
|
118
|
+
|
|
119
|
+
entries: list[dict[str, Any]] = []
|
|
120
|
+
total_size = 0
|
|
121
|
+
file_count = 0
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
for item in target.iterdir():
|
|
125
|
+
if item.is_file():
|
|
126
|
+
size_bytes = item.stat().st_size
|
|
127
|
+
total_size += size_bytes
|
|
128
|
+
file_count += 1
|
|
129
|
+
entries.append({
|
|
130
|
+
"name": item.name,
|
|
131
|
+
"size": size_bytes,
|
|
132
|
+
"size_str": _size_str(size_bytes),
|
|
133
|
+
})
|
|
134
|
+
elif item.is_dir():
|
|
135
|
+
dir_size = sum(
|
|
136
|
+
f.stat().st_size for f in item.rglob("*") if f.is_file()
|
|
137
|
+
) if any(item.iterdir()) else 0
|
|
138
|
+
total_size += dir_size
|
|
139
|
+
file_count += sum(1 for _ in item.rglob("*") if _.is_file())
|
|
140
|
+
entries.append({
|
|
141
|
+
"name": item.name + "/",
|
|
142
|
+
"size": dir_size,
|
|
143
|
+
"size_str": _size_str(dir_size),
|
|
144
|
+
})
|
|
145
|
+
except PermissionError as e:
|
|
146
|
+
return CommandResult(success=False, error=f"权限不足: {e}")
|
|
147
|
+
|
|
148
|
+
entries.sort(key=lambda x: x["size"], reverse=True)
|
|
149
|
+
top_entries = entries[:top]
|
|
150
|
+
|
|
151
|
+
return CommandResult(success=True, data={
|
|
152
|
+
"entries": top_entries,
|
|
153
|
+
"total_size": _size_str(total_size),
|
|
154
|
+
"file_count": file_count,
|
|
155
|
+
"path": str(target),
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ── cleanup:缓存清理 ─────────────────────────────
|
|
160
|
+
|
|
161
|
+
# 常见缓存目录(相对 ~ 或绝对路径)
|
|
162
|
+
CACHE_PATTERNS: list[str] = [
|
|
163
|
+
"*/__pycache__",
|
|
164
|
+
"*/.pytest_cache",
|
|
165
|
+
"*/node_modules/.cache",
|
|
166
|
+
"*/.mypy_cache",
|
|
167
|
+
"*/.ruff_cache",
|
|
168
|
+
"*/*.pyc",
|
|
169
|
+
"*/.DS_Store",
|
|
170
|
+
"*/Thumbs.db",
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def cleanup(path: str, dry_run: bool = False) -> CommandResult:
|
|
175
|
+
"""清理缓存文件和临时文件"""
|
|
176
|
+
target = Path(path).expanduser().resolve()
|
|
177
|
+
if not target.is_dir():
|
|
178
|
+
return CommandResult(success=False, error=f"目录不存在: {path}")
|
|
179
|
+
|
|
180
|
+
stats = {"已清理": 0, "已释放": 0, "跳过": 0}
|
|
181
|
+
details: list[dict[str, Any]] = []
|
|
182
|
+
|
|
183
|
+
# 清理 __pycache__
|
|
184
|
+
for pycache in target.rglob("__pycache__"):
|
|
185
|
+
if pycache.is_dir():
|
|
186
|
+
size = sum(f.stat().st_size for f in pycache.rglob("*") if f.is_file())
|
|
187
|
+
if not dry_run:
|
|
188
|
+
import shutil
|
|
189
|
+
shutil.rmtree(pycache, ignore_errors=True)
|
|
190
|
+
stats["已清理"] += 1
|
|
191
|
+
stats["已释放"] += size
|
|
192
|
+
details.append({"name": str(pycache.relative_to(target)), "size": _size_str(size)})
|
|
193
|
+
|
|
194
|
+
# 清理 .pyc 文件
|
|
195
|
+
for pyc in target.rglob("*.pyc"):
|
|
196
|
+
if pyc.is_file():
|
|
197
|
+
size = pyc.stat().st_size
|
|
198
|
+
if not dry_run:
|
|
199
|
+
pyc.unlink()
|
|
200
|
+
stats["已清理"] += 1
|
|
201
|
+
stats["已释放"] += size
|
|
202
|
+
details.append({"name": str(pyc.relative_to(target)), "size": _size_str(size)})
|
|
203
|
+
|
|
204
|
+
# 清理 .DS_Store
|
|
205
|
+
for ds in target.rglob(".DS_Store"):
|
|
206
|
+
if ds.is_file():
|
|
207
|
+
size = ds.stat().st_size
|
|
208
|
+
if not dry_run:
|
|
209
|
+
ds.unlink()
|
|
210
|
+
stats["已清理"] += 1
|
|
211
|
+
stats["已释放"] += size
|
|
212
|
+
details.append({"name": str(ds.relative_to(target)), "size": _size_str(size)})
|
|
213
|
+
|
|
214
|
+
msg = f"清理完成: 清理了 {stats['已清理']} 个文件,释放 {_size_str(stats['已释放'])}"
|
|
215
|
+
if dry_run:
|
|
216
|
+
msg = f"[预览] 将清理 {stats['已清理']} 个文件,释放 {_size_str(stats['已释放'])}"
|
|
217
|
+
|
|
218
|
+
return CommandResult(success=True, data={
|
|
219
|
+
"stats": stats,
|
|
220
|
+
"details": details,
|
|
221
|
+
"message": msg,
|
|
222
|
+
"path": str(target),
|
|
223
|
+
})
|