arpakitlib 1.8.307__py3-none-any.whl → 1.8.322__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of arpakitlib might be problematic. Click here for more details.

@@ -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,175 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import uuid
6
+ from datetime import timedelta
7
+ from typing import Any
8
+
9
+ import aiohttp
10
+ import requests
11
+ from arpakitlib.ar_dict_util import combine_dicts
12
+ from arpakitlib.ar_enumeration_util import Enumeration
13
+ from arpakitlib.ar_http_request_util import sync_make_http_request, async_make_http_request
14
+ from arpakitlib.ar_type_util import raise_for_type
15
+
16
+ """
17
+ https://yookassa.ru/developers/api
18
+ """
19
+
20
+
21
+ class YookassaAPIClient:
22
+ class PaymentStatuses(Enumeration):
23
+ pending = "pending"
24
+ waiting_for_capture = "waiting_for_capture"
25
+ succeeded = "succeeded"
26
+ canceled = "canceled"
27
+
28
+ def __init__(self, *, secret_key: str, shop_id: int):
29
+ super().__init__()
30
+ self.secret_key = secret_key
31
+ self.shop_id = shop_id
32
+ self.headers = {"Content-Type": "application/json"}
33
+ self._logger = logging.getLogger(f"{self.__class__.__name__}-{shop_id}")
34
+
35
+ def _sync_make_http_request(
36
+ self,
37
+ *,
38
+ method: str,
39
+ url: str,
40
+ headers: dict[str, Any] | None = None,
41
+ **kwargs
42
+ ) -> requests.Response:
43
+ return sync_make_http_request(
44
+ method=method,
45
+ url=url,
46
+ headers=combine_dicts(self.headers, (headers if headers is not None else {})),
47
+ max_tries_=3,
48
+ raise_for_status_=True,
49
+ timeout_=timedelta(seconds=3),
50
+ not_raise_for_statuses_=[404],
51
+ auth=(self.shop_id, self.secret_key),
52
+ enable_logging_=False,
53
+ **kwargs
54
+ )
55
+
56
+ async def _async_make_http_request(
57
+ self,
58
+ *,
59
+ method: str = "GET",
60
+ url: str,
61
+ headers: dict[str, Any] | None = None,
62
+ **kwargs
63
+ ) -> aiohttp.ClientResponse:
64
+ return await async_make_http_request(
65
+ method=method,
66
+ url=url,
67
+ headers=combine_dicts(self.headers, (headers if headers is not None else {})),
68
+ max_tries_=3,
69
+ raise_for_status_=True,
70
+ not_raise_for_statuses_=[404],
71
+ timeout_=timedelta(seconds=3),
72
+ auth=aiohttp.BasicAuth(login=str(self.shop_id), password=self.secret_key),
73
+ enable_logging_=False,
74
+ **kwargs
75
+ )
76
+
77
+ def sync_create_payment(self, *, json_body: dict[str, Any]) -> dict[str, Any]:
78
+
79
+ """
80
+ json_body example
81
+ json_body = {
82
+ "amount": {
83
+ "value": "2.0",
84
+ "currency": "RUB"
85
+ },
86
+ "description": "description",
87
+ "confirmation": {
88
+ "type": "redirect",
89
+ "return_url": f"https://t.me/{get_tg_bot_username()}",
90
+ "locale": "ru_RU"
91
+ },
92
+ "capture": True,
93
+ "metadata": {},
94
+ "merchant_customer_id": ""
95
+ }
96
+ """
97
+
98
+ response = self._sync_make_http_request(
99
+ method="POST",
100
+ url="https://api.yookassa.ru/v3/payments",
101
+ headers={"Idempotence-Key": str(uuid.uuid4())},
102
+ json=json_body,
103
+ )
104
+ json_data = response.json()
105
+ response.raise_for_status()
106
+ return json_data
107
+
108
+ def sync_get_payment(self, *, payment_id: str) -> dict[str, Any] | None:
109
+ raise_for_type(payment_id, str)
110
+ response = self._sync_make_http_request(
111
+ method="GET",
112
+ url=f"https://api.yookassa.ru/v3/payments/{payment_id}",
113
+ headers=self.headers
114
+ )
115
+ json_data = response.json()
116
+ if response.status_code == 404:
117
+ return None
118
+ response.raise_for_status()
119
+ return json_data
120
+
121
+ async def async_create_payment(self, *, json_body: dict[str, Any]) -> dict[str, Any]:
122
+
123
+ """
124
+ json_body example
125
+ json_body = {
126
+ "amount": {
127
+ "value": "2.0",
128
+ "currency": "RUB"
129
+ },
130
+ "description": "description",
131
+ "confirmation": {
132
+ "type": "redirect",
133
+ "return_url": f"https://t.me/{get_tg_bot_username()}",
134
+ "locale": "ru_RU"
135
+ },
136
+ "capture": True,
137
+ "metadata": {},
138
+ "merchant_customer_id": ""
139
+ }
140
+ """
141
+
142
+ response = await self._async_make_http_request(
143
+ method="POST",
144
+ url="https://api.yookassa.ru/v3/payments",
145
+ headers={"Idempotence-Key": str(uuid.uuid4())},
146
+ json=json_body,
147
+ )
148
+ json_data = await response.json()
149
+ response.raise_for_status()
150
+ return json_data
151
+
152
+ async def async_get_payment(self, *, payment_id: str) -> dict[str, Any] | None:
153
+ raise_for_type(payment_id, str)
154
+ response = await self._async_make_http_request(
155
+ method="GET",
156
+ url=f"https://api.yookassa.ru/v3/payments/{payment_id}",
157
+ )
158
+ json_data = await response.json()
159
+ if response.status == 404:
160
+ return None
161
+ response.raise_for_status()
162
+ return json_data
163
+
164
+
165
+ def __example():
166
+ pass
167
+
168
+
169
+ async def __async_example():
170
+ pass
171
+
172
+
173
+ if __name__ == '__main__':
174
+ __example()
175
+ asyncio.run(__async_example())
@@ -0,0 +1,46 @@
1
+ import importlib.util
2
+ import os
3
+ from fastapi import APIRouter
4
+
5
+
6
+ def include_fastapi_routers_from_dir(
7
+ *,
8
+ router: APIRouter,
9
+ base_dir: str = ".",
10
+ exclude_filenames: list[str] | None = None,
11
+ ):
12
+ """
13
+ Рекурсивно ищет все .py файлы с объектом `api_router` типа APIRouter
14
+ и подключает их к переданному `router`.
15
+
16
+ Префикс = имя файла без .py
17
+ exclude_filenames — список имён файлов, которые нужно пропустить (без путей)
18
+ """
19
+ if exclude_filenames is None:
20
+ exclude_filenames = ["__init__.py"]
21
+
22
+ for root, _, files in os.walk(base_dir):
23
+ files.sort()
24
+
25
+ for filename in files:
26
+ if not filename.endswith(".py") or filename in exclude_filenames:
27
+ continue
28
+
29
+ file_path = os.path.join(root, filename)
30
+ module_name = (
31
+ os.path.relpath(file_path, base_dir)
32
+ .replace(os.sep, ".")
33
+ .removesuffix(".py")
34
+ )
35
+
36
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
37
+ module = importlib.util.module_from_spec(spec)
38
+ spec.loader.exec_module(module)
39
+
40
+ api_router = getattr(module, "api_router", None)
41
+ if isinstance(api_router, APIRouter):
42
+ prefix = "/" + filename[:-3] # имя файла без .py
43
+ router.include_router(
44
+ router=api_router,
45
+ prefix=prefix
46
+ )
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arpakitlib
3
- Version: 1.8.307
3
+ Version: 1.8.322
4
4
  Summary: arpakitlib
5
- License: Apache-2.0
5
+ License-Expression: Apache-2.0
6
6
  License-File: LICENSE
7
7
  Keywords: arpakitlib,arpakit,arpakit-company,arpakitcompany,arpakit_company
8
8
  Author: arpakit_company
@@ -375,12 +375,9 @@ arpakitlib/_arpakit_project_template_v_5/project/util/arpakitlib_project_templat
375
375
  arpakitlib/_arpakit_project_template_v_5/project/util/etc.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
376
376
  arpakitlib/_arpakit_project_template_v_5/project/util/send_email_.py,sha256=AZ2PHRQh-l7H3WLxgxygEKD8c_WPguIq4weP2zqfF9I,4748
377
377
  arpakitlib/_arpakit_project_template_v_5/todo.txt,sha256=q132Jbx229ThY77S3YiN-Cj5AVm7k1VlJcMYIbZUHUY,3
378
- arpakitlib/ar_additional_model_util.py,sha256=GFg-glLCxH9X95R2bhTJsscVwv37FgE1qbaAAyXrnIE,917
379
378
  arpakitlib/ar_aiogram_as_tg_command_util.py,sha256=4bizX5Xg-E2-r2TXXGQGanJozsIWPVf5luO3vKUN8p8,8471
380
379
  arpakitlib/ar_arpakit_lib_module_util.py,sha256=g9uWwTK2eEzmErqwYeVgXDYVMREN8m5CdmgEumAEQfw,5919
381
380
  arpakitlib/ar_arpakit_project_template_util.py,sha256=5-o6eTmh-2DqsIqmo63SXw7ttExhmuAehMNc4s0GdtU,3278
382
- arpakitlib/ar_arpakit_schedule_uust_api_client_util.py,sha256=MRPaF31CRhYA45ldPnoRpaTPMazCJq0jbwDxW5rvMww,12937
383
- arpakitlib/ar_arpakit_schedule_uust_site_util.py,sha256=8wLct9Gd4MWkXzB6nSmETAwTPLw8lfpWgx0LoWSAOvg,1643
384
381
  arpakitlib/ar_arpakitlib_cli_util.py,sha256=RJGcfEZ_q74FJ4tqdXvt7xQpShTszOvKu1mbp3D8qzw,2599
