Metadata-Version: 2.4
Name: mailganer
Version: 0.2.1
Summary: Официальный клиент REST API Mailganer: подписчики, списки, рассылки, триггеры, отчёты
Author-email: Mailganer <api@mailganer.com>
License-Expression: MIT
Project-URL: Documentation, https://documentation.mailganer.com/
Project-URL: API Reference, https://documentation.mailganer.com/reference/
Project-URL: Homepage, https://mailganer.com
Keywords: mailganer,email,рассылки,api,email-marketing
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Classifier: Operating System :: OS Independent
Classifier: Natural Language :: Russian
Classifier: Topic :: Communications :: Email
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# mailganer

Официальный клиент [REST API Mailganer](https://documentation.mailganer.com/) для Python.
Версия **0.2.1**, лицензия MIT, Python 3.8+, без внешних зависимостей.

## Установка

```bash
pip install mailganer
```

## Ключ API

Ключ лежит в личном кабинете, раздел [Ключ API](https://mailganer.com/app/profile/settings/api/).
Передайте его в клиент или положите в переменную окружения `MAILGANER_API_KEY`.
Вставляйте только сам ключ: заголовок `Authorization: CodeRequest ...` клиент соберёт сам,
а в v1-методах подставит ключ в тело запроса.

```python
from mailganer import Mailganer

mg = Mailganer(api_key="ваш_ключ")   # или Mailganer() с MAILGANER_API_KEY
print(mg.users.balance())            # {'balance': 954.9}
```

## Подписчики

```python
# добавить одного
mg.subscribers.add("user@example.com", source=49767, name="Иван", user_vars={"city": "Москва"})

# до 50 за запрос; записи с ошибкой не добавятся и вернутся в "failed" с причиной
mg.subscribers.add_many([
    {"email": "a@example.com", "source": 49767, "name": "Аня"},
    {"email": "b@example.com", "source": 49767, "name": "Боря"},
])

# v3: подробный ответ по каждой записи, частичный успех не ошибка
result = mg.subscribers.add_many_v3(rows)
for row in result["failed"]:
    print(row["email"], row["errors"])

# найти и обновить
page = mg.subscribers.list(email="user@example.com")       # одна страница, 25 записей
for sub in mg.subscribers.list_all(source=49767):            # все страницы подряд
    ...
mg.subscribers.update(mg_hash, name="Пётр")                  # по mg_hash
mg.subscribers.update_by_email("user@example.com", 49767, city="Казань")
mg.subscribers.unsubscribe(mg_hash)
```

## Рассылки и триггеры

```python
draft = mg.mailings.create(name="Акция", theme="Скидка 20%", segment_id=123456,
                           from_email="news@example.com", from_name="Магазин",
                           html_content="<html>...</html>")
mg.mailings.send(draft["public_id"], datetime_send="01.10.2026 10:00")
mg.mailings.stats(draft["public_id"], start_date="2026-10-01", end_date="2026-10-07")

mg.triggers.send(113292, "user@example.com", variables={"order_id": 555})
mg.segments.send_trigger(245611, trigger_id=145674, ignore_trigger_conditions=True)
```

## Отчёты

```python
task = mg.reports.create("csv", site_id=49767)
mg.reports.wait(task["id"])                       # ждёт state=COMPLETED
open("list.csv", "wb").write(mg.reports.download(task["id"]))
```

## Вебхуки

Mailganer шлёт события на URL, заданный `mg.lists.set_webhook(list_id, url)`.

```python
from mailganer import parse_webhook

@app.post("/mailganer-hook")
def hook(request):
    event = parse_webhook(request.body)
    if event["event"] == "mail_interaction" and event["action_name"] == "clicked":
        ...
    return "", 200
```

`event`: `subscriber_status`, `subscriber_data`, `mail_interaction` (с `action_name`: delivered, opened,
clicked, unsubscribed, spam_complaint, not_delivered), `trigger_event` (с `user_vars`).

## Постбек

```python
from mailganer import postback_url

# ссылка для письма: {{public_id}} и {{email_hash}} подставит Mailganer при отправке
link = postback_url("https://shop.ru/order/done", nps=10)
```

## Ошибки и лимиты

Все ошибки наследуют `MailganerError`: `AuthenticationError` (401), `PermissionDeniedError` (403),
`NotFoundError` (404), `ValidationError` (400, подробности по полям в `.errors`), `RateLimitError`,
`ServerError`, `APIConnectionError` (нет сети, сервер не найден) и `APITimeoutError` (API не ответил за `timeout`
секунд, по умолчанию 30). v1-методы, которые сообщают об ошибке в теле с HTTP 200, тоже дают исключение. `subscribers.add()`
бросает `ValidationError`, если API не принял запись, а `triggers.send()` бросает `MailganerError`, если письмо
не отправлено (например, триггер выключен или на модерации).

При 429, 503 и `throttling` клиент сам ждёт и повторяет запрос, по умолчанию 2 раза (`max_retries`). Запросы на чтение
он повторяет и при обрыве связи, 502 и 504. Ключ API отправляется только на сервер API и только по https.

```python
from mailganer import ValidationError

try:
    mg.templates.create(name="Письмо", html="")
except ValidationError as e:
    print(e.errors)   # {'html': ['Это поле обязательно.']}
```

## Все разделы

`subscribers`, `lists`, `segments`, `variables`, `mailings`, `triggers`, `recommendations`, `users`, `tags`,
`templates`, `reports`, `postback`. Имена полей совпадают с [справочником API](https://documentation.mailganer.com/reference/).
Метода нет в библиотеке — `mg.request("GET", "/api/v2/...")`.

## Подсказки типов

У методов есть подсказки типов, пакет помечен как типизированный. Редактор подсказывает параметры,
а mypy или pyright ловят ошибку в вызове до запуска:

```python
mg.mailings.send(public_id, is_chunked="да")   # mypy: ожидается bool
```

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

```bash
python -m unittest discover -s tests   # тесты без обращения к API
mypy                                   # строгая проверка типов, настройки в pyproject.toml
```
