Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
- run: python -m pip install --upgrade pip
- run: pip install -e ".[dev]"
- run: ruff check .
- run: pytest -q
- run: python -m build
43 changes: 43 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Contributing

Спасибо за интерес к DONSTU Python SDK.

## Локальная разработка

```bash
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
ruff check .
pytest
```

## Что добавлять

Хороший PR обычно содержит:

- один или несколько понятных high-level методов SDK;
- snake_case имена аргументов на Python-стороне;
- преобразование в фактические имена параметров API внутри SDK;
- unit-тесты через `httpx.MockTransport`, без запросов к production;
- пример или обновление README, если меняется публичный интерфейс.

## Безопасность

Не добавляйте в репозиторий:

- access/refresh tokens;
- логины и пароли;
- cookie;
- приватные ключи и client secrets;
- реальные персональные данные пользователей.

Для тестов используйте вымышленные идентификаторы и `httpx.MockTransport`.

## Стиль

- Python 3.10+;
- типизация для публичных методов;
- `ruff check .` должен проходить без ошибок;
- `pytest` должен проходить полностью;
- синхронный и асинхронный API желательно держать функционально одинаковыми.
230 changes: 208 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,54 +1,240 @@
# DONSTU Python SDK

SDK собран по исходникам без сетевого probing production-системы.
Основная цель — дать нормальный Python-клиент к `https://edu.donstu.ru/api/` и сохранить
полный каталог маршрутов.
Неофициальный Python-клиент для API образовательных сервисов ДГТУ (`edu.donstu.ru`).
Проект ориентирован на студенческие приложения, боты, виджеты расписания и другие учебные проекты.

> SDK не является официальным продуктом ДГТУ. Доступность и формат отдельных API-методов могут
> меняться. Используйте только те данные и операции, на которые у вашего аккаунта есть права.

## Возможности

- авторизация по логину/паролю или готовому Bearer-токену;
- синхронный и асинхронный клиент;
- расписание групп, преподавателей и студентов;
- списки групп, преподавателей, аудиторий и учебных лет;
- информация о пользователе/студенте и ролях;
- базовая работа с внутренней почтой;
- универсальные `request()` / `call_route()` для API-методов, которые ещё не получили отдельную обёртку;
- нормализованные исключения для HTTP, авторизации, сети и ошибок API;
- типизированный публичный интерфейс и `py.typed`.

## Установка

Пока пакет не опубликован в PyPI, его можно установить напрямую из GitHub:

```bash
pip install "donstu-sdk @ git+https://github.com/localzet/donstu-python-sdk.git"
```

Для разработки:

```bash
git clone https://github.com/localzet/donstu-python-sdk.git
cd donstu-python-sdk
python -m venv .venv
source .venv/bin/activate
pip install -e .
pip install -e ".[dev]"
```

## Использование
## Быстрый старт: готовый токен

```python
import os

from donstu_sdk import DonstuClient

with DonstuClient() as dstu:
dstu.auth.login("login", "password")
with DonstuClient(token=os.environ["DONSTU_TOKEN"]) as dstu:
me = dstu.auth.me()
student = dstu.users.student(-123456)
schedule = dstu.schedule.rasp(idGroup=1234, sdate="2026-09-19")
groups = dstu.schedule.groups()

schedule = dstu.schedule.get(
group_id=12345,
start_date="2026-09-21",
end_date="2026-09-27",
)

print(me)
print(groups)
print(schedule)
```

Можно передать как сам JWT, так и строку вида `Bearer eyJ...` — SDK нормализует её автоматически.

## Авторизация по логину и паролю

```python
import os

from donstu_sdk import DonstuClient

with DonstuClient() as dstu:
login = dstu.auth.login(
os.environ["DONSTU_LOGIN"],
os.environ["DONSTU_PASSWORD"],
)

print(login["accessToken"])
print(dstu.auth.me())
```

Можно передать уже существующий токен:
После успешного `login()` полученный access token автоматически сохраняется в экземпляре клиента и
используется в следующих запросах.

Не храните логины, пароли и токены в исходном коде. Для локальной разработки удобнее использовать
переменные окружения или `.env`, исключённый из Git.

## Расписание

### Группа

```python
client = DonstuClient(token="...")
from datetime import date, timedelta

from donstu_sdk import DonstuClient

monday = date.today()
sunday = monday + timedelta(days=6)

with DonstuClient() as dstu:
schedule = dstu.schedule.get(
group_id=12345,
start_date=monday,
end_date=sunday,
)
```

## Полный route catalog
### Преподаватель

```python
for route in client.catalog.find("Certificates"):
print(route.method, route.path, route.params, route.authorize)
schedule = dstu.schedule.get(
teacher_id=123,
start_date="2026-09-21",
end_date="2026-09-27",
)
```

