bcmd 0.6.5__tar.gz → 0.6.7__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.

Potentially problematic release.


This version of bcmd might be problematic. Click here for more details.

Files changed (44) hide show
  1. {bcmd-0.6.5 → bcmd-0.6.7}/PKG-INFO +1 -1
  2. bcmd-0.6.7/bcmd/common/secret.py +51 -0
  3. {bcmd-0.6.5/bcmdx → bcmd-0.6.7/bcmd}/tasks/bin.py +13 -14
  4. {bcmd-0.6.5/bcmdx → bcmd-0.6.7/bcmd}/tasks/image.py +72 -0
  5. {bcmd-0.6.5/bcmdx → bcmd-0.6.7/bcmd}/tasks/lib.py +6 -5
  6. bcmd-0.6.7/bcmd/utils/tkUtil.py +243 -0
  7. {bcmd-0.6.5 → bcmd-0.6.7}/bcmd.egg-info/PKG-INFO +1 -1
  8. bcmd-0.6.7/bcmd.egg-info/SOURCES.txt +60 -0
  9. bcmd-0.6.7/bcmd.egg-info/entry_points.txt +2 -0
  10. bcmd-0.6.7/bcmd.egg-info/top_level.txt +3 -0
  11. {bcmd-0.6.5 → bcmd-0.6.7}/pyproject.toml +5 -3
  12. bcmd-0.6.7/test/conftest.py +17 -0
  13. bcmd-0.6.7/test/test_pdf.py +47 -0
  14. bcmd-0.6.7/test/test_proxy.py +8 -0
  15. bcmd-0.6.7/test/test_wasabi.py +28 -0
  16. bcmd-0.6.5/bcmd.egg-info/SOURCES.txt +0 -33
  17. bcmd-0.6.5/bcmd.egg-info/entry_points.txt +0 -2
  18. bcmd-0.6.5/bcmd.egg-info/top_level.txt +0 -1
  19. bcmd-0.6.5/bcmdx/common/password.py +0 -35
  20. bcmd-0.6.5/bcmdx/utils/tkUtil.py +0 -130
  21. {bcmd-0.6.5 → bcmd-0.6.7}/MANIFEST.in +0 -0
  22. {bcmd-0.6.5 → bcmd-0.6.7}/README.md +0 -0
  23. /bcmd-0.6.5/bcmdx/main.py → /bcmd-0.6.7/bcmd/__init__.py +0 -0
  24. {bcmd-0.6.5/bcmdx → bcmd-0.6.7/bcmd/common}/__init__.py +0 -0
  25. {bcmd-0.6.5/bcmdx → bcmd-0.6.7/bcmd}/common/func.py +0 -0
  26. {bcmd-0.6.5/bcmdx → bcmd-0.6.7/bcmd}/resources/project/main.py +0 -0
  27. {bcmd-0.6.5/bcmdx → bcmd-0.6.7/bcmd}/tasks/__init__.py +0 -0
  28. {bcmd-0.6.5/bcmdx → bcmd-0.6.7/bcmd}/tasks/code.py +0 -0
  29. {bcmd-0.6.5/bcmdx → bcmd-0.6.7/bcmd}/tasks/crypto.py +0 -0
  30. {bcmd-0.6.5/bcmdx → bcmd-0.6.7/bcmd}/tasks/debian.py +0 -0
  31. {bcmd-0.6.5/bcmdx → bcmd-0.6.7/bcmd}/tasks/download.py +0 -0
  32. {bcmd-0.6.5/bcmdx → bcmd-0.6.7/bcmd}/tasks/json.py +0 -0
  33. {bcmd-0.6.5/bcmdx → bcmd-0.6.7/bcmd}/tasks/math.py +0 -0
  34. {bcmd-0.6.5/bcmdx → bcmd-0.6.7/bcmd}/tasks/mirror.py +0 -0
  35. {bcmd-0.6.5/bcmdx → bcmd-0.6.7/bcmd}/tasks/pdf.py +0 -0
  36. {bcmd-0.6.5/bcmdx → bcmd-0.6.7/bcmd}/tasks/proxy.py +0 -0
  37. {bcmd-0.6.5/bcmdx → bcmd-0.6.7/bcmd}/tasks/time.py +0 -0
  38. {bcmd-0.6.5/bcmdx → bcmd-0.6.7/bcmd}/tasks/upgrade.py +0 -0
  39. {bcmd-0.6.5/bcmdx → bcmd-0.6.7/bcmd}/tasks/wasabi.py +0 -0
  40. {bcmd-0.6.5/bcmdx/common → bcmd-0.6.7/bcmd/utils}/__init__.py +0 -0
  41. {bcmd-0.6.5 → bcmd-0.6.7}/bcmd.egg-info/dependency_links.txt +0 -0
  42. {bcmd-0.6.5 → bcmd-0.6.7}/bcmd.egg-info/requires.txt +0 -0
  43. {bcmd-0.6.5 → bcmd-0.6.7}/setup.cfg +0 -0
  44. {bcmd-0.6.5/bcmdx/utils → bcmd-0.6.7/test}/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bcmd
3
- Version: 0.6.5
3
+ Version: 0.6.7
4
4
  Summary: Commands for Beni
5
5
  Author-email: Beni Mang <benimang@126.com>
6
6
  Maintainer-email: Beni Mang <benimang@126.com>
