arpakitlib 1.8.52__py3-none-any.whl → 1.8.55__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.
Files changed (53) hide show
  1. arpakitlib/_arpakit_project_template_v_5/.gitignore +1 -0
  2. arpakitlib/_arpakit_project_template_v_5/arpakitlib_project_template_info.json +1 -1
  3. arpakitlib/_arpakit_project_template_v_5/project/api/router/admin/check_sqlalchemy_db.py +4 -4
  4. arpakitlib/_arpakit_project_template_v_5/project/api/router/admin/clear_log_file.py +3 -3
  5. arpakitlib/_arpakit_project_template_v_5/project/api/router/admin/create_operation.py +3 -3
  6. arpakitlib/_arpakit_project_template_v_5/project/api/router/admin/get_arpakitlib_project_template_info.py +3 -3
  7. arpakitlib/_arpakit_project_template_v_5/project/api/router/admin/get_auth_data.py +3 -3
  8. arpakitlib/_arpakit_project_template_v_5/project/api/router/admin/get_operation.py +3 -3
  9. arpakitlib/_arpakit_project_template_v_5/project/api/router/admin/get_operation_allowed_statuses.py +3 -3
  10. arpakitlib/_arpakit_project_template_v_5/project/api/router/admin/get_operation_allowed_types.py +3 -3
  11. arpakitlib/_arpakit_project_template_v_5/project/api/router/admin/get_settings.py +4 -4
  12. arpakitlib/_arpakit_project_template_v_5/project/api/router/admin/get_sqlalchemy_db_table_name_to_amount.py +3 -3
  13. arpakitlib/_arpakit_project_template_v_5/project/api/router/admin/get_story_log.py +3 -3
  14. arpakitlib/_arpakit_project_template_v_5/project/api/router/admin/get_user.py +81 -0
  15. arpakitlib/_arpakit_project_template_v_5/project/api/router/admin/init_sqlalchemy_db.py +3 -3
  16. arpakitlib/_arpakit_project_template_v_5/project/api/router/admin/main_router.py +11 -1
  17. arpakitlib/_arpakit_project_template_v_5/project/api/router/admin/make_test_data_1.py +41 -0
  18. arpakitlib/_arpakit_project_template_v_5/project/api/router/admin/reinit_sqlalchemy_db.py +3 -3
  19. arpakitlib/_arpakit_project_template_v_5/project/api/router/client/get_current_user.py +3 -3
  20. arpakitlib/_arpakit_project_template_v_5/project/api/router/client/get_current_user_token.py +3 -3
  21. arpakitlib/_arpakit_project_template_v_5/project/api/router/general/check_authorization.py +5 -4
  22. arpakitlib/_arpakit_project_template_v_5/project/api/router/general/get_current_api_key.py +3 -3
  23. arpakitlib/_arpakit_project_template_v_5/project/api/router/general/get_errors_info.py +3 -3
  24. arpakitlib/_arpakit_project_template_v_5/project/api/router/general/healthcheck.py +3 -3
  25. arpakitlib/_arpakit_project_template_v_5/project/api/router/general/now_utc_datetime.py +3 -3
  26. arpakitlib/_arpakit_project_template_v_5/project/api/schema/out/admin/common.py +3 -1
  27. arpakitlib/_arpakit_project_template_v_5/project/api/schema/out/admin/user.py +35 -0
  28. arpakitlib/_arpakit_project_template_v_5/project/api/schema/out/client/common.py +1 -0
  29. arpakitlib/_arpakit_project_template_v_5/project/api/schema/out/client/user.py +3 -0
  30. arpakitlib/_arpakit_project_template_v_5/project/api/schema/out/common/raw_data.py +1 -1
  31. arpakitlib/_arpakit_project_template_v_5/project/api/schema/out/general/common.py +4 -1
  32. arpakitlib/_arpakit_project_template_v_5/project/api/schema/out/general/user.py +3 -0
  33. arpakitlib/_arpakit_project_template_v_5/project/operation_execution/scheduled_operations.py +1 -1
  34. arpakitlib/_arpakit_project_template_v_5/project/sandbox/sandbox_1.py +5 -1
  35. arpakitlib/_arpakit_project_template_v_5/project/sqladmin_/admin_authorize.py +1 -0
  36. arpakitlib/_arpakit_project_template_v_5/project/sqladmin_/model_view/user.py +1 -0
  37. arpakitlib/_arpakit_project_template_v_5/project/sqlalchemy_db_/sqlalchemy_model/api_key.py +29 -11
  38. arpakitlib/_arpakit_project_template_v_5/project/sqlalchemy_db_/sqlalchemy_model/common.py +47 -12
  39. arpakitlib/_arpakit_project_template_v_5/project/sqlalchemy_db_/sqlalchemy_model/operation.py +81 -12
  40. arpakitlib/_arpakit_project_template_v_5/project/sqlalchemy_db_/sqlalchemy_model/story_log.py +33 -6
  41. arpakitlib/_arpakit_project_template_v_5/project/sqlalchemy_db_/sqlalchemy_model/user.py +59 -18
  42. arpakitlib/_arpakit_project_template_v_5/project/sqlalchemy_db_/sqlalchemy_model/user_token.py +18 -6
  43. arpakitlib/_arpakit_project_template_v_5/project/sqlalchemy_db_/util.py +0 -28
  44. arpakitlib/_arpakit_project_template_v_5/project/test_data/make_test_api_keys.py +2 -2
  45. arpakitlib/_arpakit_project_template_v_5/project/test_data/make_test_data_1.py +36 -0
  46. arpakitlib/ar_str_util.py +9 -0
  47. {arpakitlib-1.8.52.dist-info → arpakitlib-1.8.55.dist-info}/METADATA +1 -1
  48. {arpakitlib-1.8.52.dist-info → arpakitlib-1.8.55.dist-info}/RECORD +51 -50
  49. arpakitlib/ar_steam_payment_api_client_util.py +0 -21
  50. arpakitlib/ar_wata_api_client.py +0 -21
  51. {arpakitlib-1.8.52.dist-info → arpakitlib-1.8.55.dist-info}/LICENSE +0 -0
  52. {arpakitlib-1.8.52.dist-info → arpakitlib-1.8.55.dist-info}/WHEEL +0 -0
  53. {arpakitlib-1.8.52.dist-info → arpakitlib-1.8.55.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime as dt
4
+ from typing import Any
5
+
6
+ from project.api.schema.out.admin.common import SimpleDBMAdminSO
7
+ from project.sqlalchemy_db_.sqlalchemy_model import UserDBM
8
+
9
+
10
+ class UserAdmin1SO(SimpleDBMAdminSO):
11
+ email: str | None
12
+ username: str | None
13
+ roles: list[str]
14
+ is_active: bool
15
+ password: str | None
16
+ tg_id: int | None
17
+ tg_bot_last_action_dt: dt.datetime | None
18
+ tg_data: dict[str, Any]
19
+
20
+ roles_has_admin: bool
21
+ roles_has_client: bool
22
+ allowed_roles: list[str]
23
+ tg_data_first_name: str | None
24
+ tg_data_last_name: str | None
25
+ tg_data_language_code: str | None
26
+ tg_data_username: str | None
27
+ tg_data_at_username: str | None
28
+ tg_data_fullname: str | None
29
+ tg_data_link_by_username: str | None
30
+
31
+ @classmethod
32
+ def from_dbm(cls, *, simple_dbm: UserDBM) -> UserAdmin1SO:
33
+ return cls.model_validate(simple_dbm.simple_dict_with_sd_properties(
34
+ only_columns_and_sd_properties=cls.model_fields.keys()
35
+ ))
@@ -11,6 +11,7 @@ class SimpleDBMClientSO(BaseSO):
11
11
  long_id: str
12
12
  slug: str | None
13
13
  creation_dt: dt.datetime
14
+
14
15
  entity_name: str
15
16
 
16
17
  @classmethod
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import datetime as dt
4
+ from typing import Any
4
5
 
5
6
  from project.api.schema.out.client.common import SimpleDBMClientSO
6
7
  from project.sqlalchemy_db_.sqlalchemy_model import UserDBM
@@ -8,10 +9,12 @@ from project.sqlalchemy_db_.sqlalchemy_model import UserDBM
8
9
 
9
10
  class UserClient1SO(SimpleDBMClientSO):
10
11
  email: str | None
12
+ username: str | None
11
13
  roles: list[str]
12
14
  is_active: bool
13
15
  tg_id: int | None
14
16
  tg_bot_last_action_dt: dt.datetime | None
17
+ tg_data: dict[str, Any]
15
18
 
16
19
  roles_has_admin: bool
17
20
  roles_has_client: bool
@@ -4,4 +4,4 @@ from project.api.schema.common import BaseSO
4
4
 
5
5
 
6
6
  class RawDataCommonSO(BaseSO):
7
- data: dict[str, Any] = {}
7
+ raw_data: dict[str, Any] = {}
@@ -11,8 +11,11 @@ class SimpleDBMGeneralSO(BaseSO):
11
11
  long_id: str
12
12
  slug: str | None
13
13
  creation_dt: dt.datetime
14
+
14
15
  entity_name: str
15
16
 
16
17
  @classmethod
17
18
  def from_dbm(cls, *, simple_dbm: SimpleDBM) -> SimpleDBMGeneralSO:
18
- return cls.model_validate(simple_dbm.simple_dict_with_sd_properties())
19
+ return cls.model_validate(simple_dbm.simple_dict_with_sd_properties(
20
+ only_columns_and_sd_properties=cls.model_fields.keys()
21
+ ))
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import datetime as dt
4
+ from typing import Any
4
5
 
5
6
  from project.api.schema.out.client.common import SimpleDBMClientSO
6
7
  from project.sqlalchemy_db_.sqlalchemy_model import UserDBM
@@ -8,10 +9,12 @@ from project.sqlalchemy_db_.sqlalchemy_model import UserDBM
8
9
 
9
10
  class UserGeneral1SO(SimpleDBMClientSO):
10
11
  email: str | None
12
+ username: str | None
11
13
  roles: list[str]
12
14
  is_active: bool
13
15
  tg_id: int | None
14
16
  tg_bot_last_action_dt: dt.datetime | None
17
+ tg_data: dict[str, Any]
15
18
 
16
19
  roles_has_admin: bool
17
20
  roles_has_client: bool
@@ -12,7 +12,7 @@ class ScheduledOperation(BaseModel):
12
12
  model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True, from_attributes=True)
