Compare commits

..

27 Commits

Author SHA1 Message Date
Alexander Kalinovsky
02aec23b84 feat: entity events
All checks were successful
Build Docs / changes (push) Successful in 20s
Build Docs / build-docs (push) Has been skipped
Build Docs / deploy-docs (push) Has been skipped
2025-03-10 22:10:09 +07:00
Alexander Kalinovsky
dd56d3f312 feat: awaitable entity representations
All checks were successful
Build Docs / changes (push) Successful in 28s
Build Docs / build-docs (push) Has been skipped
Build Docs / deploy-docs (push) Has been skipped
2025-03-08 14:57:09 +07:00
Alexander Kalinovsky
9e9979bf31 fix docs deploy
All checks were successful
Build Docs / changes (push) Successful in 5s
Build Docs / build-docs (push) Successful in 43s
Build Docs / deploy-docs (push) Successful in 4s
2025-03-01 15:59:16 +07:00
Alexander Kalinovsky
921c6e39f5 fix docs deploy
All checks were successful
Build Docs / changes (push) Successful in 4s
Build Docs / build-docs (push) Successful in 36s
Build Docs / deploy-docs (push) Successful in 3s
2025-03-01 15:42:57 +07:00
Alexander Kalinovsky
518d7a4856 fix docs deploy
All checks were successful
Build Docs / changes (push) Successful in 4s
Build Docs / build-docs (push) Successful in 35s
Build Docs / deploy-docs (push) Successful in 3s
2025-03-01 15:28:49 +07:00
Alexander Kalinovsky
5690a4ba12 fix docs deploy
All checks were successful
Build Docs / changes (push) Successful in 4s
Build Docs / build-docs (push) Successful in 35s
Build Docs / deploy-docs (push) Successful in 3s
2025-03-01 15:23:31 +07:00
Alexander Kalinovsky
06c9467c6e fix docs deploy
Some checks failed
Build Docs / changes (push) Successful in 5s
Build Docs / build-docs (push) Successful in 36s
Build Docs / deploy-docs (push) Failing after 2s
2025-03-01 15:21:06 +07:00
Alexander Kalinovsky
6deb49c6d7 fix docs deploy
Some checks failed
Build Docs / changes (push) Successful in 5s
Build Docs / build-docs (push) Successful in 39s
Build Docs / deploy-docs (push) Failing after 3s
2025-03-01 15:17:46 +07:00
Alexander Kalinovsky
89f7e3bee2 fix docs deploy
Some checks failed
Build Docs / changes (push) Successful in 5s
Build Docs / build-docs (push) Successful in 48s
Build Docs / deploy-docs (push) Failing after 3s
2025-03-01 14:54:20 +07:00
Alexander Kalinovsky
1b0d6d4125 fix docs deploy
Some checks failed
Build Docs / changes (push) Successful in 4s
Build Docs / build-docs (push) Failing after 25s
Build Docs / deploy-docs (push) Has been skipped
2025-03-01 14:16:18 +07:00
Alexander Kalinovsky
22ab18f648 fix docs deploy
Some checks failed
Build Docs / changes (push) Failing after 6s
Build Docs / build-docs (push) Has been skipped
Build Docs / deploy-docs (push) Has been skipped
2025-03-01 14:14:17 +07:00
Alexander Kalinovsky
0054a7e154 fix docs deploy
Some checks failed
Build Docs / changes (push) Successful in 6s
Build Docs / build-docs (push) Failing after 1m14s
Build Docs / deploy-docs (push) Has been skipped
2025-03-01 02:37:00 +07:00
Alexander Kalinovsky
2327c49e1c fix docs deploy
Some checks failed
Build Docs / changes (push) Successful in 6s
Build Docs / build-docs (push) Failing after 28s
Build Docs / deploy-docs (push) Has been skipped
2025-03-01 02:31:42 +07:00
Alexander Kalinovsky
748264f6c8 fix docs deploy
Some checks failed
Build Docs / changes (push) Successful in 5s
Build Docs / build-docs (push) Failing after 17s
Build Docs / deploy-docs (push) Has been skipped
2025-03-01 02:26:26 +07:00
Alexander Kalinovsky
55e12c0475 fix docs deploy
Some checks failed
Build Docs / changes (push) Successful in 5s
Build Docs / build-docs (push) Failing after 24s
Build Docs / deploy-docs (push) Has been skipped
2025-03-01 02:23:44 +07:00
Alexander Kalinovsky
f3fe54513c fix docs deploy
Some checks failed
Build Docs / changes (push) Successful in 5s
Build Docs / build-docs (push) Failing after 18s
Build Docs / deploy-docs (push) Has been skipped
2025-03-01 02:21:44 +07:00
Alexander Kalinovsky
50a434dae8 fix docs deploy
Some checks failed
Build Docs / changes (push) Successful in 10s
Build Docs / build-docs (push) Failing after 28s
Build Docs / deploy-docs (push) Has been skipped
2025-03-01 02:17:18 +07:00
Alexander Kalinovsky
76966301f6 fix docs deploy
Some checks failed
Build Docs / changes (push) Successful in 5s
Build Docs / build-docs (push) Failing after 1m24s
Build Docs / deploy-docs (push) Has been skipped
2025-03-01 01:50:13 +07:00
Alexander Kalinovsky
fc49b2ee47 fix docs deploy
Some checks failed
Build Docs / changes (push) Successful in 6s
Build Docs / build-docs (push) Failing after 1m25s
Build Docs / deploy-docs (push) Has been skipped
2025-03-01 01:45:11 +07:00
Alexander Kalinovsky
d59b28ad29 fix python version
Some checks failed
Build Docs / changes (push) Successful in 6s
Build Docs / build-docs (push) Failing after 8s
Build Docs / deploy-docs (push) Has been skipped
2025-03-01 01:31:52 +07:00
Alexander Kalinovsky
e413cd3ae5 test docs deploy
Some checks failed
Build Docs / changes (push) Successful in 6s
Build Docs / build-docs (push) Failing after 51s
Build Docs / deploy-docs (push) Has been skipped
2025-03-01 01:27:33 +07:00
Alexander Kalinovsky
0f479f7914 fix docs deploy
All checks were successful
Build Docs / changes (push) Successful in 8s
Build Docs / build-docs (push) Has been skipped
Build Docs / deploy-docs (push) Has been skipped
2025-03-01 01:24:41 +07:00
Alexander Kalinovsky
426ca3fad0 fix doc deploy
Some checks failed
Build Docs / changes (push) Failing after 1s
Build Docs / build-docs (push) Has been skipped
Build Docs / deploy-docs (push) Has been skipped
2025-03-01 01:22:28 +07:00
Alexander Kalinovsky
b7abd3f30f fix workflow name
Some checks failed
Build Docs / changes (push) Failing after 27s
Build Docs / build-docs (push) Has been skipped
Build Docs / deploy-docs (push) Has been skipped
2025-03-01 01:03:38 +07:00
Alexander Kalinovsky
71ba2c052b add mkdocs 2025-03-01 00:57:31 +07:00
Alexander Kalinovsky
1d162677a8 upd project structure 2025-02-18 20:57:52 +01:00
baa55d28d6 Merge pull request 'upd bot init' (#1) from main into prepack
Reviewed-on: BotForge/qbot#1
2025-02-10 19:36:54 +00:00
70 changed files with 2060 additions and 9 deletions

View File

@@ -0,0 +1,86 @@
name: Build Docs
on:
push:
branches:
- main
jobs:
changes:
runs-on: ubuntu-latest
# Set job outputs to values from filter step
outputs:
docs: ${{ steps.filter.outputs.docs }}
steps:
# - name: Base requirements
# run: |
# apk update && apk add --no-cache nodejs
- uses: actions/checkout@v4
# For pull requests it's not necessary to checkout the code but for the main branch it is
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
docs:
- README.md
- docs/**
- requirements-docs.txt
- pyproject.toml
- mkdocs.yml
- Dockerfile.docs-site
- docker-compose.docs-site.yml
- .gitea/workflows/build-docs.yml
- .gitea/workflows/deploy-docs.yml
build-docs:
needs:
- changes
if: ${{ needs.changes.outputs.docs == 'true' }}
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13.2"
- name: Setup uv
uses: astral-sh/setup-uv@v5
with:
version: "0.6.3"
enable-cache: true
cache-dependency-glob: |
requirements**.txt
pyproject.toml
- name: Install docs extras
run: |
uv venv
uv pip install mkdocs-material
- name: Build Docs
run: .venv/bin/mkdocs build
- name: Build Docker Image
run: |
docker build -f Dockerfile.docs-site -t ${{ vars.STACK_NAME }} .
docker tag ${{ vars.STACK_NAME }}:latest registry.botforge.biz/${{ vars.STACK_NAME }}
docker login registry.botforge.biz -u ${{ secrets.REGISTRY_USER }} -p ${{ secrets.REGISTRY_PASSWORD }}
docker push registry.botforge.biz/${{ vars.STACK_NAME }}
deploy-docs:
needs:
- build-docs
runs-on: staging
env:
STACK_NAME: ${{ vars.STACK_NAME }}
DOMAIN: ${{ vars.DOMAIN }}
steps:
- uses: actions/checkout@v4
- name: Copy to working directory
run: |
mkdir -p ~/${{ vars.STACK_NAME }}
cp docker-compose.docs-site.yml ~/${{ vars.STACK_NAME }}/docker-compose.yml
- name: Start Docs Site
run: |
cd ~/${{ vars.STACK_NAME }}
docker compose up -d

8
.gitignore vendored
View File

@@ -1,6 +1,10 @@
__pycache__ __pycache__
/backend/.venv
.env .env
.pytest_cache .pytest_cache
/backend/logs
.DS_Store .DS_Store
.venv
.python-version
uv.lock
dist
*.egg-info
site

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.13

10
Dockerfile.docs-site Normal file
View File

@@ -0,0 +1,10 @@
FROM nginx
WORKDIR /usr/share/nginx/html
RUN rm -rf ./*
COPY ./site/ ./
CMD ["nginx", "-g", "daemon off;"]

21
LICENSE Normal file
View File

@@ -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.

99
README.md Normal file
View File

@@ -0,0 +1,99 @@
<p align="center">
<a href="https://qbot.botforge.biz"><img src="https://qbot.botforge.biz/img/qbot.svg" alt="QBot"></a>
</p>
<p align="center">
<em>Telegram Bots Rapid Application Development (RAD) Framework.</em>
</p>
**QBot** is a library for fast development of Telegram bots and mini-apps following the **RAD (Rapid Application Development)** principle in a **declarative style**.
## Key Features
- **Automatic CRUD Interface Generation** Manage objects effortlessly without manual UI coding.
- **Built-in Field Editors** Includes text inputs, date/time pickers, boolean switches, enums, and entity selectors.
- **Advanced Pagination & Filtering** Easily organize and navigate large datasets.
- **Authentication & Authorization** Role-based access control for secure interactions.
- **Context Preservation** Store navigation stacks and user interaction states in the database.
- **Internationalization Support** Localizable UI and string fields for multilingual bots.
**QBot** powered by **[FastAPI](https://fastapi.tiangolo.com)**, **[SQLModel](https://sqlmodel.tiangolo.com)** & **[aiogram](https://aiogram.dev)** Leverage the full capabilities of these frameworks for high performance and flexibility.
## Benefits
- **Faster Development** Automate repetitive tasks and build bots in record time.
- **Highly Modular** Easily extend and customize functionality.
- **Less Code Duplication** Focus on core features while QBot handles the rest.
- **Enterprise-Grade Structure** Scalable, maintainable, and optimized for real-world usage.
## Example
```python
class AppEntity(BotEntity):
"""
BotEntity - business entity. Based on SQLModel, which provides ORM functionality,
basic CRUD functions and additional metadata, describing entities view and behaviour
"""
bot_entity_descriptor = Entity( # metadata attribute
name = "bot_entity", # Entity - descriptor for entity metadata
...
)
name: str # entity field with default sqlmodel's FieldInfo descriptor
# and default qbot's field descriptor
description: str | None = Field( # field with sqlmodel's descriptor
sa_type = String, index = True) # and default qbot's descriptor
age: int = EntityField( # field with qbot's descriptor
caption = "Age",
)
user_id: int | None = EntityField( # using both descriptors
sm_descriptor=Field(
sa_type=BigInteger,
foreign_key="user.id",
ondelete="RESTRICT",
nullable=True,
),
is_visible=False,
)
user: Optional[User] = EntityField( # using Relationship as sqlmodel descriptor
sm_descriptor=Relationship(
back_populates="entities",
sa_relationship_kwargs={
"lazy": "selectin",
"foreign_keys": "Entity.user_id",
}
),
caption="User",
)
app = QBotApp() # bot application based on FastAPI application
# providing Telegram API webhook handler
@app.command( # decorator for bot commands definition
name="menu",
caption="Main menu",
show_in_bot_commands=True, # shows command in bot's main menu
clear_navigation=True, # resets navigation stack between bot dialogues
)
async def menu(context: CommandCallbackContext):
context.message_text = "Main menu" # message text
context.keyboard_builder.row( # and buttons will be shown to user
InlineKeyboardButton(
text="Entities",
callback_data=ContextData( # callback query dataclass representing
command=CallbackCommand.ENTITY_LIST, # navigation context and its parameters
entity_name="bot_entity"
).pack(),
)
)
```
## Result
<iframe width="100%" height="691" src="https://www.youtube.com/embed/ptTnoppkYfM" title="QBot Framework The Open-Source RAD Tool for Telegram Bots" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
Here you can see the result - [YouTube Video with Bot](https://www.youtube.com/shorts/ptTnoppkYfM)

View File

@@ -0,0 +1,20 @@
services:
qbot-docs-site:
image: 'registry.botforge.biz/${STACK_NAME?Variable not set}:${TAG-latest}'
restart: always
networks:
- traefik-public
labels:
- traefik.enable=true
- traefik.docker.network=traefik-public
- traefik.constraint-label=traefik-public
- traefik.http.services.${STACK_NAME?Variable not set}.loadbalancer.server.port=80
- traefik.http.routers.${STACK_NAME?Variable not set}.rule=Host(`${DOMAIN?Variable not set}`)
- traefik.http.routers.${STACK_NAME?Variable not set}.entrypoints=websecure
- traefik.http.routers.${STACK_NAME?Variable not set}.tls=true
- traefik.http.routers.${STACK_NAME?Variable not set}.tls.certresolver=le
- traefik.http.routers.${STACK_NAME?Variable not set}.service=${STACK_NAME?Variable not set}
networks:
traefik-public:
external: true

65
docs/img/qbot.svg Normal file
View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="1500"
height="500"
viewBox="0 0 1500 500"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
sodipodi:docname="qbot.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
inkscape:zoom="0.52452521"
inkscape:cx="704.4466"
inkscape:cy="297.41182"
inkscape:window-width="1608"
inkscape:window-height="1007"
inkscape:window-x="8"
inkscape:window-y="52"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" /><defs
id="defs1"><rect
x="80.81143"
y="115.09507"
width="901.16989"
height="411.40365"
id="rect3" /><rect
x="90.606755"
y="102.85091"
width="416.30131"
height="306.1039"
id="rect2" /></defs><g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1"><g
id="g18"
transform="translate(118.93657)"><path
style="font-size:266.667px;font-family:'Arial Rounded MT Bold';-inkscape-font-specification:'Arial Rounded MT Bold, Normal';fill:#404040"
d="m 685.86904,314.58341 q 8.46355,5.72918 18.4896,11.19793 10.02606,5.33854 13.28127,8.59377 3.25521,3.125 3.25521,8.98437 0,4.16668 -3.90625,8.33335 -3.77605,4.16668 -9.24481,4.16668 -4.42708,0 -10.8073,-2.8646 -6.25001,-2.86459 -14.84377,-8.33334 -8.46355,-5.46876 -18.61981,-12.76044 -18.88023,9.63543 -46.35423,9.63543 -22.26565,0 -39.974,-7.03126 -17.57815,-7.16146 -29.55733,-20.44273 -11.97918,-13.41147 -18.09898,-31.77087 -5.98959,-18.3594 -5.98959,-39.97401 0,-22.00523 6.25001,-40.36463 6.38021,-18.3594 18.35939,-31.25004 11.97918,-12.89064 29.16671,-19.66148 17.18752,-6.90105 39.06254,-6.90105 29.68754,0 50.91152,12.10939 21.3542,11.97918 32.29171,34.24483 10.93751,22.13545 10.93751,52.0834 0,45.44276 -24.6094,72.0053 z m -30.33858,-21.09377 q 8.07293,-9.2448 11.84897,-21.87503 3.90626,-12.63022 3.90626,-29.29691 0,-20.96358 -6.77084,-36.32817 -6.77085,-15.3646 -19.40107,-23.17711 -12.50002,-7.94273 -28.77608,-7.94273 -11.58855,0 -21.4844,4.42709 -9.76563,4.29688 -16.9271,12.63022 -7.03126,8.33335 -11.19793,21.3542 -4.03647,12.89064 -4.03647,29.0365 0,32.94274 15.36461,50.78131 15.3646,17.70836 38.80213,17.70836 9.63543,0 19.79169,-4.03647 -6.1198,-4.55729 -15.3646,-9.11459 -9.1146,-4.5573 -12.50002,-7.03126 -3.38542,-2.47396 -3.38542,-7.03126 0,-3.90625 3.25521,-6.90105 3.25521,-2.99479 7.16147,-2.99479 11.84897,0 39.71359,19.79169 z m 169.53146,44.79172 h -58.3334 q -12.63023,0 -18.09898,-5.59898 -5.33855,-5.72917 -5.33855,-18.09897 V 171.09366 q 0,-12.63023 5.46876,-18.09898 5.59896,-5.59897 17.96877,-5.59897 h 61.84903 q 13.67189,0 23.69795,1.69271 10.02605,1.69271 17.96877,6.51043 6.77084,4.03646 11.97918,10.28647 5.20834,6.1198 7.94272,13.67189 2.73438,7.42188 2.73438,15.75523 0,28.64586 -28.64587,41.92713 37.63025,11.97918 37.63025,46.61464 0,16.01564 -8.20313,28.90629 -8.20314,12.76043 -22.13545,18.88023 -8.72396,3.64583 -20.0521,5.20834 -11.32814,1.43229 -26.43233,1.43229 z m -2.86458,-84.76574 h -40.23443 v 55.72924 h 41.53651 q 39.19276,0 39.19276,-28.25524 0,-14.45315 -10.15627,-20.96357 -10.15626,-6.51043 -30.33857,-6.51043 z m -40.23443,-77.08343 v 49.34902 h 35.41671 q 14.45314,0 22.26565,-2.73438 7.94272,-2.73437 12.10939,-10.41667 3.25522,-5.46876 3.25522,-12.2396 0,-14.45315 -10.28647,-19.14065 -10.28648,-4.81772 -31.38025,-4.81772 z m 283.98469,92.83866 q 0,15.88544 -4.9479,29.29691 -4.9479,13.41148 -14.3229,23.0469 -9.375,9.63543 -22.3959,14.84377 -13.0208,5.07813 -29.29688,5.07813 -16.14586,0 -29.0365,-5.20834 -12.89064,-5.20834 -22.39586,-14.84377 -9.37501,-9.76563 -14.32293,-22.91669 -4.81772,-13.28127 -4.81772,-29.29691 0,-16.14585 4.94793,-29.55733 4.94792,-13.41147 14.19272,-22.91669 9.2448,-9.50522 22.39586,-14.58335 13.15106,-5.20834 29.0365,-5.20834 16.14588,0 29.29688,5.20834 13.1511,5.20834 22.5261,14.84376 9.375,9.63543 14.1927,22.9167 4.9479,13.28126 4.9479,29.29691 z m -35.6771,0 q 0,-21.74482 -9.6354,-33.85421 -9.5052,-12.10939 -25.65108,-12.10939 -10.41668,0 -18.3594,5.46876 -7.94272,5.33855 -12.2396,15.88544 -4.29688,10.54689 -4.29688,24.60941 0,13.93231 4.16667,24.34899 4.29688,10.41668 12.10939,16.01564 7.94272,5.46876 18.61982,5.46876 16.14588,0 25.65108,-12.10939 9.6354,-12.2396 9.6354,-33.72401 z m 65.6251,-69.14071 h 3.9062 v -21.35419 q 0,-8.59376 0.3906,-13.41148 0.5209,-4.94792 2.6042,-8.46355 2.0833,-3.64584 5.9896,-5.85938 3.9063,-2.34375 8.724,-2.34375 6.7708,0 12.2396,5.07813 3.6458,3.38542 4.5573,8.33334 1.0416,4.81771 1.0416,13.8021 v 24.21878 h 13.0209 q 7.5521,0 11.4583,3.64584 4.0365,3.51563 4.0365,9.11459 0,7.16147 -5.7292,10.02606 -5.5989,2.86458 -16.1458,2.86458 h -6.6407 v 65.36467 q 0,8.33334 0.5209,12.89064 0.651,4.42709 3.125,7.29167 2.6041,2.73438 8.3333,2.73438 3.125,0 8.4636,-1.04167 5.3385,-1.17187 8.3333,-1.17187 4.2969,0 7.6823,3.51563 3.5156,3.38542 3.5156,8.46355 0,8.59376 -9.375,13.15106 -9.375,4.55729 -26.9531,4.55729 -16.6667,0 -25.2605,-5.59896 -8.5937,-5.59897 -11.3281,-15.49481 -2.6042,-9.89585 -2.6042,-26.43233 v -68.22924 h -4.6875 q -7.6823,0 -11.7188,-3.64584 -4.0364,-3.64583 -4.0364,-9.2448 0,-5.59896 4.1667,-9.11459 4.2968,-3.64584 12.3698,-3.64585 z"
id="text17"
aria-label="QBot" /><g
id="g17"
transform="matrix(1.195598,0,0,1.195598,-52.1047,-71.699942)"><path
id="path17"
style="-inkscape-font-specification:'Arial Rounded MT Bold, ';opacity:1;fill:#a587c9;fill-opacity:1"
d="m 272.44727,142.82422 c -6.05914,0.0153 -11.03743,2.03697 -14.95508,6.0293 -3.98025,4.51096 -6.09057,11.66026 -6.47657,21.24218 v 3.72461 c -0.0471,-0.0706 -0.0973,-0.13474 -0.14453,-0.20508 v 52.01758 18.20703 57.11524 c 0.0477,-0.0476 0.0968,-0.0871 0.14453,-0.13477 v 64.16602 c 2e-5,8.38844 1.13695,15.11813 3.29102,20.36523 0.73739,1.28016 1.58937,2.4395 2.56445,3.46875 3.62458,3.92663 8.21734,6.0553 13.75782,6.42383 0.5993,0.0286 1.18235,0.0711 1.8125,0.0723 6.05823,-0.0157 11.0359,-2.03742 14.95312,-6.0293 3.98025,-4.51096 6.09057,-11.66026 6.47656,-21.24219 v -3.72461 c 0.0471,0.0706 0.0974,0.13474 0.14453,0.20508 v -52.01953 -18.20508 -57.11523 c -0.0477,0.0476 -0.0968,0.0871 -0.14453,0.13476 V 173.1543 c 0,-8.38703 -1.13767,-15.11654 -3.29101,-20.36328 -0.73758,-1.28076 -1.58899,-2.44105 -2.56446,-3.47071 -3.6877,-3.99502 -8.37983,-6.1256 -14.05078,-6.4375 -0.50595,-0.0202 -0.99001,-0.0576 -1.51757,-0.0586 z" /><path
d="m 250.87109,300.95508 v -57.11524 c -0.79426,8.72199 -2.70097,16.33161 -5.73632,22.8125 -4.1111,8.77777 -9.55427,15.33464 -16.33204,19.66797 -6.77777,4.22221 -14.22287,6.33203 -22.33398,6.33203 -7.44443,0 -14.38954,-2.10982 -20.83398,-6.33203 -6.44444,-4.33333 -11.66602,-10.8902 -15.66602,-19.66797 -3.88888,-8.88888 -5.83398,-19.4438 -5.83398,-31.66601 0,-18.55553 4.05686,-32.88889 12.16796,-43 8.1111,-10.1111 18.27779,-15.16602 30.5,-15.16602 6,0 11.66668,1.22158 17,3.66602 5.33333,2.33333 10.05492,5.94509 14.16602,10.83398 4.11111,4.77777 7.33269,10.83269 9.66602,18.16602 1.65444,4.96334 2.70164,10.36877 3.23632,16.14648 v -52.01758 c -6.86066,-10.22654 -15.08687,-17.82908 -24.6875,-22.79492 -9.66665,-5.11111 -20.61177,-7.66601 -32.83398,-7.66601 -14.44443,0 -27.61113,3.60982 -39.5,10.83203 -11.77777,7.22221 -20.99936,17.77909 -27.66602,31.66797 -6.66666,13.77776 -10,30.33269 -10,49.66601 0,18.88887 3.27844,35.33204 9.83399,49.33203 6.66666,13.88887 15.77648,24.55556 27.33203,32 11.66665,7.44444 24.55687,11.16797 38.66797,11.16797 11.55555,0 22.05361,-2.27843 31.49804,-6.83398 9.50365,-4.64131 18.62118,-11.32337 27.35547,-20.03125 z"
style="-inkscape-font-specification:'Arial Rounded MT Bold, ';opacity:1;fill:#fabf9c;fill-opacity:1"
id="path13" /><path
d="m 294.01562,237.18555 v 57.11523 c 0.79416,-8.72281 2.70075,-16.33307 5.73633,-22.81445 4.1111,-8.77777 9.55426,-15.33269 16.33203,-19.66602 6.77777,-4.22221 14.22288,-6.33398 22.33399,-6.33398 7.44443,0 14.38954,2.11177 20.83398,6.33398 6.44444,4.33333 11.66602,10.88825 15.66602,19.66602 3.88888,8.88888 5.83398,19.4438 5.83398,31.66601 0,18.55553 -4.05687,32.88889 -12.16797,43 -8.1111,10.1111 -18.27779,15.16797 -30.5,15.16797 -6,0 -11.66668,-1.22353 -17,-3.66797 -5.33333,-2.33333 -10.05491,-5.94314 -14.16601,-10.83203 -4.11111,-4.77777 -7.33269,-10.83464 -9.66602,-18.16797 -1.6545,-4.96351 -2.70166,-10.36855 -3.23633,-16.14648 v 52.01953 c 6.86067,10.22655 15.08688,17.82909 24.6875,22.79492 9.66665,5.11111 20.61178,7.66602 32.83399,7.66602 14.44443,0 27.61113,-3.61178 39.5,-10.83399 11.77777,-7.22221 20.99936,-17.77713 27.66601,-31.66601 6.66667,-13.77776 10,-30.3327 10,-49.66602 10e-6,-18.88887 -3.27648,-35.33399 -9.83203,-49.33398 -6.66666,-13.88887 -15.77843,-24.55556 -27.33398,-32 -11.66665,-7.44444 -24.55492,-11.16602 -38.66602,-11.16602 -11.55555,0 -22.05557,2.27844 -31.5,6.83399 -9.50364,4.64131 -18.62118,11.32337 -27.35547,20.03125 z"
style="-inkscape-font-specification:'Arial Rounded MT Bold, ';opacity:1;fill:#a7a7fa;fill-opacity:1"
id="path15" /></g></g></g></svg>

After

Width:  |  Height:  |  Size: 9.4 KiB

59
docs/img/qbot_1_1.svg Normal file
View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="500"
height="500"
viewBox="0 0 500 500"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
sodipodi:docname="qbot_1_1.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
inkscape:zoom="0.52452521"
inkscape:cx="346.02722"
inkscape:cy="171.58374"
inkscape:window-width="1608"
inkscape:window-height="1007"
inkscape:window-x="8"
inkscape:window-y="52"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" /><defs
id="defs1"><rect
x="80.811432"
y="115.09507"
width="901.16986"
height="411.40366"
id="rect3" /><rect
x="90.606758"
y="102.85091"
width="416.3013"
height="306.10391"
id="rect2" /></defs><g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1"><g
id="g17"
transform="matrix(1.3786092,0,0,1.3786092,-125.59292,-120.94283)"><path
id="path17"
style="-inkscape-font-specification:'Arial Rounded MT Bold, ';opacity:1;fill:#a587c9;fill-opacity:1"
d="m 272.44727,142.82422 c -6.05914,0.0153 -11.03743,2.03697 -14.95508,6.0293 -3.98025,4.51096 -6.09057,11.66026 -6.47657,21.24218 v 3.72461 c -0.0471,-0.0706 -0.0973,-0.13474 -0.14453,-0.20508 v 52.01758 18.20703 57.11524 c 0.0477,-0.0476 0.0968,-0.0871 0.14453,-0.13477 v 64.16602 c 2e-5,8.38844 1.13695,15.11813 3.29102,20.36523 0.73739,1.28016 1.58937,2.4395 2.56445,3.46875 3.62458,3.92663 8.21734,6.0553 13.75782,6.42383 0.5993,0.0286 1.18235,0.0711 1.8125,0.0723 6.05823,-0.0157 11.0359,-2.03742 14.95312,-6.0293 3.98025,-4.51096 6.09057,-11.66026 6.47656,-21.24219 v -3.72461 c 0.0471,0.0706 0.0974,0.13474 0.14453,0.20508 v -52.01953 -18.20508 -57.11523 c -0.0477,0.0476 -0.0968,0.0871 -0.14453,0.13476 V 173.1543 c 0,-8.38703 -1.13767,-15.11654 -3.29101,-20.36328 -0.73758,-1.28076 -1.58899,-2.44105 -2.56446,-3.47071 -3.6877,-3.99502 -8.37983,-6.1256 -14.05078,-6.4375 -0.50595,-0.0202 -0.99001,-0.0576 -1.51757,-0.0586 z" /><path
d="m 250.87109,300.95508 v -57.11524 c -0.79426,8.72199 -2.70097,16.33161 -5.73632,22.8125 -4.1111,8.77777 -9.55427,15.33464 -16.33204,19.66797 -6.77777,4.22221 -14.22287,6.33203 -22.33398,6.33203 -7.44443,0 -14.38954,-2.10982 -20.83398,-6.33203 -6.44444,-4.33333 -11.66602,-10.8902 -15.66602,-19.66797 -3.88888,-8.88888 -5.83398,-19.4438 -5.83398,-31.66601 0,-18.55553 4.05686,-32.88889 12.16796,-43 8.1111,-10.1111 18.27779,-15.16602 30.5,-15.16602 6,0 11.66668,1.22158 17,3.66602 5.33333,2.33333 10.05492,5.94509 14.16602,10.83398 4.11111,4.77777 7.33269,10.83269 9.66602,18.16602 1.65444,4.96334 2.70164,10.36877 3.23632,16.14648 v -52.01758 c -6.86066,-10.22654 -15.08687,-17.82908 -24.6875,-22.79492 -9.66665,-5.11111 -20.61177,-7.66601 -32.83398,-7.66601 -14.44443,0 -27.61113,3.60982 -39.5,10.83203 -11.77777,7.22221 -20.99936,17.77909 -27.66602,31.66797 -6.66666,13.77776 -10,30.33269 -10,49.66601 0,18.88887 3.27844,35.33204 9.83399,49.33203 6.66666,13.88887 15.77648,24.55556 27.33203,32 11.66665,7.44444 24.55687,11.16797 38.66797,11.16797 11.55555,0 22.05361,-2.27843 31.49804,-6.83398 9.50365,-4.64131 18.62118,-11.32337 27.35547,-20.03125 z"
style="-inkscape-font-specification:'Arial Rounded MT Bold, ';opacity:1;fill:#fabf9c;fill-opacity:1"
id="path13" /><path
d="m 294.01562,237.18555 v 57.11523 c 0.79416,-8.72281 2.70075,-16.33307 5.73633,-22.81445 4.1111,-8.77777 9.55426,-15.33269 16.33203,-19.66602 6.77777,-4.22221 14.22288,-6.33398 22.33399,-6.33398 7.44443,0 14.38954,2.11177 20.83398,6.33398 6.44444,4.33333 11.66602,10.88825 15.66602,19.66602 3.88888,8.88888 5.83398,19.4438 5.83398,31.66601 0,18.55553 -4.05687,32.88889 -12.16797,43 -8.1111,10.1111 -18.27779,15.16797 -30.5,15.16797 -6,0 -11.66668,-1.22353 -17,-3.66797 -5.33333,-2.33333 -10.05491,-5.94314 -14.16601,-10.83203 -4.11111,-4.77777 -7.33269,-10.83464 -9.66602,-18.16797 -1.6545,-4.96351 -2.70166,-10.36855 -3.23633,-16.14648 v 52.01953 c 6.86067,10.22655 15.08688,17.82909 24.6875,22.79492 9.66665,5.11111 20.61178,7.66602 32.83399,7.66602 14.44443,0 27.61113,-3.61178 39.5,-10.83399 11.77777,-7.22221 20.99936,-17.77713 27.66601,-31.66601 6.66667,-13.77776 10,-30.3327 10,-49.66602 10e-6,-18.88887 -3.27648,-35.33399 -9.83203,-49.33398 -6.66666,-13.88887 -15.77843,-24.55556 -27.33398,-32 -11.66665,-7.44444 -24.55492,-11.16602 -38.66602,-11.16602 -11.55555,0 -22.05557,2.27844 -31.5,6.83399 -9.50364,4.64131 -18.62118,11.32337 -27.35547,20.03125 z"
style="-inkscape-font-specification:'Arial Rounded MT Bold, ';opacity:1;fill:#a7a7fa;fill-opacity:1"
id="path15" /></g></g></svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

101
docs/index.md Normal file
View File

@@ -0,0 +1,101 @@
<style>
h1 { display: none; }
</style>
<p align="center">
<a href="https://qbot.botforge.biz"><img src="img/qbot.svg" alt="QBot"></a>
</p>
<p align="center">
<em>Telegram Bots Rapid Application Development (RAD) Framework.</em>
</p>
**QBot** is a library for fast development of Telegram bots and mini-apps following the **RAD (Rapid Application Development)** principle in a **declarative style**.
## Key Features
- **Automatic CRUD Interface Generation** Manage objects effortlessly without manual UI coding.
- **Built-in Field Editors** Includes text inputs, date/time pickers, boolean switches, enums, and entity selectors.
- **Advanced Pagination & Filtering** Easily organize and navigate large datasets.
- **Authentication & Authorization** Role-based access control for secure interactions.
- **Context Preservation** Store navigation stacks and user interaction states in the database.
- **Internationalization Support** Localizable UI and string fields for multilingual bots.
**QBot** powered by **[FastAPI](https://fastapi.tiangolo.com)**, **[SQLModel](https://sqlmodel.tiangolo.com)** & **[aiogram](https://aiogram.dev)** Leverage the full capabilities of these frameworks for high performance and flexibility.
## Benefits
- **Faster Development** Automate repetitive tasks and build bots in record time.
- **Highly Modular** Easily extend and customize functionality.
- **Less Code Duplication** Focus on core features while QBot handles the rest.
- **Enterprise-Grade Structure** Scalable, maintainable, and optimized for real-world usage.
## Example
```python
class AppEntity(BotEntity):
"""
BotEntity - business entity. Based on SQLModel, which provides ORM functionality,
basic CRUD functions and additional metadata, describing entities view and behaviour
"""
bot_entity_descriptor = Entity( # metadata attribute
name = "bot_entity", # Entity - descriptor for entity metadata
...
)
name: str # entity field with default sqlmodel's FieldInfo descriptor
# and default qbot's field descriptor
description: str | None = Field( # field with sqlmodel's descriptor
sa_type = String, index = True) # and default qbot's descriptor
age: int = EntityField( # field with qbot's descriptor
caption = "Age",
)
user_id: int | None = EntityField( # using both descriptors
sm_descriptor=Field(
sa_type=BigInteger,
foreign_key="user.id",
ondelete="RESTRICT",
nullable=True,
),
is_visible=False,
)
user: Optional[User] = EntityField( # using Relationship as sqlmodel descriptor
sm_descriptor=Relationship(
back_populates="entities",
sa_relationship_kwargs={
"lazy": "selectin",
"foreign_keys": "Entity.user_id",
}
),
caption="User",
)
app = QBotApp() # bot application based on FastAPI application
# providing Telegram API webhook handler
@app.command( # decorator for bot commands definition
name="menu",
caption="Main menu",
show_in_bot_commands=True, # shows command in bot's main menu
clear_navigation=True, # resets navigation stack between bot dialogues
)
async def menu(context: CommandCallbackContext):
context.message_text = "Main menu" # message text
context.keyboard_builder.row( # and buttons will be shown to user
InlineKeyboardButton(
text="Entities",
callback_data=ContextData( # callback query dataclass representing
command=CallbackCommand.ENTITY_LIST, # navigation context and its parameters
entity_name="bot_entity"
).pack(),
)
)
```
## Result
<iframe width="100%" height="691" src="https://www.youtube.com/embed/ptTnoppkYfM" title="QBot Framework The Open-Source RAD Tool for Telegram Bots" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
forced_change_

14
mkdocs.yml Normal file
View File

@@ -0,0 +1,14 @@
site_name: QBot Framework
site_url: https://qbot.botforge.biz
theme:
name: material
palette:
primary: #fabf9c
accent: #a7a7fa
font:
text: 'Roboto'
code: 'Roboto Mono'
logo: 'img/qbot_1_1.svg'
favicon: 'img/qbot_1_1.svg'
repo_name: botforge/qbot
repo_url: https://git.botforge.biz/botforge/qbot

29
pyproject.toml Normal file
View File

@@ -0,0 +1,29 @@
[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",
"mkdocs-material>=9.6.5",
"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",
]

View File

@@ -12,8 +12,10 @@ from .model.descriptors import (
EntityField as EntityField, EntityField as EntityField,
EntityForm as EntityForm, EntityForm as EntityForm,
EntityList as EntityList, EntityList as EntityList,
Filter as Filter,
EntityPermission as EntityPermission, EntityPermission as EntityPermission,
CommandCallbackContext as CommandCallbackContext, CommandCallbackContext as CommandCallbackContext,
EntityEventContext as EntityEventContext,
CommandButton as CommandButton, CommandButton as CommandButton,
FieldEditButton as FieldEditButton, FieldEditButton as FieldEditButton,
InlineButton as InlineButton, InlineButton as InlineButton,

View File

@@ -10,7 +10,6 @@ from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
logger.setLevel("DEBUG")
router = APIRouter() router = APIRouter()

View File

View File

@@ -1,3 +1,4 @@
from inspect import iscoroutinefunction
from aiogram import Router, F from aiogram import Router, F
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
@@ -12,7 +13,7 @@ from ..user_handlers.main import cammand_handler
from ....model import EntityPermission from ....model import EntityPermission
from ....model.user import UserBase from ....model.user import UserBase
from ....model.settings import Settings from ....model.settings import Settings
from ....model.descriptors import FieldDescriptor from ....model.descriptors import EntityEventContext, FieldDescriptor
from ....model.language import LanguageBase from ....model.language import LanguageBase
from ....auth import authorize_command from ....auth import authorize_command
from ....utils.main import ( from ....utils.main import (
@@ -281,6 +282,12 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
commit=True, commit=True,
) )
if entity_descriptor.on_created:
if iscoroutinefunction(entity_descriptor.on_created):
await entity_descriptor.on_created(new_entity, EntityEventContext(db_session=db_session, app=app))
else:
entity_descriptor.on_created(new_entity, EntityEventContext(db_session=db_session, app=app))
form_name = ( form_name = (
callback_data.form_params.split("&")[0] callback_data.form_params.split("&")[0]
if callback_data.form_params if callback_data.form_params
@@ -325,6 +332,13 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
setattr(entity, key, value) setattr(entity, key, value)
await db_session.commit() await db_session.commit()
await db_session.refresh(entity)
if entity_descriptor.on_updated:
if iscoroutinefunction(entity_descriptor.on_updated):
await entity_descriptor.on_updated(entity, EntityEventContext(db_session=db_session, app=app))
else:
entity_descriptor.on_updated(entity, EntityEventContext(db_session=db_session, app=app))
elif callback_data.context == CommandContext.COMMAND_FORM: elif callback_data.context == CommandContext.COMMAND_FORM:
clear_state(state_data=state_data) clear_state(state_data=state_data)

View File

@@ -1,3 +1,4 @@
from inspect import iscoroutinefunction
from aiogram import Router, F from aiogram import Router, F
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.types import CallbackQuery, InlineKeyboardButton
@@ -5,6 +6,8 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from qbot.model.descriptors import EntityEventContext
from ..context import ContextData, CallbackCommand from ..context import ContextData, CallbackCommand
from ....model.user import UserBase from ....model.user import UserBase
from ....model.settings import Settings from ....model.settings import Settings
@@ -47,10 +50,18 @@ async def entity_delete_callback(query: CallbackQuery, **kwargs):
) )
if callback_data.data == "yes": if callback_data.data == "yes":
await entity_descriptor.type_.remove(
entity = await entity_descriptor.type_.remove(
session=db_session, id=int(callback_data.entity_id), commit=True session=db_session, id=int(callback_data.entity_id), commit=True
) )
if entity_descriptor.on_deleted:
if iscoroutinefunction(entity_descriptor.on_created):
await entity_descriptor.on_deleted(entity, EntityEventContext(db_session=db_session, app=app))
else:
entity_descriptor.on_deleted(entity, EntityEventContext(db_session=db_session, app=app))
await route_callback(message=query, **kwargs) await route_callback(message=query, **kwargs)
elif not callback_data.data: elif not callback_data.data:

View File

@@ -31,7 +31,7 @@ class Config(BaseSettings):
def API_DOMAIN(self) -> str: def API_DOMAIN(self) -> str:
if self.ENVIRONMENT == "local": if self.ENVIRONMENT == "local":
return self.DOMAIN return self.DOMAIN
return f"api.{self.DOMAIN}" return f"{self.DOMAIN}"
@computed_field @computed_field
@property @property
@@ -39,7 +39,7 @@ class Config(BaseSettings):
if self.USE_NGROK: if self.USE_NGROK:
return self.NGROK_URL return self.NGROK_URL
return ( return (
f"{'http' if self.ENVIRONMENT == 'local' else 'https'}://{self.API_DOMAIN}" f"https://{self.API_DOMAIN}"
) )
API_PORT: int = 8000 API_PORT: int = 8000

View File

@@ -160,7 +160,7 @@ class EnumType(TypeDecorator):
impl = AutoString impl = AutoString
cache_ok = True cache_ok = True
def __init__(self, enum_type: BotEnum): def __init__(self, enum_type):
self._enum_type = enum_type self._enum_type = enum_type
super().__init__() super().__init__()

View File

@@ -163,6 +163,9 @@ class _BaseEntityDescriptor:
EntityPermission.DELETE_ALL: [RoleBase.SUPER_USER], EntityPermission.DELETE_ALL: [RoleBase.SUPER_USER],
} }
) )
on_created: Callable[["BotEntity", "EntityEventContext"], None] | None = None
on_deleted: Callable[["BotEntity", "EntityEventContext"], None] | None = None
on_updated: Callable[["BotEntity", "EntityEventContext"], None] | None = None
@dataclass(kw_only=True) @dataclass(kw_only=True)
@@ -197,6 +200,12 @@ class CommandCallbackContext[UT: UserBase]:
kwargs: dict[str, Any] = field(default_factory=dict) kwargs: dict[str, Any] = field(default_factory=dict)
@dataclass(kw_only=True)
class EntityEventContext:
db_session: AsyncSession
app: "QBotApp"
@dataclass(kw_only=True) @dataclass(kw_only=True)
class BotCommand: class BotCommand:
name: str name: str

141
tests/bot.py Normal file
View File

@@ -0,0 +1,141 @@
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(),
)
)

0
tests/test_bot.py Normal file
View File

1366
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff