zrb 1.0.0b8__py3-none-any.whl → 1.0.0b10__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 (81) hide show
  1. zrb/__main__.py +3 -0
  2. zrb/builtin/project/add/fastapp/fastapp_task.py +1 -0
  3. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/.coveragerc +11 -0
  4. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/.gitignore +4 -0
  5. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_task.py +4 -4
  6. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/config.py +5 -0
  7. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +108 -1
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +67 -4
  9. 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
  10. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/schema/my_entity.py +1 -0
  11. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_create_my_entity.py +53 -0
  12. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_delete_my_entity.py +62 -0
  13. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_read_my_entity.py +65 -0
  14. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_update_my_entity.py +61 -0
  15. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/gateway_subroute.py +57 -13
  16. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/input.py +8 -0
  17. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +2 -2
  18. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/gateway/subroute/my_module.py +6 -1
  19. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/module_task_definition.py +10 -6
  20. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task.py +65 -14
  21. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task_util.py +106 -0
  22. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/util.py +6 -86
  23. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_db_repository.py +27 -11
  24. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_service.py +140 -51
  25. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/error.py +15 -0
  26. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/parser.py +1 -1
  27. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +22 -4
  28. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/auth_client.py +21 -0
  29. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/3093c7336477_add_auth_tables.py +106 -61
  30. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/8ed025bcc845_create_permissions.py +69 -0
  31. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration_metadata.py +3 -4
  32. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/route.py +15 -14
  33. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/permission_service.py +4 -4
  34. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_db_repository.py +24 -5
  35. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/role_service.py +14 -12
  36. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_db_repository.py +134 -97
  37. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_repository.py +28 -11
  38. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service.py +215 -13
  39. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service_factory.py +30 -2
  40. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py +216 -41
  41. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/auth.py +57 -0
  42. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/requirements.txt +7 -1
  43. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/permission.py +2 -0
  44. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/role.py +13 -12
  45. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/user.py +64 -12
  46. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/_util/access_token.py +19 -0
  47. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_create_permission.py +59 -0
  48. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_delete_permission.py +68 -0
  49. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_read_permission.py +71 -0
  50. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_update_permission.py +66 -0
  51. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/test_user_session.py +195 -0
  52. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_health_and_readiness.py +28 -0
  53. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_homepage.py +17 -0
  54. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_not_found_error.py +16 -0
  55. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test.sh +7 -0
  56. zrb/task/base_task.py +10 -10
  57. zrb/task/cmd_task.py +2 -5
  58. zrb/util/cmd/command.py +39 -48
  59. zrb/util/codemod/modification_mode.py +3 -0
  60. zrb/util/codemod/modify_class.py +58 -0
  61. zrb/util/codemod/modify_class_parent.py +68 -0
  62. zrb/util/codemod/modify_class_property.py +128 -0
  63. zrb/util/codemod/modify_dict.py +75 -0
  64. zrb/util/codemod/modify_function.py +65 -0
  65. zrb/util/codemod/modify_function_call.py +68 -0
  66. zrb/util/codemod/modify_method.py +88 -0
  67. zrb/util/codemod/{prepend_code_to_module.py → modify_module.py} +2 -3
  68. zrb/util/file.py +3 -2
  69. {zrb-1.0.0b8.dist-info → zrb-1.0.0b10.dist-info}/METADATA +2 -1
  70. {zrb-1.0.0b8.dist-info → zrb-1.0.0b10.dist-info}/RECORD +72 -55
  71. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/migrate.py +0 -3
  72. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/session.py +0 -48
  73. zrb/util/codemod/append_code_to_class.py +0 -35
  74. zrb/util/codemod/append_code_to_function.py +0 -38
  75. zrb/util/codemod/append_code_to_method.py +0 -55
  76. zrb/util/codemod/append_key_to_dict.py +0 -51
  77. zrb/util/codemod/append_param_to_function_call.py +0 -39
  78. zrb/util/codemod/prepend_parent_to_class.py +0 -38
  79. zrb/util/codemod/prepend_property_to_class.py +0 -55
  80. {zrb-1.0.0b8.dist-info → zrb-1.0.0b10.dist-info}/WHEEL +0 -0
  81. {zrb-1.0.0b8.dist-info → zrb-1.0.0b10.dist-info}/entry_points.txt +0 -0
