brilliance-admin 0.43.7__py3-none-any.whl → 0.44.1__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.
- brilliance_admin/api/views/table.py +2 -1
- brilliance_admin/locales/en.yml +4 -2
- brilliance_admin/locales/ru.yml +1 -0
- brilliance_admin/schema/__init__.py +2 -2
- brilliance_admin/schema/admin_schema.py +21 -19
- brilliance_admin/schema/category.py +85 -10
- brilliance_admin/schema/graphs/category_graphs.py +2 -3
- brilliance_admin/schema/table/category_table.py +4 -2
- brilliance_admin/schema/table/fields/base.py +21 -6
- brilliance_admin/static/{index-vlBToOhT.css → index-P_wdMBbz.css} +1 -1
- brilliance_admin/static/{index-BnnESruI.js → index-rBvEkjGg.js} +57 -57
- brilliance_admin/templates/index.html +2 -2
- brilliance_admin/utils.py +38 -0
- {brilliance_admin-0.43.7.dist-info → brilliance_admin-0.44.1.dist-info}/METADATA +48 -108
- {brilliance_admin-0.43.7.dist-info → brilliance_admin-0.44.1.dist-info}/RECORD +18 -19
- brilliance_admin-0.44.1.dist-info/licenses/LICENSE +21 -0
- brilliance_admin/schema/group.py +0 -67
- brilliance_admin-0.43.7.dist-info/licenses/LICENSE +0 -17
- {brilliance_admin-0.43.7.dist-info → brilliance_admin-0.44.1.dist-info}/WHEEL +0 -0
- {brilliance_admin-0.43.7.dist-info → brilliance_admin-0.44.1.dist-info}/top_level.txt +0 -0
|
@@ -8,7 +8,8 @@ from brilliance_admin.exceptions import AdminAPIException, APIError
|
|
|
8
8
|
from brilliance_admin.schema import AdminSchema
|
|
9
9
|
from brilliance_admin.schema.table.admin_action import ActionData, ActionResult
|
|
10
10
|
from brilliance_admin.schema.table.category_table import CategoryTable
|
|
11
|
-
from brilliance_admin.schema.table.table_models import
|
|
11
|
+
from brilliance_admin.schema.table.table_models import (
|
|
12
|
+
CreateResult, ListData, RetrieveResult, TableListResult, UpdateResult)
|
|
12
13
|
from brilliance_admin.translations import LanguageContext
|
|
13
14
|
from brilliance_admin.utils import get_logger
|
|
14
15
|
|
brilliance_admin/locales/en.yml
CHANGED
|
@@ -3,16 +3,18 @@ delete_confirmation_text: "Are you sure you want to delete those records?\nThis
|
|
|
3
3
|
deleted_successfully: 'The entries were successfully deleted.'
|
|
4
4
|
|
|
5
5
|
errors:
|
|
6
|
+
pk_not_found: 'The "%(pk_name)s" field was not found in the submitted data.'
|
|
7
|
+
record_not_found: 'No record found for %(pk_name)s=%(pk)s.'
|
|
6
8
|
db_error_create: 'Error creating a record in the database.'
|
|
7
9
|
db_error_update: 'Error updating the record in the database.'
|
|
8
10
|
db_error_retrieve: 'Error retrieving the record from the database.'
|
|
9
11
|
db_error_list: 'Failed to retrieve table data from the database.'
|
|
10
|
-
pk_not_found: 'The "%(pk_name)s" field was not found in the submitted data.'
|
|
11
|
-
record_not_found: 'No record found for %(pk_name)s=%(pk)s.'
|
|
12
12
|
connection_refused_error: 'Database connection error: %(error)s'
|
|
13
13
|
filters_exception: 'An unknown technical error occurred while filtering data.'
|
|
14
14
|
method_not_allowed: 'Error, method not allowed. This action is not permitted.'
|
|
15
15
|
filter_error: 'An error occurred during filtering: {error}'
|
|
16
|
+
serialize_field_error: 'Serialize error: %(error)s'
|
|
17
|
+
bad_type_error: 'Invalid data type: %(type)s but %(expected)s expected'
|
|
16
18
|
|
|
17
19
|
search_help: 'Available search fields: %(fields)s'
|
|
18
20
|
sqlalchemy_search_help: |
|
brilliance_admin/locales/ru.yml
CHANGED
|
@@ -14,6 +14,7 @@ errors:
|
|
|
14
14
|
method_not_allowed: 'Ошибка, данный метод недоступен.'
|
|
15
15
|
filter_error: 'Проишла ошибка при фильтрации: %(error)s'
|
|
16
16
|
serialize_field_error: 'Ошибка чтения данных: %(error)s'
|
|
17
|
+
bad_type_error: 'Некорректный тип данных: %(type)s; ожидается %(expected)s'
|
|
17
18
|
|
|
18
19
|
search_help: 'Доступные поля для поиска: %(fields)s'
|
|
19
20
|
sqlalchemy_search_help: |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pylint: disable=wildcard-import, unused-wildcard-import, unused-import
|
|
2
2
|
# flake8: noqa: F405
|
|
3
3
|
from .admin_schema import AdminSchema, AdminSchemaData
|
|
4
|
-
from .category import
|
|
4
|
+
from .category import CategoryGroup, CategoryLink
|
|
5
5
|
from .graphs import *
|
|
6
|
-
from .group import Group
|
|
7
6
|
from .table import *
|
|
7
|
+
from .table.category_table import CategoryTable
|
|
@@ -7,11 +7,12 @@ from urllib.parse import urljoin
|
|
|
7
7
|
from fastapi import FastAPI, Request
|
|
8
8
|
from fastapi.middleware.cors import CORSMiddleware
|
|
9
9
|
from fastapi.staticfiles import StaticFiles
|
|
10
|
+
from pydantic import Field
|
|
10
11
|
from pydantic.dataclasses import dataclass
|
|
11
12
|
|
|
12
13
|
from brilliance_admin.auth import UserABC
|
|
13
14
|
from brilliance_admin.docs import build_redoc_docs, build_scalar_docs
|
|
14
|
-
from brilliance_admin.schema.
|
|
15
|
+
from brilliance_admin.schema.category import BaseCategory, CategorySchemaData
|
|
15
16
|
from brilliance_admin.translations import LanguageContext, LanguageManager
|
|
16
17
|
from brilliance_admin.utils import DataclassBase, SupportsStr
|
|
17
18
|
|
|
@@ -23,8 +24,8 @@ DEFAULT_LANGUAGES = {
|
|
|
23
24
|
|
|
24
25
|
@dataclass
|
|
25
26
|
class AdminSchemaData(DataclassBase):
|
|
26
|
-
groups: Dict[str, GroupSchemaData]
|
|
27
27
|
profile: UserABC | Any
|
|
28
|
+
categories: Dict[str, CategorySchemaData] = Field(default_factory=dict)
|
|
28
29
|
|
|
29
30
|
def __post_init__(self):
|
|
30
31
|
if not isinstance(self.profile, UserABC):
|
|
@@ -50,7 +51,7 @@ class AdminIndexContextData(DataclassBase):
|
|
|
50
51
|
|
|
51
52
|
@dataclass
|
|
52
53
|
class AdminSchema:
|
|
53
|
-
|
|
54
|
+
categories: List[BaseCategory]
|
|
54
55
|
auth: Any
|
|
55
56
|
|
|
56
57
|
title: SupportsStr | None = 'Admin'
|
|
@@ -68,9 +69,9 @@ class AdminSchema:
|
|
|
68
69
|
language_manager: LanguageManager | None = None
|
|
69
70
|
|
|
70
71
|
def __post_init__(self):
|
|
71
|
-
for
|
|
72
|
-
if not issubclass(
|
|
73
|
-
raise TypeError(f'
|
|
72
|
+
for category in self.categories:
|
|
73
|
+
if not issubclass(category.__class__, BaseCategory):
|
|
74
|
+
raise TypeError(f'Root category "{category}" is not instance of BaseCategory subclass')
|
|
74
75
|
|
|
75
76
|
if not self.language_manager:
|
|
76
77
|
self.language_manager = LanguageManager(DEFAULT_LANGUAGES)
|
|
@@ -81,24 +82,25 @@ class AdminSchema:
|
|
|
81
82
|
def generate_schema(self, user: UserABC, language_slug: str | None) -> AdminSchemaData:
|
|
82
83
|
language_context: LanguageContext = self.get_language_context(language_slug)
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
result = AdminSchemaData(profile=user)
|
|
85
86
|
|
|
86
|
-
for
|
|
87
|
-
if not
|
|
88
|
-
msg = f'Category
|
|
87
|
+
for category in self.categories:
|
|
88
|
+
if not category.slug:
|
|
89
|
+
msg = f'Category {type(category).__name__}.slug is empty'
|
|
89
90
|
raise AttributeError(msg)
|
|
90
91
|
|
|
91
|
-
|
|
92
|
+
try:
|
|
93
|
+
result.categories[category.slug] = category.generate_schema(user, language_context).to_dict(keep_none=False)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
msg = f'Root category "{category.slug}" generate_schema error: {e}'
|
|
96
|
+
raise Exception(msg) from e
|
|
92
97
|
|
|
93
|
-
return
|
|
94
|
-
groups=groups,
|
|
95
|
-
profile=user,
|
|
96
|
-
)
|
|
98
|
+
return result
|
|
97
99
|
|
|
98
|
-
def get_group(self, group_slug: str) -> Optional[
|
|
99
|
-
for
|
|
100
|
-
if
|
|
101
|
-
return
|
|
100
|
+
def get_group(self, group_slug: str) -> Optional[BaseCategory]:
|
|
101
|
+
for category in self.categories:
|
|
102
|
+
if category.slug == group_slug:
|
|
103
|
+
return category
|
|
102
104
|
|
|
103
105
|
return None
|
|
104
106
|
|
|
@@ -4,10 +4,13 @@ from typing import Any, ClassVar, Dict, List
|
|
|
4
4
|
from pydantic import Field
|
|
5
5
|
from pydantic.dataclasses import dataclass
|
|
6
6
|
from pydantic_core import core_schema
|
|
7
|
+
from structlog import get_logger
|
|
7
8
|
|
|
8
9
|
from brilliance_admin.auth import UserABC
|
|
9
10
|
from brilliance_admin.translations import LanguageContext
|
|
10
|
-
from brilliance_admin.utils import DataclassBase, SupportsStr, humanize_field_name
|
|
11
|
+
from brilliance_admin.utils import DataclassBase, KwargsInitMixin, SupportsStr, humanize_field_name
|
|
12
|
+
|
|
13
|
+
logger = get_logger()
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
# pylint: disable=too-many-instance-attributes
|
|
@@ -106,37 +109,56 @@ class CategorySchemaData(DataclassBase):
|
|
|
106
109
|
icon: str | None
|
|
107
110
|
type: str
|
|
108
111
|
|
|
112
|
+
categories: dict = Field(default_factory=dict)
|
|
113
|
+
|
|
109
114
|
table_info: TableInfoSchemaData | None = None
|
|
110
115
|
graph_info: GraphInfoSchemaData | None = None
|
|
111
116
|
|
|
117
|
+
link: str | None = None
|
|
118
|
+
|
|
112
119
|
def __repr__(self):
|
|
113
120
|
return f'<CategorySchemaData type={self.type} "{self.title}">'
|
|
114
121
|
|
|
115
122
|
|
|
116
|
-
class
|
|
117
|
-
slug:
|
|
118
|
-
title:
|
|
119
|
-
description:
|
|
123
|
+
class BaseCategory(KwargsInitMixin, abc.ABC):
|
|
124
|
+
slug: str
|
|
125
|
+
title: SupportsStr | None = None
|
|
126
|
+
description: SupportsStr | None = None
|
|
120
127
|
|
|
121
128
|
# https://pictogrammers.com/library/mdi/
|
|
122
|
-
icon:
|
|
129
|
+
icon: str | None = None
|
|
123
130
|
|
|
124
131
|
_type_slug: ClassVar[str]
|
|
125
132
|
|
|
126
133
|
def generate_schema(self, user: UserABC, language_context: LanguageContext) -> CategorySchemaData:
|
|
127
|
-
|
|
134
|
+
type_slug = getattr(type(self), '_type_slug', None)
|
|
135
|
+
if not type_slug:
|
|
136
|
+
msg = f'{type(self).__name__}._type_slug must be set!'
|
|
137
|
+
raise AttributeError(msg)
|
|
138
|
+
|
|
139
|
+
result = CategorySchemaData(
|
|
128
140
|
title=language_context.get_text(self.title) or humanize_field_name(self.slug),
|
|
129
141
|
description=language_context.get_text(self.description),
|
|
130
142
|
icon=self.icon,
|
|
131
|
-
type=
|
|
143
|
+
type=type_slug,
|
|
132
144
|
)
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
def __init_subclass__(cls, **kwargs):
|
|
148
|
+
super().__init_subclass__(**kwargs)
|
|
149
|
+
|
|
150
|
+
if cls is BaseCategory:
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
if not issubclass(cls, BaseCategory):
|
|
154
|
+
raise TypeError(f'{cls.__name__} must inherit from Category')
|
|
133
155
|
|
|
134
156
|
@classmethod
|
|
135
157
|
def __get_pydantic_core_schema__(cls, source_type: Any, handler: Any) -> core_schema.CoreSchema:
|
|
136
|
-
def validate(v: Any) -> "
|
|
158
|
+
def validate(v: Any) -> "BaseCategory":
|
|
137
159
|
if isinstance(v, cls):
|
|
138
160
|
return v
|
|
139
|
-
raise TypeError(f"Expected {cls.__name__} instance")
|
|
161
|
+
raise TypeError(f"Expected {cls.__name__} instance, recieved: {type(v)} {v}")
|
|
140
162
|
|
|
141
163
|
return core_schema.no_info_plain_validator_function(
|
|
142
164
|
validate,
|
|
@@ -146,3 +168,56 @@ class Category(abc.ABC):
|
|
|
146
168
|
return_schema=core_schema.str_schema(),
|
|
147
169
|
),
|
|
148
170
|
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class CategoryLink(BaseCategory):
|
|
174
|
+
_type_slug: str = 'link'
|
|
175
|
+
|
|
176
|
+
link: str
|
|
177
|
+
|
|
178
|
+
def generate_schema(self, user: UserABC, language_context: LanguageContext) -> CategorySchemaData:
|
|
179
|
+
result = super().generate_schema(user, language_context)
|
|
180
|
+
result.link = self.link
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class CategoryGroup(BaseCategory):
|
|
185
|
+
_type_slug: str = 'group'
|
|
186
|
+
|
|
187
|
+
subcategories: list = Field(default_factory=list)
|
|
188
|
+
|
|
189
|
+
def __init__(self, *args, **kwargs):
|
|
190
|
+
super().__init__(*args, **kwargs)
|
|
191
|
+
|
|
192
|
+
for category in self.subcategories:
|
|
193
|
+
if not isinstance(category, BaseCategory):
|
|
194
|
+
raise TypeError(f'Category "{category}" is not instance of BaseCategory subclass')
|
|
195
|
+
|
|
196
|
+
def generate_schema(self, user: UserABC, language_context: LanguageContext) -> CategorySchemaData:
|
|
197
|
+
result = super().generate_schema(user, language_context)
|
|
198
|
+
|
|
199
|
+
for category in self.subcategories:
|
|
200
|
+
|
|
201
|
+
if not category.slug:
|
|
202
|
+
msg = f'Category {type(category).__name__}.slug is empty'
|
|
203
|
+
raise AttributeError(msg)
|
|
204
|
+
|
|
205
|
+
if category.slug in result.categories:
|
|
206
|
+
exists = result.categories[category.slug]
|
|
207
|
+
msg = f'Category {type(category).__name__}.slug "{self.slug}" already registered by "{exists.title}"'
|
|
208
|
+
raise KeyError(msg)
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
result.categories[category.slug] = category.generate_schema(user, language_context)
|
|
212
|
+
except Exception as e:
|
|
213
|
+
msg = f'Category "{category.slug}" {type(category)} generate_schema error: {e}'
|
|
214
|
+
raise Exception(msg) from e
|
|
215
|
+
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
def get_category(self, category_slug: str):
|
|
219
|
+
for category in self.subcategories:
|
|
220
|
+
if category.slug == category_slug:
|
|
221
|
+
return category
|
|
222
|
+
|
|
223
|
+
return None
|
|
@@ -2,8 +2,7 @@ from typing import Any, Dict, List
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
5
|
-
from brilliance_admin.schema import
|
|
6
|
-
from brilliance_admin.schema.category import GraphInfoSchemaData
|
|
5
|
+
from brilliance_admin.schema.category import BaseCategory, GraphInfoSchemaData
|
|
7
6
|
from brilliance_admin.schema.table.fields_schema import FieldsSchema
|
|
8
7
|
from brilliance_admin.translations import LanguageContext
|
|
9
8
|
from brilliance_admin.utils import SupportsStr
|
|
@@ -26,7 +25,7 @@ class GraphsDataResult(BaseModel):
|
|
|
26
25
|
charts: List[ChartData]
|
|
27
26
|
|
|
28
27
|
|
|
29
|
-
class CategoryGraphs(
|
|
28
|
+
class CategoryGraphs(BaseCategory):
|
|
30
29
|
_type_slug: str = 'graphs'
|
|
31
30
|
|
|
32
31
|
search_enabled: bool = False
|
|
@@ -8,7 +8,7 @@ from pydantic import Field
|
|
|
8
8
|
|
|
9
9
|
from brilliance_admin.auth import UserABC
|
|
10
10
|
from brilliance_admin.exceptions import AdminAPIException, APIError
|
|
11
|
-
from brilliance_admin.schema import
|
|
11
|
+
from brilliance_admin.schema.category import BaseCategory
|
|
12
12
|
from brilliance_admin.schema.category import TableInfoSchemaData
|
|
13
13
|
from brilliance_admin.schema.table.admin_action import ActionData, ActionResult
|
|
14
14
|
from brilliance_admin.schema.table.fields_schema import FieldsSchema
|
|
@@ -17,7 +17,7 @@ from brilliance_admin.translations import LanguageContext
|
|
|
17
17
|
from brilliance_admin.utils import DeserializeAction, SupportsStr
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
class CategoryTable(
|
|
20
|
+
class CategoryTable(BaseCategory):
|
|
21
21
|
_type_slug: str = 'table'
|
|
22
22
|
|
|
23
23
|
search_enabled: bool = False
|
|
@@ -32,6 +32,8 @@ class CategoryTable(Category):
|
|
|
32
32
|
pk_name: str | None = None
|
|
33
33
|
|
|
34
34
|
def __init__(self, *args, table_schema=None, table_filters=None, **kwargs):
|
|
35
|
+
super().__init__(*args, **kwargs)
|
|
36
|
+
|
|
35
37
|
if table_schema:
|
|
36
38
|
self.table_schema = table_schema
|
|
37
39
|
|
|
@@ -8,6 +8,7 @@ from pydantic.dataclasses import dataclass
|
|
|
8
8
|
from brilliance_admin.exceptions import FieldError
|
|
9
9
|
from brilliance_admin.schema.category import FieldSchemaData
|
|
10
10
|
from brilliance_admin.translations import LanguageContext
|
|
11
|
+
from brilliance_admin.translations import TranslateText as _
|
|
11
12
|
from brilliance_admin.utils import DeserializeAction, SupportsStr, humanize_field_name
|
|
12
13
|
|
|
13
14
|
|
|
@@ -80,7 +81,7 @@ class IntegerField(TableField):
|
|
|
80
81
|
async def deserialize(self, value, action: DeserializeAction, extra: dict, *args, **kwargs) -> Any:
|
|
81
82
|
value = await super().deserialize(value, action, extra, *args, **kwargs)
|
|
82
83
|
if value and not isinstance(value, int):
|
|
83
|
-
raise FieldError(
|
|
84
|
+
raise FieldError(_('errors.bad_type_error') % {'type': type(value), 'expected': 'init'})
|
|
84
85
|
|
|
85
86
|
return value
|
|
86
87
|
|
|
@@ -116,7 +117,7 @@ class StringField(TableField):
|
|
|
116
117
|
async def deserialize(self, value, action: DeserializeAction, extra: dict, *args, **kwargs) -> Any:
|
|
117
118
|
value = await super().deserialize(value, action, extra, *args, **kwargs)
|
|
118
119
|
if value and not isinstance(value, str):
|
|
119
|
-
raise FieldError(
|
|
120
|
+
raise FieldError(_('errors.bad_type_error') % {'type': type(value), 'expected': 'string'})
|
|
120
121
|
|
|
121
122
|
return value
|
|
122
123
|
|
|
@@ -151,7 +152,7 @@ class DateTimeField(TableField):
|
|
|
151
152
|
return
|
|
152
153
|
|
|
153
154
|
if value and not isinstance(value, (str, dict)):
|
|
154
|
-
raise FieldError(
|
|
155
|
+
raise FieldError(_('errors.bad_type_error') % {'type': type(value), 'expected': 'datetime'})
|
|
155
156
|
|
|
156
157
|
if isinstance(value, str):
|
|
157
158
|
return datetime.datetime.strptime(value, self.format)
|
|
@@ -166,13 +167,24 @@ class DateTimeField(TableField):
|
|
|
166
167
|
'to': datetime.datetime.strptime(value.get('to'), self.format),
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
raise
|
|
170
|
+
raise FieldError(_('errors.bad_type_error') % {'type': type(value), 'expected': 'datetime'})
|
|
170
171
|
|
|
171
172
|
|
|
172
173
|
@dataclass
|
|
173
174
|
class JSONField(TableField):
|
|
174
175
|
_type = 'json'
|
|
175
176
|
|
|
177
|
+
async def deserialize(self, value, action: DeserializeAction, extra: dict, *args, **kwargs) -> Any:
|
|
178
|
+
value = await super().deserialize(value, action, extra, *args, **kwargs)
|
|
179
|
+
|
|
180
|
+
if value is None:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
if not isinstance(value, (dict, list)):
|
|
184
|
+
raise FieldError(_('errors.bad_type_error') % {'type': type(value), 'expected': 'JSON'})
|
|
185
|
+
|
|
186
|
+
return value
|
|
187
|
+
|
|
176
188
|
|
|
177
189
|
@dataclass
|
|
178
190
|
class ArrayField(TableField):
|
|
@@ -190,8 +202,11 @@ class ArrayField(TableField):
|
|
|
190
202
|
async def deserialize(self, value, action: DeserializeAction, extra: dict, *args, **kwargs) -> Any:
|
|
191
203
|
value = await super().deserialize(value, action, extra, *args, **kwargs)
|
|
192
204
|
|
|
193
|
-
if value
|
|
194
|
-
|
|
205
|
+
if value is None:
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
if not isinstance(value, list):
|
|
209
|
+
raise FieldError(_('errors.bad_type_error') % {'type': type(value), 'expected': 'Array'})
|
|
195
210
|
|
|
196
211
|
return value
|
|
197
212
|
|