Валерий | AQA Engineer | Автотестирование на Python | REST, gRPC, GraphQL
1.46K subscribers
154 photos
149 videos
1 file
172 links
Сделаю из тебя крутого AQA инженера на Python.

• Преподаю лучшие тренинги по автоматизации тестирования API
• Senior Python developer | AQA lead, 7 лет в IT
Download Telegram
Media is too big
VIEW IN TELEGRAM
Валидация типов данных в JSON в автотестах очень важна, так как может помочь обнаружить ошибки на раннем этапе разработки и упростить как сам процесс тестирования, так и обслуживание кода в дальнейшем.

При работе с веб-сервисами и API-интерфейсами, которые используют передачу данных в формате JSON, часто требуется проверка, что запросы и ответы соответствуют ожидаемым форматам. В случае, когда формат JSON не соответствует ожидаемым типам данных, возможны ошибки, которые трудно обнаружить вручную, особенно в случае обработки большого объема данных.

Валидация типов данных в JSON позволяет:

- Проверить корректность формата данных в запросах и ответах, что может помочь предотвратить непредвиденные ошибки.

- Обнаружить ошибки в случае, если сервер возвращает данные в неправильном формате или структуре.

- Упростить разработку тестов, так как позволяет проверять формат данных автоматически вместо проверки вручную.

- Привести к сокращению времени отладки, что экономит время разработчиков.
💩23👍9
This media is not supported in your browser
VIEW IN TELEGRAM
В Python есть множество тестовых фреймворков для автоматизации тестирования, но вот три из них, которые используются чаще всего:

1. unittest – это стандартный тестовый фреймворк Python, который поставляется вместе с языком. Он предоставляет удобный способ организации, запуска и отчетности о тестах. В unittest тесты группируются в тестовые классы и выполняются с помощью методов assert*.

2. pytest – это более современный и удобный тестовый фреймворк, который также используется для автоматизации тестирования в Python. Он обладает простой и интуитивно понятный синтаксис, лучшей поддержкой параметризованных тестов и различных методов report-инга.

3. Behave – это фреймворк для BDD (Behavior Driven Development), который используется для автоматического тестирования приложений на Python. Он позволяет использовать естественный язык для написания тестов, что делает тесты более понятными для бизнес-аналитиков и заказчиков.
💩22👍9
Media is too big
VIEW IN TELEGRAM
Декоратор в Python - это специальная конструкция языка, которая позволяет изменять поведение функций или классов без изменения их исходного кода.

Декораторы в Python реализуются с помощью синтаксиса "@" перед определением функции или класса, к которым будет применяться декоратор.

Принцип работы декоратора заключается в том, что он принимает на вход функцию или класс, оборачивает их в другую функцию или класс, добавляя дополнительную функциональность, и возвращает измененную версию.

Декораторы используются, когда необходимо применить одни и те же изменения к нескольким функциям или классам. Они помогают избежать дублирования кода и обеспечивают простоту и читаемость кода.

TG-сообщество | Обучение |Отзывы
💩27👍10
Media is too big
VIEW IN TELEGRAM
Многие моменты из моей биографии были упущены.

Еще после университета я точно знал, что программирование это не мое.

IT профессию я выбирал так: "Главное, чтобы не надо было программировать".

К программированию меня склонил мой друг Деньчик , с первой айтишной работы. И вот куда это завело.

Посмотрим, что будет дальше, но желаю, чтобы каждый встретил своего Деньчика, который сможет раскрыть ваш потенциал.

Возможно, этим Деньчиком для вас стану я)

TG-сообщество | Обучение |Отзывы
🔥35💩29👍32
Media is too big
VIEW IN TELEGRAM
У каждого свои трудности в автоматизации, но самое сложное было на самом деле найти курс, который бы чему-то научил.

Применение полученных знаний также представляло некоторые сложности. Мой первый проект был десктопным приложением, и в нем не было ничего, что было привычно в автоматизации. Поэтому я писал различные скрипты для сравнения данных и работы с таблицами Excel, парсил логи.

Кстати, примерно в то же время я начал изучать библиотеки для автоматизации десктопных приложений, например, кликание по изображениям и работу с базами данных.

Алгоритмическое мышление также является отдельным аспектом. Я помню момент, когда мой мозг осознал, как правильно описывать условия. На тот момент я боролся с задачей с моего первого курса около недели, которая решалась всего лишь в 5-6 строк 🤣.

TG-сообщество | Обучение |Отзывы
💩20👍84
Как тестировать SQL-функции и как в будущем это автоматизировать?

Такой вопрос мне переадресовала Надя.

Я работал в финтехе на легаси-проекте, где вся логика была построена на хранимых процедурах. Самый большой минус в том, что тестирование на этом уровне не развито в отрасли и представляет довольно часто ряд костылей, одним из которых я поделюсь. Я пробовал писать тесты прямо на синтаксисе SQL, где результаты просто выводились обычными принтами True или False (прошел/не прошел). Это было интересно, но помогало только решению конкретной задачи, и данный подход был нерасширяемым.

Ну ладно, хватит слов, давайте расскажу свой костыль. На мой взгляд, удобнее всего это сделать на Python, на примере MS SQL Server.

Погнали!
С помощью pyodbc сначала я пишу простой клиент, который будет логгировать каждый запрос и преобразовывать его в список словарей для удобства работы с данными по колонкам.

import pyodbc
import os
import structlog

class BaseClient:
def __init__(self, server, database, user, password, autocommit=False, logging_level=logging.DEBUG):
self.log = structlog.get_logger(self.__class__.__name__).bind(service='db')
system_name = os.name
if system_name == 'posix':
driver = 'ODBC Driver 17 for SQL Server'
else:
driver = 'SQL Server'

connection_string = f'DRIVER={{{driver}}};' \
f'SERVER={server};' \
f'DATABASE={database};' \
f'UID={user};' \
f'PWD={password};'\
f'Trusted_Connection={trusted_connection}'

with pyodbc.connect(connection_string, autocommit=self.autocommit, timeout=30) as connect:
self.connect = connect
self.cursor = connect.cursor()

def logged_query(self, query):
log = self.log.bind(request_id=str(uuid.uuid4()))
print(f'\n{query}')
log.msg(
'request',
caller=inspect.stack()[1][3],
func_name=inspect.stack()[0][3]
)
result = self._to_dict(self.cursor.execute(query))
if result:
log.msg(
'response',
result=result,
)
return result

@staticmethod
def _to_dict(cursor):
try:
columns = [column[0] for column in cursor.description]
except TypeError:
return []
else:
dataset = [dict(zip(columns, row)) for row in cursor.fetchall()]
return dataset

После того как базовый клиент написан, можно уже реализовать класс с вызовами хранимых процедур. Можно отнаследовать его от базового класса, чтобы использовать его методы и реализовать вызов.
class ApiTestProcedures(BaseClient):
def some_procedure_execute(self, param1, param2):
"""
SET NOCOUNT ON - это писать обязательно, иначе не будет работать))
"""
query = f"""
SET NOCOUNT ON;
EXEC [test].[SomeProcedure]
@Parameter1 = {param1}
,@Parameter2 = {param2}"""
postings = self.logged_query(query)
return postings

Вся подготовка сделана теперь можно писать тест, тут уже как обычно.
@pytest.fixture(scope="session")
def db_client():
client = ApiTestProcedures("тут параметры подключения")
yield client
client.close()


def test_some_procedure(db_client):
result = db_client.some_procedure_execute(param1=1, param2=2)
for row in result:
assert row["column_name"] == "column_value"


Вы можете использовать другие библиотеки для сериализации данных, например, SQLAlchemy или Pandas для удобства работы и анализа данных. Для этого нужно вместо метода _to_dict реализовать свои функции упаковки данных.

Пишите вопросы в тред, если возникнут интересные темы, постараюсь ответить в новом посте!

TG-сообщество | Обучение |Отзывы
💩25🔥5👍2
Я хотел бы поделиться с вами некоторыми языковыми конструкциями, которые помогут сократить ваш код и сделать его более читаемым.

В этом посте я расскажу о нескольких приемах, таких как:

- Избежание использования оператора else.
- Применение тернарных операторов.
- Массовое присвоение значений.

Эти простые изменения могут улучшить вашу кодовую базу! Давайте рассмотрим примеры.

1. Не использовать оператор else

Например, вместо использования оператора else в некоторых случая его можно просто игнорировать:
def foo(param):
if param == 1:
return 1
else:
return 2


Данную функцию можно написать так:
def foo(param1):
if param == 1:
return 1
return 2


Логика функции в данном случае не меняется, но сокращается код, при этом не ухудшается его читаемость.

2. Тернарные операторы

Например вместо блока условных конструкций можно использовать тернарный оператор:
value = None

if param == 1:
value = 1
else:
value = 2


с использованием тернарного оператора наша конструкция сократится до одной строчки:
value = 1 if param == 1 else 2


Есть еще тернарный оператор or, который тоже помогает сократить код в некоторых случаях:
dataset = (1, 2, 3, 4)

value = dataset if dataset else "Данные из базы данных не были получены"


указанный пример с использованием оператора or можно написать так:
dataset = (1, 2, 3, 4)
value = dataset or "Данные из базы данных не были получены"


! Есть еще один интересный способ применения тернарного оператора — вместе с кортежами !
is_raining = True
weather = ("sunny", "rainy")[is_raining]
print("Today's weather is", weather)

Значения True и False конвертируются в 1 и 0, и соответственно выбираются значения из кортежа по этим индексам.

3. Массовое присвоение значений

Несмотря на то, что такой способ более читаемый и чистый:
a = 1
b = 2


В некоторых случаях можно использовать массовое присвоение значений, например:
a, b = 1, 2


Есть и другие способы использования сокращения и оптимизации кода, я расскажу о них если вспомню =))

TG-сообщество | Обучение |Отзывы
💩34🔥10👍2
Как использовать try-except?

Как вы знаете я обучаю автоматизации тестирования и в одном из домашних заданий ученик использовал такую конструкцию:

def test_register_new_user(db, api, login, email):
# Здесь какая то логика до
try:
dataset_delete = db.delete_user_by_login(login=login)
api.mailhog.delete_message_by_login(login=login)
assert len(dataset_delete) == 0
except:
pass

api.account.register_new_user(
login=login,
email=email
)
# Здесь какая то логика после


Что плохо в этом коде?
- Первое, что бросается в глаза - это использование просто оператора except без уточнений, в таком случае в данном блоке кода, мы будем перехватывать любые исключения, которые возникли в процессе его выполнения, будь это падение подключения к базе данных или то, что не смогли удалить сообщение нужного юзера из mailhog.
- Второе, использование оператора assert внутри try-except с не уточненным названием исключения, то есть даже если он и сработает мы его перехватим и тест пойдет дальше и наш assert в таком случае бесполезен.

К чему это может привести:
- Ложно положительный результат выполнения теста, потому, что ошибки, которые возникли мы их просто игнорировали
- Тяжелая локализация причины падения теста во время его выполнения, по той же причине, что все ошибки мы игнорировали и без тщательного анализа логов(если такие есть) мы не узнаем какие именно ошибки произошли.


Как сделать более правильно?
- Во первых нужно использовать try-except с уточненным названием исключения
- Во вторых оборачивать не весь блок кода, а максимально атомарную функцию в которой мы ожидаем исключение
- В третьих вынести блок try-except из теста
- В четвертых убрать assert если нам важно, чтобы тест прошел успешно, независимо от того удалось удалить сообщение или нет


# db.py
def delete_user_by_login(self, login):
# метод удаления юзера в классе базы данных
try:
return self.execute(f"DELETE FROM users WHERE login = '{login}'")
except TypeError as e:
log.message(f'В базе данных не найден пользователь с логином {login} {e}')
return []


# test_user.py
def test_register_new_user(db, api, login, email):
# Здесь какая то логика до
dataset_delete = db.delete_user_by_login(login=login)
api.mailhog.delete_message_by_login(login=login)
api.account.register_new_user(
login=login,
email=email
)
# Здесь какая то логика после

TG-сообщество | Обучение |Отзывы
💩50🔥8
Channel name was changed to «AQA Engineer | Автотестирование на Python | REST, gRPC, GraphQL»
Задача с собеседования:
Поиск индекса сбалансированного массива

У меня есть друг, который регулярно ходит на собеседования. Естественно, мне всегда интересно, какие задачки там появляются, и потом я стараюсь решить их самостоятельно без помощи ChatGPT.

Большинство задач не слишком сложные и не требуют, чтобы акцентировать на них внимание, но некоторые интересные я отмечаю.

Например:
# Дано: Массив int 
# Результат: Индекс - сумма чисел слева = сумме чисел справа
# Если не существует - вернуть -1
#
# Input: nums = [1, 7, 3, 6, 5, 6]
# Output: 3

То есть, число с индексом 3 (это 6) делит список на две части, суммы которых равны, 1 + 7 + 3 = 11 и 5 + 6 = 11, а 6 делит их пополам.

То есть, ещё раз: цель — найти индекс числа, которое делит список на две части, если сумма чисел этих двух частей равна. Если такого числа нет, то нужно выводить -1, а если число в списке равно -1, то следует выбрасывать исключение.

Скажу честно, я не сразу вкурил, что от меня требуется, и поэтому приступил к задаче только после того, как понял её условия.

Первая мысль, которая пришла мне в голову, — это одновременно итерироваться с разных концов списка, но это выглядело как полная дичь, и я начал думать дальше.

В итоге я решил использовать срезы (slice), и я буду делать срезы по индексу элемента в списке. Тем самым я буду делить список на две части и считать сумму элементов этих частей: от 0 до i — это первая часть, а от i + 1 до конца списка — вторая часть.

Чтобы предотвратить преждевременный выход из цикла и возвращать -1 только после того, как мы проверим все элементы списка, я решил создать переменную index_flag, которая по умолчанию равна -1 и изменяется только если наша цель по поиску индекса выполнена.

Итоговый код получился таким:
def search_index(list_):
index_flag = -1
for i in range(len(list_)):
if i < 0:
raise ValueError("Число в списке должно быть больше или равно 0")
left_list = sum(list_[:i])
right_list = sum(list_[i + 1:])
if left_list == right_list:
index_flag = i
return index_flag


print(search_index(list_))


Ну и конечно мне было интересно было узнать, какое решение предложит ChatGPT.
Вот его ответы:
def find_balance_index(arr):
total_sum = sum(arr)
left_sum = 0

for i in range(len(arr)):
if left_sum == total_sum - arr[i]:
return i

left_sum += arr[i]
total_sum -= arr[i]

return -1 # Если индекс не найден


arr = [1, 7, 3, 6, 5, 6]
balance_index = find_balance_index(arr)
print("Индекс сбалансированного массива:", balance_index)


и такое:
def find_balance_index(arr):
total_sum = sum(arr)

if any(num <= 0 for num in arr):
raise ValueError("Массив должен содержать только положительные числа.")

for i, num in enumerate(arr):
total_sum -= num
if total_sum == 0:
return i
return -1


arr = [1, 7, 3, 6, 5, 6]
try:
result = find_balance_index(arr)
print("Индекс сбалансированного массива:", result)
except ValueError as e:
print("Ошибка:", e)


Мое решение мне кажется гораздо понятнее, но тут вы уже решайте сами.
В любом случае, решение таких задачек неплохо развивает алгоритмическое мышление.

А какие задачки на собесах встречали вы?

TG-сообщество | Обучение |Отзывы
💩33👍8
Неочевидный блок else, в блоке try-except.

Я встречал такие вопросы на собеседовании, когда просили написать полную конструкцию try-except.
Довольно часто про него забывают или не знают, поэтому хочу про него рассказать.

Что это за блок и как его использовать?

Например:
try:
connection = DBConnection()
except ConnectionError as e:
...
else:
dataset = connection.execute("SELECT * FROM table")
finally:
connection.close()


В данном случае блок else выполнится, только если пройдет успешное выполнение try, иначе будет выполнен блок except и в качестве финального блока будет выполнен finally, который закроет соединение.

Можно ли это написать по другому?
Да можно, например, так:

try:
connection = DBConnection()
dataset = connection.execute("SELECT * FROM table")
except ConnectionError as e:
...
finally:
connection.close()


Но в данному случае, область перехвата ошибки расширяется и на сам запрос, нужно это или нет зависит от ситуации. В любом случае это расширяет область локализации потенциальных ошибок.

Еще можно написать так:

try:
connection = DBConnection()
except ConnectionError as e:
...
dataset = connection.execute("SELECT * FROM table")
connection.close()


Тоже имеет место быть, но успешное закрытие соединения произойдет только если успешно выполнится запрос, что так же накладывает свои риски.

Мне нравится использовать этот блок, на мой взгляд, он делает код более читаемым и прозрачным.

А вы знали про этот блок?
да - 👍 нет - 🔥

TG-сообщество | Обучение |Отзывы
💩45🔥6👍2
Способы построения тестовой архитектуры API тестов.

ЧАСТЬ 1. Handler Pattern


Если с UI тестами, все плюс минус понятно, самый частоиспользуемый это PageObject.

То архитектуру построения API тестов я встречал самую разную.

Расскажу некоторые подходы к построению архитектуры тестов, которые встречал.

Скажу сразу, что названия этим подходам я присвоил самостоятельно.

Handler Pattern.
Суть заключается в том, что вспомогательные методы, которые нужны для конкретного теста хранятся внутри класса тестируемого метода.

Вот упрощенный пример:
# handler_name_logic.py

from modules.api_name.handler_name import HandlerName
from modules.api_name.another_handler_name import AnotherHandlerName
from modules.api_name.models import RequestModel

class HandlerNameLogic:

def __init__(self):
self.request = HandlerName
self.model = RequestModel
self.another_request = AnotherHandlerName
self.another_model = AnotherRequestModel

def simple_request(self, **kwargs):
return self.request(self.model(**kwargs))

def prepare_data(self):
return self.request.another_request(self.another_model("some value"))

@staticmethod
def check_result(actual_result, expected_result):
assert actual_result == expected_result, "Something went wrong"

# test_handler_name.py

from handler_name_logic import HandlerNameLogic
handler = HandlerNameLogic()

def test_handler_name():
prepared_data = handler.prepare_data()
actual_result = handler.simple_request(**prepared_data)
expected_result = "some value"
handler.check_result(actual_result, expected_result)


Плюсы такого подхода:
1. При большом количестве тестов, все побито на маленькие модули в которых легче разобраться
2. Вся вспомогательная логика лежит рядом, это помогает не скакать по всему проекту и разным модулям.
3. Хорошо смотрится в смоук тестах без сложных интеграционных сценариев, где идет взамодействие нескольких ручек.

Минусы:
1. Непонятно где писать код, который переиспользуется, с одной стороны вроде бы в классе логики хэндлера, но из этого возникает следующая проблема.
2. Неочевидно откуда делать импорты уже реализованных функций, потому, что они размазаны по классам логики, не зная проекта скорее всего возникнет дублирование кода.
3. Сложная архитектура построения фреймворка, вручную поддерживать такой проект сложно, возникает много обвязок и абстракций для конвертации "метода в класс"
4. Если общие методы выносить в общие классы помощники, плюсы такого подхода смываются, потому, что логика размазывается еще и на классы помощники.

Я рассказал первый из подходов, мне он не очень нравится, но вы можете попробовать обкатать у себя на проекте и оценить.

TG-сообщество | Обучение |Отзывы
💩31👍12
Валерий | AQA Engineer | Автотестирование на Python | REST, gRPC, GraphQL
Вторая часть бесплатных видео и необходимого минимума. Смотреть по порядку, уроки пронумерованы! Затупил, надо было сделать обложки. Потом пофикшу) А пока, самое первое видео это последнее, а начиная со второго идут по порядку)
Напомню, что у меня есть бесплатные видео, которые я записывал некоторое время назад, если вы совсем нулевые имеет смысл посмотреть.

Естественно, тем кто уже знает базу, в этом необходимости нет, хотя иногда некоторые находят там что-то интересное)

TG-сообщество | Обучение |Отзывы
💩356👍1
Способы проектирования архитектуры API тестов.

ЧАСТЬ 2. Helper Pattern

В предыдущей части, мы рассматривали условный Handler Pattern, который должен был решить проблемы паттерна о котором мы будем говорить в этой части.

Повторюсь в рамках постов про архитектуру, названия паттернам я придумал самостоятельно, не отрицаю, что где-то на просторах интернета есть более правильные названия.

Так вот, в этом подходе мы выносим необходимую подготовку в классы помощники, тем самым появляется удобный способ переиспользования ранее написанных методов.

Например:

Сначала реализуются классы для необходимых API клиентов.
# first_client.py

class FirstApiClient:
def some_post_handler(self, **kwargs) -> FirstObject:
# Логика отправки запроса
return FirstObject()

def some_put_handler(self, **kwargs) -> PutObject:
# Логика отправки запроса
return PutObject()


# second_client.py

class SecondApiClient:
def some_get_handler(self, **kwargs) -> Any:
# Логика отправки запроса
return SecondObject()


Дальше пишем класс помощник, в котором реализуются методы упрощающие интерфейс работы с API клиентами, или предоставляющие более простой интерфейс объединяющий несколько шагов.

# helper.py
class SomeHelper:
def __init__(self, first_api_client, second_api_client):
self.first_api_client = first_api_client
self.second_api_client = second_api_client

def prepare_data(self) -> SomeValue:
first_object = self.first_api_client.some_post_handler(**kwargs)
second_object =self.second_api_client.some_get_handler(**kwargs)
return SomeValue(**first_object, **second_object)



Теперь производим инициализацию клиентов, и класса помощника где фикстуры являются довольно удобным способом.

python      
# conftest.py

@pytest.fixture
def first_api_client() -> FirstApiClient:
return FirstApiClient()

@pytest.fixture
def second_api_client() -> SecondApiClient:
return SecondApiClient()

