brilliance-admin 0.42.0__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 (73) hide show
  1. admin_panel/__init__.py +4 -0
  2. admin_panel/api/__init__.py +0 -0
  3. admin_panel/api/routers.py +18 -0
  4. admin_panel/api/utils.py +28 -0
  5. admin_panel/api/views/__init__.py +0 -0
  6. admin_panel/api/views/auth.py +29 -0
  7. admin_panel/api/views/autocomplete.py +33 -0
  8. admin_panel/api/views/graphs.py +30 -0
  9. admin_panel/api/views/index.py +38 -0
  10. admin_panel/api/views/schema.py +29 -0
  11. admin_panel/api/views/settings.py +29 -0
  12. admin_panel/api/views/table.py +136 -0
  13. admin_panel/auth.py +32 -0
  14. admin_panel/docs.py +37 -0
  15. admin_panel/exceptions.py +38 -0
  16. admin_panel/integrations/__init__.py +0 -0
  17. admin_panel/integrations/sqlalchemy/__init__.py +6 -0
  18. admin_panel/integrations/sqlalchemy/auth.py +144 -0
  19. admin_panel/integrations/sqlalchemy/autocomplete.py +38 -0
  20. admin_panel/integrations/sqlalchemy/fields.py +254 -0
  21. admin_panel/integrations/sqlalchemy/fields_schema.py +316 -0
  22. admin_panel/integrations/sqlalchemy/table/__init__.py +19 -0
  23. admin_panel/integrations/sqlalchemy/table/base.py +141 -0
  24. admin_panel/integrations/sqlalchemy/table/create.py +73 -0
  25. admin_panel/integrations/sqlalchemy/table/delete.py +18 -0
  26. admin_panel/integrations/sqlalchemy/table/list.py +178 -0
  27. admin_panel/integrations/sqlalchemy/table/retrieve.py +61 -0
  28. admin_panel/integrations/sqlalchemy/table/update.py +95 -0
  29. admin_panel/schema/__init__.py +7 -0
  30. admin_panel/schema/admin_schema.py +191 -0
  31. admin_panel/schema/category.py +149 -0
  32. admin_panel/schema/graphs/__init__.py +1 -0
  33. admin_panel/schema/graphs/category_graphs.py +50 -0
  34. admin_panel/schema/group.py +67 -0
  35. admin_panel/schema/table/__init__.py +8 -0
  36. admin_panel/schema/table/admin_action.py +76 -0
  37. admin_panel/schema/table/category_table.py +175 -0
  38. admin_panel/schema/table/fields/__init__.py +5 -0
  39. admin_panel/schema/table/fields/base.py +249 -0
  40. admin_panel/schema/table/fields/function_field.py +65 -0
  41. admin_panel/schema/table/fields_schema.py +216 -0
  42. admin_panel/schema/table/table_models.py +53 -0
  43. admin_panel/static/favicon.jpg +0 -0
  44. admin_panel/static/index-BeniOHDv.js +525 -0
  45. admin_panel/static/index-vlBToOhT.css +8 -0
  46. admin_panel/static/materialdesignicons-webfont-CYDMK1kx.woff2 +0 -0
  47. admin_panel/static/materialdesignicons-webfont-CgCzGbLl.woff +0 -0
  48. admin_panel/static/materialdesignicons-webfont-D3kAzl71.ttf +0 -0
  49. admin_panel/static/materialdesignicons-webfont-DttUABo4.eot +0 -0
  50. admin_panel/static/tinymce/dark-first/content.min.css +250 -0
  51. admin_panel/static/tinymce/dark-first/skin.min.css +2820 -0
  52. admin_panel/static/tinymce/dark-slim/content.min.css +249 -0
  53. admin_panel/static/tinymce/dark-slim/skin.min.css +2821 -0
  54. admin_panel/static/tinymce/img/example.png +0 -0
  55. admin_panel/static/tinymce/img/tinymce.woff2 +0 -0
  56. admin_panel/static/tinymce/lightgray/content.min.css +1 -0
  57. admin_panel/static/tinymce/lightgray/fonts/tinymce.woff +0 -0
  58. admin_panel/static/tinymce/lightgray/skin.min.css +1 -0
  59. admin_panel/static/tinymce/plugins/accordion/css/accordion.css +17 -0
  60. admin_panel/static/tinymce/plugins/accordion/plugin.js +48 -0
  61. admin_panel/static/tinymce/plugins/codesample/css/prism.css +1 -0
  62. admin_panel/static/tinymce/plugins/customLink/css/link.css +3 -0
  63. admin_panel/static/tinymce/plugins/customLink/plugin.js +147 -0
  64. admin_panel/static/tinymce/tinymce.min.js +2 -0
  65. admin_panel/static/vanilla-picker-B6E6ObS_.js +8 -0
  66. admin_panel/templates/index.html +25 -0
  67. admin_panel/translations.py +145 -0
  68. admin_panel/utils.py +50 -0
  69. brilliance_admin-0.42.0.dist-info/METADATA +155 -0
  70. brilliance_admin-0.42.0.dist-info/RECORD +73 -0
  71. brilliance_admin-0.42.0.dist-info/WHEEL +5 -0
  72. brilliance_admin-0.42.0.dist-info/licenses/LICENSE +17 -0
  73. brilliance_admin-0.42.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,191 @@
1
+ import importlib.metadata
2
+ import json
3
+ from importlib import resources
4
+ from typing import Any, Dict, List, Optional, Type
5
+ from urllib.parse import urljoin
6
+
7
+ from fastapi import FastAPI, Request
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from fastapi.staticfiles import StaticFiles
10
+ from pydantic.dataclasses import dataclass
11
+
12
+ from admin_panel.auth import UserABC
13
+ from admin_panel.docs import build_redoc_docs, build_scalar_docs
14
+ from admin_panel.schema.group import Group, GroupSchemaData
15
+ from admin_panel.translations import LanguageManager, TranslateText
16
+ from admin_panel.utils import DataclassBase
17
+
18
+
19
+ @dataclass
20
+ class AdminSchemaData(DataclassBase):
21
+ groups: Dict[str, GroupSchemaData]
22
+ profile: UserABC | Any
23
+
24
+ def __post_init__(self):
25
+ if not isinstance(self.profile, UserABC):
26
+ self.profile = UserABC(username=self.profile.username)
27
+
28
+
29
+ # pylint: disable=too-many-instance-attributes
30
+ @dataclass
31
+ class AdminSettingsData(DataclassBase):
32
+ title: str | TranslateText
33
+ description: str | TranslateText | None
34
+ login_greetings_message: str | TranslateText | None
35
+ navbar_density: str
36
+ languages: Dict[str, str] | None
37
+
38
+
39
+ @dataclass
40
+ class AdminIndexContextData(DataclassBase):
41
+ title: str
42
+ favicon_image: str
43
+ settings_json: str
44
+
45
+
46
+ @dataclass
47
+ class AdminSchema:
48
+ groups: List[Group]
49
+ auth: Any
50
+
51
+ title: str | TranslateText | None = 'Admin'
52
+ description: str | TranslateText | None = None
53
+ login_greetings_message: str | TranslateText | None = None
54
+
55
+ logo_image: str | None = None
56
+ favicon_image: str = '/admin/static/favicon.jpg'
57
+
58
+ navbar_density: str = 'default'
59
+
60
+ backend_prefix = None
61
+ static_prefix = None
62
+
63
+ language_manager_class: Type[LanguageManager] = LanguageManager
64
+
65
+ def __post_init__(self):
66
+ for group in self.groups:
67
+ if not issubclass(group.__class__, Group):
68
+ raise TypeError(f'Group "{group}" is not instance of Group subclass')
69
+
70
+ def get_language_manager(self, language_slug: str | None) -> LanguageManager:
71
+ return self.language_manager_class(language_slug)
72
+
73
+ def generate_schema(self, user: UserABC, language_slug: str | None) -> AdminSchemaData:
74
+ language_manager: LanguageManager = self.get_language_manager(language_slug)
75
+
76
+ groups = {}
77
+
78
+ for group in self.groups:
79
+ if not group.slug:
80
+ msg = f'Category group {type(group).__name__}.slug is empty'
81
+ raise AttributeError(msg)
82
+
83
+ groups[group.slug] = group.generate_schema(user, language_manager)
84
+
85
+ return AdminSchemaData(
86
+ groups=groups,
87
+ profile=user,
88
+ )
89
+
90
+ def get_group(self, group_slug: str) -> Optional[Group]:
91
+ for group in self.groups:
92
+ if group.slug == group_slug:
93
+ return group
94
+
95
+ return None
96
+
97
+ async def get_settings(self, request: Request) -> AdminSettingsData:
98
+ language_slug = request.headers.get('Accept-Language')
99
+ language_manager: LanguageManager = self.get_language_manager(language_slug)
100
+
101
+ languages = None
102
+ if language_manager.languages:
103
+ languages = {}
104
+ for k, v in language_manager.languages.items():
105
+ languages[k] = language_manager.get_text(v)
106
+
107
+ return AdminSettingsData(
108
+ title=self.title,
109
+ description=self.description,
110
+ login_greetings_message=self.login_greetings_message,
111
+ navbar_density=self.navbar_density,
112
+ languages=languages,
113
+ )
114
+
115
+ # pylint: disable=too-many-arguments
116
+ # pylint: disable=too-many-positional-arguments
117
+ def generate_app(
118
+ self,
119
+ debug=False,
120
+ allow_cors=True,
121
+
122
+ include_scalar=False,
123
+ include_docs=False,
124
+ include_redoc=False,
125
+ ) -> FastAPI:
126
+ # pylint: disable=unused-variable
127
+ language_manager = self.get_language_manager(language_slug=None)
128
+
129
+ app = FastAPI(
130
+ title=language_manager.get_text(self.title),
131
+ description=language_manager.get_text(self.description),
132
+ debug=debug,
133
+ docs_url='/docs' if include_docs else None,
134
+ redoc_url=None,
135
+ )
136
+
137
+ if allow_cors:
138
+ app.add_middleware(
139
+ CORSMiddleware,
140
+ allow_origins=["*"],
141
+ allow_credentials=True,
142
+ allow_methods=["*"],
143
+ allow_headers=["*"]
144
+ )
145
+
146
+ static_dir = resources.files("admin_panel").joinpath("static")
147
+ app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
148
+
149
+ app.state.schema = self
150
+
151
+ if include_scalar:
152
+ app.include_router(build_scalar_docs(app))
153
+
154
+ if include_redoc:
155
+ app.include_router(build_redoc_docs(app, redoc_url='/redoc'))
156
+
157
+ # pylint: disable=import-outside-toplevel
158
+ from admin_panel.api.routers import admin_panel_router
159
+ app.include_router(admin_panel_router)
160
+
161
+ return app
162
+
163
+ async def get_index_context_data(self, request: Request) -> dict:
164
+ language_manager = self.get_language_manager(language_slug=None)
165
+ context = {'language_manager': language_manager}
166
+
167
+ backend_prefix = self.backend_prefix
168
+ if not backend_prefix:
169
+ backend_prefix = urljoin(str(request.base_url), '/admin/')
170
+
171
+ static_prefix = self.static_prefix
172
+ if not static_prefix:
173
+ static_prefix = urljoin(str(request.base_url), '/admin/static/')
174
+
175
+ logo_image = self.logo_image
176
+ if logo_image and logo_image.startswith('/'):
177
+ logo_image = urljoin(str(request.base_url), logo_image)
178
+
179
+ settings_json = {
180
+ 'backend_prefix': backend_prefix,
181
+ 'static_prefix': static_prefix,
182
+ 'version': importlib.metadata.version('brilliance-admin'),
183
+ 'api_timeout_ms': 1000 * 5,
184
+ 'logo_image': logo_image,
185
+ }
186
+ data = AdminIndexContextData(
187
+ title=str(self.title),
188
+ favicon_image=self.favicon_image,
189
+ settings_json=json.dumps(settings_json),
190
+ )
191
+ return data.model_dump(mode='json', context=context)
@@ -0,0 +1,149 @@
1
+ import abc
2
+ from typing import Any, ClassVar, Dict, List
3
+
4
+ from pydantic import Field
5
+ from pydantic.dataclasses import dataclass
6
+ from pydantic_core import core_schema
7
+
8
+ from admin_panel.auth import UserABC
9
+ from admin_panel.translations import LanguageManager, TranslateText
10
+ from admin_panel.utils import DataclassBase
11
+
12
+
13
+ # pylint: disable=too-many-instance-attributes
14
+ @dataclass
15
+ class FieldSchemaData(DataclassBase):
16
+ type: str = None
17
+
18
+ label: str | None = None
19
+ help_text: str | None = None
20
+
21
+ # Table header parameters
22
+ header: dict = Field(default_factory=dict)
23
+
24
+ read_only: bool = False
25
+ default: Any | None = None
26
+ required: bool = False
27
+
28
+ max_length: int | None = None
29
+ min_length: int | None = None
30
+
31
+ choices: List[dict] | None = None
32
+
33
+ tag_colors: dict | None = None
34
+ variant: str | None = None
35
+ size: str | None = None
36
+
37
+ preview_max_height: int | None = None
38
+ preview_max_width: int | None = None
39
+
40
+ # StringField
41
+ multilined: bool | None = None
42
+ ckeditor: bool | None = None
43
+ tinymce: bool | None = None
44
+
45
+ # ArrayField
46
+ array_type: str | None = None
47
+
48
+ # SQLAlchemyRelatedField
49
+ many: bool | None = None
50
+ rel_name: str | None = None
51
+ dual_list: bool | None = None
52
+
53
+ # IntegerField
54
+ inputmode: str | None = None
55
+ precision: int | None = None
56
+ scale: int | None = None
57
+
58
+ # DateTimeField
59
+ range: bool | None = None
60
+ include_date: bool | None = None
61
+ include_time: bool | None = None
62
+
63
+
64
+ @dataclass
65
+ class FieldsSchemaData(DataclassBase):
66
+ fields: Dict[str, dict] = Field(default_factory=dict)
67
+ list_display: List[str] = Field(default_factory=list)
68
+
69
+
70
+ # pylint: disable=too-many-instance-attributes
71
+ @dataclass
72
+ class TableInfoSchemaData(DataclassBase):
73
+ table_schema: FieldsSchemaData
74
+
75
+ search_enabled: bool = Field(default=False)
76
+ search_help: str | None = Field(default=None)
77
+
78
+ pk_name: str | None = Field(default=None)
79
+ can_retrieve: bool = Field(default=False)
80
+
81
+ can_create: bool = Field(default=False)
82
+ can_update: bool = Field(default=False)
83
+
84
+ table_filters: FieldsSchemaData | None = Field(default=None)
85
+
86
+ ordering_fields: List[str] = Field(default_factory=list)
87
+ default_ordering: str | None = None
88
+
89
+ actions: Dict[str, dict] | None = Field(default_factory=dict)
90
+
91
+ def __repr__(self):
92
+ return f'<TableInfoSchemaData id={id(self)}>'
93
+
94
+
95
+ @dataclass
96
+ class GraphInfoSchemaData(DataclassBase):
97
+ search_enabled: bool
98
+ search_help: str | None
99
+
100
+ table_filters: FieldsSchemaData | None = None
101
+
102
+
103
+ @dataclass
104
+ class CategorySchemaData(DataclassBase):
105
+ title: str | None
106
+ description: str | None
107
+ icon: str | None
108
+ type: str
109
+
110
+ table_info: TableInfoSchemaData | None = None
111
+ graph_info: GraphInfoSchemaData | None = None
112
+
113
+ def __repr__(self):
114
+ return f'<CategorySchemaData type={self.type} "{self.title}">'
115
+
116
+
117
+ class Category(abc.ABC):
118
+ slug: ClassVar[str]
119
+ title: ClassVar[str | TranslateText | None] = None
120
+ description: ClassVar[str | TranslateText | None] = None
121
+
122
+ # https://pictogrammers.com/library/mdi/
123
+ icon: ClassVar[str | None] = None
124
+
125
+ _type_slug: ClassVar[str]
126
+
127
+ def generate_schema(self, user: UserABC, language_manager: LanguageManager) -> CategorySchemaData:
128
+ return CategorySchemaData(
129
+ title=language_manager.get_text(self.title) or self.slug,
130
+ description=language_manager.get_text(self.description),
131
+ icon=self.icon,
132
+ type=self._type_slug,
133
+ )
134
+
135
+ @classmethod
136
+ def __get_pydantic_core_schema__(cls, source_type: Any, handler: Any) -> core_schema.CoreSchema:
137
+ def validate(v: Any) -> "Category":
138
+ if isinstance(v, cls):
139
+ return v
140
+ raise TypeError(f"Expected {cls.__name__} instance")
141
+
142
+ return core_schema.no_info_plain_validator_function(
143
+ validate,
144
+ serialization=core_schema.plain_serializer_function_ser_schema(
145
+ lambda v: repr(v),
146
+ info_arg=False,
147
+ return_schema=core_schema.str_schema(),
148
+ ),
149
+ )
@@ -0,0 +1 @@
1
+ from .category_graphs import CategoryGraphs
@@ -0,0 +1,50 @@
1
+ from typing import Any, Dict, List
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from admin_panel.schema import Category
6
+ from admin_panel.schema.category import GraphInfoSchemaData
7
+ from admin_panel.schema.table.fields_schema import FieldsSchema
8
+ from admin_panel.translations import LanguageManager, TranslateText
9
+
10
+
11
+ class GraphData(BaseModel):
12
+ search: str | None = None
13
+ filters: Dict[str, Any] = Field(default_factory=dict)
14
+
15
+
16
+ class ChartData(BaseModel):
17
+ data: dict
18
+ options: dict
19
+ width: int | None = None
20
+ height: int = 50
21
+ type: str = 'line'
22
+
23
+
24
+ class GraphsDataResult(BaseModel):
25
+ charts: List[ChartData]
26
+
27
+
28
+ class CategoryGraphs(Category):
29
+ _type_slug: str = 'graphs'
30
+
31
+ search_enabled: bool = False
32
+ search_help: str | TranslateText | None = None
33
+
34
+ table_filters: FieldsSchema | None = None
35
+
36
+ def generate_schema(self, user, language_manager: LanguageManager) -> GraphInfoSchemaData:
37
+ schema = super().generate_schema(user, language_manager)
38
+ graph = GraphInfoSchemaData(
39
+ search_enabled=self.search_enabled,
40
+ search_help=language_manager.get_text(self.search_help),
41
+ )
42
+
43
+ if self.table_filters:
44
+ graph.table_filters = self.table_filters.generate_schema(user, language_manager)
45
+
46
+ schema.graph_info = graph
47
+ return schema
48
+
49
+ async def get_data(self, data: GraphData, user) -> GraphsDataResult:
50
+ raise NotImplementedError('get_data is not implemented')
@@ -0,0 +1,67 @@
1
+ import abc
2
+ from typing import Dict, List
3
+
4
+ from pydantic.dataclasses import dataclass
5
+
6
+ from admin_panel.auth import UserABC
7
+ from admin_panel.schema.category import Category, CategorySchemaData
8
+ from admin_panel.translations import LanguageManager, TranslateText
9
+ from admin_panel.utils import DataclassBase, get_logger
10
+
11
+ logger = get_logger()
12
+
13
+
14
+ @dataclass
15
+ class GroupSchemaData(DataclassBase):
16
+ title: str | None
17
+ description: str | None
18
+ icon: str | None
19
+ categories: Dict[str, CategorySchemaData]
20
+
21
+
22
+ @dataclass
23
+ class Group(abc.ABC):
24
+ categories: List[Category]
25
+ slug: str
26
+ title: str | TranslateText | None = None
27
+ description: str | TranslateText | None = None
28
+
29
+ # https://pictogrammers.com/library/mdi/
30
+ icon: str | None = None
31
+
32
+ def __post_init__(self):
33
+ for category in self.categories:
34
+ if not issubclass(category.__class__, Category):
35
+ raise TypeError(f'Category "{category}" is not instance of Category subclass')
36
+
37
+ def generate_schema(self, user: UserABC, language_manager: LanguageManager) -> GroupSchemaData:
38
+ result = GroupSchemaData(
39
+ title=language_manager.get_text(self.title) or self.slug,
40
+ description=language_manager.get_text(self.description),
41
+ icon=self.icon,
42
+ categories={},
43
+ )
44
+ if not self.categories:
45
+ logger.warning('Group "%s" %s.categories is empty!', self.slug, type(self).__name__)
46
+
47
+ for category in self.categories:
48
+
49
+ if not category.slug:
50
+ msg = f'Category {type(category).__name__}.slug is empty'
51
+ raise AttributeError(msg)
52
+
53
+ if category.slug in result.categories:
54
+ exists = result.categories[category.slug]
55
+ msg = f'Category {type(category).__name__}.slug "{self.slug}" already registered by "{exists.title}"'
56
+ raise KeyError(msg)
57
+
58
+ result.categories[category.slug] = category.generate_schema(user, language_manager)
59
+
60
+ return result
61
+
62
+ def get_category(self, category_slug: str) -> Category | None:
63
+ for category in self.categories:
64
+ if category.slug == category_slug:
65
+ return category
66
+
67
+ return None
@@ -0,0 +1,8 @@
1
+ # pylint: disable=wildcard-import, unused-wildcard-import, unused-import
2
+ # flake8: noqa: F405
3
+ from .admin_action import admin_action
4
+ from .category_table import CategoryTable
5
+ from .fields import *
6
+ from .fields_schema import FieldsSchema
7
+ from .table_models import (
8
+ AutocompleteData, AutocompleteResult, CreateResult, ListData, RetrieveResult, TableListResult, UpdateResult)
@@ -0,0 +1,76 @@
1
+ import functools
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ from pydantic import BaseModel, Field, validate_call
5
+ from pydantic.dataclasses import dataclass
6
+
7
+ from admin_panel.schema.table.fields_schema import FieldsSchema
8
+ from admin_panel.translations import DataclassBase, TranslateText
9
+
10
+
11
+ class ActionData(BaseModel):
12
+ pks: List[Any] = Field(default_factory=list)
13
+ form_data: dict = Field(default_factory=dict)
14
+
15
+ search: str | None = None
16
+ filters: Dict[str, Any] = Field(default_factory=dict)
17
+
18
+ send_to_all: bool = False
19
+
20
+
21
+ @dataclass
22
+ class ActionMessage(DataclassBase):
23
+ text: str | TranslateText
24
+ type: str = 'success'
25
+ position: str = 'top-center'
26
+
27
+
28
+ @dataclass
29
+ class ActionResult(DataclassBase):
30
+ message: ActionMessage | None = None
31
+ persistent_message: str | TranslateText | None = None
32
+
33
+
34
+ # pylint: disable=too-many-arguments
35
+ # pylint: disable=too-many-positional-arguments
36
+ @validate_call
37
+ def admin_action(
38
+ title: str | TranslateText,
39
+ description: Optional[str | TranslateText] = None,
40
+ confirmation_text: Optional[str | TranslateText] = None,
41
+
42
+ # https://vuetifyjs.com/en/styles/colors/#material-colors
43
+ base_color: Optional[str] = None,
44
+
45
+ # https://pictogrammers.com/library/mdi/
46
+ icon: Optional[str] = None,
47
+
48
+ # elevated, flat, tonal, outlined, text, and plain.
49
+ variant: Optional[str] = None,
50
+
51
+ allow_empty_selection: bool = False,
52
+ form_schema: Optional[FieldsSchema] = None,
53
+ ):
54
+ def wrapper(func):
55
+ func.__action__ = True
56
+
57
+ func.action_info = {
58
+ 'title': title,
59
+ 'description': description,
60
+ 'confirmation_text': confirmation_text,
61
+
62
+ 'icon': icon,
63
+ 'base_color': base_color,
64
+ 'variant': variant,
65
+
66
+ 'allow_empty_selection': allow_empty_selection,
67
+ 'form_schema': form_schema,
68
+ }
69
+
70
+ @functools.wraps(func)
71
+ async def wrapped(*args):
72
+ return await func(*args)
73
+
74
+ return wrapped
75
+
76
+ return wrapper