@@ -1,22 +1,115 @@
1
+ import datetime
1
2
  from logging import Logger
2
3
 
4
+ from jose import jwt
3
5
  from my_app_name.common.base_service import BaseService
6
+ from my_app_name.common.error import ForbiddenError, NotFoundError
4
7
  from my_app_name.module.auth.service.user.repository.user_repository import (
5
8
  UserRepository,
6
9
  )
7
10
  from my_app_name.schema.user import (
11
+ AuthUserResponse,
8
12
  MultipleUserResponse,
9
13
  UserCreateWithRolesAndAudit,
14
+ UserCredentials,
10
15
  UserResponse,
16
+ UserSessionResponse,
17
+ UserTokenData,
11
18
  UserUpdateWithRolesAndAudit,
12
19
  )
20
+ from pydantic import BaseModel
21
+
22
+
23
+ class UserServiceConfig(BaseModel):
24
+ super_user: str
25
+ super_user_password: str
26
+ guest_user: str = "guest"
27
+ guest_user_permissions: list[str] = []
28
+ max_parallel_session: int = 1
29
+ access_token_expire_minutes: int = 30
30
+ refresh_token_expire_minutes: int = 1440
31
+ secret_key: str = "my-secret-key"
32
+ prioritize_new_session: bool = True
13
33
 
14
34
 
15
35
  class UserService(BaseService):
16
36
 
17
- def __init__(self, logger: Logger, user_repository: UserRepository):
37
+ def __init__(
38
+ self, logger: Logger, user_repository: UserRepository, config: UserServiceConfig
39
+ ):
18
40
  super().__init__(logger)
19
41
  self.user_repository = user_repository
42
+ self.config = config
43
+
44
+ @BaseService.route(
45
+ "/api/v1/current-user",
46
+ methods=["get"],
47
+ response_model=AuthUserResponse,
48
+ )
49
+ async def get_current_user(self, access_token: str | None) -> AuthUserResponse:
50
+ if access_token is None or access_token == "":
51
+ return self._get_guest_user()
52
+ try:
53
+ user_session = await self.user_repository.get_user_session_by_access_token(
54
+ access_token
55
+ )
56
+ user_id = user_session.user_id
57
+ if user_id == self.config.super_user:
58
+ return self._get_super_user()
59
+ user = await self.user_repository.get_by_id(user_id)
60
+ return self._to_auth_user_response(user)
61
+ except NotFoundError:
62
+ return self._get_guest_user()
63
+
64
+ @BaseService.route(
65
+ "/api/v1/user-sessions",
66
+ methods=["post"],
67
+ response_model=UserSessionResponse,
68
+ )
69
+ async def create_user_session(
70
+ self, credentials: UserCredentials
71
+ ) -> UserSessionResponse:
72
+ current_user = await self._get_user_by_credentials(credentials)
73
+ await self.user_repository.delete_expired_user_sessions(current_user.id)
74
+ user_sessions = await self.user_repository.get_active_user_sessions(
75
+ current_user.id
76
+ )
77
+ user_session_count = len(user_sessions)
78
+ if user_session_count >= self.config.max_parallel_session:
79
+ await self._handle_excess_sessions(user_sessions)
80
+ token_data = self._create_user_token_data(current_user.username)
81
+ return await self.user_repository.create_user_session(
82
+ user_id=current_user.id, token_data=token_data
83
+ )
84
+
85
+ @BaseService.route(
86
+ "/api/v1/user-sessions",
87
+ methods=["put"],
88
+ response_model=UserSessionResponse,
89
+ )
90
+ async def update_user_session(self, refresh_token: str) -> UserSessionResponse:
91
+ current_user = await self._get_auth_user_by_refresh_token(refresh_token)
92
+ current_user_session = (
93
+ await self.user_repository.get_user_session_by_refresh_token(refresh_token)
94
+ )
95
+ token_data = self._create_user_token_data(current_user.username)
96
+ return await self.user_repository.update_user_session(
97
+ user_id=current_user.id,
98
+ session_id=current_user_session.id,
99
+ token_data=token_data,
100
+ )
101
+
102
+ @BaseService.route(
103
+ "/api/v1/user-sessions",
104
+ methods=["delete"],
105
+ response_model=UserSessionResponse,
106
+ )
107
+ async def delete_user_session(self, refresh_token: str) -> UserSessionResponse:
108
+ current_user_session = (
109
+ await self.user_repository.get_user_session_by_refresh_token(refresh_token)
110
+ )
111
+ await self.user_repository.delete_user_sessions([current_user_session.id])
112
+ return current_user_session
20
113
 
