bcmd 0.0.66__py3-none-any.whl → 0.5.2__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.

Potentially problematic release.


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

bcmd/common/func.py ADDED
@@ -0,0 +1,19 @@
1
+ import importlib.resources
2
+ from contextlib import contextmanager
3
+ from pathlib import Path
4
+
5
+ from beni import btask
6
+
7
+
8
+ def checkFileOrNotExists(file: Path):
9
+ btask.assertTrue(file.is_file() or not file.exists(), f'必须是文件 {file}')
10
+
11
+
12
+ def checkPathOrNotExists(folder: Path):
13
+ btask.assertTrue(folder.is_dir() or not folder.exists(), f'必须是目录 {folder}')
14
+
15
+
16
+ @contextmanager
17
+ def useResources(name: str):
18
+ with importlib.resources.path('bcmd.resources', name) as target:
19
+ yield target
bcmd/main.py CHANGED
@@ -6,5 +6,5 @@ from .tasks import *
6
6
 
7
7
 
8
8
  def run():
9
- btask.options.lock = ''
9
+ btask.options.lock = 0
10
10
  asyncio.run(btask.main())
@@ -0,0 +1,3 @@
1
+ __pycache__
2
+ *~$*
3
+ venv/
@@ -0,0 +1,15 @@
1
+ {
2
+ // Use IntelliSense to learn about possible attributes.
3
+ // Hover to view descriptions of existing attributes.
4
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
+ "version": "0.2.0",
6
+ "configurations": [
7
+ {
8
+ "name": "Python: main.py",
9
+ "type": "debugpy",
10
+ "request": "launch",
11
+ // "console": "internalConsole",
12
+ "program": "${workspaceFolder}/main.py"
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "files.exclude": {
3
+ ".pytest_cache": true,
4
+ "**/__pycache__": true,
5
+ ".gitignore": true,
6
+ ".venv": true,
7
+ ".venv-lock": true,
8
+ "venv": true,
9
+ },
10
+ "python.defaultInterpreterPath": "${workspaceFolder}/venv/Scripts/python.exe",
11
+ }
@@ -0,0 +1,68 @@
1
+ {
2
+ "version": "2.0.0",
3
+ "tasks": [
4
+ {
5
+ "label": "git commit",
6
+ "problemMatcher": [],
7
+ "command": "TortoiseGitProc.exe",
8
+ "args": [
9
+ "/command:commit",
10
+ "/path:${workspaceFolder}/",
11
+ ],
12
+ },
13
+ {
14
+ "label": "git commit file",
15
+ "problemMatcher": [],
16
+ "command": "TortoiseGitProc.exe",
17
+ "args": [
18
+ "/command:commit",
19
+ "/path:${file}",
20
+ ],
21
+ },
22
+ {
23
+ "label": "git revert",
24
+ "problemMatcher": [],
25
+ "command": "TortoiseGitProc.exe",
26
+ "args": [
27
+ "/command:revert",
28
+ "/path:${workspaceFolder}/",
29
+ ],
30
+ },
31
+ {
32
+ "label": "git revert file",
33
+ "problemMatcher": [],
34
+ "command": "TortoiseGitProc.exe",
35
+ "args": [
36
+ "/command:revert",
37
+ "/path:${file}",
38
+ ],
39
+ },
40
+ {
41
+ "label": "git sync",
42
+ "problemMatcher": [],
43
+ "command": "TortoiseGitProc.exe",
44
+ "args": [
45
+ "/command:sync",
46
+ "/path:${workspaceFolder}/",
47
+ ],
48
+ },
49
+ {
50
+ "label": "git log",
51
+ "problemMatcher": [],
52
+ "command": "TortoiseGitProc.exe",
53
+ "args": [
54
+ "/command:log",
55
+ "/path:${workspaceFolder}/",
56
+ ],
57
+ },
58
+ {
59
+ "label": "git log file",
60
+ "problemMatcher": [],
61
+ "command": "TortoiseGitProc.exe",
62
+ "args": [
63
+ "/command:log",
64
+ "/path:${file}",
65
+ ],
66
+ },
67
+ ],
68
+ }
@@ -0,0 +1 @@
1
+ print('OK')
bcmd/tasks/__init__.py CHANGED
@@ -1,15 +1,15 @@
1
1
  # type: ignore
2
2
  from . import bin
3
+ from . import code
3
4
  from . import crypto
4
5
  from . import debian
5
6
  from . import download
7
+ from . import image
6
8
  from . import json
7
- from . import jwt
8
9
  from . import lib
9
10
  from . import math
10
11
  from . import mirror
12
+ from . import project
11
13
  from . import proxy
12
- from . import task
13
- from . import temp
14
14
  from . import time
15
15
  from . import venv
bcmd/tasks/bin.py CHANGED
@@ -1,16 +1,15 @@
1
- import os
2
1
  from datetime import datetime
3
2
  from pathlib import Path
4
3
  from typing import Final
5
4
 
6
5
  import typer
7
6
  from beni import bcolor, bfile, bpath, btask, bzip
8
- from beni.bfunc import syncCall
7
+ from beni.bfunc import syncCall, textToAry
9
8
  from beni.bqiniu import QiniuBucket
10
9
  from beni.btype import Null
11
10
  from prettytable import PrettyTable
12
11
 
13
- from bcmd.common import password
12
+ from ..common import password
14
13
 
15
14
  app: Final = btask.newSubApp('bin 工具')
16
15
 
@@ -20,24 +19,18 @@ _PREFIX = 'bin/'
20
19
  @app.command()
21
20
  @syncCall
22
21
  async def download(
23
- names: str = typer.Argument(None, help="如果有多个使用,分割"),
22
+ names: list[str] = typer.Argument(None, help="支持多个"),
24
23
  file: Path = typer.Option(None, '--file', '-f', help="文件形式指定参数,行为单位"),
25
- output: Path = typer.Option(None, '--output', '-o', help="本地保存路径"),
24
+ output: Path = typer.Option(Path.cwd(), '--output', '-o', help="本地保存路径"),
26
25
  ):
27
26
  '从七牛云下载执行文件'
28
27
  bucket: QiniuBucket = Null
29
- if not output:
30
- output = Path(os.curdir)
31
- output = output.resolve()
32
- targetList: list[str] = []
33
- if names:
34
- targetList.extend(names.split(','))
35
28
  if file:
36
29
  content = await bfile.readText(Path(file))
37
- targetList.extend(content.split('\n'))
38
- targetList = [x.strip() for x in targetList]
39
- targetList = [x for x in targetList if x]
40
- for target in targetList:
30
+ names.extend(
31
+ textToAry(content)
32
+ )
33
+ for target in names:
41
34
  binFile = output / target
42
35
  if binFile.exists():
43
36
  bcolor.printYellow(f'已存在 {binFile}')
@@ -55,6 +48,7 @@ async def getList():
55
48
  bucket = await _getBucket()
56
49
  datas = (await bucket.getFileList(_PREFIX, limit=1000))[0]
57
50
  datas = [x for x in datas if x.key != _PREFIX and x.key.endswith('.zip')]
51
+ datas.sort(key=lambda x: x.time, reverse=True)
58
52
  table = PrettyTable()
59
53
  table.add_column(
60
54
  bcolor.yellow('文件名称'),
@@ -97,6 +91,9 @@ async def remove(
97
91
  bcolor.printGreen('OK')
98
92
 
99
93
 
94
+ # ------------------------------------------------------------------------------------
95
+
96
+
100
97
  async def _getBucket():
101
98
  ak, sk = await password.getQiniu()
102
99
  return QiniuBucket(
bcmd/tasks/code.py ADDED
@@ -0,0 +1,96 @@
1
+ from pathlib import Path
2
+ from typing import Final
3
+
4
+ import typer
5
+ from beni import bfile, bpath, btask
6
+ from beni.bcolor import printGreen, printMagenta, printYellow
7
+ from beni.bfunc import syncCall
8
+
9
+ app: Final = btask.newSubApp('code 工具')
10
+
11
+
12
+ @app.command()
13
+ @syncCall
14
+ async def tidy_tasks(
15
+ tasks_path: Path = typer.Argument(Path.cwd(), help="tasks 路径"),
16
+ ):
17
+ '整理 task 项目中的 tasks/__init__.py'
18
+
19
+ initFile = tasks_path / '__init__.py'
20
+ btask.assertTrue(initFile.is_file(), '文件不存在', initFile)
21
+ files = bpath.listFile(tasks_path)
22
+ files = [x for x in files if not x.name.startswith('_')]
23
+ contents = [f'from . import {x.stem}' for x in files]
24
+ contents.insert(0, '# type: ignore')
25
+ contents.append('')
26
+ content = '\n'.join(contents)
27
+ oldContent = await bfile.readText(initFile)
28
+ if oldContent != content:
29
+ await bfile.writeText(
30
+ initFile,
31
+ content,
32
+ )
33
+ printYellow(initFile)
34
+ printMagenta(content)
35
+ printGreen('OK')
36
+
37
+
38
+ @app.command()
39
+ @syncCall
40
+ async def tidy_modules(
41
+ modules_path: Path = typer.Argument(Path.cwd(), help="modules_path 路径"),
42
+ ):
43
+ '整理 fastapi 项目中的 modules/__init__.py'
44
+
45
+ importContents: list[str] = []
46
+ managerContents: list[str] = []
47
+
48
+ xxdict: dict[str, set[Path]] = {}
49
+ for file in sorted(modules_path.glob('**/*Manager.py')):
50
+ if file.parent == modules_path:
51
+ subName = '.'
52
+ elif file.parent.parent == modules_path:
53
+ subName = f'.{file.parent.stem}'
54
+ else:
55
+ continue
56
+ xxdict.setdefault(subName, set()).add(file)
57
+ for subName in sorted(xxdict.keys()):
58
+ files = sorted(xxdict[subName])
59
+ importContents.append(f'from {subName} import {", ".join([x.stem for x in files])}')
60
+ managerContents.extend([f' {x.stem},' for x in sorted([y for x in xxdict.values() for y in x])])
61
+
62
+ managerContents = [x for x in managerContents if x]
63
+ contents = [
64
+ '\n'.join(importContents),
65
+ 'managers = [\n' + '\n'.join(managerContents) + '\n]',
66
+ ]
67
+ content = '\n\n'.join(contents) + '\n'
68
+ file = modules_path / '__init__.py'
69
+ printYellow(str(file))
70
+ printMagenta(content)
71
+ await bfile.writeText(file, content)
72
+ printGreen('OK')
73
+
74
+
75
+ @app.command()
76
+ @syncCall
77
+ async def gen_init_py(
78
+ workspace_path: Path = typer.Argument(Path.cwd(), help='workspace 路径'),
79
+ ):
80
+ '递归生成 __init__.py 文件'
81
+
82
+ async def makeInitFiles(p: Path):
83
+ if p.name == '__pycache__':
84
+ return
85
+ if p.name.startswith('.'):
86
+ return
87
+ if workspace_path != p:
88
+ initFile = p / '__init__.py'
89
+ if not initFile.exists():
90
+ printYellow(initFile)
91
+ await bfile.writeText(initFile, '')
92
+ for x in bpath.listDir(p):
93
+ await makeInitFiles(x)
94
+
95
+ await makeInitFiles(workspace_path)
96
+ printGreen('OK')
bcmd/tasks/crypto.py CHANGED
@@ -104,6 +104,9 @@ async def decrypt_file(
104
104
  bcolor.printGreen('OK')
105
105
 
106
106
 
107
+ # ------------------------------------------------------------------------------------
108
+
109
+
107
110
  def _genPassword():
108
111
  password = ''
109
112
  while not password:
bcmd/tasks/download.py CHANGED
@@ -2,38 +2,73 @@ import asyncio
2
2
  import os
3
3
  from pathlib import Path
4
4
  from typing import Final
5
+ from urllib.parse import urlparse
6
+ from uuid import uuid4
5
7
 
6
8
  import pyperclip
7
9
  import typer
8
- from beni import bcolor, bhttp, binput, bpath, btask
9
- from beni.bfunc import syncCall
10
+ from beni import bcolor, bfile, bhttp, binput, bpath, btask
11
+ from beni.bfunc import syncCall, textToAry
10
12
 
11
- app: Final = btask.newSubApp('下载')
13
+ app: Final = btask.app
12
14
 
13
15
 
14
16
  @app.command()
15
17
  @syncCall
16
- async def urls(
17
- path: Path = typer.Option(None, '--path', '-p', help='指定路径,默认当前目录'),
18
+ async def download(
19
+ url_file: Path = typer.Option(None, '--file', '-f', help='需要下载的url文件路径,默认使用剪贴板内容'),
20
+ save_path: Path = typer.Option(None, '--path', '-p', help='下载存放目录,默认当前目录'),
21
+ keep_directory: bool = typer.Option(False, '--keep', '-k', help='保持原始目录结构,默认不保持'),
18
22
  ):
19
- '下载文件'
20
- path = path or Path(os.getcwd())
21
- content = pyperclip.paste()
22
- urlSet = set([x.strip() for x in content.strip().split('\n') if x.strip()])
23
+ '下载资源资源文件'
24
+ save_path = save_path or Path(os.getcwd())
25
+
26
+ if url_file:
27
+ if not url_file.exists():
28
+ btask.abort('指定文件不存在', url_file)
29
+ content = await bfile.readText(url_file)
30
+ else:
31
+ content = pyperclip.paste()
32
+ urlSet = set(textToAry(content))
23
33
 
24
34
  for i, url in enumerate(urlSet):
25
35
  print(f'{i + 1}. {url}')
26
- print(f'输出目录:{path}')
36
+ print(f'输出目录:{save_path}')
27
37
  await binput.confirm('是否确认?')
28
38
 
39
+ fileSet: set[Path] = set()
40
+ retryUrlSet: set[str] = set()
41
+
29
42
  async def download(url: str):
30
- file = bpath.get(path, '/'.join([x for x in url.replace('://', '/').split('/') if x]))
43
+ urlPath = urlparse(url).path
44
+ if keep_directory:
45
+ file = bpath.get(save_path, '/'.join([x for x in urlPath.split('/') if x]))
46
+ else:
47
+ file = save_path / Path(urlPath).name
48
+ if file in fileSet:
49
+ file = file.with_stem(f'{file.stem}--{uuid4()}')
50
+ fileSet.add(file)
31
51
  try:
32
52
  bcolor.printGreen(url)
33
53
  await bhttp.download(url, file)
34
54
  except:
55
+ retryUrlSet.add(url)
35
56
  bcolor.printRed(url)
36
57
 
37
58
  await asyncio.gather(*[download(x) for x in urlSet])
38
59
 
39
- bcolor.printYellow('Done')
60
+ for i in range(4):
61
+ if i > 0:
62
+ print(f'等待重试第 {i} 次')
63
+ await asyncio.sleep(3)
64
+ await asyncio.gather(*[download(x) for x in urlSet])
65
+ if not retryUrlSet:
66
+ break
67
+ urlSet = set(retryUrlSet)
68
+ retryUrlSet.clear()
69
+
70
+ if retryUrlSet:
71
+ pyperclip.copy('\n'.join(retryUrlSet))
72
+ bcolor.printYellow('部分下载失败,失败部分已复制到剪贴板')
73
+ else:
74
+ bcolor.printGreen('OK')
bcmd/tasks/image.py ADDED
@@ -0,0 +1,201 @@
1
+ import asyncio
2
+ import os
3
+ import random
4
+ from enum import StrEnum
5
+ from pathlib import Path
6
+ from typing import Final
7
+
8
+ import httpx
9
+ import typer
10
+ from beni import bcolor, bfile, bhttp, block, bpath, btask
11
+ from beni.bbyte import BytesReader, BytesWriter
12
+ from beni.bfunc import syncCall
13
+ from beni.btype import Null, XPath
14
+ from PIL import Image
15
+
16
+ app: Final = btask.newSubApp('图片工具集')
17
+
18
+
19
+ class _OutputType(StrEnum):
20
+ '输出类型'
21
+ normal = '0'
22
+ replace_ = '1'
23
+ crc_replace = '2'
24
+
25
+
26
+ @app.command()
27
+ @syncCall
28
+ async def convert(
29
+ path: Path = typer.Option(None, '--path', '-p', help='指定目录或具体图片文件,默认当前目录'),
30
+ src_format: str = typer.Option('jpg|jpeg|png', '--src-format', '-s', help='如果path是目录,指定源格式,可以指定多个,默认值:jpg|jpeg|png'),
31
+ dst_format: str = typer.Option('webp', '--dst-format', '-d', help='目标格式,只能是单个'),
32
+ rgb: bool = typer.Option(False, '--rgb', help='转换为RGB格式'),
33
+ quality: int = typer.Option(85, '--quality', '-q', help='图片质量,0-100,默认85'),
34
+ output_type: _OutputType = typer.Option(_OutputType.normal, '--output-type', help='输出类型,0:普通输出,1:删除源文件,2:输出文件使用CRC32命名并删除源文件'),
35
+ ):
36
+ '图片格式转换'
37
+ path = path or Path(os.getcwd())
38
+ fileList: list[Path] = []
39
+ if path.is_file():
40
+ fileList.append(path)
41
+ elif path.is_dir():
42
+ extNameList = [x for x in src_format.strip().split('|')]
43
+ fileList = [x for x in bpath.listFile(path, True) if x.suffix[1:].lower() in extNameList]
44
+ if not fileList:
45
+ return bcolor.printRed(f'未找到图片文件({path})')
46
+ for file in fileList:
47
+ with Image.open(file) as img:
48
+ if rgb:
49
+ img = img.convert('RGB')
50
+ with bpath.useTempFile() as tempFile:
51
+ img.save(tempFile, format=dst_format, quality=quality)
52
+ outputFile = file.with_suffix(f'.{dst_format}')
53
+ if output_type == _OutputType.crc_replace:
54
+ outputFile = outputFile.with_stem(await bfile.crc(tempFile))
55
+ bpath.copy(tempFile, outputFile)
56
+ if output_type in [_OutputType.replace_, _OutputType.crc_replace]:
57
+ if outputFile != file:
58
+ bpath.remove(file)
59
+ bcolor.printGreen(f'{file} -> {outputFile}')
60
+
61
+
62
+ # ------------------------------------------------------------------------------------
63
+
64
+ @app.command()
65
+ @syncCall
66
+ async def tiny(
67
+ path: Path = typer.Option(None, '--path', help='指定目录或具体图片文件,默认当前目录'),
68
+ compression: int = typer.Option(75, '--compression', help='如果压缩小于指定数值则使用原来的图片,单位:%,默认75'),
69
+ isKeepOriginal: bool = typer.Option(False, '--keep-original', help='保留原始图片'),
70
+ ):
71
+
72
+ keyList = [
73
+ 'MB3QmtvZ8HKRkXcDnxhWCNTXzvx6cNF3',
74
+ '7L7X2CJ35GM1bChSHdT14yZPLx7FlpNk',
75
+ 'q8YLcvrXVW2NYcr5mMyzQhsSHF4j7gny',
76
+ ]
77
+ random.shuffle(keyList)
78
+ await block.setLimit(_TinyFile.runTiny, len(keyList))
79
+
80
+ # 整理文件列表
81
+ fileList = []
82
+ path = path or Path(os.getcwd())
83
+ if path.is_file():
84
+ fileList.append(path)
85
+ elif path.is_dir():
86
+ fileList = [x for x in bpath.listFile(path, True) if x.suffix[1:].lower() in ['jpg', 'jpeg', 'png']]
87
+ else:
88
+ btask.abort('未找到图片文件', path)
89
+ fileList.sort()
90
+
91
+ # 将文件列表整理成 _TinyFile 对象
92
+ fileList = [_TinyFile(x) for x in fileList]
93
+ await asyncio.gather(*[x.updateInfo() for x in fileList])
94
+
95
+ # 过滤掉已经处理过的图片
96
+ for i in range(len(fileList)):
97
+ file = fileList[i]
98
+ if file.compression == 0:
99
+ # 未处理过的图片,进行图片的压缩处理
100
+ pass
101
+ elif not file.isTiny and file.compression < compression:
102
+ # 之前测试的压缩率不符合要求,不过现在符合了,进行图片的压缩处理
103
+ pass
104
+ else:
105
+ # 要忽略掉的文件
106
+ bcolor.printYellow(f'{file.file}({file.compression}% / 忽略)')
107
+ fileList[i] = Null
108
+ fileList = [x for x in fileList if x]
109
+
110
+ # 开始压缩
111
+ taskList = [
112
+ x.runTiny(keyList[i % len(keyList)], compression, isKeepOriginal)
113
+ for i, x in enumerate(fileList)
114
+ ]
115
+ await asyncio.gather(*taskList)
116
+
117
+
118
+ # ------------------------------------------------------------------------------------
119
+
120
+
121
+ class _TinyFile:
122
+
123
+ _endian: Final = '>'
124
+ _sep = BytesWriter(_endian).writeStr('tiny').writeUint(9527).writeUint(709394).toBytes()
125
+
126
+ @property
127
+ def compression(self):
128
+ return self._compression
129
+
130
+ @property
131
+ def isTiny(self):
132
+ return self._isTiny
133
+
134
+ @property
135
+ def file(self):
136
+ return self._file
137
+
138
+ def __init__(self, file: XPath):
139
+ self._file = file
140
+ self._compression: float = 0.0
141
+ self._isTiny: bool = False
142
+
143
+ async def updateInfo(self):
144
+ fileBytes = await bfile.readBytes(self._file)
145
+ self._compression = 0.0
146
+ self._isTiny = False
147
+ blockAry = fileBytes.split(self._sep)
148
+ if len(blockAry) > 1:
149
+ info = BytesReader(self._endian, blockAry[1])
150
+ size = info.readUint()
151
+ if size == len(blockAry[0]):
152
+ self._compression = round(info.readFloat(), 2)
153
+ self._isTiny = info.readBool()
154
+
155
+ async def _flushInfo(self, compression: float, isTiny: bool):
156
+ self._compression = compression
157
+ self._isTiny = isTiny
158
+ content = await self._getPureContent()
159
+ info = (
160
+ BytesWriter(self._endian)
161
+ .writeUint(len(content))
162
+ .writeFloat(compression)
163
+ .writeBool(isTiny)
164
+ .toBytes()
165
+ )
166
+ content += self._sep + info
167
+ await bfile.writeBytes(self._file, content)
168
+
169
+ async def _getPureContent(self):
170
+ content = await bfile.readBytes(self._file)
171
+ content = content.split(self._sep)[0]
172
+ return content
173
+
174
+ @block.limit(1)
175
+ async def runTiny(self, key: str, compression: float, isKeepOriginal: bool):
176
+ content = await self._getPureContent()
177
+ async with httpx.AsyncClient() as client:
178
+ response = await client.post(
179
+ 'https://api.tinify.com/shrink',
180
+ auth=('api', key),
181
+ content=content,
182
+ timeout=30,
183
+ )
184
+ response.raise_for_status()
185
+ result = response.json()
186
+ outputCompression = round(result['output']['ratio'] * 100, 2)
187
+ if outputCompression < compression:
188
+ # 下载文件
189
+ url = result['output']['url']
190
+ with bpath.useTempFile() as tempFile:
191
+ await bhttp.download(url, tempFile)
192
+ await _TinyFile(tempFile)._flushInfo(outputCompression, True)
193
+ outputFile = bpath.get(self._file)
194
+ if isKeepOriginal:
195
+ outputFile = outputFile.with_stem(f'{outputFile.stem}_tiny')
196
+ bpath.move(tempFile, outputFile, True)
197
+ bcolor.printGreen(f'{outputFile}({outputCompression}% / 已压缩)')
198
+ else:
199
+ # 不进行压缩
200
+ await self._flushInfo(outputCompression, False)
201
+ bcolor.printMagenta(f'{self._file} ({outputCompression}% / 不处理)')