arpakitlib 1.8.307__py3-none-any.whl → 1.8.320__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/include_fastapi_routers_from_dir.py +44 -0
- {arpakitlib-1.8.307.dist-info → arpakitlib-1.8.320.dist-info}/METADATA +2 -2
- {arpakitlib-1.8.307.dist-info → arpakitlib-1.8.320.dist-info}/RECORD +13 -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.320.dist-info}/WHEEL +0 -0
- {arpakitlib-1.8.307.dist-info → arpakitlib-1.8.320.dist-info}/entry_points.txt +0 -0
- {arpakitlib-1.8.307.dist-info → arpakitlib-1.8.320.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,44 @@
|
|
|
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
|
+
for filename in files:
|
|
24
|
+
if not filename.endswith(".py") or filename in exclude_filenames:
|
|
25
|
+
continue
|
|
26
|
+
|
|
27
|
+
file_path = os.path.join(root, filename)
|
|
28
|
+
module_name = (
|
|
29
|
+
os.path.relpath(file_path, base_dir)
|
|
30
|
+
.replace(os.sep, ".")
|
|
31
|
+
.removesuffix(".py")
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
|
35
|
+
module = importlib.util.module_from_spec(spec)
|
|
36
|
+
spec.loader.exec_module(module)
|
|
37
|
+
|
|
38
|
+
api_router = getattr(module, "api_router", None)
|
|
39
|
+
if isinstance(api_router, APIRouter):
|
|
40
|
+
prefix = "/" + filename[:-3] # имя файла без .py
|
|
41
|
+
router.include_router(
|
|
42
|
+
router=api_router,
|
|
43
|
+
prefix=prefix
|
|
44
|
+
)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arpakitlib
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.320
|
|
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,17 @@ 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-1.8.
|
|
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/include_fastapi_routers_from_dir.py,sha256=2QefsH_P3fHGfv-mAUzq1DDSDpYHWiP3Lco3f9jc2V0,1584
|
|
430
|
+
arpakitlib-1.8.320.dist-info/METADATA,sha256=MRu1SbvEmE9xo48sKTaUB5O8EzOZNk2iHpC_O0xrZZI,3804
|
|
431
|
+
arpakitlib-1.8.320.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
432
|
+
arpakitlib-1.8.320.dist-info/entry_points.txt,sha256=36xqR3PJFT2kuwjkM_EqoIy0qFUDPKSm_mJaI7emewE,87
|
|
433
|
+
arpakitlib-1.8.320.dist-info/licenses/LICENSE,sha256=GPEDQMam2r7FSTYqM1mm7aKnxLaWcBotH7UvQtea-ec,11355
|
|
434
|
+
arpakitlib-1.8.320.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()
|
|
@@ -1,422 +0,0 @@
|
|
|
1
|
-
# arpakit
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
import datetime as dt
|
|
7
|
-
import logging
|
|
8
|
-
from typing import Any
|
|
9
|
-
from urllib.parse import urljoin
|
|
10
|
-
|
|
11
|
-
import cachetools
|
|
12
|
-
from aiohttp import ClientResponse
|
|
13
|
-
from pydantic import ConfigDict, BaseModel
|
|
14
|
-
|
|
15
|
-
from arpakitlib.ar_enumeration_util import Enumeration
|
|
16
|
-
from arpakitlib.ar_http_request_util import async_make_http_request
|
|
17
|
-
|
|
18
|
-
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class Weekdays(Enumeration):
|
|
22
|
-
monday = 1
|
|
23
|
-
tuesday = 2
|
|
24
|
-
wednesday = 3
|
|
25
|
-
thursday = 4
|
|
26
|
-
friday = 5
|
|
27
|
-
saturday = 6
|
|
28
|
-
sunday = 7
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class Months(Enumeration):
|
|
32
|
-
january = 1
|
|
33
|
-
february = 2
|
|
34
|
-
march = 3
|
|
35
|
-
april = 4
|
|
36
|
-
may = 5
|
|
37
|
-
june = 6
|
|
38
|
-
july = 7
|
|
39
|
-
august = 8
|
|
40
|
-
september = 9
|
|
41
|
-
october = 10
|
|
42
|
-
november = 11
|
|
43
|
-
december = 12
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
class BaseAPIModel(BaseModel):
|
|
47
|
-
model_config = ConfigDict(extra="ignore", arbitrary_types_allowed=True, from_attributes=True)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class CurrentSemesterAPIModel(BaseAPIModel):
|
|
51
|
-
id: int
|
|
52
|
-
long_id: str
|
|
53
|
-
creation_dt: dt.datetime
|
|
54
|
-
entity_type: str
|
|
55
|
-
actualization_dt: dt.datetime
|
|
56
|
-
value: str
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class CurrentWeekAPIModel(BaseAPIModel):
|
|
60
|
-
id: int
|
|
61
|
-
long_id: str
|
|
62
|
-
creation_dt: dt.datetime
|
|
63
|
-
entity_type: str
|
|
64
|
-
actualization_dt: dt.datetime
|
|
65
|
-
value: int
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
class GroupAPIModel(BaseAPIModel):
|
|
69
|
-
id: int
|
|
70
|
-
long_id: str
|
|
71
|
-
creation_dt: dt.datetime
|
|
72
|
-
entity_type: str
|
|
73
|
-
actualization_dt: dt.datetime
|
|
74
|
-
uust_api_id: int
|
|
75
|
-
title: str
|
|
76
|
-
faculty: str | None
|
|
77
|
-
course: int | None
|
|
78
|
-
difference_level: int | None = None
|
|
79
|
-
uust_api_data: dict[str, Any]
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
class TeacherAPIModel(BaseAPIModel):
|
|
83
|
-
id: int
|
|
84
|
-
long_id: str
|
|
85
|
-
creation_dt: dt.datetime
|
|
86
|
-
entity_type: str
|
|
87
|
-
actualization_dt: dt.datetime
|
|
88
|
-
uust_api_id: int
|
|
89
|
-
name: str | None
|
|
90
|
-
surname: str | None
|
|
91
|
-
patronymic: str | None
|
|
92
|
-
fullname: str | None
|
|
93
|
-
shortname: str | None
|
|
94
|
-
posts: list[str]
|
|
95
|
-
post: str | None
|
|
96
|
-
units: list[str]
|
|
97
|
-
unit: str | None
|
|
98
|
-
difference_level: int | None
|
|
99
|
-
uust_api_data: dict[str, Any]
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
class GroupLessonAPIModel(BaseAPIModel):
|
|
103
|
-
id: int
|
|
104
|
-
long_id: str
|
|
105
|
-
creation_dt: dt.datetime
|
|
106
|
-
entity_type: str
|
|
107
|
-
actualization_dt: dt.datetime
|
|
108
|
-
uust_api_id: int
|
|
109
|
-
type: str
|
|
110
|
-
title: str
|
|
111
|
-
weeks: list[int]
|
|
112
|
-
weekday: int
|
|
113
|
-
comment: str | None
|
|
114
|
-
time_title: str | None
|
|
115
|
-
time_start: dt.time | None
|
|
116
|
-
time_end: dt.time | None
|
|
117
|
-
numbers: list[int]
|
|
118
|
-
location: str | None
|
|
119
|
-
teacher_uust_api_id: int | None
|
|
120
|
-
group_uust_api_id: int | None
|
|
121
|
-
group: GroupAPIModel
|
|
122
|
-
teacher: TeacherAPIModel | None
|
|
123
|
-
uust_api_data: dict[str, Any]
|
|
124
|
-
|
|
125
|
-
def compare_type(self, *types: str | list[str]) -> bool:
|
|
126
|
-
type_ = self.type.strip().lower()
|
|
127
|
-
for type__ in types:
|
|
128
|
-
if isinstance(type__, str):
|
|
129
|
-
if type_ == type__.strip().lower():
|
|
130
|
-
return True
|
|
131
|
-
elif isinstance(type__, list):
|
|
132
|
-
for type___ in type__:
|
|
133
|
-
if type_ == type___.strip().lower():
|
|
134
|
-
return True
|
|
135
|
-
else:
|
|
136
|
-
raise TypeError()
|
|
137
|
-
return False
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
class TeacherLessonAPIModel(BaseAPIModel):
|
|
141
|
-
id: int
|
|
142
|
-
long_id: str
|
|
143
|
-
creation_dt: dt.datetime
|
|
144
|
-
entity_type: str
|
|
145
|
-
actualization_dt: dt.datetime
|
|
146
|
-
uust_api_id: int
|
|
147
|
-
type: str
|
|
148
|
-
title: str
|
|
149
|
-
weeks: list[int]
|
|
150
|
-
weekday: int
|
|
151
|
-
comment: str | None
|
|
152
|
-
time_title: str | None
|
|
153
|
-
time_start: dt.time | None
|
|
154
|
-
time_end: dt.time | None
|
|
155
|
-
numbers: list[int]
|
|
156
|
-
location: str | None
|
|
157
|
-
group_uust_api_ids: list[int]
|
|
158
|
-
teacher_uust_api_id: int
|
|
159
|
-
teacher: TeacherAPIModel
|
|
160
|
-
groups: list[GroupAPIModel]
|
|
161
|
-
uust_api_data: dict[str, Any]
|
|
162
|
-
|
|
163
|
-
def compare_type(self, *types: str | list[str]) -> bool:
|
|
164
|
-
type_ = self.type.strip().lower()
|
|
165
|
-
for type__ in types:
|
|
166
|
-
if isinstance(type__, str):
|
|
167
|
-
if type_ == type__.strip().lower():
|
|
168
|
-
return True
|
|
169
|
-
elif isinstance(type__, list):
|
|
170
|
-
for type___ in type__:
|
|
171
|
-
if type_ == type___.strip().lower():
|
|
172
|
-
return True
|
|
173
|
-
else:
|
|
174
|
-
raise TypeError()
|
|
175
|
-
return False
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
class WeatherInUfaAPIModel(BaseAPIModel):
|
|
179
|
-
temperature: float
|
|
180
|
-
temperature_feels_like: float
|
|
181
|
-
description: str
|
|
182
|
-
wind_speed: float
|
|
183
|
-
sunrise_dt: dt.datetime
|
|
184
|
-
sunset_dt: dt.datetime
|
|
185
|
-
has_rain: bool
|
|
186
|
-
has_snow: bool
|
|
187
|
-
data: dict
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
class DatetimeAPIModel(BaseAPIModel):
|
|
191
|
-
date: dt.date
|
|
192
|
-
datetime: dt.datetime | None = None
|
|
193
|
-
year: int
|
|
194
|
-
month: int
|
|
195
|
-
day: int
|
|
196
|
-
hour: int | None = None
|
|
197
|
-
minute: int | None = None
|
|
198
|
-
second: int | None = None
|
|
199
|
-
microsecond: int | None = None
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
class ARPAKITScheduleUUSTAPIClient:
|
|
203
|
-
def __init__(
|
|
204
|
-
self,
|
|
205
|
-
*,
|
|
206
|
-
base_url: str = "https://api.schedule-uust.arpakit.com/api/v1",
|
|
207
|
-
api_key: str | None = "viewer",
|
|
208
|
-
use_cache: bool = False,
|
|
209
|
-
cache_ttl: dt.timedelta | None = dt.timedelta(minutes=10)
|
|
210
|
-
):
|
|
211
|
-
self._logger = logging.getLogger(__name__)
|
|
212
|
-
self.api_key = api_key
|
|
213
|
-
base_url = base_url.strip()
|
|
214
|
-
if not base_url.endswith("/"):
|
|
215
|
-
base_url += "/"
|
|
216
|
-
self.base_url = base_url
|
|
217
|
-
self.headers = {"Content-Type": "application/json"}
|
|
218
|
-
if api_key is not None:
|
|
219
|
-
self.headers.update({"apikey": self.api_key})
|
|
220
|
-
self.use_cache = use_cache
|
|
221
|
-
self.cache_ttl = cache_ttl
|
|
222
|
-
if cache_ttl is not None:
|
|
223
|
-
self.ttl_cache = cachetools.TTLCache(maxsize=100, ttl=cache_ttl.total_seconds())
|
|
224
|
-
else:
|
|
225
|
-
self.ttl_cache = None
|
|
226
|
-
|
|
227
|
-
async def _async_make_http_request(
|
|
228
|
-
self,
|
|
229
|
-
*,
|
|
230
|
-
method: str = "GET",
|
|
231
|
-
url: str,
|
|
232
|
-
params: dict[str, Any] | None = None,
|
|
233
|
-
**kwargs
|
|
234
|
-
) -> ClientResponse:
|
|
235
|
-
response = await async_make_http_request(
|
|
236
|
-
method=method,
|
|
237
|
-
url=url,
|
|
238
|
-
headers=self.headers,
|
|
239
|
-
params=params,
|
|
240
|
-
max_tries_=5,
|
|
241
|
-
raise_for_status_=True,
|
|
242
|
-
**kwargs
|
|
243
|
-
)
|
|
244
|
-
return response
|
|
245
|
-
|
|
246
|
-
def clear_cache(self):
|
|
247
|
-
if self.ttl_cache is not None:
|
|
248
|
-
self.ttl_cache.clear()
|
|
249
|
-
|
|
250
|
-
async def check_auth(self) -> dict[str, Any]:
|
|
251
|
-
response = await self._async_make_http_request(
|
|
252
|
-
method="GET", url=urljoin(self.base_url, "check_auth")
|
|
253
|
-
)
|
|
254
|
-
json_data = await response.json()
|
|
255
|
-
return json_data
|
|
256
|
-
|
|
257
|
-
async def get_current_week(self) -> CurrentWeekAPIModel | None:
|
|
258
|
-
response = await self._async_make_http_request(
|
|
259
|
-
method="GET", url=urljoin(self.base_url, "get_current_week")
|
|
260
|
-
)
|
|
261
|
-
json_data = await response.json()
|
|
262
|
-
if json_data is None:
|
|
263
|
-
return None
|
|
264
|
-
return CurrentWeekAPIModel.model_validate(json_data)
|
|
265
|
-
|
|
266
|
-
async def get_current_semester(self) -> CurrentSemesterAPIModel | None:
|
|
267
|
-
response = await self._async_make_http_request(
|
|
268
|
-
method="GET", url=urljoin(self.base_url, "get_current_semester")
|
|
269
|
-
)
|
|
270
|
-
json_data = await response.json()
|
|
271
|
-
if json_data is None:
|
|
272
|
-
return None
|
|
273
|
-
return CurrentSemesterAPIModel.model_validate(json_data)
|
|
274
|
-
|
|
275
|
-
async def get_weather_in_ufa(self) -> WeatherInUfaAPIModel:
|
|
276
|
-
response = await self._async_make_http_request(
|
|
277
|
-
method="GET", url=urljoin(self.base_url, "get_weather_in_ufa")
|
|
278
|
-
)
|
|
279
|
-
json_data = await response.json()
|
|
280
|
-
return WeatherInUfaAPIModel.model_validate(json_data)
|
|
281
|
-
|
|
282
|
-
async def get_now_datetime_in_ufa(self) -> DatetimeAPIModel:
|
|
283
|
-
response = await self._async_make_http_request(
|
|
284
|
-
method="GET", url=urljoin(self.base_url, "get_now_datetime_in_ufa")
|
|
285
|
-
)
|
|
286
|
-
json_data = await response.json()
|
|
287
|
-
return DatetimeAPIModel.model_validate(json_data)
|
|
288
|
-
|
|
289
|
-
async def get_log_file_content(self) -> str | None:
|
|
290
|
-
response = await self._async_make_http_request(
|
|
291
|
-
method="GET", url=urljoin(self.base_url, "get_log_file")
|
|
292
|
-
)
|
|
293
|
-
text_data = await response.text()
|
|
294
|
-
return text_data
|
|
295
|
-
|
|
296
|
-
async def get_groups(self) -> list[GroupAPIModel]:
|
|
297
|
-
response = await self._async_make_http_request(
|
|
298
|
-
method="GET", url=urljoin(self.base_url, "get_groups")
|
|
299
|
-
)
|
|
300
|
-
json_data = await response.json()
|
|
301
|
-
return [GroupAPIModel.model_validate(d) for d in json_data]
|
|
302
|
-
|
|
303
|
-
async def get_group(
|
|
304
|
-
self, *, filter_id: int | None = None, filter_uust_api_id: int | None = None
|
|
305
|
-
) -> GroupAPIModel | None:
|
|
306
|
-
params = {}
|
|
307
|
-
if filter_id is not None:
|
|
308
|
-
params["filter_id"] = filter_id
|
|
309
|
-
if filter_uust_api_id is not None:
|
|
310
|
-
params["filter_uust_api_id"] = filter_uust_api_id
|
|
311
|
-
response = await self._async_make_http_request(
|
|
312
|
-
method="GET", url=urljoin(self.base_url, "get_group"), params=params)
|
|
313
|
-
json_data = await response.json()
|
|
314
|
-
if json_data is None:
|
|
315
|
-
return None
|
|
316
|
-
return GroupAPIModel.model_validate(json_data)
|
|
317
|
-
|
|
318
|
-
async def find_groups(
|
|
319
|
-
self, *, q: str
|
|
320
|
-
) -> list[GroupAPIModel]:
|
|
321
|
-
response = await self._async_make_http_request(
|
|
322
|
-
method="GET", url=urljoin(self.base_url, "find_groups"), params={"q": q.strip()}
|
|
323
|
-
)
|
|
324
|
-
json_data = await response.json()
|
|
325
|
-
return [GroupAPIModel.model_validate(d) for d in json_data]
|
|
326
|
-
|
|
327
|
-
async def get_teachers(self) -> list[TeacherAPIModel]:
|
|
328
|
-
response = await self._async_make_http_request(
|
|
329
|
-
method="GET", url=urljoin(self.base_url, "get_teachers")
|
|
330
|
-
)
|
|
331
|
-
json_data = await response.json()
|
|
332
|
-
return [TeacherAPIModel.model_validate(d) for d in json_data]
|
|
333
|
-
|
|
334
|
-
async def get_teacher(
|
|
335
|
-
self, *, filter_id: int | None = None, filter_uust_api_id: int | None = None
|
|
336
|
-
) -> TeacherAPIModel | None:
|
|
337
|
-
params = {}
|
|
338
|
-
if filter_id is not None:
|
|
339
|
-
params["filter_id"] = filter_id
|
|
340
|
-
if filter_uust_api_id is not None:
|
|
341
|
-
params["filter_uust_api_id"] = filter_uust_api_id
|
|
342
|
-
response = await self._async_make_http_request(
|
|
343
|
-
method="GET", url=urljoin(self.base_url, "get_teacher"), params=params
|
|
344
|
-
)
|
|
345
|
-
json_data = await response.json()
|
|
346
|
-
if json_data is None:
|
|
347
|
-
return None
|
|
348
|
-
return TeacherAPIModel.model_validate(json_data)
|
|
349
|
-
|
|
350
|
-
async def find_teachers(
|
|
351
|
-
self, *, q: str
|
|
352
|
-
) -> list[TeacherAPIModel]:
|
|
353
|
-
response = await self._async_make_http_request(
|
|
354
|
-
method="GET", url=urljoin(self.base_url, "find_teachers"), params={"q": q.strip()}
|
|
355
|
-
)
|
|
356
|
-
json_data = await response.json()
|
|
357
|
-
return [TeacherAPIModel.model_validate(d) for d in json_data]
|
|
358
|
-
|
|
359
|
-
async def find_any(
|
|
360
|
-
self, *, q: str
|
|
361
|
-
) -> list[TeacherAPIModel | GroupLessonAPIModel]:
|
|
362
|
-
response = await self._async_make_http_request(
|
|
363
|
-
method="GET", url=urljoin(self.base_url, "find_any"), params={"q": q.strip()}
|
|
364
|
-
)
|
|
365
|
-
json_data = await response.json()
|
|
366
|
-
|
|
367
|
-
results = []
|
|
368
|
-
for d in json_data:
|
|
369
|
-
if d["entity_type"] == "group":
|
|
370
|
-
results.append(GroupAPIModel.model_validate(d))
|
|
371
|
-
elif d["entity_type"] == "teacher":
|
|
372
|
-
results.append(TeacherAPIModel.model_validate(d))
|
|
373
|
-
else:
|
|
374
|
-
pass
|
|
375
|
-
return results
|
|
376
|
-
|
|
377
|
-
async def get_group_lessons(
|
|
378
|
-
self,
|
|
379
|
-
*,
|
|
380
|
-
filter_group_id: int | None = None,
|
|
381
|
-
filter_group_uust_api_id: int | None = None
|
|
382
|
-
) -> list[GroupLessonAPIModel]:
|
|
383
|
-
params = {}
|
|
384
|
-
if filter_group_id is not None:
|
|
385
|
-
params["filter_group_id"] = filter_group_id
|
|
386
|
-
if filter_group_uust_api_id is not None:
|
|
387
|
-
params["filter_group_uust_api_id"] = filter_group_uust_api_id
|
|
388
|
-
response = await self._async_make_http_request(
|
|
389
|
-
method="GET", url=urljoin(self.base_url, "get_group_lessons"), params=params
|
|
390
|
-
)
|
|
391
|
-
json_data = await response.json()
|
|
392
|
-
return [GroupLessonAPIModel.model_validate(d) for d in json_data]
|
|
393
|
-
|
|
394
|
-
async def get_teacher_lessons(
|
|
395
|
-
self,
|
|
396
|
-
*,
|
|
397
|
-
filter_teacher_id: int | None = None,
|
|
398
|
-
filter_teacher_uust_api_id: int | None = None
|
|
399
|
-
) -> list[TeacherLessonAPIModel]:
|
|
400
|
-
params = {}
|
|
401
|
-
if filter_teacher_id is not None:
|
|
402
|
-
params["filter_teacher_id"] = filter_teacher_id
|
|
403
|
-
if filter_teacher_uust_api_id is not None:
|
|
404
|
-
params["filter_teacher_uust_api_id"] = filter_teacher_uust_api_id
|
|
405
|
-
response = await self._async_make_http_request(
|
|
406
|
-
method="GET", url=urljoin(self.base_url, "get_teacher_lessons"), params=params
|
|
407
|
-
)
|
|
408
|
-
json_data = await response.json()
|
|
409
|
-
return [TeacherLessonAPIModel.model_validate(d) for d in json_data]
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
def __example():
|
|
413
|
-
pass
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
async def __async_example():
|
|
417
|
-
pass
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if __name__ == '__main__':
|
|
421
|
-
__example()
|
|
422
|
-
asyncio.run(__async_example())
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
# arpakit
|
|
2
|
-
|
|
3
|
-
from urllib.parse import urlencode, urljoin
|
|
4
|
-
|
|
5
|
-
from arpakitlib.ar_type_util import raise_for_type
|
|
6
|
-
|
|
7
|
-
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def generate_arpakit_schedule_uust_site_url(
|
|
11
|
-
*,
|
|
12
|
-
base_url: str = "https://schedule-uust.arpakit.com",
|
|
13
|
-
entity_type: str | None = None,
|
|
14
|
-
uust_api_id: int | None = None,
|
|
15
|
-
session: bool | None = None,
|
|
16
|
-
week: int | None = None,
|
|
17
|
-
theme: str | None = None
|
|
18
|
-
) -> str:
|
|
19
|
-
raise_for_type(base_url, str)
|
|
20
|
-
|
|
21
|
-
params = {}
|
|
22
|
-
|
|
23
|
-
if entity_type is not None: # group/teacher
|
|
24
|
-
raise_for_type(entity_type, str)
|
|
25
|
-
params["entity_type"] = entity_type
|
|
26
|
-
|
|
27
|
-
if uust_api_id is not None: # uust_api_id of group/teacher
|
|
28
|
-
raise_for_type(uust_api_id, int)
|
|
29
|
-
params["uust_api_id"] = uust_api_id
|
|
30
|
-
|
|
31
|
-
if session is not None: # true/false
|
|
32
|
-
raise_for_type(session, bool)
|
|
33
|
-
if session:
|
|
34
|
-
params["session"] = "true"
|
|
35
|
-
else:
|
|
36
|
-
params["session"] = "false"
|
|
37
|
-
|
|
38
|
-
if week is not None:
|
|
39
|
-
raise_for_type(week, int)
|
|
40
|
-
params["week"] = week
|
|
41
|
-
|
|
42
|
-
if theme is not None: # dark/light
|
|
43
|
-
raise_for_type(theme, str)
|
|
44
|
-
params["theme"] = theme
|
|
45
|
-
|
|
46
|
-
if params:
|
|
47
|
-
res = urljoin(base_url, f"schedule?{urlencode(params)}")
|
|
48
|
-
else:
|
|
49
|
-
res = base_url
|
|
50
|
-
|
|
51
|
-
return res
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def __example():
|
|
55
|
-
base_url = "https://schedule-uust.arpakit.com"
|
|
56
|
-
url = generate_arpakit_schedule_uust_site_url(
|
|
57
|
-
base_url=base_url,
|
|
58
|
-
entity_type="group",
|
|
59
|
-
uust_api_id=6662,
|
|
60
|
-
session=True,
|
|
61
|
-
week=23,
|
|
62
|
-
theme="dark"
|
|
63
|
-
)
|
|
64
|
-
print(url)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if __name__ == '__main__':
|
|
68
|
-
__example()
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
from urllib.parse import quote_plus
|
|
2
|
-
|
|
3
|
-
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def generate_celery_url(
|
|
7
|
-
*,
|
|
8
|
-
scheme: str = "redis", # или amqp, sqs, etc.
|
|
9
|
-
user: str | None = None,
|
|
10
|
-
password: str | None = None,
|
|
11
|
-
host: str = "127.0.0.1",
|
|
12
|
-
port: int | None = 6379,
|
|
13
|
-
database: str | int | None = 0, # для Redis — номер БД; для AMQP — vhost
|
|
14
|
-
**query_params
|
|
15
|
-
) -> str:
|
|
16
|
-
"""
|
|
17
|
-
Генерирует Celery broker/backend URL.
|
|
18
|
-
|
|
19
|
-
Примеры:
|
|
20
|
-
redis://:mypassword@redis:6379/0
|
|
21
|
-
amqp://user:pass@rabbit:5672/myvhost
|
|
22
|
-
redis://localhost:6379/1?ssl_cert_reqs=none
|
|
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
|
-
# Redis-style — пароль без юзера
|
|
30
|
-
auth_part = f":{quote_plus(password)}@"
|
|
31
|
-
elif user:
|
|
32
|
-
auth_part = f"{quote_plus(user)}@"
|
|
33
|
-
|
|
34
|
-
# Формируем хост и порт
|
|
35
|
-
host_part = host
|
|
36
|
-
if port:
|
|
37
|
-
host_part += f":{port}"
|
|
38
|
-
|
|
39
|
-
# Формируем "базу" (для Redis — номер, для AMQP — vhost)
|
|
40
|
-
db_part = ""
|
|
41
|
-
if database is not None:
|
|
42
|
-
db_part = f"/{quote_plus(str(database))}"
|
|
43
|
-
|
|
44
|
-
# Формируем query параметры
|
|
45
|
-
query_part = ""
|
|
46
|
-
if query_params:
|
|
47
|
-
query_items = [f"{key}={quote_plus(str(value))}" for key, value in query_params.items()]
|
|
48
|
-
query_part = f"?{'&'.join(query_items)}"
|
|
49
|
-
|
|
50
|
-
return f"{scheme}://{auth_part}{host_part}{db_part}{query_part}"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def __example():
|
|
54
|
-
print(generate_celery_url())
|
|
55
|
-
# → redis://127.0.0.1:6379/0
|
|
56
|
-
|
|
57
|
-
# Redis с паролем
|
|
58
|
-
print(generate_celery_url(password="supersecret", host="redis"))
|
|
59
|
-
# → redis://:supersecret@redis:6379/0
|
|
60
|
-
|
|
61
|
-
# RabbitMQ (AMQP)
|
|
62
|
-
print(generate_celery_url(scheme="amqp", user="guest", password="guest", host="rabbitmq"))
|
|
63
|
-
# → amqp://guest:guest@rabbitmq:6379/0
|
|
64
|
-
|
|
65
|
-
# Redis с параметрами
|
|
66
|
-
print(generate_celery_url(password="pass", ssl_cert_reqs="none", socket_timeout=10))
|
|
67
|
-
# → redis://:pass@127.0.0.1:6379/0?ssl_cert_reqs=none&socket_timeout=10
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if __name__ == '__main__':
|
|
71
|
-
__example()
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
# arpakit
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import hashlib
|
|
5
|
-
import logging
|
|
6
|
-
from datetime import datetime, timedelta
|
|
7
|
-
from typing import Any
|
|
8
|
-
|
|
9
|
-
import pytz
|
|
10
|
-
from aiohttp import ClientResponse
|
|
11
|
-
|
|
12
|
-
from arpakitlib.ar_dict_util import combine_dicts
|
|
13
|
-
from arpakitlib.ar_http_request_util import async_make_http_request
|
|
14
|
-
|
|
15
|
-
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class ScheduleUUSTAPIClient:
|
|
19
|
-
def __init__(
|
|
20
|
-
self,
|
|
21
|
-
*,
|
|
22
|
-
api_login: str,
|
|
23
|
-
api_password: str | None = None,
|
|
24
|
-
api_password_first_part: str | None = None,
|
|
25
|
-
api_url: str = "https://isu.uust.ru/api/schedule_v2",
|
|
26
|
-
api_proxy_url: str | None = None
|
|
27
|
-
):
|
|
28
|
-
self._logger = logging.getLogger(self.__class__.__name__)
|
|
29
|
-
self.api_login = api_login
|
|
30
|
-
self.api_password = api_password
|
|
31
|
-
self.api_password_first_part = api_password_first_part
|
|
32
|
-
self.api_url = api_url
|
|
33
|
-
self.api_proxy_url = api_proxy_url
|
|
34
|
-
self.headers = {
|
|
35
|
-
"Accept": (
|
|
36
|
-
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;"
|
|
37
|
-
"q=0.8,application/signed-exchange;v=b3;q=0.7"
|
|
38
|
-
),
|
|
39
|
-
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
40
|
-
"Accept-Language": "en-US,en;q=0.9,ru-RU;q=0.8,ru;q=0.7",
|
|
41
|
-
"User-Agent": (
|
|
42
|
-
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"
|
|
43
|
-
)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async def _async_make_http_request(
|
|
47
|
-
self,
|
|
48
|
-
*,
|
|
49
|
-
method: str = "GET",
|
|
50
|
-
url: str,
|
|
51
|
-
params: dict[str, Any] | None = None,
|
|
52
|
-
**kwargs
|
|
53
|
-
) -> ClientResponse:
|
|
54
|
-
response = await async_make_http_request(
|
|
55
|
-
method=method,
|
|
56
|
-
url=url,
|
|
57
|
-
headers=self.headers,
|
|
58
|
-
params=combine_dicts(params, self.auth_params()),
|
|
59
|
-
max_tries_=9,
|
|
60
|
-
proxy_url_=self.api_proxy_url,
|
|
61
|
-
raise_for_status_=True,
|
|
62
|
-
timeout_=timedelta(seconds=15),
|
|
63
|
-
**kwargs
|
|
64
|
-
)
|
|
65
|
-
json_data = await response.json()
|
|
66
|
-
if "error" in json_data.keys():
|
|
67
|
-
raise Exception(f"error in json_data, {json_data}")
|
|
68
|
-
return response
|
|
69
|
-
|
|
70
|
-
def auth_params(self) -> dict[str, Any]:
|
|
71
|
-
if self.api_password:
|
|
72
|
-
return {
|
|
73
|
-
"login": self.api_login,
|
|
74
|
-
"pass": self.api_password
|
|
75
|
-
}
|
|
76
|
-
elif self.api_password_first_part:
|
|
77
|
-
return {
|
|
78
|
-
"login": self.api_login,
|
|
79
|
-
"pass": self.generate_v2_token()
|
|
80
|
-
}
|
|
81
|
-
else:
|
|
82
|
-
return {}
|
|
83
|
-
|
|
84
|
-
@classmethod
|
|
85
|
-
def hash_new_token(cls, token: str) -> str:
|
|
86
|
-
sha256 = hashlib.sha256()
|
|
87
|
-
sha256.update(token.encode('utf-8'))
|
|
88
|
-
return sha256.hexdigest()
|
|
89
|
-
|
|
90
|
-
@classmethod
|
|
91
|
-
def generate_new_v2_token(cls, password_first_part: str) -> str:
|
|
92
|
-
return cls.hash_new_token(
|
|
93
|
-
password_first_part + datetime.now(tz=pytz.timezone("Asia/Yekaterinburg")).strftime("%Y-%m-%d")
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
def generate_v2_token(self) -> str:
|
|
97
|
-
return self.generate_new_v2_token(password_first_part=self.api_password_first_part)
|
|
98
|
-
|
|
99
|
-
async def get_current_week(self) -> int:
|
|
100
|
-
"""
|
|
101
|
-
response.json example
|
|
102
|
-
{
|
|
103
|
-
'data': [15]
|
|
104
|
-
}
|
|
105
|
-
"""
|
|
106
|
-
|
|
107
|
-
response = await self._async_make_http_request(
|
|
108
|
-
url=self.api_url,
|
|
109
|
-
params={"ask": "get_current_week"}
|
|
110
|
-
)
|
|
111
|
-
json_data = await response.json()
|
|
112
|
-
return json_data["data"][0]
|
|
113
|
-
|
|
114
|
-
async def get_current_semester(self) -> str:
|
|
115
|
-
"""
|
|
116
|
-
response.json example
|
|
117
|
-
{
|
|
118
|
-
'data': ['Осенний семестр 2023/2024']
|
|
119
|
-
}
|
|
120
|
-
"""
|
|
121
|
-
|
|
122
|
-
response = await self._async_make_http_request(
|
|
123
|
-
url=self.api_url,
|
|
124
|
-
params={"ask": "get_current_semestr"}
|
|
125
|
-
)
|
|
126
|
-
json_data = await response.json()
|
|
127
|
-
return json_data["data"][0]
|
|
128
|
-
|
|
129
|
-
async def get_groups(self) -> list[dict[str, Any]]:
|
|
130
|
-
"""
|
|
131
|
-
response.json example
|
|
132
|
-
{
|
|
133
|
-
"data": {
|
|
134
|
-
"4438": {
|
|
135
|
-
"group_id": 4438,
|
|
136
|
-
"group_title": "АРКТ-101А",
|
|
137
|
-
"faculty": "",
|
|
138
|
-
"course": 1
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
"""
|
|
143
|
-
|
|
144
|
-
response = await self._async_make_http_request(
|
|
145
|
-
url=self.api_url,
|
|
146
|
-
params={"ask": "get_group_list"}
|
|
147
|
-
)
|
|
148
|
-
json_data = await response.json()
|
|
149
|
-
return list(json_data["data"].values())
|
|
150
|
-
|
|
151
|
-
async def get_group_lessons(self, group_id: int, semester: str | None = None) -> list[dict[str, Any]]:
|
|
152
|
-
params = {
|
|
153
|
-
"ask": "get_group_schedule",
|
|
154
|
-
"id": group_id
|
|
155
|
-
}
|
|
156
|
-
if semester is not None:
|
|
157
|
-
params["semester"] = semester
|
|
158
|
-
response = await self._async_make_http_request(
|
|
159
|
-
url=self.api_url,
|
|
160
|
-
params=params
|
|
161
|
-
)
|
|
162
|
-
json_data = await response.json()
|
|
163
|
-
return json_data["data"]
|
|
164
|
-
|
|
165
|
-
async def get_teachers(self) -> list[dict[str, Any]]:
|
|
166
|
-
response = await self._async_make_http_request(
|
|
167
|
-
url=self.api_url,
|
|
168
|
-
params={"ask": "get_teacher_list"}
|
|
169
|
-
)
|
|
170
|
-
json_data = await response.json()
|
|
171
|
-
return list(json_data["data"].values())
|
|
172
|
-
|
|
173
|
-
async def get_teacher_lessons(self, teacher_id: int, semester: str | None = None) -> list[dict[str, Any]]:
|
|
174
|
-
params = {"ask": "get_teacher_schedule", "id": teacher_id}
|
|
175
|
-
if semester is not None:
|
|
176
|
-
params["semester"] = semester
|
|
177
|
-
response = await self._async_make_http_request(
|
|
178
|
-
url=self.api_url,
|
|
179
|
-
params=params
|
|
180
|
-
)
|
|
181
|
-
json_data = await response.json()
|
|
182
|
-
return json_data["data"]
|
|
183
|
-
|
|
184
|
-
async def check_conn(self):
|
|
185
|
-
await self.get_current_week()
|
|
186
|
-
self._logger.info(f"connection is good")
|
|
187
|
-
|
|
188
|
-
async def is_conn_good(self):
|
|
189
|
-
try:
|
|
190
|
-
await self.check_conn()
|
|
191
|
-
except Exception as e:
|
|
192
|
-
self._logger.error(f"connection is bad, {e}")
|
|
193
|
-
return False
|
|
194
|
-
return True
|
|
195
|
-
|
|
196
|
-
async def check_all(self) -> dict[str, Any]:
|
|
197
|
-
current_semester = await self.get_current_semester()
|
|
198
|
-
self._logger.info(f"current_semester: {current_semester}")
|
|
199
|
-
|
|
200
|
-
current_week = await self.get_current_week()
|
|
201
|
-
self._logger.info(f"current_week: {current_week}")
|
|
202
|
-
|
|
203
|
-
groups = await self.get_groups()
|
|
204
|
-
self._logger.info(f"groups len: {len(groups)}")
|
|
205
|
-
|
|
206
|
-
teachers = await self.get_teachers()
|
|
207
|
-
self._logger.info(f"teachers len: {len(teachers)}")
|
|
208
|
-
|
|
209
|
-
return {
|
|
210
|
-
"current_semester": current_semester,
|
|
211
|
-
"current_week": current_week,
|
|
212
|
-
"len(groups)": len(groups),
|
|
213
|
-
"len(teachers)": len(teachers)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def __example():
|
|
218
|
-
pass
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
async def __async_example():
|
|
222
|
-
pass
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if __name__ == '__main__':
|
|
226
|
-
__example()
|
|
227
|
-
asyncio.run(__async_example())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|