21
114
  @BaseService.route(
22
115
  "/api/v1/users/{user_id}",
@@ -50,13 +143,13 @@ class UserService(BaseService):
50
143
  async def create_user_bulk(
51
144
  self, data: list[UserCreateWithRolesAndAudit]
52
145
  ) -> list[UserResponse]:
53
- role_ids = [row.get_role_ids() for row in data]
146
+ role_names = [row.get_role_names() for row in data]
54
147
  data = [row.get_user_create_with_audit() for row in data]
55
148
  users = await self.user_repository.create_bulk(data)
56
149
  if len(users) > 0:
57
150
  created_by = users[0].created_by
58
151
  await self.user_repository.add_roles(
59
- data={user.id: role_ids[i] for i, user in enumerate(data)},
152
+ data={user.id: role_names[i] for i, user in enumerate(users)},
60
153
  created_by=created_by,
61
154
  )
62
155
  return await self.user_repository.get_by_ids([user.id for user in users])
@@ -67,30 +160,30 @@ class UserService(BaseService):
67
160
  response_model=UserResponse,
68
161
  )
69
162
  async def create_user(self, data: UserCreateWithRolesAndAudit) -> UserResponse:
70
- role_ids = data.get_role_ids()
163
+ role_names = data.get_role_names()
71
164
  data = data.get_user_create_with_audit()
72
165
  user = await self.user_repository.create(data)
73
166
  await self.user_repository.add_roles(
74
- data={user.id: role_ids}, created_by=user.created_by
167
+ data={user.id: role_names}, created_by=user.created_by
75
168
  )
76
169
  return await self.user_repository.get_by_id(user.id)
77
170
 
78
171
  @BaseService.route(
79
172
  "/api/v1/users/bulk",
80
173
  methods=["put"],
81
- response_model=UserResponse,
174
+ response_model=list[UserResponse],
82
175
  )
83
176
  async def update_user_bulk(
84
177
  self, user_ids: list[str], data: UserUpdateWithRolesAndAudit
85
- ) -> UserResponse:
86
- role_ids = [row.get_role_ids() for row in data]
178
+ ) -> list[UserResponse]:
179
+ role_names = [row.get_role_names() for row in data]
87
180
  user_data = [row.get_user_create_with_audit() for row in data]
88
181
  await self.user_repository.update_bulk(user_ids, user_data)
89
182
  if len(user_ids) > 0:
90
183
  updated_by = user_data[0].updated_by
91
184
  await self.user_repository.remove_all_roles(user_ids)
92
185
  await self.user_repository.add_roles(
93
- data={user_id: role_ids[i] for i, user_id in enumerate(user_ids)},
186
+ data={user_id: role_names[i] for i, user_id in enumerate(user_ids)},
94
187
  updated_by=updated_by,
95
188
  )
96
189
  return await self.user_repository.get_by_ids(user_ids)
@@ -103,23 +196,23 @@ class UserService(BaseService):
103
196
  async def update_user(
104
197
  self, user_id: str, data: UserUpdateWithRolesAndAudit
105
198
  ) -> UserResponse:
