upd project structure
This commit is contained in:
43
src/qbot/model/__init__.py
Normal file
43
src/qbot/model/__init__.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from functools import wraps
|
||||
from sqlalchemy.inspection import inspect
|
||||
from sqlalchemy.orm.state import InstanceState
|
||||
from typing import cast
|
||||
|
||||
from .bot_enum import BotEnum, EnumMember
|
||||
from ..db import async_session
|
||||
|
||||
|
||||
class EntityPermission(BotEnum):
|
||||
LIST = EnumMember("list")
|
||||
READ = EnumMember("read")
|
||||
CREATE = EnumMember("create")
|
||||
UPDATE = EnumMember("update")
|
||||
DELETE = EnumMember("delete")
|
||||
LIST_ALL = EnumMember("list_all")
|
||||
READ_ALL = EnumMember("read_all")
|
||||
CREATE_ALL = EnumMember("create_all")
|
||||
UPDATE_ALL = EnumMember("update_all")
|
||||
DELETE_ALL = EnumMember("delete_all")
|
||||
|
||||
|
||||
def session_dep(func):
|
||||
@wraps(func)
|
||||
async def wrapper(cls, *args, **kwargs):
|
||||
if "session" in kwargs and kwargs["session"]:
|
||||
return await func(cls, *args, **kwargs)
|
||||
|
||||
_session = None
|
||||
|
||||
state = cast(InstanceState, inspect(cls))
|
||||
if hasattr(state, "async_session"):
|
||||
_session = state.async_session
|
||||
|
||||
if not _session:
|
||||
async with async_session() as session:
|
||||
kwargs["session"] = session
|
||||
return await func(cls, *args, **kwargs)
|
||||
else:
|
||||
kwargs["session"] = _session
|
||||
return await func(cls, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
7
src/qbot/model/_singleton.py
Normal file
7
src/qbot/model/_singleton.py
Normal file
@@ -0,0 +1,7 @@
|
||||
class Singleton(type):
|
||||
_instances = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
||||
return cls._instances[cls]
|
||||
432
src/qbot/model/bot_entity.py
Normal file
432
src/qbot/model/bot_entity.py
Normal file
@@ -0,0 +1,432 @@
|
||||
from types import NoneType, UnionType
|
||||
from typing import (
|
||||
Any,
|
||||
ClassVar,
|
||||
ForwardRef,
|
||||
Optional,
|
||||
Self,
|
||||
Union,
|
||||
get_args,
|
||||
get_origin,
|
||||
TYPE_CHECKING,
|
||||
dataclass_transform,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import SQLModel, BigInteger, Field, select, func, column, col
|
||||
from sqlmodel.main import FieldInfo
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from sqlmodel.sql.expression import SelectOfScalar
|
||||
from sqlmodel.main import SQLModelMetaclass, RelationshipInfo
|
||||
|
||||
|
||||
from .descriptors import EntityDescriptor, EntityField, FieldDescriptor, Filter
|
||||
from .entity_metadata import EntityMetadata
|
||||
from . import session_dep
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .user import UserBase
|
||||
|
||||
|
||||
@dataclass_transform(
|
||||
kw_only_default=True,
|
||||
field_specifiers=(Field, FieldInfo, EntityField, FieldDescriptor),
|
||||
)
|
||||
class BotEntityMetaclass(SQLModelMetaclass):
|
||||
_future_references = {}
|
||||
|
||||
def __new__(mcs, name, bases, namespace, **kwargs):
|
||||
bot_fields_descriptors = {}
|
||||
|
||||
if bases:
|
||||
bot_entity_descriptor = bases[0].__dict__.get("bot_entity_descriptor")
|
||||
bot_fields_descriptors = (
|
||||
{
|
||||
key: FieldDescriptor(**value.__dict__.copy())
|
||||
for key, value in bot_entity_descriptor.fields_descriptors.items()
|
||||
}
|
||||
if bot_entity_descriptor
|
||||
else {}
|
||||
)
|
||||
|
||||
if "__annotations__" in namespace:
|
||||
for annotation in namespace["__annotations__"]:
|
||||
if annotation in ["bot_entity_descriptor", "entity_metadata"]:
|
||||
continue
|
||||
|
||||
attribute_value = namespace.get(annotation)
|
||||
|
||||
if isinstance(attribute_value, RelationshipInfo):
|
||||
continue
|
||||
|
||||
descriptor_kwargs = {}
|
||||
descriptor_name = annotation
|
||||
|
||||
if attribute_value:
|
||||
if isinstance(attribute_value, EntityField):
|
||||
descriptor_kwargs = attribute_value.__dict__.copy()
|
||||
sm_descriptor = descriptor_kwargs.pop("sm_descriptor", None)
|
||||
|
||||
if sm_descriptor:
|
||||
namespace[annotation] = sm_descriptor
|
||||
else:
|
||||
namespace.pop(annotation)
|
||||
|
||||
descriptor_name = descriptor_kwargs.pop("name") or annotation
|
||||
|
||||
type_ = namespace["__annotations__"][annotation]
|
||||
|
||||
type_origin = get_origin(type_)
|
||||
|
||||
field_descriptor = FieldDescriptor(
|
||||
name=descriptor_name,
|
||||
field_name=annotation,
|
||||
type_=type_,
|
||||
type_base=type_,
|
||||
**descriptor_kwargs,
|
||||
)
|
||||
|
||||
is_list = False
|
||||
is_optional = False
|
||||
if type_origin is list:
|
||||
field_descriptor.is_list = is_list = True
|
||||
field_descriptor.type_base = type_ = get_args(type_)[0]
|
||||
|
||||
if type_origin is Union:
|
||||
args = get_args(type_)
|
||||
if isinstance(args[0], ForwardRef):
|
||||
field_descriptor.is_optional = is_optional = True
|
||||
field_descriptor.type_base = type_ = args[0].__forward_arg__
|
||||
elif args[1] is NoneType:
|
||||
field_descriptor.is_optional = is_optional = True
|
||||
field_descriptor.type_base = type_ = args[0]
|
||||
|
||||
if type_origin is UnionType and get_args(type_)[1] is NoneType:
|
||||
field_descriptor.is_optional = is_optional = True
|
||||
field_descriptor.type_base = type_ = get_args(type_)[0]
|
||||
|
||||
if isinstance(type_, str):
|
||||
type_not_found = True
|
||||
for (
|
||||
entity_descriptor
|
||||
) in EntityMetadata().entity_descriptors.values():
|
||||
if type_ == entity_descriptor.class_name:
|
||||
field_descriptor.type_base = entity_descriptor.type_
|
||||
field_descriptor.type_ = (
|
||||
list[entity_descriptor.type_]
|
||||
if is_list
|
||||
else (
|
||||
Optional[entity_descriptor.type_]
|
||||
if type_origin == Union and is_optional
|
||||
else (
|
||||
entity_descriptor.type_ | None
|
||||
if (type_origin == UnionType and is_optional)
|
||||
else entity_descriptor.type_
|
||||
)
|
||||
)
|
||||
)
|
||||
type_not_found = False
|
||||
break
|
||||
if type_not_found:
|
||||
if type_ in mcs._future_references:
|
||||
mcs._future_references[type_].append(field_descriptor)
|
||||
else:
|
||||
mcs._future_references[type_] = [field_descriptor]
|
||||
|
||||
bot_fields_descriptors[descriptor_name] = field_descriptor
|
||||
|
||||
descriptor_name = name
|
||||
|
||||
if "bot_entity_descriptor" in namespace:
|
||||
entity_descriptor = namespace.pop("bot_entity_descriptor")
|
||||
descriptor_kwargs: dict = entity_descriptor.__dict__.copy()
|
||||
descriptor_name = descriptor_kwargs.pop("name", None)
|
||||
descriptor_name = descriptor_name or name.lower()
|
||||
namespace["bot_entity_descriptor"] = EntityDescriptor(
|
||||
name=descriptor_name,
|
||||
class_name=name,
|
||||
type_=name,
|
||||
fields_descriptors=bot_fields_descriptors,
|
||||
**descriptor_kwargs,
|
||||
)
|
||||
else:
|
||||
descriptor_name = name.lower()
|
||||
namespace["bot_entity_descriptor"] = EntityDescriptor(
|
||||
name=descriptor_name,
|
||||
class_name=name,
|
||||
type_=name,
|
||||
fields_descriptors=bot_fields_descriptors,
|
||||
)
|
||||
|
||||
descriptor_fields_sequence = [
|
||||
key
|
||||
for key, val in bot_fields_descriptors.items()
|
||||
if not (val.is_optional or val.name == "id")
|
||||
]
|
||||
|
||||
entity_descriptor: EntityDescriptor = namespace["bot_entity_descriptor"]
|
||||
|
||||
if entity_descriptor.default_form.edit_field_sequence is None:
|
||||
entity_descriptor.default_form.edit_field_sequence = (
|
||||
descriptor_fields_sequence
|
||||
)
|
||||
|
||||
for form in entity_descriptor.forms.values():
|
||||
if form.edit_field_sequence is None:
|
||||
form.edit_field_sequence = descriptor_fields_sequence
|
||||
|
||||
for field_descriptor in bot_fields_descriptors.values():
|
||||
field_descriptor.entity_descriptor = namespace["bot_entity_descriptor"]
|
||||
|
||||
if "table" not in kwargs:
|
||||
kwargs["table"] = True
|
||||
|
||||
if kwargs["table"]:
|
||||
entity_metadata = EntityMetadata()
|
||||
entity_metadata.entity_descriptors[descriptor_name] = namespace[
|
||||
"bot_entity_descriptor"
|
||||
]
|
||||
|
||||
if "__annotations__" in namespace:
|
||||
namespace["__annotations__"]["entity_metadata"] = ClassVar[
|
||||
EntityMetadata
|
||||
]
|
||||
else:
|
||||
namespace["__annotations__"] = {
|
||||
"entity_metadata": ClassVar[EntityMetadata]
|
||||
}
|
||||
|
||||
namespace["entity_metadata"] = entity_metadata
|
||||
|
||||
type_ = super().__new__(mcs, name, bases, namespace, **kwargs)
|
||||
|
||||
if name in mcs._future_references:
|
||||
for field_descriptor in mcs._future_references[name]:
|
||||
type_origin = get_origin(field_descriptor.type_)
|
||||
field_descriptor.type_base = type_
|
||||
|
||||
field_descriptor.type_ = (
|
||||
list[type_]
|
||||
if type_origin is list
|
||||
else (
|
||||
Optional[type_]
|
||||
if type_origin == Union
|
||||
and isinstance(get_args(field_descriptor.type_)[0], ForwardRef)
|
||||
else type_ | None
|
||||
if type_origin == UnionType
|
||||
else type_
|
||||
)
|
||||
)
|
||||
|
||||
setattr(namespace["bot_entity_descriptor"], "type_", type_)
|
||||
|
||||
return type_
|
||||
|
||||
|
||||
class BotEntity[CreateSchemaType: BaseModel, UpdateSchemaType: BaseModel](
|
||||
SQLModel, metaclass=BotEntityMetaclass, table=False
|
||||
):
|
||||
bot_entity_descriptor: ClassVar[EntityDescriptor]
|
||||
entity_metadata: ClassVar[EntityMetadata]
|
||||
|
||||
id: int = EntityField(
|
||||
sm_descriptor=Field(primary_key=True, sa_type=BigInteger), is_visible=False
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@session_dep
|
||||
async def get(cls, *, session: AsyncSession | None = None, id: int):
|
||||
return await session.get(cls, id, populate_existing=True)
|
||||
|
||||
@classmethod
|
||||
def _static_filter_condition(
|
||||
cls, select_statement: SelectOfScalar[Self], static_filter: list[Filter]
|
||||
):
|
||||
for sfilt in static_filter:
|
||||
column = getattr(cls, sfilt.field_name)
|
||||
if sfilt.operator == "==":
|
||||
condition = column.__eq__(sfilt.value)
|
||||
elif sfilt.operator == "!=":
|
||||
condition = column.__ne__(sfilt.value)
|
||||
elif sfilt.operator == "<":
|
||||
condition = column.__lt__(sfilt.value)
|
||||
elif sfilt.operator == "<=":
|
||||
condition = column.__le__(sfilt.value)
|
||||
elif sfilt.operator == ">":
|
||||
condition = column.__gt__(sfilt.value)
|
||||
elif sfilt.operator == ">=":
|
||||
condition = column.__ge__(sfilt.value)
|
||||
elif sfilt.operator == "ilike":
|
||||
condition = col(column).ilike(f"%{sfilt.value}%")
|
||||
elif sfilt.operator == "like":
|
||||
condition = col(column).like(f"%{sfilt.value}%")
|
||||
elif sfilt.operator == "in":
|
||||
condition = col(column).in_(sfilt.value)
|
||||
elif sfilt.operator == "not in":
|
||||
condition = col(column).notin_(sfilt.value)
|
||||
elif sfilt.operator == "is none":
|
||||
condition = col(column).is_(None)
|
||||
elif sfilt.operator == "is not none":
|
||||
condition = col(column).isnot(None)
|
||||
elif sfilt.operator == "contains":
|
||||
condition = sfilt.value == col(column).any_()
|
||||
else:
|
||||
condition = None
|
||||
if condition is not None:
|
||||
select_statement = select_statement.where(condition)
|
||||
return select_statement
|
||||
|
||||
@classmethod
|
||||
def _filter_condition(
|
||||
cls,
|
||||
select_statement: SelectOfScalar[Self],
|
||||
filter: str,
|
||||
filter_fields: list[str],
|
||||
):
|
||||
condition = None
|
||||
for field in filter_fields:
|
||||
if condition is not None:
|
||||
condition = condition | (column(field).ilike(f"%{filter}%"))
|
||||
else:
|
||||
condition = column(field).ilike(f"%{filter}%")
|
||||
return select_statement.where(condition)
|
||||
|
||||
@classmethod
|
||||
@session_dep
|
||||
async def get_count(
|
||||
cls,
|
||||
*,
|
||||
session: AsyncSession | None = None,
|
||||
static_filter: list[Filter] | Any = None,
|
||||
filter: str = None,
|
||||
filter_fields: list[str] = None,
|
||||
ext_filter: Any = None,
|
||||
user: "UserBase" = None,
|
||||
) -> int:
|
||||
select_statement = select(func.count()).select_from(cls)
|
||||
if static_filter:
|
||||
if isinstance(static_filter, list):
|
||||
select_statement = cls._static_filter_condition(
|
||||
select_statement, static_filter
|
||||
)
|
||||
else:
|
||||
select_statement = select_statement.where(static_filter)
|
||||
if filter and filter_fields:
|
||||
select_statement = cls._filter_condition(
|
||||
select_statement, filter, filter_fields
|
||||
)
|
||||
if ext_filter:
|
||||
select_statement = select_statement.where(ext_filter)
|
||||
if user:
|
||||
select_statement = cls._ownership_condition(select_statement, user)
|
||||
return await session.scalar(select_statement)
|
||||
|
||||
@classmethod
|
||||
@session_dep
|
||||
async def get_multi(
|
||||
cls,
|
||||
*,
|
||||
session: AsyncSession | None = None,
|
||||
order_by=None,
|
||||
static_filter: list[Filter] | Any = None,
|
||||
filter: str = None,
|
||||
filter_fields: list[str] = None,
|
||||
ext_filter: Any = None,
|
||||
user: "UserBase" = None,
|
||||
skip: int = 0,
|
||||
limit: int = None,
|
||||
):
|
||||
select_statement = select(cls).offset(skip)
|
||||
if limit:
|
||||
select_statement = select_statement.limit(limit)
|
||||
if static_filter is not None:
|
||||
if isinstance(static_filter, list):
|
||||
select_statement = cls._static_filter_condition(
|
||||
select_statement, static_filter
|
||||
)
|
||||
else:
|
||||
select_statement = select_statement.where(static_filter)
|
||||
if filter and filter_fields:
|
||||
select_statement = cls._filter_condition(
|
||||
select_statement, filter, filter_fields
|
||||
)
|
||||
if ext_filter is not None:
|
||||
select_statement = select_statement.where(ext_filter)
|
||||
if user:
|
||||
select_statement = cls._ownership_condition(select_statement, user)
|
||||
if order_by is not None:
|
||||
select_statement = select_statement.order_by(order_by)
|
||||
return (await session.exec(select_statement)).all()
|
||||
|
||||
@classmethod
|
||||
def _ownership_condition(
|
||||
cls, select_statement: SelectOfScalar[Self], user: "UserBase"
|
||||
):
|
||||
if cls.bot_entity_descriptor.ownership_fields:
|
||||
condition = None
|
||||
for role in user.roles:
|
||||
if role in cls.bot_entity_descriptor.ownership_fields:
|
||||
owner_col = column(cls.bot_entity_descriptor.ownership_fields[role])
|
||||
if condition is not None:
|
||||
condition = condition | (owner_col == user.id)
|
||||
else:
|
||||
condition = owner_col == user.id
|
||||
else:
|
||||
condition = None
|
||||
break
|
||||
if condition is not None:
|
||||
return select_statement.where(condition)
|
||||
return select_statement
|
||||
|
||||
@classmethod
|
||||
@session_dep
|
||||
async def create(
|
||||
cls,
|
||||
*,
|
||||
session: AsyncSession | None = None,
|
||||
obj_in: CreateSchemaType,
|
||||
commit: bool = False,
|
||||
):
|
||||
if isinstance(obj_in, cls):
|
||||
obj = obj_in
|
||||
else:
|
||||
obj = cls(**obj_in.model_dump())
|
||||
session.add(obj)
|
||||
if commit:
|
||||
await session.commit()
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
@session_dep
|
||||
async def update(
|
||||
cls,
|
||||
*,
|
||||
session: AsyncSession | None = None,
|
||||
id: int,
|
||||
obj_in: UpdateSchemaType,
|
||||
commit: bool = False,
|
||||
):
|
||||
obj = await session.get(cls, id)
|
||||
if obj:
|
||||
obj_data = obj.model_dump()
|
||||
update_data = obj_in.model_dump(exclude_unset=True)
|
||||
for field in obj_data:
|
||||
if field in update_data:
|
||||
setattr(obj, field, update_data[field])
|
||||
session.add(obj)
|
||||
if commit:
|
||||
await session.commit()
|
||||
return obj
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
@session_dep
|
||||
async def remove(
|
||||
cls, *, session: AsyncSession | None = None, id: int, commit: bool = False
|
||||
):
|
||||
obj = await session.get(cls, id)
|
||||
if obj:
|
||||
await session.delete(obj)
|
||||
if commit:
|
||||
await session.commit()
|
||||
return obj
|
||||
return None
|
||||
181
src/qbot/model/bot_enum.py
Normal file
181
src/qbot/model/bot_enum.py
Normal file
@@ -0,0 +1,181 @@
|
||||
from aiogram.utils.i18n import I18n
|
||||
from pydantic_core.core_schema import str_schema
|
||||
from sqlalchemy.types import TypeDecorator
|
||||
from sqlmodel import AutoString
|
||||
from typing import Any, Self, overload
|
||||
|
||||
|
||||
class BotEnumMetaclass(type):
|
||||
def __new__(cls, name: str, bases: tuple[type], namespace: dict[str, Any]):
|
||||
all_members = {}
|
||||
if (
|
||||
bases
|
||||
and bases[0].__name__ != "BotEnum"
|
||||
and "all_members" in bases[0].__dict__
|
||||
):
|
||||
all_members = bases[0].__dict__["all_members"]
|
||||
|
||||
annotations = {}
|
||||
|
||||
for key, value in namespace.items():
|
||||
if key.isupper() and not key.startswith("__") and not key.endswith("__"):
|
||||
if not isinstance(value, EnumMember):
|
||||
value = EnumMember(value, None)
|
||||
|
||||
if key in all_members.keys() and all_members[key].value != value.value:
|
||||
raise ValueError(
|
||||
f"Enum member {key} already exists with different value. Use same value to extend it."
|
||||
)
|
||||
|
||||
if (
|
||||
value.value in [member.value for member in all_members.values()]
|
||||
and key not in all_members.keys()
|
||||
):
|
||||
raise ValueError(f"Duplicate enum value {value[0]}")
|
||||
|
||||
member = EnumMember(
|
||||
value=value.value,
|
||||
loc_obj=value.loc_obj,
|
||||
parent=None,
|
||||
name=key,
|
||||
casting=False,
|
||||
)
|
||||
|
||||
namespace[key] = member
|
||||
all_members[key] = member
|
||||
annotations[key] = type(member)
|
||||
|
||||
namespace["__annotations__"] = annotations
|
||||
namespace["all_members"] = all_members
|
||||
|
||||
type_ = super().__new__(cls, name, bases, namespace)
|
||||
|
||||
for key, value in all_members.items():
|
||||
if not value._parent:
|
||||
value._parent = type_
|
||||
|
||||
return type_
|
||||
|
||||
|
||||
class EnumMember(object):
|
||||
@overload
|
||||
def __init__(self, value: str) -> "EnumMember": ...
|
||||
|
||||
@overload
|
||||
def __init__(self, value: "EnumMember") -> "EnumMember": ...
|
||||
|
||||
@overload
|
||||
def __init__(self, value: str, loc_obj: dict[str, str]) -> "EnumMember": ...
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
value: str = None,
|
||||
loc_obj: dict[str, str] = None,
|
||||
parent: type = None,
|
||||
name: str = None,
|
||||
casting: bool = True,
|
||||
) -> "EnumMember":
|
||||
if not casting:
|
||||
self._parent = parent
|
||||
self._name = name
|
||||
self.value = value
|
||||
self.loc_obj = loc_obj
|
||||
|
||||
@overload
|
||||
def __new__(cls: Self, *args, **kwargs) -> "EnumMember": ...
|
||||
|
||||
def __new__(cls, *args, casting: bool = True, **kwargs) -> "EnumMember":
|
||||
if (cls.__name__ == "EnumMember") or not casting:
|
||||
obj = super().__new__(cls)
|
||||
kwargs["casting"] = False
|
||||
obj.__init__(*args, **kwargs)
|
||||
return obj
|
||||
if args.__len__() == 0:
|
||||
return list(cls.all_members.values())[0]
|
||||
if args.__len__() == 1 and isinstance(args[0], str):
|
||||
return {member.value: member for key, member in cls.all_members.items()}[
|
||||
args[0]
|
||||
]
|
||||
elif args.__len__() == 1:
|
||||
return {member.value: member for key, member in cls.all_members.items()}[
|
||||
args[0].value
|
||||
]
|
||||
else:
|
||||
return args[0]
|
||||
|
||||
def __get_pydantic_core_schema__(cls, *args, **kwargs):
|
||||
return str_schema()
|
||||
|
||||
def __get__(self, instance, owner) -> Self:
|
||||
return {
|
||||
member.value: member for key, member in self._parent.all_members.items()
|
||||
}[self.value]
|
||||
|
||||
def __set__(self, instance, value):
|
||||
instance.__dict__[self] = value
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self._parent.__name__ if self._parent else 'EnumMember'}.{self._name}: '{self.value}'>"
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
def __eq__(self, other: Self | str | Any | None) -> bool:
|
||||
if other is None:
|
||||
return False
|
||||
if isinstance(other, str):
|
||||
return self.value == other
|
||||
if isinstance(other, EnumMember):
|
||||
return self.value == other.value and (
|
||||
issubclass(self._parent, other._parent)
|
||||
or issubclass(other._parent, self._parent)
|
||||
)
|
||||
return other.__eq__(self.value)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.value)
|
||||
|
||||
def localized(self, lang: str = None) -> str:
|
||||
if self.loc_obj:
|
||||
if not lang:
|
||||
i18n = I18n.get_current()
|
||||
if i18n:
|
||||
lang = i18n.current_locale
|
||||
else:
|
||||
lang = list(self.loc_obj.keys())[0]
|
||||
|
||||
if lang in self.loc_obj.keys():
|
||||
return self.loc_obj[lang]
|
||||
else:
|
||||
return self.loc_obj[list(self.loc_obj.keys())[0]]
|
||||
|
||||
return self.value
|
||||
|
||||
|
||||
class BotEnum(EnumMember, metaclass=BotEnumMetaclass):
|
||||
all_members: dict[str, EnumMember]
|
||||
|
||||
|
||||
class EnumType(TypeDecorator):
|
||||
impl = AutoString
|
||||
cache_ok = True
|
||||
|
||||
def __init__(self, enum_type: BotEnum):
|
||||
self._enum_type = enum_type
|
||||
super().__init__()
|
||||
|
||||
def _process_param(self, value):
|
||||
if value and isinstance(value, EnumMember):
|
||||
return value.value
|
||||
return str(value)
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
return self._process_param(value)
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
if value:
|
||||
return self._enum_type(value)
|
||||
return None
|
||||
|
||||
def process_literal_param(self, value, dialect):
|
||||
return self._process_param(value)
|
||||
4
src/qbot/model/default_user.py
Normal file
4
src/qbot/model/default_user.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .user import UserBase
|
||||
|
||||
|
||||
class DefaultUser(UserBase): ...
|
||||
212
src/qbot/model/descriptors.py
Normal file
212
src/qbot/model/descriptors.py
Normal file
@@ -0,0 +1,212 @@
|
||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.utils.i18n import I18n
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from typing import Any, Callable, TYPE_CHECKING, Literal, Union
|
||||
from babel.support import LazyProxy
|
||||
from dataclasses import dataclass, field
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from .role import RoleBase
|
||||
from . import EntityPermission
|
||||
from ..bot.handlers.context import ContextData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .bot_entity import BotEntity
|
||||
from ..main import QBotApp
|
||||
from .user import UserBase
|
||||
|
||||
EntityCaptionCallable = Callable[["EntityDescriptor"], str]
|
||||
EntityItemCaptionCallable = Callable[["EntityDescriptor", Any], str]
|
||||
EntityFieldCaptionCallable = Callable[["FieldDescriptor", Any, Any], str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class FieldEditButton:
|
||||
field_name: str
|
||||
caption: str | LazyProxy | EntityFieldCaptionCallable | None = None
|
||||
visibility: Callable[[Any], bool] | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommandButton:
|
||||
command: ContextData | Callable[[ContextData, Any], ContextData] | str
|
||||
caption: str | LazyProxy | EntityItemCaptionCallable
|
||||
visibility: Callable[[Any], bool] | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class InlineButton:
|
||||
inline_button: InlineKeyboardButton | Callable[[Any], InlineKeyboardButton]
|
||||
visibility: Callable[[Any], bool] | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Filter:
|
||||
field_name: str
|
||||
operator: Literal[
|
||||
"==",
|
||||
"!=",
|
||||
">",
|
||||
"<",
|
||||
">=",
|
||||
"<=",
|
||||
"in",
|
||||
"not in",
|
||||
"like",
|
||||
"ilike",
|
||||
"is none",
|
||||
"is not none",
|
||||
"contains",
|
||||
]
|
||||
value_type: Literal["const", "param"] = "const"
|
||||
value: Any | None = None
|
||||
param_index: int | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntityList:
|
||||
caption: str | LazyProxy | EntityCaptionCallable | None = None
|
||||
item_repr: EntityItemCaptionCallable | None = None
|
||||
show_add_new_button: bool = True
|
||||
item_form: str | None = None
|
||||
pagination: bool = True
|
||||
static_filters: list[Filter] = None
|
||||
filtering: bool = False
|
||||
filtering_fields: list[str] = None
|
||||
order_by: str | Any | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntityForm:
|
||||
item_repr: EntityItemCaptionCallable | None = None
|
||||
edit_field_sequence: list[str] = None
|
||||
form_buttons: list[list[FieldEditButton | CommandButton | InlineButton]] = None
|
||||
show_edit_button: bool = True
|
||||
show_delete_button: bool = True
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class _BaseFieldDescriptor:
|
||||
icon: str = None
|
||||
caption: str | LazyProxy | EntityFieldCaptionCallable | None = None
|
||||
description: str | LazyProxy | EntityFieldCaptionCallable | None = None
|
||||
edit_prompt: str | LazyProxy | EntityFieldCaptionCallable | None = None
|
||||
caption_value: EntityFieldCaptionCallable | None = None
|
||||
is_visible: bool = True
|
||||
localizable: bool = False
|
||||
bool_false_value: str | LazyProxy = "no"
|
||||
bool_true_value: str | LazyProxy = "yes"
|
||||
ep_form: str | None = None
|
||||
ep_parent_field: str | None = None
|
||||
ep_child_field: str | None = None
|
||||
dt_type: Literal["date", "datetime"] = "date"
|
||||
default: Any = None
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class EntityField(_BaseFieldDescriptor):
|
||||
name: str | None = None
|
||||
sm_descriptor: Any = None
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Setting(_BaseFieldDescriptor):
|
||||
name: str | None = None
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class FormField(_BaseFieldDescriptor):
|
||||
name: str | None = None
|
||||
type_: type
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class FieldDescriptor(_BaseFieldDescriptor):
|
||||
name: str
|
||||
field_name: str
|
||||
type_: type
|
||||
type_base: type = None
|
||||
is_list: bool = False
|
||||
is_optional: bool = False
|
||||
entity_descriptor: "EntityDescriptor" = None
|
||||
command: "BotCommand" = None
|
||||
|
||||
def __hash__(self):
|
||||
return self.name.__hash__()
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class _BaseEntityDescriptor:
|
||||
icon: str = "📘"
|
||||
full_name: str | LazyProxy | EntityCaptionCallable | None = None
|
||||
full_name_plural: str | LazyProxy | EntityCaptionCallable | None = None
|
||||
description: str | LazyProxy | EntityCaptionCallable | None = None
|
||||
item_repr: EntityItemCaptionCallable | None = None
|
||||
default_list: EntityList = field(default_factory=EntityList)
|
||||
default_form: EntityForm = field(default_factory=EntityForm)
|
||||
lists: dict[str, EntityList] = field(default_factory=dict[str, EntityList])
|
||||
forms: dict[str, EntityForm] = field(default_factory=dict[str, EntityForm])
|
||||
show_in_entities_menu: bool = True
|
||||
ownership_fields: dict[RoleBase, str] = field(default_factory=dict[RoleBase, str])
|
||||
permissions: dict[EntityPermission, list[RoleBase]] = field(
|
||||
default_factory=lambda: {
|
||||
EntityPermission.LIST: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
|
||||
EntityPermission.READ: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
|
||||
EntityPermission.CREATE: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
|
||||
EntityPermission.UPDATE: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
|
||||
EntityPermission.DELETE: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
|
||||
EntityPermission.LIST_ALL: [RoleBase.SUPER_USER],
|
||||
EntityPermission.READ_ALL: [RoleBase.SUPER_USER],
|
||||
EntityPermission.CREATE_ALL: [RoleBase.SUPER_USER],
|
||||
EntityPermission.UPDATE_ALL: [RoleBase.SUPER_USER],
|
||||
EntityPermission.DELETE_ALL: [RoleBase.SUPER_USER],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Entity(_BaseEntityDescriptor):
|
||||
name: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntityDescriptor(_BaseEntityDescriptor):
|
||||
name: str
|
||||
class_name: str
|
||||
type_: type["BotEntity"]
|
||||
fields_descriptors: dict[str, FieldDescriptor]
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class CommandCallbackContext[UT: UserBase]:
|
||||
keyboard_builder: InlineKeyboardBuilder = field(
|
||||
default_factory=InlineKeyboardBuilder
|
||||
)
|
||||
message_text: str | None = None
|
||||
register_navigation: bool = True
|
||||
message: Message | CallbackQuery
|
||||
callback_data: ContextData
|
||||
db_session: AsyncSession
|
||||
user: UT
|
||||
app: "QBotApp"
|
||||
state_data: dict[str, Any]
|
||||
state: FSMContext
|
||||
form_data: dict[str, Any]
|
||||
i18n: I18n
|
||||
kwargs: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class BotCommand:
|
||||
name: str
|
||||
caption: str | dict[str, str] | None = None
|
||||
pre_check: Callable[[Union[Message, CallbackQuery], Any], bool] | None = None
|
||||
show_in_bot_commands: bool = False
|
||||
register_navigation: bool = True
|
||||
clear_navigation: bool = False
|
||||
clear_state: bool = True
|
||||
param_form: dict[str, FieldDescriptor] | None = None
|
||||
show_cancel_in_param_form: bool = True
|
||||
show_back_in_param_form: bool = True
|
||||
handler: Callable[[CommandCallbackContext], None]
|
||||
7
src/qbot/model/entity_metadata.py
Normal file
7
src/qbot/model/entity_metadata.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from .descriptors import EntityDescriptor
|
||||
from ._singleton import Singleton
|
||||
|
||||
|
||||
class EntityMetadata(metaclass=Singleton):
|
||||
def __init__(self):
|
||||
self.entity_descriptors: dict[str, EntityDescriptor] = {}
|
||||
7
src/qbot/model/fsm_storage.py
Normal file
7
src/qbot/model/fsm_storage.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from sqlmodel import SQLModel, Field
|
||||
|
||||
|
||||
class FSMStorage(SQLModel, table=True):
|
||||
__tablename__ = "fsm_storage"
|
||||
key: str = Field(primary_key=True)
|
||||
value: str | None = None
|
||||
5
src/qbot/model/language.py
Normal file
5
src/qbot/model/language.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .bot_enum import BotEnum, EnumMember
|
||||
|
||||
|
||||
class LanguageBase(BotEnum):
|
||||
EN = EnumMember("en", {"en": "🇬🇧 english"})
|
||||
6
src/qbot/model/role.py
Normal file
6
src/qbot/model/role.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .bot_enum import BotEnum, EnumMember
|
||||
|
||||
|
||||
class RoleBase(BotEnum):
|
||||
SUPER_USER = EnumMember("super_user")
|
||||
DEFAULT_USER = EnumMember("default_user")
|
||||
284
src/qbot/model/settings.py
Normal file
284
src/qbot/model/settings.py
Normal file
@@ -0,0 +1,284 @@
|
||||
from types import NoneType, UnionType
|
||||
from aiogram.utils.i18n.context import get_i18n
|
||||
from datetime import datetime
|
||||
from sqlmodel import SQLModel, Field, select
|
||||
from typing import Any, get_args, get_origin
|
||||
|
||||
from ..db import async_session
|
||||
from .role import RoleBase
|
||||
from .descriptors import FieldDescriptor, Setting
|
||||
from ..utils.serialization import deserialize, serialize
|
||||
|
||||
import ujson as json
|
||||
|
||||
|
||||
class DbSettings(SQLModel, table=True):
|
||||
__tablename__ = "settings"
|
||||
name: str = Field(primary_key=True)
|
||||
value: str
|
||||
|
||||
|
||||
class SettingsMetaclass(type):
|
||||
def __new__(cls, class_name, base_classes, attributes):
|
||||
settings_descriptors = {}
|
||||
|
||||
if base_classes:
|
||||
settings_descriptors = base_classes[0].__dict__.get(
|
||||
"_settings_descriptors", {}
|
||||
)
|
||||
|
||||
for annotation in attributes.get("__annotations__", {}):
|
||||
if annotation in ["_settings_descriptors", "_cache", "_cached_settings"]:
|
||||
continue
|
||||
|
||||
attr_value = attributes.get(annotation)
|
||||
name = annotation
|
||||
|
||||
type_ = attributes["__annotations__"][annotation]
|
||||
|
||||
if isinstance(attr_value, Setting):
|
||||
descriptor_kwargs = attr_value.__dict__.copy()
|
||||
name = descriptor_kwargs.pop("name") or annotation
|
||||
attributes[annotation] = FieldDescriptor(
|
||||
name=name,
|
||||
field_name=annotation,
|
||||
type_=type_,
|
||||
type_base=type_,
|
||||
**descriptor_kwargs,
|
||||
)
|
||||
|
||||
else:
|
||||
attributes[annotation] = FieldDescriptor(
|
||||
name=annotation,
|
||||
field_name=annotation,
|
||||
type_=type_,
|
||||
type_base=type_,
|
||||
default=attr_value,
|
||||
)
|
||||
|
||||
type_origin = get_origin(type_)
|
||||
|
||||
if type_origin is list:
|
||||
attributes[annotation].is_list = True
|
||||
attributes[annotation].type_base = type_ = get_args(type_)[0]
|
||||
|
||||
elif type_origin == UnionType and get_args(type_)[1] == NoneType:
|
||||
attributes[annotation].is_optional = True
|
||||
attributes[annotation].type_base = type_ = get_args(type_)[0]
|
||||
|
||||
settings_descriptors[name] = attributes[annotation]
|
||||
|
||||
if (
|
||||
base_classes
|
||||
and base_classes[0].__name__ == "Settings"
|
||||
and hasattr(base_classes[0], annotation)
|
||||
):
|
||||
setattr(base_classes[0], annotation, attributes[annotation])
|
||||
|
||||
attributes["__annotations__"] = {}
|
||||
attributes["_settings_descriptors"] = settings_descriptors
|
||||
|
||||
return super().__new__(cls, class_name, base_classes, attributes)
|
||||
|
||||
|
||||
class Settings(metaclass=SettingsMetaclass):
|
||||
_cache: dict[str, Any] = dict[str, Any]()
|
||||
_settings_descriptors: dict[str, FieldDescriptor] = {}
|
||||
|
||||
PAGE_SIZE: int = Setting(
|
||||
default=10,
|
||||
)
|
||||
SECURITY_PARAMETERS_ROLES: list[RoleBase] = Setting(
|
||||
name="SECPARAMS_ROLES", default=[RoleBase.SUPER_USER], is_visible=False
|
||||
)
|
||||
|
||||
APP_STRINGS_WELCOME_P_NAME: str = Setting(
|
||||
name="AS_WELCOME", default="Welcome, {name}", is_visible=False
|
||||
)
|
||||
APP_STRINGS_GREETING_P_NAME: str = Setting(
|
||||
name="AS_GREETING", default="Hello, {name}", is_visible=False
|
||||
)
|
||||
APP_STRINGS_INTERNAL_ERROR_P_ERROR: str = Setting(
|
||||
name="AS_INTERNAL_ERROR", default="Internal error\n{error}", is_visible=False
|
||||
)
|
||||
APP_STRINGS_USER_BLOCKED_P_NAME: str = Setting(
|
||||
name="AS_USER_BLOCKED", default="User {name} is blocked", is_visible=False
|
||||
)
|
||||
APP_STRINGS_FORBIDDEN: str = Setting(
|
||||
name="AS_FORBIDDEN", default="Forbidden", is_visible=False
|
||||
)
|
||||
APP_STRINGS_NOT_FOUND: str = Setting(
|
||||
name="AS_NOT_FOUND", default="Object not found", is_visible=False
|
||||
)
|
||||
APP_STRINGS_MAIN_NENU: str = Setting(
|
||||
name="AS_MAIN_MENU", default="Main menu", is_visible=False
|
||||
)
|
||||
APP_STRINGS_REFERENCES: str = Setting(
|
||||
name="AS_REFERENCES", default="References", is_visible=False
|
||||
)
|
||||
APP_STRINGS_REFERENCES_BTN: str = Setting(
|
||||
name="AS_REFERENCES_BTN", default="📚 References", is_visible=False
|
||||
)
|
||||
APP_STRINGS_SETTINGS: str = Setting(
|
||||
name="AS_SETTINGS", default="Settings", is_visible=False
|
||||
)
|
||||
APP_STRINGS_SETTINGS_BTN: str = Setting(
|
||||
name="AS_SETTINGS_BTN", default="⚙️ Settings", is_visible=False
|
||||
)
|
||||
APP_STRINGS_PARAMETERS: str = Setting(
|
||||
name="AS_PARAMETERS", default="Parameters", is_visible=False
|
||||
)
|
||||
APP_STRINGS_PARAMETERS_BTN: str = Setting(
|
||||
name="AS_PARAMETERS_BTN", default="🎛️ Parameters", is_visible=False
|
||||
)
|
||||
APP_STRINGS_LANGUAGE: str = Setting(
|
||||
name="AS_LANGUAGE", default="Language", is_visible=False
|
||||
)
|
||||
APP_STRINGS_LANGUAGE_BTN: str = Setting(
|
||||
name="AS_LANGUAGE_BTN", default="🗣️ Language", is_visible=False
|
||||
)
|
||||
APP_STRINGS_BACK_BTN: str = Setting(
|
||||
name="AS_BACK_BTN", default="⬅️ Back", is_visible=False
|
||||
)
|
||||
APP_STRINGS_DELETE_BTN: str = Setting(
|
||||
name="AS_DELETE_BTN", default="🗑️ Delete", is_visible=False
|
||||
)
|
||||
APP_STRINGS_CONFIRM_DELETE_P_NAME: str = Setting(
|
||||
name="AS_CONFIRM_DEL",
|
||||
default='Are you sure you want to delete "{name}"?',
|
||||
is_visible=False,
|
||||
)
|
||||
APP_STRINGS_EDIT_BTN: str = Setting(
|
||||
name="AS_EDIT_BTN", default="✏️ Edit", is_visible=False
|
||||
)
|
||||
APP_STRINGS_ADD_BTN: str = Setting(
|
||||
name="AS_ADD_BTN", default="➕ Add", is_visible=False
|
||||
)
|
||||
APP_STRINGS_YES_BTN: str = Setting(
|
||||
name="AS_YES_BTN", default="✅ Yes", is_visible=False
|
||||
)
|
||||
APP_STRINGS_NO_BTN: str = Setting(
|
||||
name="AS_NO_BTN", default="❌ No", is_visible=False
|
||||
)
|
||||
APP_STRINGS_CANCEL_BTN: str = Setting(
|
||||
name="AS_CANCEL_BTN", default="❌ Cancel", is_visible=False
|
||||
)
|
||||
APP_STRINGS_CLEAR_BTN: str = Setting(
|
||||
name="AS_CLEAR_BTN", default="⌫ Clear", is_visible=False
|
||||
)
|
||||
APP_STRINGS_DONE_BTN: str = Setting(
|
||||
name="AS_DONE_BTN", default="✅ Done", is_visible=False
|
||||
)
|
||||
APP_STRINGS_SKIP_BTN: str = Setting(
|
||||
name="AS_SKIP_BTN", default="⏩️ Skip", is_visible=False
|
||||
)
|
||||
APP_STRINGS_FIELD_EDIT_PROMPT_TEMPLATE_P_NAME_VALUE: str = Setting(
|
||||
name="AS_FIELDEDIT_PROMPT",
|
||||
default='Enter new value for "{name}" (current value: {value})',
|
||||
is_visible=False,
|
||||
)
|
||||
APP_STRINGS_FIELD_CREATE_PROMPT_TEMPLATE_P_NAME: str = Setting(
|
||||
name="AS_FIELDCREATE_PROMPT",
|
||||
default='Enter new value for "{name}"',
|
||||
is_visible=False,
|
||||
)
|
||||
APP_STRINGS_STRING_EDITOR_LOCALE_TEMPLATE_P_NAME: str = Setting(
|
||||
name="AS_STREDIT_LOC_TEMPLATE", default='string for "{name}"', is_visible=False
|
||||
)
|
||||
APP_STRINGS_VIEW_FILTER_EDIT_PROMPT: str = Setting(
|
||||
name="AS_FILTEREDIT_PROMPT", default="Enter filter value", is_visible=False
|
||||
)
|
||||
APP_STRINGS_INVALID_INPUT: str = Setting(
|
||||
name="AS_INVALID_INPUT", default="Invalid input", is_visible=False
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def get[T](cls, param: T, all_locales=False, locale: str = None) -> T:
|
||||
name = param.field_name
|
||||
|
||||
if name not in cls._cache.keys():
|
||||
cls._cache[name] = await cls.load_param(param)
|
||||
|
||||
ret_val = cls._cache[name]
|
||||
|
||||
if param.localizable and not all_locales:
|
||||
if not locale:
|
||||
locale = get_i18n().current_locale
|
||||
try:
|
||||
obj = json.loads(ret_val)
|
||||
except Exception:
|
||||
return ret_val
|
||||
return obj.get(locale, obj[list(obj.keys())[0]])
|
||||
|
||||
return ret_val
|
||||
|
||||
@classmethod
|
||||
async def load_param(cls, param: FieldDescriptor) -> Any:
|
||||
async with async_session() as session:
|
||||
db_setting = (
|
||||
await session.exec(
|
||||
select(DbSettings).where(DbSettings.name == param.field_name)
|
||||
)
|
||||
).first()
|
||||
|
||||
if db_setting:
|
||||
return await deserialize(
|
||||
session=session, type_=param.type_, value=db_setting.value
|
||||
)
|
||||
|
||||
return (
|
||||
param.default
|
||||
if param.default
|
||||
else (
|
||||
[]
|
||||
if (get_origin(param.type_) is list or param.type_ is list)
|
||||
else datetime(2000, 1, 1)
|
||||
if param.type_ == datetime
|
||||
else param.type_()
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def load_params(cls):
|
||||
async with async_session() as session:
|
||||
db_settings = (await session.exec(select(DbSettings))).all()
|
||||
for db_setting in db_settings:
|
||||
if db_setting.name in cls.__dict__:
|
||||
setting = cls.__dict__[db_setting.name] # type: FieldDescriptor
|
||||
cls._cache[db_setting.name] = await deserialize(
|
||||
session=session,
|
||||
type_=setting.type_,
|
||||
value=db_setting.value,
|
||||
default=setting.default,
|
||||
)
|
||||
|
||||
cls._loaded = True
|
||||
|
||||
@classmethod
|
||||
async def set_param(cls, param: str | FieldDescriptor, value) -> None:
|
||||
if isinstance(param, str):
|
||||
param = cls._settings_descriptors[param]
|
||||
ser_value = serialize(value, param)
|
||||
async with async_session() as session:
|
||||
db_setting = (
|
||||
await session.exec(
|
||||
select(DbSettings).where(DbSettings.name == param.field_name)
|
||||
)
|
||||
).first()
|
||||
if db_setting is None:
|
||||
db_setting = DbSettings(name=param.field_name)
|
||||
db_setting.value = str(ser_value)
|
||||
session.add(db_setting)
|
||||
await session.commit()
|
||||
cls._cache[param.field_name] = value
|
||||
|
||||
@classmethod
|
||||
def list_params(cls) -> dict[str, FieldDescriptor]:
|
||||
return cls._settings_descriptors
|
||||
|
||||
@classmethod
|
||||
async def get_params(cls) -> dict[FieldDescriptor, Any]:
|
||||
params = cls.list_params()
|
||||
return {
|
||||
param: await cls.get(param, all_locales=True) for _, param in params.items()
|
||||
}
|
||||
23
src/qbot/model/user.py
Normal file
23
src/qbot/model/user.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from sqlmodel import Field, ARRAY
|
||||
|
||||
from .bot_entity import BotEntity
|
||||
from .bot_enum import EnumType
|
||||
from .language import LanguageBase
|
||||
from .role import RoleBase
|
||||
|
||||
from .settings import DbSettings as DbSettings
|
||||
from .fsm_storage import FSMStorage as FSMStorage
|
||||
from .view_setting import ViewSetting as ViewSetting
|
||||
|
||||
|
||||
class UserBase(BotEntity, table=False):
|
||||
__tablename__ = "user"
|
||||
|
||||
lang: LanguageBase = Field(sa_type=EnumType(LanguageBase), default=LanguageBase.EN)
|
||||
is_active: bool = True
|
||||
|
||||
name: str
|
||||
|
||||
roles: list[RoleBase] = Field(
|
||||
sa_type=ARRAY(EnumType(RoleBase)), default=[RoleBase.DEFAULT_USER]
|
||||
)
|
||||
39
src/qbot/model/view_setting.py
Normal file
39
src/qbot/model/view_setting.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from sqlmodel import SQLModel, Field, BigInteger
|
||||
from sqlalchemy.ext.asyncio.session import AsyncSession
|
||||
|
||||
from . import session_dep
|
||||
|
||||
|
||||
class ViewSetting(SQLModel, table=True):
|
||||
__tablename__ = "view_setting"
|
||||
user_id: int = Field(
|
||||
sa_type=BigInteger, primary_key=True, foreign_key="user.id", ondelete="CASCADE"
|
||||
)
|
||||
entity_name: str = Field(primary_key=True)
|
||||
filter: str | None = None
|
||||
|
||||
@classmethod
|
||||
@session_dep
|
||||
async def get_filter(
|
||||
cls, *, session: AsyncSession | None = None, user_id: int, entity_name: str
|
||||
):
|
||||
setting = await session.get(cls, (user_id, entity_name))
|
||||
return setting.filter if setting else None
|
||||
|
||||
@classmethod
|
||||
@session_dep
|
||||
async def set_filter(
|
||||
cls,
|
||||
*,
|
||||
session: AsyncSession | None = None,
|
||||
user_id: int,
|
||||
entity_name: str,
|
||||
filter: str,
|
||||
):
|
||||
setting = await session.get(cls, (user_id, entity_name))
|
||||
if setting:
|
||||
setting.filter = filter
|
||||
else:
|
||||
setting = cls(user_id=user_id, entity_name=entity_name, filter=filter)
|
||||
session.add(setting)
|
||||
await session.commit()
|
||||
Reference in New Issue
Block a user