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.
Files changed (49) hide show
  1. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/PKG-INFO +3 -1
  2. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/README.md +2 -0
  3. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/pyproject.toml +1 -1
  4. mainshortcuts2-2.7.2/src/MainShortcuts2/api/github.py +383 -0
  5. mainshortcuts2-2.7.2/src/MainShortcuts2/ex/aiohttp_ex/web.py +158 -0
  6. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/term.py +128 -0
  7. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/utils.py +49 -12
  8. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/__init__.py +0 -0
  9. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/__main__.py +0 -0
  10. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/_any2json_regs.py +0 -0
  11. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/_module_info.py +0 -0
  12. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/_ms2app_regs.py +0 -0
  13. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/advanced.py +0 -0
  14. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/any2json.py +0 -0
  15. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/api/base.py +0 -0
  16. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/api/gigachat.py +0 -0
  17. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/api/russian_trusted_root_ca_pem.crt +0 -0
  18. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/api/russian_trusted_sub_ca_pem.crt +0 -0
  19. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/api/webdav.py +0 -0
  20. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/cfg.py +0 -0
  21. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/core.py +0 -0
  22. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/core_config.py +0 -0
  23. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/dict.py +0 -0
  24. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/dir.py +0 -0
  25. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/ex/__init__.py +0 -0
  26. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/ex/datetime_ex.py +0 -0
  27. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/ex/pathlib_ex.py +0 -0
  28. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/ex/pil_ex.py +0 -0
  29. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/ex/psutil_ex.py +0 -0
  30. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/ex/sqlite_ex.py +0 -0
  31. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/file.py +0 -0
  32. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/gui_scripts/__init__.py +0 -0
  33. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/gui_scripts/ms2_hash_gen.py +0 -0
  34. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/gui_scripts/utils.py +0 -0
  35. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/json.py +0 -0
  36. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/linux.py +0 -0
  37. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/list.py +0 -0
  38. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/ms2app.py +0 -0
  39. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/ms2hash.py +0 -0
  40. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/path.py +0 -0
  41. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/proc.py +0 -0
  42. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/regex.py +0 -0
  43. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/special_chars.py +0 -0
  44. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/sql/_sql_base.py +0 -0
  45. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/sql/postgresql.py +0 -0
  46. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/sql/sqlite.py +0 -0
  47. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/str.py +0 -0
  48. {mainshortcuts2-2.7.0 → mainshortcuts2-2.7.2}/src/MainShortcuts2/types.py +0 -0
  49. {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.0
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
+ [![Build and upload release](https://github.com/MainPlay-TG/MainShortcuts2.py/actions/workflows/build.yml/badge.svg)](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
+ [![Build and upload release](https://github.com/MainPlay-TG/MainShortcuts2.py/actions/workflows/build.yml/badge.svg)](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
  ## Установка
@@ -1,5 +1,5 @@
1
1
  [tool.poetry]
2
- version = "2.7.0"
2
+ version = "2.7.2"
3
3
  name = "MainShortcuts2"
4
4
  description = "Сокращение и улучшение функций + консольные утилиты"
5
5
  authors = ["MainPlay TG <xbox.roman6666666666@gmail.com>"]
@@ -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
- for i in classes:
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: int = logging.CRITICAL
885
- DEBUG: int = logging.DEBUG
886
- ERROR: int = logging.ERROR
887
- FATAL: int = logging.FATAL
888
- INFO: int = logging.INFO
889
- NOTSET: int = logging.NOTSET
890
- WARN: int = logging.WARN
891
- WARNING: int = logging.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