zrb 1.0.0b2__py3-none-any.whl → 1.0.0b4__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 (45) hide show
  1. zrb/__main__.py +3 -0
  2. zrb/builtin/llm/llm_chat.py +85 -5
  3. zrb/builtin/llm/previous-session.js +13 -0
  4. zrb/builtin/llm/tool/api.py +29 -0
  5. zrb/builtin/llm/tool/cli.py +1 -1
  6. zrb/builtin/llm/tool/rag.py +108 -145
  7. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/client_method.py +6 -6
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/gateway_subroute.py +3 -1
  9. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_db_repository.py +88 -44
  10. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +12 -0
  11. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/auth_client.py +28 -22
  12. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/3093c7336477_add_auth_tables.py +6 -6
  13. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_db_repository.py +43 -29
  14. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_repository.py +8 -0
  15. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/role_service.py +46 -14
  16. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_db_repository.py +158 -20
  17. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_repository.py +29 -0
  18. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service.py +36 -14
  19. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py +14 -14
  20. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/permission.py +1 -1
  21. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/role.py +34 -6
  22. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/session.py +2 -6
  23. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/user.py +41 -2
  24. zrb/builtin/todo.py +1 -0
  25. zrb/config.py +23 -4
  26. zrb/input/any_input.py +5 -0
  27. zrb/input/base_input.py +6 -0
  28. zrb/input/bool_input.py +2 -0
  29. zrb/input/float_input.py +2 -0
  30. zrb/input/int_input.py +2 -0
  31. zrb/input/option_input.py +2 -0
  32. zrb/input/password_input.py +2 -0
  33. zrb/input/text_input.py +2 -0
  34. zrb/runner/common_util.py +1 -1
  35. zrb/runner/web_route/error_page/show_error_page.py +2 -1
  36. zrb/runner/web_route/static/resources/session/current-session.js +4 -2
  37. zrb/runner/web_route/static/resources/session/event.js +8 -2
  38. zrb/runner/web_route/task_session_api_route.py +48 -3
  39. zrb/task/base_task.py +14 -13
  40. zrb/task/llm_task.py +214 -84
  41. zrb/util/llm/tool.py +3 -7
  42. {zrb-1.0.0b2.dist-info → zrb-1.0.0b4.dist-info}/METADATA +2 -1
  43. {zrb-1.0.0b2.dist-info → zrb-1.0.0b4.dist-info}/RECORD +45 -43
  44. {zrb-1.0.0b2.dist-info → zrb-1.0.0b4.dist-info}/WHEEL +0 -0
  45. {zrb-1.0.0b2.dist-info → zrb-1.0.0b4.dist-info}/entry_points.txt +0 -0
@@ -4,10 +4,15 @@ from typing import Any, Callable, Generic, Type, TypeVar
4
4
 
5
5
  import ulid
6
6
  from my_app_name.common.error import InvalidValueError, NotFoundError
7
- from my_app_name.common.parser_factory import parse_filter_param, parse_sort_param
7
+ from my_app_name.common.parser_factory import (
8
+ parse_filter_param as default_parse_filter_param,
9
+ )
10
+ from my_app_name.common.parser_factory import (
11
+ parse_sort_param as default_parse_sort_param,
12
+ )
8
13
  from sqlalchemy import Engine, delete, func, insert, select, update
9
14
  from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
10
- from sqlalchemy.sql import Select
15
+ from sqlalchemy.sql import ClauseElement, ColumnElement, Select
11
16
  from sqlmodel import Session, SQLModel
12
17
 
13
18
  DBModel = TypeVar("DBModel", bound=SQLModel)