13
13
 
14
14
  type: str
15
- input_data: dict[str, Any] | None = None
15
+ input_data: dict[str, Any] = {}
16
16
  is_time_func: Callable
17
17
  timeout_after_creation: timedelta | None = None
18
18
 
@@ -1,8 +1,12 @@
1
1
  import asyncio
2
2
 
3
+ from project.sqlalchemy_db_.sqlalchemy_db import get_cached_sqlalchemy_db
4
+ from project.sqlalchemy_db_.sqlalchemy_model import ApiKeyDBM
5
+
3
6
 
4
7
  def __sandbox():
5
- pass
8
+ with get_cached_sqlalchemy_db().new_session() as session:
9
+ ApiKeyDBM()
6
10
 
7
11
 
8
12
  async def __async_sandbox():
@@ -63,6 +63,7 @@ class SQLAdminAuth(AuthenticationBackend):
63
63
  UserDBM.long_id == username,
64
64
  UserDBM.slug == username,
65
65
  UserDBM.email == username,
66
+ UserDBM.username == username,
66
67
  ]
67
68
  if username.isdigit():
68
69
  _or_conditions.append(UserDBM.id == int(username))
@@ -11,6 +11,7 @@ class UserMV(SimpleMV, model=UserDBM):
11
11
  form_columns = [
12
12
  UserDBM.slug,
13
13
  UserDBM.email,
14
+ UserDBM.username,
14
15
  UserDBM.roles,
15
16
  UserDBM.is_active,
16
17
  UserDBM.password,
@@ -4,9 +4,10 @@ from typing import TYPE_CHECKING
4
4
  from uuid import uuid4
5
5
 
6
6
  import sqlalchemy
7
- from sqlalchemy.orm import Mapped, mapped_column
7
+ from sqlalchemy.orm import Mapped, mapped_column, validates
8
8
 
9
9
  from arpakitlib.ar_datetime_util import now_utc_dt
10
+ from arpakitlib.ar_str_util import make_none_if_blank
10
11
  from project.sqlalchemy_db_.sqlalchemy_model.common import SimpleDBM
11
12
 
12
13
  if TYPE_CHECKING:
@@ -17,6 +18,7 @@ def generate_default_api_key_value() -> str:
17
18
  return (
18
19
  f"apikey"
19
20
  f"{str(uuid4()).replace('-', '')}"
21
+ f"{str(uuid4()).replace('-', '')}"
20
22
  f"{str(now_utc_dt().timestamp()).replace('.', '')}"
21
23
  )
22
24
 
@@ -26,27 +28,43 @@ class ApiKeyDBM(SimpleDBM):
26
28
 
27
29
  title: Mapped[str | None] = mapped_column(
28
30
  sqlalchemy.TEXT,
29
- insert_default=None,
30
31
  nullable=True
31
32
  )
32
33
  value: Mapped[str] = mapped_column(
33
34
  sqlalchemy.TEXT,
35
+ nullable=False,
34
36
  unique=True,
35
37
  insert_default=generate_default_api_key_value,
36
- server_default=sqlalchemy.func.gen_random_uuid(),
37
- nullable=False
38
+ server_default=sqlalchemy.func.gen_random_uuid()
38
39
  )
39
40
  is_active: Mapped[bool] = mapped_column(
40
41
  sqlalchemy.Boolean,
42
+ nullable=False,
41
43
  index=True,
42
44
  insert_default=True,
43
- server_default="true",
44
- nullable=False
45
+ server_default="true"
45
46
  )
46
47
 
47
48
  def __repr__(self) -> str:
48
- res = f"{self.entity_name} (id={self.id}, is_active={self.is_active}"
49
- if self.title:
50
- res += f", title={self.title}"
51
- res += ")"
52
- return res
49
+ parts = [f"id={self.id}"]
50
+ if self.title is not None:
51
+ parts.append(f"title={self.title}")
52
+ return f"{self.entity_name} ({', '.join(parts)})"
53
+
54
+ @validates("title")
55
+ def _validate_title(self, key, value, *args, **kwargs):
56
+ if value is None:
57
+ return None
58
+ if not isinstance(value, str):
59
+ raise ValueError(f"{key=}, {value=}, value is not str")
60
+ value = make_none_if_blank(value.strip())
61
+ return value
62
+
63
+ @validates("value")
64
+ def _validate_value(self, key, value, *args, **kwargs):
65
+ if not isinstance(value, str):
66
+ raise ValueError(f"{key=}, {value=}, value is not str")
67
+ value = value.strip()
68
+ if not value:
69
+ raise ValueError(f"{key=}, {value=}, value is empty")
70
+ return value
@@ -1,13 +1,28 @@
1
1
  from datetime import datetime
2
2
  from typing import Any
3
+ from uuid import uuid4
3
4
 
4
5
  import sqlalchemy
5
6
  from sqlalchemy import func
6
- from sqlalchemy.orm import mapped_column, Mapped
7
+ from sqlalchemy.orm import mapped_column, Mapped, validates
7
8
 
8
9
  from arpakitlib.ar_datetime_util import now_utc_dt
9
10
  from arpakitlib.ar_sqlalchemy_util import get_string_info_from_declarative_base, BaseDBM
10
- from project.sqlalchemy_db_.util import generate_default_long_id
11
+
12
+
13
+ def generate_default_long_id() -> str:
14
+ return (
15
+ f"longid"
16
+ f"{str(uuid4()).replace('-', '')}"
17
+ f"{str(uuid4()).replace('-', '')}"
18
+ f"{str(now_utc_dt().timestamp()).replace('.', '')}"
19
+ )
20
+
21
+
22
+ def make_slug_from_string(string: str) -> str:
23
+ string = string.strip()
24
+ string = string.replace(" ", "-")
25
+ return string
11
26
 
12
27
 
13
28
  class SimpleDBM(BaseDBM):
@@ -15,46 +30,66 @@ class SimpleDBM(BaseDBM):
15
30
 
16
31
  id: Mapped[int] = mapped_column(
17
32
  sqlalchemy.BIGINT,
33
+ nullable=False,
18
34
  primary_key=True,
19
35
  autoincrement=True,
20
36
  sort_order=-103,
21
- nullable=False
22
37
  )
23
38
  long_id: Mapped[str] = mapped_column(
24
39
  sqlalchemy.TEXT,
40
+ nullable=False,
41
+ unique=True,
25
42
  insert_default=generate_default_long_id,
26
43
  server_default=func.gen_random_uuid(),
27
- unique=True,
28
44
  sort_order=-102,
29
- nullable=False
30
45
  )
31
46
  slug: Mapped[str | None] = mapped_column(
32
47
  sqlalchemy.TEXT,
48
+ nullable=True,
33
49
  unique=True,
34
50
  sort_order=-101,
35
- nullable=True
36
51
  )
37
52
  creation_dt: Mapped[datetime] = mapped_column(
38
53
  sqlalchemy.TIMESTAMP(timezone=True),
54
+ nullable=False,
55
+ index=True,
39
56
  insert_default=now_utc_dt,
40
57
  server_default=func.now(),
41
- index=True,
42
58
  sort_order=-100,
43
- nullable=False
44
59
  )
45
60
  extra_data: Mapped[dict[str, Any]] = mapped_column(
46
61
  sqlalchemy.JSON,
47
- index=False,
48
62
  nullable=False,
63
+ index=False,
49
64
  insert_default={},
50
65
  server_default="{}",
51
66
  sort_order=1000,
52
67
  )
53
68
 
54
69
  def __repr__(self) -> str:
55
- if self.slug is None:
56
- return f"{self.__class__.__name__.removesuffix('DBM')} (id={self.id})"
57
- return f"{self.__class__.__name__.removesuffix('DBM')} (id={self.id}, slug={self.slug})"
70
+ parts = [f"id={self.id}"]
71
+ if self.slug is not None:
72
+ parts.append(f"slug={self.slug}")
73
+ return f"{self.entity_name} ({', '.join(parts)})"
74
+
75
+ @validates("slug")
76
+ def _validate_slug(self, key, value, *args, **kwargs):
77
+ if value is None:
78
+ return None
79
+ if not isinstance(value, str):
80
+ raise ValueError(f"{key=}, {value=}, value is empty")
81
+ value = value.strip()
82
+ if " " in value:
83
+ raise ValueError(f"{key=}, {value=}, value contains spaces")
84
+ return value
85
+
86
+ @validates("extra_data")
87
+ def _validate_extra_data(self, key, value, *args, **kwargs):
88
+ if value is None:
89
+ value = {}
90
+ if not isinstance(value, dict):
91
+ raise ValueError(f"{key=}, {value=}, value is not dict")
92
+ return value
58
93
 
59
94
  @property
60
95
  def entity_name(self) -> str:
@@ -8,6 +8,7 @@ from sqlalchemy.dialects import postgresql
8
8
  from sqlalchemy.orm import mapped_column, Mapped, validates
9
9
 
10
10
  from arpakitlib.ar_enumeration_util import Enumeration
11
+ from arpakitlib.ar_str_util import make_none_if_blank
11
12
  from project.sqlalchemy_db_.sqlalchemy_model.common import SimpleDBM
12
13
 
13
14
  if TYPE_CHECKING:
@@ -28,59 +29,127 @@ class OperationDBM(SimpleDBM):
28
29
  raise_fake_error_ = "raise_fake_error"
29
30
 
30
31
  status: Mapped[str] = mapped_column(
31
- sqlalchemy.TEXT, index=True, insert_default=Statuses.waiting_for_execution,
32
- server_default=Statuses.waiting_for_execution, nullable=False
32
+ sqlalchemy.TEXT,
33
+ nullable=False,
34
+ index=True,
35
+ insert_default=Statuses.waiting_for_execution,
36
+ server_default=Statuses.waiting_for_execution
33
37
  )
34
38
  type: Mapped[str] = mapped_column(
35
- sqlalchemy.TEXT, index=True, insert_default=Types.healthcheck_, nullable=False
39
+ sqlalchemy.TEXT,
40
+ nullable=False,
41
+ index=True,
42
+ insert_default=Types.healthcheck_
36
43
  )
37
44
  title: Mapped[str | None] = mapped_column(
38
45
  sqlalchemy.TEXT,
39
- insert_default=None,
40
- nullable=True
46
+ nullable=True,
47
+ index=False
41
48
  )
42
49
  execution_start_dt: Mapped[datetime | None] = mapped_column(
43
50
  sqlalchemy.TIMESTAMP(timezone=True),
44
- nullable=True
51
+ nullable=True,
52
+ index=False
45
53
  )
46
54
  execution_finish_dt: Mapped[datetime | None] = mapped_column(
47
55
  sqlalchemy.TIMESTAMP(timezone=True),
48
- nullable=True
56
+ nullable=True,
57
+ index=False
49
58
  )
50
59
  input_data: Mapped[dict[str, Any]] = mapped_column(
51
60
  postgresql.JSON,
61
+ nullable=False,
62
+ index=False,
52
63
  insert_default={},
53
64
  server_default="{}",
54
- nullable=False
55
65
  )
56
66
  output_data: Mapped[dict[str, Any]] = mapped_column(
57
67
  postgresql.JSON,
68
+ nullable=False,
69
+ index=False,
58
70
  insert_default={},
59
71
  server_default="{}",
60
- nullable=False
61
72
  )
62
73
  error_data: Mapped[dict[str, Any]] = mapped_column(
63
74
  postgresql.JSON,
75
+ nullable=False,
76
+ index=False,
64
77
  insert_default={},
65
78
  server_default="{}",
66
- nullable=False
67
79
  )
68
80
 
81
+ def __repr__(self) -> str:
82
+ parts = [f"id={self.id}"]
83
+ if self.status is not None:
84
+ parts.append(f"status={self.status}")
85
+ if self.type is not None:
86
+ parts.append(f"type={self.type}")
87
+ return f"{self.entity_name} ({', '.join(parts)})"
88
+
69
89
  @validates("status")
70
90
  def _validate_status(self, key, value, *args, **kwargs):
91
+ if not isinstance(value, str):
92
+ raise ValueError(f"{key=}, {value=}, value is not str")
93
+ value = value.strip()
94
+ if not value:
95
+ raise ValueError(f"{key=}, {value=}, value is empty")
71
96
  self.Statuses.parse_and_validate_values(value)
72
97
  return value
73
98
 
99
+ @validates("type")
100
+ def _validate_type(self, key, value, *args, **kwargs):
101
+ if not isinstance(value, str):
102
+ raise ValueError(f"{key=}, {value=}, value is not str")
103
+ value = value.strip()
104
+ if not value:
105
+ raise ValueError(f"{key=}, {value=}, value is empty")
106
+ return value
107
+
108
+ @validates("title")
109
+ def _validate_title(self, key, value, *args, **kwargs):
110
+ if value is None:
111
+ return None
112
+ if not isinstance(value, str):
113
+ raise ValueError(f"{key=}, {value=}, value is not str")
114
+ value = make_none_if_blank(value.strip())
115
+ return value
116
+
117
+ @validates("input_data")
118
+ def _validate_input_data(self, key, value, *args, **kwargs):
119
+ if value is None:
120
+ value = {}
121
+ if not isinstance(value, dict):
122
+ raise ValueError(f"{key=}, {value=}, value is not dict")
123
+ return value
124
+
125
+ @validates("output_data")
126
+ def _validate_output_data(self, key, value, *args, **kwargs):
127
+ if value is None:
128
+ value = {}
129
+ if not isinstance(value, dict):
130
+ raise ValueError(f"{key=}, {value=}, value is not dict")
131
+ return value
132
+
133
+ @validates("error_data")
134
+ def _validate_error_data(self, key, value, *args, **kwargs):
135
+ if value is None:
136
+ value = {}
137
+ if not isinstance(value, dict):
138
+ raise ValueError(f"{key=}, {value=}, value is not dict")
139
+ return value
140
+
74
141
  def raise_if_executed_with_error(self):
75
142
  if self.status == self.Statuses.executed_with_error:
76
143
  raise Exception(
77
- f"Operation (id={self.id}, type={self.type}) executed with error, error_data={self.error_data}"
144
+ f"Operation ({self.id=}, {self.type=}, {self.status=})"
145
+ f" executed with error, error_data={self.error_data}"
78
146
  )
79
147
 
80
148
  def raise_if_error_data(self):
81
149
  if self.error_data:
82
150
  raise Exception(
83
- f"Operation (id={self.id}, type={self.type}) has error_data, error_data={self.error_data}"
151
+ f"Operation ({self.id=}, {self.type=}, {self.status=})"
152
+ f" has error_data, error_data={self.error_data}"
84
153
  )
85
154
 
86
155
  @property
@@ -6,6 +6,7 @@ import sqlalchemy
6
6
  from sqlalchemy.orm import mapped_column, Mapped, validates
7
7
 
8
8
  from arpakitlib.ar_enumeration_util import Enumeration
9
+ from arpakitlib.ar_str_util import make_none_if_blank
9
10
  from project.sqlalchemy_db_.sqlalchemy_model.common import SimpleDBM
10
11
 
11
12
  if TYPE_CHECKING:
@@ -27,25 +28,51 @@ class StoryLogDBM(SimpleDBM):
27
28
 
28
29
  level: Mapped[str] = mapped_column(
29
30
  sqlalchemy.TEXT,
31
+ nullable=False,
32
+ index=True,
30
33
  insert_default=Levels.info,
31
34
  server_default=Levels.info,
32
- index=True,
33
- nullable=False
34
35
  )
35
36
  type: Mapped[str | None] = mapped_column(
36
37
  sqlalchemy.TEXT,
38
+ nullable=True,
37
39
  index=True,
38
- insert_default=None,
39
- nullable=True)
40
+ )
40
41
  title: Mapped[str | None] = mapped_column(
41
42
  sqlalchemy.TEXT,
42
- insert_default=None,
43
- nullable=True
43
+ nullable=True,
44
+ index=False
44
45
  )
45
46
 
47
+ def __repr__(self) -> str:
48
+ res = f"{self.entity_name} (id={self.id}, level={self.level}, type{self.type})"
49
+ return res
50
+
46
51
  @validates("level")
47
52
  def _validate_level(self, key, value, *args, **kwargs):
53
+ if not isinstance(value, str):
54
+ raise ValueError(f"{key=}, {value=}, value is not str")
55
+ value = value.strip()
48
56
  self.Levels.parse_and_validate_values(value)
57
+ return value
58
+
59
+ @validates("type")
60
+ def _validate_type(self, key, value, *args, **kwargs):
61
+ if value is None:
62
+ return None
63
+ if not isinstance(value, str):
64
+ raise ValueError(f"{key=}, {value=}, value is not str")
65
+ value = make_none_if_blank(value.strip())
66
+ return value
67
+
68
+ @validates("title")
69
+ def _validate_title(self, key, value, *args, **kwargs):
70
+ if value is None:
71
+ return None
72
+ if not isinstance(value, str):
73
+ raise ValueError(f"{key=}, {value=}, value is not str")
74
+ value = make_none_if_blank(value.strip())
75
+ return value
49
76
 
50
77
  @property
51
78
  def sdp_allowed_levels(self) -> list[str]:
@@ -5,9 +5,12 @@ from typing import TYPE_CHECKING, Any
5
5
  from uuid import uuid4
