arpakitlib 1.4.0__py3-none-any.whl
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.
Potentially problematic release.
This version of arpakitlib might be problematic. Click here for more details.
- arpakitlib/AUTHOR.md +6 -0
- arpakitlib/LICENSE +201 -0
- arpakitlib/NOTICE +2 -0
- arpakitlib/README.md +6 -0
- arpakitlib/__init__.py +0 -0
- arpakitlib/ar_additional_model_util.py +8 -0
- arpakitlib/ar_aiogram_util.py +363 -0
- arpakitlib/ar_arpakit_lib_module_util.py +150 -0
- arpakitlib/ar_arpakit_schedule_uust_api_client.py +527 -0
- arpakitlib/ar_arpakitlib_info.py +11 -0
- arpakitlib/ar_base64_util.py +30 -0
- arpakitlib/ar_base_worker.py +77 -0
- arpakitlib/ar_cache_file.py +124 -0
- arpakitlib/ar_datetime_util.py +38 -0
- arpakitlib/ar_dict_util.py +24 -0
- arpakitlib/ar_dream_ai_api_client.py +120 -0
- arpakitlib/ar_encrypt_and_decrypt_util.py +23 -0
- arpakitlib/ar_enumeration.py +76 -0
- arpakitlib/ar_fastapi_static/redoc/redoc.standalone.js +1826 -0
- arpakitlib/ar_fastapi_static/swagger-ui/favicon-16x16.png +0 -0
- arpakitlib/ar_fastapi_static/swagger-ui/favicon-32x32.png +0 -0
- arpakitlib/ar_fastapi_static/swagger-ui/index.css +16 -0
- arpakitlib/ar_fastapi_static/swagger-ui/index.html +19 -0
- arpakitlib/ar_fastapi_static/swagger-ui/oauth2-redirect.html +79 -0
- arpakitlib/ar_fastapi_static/swagger-ui/swagger-initializer.js +20 -0
- arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui-bundle.js +2 -0
- arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui-bundle.js.map +1 -0
- arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui-es-bundle-core.js +3 -0
- arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui-es-bundle-core.js.map +1 -0
- arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui-es-bundle.js +2 -0
- arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui-es-bundle.js.map +1 -0
- arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui-standalone-preset.js +2 -0
- arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui-standalone-preset.js.map +1 -0
- arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui.css +3 -0
- arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui.css.map +1 -0
- arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui.js +2 -0
- arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui.js.map +1 -0
- arpakitlib/ar_fastapi_util.py +294 -0
- arpakitlib/ar_file_storage_in_dir.py +127 -0
- arpakitlib/ar_generate_env_example.py +16 -0
- arpakitlib/ar_hash_util.py +19 -0
- arpakitlib/ar_http_request_util.py +75 -0
- arpakitlib/ar_ip_util.py +50 -0
- arpakitlib/ar_json_db.py +231 -0
- arpakitlib/ar_json_util.py +28 -0
- arpakitlib/ar_jwt_util.py +38 -0
- arpakitlib/ar_list_of_dicts_to_xlsx.py +32 -0
- arpakitlib/ar_list_util.py +26 -0
- arpakitlib/ar_logging_util.py +45 -0
- arpakitlib/ar_mongodb_util.py +143 -0
- arpakitlib/ar_need_type_util.py +58 -0
- arpakitlib/ar_openai_util.py +59 -0
- arpakitlib/ar_parse_command.py +102 -0
- arpakitlib/ar_postgresql_util.py +45 -0
- arpakitlib/ar_run_cmd.py +48 -0
- arpakitlib/ar_safe_sleep.py +23 -0
- arpakitlib/ar_schedule_uust_api_client.py +216 -0
- arpakitlib/ar_sqlalchemy_util.py +124 -0
- arpakitlib/ar_ssh_runner.py +260 -0
- arpakitlib/ar_str_util.py +79 -0
- arpakitlib/ar_type_util.py +82 -0
- arpakitlib/ar_yookassa_api_client.py +224 -0
- arpakitlib/ar_zabbix_util.py +190 -0
- arpakitlib-1.4.0.dist-info/LICENSE +201 -0
- arpakitlib-1.4.0.dist-info/METADATA +327 -0
- arpakitlib-1.4.0.dist-info/NOTICE +2 -0
- arpakitlib-1.4.0.dist-info/RECORD +68 -0
- arpakitlib-1.4.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
from typing import Optional, Any
|
|
7
|
+
|
|
8
|
+
import asyncssh
|
|
9
|
+
import paramiko
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from arpakitlib.ar_json_util import safely_transfer_to_json_str
|
|
13
|
+
|
|
14
|
+
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SSHRunTimeouts:
|
|
18
|
+
fast_command = timedelta(minutes=0, seconds=15).total_seconds()
|
|
19
|
+
medium_command = timedelta(minutes=2, seconds=30).total_seconds()
|
|
20
|
+
long_command = timedelta(minutes=5, seconds=0).total_seconds()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SSHBaseErr(Exception):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SSHCannotConnect(SSHBaseErr):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SSHCannotRun(SSHBaseErr):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SSHRunResHasErr(SSHBaseErr):
|
|
36
|
+
|
|
37
|
+
def __init__(self, ssh_run_res: SSHRunRes):
|
|
38
|
+
self.ssh_run_res = ssh_run_res
|
|
39
|
+
|
|
40
|
+
def __repr__(self):
|
|
41
|
+
return f"return_code={self.ssh_run_res.return_code}, stderr={self.ssh_run_res.err.strip()}"
|
|
42
|
+
|
|
43
|
+
def __str__(self):
|
|
44
|
+
return f"return_code={self.ssh_run_res.return_code}, stderr={self.ssh_run_res.err.strip()}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SSHRunRes(BaseModel):
|
|
48
|
+
out: str
|
|
49
|
+
err: str
|
|
50
|
+
return_code: int | None = None
|
|
51
|
+
|
|
52
|
+
def simple_dict(self) -> dict[str, Any]:
|
|
53
|
+
return {
|
|
54
|
+
"out": self.out,
|
|
55
|
+
"err": self.err,
|
|
56
|
+
"return_code": self.return_code,
|
|
57
|
+
"has_bad_return_code": self.has_bad_return_code,
|
|
58
|
+
"has_err": self.has_err
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
def simple_json(self) -> str:
|
|
62
|
+
return safely_transfer_to_json_str(self.simple_dict())
|
|
63
|
+
|
|
64
|
+
def __repr__(self) -> str:
|
|
65
|
+
return self.simple_json()
|
|
66
|
+
|
|
67
|
+
def __str__(self) -> str:
|
|
68
|
+
return self.simple_json()
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def has_bad_return_code(self) -> bool:
|
|
72
|
+
if self.return_code is None:
|
|
73
|
+
return False
|
|
74
|
+
return self.return_code != 0
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def has_err(self) -> bool:
|
|
78
|
+
if self.err:
|
|
79
|
+
return True
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def raise_for_bad_return_code(self):
|
|
83
|
+
if self.has_bad_return_code:
|
|
84
|
+
raise SSHRunResHasErr(ssh_run_res=self)
|
|
85
|
+
|
|
86
|
+
def raise_for_err(self):
|
|
87
|
+
if self.has_err:
|
|
88
|
+
raise SSHRunResHasErr(ssh_run_res=self)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class SSHRunner:
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
*,
|
|
96
|
+
hostname: str,
|
|
97
|
+
port: int = 22,
|
|
98
|
+
username: str = "root",
|
|
99
|
+
password: Optional[str] = None,
|
|
100
|
+
base_timeout: int = timedelta(seconds=5).total_seconds()
|
|
101
|
+
):
|
|
102
|
+
self.hostname = hostname
|
|
103
|
+
self.port = port
|
|
104
|
+
self.username = username
|
|
105
|
+
self.password = password
|
|
106
|
+
|
|
107
|
+
self.base_timeout = base_timeout
|
|
108
|
+
|
|
109
|
+
self._logger = logging.getLogger(
|
|
110
|
+
f"{logging.getLogger(self.__class__.__name__)} - {self.username}@{self.hostname}:{self.port}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
self.async_conn: Optional[asyncssh.SSHClientConnection] = None
|
|
114
|
+
|
|
115
|
+
self.sync_client = paramiko.SSHClient()
|
|
116
|
+
self.sync_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
117
|
+
|
|
118
|
+
"""SYNC"""
|
|
119
|
+
|
|
120
|
+
def sync_connect(self) -> SSHRunner:
|
|
121
|
+
try:
|
|
122
|
+
self.sync_client.connect(
|
|
123
|
+
hostname=self.hostname,
|
|
124
|
+
username=self.username,
|
|
125
|
+
password=self.password,
|
|
126
|
+
port=self.port,
|
|
127
|
+
timeout=self.base_timeout,
|
|
128
|
+
auth_timeout=self.base_timeout,
|
|
129
|
+
banner_timeout=self.base_timeout,
|
|
130
|
+
channel_timeout=self.base_timeout
|
|
131
|
+
)
|
|
132
|
+
except Exception as err:
|
|
133
|
+
raise SSHCannotConnect(err)
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
def sync_check_connection(self):
|
|
137
|
+
self.sync_connect()
|
|
138
|
+
|
|
139
|
+
def sync_is_conn_good(self) -> bool:
|
|
140
|
+
try:
|
|
141
|
+
self.sync_check_connection()
|
|
142
|
+
except SSHCannotConnect:
|
|
143
|
+
return False
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
def sync_run(
|
|
147
|
+
self,
|
|
148
|
+
command: str,
|
|
149
|
+
*,
|
|
150
|
+
timeout: float | None = SSHRunTimeouts.medium_command,
|
|
151
|
+
raise_for_bad_return_code: bool = True
|
|
152
|
+
) -> SSHRunRes:
|
|
153
|
+
if timeout is None:
|
|
154
|
+
timeout = self.base_timeout
|
|
155
|
+
|
|
156
|
+
self._logger.info(command)
|
|
157
|
+
|
|
158
|
+
self.sync_connect()
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
stdin, stdout, stderr = self.sync_client.exec_command(
|
|
162
|
+
command=command,
|
|
163
|
+
timeout=timeout
|
|
164
|
+
)
|
|
165
|
+
return_code = stdout.channel.recv_exit_status()
|
|
166
|
+
stdout = stdout.read().decode()
|
|
167
|
+
stderr = stderr.read().decode()
|
|
168
|
+
except Exception as err:
|
|
169
|
+
raise SSHCannotRun(err)
|
|
170
|
+
|
|
171
|
+
ssh_run_res = SSHRunRes(
|
|
172
|
+
out=stdout,
|
|
173
|
+
err=stderr,
|
|
174
|
+
return_code=return_code
|
|
175
|
+
)
|
|
176
|
+
if raise_for_bad_return_code is True:
|
|
177
|
+
ssh_run_res.raise_for_bad_return_code()
|
|
178
|
+
|
|
179
|
+
return ssh_run_res
|
|
180
|
+
|
|
181
|
+
def sync_close(self):
|
|
182
|
+
self.sync_client.close()
|
|
183
|
+
|
|
184
|
+
"""ASYNC SYNC"""
|
|
185
|
+
|
|
186
|
+
async def async_connect(self) -> SSHRunner:
|
|
187
|
+
if self.async_conn is None:
|
|
188
|
+
try:
|
|
189
|
+
self.async_conn = await asyncssh.connect(
|
|
190
|
+
host=self.hostname,
|
|
191
|
+
username=self.username,
|
|
192
|
+
password=self.password,
|
|
193
|
+
port=self.port,
|
|
194
|
+
connect_timeout=self.base_timeout,
|
|
195
|
+
known_hosts=None
|
|
196
|
+
)
|
|
197
|
+
except Exception as err:
|
|
198
|
+
raise SSHCannotConnect(err)
|
|
199
|
+
return self
|
|
200
|
+
|
|
201
|
+
async def async_check_connection(self):
|
|
202
|
+
await self.async_connect()
|
|
203
|
+
|
|
204
|
+
async def async_is_conn_good(self) -> bool:
|
|
205
|
+
try:
|
|
206
|
+
await self.async_check_connection()
|
|
207
|
+
except SSHCannotConnect:
|
|
208
|
+
return False
|
|
209
|
+
return True
|
|
210
|
+
|
|
211
|
+
async def async_run(
|
|
212
|
+
self,
|
|
213
|
+
command: str,
|
|
214
|
+
*,
|
|
215
|
+
timeout: float | None = SSHRunTimeouts.medium_command,
|
|
216
|
+
raise_for_bad_return_code: bool = True
|
|
217
|
+
) -> SSHRunRes:
|
|
218
|
+
if timeout is None:
|
|
219
|
+
timeout = self.base_timeout
|
|
220
|
+
|
|
221
|
+
self._logger.info(command)
|
|
222
|
+
|
|
223
|
+
await self.async_connect()
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
result: asyncssh.SSHCompletedProcess = await self.async_conn.run(
|
|
227
|
+
command,
|
|
228
|
+
check=False,
|
|
229
|
+
timeout=timeout
|
|
230
|
+
)
|
|
231
|
+
except Exception as err:
|
|
232
|
+
raise SSHCannotRun(err)
|
|
233
|
+
|
|
234
|
+
ssh_run_res = SSHRunRes(
|
|
235
|
+
out=result.stdout,
|
|
236
|
+
err=result.stderr,
|
|
237
|
+
return_code=result.returncode
|
|
238
|
+
)
|
|
239
|
+
if raise_for_bad_return_code is True:
|
|
240
|
+
ssh_run_res.raise_for_bad_return_code()
|
|
241
|
+
|
|
242
|
+
return ssh_run_res
|
|
243
|
+
|
|
244
|
+
def async_close(self):
|
|
245
|
+
if self.async_conn is not None:
|
|
246
|
+
self.async_conn.close()
|
|
247
|
+
self.async_conn = None
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def __example():
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
async def __async_example():
|
|
255
|
+
pass
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
if __name__ == '__main__':
|
|
259
|
+
__example()
|
|
260
|
+
asyncio.run(__async_example())
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from bs4 import BeautifulSoup
|
|
4
|
+
|
|
5
|
+
from arpakitlib.ar_type_util import raise_for_type
|
|
6
|
+
|
|
7
|
+
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def str_in(string: str, main_string: str, *, max_diff: Optional[int] = None) -> bool:
|
|
11
|
+
if string not in main_string:
|
|
12
|
+
return False
|
|
13
|
+
|
|
14
|
+
if max_diff is None:
|
|
15
|
+
return True
|
|
16
|
+
|
|
17
|
+
diff = len(main_string) - len(string)
|
|
18
|
+
if diff <= max_diff:
|
|
19
|
+
return True
|
|
20
|
+
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def bidirectional_str_in(string1: str, string2: str, *, max_diff: Optional[int] = None) -> bool:
|
|
25
|
+
if (
|
|
26
|
+
str_in(string=string1, main_string=string2, max_diff=max_diff)
|
|
27
|
+
or str_in(string=string2, main_string=string1, max_diff=max_diff)
|
|
28
|
+
):
|
|
29
|
+
return True
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def str_startswith(string: str, main_string: str, max_diff: Optional[int] = None) -> bool:
|
|
34
|
+
if not main_string.startswith(string):
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
if max_diff is None:
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
diff = len(main_string) - len(string)
|
|
41
|
+
if diff <= max_diff:
|
|
42
|
+
return True
|
|
43
|
+
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def bidirectional_str_startswith(string1: str, string2: str, max_diff: Optional[int] = None) -> bool:
|
|
48
|
+
if str_startswith(string1, string2, max_diff=max_diff) or str_startswith(string2, string1, max_diff=max_diff):
|
|
49
|
+
return True
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def blank_if_none(string: Optional[str] = None) -> str:
|
|
54
|
+
if string is None:
|
|
55
|
+
return ""
|
|
56
|
+
return string
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def remove_html(string: str) -> str:
|
|
60
|
+
raise_for_type(string, str)
|
|
61
|
+
return BeautifulSoup(string, "html.parser").text
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def remove_tags(string: str) -> str:
|
|
65
|
+
raise_for_type(string, str)
|
|
66
|
+
return string.replace("<", "").replace(">", "")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def remove_tags_and_html(string: str) -> str:
|
|
70
|
+
raise_for_type(string, str)
|
|
71
|
+
return remove_tags(remove_html(string))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def __example():
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == '__main__':
|
|
79
|
+
__example()
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from typing import Optional, Any
|
|
2
|
+
|
|
3
|
+
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# ---
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NotSet:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def is_set(v: Any) -> bool:
|
|
14
|
+
return not (v is NotSet or isinstance(v, NotSet))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def is_set_and_not_none(v: Any) -> bool:
|
|
18
|
+
return is_set(v) and (v is not None)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_not_set(v: Any) -> bool:
|
|
22
|
+
return not is_set(v)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_not_set_or_none(v: Any) -> bool:
|
|
26
|
+
return is_not_set(v) or (v is None)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def raise_if_set(v: Any):
|
|
30
|
+
if is_set(v):
|
|
31
|
+
raise ValueError("value is set")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def raise_if_not_set(v: Any):
|
|
35
|
+
if not is_set(v):
|
|
36
|
+
raise ValueError("value not set")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def make_none_if_not_set(v: Any) -> Any:
|
|
40
|
+
if is_not_set(v):
|
|
41
|
+
return None
|
|
42
|
+
return v
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def raise_for_type(comparable, need_type, comment_for_error: Optional[str] = None):
|
|
49
|
+
if comparable is need_type:
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
if not isinstance(comparable, need_type):
|
|
53
|
+
err = f"raise_for_type, {comparable} != {need_type}, type {type(comparable)} != type {need_type}"
|
|
54
|
+
if comment_for_error:
|
|
55
|
+
err += f"\n{comment_for_error}"
|
|
56
|
+
raise TypeError(err)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def raise_for_types(comparable, need_types, comment_for_error: Optional[str] = None):
|
|
60
|
+
exceptions = []
|
|
61
|
+
for need_type in need_types:
|
|
62
|
+
try:
|
|
63
|
+
raise_for_type(comparable=comparable, need_type=need_type, comment_for_error=comment_for_error)
|
|
64
|
+
return
|
|
65
|
+
except TypeError as e:
|
|
66
|
+
exceptions.append(e)
|
|
67
|
+
if exceptions:
|
|
68
|
+
err = f"raise_for_types, {comparable} not in {need_types}, type {type(comparable)} not in {need_types}"
|
|
69
|
+
if comment_for_error:
|
|
70
|
+
err += f"\n{comment_for_error}"
|
|
71
|
+
raise TypeError(err)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def __example():
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == '__main__':
|
|
82
|
+
__example()
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
from typing import Optional, Any
|
|
8
|
+
|
|
9
|
+
import aiohttp
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from arpakitlib.ar_dict_util import combine_dicts
|
|
13
|
+
from arpakitlib.ar_enumeration import EasyEnumeration
|
|
14
|
+
from arpakitlib.ar_safe_sleep import safe_sleep
|
|
15
|
+
from arpakitlib.ar_type_util import raise_for_type
|
|
16
|
+
|
|
17
|
+
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
https://yookassa.ru/developers/api
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class YookassaPaymentStatuses(EasyEnumeration):
|
|
25
|
+
pending = "pending"
|
|
26
|
+
waiting_for_capture = "waiting_for_capture"
|
|
27
|
+
succeeded = "succeeded"
|
|
28
|
+
canceled = "canceled"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class YookassaAPIException(Exception):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class YookassaAPIClient:
|
|
36
|
+
def __init__(self, *, secret_key: str, shop_id: int):
|
|
37
|
+
super().__init__()
|
|
38
|
+
self.secret_key = secret_key
|
|
39
|
+
self.shop_id = shop_id
|
|
40
|
+
self.headers = {"Content-Type": "application/json"}
|
|
41
|
+
self._logger = logging.getLogger(self.__class__.__name__)
|
|
42
|
+
|
|
43
|
+
def _sync_make_request(self, method: str, url: str, **kwargs) -> requests.Response:
|
|
44
|
+
max_tries = 7
|
|
45
|
+
tries = 0
|
|
46
|
+
|
|
47
|
+
kwargs["url"] = url
|
|
48
|
+
kwargs["method"] = method
|
|
49
|
+
kwargs["timeout"] = (timedelta(seconds=3).total_seconds(), timedelta(seconds=3).total_seconds())
|
|
50
|
+
if "headers" not in kwargs:
|
|
51
|
+
kwargs["headers"] = {}
|
|
52
|
+
kwargs["headers"] = combine_dicts(self.headers, kwargs["headers"])
|
|
53
|
+
kwargs["auth"] = (self.shop_id, self.secret_key)
|
|
54
|
+
|
|
55
|
+
while True:
|
|
56
|
+
self._logger.info(f"{method} {url}")
|
|
57
|
+
tries += 1
|
|
58
|
+
try:
|
|
59
|
+
return requests.request(**kwargs)
|
|
60
|
+
except Exception as err:
|
|
61
|
+
self._logger.warning(f"{tries}/{max_tries} {err} {method} {url}")
|
|
62
|
+
if tries >= max_tries:
|
|
63
|
+
raise YookassaAPIException(err)
|
|
64
|
+
safe_sleep(timedelta(seconds=0.1).total_seconds())
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
async def _async_make_request(self, method: str, url: str, **kwargs) -> aiohttp.ClientResponse:
|
|
68
|
+
max_tries = 7
|
|
69
|
+
tries = 0
|
|
70
|
+
|
|
71
|
+
kwargs["url"] = url
|
|
72
|
+
kwargs["method"] = method
|
|
73
|
+
kwargs["timeout"] = aiohttp.ClientTimeout(total=timedelta(seconds=15).total_seconds())
|
|
74
|
+
if "headers" not in kwargs:
|
|
75
|
+
kwargs["headers"] = {}
|
|
76
|
+
kwargs["headers"] = combine_dicts(self.headers, kwargs["headers"])
|
|
77
|
+
kwargs["auth"] = aiohttp.BasicAuth(login=str(self.shop_id), password=self.secret_key)
|
|
78
|
+
|
|
79
|
+
while True:
|
|
80
|
+
self._logger.info(f"{method} {url}")
|
|
81
|
+
tries += 1
|
|
82
|
+
try:
|
|
83
|
+
async with aiohttp.ClientSession() as session:
|
|
84
|
+
async with session.request(**kwargs) as response:
|
|
85
|
+
await response.read()
|
|
86
|
+
return response
|
|
87
|
+
except Exception as err:
|
|
88
|
+
self._logger.warning(f"{tries}/{max_tries} {err} {method} {url}")
|
|
89
|
+
if tries >= max_tries:
|
|
90
|
+
raise YookassaAPIException(err)
|
|
91
|
+
await asyncio.sleep(timedelta(seconds=0.1).total_seconds())
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
def sync_create_payment(
|
|
95
|
+
self,
|
|
96
|
+
json_body: dict[str, Any],
|
|
97
|
+
idempotence_key: Optional[str] = None
|
|
98
|
+
) -> dict[str, Any]:
|
|
99
|
+
|
|
100
|
+
"""
|
|
101
|
+
json_body example
|
|
102
|
+
json_body = {
|
|
103
|
+
"amount": {
|
|
104
|
+
"value": "2.0",
|
|
105
|
+
"currency": "RUB"
|
|
106
|
+
},
|
|
107
|
+
"description": "description",
|
|
108
|
+
"confirmation": {
|
|
109
|
+
"type": "redirect",
|
|
110
|
+
"return_url": f"https://t.me/{get_tg_bot_username()}",
|
|
111
|
+
"locale": "ru_RU"
|
|
112
|
+
},
|
|
113
|
+
"capture": True,
|
|
114
|
+
"metadata": {},
|
|
115
|
+
"merchant_customer_id": ""
|
|
116
|
+
}
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
if idempotence_key is None:
|
|
120
|
+
idempotence_key = str(uuid.uuid4())
|
|
121
|
+
|
|
122
|
+
headers = combine_dicts({"Idempotence-Key": idempotence_key})
|
|
123
|
+
|
|
124
|
+
response = self._sync_make_request(
|
|
125
|
+
method="POST",
|
|
126
|
+
url="https://api.yookassa.ru/v3/payments",
|
|
127
|
+
headers=headers,
|
|
128
|
+
json=json_body,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
json_data = response.json()
|
|
132
|
+
|
|
133
|
+
response.raise_for_status()
|
|
134
|
+
|
|
135
|
+
return json_data
|
|
136
|
+
|
|
137
|
+
def sync_get_payment(self, payment_id: str) -> Optional[dict[str, Any]]:
|
|
138
|
+
raise_for_type(payment_id, str)
|
|
139
|
+
|
|
140
|
+
response = self._sync_make_request(
|
|
141
|
+
method="GET",
|
|
142
|
+
url=f"https://api.yookassa.ru/v3/payments/{payment_id}",
|
|
143
|
+
headers=self.headers
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
json_data = response.json()
|
|
147
|
+
|
|
148
|
+
if response.status_code == 404:
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
response.raise_for_status()
|
|
152
|
+
|
|
153
|
+
return json_data
|
|
154
|
+
|
|
155
|
+
async def async_create_payment(
|
|
156
|
+
self, json_body: dict[str, Any], idempotence_key: Optional[str] = None
|
|
157
|
+
) -> dict[str, Any]:
|
|
158
|
+
|
|
159
|
+
"""
|
|
160
|
+
json_body example
|
|
161
|
+
json_body = {
|
|
162
|
+
"amount": {
|
|
163
|
+
"value": "2.0",
|
|
164
|
+
"currency": "RUB"
|
|
165
|
+
},
|
|
166
|
+
"description": "description",
|
|
167
|
+
"confirmation": {
|
|
168
|
+
"type": "redirect",
|
|
169
|
+
"return_url": f"https://t.me/{get_tg_bot_username()}",
|
|
170
|
+
"locale": "ru_RU"
|
|
171
|
+
},
|
|
172
|
+
"capture": True,
|
|
173
|
+
"metadata": {},
|
|
174
|
+
"merchant_customer_id": ""
|
|
175
|
+
}
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
if idempotence_key is None:
|
|
179
|
+
idempotence_key = str(uuid.uuid4())
|
|
180
|
+
|
|
181
|
+
headers = combine_dicts({"Idempotence-Key": idempotence_key})
|
|
182
|
+
|
|
183
|
+
response = await self._async_make_request(
|
|
184
|
+
method="POST",
|
|
185
|
+
url="https://api.yookassa.ru/v3/payments",
|
|
186
|
+
headers=headers,
|
|
187
|
+
json=json_body,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
json_data = await response.json()
|
|
191
|
+
|
|
192
|
+
response.raise_for_status()
|
|
193
|
+
|
|
194
|
+
return json_data
|
|
195
|
+
|
|
196
|
+
async def async_get_payment(self, payment_id: str) -> Optional[dict[str, Any]]:
|
|
197
|
+
raise_for_type(payment_id, str)
|
|
198
|
+
|
|
199
|
+
response = await self._async_make_request(
|
|
200
|
+
method="GET",
|
|
201
|
+
url=f"https://api.yookassa.ru/v3/payments/{payment_id}",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
json_data = await response.json()
|
|
205
|
+
|
|
206
|
+
if response.status == 404:
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
response.raise_for_status()
|
|
210
|
+
|
|
211
|
+
return json_data
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def __example():
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
async def __async_example():
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
if __name__ == '__main__':
|
|
223
|
+
__example()
|
|
224
|
+
asyncio.run(__async_example())
|