MainShortcuts2 2.7.2__tar.gz → 2.7.4__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 (54) hide show
  1. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/PKG-INFO +1 -1
  2. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/pyproject.toml +2 -1
  3. mainshortcuts2-2.7.4/src/MainShortcuts2/_module_info.py +2 -0
  4. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/api/github.py +60 -4
  5. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/core_config.py +9 -14
  6. mainshortcuts2-2.7.2/src/MainShortcuts2/ex/aiohttp_ex/web.py → mainshortcuts2-2.7.4/src/MainShortcuts2/ex/aiohttp_ex/web/__init__.py +0 -55
  7. mainshortcuts2-2.7.4/src/MainShortcuts2/ex/aiohttp_ex/web/__main__.py +56 -0
  8. mainshortcuts2-2.7.4/src/MainShortcuts2/ex/urlparse_ex.py +24 -0
  9. mainshortcuts2-2.7.4/src/MainShortcuts2/java_ext/__init__.py +339 -0
  10. mainshortcuts2-2.7.4/src/MainShortcuts2/java_ext/ms2ext.py +304 -0
  11. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/ms2hash.py +20 -2
  12. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/types.py +92 -0
  13. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/utils.py +43 -22
  14. mainshortcuts2-2.7.2/src/MainShortcuts2/_module_info.py +0 -2
  15. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/README.md +0 -0
  16. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/__init__.py +0 -0
  17. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/__main__.py +0 -0
  18. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/_any2json_regs.py +0 -0
  19. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/_ms2app_regs.py +0 -0
  20. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/advanced.py +0 -0
  21. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/any2json.py +0 -0
  22. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/api/base.py +0 -0
  23. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/api/gigachat.py +0 -0
  24. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/api/russian_trusted_root_ca_pem.crt +0 -0
  25. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/api/russian_trusted_sub_ca_pem.crt +0 -0
  26. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/api/webdav.py +0 -0
  27. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/cfg.py +0 -0
  28. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/core.py +0 -0
  29. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/dict.py +0 -0
  30. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/dir.py +0 -0
  31. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/ex/__init__.py +0 -0
  32. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/ex/datetime_ex.py +0 -0
  33. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/ex/pathlib_ex.py +0 -0
  34. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/ex/pil_ex.py +0 -0
  35. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/ex/psutil_ex.py +0 -0
  36. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/ex/sqlite_ex.py +0 -0
  37. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/file.py +0 -0
  38. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/gui_scripts/__init__.py +0 -0
  39. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/gui_scripts/ms2_hash_gen.py +0 -0
  40. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/gui_scripts/utils.py +0 -0
  41. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/json.py +0 -0
  42. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/linux.py +0 -0
  43. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/list.py +0 -0
  44. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/ms2app.py +0 -0
  45. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/path.py +0 -0
  46. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/proc.py +0 -0
  47. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/regex.py +0 -0
  48. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/special_chars.py +0 -0
  49. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/sql/_sql_base.py +0 -0
  50. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/sql/postgresql.py +0 -0
  51. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/sql/sqlite.py +0 -0
  52. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/str.py +0 -0
  53. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/term.py +0 -0
  54. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/win.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MainShortcuts2
3
- Version: 2.7.2
3
+ Version: 2.7.4
4
4
  Summary: Сокращение и улучшение функций + консольные утилиты
5
5
  Author: MainPlay TG
6
6
  Author-email: xbox.roman6666666666@gmail.com
@@ -1,5 +1,5 @@
1
1
  [tool.poetry]
2
- version = "2.7.2"
2
+ version = "2.7.4"
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"
@@ -0,0 +1,2 @@
1
+ name = 'mainshortcuts2'
2
+ version = '2.7.4'
@@ -2,6 +2,7 @@ import os
2
2
  import requests
3
3
  from .base import BaseClient, ObjectBase
4
4
  from functools import cached_property
5
+ from MainShortcuts2 import ms
5
6
  from pathlib import Path
6
7
  from typing import IO
7
8
  from urllib.parse import urlparse
@@ -12,6 +13,7 @@ __all__ = [
12
13
  "UrlInfo",
13
14
  "User",
14
15
  ]
16
+ MAX_PER_PAGE = 100
15
17
 
16
18
 
17
19
  class _CatBase:
@@ -149,6 +151,15 @@ class Release(_HasUrl):
149
151
  self.upload_url: str = self["upload_url"]
150
152
  self.zipball_url: str | None = self["zipball_url"]
151
153
 
154
+ @property
155
+ def asset_names(self):
156
+ return {i.name for i in self.assets}
157
+
158
+ @property
159
+ def assets_by_name(self):
160
+ return {i.name: i for i in self.assets}
161
+
162
+ @property
152
163
  def delete(self):
153
164
  self.client.releases.delete(self.url_info.username, self.url_info.repo, self.id)
154
165
 
@@ -158,6 +169,9 @@ class Release(_HasUrl):
158
169
  def update(self, **data):
159
170
  return self.client.releases.update(self.url_info.username, self.url_info.repo, self.id, **data)
160
171
 
172
+ def iter_online_assets(self, count=MAX_PER_PAGE):
173
+ return self.client.release_assets.list_iter(self.url_info.username, self.url_info.repo, self.id, count)
174
+
161
175
 
162
176
  class ReleaseAsset(_HasUrl):
163
177
  """Data related to a release."""
@@ -181,6 +195,22 @@ class ReleaseAsset(_HasUrl):
181
195
  def delete(self):
182
196
  self.client.release_assets.delete(self.url_info.username, self.url_info.repo, self.id)
183
197
 
198
+ def download(self, path, check=True, **kw):
199
+ kw.setdefault("session", self.client.http)
200
+ result = ms.utils.download_file(self.browser_download_url, path, **kw)
201
+ if check:
202
+ mpath = ms.path.Path(path)
203
+ if self.size != mpath.size:
204
+ mpath.delete()
205
+ raise RuntimeError("The file is corrupted (size difference)")
206
+ if self.digest:
207
+ alg, asset_hex = self.digest.split(":", 1)
208
+ local_hex = mpath.hash_hex(alg)
209
+ if asset_hex != local_hex:
210
+ mpath.delete()
211
+ raise RuntimeError("The file is corrupted (hash difference)")
212
+ return result
213
+
184
214
  def reload(self):
185
215
  return self.client.release_assets.get(self.url_info.username, self.url_info.repo, self.id)
186
216
 
@@ -201,11 +231,23 @@ class Client(BaseClient):
201
231
  """Get a release asset"""
202
232
  return ReleaseAsset(self._c, self._r("GET", owner, repo, asset_id))
203
233
 
204
- def list(self, owner: str, repo: str, release_id: int, per_page=30, page=1):
234
+ def list(self, owner: str, repo: str, release_id: int, per_page=MAX_PER_PAGE, page=1):
205
235
  """List release assets"""
206
236
  resp: list = self._c.request("GET", f"repos/{owner}/{repo}/releases/{release_id}/assets", params={"per_page": per_page, "page": page})
207
237
  return [ReleaseAsset(self._c, i) for i in resp]
208
238
 
239
+ def list_iter(self, owner: str, repo: str, release_id: int, count=100):
240
+ page = 1
241
+ completed = 0
242
+ while completed < count:
243
+ per_page = min(count - completed, MAX_PER_PAGE)
244
+ releases = self.list(owner, repo, release_id, per_page, page)
245
+ if not releases:
246
+ break
247
+ yield from releases
248
+ completed += len(releases)
249
+ page += 1
250
+
209
251
  def update(self, owner: str, repo: str, asset_id: int, *,
210
252
  label: str = None,
211
253
  name: str = None,
@@ -297,10 +339,22 @@ class Client(BaseClient):
297
339
  def get_latest(self, owner: str, repo: str):
298
340
  return Release(self._c, self._r("GET", owner, repo, "latest"))
299
341
 
300
- def list(self, owner: str, repo: str, per_page=30, page=1, **kw):
342
+ def list(self, owner: str, repo: str, per_page=MAX_PER_PAGE, page=1):
301
343
  resp: list = self._c.request("GET", f"repos/{owner}/{repo}/releases", params={"per_page": per_page, "page": page})
302
344
  return [Release(self._c, i) for i in resp]
303
345
 
346
+ def list_iter(self, owner: str, repo: str, count=100):
347
+ page = 1
348
+ completed = 0
349
+ while completed < count:
350
+ per_page = min(count - completed, MAX_PER_PAGE)
351
+ releases = self.list(owner, repo, per_page, page)
352
+ if not releases:
353
+ break
354
+ yield from releases
355
+ completed += len(releases)
356
+ page += 1
357
+
304
358
  def update(self, owner: str, repo: str, release_id: int,
305
359
  body: str = None,
306
360
  discussion_category_name: str = None,
@@ -373,8 +427,10 @@ class Client(BaseClient):
373
427
  if kw.get("token"):
374
428
  return cls(**kw) # Токен указан в аргументах
375
429
  if allow_no_token:
376
- return cls(os.environ.get("GITHUB_TOKEN"), **kw) # Может быть без токена
377
- return cls(os.environ["GITHUB_TOKEN"], **kw) # Токен обязателен
430
+ kw["token"] = os.environ.get("GITHUB_TOKEN")
431
+ else:
432
+ kw["token"] = os.environ["GITHUB_TOKEN"]
433
+ return cls(**kw)
378
434
 
379
435
  def request(self, httpm, apim, raw=False, **kw) -> dict | list | requests.Response:
380
436
  if raw:
@@ -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,4 +1,3 @@
1
- import sys
2
1
  import typing
3
2
  import warnings
4
3
  from aiohttp import BasicAuth, hdrs, web
@@ -102,57 +101,3 @@ def auth_basic(header=hdrs.AUTHORIZATION, filter: typing.Callable[[BasicAuth], b
102
101
  return Response(status=401, headers=basic_unauthorized_headers) # Заголовок отсутствует
103
102
  return wrapper
104
103
  return deco
105
-
106
-
107
- @ms.utils.main_func(__name__)
108
- def main(args=None, **kw):
109
- if args is None:
110
- from argparse import ArgumentParser
111
- argp = ArgumentParser()
112
- argp.add_argument("--backlog", default=128, type=int)
113
- argp.add_argument("--handler-cancellation", action="store_true")
114
- argp.add_argument("--handler", help="функция для обработки всех запросов (module:variable)")
115
- argp.add_argument("--keepalive-timeout", default=75, type=int)
116
- argp.add_argument("--no-handle-signals", action="store_true")
117
- argp.add_argument("--no-print", action="store_true", help="отключить вывод запросов")
118
- argp.add_argument("--shutdown-timeout", default=60, type=int)
119
- argp.add_argument("-H", "--host", default="127.0.0.1", help="IP для прослушивания")
120
- argp.add_argument("-p", "--port", default=8080, type=int, help="порт для прослушивания")
121
- argp.description = "Простой HTTP сервер, выводящий содержимое всех запросов"
122
- args = argp.parse_args()
123
- app = Application()
124
- log = ms.utils.mini_log
125
- if args.handler:
126
- import importlib
127
- m_name, f_name = args.handler.split(":", 1)
128
- custom_handler = getattr(importlib.import_module(m_name), f_name)
129
- else:
130
- async def custom_handler(req: Request):
131
- return 200
132
- do_print = not args.no_print
133
-
134
- @app.on_any_path_request(["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"])
135
- async def _(req: Request):
136
- if do_print:
137
- log("Request %s %s from %s", req.method, req.raw_path, req.remote)
138
- log("Headers:")
139
- for k, v in req.headers.items():
140
- log("- %s: %s", k, v)
141
- has_content = False
142
- async for chunk, _ in req.content.iter_chunks():
143
- if chunk:
144
- if not has_content:
145
- has_content = True
146
- log("Content: ")
147
- sys.stderr.buffer.write(chunk)
148
- log("")
149
- return await custom_handler(req)
150
- kw["backlog"] = args.backlog
151
- kw["handle_signals"] = not args.no_handle_signals
152
- kw["handler_cancellation"] = args.handler_cancellation
153
- kw["host"] = args.host
154
- kw["keepalive_timeout"] = args.keepalive_timeout
155
- kw["port"] = args.port
156
- kw["print"] = log
157
- kw["shutdown_timeout"] = args.shutdown_timeout
158
- app.run(**kw)
@@ -0,0 +1,56 @@
1
+ import importlib
2
+ import sys
3
+ from MainShortcuts2.ex.aiohttp_ex.web import *
4
+
5
+
6
+ @ms.utils.main_func(__name__)
7
+ def main(args=None, **kw):
8
+ if args is None:
9
+ from argparse import ArgumentParser
10
+ argp = ArgumentParser()
11
+ argp.add_argument("--backlog", default=128, type=int)
12
+ argp.add_argument("--handler-cancellation", action="store_true")
13
+ argp.add_argument("--handler", help="функция для обработки всех запросов (module:variable)")
14
+ argp.add_argument("--keepalive-timeout", default=75, type=int)
15
+ argp.add_argument("--no-handle-signals", action="store_true")
16
+ argp.add_argument("--no-print", action="store_true", help="отключить вывод запросов")
17
+ argp.add_argument("--shutdown-timeout", default=60, type=int)
18
+ argp.add_argument("-H", "--host", default="127.0.0.1", help="IP для прослушивания")
19
+ argp.add_argument("-p", "--port", default=8080, type=int, help="порт для прослушивания")
20
+ argp.description = "Простой HTTP сервер, выводящий содержимое всех запросов"
21
+ args = argp.parse_args()
22
+ app = Application()
23
+ log = ms.utils.mini_log
24
+ if args.handler:
25
+ m_name, f_name = args.handler.split(":", 1)
26
+ custom_handler = getattr(importlib.import_module(m_name), f_name)
27
+ else:
28
+ async def custom_handler(req: Request):
29
+ return 200
30
+ do_print = not args.no_print
31
+
32
+ @app.on_any_path_request(["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"])
33
+ async def _(req: Request):
34
+ if do_print:
35
+ log("Request %s %s from %s", req.method, req.raw_path, req.remote)
36
+ log("Headers:")
37
+ for k, v in req.headers.items():
38
+ log("- %s: %s", k, v)
39
+ has_content = False
40
+ async for chunk, _ in req.content.iter_chunks():
41
+ if chunk:
42
+ if not has_content:
43
+ has_content = True
44
+ log("Content: ")
45
+ sys.stderr.buffer.write(chunk)
46
+ log("")
47
+ return await custom_handler(req)
48
+ kw["backlog"] = args.backlog
49
+ kw["handle_signals"] = not args.no_handle_signals
50
+ kw["handler_cancellation"] = args.handler_cancellation
51
+ kw["host"] = args.host
52
+ kw["keepalive_timeout"] = args.keepalive_timeout
53
+ kw["port"] = args.port
54
+ kw["print"] = log
55
+ kw["shutdown_timeout"] = args.shutdown_timeout
56
+ app.run(**kw)
@@ -0,0 +1,24 @@
1
+ from functools import cached_property
2
+ from urllib import parse
3
+ from types import MappingProxyType
4
+
5
+
6
+ class ParseResult(parse.ParseResult):
7
+ @cached_property
8
+ def path_parts(self):
9
+ """`path.split('/')`"""
10
+ return tuple(self.path.split("/"))
11
+
12
+ @cached_property
13
+ def query_dict(self):
14
+ return MappingProxyType(dict(self.parse_qsl(keep_blank_values=True)))
15
+
16
+ @classmethod
17
+ def from_url(cls, url: str, **kw):
18
+ return cls(*parse.urlparse(url, **kw))
19
+
20
+ def parse_qs(self, **kw):
21
+ return parse.parse_qs(self.query, **kw)
22
+
23
+ def parse_qsl(self, **kw):
24
+ return parse.parse_qsl(self.query, **kw)
@@ -0,0 +1,339 @@
1
+ import os
2
+ import re
3
+ import shutil
4
+ import subprocess
5
+ import sys
6
+ from datetime import datetime
7
+ from functools import cached_property
8
+ from MainShortcuts2 import ms
9
+ from MainShortcuts2.api import github
10
+ from MainShortcuts2.ex.pathlib_ex import Path
11
+ JAVA_VERSION_PATTERN = re.compile(r'version\s+"([^"]+)"')
12
+ SEARCH_RELEASE_COUNT = 1000
13
+
14
+
15
+ class AssetNotFoundError(Exception):
16
+ pass
17
+
18
+
19
+ class VersionNotFoundError(Exception):
20
+ pass
21
+
22
+
23
+ def find_system_java():
24
+ plat = ms.advanced.get_platform()
25
+ suffix = ".exe" if plat.is_mustdie else ""
26
+ if os.environ.get("JAVA_HOME"):
27
+ file = (Path(os.environ["JAVA_HOME"]) / f"bin/java{suffix}").resolve()
28
+ else:
29
+ str_file = shutil.which("java")
30
+ if not str_file:
31
+ raise RuntimeError("System java not found")
32
+ file = Path(str_file).resolve()
33
+ if not file.is_file():
34
+ raise RuntimeError("System java not found")
35
+ return file
36
+
37
+
38
+ def _parse_java_version(text: str):
39
+ return list(map(int, text.split("_", 1)[0].split(".")))
40
+
41
+
42
+ def detect_java_version(file: Path):
43
+ release_file = file.parent.parent / "release"
44
+ if release_file.exists():
45
+ try:
46
+ for line in release_file.read_lines_iter(True):
47
+ if "=" in line:
48
+ k, v = map(str.strip, line.split("=", 1))
49
+ if k == "JAVA_VERSION":
50
+ return _parse_java_version(v.strip('"').strip("'"))
51
+ except Exception:
52
+ pass
53
+ try:
54
+ p = subprocess.run([str(file), "-version"], capture_output=True, check=True, text=True)
55
+ match = JAVA_VERSION_PATTERN.search(p.stderr + p.stdout)
56
+ return _parse_java_version(match.group(1))
57
+ except Exception:
58
+ raise RuntimeError("Couldn't determine the Java version") from None
59
+
60
+
61
+ class IncompatibleJava(Exception):
62
+ pass
63
+
64
+
65
+ class InfoDict(dict):
66
+ @property
67
+ def builded_at(self):
68
+ """Дата сборки релиза (Unix timestamp)"""
69
+ return self["builded_at"]
70
+
71
+ @cached_property
72
+ def builded_at_dt(self):
73
+ """Дата сборки релиза (`datetime`)"""
74
+ return datetime.fromtimestamp(self.builded_at)
75
+
76
+ @property
77
+ def classes(self) -> dict[str, str]:
78
+ """Словарь классов"""
79
+ return self.get("classes", {})
80
+
81
+ @property
82
+ def max_java_version(self) -> list[int] | None:
83
+ """Максимальная версия Java"""
84
+ return self.get("max_java_version")
85
+
86
+ @property
87
+ def min_java_version(self) -> list[int] | None:
88
+ """Минимальная версия Java"""
89
+ return self.get("max_java_version")
90
+
91
+ @property
92
+ def name(self):
93
+ """Название"""
94
+ return self["name"]
95
+
96
+ @property
97
+ def version(self):
98
+ """Название версии"""
99
+ return self["version"]
100
+
101
+ @property
102
+ def version_id(self):
103
+ """ID версии"""
104
+ return self["version_id"]
105
+
106
+ def check_java_file(self, file: Path):
107
+ if (self.max_java_version is None) and (self.min_java_version is None):
108
+ return True # Нет информации о версии
109
+ return self.check_java_version(detect_java_version(file))
110
+
111
+ def check_java_version(self, version: list[int]):
112
+ if self.max_java_version is not None:
113
+ if version > self.max_java_version:
114
+ raise IncompatibleJava(f"Incompatible Java version (current: {version}, max: {self.max_java_version})")
115
+ if self.min_java_version is not None:
116
+ if self.min_java_version > version:
117
+ raise IncompatibleJava(f"Incompatible Java version (current: {version}, min: {self.min_java_version})")
118
+ return True
119
+
120
+ def version_id_in_range(self, min_id=0, max_id=sys.maxsize):
121
+ """Проверить находится ли ID версии в диапазоне"""
122
+ return bool(min_id <= self.version_id < max_id)
123
+
124
+
125
+ class JavaExtManager(ms.ObjectBase):
126
+ """Менеджер версий JAR файла с кешированием"""
127
+
128
+ def __init__(self, repo_owner: str, repo_name: str, github_token=None):
129
+ self.gh = github.Client.from_env(True, token=github_token)
130
+ self.repo = repo_owner, repo_name
131
+ self.version_cache: "dict[str,JavaExtVersion]" = {}
132
+
133
+ @cached_property
134
+ def dir(self):
135
+ """Папка кеша"""
136
+ plat = ms.advanced.get_platform()
137
+ if plat.is_mustdie:
138
+ if os.environ.get("LOCALAPPDATA"):
139
+ base = Path(os.environ["LOCALAPPDATA"])
140
+ else:
141
+ base = Path.home() / "AppData/Local"
142
+ else:
143
+ if os.environ.get("XDG_DATA_HOME"):
144
+ base = Path(os.environ["XDG_DATA_HOME"])
145
+ else:
146
+ base = Path.home() / ".local/share"
147
+ result = base / "MainPlay_TG/MainShortcuts2/java_ext_cache_v1" / self.repo[0] / self.repo[1]
148
+ return result.resolve().any_mkdir()
149
+
150
+ def get_version(self, version_name: str):
151
+ """Получить определённую версию (по названию)"""
152
+ if not version_name in self.version_cache:
153
+ version = JavaExtVersion(self, version_name)
154
+ if version.info.version == version_name:
155
+ self.version_cache[version_name] = version
156
+ return self.version_cache[version_name]
157
+
158
+ def iter_online_releases(self, count=github.MAX_PER_PAGE):
159
+ """Итерация релизов с GitHub"""
160
+ return self.gh.releases.list_iter(*self.repo, count)
161
+
162
+ def iter_online_versions(self, count=github.MAX_PER_PAGE):
163
+ """Итерация версий с GitHub"""
164
+ for rel in self.iter_online_releases(count):
165
+ if rel.tag_name.startswith("v"):
166
+ ver = self.get_version(rel.tag_name[1:])
167
+ ver.__dict__["release"] = rel # cached_property
168
+ yield ver
169
+
170
+ def iter_offline_versions(self):
171
+ """Итерация версий из кэша"""
172
+ info_list: list[InfoDict] = []
173
+ for file in self.dir.iterdir():
174
+ if file.suffix == ".json" and file.is_file():
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
181
+
182
+ def iter_all_versions(self, online_count=github.MAX_PER_PAGE):
183
+ """Итерация версий из кэша и с GitHub"""
184
+ yield from self.iter_offline_versions() # Сначала локальные
185
+ yield from self.iter_online_versions(online_count)
186
+
187
+ def search_has_class(self, class_name: str, max_count=SEARCH_RELEASE_COUNT):
188
+ """Найти версию с указанным классом"""
189
+ return self.search_has_classes([class_name], max_count)
190
+
191
+ def search_has_classes(self, class_names: set[str], max_count=SEARCH_RELEASE_COUNT):
192
+ """Найти версию, в которой есть все указанные классы"""
193
+ class_names = set(class_names)
194
+ for ver in self.iter_all_versions(max_count):
195
+ classes = ver.info.classes
196
+ if all(i in classes for i in class_names):
197
+ return ver
198
+ if len(class_names) == 1:
199
+ raise VersionNotFoundError("Has class: " + class_names.pop())
200
+ raise VersionNotFoundError("Has classes: " + ", ".join(class_names))
201
+
202
+ def search_version_id_range(self, min_id: int = 0, max_id=sys.maxsize, max_count=SEARCH_RELEASE_COUNT):
203
+ """Найти версию с ID в указанном диапазоне"""
204
+ for ver in self.iter_all_versions(max_count):
205
+ if ver.info.version_id_in_range(min_id, max_id):
206
+ return ver
207
+ raise VersionNotFoundError(f"{min_id} <= version_id <= {max_id}")
208
+
209
+ def search_version_id(self, version_id: int, max_count=SEARCH_RELEASE_COUNT):
210
+ """Найти версию с указанным ID"""
211
+ for ver in self.iter_all_versions(max_count):
212
+ if ver.info.version_id == version_id:
213
+ return ver
214
+ raise VersionNotFoundError(f"version_id == {version_id}")
215
+
216
+ def download_latest(self, count=1, download_jar=True):
217
+ """Скачать последнюю версию (если не скачана)"""
218
+ results: list[JavaExtVersion] = []
219
+ for ver in self.iter_online_versions(count):
220
+ ver.info # Скачать файл JSON
221
+ if download_jar:
222
+ ver.jar_path # Скачать файл JAR
223
+ results.append(ver)
224
+ return results
225
+
226
+
227
+ class JavaExtVersion(ms.ObjectBase):
228
+ """Версия JAR файла"""
229
+
230
+ def __init__(self, mgr: JavaExtManager, version_name: str):
231
+ self._java_bin = None
232
+ self._java_checked = True
233
+ self.mgr = mgr
234
+ self.version_name = version_name
235
+
236
+ @cached_property
237
+ def release(self):
238
+ """Релиз этой версии на GitHub"""
239
+ return self.mgr.gh.releases.get_by_tag_name(*self.mgr.repo, "v" + self.version_name)
240
+
241
+ @cached_property
242
+ def info_asset(self):
243
+ """Ассет файла информации на GitHub"""
244
+ return self._get_asset("info.json")
245
+
246
+ @cached_property
247
+ def info(self):
248
+ """Информация о версии (кэшируется)"""
249
+ if self.info_path.exists():
250
+ return InfoDict(self.info_path.read_json())
251
+ with ms.utils.request("GET", self.info_asset.browser_download_url, session=self.mgr.gh.http) as resp:
252
+ data = InfoDict(resp.json())
253
+ self.info_path.write_json(data)
254
+ return data
255
+
256
+ @cached_property
257
+ def info_path(self):
258
+ """Путь к файлу информации"""
259
+ return self.mgr.dir / f"{self.version_name}.json"
260
+
261
+ @cached_property
262
+ def jar_asset(self):
263
+ """Ассет JAR файла на GitHub"""
264
+ return self._get_asset(f"{self.info.name}-{self.info.version}.jar")
265
+
266
+ @cached_property
267
+ def _jar_path(self):
268
+ """Файл JAR (без скачивания)"""
269
+ return self.mgr.dir / f"{self.version_name}.jar"
270
+
271
+ @property
272
+ def jar_path(self):
273
+ """Файл JAR (кэшируется)"""
274
+ if not self._jar_path.exists():
275
+ ms.utils.mini_log("[MainShortcuts2/java_ext] Downloading %s from %s/%s (%s)", self._jar_path.name, *self.mgr.repo, self.jar_asset.browser_download_url)
276
+ self.jar_asset.download(self._jar_path)
277
+ return self._jar_path
278
+
279
+ @property
280
+ def java_bin(self) -> Path:
281
+ """Путь к исполняемому файлу Java (можно изменить)"""
282
+ if self._java_bin is None:
283
+ self._java_bin = find_system_java()
284
+ self._java_checked = False
285
+ return self._java_bin
286
+
287
+ @java_bin.setter
288
+ def java_bin(self, v):
289
+ if v is None:
290
+ self._java_bin = None
291
+ return
292
+ file = Path(v).resolve()
293
+ if not file.exists():
294
+ raise FileNotFoundError(file)
295
+ self._java_bin = file
296
+ self._java_checked = False
297
+
298
+ def _get_asset(self, name: str):
299
+ for i in self.release.assets:
300
+ if i.name == name:
301
+ return i
302
+ raise AssetNotFoundError(name)
303
+
304
+ def delete_cache(self, keep_info=True):
305
+ """Удалить версию из кэша (будет скачана автоматически при использовании"""
306
+ self._jar_path.remove()
307
+ if not keep_info:
308
+ self.info_path.remove()
309
+
310
+ def _popen(self, args, **kw):
311
+ if not self._java_checked:
312
+ self.info.check_java_file(self.java_bin)
313
+ self._java_checked = True
314
+ return subprocess.Popen([str(self.java_bin), *args], **kw)
315
+
316
+ def _run(self, args, **kw) -> subprocess.CompletedProcess:
317
+ kw.setdefault("check", True)
318
+ if not self._java_checked:
319
+ self.info.check_java_file(self.java_bin)
320
+ self._java_checked = True
321
+ return subprocess.run([str(self.java_bin), *args], **kw)
322
+
323
+ def popen_class_raw(self, class_path: str, args: list[str] = [], **kw):
324
+ return self._popen(["-cp", str(self.jar_path), class_path, *args], **kw)
325
+
326
+ def popen_class(self, class_name: str, args: list[str] = [], **kw):
327
+ return self.popen_class_raw(self.info.classes[class_name], args, **kw)
328
+
329
+ def popen(self, args: list[str], **kw):
330
+ return self._popen(["-jar", str(self.jar_path), *args], **kw)
331
+
332
+ def run_class_raw(self, class_path: str, args: list[str] = [], **kw):
333
+ return self._run(["-cp", str(self.jar_path), class_path, *args], **kw)
334
+
335
+ def run_class(self, class_name: str, args: list[str] = [], **kw):
336
+ return self.run_class_raw(self.info.classes[class_name], args, **kw)
337
+
338
+ def run(self, args: list[str], **kw):
339
+ return self._run(["-jar", str(self.jar_path), *args], **kw)