6
6
 
7
7
  import sqlalchemy
8
- from sqlalchemy.orm import Mapped, mapped_column, relationship
8
+ from email_validator import validate_email
9
+ from sqlalchemy.orm import Mapped, mapped_column, relationship, validates
9
10
 
11
+ from arpakitlib.ar_datetime_util import now_utc_dt
10
12
  from arpakitlib.ar_enumeration_util import Enumeration
13
+ from arpakitlib.ar_str_util import make_none_if_blank
11
14
  from arpakitlib.ar_type_util import raise_for_type
12
15
  from project.sqlalchemy_db_.sqlalchemy_model.common import SimpleDBM
13
16
 
@@ -16,7 +19,11 @@ if TYPE_CHECKING:
16
19
 
17
20
 
18
21
  def generate_default_user_password() -> str:
19
- return str(uuid4()).replace("-", "")
22
+ return (
23
+ "userpassword"
24
+ f"{str(uuid4()).replace('-', '')}"
25
+ f"{str(now_utc_dt().timestamp()).replace('.', '')}"
26
+ )
20
27
 
21
28
 
22
29
  class UserDBM(SimpleDBM):
@@ -28,45 +35,49 @@ class UserDBM(SimpleDBM):
28
35
 
29
36
  email: Mapped[str | None] = mapped_column(
30
37
  sqlalchemy.TEXT,
31
- unique=True,
32
- insert_default=None,
33
- nullable=True
38
+ nullable=True,
39
+ unique=True
40
+ )
41
+ username: Mapped[str | None] = mapped_column(
42
+ sqlalchemy.TEXT,
43
+ nullable=True,
44
+ unique=True
34
45
  )