@@ -0,0 +1,51 @@
1
+ import getpass
2
+ from typing import Any, TypedDict, cast
3
+
4
+ from async_lru import alru_cache
5
+ from beni import bcrypto, btask
6
+ from beni.bcolor import printRed
7
+
8
+
9
+ class PypiSecret(TypedDict):
10
+ username: str
11
+ password: str
12
+
13
+
14
+ @alru_cache
15
+ async def getPypi(value: str = '') -> PypiSecret:
16
+ return cast(
17
+ Any,
18
+ _getData(value or 'QbuF2mV/lqovtF5dskZGD7qHknYbNuF2QseWRtWxLZTPrC/jL1tcxV8JEKaRjLsu46PxJZ7zepJwggnUTIWnEAoV5VtgP2/hbuzxxHha8817kR5c65H9fXm8eOal7DYXsUoGPQMnm59UWNXUKjmIaP4sn9nySFlRYqa8sEZSbYQ4N0NL35Dpj1e3wyQxJ+7h2jwKAz50Hh8G4yAM3/js9+NUe4ymts+UXcwsP3ADIBMkzjnFc0lEYg2d+fw0A74XWCvoZPoGqHZR/THUOVNAYxoGgDzP4SPIk1XsmtpxvfO/DpJd/Cg/0fB3MYagGKI1+m6Bxqhvd1I/lf0YbM5y4E4='),
19
+ )
20
+
21
+
22
+ class QiniuSecret(TypedDict):
23
+ bucket: str
24
+ baseUrl: str
25
+ ak: str
26
+ sk: str
27
+
28
+
29
+ @alru_cache
30
+ async def getQiniu(value: str = '') -> QiniuSecret:
31
+ return cast(
32
+ Any,
33
+ _getData(value or 'vNroFKeKklrdcJ89suFm+iyuJsq/cyUB5+QWoeeiMc/J0oSLF9cg5rqbK1IRxF0cCQ8KmkQQhdVa+PI6kuTBhoSH6IviVTylzAOrJywEccz9jWkJkW28Y9Vo4ePZmfWf/j7wdxNB144z234KD8IxJn4lR2A0L9JN5kk1o1/hpcydXL74FNtt03lYL/E3WVcvpUfw37mri2HMYOfUw81dRwW35/hMuQjtq1BBrKrIsSKTHH44tROMcgyvt+Qy292AtDBcsYiZxBKhQtBFPMq/vUs='),
34
+ )
35
+
36
+
37
+ def _getData(content: str) -> dict[str, Any]:
38
+ index = content.find(' ')
39
+ if index > -1:
40
+ tips = f'请输入密码({content[:index]}):'
41
+ else:
42
+ tips = '请输入密码:'
43
+ while True:
44
+ try:
45
+ pwd = getpass.getpass(tips)
46
+ return bcrypto.decryptJson(content, pwd)
47
+ except KeyboardInterrupt:
48
+ print('')
49
+ btask.abort('用户操作取消')
50
+ except BaseException:
51
+ printRed('密码错误')
@@ -9,7 +9,7 @@ from beni.bqiniu import QiniuBucket
9
9
  from beni.btype import Null
10
10
  from prettytable import PrettyTable
11
11
 
12
- from ..common import password
12
+ from ..common import secret
13
13
 
14
14
  app: Final = btask.newSubApp('bin 工具')
15
15
 
@@ -22,6 +22,7 @@ async def download(
22
22
  names: list[str] = typer.Argument(None, help="支持多个"),
23
23
  file: Path = typer.Option(None, '--file', '-f', help="文件形式指定参数,行为单位"),
24
24
  output: Path = typer.Option(Path.cwd(), '--output', '-o', help="本地保存路径"),
25
+ secretValue: str = typer.Option('', '--secret', '-s', help='密钥信息'),
25
26
  ):
26
27
  '从七牛云下载执行文件'
27
28
  bucket: QiniuBucket = Null
@@ -36,16 +37,18 @@ async def download(
36
37
  bcolor.printYellow(f'已存在 {binFile}')
37
38
  else:
38
39
  key = f'bin/{target}.zip'
39
- bucket = bucket or await _getBucket()
40
+ bucket = bucket or await _getBucket(secretValue)
40
41
  await bucket.downloadPrivateFileUnzip(key, output)
41
42
  bcolor.printGreen(f'added {binFile}')
42
43
 
43
44
 
44
45
  @app.command('list')
45
46
  @syncCall
46
- async def getList():
47
+ async def getList(
48
+ secretValue: str = typer.Option('', '--secret', '-s', help='密钥信息'),
49
+ ):
47
50
  '列出可下载的文件'
48
- bucket = await _getBucket()
51
+ bucket = await _getBucket(secretValue)
49
52
  datas = (await bucket.getFileList(_PREFIX, limit=1000))[0]
50
53
  datas = [x for x in datas if x.key != _PREFIX and x.key.endswith('.zip')]
51
54
  datas.sort(key=lambda x: x.time, reverse=True)
@@ -68,9 +71,10 @@ async def getList():
68
71
  async def upload(
69
72
  file: Path = typer.Argument(..., help="本地文件路径"),
70
73
  force: bool = typer.Option(False, '--force', '-f', help="强制覆盖"),
74
+ secretValue: str = typer.Option('', '--secret', '-s', help='密钥信息'),
71
75
  ):
72
76
  '上传'
73
- bucket = await _getBucket()
77
+ bucket = await _getBucket(secretValue)
74
78
  key = f'{_PREFIX}{file.name}.zip'
75
79
  if not force:
76
80
  if await bucket.getFileStatus(key):
@@ -85,8 +89,9 @@ async def upload(
85
89
  @syncCall
86
90
  async def remove(
87
91
  key: str = typer.Argument(..., help="云端文件key"),
92
+ secretValue: str = typer.Option('', '--secret', '-s', help='密钥信息'),
88
93
  ):
89
- bucket = await _getBucket()
94
+ bucket = await _getBucket(secretValue)
90
95
  await bucket.deleteFiles(f'{_PREFIX}{key}.zip')
91
96
  bcolor.printGreen('OK')
92
97
 
@@ -94,11 +99,5 @@ async def remove(
94
99
  # ------------------------------------------------------------------------------------
95
100
 
96
101
 
97
- async def _getBucket():
98
- ak, sk = await password.getQiniu()
99
- return QiniuBucket(
100
- 'pytask',
101
- 'http://qiniu-cdn.pytask.com',
102
- ak,
103
- sk,
104
- )
102
+ async def _getBucket(secretValue: str):
103
+ return QiniuBucket(**await secret.getQiniu())
@@ -1,6 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
4
  import os
3
5
  import random
6
+ import tkinter as tk
4
7
  from enum import StrEnum
5
8
  from pathlib import Path
6
9
  from typing import Final, List, Tuple
@@ -13,6 +16,9 @@ from beni.bfunc import syncCall
13
16
  from beni.btype import Null, XPath
14
17
  from PIL import Image, ImageDraw, ImageFont
15
18
 
19
+ from bcmd.utils.tkUtil import TkForm, setWidgetEnabled
20
+ from tkinter import messagebox
21
+
16
22
  app: Final = btask.newSubApp('图片工具集')
17
23
 
18
24
 
@@ -302,3 +308,69 @@ async def merge(
302
308
  _merge_images(image_files, output_image)
303
309
  bcolor.printMagenta(output_image)
304
310
  bcolor.printGreen('OK')
311
+
312
+
313
+ @app.command()
314
+ @syncCall
315
+ async def xone():
316
+ def showGui():
317
+ app = TkForm()
318
+ app.title('图片操作')
319
+ entry = app.addEntry('保存文件', tk.StringVar(value=r'C:\project\docs\source\docs\public\icon.webp'), width=60)
320
+ app.addEntry('输入密码', tk.StringVar(), password=True, width=60, command=lambda: messagebox.showerror('错误', '密码错误'))
321
+
322
+ scrolltextVar = tk.StringVar(value='xxiioo')
323
+ app.addScrolledText('测试信箱', scrolltextVar)
324
+
325
+
326
+ radioVarScale = app.addRadioBtnList(
327
+ '缩放操作',
328
+ [
329
+ '保持',
330
+ '缩放比',
331
+ '指定宽度',
332
+ '指定高度',
333
+ ],
334
+ selectedIndex=0,
335
+ onChanged=lambda x: onScaleValueChanged(x),
336
+ )
337
+
338
+ scaleValueEntry = app.addEntry('缩放参数', tk.StringVar(), width=10, justify=tk.CENTER)
339
+
340
+ app.addChoisePath('选择文件', tk.StringVar(), isDir=True, focus=True)
341
+
342
+ radioVarFormat = app.addRadioBtnList(
343
+ '格式转换',
344
+ [
345
+ '保持',
346
+ 'PNG',
347
+ 'JPEG',
348
+ 'WEBP',
349
+ ],
350
+ selectedIndex=0,
351
+ )
352
+ app.addCheckBox('去除透明', '去除透明', False)
353
+ app.addCheckBox('TinyPng', 'TinyPng', True)
354
+ app.addCheckBoxList('其他选项', [
355
+ ('去除透明', False),
356
+ ('TinyPng', True),
357
+ ])
358
+
359
+ def onScaleValueChanged(value: str):
360
+ match value:
361
+ case '保持':
362
+ setWidgetEnabled(scaleValueEntry, False)
363
+ case _:
364
+ setWidgetEnabled(scaleValueEntry, True)
365
+
366
+ app.addBtn('确定', lambda: onBtn())
367
+
368
+ def onBtn():
369
+ nonlocal result
370
+ app.destroy()
371
+
372
+ result: str = ''
373
+ app.run()
374
+ return result
375
+
376
+ showGui()
@@ -6,9 +6,9 @@ import typer
6
6
  from beni import bcolor, bfile, bpath, brun, btask
7
7
  from beni.bfunc import syncCall
8
8
 
9
- from bcmdx.utils.tkUtil import TkForm
9
+ from bcmd.utils.tkUtil import TkForm
10
10
 
11
- from ..common import password
11
+ from ..common import secret
12
12
 
13
13
  app: Final = btask.newSubApp('lib 工具')
14
14
 
@@ -35,7 +35,7 @@ async def update_version(
35
35
  app = TkForm()
36
36
  app.title('bcmd 版本更新')
37
37
  app.addLabel('当前版本号', latestVersion)
38
- version_var = app.addRadioBtn(
38
+ version_var = app.addRadioBtnList(
39
39
  '请选择新版本',
40
40
  versionList,
41
41
  selectedIndex=-1,
@@ -80,14 +80,15 @@ async def update_version(
80
80
  @syncCall
81
81
  async def build(
82
82
  path: Path = typer.Argument(Path.cwd(), help='workspace 路径'),
83
+ secretValue: str = typer.Option('', '--secret', '-s', help='密钥信息'),
83
84
  ):
84
85
  '发布项目'
85
- user, pwd = await password.getPypi()
86
+ data = await secret.getPypi(secretValue)
86
87
  bpath.remove(path / 'dist')
87
88
  bpath.remove(
88
89
  *list(path.glob('*.egg-info'))
89
90
  )
90
91
  with bpath.changePath(path):
91
92
  await brun.run(f'uv build')
92
- await brun.run(f'uv publish -u {user} -p {pwd}')
93
+ await brun.run(f'uv publish -u {data['username']} -p {data['password']}')
93
94
  bcolor.printGreen('OK')
@@ -0,0 +1,243 @@
1
+ import tkinter as tk
2
+ from tkinter import filedialog
3
+ from tkinter.scrolledtext import ScrolledText
4
+ from typing import Any, Callable, Literal, TypeVar, Union
5
+ from uuid import uuid4
6
+
7
+ RADIO_NOTHING = uuid4().hex
8
+ TkVar = TypeVar('TkVar', bound=Union[tk.StringVar, tk.IntVar, tk.DoubleVar, tk.BooleanVar])
9
+
10
+
11
+ class TkForm(tk.Tk):
12
+
13
+ _rowIndex = -1
14
+ _initList: list[Callable[..., None]] = []
15
+ _varList: list[tk.Variable] = [] # 用来存储 var 变量,避免传出去,外面没有接收导致界面异常
16
+
17
+ def __init__(self):
18
+ super().__init__()
19
+ self.resizable(False, False)
20
+ self.bind("<Map>", self._onInit)
21
+
22
+ def _onInit(self, evt: tk.Event):
23
+ if evt.widget == self:
24
+ for callback in self._initList:
25
+ callback()
26
+ self._initList.clear()
27
+
28
+ def addInitHandler(self, handler: Callable[..., None]):
29
+ self._initList.append(handler)
30
+
31
+ def _initVar(self, var: TkVar) -> TkVar:
32
+ self._varList.append(var)
33
+ return var
34
+
35
+ def run(self):
36
+ self.center()
37
+ self.mainloop()
38
+
39
+ def center(self):
40
+ self.withdraw() # 先隐藏窗口,避免闪动
41
+ self.update_idletasks() # 确保获取正确的窗口尺寸
42
+ width = self.winfo_width() # 获取窗口宽度
43
+ height = self.winfo_height() # 获取窗口高度
44
+ screen_width = self.winfo_screenwidth() # 屏幕宽度
45
+ screen_height = self.winfo_screenheight() # 屏幕高度
46
+ x = (screen_width - width) // 2 # 水平居中
47
+ y = (screen_height - height) // 2 # 垂直居中
48
+ self.geometry(f"+{x}+{y}") # 设置窗口位置
49
+ self.deiconify() # 恢复显示窗口
50
+
51
+ def addRow(self, desc: str, widget: tk.Widget):
52
+ self._rowIndex += 1
53
+ tk.Label(text=desc).grid(row=self._rowIndex, column=0, padx=10, pady=5, sticky='e')
54
+ widget.grid(row=self._rowIndex, column=1, padx=10, pady=5, sticky='w')
55
+
56
+ def addRowFrame(self):
57
+ self._rowIndex += 1
58
+ frame = tk.Frame(self)
59
+ frame.grid(row=self._rowIndex, column=0, columnspan=2, padx=10, pady=5)
60
+ return frame
61
+
62
+ def addRowFrameWithDesc(self, desc: str):
63
+ self._rowIndex += 1
64
+ tk.Label(text=desc).grid(row=self._rowIndex, column=0, padx=10, pady=5, sticky='e')
65
+ frame = tk.Frame(self)
66
+ frame.grid(row=self._rowIndex, column=1, padx=10, pady=5, sticky='w')
67
+ return frame
68
+
69
+ def addLabel(
70
+ self,
71
+ desc: str,
72
+ text: str
73
+ ):
74
+ self.addRow(desc, tk.Label(text=text))
75
+
76
+ def addBtn(
77
+ self,
78
+ label: str,
79
+ command: Callable[..., None],
80
+ *,
81
+ width: int = 20,
82
+ focus: bool = False
83
+ ):
84
+ frame = self.addRowFrame()
85
+ btn = tk.Button(frame, text=label, width=width, command=command)
86
+ btn.pack(side="left", expand=True, padx=15)
87
+ if focus:
88
+ self._initFocus(btn)
89
+
90
+ def addRadioBtnList(
91
+ self,
92
+ desc: str,
93
+ selectionList: list[str],
94
+ *,
95
+ selectedIndex: int | None = None,
96
+ focusIndex: int | None = None,
97
+ onChanged: Callable[[str], None] | None = None,
98
+ ):
99
+ frame = tk.Frame()
100
+ self.addRow(desc, frame)
101
+ var = tk.StringVar(value=selectionList[selectedIndex] if selectedIndex is not None else RADIO_NOTHING)
102
+ radioBtnList: list[tk.Radiobutton] = []
103
+ for version in selectionList:
104
+ radioBtn = tk.Radiobutton(frame, text=version, variable=var, value=version)
105
+ radioBtn.pack(side="left", padx=(0, 15))
106
+ setWidgetClickFocus(radioBtn)
107
+ radioBtnList.append(radioBtn)
108
+ if focusIndex is not None:
109
+ self._initFocus(radioBtnList[focusIndex])
110
+ if onChanged:
111
+ var.trace_add('write', lambda *args: onChanged(var.get())) # type: ignore
112
+ self.addInitHandler(
113
+ lambda: onChanged(var.get())
114
+ )
115
+ return var
116
+
117
+ def addEntry(
118
+ self,
119
+ desc: str,
120
+ var: tk.StringVar,
121
+ *,
122
+ width: int = 60,
123
+ focus: bool = False,
124
+ justify: Literal['left', 'right', 'center'] = tk.LEFT,
125
+ password: bool = False,
126
+ command: Callable[..., Any] | None = None,
127
+ ):
128
+ self._initVar(var)
129
+ entry = tk.Entry(self, width=width, justify=justify, textvariable=var)
130
+ entry.icursor(tk.END)
131
+ self.addRow(desc, entry)
132
+ if password:
133
+ entry.config(show='*')
134
+ if focus:
135
+ self._initFocus(entry)
136
+ if command:
137
+ entry.bind('<Return>', lambda event: command())
138
+ return entry
139
+
140
+ def addChoisePath(
141
+ self,
142
+ desc: str,
143
+ var: tk.StringVar,
144
+ *,
145
+ width: int = 47,
146
+ focus: bool = False,
147
+ isDir: bool = False,
148
+ ):
149
+ self._initVar(var)
150
+ frame = self.addRowFrameWithDesc(desc)
151
+ entry = tk.Entry(frame, width=width, textvariable=var)
152
+ entry.icursor(tk.END)
153
+ entry.pack(side="left")
154
+ btn = tk.Button(frame, text=f'选择{'目录' if isDir else '文件'} ...', width=10, command=lambda: onBtn())
155
+ btn.pack(side="left", padx=(10, 0))
156
+ if focus:
157
+ self._initFocus(btn)
158
+
159
+ def onBtn():
160
+ if isDir:
161
+ var.set(filedialog.askdirectory())
162
+ else:
163
+ var.set(filedialog.askopenfilename())
164
+
165
+ def addScrolledText(
166
+ self,
167
+ desc: str,
168
+ var: tk.StringVar,
169
+ *,
170
+ width: int = 60,
171
+ height: int = 3,
172
+ focus: bool = False,
173
+ ):
174
+ self._initVar(var)
175
+ scrolledText = ScrolledText(self, width=width, height=height)
176
+ scrolledText.insert(tk.END, var.get())
177
+ self.addRow(desc, scrolledText)
178
+
179
+ def on_text_change(*args: Any):
180
+ new_value = scrolledText.get("1.0", tk.END)
181
+ if new_value != var.get():
182
+ var.set(new_value)
183
+
184
+ scrolledText.bind("<KeyRelease>", on_text_change)
185
+
186
+ def on_tab(event: tk.Event):
187
+ widget = event.widget.tk_focusNext()
188
+ assert widget
189
+ widget.focus_set()
190
+ return "break"
191
+
192
+ scrolledText.bind("<Tab>", on_tab)
193
+
194
+ if focus:
195
+ self._initFocus(scrolledText)
196
+ return scrolledText
197
+
198
+ def addCheckBox(
199
+ self,
200
+ desc: str,
201
+ text: str,
202
+ value: bool = False,
203
+ ):
204
+ var = tk.BooleanVar(value=value)
205
+ self._initVar(var)
206
+ check_btn = tk.Checkbutton(text=text, variable=var)
207
+ self.addRow(desc, check_btn)
208
+ self.addInitHandler(
209
+ lambda: var.set(var.get())
210
+ )
211
+ setWidgetClickFocus(check_btn)
212
+ return var
213
+
214
+ def addCheckBoxList(
215
+ self,
216
+ desc: str,
217
+ dataList: list[tuple[str, bool]],
218
+ ):
219
+ varDict: dict[str, tk.BooleanVar] = {}
220
+ frame = tk.Frame(self)
221
+ self.addRow(desc, frame)
222
+ for label, value in dataList:
223
+ varDict[label] = tk.BooleanVar(value=value)
224
+ checkbox = tk.Checkbutton(frame, text=label, variable=varDict[label])
225
+ checkbox.pack(side="left", expand=True, padx=(0, 15))
226
+ setWidgetClickFocus(checkbox)
227
+ self.addInitHandler(
228
+ lambda: varDict[label].set(varDict[label].get())
229
+ )
230
+ return varDict
231
+
232
+ def _initFocus(self, widget: tk.Widget):
233
+ self.addInitHandler(
234
+ lambda: widget.focus_set()
235
+ )
236
+
237
+
238
+ def setWidgetEnabled(widget: tk.Widget, value: bool):
239
+ widget['state'] = tk.NORMAL if value else tk.DISABLED
240
+
241
+
242
+ def setWidgetClickFocus(widget: tk.Widget):
243
+ widget.bind("<Button-1>", lambda args: args.widget.focus_set())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bcmd
3
- Version: 0.6.5
3
+ Version: 0.6.7
4
4
  Summary: Commands for Beni
5
5
  Author-email: Beni Mang <benimang@126.com>
6
6
  Maintainer-email: Beni Mang <benimang@126.com>
@@ -0,0 +1,60 @@
1
+ MANIFEST.in
2
+ README.md
3
+ pyproject.toml
4
+ ./bcmd/__init__.py
5
+ ./bcmd/common/__init__.py
6
+ ./bcmd/common/func.py
7
+ ./bcmd/common/secret.py
8
+ ./bcmd/resources/project/main.py
9
+ ./bcmd/tasks/__init__.py
10
+ ./bcmd/tasks/bin.py
11
+ ./bcmd/tasks/code.py
12
+ ./bcmd/tasks/crypto.py
13
+ ./bcmd/tasks/debian.py
14
+ ./bcmd/tasks/download.py
15
+ ./bcmd/tasks/image.py
16
+ ./bcmd/tasks/json.py
17
+ ./bcmd/tasks/lib.py
18
+ ./bcmd/tasks/math.py
19
+ ./bcmd/tasks/mirror.py
20
+ ./bcmd/tasks/pdf.py
21
+ ./bcmd/tasks/proxy.py
22
+ ./bcmd/tasks/time.py
23
+ ./bcmd/tasks/upgrade.py
24
+ ./bcmd/tasks/wasabi.py
25
+ ./bcmd/utils/__init__.py
26
+ ./bcmd/utils/tkUtil.py
27
+ ./test/__init__.py
28
+ ./test/conftest.py
29
+ ./test/test_pdf.py
30
+ ./test/test_proxy.py
31
+ ./test/test_wasabi.py
32
+ bcmd/__init__.py
33
+ bcmd.egg-info/PKG-INFO
34
+ bcmd.egg-info/SOURCES.txt
35
+ bcmd.egg-info/dependency_links.txt
36
+ bcmd.egg-info/entry_points.txt
37
+ bcmd.egg-info/requires.txt
38
+ bcmd.egg-info/top_level.txt
39
+ bcmd/common/__init__.py
40
+ bcmd/common/func.py
41
+ bcmd/common/secret.py
42
+ bcmd/resources/project/main.py
43
+ bcmd/tasks/__init__.py
44
+ bcmd/tasks/bin.py
45
+ bcmd/tasks/code.py
46
+ bcmd/tasks/crypto.py
47
+ bcmd/tasks/debian.py
48
+ bcmd/tasks/download.py
49
+ bcmd/tasks/image.py
50
+ bcmd/tasks/json.py
51
+ bcmd/tasks/lib.py
52
+ bcmd/tasks/math.py
53
+ bcmd/tasks/mirror.py
54
+ bcmd/tasks/pdf.py
55
+ bcmd/tasks/proxy.py
56
+ bcmd/tasks/time.py
57
+ bcmd/tasks/upgrade.py
58
+ bcmd/tasks/wasabi.py
59
+ bcmd/utils/__init__.py
60
+ bcmd/utils/tkUtil.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ beni = bcmd:run
@@ -0,0 +1,3 @@
1
+ bcmd
2
+ dist
3
+ test
@@ -3,7 +3,7 @@
3
3
 
4
4
  [project]
5
5
  name = 'bcmd'
6
- version = '0.6.5'
6
+ version = '0.6.7'
7
7
  description = 'Commands for Beni'
8
8
  requires-python = '>=3.10'
9
9
  keywords = ['benimang', 'beni', 'bcmd']
@@ -33,12 +33,14 @@ full = [
33
33
  ]
34
34
 
35
35
  [project.scripts]
36
- beni = 'bcmdx.main:run'
36
+ beni = 'bcmd:run'
37
37
 
38
38
  [tool.uv]
39
39
  package = true
40
40
 
41
- # 使用默认镜像地址
41
+ [tool.setuptools]
42
+ package-dir = { "" = "." }
43
+
42
44
  [[tool.uv.index]]
43
45
  url = "https://mirrors.aliyun.com/pypi/simple"
44
46
  default = true
@@ -0,0 +1,17 @@
1
+ # '''
2
+ # 快捷键(默认)
3
+ # CTRL+; A 执行全部单元测试
4
+ # CTRL+; E 只执行上次出错的用例
5
+ # CTRL+; C 清除结果
6
+ # CTRL+; CTRL+A 调试全部单元测试
7
+ # CTRL+; CTRL+E 只调试上次出错的用例
8
+ # '''
9
+
10
+ import pytest_asyncio
11
+
12
+ from bcmd.tasks import *
13
+
14
+
15
+ @pytest_asyncio.fixture(scope='session', autouse=True) # type: ignore
16
+ def prepareSession():
17
+ yield
@@ -0,0 +1,47 @@
1
+ from contextlib import asynccontextmanager
2
+ from pathlib import Path
3
+ from typing import Literal
4
+
5
+ import img2pdf
6
+ import pytest
7
+ from beni import bfile, bpath, btask
8
+ from PIL import Image
9
+
10
+
11
+ @pytest.mark.asyncio
12
+ async def test_pdf_output_images():
13
+ async with _createTempPdfFile() as pdfFile:
14
+ result = btask.testCall('pdf', 'output-images', '--target', pdfFile.as_posix())
15
+ assert result.exit_code == 0
16
+ outputImagesPath = pdfFile.parent / f'{pdfFile.stem}-PDF图片文件'
17
+ assert outputImagesPath.is_dir()
18
+ assert len(list(outputImagesPath.glob('*.png'))) == 1
19
+ assert len(list(outputImagesPath.glob('*.jpeg'))) == 1
20
+
21
+
22
+ @asynccontextmanager
23
+ async def _createTempPdfFile():
24
+
25
+ def create_color_block_image(
26
+ file: Path,
27
+ color: tuple[int, int, int],
28
+ size: tuple[int, int] = (500, 500),
29
+ image_format: Literal['JPEG', 'PNG'] = 'JPEG'
30
+ ):
31
+ image = Image.new('RGB', size, color)
32
+ image.save(file, format=image_format)
33
+ image.close()
34
+ return file
35
+
36
+ with bpath.useTempPath(True) as tempPath:
37
+ # 创建色块图片
38
+ fileList = [
39
+ create_color_block_image(tempPath / 'blue.png', (0, 0, 255), image_format='PNG'),
40
+ create_color_block_image(tempPath / 'red.jpeg', (255, 0, 0), image_format='JPEG'),
41
+ ]
42
+
43
+ # 生成PDF文件
44
+ pdfFile = tempPath / 'output.pdf'
45
+ await bfile.writeBytes(pdfFile, img2pdf.convert(fileList)) # type: ignore
46
+
47
+ yield pdfFile
@@ -0,0 +1,8 @@
1
+ import pytest
2
+ from beni import btask
3
+
4
+
5
+ @pytest.mark.asyncio
6
+ async def test_proxy():
7
+ result = btask.testCall('proxy', '--help')
8
+ assert result.exit_code == 0
@@ -0,0 +1,28 @@
1
+ from uuid import uuid4
2
+
3
+ import pytest
4
+ from beni import bfile, bpath, btask
5
+
6
+
7
+ @pytest.mark.asyncio
8
+ async def test_wasabi():
9
+ with bpath.useTempPath() as tempPath:
10
+ target = tempPath / uuid4().hex
11
+ file = target / uuid4().hex
12
+ content = uuid4().hex
13
+ password = uuid4().hex
14
+ await bfile.writeText(file, content)
15
+
16
+ result = btask.testCall('wasabi', 'zip', target.as_posix(), '--password', password)
17
+ assert result.exit_code == 0
18
+ assert target.is_file(), '生成加密文件失败'
19
+
20
+ newPassword = uuid4().hex
21
+ result = btask.testCall('wasabi', 'change-pass', target.as_posix(), '--password', password, '--new-password', newPassword)
22
+ assert result.exit_code == 0
23
+ assert target.is_file(), '修改密码失败'
24
+
25
+ result = btask.testCall('wasabi', 'unzip', target.as_posix(), '--password', newPassword)
26
+ assert result.exit_code == 0
27
+ assert target.is_dir(), '解密文件失败'
28
+ assert await bfile.readText(file) == content, '解密文件内容不一致'
@@ -1,33 +0,0 @@
1
- MANIFEST.in
2
- README.md
3
- pyproject.toml
4
- bcmd.egg-info/PKG-INFO
5
- bcmd.egg-info/SOURCES.txt
6
- bcmd.egg-info/dependency_links.txt
7
- bcmd.egg-info/entry_points.txt
8
- bcmd.egg-info/requires.txt
9
- bcmd.egg-info/top_level.txt
10
- bcmdx/__init__.py
11
- bcmdx/main.py
12
- bcmdx/common/__init__.py
13
- bcmdx/common/func.py
14
- bcmdx/common/password.py
15
- bcmdx/resources/project/main.py
16
- bcmdx/tasks/__init__.py
17
- bcmdx/tasks/bin.py
18
- bcmdx/tasks/code.py
19
- bcmdx/tasks/crypto.py
20
- bcmdx/tasks/debian.py
21
- bcmdx/tasks/download.py
22
- bcmdx/tasks/image.py
23
- bcmdx/tasks/json.py
24
- bcmdx/tasks/lib.py
25
- bcmdx/tasks/math.py
26
- bcmdx/tasks/mirror.py
27
- bcmdx/tasks/pdf.py
28
- bcmdx/tasks/proxy.py
29
- bcmdx/tasks/time.py
30
- bcmdx/tasks/upgrade.py
31
- bcmdx/tasks/wasabi.py
32
- bcmdx/utils/__init__.py
33
- bcmdx/utils/tkUtil.py
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- beni = bcmdx.main:run
@@ -1 +0,0 @@
1
- bcmdx
@@ -1,35 +0,0 @@
1
- import getpass
2
- from typing import Any
3
-
4
- from async_lru import alru_cache
5
- from beni import bcrypto
6
-
7
-
8
- @alru_cache
9
- async def getPypi() -> tuple[str, str]:
10
- content = 'QbuF2mV/lqovtF5dskZGD7qHknYbNuF2QseWRtWxLZTPrC/jL1tcxV8JEKaRjLsu46PxJZ7zepJwggnUTIWnEAoV5VtgP2/hbuzxxHha8817kR5c65H9fXm8eOal7DYXsUoGPQMnm59UWNXUKjmIaP4sn9nySFlRYqa8sEZSbYQ4N0NL35Dpj1e3wyQxJ+7h2jwKAz50Hh8G4yAM3/js9+NUe4ymts+UXcwsP3ADIBMkzjnFc0lEYg2d+fw0A74XWCvoZPoGqHZR/THUOVNAYxoGgDzP4SPIk1XsmtpxvfO/DpJd/Cg/0fB3MYagGKI1+m6Bxqhvd1I/lf0YbM5y4E4='
11
- data = _getData(content)
12
- return data['username'], data['password']
13
-
14
-
15
- @alru_cache
16
- async def getQiniu() -> tuple[str, str]:
17
- content = '7xOuA0FPCndTWcWmWLbqklQTqLTAhuEw9CarRTBYhWQ/g8wPxktw6VAiu50TLv49D1L8oCVfGafsowYDZw/prF6NQwCluPcCMy5JfdC9sKauvuZa51Nsf6PTR1UIyU8ZLUSzH+Ec2Ufcz/yAZCrcAtn63zMHNu3tTAVcZNPL597lSHdSRkpmDR8CaoUh/raH/Q=='
18
- data = _getData(content)
19
- return data['ak'], data['sk']
20
-
21
-
22
- def _getData(content: str) -> dict[str, Any]:
23
- index = content.find(' ')
24
- if index > -1:
25
- tips = f'请输入密码({content[:index]}):'
26
- else:
27
- tips = '请输入密码:'
28
- while True:
29
- try:
30
- pwd = getpass.getpass(tips)
31
- return bcrypto.decryptJson(content, pwd)
32
- except KeyboardInterrupt:
33
- raise Exception('操作取消')
34
- except BaseException:
35
- pass
@@ -1,130 +0,0 @@
1
- import tkinter as tk
2
- from tkinter.scrolledtext import ScrolledText
3
- from typing import Callable
4
- from uuid import uuid4
5
-
6
- # version 2026-0621
7
-
8
-
9
- class TkForm(tk.Tk):
10
-
11
- _row = -1
12
- _initFocusItem: tk.Widget | None = None
13
-
14
- RADIO_NOTHING = uuid4().hex
15
-
16
- def __init__(self):
17
- super().__init__()
18
- self.resizable(False, False)
19
- self.bind("<Map>", self._onMap)
20
-
21
- def _onMap(self, event: tk.Event):
22
- if self._initFocusItem:
23
- self._initFocusItem.focus_set()
24
- self._initFocusItem = None
25
-
26
- def add(self, desc: str, widget: tk.Widget):
27
- self._row += 1
28
- tk.Label(text=desc).grid(row=self._row, column=0, padx=10, pady=5, sticky='n')
29
- widget.grid(row=self._row, column=1, padx=10, pady=5, sticky='w')
30
-
31
- def addFrame(self, frame: tk.Frame):
32
- self._row += 1
33
- frame.grid(row=self._row, column=0, columnspan=2, padx=10, pady=5)
34
-
35
- def run(self):
36
- self.center()
37
- self.mainloop()
38
-
39
- def center(self):
40
- self.withdraw() # 先隐藏窗口,避免闪动
41
- self.update_idletasks() # 确保获取正确的窗口尺寸
42
- width = self.winfo_width() # 获取窗口宽度
43
- height = self.winfo_height() # 获取窗口高度
44
- screen_width = self.winfo_screenwidth() # 屏幕宽度
45
- screen_height = self.winfo_screenheight() # 屏幕高度
46
- x = (screen_width - width) // 2 # 水平居中
47
- y = (screen_height - height) // 2 # 垂直居中
48
- self.geometry(f"+{x}+{y}") # 设置窗口位置
49
- self.deiconify() # 恢复显示窗口
50
-
51
- def addLabel(self, desc: str, text: str):
52
- self.add(desc, tk.Label(text=text))
53
-
54
- def addBtn(self, label: str, command: Callable[..., None], *, width: int = 20, focus: bool = False):
55
- frame = tk.Frame(self)
56
- self.addFrame(frame)
57
- btn = tk.Button(frame, text=label, width=width, command=command)
58
- btn.pack(side="left", expand=True, padx=15)
59
- if focus:
60
- self._initFocusItem = btn
61
-
62
- def addRadioBtn(self, desc: str, selectionList: list[str], *, selectedIndex: int | None = None, focusIndex: int | None = None):
63
- frame = tk.Frame()
64
- self.add(desc, frame)
65
- var = tk.StringVar(value=selectionList[selectedIndex] if selectedIndex is not None else self.RADIO_NOTHING)
66
- radioBtnList: list[tk.Radiobutton] = []
67
- for version in selectionList:
68
- radioBtn = tk.Radiobutton(frame, text=version, variable=var, value=version)
69
- radioBtn.pack(side="left", padx=(0, 15))
70
- radioBtnList.append(radioBtn)
71
- if focusIndex is not None:
72
- self._initFocusItem = radioBtnList[focusIndex]
73
- return var
74
-
75
- def addEntry(self, desc: str, text: str = '', *, width: int = 60, focus: bool = False):
76
- entry = tk.Entry(self, width=30)
77
- entry.insert(0, text)
78
- self.add(desc, entry)
79
- if focus:
80
- self._initFocusItem = entry
81
- return entry
82
-
83
- def addScrolledText(self, desc: str, text: str = '', *, width: int = 60, height: int = 20, focus: bool = False):
84
- scrolledText = ScrolledText(self, width=width, height=height)
85
- scrolledText.insert("1.0", text)
86
- self.add(desc, scrolledText)
87
- if focus:
88
- self._initFocusItem = scrolledText
89
- return scrolledText
90
-
91
- def addCheckBox(self, text: str, value: bool = False):
92
- remember_var = tk.BooleanVar(value=value)
93
- check_btn = tk.Checkbutton(text=text, variable=remember_var)
94
- self.add("", check_btn)
95
- return remember_var
96
-
97
- def addPasswordInput(self, desc: str, *, width: int = 60, command: Callable[..., None] | None = None):
98
- password_entry = tk.Entry(self, show="*", width=width) # 使用 show="*" 隐藏输入内容
99
- if command:
100
- password_entry.bind('<Return>', lambda event: command())
101
- self.add(desc, password_entry)
102
- return password_entry
103
-
104
-
105
- ''' 例子
106
- app = TkForm()
107
- app.title('更新版本信息')
108
- app.addLabel('当前版本号', '3.11.8')
109
- app.addLabel('上次更新时间', '2025-06-16 16:00:00')
110
- version_var = app.addRadioBtn(
111
- '请选择新版本',
112
- ['3.11.8', '3.11.9', '3.11.10'],
113
- 2,
114
- )
115
- log_text = app.addScrolledText('更新日志', '123\n23454asd这个')
116
- vara = app.addCheckBox('记住密码', False)
117
- varb = app.addCheckBox('阿萨德法师大润发', True)
118
- passwordInput = app.addPasswordInput('密码', command=lambda: onBtn())
119
-
120
-
121
- def onBtn():
122
- print(version_var.get())
123
- print(log_text.get('1.0', "end-1c").split('\n'))
124
- app.destroy()
125
-
126
-
127
- app.addBtn('确定', onBtn)
128
- app.bind("<Visibility>", lambda e: passwordInput.focus_set())
129
- app.run()
130
- '''
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes