├── .gitignore ├── Procfile ├── README.md ├── exceptions.py ├── homework.py ├── pytest.ini ├── requirements.txt ├── setup.cfg └── tests ├── conftest.py ├── fixtures └── fixture_data.py ├── test_bot.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Папки, создаваемые средой разработки 132 | .idea 133 | .DS_Store 134 | .AppleDouble 135 | .LSOverride 136 | 137 | *.sublime-project 138 | *.sublime-workspace 139 | 140 | .vscode/ 141 | *.code-workspace 142 | 143 | # Local History for Visual Studio Code 144 | .history/ 145 | 146 | .mypy_cache 147 | 148 | # папки со статикой и медиа 149 | static/ 150 | posts/static/ 151 | media/ 152 | 153 | log.txt/ 154 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: python homework.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Telegram-bot 2 | 3 | ``` 4 | Телеграм-бот для отслеживания статуса проверки домашней работы на Яндекс.Практикум. 5 | Присылает сообщения, когда статус изменен - взято в проверку, есть замечания, зачтено. 6 | ``` 7 | 8 | ### Технологии: 9 | - Python 3.9 10 | - python-dotenv 0.19.0 11 | - python-telegram-bot 13.7 12 | 13 | ### Как запустить проект: 14 | 15 | Клонировать репозиторий и перейти в него в командной строке: 16 | 17 | ``` 18 | git clone git@github.com:PashkaVRN/homework_bot.git 19 | ``` 20 | 21 | ``` 22 | cd homework_bot 23 | ``` 24 | 25 | Cоздать и активировать виртуальное окружение: 26 | 27 | ``` 28 | python -m venv env 29 | ``` 30 | 31 | ``` 32 | source env/bin/activate 33 | ``` 34 | 35 | Установить зависимости из файла requirements.txt: 36 | 37 | ``` 38 | python -m pip install --upgrade pip 39 | ``` 40 | 41 | ``` 42 | pip install -r requirements.txt 43 | ``` 44 | 45 | Записать в переменные окружения (файл .env) необходимые ключи: 46 | - токен профиля на Яндекс.Практикуме 47 | - токен телеграм-бота 48 | - свой ID в телеграме 49 | 50 | 51 | Запустить проект: 52 | 53 | ``` 54 | python homework.py 55 | ``` 56 | -------------------------------------------------------------------------------- /exceptions.py: -------------------------------------------------------------------------------- 1 | class NotForSending(Exception): 2 | """Не для пересылки в телеграм.""" 3 | pass 4 | 5 | 6 | class ProblemDescriptions(Exception): 7 | """Описания проблемы.""" 8 | pass 9 | 10 | 11 | class InvalidResponseCode(Exception): 12 | """Не верный код ответа.""" 13 | pass 14 | 15 | 16 | class ConnectinError(Exception): 17 | """Не верный код ответа.""" 18 | pass 19 | 20 | 21 | class EmptyResponseFromAPI(NotForSending): 22 | """Пустой ответ от API.""" 23 | pass 24 | 25 | 26 | class TelegramError(NotForSending): 27 | """Ошибка телеграма.""" 28 | pass 29 | -------------------------------------------------------------------------------- /homework.py: -------------------------------------------------------------------------------- 1 | import telegram 2 | import time 3 | import requests 4 | import logging 5 | import sys 6 | import os 7 | import exceptions 8 | from dotenv import load_dotenv 9 | from http import HTTPStatus 10 | 11 | load_dotenv() 12 | 13 | PRACTICUM_TOKEN = os.getenv('PRACTICUM_TOKEN') 14 | TELEGRAM_TOKEN = os.getenv('TELEGRAM_TOKEN') 15 | TELEGRAM_CHAT_ID = os.getenv('TELEGRAM_CHAT_ID') 16 | 17 | RETRY_TIME = 600 18 | ENDPOINT = 'https://practicum.yandex.ru/api/user_api/homework_statuses/' 19 | HEADERS = {'Authorization': f'OAuth {PRACTICUM_TOKEN}'} 20 | 21 | 22 | HOMEWORK_STATUSES = { 23 | 'approved': 'Работа проверена: ревьюеру всё понравилось. Ура!', 24 | 'reviewing': 'Работа взята на проверку ревьюером.', 25 | 'rejected': 'Работа проверена: у ревьюера есть замечания.' 26 | } 27 | 28 | 29 | def send_message(bot, message): 30 | """Отправляет сообщение в Telegram чат.""" 31 | try: 32 | logging.info('Начало отправки') 33 | bot.send_message( 34 | chat_id=TELEGRAM_CHAT_ID, 35 | text=message, 36 | ) 37 | except exceptions.TelegramError as error: 38 | raise exceptions.TelegramError( 39 | f'Не удалось отправить сообщение {error}') 40 | else: 41 | logging.info(f'Сообщение отправлено {message}') 42 | 43 | 44 | def get_api_answer(current_timestamp): 45 | """Получить статус домашней работы.""" 46 | timestamp = current_timestamp or int(time.time()) 47 | params_request = { 48 | 'url': ENDPOINT, 49 | 'headers': HEADERS, 50 | 'params': {'from_date': timestamp}, 51 | } 52 | try: 53 | logging.info( 54 | 'Начало запроса: url = {url},' 55 | 'headers = {headers},' 56 | 'params = {params}'.format(**params_request)) 57 | homework_statuses = requests.get(**params_request) 58 | if homework_statuses.status_code != HTTPStatus.OK: 59 | raise exceptions.InvalidResponseCode( 60 | 'Не удалось получить ответ API, ' 61 | f'ошибка: {homework_statuses.status_code}' 62 | f'причина: {homework_statuses.reason}' 63 | f'текст: {homework_statuses.text}') 64 | return homework_statuses.json() 65 | except Exception: 66 | raise exceptions.ConnectinError( 67 | 'Не верный код ответа параметры запроса: url = {url},' 68 | 'headers = {headers},' 69 | 'params = {params}'.format(**params_request)) 70 | 71 | 72 | def check_response(response): 73 | """Проверить валидность ответа.""" 74 | logging.debug('Начало проверки') 75 | if not isinstance(response, dict): 76 | raise TypeError('Ошибка в типе ответа API') 77 | if 'homeworks' not in response or 'current_date' not in response: 78 | raise exceptions.EmptyResponseFromAPI('Пустой ответ от API') 79 | homeworks = response.get('homeworks') 80 | if not isinstance(homeworks, list): 81 | raise KeyError('Homeworks не является списком') 82 | return homeworks 83 | 84 | 85 | def parse_status(homework): 86 | """Распарсить ответ.""" 87 | if 'homework_name' not in homework: 88 | raise KeyError('В ответе отсутсвует ключ homework_name') 89 | homework_name = homework.get('homework_name') 90 | homework_status = homework.get('status') 91 | if homework_status not in HOMEWORK_STATUSES: 92 | raise ValueError(f'Неизвестный статус работы - {homework_status}') 93 | return( 94 | 'Изменился статус проверки работы "{homework_name}" {verdict}' 95 | ).format( 96 | homework_name=homework_name, 97 | verdict=HOMEWORK_STATUSES[homework_status] 98 | ) 99 | 100 | 101 | def check_tokens(): 102 | """Проверка доступности переменных окружения.""" 103 | return all([TELEGRAM_TOKEN, PRACTICUM_TOKEN, TELEGRAM_CHAT_ID]) 104 | 105 | 106 | def main(): 107 | """Основная логика работы бота.""" 108 | if not check_tokens(): 109 | logging.critical('Отсутствует необходимое кол-во' 110 | ' переменных окружения') 111 | sys.exit('Отсутсвуют переменные окружения') 112 | bot = telegram.Bot(token=TELEGRAM_TOKEN) 113 | current_timestamp = int(time.time()) 114 | current_report = { 115 | 'name': '', 116 | 'output': '' 117 | } 118 | prev_report = current_report.copy() 119 | while True: 120 | try: 121 | response = get_api_answer(current_timestamp) 122 | current_timestamp = response.get( 123 | 'current_data', current_timestamp) 124 | new_homeworks = check_response(response) 125 | if new_homeworks: 126 | homework = new_homeworks[0] 127 | current_report['name'] = homework.get('homework_name') 128 | current_report['output'] = homework.get('status') 129 | else: 130 | current_report['output'] = 'Нет новых статусов работ.' 131 | if current_report != prev_report: 132 | send = f' {current_report["name"]}, {current_report["output"]}' 133 | send_message(bot, send) 134 | prev_report = current_report.copy() 135 | else: 136 | logging.debug('Статус не поменялся') 137 | except exceptions.NotForSending as error: 138 | message = f'Сбой в работе программы: {error}' 139 | logging.error(message) 140 | except Exception as error: 141 | message = f'Сбой в работе программы: {error}' 142 | current_report['output'] = message 143 | logging.error(message) 144 | if current_report != prev_report: 145 | send_message(bot, message) 146 | prev_report = current_report.copy 147 | finally: 148 | time.sleep(RETRY_TIME) 149 | 150 | 151 | if __name__ == '__main__': 152 | logging.basicConfig( 153 | level=logging.INFO, 154 | format=( 155 | '%(asctime)s, %(levelname)s, Путь - %(pathname)s, ' 156 | 'Файл - %(filename)s, Функция - %(funcName)s, ' 157 | 'Номер строки - %(lineno)d, %(message)s' 158 | ), 159 | handlers=[logging.FileHandler('log.txt', encoding='UTF-8'), 160 | logging.StreamHandler(sys.stdout)]) 161 | main() 162 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs = env/* 3 | addopts = -vv -p no:cacheprovider -p no:warnings 4 | testpaths = tests/ 5 | python_files = test_*.py 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flake8==3.9.2 2 | flake8-docstrings==1.6.0 3 | pytest==6.2.5 4 | python-dotenv==0.19.0 5 | python-telegram-bot==13.7 6 | requests==2.26.0 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | W503, 4 | D100, 5 | D205, 6 | D401 7 | filename = 8 | ./homework.py 9 | exclude = 10 | tests/, 11 | venv/, 12 | env/ 13 | max-complexity = 10 -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os.path import abspath, dirname 3 | 4 | root_dir = dirname(dirname(abspath(__file__))) 5 | sys.path.append(root_dir) 6 | 7 | pytest_plugins = [ 8 | 'tests.fixtures.fixture_data' 9 | ] 10 | -------------------------------------------------------------------------------- /tests/fixtures/fixture_data.py: -------------------------------------------------------------------------------- 1 | import random 2 | from datetime import datetime 3 | 4 | import pytest 5 | 6 | 7 | @pytest.fixture 8 | def random_timestamp(): 9 | left_ts = 1000198000 10 | right_ts = 1000198991 11 | return random.randint(left_ts, right_ts) 12 | 13 | 14 | @pytest.fixture 15 | def current_timestamp(): 16 | return datetime.now().timestamp() 17 | 18 | 19 | @pytest.fixture 20 | def api_url(): 21 | return 'https://practicum.yandex.ru/api/user_api/homework_statuses/' 22 | -------------------------------------------------------------------------------- /tests/test_bot.py: -------------------------------------------------------------------------------- 1 | import os 2 | from http import HTTPStatus 3 | 4 | import requests 5 | import telegram 6 | import utils 7 | 8 | 9 | class MockResponseGET: 10 | 11 | def __init__(self, url, params=None, random_timestamp=None, 12 | current_timestamp=None, http_status=HTTPStatus.OK, **kwargs): 13 | assert ( 14 | url.startswith( 15 | 'https://practicum.yandex.ru/api/user_api/homework_statuses' 16 | ) 17 | ), ( 18 | 'Проверьте, что вы делаете запрос на правильный ' 19 | 'ресурс API для запроса статуса домашней работы' 20 | ) 21 | assert 'headers' in kwargs, ( 22 | 'Проверьте, что вы передали заголовки `headers` для запроса ' 23 | 'статуса домашней работы' 24 | ) 25 | assert 'Authorization' in kwargs['headers'], ( 26 | 'Проверьте, что в параметры `headers` для запроса статуса ' 27 | 'домашней работы добавили Authorization' 28 | ) 29 | assert kwargs['headers']['Authorization'].startswith('OAuth '), ( 30 | 'Проверьте, что в параметрах `headers` для запроса статуса ' 31 | 'домашней работы Authorization начинается с OAuth' 32 | ) 33 | assert params is not None, ( 34 | 'Проверьте, что передали параметры `params` для запроса ' 35 | 'статуса домашней работы' 36 | ) 37 | assert 'from_date' in params, ( 38 | 'Проверьте, что в параметрах `params` для запроса статуса ' 39 | 'домашней работы передали `from_date`' 40 | ) 41 | assert params['from_date'] == current_timestamp, ( 42 | 'Проверьте, что в параметрах `params` для запроса статуса ' 43 | 'домашней работы `from_date` передаете timestamp' 44 | ) 45 | self.random_timestamp = random_timestamp 46 | self.status_code = http_status 47 | 48 | def json(self): 49 | data = { 50 | "homeworks": [], 51 | "current_date": self.random_timestamp 52 | } 53 | return data 54 | 55 | 56 | class MockTelegramBot: 57 | 58 | def __init__(self, token=None, random_timestamp=None, **kwargs): 59 | assert token is not None, ( 60 | 'Проверьте, что вы передали токен бота Telegram' 61 | ) 62 | self.random_timestamp = random_timestamp 63 | 64 | def send_message(self, chat_id=None, text=None, **kwargs): 65 | assert chat_id is not None, ( 66 | 'Проверьте, что вы передали chat_id= при отправке ' 67 | 'сообщения ботом Telegram' 68 | ) 69 | assert text is not None, ( 70 | 'Проверьте, что вы передали text= при отправке ' 71 | 'сообщения ботом Telegram' 72 | ) 73 | return self.random_timestamp 74 | 75 | 76 | class TestHomework: 77 | HOMEWORK_STATUSES = { 78 | 'approved': 'Работа проверена: ревьюеру всё понравилось. Ура!', 79 | 'reviewing': 'Работа взята на проверку ревьюером.', 80 | 'rejected': 'Работа проверена: у ревьюера есть замечания.' 81 | } 82 | ENV_VARS = ['PRACTICUM_TOKEN', 'TELEGRAM_TOKEN', 'TELEGRAM_CHAT_ID'] 83 | for v in ENV_VARS: 84 | try: 85 | os.environ.pop(v) 86 | except KeyError: 87 | pass 88 | try: 89 | import homework 90 | except KeyError as e: 91 | for arg in e.args: 92 | if arg in ENV_VARS: 93 | assert False, ( 94 | 'Убедитесь, что при запуске бота, проверяете наличие ' 95 | 'переменных окружения, и при их отсутствии происходит ' 96 | 'выход из программы `SystemExit`\n' 97 | f'{repr(e)}' 98 | ) 99 | else: 100 | raise 101 | except SystemExit: 102 | for v in ENV_VARS: 103 | os.environ[v] = '' 104 | 105 | def test_check_tokens_false(self): 106 | for v in self.ENV_VARS: 107 | try: 108 | os.environ.pop(v) 109 | except KeyError: 110 | pass 111 | 112 | import homework 113 | 114 | for v in self.ENV_VARS: 115 | utils.check_default_var_exists(homework, v) 116 | 117 | homework.PRACTICUM_TOKEN = None 118 | homework.TELEGRAM_TOKEN = None 119 | homework.TELEGRAM_CHAT_ID = None 120 | 121 | func_name = 'check_tokens' 122 | utils.check_function(homework, func_name, 0) 123 | tokens = homework.check_tokens() 124 | assert not tokens, ( 125 | 'Проверьте, что при отсутствии необходимых переменных окружения, ' 126 | f'функция {func_name} возвращает False' 127 | ) 128 | 129 | def test_check_tokens_true(self): 130 | for v in self.ENV_VARS: 131 | try: 132 | os.environ.pop(v) 133 | except KeyError: 134 | pass 135 | 136 | import homework 137 | 138 | for v in self.ENV_VARS: 139 | utils.check_default_var_exists(homework, v) 140 | 141 | homework.PRACTICUM_TOKEN = 'sometoken' 142 | homework.TELEGRAM_TOKEN = '1234:abcdefg' 143 | homework.TELEGRAM_CHAT_ID = 12345 144 | 145 | func_name = 'check_tokens' 146 | utils.check_function(homework, func_name, 0) 147 | tokens = homework.check_tokens() 148 | assert tokens, ( 149 | 'Проверьте, что при наличии необходимых переменных окружения, ' 150 | f'функция {func_name} возвращает True' 151 | ) 152 | 153 | def test_bot_init_not_global(self): 154 | import homework 155 | 156 | assert not (hasattr(homework, 'bot') and isinstance(getattr(homework, 'bot'), telegram.Bot)), ( 157 | 'Убедитесь, что бот инициализирован только в main()' 158 | ) 159 | 160 | def test_logger(self, monkeypatch, random_timestamp): 161 | def mock_telegram_bot(*args, **kwargs): 162 | return MockTelegramBot(*args, random_timestamp=random_timestamp, **kwargs) 163 | 164 | monkeypatch.setattr(telegram, "Bot", mock_telegram_bot) 165 | 166 | import homework 167 | 168 | assert hasattr(homework, 'logging'), ( 169 | 'Убедитесь, что настроили логирование для вашего бота' 170 | ) 171 | 172 | def test_send_message(self, monkeypatch, random_timestamp): 173 | def mock_telegram_bot(*args, **kwargs): 174 | return MockTelegramBot(*args, random_timestamp=random_timestamp, **kwargs) 175 | 176 | monkeypatch.setattr(telegram, "Bot", mock_telegram_bot) 177 | 178 | import homework 179 | utils.check_function(homework, 'send_message', 2) 180 | 181 | def test_get_api_answers(self, monkeypatch, random_timestamp, 182 | current_timestamp, api_url): 183 | def mock_response_get(*args, **kwargs): 184 | return MockResponseGET( 185 | *args, random_timestamp=random_timestamp, 186 | current_timestamp=current_timestamp, **kwargs 187 | ) 188 | 189 | monkeypatch.setattr(requests, 'get', mock_response_get) 190 | 191 | import homework 192 | 193 | func_name = 'get_api_answer' 194 | utils.check_function(homework, func_name, 1) 195 | 196 | result = homework.get_api_answer(current_timestamp) 197 | assert type(result) == dict, ( 198 | f'Проверьте, что из функции `{func_name}` ' 199 | 'возвращается словарь' 200 | ) 201 | keys_to_check = ['homeworks', 'current_date'] 202 | for key in keys_to_check: 203 | assert key in result, ( 204 | f'Проверьте, что функция `{func_name}` ' 205 | f'возвращает словарь, содержащий ключ `{key}`' 206 | ) 207 | assert type(result['current_date']) == int, ( 208 | f'Проверьте, что функция `{func_name}` ' 209 | 'в ответе API возвращает значение ' 210 | 'ключа `current_date` типа `int`' 211 | ) 212 | assert result['current_date'] == random_timestamp, ( 213 | f'Проверьте, что функция `{func_name}` ' 214 | 'в ответе API возвращает корректное значение ' 215 | 'ключа `current_date`' 216 | ) 217 | 218 | def test_get_500_api_answer(self, monkeypatch, random_timestamp, 219 | current_timestamp, api_url): 220 | def mock_500_response_get(*args, **kwargs): 221 | response = MockResponseGET( 222 | *args, random_timestamp=random_timestamp, 223 | current_timestamp=current_timestamp, 224 | http_status=HTTPStatus.INTERNAL_SERVER_ERROR, **kwargs 225 | ) 226 | 227 | def json_invalid(): 228 | data = { 229 | } 230 | return data 231 | 232 | response.json = json_invalid 233 | return response 234 | 235 | monkeypatch.setattr(requests, 'get', mock_500_response_get) 236 | 237 | import homework 238 | 239 | func_name = 'get_api_answer' 240 | try: 241 | homework.get_api_answer(current_timestamp) 242 | except: 243 | pass 244 | else: 245 | assert False, ( 246 | f'Убедитесь, что в функции `{func_name}` обрабатываете ситуацию, ' 247 | 'когда API возвращает код, отличный от 200' 248 | ) 249 | 250 | def test_parse_status(self, random_timestamp): 251 | test_data = { 252 | "id": 123, 253 | "status": "approved", 254 | "homework_name": str(random_timestamp), 255 | "reviewer_comment": "Всё нравится", 256 | "date_updated": "2020-02-13T14:40:57Z", 257 | "lesson_name": "Итоговый проект" 258 | } 259 | 260 | import homework 261 | 262 | func_name = 'parse_status' 263 | 264 | utils.check_function(homework, func_name, 1) 265 | 266 | result = homework.parse_status(test_data) 267 | assert result.startswith( 268 | f'Изменился статус проверки работы "{random_timestamp}"' 269 | ), ( 270 | 'Проверьте, что возвращаете название домашней работы в возврате ' 271 | f'функции `{func_name}`' 272 | ) 273 | status = 'approved' 274 | assert result.endswith(self.HOMEWORK_STATUSES[status]), ( 275 | 'Проверьте, что возвращаете правильный вердикт для статуса ' 276 | f'`{status}` в возврате функции `{func_name}`' 277 | ) 278 | 279 | test_data['status'] = status = 'rejected' 280 | result = homework.parse_status(test_data) 281 | assert result.startswith( 282 | f'Изменился статус проверки работы "{random_timestamp}"' 283 | ), ( 284 | 'Проверьте, что возвращаете название домашней работы ' 285 | 'в возврате функции parse_status()' 286 | ) 287 | assert result.endswith( 288 | self.HOMEWORK_STATUSES[status] 289 | ), ( 290 | 'Проверьте, что возвращаете правильный вердикт для статуса ' 291 | f'`{status}` в возврате функции parse_status()' 292 | ) 293 | 294 | def test_check_response(self, monkeypatch, random_timestamp, 295 | current_timestamp, api_url): 296 | def mock_response_get(*args, **kwargs): 297 | response = MockResponseGET( 298 | *args, random_timestamp=random_timestamp, 299 | current_timestamp=current_timestamp, 300 | **kwargs 301 | ) 302 | 303 | def valid_response_json(): 304 | data = { 305 | "homeworks": [ 306 | { 307 | 'homework_name': 'hw123', 308 | 'status': 'approved' 309 | } 310 | ], 311 | "current_date": random_timestamp 312 | } 313 | return data 314 | 315 | response.json = valid_response_json 316 | return response 317 | 318 | monkeypatch.setattr(requests, 'get', mock_response_get) 319 | 320 | import homework 321 | 322 | func_name = 'check_response' 323 | response = homework.get_api_answer(current_timestamp) 324 | status = homework.check_response(response) 325 | assert status, ( 326 | f'Убедитесь, что функция `{func_name} ' 327 | 'правильно работает ' 328 | 'при корректном ответе от API' 329 | ) 330 | 331 | def test_parse_status_unknown_status(self, monkeypatch, random_timestamp, 332 | current_timestamp, api_url): 333 | def mock_response_get(*args, **kwargs): 334 | response = MockResponseGET( 335 | *args, random_timestamp=random_timestamp, 336 | current_timestamp=current_timestamp, 337 | **kwargs 338 | ) 339 | 340 | def valid_response_json(): 341 | data = { 342 | "homeworks": [ 343 | { 344 | 'homework_name': 'hw123', 345 | 'status': 'unknown' 346 | } 347 | ], 348 | "current_date": random_timestamp 349 | } 350 | return data 351 | 352 | response.json = valid_response_json 353 | return response 354 | 355 | monkeypatch.setattr(requests, 'get', mock_response_get) 356 | 357 | import homework 358 | 359 | func_name = 'parse_status' 360 | response = homework.get_api_answer(current_timestamp) 361 | homeworks = homework.check_response(response) 362 | for hw in homeworks: 363 | status_message = None 364 | try: 365 | status_message = homework.parse_status(hw) 366 | except: 367 | pass 368 | else: 369 | assert False, ( 370 | f'Убедитесь, что функция `{func_name}` выбрасывает ошибку ' 371 | 'при недокументированном статусе домашней работы в ответе от API' 372 | ) 373 | if status_message is not None: 374 | for hw_status in self.HOMEWORK_STATUSES: 375 | assert not status_message.endswith(hw_status), ( 376 | f'Убедитесь, что функция `{func_name} не возвращает корректный ' 377 | 'ответ при получении домашки с недокументированным статусом' 378 | ) 379 | 380 | def test_parse_status_no_status_key(self, monkeypatch, random_timestamp, 381 | current_timestamp, api_url): 382 | def mock_response_get(*args, **kwargs): 383 | response = MockResponseGET( 384 | *args, random_timestamp=random_timestamp, 385 | current_timestamp=current_timestamp, 386 | **kwargs 387 | ) 388 | 389 | def valid_response_json(): 390 | data = { 391 | "homeworks": [ 392 | { 393 | 'homework_name': 'hw123', 394 | } 395 | ], 396 | "current_date": random_timestamp 397 | } 398 | return data 399 | 400 | response.json = valid_response_json 401 | return response 402 | 403 | monkeypatch.setattr(requests, 'get', mock_response_get) 404 | 405 | import homework 406 | 407 | func_name = 'parse_status' 408 | response = homework.get_api_answer(current_timestamp) 409 | homeworks = homework.check_response(response) 410 | for hw in homeworks: 411 | status_message = None 412 | try: 413 | status_message = homework.parse_status(hw) 414 | except: 415 | pass 416 | else: 417 | assert False, ( 418 | f'Убедитесь, что функция `{func_name}` выбрасывает ошибку ' 419 | 'при отсутствии ключа `homework_status` домашней работы в ответе от API' 420 | ) 421 | if status_message is not None: 422 | for hw_status in self.HOMEWORK_STATUSES: 423 | assert not status_message.endswith(hw_status), ( 424 | f'Убедитесь, что функция `{func_name} не возвращает корректный ' 425 | 'ответ при получении домашки без ключа `homework_status`' 426 | ) 427 | 428 | def test_parse_status_no_homework_name_key(self, monkeypatch, random_timestamp, 429 | current_timestamp, api_url): 430 | def mock_response_get(*args, **kwargs): 431 | response = MockResponseGET( 432 | *args, random_timestamp=random_timestamp, 433 | current_timestamp=current_timestamp, 434 | **kwargs 435 | ) 436 | 437 | def valid_response_json(): 438 | data = { 439 | "homeworks": [ 440 | { 441 | 'status': 'unknown' 442 | } 443 | ], 444 | "current_date": random_timestamp 445 | } 446 | return data 447 | 448 | response.json = valid_response_json 449 | return response 450 | 451 | monkeypatch.setattr(requests, 'get', mock_response_get) 452 | 453 | import homework 454 | 455 | func_name = 'parse_status' 456 | response = homework.get_api_answer(current_timestamp) 457 | homeworks = homework.check_response(response) 458 | try: 459 | for hw in homeworks: 460 | homework.parse_status(hw) 461 | except KeyError: 462 | pass 463 | else: 464 | assert False, ( 465 | f'Убедитесь, что функция `{func_name}` правильно работает ' 466 | 'при отсутствии ключа `homework_name` в ответе от API' 467 | ) 468 | 469 | def test_check_response_no_homeworks(self, monkeypatch, random_timestamp, 470 | current_timestamp, api_url): 471 | def mock_no_homeworks_response_get(*args, **kwargs): 472 | response = MockResponseGET( 473 | *args, random_timestamp=random_timestamp, 474 | current_timestamp=current_timestamp, 475 | **kwargs 476 | ) 477 | 478 | def json_invalid(): 479 | data = { 480 | "current_date": random_timestamp 481 | } 482 | return data 483 | 484 | response.json = json_invalid 485 | return response 486 | 487 | monkeypatch.setattr(requests, 'get', mock_no_homeworks_response_get) 488 | 489 | import homework 490 | 491 | func_name = 'check_response' 492 | result = homework.get_api_answer(current_timestamp) 493 | try: 494 | homework.check_response(result) 495 | except: 496 | pass 497 | else: 498 | assert False, ( 499 | f'Убедитесь, что в функции `{func_name} ' 500 | 'обрабатываете ситуацию, когда ответ от API ' 501 | 'не содержит ключа `homeworks`, и выбрасываете ошибку' 502 | ) 503 | 504 | def test_check_response_not_dict(self, monkeypatch, random_timestamp, 505 | current_timestamp, api_url): 506 | def mock_response_get(*args, **kwargs): 507 | response = MockResponseGET( 508 | *args, random_timestamp=random_timestamp, 509 | current_timestamp=current_timestamp, 510 | **kwargs 511 | ) 512 | 513 | def valid_response_json(): 514 | data = [{ 515 | "homeworks": [ 516 | { 517 | 'homework_name': 'hw123', 518 | 'status': 'approved' 519 | } 520 | ], 521 | "current_date": random_timestamp 522 | }] 523 | return data 524 | 525 | response.json = valid_response_json 526 | return response 527 | 528 | monkeypatch.setattr(requests, 'get', mock_response_get) 529 | 530 | import homework 531 | 532 | func_name = 'check_response' 533 | response = homework.get_api_answer(current_timestamp) 534 | try: 535 | status = homework.check_response(response) 536 | except TypeError: 537 | pass 538 | else: 539 | assert status, ( 540 | f'Убедитесь, что в функции `{func_name} ' 541 | 'обрабатывается ситуация, при которой ' 542 | 'ответ от API имеет некорректный тип.' 543 | ) 544 | 545 | def test_check_response_homeworks_not_in_list(self, monkeypatch, random_timestamp, 546 | current_timestamp, api_url): 547 | def mock_response_get(*args, **kwargs): 548 | response = MockResponseGET( 549 | *args, random_timestamp=random_timestamp, 550 | current_timestamp=current_timestamp, 551 | **kwargs 552 | ) 553 | 554 | def valid_response_json(): 555 | data = { 556 | "homeworks": 557 | { 558 | 'homework_name': 'hw123', 559 | 'status': 'approved' 560 | }, 561 | "current_date": random_timestamp 562 | } 563 | return data 564 | 565 | response.json = valid_response_json 566 | return response 567 | 568 | monkeypatch.setattr(requests, 'get', mock_response_get) 569 | 570 | import homework 571 | 572 | func_name = 'check_response' 573 | response = homework.get_api_answer(current_timestamp) 574 | try: 575 | homeworks = homework.check_response(response) 576 | except: 577 | pass 578 | else: 579 | assert not homeworks, ( 580 | f'Убедитесь, что в функции `{func_name} ' 581 | 'обрабатывается ситуация, при которой под ключом `homeworks` ' 582 | 'домашки приходят не в виде списка в ответ от API.' 583 | ) 584 | 585 | def test_check_response_empty(self, monkeypatch, random_timestamp, 586 | current_timestamp, api_url): 587 | def mock_empty_response_get(*args, **kwargs): 588 | response = MockResponseGET( 589 | *args, random_timestamp=random_timestamp, 590 | current_timestamp=current_timestamp, 591 | **kwargs 592 | ) 593 | 594 | def json_invalid(): 595 | data = { 596 | } 597 | return data 598 | 599 | response.json = json_invalid 600 | return response 601 | 602 | monkeypatch.setattr(requests, 'get', mock_empty_response_get) 603 | 604 | import homework 605 | 606 | func_name = 'check_response' 607 | result = homework.get_api_answer(current_timestamp) 608 | try: 609 | homework.check_response(result) 610 | except: 611 | pass 612 | else: 613 | assert False, ( 614 | f'Убедитесь, что в функции `{func_name} ' 615 | 'обрабатываете ситуацию, когда ответ от API ' 616 | 'содержит пустой словарь`, и выбрасываете ошибку' 617 | ) 618 | 619 | def test_api_response_timeout(self, monkeypatch, random_timestamp, 620 | current_timestamp, api_url): 621 | def mock_response_get(*args, **kwargs): 622 | response = MockResponseGET( 623 | *args, random_timestamp=random_timestamp, 624 | current_timestamp=current_timestamp, 625 | http_status=HTTPStatus.REQUEST_TIMEOUT, **kwargs 626 | ) 627 | return response 628 | 629 | monkeypatch.setattr(requests, 'get', mock_response_get) 630 | 631 | import homework 632 | 633 | func_name = 'check_response' 634 | try: 635 | homework.get_api_answer(current_timestamp) 636 | except: 637 | pass 638 | else: 639 | assert False, ( 640 | f'Убедитесь, что в функции `{func_name}` обрабатываете ситуацию, ' 641 | 'когда API возвращает код, отличный от 200' 642 | ) 643 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from inspect import signature 2 | from types import ModuleType 3 | 4 | 5 | def check_function(scope: ModuleType, func_name: str, params_qty: int = 0): 6 | """Checks if scope has a function with specific name and params with qty""" 7 | assert hasattr(scope, func_name), ( 8 | f'Не найдена функция `{func_name}`. ' 9 | 'Не удаляйте и не переименовывайте её.' 10 | ) 11 | 12 | func = getattr(scope, func_name) 13 | 14 | assert callable(func), ( 15 | f'`{func_name}` должна быть функцией' 16 | ) 17 | 18 | sig = signature(func) 19 | assert len(sig.parameters) == params_qty, ( 20 | f'Функция `{func_name}` должна принимать ' 21 | f'количество аргументов: {params_qty}' 22 | ) 23 | 24 | 25 | def check_default_var_exists(scope: ModuleType, var_name: str) -> None: 26 | """ 27 | Checks if precode variable exists in scope with a proper type. 28 | :param scope: Module to look for a variable 29 | :param var_name: Variable you want to check 30 | :return: None. It's an assert 31 | """ 32 | assert hasattr(scope, var_name), ( 33 | f'Не найдена переменная `{var_name}`. Не удаляйте и не переименовывайте ее.' 34 | ) 35 | var = getattr(scope, var_name) 36 | assert not callable(var), ( 37 | f'{var_name} должна быть переменной, а не функцией.' 38 | ) 39 | 40 | --------------------------------------------------------------------------------