botty-framework 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,535 @@
1
+ Metadata-Version: 2.3
2
+ Name: botty-framework
3
+ Version: 0.0.1
4
+ Summary: Botty brings the elegance of FastAPI's dependency injection and type hints to Telegram bot development. Write clean, testable bot code with automatic dependency resolution, built-in message registry, and a developer-friendly API.
5
+ Author: melaveetha
6
+ Author-email: melaveetha <melaveetha@gmail.com>
7
+ Requires-Dist: loguru>=0.7.3
8
+ Requires-Dist: python-telegram-bot>=22.6
9
+ Requires-Dist: sqlmodel>=0.0.32
10
+ Requires-Python: >=3.13
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Botty
14
+
15
+ **A FastAPI-inspired modern framework for building Telegram bots with Python**
16
+
17
+ Botty is the elegance of FastAPI's dependency injection and type hints to Telegram bot development (based on `python-telegram-bot`).
18
+ Write clean, code with automatic dependency resolution and a developer-friendly API.
19
+
20
+
21
+ ```python
22
+ from botty import Router, HandlerResponse, Context, Answer, Update
23
+
24
+ router = Router()
25
+
26
+ @router.command("start")
27
+ async def start_handler(
28
+ update: Update,
29
+ context: Context,
30
+ user_repo: UserRepository # Auto-injected!
31
+ ) -> HandlerResponse:
32
+ user = user_repo.get_or_create(update.effective_user.id)
33
+ yield Answer(text=f"Welcome back, {user.name}! πŸ‘‹")
34
+ ```
35
+
36
+ ## 🎯 Main Idea
37
+
38
+ Traditional Telegram bot frameworks require a lot of boilerplate and make it hard to:
39
+ - Share code between handlers (no clean dependency injection)
40
+ - Track and edit messages you've sent
41
+ - Write testable code (everything is tightly coupled)
42
+ - Get type hints and IDE support
43
+
44
+ **Botty uses** bringing FastAPI's best ideas to Telegram bots:
45
+ - **Dependency Injection** - Repositories and services are automatically injected
46
+ - **Type Hints** - Full type safety with IDE autocomplete
47
+ - **Async Generators** - Handlers yield responses, making sending and editing messages trivial
48
+
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ pip install botty-framework
54
+ ```
55
+ or
56
+ ```bash
57
+ uv add botty-framework
58
+ ```
59
+
60
+
61
+ ## 🌟 Features
62
+
63
+ ### FastAPI-Style Dependency Injection
64
+
65
+ ```python
66
+ async def get_repository(update: Update, context: Context, session: Session): # ← Session handled automatically
67
+ return UserRepository(session)
68
+
69
+ async def get_settings(update: Update, context: Context): # Database session NOT created here
70
+ return SettingsService()
71
+
72
+ UserRepositoryDep = Annotated[UserRepository, Depends(get_repository)]
73
+ SettingsServiceDep = Annotated[SettingsService, Depends(get_settings)]
74
+
75
+ @router.command("profile")
76
+ async def show_profile(
77
+ update: Update,
78
+ context: Context,
79
+ user_repo: UserRepositoryDep, # Injected automatically
80
+ settings_svc: SettingsServiceDep # Services too!
81
+ ) -> HandlerResponse:
82
+ user = user_repo.get(update.effective_user.id)
83
+ settings = settings_svc.get_user_settings(user.id)
84
+
85
+ yield Answer(f"πŸ‘€ {user.name}\nβš™οΈ Theme: {settings.theme}")
86
+ ```
87
+
88
+
89
+ ### Message Registry & Smart Editing
90
+
91
+ Track messages and edit them later by key, handler name, or automatically:
92
+
93
+ ```python
94
+ @router.command("countdown")
95
+ async def countdown_handler(
96
+ update: Update,
97
+ context: Context
98
+ ) -> HandlerResponse:
99
+ # Send message with a key
100
+ yield Answer("Starting in 3...", message_key="countdown")
101
+
102
+ await asyncio.sleep(1)
103
+
104
+ # Edit by key
105
+ yield EditAnswer("2...", message_key="countdown")
106
+
107
+ await asyncio.sleep(1)
108
+ yield EditAnswer("1...", message_key="countdown")
109
+
110
+ await asyncio.sleep(1)
111
+ yield EditAnswer("GO! πŸš€", message_key="countdown")
112
+ ```
113
+
114
+ ### Clean Handler Syntax
115
+
116
+ Handlers are async generators that yield responses:
117
+
118
+ ```python
119
+ @router.command("weather")
120
+ async def weather_handler(
121
+ update: Update,
122
+ context: Context,
123
+ weather_api: WeatherService
124
+ ) -> HandlerResponse:
125
+ city = " ".join(context.args)
126
+
127
+ # Show loading state
128
+ yield Answer("πŸ” Fetching weather...", message_key="weather")
129
+
130
+ # Get data
131
+ data = await weather_api.get_current(city)
132
+
133
+ # Update message
134
+ yield EditAnswer(
135
+ f"🌀️ {city}: {data.temp}°C, {data.condition}",
136
+ message_key="weather"
137
+ )
138
+ ```
139
+
140
+ ### Repository Pattern
141
+
142
+ Built-in support for the repository pattern with SQLModel:
143
+
144
+ ```python
145
+ from botty import BaseRepository
146
+ from sqlmodel import Session, select
147
+
148
+ class UserRepository(BaseRepository[User]):
149
+ model = User
150
+
151
+ def get_by_telegram_id(self, telegram_id: int) -> User | None:
152
+ statement = select(User).where(User.telegram_id == telegram_id)
153
+ return self.session.exec(statement).first()
154
+
155
+ def get_active_users(self) -> list[User]:
156
+ statement = select(User).where(User.is_active == True)
157
+ return list(self.session.exec(statement).all())
158
+
159
+
160
+ UserRepositoryDep = Annotated[UserRepository, Depends(get_repository)]
161
+
162
+ # Automatically injected with proper session management!
163
+ @router.command("stats")
164
+ async def stats_handler(
165
+ update: Update,
166
+ context: Context,
167
+ user_repo: UserRepositoryDep
168
+ ) -> HandlerResponse:
169
+ active = user_repo.get_active_users()
170
+ yield answer(f"πŸ“Š Active users: {len(active)}")
171
+ ```
172
+
173
+
174
+ ### Type Safety & Validation
175
+
176
+ Handlers are validated at registration time with helpful error messages:
177
+
178
+ ```python
179
+ @router.command("test")
180
+ def wrong_handler(update: Update, context: Context): # ❌ Forgot 'async'
181
+ yield Answer("Hi")
182
+
183
+ # Error: Handler must be an async function (use 'async def')
184
+ # πŸ’‘ Suggestion: Change 'def wrong_handler(...)' to 'async def wrong_handler(...)'
185
+ ```
186
+
187
+ ### Built-in Database Support
188
+
189
+ SQLModel integration with automatic session management:
190
+
191
+ ```python
192
+ from botty import AppBuilder, SQLiteProvider
193
+ from sqlmodel import SQLModel, Field
194
+
195
+ # Define your models
196
+ class User(SQLModel, table=True):
197
+ id: int | None = Field(default=None, primary_key=True)
198
+ telegram_id: int = Field(unique=True)
199
+ name: str
200
+ created_at: datetime = Field(default_factory=lambda: datetime.now(datetime.UTC))
201
+
202
+ # Build app with database
203
+ app = (
204
+ AppBuilder(token="YOUR_TOKEN")
205
+ .database(SQLiteProvider("bot.db"))
206
+ .build()
207
+ )
208
+ ```
209
+
210
+ ## 🎨 Inspirations
211
+
212
+ ### FastAPI
213
+ Botty is heavily inspired by FastAPI's elegant dependency injection system:
214
+ - Type hints for automatic injection
215
+ - Clean decorator-based routing
216
+ - Dependency resolution with `Depends()`
217
+
218
+ ### Django
219
+ Repository pattern and clean architecture:
220
+ - Repository layer for data access
221
+ - Service layer for business logic
222
+
223
+
224
+ ## πŸ“š Complete Example
225
+
226
+ ### Project Structure
227
+
228
+ Botty auto-discovers handlers from project structure:
229
+
230
+ ```
231
+ todo_bot/
232
+ β”œβ”€β”€ src/
233
+ β”‚ β”œβ”€β”€ handlers/
234
+ β”‚ β”‚ β”œβ”€β”€ __init__.py
235
+ β”‚ β”‚ β”œβ”€β”€ start.py # Start command handlers
236
+ β”‚ β”‚ β”œβ”€β”€ todos.py # Todo-related handlers
237
+ β”‚ β”‚ └── settings.py # Settings handlers
238
+ β”‚ β”œβ”€β”€ repositories/
239
+ β”‚ β”‚ β”œβ”€β”€ __init__.py
240
+ β”‚ β”‚ β”œβ”€β”€ user_repository.py
241
+ β”‚ β”‚ └── todo_repository.py
242
+ β”‚ β”œβ”€β”€ services/
243
+ β”‚ β”‚ β”œβ”€β”€ __init__.py
244
+ β”‚ β”‚ └── notification_service.py
245
+ β”‚ β”œβ”€β”€ models/
246
+ β”‚ β”‚ β”œβ”€β”€ __init__.py
247
+ β”‚ β”‚ β”œβ”€β”€ user.py
248
+ β”‚ β”‚ └── todo.py
249
+ β”œβ”€β”€ main.py # App entry point
250
+ └── pyproject.toml
251
+ ```
252
+
253
+ ### Implementation
254
+
255
+ Here's a full todo bot showing all features:
256
+
257
+ ```python
258
+ from datetime import datetime
259
+ from telegram import InlineKeyboardButton, InlineKeyboardMarkup
260
+ from sqlmodel import SQLModel, Field, Session, select
261
+ from botty import (
262
+ AppBuilder,
263
+ Router,
264
+ Context,
265
+ HandlerResponse,
266
+ BaseRepository,
267
+ Answer,
268
+ EditAnswer,
269
+ SQLiteProvider,
270
+ Update,
271
+ Depends
272
+ )
273
+
274
+ # ============================================================================
275
+ # Models
276
+ # ============================================================================
277
+
278
+ class Todo(SQLModel, table=True):
279
+ id: int | None = Field(default=None, primary_key=True)
280
+ user_id: int
281
+ task: str
282
+ completed: bool = False
283
+ created_at: datetime = Field(default_factory=lambda: datetime.now(datetime.UTC))
284
+
285
+
286
+ # ============================================================================
287
+ # Repositories
288
+ # ============================================================================
289
+
290
+ class TodoRepository(BaseRepository[Todo]):
291
+ model = Todo
292
+
293
+ def get_by_user(self, user_id: int) -> list[Todo]:
294
+ """Get all todos for a user."""
295
+ statement = select(Todo).where(Todo.user_id == user_id)
296
+ return list(self.session.exec(statement).all())
297
+
298
+ def get_pending(self, user_id: int) -> list[Todo]:
299
+ """Get incomplete todos."""
300
+ statement = (
301
+ select(Todo)
302
+ .where(Todo.user_id == user_id)
303
+ .where(Todo.completed == False)
304
+ )
305
+ return list(self.session.exec(statement).all())
306
+
307
+ def toggle_complete(self, todo_id: int) -> Todo | None:
308
+ """Toggle completion status."""
309
+ todo = self.get(todo_id)
310
+ if todo:
311
+ todo.completed = not todo.completed
312
+ self.update(todo)
313
+ return todo
314
+
315
+ def get_todo_repo(update: Update, context: Context, session: Session):
316
+ return TodoRepository(session)
317
+
318
+ TodoRepositoryDep = Annotated[TodoRepository, Depends(get_todo_repo)]
319
+
320
+ # ============================================================================
321
+ # Handlers
322
+ # ============================================================================
323
+
324
+ router = Router()
325
+
326
+ @router.command("start")
327
+ async def start_handler(
328
+ update: Update,
329
+ context: Context
330
+ ) -> HandlerResponse:
331
+ """Welcome message."""
332
+ yield answer(
333
+ "πŸ‘‹ Welcome to Todo Bot!\n\n"
334
+ "Commands:\n"
335
+ "/add <task> - Add a new todo\n"
336
+ "/list - Show all todos\n"
337
+ "/pending - Show incomplete todos"
338
+ )
339
+
340
+
341
+ @router.command("add")
342
+ async def add_todo_handler(
343
+ update: Update,
344
+ context: Context,
345
+ todo_repo: TodoRepositoryDep # Auto-injected!
346
+ ) -> HandlerResponse:
347
+ """Add a new todo."""
348
+ if not context.args:
349
+ yield answer("❌ Usage: /add <task description>")
350
+ return
351
+
352
+ task = " ".join(context.args)
353
+ todo = Todo(
354
+ user_id=update.effective_user.id,
355
+ task=task
356
+ )
357
+
358
+ todo_repo.create(todo)
359
+ yield answer(f"βœ… Added: {task}")
360
+
361
+
362
+ @router.command("list")
363
+ async def list_todos_handler(
364
+ update: Update,
365
+ context: Context,
366
+ todo_repo: TodoRepositoryDep # Auto-injected!
367
+ ) -> HandlerResponse:
368
+ """List all todos."""
369
+ todos = todo_repo.get_by_user(update.effective_user.id)
370
+
371
+ if not todos:
372
+ yield answer("πŸ“ You have no todos!\nUse /add to create one.")
373
+ return
374
+
375
+ # Build message with buttons
376
+ text = "πŸ“ Your todos:\n\n"
377
+ buttons = []
378
+
379
+ for i, todo in enumerate(todos, 1):
380
+ status = "βœ…" if todo.completed else "⏺️"
381
+ text += f"{i}. {status} {todo.task}\n"
382
+
383
+ button_text = "βœ… Complete" if not todo.completed else "↩️ Undo"
384
+ buttons.append([
385
+ InlineKeyboardButton(
386
+ f"{i}. {button_text}",
387
+ callback_data=f"toggle_{todo.id}"
388
+ )
389
+ ])
390
+
391
+ keyboard = InlineKeyboardMarkup(buttons)
392
+
393
+ yield answer(
394
+ text=text,
395
+ reply_markup=keyboard,
396
+ message_key="todo_list"
397
+ )
398
+
399
+
400
+ @router.command("pending")
401
+ async def pending_todos_handler(
402
+ update: Update,
403
+ context: Context,
404
+ todo_repo: TodoRepositoryDep # Auto-injected!
405
+ ) -> HandlerResponse:
406
+ """List incomplete todos."""
407
+ todos = todo_repo.get_pending(update.effective_user.id)
408
+
409
+ if not todos:
410
+ yield answer("πŸŽ‰ All done! No pending todos.")
411
+ return
412
+
413
+ text = "⏺️ Pending todos:\n\n"
414
+ for i, todo in enumerate(todos, 1):
415
+ text += f"{i}. {todo.task}\n"
416
+
417
+ yield answer(text)
418
+
419
+
420
+ @router.callback_query(r"^toggle_(\d+)")
421
+ async def toggle_todo_handler(
422
+ update: Update,
423
+ context: Context,
424
+ todo_repo: TodoRepositoryDep # Auto-injected!
425
+ ) -> HandlerResponse:
426
+ """Toggle todo completion."""
427
+ query = update.callback_query
428
+ await query.answer()
429
+
430
+ # Extract todo ID from callback data
431
+ todo_id = int(query.data.split("_")[1])
432
+
433
+ # Toggle the todo
434
+ todo = todo_repo.toggle_complete(todo_id)
435
+
436
+ if not todo:
437
+ yield edit_answer("❌ Todo not found")
438
+ return
439
+
440
+ # Refresh the list
441
+ todos = todo_repo.get_by_user(update.effective_user.id)
442
+
443
+ text = "πŸ“ Your todos:\n\n"
444
+ buttons = []
445
+
446
+ for i, t in enumerate(todos, 1):
447
+ status = "βœ…" if t.completed else "⏺️"
448
+ text += f"{i}. {status} {t.task}\n"
449
+
450
+ button_text = "βœ… Complete" if not t.completed else "↩️ Undo"
451
+ buttons.append([
452
+ InlineKeyboardButton(
453
+ f"{i}. {button_text}",
454
+ callback_data=f"toggle_{t.id}"
455
+ )
456
+ ])
457
+
458
+ keyboard = InlineKeyboardMarkup(buttons)
459
+
460
+ yield edit_answer(
461
+ text=text,
462
+ reply_markup=keyboard,
463
+ message_key="todo_list"
464
+ )
465
+
466
+
467
+ # ============================================================================
468
+ # Application Setup
469
+ # ============================================================================
470
+
471
+ if __name__ == "__main__":
472
+ app = (
473
+ AppBuilder(token="YOUR_BOT_TOKEN_HERE")
474
+ .database(SQLiteProvider("todos.db"))
475
+ .build()
476
+ )
477
+
478
+ app.launch()
479
+ ```
480
+
481
+ ## πŸ” Comparison
482
+
483
+ ### vs. python-telegram-bot
484
+
485
+ **python-telegram-bot:**
486
+ ```python
487
+ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
488
+ # Manual session management
489
+ session = Session(engine)
490
+ try:
491
+ user_repo = UserRepository(session)
492
+ user = user_repo.get(update.effective_user.id)
493
+ await update.message.reply_text(f"Hello {user.name}")
494
+ finally:
495
+ session.close()
496
+
497
+ application.add_handler(CommandHandler("start", start))
498
+ ```
499
+
500
+ **Botty:**
501
+ ```python
502
+ @router.command("start")
503
+ async def start_handler(
504
+ update: Update,
505
+ context: Context,
506
+ user_repo: UserRepositoryDep # Auto-injected!
507
+ ) -> HandlerResponse:
508
+ user = user_repo.get(update.effective_user.id)
509
+ yield Answer(f"Hello {user.name}")
510
+ # Session managed automatically
511
+ ```
512
+
513
+ ## πŸ“„ License
514
+
515
+ MIT License - see LICENSE file for details
516
+
517
+ ## Acknowledgments
518
+
519
+ - [FastAPI](https://fastapi.tiangolo.com/) - For the inspiration
520
+ - [python-telegram-bot](https://python-telegram-bot.org/) - For the excellent Telegram wrapper
521
+ - [SQLModel](https://sqlmodel.tiangolo.com/) - For the ORM integration
522
+
523
+ ## πŸ—ΊοΈ Roadmap
524
+
525
+ - [ ] More database providers (PostgreSQL, MySQL)
526
+ - [ ] Conversation state management
527
+ - [ ] Admin panel
528
+ - [ ] CLI for scaffolding projects
529
+ - [ ] Built-in middleware support
530
+ - [ ] Metrics and monitoring
531
+ - [ ] Plugin system
532
+
533
+ ---
534
+
535
+ *Botty - Because building bots should be as elegant as using them*