Конфигурация
Три способа построить клиент, все опции и то, что он отклоняет до отправки запроса.
Опции
import os from openemail import AsyncOpenEmail, OpenEmail, init, openemail init(os.environ['OPENEMAIL_API_KEY'])openemail.me.ping() billing = OpenEmail(os.environ['BILLING_API_KEY']) pinned = OpenEmail('oe_live_...', base_url='https://api.openemail.uk') background = AsyncOpenEmail()| Точка входа | Что вы получаете |
|---|---|
| init(...) | Настраивает общий клиент и возвращает его. С этого момента openemail и есть этот клиент, в любом модуле, а всё, что вы не указали, читается из окружения. |
| openemail | Общий клиент. При использовании до init он строит себя из OPENEMAIL_API_KEY и OPENEMAIL_BASE_URL при первом вызове. |
| OpenEmail(...) | Отдельный клиент: для второго ключа рядом с общим или чтобы построить экземпляр, который экспортирует ваш собственный модуль. Всё, что вы не указали, читается из окружения, как это делает init, а create_client является тем же классом под другим именем. |
| AsyncOpenEmail(...) | Отдельный асинхронный клиент с теми же опциями, в котором каждый метод вызывается через await. Общий openemail синхронный, поэтому этот клиент вы строите сами. |
| get_client() и reset_client() | get_client возвращает сам общий клиент и строит его из окружения, если init ещё не вызывался. reset_client забывает его, так что при следующем использовании строится новый. |
import httpxfrom openemail import init init( 'oe_live_...', base_url='https://api.openemail.uk', timeout=30, max_retries=2, http_client=httpx.Client(proxy='http://proxy.internal:3128'), headers={'X-Team': 'billing'}, user_agent='billing-service/1.4', disable_update_notice=True,)| Опция | По умолчанию | Примечания |
|---|---|---|
| api_key | OPENEMAIL_API_KEY | Читается из окружения функциями init и OpenEmail. Должен начинаться с oe_live_ или oe_test_. |
| access_token | OPENEMAIL_ACCESS_TOKEN | Токен доступа OAuth или функция, которая его возвращает, вместо api_key. См. раздел «Токены доступа OAuth» ниже. |
| base_url | https://api.openemail.uk | Или OPENEMAIL_BASE_URL. Завершающий слеш срезается, а init и OpenEmail подставляют https:// перед голым хостом и http:// перед хостом на этой машине: localhost, адресом 127.x.x.x или ::1. Учётные данные никогда не отправляются по обычному http на любой другой хост, а 0.0.0.0 или [::] выбрасывает исключение при создании клиента, потому что это адреса, на которых сервер слушает, а не адреса для отправки запросов. |
| timeout | 30 | В секундах, на попытку, а не на вызов. Покрывает чтение тела, а не только заголовков. 0 отключает его. files.upload ждёт не меньше 600 секунд, если вызов не передаёт собственный timeout. |
| max_retries | 2 | Дополнительные попытки после первой, для вызовов, которые безопасно повторять. Задаётся на клиенте, а не на вызове. |
| http_client | новый httpx.Client | Передайте свой для прокси, собственных настроек TLS, смонтированного транспорта или тестового двойника: httpx.Client для OpenEmail, httpx.AsyncClient для AsyncOpenEmail. Закрытие клиента оставляет переданный вами клиент открытым. |
| headers | {} | Отправляются с каждым запросом. |
| user_agent | openemail-python/<version> | Отправляются с каждым запросом. |
| disable_update_notice | False | Пропускает однократную за процесс проверку новой версии на PyPI. Проверка выполняется, только когда вывод идёт в терминал, и OPENEMAIL_DISABLE_UPDATE_NOTICE тоже её отключает. |
Что он отклоняет до отправки
В этих случаях ещё до отправки какого-либо запроса выбрасывается ValueError (или TypeError, где это указано в таблице), вместо того чтобы при вашей первой отправке всплыл непонятный сбой. В сообщении сказано, что было не так и что передать вместо этого.
| Отклонено | Почему |
|---|---|
| Ключа нет вовсе | Ни api_key, ни OPENEMAIL_API_KEY не заданы, так что аутентифицироваться нечем. |
| Сессионная кука или сессионный токен | Здесь аутентифицируют только oe_live_ и oe_test_, и API говорит то же самое. Проверяется префикс и ничего больше, так что отозванный ключ всё равно упадёт на проводе. |
| base_url, не являющийся http- или https-адресом | Ничего другого получить нельзя, поэтому клиент отклоняет такой адрес при создании, а не падает на первом запросе. |
| base_url на 0.0.0.0 или [::] | Это адрес, на котором сервер слушает, а не адрес для отправки запросов. Вместо него сообщение называет 127.0.0.1 или [::1] с тем же портом. |
| Учётные данные по обычному http | Отклоняется при вызове, до отправки запроса, если только сервер не находится на этой машине. Их мог бы прочитать любой в сети. |
| Пустой идентификатор или идентификатор из одних точек в любом методе | Исключение выбрасывается при вызове метода. Сегмент пути из точек удаляется любым парсером URL, так что запрос попал бы на другой эндпоинт. |
| http_client не того типа | TypeError при создании клиента. OpenEmail принимает httpx.Client, а AsyncOpenEmail принимает httpx.AsyncClient. |
| Значение в теле, которое JSON не может передать | TypeError с названием его типа. Типы JSON проходят как есть, а datetime, date или set преобразуются за вас. |
Опции test_mode нет и не будет. Схема ключа входит в учётные данные, а не служит подсказкой, так что режим является свойством ключа. client.mode читает префикс и ничего не решает.
Один клиент, несколько ключей
Постройте клиент один раз и используйте его совместно. Новый экземпляр на каждый запрос без всякой пользы выбрасывает пул соединений и конфигурацию, а никакое состояние в нём не привязано к вызывающей стороне.
Один клиент можно безопасно использовать из нескольких потоков. close() или конец блока with закрывает пул соединений, который он открыл, а переданный вами http_client остаётся открытым, и закрыть его должны вы.
Для случая, который иначе потребовал бы по экземпляру на ключ (например, задания, отправляющего от имени нескольких рабочих пространств), передайте api_key в вызов. Он заменяет заголовок Authorization для этого запроса и ничего не оставляет на клиенте.
from openemail.types import EmailSend workspace_key = 'oe_live_...' message: EmailSend = {'from': sender, 'to': recipient, 'subject': subject, 'text': text} client.emails.send(message) client.emails.send(message, api_key=workspace_key) client.threads.list(folder='inbox', api_key=workspace_key)client.webhooks.list(api_key=workspace_key)Каждый метод вне temp_mail принимает его как именованный аргумент рядом с timeout. Он проверяется до отправки запроса по тому же правилу, что и в конструкторе, так что опечатка выбросит ValueError с упоминанием api_key= on this call, а не 401 про учётные данные, которые потом придётся искать. Повторённый вызов сохраняет выданный ему ключ.
timeout задаётся в секундах и заменяет таймаут клиента для этого единственного вызова, на каждой его попытке.
client.mode описывает ключ, с которым клиент был СОЗДАН, и не следует за переопределением. Как только один клиент обслуживает несколько ключей, единого режима, о котором можно сообщить, не существует, так что читайте его по переданному ключу.
Эндпоинт, который не оборачивает ни один метод
client.raw.request отправляет запрос, применяя учётные данные, базовый URL, таймаут и политику повторов клиента, и возвращает разобранный JSON. Он принимает method, query, body, api_key и timeout. Он повторяет GET, а всё остальное отправляет один раз, если вы не передали repeatable=True. idempotent=True добавляет Idempotency-Key: тот, что вы передали как idempotency_key, или новый.
result = client.raw.request('/something-new', method='POST', body={'name': 'Invoices'})Путь должен начинаться с одного /. Любой другой, например //host/x, выбрасывает исключение до отправки запроса, как и путь, итоговый URL которого выходит за пределы origin базового URL, так что передаваемые учётные данные никогда не попадут на другой хост.
Одноразовые ящики
create_temp_mail() строит клиент, который не несёт API-ключа, а create_async_temp_mail() является его асинхронным аналогом. Он создаёт ящики анонимно, и каждое чтение отправляет токен ящика, который вернул create, или более новый, который вернул extend, либо в каждом вызове как inbox_token, либо один раз как create_temp_mail(inbox_token=...).
from openemail import create_temp_mail temp_mail = create_temp_mail() inbox = temp_mail.create()messages = temp_mail.list_messages(inbox['id'], inbox_token=inbox['token']) print(messages['items'], messages['expiresAt'])Токены доступа OAuth
Ещё не выпущеноПриложение, которое человек подключил через OAuth, например инструмент командной строки или агент, держит токен доступа вместо API-ключа. Передайте его как access_token: либо сам токен, либо функцию, которая его возвращает и в AsyncOpenEmail может быть async. Функция вызывается один раз на каждый вызов, а повторы этого вызова используют то, что она вернула, поэтому обновляйте токен внутри неё, когда срок его действия подходит к концу, и клиент никогда не придётся пересоздавать.
from openemail import OpenEmail client = OpenEmail(access_token=session.fresh_access_token) me = client.me.get() if me['object'] == 'oauth_token': print(me['clientId'], me['expiresAt'])| Случай | Что происходит |
|---|---|
| api_key и access_token вместе или ни одного | Конструктор выбрасывает ValueError. Если нет ни одного, сообщение называет OPENEMAIL_API_KEY и OPENEMAIL_ACCESS_TOKEN. |
| Значение, которое не является токеном | Токен имеет длину от 1 до 512 символов и не начинается с oe_: именно это проверяет is_access_token. Строка, не прошедшая проверку, выбрасывает исключение из конструктора, а функция, которая возвращает такую строку, проваливает вызов до какой-либо отправки. |
| OPENEMAIL_ACCESS_TOKEN | Читается init, OpenEmail и общим openemail, когда вы не передаёте ни одного из двух видов доступа и OPENEMAIL_API_KEY не задан, так что ключ в окружении имеет приоритет. |
| Функция, которая выбрасывает исключение | Вызов выбрасывает эту же ошибку без изменений, и ничего не отправляется. |
| Функция в OpenEmail, которая возвращает awaitable-объект | ValueError, потому что синхронный клиент не может его дождаться. В AsyncOpenEmail функция может быть async. |
| api_key на один вызов | Заменяет токен для этого единственного запроса, и функция не вызывается. |
| mode | С токеном всегда live. |
| create_temp_mail() | Не отправляет никаких учётных данных, что бы ни было в окружении. |
| me.get() и me.ping() | Для токена get отвечает с object, равным oauth_token, id и roleId, равными None, clientId подключённого приложения и expiresAt, моментом, когда истекает одобрение, данное человеком приложению. ping отвечает с kind, равным oauth, keyId, равным None, и clientId. KeyResource и PingResource являются объединениями, поэтому различайте их по object или kind, прежде чем читать clientId или expiresAt. |
Токен действует от имени человека и читает его почту так, как может он сам, поэтому держите его на сервере, как и ключ.
Коды подтверждения
Ещё не выпущеноПеред чувствительным изменением, например удалением домена или изменением вебхука, API спрашивает у токена доступа код подтверждения, который веб-приложение спросило бы у человека. Вызов выбрасывает OpenEmailApiError, у которого is_step_up_required равен True, и ничего не изменилось. Запросите код, подтвердите тот, что даст вам человек, затем сделайте вызов снова. У API-ключа код никогда не спрашивают.
from openemail import OpenEmailApiError try: client.domains.delete(domain_id)except OpenEmailApiError as error: if not error.is_step_up_required: raise challenge = client.security.begin_step_up() if challenge['method'] == 'email': prompt = f'Enter the code we emailed to {challenge.get("sentTo")}: ' else: prompt = 'Enter the code from your authenticator app, or a backup code: ' client.security.verify_step_up({'code': input(prompt)}) client.domains.delete(domain_id)| Метод | Что делает |
|---|---|
| security.step_up_status() | Подтверждено ли приложение прямо сейчас (elevated, elevatedUntil), как будет проверяться следующий код (method, email или totp) и minutes, длина окна. Ничего не отправляет и не сообщает о паузе. |
| security.begin_step_up(body=None) | Открывает проверку. При email шестизначный код уходит на адрес, с которым человек входит, и sentTo показывает его замаскированным. При totp человек берёт код из приложения-аутентификатора или использует резервный код. Ещё открытая проверка, у которой остались попытки, используется повторно, если не передать {'resend': True}, а заблокированная или истёкшая заменяется простым вызовом. Каждое приложение может открыть 5 проверок в час и 20 за 24 часа для каждого человека, а следующая выбрасывает 429 step_up_throttled. |
| security.verify_step_up({'code': code}) | Проверяет код и открывает чувствительные изменения для этого приложения на 60 минут, до elevatedUntil, через REST и через MCP-инструменты, которые вносят те же изменения. После 10 неверных кодов за 24 часа от этого приложения или 20 от всех приложений человека вместе этот вызов и begin_step_up выбрасывают 429 step_up_locked с сообщением о том, когда подтверждение снова станет доступно. |
Клиент никогда сам не спрашивает код и не повторяет вызов, и ни begin_step_up, ни verify_step_up не повторяются автоматически, потому что повтор после потерянного ответа мог бы отправить второе письмо или потратить вторую попытку. Этим методам не нужна область доступа, а API-ключ, вызвавший один из них, получает 400 step_up_not_applicable. STEP_UP_ERROR_CODES называет все причины, по которым подтверждение может не пройти, а страница ошибок API говорит, что делать в каждом случае.