arpakitlib 1.8.305__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.

@@ -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
- conn_inspector = sqlalchemy.inspect(conn)
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 = conn_inspector.get_check_constraints(table.name, schema=table.schema)
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()
@@ -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,
@@ -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
- res += f"# {k}=\n"
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) and value.lower().strip() in {"null", "none", "nil"}:
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-и из базы
@@ -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
- COMMENT_RE = re.compile(r"^\s*#")
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(*, path: str | Path,
20
- output: Optional[str | Path] = None,
21
- backup: bool = False) -> Path:
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
- if not line.strip() or COMMENT_RE.match(line):
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}\n"
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.305
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=JJbgaUiLT8D8oDVevP25wlMz1BTtx0JMasJ_ILyIJ3o,5328
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/ar_generate_celery_url.py,sha256=ddzOokXKZRGRDQu18amnW5ZaekcMihwsmyUn0FzpgVU,2274
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=2ECkTnGAZ92qxioL-fmN6R4yZOSr3bXdXLWTzT1C3vk,4038
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/ar_schedule_uust_api_client_util.py,sha256=rXI2_3OTaIBgR-GixM1Ti-Ue1f9nOcO3EUpYRqdGpYM,6973
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=XEqnMrIwSYasW_UOJ8xU-JhsVrcYeyehalFuSvmJMak,3518
429
- arpakitlib/ar_sqlalchemy_util.py,sha256=vWNCzFv13YsGRZaemkNI1_BF__4Z1_Vqm-gqQL1PRrQ,17034
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=UFHLGIR70UB02eD7IAIBrZIbycwWCx3OP_W4J2cx2Jw,2395
433
- arpakitlib-1.8.305.dist-info/METADATA,sha256=3Af4Vr5d4aLDSVb8SlIuRofHnnfHvp3zYJxRQ0H7_98,3793
434
- arpakitlib-1.8.305.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
435
- arpakitlib-1.8.305.dist-info/entry_points.txt,sha256=36xqR3PJFT2kuwjkM_EqoIy0qFUDPKSm_mJaI7emewE,87
436
- arpakitlib-1.8.305.dist-info/licenses/LICENSE,sha256=GPEDQMam2r7FSTYqM1mm7aKnxLaWcBotH7UvQtea-ec,11355
437
- arpakitlib-1.8.305.dist-info/RECORD,,
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())