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.
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/PKG-INFO +1 -1
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/pyproject.toml +2 -1
- mainshortcuts2-2.7.4/src/MainShortcuts2/_module_info.py +2 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/api/github.py +60 -4
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/core_config.py +9 -14
- 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
- mainshortcuts2-2.7.4/src/MainShortcuts2/ex/aiohttp_ex/web/__main__.py +56 -0
- mainshortcuts2-2.7.4/src/MainShortcuts2/ex/urlparse_ex.py +24 -0
- mainshortcuts2-2.7.4/src/MainShortcuts2/java_ext/__init__.py +339 -0
- mainshortcuts2-2.7.4/src/MainShortcuts2/java_ext/ms2ext.py +304 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/ms2hash.py +20 -2
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/types.py +92 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/utils.py +43 -22
- mainshortcuts2-2.7.2/src/MainShortcuts2/_module_info.py +0 -2
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/README.md +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/__init__.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/__main__.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/_any2json_regs.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/_ms2app_regs.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/advanced.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/any2json.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/api/base.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/api/gigachat.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/api/russian_trusted_root_ca_pem.crt +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/api/russian_trusted_sub_ca_pem.crt +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/api/webdav.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/cfg.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/core.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/dict.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/dir.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/ex/__init__.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/ex/datetime_ex.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/ex/pathlib_ex.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/ex/pil_ex.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/ex/psutil_ex.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/ex/sqlite_ex.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/file.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/gui_scripts/__init__.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/gui_scripts/ms2_hash_gen.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/gui_scripts/utils.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/json.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/linux.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/list.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/ms2app.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/path.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/proc.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/regex.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/special_chars.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/sql/_sql_base.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/sql/postgresql.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/sql/sqlite.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/str.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/term.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.4}/src/MainShortcuts2/win.py +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
|
-
version = "2.7.
|
|
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"
|
|
@@ -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=
|
|
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=
|
|
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
|
-
|
|
377
|
-
|
|
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
|
-
|
|
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
|
|
61
|
-
import
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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)
|