zrb 1.0.0b7__py3-none-any.whl → 1.0.0b9__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 (40) hide show
  1. zrb/__main__.py +3 -0
  2. zrb/builtin/git.py +15 -15
  3. zrb/builtin/git_subtree.py +6 -6
  4. zrb/builtin/project/add/fastapp/fastapp_task.py +1 -0
  5. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +1 -0
  6. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/my_module/service/my_entity/my_entity_service.py +5 -5
  7. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/schema/my_entity.py +1 -0
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/input.py +8 -0
  9. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task.py +9 -2
  10. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task_util.py +100 -0
  11. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/util.py +6 -86
  12. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_db_repository.py +27 -11
  13. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_service.py +32 -27
  14. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/error.py +15 -0
  15. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +22 -5
  16. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/auth_client.py +21 -0
  17. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/3093c7336477_add_auth_tables.py +103 -61
  18. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration_metadata.py +3 -4
  19. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/route.py +15 -14
  20. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/permission_service.py +4 -4
  21. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_db_repository.py +24 -5
  22. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/role_service.py +14 -12
  23. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_db_repository.py +130 -96
  24. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_repository.py +28 -11
  25. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service.py +220 -13
  26. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service_factory.py +30 -2
  27. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py +27 -2
  28. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/requirements.txt +2 -1
  29. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/permission.py +1 -0
  30. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/role.py +13 -12
  31. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/user.py +55 -12
  32. zrb/task/cmd_task.py +4 -7
  33. zrb/util/cmd/command.py +41 -50
  34. zrb/util/git.py +18 -18
  35. zrb/util/git_subtree.py +6 -6
  36. {zrb-1.0.0b7.dist-info → zrb-1.0.0b9.dist-info}/METADATA +2 -1
  37. {zrb-1.0.0b7.dist-info → zrb-1.0.0b9.dist-info}/RECORD +39 -39
  38. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/session.py +0 -48
  39. {zrb-1.0.0b7.dist-info → zrb-1.0.0b9.dist-info}/WHEEL +0 -0
  40. {zrb-1.0.0b7.dist-info → zrb-1.0.0b9.dist-info}/entry_points.txt +0 -0
@@ -1,35 +1,27 @@
1
1
  import datetime
2
- from typing import Any, Callable
2
+ from typing import Any
3
3
 
4
4
  import ulid
5
5
  from my_app_name.common.base_db_repository import BaseDBRepository
6
- from my_app_name.common.error import NotFoundError
7
- from my_app_name.config import (
8
- APP_AUTH_GUEST_USER,
9
- APP_AUTH_GUEST_USER_PERMISSIONS,
10
- APP_AUTH_SUPER_USER,
11
- APP_AUTH_SUPER_USER_PASSWORD,
12
- APP_MAX_PARALLEL_SESSION,
13
- APP_SESSION_EXPIRE_MINUTES,
14
- )
6
+ from my_app_name.common.error import NotFoundError, UnauthorizedError
15
7
  from my_app_name.module.auth.service.user.repository.user_repository import (
16
8
  UserRepository,
17
9
  )
18
10
  from my_app_name.schema.permission import Permission
19
11
  from my_app_name.schema.role import Role, RolePermission
20
- from my_app_name.schema.session import Session, SessionResponse
21
12
  from my_app_name.schema.user import (
22
13
  User,
23
14
  UserCreateWithAudit,
24
15
  UserResponse,
25
16
  UserRole,
17
+ UserSession,
18
+ UserSessionResponse,
19
+ UserTokenData,
26
20
  UserUpdateWithAudit,
27
21
  )
28
22
  from passlib.context import CryptContext
29
- from sqlalchemy.engine import Engine
30
- from sqlalchemy.ext.asyncio import AsyncEngine
31
- from sqlalchemy.sql import ClauseElement, ColumnElement, Select
32
- from sqlmodel import SQLModel, delete, insert, select
23
+ from sqlalchemy.sql import Select
24
+ from sqlmodel import delete, insert, select, update
33
25
 
34
26
  # Password hashing context
35
27
  pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
@@ -39,6 +31,11 @@ def hash_password(password: str) -> str:
39
31
  return pwd_context.hash(password)
40
32
 
41
33
 