Либо вызвать endpoint напрямую:
### Справочники расписания

```python
result = client.call_route(
years = dstu.schedule.years()
groups = dstu.schedule.groups(year="2026-2027")
teachers = dstu.schedule.teachers(year="2026-2027")
auditories = dstu.schedule.auditories(year="2026-2027")
```

Для совместимости доступен низкоуровневый вариант с исходными именами query-параметров:

```python
schedule = dstu.schedule.rasp(
idGroup=12345,
sdate="2026-09-21",
edate="2026-09-27",
)
```

## Async API

Для FastAPI, aiogram и других asyncio-приложений используйте `AsyncDonstuClient`:

```python
import asyncio
import os

from donstu_sdk import AsyncDonstuClient


async def main() -> None:
async with AsyncDonstuClient(token=os.environ["DONSTU_TOKEN"]) as dstu:
schedule = await dstu.schedule.get(group_id=12345)
print(schedule)


asyncio.run(main())
```

Синхронный и асинхронный клиенты имеют одинаковую структуру: `auth`, `users`, `schedule`, `mail`.

## Пользователи

```python
student = dstu.users.student(123456)
staff = dstu.users.staff(123)
roles = dstu.users.roles(123)
```

Эти методы требуют токен с соответствующими правами.

## Универсальный запрос

Если нужный endpoint ещё не обёрнут отдельным методом SDK:

```python
result = dstu.request(
"GET",
"/api/Rasp",
params={"idGroup": 1234, "sdate": "2026-09-19"},
"/api/SomeEndpoint",
params={"id": 123},
)
```

Для route-параметров:

```python
result = dstu.call_route(
"DELETE",
"/api/SomeEndpoint/{id}",
route_values={"id": 123},
)
```

## Ограничения реверса
`call_route()` не обходит серверную авторизацию и не расширяет права пользователя — запрос выполняется
с тем же токеном и теми же ограничениями, что и обычный API-вызов.

## Ответы API

Большинство методов API используют оболочку вида `state / msg / data`. SDK автоматически:

1. проверяет HTTP-статус;
2. разбирает JSON;
3. проверяет `state`;
4. возвращает `data`.

Если нужен полный JSON без автоматического распаковывания:

```python
payload = dstu.request("GET", "Rasp/ListYears", unwrap=False)
```

Если нужен сам `httpx.Response` — например, для файла:

```python
response = dstu.request_raw("GET", "/api/SomeFileEndpoint")
content = response.content
```

## Ошибки

```python
from donstu_sdk import (
DonstuAPIError,
DonstuAuthenticationError,
DonstuHTTPError,
DonstuNetworkError,
DonstuProtocolError,
)
```

- `DonstuAuthenticationError` — HTTP 401/403;
- `DonstuHTTPError` — остальные HTTP-ошибки;
- `DonstuNetworkError` — таймаут/ошибка соединения;
- `DonstuAPIError` — API ответил успешно по HTTP, но `state` сообщает об ошибке;
- `DonstuProtocolError` — неожиданный формат ответа.

## Разработка

```bash
ruff check .
pytest
python -m build
```

Исходники декомпилированы, а часть маршрутов содержит сложные DTO. Поэтому полный каталог
сохраняет оригинальные C# типы параметров, но высокоуровневые Python-модели пока сделаны только для
наиболее используемых частей. Для редких endpoint'ов используйте `call_route()` и `catalog.find()`.
Pull Request'ы с новыми безопасными high-level методами, тестами и улучшением типизации приветствуются.
14 changes: 14 additions & 0 deletions examples/async_schedule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import asyncio
import os

from donstu_sdk import AsyncDonstuClient


async def main() -> None:
async with AsyncDonstuClient(token=os.environ["DONSTU_TOKEN"]) as dstu:
schedule = await dstu.schedule.get(group_id=12345)
print(schedule)


if __name__ == "__main__":
asyncio.run(main())
14 changes: 3 additions & 11 deletions examples/basic.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import os

from donstu_sdk import DonstuClient


Expand All @@ -9,13 +8,6 @@
os.environ["DONSTU_PASSWORD"],
)

me = dstu.auth.me()
print(me)

# Пример обычного расписания группы.
rasp = dstu.schedule.rasp(idGroup=12345, sdate="2026-09-19")
print(rasp)

# Найти неизвестный заранее endpoint в каталоге.
for route in dstu.catalog.find("Rasp", method="GET")[:10]:
print(route.method, route.path, route.params)
print(dstu.auth.me())
print(dstu.schedule.groups())
print(dstu.schedule.get(group_id=12345, start_date="2026-09-21"))
Loading
Loading