385
382
  arpakitlib/ar_base64_util.py,sha256=udSSpeXMZx0JgQknl4hQgZ8kr1Ps_aQOloIXu4T9dMQ,1286
386
383
  arpakitlib/ar_base_worker_util.py,sha256=4XN29vkju1OBmvZj1MVjxGXVWQzcs9Sdd90JBj1QoFg,6336
@@ -391,13 +388,13 @@ arpakitlib/ar_clone_pydantic_model_fields.py,sha256=5i77NGEjnY2ppk_Ot179egQGNDvg
391
388
  arpakitlib/ar_datetime_util.py,sha256=3Pw8ljsBKkkEUCmu1PsnJqNeG5bVLqtpsUGaztJF8jQ,1432
392
389
  arpakitlib/ar_dict_util.py,sha256=oet-9AJEjQZfG_EI82BuYW0jdW2NQxKjPXol_nfTXjw,447
393
390
  arpakitlib/ar_encrypt_decrypt_util.py,sha256=GhWnp7HHkbhwFVVCzO1H07m-5gryr4yjWsXjOaNQm1Y,520
394
- arpakitlib/ar_ensure_sqlalchemy_check_constraints.py,sha256=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,18 @@ arpakitlib/ar_really_validate_url.py,sha256=aaSPVMbz2DSqlC2yk2g44-kTIiHlITfJwIG9
421
418
  arpakitlib/ar_retry_func_util.py,sha256=LB4FJRsu2cssnPw6X8bCEcaGpQsXhkLkgeU37w1t9fU,2250
422
419
  arpakitlib/ar_run_cmd_util.py,sha256=D_rPavKMmWkQtwvZFz-Io5Ak8eSODHkcFeLPzNVC68g,1072
423
420
  arpakitlib/ar_safe_func.py,sha256=JQNaM3q4Z6nUE8bDfzXNBGkWSXm0PZ-GMMvu6UWUIYk,2400
424
- arpakitlib/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.307.dist-info/METADATA,sha256=oYo45m-EQwszIIuS46JanZzDJVMj6LLAMnapYJcRLP8,3793
434
- arpakitlib-1.8.307.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
435
- arpakitlib-1.8.307.dist-info/entry_points.txt,sha256=36xqR3PJFT2kuwjkM_EqoIy0qFUDPKSm_mJaI7emewE,87
436
- arpakitlib-1.8.307.dist-info/licenses/LICENSE,sha256=GPEDQMam2r7FSTYqM1mm7aKnxLaWcBotH7UvQtea-ec,11355
437
- arpakitlib-1.8.307.dist-info/RECORD,,
428
+ arpakitlib/ar_uppercase_env_keys.py,sha256=BsUCJhfchBIav0AE54_tVgYcE4p1JYoWdPGCHWZnROA,2790
429
+ arpakitlib/ar_yookassa_api_client_util.py,sha256=Mst07IblAJmU98HfOfJqT_RRfnuGrIB1UOQunGGWO8I,5264
430
+ arpakitlib/include_fastapi_routers_from_dir.py,sha256=Umg16sPQC6R-T8FD7mlmqP1TbgH-_v2eDasrZJzImQM,1606
431
+ arpakitlib-1.8.322.dist-info/METADATA,sha256=7I4FON25AN9tOuK2iozXNi4M_4F3t1FJkbpfWCFrO08,3804
432
+ arpakitlib-1.8.322.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
433
+ arpakitlib-1.8.322.dist-info/entry_points.txt,sha256=36xqR3PJFT2kuwjkM_EqoIy0qFUDPKSm_mJaI7emewE,87
434
+ arpakitlib-1.8.322.dist-info/licenses/LICENSE,sha256=GPEDQMam2r7FSTYqM1mm7aKnxLaWcBotH7UvQtea-ec,11355
435
+ arpakitlib-1.8.322.dist-info/RECORD,,
@@ -1,40 +0,0 @@
1
- # arpakit
2
- from typing import Any
3
-
4
- from pydantic import BaseModel, ConfigDict
5
-
6
- _ARPAKIT_LIB_MODULE_VERSION = "3.0"
7
-
8
-
9
- class BaseAM(BaseModel):
10
- model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True, from_attributes=True)
11
- _bus_data: dict[str, Any] | None = None
12
-
13
- @property
14
- def bus_data(self) -> dict[str, Any]:
15
- if self._bus_data is None:
16
- self._bus_data = {}
17
- return self._bus_data
18
-
19
-
20
- def __example():
21
- class UserAM(BaseAM):
22
- id: int
23
- name: str
24
- email: str
25
-
26
- @property
27
- def bus_data_age(self) -> int | None:
28
- return self.bus_data.get("age")
29
-
30
- user = UserAM(id=1, name="John Doe", email="john.doe@example.com")
31
- print(user.name) # John Doe
32
-
33
- # bus_data
34
- user.bus_data["age"] = 22
35
- print(user.bus_data) # {'age': '22'}
36
- print(user.bus_data_age) # 22
37
-
38
-
39
- if __name__ == '__main__':
40
- __example()