fastapi-rtk 0.2.27__py3-none-any.whl → 1.0.13__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 (98) hide show
  1. fastapi_rtk/__init__.py +39 -35
  2. fastapi_rtk/_version.py +1 -0
  3. fastapi_rtk/api/model_rest_api.py +476 -221
  4. fastapi_rtk/auth/auth.py +0 -9
  5. fastapi_rtk/backends/generic/__init__.py +6 -0
  6. fastapi_rtk/backends/generic/column.py +21 -12
  7. fastapi_rtk/backends/generic/db.py +42 -7
  8. fastapi_rtk/backends/generic/filters.py +21 -16
  9. fastapi_rtk/backends/generic/interface.py +14 -8
  10. fastapi_rtk/backends/generic/model.py +19 -11
  11. fastapi_rtk/backends/sqla/__init__.py +1 -0
  12. fastapi_rtk/backends/sqla/db.py +77 -17
  13. fastapi_rtk/backends/sqla/extensions/audit/audit.py +401 -189
  14. fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py +15 -12
  15. fastapi_rtk/backends/sqla/filters.py +50 -21
  16. fastapi_rtk/backends/sqla/interface.py +96 -34
  17. fastapi_rtk/backends/sqla/model.py +56 -39
  18. fastapi_rtk/bases/__init__.py +20 -0
  19. fastapi_rtk/bases/db.py +94 -7
  20. fastapi_rtk/bases/file_manager.py +47 -3
  21. fastapi_rtk/bases/filter.py +22 -0
  22. fastapi_rtk/bases/interface.py +49 -5
  23. fastapi_rtk/bases/model.py +3 -0
  24. fastapi_rtk/bases/session.py +2 -0
  25. fastapi_rtk/cli/cli.py +62 -9
  26. fastapi_rtk/cli/commands/__init__.py +23 -0
  27. fastapi_rtk/cli/{db.py → commands/db/__init__.py} +107 -50
  28. fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/env.py +2 -3
  29. fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/env.py +10 -9
  30. fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/script.py.mako +3 -1
  31. fastapi_rtk/cli/{export.py → commands/export.py} +12 -10
  32. fastapi_rtk/cli/{security.py → commands/security.py} +73 -7
  33. fastapi_rtk/cli/commands/translate.py +299 -0
  34. fastapi_rtk/cli/decorators.py +9 -4
  35. fastapi_rtk/cli/utils.py +46 -0
  36. fastapi_rtk/config.py +41 -1
  37. fastapi_rtk/const.py +29 -1
  38. fastapi_rtk/db.py +76 -40
  39. fastapi_rtk/decorators.py +1 -1
  40. fastapi_rtk/dependencies.py +134 -62
  41. fastapi_rtk/exceptions.py +51 -1
  42. fastapi_rtk/fastapi_react_toolkit.py +186 -171
  43. fastapi_rtk/file_managers/file_manager.py +8 -6
  44. fastapi_rtk/file_managers/s3_file_manager.py +69 -33
  45. fastapi_rtk/globals.py +22 -12
  46. fastapi_rtk/lang/__init__.py +3 -0
  47. fastapi_rtk/lang/babel/__init__.py +4 -0
  48. fastapi_rtk/lang/babel/cli.py +40 -0
  49. fastapi_rtk/lang/babel/config.py +17 -0
  50. fastapi_rtk/lang/babel.cfg +1 -0
  51. fastapi_rtk/lang/lazy_text.py +120 -0
  52. fastapi_rtk/lang/messages.pot +238 -0
  53. fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
  54. fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +248 -0
  55. fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
  56. fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +244 -0
  57. fastapi_rtk/manager.py +355 -37
  58. fastapi_rtk/mixins.py +12 -0
  59. fastapi_rtk/routers.py +208 -72
  60. fastapi_rtk/schemas.py +142 -39
  61. fastapi_rtk/security/sqla/apis.py +39 -13
  62. fastapi_rtk/security/sqla/models.py +8 -23
  63. fastapi_rtk/security/sqla/security_manager.py +369 -11
  64. fastapi_rtk/setting.py +446 -88
  65. fastapi_rtk/types.py +94 -27
  66. fastapi_rtk/utils/__init__.py +8 -0
  67. fastapi_rtk/utils/async_task_runner.py +286 -61
  68. fastapi_rtk/utils/csv_json_converter.py +243 -40
  69. fastapi_rtk/utils/hooks.py +34 -0
  70. fastapi_rtk/utils/merge_schema.py +3 -3
  71. fastapi_rtk/utils/multiple_async_contexts.py +21 -0
  72. fastapi_rtk/utils/pydantic.py +46 -1
  73. fastapi_rtk/utils/run_utils.py +31 -1
  74. fastapi_rtk/utils/self_dependencies.py +1 -1
  75. fastapi_rtk/utils/use_default_when_none.py +1 -1
  76. fastapi_rtk/version.py +6 -1
  77. fastapi_rtk-1.0.13.dist-info/METADATA +28 -0
  78. fastapi_rtk-1.0.13.dist-info/RECORD +133 -0
  79. {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/WHEEL +1 -2
  80. fastapi_rtk/backends/gremlinpython/__init__.py +0 -108
  81. fastapi_rtk/backends/gremlinpython/column.py +0 -208
  82. fastapi_rtk/backends/gremlinpython/db.py +0 -228
  83. fastapi_rtk/backends/gremlinpython/exceptions.py +0 -34
  84. fastapi_rtk/backends/gremlinpython/filters.py +0 -461
  85. fastapi_rtk/backends/gremlinpython/interface.py +0 -734
  86. fastapi_rtk/backends/gremlinpython/model.py +0 -364
  87. fastapi_rtk/backends/gremlinpython/session.py +0 -23
  88. fastapi_rtk/cli/commands.py +0 -295
  89. fastapi_rtk-0.2.27.dist-info/METADATA +0 -23
  90. fastapi_rtk-0.2.27.dist-info/RECORD +0 -126
  91. fastapi_rtk-0.2.27.dist-info/top_level.txt +0 -1
  92. /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/README +0 -0
  93. /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/alembic.ini.mako +0 -0
  94. /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/script.py.mako +0 -0
  95. /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/README +0 -0
  96. /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/alembic.ini.mako +0 -0
  97. {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/entry_points.txt +0 -0
  98. {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/licenses/LICENSE +0 -0
fastapi_rtk/schemas.py CHANGED
@@ -3,7 +3,7 @@ import typing
3
3
  from datetime import datetime
4
4
  from typing import Annotated, Any, Dict, Literal
5
5
 
6
- from fastapi import HTTPException
6
+ import fastapi
7
7
  from pydantic import (
8
8
  AfterValidator,
9
9
  BaseModel,
@@ -16,6 +16,8 @@ from pydantic import (
16
16
  )
17
17
 
18
18
  from .const import logger
19
+ from .exceptions import HTTPWithValidationException
20
+ from .lang import translate
19
21
  from .utils import ensure_tz_info, merge_schema, validate_utc
20
22
 
21
23
  if typing.TYPE_CHECKING:
@@ -272,11 +274,13 @@ class InfoResponse(BaseModel):
272
274
  add_title: str = ""
273
275
  add_schema: dict[str, Any] = {}
274
276
  add_uischema: dict[str, Any] | None = None
277
+ add_translations: dict[str, dict[str, str]] | None = None
275
278
  add_type: typing.Literal["json"] | typing.Literal["form"] = "json"
276
279
  edit_columns: list[ColumnInfo | ColumnEnumInfo | ColumnRelationInfo] = []
277
280
  edit_title: str = ""
278
281
  edit_schema: dict[str, Any] = {}
279
282
  edit_uischema: dict[str, Any] | None = None
283
+ edit_translations: dict[str, dict[str, str]] | None = None
280
284
  edit_type: typing.Literal["json"] | typing.Literal["form"] = "json"
281
285
  filters: dict[str, Any] = {}
282
286
  filter_options: dict[str, Any] = {}
@@ -319,7 +323,7 @@ class BaseResponseMany(BaseResponse):
319
323
  Represents a response containing multiple items.
320
324
 
321
325
  Attributes:
322
- count (int): The count of items.
326
+ count (int | None): The count of items. If None, count is not provided.
323
327
  ids (List[PRIMARY_KEY]): The IDs of the items.
324
328
  list_columns (List[str]): The columns to show in the list.
325
329
  list_title (str): The title for the list.
@@ -327,7 +331,7 @@ class BaseResponseMany(BaseResponse):
327
331
  result (List[BaseModel] | None): The result of the response.
328
332
  """
329
333
 
330
- count: int
334
+ count: int | None = None
331
335
  ids: list[PRIMARY_KEY]
332
336
  list_columns: list[str] = []
333
337
  list_title: str = ""
@@ -346,45 +350,60 @@ class GeneralResponse(BaseModel):
346
350
  detail: str
347
351
 
348
352
 
349
- class FilterSchema(BaseModel):
353
+ class OprFilterSchema(BaseModel):
350
354
  """
351
- Represents a filter for querying data.
355
+ Represents a filter for querying data without column name.
352
356
 
353
357
  Attributes:
354
- col (str): The column name to filter on.
355
358
  opr (str): The operator to use for the filter.
356
359
  value (Union[PRIMARY_KEY, bool, Dict[str, Any], List[Union[PRIMARY_KEY, bool, Dict[str, Any]]]]): The value to filter on.
357
-
358
360
  """
359
361
 
360
- col: str
361
362
  opr: str
362
363
  value: (
363
364
  PRIMARY_KEY | bool | dict[str, Any] | list[PRIMARY_KEY | bool | dict[str, Any]]
364
365
  )
365
366
 
366
367
 
368
+ class FilterSchema(OprFilterSchema):
369
+ """
370
+ Represents a filter for querying data.
371
+
372
+ Attributes:
373
+ col (str): The column name to filter on.
374
+ opr (str): The operator to use for the filter.
375
+ value (Union[PRIMARY_KEY, bool, Dict[str, Any], List[Union[PRIMARY_KEY, bool, Dict[str, Any]]]]): The value to filter on.
376
+
377
+ """
378
+
379
+ col: str
380
+
381
+
367
382
  class QuerySchema(BaseModel):
368
383
  """
369
384
  Represents the query parameters for selection, pagination, ordering, and filtering.
370
385
 
371
386
  Attributes:
372
- columns (str, optional): The columns to select in the query. Defaults to '[]'.
373
387
  page (int): The page number for pagination. Defaults to 0.
374
388
  page_size (int, optional): The number of items per page. Defaults to 25.
375
389
  order_column (str, optional): The column to order the results by.
376
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 '[]'.
377
392
  filters (str, optional): The filters to apply to the query. Defaults to '[]'.
393
+ opr_filters (str, optional): The filters to apply without column name. Defaults to '[]'.
378
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.
379
396
  """
380
397
 
381
- columns: str = "[]"
382
398
  page: int = 0
383
399
  page_size: int | None = 25
384
400
  order_column: str | None = None
385
401
  order_direction: Literal["asc", "desc"] | None = None
402
+ columns: str = "[]"
386
403
  filters: str = "[]"
404
+ opr_filters: str = "[]"
387
405
  global_filter: str | None = None
406
+ with_count: bool = True
388
407
 
389
408
  @field_validator("columns")
390
409
  @classmethod
@@ -394,30 +413,48 @@ class QuerySchema(BaseModel):
394
413
  columns = json.loads(v) if isinstance(v, str) else v
395
414
  logger.debug(f"Columns: {columns}")
396
415
  if not isinstance(columns, list):
397
- raise ValueError("Columns must be a list")
416
+ raise HTTPWithValidationException(
417
+ fastapi.status.HTTP_400_BAD_REQUEST,
418
+ "list_type",
419
+ "query",
420
+ "columns",
421
+ translate("Columns must be a list"),
422
+ input=columns,
423
+ )
398
424
  return [str(col) for col in columns]
399
425
  except json.JSONDecodeError:
400
- raise HTTPException(
401
- status_code=400, detail="Invalid columns: Not a valid JSON string"
426
+ raise HTTPWithValidationException(
427
+ fastapi.status.HTTP_400_BAD_REQUEST,
428
+ "json_invalid",
429
+ "query",
430
+ "columns",
431
+ translate("Invalid columns: Not a valid JSON string"),
432
+ input=v,
433
+ )
434
+ except ValidationError as e:
435
+ raise HTTPWithValidationException(
436
+ fastapi.status.HTTP_400_BAD_REQUEST,
437
+ "literal_error",
438
+ "query",
439
+ "columns",
440
+ e.errors(),
402
441
  )
403
- except ValueError as e:
404
- raise HTTPException(status_code=400, detail=str(e))
405
442
 
406
443
  @field_validator("order_direction")
407
444
  @classmethod
408
445
  def validate_order_fields(cls, v: str | None, info: ValidationInfo):
409
446
  data = info.data
410
- try:
411
- if bool(v) != bool(data.get("order_column")):
412
- raise ValueError(
413
- "Both order_column and order_direction must be filled or empty"
414
- )
415
- return str(v) if v else v
416
- except ValueError:
417
- raise HTTPException(
418
- status_code=400,
419
- detail="Both order_column and order_direction must be filled or empty",
447
+ if bool(v) != bool(data.get("order_column")):
448
+ raise HTTPWithValidationException(
449
+ fastapi.status.HTTP_400_BAD_REQUEST,
450
+ "missing",
451
+ "query",
452
+ "order_direction" if not v else "order_column",
453
+ translate(
454
+ "Both 'order_column' and 'order_direction' must be filled or empty"
455
+ ),
420
456
  )
457
+ return str(v) if v else v
421
458
 
422
459
  @field_validator("filters")
423
460
  @classmethod
@@ -427,31 +464,97 @@ class QuerySchema(BaseModel):
427
464
  filters = json.loads(v) if isinstance(v, str) else v
428
465
  logger.debug(f"Filters: {filters}")
429
466
  if not isinstance(filters, list):
430
- raise ValueError("Filters must be a list")
467
+ raise HTTPWithValidationException(
468
+ fastapi.status.HTTP_400_BAD_REQUEST,
469
+ "list_type",
470
+ "query",
471
+ "filters",
472
+ translate("Filters must be a list"),
473
+ input=filters,
474
+ )
431
475
  new_filters = []
432
- for filter in filters:
476
+ for index, filter in enumerate(filters):
433
477
  if isinstance(filter, FilterSchema):
434
478
  new_filters.append(filter)
435
479
  elif isinstance(filter, dict):
436
480
  new_filters.append(FilterSchema(**filter))
437
481
  else:
438
- raise ValueError("Filter must be a dictionary")
482
+ raise HTTPWithValidationException(
483
+ fastapi.status.HTTP_400_BAD_REQUEST,
484
+ "dict_type",
485
+ "query",
486
+ f"filters[{index}]",
487
+ translate("Filter must be an object"),
488
+ input=filter,
489
+ )
490
+ return new_filters
491
+ except json.JSONDecodeError:
492
+ raise HTTPWithValidationException(
493
+ fastapi.status.HTTP_400_BAD_REQUEST,
494
+ "json_invalid",
495
+ "query",
496
+ "filters",
497
+ translate("Invalid filters: Not a valid JSON string"),
498
+ input=v,
499
+ )
500
+ except ValidationError as e:
501
+ raise HTTPWithValidationException(
502
+ fastapi.status.HTTP_400_BAD_REQUEST,
503
+ "literal_error",
504
+ "query",
505
+ "filters",
506
+ e.errors(),
507
+ )
508
+
509
+ @field_validator("opr_filters")
510
+ @classmethod
511
+ def validate_opr_filters(cls, v: str | list[OprFilterSchema]):
512
+ try:
513
+ logger.debug(f"Validating opr_filters: {v}")
514
+ filters = json.loads(v) if isinstance(v, str) else v
515
+ logger.debug(f"Opr Filters: {filters}")
516
+ if not isinstance(filters, list):
517
+ raise HTTPWithValidationException(
518
+ fastapi.status.HTTP_400_BAD_REQUEST,
519
+ "list_type",
520
+ "query",
521
+ "opr_filters",
522
+ translate("Opr Filters must be a list"),
523
+ input=filters,
524
+ )
525
+ new_filters = []
526
+ for index, filter in enumerate(filters):
527
+ if isinstance(filter, OprFilterSchema):
528
+ new_filters.append(filter)
529
+ elif isinstance(filter, dict):
530
+ new_filters.append(OprFilterSchema(**filter))
531
+ else:
532
+ raise HTTPWithValidationException(
533
+ fastapi.status.HTTP_400_BAD_REQUEST,
534
+ "dict_type",
535
+ "query",
536
+ f"opr_filters[{index}]",
537
+ translate("Opr Filter must be an object"),
538
+ input=filter,
539
+ )
439
540
  return new_filters
440
541
  except json.JSONDecodeError:
441
- raise HTTPException(
442
- status_code=400, detail="Invalid filters: Not a valid JSON string"
542
+ raise HTTPWithValidationException(
543
+ fastapi.status.HTTP_400_BAD_REQUEST,
544
+ "json_invalid",
545
+ "query",
546
+ "opr_filters",
547
+ translate("Invalid opr_filters: Not a valid JSON string"),
548
+ input=v,
443
549
  )
444
550
  except ValidationError as e:
445
- raise HTTPException(
446
- status_code=400,
447
- detail={
448
- "type": "ValidationError",
449
- "loc": ["query", "filters"],
450
- "msg": e.errors(),
451
- },
551
+ raise HTTPWithValidationException(
552
+ fastapi.status.HTTP_400_BAD_REQUEST,
553
+ "literal_error",
554
+ "query",
555
+ "opr_filters",
556
+ e.errors(),
452
557
  )
453
- except ValueError as e:
454
- raise HTTPException(status_code=400, detail=str(e))
455
558
 
456
559
 
457
560
  class QueryBody(QuerySchema):
@@ -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
@@ -7,9 +8,11 @@ from sqlalchemy.orm import Session
7
8
 
8
9
  from ...api import BaseApi, ModelRestApi
9
10
  from ...backends.sqla.interface import SQLAInterface
10
- from ...db import UserDatabase, get_user_db
11
+ from ...const import ErrorCode
12
+ from ...db import UserDatabase, db, get_user_db
11
13
  from ...decorators import expose, login_required
12
14
  from ...globals import g
15
+ from ...lang import translate
13
16
  from ...routers import get_auth_router, get_oauth_router
14
17
  from ...schemas import (
15
18
  GeneralResponse,
@@ -19,7 +22,7 @@ from ...schemas import (
19
22
  generate_user_update_schema,
20
23
  )
21
24
  from ...setting import Setting
22
- from ...utils import SelfType, lazy, merge_schema
25
+ from ...utils import SelfType, lazy, merge_schema, smart_run
23
26
  from .models import Api, Permission, PermissionApi, Role, User
24
27
 
25
28
  __all__ = [
@@ -161,7 +164,7 @@ class UsersApi(ModelRestApi):
161
164
  col.required = False
162
165
 
163
166
  async def post_add(self, item, params):
164
- await g.current_app.security.update_user(
167
+ await g.current_app.sm.update_user(
165
168
  user_update={"password": item.password},
166
169
  user=item,
167
170
  session=params.session,
@@ -170,7 +173,7 @@ class UsersApi(ModelRestApi):
170
173
 
171
174
  async def post_update(self, item, params):
172
175
  if params.body.password:
173
- await g.current_app.security.update_user(
176
+ await g.current_app.sm.update_user(
174
177
  user_update={"password": params.body.password},
175
178
  user=item,
176
179
  session=params.session,
@@ -194,6 +197,7 @@ class AuthApi(BaseApi):
194
197
  g.auth.cookie_backend,
195
198
  g.auth.fastapi_users.get_user_manager,
196
199
  g.auth.fastapi_users.authenticator,
200
+ auth_type=Setting.AUTH_TYPE,
197
201
  )
198
202
  )
199
203
  if Setting.AUTH_LOGIN_JWT:
@@ -202,6 +206,7 @@ class AuthApi(BaseApi):
202
206
  g.auth.bearer_backend,
203
207
  g.auth.fastapi_users.get_user_manager,
204
208
  g.auth.fastapi_users.authenticator,
209
+ auth_type=Setting.AUTH_TYPE,
205
210
  ),
206
211
  prefix="/jwt",
207
212
  )
@@ -225,6 +230,11 @@ class AuthApi(BaseApi):
225
230
  for client in oauth_clients:
226
231
  oauth_client = client["oauth_client"]
227
232
  associate_by_email = client.get("associate_by_email", False)
233
+ redirect_url = client.get("redirect_url", None)
234
+ redirect_url_factory = client.get("redirect_url_factory", None)
235
+ redirect_url_after_callback = client.get(
236
+ "redirect_url_after_callback", Setting.OAUTH_REDIRECT_URI
237
+ )
228
238
  on_before_register = client.get("on_before_register", None)
229
239
  on_after_register = client.get("on_after_register", None)
230
240
  on_before_login = client.get("on_before_login", None)
@@ -235,7 +245,9 @@ class AuthApi(BaseApi):
235
245
  backend=g.auth.cookie_backend,
236
246
  get_user_manager=g.auth.fastapi_users.get_user_manager,
237
247
  state_secret=g.auth.secret_key,
238
- redirect_url=Setting.OAUTH_REDIRECT_URI,
248
+ redirect_url=redirect_url,
249
+ redirect_url_factory=redirect_url_factory,
250
+ redirect_url_after_callback=redirect_url_after_callback,
239
251
  associate_by_email=associate_by_email,
240
252
  on_before_register=on_before_register,
241
253
  on_after_register=on_after_register,
@@ -250,7 +262,7 @@ class AuthApi(BaseApi):
250
262
  response_model=self.get_user_schema,
251
263
  responses={
252
264
  status.HTTP_401_UNAUTHORIZED: {
253
- "description": "Missing token or inactive user.",
265
+ "description": ErrorCode.GET_USER_MISSING_TOKEN_OR_INACTIVE_USER,
254
266
  }
255
267
  },
256
268
  )(self.get_user)
@@ -259,17 +271,31 @@ class AuthApi(BaseApi):
259
271
  methods=["PUT"],
260
272
  responses={
261
273
  status.HTTP_401_UNAUTHORIZED: {
262
- "description": "Missing token or inactive user.",
274
+ "description": ErrorCode.GET_USER_MISSING_TOKEN_OR_INACTIVE_USER,
263
275
  }
264
276
  },
265
277
  )(self.update_user)
266
278
 
267
- def get_user(self):
279
+ async def get_user(self):
268
280
  if not g.user:
269
281
  raise HTTPException(
270
- status_code=status.HTTP_401_UNAUTHORIZED,
271
- detail="Missing token or inactive user.",
282
+ status.HTTP_401_UNAUTHORIZED,
283
+ ErrorCode.GET_USER_MISSING_TOKEN_OR_INACTIVE_USER,
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()
272
295
  )
296
+ result = await smart_run(db.current_session.scalars, query)
297
+ g.user.permissions = list(result)
298
+
273
299
  return g.user
274
300
 
275
301
  async def update_user(
@@ -280,9 +306,9 @@ class AuthApi(BaseApi):
280
306
  ):
281
307
  if not g.user:
282
308
  raise HTTPException(
283
- status_code=status.HTTP_401_UNAUTHORIZED,
284
- detail="Missing token or inactive user.",
309
+ status.HTTP_401_UNAUTHORIZED,
310
+ ErrorCode.GET_USER_MISSING_TOKEN_OR_INACTIVE_USER,
285
311
  )
286
312
  user_manager = next(g.auth.get_user_manager(user_db))
287
313
  await user_manager.update(user_update, g.user, safe=True, request=request)
288
- return GeneralResponse(detail="User updated successfully.")
314
+ return GeneralResponse(detail=translate("User updated successfully."))
@@ -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(