upd project structure
This commit is contained in:
1 parent
baa55d28d6
commit
1d162677a8
62 files changed
+1302
-45
No files matched your search
+3
-2
@@ -1,6 +1,7 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
/backend/.venv
|
|
||||||
.env
|
.env
|
||||||
.pytest_cache
|
.pytest_cache
|
||||||
/backend/logs
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.venv
|
||||||
|
dist
|
||||||
|
*.egg-info
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Alexander Kalinovsky
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# QBot - Telegram Bots Rapid Application Development (RAD) Framework
|
||||||
|
QBot is designed to quickly create complex and feature rich Telegram bots using declarative programming.
|
||||||
|
##
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
[project]
|
||||||
|
name = "quickbot"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "QBot - Rapid Application Development Framework for Telegram Bots"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Development Status :: 3 - Alpha",
|
||||||
|
]
|
||||||
|
authors = [
|
||||||
|
{ name = "Alexander Kalinovsky", email = "ak@botforge.biz" },
|
||||||
|
]
|
||||||
|
license = { file = "LICENSE" }
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"aiogram>=3.17.0",
|
||||||
|
"babel>=2.17.0",
|
||||||
|
"fastapi[standard]>=0.115.8",
|
||||||
|
"greenlet>=3.1.1",
|
||||||
|
"pydantic-settings>=2.7.1",
|
||||||
|
"pyngrok>=7.2.3",
|
||||||
|
"pytest>=8.3.4",
|
||||||
|
"ruff>=0.9.6",
|
||||||
|
"sqlmodel>=0.0.22",
|
||||||
|
"ujson>=5.10.0",
|
||||||
|
]
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
from .main import QBotApp as QBotApp, Config as Config
|
from .main import QBotApp as QBotApp
|
||||||
|
from .config import Config as Config
|
||||||
from .router import Router as Router
|
from .router import Router as Router
|
||||||
from .model.bot_entity import BotEntity as BotEntity
|
from .model.bot_entity import BotEntity as BotEntity
|
||||||
from .model.bot_enum import BotEnum as BotEnum, EnumMember as EnumMember
|
from .model.bot_enum import BotEnum as BotEnum, EnumMember as EnumMember
|
||||||
@@ -10,6 +11,7 @@ from .bot.handlers.context import (
|
|||||||
from .model.descriptors import (
|
from .model.descriptors import (
|
||||||
Entity as Entity,
|
Entity as Entity,
|
||||||
EntityField as EntityField,
|
EntityField as EntityField,
|
||||||
|
FormField as FormField,
|
||||||
EntityForm as EntityForm,
|
EntityForm as EntityForm,
|
||||||
EntityList as EntityList,
|
EntityList as EntityList,
|
||||||
EntityPermission as EntityPermission,
|
EntityPermission as EntityPermission,
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+31
-42
@@ -15,19 +15,18 @@ from .config import Config
|
|||||||
from .fsm.db_storage import DbStorage
|
from .fsm.db_storage import DbStorage
|
||||||
from .middleware.telegram import AuthMiddleware, I18nMiddleware
|
from .middleware.telegram import AuthMiddleware, I18nMiddleware
|
||||||
from .model.user import UserBase
|
from .model.user import UserBase
|
||||||
from .model.entity_metadata import EntityMetadata
|
|
||||||
from .model.descriptors import BotCommand
|
from .model.descriptors import BotCommand
|
||||||
from .router import Router
|
from .router import Router
|
||||||
|
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def default_lifespan(app: "QBotApp"):
|
async def default_lifespan(app: "QBotApp"):
|
||||||
logger.debug("starting qbot app")
|
logger.debug("starting qbot app")
|
||||||
|
|
||||||
if app.lifespan_bot_init:
|
if app.lifespan_bot_init:
|
||||||
|
|
||||||
if app.config.USE_NGROK:
|
if app.config.USE_NGROK:
|
||||||
app.ngrok_init()
|
app.ngrok_init()
|
||||||
|
|
||||||
@@ -52,37 +51,40 @@ async def default_lifespan(app: "QBotApp"):
|
|||||||
logger.info("qbot app stopped")
|
logger.info("qbot app stopped")
|
||||||
|
|
||||||
|
|
||||||
class QBotApp(FastAPI):
|
class QBotApp[UserType: UserBase](FastAPI):
|
||||||
"""
|
"""
|
||||||
Main class for the QBot application
|
`QBotApp` app class, the main entrypoint to use QBot.
|
||||||
|
Derives from FastAPI.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
from qbot import QbotApp
|
||||||
|
|
||||||
|
app = QBotApp()
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__[UserType: UserBase](
|
def __init__(
|
||||||
self,
|
self,
|
||||||
user_class: (
|
user_class: Annotated[
|
||||||
Annotated[
|
type[UserType],
|
||||||
type[UserType], Doc("User class that will be used in the application")
|
Doc(
|
||||||
]
|
"""
|
||||||
| None
|
User entity class, derived from :class:`UserBase`.
|
||||||
) = None,
|
If not provided, default :class:`DefaultUser` will be used.
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
] = None,
|
||||||
config: Config | None = None,
|
config: Config | None = None,
|
||||||
bot_start: (
|
bot_start: Callable[
|
||||||
Annotated[
|
[
|
||||||
Callable[
|
Callable[[Message, Any], tuple[UserType, bool]],
|
||||||
[
|
Message,
|
||||||
Annotated[
|
Any,
|
||||||
Callable[[Message, Any], None],
|
],
|
||||||
Doc("Default handler for the start command"),
|
None,
|
||||||
],
|
] = None,
|
||||||
Message,
|
|
||||||
Any,
|
|
||||||
],
|
|
||||||
None,
|
|
||||||
],
|
|
||||||
Doc("Handler for the start command"),
|
|
||||||
]
|
|
||||||
| None
|
|
||||||
) = None,
|
|
||||||
lifespan: Lifespan[AppType] | None = None,
|
lifespan: Lifespan[AppType] | None = None,
|
||||||
lifespan_bot_init: bool = True,
|
lifespan_bot_init: bool = True,
|
||||||
allowed_updates: list[str] | None = None,
|
allowed_updates: list[str] | None = None,
|
||||||
@@ -92,15 +94,10 @@ class QBotApp(FastAPI):
|
|||||||
if config is None:
|
if config is None:
|
||||||
config = Config()
|
config = Config()
|
||||||
|
|
||||||
if user_class is None:
|
|
||||||
from .model.default_user import DefaultUser
|
|
||||||
|
|
||||||
user_class = DefaultUser
|
|
||||||
|
|
||||||
self.allowed_updates = allowed_updates or ["message", "callback_query"]
|
self.allowed_updates = allowed_updates or ["message", "callback_query"]
|
||||||
|
|
||||||
self.user_class = user_class
|
self.user_class = user_class
|
||||||
self.entity_metadata: EntityMetadata = user_class.entity_metadata
|
self.entity_metadata = user_class.entity_metadata
|
||||||
self.config = config
|
self.config = config
|
||||||
self.lifespan = lifespan
|
self.lifespan = lifespan
|
||||||
self.bot = Bot(
|
self.bot = Bot(
|
||||||
@@ -144,13 +141,11 @@ class QBotApp(FastAPI):
|
|||||||
self.root_router._commands = self.bot_commands
|
self.root_router._commands = self.bot_commands
|
||||||
self.command = self.root_router.command
|
self.command = self.root_router.command
|
||||||
|
|
||||||
|
|
||||||
def register_routers(self, *routers: Router):
|
def register_routers(self, *routers: Router):
|
||||||
for router in routers:
|
for router in routers:
|
||||||
for command_name, command in router._commands.items():
|
for command_name, command in router._commands.items():
|
||||||
self.bot_commands[command_name] = command
|
self.bot_commands[command_name] = command
|
||||||
|
|
||||||
|
|
||||||
def ngrok_init(self):
|
def ngrok_init(self):
|
||||||
try:
|
try:
|
||||||
from pyngrok import ngrok
|
from pyngrok import ngrok
|
||||||
@@ -166,7 +161,6 @@ class QBotApp(FastAPI):
|
|||||||
)
|
)
|
||||||
self.config.NGROK_URL = tunnel.public_url
|
self.config.NGROK_URL = tunnel.public_url
|
||||||
|
|
||||||
|
|
||||||
def ngrok_stop(self):
|
def ngrok_stop(self):
|
||||||
try:
|
try:
|
||||||
from pyngrok import ngrok
|
from pyngrok import ngrok
|
||||||
@@ -178,10 +172,7 @@ class QBotApp(FastAPI):
|
|||||||
ngrok.disconnect(self.config.NGROK_URL)
|
ngrok.disconnect(self.config.NGROK_URL)
|
||||||
ngrok.kill()
|
ngrok.kill()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def bot_init(self):
|
async def bot_init(self):
|
||||||
|
|
||||||
commands_captions = dict[str, list[tuple[str, str]]]()
|
commands_captions = dict[str, list[tuple[str, str]]]()
|
||||||
|
|
||||||
for command_name, command in self.bot_commands.items():
|
for command_name, command in self.bot_commands.items():
|
||||||
@@ -214,7 +205,5 @@ class QBotApp(FastAPI):
|
|||||||
secret_token=self.bot_auth_token,
|
secret_token=self.bot_auth_token,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def bot_close(self):
|
async def bot_close(self):
|
||||||
await self.bot.delete_webhook()
|
await self.bot.delete_webhook()
|
||||||
|
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+139
@@ -0,0 +1,139 @@
|
|||||||
|
from qbot import (
|
||||||
|
QBotApp,
|
||||||
|
BotEntity,
|
||||||
|
Entity,
|
||||||
|
EntityForm,
|
||||||
|
EntityList,
|
||||||
|
FieldEditButton,
|
||||||
|
EntityField,
|
||||||
|
CommandCallbackContext,
|
||||||
|
ContextData,
|
||||||
|
CallbackCommand,
|
||||||
|
)
|
||||||
|
from qbot.model.user import UserBase
|
||||||
|
from qbot.model.descriptors import Filter
|
||||||
|
from qbot.model.role import RoleBase
|
||||||
|
|
||||||
|
from aiogram.types import InlineKeyboardButton
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlmodel import BigInteger, Field, Relationship
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class User(UserBase):
|
||||||
|
bot_entity_descriptor = Entity(
|
||||||
|
icon="👤",
|
||||||
|
full_name="User",
|
||||||
|
full_name_plural="Users",
|
||||||
|
item_repr=lambda d, e: e.name,
|
||||||
|
default_list=EntityList(
|
||||||
|
show_add_new_button=False,
|
||||||
|
static_filters=[Filter("roles", "contains", value=RoleBase.SUPER_USER)],
|
||||||
|
filtering=True,
|
||||||
|
filtering_fields=["name"],
|
||||||
|
),
|
||||||
|
default_form=EntityForm(
|
||||||
|
show_delete_button=False,
|
||||||
|
show_edit_button=False,
|
||||||
|
form_buttons=[
|
||||||
|
[
|
||||||
|
FieldEditButton("name"),
|
||||||
|
FieldEditButton("is_active"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
FieldEditButton("lang"),
|
||||||
|
FieldEditButton("roles"),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
item_repr=lambda d, e: f"{e.name}\n{
|
||||||
|
'is active' if e.is_active else 'is not active'
|
||||||
|
}\nlang: {e.lang.localized()}\nroles: [{
|
||||||
|
', '.join([r.localized() for r in e.roles]) if e.roles else 'none'
|
||||||
|
}]",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Entity(BotEntity):
|
||||||
|
bot_entity_descriptor = Entity(
|
||||||
|
icon="📦",
|
||||||
|
full_name="Entity",
|
||||||
|
full_name_plural="Entities",
|
||||||
|
item_repr=lambda d, e: e.name,
|
||||||
|
default_list=EntityList(
|
||||||
|
filtering=True,
|
||||||
|
filtering_fields=["name"],
|
||||||
|
order_by="name",
|
||||||
|
),
|
||||||
|
default_form=EntityForm(
|
||||||
|
form_buttons=[
|
||||||
|
[
|
||||||
|
FieldEditButton("name"),
|
||||||
|
FieldEditButton("user"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
FieldEditButton("creation_dt"),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: int = EntityField(
|
||||||
|
caption="ID",
|
||||||
|
sm_descriptor=Field(
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
is_visible=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
name: str = EntityField(
|
||||||
|
caption="Name",
|
||||||
|
)
|
||||||
|
|
||||||
|
creation_dt: datetime = EntityField(
|
||||||
|
caption="Creation date",
|
||||||
|
dt_type="datetime",
|
||||||
|
)
|
||||||
|
|
||||||
|
user_id: int | None = EntityField(
|
||||||
|
sm_descriptor=Field(
|
||||||
|
sa_type=BigInteger,
|
||||||
|
foreign_key="user.id",
|
||||||
|
ondelete="RESTRICT",
|
||||||
|
nullable=True,
|
||||||
|
),
|
||||||
|
is_visible=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
user: Optional[User] = EntityField(
|
||||||
|
sm_descriptor=Relationship(
|
||||||
|
sa_relationship_kwargs={
|
||||||
|
"lazy": "selectin",
|
||||||
|
"foreign_keys": "Entity.user_id",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
caption="User",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
app = QBotApp(
|
||||||
|
user_class=User,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command(
|
||||||
|
name="menu",
|
||||||
|
caption="Main menu",
|
||||||
|
show_in_bot_commands=True,
|
||||||
|
clear_navigation=True,
|
||||||
|
)
|
||||||
|
async def menu(context: CommandCallbackContext):
|
||||||
|
context.message_text = "Main menu"
|
||||||
|
context.keyboard_builder.row(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="Entities",
|
||||||
|
callback_data=ContextData(
|
||||||
|
command=CallbackCommand.MENU_ENTRY_ENTITIES,
|
||||||
|
).pack(),
|
||||||
|
)
|
||||||
|
)
|
||||||
Whitespace-only changes.
Reference in new issue
Block a user