Metadata-Version: 2.4
Name: novdev
Version: 0.0.5
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
Provides-Extra: sqlite
Requires-Dist: aiosqlite>=0.19; extra == "sqlite"
Provides-Extra: full
Requires-Dist: aiosqlite>=0.19; extra == "full"
Dynamic: author
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: provides-extra
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# NovDev

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

---

## Установка

```bash
pip install novdev              # базовая
pip install novdev[sqlite]      # + SQLite хранилище
pip install novdev[full]        # всё включено
pip install aiosqlite           # если используешь novsqlite/SQLiteStorage
```

---

## Пакеты в комплекте

| Пакет | Описание |
|---|---|
| `novdev` | Основная библиотека (бот, роутер, фильтры, FSM, клавиатуры) |
| `novhttp` | Async HTTP клиент для запросов к внешним API |
| `novsqlite` | Async SQLite база данных для хранения данных бота |

---

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

```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 setup(bot):
    await bot.set_my_commands([("start", "Старт"), ("help", "Помощь")])

@router.message(Command("start"))
async def start(message):
    kb = InlineKeyboardBuilder()
    kb.button("Привет!", callback_data="hi")
    await message.answer(f"Привет, {message.from_user.mention}!",
                         reply_markup=kb.as_markup())

@router.callback_query(F.data == "hi")
async def on_hi(query):
    await query.answer("👋")
    await query.edit_message("Ты нажал кнопку!")

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

async def main():
    async with Bot("ТОКЕН") as bot:
        await dp.start_polling(bot)

asyncio.run(main())
```

---

## novhttp — HTTP клиент

```python
from novhttp import NovHttp

# Простой запрос
async with NovHttp() as http:
    resp = await http.get("https://api.example.com/data")
    print(resp.status, resp.json)

# С базовым URL и заголовками
http = NovHttp(
    base_url="https://api.example.com",
    headers={"Authorization": "Bearer TOKEN"},
    timeout=10,
)
await http.start()

# GET → JSON
data = await http.get_json("/users")

# POST JSON → JSON
result = await http.post_json("/users", {"name": "Вася"})

# Другие методы
await http.put("/users/1", json={"name": "Петя"})
await http.delete("/users/1")
await http.patch("/users/1", json={"active": False})

await http.close()

# Пример в боте — получить курс валюты
@router.message(Command("rate"))
async def get_rate(message):
    async with NovHttp() as http:
        data = await http.get_json("https://open.er-api.com/v6/latest/USD")
        rub = data["rates"]["RUB"]
        await message.answer(f"💵 1 USD = {rub:.2f} RUB")
```

---

## novsqlite — база данных

```python
from novsqlite import NovDB

# Инициализация
db = NovDB("bot.db")
await db.init()

# --- Базовые операции ---

# Сохранить
await db.set("users", "user_123", {"name": "Вася", "score": 0, "level": 1})

# Получить
user = await db.get("users", "user_123")
print(user["name"])  # Вася

# Получить с дефолтом
score = await db.get("users", "unknown", default={"score": 0})

# Обновить поля
await db.update("users", "user_123", score=42, level=2)

# Проверить существование
if await db.exists("users", "user_123"):
    print("Пользователь есть")

# Удалить
await db.delete("users", "user_123")

# --- Удобные методы ---

# Счётчики
count = await db.incr("stats", "total_messages")    # +1
await db.incr("stats", "total_messages", by=5)      # +5
await db.decr("stats", "active_users")              # -1

# Списки
await db.append("history", "user_123", "текст сообщения")
msgs = await db.get("history", "user_123")  # ["текст сообщения"]

# Получить или создать
user, created = await db.get_or_create("users", user_id, {
    "name": "Новый",
    "score": 0,
})
if created:
    await message.answer("Добро пожаловать!")

# Поиск по полям
admins = await db.search("users", role="admin")
top_users = await db.search("users", level=5)

# Все записи
all_users = await db.all("users")

# Количество
total = await db.count("users")

# Очистить таблицу
deleted = await db.clear_table("temp_data")

# Контекстный менеджер
async with NovDB("bot.db") as db:
    await db.set("config", "key", "value")

# --- Пример в боте ---
@router.message(Command("profile"))
async def profile(message):
    user, created = await db.get_or_create(
        "users", message.chat_id,
        {"name": message.from_user.full_name, "messages": 0}
    )
    await db.incr("users_msgs", message.chat_id)
    msgs = await db.get("users_msgs", message.chat_id, 0)
    await message.answer(f"👤 {user['name']}\n💬 Сообщений: {msgs}")
```

