arpakitlib 1.8.307__py3-none-any.whl → 1.8.322__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/ar_ensure_sqlalchemy_check_constraints.py +4 -2
- arpakitlib/ar_generate_connection_url_util.py +79 -0
- arpakitlib/ar_mongodb_util.py +0 -23
- arpakitlib/ar_settings_util.py +24 -3
- arpakitlib/ar_sqlalchemy_drop_check_constraints.py +3 -0
- arpakitlib/ar_sqlalchemy_util.py +3 -40
- arpakitlib/ar_uppercase_env_keys.py +24 -13
- arpakitlib/ar_yookassa_api_client_util.py +175 -0
- arpakitlib/include_fastapi_routers_from_dir.py +46 -0
- {arpakitlib-1.8.307.dist-info → arpakitlib-1.8.322.dist-info}/METADATA +2 -2
- {arpakitlib-1.8.307.dist-info → arpakitlib-1.8.322.dist-info}/RECORD +14 -16
- arpakitlib/ar_additional_model_util.py +0 -40
- arpakitlib/ar_arpakit_schedule_uust_api_client_util.py +0 -422
- arpakitlib/ar_arpakit_schedule_uust_site_util.py +0 -68
- arpakitlib/ar_generate_celery_url.py +0 -71
- arpakitlib/ar_schedule_uust_api_client_util.py +0 -227
- {arpakitlib-1.8.307.dist-info → arpakitlib-1.8.322.dist-info}/WHEEL +0 -0
- {arpakitlib-1.8.307.dist-info → arpakitlib-1.8.322.dist-info}/entry_points.txt +0 -0
- {arpakitlib-1.8.307.dist-info → arpakitlib-1.8.322.dist-info}/licenses/LICENSE +0 -0
|
@@ -73,12 +73,14 @@ def ensure_sqlalchemy_check_constraints(*, base_: type[DeclarativeBase], engine:
|
|
|
73
73
|
# Возьмём соединение и транзакцию (BEGIN), чтобы изменения были атомарными
|
|
74
74
|
|
|
75
75
|
with engine.begin() as conn:
|
|
76
|
-
|
|
76
|
+
conn_inspection_ = sqlalchemy.inspect(conn)
|
|
77
77
|
|
|
78
78
|
for table in base_.metadata.tables.values():
|
|
79
|
+
if not conn_inspection_.has_table(table.name, schema=table.schema):
|
|
80
|
+
continue
|
|
79
81
|
|
|
80
82
|
# Соберём существующие CHECK-и из БД
|
|
81
|
-
existing =
|
|
83
|
+
existing = conn_inspection_.get_check_constraints(table.name, schema=table.schema)
|
|
82
84
|
|
|
83
85
|
# Множества для быстрых проверок
|
|
84
86
|
existing_names: Set[str] = set()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from urllib.parse import quote_plus
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def generate_connection_url(
|
|
5
|
+
*,
|
|
6
|
+
scheme: str = "postgresql", # общий случай: postgresql, sqlite, redis, amqp и т.п.
|
|
7
|
+
user: str | None = None,
|
|
8
|
+
password: str | None = None,
|
|
9
|
+
host: str | None = "127.0.0.1",
|
|
10
|
+
port: int | None = None,
|
|
11
|
+
database: str | int | None = None,
|
|
12
|
+
quote_query_params: bool = True,
|
|
13
|
+
**query_params
|
|
14
|
+
) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Универсальная функция для генерации URL соединений (Postgres, Redis, AMQP, SQLite и др.)
|
|
17
|
+
|
|
18
|
+
Примеры:
|
|
19
|
+
postgresql://user:pass@localhost:5432/dbname
|
|
20
|
+
sqlite:///path/to/db.sqlite3
|
|
21
|
+
redis://:mypassword@redis:6379/0
|
|
22
|
+
amqp://user:pass@rabbit:5672/myvhost
|
|
23
|
+
"""
|
|
24
|
+
# Формируем часть авторизации
|
|
25
|
+
auth_part = ""
|
|
26
|
+
if user and password:
|
|
27
|
+
auth_part = f"{quote_plus(user)}:{quote_plus(password)}@"
|
|
28
|
+
elif password and not user:
|
|
29
|
+
auth_part = f":{quote_plus(password)}@"
|
|
30
|
+
elif user:
|
|
31
|
+
auth_part = f"{quote_plus(user)}@"
|
|
32
|
+
|
|
33
|
+
# Формируем хост и порт
|
|
34
|
+
if scheme.startswith("sqlite"):
|
|
35
|
+
host_part = ""
|
|
36
|
+
else:
|
|
37
|
+
host_part = host or ""
|
|
38
|
+
if port:
|
|
39
|
+
host_part += f":{port}"
|
|
40
|
+
|
|
41
|
+
# Формируем "базу" (database / номер / путь)
|
|
42
|
+
db_part = ""
|
|
43
|
+
if database is not None:
|
|
44
|
+
db_part = f"/{quote_plus(str(database))}"
|
|
45
|
+
|
|
46
|
+
# Формируем query параметры
|
|
47
|
+
query_part = ""
|
|
48
|
+
if query_params:
|
|
49
|
+
query_items = []
|
|
50
|
+
for k, v in query_params.items():
|
|
51
|
+
value = str(v)
|
|
52
|
+
if quote_query_params:
|
|
53
|
+
value = quote_plus(value)
|
|
54
|
+
query_items.append(f"{k}={value}")
|
|
55
|
+
query_part = f"?{'&'.join(query_items)}"
|
|
56
|
+
|
|
57
|
+
return f"{scheme}://{auth_part}{host_part}{db_part}{query_part}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def __example():
|
|
61
|
+
print(generate_connection_url())
|
|
62
|
+
# → redis://127.0.0.1:6379/0
|
|
63
|
+
|
|
64
|
+
# Redis с паролем
|
|
65
|
+
print(generate_connection_url(password="supersecret", host="redis"))
|
|
66
|
+
# → redis://:supersecret@redis:6379/0
|
|
67
|
+
|
|
68
|
+
# RabbitMQ (AMQP)
|
|
69
|
+
print(generate_connection_url(scheme="amqp", user="guest", password="guest", host="rabbitmq", port=6789))
|
|
70
|
+
# → amqp://guest:guest@rabbitmq:6379/0
|
|
71
|
+
|
|
72
|
+
# Redis с параметрами
|
|
73
|
+
print(generate_connection_url(scheme="mongodb", user="root", password="pass", host="localhost", port=27017,
|
|
74
|
+
authSource="fghjkl;", quote_query_params=True))
|
|
75
|
+
# → redis://:pass@127.0.0.1:6379/0?ssl_cert_reqs=none&socket_timeout=10
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == '__main__':
|
|
79
|
+
__example()
|
arpakitlib/ar_mongodb_util.py
CHANGED
|
@@ -4,8 +4,6 @@ import asyncio
|
|
|
4
4
|
import logging
|
|
5
5
|
from abc import abstractmethod
|
|
6
6
|
from random import randint
|
|
7
|
-
from typing import Optional
|
|
8
|
-
from urllib.parse import quote
|
|
9
7
|
|
|
10
8
|
from pymongo import MongoClient
|
|
11
9
|
from pymongo.collection import Collection
|
|
@@ -14,27 +12,6 @@ from pymongo.database import Database
|
|
|
14
12
|
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
|
15
13
|
|
|
16
14
|
|
|
17
|
-
def generate_mongo_uri(
|
|
18
|
-
*,
|
|
19
|
-
mongo_user: Optional[str] = None,
|
|
20
|
-
mongo_password: Optional[str] = None,
|
|
21
|
-
mongo_hostname: str = "localhost",
|
|
22
|
-
mongo_port: int = 27017,
|
|
23
|
-
mongo_auth_db: Optional[str] = None
|
|
24
|
-
) -> str:
|
|
25
|
-
res: str = f'mongodb://'
|
|
26
|
-
if mongo_user:
|
|
27
|
-
res += f"{mongo_user}"
|
|
28
|
-
if mongo_password:
|
|
29
|
-
res += f":{quote(mongo_password)}"
|
|
30
|
-
res += "@"
|
|
31
|
-
res += f"{mongo_hostname}:{mongo_port}"
|
|
32
|
-
if mongo_auth_db is not None:
|
|
33
|
-
res += f"/?authSource={mongo_auth_db}"
|
|
34
|
-
|
|
35
|
-
return res
|
|
36
|
-
|
|
37
|
-
|
|
38
15
|
class EasyMongoDb:
|
|
39
16
|
def __init__(
|
|
40
17
|
self,
|
arpakitlib/ar_settings_util.py
CHANGED
|
@@ -8,12 +8,24 @@ from pydantic_settings import BaseSettings
|
|
|
8
8
|
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def generate_env_example(settings_class: Union[BaseSettings, type[BaseSettings]]):
|
|
11
|
+
def generate_env_example(settings_class: Union[BaseSettings, type[BaseSettings]]) -> str:
|
|
12
12
|
res = ""
|
|
13
13
|
for k, f in settings_class.model_fields.items():
|
|
14
14
|
if f.default is not PydanticUndefined:
|
|
15
|
-
|
|
15
|
+
v = f.default
|
|
16
|
+
# Приводим к строке для .env
|
|
17
|
+
if isinstance(v, bool):
|
|
18
|
+
s = "true" if v else "false"
|
|
19
|
+
elif v is None:
|
|
20
|
+
s = "None"
|
|
21
|
+
else:
|
|
22
|
+
s = str(v)
|
|
23
|
+
# Если дефолт — строка с пробелами → в кавычки (экранируем \ и ")
|
|
24
|
+
if isinstance(v, str) and any(ch.isspace() for ch in v):
|
|
25
|
+
s = '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
26
|
+
res += f"{k}={s}\n"
|
|
16
27
|
else:
|
|
28
|
+
# обязательное поле — без значения
|
|
17
29
|
res += f"{k}=\n"
|
|
18
30
|
return res
|
|
19
31
|
|
|
@@ -25,8 +37,17 @@ class SimpleSettings(BaseSettings):
|
|
|
25
37
|
@classmethod
|
|
26
38
|
def validate_all_fields(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
27
39
|
for key, value in values.items():
|
|
28
|
-
if isinstance(value, str)
|
|
40
|
+
if not isinstance(value, str):
|
|
41
|
+
continue
|
|
42
|
+
if value.lower().strip() in {"null", "none", "nil"}:
|
|
29
43
|
values[key] = None
|
|
44
|
+
elif value.lower().strip() == "default_value":
|
|
45
|
+
field = cls.model_fields.get(key)
|
|
46
|
+
if field is not None:
|
|
47
|
+
if field.default is not None:
|
|
48
|
+
values[key] = field.default
|
|
49
|
+
elif field.default_factory is not None:
|
|
50
|
+
values[key] = field.default_factory()
|
|
30
51
|
return values
|
|
31
52
|
|
|
32
53
|
@classmethod
|
|
@@ -58,6 +58,9 @@ def drop_sqlalchemy_check_constraints(*, base_: type[DeclarativeBase], engine: E
|
|
|
58
58
|
conn_inspection_ = sqlalchemy.inspect(conn)
|
|
59
59
|
|
|
60
60
|
for table in base_.metadata.tables.values():
|
|
61
|
+
if not conn_inspection_.has_table(table.name, schema=table.schema):
|
|
62
|
+
continue
|
|
63
|
+
|
|
61
64
|
fqtn = _qualified_name(table.schema, table.name, engine.dialect.name)
|
|
62
65
|
|
|
63
66
|
# берём ВСЕ реальные CHECK-и из базы
|
arpakitlib/ar_sqlalchemy_util.py
CHANGED
|
@@ -3,7 +3,6 @@ import asyncio
|
|
|
3
3
|
import logging
|
|
4
4
|
from datetime import timedelta, datetime
|
|
5
5
|
from typing import Any, Collection
|
|
6
|
-
from urllib.parse import quote_plus
|
|
7
6
|
from uuid import uuid4
|
|
8
7
|
|
|
9
8
|
import sqlalchemy
|
|
@@ -17,44 +16,6 @@ from arpakitlib.ar_datetime_util import now_utc_dt
|
|
|
17
16
|
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
|
18
17
|
|
|
19
18
|
|
|
20
|
-
def generate_sqlalchemy_url(
|
|
21
|
-
*,
|
|
22
|
-
base: str = "postgresql",
|
|
23
|
-
user: str | None = None,
|
|
24
|
-
password: str | None = None,
|
|
25
|
-
host: str | None = "127.0.0.1",
|
|
26
|
-
port: int | None = 5432,
|
|
27
|
-
database: str | None = None,
|
|
28
|
-
**query_params
|
|
29
|
-
) -> str | None:
|
|
30
|
-
if host is None or port is None:
|
|
31
|
-
return None
|
|
32
|
-
|
|
33
|
-
auth_part = ""
|
|
34
|
-
if user and password:
|
|
35
|
-
auth_part = f"{quote_plus(user)}:{quote_plus(password)}@"
|
|
36
|
-
elif user:
|
|
37
|
-
auth_part = f"{quote_plus(user)}@"
|
|
38
|
-
|
|
39
|
-
if base.startswith("sqlite"):
|
|
40
|
-
host_part = ""
|
|
41
|
-
else:
|
|
42
|
-
host_part = f"{host}"
|
|
43
|
-
if port:
|
|
44
|
-
host_part += f":{port}"
|
|
45
|
-
|
|
46
|
-
database_part = f"/{database}" if database else ""
|
|
47
|
-
if base.startswith("sqlite") and database:
|
|
48
|
-
database_part = f"/{database}"
|
|
49
|
-
|
|
50
|
-
query_part = ""
|
|
51
|
-
if query_params:
|
|
52
|
-
query_items = [f"{key}={quote_plus(str(value))}" for key, value in query_params.items()]
|
|
53
|
-
query_part = f"?{'&'.join(query_items)}"
|
|
54
|
-
|
|
55
|
-
return f"{base}://{auth_part}{host_part}{database_part}{query_part}"
|
|
56
|
-
|
|
57
|
-
|
|
58
19
|
class BaseDBM(DeclarativeBase):
|
|
59
20
|
__abstract__ = True
|
|
60
21
|
__table_args__ = {"extend_existing": True}
|
|
@@ -333,9 +294,11 @@ class SQLAlchemyDb:
|
|
|
333
294
|
self.base_dbm.metadata.drop_all(bind=self.engine, checkfirst=True)
|
|
334
295
|
self._logger.info("dropped")
|
|
335
296
|
|
|
336
|
-
def reinit(self):
|
|
297
|
+
def reinit(self, ensure_check_constraints: bool = True):
|
|
337
298
|
self.base_dbm.metadata.drop_all(bind=self.engine, checkfirst=True)
|
|
338
299
|
self.base_dbm.metadata.create_all(bind=self.engine, checkfirst=True)
|
|
300
|
+
if ensure_check_constraints:
|
|
301
|
+
self.ensure_check_constraints()
|
|
339
302
|
self._logger.info("reinited")
|
|
340
303
|
|
|
341
304
|
def reinit_all(self):
|
|
@@ -13,14 +13,16 @@ LINE_RE = re.compile(r"""
|
|
|
13
13
|
$
|
|
14
14
|
""", re.VERBOSE)
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
COMMENT_KEY_RE = re.compile(r"^(\s*#\s*)([A-Za-z_][A-Za-z0-9_]*)(\s*=\s*.*)$")
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def uppercase_env_keys(
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
def uppercase_env_keys(
|
|
20
|
+
*, path: str | Path,
|
|
21
|
+
output: Optional[str | Path] = None,
|
|
22
|
+
backup: bool = False
|
|
23
|
+
) -> Path:
|
|
22
24
|
"""
|
|
23
|
-
Преобразует имена переменных в .env в верхний регистр.
|
|
25
|
+
Преобразует имена переменных в .env и в комментариях в верхний регистр.
|
|
24
26
|
|
|
25
27
|
:param path: путь к исходному .env
|
|
26
28
|
:param output: путь к файлу вывода. Если None — правит на месте.
|
|
@@ -33,18 +35,29 @@ def uppercase_env_keys(*, path: str | Path,
|
|
|
33
35
|
|
|
34
36
|
text = src.read_text(encoding="utf-8-sig").splitlines(keepends=True)
|
|
35
37
|
|
|
36
|
-
seen_upper = set()
|
|
37
38
|
out_lines = []
|
|
38
39
|
|
|
39
40
|
for line in text:
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
stripped = line.strip()
|
|
42
|
+
|
|
43
|
+
# пустые строки — оставляем как есть
|
|
44
|
+
if not stripped:
|
|
42
45
|
out_lines.append(line)
|
|
43
46
|
continue
|
|
44
47
|
|
|
48
|
+
# комментарий с "ключом" вида # KEY=
|
|
49
|
+
m_comment = COMMENT_KEY_RE.match(line.rstrip("\n"))
|
|
50
|
+
if m_comment:
|
|
51
|
+
prefix, key, rest = m_comment.groups()
|
|
52
|
+
key_up = key.upper()
|
|
53
|
+
# сохраняем оригинальный символ конца строки, если он был
|
|
54
|
+
newline = "\n" if line.endswith("\n") else ""
|
|
55
|
+
out_lines.append(f"{prefix}{key_up}{rest}{newline}")
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
# стандартная строка с KEY=VAL
|
|
45
59
|
m = LINE_RE.match(line.rstrip("\n"))
|
|
46
60
|
if not m:
|
|
47
|
-
# строки без "=" или нестандартные — не трогаем
|
|
48
61
|
out_lines.append(line)
|
|
49
62
|
continue
|
|
50
63
|
|
|
@@ -55,12 +68,10 @@ def uppercase_env_keys(*, path: str | Path,
|
|
|
55
68
|
val = m.group("val")
|
|
56
69
|
|
|
57
70
|
key_up = key.upper()
|
|
58
|
-
|
|
59
|
-
new_line = f"{lead}{(export_kw + ' ') if export_kw else ''}{key_up}{eq}{val}
|
|
71
|
+
newline = "\n" if line.endswith("\n") else ""
|
|
72
|
+
new_line = f"{lead}{(export_kw + ' ') if export_kw else ''}{key_up}{eq}{val}{newline}"
|
|
60
73
|
out_lines.append(new_line)
|
|
61
74
|
|
|
62
|
-
seen_upper.add(key_up)
|
|
63
|
-
|
|
64
75
|
# запись
|
|
65
76
|
if output is None:
|
|
66
77
|
if backup:
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import aiohttp
|
|
10
|
+
import requests
|
|
11
|
+
from arpakitlib.ar_dict_util import combine_dicts
|
|
12
|
+
from arpakitlib.ar_enumeration_util import Enumeration
|
|
13
|
+
from arpakitlib.ar_http_request_util import sync_make_http_request, async_make_http_request
|
|
14
|
+
from arpakitlib.ar_type_util import raise_for_type
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
https://yookassa.ru/developers/api
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class YookassaAPIClient:
|
|
22
|
+
class PaymentStatuses(Enumeration):
|
|
23
|
+
pending = "pending"
|
|
24
|
+
waiting_for_capture = "waiting_for_capture"
|
|
25
|
+
succeeded = "succeeded"
|
|
26
|
+
canceled = "canceled"
|
|
27
|
+
|
|
28
|
+
def __init__(self, *, secret_key: str, shop_id: int):
|
|
29
|
+
super().__init__()
|
|
30
|
+
self.secret_key = secret_key
|
|
31
|
+
self.shop_id = shop_id
|
|
32
|
+
self.headers = {"Content-Type": "application/json"}
|
|
33
|
+
self._logger = logging.getLogger(f"{self.__class__.__name__}-{shop_id}")
|
|
34
|
+
|
|
35
|
+
def _sync_make_http_request(
|
|
36
|
+
self,
|
|
37
|
+
*,
|
|
38
|
+
method: str,
|
|
39
|
+
url: str,
|
|
40
|
+
headers: dict[str, Any] | None = None,
|
|
41
|
+
**kwargs
|
|
42
|
+
) -> requests.Response:
|
|
43
|
+
return sync_make_http_request(
|
|
44
|
+
method=method,
|
|
45
|
+
url=url,
|
|
46
|
+
headers=combine_dicts(self.headers, (headers if headers is not None else {})),
|
|
47
|
+
max_tries_=3,
|
|
48
|
+
raise_for_status_=True,
|
|
49
|
+
timeout_=timedelta(seconds=3),
|
|
50
|
+
not_raise_for_statuses_=[404],
|
|
51
|
+
auth=(self.shop_id, self.secret_key),
|
|
52
|
+
enable_logging_=False,
|
|
53
|
+
**kwargs
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
async def _async_make_http_request(
|
|
57
|
+
self,
|
|
58
|
+
*,
|
|
59
|
+
method: str = "GET",
|
|
60
|
+
url: str,
|
|
61
|
+
headers: dict[str, Any] | None = None,
|
|
62
|
+
**kwargs
|
|
63
|
+
) -> aiohttp.ClientResponse:
|
|
64
|
+
return await async_make_http_request(
|
|
65
|
+
method=method,
|
|
66
|
+
url=url,
|
|
67
|
+
headers=combine_dicts(self.headers, (headers if headers is not None else {})),
|
|
68
|
+
max_tries_=3,
|
|
69
|
+
raise_for_status_=True,
|
|
70
|
+
not_raise_for_statuses_=[404],
|
|
71
|
+
timeout_=timedelta(seconds=3),
|
|
72
|
+
auth=aiohttp.BasicAuth(login=str(self.shop_id), password=self.secret_key),
|
|
73
|
+
enable_logging_=False,
|
|
74
|
+
**kwargs
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def sync_create_payment(self, *, json_body: dict[str, Any]) -> dict[str, Any]:
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
json_body example
|
|
81
|
+
json_body = {
|
|
82
|
+
"amount": {
|
|
83
|
+
"value": "2.0",
|
|
84
|
+
"currency": "RUB"
|
|
85
|
+
},
|
|
86
|
+
"description": "description",
|
|
87
|
+
"confirmation": {
|
|
88
|
+
"type": "redirect",
|
|
89
|
+
"return_url": f"https://t.me/{get_tg_bot_username()}",
|
|
90
|
+
"locale": "ru_RU"
|
|
91
|
+
},
|
|
92
|
+
"capture": True,
|
|
93
|
+
"metadata": {},
|
|
94
|
+
"merchant_customer_id": ""
|
|
95
|
+
}
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
response = self._sync_make_http_request(
|
|
99
|
+
method="POST",
|
|
100
|
+
url="https://api.yookassa.ru/v3/payments",
|
|
101
|
+
headers={"Idempotence-Key": str(uuid.uuid4())},
|
|
102
|
+
json=json_body,
|
|
103
|
+
)
|
|
104
|
+
json_data = response.json()
|
|
105
|
+
response.raise_for_status()
|
|
106
|
+
return json_data
|
|
107
|
+
|
|
108
|
+
def sync_get_payment(self, *, payment_id: str) -> dict[str, Any] | None:
|
|
109
|
+
raise_for_type(payment_id, str)
|
|
110
|
+
response = self._sync_make_http_request(
|
|
111
|
+
method="GET",
|
|
112
|
+
url=f"https://api.yookassa.ru/v3/payments/{payment_id}",
|
|
113
|
+
headers=self.headers
|
|
114
|
+
)
|
|
115
|
+
json_data = response.json()
|
|
116
|
+
if response.status_code == 404:
|
|
117
|
+
return None
|
|
118
|
+
response.raise_for_status()
|
|
119
|
+
return json_data
|
|
120
|
+
|
|
121
|
+
async def async_create_payment(self, *, json_body: dict[str, Any]) -> dict[str, Any]:
|
|
122
|
+
|
|
123
|
+
"""
|
|
124
|
+
json_body example
|
|
125
|
+
json_body = {
|
|
126
|
+
"amount": {
|
|
127
|
+
"value": "2.0",
|
|
128
|
+
"currency": "RUB"
|
|
129
|
+
},
|
|
130
|
+
"description": "description",
|
|
131
|
+
"confirmation": {
|
|
132
|
+
"type": "redirect",
|
|
133
|
+
"return_url": f"https://t.me/{get_tg_bot_username()}",
|
|
134
|
+
"locale": "ru_RU"
|
|
135
|
+
},
|
|
136
|
+
"capture": True,
|
|
137
|
+
"metadata": {},
|
|
138
|
+
"merchant_customer_id": ""
|
|
139
|
+
}
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
response = await self._async_make_http_request(
|
|
143
|
+
method="POST",
|
|
144
|
+
url="https://api.yookassa.ru/v3/payments",
|
|
145
|
+
headers={"Idempotence-Key": str(uuid.uuid4())},
|
|
146
|
+
json=json_body,
|
|
147
|
+
)
|
|
148
|
+
json_data = await response.json()
|
|
149
|
+
response.raise_for_status()
|
|
150
|
+
return json_data
|
|
151
|
+
|
|
152
|
+
async def async_get_payment(self, *, payment_id: str) -> dict[str, Any] | None:
|
|
153
|
+
raise_for_type(payment_id, str)
|
|
154
|
+
response = await self._async_make_http_request(
|
|
155
|
+
method="GET",
|
|
156
|
+
url=f"https://api.yookassa.ru/v3/payments/{payment_id}",
|
|
157
|
+
)
|
|
158
|
+
json_data = await response.json()
|
|
159
|
+
if response.status == 404:
|
|
160
|
+
return None
|
|
161
|
+
response.raise_for_status()
|
|
162
|
+
return json_data
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def __example():
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def __async_example():
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == '__main__':
|
|
174
|
+
__example()
|
|
175
|
+
asyncio.run(__async_example())
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import os
|
|
3
|
+
from fastapi import APIRouter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def include_fastapi_routers_from_dir(
|
|
7
|
+
*,
|
|
8
|
+
router: APIRouter,
|
|
9
|
+
base_dir: str = ".",
|
|
10
|
+
exclude_filenames: list[str] | None = None,
|
|
11
|
+
):
|
|
12
|
+
"""
|
|
13
|
+
Рекурсивно ищет все .py файлы с объектом `api_router` типа APIRouter
|
|
14
|
+
и подключает их к переданному `router`.
|
|
15
|
+
|
|
16
|
+
Префикс = имя файла без .py
|
|
17
|
+
exclude_filenames — список имён файлов, которые нужно пропустить (без путей)
|
|
18
|
+
"""
|
|
19
|
+
if exclude_filenames is None:
|
|
20
|
+
exclude_filenames = ["__init__.py"]
|
|
21
|
+
|
|
22
|
+
for root, _, files in os.walk(base_dir):
|
|
23
|
+
files.sort()
|
|
24
|
+
|
|
25
|
+
for filename in files:
|
|
26
|
+
if not filename.endswith(".py") or filename in exclude_filenames:
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
file_path = os.path.join(root, filename)
|
|
30
|
+
module_name = (
|
|
31
|
+
os.path.relpath(file_path, base_dir)
|
|
32
|
+
.replace(os.sep, ".")
|
|
33
|
+
.removesuffix(".py")
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
|
37
|
+
module = importlib.util.module_from_spec(spec)
|
|
38
|
+
spec.loader.exec_module(module)
|
|
39
|
+
|
|
40
|
+
api_router = getattr(module, "api_router", None)
|
|
41
|
+
if isinstance(api_router, APIRouter):
|
|
42
|
+
prefix = "/" + filename[:-3] # имя файла без .py
|
|
43
|
+
router.include_router(
|
|
44
|
+
router=api_router,
|
|
45
|
+
prefix=prefix
|
|
46
|
+
)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arpakitlib
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.322
|
|
4
4
|
Summary: arpakitlib
|
|
5
|
-
License: Apache-2.0
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Keywords: arpakitlib,arpakit,arpakit-company,arpakitcompany,arpakit_company
|
|
8
8
|
Author: arpakit_company
|
|
@@ -375,12 +375,9 @@ arpakitlib/_arpakit_project_template_v_5/project/util/arpakitlib_project_templat
|
|
|
375
375
|
arpakitlib/_arpakit_project_template_v_5/project/util/etc.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
376
376
|
arpakitlib/_arpakit_project_template_v_5/project/util/send_email_.py,sha256=AZ2PHRQh-l7H3WLxgxygEKD8c_WPguIq4weP2zqfF9I,4748
|
|
377
377
|
arpakitlib/_arpakit_project_template_v_5/todo.txt,sha256=q132Jbx229ThY77S3YiN-Cj5AVm7k1VlJcMYIbZUHUY,3
|
|
378
|
-
arpakitlib/ar_additional_model_util.py,sha256=GFg-glLCxH9X95R2bhTJsscVwv37FgE1qbaAAyXrnIE,917
|
|
379
378
|
arpakitlib/ar_aiogram_as_tg_command_util.py,sha256=4bizX5Xg-E2-r2TXXGQGanJozsIWPVf5luO3vKUN8p8,8471
|
|
380
379
|
arpakitlib/ar_arpakit_lib_module_util.py,sha256=g9uWwTK2eEzmErqwYeVgXDYVMREN8m5CdmgEumAEQfw,5919
|
|
381
380
|
arpakitlib/ar_arpakit_project_template_util.py,sha256=5-o6eTmh-2DqsIqmo63SXw7ttExhmuAehMNc4s0GdtU,3278
|
|
382
|
-
arpakitlib/ar_arpakit_schedule_uust_api_client_util.py,sha256=MRPaF31CRhYA45ldPnoRpaTPMazCJq0jbwDxW5rvMww,12937
|
|
383
|
-
arpakitlib/ar_arpakit_schedule_uust_site_util.py,sha256=8wLct9Gd4MWkXzB6nSmETAwTPLw8lfpWgx0LoWSAOvg,1643
|
|
384
381
|
arpakitlib/ar_arpakitlib_cli_util.py,sha256=RJGcfEZ_q74FJ4tqdXvt7xQpShTszOvKu1mbp3D8qzw,2599
|
|
385
382
|
arpakitlib/ar_base64_util.py,sha256=udSSpeXMZx0JgQknl4hQgZ8kr1Ps_aQOloIXu4T9dMQ,1286
|
|
386
383
|
arpakitlib/ar_base_worker_util.py,sha256=4XN29vkju1OBmvZj1MVjxGXVWQzcs9Sdd90JBj1QoFg,6336
|
|
@@ -391,13 +388,13 @@ arpakitlib/ar_clone_pydantic_model_fields.py,sha256=5i77NGEjnY2ppk_Ot179egQGNDvg
|
|
|
391
388
|
arpakitlib/ar_datetime_util.py,sha256=3Pw8ljsBKkkEUCmu1PsnJqNeG5bVLqtpsUGaztJF8jQ,1432
|
|
392
389
|
arpakitlib/ar_dict_util.py,sha256=oet-9AJEjQZfG_EI82BuYW0jdW2NQxKjPXol_nfTXjw,447
|
|
393
390
|
arpakitlib/ar_encrypt_decrypt_util.py,sha256=GhWnp7HHkbhwFVVCzO1H07m-5gryr4yjWsXjOaNQm1Y,520
|
|
394
|
-
arpakitlib/ar_ensure_sqlalchemy_check_constraints.py,sha256=
|
|
391
|
+
arpakitlib/ar_ensure_sqlalchemy_check_constraints.py,sha256=gqZTPSCAPUMRiXcmv9xls5S8YkUAg-gwFIEvqQsJ_JM,5437
|
|
395
392
|
arpakitlib/ar_enumeration_util.py,sha256=XoFInWtGzbIr6fJq0un5nettaDfOLAyY84ovwj7n_7g,3030
|
|
396
393
|
arpakitlib/ar_exception_util.py,sha256=3hZKsj34TZVdmd4JAQz7w515smWqB8o3gTwAEjuMdnI,408
|
|
397
394
|
arpakitlib/ar_file_storage_in_dir_util.py,sha256=Zh922S6-aIy0p_Fen8GTTrGpixpPQ6c-wFLukiSK4Ck,4091
|
|
398
395
|
arpakitlib/ar_file_util.py,sha256=GUdJYm1tUZnYpY-SIPRHAZBHGra8NKy1eYEI0D5AfhY,489
|
|
399
396
|
arpakitlib/ar_func_util.py,sha256=lG0bx_DtxWN4skbUim0liRZ6WUyLVV8Qfk6iZNtCZOs,1042
|
|
400
|
-
arpakitlib/
|
|
397
|
+
arpakitlib/ar_generate_connection_url_util.py,sha256=sU55IA0v3TZ5PzJgc01Q9cABIzF9BrgSXu4USxOIlVY,2665
|
|
401
398
|
arpakitlib/ar_generate_simple_code.py,sha256=EkrebrTi7sArSRAuxvN5BPm_A0-dFSCZgdoJhx5kPhk,344
|
|
402
399
|
arpakitlib/ar_hash_util.py,sha256=Iqy6KBAOLBQMFLWv676boI5sV7atT2B-fb7aCdHOmIQ,340
|
|
403
400
|
arpakitlib/ar_http_request_util.py,sha256=PCUtGOQIvNScrLqD_9Z8LqT-7a-lP2y-Y-CH5vGdn7Q,7663
|
|
@@ -409,7 +406,7 @@ arpakitlib/ar_list_of_dicts_to_xlsx.py,sha256=MyjEl4Jl4beLVZqLVQMMv0-XDtBD3Xh4Z_
|
|
|
409
406
|
arpakitlib/ar_list_util.py,sha256=xaUk2BnLvDMP5HDh_GFfH-nIXCg-f8NsrrUKXRcVUsU,1788
|
|
410
407
|
arpakitlib/ar_log_async_func_if_error.py,sha256=qtr_eK9o1BrcA_7S9Ns7nVmOS81Yv6eMXHdasc4MftQ,495
|
|
411
408
|
arpakitlib/ar_logging_util.py,sha256=Q9R4-Cx3TAn3VLcKyNFeKS3luYBouj-umR6_TSKmVYw,1336
|
|
412
|
-
arpakitlib/ar_mongodb_util.py,sha256=
|
|
409
|
+
arpakitlib/ar_mongodb_util.py,sha256=orRb81o9CzD9kkWbbNQCbSMTl2xLcctUDdx_FxirKww,3411
|
|
413
410
|
arpakitlib/ar_need_type_util.py,sha256=XmY1kswz8j9oo5f9CxRu0_zgfvxWrXPYKOj6MM04sGk,2604
|
|
414
411
|
arpakitlib/ar_parse_command.py,sha256=1WTdQoWVshoDZ1jDaKeTzajfqaYHP3FNO0-REyo1aMY,3003
|
|
415
412
|
arpakitlib/ar_postgresql_util.py,sha256=1AuLjEaa1Lg4pzn-ukCVnDi35Eg1k91APRTqZhIJAdo,945
|
|
@@ -421,17 +418,18 @@ arpakitlib/ar_really_validate_url.py,sha256=aaSPVMbz2DSqlC2yk2g44-kTIiHlITfJwIG9
|
|
|
421
418
|
arpakitlib/ar_retry_func_util.py,sha256=LB4FJRsu2cssnPw6X8bCEcaGpQsXhkLkgeU37w1t9fU,2250
|
|
422
419
|
arpakitlib/ar_run_cmd_util.py,sha256=D_rPavKMmWkQtwvZFz-Io5Ak8eSODHkcFeLPzNVC68g,1072
|
|
423
420
|
arpakitlib/ar_safe_func.py,sha256=JQNaM3q4Z6nUE8bDfzXNBGkWSXm0PZ-GMMvu6UWUIYk,2400
|
|
424
|
-
arpakitlib/
|
|
425
|
-
arpakitlib/ar_settings_util.py,sha256=Y5wi_cmsjDjfJpM0VJHjbo0NoVPKfypKaD1USowwDtQ,1327
|
|
421
|
+
arpakitlib/ar_settings_util.py,sha256=FeQQkuVrLVYkFAIg3Wy6ysyTt_sqLTX0REAe60gbM3k,2361
|
|
426
422
|
arpakitlib/ar_sleep_util.py,sha256=ggaj7ML6QK_ADsHMcyu6GUmUpQ_9B9n-SKYH17h-9lM,1045
|
|
427
423
|
arpakitlib/ar_sqladmin_util.py,sha256=SEoaowAPF3lhxPsNjwmOymNJ55Ty9rmzvsDm7gD5Ceo,861
|
|
428
|
-
arpakitlib/ar_sqlalchemy_drop_check_constraints.py,sha256=
|
|
429
|
-
arpakitlib/ar_sqlalchemy_util.py,sha256=
|
|
424
|
+
arpakitlib/ar_sqlalchemy_drop_check_constraints.py,sha256=uVktYLjNHrMPWQAq8eBpapShPKbLb3LrRBnnss3gaYY,3624
|
|
425
|
+
arpakitlib/ar_sqlalchemy_util.py,sha256=_7sGYLgKAq_YYVuphyc9yXlaGbV-FTmLI2uEnNwBmjE,16040
|
|
430
426
|
arpakitlib/ar_str_util.py,sha256=2lGpnXDf2h1cBZpVf5i1tX_HCv5iBd6IGnrCw4QWWlY,4350
|
|
431
427
|
arpakitlib/ar_type_util.py,sha256=Cs_tef-Fc5xeyAF54KgISCsP11NHyzIsglm4S3Xx7iM,4049
|
|
432
|
-
arpakitlib/ar_uppercase_env_keys.py,sha256=
|
|
433
|
-
arpakitlib
|
|
434
|
-
arpakitlib
|
|
435
|
-
arpakitlib-1.8.
|
|
436
|
-
arpakitlib-1.8.
|
|
437
|
-
arpakitlib-1.8.
|
|
428
|
+
arpakitlib/ar_uppercase_env_keys.py,sha256=BsUCJhfchBIav0AE54_tVgYcE4p1JYoWdPGCHWZnROA,2790
|
|
429
|
+
arpakitlib/ar_yookassa_api_client_util.py,sha256=Mst07IblAJmU98HfOfJqT_RRfnuGrIB1UOQunGGWO8I,5264
|
|
430
|
+
arpakitlib/include_fastapi_routers_from_dir.py,sha256=Umg16sPQC6R-T8FD7mlmqP1TbgH-_v2eDasrZJzImQM,1606
|
|
431
|
+
arpakitlib-1.8.322.dist-info/METADATA,sha256=7I4FON25AN9tOuK2iozXNi4M_4F3t1FJkbpfWCFrO08,3804
|
|
432
|
+
arpakitlib-1.8.322.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
433
|
+
arpakitlib-1.8.322.dist-info/entry_points.txt,sha256=36xqR3PJFT2kuwjkM_EqoIy0qFUDPKSm_mJaI7emewE,87
|
|
434
|
+
arpakitlib-1.8.322.dist-info/licenses/LICENSE,sha256=GPEDQMam2r7FSTYqM1mm7aKnxLaWcBotH7UvQtea-ec,11355
|
|
435
|
+
arpakitlib-1.8.322.dist-info/RECORD,,
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# arpakit
|
|
2
|
-
from typing import Any
|
|
3
|
-
|
|
4
|
-
from pydantic import BaseModel, ConfigDict
|
|
5
|
-
|
|
6
|
-
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class BaseAM(BaseModel):
|
|
10
|
-
model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True, from_attributes=True)
|
|
11
|
-
_bus_data: dict[str, Any] | None = None
|
|
12
|
-
|
|
13
|
-
@property
|
|
14
|
-
def bus_data(self) -> dict[str, Any]:
|
|
15
|
-
if self._bus_data is None:
|
|
16
|
-
self._bus_data = {}
|
|
17
|
-
return self._bus_data
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def __example():
|
|
21
|
-
class UserAM(BaseAM):
|
|
22
|
-
id: int
|
|
23
|
-
name: str
|
|
24
|
-
email: str
|
|
25
|
-
|
|
26
|
-
@property
|
|
27
|
-
def bus_data_age(self) -> int | None:
|
|
28
|
-
return self.bus_data.get("age")
|
|
29
|
-
|
|
30
|
-
user = UserAM(id=1, name="John Doe", email="john.doe@example.com")
|
|
31
|
-
print(user.name) # John Doe
|
|
32
|
-
|
|
33
|
-
# bus_data
|
|
34
|
-
user.bus_data["age"] = 22
|
|
35
|
-
print(user.bus_data) # {'age': '22'}
|
|
36
|
-
print(user.bus_data_age) # 22
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if __name__ == '__main__':
|
|
40
|
-
__example()
|