106
- role_ids = data.get_role_ids()
199
+ role_names = data.get_role_names()
107
200
  user_data = data.get_user_update_with_audit()
108
201
  await self.user_repository.update(user_id, user_data)
109
202
  await self.user_repository.remove_all_roles([user_id])
110
203
  await self.user_repository.add_roles(
111
- data={user_id: role_ids}, created_by=user_data.updated_by
204
+ data={user_id: role_names}, created_by=user_data.updated_by
112
205
  )
113
206
  return await self.user_repository.get_by_id(user_id)
114
207
 
115
208
  @BaseService.route(
116
209
  "/api/v1/users/bulk",
117
210
  methods=["delete"],
118
- response_model=UserResponse,
211
+ response_model=list[UserResponse],
119
212
  )
120
213
  async def delete_user_bulk(
121
214
  self, user_ids: list[str], deleted_by: str
122
- ) -> UserResponse:
215
+ ) -> list[UserResponse]:
123
216
  roles = await self.user_repository.get_by_ids(user_ids)
124
217
  await self.user_repository.delete_bulk(user_ids)
125
218
  await self.user_repository.remove_all_roles(user_ids)
@@ -135,3 +228,112 @@ class UserService(BaseService):
135
228
  await self.user_repository.delete(user_id)
136
229
  await self.user_repository.remove_all_roles([user_id])
137
230
  return user
231
+
232
+ async def _get_auth_user_by_refresh_token(
233
+ self, refresh_token: str
234
+ ) -> AuthUserResponse:
235
+ if refresh_token is None or refresh_token == "":
236
+ raise NotFoundError("User not found")
237
+ user_session = await self.user_repository.get_user_session_by_refresh_token(
238
+ refresh_token
239
+ )
240
+ user_id = user_session.user_id
241
+ if user_id == self.config.super_user:
242
+ return self._get_super_user()
243
+ user = await self.user_repository.get_by_id(user_id)
244
+ return self._to_auth_user_response(user)
245
+
246
+ async def _get_user_by_credentials(
247
+ self, credentials: UserCredentials
248
+ ) -> AuthUserResponse:
249
+ if (
250
+ credentials.username == self.config.super_user
251
+ and credentials.password == self.config.super_user_password
252
+ ):
253
+ return self._get_super_user()
254
+ user = await self.user_repository.get_by_credentials(
255
+ username=credentials.username,
256
+ password=credentials.password,
257
+ )
258
+ return self._to_auth_user_response(user)
259
+
260
+ def _to_auth_user_response(self, user_response: UserResponse) -> AuthUserResponse:
261
+ return AuthUserResponse(
262
+ **user_response.model_dump(), is_guest=False, is_super_user=False
263
+ )
264
+
265
+ def _get_guest_user(self):
266
+ return AuthUserResponse(
267
+ id=self.config.guest_user,
268
+ username=self.config.guest_user,
269
+ active=True,
270
+ role_names=[],
271
+ permission_names=self.config.guest_user_permissions,
272
+ is_guest=True,
273
+ is_super_user=False,
274
+ )
275
+
276
+ def _get_super_user(self):
277
+ return AuthUserResponse(
278
+ id=self.config.super_user,
279
+ username=self.config.super_user,
280
+ active=True,
281
+ role_names=[],
282
+ permission_names=[],
283
+ is_guest=False,
284
+ is_super_user=True,
285
+ )
286
+
287
+ async def _handle_excess_sessions(self, active_sessions: list[UserSessionResponse]):
288
+ """Handles excess user sessions by deleting the oldest if necessary."""
289
+ if not self.config.prioritize_new_session:
290
+ raise ForbiddenError("No additional session allowed")
291
+ # Sort sessions by expiration and remove the oldest ones
292
+ sessions_to_delete = sorted(
293
+ active_sessions, key=lambda s: s.refresh_token_expired_at
294
+ )
295
+ excess_count = len(active_sessions) + 1 - self.config.max_parallel_session
296
+ await self.user_repository.delete_user_sessions(
297
+ [session.id for session in sessions_to_delete[:excess_count]]
298
+ )
299
+
300
+ def _create_user_token_data(self, username: str) -> UserTokenData:
301
+ now = datetime.datetime.now(datetime.timezone.utc)
302
+ access_token_expire_at = now + datetime.timedelta(
303
+ minutes=self.config.access_token_expire_minutes
304
+ )
305
+ refresh_token_expire_at = now + datetime.timedelta(
306
+ minutes=self.config.refresh_token_expire_minutes
307
+ )
308
+ return UserTokenData(
309
+ access_token=self._generate_access_token(
310
+ username=username,
311
+ expire_at=access_token_expire_at,
312
+ ),
313
+ refresh_token=self._generate_refresh_token(
314
+ username=username,
315
+ expire_at=refresh_token_expire_at,
316
+ ),
317
+ access_token_expired_at=access_token_expire_at,
318
+ refresh_token_expired_at=refresh_token_expire_at,
319
+ )
320
+
321
+ def _generate_access_token(
322
+ self, username: str, expire_at: datetime.datetime
323
+ ) -> str:
324
+ return self._generate_user_token(
325
+ username=username, expire_at=expire_at, token_type="access"
326
+ )
327
+
328
+ def _generate_refresh_token(
329
+ self, username: str, expire_at: datetime.datetime
330
+ ) -> str:
331
+ return self._generate_user_token(
332
+ username=username, expire_at=expire_at, token_type="refresh"
333
+ )
334
+
335
+ def _generate_user_token(
336
+ self, username: str, expire_at: datetime.datetime, token_type: str
337
+ ) -> str:
338
+ to_encode = {"sub": username, "exp": expire_at, "type": token_type}
339
+ return jwt.encode(to_encode, self.config.secret_key)
@@ -1,7 +1,35 @@
1
1
  from my_app_name.common.logger_factory import logger