@@ -24,16 +29,48 @@ class BaseDBRepository(Generic[DBModel, ResponseModel, CreateModel, UpdateModel]
24
29
  entity_name: str = "entity"
25
30
  column_preprocessors: dict[str, Callable[[Any], Any]] = {}
26
31
 
27
- def __init__(self, engine: Engine | AsyncEngine):
32
+ def __init__(
33
+ self,
34
+ engine: Engine | AsyncEngine,
35
+ filter_param_parser: (
36
+ Callable[[SQLModel, str], list[ClauseElement]] | None
37
+ ) = None,
38
+ sort_param_parser: Callable[[SQLModel, str], list[ColumnElement]] | None = None,
39
+ ):
28
40
  self.engine = engine
29
- self.is_async = isinstance(engine, AsyncEngine)
41
+ self._is_async = isinstance(engine, AsyncEngine)
42
+ self._parse_filter_param = (
43
+ filter_param_parser if filter_param_parser else default_parse_filter_param
44
+ )
45
+ self._parse_sort_param = (
46
+ sort_param_parser if sort_param_parser else default_parse_sort_param
47
+ )
48
+
49
+ @property
50
+ def is_async(self) -> bool:
51
+ return self._is_async
30
52
 
31
53
  def _select(self) -> Select:
54
+ """
55
+ This method is used to contruct select statement for get, get_by_id and get_by_ids.
56
+ To parse the result of the statement, make sure you override _rows_to_response as well.
57
+ """
32
58
  return select(self.db_model)
33
59
 
34
- def _rows_to_responses(self, rows: list[tuple[Any]]) -> list[ResponseModel]:
60
+ def _rows_to_responses(self, rows: list[tuple[Any, ...]]) -> list[ResponseModel]:
61
+ """
62
+ This method is used to parse the result of select statement generated by _select.
63
+ """
35
64
  return [self.response_model.model_validate(row[0]) for row in rows]
36
65
 
66
+ async def _select_to_response(
67
+ self, query_modifier: Callable[[Select], Any]
68
+ ) -> list[ResponseModel]:
69
+ statement = query_modifier(self._select())
70
+ async with self._session_scope() as session:
71
+ result = await self._execute_statement(session, statement)
72
+ return self._rows_to_responses(result.all())
73
+
37
74
  def _ensure_one(self, responses: list[ResponseModel]) -> ResponseModel:
38
75
  if not responses:
39
76
  raise NotFoundError(f"{self.entity_name} not found")
@@ -41,6 +78,22 @@ class BaseDBRepository(Generic[DBModel, ResponseModel, CreateModel, UpdateModel]
41
78
  raise InvalidValueError(f"Duplicate {self.entity_name}")
42
79
  return responses[0]
43
80
 
81
+ def _model_to_data_dict(
82
+ self, data: SQLModel, **additional_data: Any
83
+ ) -> dict[str, Any]:
84
+ """
85
+ This method transform SQLModel into dictionary for insert/update operation.
86
+ """
87
+ data_dict = data.model_dump(exclude_unset=True)
88
+ data_dict.update(additional_data)
89
+ for key, preprocessor in self.column_preprocessors.items():
90
+ if key not in data_dict:
91
+ continue
92
+ if not hasattr(self.db_model, key):
93
+ raise InvalidValueError(f"Invalid {self.entity_name} property: {key}")
94
+ data_dict[key] = preprocessor(data_dict[key])
95
+ return data_dict
96
+
44
97
  @asynccontextmanager
45
98
  async def _session_scope(self):
46
99
  if self.is_async:
@@ -65,25 +118,18 @@ class BaseDBRepository(Generic[DBModel, ResponseModel, CreateModel, UpdateModel]
65
118
  return session.execute(statement)
66
119
 
67
120
  async def get_by_id(self, id: str) -> ResponseModel:
68
- statement = self._select().where(self.db_model.id == id)
69
- async with self._session_scope() as session:
70
- result = await self._execute_statement(session, statement)
71
- responses = self._rows_to_responses(result.all())
72
- return self._ensure_one(responses)
121
+ rows = await self._select_to_response(lambda q: q.where(self.db_model.id == id))
122
+ return self._ensure_one(rows)
73
123
 
74
124
  async def get_by_ids(self, id_list: list[str]) -> list[ResponseModel]:
75
- statement = self._select().where(self.db_model.id.in_(id_list))
76
- async with self._session_scope() as session:
77
- result = await self._execute_statement(session, statement)
78
- return [
79
- self.db_model(**entity.model_dump())
80
- for entity in result.scalars().all()
81
- ]
125
+ return await self._select_to_response(
126
+ lambda q: q.where(self.db_model.id.in_(id_list))
127
+ )
82
128
 
83
129
  async def count(self, filter: str | None = None) -> int:
84
130
  count_statement = select(func.count(1)).select_from(self.db_model)
85
131
  if filter:
86
- filter_param = parse_filter_param(self.db_model, filter)
132
+ filter_param = self._parse_filter_param(self.db_model, filter)
87
133
  count_statement = count_statement.where(*filter_param)
88
134
  async with self._session_scope() as session:
89
135
  result = await self._execute_statement(session, count_statement)
@@ -96,33 +142,31 @@ class BaseDBRepository(Generic[DBModel, ResponseModel, CreateModel, UpdateModel]
96
142
  filter: str | None = None,
97
143
  sort: str | None = None,
98
144
  ) -> list[ResponseModel]:
99
- offset = (page - 1) * page_size
100
- statement = self._select().offset(offset).limit(page_size)
101
- if filter:
102
- filter_param = parse_filter_param(self.db_model, filter)
103
- statement = statement.where(*filter_param)
104
- if sort:
105
- sort_param = parse_sort_param(self.db_model, sort)
106
- statement = statement.order_by(*sort_param)
107
- async with self._session_scope() as session:
108
- result = await self._execute_statement(session, statement)
109
- return [
110
- self.db_model(**entity.model_dump())
111
- for entity in result.scalars().all()
112
- ]
145
+ return await self._select_to_response(
146
+ self._get_pagination_query_modifier(
147
+ page=page, page_size=page_size, filter=filter, sort=sort
148
+ )
149
+ )
113
150
 
114
- def _model_to_data_dict(
115
- self, data: SQLModel, **additional_data: Any
116
- ) -> dict[str, Any]:
117
- data_dict = data.model_dump(exclude_unset=True)
118
- data_dict.update(additional_data)
119
- for key, preprocessor in self.column_preprocessors.items():
120
- if key not in data_dict:
121
- continue
122
- if not hasattr(self.db_model, key):
123
- raise InvalidValueError(f"Invalid {self.entity_name} property: {key}")
124
- data_dict[key] = preprocessor(data_dict[key])
125
- return data_dict
151
+ def _get_pagination_query_modifier(
152
+ self,
153
+ page: int = 1,
154
+ page_size: int = 10,
155
+ filter: str | None = None,
156
+ sort: str | None = None,
157
+ ) -> Callable[[Select], Any]:
158
+ def pagination_query_modifier(statement: Select) -> Any:
159
+ offset = (page - 1) * page_size
160
+ statement = statement.offset(offset).limit(page_size)
161
+ if filter:
162
+ filter_param = self._parse_filter_param(self.db_model, filter)
163
+ statement = statement.where(*filter_param)
164
+ if sort:
165
+ sort_param = self._parse_sort_param(self.db_model, sort)
166
+ statement = statement.order_by(*sort_param)
167
+ return statement
168
+
169
+ return pagination_query_modifier
126
170
 
127
171
  async def create(self, data: CreateModel) -> DBModel:
128
172
  now = datetime.datetime.now(datetime.timezone.utc)
@@ -50,5 +50,17 @@ APP_AUTH_SUPER_USER = os.getenv("MY_APP_NAME_AUTH_SUPER_USER", "admin")
50
50
  APP_AUTH_SUPER_USER_PASSWORD = os.getenv(
51
51
  "MY_APP_NAME_AUTH_SUPER_USER_PASSWORD", "my-secure-password"
52
52
  )
53
+ APP_AUTH_GUEST_USER = os.getenv("MY_APP_NAME_AUTH_GUEST_USER", "user")
54
+ APP_AUTH_GUEST_USER_PERMISSIONS = (
55
+ permission_name.strip()
56
+ for permission_name in os.getenv(
57
+ "MY_APP_NAME_AUTH_GUEST_USER_PERMISSIONS", ""
58
+ ).split(",")
59
+ if permission_name.strip() != ""
60
+ )
61
+ APP_MAX_PARALLEL_SESSION = int(os.getenv("MY_APP_NAME_MAX_PARALLEL_SESSION", "1"))
62
+ APP_SESSION_EXPIRE_MINUTES = int(
63
+ os.getenv("MY_APP_NAME_SESSION_EXPIRE_MINUTES", "1440")
64
+ )
53
65
 
54
66
  APP_AUTH_BASE_URL = os.getenv("MY_APP_NAME_AUTH_BASE_URL", "http://localhost:3001")
@@ -8,15 +8,15 @@ from my_app_name.schema.permission import (
8
8
  )
9
9
  from my_app_name.schema.role import (
10
10
  MultipleRoleResponse,
11
- RoleCreateWithAudit,
11
+ RoleCreateWithPermissionsAndAudit,
12
12
  RoleResponse,
13
- RoleUpdateWithAudit,
13
+ RoleUpdateWithPermissionsAndAudit,
14
14
  )
15
15
  from my_app_name.schema.user import (
16
16
  MultipleUserResponse,
17
- UserCreateWithAudit,
17
+ UserCreateWithRolesAndAudit,
18
18
  UserResponse,
19
- UserUpdateWithAudit,
19
+ UserUpdateWithRolesAndAudit,
20
20
  )
21
21
 
22
22
 
@@ -38,18 +38,18 @@ class AuthClient(ABC):
38
38
  ) -> MultiplePermissionResponse:
39
39
  """Get permissions by filter and sort"""
40
40
 
41
+ @abstractmethod
42
+ async def create_permission_bulk(
43
+ self, data: list[PermissionCreateWithAudit]
44
+ ) -> list[PermissionResponse]:
45
+ """Create new permissions"""
46
+
41
47
  @abstractmethod
42
48
  async def create_permission(
43
49
  self, data: PermissionCreateWithAudit
44
50
  ) -> PermissionResponse:
45
51
  """Create a new permission"""
46
52
 
47
- @abstractmethod
48
- async def create_permission(
49
- self, data: list[PermissionCreateWithAudit]
50
- ) -> list[PermissionResponse]:
51
- """Create new permissions"""
52
-
53
53
  @abstractmethod
54
54
  async def update_permission_bulk(
55
55
  self, permission_ids: list[str], data: PermissionUpdateWithAudit
@@ -91,22 +91,26 @@ class AuthClient(ABC):
91
91
  """Get roles by filter and sort"""
92
92
 
93
93
  @abstractmethod
94
- async def create_role(self, data: RoleCreateWithAudit) -> RoleResponse:
95
- """Create a new role"""
94
+ async def create_role_bulk(
95
+ self, data: list[RoleCreateWithPermissionsAndAudit]
96
+ ) -> list[RoleResponse]:
97
+ """Create new roles"""
96
98
 
97
99
  @abstractmethod
98
- async def create_role(self, data: list[RoleCreateWithAudit]) -> list[RoleResponse]:
99
- """Create new roles"""
100
+ async def create_role(
101
+ self, data: RoleCreateWithPermissionsAndAudit
102
+ ) -> RoleResponse:
103
+ """Create a new role"""
100
104
 
101
105
  @abstractmethod
102
106
  async def update_role_bulk(
103
- self, role_ids: list[str], data: RoleUpdateWithAudit
107
+ self, role_ids: list[str], data: RoleUpdateWithPermissionsAndAudit
104
108
  ) -> RoleResponse:
105
109
  """Update some roles"""
106
110
 
107
111
  @abstractmethod
108
112
  async def update_role(
109
- self, role_id: str, data: RoleUpdateWithAudit
113
+ self, role_id: str, data: RoleUpdateWithPermissionsAndAudit
110
114
  ) -> RoleResponse:
111
115
  """Update a role"""
112
116
 
@@ -135,22 +139,24 @@ class AuthClient(ABC):
135
139
  """Get users by filter and sort"""
136
140
 
137
141
  @abstractmethod
138
- async def create_user(self, data: UserCreateWithAudit) -> UserResponse:
139
- """Create a new user"""
142
+ async def create_user_bulk(
143
+ self, data: list[UserCreateWithRolesAndAudit]
144
+ ) -> list[UserResponse]:
145
+ """Create new users"""
140
146
 
141
147
  @abstractmethod
142
- async def create_user(self, data: list[UserCreateWithAudit]) -> list[UserResponse]:
143
- """Create new users"""
148
+ async def create_user(self, data: UserCreateWithRolesAndAudit) -> UserResponse:
149
+ """Create a new user"""
144
150
 
145
151
  @abstractmethod
146
152
  async def update_user_bulk(
147
- self, user_ids: list[str], data: UserUpdateWithAudit
153
+ self, user_ids: list[str], data: UserUpdateWithRolesAndAudit
148
154
  ) -> UserResponse:
149
155
  """Update some users"""
150
156
 
151
157
  @abstractmethod
152
158
  async def update_user(
153
- self, user_id: str, data: UserUpdateWithAudit
159
+ self, user_id: str, data: UserUpdateWithRolesAndAudit
154
160
  ) -> UserResponse:
155
161
  """Update a user"""
156
162
 
@@ -32,7 +32,7 @@ def upgrade() -> None:
32
32
  sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
33
33
  sa.PrimaryKeyConstraint("id"),
34
34
  )
35
- op.create_index(op.f("ix_permission_name"), "permission", ["name"], unique=False)
35
+ op.create_index(op.f("ix_permission_name"), "permission", ["name"], unique=True)
36
36
  op.create_index(
37
37
  op.f("ix_permission_created_at"), "permission", ["created_at"], unique=False
38
38
  )
@@ -57,7 +57,7 @@ def upgrade() -> None:
57
57
  sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
58
58
  sa.PrimaryKeyConstraint("id"),
59
59
  )
60
- op.create_index(op.f("ix_role_name"), "role", ["name"], unique=False)
60
+ op.create_index(op.f("ix_role_name"), "role", ["name"], unique=True)
61
61
  op.create_index(op.f("ix_role_created_at"), "role", ["created_at"], unique=False)
62
62
  op.create_index(op.f("ix_role_created_by"), "role", ["created_by"], unique=False)
63
63
  op.create_index(op.f("ix_role_updated_at"), "role", ["updated_at"], unique=False)
@@ -93,7 +93,7 @@ def upgrade() -> None:
93
93
  sa.Column("updated_by", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
94
94
  sa.PrimaryKeyConstraint("id"),
95
95
  )
96
- op.create_index(op.f("ix_user_username"), "user", ["username"], unique=False)
96
+ op.create_index(op.f("ix_user_username"), "user", ["username"], unique=True)
97
97
  op.create_index(op.f("ix_user_created_at"), "user", ["created_at"], unique=False)
98
98
  op.create_index(op.f("ix_user_created_by"), "user", ["created_by"], unique=False)
99
99
  op.create_index(op.f("ix_user_updated_at"), "user", ["updated_at"], unique=False)
@@ -115,17 +115,17 @@ def upgrade() -> None:
115
115
  "session",
116
116
  sa.Column("id", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
117
117
  sa.Column("user_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
118
- sa.Column("device", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
119
- sa.Column("os", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
120
- sa.Column("browser", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
118
+ sa.Column("token", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
121
119
  sa.PrimaryKeyConstraint("id"),
122
120
  )
123
121
  op.create_index(op.f("ix_session_user_id"), "session", ["user_id"], unique=False)
122
+ op.create_index(op.f("ix_session_token"), "session", ["token"], unique=True)
124
123
  # ### end Alembic commands ###
125
124
 
126
125
 
127
126
  def downgrade() -> None:
128
127
  # ### commands auto generated by Alembic - please adjust! ###
128
+ op.drop_index(op.f("ix_session_token"), table_name="session")
129
129
  op.drop_index(op.f("ix_session_user_id"), table_name="session")
130
130
  op.drop_table("session")
131
131
 
@@ -1,5 +1,7 @@
1
+ import datetime
1
2
  from typing import Any
2
3
 
4
+ import ulid
3
5
  from my_app_name.common.base_db_repository import BaseDBRepository
4
6
  from my_app_name.module.auth.service.role.repository.role_repository import (
5
7
  RoleRepository,
@@ -13,7 +15,7 @@ from my_app_name.schema.role import (
13
15
  RoleUpdateWithAudit,
14
16
  )
15
17
  from sqlalchemy.sql import Select
16
- from sqlmodel import select
18
+ from sqlmodel import delete, insert, select
17
19
 
18
20
 
19
21
  class RoleDBRepository(
@@ -35,41 +37,53 @@ class RoleDBRepository(
35
37
  return (
36
38
  select(Role, Permission)
37
39
  .join(RolePermission, RolePermission.role_id == Role.id, isouter=True)
38
- .join(Permission, Permission.id == RolePermission.role_id, isouter=True)
40
+ .join(
41
+ Permission, Permission.id == RolePermission.permission_id, isouter=True
42
+ )
39
43
  )
40
44
 
41
- def _rows_to_responses(self, rows: list[tuple[Role, Permission]]) -> RoleResponse:
45
+ def _rows_to_responses(self, rows: list[tuple[Any, ...]]) -> list[RoleResponse]:
42
46
  role_map: dict[str, dict[str, Any]] = {}
47
+ role_permission_map: dict[str, list[str]] = {}
43
48
  for role, permission in rows:
44
49
  if role.id not in role_map:
45
- role_map[role.id] = {"role": role, "permissions": set()}
46
- if permission:
47
- role_map[role.id]["permissions"].add(permission)
50
+ role_map[role.id] = {"role": role, "permissions": []}
51
+ role_permission_map[role.id] = []
52
+ if (
53
+ permission is not None
54
+ and permission.id not in role_permission_map[role.id]
55
+ ):
56
+ role_permission_map[role.id].append(permission.id)
57
+ role_map[role.id]["permissions"].append(permission.model_dump())
48
58
  return [
49
- RoleResponse(
50
- **data["role"].model_dump(), permissions=list(data["permissions"])
51
- )
59
+ RoleResponse(**data["role"].model_dump(), permissions=data["permissions"])
52
60
  for data in role_map.values()
53
61
  ]
54
62
 
55
- async def add_permissions(self, role_id, permission_names: list[str]) -> Role:
56
- # TODO: complete this
57
- select_statement = self._select().where(self.db_model.id == role_id)
58
- rows = await self._execute_select_statement(select_statement)
59
- responses = self._rows_to_responses(rows)
60
- return self._ensure_one(responses)
61
-
62
- async def remove_permissions(self, role_id, permission_names: list[str]) -> Role:
63
- # TODO: complete this
64
- select_statement = self._select().where(self.db_model.id == role_id)
65
- rows = await self._execute_select_statement(select_statement)
66
- responses = self._rows_to_responses(rows)
67
- return self._ensure_one(responses)
68
-
63
+ async def add_permissions(self, data: dict[str, list[str]], created_by: str):
64
+ now = datetime.datetime.now(datetime.timezone.utc)
65
+ data_dict_list: list[dict[str, Any]] = []
66
+ for role_id, permission_ids in data.items():
67
+ for permission_id in permission_ids:
68
+ data_dict_list.append(
69
+ self._model_to_data_dict(
70
+ RolePermission(
71
+ id=ulid.new().str,
72
+ role_id=role_id,
73
+ permission_id=permission_id,
74
+ created_at=now,
75
+ created_by=created_by,
76
+ )
77
+ )
78
+ )
79
+ async with self._session_scope() as session:
80
+ await self._execute_statement(
81
+ session, insert(RolePermission).values(data_dict_list)
82
+ )
69
83
 
70
- def _remove_create_model_additional_property(
71
- data: RoleCreateWithAudit,
72
- ) -> RoleCreateWithAudit:
73
- return RoleCreateWithAudit(
74
- **{key: val for key, val in data.model_dump().items() if key != "permissions"}
75
- )
84
+ async def remove_all_permissions(self, role_ids: list[str] = []):
85
+ async with self._session_scope() as session:
86
+ await self._execute_statement(
87
+ session,
88
+ delete(RolePermission).where(RolePermission.role_id._in(role_ids)),
89
+ )
@@ -18,6 +18,14 @@ class RoleRepository(ABC):
18
18
  async def get_by_ids(self, id_list: list[str]) -> RoleResponse:
19
19
  """Get roles by ids"""
20
20
 
21
+ @abstractmethod
22
+ async def add_permissions(self, data: dict[str, list[str]], created_by: str):
23
+ """Adding permissions to roles"""
24
+
25
+ @abstractmethod
26
+ async def remove_all_permissions(self, role_ids: list[str] = []):
27
+ """Remove permissions from roles"""
28
+
21
29
  @abstractmethod
22
30
  async def get(
23
31
  self,
@@ -6,9 +6,9 @@ from my_app_name.module.auth.service.role.repository.role_repository import (
6
6
  )
7
7
  from my_app_name.schema.role import (
8
8
  MultipleRoleResponse,
9
- RoleCreateWithAudit,
9
+ RoleCreateWithPermissionsAndAudit,
10
10
  RoleResponse,
11
- RoleUpdateWithAudit,
11
+ RoleUpdateWithPermissionsAndAudit,
12
12
  )
13
13
 
14
14
 
@@ -42,35 +42,61 @@ class RoleService(BaseService):
42
42
  count = await self.role_repository.count(filter)
43
43
  return MultipleRoleResponse(data=roles, count=count)
44
44
 
45
- @BaseService.route(
46
- "/api/v1/roles",
47
- methods=["post"],
48
- response_model=RoleResponse,
49
- )
50
- async def create_role(self, data: RoleCreateWithAudit) -> RoleResponse:
51
- role = await self.role_repository.create(data)
52
- return await self.role_repository.get_by_id(role.id)
53
-
54
45
  @BaseService.route(
55
46
  "/api/v1/roles/bulk",
56
47
  methods=["post"],
57
48
  response_model=list[RoleResponse],
58
49
  )
59
50
  async def create_role_bulk(
60
- self, data: list[RoleCreateWithAudit]
51
+ self, data: list[RoleCreateWithPermissionsAndAudit]
61
52
  ) -> list[RoleResponse]:
53
+ permission_ids = [row.get_permission_ids() for row in data]
54
+ data = [row.get_role_create_with_audit() for row in data]
62
55
  roles = await self.role_repository.create_bulk(data)
56
+ if len(roles) > 0:
57
+ created_by = roles[0].created_by
58
+ await self.role_repository.add_permissions(
59
+ data={role.id: permission_ids[i] for i, role in enumerate(roles)},
60
+ created_by=created_by,
61
+ )
63
62
  return await self.role_repository.get_by_ids([role.id for role in roles])
64
63
 
64
+ @BaseService.route(
65
+ "/api/v1/roles",
66
+ methods=["post"],
67
+ response_model=RoleResponse,
68
+ )
69
+ async def create_role(
70
+ self, data: RoleCreateWithPermissionsAndAudit
71
+ ) -> RoleResponse:
72
+ permission_ids = data.get_permission_ids()
73
+ data = data.get_role_create_with_audit()
74
+ role = await self.role_repository.create(data)
75
+ await self.role_repository.add_permissions(
76
+ data={role.id: permission_ids}, created_by=role.created_by
77
+ )
78
+ return await self.role_repository.get_by_id(role.id)
79
+
65
80
  @BaseService.route(
66
81
  "/api/v1/roles/bulk",
67
82
  methods=["put"],
68
83
  response_model=RoleResponse,
69
84
  )
70
85
  async def update_role_bulk(
71
- self, role_ids: list[str], data: RoleUpdateWithAudit
86
+ self, role_ids: list[str], data: RoleUpdateWithPermissionsAndAudit
72
87
  ) -> RoleResponse:
88
+ permission_ids = [row.get_permission_ids() for row in data]
89
+ data = [row.get_role_update_with_audit() for row in data]
73
90
  roles = await self.role_repository.update_bulk(role_ids, data)
91
+ if len(roles) > 0:
92
+ updated_by = roles[0].updated_by
93
+ await self.role_repository.remove_all_permissions(
94
+ [role.id for role in roles]
95
+ )
96
+ await self.role_repository.add_permissions(
97
+ data={role.id: permission_ids[i] for i, role in enumerate(roles)},
98
+ created_by=updated_by,
99
+ )
74
100
  return await self.role_repository.get_by_ids([role.id for role in roles])
75
101
 
76
102
  @BaseService.route(
@@ -79,9 +105,15 @@ class RoleService(BaseService):
79
105
  response_model=RoleResponse,
80
106
  )
81
107
  async def update_role(
82
- self, role_id: str, data: RoleUpdateWithAudit
108
+ self, role_id: str, data: RoleUpdateWithPermissionsAndAudit
83
109
  ) -> RoleResponse:
110
+ permission_ids = data.get_permission_ids()
111
+ data = data.get_role_update_with_audit()
84
112
  role = await self.role_repository.update(role_id, data)
113
+ await self.role_repository.remove_all_permissions([role.id])
114
+ await self.role_repository.add_permissions(
115
+ data={role.id: permission_ids}, created_by=role.updated_by
116
+ )
85
117
  return await self.role_repository.get_by_id(role.id)
86
118
 
87
119
  @BaseService.route(