Metadata-Version: 2.4
Name: pymirea
Version: 0.3.2
Summary: Async Python client for Мирэа LKS (schedule, grades, attendance, ACS, SSO)
Project-URL: Homepage, https://github.com/silverhans/pymirea
Project-URL: Issues, https://github.com/silverhans/pymirea/issues
Author: silverhans
License-Expression: MIT
License-File: LICENSE
Keywords: async,lks,mirea,scraper,university
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Education
Requires-Python: >=3.11
Requires-Dist: beautifulsoup4>=4.12
Requires-Dist: cryptography>=42
Requires-Dist: httpx>=0.25
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: respx>=0.20; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Provides-Extra: tls
Requires-Dist: curl-cffi>=0.7; extra == 'tls'
Description-Content-Type: text/markdown

# pymirea

[![PyPI](https://img.shields.io/pypi/v/pymirea.svg)](https://pypi.org/project/pymirea/)
[![CI](https://github.com/silverhans/pymirea/actions/workflows/ci.yml/badge.svg)](https://github.com/silverhans/pymirea/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)

Асинхронный Python-клиент для [Пульса МИРЭА](https://pulse.mirea.ru).

Поддерживает: вход с 2FA через Keycloak SSO, расписание занятий, оценки, посещаемость (отметка + детали), события турникетов (ACS), регистрацию в e-sports, шифрование сессий (Fernet + HKDF).

## Установка

```bash
pip install pymirea
```

## Первый запрос за 30 секунд

```python
import asyncio
from pymirea import Config, configure, MireaAuth

# Сгенерируйте свой ключ один раз и сохраните:
#   python -c "import secrets,base64; print(base64.b64encode(secrets.token_bytes(32)).decode())"
configure(Config(session_keys="ВАША_BASE64_СТРОКА_32_БАЙТА"))

async def main():
    auth = MireaAuth()
    result = await auth.login("ваш_логин@edu.mirea.ru", "пароль")

    if result.challenge:
        # МИРЭА просит OTP (приходит на университетскую почту)
        code = input("Код из email: ")
        result = await auth.complete_2fa(result.challenge, code)

    print("Вошли! Токены:", result.tokens)

asyncio.run(main())
```

## Примеры использования

### 1. Простой скрипт — получить расписание

```python
import asyncio
from datetime import datetime
from pymirea import Config, configure, MireaAuth
from pymirea.grades import MireaGrades

configure(Config(session_keys="..."))

async def main():
    auth = MireaAuth()
    result = await auth.login("login@edu.mirea.ru", "пароль")
    # ... обработка 2FA если потребуется ...

    api = MireaGrades(session_cookies=result.tokens)
    schedule = await api.get_schedule(days=7)
    for lesson in schedule.lessons:
        when = datetime.fromtimestamp(lesson.start_epoch).strftime("%d.%m %H:%M")
        print(f"{when}  {lesson.name} — {lesson.teacher or '?'} ({lesson.room or '?'})")

asyncio.run(main())
```

Полная версия: [`examples/cli_schedule.py`](examples/cli_schedule.py).

### 2. Telegram-бот на aiogram

```python
from datetime import datetime
from aiogram import Bot, Dispatcher, types
from pymirea import Config, configure, MireaAuth
from pymirea.grades import MireaGrades

configure(Config(session_keys="..."))

bot = Bot(token="...")
dp = Dispatcher()

# user_id -> session cookies (в продакшене — в БД, зашифрованно через SessionCrypto)
sessions: dict[int, dict] = {}

@dp.message(commands=["schedule"])
async def cmd_schedule(msg: types.Message):
    cookies = sessions.get(msg.from_user.id)
    if not cookies:
        await msg.answer("Сначала /login")
        return
    api = MireaGrades(session_cookies=cookies)
    sched = await api.get_schedule(days=3)
    lines = [
        f"{datetime.fromtimestamp(l.start_epoch).strftime('%d.%m %H:%M')} — {l.name}"
        for l in sched.lessons
    ]
    await msg.answer("\n".join(lines) or "Пар не найдено")
```

Полная версия: [`examples/telegram_bot.py`](examples/telegram_bot.py).

### 3. Отметить посещаемость себе и друзьям одним QR-сканом

Классический MireaScanner-флоу — сделайте своё приложение за 20 минут:

```python
from pymirea import MireaAPI, SessionCrypto
from pymirea.crypto import get_crypto

crypto = get_crypto()

# Загрузите из своей БД зашифрованные сессии друзей
friends = [
    (123, "Эллиот", crypto.decrypt_session(row_elliot)),
    (456, "Уолтер", crypto.decrypt_session(row_walter)),
]

api = MireaAPI(session_cookies=my_session)
results = await api.mark_attendance_for_group(qr_data, friends)

for r in results:
    print(f"{r.user_name}: {'✓' if r.success else '✗ ' + r.message}")
```

`mark_attendance_for_group` сам извлекает токен из QR-URL, проходит по всем переданным сессиям и возвращает список `AttendanceResult` — готово к отображению в UI.

### 4. FastAPI-веб-сервис

```python
from fastapi import FastAPI, Depends, HTTPException
from pymirea import Config, configure
from pymirea.grades import MireaGrades
from pymirea.crypto import get_crypto

configure(Config(session_keys="..."))
crypto = get_crypto()
app = FastAPI()

def current_session(token: str = Depends(...)) -> dict:
    """Расшифровать сессию пользователя из вашего хранилища."""
    encrypted = your_db.get_mirea_session(user_id_from(token))
    if not encrypted:
        raise HTTPException(401)
    return crypto.decrypt_session(encrypted)

@app.get("/api/schedule")
async def get_schedule(session: dict = Depends(current_session)):
    api = MireaGrades(session_cookies=session)
    result = await api.get_schedule(days=7)
    return {
        "success": result.success,
        "lessons": [
            {"start": l.start_epoch, "end": l.end_epoch, "name": l.name,
             "teacher": l.teacher, "room": l.room}
            for l in (result.lessons or [])
        ],
    }
```

Полная версия: [`examples/fastapi_app.py`](examples/fastapi_app.py).

> Примеров для QR-сканирования в папке `examples/` пока нет — PR приветствуется.

## Шифрование сессий

Хранить cookies МИРЭА в БД в открытом виде нельзя — `SessionCrypto` оборачивает их в Fernet-токен с ключом, выведенным через HKDF из вашего `session_keys`:

```python
from pymirea.crypto import get_crypto

crypto = get_crypto()
encrypted: str = crypto.encrypt_session(cookies_dict)   # сохраните в БД
decrypted: dict = crypto.decrypt_session(encrypted)     # прочтите из БД
```

При смене `session_keys` старые токены становятся нечитаемыми — храните старый ключ в `legacy_bot_token` для grace-period миграции.

## Public API

| Имя | Назначение |
|---|---|
| `Config` / `configure(Config)` | Конфигурация runtime (DI-style, вызывается один раз на старте) |
| `MireaAuth` | `login()`, `complete_2fa()` (алиас `submit_otp()`), `refresh_tokens()`, `verify_session()` |
| `MireaAPI` (`pymirea.session`) | `mark_attendance()`, `mark_attendance_for_group()`, `extract_token_from_qr()`, `get_attendance_score()` |
| `MireaGrades` (`pymirea.grades`) | `get_schedule()`, `get_grades()`, `get_attendance_detail()`, `self_approve_attendance()` |
| `MireaACS` | `get_today_events()` — события турникетов |
| `MireaEsports` | `login()`, `get_slots()`, `create_booking()`, `get_my_bookings()`, `cancel_booking()` |
| `SessionCrypto` | Шифрование/расшифровка cookies (Fernet + HKDF) |
| `AuthChallenge` / `AuthResult` | Результаты login-флоу |
| `get_authorization_header(cookies)` | Bearer-токен из dict сессии |
| `try_refresh_tokens(cookies)` | Best-effort обновление access-токена |
| `get_token_age_seconds(cookies)` | Сколько секунд токен живёт |

Подробнее в исходниках: [`pymirea/`](pymirea/).

## Конфигурация

```python
from pymirea import Config

Config(
    session_keys="base64-32-bytes",     # обязательно
    mirea_proxy="http://proxy:8080",    # опционально, для pulse.mirea.ru (датацентры заблокированы)
    legacy_bot_token=None,              # старый ключ для миграции при ротации
    request_timeout_s=15.0,
    breaker_failure_threshold=5,
    breaker_recovery_s=30.0,
)
```

## Совместимость

- **Python** 3.11+
- **Только async** (нет sync-обёртки; для скриптов используйте `asyncio.run`)
- **Любой web-фреймворк**: FastAPI, aiohttp, Sanic, Litestar, Quart — pymirea не привязан ни к одному
- **Любая БД** для хранения сессий — pymirea даёт только примитивы шифрования

## Что pymirea НЕ делает

- ❌ Не хранит сессии — это задача вашего приложения (БД/Redis/файл)
- ❌ Не управляет пользователями — у вас своя auth-система
- ❌ Не предоставляет HTTP-handlers — пишите свои под свой фреймворк
- ❌ Не парсит сложные нестандартные формы МИРЭА (профили, дипломы — TBD)

Если хочется готовый микросервис — соберите Telegram-бот / FastAPI-сервис на основе примеров.

## Внести вклад

Issues и PR приветствуются. Особенно полезно:
- Тесты для новых сценариев (logout, edge-cases в 2FA, etc.)
- Поддержка дополнительных эндпоинтов МИРЭА
- Примеры для других фреймворков (Discord-боты, CLI-tools, ...)

## Лицензия

MIT — см. [LICENSE](LICENSE).