35
46
  roles: Mapped[list[str]] = mapped_column(
36
47
  sqlalchemy.ARRAY(sqlalchemy.TEXT),
37
- insert_default=[Roles.client],
48
+ nullable=False,
38
49
  index=True,
39
- nullable=False
50
+ insert_default=[Roles.client],
40
51
  )
41
52
  is_active: Mapped[bool] = mapped_column(
42
53
  sqlalchemy.Boolean,
54
+ nullable=False,
43
55
  index=True,
44
56
  insert_default=True,
45
57
  server_default="true",
46
- nullable=False
47
58
  )
48
59
  password: Mapped[str | None] = mapped_column(
49
60
  sqlalchemy.TEXT,
50
- index=True,
51
61
  nullable=True,
62
+ index=True,
52
63
  insert_default=generate_default_user_password,
53
64
  server_default=sqlalchemy.func.gen_random_uuid(),
54
65
  )
55
66
  tg_id: Mapped[int | None] = mapped_column(
56
67
  sqlalchemy.BIGINT,
57
- unique=True,
58
- nullable=True
68
+ nullable=True,
69
+ unique=True
59
70
  )
60
71
  tg_bot_last_action_dt: Mapped[dt.datetime | None] = mapped_column(
61
72
  sqlalchemy.TIMESTAMP(timezone=True),
62
- insert_default=None,
63
73
  nullable=True
64
74
  )
