crud service
This commit is contained in:
@@ -6,6 +6,8 @@ from typing import Any, Callable, TYPE_CHECKING, Literal, Union
|
||||
from babel.support import LazyProxy
|
||||
from dataclasses import dataclass, field
|
||||
from fastapi.datastructures import State
|
||||
from pydantic import BaseModel
|
||||
from pydantic_core import PydanticUndefined
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from sqlalchemy.orm import InstrumentedAttribute
|
||||
|
||||
@@ -17,6 +19,8 @@ if TYPE_CHECKING:
|
||||
from .bot_entity import BotEntity
|
||||
from ..main import QBotApp
|
||||
from .user import UserBase
|
||||
from .crud_service import CrudService
|
||||
from .bot_process import BotProcess
|
||||
|
||||
# EntityCaptionCallable = Callable[["EntityDescriptor"], str]
|
||||
# EntityItemCaptionCallable = Callable[["EntityDescriptor", Any], str]
|
||||
@@ -46,8 +50,8 @@ class InlineButton[T: "BotEntity"]:
|
||||
|
||||
|
||||
@dataclass
|
||||
class Filter:
|
||||
field_name: str
|
||||
class Filter[T: "BotEntity"]:
|
||||
field: str | Callable[[type[T]], InstrumentedAttribute]
|
||||
operator: Literal[
|
||||
"==",
|
||||
"!=",
|
||||
@@ -67,6 +71,89 @@ class Filter:
|
||||
value: Any | None = None
|
||||
param_index: int | None = None
|
||||
|
||||
def __or__(self, other: "Filter[T] | FilterExpression[T]") -> "FilterExpression[T]":
|
||||
"""Create OR expression with another filter or expression"""
|
||||
if isinstance(other, Filter):
|
||||
return FilterExpression("or", [self, other])
|
||||
elif isinstance(other, FilterExpression):
|
||||
if other.operator == "or":
|
||||
# Simplify: filter | (a | b) = (filter | a | b)
|
||||
return FilterExpression("or", [self] + other.filters)
|
||||
else:
|
||||
return FilterExpression("or", [self, other])
|
||||
else:
|
||||
raise TypeError(f"Cannot combine Filter with {type(other)}")
|
||||
|
||||
def __and__(
|
||||
self, other: "Filter[T] | FilterExpression[T]"
|
||||
) -> "FilterExpression[T]":
|
||||
"""Create AND expression with another filter or expression"""
|
||||
if isinstance(other, Filter):
|
||||
return FilterExpression("and", [self, other])
|
||||
elif isinstance(other, FilterExpression):
|
||||
if other.operator == "and":
|
||||
# Simplify: filter & (a & b) = (filter & a & b)
|
||||
return FilterExpression("and", [self] + other.filters)
|
||||
else:
|
||||
return FilterExpression("and", [self, other])
|
||||
else:
|
||||
raise TypeError(f"Cannot combine Filter with {type(other)}")
|
||||
|
||||
|
||||
class FilterExpression[T: "BotEntity"]:
|
||||
"""
|
||||
Represents a logical expression combining multiple filters with AND/OR operations.
|
||||
Supports expression simplification for optimal query building.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
operator: Literal["or", "and"],
|
||||
filters: list["Filter[T] | FilterExpression[T]"],
|
||||
):
|
||||
self.operator = operator
|
||||
self.filters = self._simplify_filters(filters)
|
||||
|
||||
def _simplify_filters(
|
||||
self, filters: list["Filter[T] | FilterExpression[T]"]
|
||||
) -> list["Filter[T] | FilterExpression[T]"]:
|
||||
"""Simplify filters by flattening nested expressions with the same operator"""
|
||||
simplified = []
|
||||
for filter_obj in filters:
|
||||
if (
|
||||
isinstance(filter_obj, FilterExpression)
|
||||
and filter_obj.operator == self.operator
|
||||
):
|
||||
# Flatten nested expressions with the same operator
|
||||
simplified.extend(filter_obj.filters)
|
||||
else:
|
||||
simplified.append(filter_obj)
|
||||
return simplified
|
||||
|
||||
def __or__(self, other: "Filter[T] | FilterExpression[T]") -> "FilterExpression[T]":
|
||||
"""Combine with another filter or expression using OR"""
|
||||
if isinstance(other, (Filter, FilterExpression)):
|
||||
if isinstance(other, FilterExpression) and other.operator == "or":
|
||||
# Simplify: (a | b) | (c | d) = (a | b | c | d)
|
||||
return FilterExpression("or", self.filters + other.filters)
|
||||
else:
|
||||
return FilterExpression("or", [self, other])
|
||||
else:
|
||||
raise TypeError(f"Cannot combine FilterExpression with {type(other)}")
|
||||
|
||||
def __and__(
|
||||
self, other: "Filter[T] | FilterExpression[T]"
|
||||
) -> "FilterExpression[T]":
|
||||
"""Combine with another filter or expression using AND"""
|
||||
if isinstance(other, (Filter, FilterExpression)):
|
||||
if isinstance(other, FilterExpression) and other.operator == "and":
|
||||
# Simplify: (a & b) & (c & d) = (a & b & c & d)
|
||||
return FilterExpression("and", self.filters + other.filters)
|
||||
else:
|
||||
return FilterExpression("and", [self, other])
|
||||
else:
|
||||
raise TypeError(f"Cannot combine FilterExpression with {type(other)}")
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntityList[T: "BotEntity"]:
|
||||
@@ -77,7 +164,7 @@ class EntityList[T: "BotEntity"]:
|
||||
show_add_new_button: bool = True
|
||||
item_form: str | None = None
|
||||
pagination: bool = True
|
||||
static_filters: list[Filter] = None
|
||||
static_filters: Filter[T] | FilterExpression[T] | None = None
|
||||
filtering: bool = False
|
||||
filtering_fields: list[str] = None
|
||||
order_by: str | Any | None = None
|
||||
@@ -99,9 +186,7 @@ class _BaseFieldDescriptor[T: "BotEntity"]:
|
||||
caption: (
|
||||
str | LazyProxy | Callable[["FieldDescriptor", "BotContext"], str] | None
|
||||
) = None
|
||||
description: (
|
||||
str | LazyProxy | Callable[["FieldDescriptor", "BotContext"], str] | None
|
||||
) = None
|
||||
description: str | LazyProxy | None = PydanticUndefined
|
||||
edit_prompt: (
|
||||
str
|
||||
| LazyProxy
|
||||
@@ -122,8 +207,8 @@ class _BaseFieldDescriptor[T: "BotEntity"]:
|
||||
bool_false_value: str | LazyProxy = "no"
|
||||
bool_true_value: str | LazyProxy = "yes"
|
||||
ep_form: str | Callable[["BotContext"], str] | None = None
|
||||
ep_parent_field: str | None = None
|
||||
ep_child_field: str | None = None
|
||||
ep_parent_field: str | Callable[[type[T]], InstrumentedAttribute] | None = None
|
||||
ep_child_field: str | Callable[[type[T]], InstrumentedAttribute] | None = None
|
||||
dt_type: Literal["date", "datetime"] = "date"
|
||||
options: (
|
||||
list[list[Union[Any, tuple[Any, str]]]]
|
||||
@@ -133,7 +218,7 @@ class _BaseFieldDescriptor[T: "BotEntity"]:
|
||||
options_custom_value: bool = True
|
||||
show_current_value_button: bool = True
|
||||
show_skip_in_editor: Literal[False, "Auto"] = "Auto"
|
||||
default: Any = None
|
||||
default: Any = PydanticUndefined
|
||||
default_factory: Callable[[], Any] | None = None
|
||||
|
||||
|
||||
@@ -178,7 +263,8 @@ class _BaseEntityDescriptor[T: "BotEntity"]:
|
||||
full_name_plural: (
|
||||
str | LazyProxy | Callable[["EntityDescriptor", "BotContext"], str] | None
|
||||
) = None
|
||||
description: (
|
||||
description: str | None = None
|
||||
ui_description: (
|
||||
str | LazyProxy | Callable[["EntityDescriptor", "BotContext"], str] | None
|
||||
) = None
|
||||
item_repr: Callable[[T, "BotContext"], str] | None = None
|
||||
@@ -190,11 +276,11 @@ class _BaseEntityDescriptor[T: "BotEntity"]:
|
||||
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_RLS: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
|
||||
EntityPermission.READ_RLS: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
|
||||
EntityPermission.CREATE_RLS: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
|
||||
EntityPermission.UPDATE_RLS: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
|
||||
EntityPermission.DELETE_RLS: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
|
||||
EntityPermission.LIST_ALL: [RoleBase.SUPER_USER],
|
||||
EntityPermission.READ_ALL: [RoleBase.SUPER_USER],
|
||||
EntityPermission.CREATE_ALL: [RoleBase.SUPER_USER],
|
||||
@@ -202,6 +288,8 @@ class _BaseEntityDescriptor[T: "BotEntity"]:
|
||||
EntityPermission.DELETE_ALL: [RoleBase.SUPER_USER],
|
||||
}
|
||||
)
|
||||
rls_filters: Filter[T] | FilterExpression[T] | None = None
|
||||
rls_filters_params: Callable[["UserBase"], list[Any]] = lambda user: [user.id]
|
||||
before_create: Callable[["BotContext"], Union[bool, str]] | None = None
|
||||
before_create_save: Callable[[T, "BotContext"], Union[bool, str]] | None = None
|
||||
before_update_save: (
|
||||
@@ -212,6 +300,7 @@ class _BaseEntityDescriptor[T: "BotEntity"]:
|
||||
on_created: Callable[[T, "BotContext"], None] | None = None
|
||||
on_deleted: Callable[[T, "BotContext"], None] | None = None
|
||||
on_updated: Callable[[dict[str, Any], T, "BotContext"], None] | None = None
|
||||
crud: Union["CrudService", None] = None
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
@@ -228,7 +317,7 @@ class EntityDescriptor(_BaseEntityDescriptor):
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class CommandCallbackContext[UT: UserBase]:
|
||||
class CommandCallbackContext:
|
||||
keyboard_builder: InlineKeyboardBuilder = field(
|
||||
default_factory=InlineKeyboardBuilder
|
||||
)
|
||||
@@ -238,7 +327,7 @@ class CommandCallbackContext[UT: UserBase]:
|
||||
message: Message | CallbackQuery
|
||||
callback_data: ContextData
|
||||
db_session: AsyncSession
|
||||
user: UT
|
||||
user: "UserBase"
|
||||
app: "QBotApp"
|
||||
app_state: State
|
||||
state_data: dict[str, Any]
|
||||
@@ -249,11 +338,11 @@ class CommandCallbackContext[UT: UserBase]:
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class BotContext[UT: UserBase]:
|
||||
class BotContext:
|
||||
db_session: AsyncSession
|
||||
app: "QBotApp"
|
||||
app_state: State
|
||||
user: UT
|
||||
user: "UserBase"
|
||||
message: Message | CallbackQuery | None = None
|
||||
default_handler: Callable[["BotEntity", "BotContext"], None] | None = None
|
||||
|
||||
@@ -271,3 +360,31 @@ class BotCommand:
|
||||
show_cancel_in_param_form: bool = True
|
||||
show_back_in_param_form: bool = True
|
||||
handler: Callable[[CommandCallbackContext], None]
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class _BaseProcessDescriptor:
|
||||
description: str | LazyProxy | None = None
|
||||
roles: list[RoleBase] = field(
|
||||
default_factory=lambda: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER]
|
||||
)
|
||||
icon: str | None = None
|
||||
caption: str | LazyProxy | None = None
|
||||
pre_check: Callable[[BotContext], bool | str] | None = None
|
||||
show_in_bot_menu: bool = False
|
||||
answer_message: Callable[[BotContext, BaseModel], str] | None = None
|
||||
answer_inline_buttons: (
|
||||
Callable[[BotContext, BaseModel], list[InlineKeyboardButton]] | None
|
||||
) = None
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class ProcessDescriptor(_BaseProcessDescriptor):
|
||||
name: str
|
||||
process_class: type["BotProcess"]
|
||||
input_schema: type[BaseModel] | None = None
|
||||
output_schema: type[BaseModel] | None = None
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Process(_BaseProcessDescriptor): ...
|
||||
|
||||
Reference in New Issue
Block a user