add ruff format, ruff check, time_picker, project structure and imports reorganized

This commit is contained in:
Alexander Kalinovsky
2025-01-21 23:50:19 +01:00
parent ced47ac993
commit 9dd0708a5b
58 changed files with 3690 additions and 2583 deletions

199
utils/main.py Normal file
View File

@@ -0,0 +1,199 @@
from babel.support import LazyProxy
from inspect import signature
from aiogram.types import Message, CallbackQuery
from typing import Any, TYPE_CHECKING
import ujson as json
from ..model.bot_entity import BotEntity
from ..model.bot_enum import BotEnum
from ..model.settings import Settings
from ..model.descriptors import (
EntityFieldDescriptor,
EntityDescriptor,
EntityItemCaptionCallable,
EntityFieldCaptionCallable,
EntityPermission,
EntityCaptionCallable,
)
from ..bot.handlers.context import ContextData, CommandContext
if TYPE_CHECKING:
from ..model.user import UserBase
from ..main import QBotApp
def get_user_permissions(
user: "UserBase", entity_descriptor: EntityDescriptor
) -> list[EntityPermission]:
permissions = list[EntityPermission]()
for permission, roles in entity_descriptor.permissions.items():
for role in roles:
if role in user.roles:
permissions.append(permission)
break
return permissions
def get_local_text(text: str, locale: str) -> str:
try:
obj = json.loads(text) # @IgnoreException
except Exception:
return text
else:
return obj.get(locale, obj[list(obj.keys())[0]])
def check_entity_permission(
entity: BotEntity, user: "UserBase", permission: EntityPermission
) -> bool:
perm_mapping = {
EntityPermission.LIST: EntityPermission.LIST_ALL,
EntityPermission.READ: EntityPermission.READ_ALL,
EntityPermission.UPDATE: EntityPermission.UPDATE_ALL,
EntityPermission.CREATE: EntityPermission.CREATE_ALL,
EntityPermission.DELETE: EntityPermission.DELETE_ALL,
}
if permission not in perm_mapping:
raise ValueError(f"Invalid permission: {permission}")
entity_descriptor = entity.__class__.bot_entity_descriptor
permissions = get_user_permissions(user, entity_descriptor)
if perm_mapping[permission] in permissions:
return True
ownership_filds = entity_descriptor.ownership_fields
for role in user.roles:
if role in ownership_filds:
if getattr(entity, ownership_filds[role]) == user.id:
return True
else:
if permission in permissions:
return True
return False
def get_send_message(message: Message | CallbackQuery):
if isinstance(message, Message):
return message.answer
else:
return message.message.edit_text
def clear_state(state_data: dict, clear_nav: bool = False):
if clear_nav:
state_data.clear()
else:
stack = state_data.get("navigation_stack")
context = state_data.get("navigation_context")
state_data.clear()
if stack:
state_data["navigation_stack"] = stack
if context:
state_data["navigation_context"] = context
def get_entity_item_repr(
entity: BotEntity, item_repr: EntityItemCaptionCallable | None = None
) -> str:
descr = entity.bot_entity_descriptor
if item_repr:
return item_repr(descr, entity)
return (
descr.item_repr(descr, entity)
if descr.item_repr
else f"{
get_callable_str(descr.full_name, descr, entity)
if descr.full_name
else descr.name
}: {str(entity.id)}"
)
def get_value_repr(
value: Any, field_descriptor: EntityFieldDescriptor, locale: str | None = None
) -> str:
if value is None:
return ""
type_ = field_descriptor.type_base
if isinstance(value, bool):
return "【✔︎】" if value else "【 】"
elif field_descriptor.is_list:
if issubclass(type_, BotEntity):
return f"[{', '.join([get_entity_item_repr(item) for item in value])}]"
elif issubclass(type_, BotEnum):
return f"[{', '.join(item.localized(locale) for item in value)}]"
elif type_ is str:
return f"[{', '.join([f'"{item}"' for item in value])}]"
else:
return f"[{', '.join([str(item) for item in value])}]"
elif issubclass(type_, BotEntity):
return get_entity_item_repr(value)
elif issubclass(type_, BotEnum):
return value.localized(locale)
elif isinstance(value, str):
if field_descriptor and field_descriptor.localizable:
return get_local_text(text=value, locale=locale)
return value
elif isinstance(value, int):
return str(value)
elif isinstance(value, float):
return str(value)
else:
return str(value)
def get_callable_str(
callable_str: (
str
| LazyProxy
| EntityCaptionCallable
| EntityItemCaptionCallable
| EntityFieldCaptionCallable
),
descriptor: EntityFieldDescriptor | EntityDescriptor,
entity: Any = None,
value: Any = None,
) -> str:
if isinstance(callable_str, str):
return callable_str
elif isinstance(callable_str, LazyProxy):
return callable_str.value
elif callable(callable_str):
args = signature(callable_str).parameters
if len(args) == 1:
return callable_str(descriptor)
elif len(args) == 2:
return callable_str(descriptor, entity)
elif len(args) == 3:
return callable_str(descriptor, entity, value)
def get_entity_descriptor(
app: "QBotApp", callback_data: ContextData
) -> EntityDescriptor:
if callback_data.entity_name:
return app.entity_metadata.entity_descriptors[callback_data.entity_name]
return None
def get_field_descriptor(
app: "QBotApp", callback_data: ContextData
) -> EntityFieldDescriptor | None:
if callback_data.context == CommandContext.SETTING_EDIT:
return Settings.list_params()[callback_data.field_name]
elif callback_data.context in [
CommandContext.ENTITY_CREATE,
CommandContext.ENTITY_EDIT,
CommandContext.ENTITY_FIELD_EDIT,
]:
entity_descriptor = get_entity_descriptor(app, callback_data)
if entity_descriptor:
return entity_descriptor.fields_descriptors.get(callback_data.field_name)
return None

View File

@@ -1,18 +1,14 @@
from datetime import datetime
from datetime import datetime, time
from decimal import Decimal
from types import NoneType, UnionType
from sqlmodel import select, column
from sqlmodel.ext.asyncio.session import AsyncSession
from typing import Any, Union, get_origin, get_args, TYPE_CHECKING
from typing import Any, Union, get_origin, get_args
from types import UnionType, NoneType
import ujson as json
from ..model.bot_entity import BotEntity
from ..model.bot_enum import BotEnum
from ..model.descriptors import EntityFieldDescriptor, EntityDescriptor
from ..model import EntityPermission
if TYPE_CHECKING:
from ..model.user import UserBase
from ..model.descriptors import EntityFieldDescriptor
async def deserialize[T](session: AsyncSession, type_: type[T], value: str = None) -> T:
@@ -20,12 +16,12 @@ async def deserialize[T](session: AsyncSession, type_: type[T], value: str = Non
is_optional = False
if type_origin in [UnionType, Union]:
args = get_args(type_)
if args[1] == NoneType:
if args[1] is NoneType:
type_ = args[0]
if value is None:
return None
is_optional = True
if get_origin(type_) == list:
if get_origin(type_) is list:
arg_type = None
args = get_args(type_)
if args:
@@ -34,7 +30,9 @@ async def deserialize[T](session: AsyncSession, type_: type[T], value: str = Non
if arg_type:
if issubclass(arg_type, BotEntity):
ret = list[arg_type]()
items = (await session.exec(select(arg_type).where(column("id").in_(values)))).all()
items = (
await session.exec(select(arg_type).where(column("id").in_(values)))
).all()
for item in items:
ret.append(item)
return ret
@@ -52,55 +50,43 @@ async def deserialize[T](session: AsyncSession, type_: type[T], value: str = Non
if is_optional and not value:
return None
return type_(value)
elif type_ == datetime:
elif type_ is time:
if is_optional and not value:
return None
return datetime.fromisoformat(value)
elif type_ == bool:
return time.fromisoformat(value.replace("-", ":"))
elif type_ is datetime:
if is_optional and not value:
return None
if value[-3] == ":":
return datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
elif value[-3] == "-":
return datetime.strptime(value, "%Y-%m-%d %H-%M")
else:
raise ValueError("Invalid datetime format")
elif type_ is bool:
return value == "True"
elif type_ == Decimal:
elif type_ is Decimal:
if is_optional and not value:
return None
return Decimal(value)
if is_optional and not value:
return None
return type_(value)
def serialize(value: Any, field_descriptor: EntityFieldDescriptor) -> str:
if value is None:
return ""
type_ = field_descriptor.type_base
if field_descriptor.is_list:
if issubclass(type_, BotEntity):
return json.dumps([item.id for item in value], ensure_ascii = False)
return json.dumps([item.id for item in value], ensure_ascii=False)
elif issubclass(type_, BotEnum):
return json.dumps([item.value for item in value], ensure_ascii = False)
return json.dumps([item.value for item in value], ensure_ascii=False)
else:
return json.dumps(value, ensure_ascii = False)
return json.dumps(value, ensure_ascii=False)
elif issubclass(type_, BotEntity):
return str(value.id) if value else ""
return str(value)
def get_user_permissions(user: "UserBase", entity_descriptor: EntityDescriptor) -> list[EntityPermission]:
permissions = list[EntityPermission]()
for permission, roles in entity_descriptor.permissions.items():
for role in roles:
if role in user.roles:
permissions.append(permission)
break
return permissions
def get_local_text(text: str, locale: str) -> str:
try:
obj = json.loads(text) #@IgnoreException
except:
return text
else:
return obj.get(locale, obj[list(obj.keys())[0]])