65
- tg_data: Mapped[dict[str, Any] | None] = mapped_column(
75
+ tg_data: Mapped[dict[str, Any]] = mapped_column(
66
76
  sqlalchemy.JSON,
77
+ nullable=False,
78
+ index=False,
67
79
  insert_default={},
68
80
  server_default="{}",
69
- nullable=True
70
81
  )
71
82
 
72
83
  # many to one
@@ -78,11 +89,41 @@ class UserDBM(SimpleDBM):
78
89
  )
79
90
 
80
91
  def __repr__(self) -> str:
92
+ parts = [f"id={self.id}"]
81
93
  if self.email is not None:
82
- res = f"{self.entity_name} (id={self.id}, email={self.email})"
83
- else:
84
- res = f"{self.entity_name} (id={self.id})"
85
- return res
94
+ parts.append(f"email={self.email}")
95
+ if self.username is not None:
96
+ parts.append(f"username={self.username}")
97
+ return f"{self.entity_name} ({', '.join(parts)})"
98
+
99
+ @validates("email")
100
+ def _validate_email(self, key, value, *args, **kwargs):
101
+ if value is None:
102
+ return None
103
+ if not isinstance(value, str):
104
+ raise ValueError(f"{key=}, {value=}, value is not str")
105
+ value = make_none_if_blank(value.strip())
106
+ if value is None:
107
+ return None
108
+ validate_email(value)
109
+ return value
110
+
111
+ @validates("username")
112
+ def _validate_username(self, key, value, *args, **kwargs):
113
+ if value is None:
114
+ return None
115
+ if not isinstance(value, str):
116
+ raise ValueError(f"{key=}, {value=}, value is not str")
117
+ value = make_none_if_blank(value.strip())
118
+ return value
119
+
120
+ @validates("tg_data")
121
+ def _validate_tg_data(self, key, value, *args, **kwargs):
122
+ if value is None:
123
+ value = {}
124
+ if not isinstance(value, dict):
125
+ raise ValueError(f"{key=}, {value=}, value is not str")
126
+ return value
86
127
 
87
128
  @property
88
129
  def sdp_allowed_roles(self) -> list[str]: