MainShortcuts2 2.7.3__tar.gz → 2.8.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 (55) hide show
  1. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/PKG-INFO +2 -6
  2. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/pyproject.toml +3 -2
  3. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/_module_info.py +1 -1
  4. mainshortcuts2-2.8.0/src/MainShortcuts2/_ms2dat_auto.py +28 -0
  5. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/core.py +16 -5
  6. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/core_config.py +9 -14
  7. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ex/aiohttp_ex/web/__init__.py +94 -5
  8. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/java_ext/__init__.py +10 -4
  9. mainshortcuts2-2.8.0/src/MainShortcuts2/ms2dat1.py +549 -0
  10. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ms2hash.py +16 -0
  11. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/types.py +92 -0
  12. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/utils.py +106 -0
  13. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/README.md +0 -0
  14. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/__init__.py +0 -0
  15. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/__main__.py +0 -0
  16. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/_any2json_regs.py +0 -0
  17. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/_ms2app_regs.py +0 -0
  18. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/advanced.py +0 -0
  19. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/any2json.py +0 -0
  20. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/api/base.py +0 -0
  21. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/api/gigachat.py +0 -0
  22. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/api/github.py +0 -0
  23. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/api/russian_trusted_root_ca_pem.crt +0 -0
  24. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/api/russian_trusted_sub_ca_pem.crt +0 -0
  25. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/api/webdav.py +0 -0
  26. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/cfg.py +0 -0
  27. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/dict.py +0 -0
  28. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/dir.py +0 -0
  29. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ex/__init__.py +0 -0
  30. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ex/aiohttp_ex/web/__main__.py +0 -0
  31. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ex/datetime_ex.py +0 -0
  32. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ex/pathlib_ex.py +0 -0
  33. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ex/pil_ex.py +0 -0
  34. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ex/psutil_ex.py +0 -0
  35. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ex/sqlite_ex.py +0 -0
  36. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ex/urlparse_ex.py +0 -0
  37. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/file.py +0 -0
  38. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/gui_scripts/__init__.py +0 -0
  39. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/gui_scripts/ms2_hash_gen.py +0 -0
  40. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/gui_scripts/utils.py +0 -0
  41. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/java_ext/ms2ext.py +0 -0
  42. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/json.py +0 -0
  43. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/linux.py +0 -0
  44. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/list.py +0 -0
  45. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/ms2app.py +0 -0
  46. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/path.py +0 -0
  47. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/proc.py +0 -0
  48. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/regex.py +0 -0
  49. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/special_chars.py +0 -0
  50. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/sql/_sql_base.py +0 -0
  51. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/sql/postgresql.py +0 -0
  52. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/sql/sqlite.py +0 -0
  53. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/str.py +0 -0
  54. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/term.py +0 -0
  55. {mainshortcuts2-2.7.3 → mainshortcuts2-2.8.0}/src/MainShortcuts2/win.py +0 -0
@@ -1,15 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MainShortcuts2
3
- Version: 2.7.3
3
+ Version: 2.8.0
4
4
  Summary: Сокращение и улучшение функций + консольные утилиты
5
5
  Author: MainPlay TG
6
6
  Author-email: xbox.roman6666666666@gmail.com
7
- Requires-Python: >=3.6,<4.0
7
+ Requires-Python: >=3.10,<4.0
8
8
  Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.6
10
- Classifier: Programming Language :: Python :: 3.7
11
- Classifier: Programming Language :: Python :: 3.8
12
- Classifier: Programming Language :: Python :: 3.9
13
9
  Classifier: Programming Language :: Python :: 3.10
14
10
  Classifier: Programming Language :: Python :: 3.11
15
11
  Classifier: Programming Language :: Python :: 3.12
@@ -1,5 +1,5 @@
1
1
  [tool.poetry]
2
- version = "2.7.3"
2
+ version = "2.8.0"
3
3
  name = "MainShortcuts2"
4
4
  description = "Сокращение и улучшение функций + консольные утилиты"
5
5
  authors = ["MainPlay TG <xbox.roman6666666666@gmail.com>"]
@@ -13,6 +13,7 @@ packages = [
13
13
  ms2-app = "MainShortcuts2.ms2app:main"
14
14
  ms2-hash_check = "MainShortcuts2.ms2hash:hash_check"
15
15
  ms2-hash_gen = "MainShortcuts2.ms2hash:hash_gen"
16
+ ms2-hash_java = "MainShortcuts2.ms2hash:run_java_ext"
16
17
  ms2-import_example = "MainShortcuts2.__main__:import_example"
17
18
  ms2-ln = "MainShortcuts2.__main__:ln"
18
19
  ms2-which_real = "MainShortcuts2.__main__:which_real"
@@ -21,7 +22,7 @@ nginx-reload = "MainShortcuts2.__main__:nginx_reload"
21
22
  nginx-restart = "MainShortcuts2.__main__:nginx_restart"
22
23
 
23
24
  [tool.poetry.dependencies]
24
- python = "^3.6"
25
+ python = "^3.10"
25
26
 
26
27
  [build-system]
27
28
  build-backend = "poetry.core.masonry.api"
@@ -1,2 +1,2 @@
1
1
  name = 'mainshortcuts2'
2
- version = '2.7.3'
2
+ version = '2.8.0'
@@ -0,0 +1,28 @@
1
+ import typing
2
+ from . import ms2dat1
3
+ from io import BytesIO
4
+ # Использовать последнюю версию для сохранения
5
+ dump = ms2dat1.dump
6
+ dumps = ms2dat1.dumps
7
+ write_file = ms2dat1.write_file
8
+ # Авто определение версии при загрузке
9
+
10
+
11
+ def load(file: typing.BinaryIO, **kw):
12
+ """Загрузить объект из IO"""
13
+ header = ms2dat1.FileHeader.from_file(file)
14
+ if header.version == 1:
15
+ return ms2dat1.load(file, _h=header, **kw)
16
+ raise Exception(f"Unsupported version {header.version}")
17
+
18
+
19
+ def loads(data: bytes, **kw):
20
+ """Загрузить объект из `bytes`"""
21
+ with BytesIO(data) as f:
22
+ return load(f, **kw)
23
+
24
+
25
+ def read_file(path, **kw):
26
+ """Загрузить объект из локального файла"""
27
+ with open(path, "rb") as f:
28
+ return load(f, **kw)
@@ -192,29 +192,40 @@ class MS2:
192
192
  return cls(**kw)
193
193
 
194
194
  @property
195
- def now(self) -> float:
195
+ def now(self):
196
196
  """Текущее локальное время (`timestamp`)"""
197
197
  return time()
198
198
 
199
199
  @property
200
- def now_dt(self) -> datetime:
200
+ def now_dt(self):
201
201
  """Текущее локальное время (`datetime`)"""
202
202
  return datetime.fromtimestamp(time())
203
203
 
204
204
  @property
205
- def utcnow(self) -> float:
205
+ def utcnow(self):
206
206
  """Текущее время по UTC (`timestamp`)"""
207
207
  return self.utcnow_dt.timestamp()
208
208
  if timezone is None:
209
209
  @property
210
- def utcnow_dt(self) -> datetime:
210
+ def utcnow_dt(self):
211
211
  """Текущее время по UTC (`datetime`)"""
212
212
  return datetime.utcfromtimestamp(time())
213
213
  else:
214
214
  @property
215
- def utcnow_dt(self) -> datetime:
215
+ def utcnow_dt(self):
216
216
  """Текущее время по UTC (`datetime`)"""
217
217
  return datetime.fromtimestamp(time(), timezone.utc)
218
218
 
219
+ @cached_property
220
+ def ms2dat_v1(self):
221
+ from . import ms2dat1
222
+ return ms2dat1
223
+
224
+ @cached_property
225
+ def ms2dat(self):
226
+ """Авто выбор при загрузке, последняя версия при сохранении"""
227
+ from . import _ms2dat_auto
228
+ return _ms2dat_auto
229
+
219
230
 
220
231
  ms = MS2()
@@ -43,11 +43,7 @@ class CoreConfig(cfg):
43
43
 
44
44
  def _cu_thread(self):
45
45
  try:
46
- import requests
47
- url = "https://github.com/MainPlay-TG/MainShortcuts2.py/raw/refs/heads/master/src/MainShortcuts2/_module_info.py"
48
- with requests.get(url) as resp:
49
- resp.raise_for_status()
50
- version = self._parse_version(resp.text)
46
+ version = self._get_github_version()
51
47
  if version != _module_info.version:
52
48
  mini_log("New MainShortcuts2 version available: %s", version)
53
49
  mini_log("Please update by running:")
@@ -57,13 +53,12 @@ class CoreConfig(cfg):
57
53
  except:
58
54
  return
59
55
 
60
- def _parse_version(self, code) -> str:
61
- import ast
62
- tree = ast.parse(code)
63
- for node in ast.walk(tree):
64
- if isinstance(node, ast.Assign):
65
- for target in node.targets:
66
- if isinstance(target, ast.Name):
67
- if target.id == "version":
68
- return ast.literal_eval(node.value)
56
+ def _get_github_version(self) -> str:
57
+ from MainShortcuts2.api.github import Client
58
+ gh = Client.from_env(True)
59
+ rel = gh.releases.get_latest("MainPlay-TG", "MainShortcuts2.py")
60
+ for asset in rel.assets:
61
+ if asset.name == "changelog.json":
62
+ with gh.http.get(asset.browser_download_url) as resp:
63
+ return resp.json()["version"]
69
64
  return _module_info.version
@@ -1,29 +1,41 @@
1
+ import base64
2
+ import json
3
+ import os
1
4
  import typing
5
+ import uuid
2
6
  import warnings
3
7
  from aiohttp import BasicAuth, hdrs, web
4
8
  from aiohttp.typedefs import Handler
5
9
  from aiohttp.web import FileResponse, Request, Response, StreamResponse
10
+ from datetime import datetime, timedelta
6
11
  from logging import DEBUG
7
12
  from MainShortcuts2 import ms
8
- from pathlib import Path
13
+ from pathlib import Path, PurePath
9
14
  warnings.filterwarnings("ignore", "Inheritance class .* from web.Application is discouraged", DeprecationWarning)
15
+ PATH_TYPES = os.PathLike, PurePath
10
16
 
11
17
 
12
18
  def wrap_handler(app: "Application", handler: Handler):
19
+ """Обернуть обработчик запросов"""
13
20
  async def wrapper(req: Request) -> StreamResponse | typing.Any:
21
+ """Обёртка с поддержкой ответов `int`, `ApiResult` и `os.PathLike`"""
14
22
  try:
15
23
  resp = await handler(req)
16
- if isinstance(resp, int):
24
+ if isinstance(resp, int): # Код статуса
17
25
  resp = Response(status=resp)
18
- elif isinstance(resp, Path):
26
+ elif isinstance(resp, ApiResult): # Результат API обработчика
27
+ resp = resp.make_resp(req.content_type)
28
+ elif isinstance(resp, PATH_TYPES): # Локальный файл
19
29
  resp = FileResponse(resp)
30
+ except ApiResult as exc: # Результат API исключения
31
+ resp = exc.make_resp(req.content_type)
20
32
  except Exception as exc:
21
33
  app.logger.exception("Failed to process request", exc_info=exc)
22
34
  resp = Response(status=500, reason="Internal server error")
23
35
  if app.logger.isEnabledFor(DEBUG):
24
36
  remote = req.remote or "unknown"
25
37
  if "X-Real-IP" in req.headers:
26
- remote += " (real IP: %s)" % req.headers["X-Real-IP"]
38
+ remote += " (X-Real-IP: %s)" % req.headers["X-Real-IP"]
27
39
  status = resp.status if isinstance(resp, StreamResponse) else repr(resp)
28
40
  app.logger.debug("Request %s %s from %s, return %s", req.method, req.raw_path, remote, status)
29
41
  return resp
@@ -97,7 +109,84 @@ def auth_basic(header=hdrs.AUTHORIZATION, filter: typing.Callable[[BasicAuth], b
97
109
  return Response(status=400, headers=basic_unauthorized_headers) # Неправильный заголовок
98
110
  if filter(auth):
99
111
  return await func(req, auth) # Одобрено
100
- return Response(status=403) # Запрещено
112
+ return Response(status=403, headers=basic_unauthorized_headers) # Запрещено
101
113
  return Response(status=401, headers=basic_unauthorized_headers) # Заголовок отсутствует
102
114
  return wrapper
103
115
  return deco
116
+
117
+
118
+ class ApiResult(BaseException):
119
+ """Результат для вызова API. Может быть вызван как исключение"""
120
+
121
+ def __init__(self, result=None, status=200, **base_dict):
122
+ super().__init__("This exception should be handled as a response to the request")
123
+ self.base_dict = base_dict
124
+ self.result = result
125
+ self.status = status
126
+
127
+ def to_dict(self):
128
+ r = self.base_dict.copy()
129
+ r["ok"] = self.status < 400
130
+ if self.result is not None:
131
+ r["result"] = self.result
132
+ return r
133
+
134
+ def _serialize_ms2dat(self, obj, other_serializer=None):
135
+ """Для форматов, не поддерживающих `bytes`, `datetime`, `timedelta` и `UUID`"""
136
+ if isinstance(obj, bytes): # Base64
137
+ return base64.b64encode(obj).decode("utf-8")
138
+ if isinstance(obj, datetime): # ISO
139
+ return obj.isoformat()
140
+ if isinstance(obj, timedelta): # секунды
141
+ return obj.total_seconds()
142
+ if isinstance(obj, uuid.UUID): # HEX
143
+ return str(obj)
144
+ if other_serializer is None: # Другого сериализатора нет
145
+ raise TypeError(f"Object of type {obj.__class__.__name__} is not serializable")
146
+ return other_serializer(obj)
147
+
148
+ def _to_json(self):
149
+ # Минимальный размер за счёт
150
+ # коротких разделителей
151
+ # и отсутвия \uXXXX кодов
152
+ return "application/json", json.dumps(
153
+ self.to_dict(),
154
+ default=self._serialize_ms2dat,
155
+ ensure_ascii=False,
156
+ separators=(",", ":"),
157
+ )
158
+
159
+ def _to_yaml(self):
160
+ import yaml
161
+ return "application/yaml", yaml.safe_dump(self.to_dict())
162
+
163
+ def _to_xml(self):
164
+ raise NotImplementedError()
165
+
166
+ def _to_ms2dat1(self) -> tuple[str, bytes]:
167
+ from MainShortcuts2 import ms2dat1
168
+ return "application/x-ms2dat1", ms2dat1.dumps(self.to_dict())
169
+
170
+ def make_resp(self, content_type=None):
171
+ """Создать ответ в указаном формате, по умолчанию JSON"""
172
+ try:
173
+ if content_type == "application/x-ms2dat1":
174
+ ctype, body = self._to_ms2dat1()
175
+ elif content_type == "application/yaml":
176
+ ctype, body = self._to_yaml()
177
+ else: # Если запрошенный формат не поддерживается
178
+ ctype, body = self._to_json()
179
+ except ImportError: # Если формат не поддерживается
180
+ ctype, body = self._to_json() # JSON должен сработать
181
+ # Клиент должен проверять формат ответа, на случай если он не соответствует запрошенному
182
+ if isinstance(body, str):
183
+ return web.Response(status=self.status, text=body, content_type=ctype)
184
+ return web.Response(status=self.status, body=body, content_type=ctype)
185
+
186
+
187
+ class ApiError(ApiResult):
188
+ """Ошибка API. Можно вызывать как исключение"""
189
+
190
+ def __init__(self, message: str, status=400, **base_dict):
191
+ base_dict["message"] = message
192
+ super().__init__(status=status, **base_dict)
@@ -169,12 +169,15 @@ class JavaExtManager(ms.ObjectBase):
169
169
 
170
170
  def iter_offline_versions(self):
171
171
  """Итерация версий из кэша"""
172
+ info_list: list[InfoDict] = []
172
173
  for file in self.dir.iterdir():
173
174
  if file.suffix == ".json" and file.is_file():
174
- info = InfoDict(file.read_json())
175
- ver = self.get_version(info.version)
176
- ver.__dict__["info"] = info # cached_property
177
- yield ver
175
+ info_list.append(InfoDict(file.read_json()))
176
+ info_list.sort(key=lambda i: i.version_id)
177
+ for info in info_list[::-1]:
178
+ ver = self.get_version(info.version)
179
+ ver.__dict__["info"] = info # cached_property
180
+ yield ver
178
181
 
179
182
  def iter_all_versions(self, online_count=github.MAX_PER_PAGE):
180
183
  """Итерация версий из кэша и с GitHub"""
@@ -212,10 +215,13 @@ class JavaExtManager(ms.ObjectBase):
212
215
 
213
216
  def download_latest(self, count=1, download_jar=True):
214
217
  """Скачать последнюю версию (если не скачана)"""
218
+ results: list[JavaExtVersion] = []
215
219
  for ver in self.iter_online_versions(count):
216
220
  ver.info # Скачать файл JSON
217
221
  if download_jar:
218
222
  ver.jar_path # Скачать файл JAR
223
+ results.append(ver)
224
+ return results
219
225
 
220
226
 
221
227
  class JavaExtVersion(ms.ObjectBase):
@@ -0,0 +1,549 @@
1
+ import datetime
2
+ import hashlib
3
+ import math
4
+ import secrets
5
+ import struct
6
+ import typing
7
+ import uuid
8
+ from io import BytesIO
9
+ from MainShortcuts2.utils import int_size_unsigned
10
+ HASH_NAMES = None, "sha256", "sha3-256", "sha512"
11
+ HASH_SIZES = 0, 32, 32, 64
12
+ MAGIC_HEAD = b"MS2D"
13
+ SPECIAL_TYPES = None, False, True, float("-inf"), float("inf"), float("nan")
14
+
15
+
16
+ def hash_data(hash_type: int, data: bytes):
17
+ """Хеширование данных"""
18
+ if hash_type == 0:
19
+ return b""
20
+ return hashlib.new(HASH_NAMES[hash_type], data).digest()
21
+
22
+
23
+ def compress_data(compress_type: int, data: bytes):
24
+ """Сжатие данных, применяется только если результат меньше исходных данных"""
25
+ if compress_type == 1:
26
+ import zlib
27
+ result = zlib.compress(data)
28
+ if len(result) < len(data):
29
+ return 1, result
30
+ elif compress_type == 2:
31
+ import bz2
32
+ result = bz2.compress(data)
33
+ if len(result) < len(data):
34
+ return 2, result
35
+ elif compress_type == 3:
36
+ import lzma # СУПЕР сжатие
37
+ result = lzma.compress(data, lzma.FORMAT_ALONE, preset=9 | lzma.PRESET_EXTREME)
38
+ if len(result) < len(data):
39
+ return 3, result
40
+ return 0, data
41
+
42
+
43
+ def decompress_data(compress_type: int, data: bytes):
44
+ if compress_type == 0:
45
+ return data
46
+ if compress_type == 1:
47
+ import zlib
48
+ return zlib.decompress(data)
49
+ if compress_type == 2:
50
+ import bz2
51
+ return bz2.decompress(data)
52
+ if compress_type == 3:
53
+ import lzma
54
+ return lzma.decompress(data)
55
+ raise ValueError("Invalid compress type")
56
+
57
+
58
+ class FileHeader:
59
+ """Подходит для всех версий"""
60
+
61
+ def __init__(self, version: int):
62
+ self.version = version
63
+
64
+ @classmethod
65
+ def from_file(cls, f: typing.IO[bytes]):
66
+ if f.read(4) != MAGIC_HEAD:
67
+ raise ValueError("Invalid file header")
68
+ return cls(f.read(1)[0])
69
+
70
+ def build(self):
71
+ return bytes([*MAGIC_HEAD, self.version])
72
+
73
+
74
+ class UnknownType(typing.NamedTuple):
75
+ typename: str
76
+ body: bytes
77
+
78
+
79
+ class Reader:
80
+ """v1"""
81
+
82
+ def __init__(self, ms2dat: "MS2Dat1", f: typing.IO[bytes]):
83
+ self.f = f
84
+ self.ms2dat = ms2dat
85
+ # MAGIC заголовок уже должен быть прочитан и проверен
86
+ # Флаги
87
+ flags = self._read1()
88
+ self.compress_type = flags >> 6 # 0b11000000 0-3
89
+ self.hash_type = flags >> 4 & 0b11 # 0b00110000 0-3
90
+ self.encrypted = flags >> 3 & 1 # 0b00001000 0-1
91
+ bodysize_size = flags & 0b111 # 0b00000111 0-7
92
+ # Пользовательские типы
93
+ self.custom_types: list[str] = []
94
+ for i in range(self._read1()):
95
+ self.custom_types.append(self._read(self._read1()).decode("utf-8"))
96
+ # Размер тела
97
+ self.body_size = int.from_bytes(self._read(bodysize_size), "big")
98
+
99
+ def _read(self, n: int):
100
+ buf = self.f.read(n)
101
+ if len(buf) != n:
102
+ raise ValueError(f"Expected {n} bytes, got {len(buf)}")
103
+ return buf
104
+
105
+ def _read1(self):
106
+ return self._read(1)[0]
107
+
108
+ def read_body(self, allow_unknown=False, verify=True) -> typing.Optional[typing.Any]:
109
+ raw = self._read(self.body_size)
110
+ if self.encrypted:
111
+ raw = self.ms2dat.decrypt_body(raw)
112
+ body = decompress_data(self.compress_type, raw)
113
+ if verify and self.hash_type:
114
+ reald = hash_data(self.hash_type, body)
115
+ saved = self._read(HASH_SIZES[self.hash_type])
116
+ if reald != saved:
117
+ raise ValueError("The file is corrupted")
118
+ with BytesIO(body) as buf:
119
+ # Чтение словаря
120
+ d = []
121
+ for i in range(int.from_bytes(buf.read(3), "big")):
122
+ d.append(self.decode_obj(buf, d, allow_unknown))
123
+ # Чтение тела
124
+ return self.decode_obj(buf, d, allow_unknown)
125
+
126
+ def decode_obj(self, buf: typing.IO[bytes], d: list, allow_unknown=False):
127
+ head = buf.read(1)[0]
128
+ typeid = head >> 4
129
+ sizesize = head & 0b1111
130
+ size = int.from_bytes(buf.read(sizesize), "big")
131
+ if typeid == 0: # Специальный тип
132
+ return SPECIAL_TYPES[size]
133
+ if typeid == 6: # dict
134
+ result = {}
135
+ for i in range(size):
136
+ k = self.decode_obj(buf, d, allow_unknown)
137
+ result[k] = self.decode_obj(buf, d, allow_unknown)
138
+ return result
139
+ if typeid in (7, 8, 9): # list, tuple, set
140
+ result = [self.decode_obj(buf, d, allow_unknown) for i in range(size)]
141
+ if typeid == 8:
142
+ return tuple(result)
143
+ if typeid == 9:
144
+ return set(result)
145
+ return result
146
+ if typeid == 12: # datetime + timezone
147
+ dt: datetime.datetime = self.decode_obj(buf, d)
148
+ td: datetime.timedelta = self.decode_obj(buf, d)
149
+ return dt.replace(tzinfo=datetime.timezone(td))
150
+ if typeid == 14: # Ссылка на словарь
151
+ return d[size]
152
+ if typeid == 15: # Пользовательский тип
153
+ ctypename = self.custom_types[buf.read(1)[0]]
154
+ ctype = self.ms2dat.custom_types.get(ctypename)
155
+ cbody = buf.read(size)
156
+ if ctype is None:
157
+ if allow_unknown:
158
+ return UnknownType(ctypename, cbody)
159
+ raise ValueError(f"Unknown custom type: {ctypename}")
160
+ return ctype.decode_obj(self, cbody, d)
161
+ body = buf.read(size)
162
+ if typeid == 1: # int
163
+ return int.from_bytes(body, "big")
164
+ if typeid == 2: # int
165
+ return -int.from_bytes(body, "big")
166
+ if typeid == 3: # float
167
+ return struct.unpack("d", body)[0]
168
+ if typeid == 4: # bytes
169
+ return body
170
+ if typeid == 5: # str
171
+ return body.decode("utf-8")
172
+ if typeid == 10: # datetime
173
+ return datetime.datetime.fromtimestamp(struct.unpack("d", body)[0])
174
+ if typeid == 11: # timedelta
175
+ return datetime.timedelta(seconds=struct.unpack("d", body)[0])
176
+ if typeid == 13: # UUID
177
+ return uuid.UUID(bytes=body)
178
+ raise Exception("Эта ошибка никогда не вылезет")
179
+
180
+
181
+ class Writer:
182
+ """v1"""
183
+
184
+ def __init__(self, ms2dat: "MS2Dat1", f: typing.IO[bytes], obj):
185
+ self.f = f
186
+ self.ms2dat = ms2dat
187
+ self.obj = obj
188
+ self.obj_dict: list[bytes] = []
189
+ self.used_ctypes: list[CustomType] = []
190
+
191
+ def _write(self, data: bytes):
192
+ return self.f.write(data)
193
+
194
+ def _write1(self, byte: int):
195
+ return self._write(bytes([byte]))
196
+
197
+ def _addget_dict(self, typeid: int, size: int, body: bytes):
198
+ if typeid == 14:
199
+ # Если дана ссылка на словарь, то вернуть ее
200
+ return typeid, size, b""
201
+ # В словаре только уже собранные объекты
202
+ buf = bytes(self._build_obj(typeid, size, body))
203
+ if buf in self.obj_dict:
204
+ # Объект уже есть в словаре
205
+ return 14, self.obj_dict.index(buf), b""
206
+ if len(self.obj_dict) >= 0xffffff:
207
+ # Словарь переполнен
208
+ return typeid, size, body
209
+ # Добавить объект в словарь
210
+ self.obj_dict.append(buf)
211
+ return 14, len(self.obj_dict) - 1, b""
212
+
213
+ def _encode_obj(self, data, sort_keys: bool, use_dict: bool) -> tuple[int, int, bytes | bytearray]:
214
+ for m, n in enumerate(SPECIAL_TYPES):
215
+ if m < 3: # None, bool
216
+ if data is n:
217
+ return 0, m, b""
218
+ else: # float
219
+ if data == n:
220
+ return 0, m, b""
221
+ if isinstance(data, int):
222
+ typeid = 1
223
+ if data < 0:
224
+ data = -data
225
+ typeid = 2
226
+ size = int_size_unsigned(data)
227
+ return typeid, size, data.to_bytes(size, "big")
228
+ if isinstance(data, float):
229
+ if math.isnan(data):
230
+ return 0, 5, b""
231
+ result = struct.pack("d", data)
232
+ return 3, len(result), result
233
+ if isinstance(data, bytes):
234
+ if use_dict:
235
+ return self._addget_dict(4, len(data), data)
236
+ return 4, len(data), data
237
+ if isinstance(data, str):
238
+ result = data.encode("utf-8")
239
+ if use_dict:
240
+ return self._addget_dict(5, len(result), result)
241
+ return 5, len(result), result
242
+ if isinstance(data, dict):
243
+ items = list(data.items())
244
+ if sort_keys:
245
+ try:
246
+ items.sort()
247
+ except Exception:
248
+ pass
249
+ buf = bytearray()
250
+ for k, v in items:
251
+ buf.extend(self.encode_obj(k, sort_keys, use_dict))
252
+ buf.extend(self.encode_obj(v, sort_keys, use_dict))
253
+ return 6, len(items), buf
254
+ if isinstance(data, (list, tuple, set)) and not isinstance(data, UnknownType):
255
+ typeid = 7
256
+ if isinstance(data, tuple):
257
+ typeid = 8
258
+ elif isinstance(data, set):
259
+ typeid = 9
260
+ if sort_keys:
261
+ try:
262
+ data = sorted(data)
263
+ except Exception:
264
+ pass
265
+ buf = bytearray()
266
+ for v in data:
267
+ buf.extend(self.encode_obj(v, sort_keys, use_dict))
268
+ return typeid, len(data), buf
269
+ if isinstance(data, datetime.datetime):
270
+ if data.tzinfo is None:
271
+ result = struct.pack("d", data.timestamp())
272
+ return 10, len(result), result
273
+ td = data.tzinfo.utcoffset(data)
274
+ if td is None:
275
+ return self._encode_obj(data.replace(tzinfo=None))
276
+ buf = bytearray()
277
+ buf.extend(self.encode_obj(data.replace(tzinfo=None)))
278
+ buf.extend(self.encode_obj(td))
279
+ return 12, len(buf), buf
280
+ if isinstance(data, datetime.timedelta):
281
+ result = struct.pack("d", data.total_seconds())
282
+ return 11, len(result), result
283
+ if isinstance(data, uuid.UUID):
284
+ result = data.bytes
285
+ return 13, len(result), result
286
+ # Пользовательский тип
287
+ if isinstance(data, UnknownType):
288
+ handler = UnknownTypeHandler(self.ms2dat, data.typename)
289
+ else:
290
+ handler = self.ms2dat.find_handler(type(data))
291
+ if handler is None:
292
+ raise Exception(f"Unknown type: {type(data)}")
293
+ if handler in self.used_ctypes:
294
+ ctypeid = self.used_ctypes.index(handler)
295
+ else:
296
+ ctypeid = len(self.used_ctypes)
297
+ self.used_ctypes.append(handler)
298
+ body = handler.encode_obj(data, sort_keys)
299
+ result = 15, len(body), bytes([ctypeid]) + body
300
+ if use_dict and getattr(handler, "allow_dict", False):
301
+ return self._addget_dict(*result)
302
+ return result
303
+
304
+ def _build_obj(self, typeid: int, size: int, body: bytes):
305
+ sizesize = int_size_unsigned(size)
306
+ if sizesize > 0b1111:
307
+ raise Exception("Too big size")
308
+ buf = bytearray()
309
+ buf.append((typeid << 4) | sizesize)
310
+ buf.extend(size.to_bytes(sizesize, "big"))
311
+ buf.extend(body)
312
+ return buf
313
+
314
+ def encode_obj(self, data, sort_keys=False, use_dict=True):
315
+ typeid, size, body = self._encode_obj(data, sort_keys, use_dict)
316
+ return self._build_obj(typeid, size, body)
317
+
318
+ def write_all(self, compress_type=0, encrypted=0, hash_type=1, sort_keys=False, use_dict=True):
319
+ self._write(MAGIC_HEAD)
320
+ self._write1(1)
321
+ # Сохранение объекта
322
+ raw_obj = self.encode_obj(self.obj, sort_keys, use_dict)
323
+ # Тело
324
+ raw_body = bytearray()
325
+ raw_body.extend(len(self.obj_dict).to_bytes(3, "big"))
326
+ for i in self.obj_dict:
327
+ raw_body.extend(i)
328
+ raw_body.extend(raw_obj)
329
+ # Сжатие
330
+ compress_type, body = compress_data(compress_type, raw_body)
331
+ # Шифрование
332
+ encrypted = 1 if encrypted else 0 # Если дадут bool
333
+ if encrypted:
334
+ body = self.ms2dat.encrypt_body(body)
335
+ size = len(body)
336
+ sizesize = int_size_unsigned(size)
337
+ if sizesize > 0b111:
338
+ raise Exception("Too big body")
339
+ # Запись
340
+ # Флаги
341
+ self._write1((compress_type << 6) | (hash_type << 4) | (encrypted << 3) | sizesize)
342
+ # Пользовательские типы
343
+ self._write1(len(self.used_ctypes))
344
+ for i in self.used_ctypes:
345
+ name = i.typename.encode("utf-8")
346
+ self._write1(len(name))
347
+ self._write(name)
348
+ # Тело
349
+ self._write(size.to_bytes(sizesize, "big"))
350
+ self._write(body)
351
+ # Хеш
352
+ self._write(hash_data(hash_type, body))
353
+
354
+
355
+ class CustomType:
356
+ allow_dict: bool
357
+ handled_types: set[type]
358
+ typename: str
359
+
360
+ def __init__(self, ms2dat: "MS2Dat1"):
361
+ self.ms2dat = ms2dat
362
+
363
+ def __eq__(self, other):
364
+ if isinstance(other, CustomType):
365
+ return (self.ms2dat is other.ms2dat) and (self.typename == other.typename)
366
+
367
+ def encode_obj(self, data: object, sort_keys=False) -> bytes:
368
+ raise NotImplementedError()
369
+
370
+ def decode_obj(self, reader: Reader, body: bytes, d: list) -> object:
371
+ raise NotImplementedError()
372
+
373
+
374
+ class UnknownTypeHandler(CustomType):
375
+ def __init__(self, ms2dat: "MS2Dat1", typename: str):
376
+ super().__init__(ms2dat)
377
+ self.allow_dict = True
378
+ self.handled_types = set()
379
+ self.typename = typename
380
+
381
+ def encode_obj(self, data: UnknownType, sort_keys=False):
382
+ return data.body
383
+
384
+
385
+ class MS2Dat1:
386
+ VERSION = 1
387
+ COMPRESS_NONE = 0
388
+ COMPRESS_ZLIB = 1
389
+ COMPRESS_BZ2 = 2
390
+ COMPRESS_LZMA = 3
391
+ HASH_NONE = 0
392
+ HASH_SHA256 = 1
393
+ HASH_SHA3_256 = 2
394
+ HASH_SHA512 = 3
395
+
396
+ def __init__(self):
397
+ self._dump_kw = {}
398
+ self._load_kw = {}
399
+ self.custom_types: dict[str, CustomType] = {}
400
+
401
+ def set_allow_unknown(self, value: bool):
402
+ """Разрешить загрузку неизвестных пользовательских типов? Такие объекты будут загружены в виде `UnknownType`"""
403
+ self._load_kw["allow_unknown"] = bool(value)
404
+
405
+ def set_compress(self, value: int):
406
+ """Уровень сжатия (см. константы класса)"""
407
+ self._dump_kw["compress_type"] = value
408
+
409
+ def set_encrypt(self, value: bool):
410
+ """Включить шифрование? Шифрование должно быть реализовано подклассом"""
411
+ self._dump_kw["encrypted"] = 1 if value else 0
412
+
413
+ def set_hash(self, value: int):
414
+ """Алгоритм хеширования (см. константы класса)"""
415
+ self._dump_kw["hash_type"] = value
416
+
417
+ def set_sort_keys(self, value: bool):
418
+ """Сортировать ключи `dict` и `set`?"""
419
+ self._dump_kw["sort_keys"] = bool(value)
420
+
421
+ def set_use_dict(self, value: bool):
422
+ """Использовать словарь? Словарь уменьшает объём данных при наличии одинаковых объектов"""
423
+ self._dump_kw["use_dict"] = bool(value)
424
+
425
+ def set_verify(self, value: bool):
426
+ """Проверять хеш данных (если есть) при загрузке?"""
427
+ self._load_kw["verify"] = bool(value)
428
+
429
+ def profile_fast(self):
430
+ """Отключение сжатия, хеширования, сортировки, и словаря для быстрейшей работы"""
431
+ self.set_compress(self.COMPRESS_NONE)
432
+ self.set_hash(self.HASH_NONE)
433
+ self.set_sort_keys(False)
434
+ self.set_use_dict(False)
435
+
436
+ def profile_fastest(self):
437
+ """Быстрый профиль + отключение проверки данных"""
438
+ self.profile_fast()
439
+ self.set_verify(False)
440
+
441
+ def profile_safe(self):
442
+ """Лучшее хеширование"""
443
+ self.set_hash(self.HASH_SHA512)
444
+ self.set_verify(True)
445
+
446
+ def profile_smaller_size(self):
447
+ """Максимальное сжатие, но медленная работа"""
448
+ self.set_compress(self.COMPRESS_LZMA)
449
+
450
+ def profile_minimum_size(self):
451
+ """Максимальное сжатие без хеширования и с включенным словарём"""
452
+ self.set_compress(self.COMPRESS_LZMA)
453
+ self.set_hash(self.HASH_NONE)
454
+ self.set_use_dict(True)
455
+
456
+ def reg_custom_type(self, obj: CustomType, replace=False):
457
+ """Зарегистрировать пользовательский тип"""
458
+ if (obj.typename in self.custom_types) and not replace:
459
+ raise Exception(f"Type {obj.typename} already registered")
460
+ self.custom_types[obj.typename] = obj
461
+
462
+ def reg_custom_type_deco(self, replace=False):
463
+ """Регистрация пользовательского типа в виде декоратора над классом"""
464
+ def deco(cls: type[CustomType]):
465
+ self.reg_custom_type(cls(self), replace)
466
+ return cls
467
+ return deco
468
+
469
+ def find_handler(self, cls: type) -> CustomType | None:
470
+ """Найти обработчик для типа данных (только пользовательские типы)"""
471
+ for i in self.custom_types.values():
472
+ if cls in i.handled_types:
473
+ return i
474
+
475
+ def decrypt_body(self, data: bytes) -> bytes:
476
+ """Расшифровать тело. Должен быть переопределен для работы"""
477
+ raise NotImplementedError("This class cannot work with encryption")
478
+
479
+ def encrypt_body(self, data: bytes) -> bytes:
480
+ """Зашифровать тело. Должен быть переопределен для работы"""
481
+ raise NotImplementedError("This class cannot work with encryption")
482
+
483
+ def dump(self, obj, file: typing.BinaryIO, **kw):
484
+ """Сохранить объект в IO"""
485
+ writer = Writer(self, file, obj)
486
+ for k, v in self._dump_kw.items():
487
+ kw.setdefault(k, v)
488
+ writer.write_all(**kw)
489
+
490
+ def dumps(self, obj, **kw):
491
+ """Сохранить объект в `bytes`"""
492
+ with BytesIO() as f:
493
+ self.dump(obj, f, **kw)
494
+ return f.getvalue()
495
+
496
+ def load(self, file: typing.BinaryIO, *, _h: FileHeader = None, **kw):
497
+ """Загрузить объект из IO"""
498
+ if _h is None:
499
+ _h = FileHeader.from_file(file)
500
+ if _h.version != self.VERSION:
501
+ raise Exception(f"Unsupported version {_h.version}")
502
+ reader = Reader(self, file)
503
+ for k, v in self._load_kw.items():
504
+ kw.setdefault(k, v)
505
+ return reader.read_body(**kw)
506
+
507
+ def loads(self, data: bytes, **kw):
508
+ """Загрузить объект из `bytes`"""
509
+ with BytesIO(data) as f:
510
+ return self.load(f, **kw)
511
+
512
+ def read_file(self, path, **kw):
513
+ """Загрузить объект из локального файла"""
514
+ with open(path, "rb") as f:
515
+ return self.load(f, **kw)
516
+
517
+ def write_file(self, obj, path, **kw):
518
+ """Сохранить объект в локальный файл"""
519
+ with open(path, "wb") as f:
520
+ self.dump(obj, f, **kw)
521
+
522
+
523
+ class MS2Dat1EncryptExample(MS2Dat1):
524
+ """Пример реализации шифрования через XOR. Ненадёжное шифрование!"""
525
+
526
+ def __init__(self, key: bytes):
527
+ super().__init__()
528
+ self.encrypt_key = key
529
+
530
+ @classmethod
531
+ def create_with_random_key(cls, keysize=32):
532
+ return cls(secrets.token_bytes(keysize))
533
+
534
+ def decrypt_body(self, data: bytes):
535
+ """Зашифровать/расшифровать данные, используя XOR-шифрование"""
536
+ key = self.encrypt_key
537
+ lkey = len(key)
538
+ return bytes([b ^ key[i % lkey] for i, b in enumerate(data)])
539
+ encrypt_body = decrypt_body
540
+
541
+
542
+ inst = MS2Dat1()
543
+ """Настройки по умолчанию"""
544
+ dump = inst.dump
545
+ dumps = inst.dumps
546
+ load = inst.load
547
+ loads = inst.loads
548
+ read_file = inst.read_file
549
+ write_file = inst.write_file
@@ -142,3 +142,19 @@ def hash_check(args: argparse.Namespace = None):
142
142
  else:
143
143
  print("Ошибка: файл " + shlex.quote(file) + " изменён")
144
144
  completed.append(file)
145
+
146
+
147
+ def run_java_ext(argv: list[str] = None):
148
+ if argv is None:
149
+ argv = sys.argv[1:]
150
+ from MainShortcuts2 import java_ext
151
+ mgr = java_ext.JavaExtManager("MainPlay-TG", "MS2Hash.java")
152
+ ver = None
153
+ for i in mgr.iter_all_versions(1):
154
+ ver = i
155
+ break
156
+ if ver is None:
157
+ ms.utils.mini_log("Ошибка: не удалось найти версию MS2Hash. Проверьте соединение с GitHub")
158
+ sys.exit(1)
159
+ p = ver.run(argv, check=False)
160
+ sys.exit(p.returncode)
@@ -1,4 +1,5 @@
1
1
  """Различные объекты и исключения"""
2
+ from typing import NamedTuple
2
3
 
3
4
 
4
5
  class Base:
@@ -468,6 +469,97 @@ class ThreadsFlag(BoolFlag):
468
469
  self.value.remove(t)
469
470
 
470
471
 
472
+ class BitsTuple(NamedTuple):
473
+ b0: int
474
+ b1: int
475
+ b2: int
476
+ b3: int
477
+ b4: int
478
+ b5: int
479
+ b6: int
480
+ b7: int
481
+
482
+ def __int__(self):
483
+ return self.to_byte()
484
+
485
+ def __index__(self):
486
+ return self.to_byte()
487
+ # Возможны баги
488
+
489
+ def __and__(self, other):
490
+ return int(self) & other
491
+
492
+ def __or__(self, other):
493
+ return int(self) | other
494
+
495
+ def __xor__(self, other):
496
+ return int(self) ^ other
497
+
498
+ def __lshift__(self, other):
499
+ return int(self) << other
500
+
501
+ def __rshift__(self, other):
502
+ return int(self) >> other
503
+
504
+ def __invert__(self):
505
+ return (~int(self)) & 0xFF
506
+
507
+ def __rand__(self, other):
508
+ return other & int(self)
509
+
510
+ def __ror__(self, other):
511
+ return other | int(self)
512
+
513
+ def __rxor__(self, other):
514
+ return other ^ int(self)
515
+
516
+ def __rlshift__(self, other):
517
+ return other << int(self)
518
+
519
+ def __rrshift__(self, other):
520
+ return other >> int(self)
521
+ # ---
522
+
523
+ @classmethod
524
+ def from_byte(cls, b: int):
525
+ """Разбить **один** байт на биты"""
526
+ return cls(*[(b >> i) & 1 for i in range(7, -1, -1)])
527
+
528
+ @classmethod
529
+ def from_bytes_iter(cls, b: bytes):
530
+ """Разбить **несколько** байт на группы битов (итерация)"""
531
+ return map(cls.from_byte, b)
532
+
533
+ @classmethod
534
+ def from_bytes(cls, b: bytes):
535
+ """Разбить **несколько** байт на группы битов"""
536
+ return list(cls.from_bytes_iter(b))
537
+
538
+ def to_byte(self):
539
+ """Соединить биты в байт"""
540
+ b = 0
541
+ for i in self:
542
+ b = (b << 1) | i
543
+ return b
544
+
545
+ def replace(self, *args: bool | int, **kw: bool | int):
546
+ for n, bit in enumerate(args):
547
+ kw[f"b{n}"] = bit
548
+ fields = set(self._fields)
549
+ for k in list(kw):
550
+ if k not in fields:
551
+ raise ValueError(f"Invalid field: {k}, must be one of {sorted(fields)}")
552
+ kw[k] = 1 if kw[k] else 0
553
+ return self._replace(**kw)
554
+
555
+ def to_bool_list(self):
556
+ return list(map(bool, self))
557
+
558
+ @classmethod
559
+ def from_bool_list(cls, l):
560
+ return cls(*l)
561
+
562
+
471
563
  COLORS = _COLORS()
472
564
  Error401 = AccessDeniedError
473
565
  Error403 = AccessDeniedError
@@ -1102,3 +1102,109 @@ def guess_checksum_alg(digest_len: int):
1102
1102
  if digest_len == v:
1103
1103
  result.add(k)
1104
1104
  return result
1105
+
1106
+
1107
+ def int_size_unsigned(n: int):
1108
+ """Минимальное количество байт для неотрицательного `int`"""
1109
+ if n == 0:
1110
+ return 1
1111
+ return (n.bit_length() + 7) // 8
1112
+
1113
+
1114
+ def int_size_signed(n: int) -> int:
1115
+ """Минимальное количество байт для `int` с поддержкой отрицательных"""
1116
+ if n == 0:
1117
+ return 1
1118
+ return (n.bit_length() + 8) // 8
1119
+
1120
+
1121
+ def int2bytes(n: int, byteorder='big', signed=False) -> bytes:
1122
+ """Конвертировать `int` в `bytes` минимального размера"""
1123
+ f = int_size_signed if signed else int_size_unsigned
1124
+ return n.to_bytes(f(n), byteorder=byteorder, signed=signed)
1125
+
1126
+
1127
+ class SyncSignal(list):
1128
+ def __init__(self, ignore_errors=False):
1129
+ super().__init__()
1130
+ self.ignore_errors = bool(ignore_errors)
1131
+
1132
+ def send(self, *args, **kw):
1133
+ """Отправить сигнал всем подписчикам"""
1134
+ e = not self.ignore_errors
1135
+ for func in list(self):
1136
+ try:
1137
+ func(*args, **kw)
1138
+ except Exception:
1139
+ if e:
1140
+ raise
1141
+
1142
+
1143
+ class AsyncSignal(SyncSignal):
1144
+ async def send(self, *args, **kw):
1145
+ e = not self.ignore_errors
1146
+ for func in list(self):
1147
+ try:
1148
+ await func(*args, **kw)
1149
+ except Exception:
1150
+ if e:
1151
+ raise
1152
+
1153
+
1154
+ class Filter:
1155
+ # Спасибо Pyrogram за идею
1156
+ def __call__(self, *a, **b):
1157
+ raise NotImplementedError
1158
+
1159
+ def __invert__(self):
1160
+ return _InvertedFilter(self)
1161
+
1162
+ def __and__(self, other):
1163
+ return _AndFilter(self, other)
1164
+
1165
+ def __or__(self, other):
1166
+ return _OrFilter(self, other)
1167
+
1168
+
1169
+ class _InvertedFilter(Filter):
1170
+ def __init__(self, f1: Filter):
1171
+ self.f1 = f1
1172
+
1173
+ def __call__(self, *a, **b):
1174
+ return not self.f1(*a, **b)
1175
+
1176
+
1177
+ class _AndFilter(Filter):
1178
+ def __init__(self, f1: Filter, f2: Filter):
1179
+ self.f1 = f1
1180
+ self.f2 = f2
1181
+
1182
+ def __call__(self, *a, **b):
1183
+ return self.f1(*a, **b) and self.f2(*a, **b)
1184
+
1185
+
1186
+ class _OrFilter(_AndFilter):
1187
+ def __call__(self, *a, **b):
1188
+ return self.f1(*a, **b) or self.f2(*a, **b)
1189
+
1190
+
1191
+ def create_filter(func: Callable, name: str = None, **d) -> Filter:
1192
+ d["__call__"] = func
1193
+ return type(name or func.__name__ or "CustomFilter", (Filter,), d)()
1194
+
1195
+
1196
+ filter_all = create_filter(return_True)
1197
+ filter_none = create_filter(return_False)
1198
+
1199
+
1200
+ class FilterGetAttr(Filter):
1201
+ def __init__(self, name: str):
1202
+ self.name = name
1203
+
1204
+ def __call__(self, obj):
1205
+ return getattr(obj, self.name, False)
1206
+
1207
+
1208
+ class FilterGetItem(FilterGetAttr):
1209
+ def __call__(self, obj):
1210
+ return obj[self.name]
File without changes