MainShortcuts2 2.7.2__tar.gz → 2.7.3__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.3}/PKG-INFO +1 -1
  2. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/pyproject.toml +1 -1
  3. mainshortcuts2-2.7.3/src/MainShortcuts2/_module_info.py +2 -0
  4. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/api/github.py +60 -4
  5. mainshortcuts2-2.7.2/src/MainShortcuts2/ex/aiohttp_ex/web.py → mainshortcuts2-2.7.3/src/MainShortcuts2/ex/aiohttp_ex/web/__init__.py +0 -55
  6. mainshortcuts2-2.7.3/src/MainShortcuts2/ex/aiohttp_ex/web/__main__.py +56 -0
  7. mainshortcuts2-2.7.3/src/MainShortcuts2/ex/urlparse_ex.py +24 -0
  8. mainshortcuts2-2.7.3/src/MainShortcuts2/java_ext/__init__.py +333 -0
  9. mainshortcuts2-2.7.3/src/MainShortcuts2/java_ext/ms2ext.py +304 -0
  10. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/ms2hash.py +4 -2
  11. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/utils.py +23 -22
  12. mainshortcuts2-2.7.2/src/MainShortcuts2/_module_info.py +0 -2
  13. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/README.md +0 -0
  14. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/__init__.py +0 -0
  15. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/__main__.py +0 -0
  16. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/_any2json_regs.py +0 -0
  17. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/_ms2app_regs.py +0 -0
  18. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/advanced.py +0 -0
  19. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/any2json.py +0 -0
  20. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/api/base.py +0 -0
  21. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/api/gigachat.py +0 -0
  22. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/api/russian_trusted_root_ca_pem.crt +0 -0
  23. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/api/russian_trusted_sub_ca_pem.crt +0 -0
  24. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/api/webdav.py +0 -0
  25. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/cfg.py +0 -0
  26. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/core.py +0 -0
  27. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/core_config.py +0 -0
  28. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/dict.py +0 -0
  29. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/dir.py +0 -0
  30. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/ex/__init__.py +0 -0
  31. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/ex/datetime_ex.py +0 -0
  32. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/ex/pathlib_ex.py +0 -0
  33. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/ex/pil_ex.py +0 -0
  34. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/ex/psutil_ex.py +0 -0
  35. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/ex/sqlite_ex.py +0 -0
  36. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/file.py +0 -0
  37. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/gui_scripts/__init__.py +0 -0
  38. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/gui_scripts/ms2_hash_gen.py +0 -0
  39. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/gui_scripts/utils.py +0 -0
  40. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/json.py +0 -0
  41. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/linux.py +0 -0
  42. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/list.py +0 -0
  43. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/ms2app.py +0 -0
  44. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/path.py +0 -0
  45. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/proc.py +0 -0
  46. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/regex.py +0 -0
  47. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/special_chars.py +0 -0
  48. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/sql/_sql_base.py +0 -0
  49. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/sql/postgresql.py +0 -0
  50. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/sql/sqlite.py +0 -0
  51. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/str.py +0 -0
  52. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/term.py +0 -0
  53. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/types.py +0 -0
  54. {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/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.3
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.3"
3
3
  name = "MainShortcuts2"
4
4
  description = "Сокращение и улучшение функций + консольные утилиты"
5
5
  authors = ["MainPlay TG <xbox.roman6666666666@gmail.com>"]
@@ -0,0 +1,2 @@
1
+ name = 'mainshortcuts2'
2
+ version = '2.7.3'
@@ -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:
@@ -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,333 @@
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
+ for file in self.dir.iterdir():
173
+ 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
178
+
179
+ def iter_all_versions(self, online_count=github.MAX_PER_PAGE):
180
+ """Итерация версий из кэша и с GitHub"""
181
+ yield from self.iter_offline_versions() # Сначала локальные
182
+ yield from self.iter_online_versions(online_count)
183
+
184
+ def search_has_class(self, class_name: str, max_count=SEARCH_RELEASE_COUNT):
185
+ """Найти версию с указанным классом"""
186
+ return self.search_has_classes([class_name], max_count)
187
+
188
+ def search_has_classes(self, class_names: set[str], max_count=SEARCH_RELEASE_COUNT):
189
+ """Найти версию, в которой есть все указанные классы"""
190
+ class_names = set(class_names)
191
+ for ver in self.iter_all_versions(max_count):
192
+ classes = ver.info.classes
193
+ if all(i in classes for i in class_names):
194
+ return ver
195
+ if len(class_names) == 1:
196
+ raise VersionNotFoundError("Has class: " + class_names.pop())
197
+ raise VersionNotFoundError("Has classes: " + ", ".join(class_names))
198
+
199
+ def search_version_id_range(self, min_id: int = 0, max_id=sys.maxsize, max_count=SEARCH_RELEASE_COUNT):
200
+ """Найти версию с ID в указанном диапазоне"""
201
+ for ver in self.iter_all_versions(max_count):
202
+ if ver.info.version_id_in_range(min_id, max_id):
203
+ return ver
204
+ raise VersionNotFoundError(f"{min_id} <= version_id <= {max_id}")
205
+
206
+ def search_version_id(self, version_id: int, max_count=SEARCH_RELEASE_COUNT):
207
+ """Найти версию с указанным ID"""
208
+ for ver in self.iter_all_versions(max_count):
209
+ if ver.info.version_id == version_id:
210
+ return ver
211
+ raise VersionNotFoundError(f"version_id == {version_id}")
212
+
213
+ def download_latest(self, count=1, download_jar=True):
214
+ """Скачать последнюю версию (если не скачана)"""
215
+ for ver in self.iter_online_versions(count):
216
+ ver.info # Скачать файл JSON
217
+ if download_jar:
218
+ ver.jar_path # Скачать файл JAR
219
+
220
+
221
+ class JavaExtVersion(ms.ObjectBase):
222
+ """Версия JAR файла"""
223
+
224
+ def __init__(self, mgr: JavaExtManager, version_name: str):
225
+ self._java_bin = None
226
+ self._java_checked = True
227
+ self.mgr = mgr
228
+ self.version_name = version_name
229
+
230
+ @cached_property
231
+ def release(self):
232
+ """Релиз этой версии на GitHub"""
233
+ return self.mgr.gh.releases.get_by_tag_name(*self.mgr.repo, "v" + self.version_name)
234
+
235
+ @cached_property
236
+ def info_asset(self):
237
+ """Ассет файла информации на GitHub"""
238
+ return self._get_asset("info.json")
239
+
240
+ @cached_property
241
+ def info(self):
242
+ """Информация о версии (кэшируется)"""
243
+ if self.info_path.exists():
244
+ return InfoDict(self.info_path.read_json())
245
+ with ms.utils.request("GET", self.info_asset.browser_download_url, session=self.mgr.gh.http) as resp:
246
+ data = InfoDict(resp.json())
247
+ self.info_path.write_json(data)
248
+ return data
249
+
250
+ @cached_property
251
+ def info_path(self):
252
+ """Путь к файлу информации"""
253
+ return self.mgr.dir / f"{self.version_name}.json"
254
+
255
+ @cached_property
256
+ def jar_asset(self):
257
+ """Ассет JAR файла на GitHub"""
258
+ return self._get_asset(f"{self.info.name}-{self.info.version}.jar")
259
+
260
+ @cached_property
261
+ def _jar_path(self):
262
+ """Файл JAR (без скачивания)"""
263
+ return self.mgr.dir / f"{self.version_name}.jar"
264
+
265
+ @property
266
+ def jar_path(self):
267
+ """Файл JAR (кэшируется)"""
268
+ if not self._jar_path.exists():
269
+ 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)
270
+ self.jar_asset.download(self._jar_path)
271
+ return self._jar_path
272
+
273
+ @property
274
+ def java_bin(self) -> Path:
275
+ """Путь к исполняемому файлу Java (можно изменить)"""
276
+ if self._java_bin is None:
277
+ self._java_bin = find_system_java()
278
+ self._java_checked = False
279
+ return self._java_bin
280
+
281
+ @java_bin.setter
282
+ def java_bin(self, v):
283
+ if v is None:
284
+ self._java_bin = None
285
+ return
286
+ file = Path(v).resolve()
287
+ if not file.exists():
288
+ raise FileNotFoundError(file)
289
+ self._java_bin = file
290
+ self._java_checked = False
291
+
292
+ def _get_asset(self, name: str):
293
+ for i in self.release.assets:
294
+ if i.name == name:
295
+ return i
296
+ raise AssetNotFoundError(name)
297
+
298
+ def delete_cache(self, keep_info=True):
299
+ """Удалить версию из кэша (будет скачана автоматически при использовании"""
300
+ self._jar_path.remove()
301
+ if not keep_info:
302
+ self.info_path.remove()
303
+
304
+ def _popen(self, args, **kw):
305
+ if not self._java_checked:
306
+ self.info.check_java_file(self.java_bin)
307
+ self._java_checked = True
308
+ return subprocess.Popen([str(self.java_bin), *args], **kw)
309
+
310
+ def _run(self, args, **kw) -> subprocess.CompletedProcess:
311
+ kw.setdefault("check", True)
312
+ if not self._java_checked:
313
+ self.info.check_java_file(self.java_bin)
314
+ self._java_checked = True
315
+ return subprocess.run([str(self.java_bin), *args], **kw)
316
+
317
+ def popen_class_raw(self, class_path: str, args: list[str] = [], **kw):
318
+ return self._popen(["-cp", str(self.jar_path), class_path, *args], **kw)
319
+
320
+ def popen_class(self, class_name: str, args: list[str] = [], **kw):
321
+ return self.popen_class_raw(self.info.classes[class_name], args, **kw)
322
+
323
+ def popen(self, args: list[str], **kw):
324
+ return self._popen(["-jar", str(self.jar_path), *args], **kw)
325
+
326
+ def run_class_raw(self, class_path: str, args: list[str] = [], **kw):
327
+ return self._run(["-cp", str(self.jar_path), class_path, *args], **kw)
328
+
329
+ def run_class(self, class_name: str, args: list[str] = [], **kw):
330
+ return self.run_class_raw(self.info.classes[class_name], args, **kw)
331
+
332
+ def run(self, args: list[str], **kw):
333
+ return self._run(["-jar", str(self.jar_path), *args], **kw)
@@ -0,0 +1,304 @@
1
+ """Функции для работы с большими файлами с помощью расширения на Java. При недоступности расширения будут использоваться встроенные функции"""
2
+ import base64
3
+ import hashlib
4
+ import os
5
+ import requests
6
+ import subprocess
7
+ import typing
8
+ from concurrent.futures import ThreadPoolExecutor
9
+ from contextlib import ExitStack
10
+ from MainShortcuts2 import java_ext
11
+ from MainShortcuts2 import ms
12
+ from MainShortcuts2.ex.pathlib_ex import Path
13
+ mgr = java_ext.JavaExtManager("MainPlay-TG", "MS2Ext.java")
14
+ class2version: dict[str, java_ext.JavaExtVersion] = {}
15
+
16
+
17
+ def get_ver(class_name):
18
+ if not class_name in class2version:
19
+ class2version[class_name] = mgr.search_has_class(class_name)
20
+ return class2version[class_name]
21
+
22
+
23
+ def popen_class(class_name, **kw) -> subprocess.Popen[bytes]:
24
+ kw["stderr"] = subprocess.PIPE
25
+ kw["stdin"] = subprocess.PIPE
26
+ return get_ver(class_name).popen_class(class_name, **kw)
27
+
28
+
29
+ class ExtCoreError(subprocess.CalledProcessError):
30
+ """Ошибка при запуске расширения (повтор через fallback)"""
31
+ pass
32
+
33
+
34
+ class ExtOperationError(subprocess.CalledProcessError):
35
+ """Ошибка при работе расширения (конечная ошибка)"""
36
+ pass
37
+
38
+
39
+ ErrorsForFallback = (
40
+ ExtCoreError, # Не удалось запустить расширение
41
+ java_ext.IncompatibleJava, # Несовместимая Java
42
+ requests.exceptions.RequestException, # Не удалось скачать расширение
43
+ )
44
+
45
+
46
+ def run_class(class_name, config, timeout=None, **kw):
47
+ with popen_class(class_name) as p:
48
+ p.stdin.write(ms.json.encode(config).encode("utf-8"))
49
+ p.stdin.flush()
50
+ p.stdin.close()
51
+ if p.wait(timeout):
52
+ try:
53
+ stderr = p.stderr.read().decode("utf-8")
54
+ err_data = ms.json.decode(stderr)
55
+ except Exception:
56
+ raise ExtCoreError(p.returncode, p.args, p.stdout, p.stderr)
57
+ raise ExtOperationError(p.returncode, p.args, p.stdout, err_data)
58
+ stderr = p.stderr.read()
59
+ if not stderr:
60
+ return {}
61
+ return ms.json.decode(stderr.decode("utf-8"))
62
+
63
+
64
+ class configs:
65
+ class _Base(dict):
66
+ def __setitem__(self, k, v):
67
+ if v is None:
68
+ return
69
+ dict.__setitem__(self, k, v)
70
+
71
+ def get(self, k, d=None):
72
+ if k in self:
73
+ return self[k] or d
74
+ return d
75
+
76
+ class FileDownloaderV1(_Base):
77
+ @property
78
+ def bufSize(self) -> int:
79
+ return self.get("bufSize") or 1024 * 32
80
+
81
+ @property
82
+ def checkStatus(self) -> bool:
83
+ return self.get("checkStatus", True)
84
+
85
+ @property
86
+ def followRedirects(self) -> bool:
87
+ return self.get("followRedirects", True)
88
+
89
+ @property
90
+ def headers(self) -> dict[str, str]:
91
+ return self.get("headers") or {}
92
+
93
+ @property
94
+ def method(self) -> str:
95
+ return self.get("method") or "GET"
96
+
97
+ @property
98
+ def path(self) -> str:
99
+ return self["path"]
100
+
101
+ @property
102
+ def query(self) -> dict[str, str]:
103
+ return self.get("query") or {}
104
+
105
+ @property
106
+ def sizeLimit(self) -> int | None:
107
+ return self.get("sizeLimit")
108
+
109
+ @property
110
+ def url(self) -> str:
111
+ return self["url"]
112
+
113
+ @property
114
+ def verify(self) -> bool:
115
+ return self.get("verify", True)
116
+
117
+ class _FileHasherBase(_Base):
118
+ @property
119
+ def algs(self) -> list[str]:
120
+ return self["algs"]
121
+
122
+ @property
123
+ def bufSize(self) -> int:
124
+ return self.get("bufSize") or 1024 * 1024 * 4
125
+
126
+ class FileHasherV1(_FileHasherBase):
127
+ @property
128
+ def path(self) -> str:
129
+ return self["path"]
130
+
131
+ class FileHasherV2(_FileHasherBase):
132
+ @property
133
+ def maxThreads(self) -> int:
134
+ return self.get("maxThreads") or 8
135
+
136
+ @property
137
+ def paths(self) -> list[str]:
138
+ return self["paths"]
139
+
140
+
141
+ class results:
142
+ class FileDownloaderV1(dict):
143
+ @property
144
+ def downloadedSize(self) -> int:
145
+ return self["downloadedSize"]
146
+
147
+ @property
148
+ def headers(self) -> dict[str, list[str]]:
149
+ return self["headers"]
150
+
151
+ @property
152
+ def statusCode(self) -> int:
153
+ return self["statusCode"]
154
+
155
+ class FileHasherV1(dict[str, bytes]):
156
+ @classmethod
157
+ def _from_b64(cls, data: dict[str, str]):
158
+ return cls({k: base64.b64decode(v) for k, v in data.items()})
159
+ FileHasherV2 = dict[str, FileHasherV1]
160
+
161
+
162
+ class real:
163
+ @classmethod
164
+ def file_downloader_v1(cls, config: configs.FileDownloaderV1, **kw):
165
+ return results.FileDownloaderV1(run_class("file_downloader_v1", config, **kw))
166
+
167
+ @classmethod
168
+ def file_hasher_v1(cls, config: configs.FileHasherV1, **kw):
169
+ if not config.get("algs"):
170
+ return results.FileHasherV1()
171
+ return results.FileHasherV1._from_b64(run_class("file_hasher_v1", config, **kw))
172
+
173
+ @classmethod
174
+ def file_hasher_v2(cls, config: configs.FileHasherV2, **kw):
175
+ if not config.get("paths"):
176
+ return results.FileHasherV2()
177
+ if not config.get("algs"):
178
+ return results.FileHasherV2({i: results.FileHasherV1() for i in config.paths})
179
+ result = run_class("file_hasher_v2", config, **kw)
180
+ return results.FileHasherV2({k: results.FileHasherV1._from_b64(v) for k, v in result.items()})
181
+
182
+
183
+ class fallback:
184
+ @classmethod
185
+ def file_downloader_v1(cls, config: configs.FileDownloaderV1):
186
+ file = Path(config.path).resolve()
187
+ with ExitStack() as stack:
188
+ resp = stack.enter_context(requests.request(config.method, config.url,
189
+ allow_redirects=config.followRedirects,
190
+ headers=config.headers,
191
+ params=config.query,
192
+ stream=True,
193
+ verify=config.verify,
194
+ ))
195
+ if config.checkStatus:
196
+ resp.raise_for_status()
197
+ tmp = stack.enter_context(ms.path.TempFiles(file)) # Авто удаление недокачанного файла
198
+ f = stack.enter_context(file.open("wb")) # Открытие файла
199
+ cls._download_file(resp.iter_content(config.bufSize), f.write, config.sizeLimit) # Скачивание файла
200
+ tmp.files.clear() # Отключить авто удаление
201
+ result = results.FileDownloaderV1()
202
+ result["statusCode"] = resp.status_code
203
+ result["downloadedSize"] = f.tell()
204
+ result["headers"] = {k: [v] for k, v in resp.headers.items()}
205
+ return result
206
+
207
+ @classmethod
208
+ def _download_file(cls, stream: typing.Iterable[bytes], write_func: typing.Callable[[bytes], None], sizeLimit: int | None):
209
+ if sizeLimit is None:
210
+ for buf in stream:
211
+ write_func(buf)
212
+ return
213
+ downloaded = 0
214
+ for buf in stream:
215
+ downloaded += len(buf)
216
+ if sizeLimit > downloaded:
217
+ raise OverflowError("The file is too big")
218
+ write_func(buf)
219
+
220
+ @classmethod
221
+ def file_hasher_v1(cls, config: configs.FileHasherV1):
222
+ if not config.get("algs"):
223
+ return results.FileHasherV1()
224
+ return cls._hash_file(Path(config.path), set(config.algs), config.bufSize)
225
+
226
+ @classmethod
227
+ def file_hasher_v2(cls, config: configs.FileHasherV2):
228
+ if not config.get("paths"):
229
+ return results.FileHasherV2()
230
+ if not config.get("algs"):
231
+ return results.FileHasherV2({i: results.FileHasherV1() for i in config.paths})
232
+ algs, bufsize = set(config.algs), config.bufSize
233
+ with ThreadPoolExecutor(config.maxThreads) as pool:
234
+ futures = {i: pool.submit(cls._hash_file, Path(i), algs, bufsize) for i in config.paths}
235
+ return results.FileHasherV2({k: v.result() for k, v in futures.items()})
236
+
237
+ @classmethod
238
+ def _hash_file(cls, file: Path, algs: set[str], bufSize: int):
239
+ hashmap = {i: hashlib.new(i) for i in algs}
240
+ update_func = [i.update for i in hashmap.values()]
241
+ with file.open("rb") as f:
242
+ read_func = f.read
243
+ while True:
244
+ buf = read_func(bufSize)
245
+ if not buf:
246
+ break
247
+ for uf in update_func:
248
+ uf(buf)
249
+ return results.FileHasherV1({k: v.digest() for k, v in hashmap.items()})
250
+
251
+
252
+ def download_file(url: str, path: os.PathLike, *,
253
+ bufSize: int = None,
254
+ checkStatus=True,
255
+ followRedirects=True,
256
+ headers: dict[str, str] = None,
257
+ method="GET",
258
+ query: dict[str, str] = None,
259
+ sizeLimit: int = None,
260
+ verify=True,
261
+ **kw):
262
+ """Скачать большой файл по HTTP"""
263
+ config = configs.FileDownloaderV1()
264
+ config["bufSize"] = bufSize
265
+ config["checkStatus"] = checkStatus
266
+ config["followRedirects"] = followRedirects
267
+ config["headers"] = headers
268
+ config["method"] = method
269
+ config["path"] = os.fspath(path)
270
+ config["query"] = query
271
+ config["sizeLimit"] = sizeLimit
272
+ config["url"] = url
273
+ config["verify"] = verify
274
+ if config.sizeLimit:
275
+ if config.bufSize > config.sizeLimit:
276
+ return fallback.file_downloader_v1(config)
277
+ try:
278
+ return real.file_downloader_v1(config, **kw)
279
+ except ErrorsForFallback:
280
+ return fallback.file_downloader_v1(config)
281
+
282
+
283
+ def hash_file(path: os.PathLike, algs: set[str], *, bufSize: int = None, **kw):
284
+ """Хешировать большой файл"""
285
+ config = configs.FileHasherV1()
286
+ config["algs"] = list(algs)
287
+ config["bufSize"] = bufSize
288
+ config["path"] = os.fspath(path)
289
+ try:
290
+ return real.file_hasher_v1(config, **kw)
291
+ except ErrorsForFallback:
292
+ return fallback.file_hasher_v1(config)
293
+
294
+
295
+ def hash_many_files(paths: set[os.PathLike], algs: set[str], *, bufSize: int = None, **kw):
296
+ """Хешировать несколько файлов"""
297
+ config = configs.FileHasherV2()
298
+ config["algs"] = list(algs)
299
+ config["bufSize"] = bufSize
300
+ config["paths"] = [os.fspath(i) for i in paths]
301
+ try:
302
+ return real.file_hasher_v2(config, **kw)
303
+ except ErrorsForFallback:
304
+ return fallback.file_hasher_v2(config)
@@ -86,7 +86,8 @@ class Format1:
86
86
 
87
87
  def hash_gen(args: argparse.Namespace = None):
88
88
  if args is None:
89
- argp = argparse.ArgumentParser("ms2-hash_gen", description="создание контрольной суммы для файла")
89
+ argp = argparse.ArgumentParser("ms2-hash_gen", description="Создание контрольной суммы для файла")
90
+ argp.epilog = "Написано на Python"
90
91
  argp.add_argument("files", nargs="+", help="пути к файлам")
91
92
  argp.add_argument("-b", "--bar", action="store_true", help="показывать прогрессбар (нужен модуль progressbar2)")
92
93
  argp.add_argument("-f", "--force", action="store_true", help="перезаписывать существующие хеши")
@@ -113,7 +114,8 @@ def hash_gen(args: argparse.Namespace = None):
113
114
 
114
115
  def hash_check(args: argparse.Namespace = None):
115
116
  if args is None:
116
- argp = argparse.ArgumentParser("ms2-hash_check", description="проверка размера и контрольной суммы файла")
117
+ argp = argparse.ArgumentParser("ms2-hash_check", description="Проверка размера и контрольной суммы файла")
118
+ argp.epilog = "Написано на Python"
117
119
  argp.add_argument("files", nargs="+", help="пути к файлам")
118
120
  argp.add_argument("-b", "--bar", action="store_true", help="показывать прогрессбар (нужен модуль progressbar2)")
