fastapi-rtk 0.2.60__py3-none-any.whl → 1.0.18__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. fastapi_rtk/__init__.py +0 -1
  2. fastapi_rtk/_version.py +1 -0
  3. fastapi_rtk/api/model_rest_api.py +182 -87
  4. fastapi_rtk/auth/auth.py +0 -9
  5. fastapi_rtk/backends/sqla/db.py +32 -7
  6. fastapi_rtk/backends/sqla/filters.py +16 -0
  7. fastapi_rtk/backends/sqla/interface.py +11 -62
  8. fastapi_rtk/backends/sqla/model.py +16 -1
  9. fastapi_rtk/bases/db.py +20 -2
  10. fastapi_rtk/bases/file_manager.py +12 -0
  11. fastapi_rtk/bases/filter.py +1 -1
  12. fastapi_rtk/cli/cli.py +61 -0
  13. fastapi_rtk/cli/commands/security.py +6 -6
  14. fastapi_rtk/const.py +1 -1
  15. fastapi_rtk/db.py +3 -0
  16. fastapi_rtk/dependencies.py +110 -64
  17. fastapi_rtk/fastapi_react_toolkit.py +123 -172
  18. fastapi_rtk/file_managers/s3_file_manager.py +63 -32
  19. fastapi_rtk/lang/messages.pot +12 -12
  20. fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
  21. fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +12 -12
  22. fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
  23. fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +12 -12
  24. fastapi_rtk/manager.py +10 -14
  25. fastapi_rtk/schemas.py +6 -4
  26. fastapi_rtk/security/sqla/apis.py +20 -5
  27. fastapi_rtk/security/sqla/models.py +8 -23
  28. fastapi_rtk/security/sqla/security_manager.py +367 -10
  29. fastapi_rtk/utils/async_task_runner.py +119 -30
  30. fastapi_rtk/utils/csv_json_converter.py +242 -39
  31. fastapi_rtk/utils/hooks.py +7 -4
  32. fastapi_rtk/utils/self_dependencies.py +1 -1
  33. fastapi_rtk/version.py +6 -1
  34. fastapi_rtk-1.0.18.dist-info/METADATA +28 -0
  35. {fastapi_rtk-0.2.60.dist-info → fastapi_rtk-1.0.18.dist-info}/RECORD +38 -38
  36. {fastapi_rtk-0.2.60.dist-info → fastapi_rtk-1.0.18.dist-info}/WHEEL +1 -2
  37. fastapi_rtk-0.2.60.dist-info/METADATA +0 -25
  38. fastapi_rtk-0.2.60.dist-info/top_level.txt +0 -1
  39. {fastapi_rtk-0.2.60.dist-info → fastapi_rtk-1.0.18.dist-info}/entry_points.txt +0 -0
  40. {fastapi_rtk-0.2.60.dist-info → fastapi_rtk-1.0.18.dist-info}/licenses/LICENSE +0 -0
@@ -7,7 +7,7 @@ msgid ""
7
7
  msgstr ""
8
8
  "Project-Id-Version: PROJECT VERSION\n"
9
9
  "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
10
- "POT-Creation-Date: 2025-10-16 13:49+0200\n"
10
+ "POT-Creation-Date: 2025-10-30 10:07+0100\n"
11
11
  "PO-Revision-Date: 2025-07-16 18:01+0200\n"
12
12
  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13
13
  "Language: de\n"
@@ -61,54 +61,54 @@ msgstr "Opr Filter muss ein Objekt sein"
61
61
  msgid "Invalid opr_filters: Not a valid JSON string"
62
62
  msgstr "Ungültige opr_filters: Kein gültiger JSON-String"
63
63
 
64
- #: fastapi_rtk/api/model_rest_api.py:461
64
+ #: fastapi_rtk/api/model_rest_api.py:476
65
65
  #, python-brace-format
66
66
  msgid "List {ModelName}"
67
67
  msgstr "Liste von {ModelName}"
68
68
 
69
- #: fastapi_rtk/api/model_rest_api.py:470
69
+ #: fastapi_rtk/api/model_rest_api.py:485
70
70
  #, python-brace-format
71
71
  msgid "Show {ModelName}"
72
72
  msgstr "Anzeigen von {ModelName}"
73
73
 
74
- #: fastapi_rtk/api/model_rest_api.py:479
74
+ #: fastapi_rtk/api/model_rest_api.py:494
75
75
  #, python-brace-format
76
76
  msgid "Add {ModelName}"
77
77
  msgstr "Hinzufügen von {ModelName}"
78
78
 
79
- #: fastapi_rtk/api/model_rest_api.py:488
79
+ #: fastapi_rtk/api/model_rest_api.py:503
80
80
  #, python-brace-format
81
81
  msgid "Edit {ModelName}"
82
82
  msgstr "Bearbeiten von {ModelName}"
83
83
 
84
- #: fastapi_rtk/api/model_rest_api.py:1338
84
+ #: fastapi_rtk/api/model_rest_api.py:1395
85
85
  msgid "OK"
86
86
  msgstr "OK"
87
87
 
88
- #: fastapi_rtk/api/model_rest_api.py:2354
89
- #: fastapi_rtk/api/model_rest_api.py:2753
88
+ #: fastapi_rtk/api/model_rest_api.py:2411
89
+ #: fastapi_rtk/api/model_rest_api.py:2810
90
90
  #, python-brace-format
91
91
  msgid "Number of items in '{column}' does not match the number of items found."
92
92
  msgstr ""
93
93
  "Anzahl der Elemente in '{column}' stimmt nicht mit der Anzahl der "
94
94
  "gefundenen Elemente überein."
95
95
 
96
- #: fastapi_rtk/api/model_rest_api.py:2375
96
+ #: fastapi_rtk/api/model_rest_api.py:2432
97
97
  #, python-brace-format
98
98
  msgid "Could not find related item for column '{column}'"
99
99
  msgstr "Konnte kein zugehöriges Element für die Spalte '{column}' finden"
100
100
 
101
- #: fastapi_rtk/api/model_rest_api.py:2407
101
+ #: fastapi_rtk/api/model_rest_api.py:2464
102
102
  #, python-brace-format
103
103
  msgid "File type from '{filename}' is not allowed."
104
104
  msgstr "Dateityp von '{filename}' ist nicht erlaubt."
105
105
 
106
- #: fastapi_rtk/api/model_rest_api.py:2815
106
+ #: fastapi_rtk/api/model_rest_api.py:2872
107
107
  #, fuzzy, python-brace-format
108
108
  msgid "Invalid column: {column}"
109
109
  msgstr "Ungültige Spalte: {column}"
110
110
 
111
- #: fastapi_rtk/api/model_rest_api.py:2825
111
+ #: fastapi_rtk/api/model_rest_api.py:2882
112
112
  #, python-brace-format
113
113
  msgid "Invalid filter: {column}"
114
114
  msgstr "Ungültiger Filter: {column}"
@@ -7,7 +7,7 @@ msgid ""
7
7
  msgstr ""
8
8
  "Project-Id-Version: PROJECT VERSION\n"
9
9
  "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
10
- "POT-Creation-Date: 2025-10-16 13:49+0200\n"
10
+ "POT-Creation-Date: 2025-10-30 10:07+0100\n"
11
11
  "PO-Revision-Date: 2025-07-16 18:01+0200\n"
12
12
  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13
13
  "Language: en\n"
@@ -59,52 +59,52 @@ msgstr "Opr Filter must be an object"
59
59
  msgid "Invalid opr_filters: Not a valid JSON string"
60
60
  msgstr "Invalid opr_filters: Not a valid JSON string"
61
61
 
62
- #: fastapi_rtk/api/model_rest_api.py:461
62
+ #: fastapi_rtk/api/model_rest_api.py:476
63
63
  #, python-brace-format
64
64
  msgid "List {ModelName}"
65
65
  msgstr "List of {ModelName}"
66
66
 
67
- #: fastapi_rtk/api/model_rest_api.py:470
67
+ #: fastapi_rtk/api/model_rest_api.py:485
68
68
  #, python-brace-format
69
69
  msgid "Show {ModelName}"
70
70
  msgstr "Show {ModelName}"
71
71
 
72
- #: fastapi_rtk/api/model_rest_api.py:479
72
+ #: fastapi_rtk/api/model_rest_api.py:494
73
73
  #, python-brace-format
74
74
  msgid "Add {ModelName}"
75
75
  msgstr "Add {ModelName}"
76
76
 
77
- #: fastapi_rtk/api/model_rest_api.py:488
77
+ #: fastapi_rtk/api/model_rest_api.py:503
78
78
  #, python-brace-format
79
79
  msgid "Edit {ModelName}"
80
80
  msgstr "Edit {ModelName}"
81
81
 
82
- #: fastapi_rtk/api/model_rest_api.py:1338
82
+ #: fastapi_rtk/api/model_rest_api.py:1395
83
83
  msgid "OK"
84
84
  msgstr "OK"
85
85
 
86
- #: fastapi_rtk/api/model_rest_api.py:2354
87
- #: fastapi_rtk/api/model_rest_api.py:2753
86
+ #: fastapi_rtk/api/model_rest_api.py:2411
87
+ #: fastapi_rtk/api/model_rest_api.py:2810
88
88
  #, python-brace-format
89
89
  msgid "Number of items in '{column}' does not match the number of items found."
90
90
  msgstr "Number of items in '{column}' does not match the number of items found."
91
91
 
92
- #: fastapi_rtk/api/model_rest_api.py:2375
92
+ #: fastapi_rtk/api/model_rest_api.py:2432
93
93
  #, python-brace-format
94
94
  msgid "Could not find related item for column '{column}'"
95
95
  msgstr "Could not find related item for column '{column}'"
96
96
 
97
- #: fastapi_rtk/api/model_rest_api.py:2407
97
+ #: fastapi_rtk/api/model_rest_api.py:2464
98
98
  #, python-brace-format
99
99
  msgid "File type from '{filename}' is not allowed."
100
100
  msgstr "File type from '{filename}' is not allowed."
101
101
 
102
- #: fastapi_rtk/api/model_rest_api.py:2815
102
+ #: fastapi_rtk/api/model_rest_api.py:2872
103
103
  #, fuzzy, python-brace-format
104
104
  msgid "Invalid column: {column}"
105
105
  msgstr "Invalid column: {column}"
106
106
 
107
- #: fastapi_rtk/api/model_rest_api.py:2825
107
+ #: fastapi_rtk/api/model_rest_api.py:2882
108
108
  #, python-brace-format
109
109
  msgid "Invalid filter: {column}"
110
110
  msgstr "Invalid filter: {column}"
fastapi_rtk/manager.py CHANGED
@@ -81,6 +81,8 @@ class UserManager(IDParser, BaseUserManager[User, int]):
81
81
  Returns:
82
82
  list[Role]: A list of Role objects that match the given role names.
83
83
  """
84
+ if not roles:
85
+ return []
84
86
  if isinstance(roles, str):
85
87
  roles = [roles]
86
88
  statement = select(Role).where(Role.name.in_(roles))
@@ -354,18 +356,14 @@ class UserManager(IDParser, BaseUserManager[User, int]):
354
356
  password = user_dict.pop("password")
355
357
  user_dict["hashed_password"] = self.password_helper.hash(password)
356
358
 
357
- role_names = roles
358
- if role_names is None:
359
- if request:
360
- from .setting import Setting
359
+ if roles is None and request:
360
+ from .setting import Setting
361
361
 
362
- role_names = Setting.AUTH_USER_REGISTRATION_ROLE
362
+ roles = Setting.AUTH_USER_REGISTRATION_ROLE
363
363
 
364
364
  # Get the roles if they exist
365
- if role_names:
366
- if isinstance(role_names, str):
367
- role_names = [role_names]
368
- user_dict["roles"] = await self.get_roles_by_names(role_names)
365
+ if roles is not None:
366
+ user_dict["roles"] = await self.get_roles_by_names(roles)
369
367
 
370
368
  created_user = await self.user_db.create(user_dict)
371
369
 
@@ -408,8 +406,6 @@ class UserManager(IDParser, BaseUserManager[User, int]):
408
406
  )
409
407
  )
410
408
  if roles is not None:
411
- if isinstance(roles, str):
412
- roles = [roles]
413
409
  updated_user_data["roles"] = await self.get_roles_by_names(roles)
414
410
  updated_user = await self._update(user, updated_user_data)
415
411
  await self.on_after_update(updated_user, updated_user_data, request)
@@ -500,6 +496,7 @@ class UserManager(IDParser, BaseUserManager[User, int]):
500
496
  # Associate account
501
497
  user = await self.get_by_email(account_email)
502
498
  if not associate_by_email:
499
+ await self.on_after_authenticate(user, False)
503
500
  raise exceptions.UserAlreadyExists()
504
501
  user = await self.user_db.add_oauth_account(user, oauth_account_dict)
505
502
  except exceptions.UserNotExists:
@@ -557,6 +554,7 @@ class UserManager(IDParser, BaseUserManager[User, int]):
557
554
  user = await self._handle_role_keys(
558
555
  user, oauth_callback_params["user_dict"].pop("role_keys", None)
559
556
  )
557
+ await self.on_after_authenticate(user)
560
558
  return user
561
559
 
562
560
  @overload
@@ -716,13 +714,11 @@ class UserManager(IDParser, BaseUserManager[User, int]):
716
714
  Returns:
717
715
  User | dict[str, Any]: The updated user or user dictionary with roles.
718
716
  """
719
- if not role_keys:
717
+ if role_keys is None:
720
718
  return user_or_user_dict
721
719
  role_names = [Setting.AUTH_ROLES_MAPPING.get(key, []) for key in role_keys]
722
720
  role_names = [name for sublist in role_names for name in sublist]
723
721
  role_names = list(dict.fromkeys(role_names).keys())
724
- if not role_names:
725
- return user_or_user_dict
726
722
  if isinstance(user_or_user_dict, User):
727
723
  user_or_user_dict = await self.update({}, user_or_user_dict, role_names)
728
724
  else:
fastapi_rtk/schemas.py CHANGED
@@ -323,7 +323,7 @@ class BaseResponseMany(BaseResponse):
323
323
  Represents a response containing multiple items.
324
324
 
325
325
  Attributes:
326
- count (int): The count of items.
326
+ count (int | None): The count of items. If None, count is not provided.
327
327
  ids (List[PRIMARY_KEY]): The IDs of the items.
328
328
  list_columns (List[str]): The columns to show in the list.
329
329
  list_title (str): The title for the list.
@@ -331,7 +331,7 @@ class BaseResponseMany(BaseResponse):
331
331
  result (List[BaseModel] | None): The result of the response.
332
332
  """
333
333
 
334
- count: int
334
+ count: int | None = None
335
335
  ids: list[PRIMARY_KEY]
336
336
  list_columns: list[str] = []
337
337
  list_title: str = ""
@@ -384,24 +384,26 @@ class QuerySchema(BaseModel):
384
384
  Represents the query parameters for selection, pagination, ordering, and filtering.
385
385
 
386
386
  Attributes:
387
- columns (str, optional): The columns to select in the query. Defaults to '[]'.
388
387
  page (int): The page number for pagination. Defaults to 0.
389
388
  page_size (int, optional): The number of items per page. Defaults to 25.
390
389
  order_column (str, optional): The column to order the results by.
391
390
  order_direction (Literal['asc', 'desc'], optional): The direction of the ordering (asc or desc).
391
+ columns (str, optional): The columns to select in the query. Defaults to '[]'.
392
392
  filters (str, optional): The filters to apply to the query. Defaults to '[]'.
393
393
  opr_filters (str, optional): The filters to apply without column name. Defaults to '[]'.
394
394
  global_filter (str, optional): The filter to apply globally across all columns.
395
+ with_count (bool): Whether to include the total count of items. Defaults to True.
395
396
  """
396
397
 
397
- columns: str = "[]"
398
398
  page: int = 0
399
399
  page_size: int | None = 25
400
400
  order_column: str | None = None
401
401
  order_direction: Literal["asc", "desc"] | None = None
402
+ columns: str = "[]"
402
403
  filters: str = "[]"
403
404
  opr_filters: str = "[]"
404
405
  global_filter: str | None = None
406
+ with_count: bool = True
405
407
 
406
408
  @field_validator("columns")
407
409
  @classmethod
@@ -1,5 +1,6 @@
1
1
  from typing import List
2
2
 
3
+ import sqlalchemy
3
4
  from fastapi import Depends, HTTPException, Request, status
4
5
  from pydantic import BaseModel, EmailStr, Field
5
6
  from sqlalchemy.ext.asyncio import AsyncSession
@@ -8,7 +9,7 @@ from sqlalchemy.orm import Session
8
9
  from ...api import BaseApi, ModelRestApi
9
10
  from ...backends.sqla.interface import SQLAInterface
10
11
  from ...const import ErrorCode
11
- from ...db import UserDatabase, get_user_db
12
+ from ...db import UserDatabase, db, get_user_db
12
13
  from ...decorators import expose, login_required
13
14
  from ...globals import g
14
15
  from ...lang import translate
@@ -21,7 +22,7 @@ from ...schemas import (
21
22
  generate_user_update_schema,
22
23
  )
23
24
  from ...setting import Setting
24
- from ...utils import SelfType, lazy, merge_schema
25
+ from ...utils import SelfType, lazy, merge_schema, smart_run
25
26
  from .models import Api, Permission, PermissionApi, Role, User
26
27
 
27
28
  __all__ = [
@@ -163,7 +164,7 @@ class UsersApi(ModelRestApi):
163
164
  col.required = False
164
165
 
165
166
  async def post_add(self, item, params):
166
- await g.current_app.security.update_user(
167
+ await g.current_app.sm.update_user(
167
168
  user_update={"password": item.password},
168
169
  user=item,
169
170
  session=params.session,
@@ -172,7 +173,7 @@ class UsersApi(ModelRestApi):
172
173
 
173
174
  async def post_update(self, item, params):
174
175
  if params.body.password:
175
- await g.current_app.security.update_user(
176
+ await g.current_app.sm.update_user(
176
177
  user_update={"password": params.body.password},
177
178
  user=item,
178
179
  session=params.session,
@@ -275,12 +276,26 @@ class AuthApi(BaseApi):
275
276
  },
276
277
  )(self.update_user)
277
278
 
278
- def get_user(self):
279
+ async def get_user(self):
279
280
  if not g.user:
280
281
  raise HTTPException(
281
282
  status.HTTP_401_UNAUTHORIZED,
282
283
  ErrorCode.GET_USER_MISSING_TOKEN_OR_INACTIVE_USER,
283
284
  )
285
+
286
+ g.user.permissions = []
287
+ if g.user.roles:
288
+ # Retrieve list of api names that user has access to
289
+ query = (
290
+ sqlalchemy.select(Api.name)
291
+ .join(PermissionApi)
292
+ .join(PermissionApi.roles)
293
+ .where(Role.id.in_([role.id for role in g.user.roles]))
294
+ .distinct()
295
+ )
296
+ result = await smart_run(db.current_session.scalars, query)
297
+ g.user.permissions = list(result)
298
+
284
299
  return g.user
285
300
 
286
301
  async def update_user(
@@ -77,21 +77,18 @@ class PermissionApi(Model):
77
77
  Integer, ForeignKey(f"{API_TABLE}.id"), name=view_menu_id, nullable=False
78
78
  )
79
79
  api: Mapped["Api"] = relationship(
80
- "Api", back_populates="permissions", lazy="selectin"
80
+ "Api", back_populates="permissions", lazy="joined"
81
81
  )
82
82
 
83
83
  permission_id: Mapped[int] = mapped_column(
84
84
  Integer, ForeignKey(f"{PERMISSION_TABLE}.id"), nullable=False
85
85
  )
86
86
  permission: Mapped["Permission"] = relationship(
87
- "Permission", back_populates="apis", lazy="selectin"
87
+ "Permission", back_populates="apis", lazy="joined"
88
88
  )
89
89
 
90
90
  roles: Mapped[list["Role"]] = relationship(
91
- "Role",
92
- secondary=assoc_permission_api_role,
93
- back_populates="permissions",
94
- lazy="selectin",
91
+ "Role", secondary=assoc_permission_api_role, back_populates="permissions"
95
92
  )
96
93
 
97
94
  def __repr__(self) -> str:
@@ -104,10 +101,7 @@ class Api(Model):
104
101
  name: Mapped[str] = mapped_column(String, unique=True, nullable=False)
105
102
 
106
103
  permissions: Mapped[list[PermissionApi]] = relationship(
107
- PermissionApi,
108
- back_populates="api",
109
- lazy="selectin",
110
- cascade="all, delete-orphan",
104
+ PermissionApi, back_populates="api", cascade="all, delete-orphan"
111
105
  )
112
106
 
113
107
  def __eq__(self, other):
@@ -128,10 +122,7 @@ class Permission(Model):
128
122
  name: Mapped[str] = mapped_column(String, unique=True, nullable=False)
129
123
 
130
124
  apis: Mapped[list[PermissionApi]] = relationship(
131
- PermissionApi,
132
- back_populates="permission",
133
- lazy="selectin",
134
- cascade="all, delete-orphan",
125
+ PermissionApi, back_populates="permission", cascade="all, delete-orphan"
135
126
  )
136
127
 
137
128
  def __repr__(self) -> str:
@@ -144,14 +135,11 @@ class Role(Model):
144
135
  name: Mapped[str] = mapped_column(String, unique=True, nullable=False)
145
136
 
146
137
  users: Mapped[list["User"]] = relationship(
147
- "User", secondary=assoc_user_role, back_populates="roles", lazy="selectin"
138
+ "User", secondary=assoc_user_role, back_populates="roles"
148
139
  )
149
140
 
150
141
  permissions: Mapped[list[PermissionApi]] = relationship(
151
- PermissionApi,
152
- secondary=assoc_permission_api_role,
153
- back_populates="roles",
154
- lazy="selectin",
142
+ PermissionApi, secondary=assoc_permission_api_role, back_populates="roles"
155
143
  )
156
144
 
157
145
  def __repr__(self) -> str:
@@ -203,10 +191,7 @@ class User(Model):
203
191
  return Column(Integer, ForeignKey("ab_user.id"), default=self.get_user_id)
204
192
 
205
193
  oauth_accounts: Mapped[list[OAuthAccount]] = relationship(
206
- "OAuthAccount",
207
- back_populates="user",
208
- lazy="selectin",
209
- cascade="all, delete-orphan",
194
+ "OAuthAccount", back_populates="user", cascade="all, delete-orphan"
210
195
  )
211
196
 
212
197
  roles: Mapped[list[Role]] = relationship(