diff --git a/poetry.lock b/poetry.lock index 1a7fccf..73c549c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -61,17 +61,17 @@ zookeeper = ["kazoo"] [[package]] name = "boto3" -version = "1.34.84" +version = "1.34.86" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.84-py3-none-any.whl", hash = "sha256:7a02f44af32095946587d748ebeb39c3fa15b9d7275307ff612a6760ead47e04"}, - {file = "boto3-1.34.84.tar.gz", hash = "sha256:91e6343474173e9b82f603076856e1d5b7b68f44247bdd556250857a3f16b37b"}, + {file = "boto3-1.34.86-py3-none-any.whl", hash = "sha256:be594c449a0079bd1898ba1b7d90e0e5ac6b5803b2ada03993da01179073808d"}, + {file = "boto3-1.34.86.tar.gz", hash = "sha256:992ba74459fef2bf1572050408db73d33c43e7531d81bda85a027f39156926a1"}, ] [package.dependencies] -botocore = ">=1.34.84,<1.35.0" +botocore = ">=1.34.86,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -80,13 +80,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.84" +version = "1.34.86" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.84-py3-none-any.whl", hash = "sha256:da1ae0a912e69e10daee2a34dafd6c6c106450d20b8623665feceb2d96c173eb"}, - {file = "botocore-1.34.84.tar.gz", hash = "sha256:a2b309bf5594f0eb6f63f355ade79ba575ce8bf672e52e91da1a7933caa245e6"}, + {file = "botocore-1.34.86-py3-none-any.whl", hash = "sha256:57c1e3b2e1db745d22c45cbd761bbc0c143d2cfc2b532e3245cf5d874aa30b6d"}, + {file = "botocore-1.34.86.tar.gz", hash = "sha256:2fd62b63d8788e15629bfc95be1bd2d99c0da6c1d45ef1f40c0a0101e412f6b5"}, ] [package.dependencies] @@ -95,7 +95,7 @@ python-dateutil = ">=2.1,<3.0.0" urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} [package.extras] -crt = ["awscrt (==0.19.19)"] +crt = ["awscrt (==0.20.9)"] [[package]] name = "build" @@ -1992,13 +1992,13 @@ six = ">=1.5" [[package]] name = "python-telegram-bot" -version = "21.1" +version = "21.1.1" description = "We have made you a wrapper you can't refuse" optional = false python-versions = ">=3.8" files = [ - {file = "python-telegram-bot-21.1.tar.gz", hash = "sha256:e42d5cfec69d64fd921a41ff56215f92a48b8ddb5500235798c2bd3d9eb7242e"}, - {file = "python_telegram_bot-21.1-py3-none-any.whl", hash = "sha256:72e27445633a6339eacd2a9ebecec1eeb44b627056f84c2c2b907b448860cb62"}, + {file = "python-telegram-bot-21.1.1.tar.gz", hash = "sha256:5fd21710902270f5946c6a484918227b361b8119e1dadae6b3dd7fcee3b1a74e"}, + {file = "python_telegram_bot-21.1.1-py3-none-any.whl", hash = "sha256:dded93d733585a37b382fd5e088faffc37c3ae6d2bd4052d105d9a40f8a0152a"}, ] [package.dependencies] @@ -2404,13 +2404,13 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.25.1" +version = "20.25.3" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, - {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, + {file = "virtualenv-20.25.3-py3-none-any.whl", hash = "sha256:8aac4332f2ea6ef519c648d0bc48a5b1d324994753519919bddbb1aff25a104e"}, + {file = "virtualenv-20.25.3.tar.gz", hash = "sha256:7bb554bbdfeaacc3349fa614ea5bff6ac300fc7c335e9facf3a3bcfc703f45be"}, ] [package.dependencies] @@ -2419,7 +2419,7 @@ filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<5" [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 91e58cc..5e0a3d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "telegram-mood-tracker" -version = "0.4.0" +version = "0.4.4" description = "" authors = ["Tobias Waslowski "] readme = "README.md" diff --git a/scripts/run.sh b/scripts/run.sh index 13fd0f3..4c44d0e 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -14,6 +14,7 @@ sudo docker run -d --rm \ --log-driver=awslogs \ --log-opt awslogs-region="$LOG_REGION" \ --log-opt awslogs-group="$LOG_GROUP" \ + --log-opt awslogs-multiline-pattern='^ERROR' \ --security-opt seccomp:unconfined \ -v "$HOME/.aws/credentials:/root/.aws/credentials:ro" \ -v ./config.yaml:/app/config.yaml \ diff --git a/src/app.py b/src/app.py index 3176d09..03dc96c 100644 --- a/src/app.py +++ b/src/app.py @@ -24,7 +24,7 @@ TOKEN = os.environ.get("TELEGRAM_TOKEN") logging.basicConfig( - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO + format=" %(levelname)s - %(asctime)s - %(name)s - %(message)s", level=logging.INFO ) logging.getLogger("httpx").setLevel(logging.WARNING) diff --git a/src/handlers/error_handler.py b/src/handlers/error_handler.py index 74b5014..f9aeec6 100644 --- a/src/handlers/error_handler.py +++ b/src/handlers/error_handler.py @@ -4,11 +4,14 @@ from telegram.ext import CallbackContext -async def error_handler(update: Update, context: CallbackContext): +async def error_handler(update: Update, context: CallbackContext) -> None: """Log all errors.""" error_message = context.error logging.error(msg="Exception while handling an update:", exc_info=error_message) # This fails when the error is not directly caused by a user updated, e.g. in a JobQueue job + if update is None: + logging.error("Update is None, cannot send error message to user.") + return await update.effective_user.send_message( text=f"An error occurred while processing your message: {error_message}." ) diff --git a/src/handlers/record_handlers.py b/src/handlers/record_handlers.py index 990358f..b3329db 100644 --- a/src/handlers/record_handlers.py +++ b/src/handlers/record_handlers.py @@ -157,7 +157,7 @@ def store_record( :param user_record: the temporary record being saved :return: """ - logging.info(f"Record for user {user_id} is complete: {user_record}") + logging.info(f"Persisting record for user {user_id}: {user_record}") record_repository.create_record( user_id, user_record.data, diff --git a/src/handlers/util.py b/src/handlers/util.py index 8624089..5d9e093 100644 --- a/src/handlers/util.py +++ b/src/handlers/util.py @@ -1,13 +1,14 @@ import logging from telegram import Update +from telegram.error import TimedOut from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_exception_type @retry( stop=stop_after_attempt(3), wait=wait_fixed(1), - retry=retry_if_exception_type(TimeoutError), + retry=retry_if_exception_type((TimeoutError, TimedOut)), ) async def send(update: Update, text: str): """ diff --git a/src/model/record.py b/src/model/record.py index 88f2620..4f2d75d 100644 --- a/src/model/record.py +++ b/src/model/record.py @@ -21,6 +21,9 @@ def serialize(self): "timestamp": self.timestamp.isoformat(), } + def __str__(self): + return f"user_id: {self.user_id}, data: {self.data}, timestamp: {self.timestamp.isoformat()}" + class TempRecord(BaseModel): """ @@ -36,6 +39,9 @@ def __init__(self, **data): super().__init__(**data) self.data = {metric.name: None for metric in self.metrics} + def __str__(self): + return f"data: {self.data}, timestamp: {self.timestamp.isoformat()}" + def update_data(self, metric_name: str, value: int): if metric_name in self.data: self.data[metric_name] = value diff --git a/src/notifier.py b/src/notifier.py index dcc25e7..a9f6fad 100644 --- a/src/notifier.py +++ b/src/notifier.py @@ -3,6 +3,7 @@ import datetime from functools import partial +from telegram.error import TimedOut from telegram.ext import CallbackContext, JobQueue from pyautowire import Injectable, autowire @@ -29,7 +30,7 @@ async def reminder(self, context: CallbackContext, user_id: int, text: str = Non @retry( stop=stop_after_attempt(3), wait=wait_fixed(1), - retry=retry_if_exception_type(TimeoutError), + retry=retry_if_exception_type((TimeoutError, TimedOut)), ) async def send_from_context(context: CallbackContext, user_id: int, text: str): """Send a message to a user from a context.""" diff --git a/src/repository/db_utils.py b/src/repository/db_utils.py new file mode 100644 index 0000000..3a2a26a --- /dev/null +++ b/src/repository/db_utils.py @@ -0,0 +1,21 @@ +""" +Mechanisms for manually interacting with the database for diagnostics work etc. +""" +import logging +import os + +from src.config.config import ConfigurationProvider +from src.repository.initialize import initialize_database +from src.repository.record_repository import RecordRepository +from src.repository.user_repository import UserRepository + + +def init() -> (UserRepository, RecordRepository): + configuration = ConfigurationProvider().get_configuration().register() + return initialize_database(configuration) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + user_id = int(os.getenv("USER_ID")) + user_repository, record_repository = init() diff --git a/src/repository/dynamodb/dynamodb_record_repository.py b/src/repository/dynamodb/dynamodb_record_repository.py index 0f0e11c..faff7fa 100644 --- a/src/repository/dynamodb/dynamodb_record_repository.py +++ b/src/repository/dynamodb/dynamodb_record_repository.py @@ -1,4 +1,5 @@ import datetime +import logging import boto3 from boto3.dynamodb.conditions import Key @@ -11,15 +12,25 @@ class DynamoDBRecordRepository(RecordRepository): def __init__(self, dynamodb: boto3.resource): self.table = dynamodb.Table("record") self.table.load() + logging.info("DynamoDBRecordRepository initialized.") def get_latest_record_for_user(self, user_id: int) -> Record | None: + result = self.get_latest_records_for_user(user_id, 1) + if result: + return result[0] + + def get_latest_records_for_user( + self, user_id: int, limit: int + ) -> list[Record] | None: + logging.info(f"Retrieving {limit} latest records for user {user_id}") result = self.table.query( KeyConditionExpression=Key("user_id").eq(user_id), ScanIndexForward=False, - Limit=1, + Limit=limit, ) if result.get("Items"): - return self.parse_record(result["Items"][0]) + return [self.parse_record(record) for record in result["Items"]] + return [] def create_record(self, user_id: int, record_data: dict, timestamp: str): self.table.put_item( diff --git a/src/repository/dynamodb/dynamodb_user_repository.py b/src/repository/dynamodb/dynamodb_user_repository.py index 64f00a7..9ab8b12 100644 --- a/src/repository/dynamodb/dynamodb_user_repository.py +++ b/src/repository/dynamodb/dynamodb_user_repository.py @@ -1,4 +1,5 @@ import json +import logging import boto3 from pyautowire import autowire @@ -12,6 +13,7 @@ class DynamoDBUserRepository(UserRepository): def __init__(self, dynamodb: boto3.resource): self.table = dynamodb.Table("user") self.table.load() + logging.info("DynamoDBUserRepository initialized.") @autowire("configuration") def create_user(self, user_id: int, configuration: Configuration) -> User: diff --git a/src/repository/mongodb/mongodb_record_repository.py b/src/repository/mongodb/mongodb_record_repository.py index 72aa6ab..7261eb9 100644 --- a/src/repository/mongodb/mongodb_record_repository.py +++ b/src/repository/mongodb/mongodb_record_repository.py @@ -13,6 +13,14 @@ def __init__(self, mongo_client: MongoClient): super().__init__() mood_tracker = mongo_client["mood_tracker"] self.records = mood_tracker["records"] + logging.info("MongoDBRecordRepository initialized.") + + def get_latest_records_for_user(self, user_id: int, limit: int) -> list[Record]: + result = self.records.find( + {"user_id": user_id}, sort=[("timestamp", pymongo.DESCENDING)], limit=limit + ) + if result: + return [self.parse_record(r) for r in result] def get_latest_record_for_user(self, user_id: int) -> Record | None: result = self.records.find_one( diff --git a/src/repository/mongodb/mongodb_user_repository.py b/src/repository/mongodb/mongodb_user_repository.py index 0a36543..36d9465 100644 --- a/src/repository/mongodb/mongodb_user_repository.py +++ b/src/repository/mongodb/mongodb_user_repository.py @@ -13,6 +13,7 @@ def __init__(self, mongo_client: MongoClient): super().__init__() mood_tracker = mongo_client["mood_tracker"] self.user = mood_tracker["user"] + logging.info("MongoDBUserRepository initialized.") def find_user(self, user_id: int) -> User | None: result = self.user.find_one({"user_id": user_id}) diff --git a/src/repository/record_repository.py b/src/repository/record_repository.py index bbaf151..eaf72cc 100644 --- a/src/repository/record_repository.py +++ b/src/repository/record_repository.py @@ -21,6 +21,10 @@ def modify_timestamp(timestamp: str, offset: int) -> datetime.datetime: def get_latest_record_for_user(self, user_id: int) -> Record | None: pass + @abstractmethod + def get_latest_records_for_user(self, user_id: int, limit: int) -> list[Record]: + pass + @abstractmethod def create_record(self, user_id: int, record_data: dict, timestamp: str): pass