- Added AGENTS.md for AI agent documentation and maintainers, outlining project structure and conventions. - Updated CONTRIBUTING.md to specify that all project documentation must be in English, including README and docstrings. - Enhanced README.md to reference documentation guidelines and the new AGENTS.md file. - Cleaned up .gitignore by removing unnecessary entries for cursor-related files. - Introduced new .cursor rules for backend, frontend, project architecture, and testing to standardize development practices.
151 lines
5.5 KiB
Plaintext
151 lines
5.5 KiB
Plaintext
---
|
|
description: Rules for working with the Python backend (duty_teller/)
|
|
globs:
|
|
- duty_teller/**
|
|
- alembic/**
|
|
- tests/**
|
|
---
|
|
|
|
# Backend — Python
|
|
|
|
## Package layout
|
|
|
|
```
|
|
duty_teller/
|
|
├── main.py / run.py # Entry point: bot + uvicorn
|
|
├── config.py # Settings from env vars (python-dotenv)
|
|
├── cache.py # TTL caches with pattern invalidation
|
|
├── api/ # FastAPI app, routes, auth, ICS endpoints
|
|
│ ├── app.py # FastAPI app creation, route registration, static mount
|
|
│ ├── dependencies.py # get_db_session, require_miniapp_username, get_validated_dates
|
|
│ ├── telegram_auth.py # initData HMAC validation
|
|
│ ├── calendar_ics.py # External ICS fetch & parse
|
|
│ └── personal_calendar_ics.py
|
|
├── db/ # Database layer
|
|
│ ├── models.py # SQLAlchemy ORM models (Base)
|
|
│ ├── repository.py # CRUD functions (receive Session)
|
|
│ ├── schemas.py # Pydantic response schemas
|
|
│ └── session.py # session_scope, get_engine, get_session
|
|
├── handlers/ # Telegram bot command/message handlers
|
|
│ ├── commands.py # /start, /help, /set_phone, /calendar_link, /set_role
|
|
│ ├── common.py # is_admin_async, invalidate_is_admin_cache
|
|
│ ├── errors.py # Global error handler
|
|
│ ├── group_duty_pin.py # Pinned duty message in group chats
|
|
│ └── import_duty_schedule.py
|
|
├── i18n/ # Translations
|
|
│ ├── core.py # get_lang, t()
|
|
│ ├── lang.py # normalize_lang
|
|
│ └── messages.py # MESSAGES dict (ru/en)
|
|
├── importers/ # File parsers
|
|
│ └── duty_schedule.py # parse_duty_schedule
|
|
├── services/ # Business logic (receives Session)
|
|
│ ├── import_service.py # run_import
|
|
│ └── group_duty_pin_service.py
|
|
└── utils/ # Shared helpers
|
|
├── dates.py
|
|
├── handover.py
|
|
├── http_client.py
|
|
└── user.py
|
|
```
|
|
|
|
## Imports
|
|
|
|
- Use absolute imports from the `duty_teller` package: `from duty_teller.db.repository import get_or_create_user`.
|
|
- Never import handler modules from services or repository — dependency flows
|
|
downward: handlers → services → repository → models.
|
|
|
|
## DB access pattern
|
|
|
|
Handlers are async; SQLAlchemy sessions are synchronous. Two patterns:
|
|
|
|
### In bot handlers — `session_scope` + `run_in_executor`
|
|
|
|
```python
|
|
def do_work():
|
|
with session_scope(config.DATABASE_URL) as session:
|
|
# synchronous DB code here
|
|
...
|
|
|
|
await asyncio.get_running_loop().run_in_executor(None, do_work)
|
|
```
|
|
|
|
### In FastAPI endpoints — dependency injection
|
|
|
|
```python
|
|
@router.get("/api/duties")
|
|
def get_duties(session: Session = Depends(get_db_session)):
|
|
...
|
|
```
|
|
|
|
`get_db_session` yields a `session_scope` context — FastAPI closes it after the request.
|
|
|
|
## Handler patterns
|
|
|
|
### Admin-only commands
|
|
|
|
```python
|
|
if not await is_admin_async(update.effective_user.id):
|
|
await update.message.reply_text(t(lang, "import.admin_only"))
|
|
return
|
|
```
|
|
|
|
`is_admin_async` is cached for 60 s and invalidated on role changes.
|
|
|
|
### i18n in handlers
|
|
|
|
```python
|
|
lang = get_lang(update.effective_user)
|
|
text = t(lang, "start.greeting")
|
|
```
|
|
|
|
`t(lang, key, **kwargs)` substitutes `{placeholders}` and falls back to English → raw key.
|
|
|
|
### Error handling
|
|
|
|
The global `error_handler` (registered via `app.add_error_handler`) logs the
|
|
exception and sends a generic localized error reply.
|
|
|
|
## Service layer
|
|
|
|
- Services receive a `Session` — they never open their own.
|
|
- Services call repository functions, never handler code.
|
|
- After mutations that affect cached data, call `invalidate_duty_related_caches()`.
|
|
|
|
## Cache invalidation
|
|
|
|
Three TTL caches in `cache.py`:
|
|
|
|
| Cache | TTL | Invalidation trigger |
|
|
|-------|-----|---------------------|
|
|
| `ics_calendar_cache` | 600 s | After duty import |
|
|
| `duty_pin_cache` | 90 s | After duty import |
|
|
| `is_admin_cache` | 60 s | After `set_user_role` |
|
|
|
|
- `invalidate_duty_related_caches()` clears ICS and pin caches (call after any duty mutation).
|
|
- `invalidate_is_admin_cache(telegram_user_id)` clears a single admin entry.
|
|
- Pattern invalidation: `cache.invalidate_pattern(key_prefix_tuple)`.
|
|
|
|
## Alembic migrations
|
|
|
|
- Config in `pyproject.toml` under `[tool.alembic]`, script location: `alembic/`.
|
|
- **Naming convention:** `NNN_description.py` — sequential zero-padded number (`001`, `002`, …).
|
|
- Revision IDs are the string number: `revision = "008"`, `down_revision = "007"`.
|
|
- Run: `alembic -c pyproject.toml upgrade head` / `downgrade -1`.
|
|
- `entrypoint.sh` runs `alembic upgrade head` before starting the app in Docker.
|
|
|
|
## Configuration
|
|
|
|
- All config comes from environment variables, loaded with `python-dotenv`.
|
|
- `duty_teller/config.py` exposes module-level constants (`BOT_TOKEN`, `DATABASE_URL`, etc.)
|
|
built from `Settings.from_env()`.
|
|
- Never hardcode secrets or URLs — always use `config.*` constants.
|
|
- Key variables: `BOT_TOKEN`, `DATABASE_URL`, `MINI_APP_BASE_URL`, `HTTP_HOST`, `HTTP_PORT`,
|
|
`ADMIN_USERNAMES`, `ALLOWED_USERNAMES`, `DUTY_DISPLAY_TZ`, `DEFAULT_LANGUAGE`.
|
|
|
|
## Code style
|
|
|
|
- Formatter: Black (line-length 120).
|
|
- Linter: Ruff (`ruff check duty_teller tests`).
|
|
- Follow PEP 8 and Google Python Style Guide for docstrings.
|
|
- Max line length: 120 characters.
|