bcmd 0.4.12__tar.gz → 0.5.1__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 (37) hide show
  1. {bcmd-0.4.12 → bcmd-0.5.1}/PKG-INFO +1 -1
  2. bcmd-0.5.1/bcmd/tasks/image.py +201 -0
  3. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/tasks/project.py +5 -3
  4. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/tasks/venv.py +67 -31
  5. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd.egg-info/PKG-INFO +1 -1
  6. {bcmd-0.4.12 → bcmd-0.5.1}/pyproject.toml +1 -1
  7. bcmd-0.4.12/bcmd/tasks/image.py +0 -54
  8. {bcmd-0.4.12 → bcmd-0.5.1}/MANIFEST.in +0 -0
  9. {bcmd-0.4.12 → bcmd-0.5.1}/README.md +0 -0
  10. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/__init__.py +0 -0
  11. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/common/__init__.py +0 -0
  12. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/common/func.py +0 -0
  13. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/common/password.py +0 -0
  14. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/main.py +0 -0
  15. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/resources/project/.gitignore +0 -0
  16. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/resources/project/.vscode/launch.json +0 -0
  17. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/resources/project/.vscode/settings.json +0 -0
  18. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/resources/project/.vscode/tasks.json +0 -0
  19. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/resources/project/main.py +0 -0
  20. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/tasks/__init__.py +0 -0
  21. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/tasks/bin.py +0 -0
  22. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/tasks/code.py +0 -0
  23. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/tasks/crypto.py +0 -0
  24. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/tasks/debian.py +0 -0
  25. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/tasks/download.py +0 -0
  26. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/tasks/json.py +0 -0
  27. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/tasks/lib.py +0 -0
  28. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/tasks/math.py +0 -0
  29. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/tasks/mirror.py +0 -0
  30. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/tasks/proxy.py +0 -0
  31. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd/tasks/time.py +0 -0
  32. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd.egg-info/SOURCES.txt +0 -0
  33. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd.egg-info/dependency_links.txt +0 -0
  34. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd.egg-info/entry_points.txt +0 -0
  35. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd.egg-info/requires.txt +0 -0
  36. {bcmd-0.4.12 → bcmd-0.5.1}/bcmd.egg-info/top_level.txt +0 -0
  37. {bcmd-0.4.12 → bcmd-0.5.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bcmd
3
- Version: 0.4.12
3
+ Version: 0.5.1
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,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(60, '--compression', help='如果压缩尺寸小于指定数值则使用原来的图片,单位:%,默认60'),
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}% / 不处理)')
@@ -2,11 +2,11 @@ from pathlib import Path
2
2
  from typing import Final
3
3
 
4
4
  import typer
5
- from beni import bcolor, bpath, btask
5
+ from beni import bcolor, binput, bpath, btask
6
6
  from beni.bfunc import syncCall
7
7
 
8
8
  from ..common.func import useResources
9
- from .venv import venv
9
+ from .venv import add as venvAdd
10
10
 
11
11
  app: Final = btask.app
12
12
 
@@ -27,6 +27,8 @@ async def _(
27
27
  bcolor.printRed('目标路径不是空目录', path)
28
28
  return
29
29
 
30
- venv(['benimang'], path, False, False, False, False)
30
+ bcolor.printYellow(path)
31
+ await binput.confirm('即将在此路径生成新项目,是否继续?')
32
+ venvAdd(['benimang'], path)
31
33
  with useResources('project') as sourceProjectPath:
32
34
  bpath.copyOverwrite(sourceProjectPath, path)
@@ -7,37 +7,93 @@ from typing import Final
7
7
 
8
8
  import typer
9
9
  from beni import bcolor, bexecute, bfile, bhttp, bpath, btask
10
- from beni.bfunc import syncCall, textToAry
10
+ from beni.bfunc import syncCall
11
11
  from beni.btype import Null
12
12
  from prettytable import PrettyTable
13
13
 
14
- from ..common import password
15
14
  from ..common.func import checkFileOrNotExists, checkPathOrNotExists
16
- from . import bin
17
15
 
18
- app: Final = btask.app
16
+ app: Final = btask.newSubApp('venv 相关')
19
17
 
20
18
 
21
19
  @app.command()
22
20
  @syncCall
23
- async def venv(
21
+ async def add(
24
22
  packages: list[str] = typer.Argument(None),
25
23
  path: Path = typer.Option(None, '--path', help='指定路径,默认当前目录'),
26
24
  isOfficial: bool = typer.Option(False, '--official', help='是否使用官方地址安装(https://pypi.org/simple)'),
27
- isUseBase: bool = typer.Option(False, '--use-base', help='是否强制使用基础库去安装'),
28
- isUseLock: bool = typer.Option(False, '--use-lock', help='是否使用锁定库安装'),
29
- isCleanup: bool = typer.Option(False, '--cleanup', help='是否先清空 venv 目录再执行'),
25
+ ):
26
+ '添加指定库'
27
+ await _venv(
28
+ packages,
29
+ path=path,
30
+ isOfficial=isOfficial,
31
+ )
32
+
33
+
34
+ @app.command()
35
+ @syncCall
36
+ async def benimang(
37
+ path: Path = typer.Option(None, '--path', help='指定路径,默认当前目录'),
38
+ ):
39
+ '更新 benimang 库,强制使用官方源'
40
+ await _venv(
41
+ ['benimang==now'],
42
+ path=path,
43
+ isOfficial=True,
44
+ )
45
+
46
+
47
+ @app.command()
48
+ @syncCall
49
+ async def base(
50
+ path: Path = typer.Option(None, '--path', help='指定路径,默认当前目录'),
51
+ isOfficial: bool = typer.Option(False, '--official', help='是否使用官方地址安装(https://pypi.org/simple)'),
52
+ isReinstall: bool = typer.Option(False, '--reinstall', help='是否清空venv目录后重新安装'),
53
+ ):
54
+ '安装基础库'
55
+ await _venv(
56
+ path=path,
57
+ isOfficial=isOfficial,
58
+ isUseBase=True,
59
+ isCleanup=isReinstall,
60
+ )
61
+
62
+
63
+ @app.command()
64
+ @syncCall
65
+ async def lock(
66
+ path: Path = typer.Option(None, '--path', help='指定路径,默认当前目录'),
67
+ isOfficial: bool = typer.Option(False, '--official', help='是否使用官方地址安装(https://pypi.org/simple)'),
68
+ isReinstall: bool = typer.Option(False, '--reinstall', help='是否清空venv目录后重新安装'),
69
+ ):
70
+ '安装指定版本的库'
71
+ await _venv(
72
+ path=path,
73
+ isOfficial=isOfficial,
74
+ isUseLock=True,
75
+ isCleanup=isReinstall,
76
+ )
77
+
78
+
79
+ # ------------------------------------------------------------------------------------
80
+
81
+ async def _venv(
82
+ packages: list[str] = [],
83
+ *,
84
+ path: Path = Null,
85
+ isOfficial: bool = False,
86
+ isUseBase: bool = False,
87
+ isUseLock: bool = False,
88
+ isCleanup: bool = False,
30
89
  ):
31
90
  'python 虚拟环境配置'
32
91
  btask.assertTrue(not (isUseBase == isUseLock == True), '2个选项只能选择其中一个 --use-base / --use-lock')
33
92
  path = path or Path(os.getcwd())
34
- binPath = path / 'bin'
35
- binListFile = bpath.get(path, 'bin.list')
36
93
  venvPath = bpath.get(path, 'venv')
37
94
  checkPathOrNotExists(venvPath)
38
95
  venvFile = bpath.get(path, '.venv')
39
96
  checkFileOrNotExists(venvFile)
40
- await _inputQiniuPassword(binListFile, binPath)
41
97
  if isCleanup:
42
98
  bpath.remove(venvPath)
43
99
  btask.assertTrue(not venvPath.exists(), f'无法删除 venv 目录 {venvPath}')
@@ -69,20 +125,9 @@ async def venv(
69
125
  basePackages = _mergePackageList(basePackages, packages)
70
126
  lockPackages = (await bfile.readText(tempFile)).strip().split('\n')
71
127
  await updatePackageList(venvFile, basePackages, lockPackages)
72
-
73
- # 下载 bin 文件
74
- if binListFile.exists():
75
- bin.download(
76
- names=Null,
77
- file=binListFile,
78
- output=binPath,
79
- )
80
128
  bcolor.printGreen('OK')
81
129
 
82
130
 
83
- # ------------------------------------------------------------------------------------
84
-
85
-
86
131
  async def _pipInstall(pip: Path, installPackages: list[str], disabled_mirror: bool):
87
132
  python = pip.with_stem('python')
88
133
  btask.assertTrue(python.is_file(), f'无法找到指定文件 {python}')
@@ -170,12 +215,3 @@ def _getPackageName(package: str):
170
215
  if package.startswith('#'):
171
216
  package = package.replace('#', '', 1).strip()
172
217
  return package
173
-
174
-
175
- async def _inputQiniuPassword(binListFile: Path, binPath: Path) -> None:
176
- '根据需要输入七牛云密码'
177
- if binListFile.exists():
178
- aaSet = set(textToAry(await bfile.readText(binListFile)))
179
- bbSet = set([x.name for x in bpath.listFile(binPath)])
180
- if aaSet != bbSet:
181
- await password.getQiniu()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bcmd
3
- Version: 0.4.12
3
+ Version: 0.5.1
4
4
  Summary: Commands for Beni
5
5
  Author-email: Beni Mang <benimang@126.com>
6
6
  Maintainer-email: Beni Mang <benimang@126.com>
@@ -3,7 +3,7 @@
3
3
 
4
4
  [project]
5
5
  name = 'bcmd'
6
- version = '0.4.12'
6
+ version = '0.5.1'
7
7
  description = 'Commands for Beni'
8
8
  requires-python = '>=3.10'
9
9
  keywords = ['benimang', 'beni', 'bcmd']
@@ -1,54 +0,0 @@
1
- import os
2
- from enum import StrEnum
3
- from pathlib import Path
4
- from typing import Final
5
-
6
- import typer
7
- from beni import bcolor, bfile, bpath, btask
8
- from beni.bfunc import syncCall
9
- from PIL import Image
10
-
11
- app: Final = btask.newSubApp('图片工具集')
12
-
13
-
14
- class _OutputType(StrEnum):
15
- '输出类型'
16
- normal = '0'
17
- replace_ = '1'
18
- crc_replace = '2'
19
-
20
-
21
- @app.command()
22
- @syncCall
23
- async def convert(
24
- path: Path = typer.Option(None, '--path', '-p', help='指定目录或具体图片文件,默认当前目录'),
25
- src_format: str = typer.Option('jpg|jpeg|png', '--src-format', '-s', help='如果path是目录,指定源格式,可以指定多个,默认值:jpg|jpeg|png'),
26
- dst_format: str = typer.Option('webp', '--dst-format', '-d', help='目标格式,只能是单个'),
27
- rgb: bool = typer.Option(False, '--rgb', help='转换为RGB格式'),
28
- quality: int = typer.Option(85, '--quality', '-q', help='图片质量,0-100,默认85'),
29
- output_type: _OutputType = typer.Option(_OutputType.normal, '--output-type', help='输出类型,0:普通输出,1:删除源文件,2:输出文件使用CRC32命名并删除源文件'),
30
- ):
31
- '图片格式转换'
32
- path = path or Path(os.getcwd())
33
- fileList: list[Path] = []
34
- if path.is_file():
35
- fileList.append(path)
36
- elif path.is_dir():
37
- extNameList = [x for x in src_format.strip().split('|')]
38
- fileList = [x for x in bpath.listFile(path, True) if x.suffix[1:].lower() in extNameList]
39
- if not fileList:
40
- return bcolor.printRed(f'未找到图片文件({path})')
41
- for file in fileList:
42
- with Image.open(file) as img:
43
- if rgb:
44
- img = img.convert('RGB')
45
- with bpath.useTempFile() as tempFile:
46
- img.save(tempFile, format=dst_format, quality=quality)
47
- outputFile = file.with_suffix(f'.{dst_format}')
48
- if output_type == _OutputType.crc_replace:
49
- outputFile = outputFile.with_stem(await bfile.crc(tempFile))
50
- bpath.copy(tempFile, outputFile)
51
- if output_type in [_OutputType.replace_, _OutputType.crc_replace]:
52
- if outputFile != file:
53
- bpath.remove(file)
54
- bcolor.printGreen(f'{file} -> {outputFile}')
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
File without changes
File without changes
File without changes