34
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
35
+ """Verifies if a password matches the stored hash."""
36
+ return pwd_context.verify(plain_password, hashed_password)
37
+
38
+
42
39
  class UserDBRepository(
43
40
  BaseDBRepository[User, UserResponse, UserCreateWithAudit, UserUpdateWithAudit],
44
41
  UserRepository,
@@ -50,46 +47,15 @@ class UserDBRepository(
50
47
  entity_name = "user"
51
48
  column_preprocessors = {"password": hash_password}
52
49
 
53
- def __init__(
54
- self,
55
- engine: Engine | AsyncEngine,
56
- super_user_username: str = APP_AUTH_SUPER_USER,
57
- super_user_password: str = APP_AUTH_SUPER_USER_PASSWORD,
58
- guest_user_username: str = APP_AUTH_GUEST_USER,
59
- guest_user_password: str = APP_AUTH_SUPER_USER_PASSWORD,
60
- guest_user_permission_names: list[str] = APP_AUTH_GUEST_USER_PERMISSIONS,
61
- max_parallel_session: int = APP_MAX_PARALLEL_SESSION,
62
- session_expire_minutes: int = APP_SESSION_EXPIRE_MINUTES,
63
- filter_param_parser: (
64
- Callable[[SQLModel, str], list[ClauseElement]] | None
65
- ) = None,
66
- sort_param_parser: Callable[[SQLModel, str], list[ColumnElement]] | None = None,
67
- ):
68
- super().__init__(
69
- engine=engine,
70
- filter_param_parser=filter_param_parser,
71
- sort_param_parser=sort_param_parser,
72
- )
73
- self._super_user_username = super_user_username
74
- self._super_user_passwored = super_user_password
75
- self._guest_user_username = guest_user_username
76
- self._guest_user_password = guest_user_password
77
- self._guest_user_permission_names = guest_user_permission_names
78
- self._max_parallel_session = max_parallel_session
79
- self._session_expire_minutes = session_expire_minutes
80
- self._super_user: User | None = None
81
- self._guest_user: User | None = None
82
-
83
50
  def _select(self) -> Select:
84
51
  return (
85
- select(User, Role, Permission, Session)
52
+ select(User, Role, Permission, UserSession)
86
53
  .join(UserRole, UserRole.user_id == User.id, isouter=True)
87
54
  .join(Role, Role.id == UserRole.role_id, isouter=True)
88
55
  .join(RolePermission, RolePermission.role_id == Role.id, isouter=True)
89
56
  .join(
90
57
  Permission, Permission.id == RolePermission.permission_id, isouter=True
91
58
  )
92
- .join(Session, Session.user_id == User.id)
93
59
  )
94
60
 
95
61
  def _rows_to_responses(self, rows: list[tuple[Any, ...]]) -> list[UserResponse]:
@@ -103,33 +69,43 @@ class UserDBRepository(
103
69
  user_permission_map[user.id] = []
104
70
  if role is not None and role.id not in user_role_map[user.id]:
105
71
  user_role_map[user.id].append(role.id)
106
- user_map[user.id]["roles"].append(role.model_dump())
72
+ user_map[user.id]["roles"].append(role)
107
73
  if (
108
74
  permission is not None
109
75
  and permission.id not in user_permission_map[user.id]
110
76
  ):
111
77
  user_permission_map[user.id].append(permission.id)
112
- user_map[user.id]["permissions"].append(permission.model_dump())
78
+ user_map[user.id]["permissions"].append(permission)
113
79
  return [
114
80
  UserResponse(
115
81
  **data["user"].model_dump(),
116
- roles=list(data["roles"]),
117
- permissions=list(data["permissions"]),
82
+ role_names=[role.name for role in data["roles"]],
83
+ permission_names=[
84
+ permission.name for permission in data["permissions"]
85
+ ],
118
86
  )
119
87
  for data in user_map.values()
120
88
  ]
121
89
 
122
90
  async def add_roles(self, data: dict[str, list[str]], created_by: str):
123
91
  now = datetime.datetime.now(datetime.timezone.utc)
92
+ # get mapping from role names to role ids
93
+ all_role_names = {name for role_names in data.values() for name in role_names}
94
+ async with self._session_scope() as session:
95
+ result = await self._execute_statement(
96
+ session, select(Role.id, Role.name).where(Role.name.in_(all_role_names))
97
+ )
98
+ role_mapping = {row.name: row.id for row in result}
99
+ # Assemble data dict
124
100
  data_dict_list: list[dict[str, Any]] = []
125
- for user_id, role_ids in data.items():
126
- for role_id in role_ids:
101
+ for user_id, role_names in data.items():
102
+ for role_name in role_names:
127
103
  data_dict_list.append(
128
104
  self._model_to_data_dict(
129
105
  UserRole(
130
106
  id=ulid.new().str,
131
107
  user_id=user_id,
132
- role_id=role_id,
108
+ role_id=role_mapping.get(role_name),
133
109
  created_at=now,
134
110
  created_by=created_by,
135
111
  )
@@ -148,65 +124,123 @@ class UserDBRepository(
148
124
  )
149
125
 
150
126
  async def get_by_credentials(self, username: str, password: str) -> UserResponse:
151
- rows = await self._select_to_response(
152
- lambda q: q.where(
153
- User.username == username, User.password == hash_password(password)
127
+ async with self._session_scope() as session:
128
+ result = await self._execute_statement(
129
+ session, select(User).where(User.username == username, User.active)
154
130
  )
155
- )
156
- return self._ensure_one(rows)
131
+ user = result.scalar_one_or_none()
132
+ if user is None or not verify_password(password, user.password):
133
+ raise UnauthorizedError("Invalid username or password")
134
+ return await self.get_by_id(user.id)
157
135
 
158
- async def get_by_token(self, token: str) -> UserResponse:
159
- rows = await self._select_tor_response(
160
- lambda q: q.where(Session.token == token)
161
- )
162
- return self._ensure_one(rows)
163
-
164
- async def add_token(self, user_id: str, token: str):
136
+ async def delete_expired_user_sessions(self, user_id: str):
137
+ now = datetime.datetime.now(datetime.timezone.utc)
165
138
  async with self._session_scope() as session:
166
139
  await self._execute_statement(
167
140
  session,
168
- insert(Session).values(
169
- {
170
- "id": ulid.new().str,
171
- "user_id": user_id,
172
- "token": token,
173
- "created_by": "system",
174
- "created_at": datetime.datetime.now(datetime.timezone.utc),
175
- }
141
+ delete(UserSession).where(
142
+ UserSession.user_id == user_id,
143
+ UserSession.refresh_token_expired_at < now,
176
144
  ),
177
145
  )
178
146
 
179
- async def remove_token(self, user_id: str, token: str):
147
+ async def get_active_user_sessions(self, user_id: str) -> list[UserSessionResponse]:
148
+ now = datetime.datetime.now(datetime.timezone.utc)
180
149
  async with self._session_scope() as session:
181
- await self._execute_statement(
150
+ result = await self._execute_statement(
182
151
  session,
183
- delete(Session).where(
184
- Session.token == token, Session.user_id == user_id
152
+ select(UserSession).where(
153
+ UserSession.user_id == user_id,
154
+ UserSession.refresh_token_expired_at > now,
185
155
  ),
186
156
  )
157
+ return [self._user_session_to_response(row[0]) for row in result.all()]
187
158
 
188
- async def get_sessions(self, user_id: str) -> list[SessionResponse]:
159
+ async def get_user_session_by_access_token(
160
+ self, access_token: str
161
+ ) -> UserSessionResponse:
162
+ now = datetime.datetime.now(datetime.timezone.utc)
163
+ async with self._session_scope() as session:
164
+ result = await self._execute_statement(
165
+ session,
166
+ select(UserSession).where(
167
+ UserSession.access_token == access_token,
168
+ UserSession.access_token_expired_at > now,
169
+ ),
170
+ )
171
+ user_session = result.scalar_one_or_none()
172
+ if user_session is None:
173
+ raise NotFoundError("User session not found")
174
+ return self._user_session_to_response(user_session)
175
+
176
+ async def get_user_session_by_refresh_token(
177
+ self, refresh_token: str
178
+ ) -> UserSessionResponse:
179
+ now = datetime.datetime.now(datetime.timezone.utc)
189
180
  async with self._session_scope() as session:
190
- statement = select(Session).where(Session.user_id == user_id)
191
- result = await self._execute_statement(session, statement)
192
- return [
193
- SessionResponse(**session.model_dump())
194
- for session in result.scalars().all()
195
- ]
196
-
197
- async def remove_session(self, user_id: str, session_id: str) -> SessionResponse:
181
+ result = await self._execute_statement(
182
+ session,
183
+ select(UserSession).where(
184
+ UserSession.refresh_token == refresh_token,
185
+ UserSession.refresh_token_expired_at > now,
186
+ ),
187
+ )
188
+ user_session = result.scalar_one_or_none()
189
+ if user_session is None:
190
+ raise NotFoundError("User session not found")
191
+ return self._user_session_to_response(user_session)
192
+
193
+ async def create_user_session(
194
+ self, user_id: str, token_data: UserTokenData
195
+ ) -> UserSessionResponse:
196
+ data_dict = self._model_to_data_dict(
197
+ token_data, user_id=user_id, id=ulid.new().str
198
+ )
198
199
  async with self._session_scope() as session:
199
- statement = select(Session).where(
200
- Session.user_id == user_id, Session.id == session_id
200
+ await self._execute_statement(
201
+ session, insert(UserSession).values(**data_dict)
202
+ )
203
+ result = await self._execute_statement(
204
+ session, select(UserSession).where(UserSession.id == data_dict["id"])
201
205
  )
202
- result = await self._execute_statement(session, statement)
203
- session = result.scalar_one_or_none()
204
- if not session:
205
- raise NotFoundError(f"{self.entity_name} not found")
206
+ user_session = result.scalar_one_or_none()
207
+ if user_session is None:
208
+ raise NotFoundError("User session not found after created")
209
+ return self._user_session_to_response(user_session)
210
+
211
+ async def update_user_session(
212
+ self, user_id: str, session_id: str, token_data: UserTokenData
213
+ ) -> UserSessionResponse:
214
+ data_dict = self._model_to_data_dict(token_data, user_id=user_id)
215
+ async with self._session_scope() as session:
206
216
  await self._execute_statement(
207
217
  session,
208
- delete(Session).where(
209
- Session.id == session_id, Session.user_id == user_id
218
+ (
219
+ update(UserSession)
220
+ .where(UserSession.id == session_id)
221
+ .values(**data_dict)
210
222
  ),
211
223
  )
212
- return SessionResponse(**session.model_dump())
224
+ result = await self._execute_statement(
225
+ session, select(UserSession).where(UserSession.id == session_id)
226
+ )
227
+ user_session = result.scalar_one_or_none()
228
+ if user_session is None:
229
+ raise NotFoundError("User session not found after created")
230
+ return self._user_session_to_response(user_session)
231
+
232
+ async def delete_user_sessions(self, session_ids: list[str]):
233
+ async with self._session_scope() as session:
234
+ await self._execute_statement(
235
+ session, delete(UserSession).where(UserSession.id.in_(session_ids))
236
+ )
237
+
238
+ def _user_session_to_response(
239
+ self, user_session: UserSession
240
+ ) -> UserSessionResponse:
241
+ return UserSessionResponse(
242
+ id=user_session.id,
243
+ user_id=user_session.user_id,
244
+ access_token_expired_at=user_session.access_token_expired_at,
245
+ refresh_token_expired_at=user_session.refresh_token_expired_at,
246
+ )
@@ -1,10 +1,11 @@
1
1
  from abc import ABC, abstractmethod
2
2
 
3
- from my_app_name.schema.session import SessionResponse
4
3
  from my_app_name.schema.user import (
5
4
  User,
6
5
  UserCreateWithAudit,
7
6
  UserResponse,
7
+ UserSessionResponse,
8
+ UserTokenData,
8
9
  UserUpdateWithAudit,
9
10
  )
10
11
 
@@ -72,21 +73,37 @@ class UserRepository(ABC):
72
73
  """Get user by credential"""
73
74
 
74
75
  @abstractmethod
75
- async def get_by_token(self, token: str) -> UserResponse:
76
- """Get user by token"""
76
+ async def get_active_user_sessions(self, user_id: str) -> list[UserSessionResponse]:
77
+ """Get user sessions"""
77
78
 
78
79
  @abstractmethod
79
- async def add_token(self, user_id: str, token: str):
80
- """Add token to user"""
80
+ async def get_user_session_by_access_token(
81
+ self, access_token: str
82
+ ) -> UserSessionResponse:
83
+ """Get user session by access token"""
81
84
 
82
85
  @abstractmethod
83
- async def remove_token(self, user_id: str, token: str):
84
- """Remove token from user"""
86
+ async def get_user_session_by_refresh_token(
87
+ self, refresh_token: str
88
+ ) -> UserSessionResponse:
89
+ """Get user session by refresh token"""
85
90
 
86
91
  @abstractmethod
87
- async def get_sessions(self, user_id: str) -> list[SessionResponse]:
88
- """Get sessions"""
92
+ async def create_user_session(
93
+ self, user_id: str, token_data: UserTokenData
94
+ ) -> UserSessionResponse:
95
+ """Create new user session"""
89
96
 
90
97
  @abstractmethod
91
- async def remove_session(self, user_id: str, session_id: str) -> SessionResponse:
92
- """Remove a session"""
98
+ async def update_user_session(
99
+ self, user_id: str, session_id: str, token_data: UserTokenData
100
+ ) -> UserSessionResponse:
101
+ """Update user session"""
102
+
103
+ @abstractmethod
104
+ async def delete_expired_user_sessions(self, user_id: str):
105
+ """Delete expired user sessions"""
106
+
107
+ @abstractmethod
108
+ async def delete_user_sessions(self, session_ids: list[str]):
109
+ """Delete user session"""