MainShortcuts2 2.7.0__tar.gz → 2.7.2__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.0 → mainshortcuts2-2.7.2}/PKG-INFO +3 -1
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/README.md +2 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/pyproject.toml +1 -1
- mainshortcuts2-2.7.2/src/MainShortcuts2/api/github.py +383 -0
- mainshortcuts2-2.7.2/src/MainShortcuts2/ex/aiohttp_ex/web.py +158 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/term.py +128 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/utils.py +49 -12
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/__init__.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/__main__.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/_any2json_regs.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/_module_info.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/_ms2app_regs.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/advanced.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/any2json.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/api/base.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/api/gigachat.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/api/russian_trusted_root_ca_pem.crt +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/api/russian_trusted_sub_ca_pem.crt +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/api/webdav.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/cfg.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/core.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/core_config.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/dict.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/dir.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/ex/__init__.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/ex/datetime_ex.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/ex/pathlib_ex.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/ex/pil_ex.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/ex/psutil_ex.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/ex/sqlite_ex.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/file.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/gui_scripts/__init__.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/gui_scripts/ms2_hash_gen.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/gui_scripts/utils.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/json.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/linux.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/list.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/ms2app.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/ms2hash.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/path.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/proc.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/regex.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/special_chars.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/sql/_sql_base.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/sql/postgresql.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/sql/sqlite.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/str.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/types.py +0 -0
- {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/win.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MainShortcuts2
|
|
3
|
-
Version: 2.7.
|
|
3
|
+
Version: 2.7.2
|
|
4
4
|
Summary: Сокращение и улучшение функций + консольные утилиты
|
|
5
5
|
Author: MainPlay TG
|
|
6
6
|
Author-email: xbox.roman6666666666@gmail.com
|
|
@@ -19,6 +19,8 @@ Project-URL: Repository, https://github.com/MainPlay-TG/MainShortcuts2.py
|
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
|
|
21
21
|
# MainShortcuts2
|
|
22
|
+
[](https://github.com/MainPlay-TG/MainShortcuts2.py/actions/workflows/build.yml)
|
|
23
|
+
<a href="https://pypi.org/project/MainShortcuts2/"><img alt="PyPi" src="https://img.shields.io/pypi/format/mainshortcuts2?style=flat&label=PyPi"/></a>
|
|
22
24
|
## Описание
|
|
23
25
|
Сокращение и улучшение функций
|
|
24
26
|
## Установка
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# MainShortcuts2
|
|
2
|
+
[](https://github.com/MainPlay-TG/MainShortcuts2.py/actions/workflows/build.yml)
|
|
3
|
+
<a href="https://pypi.org/project/MainShortcuts2/"><img alt="PyPi" src="https://img.shields.io/pypi/format/mainshortcuts2?style=flat&label=PyPi"/></a>
|
|
2
4
|
## Описание
|
|
3
5
|
Сокращение и улучшение функций
|
|
4
6
|
## Установка
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
from .base import BaseClient, ObjectBase
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import IO
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Client",
|
|
10
|
+
"Release",
|
|
11
|
+
"ReleaseAsset",
|
|
12
|
+
"UrlInfo",
|
|
13
|
+
"User",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _CatBase:
|
|
18
|
+
_n: str
|
|
19
|
+
|
|
20
|
+
def __init__(self, client: "Client"):
|
|
21
|
+
self._c = client
|
|
22
|
+
|
|
23
|
+
def _r(self, httpm: str, apim: str, **kw):
|
|
24
|
+
return self._c.request(httpm, self._n + "/" + apim, **kw)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _ObjBase(ObjectBase):
|
|
28
|
+
client: "Client"
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def _get(cls, client, data, **kw):
|
|
32
|
+
if data:
|
|
33
|
+
return cls(client, data, **kw)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _HasUrl(_ObjBase):
|
|
37
|
+
def __init__(self, client: BaseClient, raw: dict, *args, **kwargs):
|
|
38
|
+
super().__init__(client, raw, *args, **kwargs)
|
|
39
|
+
self.url: str = self["url"]
|
|
40
|
+
|
|
41
|
+
@cached_property
|
|
42
|
+
def url_info(self):
|
|
43
|
+
return UrlInfo(self.url)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class UrlInfo:
|
|
47
|
+
asset_id: int | None
|
|
48
|
+
release_id: int | None
|
|
49
|
+
release_tag: str | None
|
|
50
|
+
repo: str | None
|
|
51
|
+
username: str | None
|
|
52
|
+
|
|
53
|
+
def __init__(self, url: str):
|
|
54
|
+
parsed = urlparse(url)
|
|
55
|
+
path = parsed.path.split("/")
|
|
56
|
+
self.repo = None
|
|
57
|
+
self.username = None
|
|
58
|
+
l = len(path)
|
|
59
|
+
if parsed.hostname == "api.github.com":
|
|
60
|
+
if l > 2 and path[1] in ("repos", "users"):
|
|
61
|
+
self.username = path[2]
|
|
62
|
+
if l > 3 and path[1] == "repos":
|
|
63
|
+
self.repo = path[3]
|
|
64
|
+
if l > 5 and path[4] == "releases":
|
|
65
|
+
if path[5] == "assets" and l > 6 and path[6].isdigit():
|
|
66
|
+
self.asset_id = int(path[6])
|
|
67
|
+
elif path[5].isdigit():
|
|
68
|
+
self.release_id = int(path[5])
|
|
69
|
+
elif parsed.hostname == "github.com":
|
|
70
|
+
if l > 1:
|
|
71
|
+
self.username = path[1]
|
|
72
|
+
if l > 2:
|
|
73
|
+
self.repo = path[2]
|
|
74
|
+
if l > 4 and path[3] == "releases":
|
|
75
|
+
self.release_tag = path[4]
|
|
76
|
+
else:
|
|
77
|
+
raise ValueError("Unknown hostname: %s" % parsed.hostname)
|
|
78
|
+
|
|
79
|
+
def get_asset(self, client: "Client"):
|
|
80
|
+
if self.asset_id:
|
|
81
|
+
return client.release_assets.get(self.username, self.repo, self.asset_id)
|
|
82
|
+
|
|
83
|
+
def get_release(self, client: "Client"):
|
|
84
|
+
if self.release_id:
|
|
85
|
+
return client.releases.get(self.username, self.repo, self.release_id)
|
|
86
|
+
if self.release_tag:
|
|
87
|
+
return client.releases.get_by_tag_name(self.username, self.repo, self.release_tag)
|
|
88
|
+
|
|
89
|
+
def get_user(self, client: "Client"):
|
|
90
|
+
if self.username:
|
|
91
|
+
return client.users.get(self.username)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class User(_HasUrl):
|
|
95
|
+
"""A GitHub user."""
|
|
96
|
+
|
|
97
|
+
def _init(self):
|
|
98
|
+
self.avatar_url: str = self["avatar_url"]
|
|
99
|
+
self.email: str | None = self["email"]
|
|
100
|
+
self.events_url: str = self["events_url"]
|
|
101
|
+
self.followers_url: str = self["followers_url"]
|
|
102
|
+
self.following_url: str = self["following_url"]
|
|
103
|
+
self.gists_url: str = self["gists_url"]
|
|
104
|
+
self.gravatar_id: str | None = self["gravatar_id"]
|
|
105
|
+
self.html_url: str = self["html_url"]
|
|
106
|
+
self.id: int = self["id"]
|
|
107
|
+
self.login: str = self["login"]
|
|
108
|
+
self.name: str | None = self["name"]
|
|
109
|
+
self.node_id: str = self["node_id"]
|
|
110
|
+
self.organizations_url: str = self["organizations_url"]
|
|
111
|
+
self.received_events_url: str = self["received_events_url"]
|
|
112
|
+
self.repos_url: str = self["repos_url"]
|
|
113
|
+
self.site_admin: bool = self["site_admin"]
|
|
114
|
+
self.starred_at: str | None = self["starred_at"]
|
|
115
|
+
self.starred_url: str = self["starred_url"]
|
|
116
|
+
self.subscriptions_url: str = self["subscriptions_url"]
|
|
117
|
+
self.type: str = self["type"]
|
|
118
|
+
self.user_view_type: str | None = self["user_view_type"]
|
|
119
|
+
|
|
120
|
+
def reload(self):
|
|
121
|
+
return self.client.users.get(self.login)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Release(_HasUrl):
|
|
125
|
+
"""A release."""
|
|
126
|
+
|
|
127
|
+
def _init(self):
|
|
128
|
+
self.assets_url: str = self["assets_url"]
|
|
129
|
+
self.assets = [ReleaseAsset(self.client, i) for i in self["assets"]] # type: ignore
|
|
130
|
+
self.author = User(self.client, self["author"])
|
|
131
|
+
self.body_html: str | None = self["body_html"]
|
|
132
|
+
self.body_text: str | None = self["body_text"]
|
|
133
|
+
self.body: str | None = self["body"]
|
|
134
|
+
self.created_at: str = self["created_at"]
|
|
135
|
+
self.discussion_url: str | None = self["discussion_url"]
|
|
136
|
+
self.draft: bool = self["draft"]
|
|
137
|
+
self.html_url: str = self["html_url"]
|
|
138
|
+
self.id: int = self["id"]
|
|
139
|
+
self.immutable: bool = self["immutable"]
|
|
140
|
+
self.mentions_count: int | None = self["mentions_count"]
|
|
141
|
+
self.name: str | None = self["name"]
|
|
142
|
+
self.node_id: str = self["node_id"]
|
|
143
|
+
self.prerelease: bool = self["prerelease"]
|
|
144
|
+
self.published_at: str | None = self["published_at"]
|
|
145
|
+
self.tag_name: str = self["tag_name"]
|
|
146
|
+
self.tarball_url: str | None = self["tarball_url"]
|
|
147
|
+
self.target_commitish: str = self["target_commitish"]
|
|
148
|
+
self.updated_at: str | None = self["updated_at"]
|
|
149
|
+
self.upload_url: str = self["upload_url"]
|
|
150
|
+
self.zipball_url: str | None = self["zipball_url"]
|
|
151
|
+
|
|
152
|
+
def delete(self):
|
|
153
|
+
self.client.releases.delete(self.url_info.username, self.url_info.repo, self.id)
|
|
154
|
+
|
|
155
|
+
def reload(self):
|
|
156
|
+
return self.client.releases.get(self.url_info.username, self.url_info.repo, self.id)
|
|
157
|
+
|
|
158
|
+
def update(self, **data):
|
|
159
|
+
return self.client.releases.update(self.url_info.username, self.url_info.repo, self.id, **data)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class ReleaseAsset(_HasUrl):
|
|
163
|
+
"""Data related to a release."""
|
|
164
|
+
|
|
165
|
+
def _init(self):
|
|
166
|
+
self.browser_download_url: str = self["browser_download_url"]
|
|
167
|
+
self.content_type: str = self["content_type"]
|
|
168
|
+
self.created_at: str = self["created_at"]
|
|
169
|
+
self.digest: str | None = self["digest"]
|
|
170
|
+
self.download_count: int = self["download_count"]
|
|
171
|
+
self.id: int = self["id"]
|
|
172
|
+
self.label: str | None = self["label"]
|
|
173
|
+
self.name: str = self["name"]
|
|
174
|
+
self.node_id: str = self["node_id"]
|
|
175
|
+
self.size: int = self["size"]
|
|
176
|
+
self.state: str = self["state"]
|
|
177
|
+
self.type: str | None = self["type"]
|
|
178
|
+
self.updated_at: str = self["updated_at"]
|
|
179
|
+
self.uploader = User._get(self.client, self["uploader"])
|
|
180
|
+
|
|
181
|
+
def delete(self):
|
|
182
|
+
self.client.release_assets.delete(self.url_info.username, self.url_info.repo, self.id)
|
|
183
|
+
|
|
184
|
+
def reload(self):
|
|
185
|
+
return self.client.release_assets.get(self.url_info.username, self.url_info.repo, self.id)
|
|
186
|
+
|
|
187
|
+
def update(self, **data):
|
|
188
|
+
return self.client.release_assets.update(self.url_info.username, self.url_info.repo, self.id, **data)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class Client(BaseClient):
|
|
192
|
+
class _ReleaseAssets(_CatBase):
|
|
193
|
+
def _r(self, httpm, owner, repo, asset_id, **kw):
|
|
194
|
+
return self._c.request(httpm, f"repos/{owner}/{repo}/releases/assets/{asset_id}", **kw)
|
|
195
|
+
|
|
196
|
+
def delete(self, owner: str, repo: str, asset_id: int):
|
|
197
|
+
"""Delete a release asset"""
|
|
198
|
+
self._r("DELETE", owner, repo, asset_id)
|
|
199
|
+
|
|
200
|
+
def get(self, owner: str, repo: str, asset_id: int):
|
|
201
|
+
"""Get a release asset"""
|
|
202
|
+
return ReleaseAsset(self._c, self._r("GET", owner, repo, asset_id))
|
|
203
|
+
|
|
204
|
+
def list(self, owner: str, repo: str, release_id: int, per_page=30, page=1):
|
|
205
|
+
"""List release assets"""
|
|
206
|
+
resp: list = self._c.request("GET", f"repos/{owner}/{repo}/releases/{release_id}/assets", params={"per_page": per_page, "page": page})
|
|
207
|
+
return [ReleaseAsset(self._c, i) for i in resp]
|
|
208
|
+
|
|
209
|
+
def update(self, owner: str, repo: str, asset_id: int, *,
|
|
210
|
+
label: str = None,
|
|
211
|
+
name: str = None,
|
|
212
|
+
state: str = None,
|
|
213
|
+
):
|
|
214
|
+
"""Update a release asset"""
|
|
215
|
+
data = {}
|
|
216
|
+
if label:
|
|
217
|
+
data["label"] = label
|
|
218
|
+
if name:
|
|
219
|
+
data["name"] = name
|
|
220
|
+
if state:
|
|
221
|
+
data["state"] = state
|
|
222
|
+
if not data:
|
|
223
|
+
return self.get(owner, repo, asset_id)
|
|
224
|
+
return ReleaseAsset(self._c, self._r("PATCH", owner, repo, asset_id, json=data))
|
|
225
|
+
|
|
226
|
+
def upload(self, owner: str, repo: str, release_id: int, data: bytes | Path | IO[bytes], name: str = None, label: str = None, **kw):
|
|
227
|
+
"""Upload a release asset"""
|
|
228
|
+
kw.setdefault("headers", {})
|
|
229
|
+
if isinstance(data, Path):
|
|
230
|
+
with data.open("rb") as f:
|
|
231
|
+
kw["label"] = label
|
|
232
|
+
kw["name"] = name or data.name
|
|
233
|
+
return self.upload(owner, repo, release_id, f, **kw)
|
|
234
|
+
elif isinstance(data, bytes):
|
|
235
|
+
kw["data"] = data
|
|
236
|
+
kw["headers"]["Content-Length"] = len(data)
|
|
237
|
+
else:
|
|
238
|
+
pos = data.tell()
|
|
239
|
+
data.seek(0, os.SEEK_END)
|
|
240
|
+
kw["headers"]["Content-Length"] = data.tell()
|
|
241
|
+
data.seek(pos)
|
|
242
|
+
kw["data"] = data
|
|
243
|
+
if not name:
|
|
244
|
+
fname = getattr(data, "name", None)
|
|
245
|
+
if fname:
|
|
246
|
+
name = os.path.basename(fname)
|
|
247
|
+
kw.setdefault("params", {})
|
|
248
|
+
if label:
|
|
249
|
+
kw["params"]["label"] = label
|
|
250
|
+
if not name:
|
|
251
|
+
raise ValueError("You must specify the name")
|
|
252
|
+
kw["headers"].setdefault("Content-Type", "application/octet-stream")
|
|
253
|
+
kw["params"]["name"] = name
|
|
254
|
+
resp = self._c.request("POST", f"/repos/{owner}/{repo}/releases/{release_id}/assets", **kw)
|
|
255
|
+
return ReleaseAsset(self._c, resp)
|
|
256
|
+
|
|
257
|
+
class _Releases(_CatBase):
|
|
258
|
+
def _r(self, httpm, owner, repo, release_id, **kw):
|
|
259
|
+
return self._c.request(httpm, f"repos/{owner}/{repo}/releases/{release_id}", **kw)
|
|
260
|
+
|
|
261
|
+
def create(self, owner: str, repo: str, tag_name: str,
|
|
262
|
+
body: str = None,
|
|
263
|
+
discussion_category_name: str = None,
|
|
264
|
+
draft=False,
|
|
265
|
+
generate_release_notes=False,
|
|
266
|
+
make_latest="true",
|
|
267
|
+
name: str = None,
|
|
268
|
+
prerelease=False,
|
|
269
|
+
target_commitish: str = None,
|
|
270
|
+
**kw):
|
|
271
|
+
kw.setdefault("json", {})
|
|
272
|
+
kw["json"]["draft"] = bool(draft)
|
|
273
|
+
kw["json"]["generate_release_notes"] = bool(generate_release_notes)
|
|
274
|
+
kw["json"]["make_latest"] = make_latest
|
|
275
|
+
kw["json"]["prerelease"] = bool(prerelease)
|
|
276
|
+
kw["json"]["tag_name"] = tag_name
|
|
277
|
+
if body:
|
|
278
|
+
kw["json"]["body"] = body
|
|
279
|
+
if discussion_category_name:
|
|
280
|
+
kw["json"]["discussion_category_name"] = discussion_category_name
|
|
281
|
+
if name:
|
|
282
|
+
kw["json"]["name"] = name
|
|
283
|
+
if target_commitish:
|
|
284
|
+
kw["json"]["target_commitish"] = target_commitish
|
|
285
|
+
resp = self._c.request("POST", f"repos/{owner}/{repo}/releases", **kw)
|
|
286
|
+
return Release(self._c, resp)
|
|
287
|
+
|
|
288
|
+
def delete(self, owner: str, repo: str, release_id: int):
|
|
289
|
+
self._r("DELETE", owner, repo, release_id)
|
|
290
|
+
|
|
291
|
+
def get(self, owner: str, repo: str, release_id: int):
|
|
292
|
+
return Release(self._c, self._r("GET", owner, repo, release_id))
|
|
293
|
+
|
|
294
|
+
def get_by_tag_name(self, owner: str, repo: str, tag_name: str):
|
|
295
|
+
return Release(self._c, self._r("GET", owner, repo, f"tags/{tag_name}"))
|
|
296
|
+
|
|
297
|
+
def get_latest(self, owner: str, repo: str):
|
|
298
|
+
return Release(self._c, self._r("GET", owner, repo, "latest"))
|
|
299
|
+
|
|
300
|
+
def list(self, owner: str, repo: str, per_page=30, page=1, **kw):
|
|
301
|
+
resp: list = self._c.request("GET", f"repos/{owner}/{repo}/releases", params={"per_page": per_page, "page": page})
|
|
302
|
+
return [Release(self._c, i) for i in resp]
|
|
303
|
+
|
|
304
|
+
def update(self, owner: str, repo: str, release_id: int,
|
|
305
|
+
body: str = None,
|
|
306
|
+
discussion_category_name: str = None,
|
|
307
|
+
draft: bool = None,
|
|
308
|
+
make_latest: str = None,
|
|
309
|
+
name: str = None,
|
|
310
|
+
prerelease: bool = None,
|
|
311
|
+
tag_name: str = None,
|
|
312
|
+
target_commitish: str = None,
|
|
313
|
+
**kw):
|
|
314
|
+
kw.setdefault("json", {})
|
|
315
|
+
if body is not None:
|
|
316
|
+
kw["json"]["body"] = body
|
|
317
|
+
if discussion_category_name is not None:
|
|
318
|
+
kw["json"]["discussion_category_name"] = discussion_category_name
|
|
319
|
+
if draft is not None:
|
|
320
|
+
kw["json"]["draft"] = bool(draft)
|
|
321
|
+
if make_latest is not None:
|
|
322
|
+
kw["json"]["make_latest"] = make_latest
|
|
323
|
+
if name is not None:
|
|
324
|
+
kw["json"]["name"] = name
|
|
325
|
+
if prerelease is not None:
|
|
326
|
+
kw["json"]["prerelease"] = bool(prerelease)
|
|
327
|
+
if tag_name is not None:
|
|
328
|
+
kw["json"]["tag_name"] = tag_name
|
|
329
|
+
if target_commitish is not None:
|
|
330
|
+
kw["json"]["target_commitish"] = target_commitish
|
|
331
|
+
if not kw["json"]:
|
|
332
|
+
return self.get(owner, repo, release_id)
|
|
333
|
+
return Release(self._c, self._r("PATCH", owner, repo, release_id, **kw))
|
|
334
|
+
|
|
335
|
+
class _Users(_CatBase):
|
|
336
|
+
_n = "users"
|
|
337
|
+
|
|
338
|
+
def get(self, username: str):
|
|
339
|
+
return User(self._c, self._r("GET", username))
|
|
340
|
+
|
|
341
|
+
def __init__(self, token: str = None, **kw):
|
|
342
|
+
self._init(**kw)
|
|
343
|
+
self._headers["Accept"] = "application/vnd.github+json"
|
|
344
|
+
self._headers["X-GitHub-Api-Version"] = "2022-11-28"
|
|
345
|
+
self._url = "https://api.github.com/{method}"
|
|
346
|
+
self.token = token
|
|
347
|
+
# Категории запросов
|
|
348
|
+
self.release_assets = self._ReleaseAssets(self)
|
|
349
|
+
self.releases = self._Releases(self)
|
|
350
|
+
self.users = self._Users(self)
|
|
351
|
+
|
|
352
|
+
@property
|
|
353
|
+
def token(self) -> str | None:
|
|
354
|
+
return self.__dict__.get("token")
|
|
355
|
+
|
|
356
|
+
@token.setter
|
|
357
|
+
def token(self, v):
|
|
358
|
+
if v is None:
|
|
359
|
+
self.__dict__.pop("token", None)
|
|
360
|
+
self._headers.pop("Authorization", None)
|
|
361
|
+
elif isinstance(v, str):
|
|
362
|
+
if not v:
|
|
363
|
+
self.token = None
|
|
364
|
+
return
|
|
365
|
+
self.__dict__["token"] = v
|
|
366
|
+
self._headers["Authorization"] = "Bearer " + v
|
|
367
|
+
else:
|
|
368
|
+
raise TypeError("Token must be str")
|
|
369
|
+
|
|
370
|
+
@classmethod
|
|
371
|
+
def from_env(cls, allow_no_token=False, **kw):
|
|
372
|
+
"""Создать клиент, взяв токен из переменной среды если он не указан аргументом"""
|
|
373
|
+
if kw.get("token"):
|
|
374
|
+
return cls(**kw) # Токен указан в аргументах
|
|
375
|
+
if allow_no_token:
|
|
376
|
+
return cls(os.environ.get("GITHUB_TOKEN"), **kw) # Может быть без токена
|
|
377
|
+
return cls(os.environ["GITHUB_TOKEN"], **kw) # Токен обязателен
|
|
378
|
+
|
|
379
|
+
def request(self, httpm, apim, raw=False, **kw) -> dict | list | requests.Response:
|
|
380
|
+
if raw:
|
|
381
|
+
return self._request(httpm, apim, **kw)
|
|
382
|
+
with self._request(httpm, apim, **kw) as resp:
|
|
383
|
+
return resp.json()
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import typing
|
|
3
|
+
import warnings
|
|
4
|
+
from aiohttp import BasicAuth, hdrs, web
|
|
5
|
+
from aiohttp.typedefs import Handler
|
|
6
|
+
from aiohttp.web import FileResponse, Request, Response, StreamResponse
|
|
7
|
+
from logging import DEBUG
|
|
8
|
+
from MainShortcuts2 import ms
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
warnings.filterwarnings("ignore", "Inheritance class .* from web.Application is discouraged", DeprecationWarning)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def wrap_handler(app: "Application", handler: Handler):
|
|
14
|
+
async def wrapper(req: Request) -> StreamResponse | typing.Any:
|
|
15
|
+
try:
|
|
16
|
+
resp = await handler(req)
|
|
17
|
+
if isinstance(resp, int):
|
|
18
|
+
resp = Response(status=resp)
|
|
19
|
+
elif isinstance(resp, Path):
|
|
20
|
+
resp = FileResponse(resp)
|
|
21
|
+
except Exception as exc:
|
|
22
|
+
app.logger.exception("Failed to process request", exc_info=exc)
|
|
23
|
+
resp = Response(status=500, reason="Internal server error")
|
|
24
|
+
if app.logger.isEnabledFor(DEBUG):
|
|
25
|
+
remote = req.remote or "unknown"
|
|
26
|
+
if "X-Real-IP" in req.headers:
|
|
27
|
+
remote += " (real IP: %s)" % req.headers["X-Real-IP"]
|
|
28
|
+
status = resp.status if isinstance(resp, StreamResponse) else repr(resp)
|
|
29
|
+
app.logger.debug("Request %s %s from %s, return %s", req.method, req.raw_path, remote, status)
|
|
30
|
+
return resp
|
|
31
|
+
return wrapper
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Application(web.Application):
|
|
35
|
+
def run(self, **kw):
|
|
36
|
+
"""Запустить приложение"""
|
|
37
|
+
kw.setdefault("print", self.logger.info)
|
|
38
|
+
web.run_app(self, **kw)
|
|
39
|
+
|
|
40
|
+
def run_local(self, port: int, **kw):
|
|
41
|
+
"""Запустить локально на указанном порту"""
|
|
42
|
+
self.run(host="127.0.0.1", port=port, **kw)
|
|
43
|
+
|
|
44
|
+
def on_request(self, methods: str | list[str], paths: str | list[str], add_slash=True, **kw):
|
|
45
|
+
"""Зарегистрировать обработчик запросов"""
|
|
46
|
+
if isinstance(methods, str):
|
|
47
|
+
methods = [methods]
|
|
48
|
+
if isinstance(paths, str):
|
|
49
|
+
paths = [paths]
|
|
50
|
+
|
|
51
|
+
def deco(func: Handler):
|
|
52
|
+
wrapper = wrap_handler(self, func)
|
|
53
|
+
for m in methods:
|
|
54
|
+
for p in paths:
|
|
55
|
+
self._on_request(wrapper, m, p, add_slash, **kw)
|
|
56
|
+
return func
|
|
57
|
+
return deco
|
|
58
|
+
|
|
59
|
+
def _on_request(self, func: Handler, method: str, path: str, add_slash=True, **kw):
|
|
60
|
+
if path == "/":
|
|
61
|
+
add_slash = False
|
|
62
|
+
if add_slash:
|
|
63
|
+
path = path.rstrip("/")
|
|
64
|
+
self.router.add_route(method, path, func, **kw)
|
|
65
|
+
if add_slash:
|
|
66
|
+
self.router.add_route(method, path + "/", func, **kw)
|
|
67
|
+
|
|
68
|
+
def on_any_path_request(self, methods: str | list[str], **kw):
|
|
69
|
+
"""Зарегистрировать обработчик запросов на любой путь"""
|
|
70
|
+
return self.on_request(methods, "/{tail:.*}", False, **kw)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def auth_header(header=hdrs.AUTHORIZATION, value=None):
|
|
74
|
+
"""Простая авторизация заголовком. Если не указать требуемое значение, будет проверять только наличие заголовка"""
|
|
75
|
+
def deco(func: Handler):
|
|
76
|
+
async def wrapper(req: Request):
|
|
77
|
+
if header in req.headers:
|
|
78
|
+
if value is not None:
|
|
79
|
+
if req.headers[header] != value:
|
|
80
|
+
return Response(status=403) # Заголовок не совпадает
|
|
81
|
+
return await func(req) # Заголовок совпадает или значение не указано
|
|
82
|
+
return Response(status=401) # Заголовок отсутствует
|
|
83
|
+
return wrapper
|
|
84
|
+
return deco
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
basic_unauthorized_headers = {"WWW-Authenticate": 'Basic realm="Basic Auth"'}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def auth_basic(header=hdrs.AUTHORIZATION, filter: typing.Callable[[BasicAuth], bool] = ms.utils.return_True):
|
|
91
|
+
"""Авторизация `Basic`. Если не указать фильтр, только проверяет наличие заголовка"""
|
|
92
|
+
def deco(func: typing.Callable[[Request, BasicAuth], typing.Awaitable[StreamResponse]]):
|
|
93
|
+
async def wrapper(req: Request):
|
|
94
|
+
if header in req.headers:
|
|
95
|
+
try:
|
|
96
|
+
auth = BasicAuth.decode(req.headers[header])
|
|
97
|
+
except Exception:
|
|
98
|
+
return Response(status=400, headers=basic_unauthorized_headers) # Неправильный заголовок
|
|
99
|
+
if filter(auth):
|
|
100
|
+
return await func(req, auth) # Одобрено
|
|
101
|
+
return Response(status=403) # Запрещено
|
|
102
|
+
return Response(status=401, headers=basic_unauthorized_headers) # Заголовок отсутствует
|
|
103
|
+
return wrapper
|
|
104
|
+
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)
|
|
@@ -299,3 +299,131 @@ def countful_countdown(time: float, interval: float = 1, format: str = None, rou
|
|
|
299
299
|
|
|
300
300
|
|
|
301
301
|
cls = clear
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class Choice(ms.ObjectBase):
|
|
305
|
+
"""Промпт пользователю на выбор опций"""
|
|
306
|
+
|
|
307
|
+
def __init__(self, choices: dict[str, str | None],
|
|
308
|
+
aliases: dict[str, str] = None,
|
|
309
|
+
default: str | set[str] = None,
|
|
310
|
+
multiple=False,
|
|
311
|
+
values_in_prompt=True,
|
|
312
|
+
):
|
|
313
|
+
self.choices: dict[str, str | None] = {}
|
|
314
|
+
for k, v in choices.items():
|
|
315
|
+
self.add_choice(k, v)
|
|
316
|
+
self.aliases = aliases or {}
|
|
317
|
+
if default is None:
|
|
318
|
+
self.default = None
|
|
319
|
+
elif isinstance(default, str):
|
|
320
|
+
self.default = set([default])
|
|
321
|
+
elif isinstance(default, (list, set, tuple)):
|
|
322
|
+
self.default = set(default)
|
|
323
|
+
else:
|
|
324
|
+
raise TypeError("Default choice must be str, set[str] or None")
|
|
325
|
+
self.multiple = bool(multiple)
|
|
326
|
+
self.values_in_prompt = bool(values_in_prompt)
|
|
327
|
+
|
|
328
|
+
def add_choice(self, value: str, desc: str | None, override=False):
|
|
329
|
+
"""Добавить пункт для выбора. Описание может быть пустым"""
|
|
330
|
+
if not isinstance(value, str):
|
|
331
|
+
raise TypeError("Value must be str")
|
|
332
|
+
if (desc is not None) and (not isinstance(desc, str)):
|
|
333
|
+
raise TypeError("The description must be None or str")
|
|
334
|
+
if (not override) and (value in self.choices):
|
|
335
|
+
raise ValueError(f"Choice already exists: {value}")
|
|
336
|
+
self.choices[value] = desc or None
|
|
337
|
+
|
|
338
|
+
def _print_choices(self):
|
|
339
|
+
if not any(self.choices.values()):
|
|
340
|
+
return
|
|
341
|
+
for value, desc in self.choices.items():
|
|
342
|
+
if not desc:
|
|
343
|
+
desc = "{no description}"
|
|
344
|
+
print(f"{value}: {desc}")
|
|
345
|
+
|
|
346
|
+
def _make_prompt(self):
|
|
347
|
+
parts = ["Enter choice"]
|
|
348
|
+
if self.multiple:
|
|
349
|
+
parts.append("s")
|
|
350
|
+
if self.values_in_prompt:
|
|
351
|
+
parts.append(" (")
|
|
352
|
+
parts.append("/".join(self.choices))
|
|
353
|
+
parts.append(")")
|
|
354
|
+
parts.append(": ")
|
|
355
|
+
return "".join(parts)
|
|
356
|
+
|
|
357
|
+
def _prompt(self):
|
|
358
|
+
result: set[str] = set()
|
|
359
|
+
split = [i.strip() for i in input(self._make_prompt()).split(",")]
|
|
360
|
+
for i in split:
|
|
361
|
+
if i:
|
|
362
|
+
result.add(i)
|
|
363
|
+
return result
|
|
364
|
+
|
|
365
|
+
def run(self):
|
|
366
|
+
self._print_choices()
|
|
367
|
+
while True:
|
|
368
|
+
print()
|
|
369
|
+
results = self._prompt()
|
|
370
|
+
if len(results) == 0:
|
|
371
|
+
if self.default is None:
|
|
372
|
+
print("Error: enter the value")
|
|
373
|
+
continue
|
|
374
|
+
results = self.default
|
|
375
|
+
elif (not self.multiple) and len(results) > 1:
|
|
376
|
+
print("Error: only one choice is allowed")
|
|
377
|
+
continue
|
|
378
|
+
completed_results: set[str] = set()
|
|
379
|
+
not_exist: set[str] = set()
|
|
380
|
+
for i in results:
|
|
381
|
+
while i in self.aliases:
|
|
382
|
+
i = self.aliases[i]
|
|
383
|
+
if i not in self.choices:
|
|
384
|
+
not_exist.add(i)
|
|
385
|
+
completed_results.add(i)
|
|
386
|
+
if len(not_exist) == 1:
|
|
387
|
+
print("Error: value %s do not exist" % not_exist.pop())
|
|
388
|
+
continue
|
|
389
|
+
elif len(not_exist) > 1:
|
|
390
|
+
print("Error: values %s do not exist" % ", ".join(not_exist))
|
|
391
|
+
continue
|
|
392
|
+
return completed_results
|
|
393
|
+
|
|
394
|
+
@classmethod
|
|
395
|
+
def simple_yes_no(cls, **kw):
|
|
396
|
+
"""Спросить `y` или `n`"""
|
|
397
|
+
self = cls({"n": None, "y": None}, multiple=False, **kw)
|
|
398
|
+
for func in (str, str.lower, str.upper):
|
|
399
|
+
self.aliases[func("Д")] = "y"
|
|
400
|
+
self.aliases[func("Да")] = "y"
|
|
401
|
+
self.aliases[func("Н")] = "n"
|
|
402
|
+
self.aliases[func("Нет")] = "n"
|
|
403
|
+
self.aliases[func("No")] = "n"
|
|
404
|
+
self.aliases[func("Yes")] = "y"
|
|
405
|
+
return self.run().pop() == "y"
|
|
406
|
+
|
|
407
|
+
@classmethod
|
|
408
|
+
def simple_list(cls, choices: set[str], **kw):
|
|
409
|
+
"""Спросить номер пункта"""
|
|
410
|
+
c = {}
|
|
411
|
+
for n, i in enumerate(choices):
|
|
412
|
+
c[str(n + 1)] = i
|
|
413
|
+
self = cls(c, **kw)
|
|
414
|
+
return {int(i) for i in self.run()}
|
|
415
|
+
|
|
416
|
+
@classmethod
|
|
417
|
+
def simple_ynAN(cls, **kw):
|
|
418
|
+
"""`yes`/`no`/`All`/`Never`"""
|
|
419
|
+
self = cls({}, multiple=False, **kw)
|
|
420
|
+
self.add_choice("y", "Yes")
|
|
421
|
+
self.add_choice("n", "No")
|
|
422
|
+
self.add_choice("A", "All")
|
|
423
|
+
self.add_choice("N", "Never")
|
|
424
|
+
for func in (str, str.lower, str.upper):
|
|
425
|
+
self.aliases[func("All")] = "A"
|
|
426
|
+
self.aliases[func("Never")] = "N"
|
|
427
|
+
self.aliases[func("No")] = "n"
|
|
428
|
+
self.aliases[func("Yes")] = "y"
|
|
429
|
+
return self.run().pop()
|
|
@@ -585,10 +585,7 @@ def disable_warnings():
|
|
|
585
585
|
|
|
586
586
|
def is_instance_of_one(obj, *classes: type) -> bool:
|
|
587
587
|
"""Это экземпляр одного из классов?"""
|
|
588
|
-
|
|
589
|
-
if isinstance(obj, i):
|
|
590
|
-
return True
|
|
591
|
-
return False
|
|
588
|
+
return isinstance(obj, classes)
|
|
592
589
|
|
|
593
590
|
|
|
594
591
|
def is_instance_of_all(obj, *classes: type) -> bool:
|
|
@@ -881,14 +878,14 @@ class TempFlag(ms.types.BoolFlag):
|
|
|
881
878
|
|
|
882
879
|
class SimpleLogger(logging.Logger):
|
|
883
880
|
"""Простой логгер"""
|
|
884
|
-
CRITICAL
|
|
885
|
-
DEBUG
|
|
886
|
-
ERROR
|
|
887
|
-
FATAL
|
|
888
|
-
INFO
|
|
889
|
-
NOTSET
|
|
890
|
-
WARN
|
|
891
|
-
WARNING
|
|
881
|
+
CRITICAL = logging.CRITICAL
|
|
882
|
+
DEBUG = logging.DEBUG
|
|
883
|
+
ERROR = logging.ERROR
|
|
884
|
+
FATAL = logging.FATAL
|
|
885
|
+
INFO = logging.INFO
|
|
886
|
+
NOTSET = logging.NOTSET
|
|
887
|
+
WARN = logging.WARN
|
|
888
|
+
WARNING = logging.WARNING
|
|
892
889
|
|
|
893
890
|
def __init__(self, name: str, level: int = logging.INFO, full_global=False, init_msg=True):
|
|
894
891
|
# Выбрать случайное название
|
|
@@ -1064,3 +1061,43 @@ def iter_caller_modules():
|
|
|
1064
1061
|
frame = frame.f_back
|
|
1065
1062
|
finally:
|
|
1066
1063
|
del frame
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
def parse_shaXsums_hex(lines: str | list[str]):
|
|
1067
|
+
"""Парсить файлы в формате `sha256sums.txt`"""
|
|
1068
|
+
if isinstance(lines, str):
|
|
1069
|
+
lines = lines.splitlines()
|
|
1070
|
+
result: dict[str, str] = {} # filename: hex value
|
|
1071
|
+
for line in lines:
|
|
1072
|
+
line = line.strip()
|
|
1073
|
+
if not line or line.startswith('#'):
|
|
1074
|
+
continue
|
|
1075
|
+
parts = line.replace(" ", " ").split(None, 1)
|
|
1076
|
+
if len(parts) == 2:
|
|
1077
|
+
hex_value, filename = parts
|
|
1078
|
+
result[filename] = hex_value.lower()
|
|
1079
|
+
return result
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def parse_shaXsums_bytes(lines: str | list[str]):
|
|
1083
|
+
"""Парсить файлы в формате `sha256sums.txt`, декодируя хеш"""
|
|
1084
|
+
return {k: bytes.fromhex(v) for k, v in parse_shaXsums_hex(lines).items()}
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
def guess_checksum_alg(digest_len: int):
|
|
1088
|
+
"""Найти алгоритм хеширования по длине хеша"""
|
|
1089
|
+
if not "hash_len" in cache:
|
|
1090
|
+
import hashlib
|
|
1091
|
+
c = {}
|
|
1092
|
+
for alg in hashlib.algorithms_available:
|
|
1093
|
+
try:
|
|
1094
|
+
c[alg] = len(hashlib.new(alg).digest())
|
|
1095
|
+
except Exception:
|
|
1096
|
+
pass
|
|
1097
|
+
cache["hash_len"] = c
|
|
1098
|
+
c: dict[str, int] = cache["hash_len"]
|
|
1099
|
+
result: set[str] = set()
|
|
1100
|
+
for k, v in c.items():
|
|
1101
|
+
if digest_len == v:
|
|
1102
|
+
result.add(k)
|
|
1103
|
+
return result
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/api/russian_trusted_root_ca_pem.crt
RENAMED
|
File without changes
|
{mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/api/russian_trusted_sub_ca_pem.crt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/gui_scripts/ms2_hash_gen.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|