---

## FSM хранилища

```python
# По умолчанию — в памяти (теряется при перезапуске)
dp = Dispatcher()

# JSON файл (сохраняется)
from novdev.contrib.storage import FileStorage
dp = Dispatcher(storage=FileStorage("fsm.json"))

# SQLite (надёжно, требует aiosqlite)
from novdev.contrib.storage import SQLiteStorage
dp = Dispatcher(storage=SQLiteStorage("fsm.db"))
```

---

## Антиспам

```python
from novdev.contrib.ratelimit import RateLimiter, Throttle

limiter = RateLimiter(max_messages=5, period=10)  # 5 сообщений за 10 сек

@dp.middleware
async def anti_spam(obj, next_handler):
    if await limiter.is_limited(obj.chat_id):
        if hasattr(obj, "answer"):
            await obj.answer("⛔ Слишком часто!")
        return
    await next_handler()
```

---

## Middleware

```python
@dp.middleware
async def logger_mw(obj, next_handler):
    print(f"▶ Апдейт от {obj.chat_id}")
    await next_handler()
    print(f"✓ Выполнено")
```

---

## Вложенные роутеры

```python
# [NEW 0.0.5] Роутер может включать другой роутер
main = Router()
admin = Router("admin")

@admin.message(Command("ban"))
async def ban(message): ...

main.include_router(admin)   # все хендлеры admin попадают в main
dp.include_router(main)
```

---

## Хуки on_startup / on_shutdown

```python
@dp.on_startup
async def setup(bot):
    await bot.set_my_commands([("start", "Старт")])

@dp.on_shutdown
async def cleanup(bot):
    await db.close()

# Без аргументов тоже работает
@router.on_startup
async def ready():
    print("Роутер готов")
```

---

## Фильтры

```python
Command("start")                          # /start
Command("help", "h")                      # /help или /h (и /help@bot тоже)
TextFilter("привет")                      # точное совпадение
TextFilter(startswith="!")                # начинается с !
TextFilter(contains="слово")             # содержит слово
TextFilter(regex=r"\d+")                  # регулярка
CallbackDataFilter("confirm")             # точный data
CallbackDataFilter(startswith="menu:")    # начинается с menu:
FromUserFilter("id1", "id2")             # только эти пользователи
HasText()                                 # есть текст
HasFile()                                 # есть файл
IsBot(False)                              # не бот

# Комбинирование: &, |, ~
HasText() & ~IsBot()
Command("start") | TextFilter("старт")
```

---

## Bot

```python
async with Bot("ТОКЕН") as bot:          # контекстный менеджер
    me = await bot.get_me()              # кэшируется
    me = await bot.get_me(use_cache=False)  # принудительный запрос

await bot.send_message(chat_id, text, parse_mode="Markdown")
await bot.send_photo(chat_id, photo="file.jpg")
await bot.send_photo(chat_id, file_url="https://...")
await bot.send_document(chat_id, document="file.pdf")
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.send_temp_message(chat_id, "Исчезну через 5 сек", delay=5)
await bot.broadcast(["id1", "id2", "id3"], "Всем привет!")  # рассылка
```

---

## Changelog

### 0.0.5
- **[FIX]** `_call_with_middleware` — `idx` как список, правильно работает в Python 3.10
- **[FIX]** `_split_filters` вынесен на уровень модуля
- **[FIX]** `bot._request` — единый метод вместо трёх дублирующих
- **[FIX]** `get_me` кэширует результат (нет лишних запросов)
- **[FIX]** `Message(**r)` защищён от не-dict ответа
- **[NEW]** `novhttp` — async HTTP клиент (GET/POST/PUT/DELETE/PATCH)
- **[NEW]** `novsqlite` — async SQLite база данных с удобным API
- **[NEW]** `Router.include_router()` — вложенные роутеры
- **[NEW]** `bot.broadcast()` — рассылка нескольким пользователям
- **[NEW]** `bot.get_me(use_cache=True)` — кэш информации о боте

*NovDev v0.0.5 · XenonE*