@pytest.fixture
def helper(first_api_client, second_api_client) -> SomeHelper:
return SomeHelper(first_api_client, second_api_client)


Сам тест у нас выглядит так.

python      
# test_some_put_handler.py

def test_some_put_handler(helper):
data = helper.prepare_data()
response = helper.first_api_client.some_put_handler(**data)
assert response.json()["value"] == "some_value"



Плюсы:
1. Удобное переиспользование функций, достаточно использовать необходимый класс помощник
2. Удобное масштабирование, мы можем делать классы помощники в зависимости от доменной области например UserHelper, ProductHelper, OrderHelper и т.д.
3. Удобная сборка, класс помощник можно собирать, как конструктор передавая нужные клиенты
4. Лаконичные и читаемые тесты, вся логика инкапсулируется внутри классов помощников и оберток
5. Модульность, понятность и прозрачность кода и архитектуры

Минусы:
1. На большом проекте классы помощники могут разрастаться до нескольких тысяч строк кода
2. Паттерн порождает большое количество фикстур (Но эта проблема решаема)


Исходя из количества плюсов, которые я написал наверняка можно понять, что это мой любимый паттерн и его я использую на проектах.

Пишите минусы которые я не написал в тредик, буду благодарен и добавлю в пост)

TG-сообщество | Обучение |Отзывы
💩305
Неочевидный блок else.


Давайте немного разбавим посты про архитектуру просто приколюшками, которые некоторые не знают.

В этом посте речь пойдет про неочевидный блок else, о котором я узнал из книжки, ее название я уже не помню )))
Но обычно на курсах про него не говорят.

Так вот это блок else в в цикле for.
Вы спросите нафига он там нужен? А я сейчас расскажу. Представим, что при создании сущности у нас есть определенный временной лаг,
такую ситуацию мы рассматриваем с учениками на курсе.

Есть следующий сценарий:
1. Отправляем запрос на создание пользователя
2. Сообщение попадает в rabbitMQ (очередь)
3. Очередь обрабатывается, создается активационный токен, который приходит письмом на почту
4. Активируем пользователя

На шагах 2-3 возникает временной лаг, поэтому может возникнуть ситуация, когда мы в автотесте пытаемся прочитать письмо, которое еще не пришло, тем самым мы получим ошибку.

Чтобы этого избежать, мы можем реализовать явные ожидания следующим образом:

def wait_for_email(user_name):
for _ in range(10):
response = requests.get('https://mail-server.com/emails')

# проверяем, что появилось письмо для нужного пользователя
for email in response.json():
if user_name in email['body']:
return email
# если письма не нашли то ждем еще 1 секунду и повторяем цикл
time.sleep(1)
else:
# если в течение 10 секунд ничего не появилось, то бросаем исключение
raise TimeoutError('Не смогли получить письмо в течение 10 секунд!')






# Создаем нового пользователя
requests.post('https://example.com/account', json={"username": "username", "password": "password"})

# ждем пока появится письмо в течение 10 попыток
email = wait_for_email('username')

# какие-то дальнейшие действия активация пользователя и тп.


Таким образом, мы можем выяснить, что письмо появилось в течение 10 секунд, и если это не так, то бросаем исключение.

Можно ли это переписать по другому?
Да можно например так:
def wait_for_email(user_name):
attempt = 0
while True:
response = requests.get('https://mail-server.com/emails')
for email in response.json():
if user_name in email['body']:
return email
if attempt == 10:
# если в течение 10 секунд ничего не появилось, то бросаем исключение
raise TimeoutError('Не смогли получить письмо в течение 10 секунд!')

# если письма не нашли то ждем еще 1 секунду и повторяем цикл
time.sleep(1)
attempt += 1



Или так, но такой код имеет смысл только если после выброса исключений нет других инструкций.
def wait_for_email(user_name):
while attempt := 0 <= 10:
response = requests.get('https://mail-server.com/emails')
for email in response.json():
if user_name in email['body']:
return email
attempt += 1
time.sleep(1)

# если в течение 10 секунд ничего не появилось, то бросаем исключение
raise TimeoutError('Не смогли получить письмо в течение 10 секунд!')
# то, что будет написано здесь никогда не выполнится


Вы можете писать любым из предложенных способов, но мне нравится использовать первый, на мой взгляд он самый легкочитаемый и понятный.

А вы знали про блок else в цикле for?
да - 👍 нет - 🔥

TG-сообщество | Обучение |Отзывы
💩27👍5🔥2🤗1