Metadata-Version: 2.4
Name: novdev
Version: 0.0.3
Summary: Библиотека для создания ботов NovCord
Author: XenonE
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Framework :: AsyncIO
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: aiohttp>=3.9
Requires-Dist: aiofiles>=23.0
Dynamic: author
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# NovDev

**Библиотека для создания ботов NovCord**  
Автор: XenonE · Версия: 0.0.3

---

## Установка

```bash
pip install novdev
```

**Зависимости:** Python ≥ 3.10, aiohttp, aiofiles

---

## Быстрый старт

```python
import asyncio
from novdev import Bot, Dispatcher, Router, F, Command, InlineKeyboardBuilder

dp = Dispatcher()
router = Router()
dp.include_router(router)


@dp.on_startup
async def on_start(bot):
    await bot.set_my_commands([
        ("start", "Главное меню"),
        ("help",  "Помощь"),
    ])
    print("Бот готов!")


@router.message(Command("start"))
async def start(message):
    kb = InlineKeyboardBuilder()
    kb.button("Нажми меня", callback_data="hello")
    kb.button("Сайт", url="https://novcord.online")
    kb.adjust(2)
    await message.answer("Привет! Я бот на NovDev 🤖", reply_markup=kb.as_markup())


@router.callback_query(F.data == "hello")
async def on_hello(query):
    await query.answer("Принято!")
    await query.edit_message("Ты нажал кнопку 👍")


@router.message(F.text)
async def echo(message):
    await message.typing()
    await message.answer(f"Ты написал: {message.text}")


async def main():
    bot = Bot("ВСТАВЬ_ТОКЕН")
    await dp.start_polling(bot)


asyncio.run(main())
```

---

## Оглавление

1. [Bot](#bot)
2. [Dispatcher и Router](#dispatcher-и-router)
3. [Middleware](#middleware)
4. [Хуки on_startup / on_shutdown](#хуки)
5. [Фильтры](#фильтры)
6. [Magic-фильтр F](#magic-фильтр-f)
7. [Клавиатуры](#клавиатуры)
8. [FSM — машина состояний](#fsm)
9. [Объекты Message и CallbackQuery](#объекты)
10. [Исключения](#исключения)
11. [Что нового в 0.0.3](#changelog)

---

## Bot

```python
from novdev import Bot

bot = Bot(token="botId:secret", timeout=30, retry_on_error=True)
```

### Методы

| Метод | Описание |
|---|---|
| `await bot.get_me()` | Информация о боте → `BotInfo` |
| `await bot.send_message(chat_id, text, reply_markup?, reply_to_message_id?, parse_mode?)` | Отправить сообщение |
| `await bot.send_photo(chat_id, photo=?, file_url=?, caption?, reply_markup?)` | Отправить фото |
| `await bot.send_document(chat_id, document=?, file_url=?, caption?)` | Отправить файл |
| `await bot.send_typing(chat_id)` | Показать «печатает...» |
| `await bot.edit_message_text(chat_id, message_id, text, reply_markup?)` | Изменить сообщение |
| `await bot.delete_message(chat_id, message_id)` | Удалить сообщение |
| `await bot.forward_message(chat_id, from_chat_id, message_id)` | Переслать сообщение |
| `await bot.pin_message(chat_id, message_id)` | Закрепить сообщение |
| `await bot.unpin_message(chat_id, message_id)` | Открепить сообщение |
| `await bot.answer_callback_query(id, text?)` | Подтвердить кнопку |
| `await bot.set_my_commands([(cmd, desc)])` | Задать команды |
| `await bot.get_my_commands()` | Получить команды |
| `await bot.close()` | Закрыть сессию |

---

## Dispatcher и Router

```python
from novdev import Dispatcher, Router

dp = Dispatcher()          # главный диспетчер
router = Router("main")    # роутер
dp.include_router(router)  # подключить

# Можно подключать несколько роутеров
admin_router = Router("admin")
dp.include_router(admin_router)
```

### Запуск polling

```python
await dp.start_polling(
    bot,
    timeout=25,           # сек ожидания апдейтов
    poll_interval=0.1,    # пауза при пустом ответе
    retry_delay=3.0,      # задержка при ошибке сети
    max_retry_delay=60.0, # максимальная задержка
)
```

---

## Middleware

Middleware вызывается перед каждым хендлером. Удобно для логирования, антиспама и т.д.

```python
@dp.middleware
async def logger(obj, next_handler):
    print(f"Апдейт от {obj.chat_id}")
    await next_handler()   # передать дальше

@dp.middleware
async def anti_spam(obj, next_handler):
    # можно не вызывать next_handler() — хендлер не выполнится
    if is_spammer(obj.chat_id):
        return
    await next_handler()
```

---

## Хуки

```python
# На диспетчере
@dp.on_startup
async def setup(bot):
    await bot.set_my_commands([("start", "Старт")])

@dp.on_shutdown
async def cleanup(bot):
    print("Бот остановлен")

# На роутере
@router.on_startup
async def router_ready(bot):
    print("Роутер готов")
```

Хуки без аргументов тоже работают:

```python
@dp.on_startup
async def ping():
    print("Запуск!")
```

---

## Фильтры

```python
from novdev import Command, TextFilter, CallbackDataFilter, FromUserFilter, IsBot, HasText, HasFile
```

### Command

```python
@router.message(Command("start"))
@router.message(Command("help", "h"))         # несколько алиасов
# [NEW] автоматически убирает @botname: /start@MyBot → start
```

### TextFilter

```python
@router.message(TextFilter("привет"))
@router.message(TextFilter(startswith="!"))
@router.message(TextFilter(contains="слово"))
@router.message(TextFilter(regex=r"\d{4}"))
@router.message(TextFilter(endswith="?", ignore_case=False))
```

### CallbackDataFilter

```python
@router.callback_query(CallbackDataFilter("confirm"))
@router.callback_query(CallbackDataFilter(startswith="menu:"))
@router.callback_query(CallbackDataFilter(regex=r"^item:\d+$"))
```

### Остальные фильтры

```python
@router.message(FromUserFilter("id1", "id2"))  # только эти пользователи
@router.message(IsBot(False))                  # [NEW] только не-боты
@router.message(HasText())                     # [NEW] есть текст
@router.message(HasFile())                     # [NEW] есть файл/фото
```

### Комбинирование

```python
@router.message(Command("start") | TextFilter("старт"))
@router.message(HasText() & ~IsBot())
@router.message(TextFilter(startswith="!") & FromUserFilter("admin_id"))
```

---

## Magic-фильтр F

```python
from novdev import F

@router.message(F.text == "привет")
@router.message(F.text.startswith("!"))
@router.message(F.text.contains("help"))
@router.message(F.text.matches(r"\d+"))
@router.message(F.text.endswith("?"))
@router.message(F.text)              # любой непустой текст

@router.callback_query(F.data == "confirm")
@router.callback_query(F.data.startswith("action:"))
@router.callback_query(F.data != "cancel")
```

---

## Клавиатуры

```python
from novdev import InlineKeyboardBuilder

kb = InlineKeyboardBuilder()
kb.button("Да",   callback_data="yes")
kb.button("Нет",  callback_data="no")
kb.button("Сайт", url="https://novcord.online")
kb.adjust(2)   # по 2 кнопки в ряд

await message.answer("Выбери:", reply_markup=kb.as_markup())
```

Цепочка:

```python
markup = (
    InlineKeyboardBuilder()
    .button("A", callback_data="a")
    .button("B", callback_data="b")
    .button("C", callback_data="c")
    .adjust(2, 1)
    .as_markup()
)
```

Копирование builder-а:

```python
base = InlineKeyboardBuilder().button("Назад", callback_data="back")
kb1 = base.copy().button("Далее", callback_data="next").adjust(2)
kb2 = base.copy().button("Отмена", callback_data="cancel").adjust(2)
```

---

## FSM

```python
from novdev import StatesGroup, State, FSMContext

class OrderForm(StatesGroup):
    product = State()
    address = State()
    confirm = State()
```

```python
@router.message(Command("order"))
async def start_order(message, state: FSMContext):
    await state.set(OrderForm.product)
    await message.answer("Что заказать?")


@router.message(OrderForm.product)
async def ask_address(message, state: FSMContext):
    await state.update(product=message.text)
    await state.set(OrderForm.address)
    await message.answer("Адрес доставки?")


@router.message(OrderForm.address)
async def ask_confirm(message, state: FSMContext):
    await state.update(address=message.text)
    data = await state.get_data()

    kb = InlineKeyboardBuilder()
    kb.button("✅ Подтвердить", callback_data="order_ok")
    kb.button("❌ Отмена",      callback_data="order_cancel")
    kb.adjust(2)

    await state.set(OrderForm.confirm)
    await message.answer(
        f"Заказ: {data['product']}\nАдрес: {data['address']}",
        reply_markup=kb.as_markup(),
    )


@router.callback_query(OrderForm.confirm, F.data == "order_ok")
async def finish(query, state: FSMContext):
    data = await state.get_data()
    await query.answer("Принято!")
    await query.edit_message(f"✅ Заказ оформлен: {data['product']}")
    await state.clear()
```

### FSMContext API

| Метод | Описание |
|---|---|
| `await state.get()` | Текущее состояние |
| `await state.set(State)` | Установить состояние |
| `await state.clear()` | Сбросить всё |
| `await state.get_data()` | Получить все данные |
| `await state.get_value("key", default)` | **[NEW]** Получить одно поле |
| `await state.update(key=val)` | Добавить данные |
| `await state.set_data(dict)` | Заменить данные |

---

## Объекты

### Message

```python
message.message_id       # str
message.text             # str | None
message.text_lower       # str (в нижнем регистре)
message.from_user        # User
message.chat_id          # str
message.date             # int
message.file_url         # str | None
message.file_name        # str | None

await message.answer("текст", parse_mode="Markdown")
await message.reply("реплай")
await message.edit("новый текст")
await message.delete()
await message.typing()    # показать «печатает...»
await message.pin()       # закрепить
```

### CallbackQuery

```python
query.id          # str
query.data        # str | None
query.from_user   # User
query.chat_id     # str

await query.answer("текст")
await query.answer()
await query.edit_message("новый текст", reply_markup=...)
```

### User

```python
user.id           # str
user.username     # str | None
user.first_name   # str | None
user.is_bot       # bool
user.mention      # "@username" или first_name
user.full_name    # first_name или username или id
```

---

## Исключения

```python
from novdev import NovDevAPIError, NovDevNetworkError

try:
    await bot.send_message(chat_id, text)
except NovDevAPIError as e:
    print(e.method, e.code, e.description)
except NovDevNetworkError as e:
    print("Нет соединения:", e)
```

---

## Changelog

### 0.0.3
- **[FIX]** `Router.on_startup` / `on_shutdown` — хуки больше не перезаписываются (теперь список)
- **[FIX]** `asyncio.ensure_future` заменён на `loop.create_task` — не падает вне event loop
- **[FIX]** Хуки роутеров теперь выполняются при `start_polling` (раньше игнорировались)
- **[FIX]** Хуки принимают как `async def f(bot)` так и `async def f()` без аргументов
- **[FIX]** Фильтр `Command` убирает `@botname` из команды
- **[FIX]** Ошибка в фильтре больше не роняет всё — возвращает `False`
- **[NEW]** Middleware поддержка (`@dp.middleware`)
- **[NEW]** Фильтры `IsBot`, `HasText`, `HasFile`
- **[NEW]** `FSMContext.get_value(key, default)` — получить одно поле
- **[NEW]** `MemoryStorage.stats()` — статистика активных пользователей
- **[NEW]** `InlineKeyboardBuilder.copy()` — копия builder-а
- **[NEW]** `parse_mode` в `send_message` и `message.answer`
- **[NEW]** Все классы имеют `__repr__`
- **[NEW]** Все основные классы экспортируются напрямую из `novdev`

### 0.0.2
- Исправлены краши при не-JSON ответах сервера
- Добавлены `send_typing`, `pin_message`, `forward_message`
- Экспоненциальный backoff при ошибках сети
- `message.text_lower`, `message.typing()`, `message.pin()`

### 0.0.1
- Первый релиз

---

## Структура пакета

```
novdev/
├── __init__.py        — весь публичный API
├── bot.py             — HTTP-клиент NovCord
├── dispatcher.py      — Router, Dispatcher, polling
├── exceptions.py      — исключения
├── filters/           — Command, TextFilter, F, IsBot, HasText...
├── keyboard/          — InlineKeyboardBuilder
├── fsm/               — StatesGroup, State, FSMContext, MemoryStorage
└── types/             — Message, CallbackQuery, Update, User, Chat
```

---

*NovDev v0.0.3 · XenonE*