119
121
  args = argp.parse_args()
@@ -7,6 +7,7 @@ import os
7
7
  import sys
8
8
  import time
9
9
  from .core import ms
10
+ from contextlib import ExitStack
10
11
  from functools import wraps
11
12
  from typing import *
12
13
  from warnings import warn
@@ -167,12 +168,12 @@ async def async_download_file(url: str, path: str, *, cb_end=return_None, cb_pro
167
168
  """Асинхронная функция для скачивания файла | `aiohttp`"""
168
169
  kw.setdefault("method", "GET")
169
170
  kw["url"] = url
170
- async with async_request(**kw) as resp: # type: ignore
171
- if callable(getattr(path, "write", None)):
172
- f: IO[bytes] = path
173
- else:
174
- f = open(path, "wb")
175
- with f:
171
+ with ExitStack() as stack:
172
+ async with async_request(**kw) as resp: # type: ignore
173
+ if callable(getattr(path, "write", None)):
174
+ f: IO[bytes] = path
175
+ else:
176
+ f = stack.enter_context(open(path, "wb"))
176
177
  size = 0
177
178
  await cb_start(f, resp, size)
178
179
  try:
@@ -311,25 +312,25 @@ def sync_download_file(url: str, path: str, *, cb_end=return_None, cb_progress=r
311
312
  kw.setdefault("method", "GET")
312
313
  kw["stream"] = True
313
314
  kw["url"] = url
314
- with sync_request(**kw) as resp:
315
+ with ExitStack() as stack:
316
+ resp = stack.enter_context(sync_request(**kw))
315
317
  if callable(getattr(path, "write", None)):
316
318
  f: IO[bytes] = path
317
319
  else:
318
- f = open(path, "wb")
319
- with f:
320
- size = 0
321
- cb_start(f, resp, size)
322
- try:
323
- for chunk in resp.iter_content(chunk_size):
324
- size += f.write(chunk)
325
- cb_progress(f, resp, size)
326
- except:
327
- f.close()
328
- if delete_on_error:
329
- if os.path.isfile(path):
330
- os.remove(path)
331
- raise
332
- cb_end(f, resp, size)
320
+ f = stack.enter_context(open(path, "wb"))
321
+ size = 0
322
+ cb_start(f, resp, size)
323
+ try:
324
+ for chunk in resp.iter_content(chunk_size):
325
+ size += f.write(chunk)
326
+ cb_progress(f, resp, size)
327
+ except:
328
+ f.close()
329
+ if delete_on_error:
330
+ if os.path.isfile(path):
331
+ os.remove(path)
332
+ raise
333
+ cb_end(f, resp, size)
333
334
  return size
334
335
 
335
336
 
@@ -1,2 +0,0 @@
1
- name = "MainShortcuts2"
2
- version = "2.6.5"
File without changes