diff --git a/requirements.txt b/requirements.txt index d21b07e..40d51db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ requests==2.25.1 boto3==1.17.93 -aws_lambda_powertools==1.17.0 \ No newline at end of file +aws_lambda_powertools==1.17.0 +func_timeout==4.3.5 \ No newline at end of file diff --git a/serverless.yml b/serverless.yml index b9e712a..98b8956 100644 --- a/serverless.yml +++ b/serverless.yml @@ -24,6 +24,9 @@ custom: min_date_param: dev: "/autocita/madrid/dev/min-date-info" pro: "/autocita/madrid/pro/min-date-info" + min_years_param: + dev: "/autocita/madrid/dev/min-years" + pro: "/autocita/madrid/pro/min-years" table: dev: "vaccine-notifications-dev" pro: "vaccine-notifications-pro" @@ -60,6 +63,7 @@ provider: PARAM_MIN_DATE: ${self:custom.min_date_param.${opt:stage, self:provider.stage}} BOT_TOKEN: ${env:BOT_TOKEN} UPDATE_CENTRES_TIME: ${self:custom.update_centres_time.${opt:stage, self:provider.stage}} + PARAM_MIN_YEARS: ${self:custom.min_years_param.${opt:stage, self:provider.stage}} functions: message_handler: diff --git a/src/checker.py b/src/checker.py index 24eee50..c901e1e 100644 --- a/src/checker.py +++ b/src/checker.py @@ -2,7 +2,9 @@ from aws_lambda_powertools import Logger from datetime import datetime from src.telegram_helpers import send_text -from src.db import save_notification, get_non_notified_people +from src.db import save_notification, get_non_notified_people, get_min_years as db_get_min_years, save_min_years +from func_timeout import func_set_timeout +from func_timeout.exceptions import FunctionTimedOut logger = Logger(service="vacunacovidmadridbot") @@ -21,16 +23,30 @@ def mark_as_notified(user_info): def get_min_years(): - data = requests.get("https://autocitavacuna.sanidadmadrid.org/ohcitacovid/assets/config/app-config.json", - verify=False, timeout=5).json() + try: + years = _get_min_years() + except (FunctionTimedOut, requests.exceptions.RequestException): + years = db_get_min_years() + return years + + +@func_set_timeout(15) +def _get_min_years(): + req = requests.get("https://autocitavacuna.sanidadmadrid.org/ohcitacovid/assets/config/app-config.json", + verify=False, timeout=5) + + req.raise_for_status() + data = req.json() max_birthday = datetime.strptime(data["dFin_Birthday"], "%d/%m/%Y") curr_date = datetime.now() - return curr_date.year - max_birthday.year + max_years = curr_date.year - max_birthday.year + save_min_years(max_years) + return max_years def main(): - min_years = get_min_years() + min_years = _get_min_years() non_notified = get_non_notified_people() for person in non_notified: diff --git a/src/db.py b/src/db.py index 80b472c..36d974f 100644 --- a/src/db.py +++ b/src/db.py @@ -2,9 +2,11 @@ import os import json from datetime import datetime +from src.exceptions import ImpossibleToDetermineMaxAge TABLE_NAME = os.environ.get("NOTIFICATIONS_TABLE") MIN_DATE_PARAMETER = os.environ.get("PARAM_MIN_DATE") +MIN_YEARS_PARAMETER = os.environ.get("PARAM_MIN_YEARS") CLIENT = boto3.client('dynamodb') CLIENT_SSM = boto3.client('ssm') @@ -70,3 +72,20 @@ def get_min_date_info(): return centres_by_date, datetime.fromtimestamp(decoded_content[__UPDATED_AT]) except CLIENT_SSM.exceptions.ParameterNotFound: return {}, None + + +def save_min_years(max_years): + try: + current_years = get_min_years() + except ImpossibleToDetermineMaxAge: + current_years = None + + if current_years != max_years: + CLIENT_SSM.put_parameter(Name=MIN_YEARS_PARAMETER, Value=str(max_years), Type="String") + + +def get_min_years(): + try: + return int(CLIENT_SSM.get_parameter(Name=MIN_YEARS_PARAMETER)["Parameter"]["Value"]) + except CLIENT_SSM.exceptions.ParameterNotFound: + raise ImpossibleToDetermineMaxAge() diff --git a/src/exceptions.py b/src/exceptions.py new file mode 100644 index 0000000..d4ebbeb --- /dev/null +++ b/src/exceptions.py @@ -0,0 +1,2 @@ +class ImpossibleToDetermineMaxAge(BaseException): + pass diff --git a/src/message_handler.py b/src/message_handler.py index bbb2eaa..7ac4f41 100644 --- a/src/message_handler.py +++ b/src/message_handler.py @@ -7,6 +7,8 @@ from src import telegram_helpers from src import db from src.checker import get_min_years +from func_timeout import func_set_timeout +from func_timeout.exceptions import FunctionTimedOut UPDATE_CENTRES_TIME = int(os.environ.get("UPDATE_CENTRES_TIME", 300)) @@ -178,16 +180,25 @@ def handle_min_date(_): centres_by_date, last_update = db.get_min_date_info() if last_update is None or (datetime.now() - last_update).seconds >= UPDATE_CENTRES_TIME: - centres_by_date, last_update = update_centres() + try: + centres_by_date, last_update = update_centres() + except (FunctionTimedOut, requests.exceptions.RequestException): + pass if centres_by_date: - message = "Ā”Estupendo šŸ˜Š! AquĆ­ tienes las primeras fechas disponibles en el sistema de autocita:\n\n" + updated_ago_seconds = (datetime.now() - last_update).seconds + message = "Ā”Estupendo šŸ˜Š! AquĆ­ tienes las primeras fechas disponibles en el sistema de autocita:" \ + if updated_ago_seconds < UPDATE_CENTRES_TIME else "Perdona šŸ˜”, pero me estĆ” costando un poquito contactar " \ + "con el servicio de autocita de la Comunidad de " \ + "Madrid. AquĆ­ tienes la Ćŗltima informaciĆ³n que puede " \ + "extraer:" + message += "\n\n" for date in sorted(centres_by_date.keys()): date_str = date.strftime("%d/%m/%Y") centres = "\n".join(map(lambda x: f"- {x}", centres_by_date[date])) message += f"*{date_str}*:\n{centres}\n\n" - updated_ago = int((datetime.now() - last_update).seconds / 60) + updated_ago = int(round(updated_ago_seconds / 60)) updated_at_msg = f"Actualizado hace {updated_ago} minutos" updated_at_msg = updated_at_msg[:-1] if updated_ago == 1 else updated_at_msg message += updated_at_msg @@ -197,6 +208,7 @@ def handle_min_date(_): return message +@func_set_timeout(22) def update_centres(): centres_by_date = defaultdict(lambda: list()) centres = requests.post("https://autocitavacuna.sanidadmadrid.org/ohcitacovid/autocita/obtenerCentros", diff --git a/tests/unit/test_checker.py b/tests/unit/test_checker.py index 80340fc..9d27771 100644 --- a/tests/unit/test_checker.py +++ b/tests/unit/test_checker.py @@ -1,5 +1,6 @@ from unittest.mock import patch, MagicMock, ANY, call from freezegun import freeze_time +from func_timeout.exceptions import FunctionTimedOut from src import checker @@ -29,20 +30,33 @@ def test_when_mark_as_notified_then_db_updated(save_notification_mock): save_notification_mock.assert_called_once_with(user_id, name, age, True) +@patch("src.checker._get_min_years", return_value=24) +def test_given_no_error_when_get_min_years_then_aux_result_returned(get_min_years_aux_mock): + assert checker.get_min_years() == 24 + + +@patch("src.checker._get_min_years", side_effect=FunctionTimedOut) +@patch("src.checker.db_get_min_years", return_value=32) +def test_given_error_when_get_min_years_then_aux_result_returned(db_get_min_years_mock, get_min_years_aux_mock): + assert checker.get_min_years() == 32 + + @patch("src.checker.requests") +@patch("src.checker.save_min_years") @freeze_time("2021-06-20") -def test_given_1990_as_year_when_get_min_years_then_31_returned(requests_mock): +def test_given_1990_as_year_when_get_min_years_aux_then_31_returned(save_min_years_mock, requests_mock): requests_mock.get.return_value.json.return_value = { "dFin_Birthday": "31/12/1990" } - assert checker.get_min_years() == 31 + assert checker._get_min_years() == 31 + save_min_years_mock.assert_called_once_with(31) @patch("src.checker.mark_as_notified") @patch("src.checker.notify") @patch("src.checker.get_non_notified_people") -@patch("src.checker.get_min_years", return_value=45) +@patch("src.checker._get_min_years", return_value=45) def test_when_main_then_only_people_above_min_age_notified(get_min_years_mock, get_non_notified_people_mock, notify_mock, mark_as_notified_mock): diff --git a/tests/unit/test_db.py b/tests/unit/test_db.py index 2728ded..b063c4a 100644 --- a/tests/unit/test_db.py +++ b/tests/unit/test_db.py @@ -1,7 +1,9 @@ from unittest.mock import patch, MagicMock from datetime import datetime import json +import pytest from src import db +from src.exceptions import ImpossibleToDetermineMaxAge TABLE_NAME = "test-table" @@ -156,3 +158,49 @@ def test_given_parameter_when_save_min_date_info_then_info_stored_in_ssm(ssm_moc datetime(2021, 5, 9): ["hosp3", "hosp4"] } assert received_last_update == datetime.fromtimestamp(int(last_update.timestamp())) + + +@patch("src.db.CLIENT_SSM") +@patch("src.db.get_min_years") +@patch("src.db.MIN_YEARS_PARAMETER", "param2_name") +def test_given_param_not_updated_when_save_min_years_then_put_parameter_called(get_min_years_mock, client_ssm_mock): + years = 20 + get_min_years_mock.return_value = 21 + db.save_min_years(years) + client_ssm_mock.put_parameter.assert_called_once_with(Name=db.MIN_YEARS_PARAMETER, Value=str(years), + Type="String") + + +@patch("src.db.CLIENT_SSM") +@patch("src.db.get_min_years") +@patch("src.db.MIN_YEARS_PARAMETER", "param2_name") +def test_given_param_not_updated_when_save_min_years_then_put_parameter_not_called(get_min_years_mock, client_ssm_mock): + years = 21 + get_min_years_mock.return_value = 21 + db.save_min_years(years) + client_ssm_mock.put_parameter.assert_not_called() + + +@patch("src.db.CLIENT_SSM") +@patch("src.db.MIN_YEARS_PARAMETER", "param2_name") +def test_given_param_exists_when_get_min_years_then_converted_param_returned(client_ssm_mock): + years = "20" + client_ssm_mock.exceptions.ParameterNotFound = ParameterNotFound + client_ssm_mock.get_parameter.return_value = { + "Parameter": { + "Value": years + } + } + + assert db.get_min_years() == int(years) + client_ssm_mock.get_parameter.assert_called_once_with(Name=db.MIN_YEARS_PARAMETER) + + +@patch("src.db.CLIENT_SSM") +@patch("src.db.MIN_YEARS_PARAMETER", "param2_name") +def test_given_param_does_not_exists_when_get_min_years_then_custom_exception_raises(client_ssm_mock): + client_ssm_mock.exceptions.ParameterNotFound = ParameterNotFound + client_ssm_mock.get_parameter.side_effect = ParameterNotFound + + with pytest.raises(ImpossibleToDetermineMaxAge): + db.get_min_years() diff --git a/tests/unit/test_message_handler.py b/tests/unit/test_message_handler.py index d7f4102..7d2f1b4 100644 --- a/tests/unit/test_message_handler.py +++ b/tests/unit/test_message_handler.py @@ -610,7 +610,7 @@ def test_given_info_updated_when_handle_min_date_then_update_centres_not_called( assert "*06/03/2021*:\n- hosp1\n- hosp2" in answer assert "*09/05/2021*:\n- hosp3\n- hosp4" in answer - assert "Actualizado hace 19 minutos" in answer + assert "Actualizado hace 20 minutos" in answer @freeze_time("2021-06-22 10:01:02")