Перейти к документации
Python

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

Три способа построить клиент, все опции и то, что он отклоняет до отправки запроса.

Опции

clients.py
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 забывает его, так что при следующем использовании строится новый.
options.py
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_keyOPENEMAIL_API_KEYЧитается из окружения функциями init и OpenEmail. Должен начинаться с oe_live_ или oe_test_.
access_tokenOPENEMAIL_ACCESS_TOKENТокен доступа OAuth или функция, которая его возвращает, вместо api_key. См. раздел «Токены доступа OAuth» ниже.
base_urlhttps://api.openemail.ukИли OPENEMAIL_BASE_URL. Завершающий слеш срезается, а init и OpenEmail подставляют https:// перед голым хостом и http:// перед хостом на этой машине: localhost, адресом 127.x.x.x или ::1. Учётные данные никогда не отправляются по обычному http на любой другой хост, а 0.0.0.0 или [::] выбрасывает исключение при создании клиента, потому что это адреса, на которых сервер слушает, а не адреса для отправки запросов.
timeout30В секундах, на попытку, а не на вызов. Покрывает чтение тела, а не только заголовков. 0 отключает его. files.upload ждёт не меньше 600 секунд, если вызов не передаёт собственный timeout.
max_retries2Дополнительные попытки после первой, для вызовов, которые безопасно повторять. Задаётся на клиенте, а не на вызове.
http_clientновый httpx.ClientПередайте свой для прокси, собственных настроек TLS, смонтированного транспорта или тестового двойника: httpx.Client для OpenEmail, httpx.AsyncClient для AsyncOpenEmail. Закрытие клиента оставляет переданный вами клиент открытым.
headers{}Отправляются с каждым запросом.
user_agentopenemail-python/<version>Отправляются с каждым запросом.
disable_update_noticeFalseПропускает однократную за процесс проверку новой версии на 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 для этого запроса и ничего не оставляет на клиенте.

per_call_key.py
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, или новый.

raw_request.py
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=...).

temp_mail.py
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. Функция вызывается один раз на каждый вызов, а повторы этого вызова используют то, что она вернула, поэтому обновляйте токен внутри неё, когда срок его действия подходит к концу, и клиент никогда не придётся пересоздавать.

access_token.py
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-ключа код никогда не спрашивают.

step_up.py
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 говорит, что делать в каждом случае.