2
+ from my_app_name.config import (
3
+ APP_AUTH_ACCESS_TOKEN_EXPIRE_MINUTES,
4
+ APP_AUTH_GUEST_USER,
5
+ APP_AUTH_GUEST_USER_PERMISSIONS,
6
+ APP_AUTH_MAX_PARALLEL_SESSION,
7
+ APP_AUTH_PRIORITIZE_NEW_SESSION,
8
+ APP_AUTH_REFRESH_TOKEN_EXPIRE_MINUTES,
9
+ APP_AUTH_SECRET_KEY,
10
+ APP_AUTH_SUPER_USER,
11
+ APP_AUTH_SUPER_USER_PASSWORD,
12
+ )
2
13
  from my_app_name.module.auth.service.user.repository.user_repository_factory import (
3
14
  user_repository,
4
15
  )
5
- from my_app_name.module.auth.service.user.user_service import UserService
16
+ from my_app_name.module.auth.service.user.user_service import (
17
+ UserService,
18
+ UserServiceConfig,
19
+ )
6
20
 
7
- user_service = UserService(logger, user_repository=user_repository)
21
+ user_service = UserService(
22
+ logger,
23
+ user_repository=user_repository,
24
+ config=UserServiceConfig(
25
+ super_user=APP_AUTH_SUPER_USER,
26
+ super_user_password=APP_AUTH_SUPER_USER_PASSWORD,
27
+ guest_user=APP_AUTH_GUEST_USER,
28
+ guest_user_permissions=APP_AUTH_GUEST_USER_PERMISSIONS,
29
+ max_parallel_session=APP_AUTH_MAX_PARALLEL_SESSION,
30
+ access_token_expire_minutes=APP_AUTH_ACCESS_TOKEN_EXPIRE_MINUTES,
31
+ refresh_token_expire_minutes=APP_AUTH_REFRESH_TOKEN_EXPIRE_MINUTES,
32
+ secret_key=APP_AUTH_SECRET_KEY,
33
+ prioritize_new_session=APP_AUTH_PRIORITIZE_NEW_SESSION,
34
+ ),
35
+ )