damon-devtools 0.4.0__tar.gz → 0.6.0__tar.gz

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.
Files changed (34) hide show
  1. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/PKG-INFO +4 -1
  2. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/damon_devtools.egg-info/PKG-INFO +4 -1
  3. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/damon_devtools.egg-info/SOURCES.txt +5 -1
  4. damon_devtools-0.6.0/damon_devtools.egg-info/requires.txt +4 -0
  5. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/devtools/tools/__init__.py +35 -29
  6. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/devtools/tools/apkpull/core.py +46 -1
  7. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/devtools/tools/apkpull/ui.py +88 -23
  8. damon_devtools-0.6.0/devtools/tools/url2md/__init__.py +0 -0
  9. damon_devtools-0.6.0/devtools/tools/url2md/__main__.py +5 -0
  10. damon_devtools-0.6.0/devtools/tools/url2md/cli.py +111 -0
  11. damon_devtools-0.6.0/devtools/tools/url2md/core.py +92 -0
  12. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/pyproject.toml +24 -24
  13. damon_devtools-0.4.0/damon_devtools.egg-info/requires.txt +0 -1
  14. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/README.md +0 -0
  15. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/damon_devtools.egg-info/dependency_links.txt +0 -0
  16. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/damon_devtools.egg-info/entry_points.txt +0 -0
  17. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/damon_devtools.egg-info/top_level.txt +0 -0
  18. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/devtools/__init__.py +0 -0
  19. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/devtools/__main__.py +0 -0
  20. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/devtools/launcher.py +0 -0
  21. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/devtools/tools/apkpull/__init__.py +0 -0
  22. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/devtools/tools/apkpull/__main__.py +0 -0
  23. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/devtools/tools/apkpull/models.py +0 -0
  24. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/devtools/tools/portkill/__init__.py +0 -0
  25. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/devtools/tools/portkill/__main__.py +0 -0
  26. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/devtools/tools/portkill/core.py +0 -0
  27. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/devtools/tools/portkill/models.py +0 -0
  28. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/devtools/tools/portkill/ui.py +0 -0
  29. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/devtools/tools/removebg/__init__.py +0 -0
  30. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/devtools/tools/removebg/__main__.py +0 -0
  31. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/devtools/tools/removebg/cli.py +0 -0
  32. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/devtools/tools/removebg/core.py +0 -0
  33. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/devtools/types.py +0 -0
  34. {damon_devtools-0.4.0 → damon_devtools-0.6.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: damon-devtools
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: Developer CLI tools — portkill, removebg, apkpull
5
5
  Author: damon
6
6
  License: MIT
@@ -8,6 +8,9 @@ Keywords: cli,devtools,android,apk,adb
8
8
  Requires-Python: >=3.10
9
9
  Description-Content-Type: text/markdown
10
10
  Requires-Dist: psutil>=5.9
11
+ Requires-Dist: markdownify
12
+ Requires-Dist: beautifulsoup4
13
+ Requires-Dist: requests
11
14
 
12
15
  # devtools
13
16
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: damon-devtools
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: Developer CLI tools — portkill, removebg, apkpull
5
5
  Author: damon
6
6
  License: MIT
@@ -8,6 +8,9 @@ Keywords: cli,devtools,android,apk,adb
8
8
  Requires-Python: >=3.10
9
9
  Description-Content-Type: text/markdown
10
10
  Requires-Dist: psutil>=5.9
11
+ Requires-Dist: markdownify
12
+ Requires-Dist: beautifulsoup4
13
+ Requires-Dist: requests
11
14
 
12
15
  # devtools
13
16
 
@@ -24,4 +24,8 @@ devtools/tools/portkill/ui.py
24
24
  devtools/tools/removebg/__init__.py
25
25
  devtools/tools/removebg/__main__.py
26
26
  devtools/tools/removebg/cli.py
27
- devtools/tools/removebg/core.py
27
+ devtools/tools/removebg/core.py
28
+ devtools/tools/url2md/__init__.py
29
+ devtools/tools/url2md/__main__.py
30
+ devtools/tools/url2md/cli.py
31
+ devtools/tools/url2md/core.py
@@ -0,0 +1,4 @@
1
+ psutil>=5.9
2
+ markdownify
3
+ beautifulsoup4
4
+ requests
@@ -1,29 +1,35 @@
1
- """Tools registry for devtools"""
2
- from devtools.types import Tool
3
-
4
- TOOLS = [
5
- Tool(
6
- name="portkill",
7
- category="进程/端口",
8
- description="按端口杀 Node.js 进程",
9
- module="devtools.tools.portkill",
10
- ),
11
- Tool(
12
- name="removebg",
13
- category="图像处理",
14
- description="批量删除图片背景",
15
- module="devtools.tools.removebg",
16
- ),
17
- Tool(
18
- name="apkpull",
19
- category="设备管理",
20
- description="提取手机 APK 文件",
21
- module="devtools.tools.apkpull",
22
- ),
23
- ]
24
-
25
- def get_tool(name: str):
26
- for tool in TOOLS:
27
- if tool.name == name:
28
- return tool
29
- return None
1
+ """Tools registry for devtools"""
2
+ from devtools.types import Tool
3
+
4
+ TOOLS = [
5
+ Tool(
6
+ name="portkill",
7
+ category="进程/端口",
8
+ description="按端口杀 Node.js 进程",
9
+ module="devtools.tools.portkill",
10
+ ),
11
+ Tool(
12
+ name="removebg",
13
+ category="图像处理",
14
+ description="批量删除图片背景",
15
+ module="devtools.tools.removebg",
16
+ ),
17
+ Tool(
18
+ name="url2md",
19
+ category="网页工具",
20
+ description="将网页链接转换为 Markdown 文件",
21
+ module="devtools.tools.url2md",
22
+ ),
23
+ Tool(
24
+ name="apkpull",
25
+ category="设备管理",
26
+ description="提取手机 APK 文件",
27
+ module="devtools.tools.apkpull",
28
+ ),
29
+ ]
30
+
31
+ def get_tool(name: str):
32
+ for tool in TOOLS:
33
+ if tool.name == name:
34
+ return tool
35
+ return None
@@ -58,6 +58,49 @@ def get_label_cache() -> dict[str, str]:
58
58
  _label_cache = load_label_cache()
59
59
  return _label_cache
60
60
 
61
+ # Selected device serial (set by user via CLI or interactive selection)
62
+ selected_device: str | None = None
63
+
64
+ def set_selected_device(serial: str | None) -> None:
65
+ """设置当前选中的设备"""
66
+ global selected_device
67
+ selected_device = serial
68
+
69
+ def get_connected_devices() -> list[tuple[str, str, str]]:
70
+ """返回 [(serial, brand, model), ...],仅返回 state=device 的设备"""
71
+ try:
72
+ result = subprocess.run(
73
+ ["adb", "devices"], capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
74
+ )
75
+ devices: list[tuple[str, str, str]] = []
76
+ for line in result.stdout.strip().split("\n")[1:]:
77
+ if line.strip() and not line.startswith("*"):
78
+ parts = line.split()
79
+ if len(parts) >= 2 and parts[1] == "device":
80
+ serial = parts[0]
81
+ brand, model = get_device_info(serial)
82
+ devices.append((serial, brand, model))
83
+ return devices
84
+ except (subprocess.CalledProcessError, FileNotFoundError):
85
+ return []
86
+
87
+ def get_device_info(serial: str) -> tuple[str, str]:
88
+ """获取指定设备的 (品牌, 型号)"""
89
+ try:
90
+ brand_result = subprocess.run(
91
+ ["adb", "-s", serial, "shell", "getprop", "ro.product.brand"],
92
+ capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
93
+ )
94
+ model_result = subprocess.run(
95
+ ["adb", "-s", serial, "shell", "getprop", "ro.product.model"],
96
+ capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
97
+ )
98
+ brand = brand_result.stdout.strip() or "Unknown"
99
+ model = model_result.stdout.strip() or "Unknown"
100
+ return brand, model
101
+ except (subprocess.CalledProcessError, FileNotFoundError):
102
+ return "Unknown", "Unknown"
103
+
61
104
  def check_adb() -> bool:
62
105
  """检查 ADB 是否可用"""
63
106
  try:
@@ -67,7 +110,9 @@ def check_adb() -> bool:
67
110
  return False
68
111
 
69
112
  def get_device_serial() -> str | None:
70
- """获取第一个已连接的设备序列号,多设备时返回第一个"""
113
+ """获取设备 serial:有选中设备时返回之,否则返回第一个已连接设备"""
114
+ if selected_device:
115
+ return selected_device
71
116
  try:
72
117
  result = subprocess.run(["adb", "devices"], capture_output=True, text=True, encoding="utf-8", errors="replace", check=True)
73
118
  lines = result.stdout.strip().split("\n")
@@ -113,9 +113,59 @@ def run(args: list[str] | None = None):
113
113
  parser = argparse.ArgumentParser(prog="apkpull", description="Pull APK from Android device")
114
114
  parser.add_argument("dest_dir", nargs="?", default=os.getcwd(), help="目标目录 (默认当前目录)")
115
115
  parser.add_argument("-s", "--search", metavar="KEYWORD", help="模糊搜索应用名/包名并退出")
116
+ parser.add_argument("-n", "--no-merge", action="store_true", help="不合并 split APK,直接导出所有分片")
117
+ parser.add_argument("-d", "--device", metavar="SERIAL", help="指定设备序列号,跳过设备选择")
116
118
 
117
119
  parsed = parser.parse_args(args)
118
120
 
121
+ # Device selection
122
+ from devtools.tools.apkpull.core import get_connected_devices, set_selected_device, check_adb
123
+
124
+ if not check_adb():
125
+ print("错误: 未找到 adb 命令,请确保 Android SDK platform-tools 已安装并加入 PATH")
126
+ return
127
+
128
+ devices = get_connected_devices()
129
+ if not devices:
130
+ print("错误: 未检测到设备,请确保手机已通过 USB 连接并开启了 USB 调试模式")
131
+ return
132
+
133
+ if parsed.device:
134
+ # CLI指定的设备
135
+ serials = [d[0] for d in devices]
136
+ if parsed.device not in serials:
137
+ print(f"错误: 指定设备 {parsed.device} 未找到或未连接")
138
+ return
139
+ serial = parsed.device
140
+ _, brand, model = devices[serials.index(serial)]
141
+ set_selected_device(serial)
142
+ print(f"已选择: {brand} {model} ({serial})")
143
+ elif len(devices) == 1:
144
+ # 单设备,自动使用
145
+ serial, brand, model = devices[0]
146
+ set_selected_device(serial)
147
+ print(f"已连接: {brand} {model} ({serial})")
148
+ else:
149
+ # 多设备,交互选择
150
+ print()
151
+ print("已检测到多个设备:")
152
+ for i, (s, brand, model) in enumerate(devices):
153
+ print(f" {i + 1}. {s} {brand} {model}")
154
+ while True:
155
+ try:
156
+ default = "1"
157
+ user_input = input(f"\n请选择目标设备 [{default}]: ").strip() or default
158
+ idx = int(user_input) - 1
159
+ if 0 <= idx < len(devices):
160
+ break
161
+ print("无效序号,请重试")
162
+ except (ValueError, EOFError, KeyboardInterrupt):
163
+ print("\n取消")
164
+ return
165
+ serial, brand, model = devices[idx]
166
+ set_selected_device(serial)
167
+ print(f"已选择: {brand} {model} ({serial})")
168
+
119
169
  packages = scan_packages_with_labels()
120
170
  if not packages:
121
171
  return
@@ -177,31 +227,46 @@ def run(args: list[str] | None = None):
177
227
  else:
178
228
  failed.append((p.label, "拉取失败"))
179
229
  else:
180
- # Split APK - pull all then merge
181
- print(f"\n检测到 split APK (共 {len(apk_paths)} 个分片),正在拉取并合并...")
182
- tmp_apks: list[str] = []
183
- tmp_dir = os.path.join(parsed.dest_dir, f".tmp_{sanitize_filename(file_base)}")
184
- try:
185
- os.makedirs(tmp_dir, exist_ok=True)
186
- all_ok = True
230
+ # Split APK
231
+ if parsed.no_merge:
232
+ # No merge - export each split APK directly
233
+ print(f"\n检测到 split APK (共 {len(apk_paths)} 个分片),正在导出所有分片(不合并)...")
187
234
  for i, apk_path in enumerate(apk_paths):
188
- tmp_file = os.path.join(tmp_dir, f"split_{i}.apk")
189
- if not pull_apk(apk_path, tmp_file):
190
- failed.append((p.label, f"拉取分片 {i+1} 失败"))
191
- all_ok = False
192
- break
193
- tmp_apks.append(tmp_file)
194
-
195
- if all_ok:
196
- if merge_split_apks(tmp_apks, dest_apk):
197
- succeeded.append(p.label)
235
+ if i == 0:
236
+ out_path = dest_apk # base APK: app.apk
198
237
  else:
199
- failed.append((p.label, "合并 APK 失败"))
200
- finally:
201
- # Cleanup temp files
202
- import shutil
203
- if os.path.isdir(tmp_dir):
204
- shutil.rmtree(tmp_dir, ignore_errors=True)
238
+ out_path = os.path.join(parsed.dest_dir, f"{sanitize_filename(file_base)}-split-{i}.apk")
239
+ if pull_apk(apk_path, out_path):
240
+ print(f" 已导出: {os.path.basename(out_path)}")
241
+ else:
242
+ failed.append((p.label, f"导出分片 {i+1} 失败"))
243
+ succeeded.append(p.label)
244
+ else:
245
+ # Merge - pull all to temp dir, merge, then cleanup
246
+ print(f"\n检测到 split APK (共 {len(apk_paths)} 个分片),正在拉取并合并...")
247
+ tmp_apks: list[str] = []
248
+ tmp_dir = os.path.join(parsed.dest_dir, f".tmp_{sanitize_filename(file_base)}")
249
+ try:
250
+ os.makedirs(tmp_dir, exist_ok=True)
251
+ all_ok = True
252
+ for i, apk_path in enumerate(apk_paths):
253
+ tmp_file = os.path.join(tmp_dir, f"split_{i}.apk")
254
+ if not pull_apk(apk_path, tmp_file):
255
+ failed.append((p.label, f"拉取分片 {i+1} 失败"))
256
+ all_ok = False
257
+ break
258
+ tmp_apks.append(tmp_file)
259
+
260
+ if all_ok:
261
+ if merge_split_apks(tmp_apks, dest_apk):
262
+ succeeded.append(p.label)
263
+ else:
264
+ failed.append((p.label, "合并 APK 失败"))
265
+ finally:
266
+ # Cleanup temp files
267
+ import shutil
268
+ if os.path.isdir(tmp_dir):
269
+ shutil.rmtree(tmp_dir, ignore_errors=True)
205
270
 
206
271
  print()
207
272
  if succeeded:
File without changes
@@ -0,0 +1,5 @@
1
+ import sys
2
+ from devtools.tools.url2md.cli import run
3
+
4
+ if __name__ == "__main__":
5
+ run(sys.argv[1:])
@@ -0,0 +1,111 @@
1
+ """CLI entry point for url2md."""
2
+ import argparse
3
+ import re
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from devtools.tools.url2md.core import fetch_and_convert
8
+
9
+ INVALID_FILENAME_CHARS = re.escape(r'/\:*?"<>|')
10
+
11
+
12
+ def run(args: list[str] | None = None):
13
+ if args is None:
14
+ args = sys.argv[1:]
15
+
16
+ parser = argparse.ArgumentParser(
17
+ prog="python -m devtools url2md",
18
+ description="Convert URLs to markdown files",
19
+ )
20
+ parser.add_argument(
21
+ "-o", "--output",
22
+ type=Path,
23
+ default=None,
24
+ metavar="DIR",
25
+ help="Output directory (default: current directory)",
26
+ )
27
+ parser.add_argument(
28
+ "urls",
29
+ nargs="+",
30
+ metavar="URL-OR-FILE",
31
+ help="URL or file containing URLs (one per line)",
32
+ )
33
+
34
+ ns = parser.parse_args(args)
35
+
36
+ # Resolve all URLs
37
+ all_urls: list[str] = []
38
+ for item in ns.urls:
39
+ path = Path(item)
40
+ if path.exists() and path.is_file():
41
+ lines = path.read_text(encoding="utf-8").splitlines()
42
+ for line in lines:
43
+ line = line.strip()
44
+ if line:
45
+ all_urls.append(line)
46
+ else:
47
+ all_urls.append(item)
48
+
49
+ if not all_urls:
50
+ print("No URLs to process.", file=sys.stderr)
51
+ sys.exit(1)
52
+
53
+ # Create output dir if specified
54
+ output_dir = ns.output or Path(".")
55
+ if output_dir != Path("."):
56
+ output_dir.mkdir(parents=True, exist_ok=True)
57
+
58
+ succeeded: list[str] = []
59
+ failed: list[tuple[str, str]] = []
60
+
61
+ print(f"Processing {len(all_urls)} URL(s)...")
62
+
63
+ for url in all_urls:
64
+ try:
65
+ result = fetch_and_convert(url)
66
+ filename = _sanitize_filename(result["title"]) or _url_to_fallback_title(url)
67
+ if not filename.endswith(".md"):
68
+ filename += ".md"
69
+
70
+ date_line = ""
71
+ if result["date"]:
72
+ date_line = f"\nPublished: {result['date']}"
73
+
74
+ content = (
75
+ f"# {result['title']}\n\n"
76
+ f"{result['body_md']}\n\n"
77
+ f"---\n\n"
78
+ f"Source: [{result['source_url']}]({result['source_url']}){date_line}\n"
79
+ )
80
+
81
+ out_path = output_dir / filename
82
+ out_path.write_text(content, encoding="utf-8")
83
+ succeeded.append(url)
84
+ print(f" OK: {url} -> {out_path.name}")
85
+
86
+ except Exception as e:
87
+ failed.append((url, str(e)))
88
+ print(f" FAIL: {url} — {e}", file=sys.stderr)
89
+
90
+ # Summary
91
+ total = len(succeeded) + len(failed)
92
+ print(f"\nProcessed {total} URL(s): {len(succeeded)} succeeded, {len(failed)} failed")
93
+
94
+ if failed:
95
+ print("\nFailed URLs:")
96
+ for url, err in failed:
97
+ print(f" {url}: {err}")
98
+ sys.exit(1)
99
+
100
+
101
+ def _sanitize_filename(title: str) -> str:
102
+ # Replace any char that's invalid in Windows/Unix filenames
103
+ return re.sub(r'[/\\:*?"<>|]', "_", title).strip()
104
+
105
+
106
+ def _url_to_fallback_title(url: str) -> str:
107
+ from urllib.parse import urlparse
108
+ parsed = urlparse(url)
109
+ path = parsed.path.strip("/").replace("/", "_")
110
+ host = parsed.netloc.replace(":", "_")
111
+ return f"{host}_{path}" if path else host
@@ -0,0 +1,92 @@
1
+ """Core logic for fetching and converting URLs to markdown."""
2
+ import re
3
+ from datetime import datetime
4
+ from typing import Optional
5
+
6
+ import requests
7
+ requests.packages.urllib3.disable_warnings()
8
+ from bs4 import BeautifulSoup
9
+ from markdownify import markdownify
10
+
11
+
12
+ def fetch_and_convert(url: str) -> dict:
13
+ """
14
+ Fetch a URL and convert it to markdown.
15
+
16
+ Returns:
17
+ dict with keys: title, body_md, source_url, date, url
18
+ date is None if not found.
19
+ """
20
+ response = requests.get(url, timeout=15, verify=False)
21
+ response.raise_for_status()
22
+ response.encoding = response.apparent_encoding or "utf-8"
23
+
24
+ soup = BeautifulSoup(response.text, "html.parser")
25
+
26
+ title = _extract_title(soup)
27
+ body_md = _extract_body(soup)
28
+ date = _extract_date(soup)
29
+
30
+ return {
31
+ "title": title,
32
+ "body_md": body_md,
33
+ "source_url": url,
34
+ "date": date,
35
+ "url": url,
36
+ }
37
+
38
+
39
+ def _extract_title(soup: BeautifulSoup) -> str:
40
+ # Try <title> tag first
41
+ title_tag = soup.find("title")
42
+ if title_tag and title_tag.get_text(strip=True):
43
+ return title_tag.get_text(strip=True)
44
+
45
+ # Fallback: first <h1>
46
+ h1 = soup.find("h1")
47
+ if h1 and h1.get_text(strip=True):
48
+ return h1.get_text(strip=True)
49
+
50
+ return ""
51
+
52
+
53
+ def _extract_body(soup: BeautifulSoup) -> str:
54
+ # markdownify converts the whole document, which is fine for v1
55
+ return markdownify(str(soup))
56
+
57
+
58
+ def _extract_date(soup: BeautifulSoup) -> Optional[str]:
59
+ # Try <time> element with datetime attribute
60
+ time_tag = soup.find("time")
61
+ if time_tag and time_tag.get("datetime"):
62
+ try:
63
+ dt = datetime.fromisoformat(time_tag["datetime"].strip())
64
+ return dt.strftime("%Y-%m-%d")
65
+ except ValueError:
66
+ pass
67
+
68
+ # Try meta tags
69
+ for meta_name in ("datePublished", "pubdate", "article:published_time"):
70
+ meta = soup.find("meta", attrs={"property": meta_name}) or soup.find("meta", attrs={"name": meta_name})
71
+ if meta and meta.get("content"):
72
+ return _parse_date(meta["content"])
73
+
74
+ # Try first paragraph with a date pattern
75
+ first_p = soup.find("p")
76
+ if first_p:
77
+ text = first_p.get_text()
78
+ date_match = re.search(r"(\d{4}[-/]\d{2}[-/]\d{2})", text)
79
+ if date_match:
80
+ return _parse_date(date_match.group(1))
81
+
82
+ return None
83
+
84
+
85
+ def _parse_date(date_str: str) -> Optional[str]:
86
+ """Try to parse a date string into YYYY-MM-DD."""
87
+ for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%d-%m-%Y", "%d/%m/%Y"):
88
+ try:
89
+ return datetime.strptime(date_str.strip(), fmt).strftime("%Y-%m-%d")
90
+ except ValueError:
91
+ pass
92
+ return None
@@ -1,24 +1,24 @@
1
- [project]
2
- name = "damon-devtools"
3
- version = "0.4.0"
4
- description = "Developer CLI tools — portkill, removebg, apkpull"
5
- readme = "README.md"
6
- requires-python = ">=3.10"
7
- dependencies = ["psutil>=5.9"]
8
- license = {text = "MIT"}
9
- authors = [
10
- {name = "damon"},
11
- ]
12
- keywords = ["cli", "devtools", "android", "apk", "adb"]
13
-
14
- [project.scripts]
15
- devtools = "devtools.__main__:main"
16
-
17
- [tool.setuptools.packages.find]
18
- where = ["."]
19
- include = ["devtools*"]
20
- exclude = ["dotfiles*", "tests*", "build*", "dist*"]
21
-
22
- [build-system]
23
- requires = ["setuptools>=61.0"]
24
- build-backend = "setuptools.build_meta"
1
+ [project]
2
+ name = "damon-devtools"
3
+ version = "0.6.0"
4
+ description = "Developer CLI tools — portkill, removebg, apkpull"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = ["psutil>=5.9", "markdownify", "beautifulsoup4", "requests"]
8
+ license = {text = "MIT"}
9
+ authors = [
10
+ {name = "damon"},
11
+ ]
12
+ keywords = ["cli", "devtools", "android", "apk", "adb"]
13
+
14
+ [project.scripts]
15
+ devtools = "devtools.__main__:main"
16
+
17
+ [tool.setuptools.packages.find]
18
+ where = ["."]
19
+ include = ["devtools*"]
20
+ exclude = ["dotfiles*", "tests*", "build*", "dist*"]
21
+
22
+ [build-system]
23
+ requires = ["setuptools>=61.0"]
24
+ build-backend = "setuptools.build_meta"
@@ -1 +0,0 @@
1
- psutil>=5.9
File without changes
File without changes