From b60971206f9d63c87463636b5e1c115f3fd5e823 Mon Sep 17 00:00:00 2001 From: ComradeSlyK Date: Fri, 23 Apr 2021 17:04:27 +0200 Subject: [PATCH 1/7] Add abstract functioning for publishers, mock-up for Telegram publisher --- mobilizon_bots/__init__.py | 3 ++ mobilizon_bots/publishers/__init__.py | 40 ++++++++++++++ mobilizon_bots/publishers/abstract.py | 78 +++++++++++++++++++++++++++ mobilizon_bots/publishers/telegram.py | 66 +++++++++++++++++++++++ 4 files changed, 187 insertions(+) create mode 100644 mobilizon_bots/publishers/__init__.py create mode 100644 mobilizon_bots/publishers/abstract.py create mode 100644 mobilizon_bots/publishers/telegram.py diff --git a/mobilizon_bots/__init__.py b/mobilizon_bots/__init__.py index b794fd4..df552ad 100644 --- a/mobilizon_bots/__init__.py +++ b/mobilizon_bots/__init__.py @@ -1 +1,4 @@ __version__ = '0.1.0' + + +from . import publishers diff --git a/mobilizon_bots/publishers/__init__.py b/mobilizon_bots/publishers/__init__.py new file mode 100644 index 0000000..7832e96 --- /dev/null +++ b/mobilizon_bots/publishers/__init__.py @@ -0,0 +1,40 @@ +from typing import Iterable + +from . import abstract +from . import telegram + +# WIP +# from . import twitter + + +def run(publishers: Iterable[abstract.AbstractPublisher]) -> dict: + invalid_credentials, invalid_event = [], [] + for p in publishers: + if not p.validate_credentials(): + invalid_credentials.append(p) + if not p.validate_event(): + invalid_event.append(p) + if invalid_credentials or invalid_event: + return { + 'status': 'fail', + 'description': "Validation failed for at least 1 publisher", + 'invalid_credentials': invalid_credentials, + 'invalid_event': invalid_event, + } + failed_publishers, successful_publishers = [], [] + for p in publishers: + if p.post(): + successful_publishers.append(p) + else: + failed_publishers.append(p) + if failed_publishers: + return { + 'status': 'fail', + 'description': "Posting failed for at least 1 publisher", + 'failed_publishers': failed_publishers, + 'successful_publishers': successful_publishers, + } + return { + 'status': 'success', + 'description': "https://www.youtube.com/watch?v=2lHgmC6PBBE", + } diff --git a/mobilizon_bots/publishers/abstract.py b/mobilizon_bots/publishers/abstract.py new file mode 100644 index 0000000..04e1d38 --- /dev/null +++ b/mobilizon_bots/publishers/abstract.py @@ -0,0 +1,78 @@ +import inspect +import logging + +from abc import ABC, abstractmethod + +logging.basicConfig( + format='%(asctime)s - %(levelname)s - %(name)s - %(message)s', + level=logging.INFO +) +logger = logging.getLogger("Publishers") + + +class AbstractPublisher(ABC): + """ + Generic publisher class. + + Shall be inherited from specific subclasses that will manage validation + process for events and credentials, text formatting, posting, etc. + + Class attributes: + - ``credentials``: a ``dict`` containing every useful info that the + current publisher will need to correctly login to its platform + - ``event``: a ``dict`` containing every useful info from the event + """ + + # TODO: will the actual event be managed by its own class? + def __init__(self, credentials: dict, event: dict): + self.credentials = credentials + self.event = event + + def __repr__(self): + return type(self).__name__ + + __str__ = __repr__ + + @abstractmethod + def post(self) -> bool: + """ + Publishes the actual post on social media using ``data`` info. + :return: True or False according to whether publisher was able to + complete its task + """ + raise NotImplementedError + + @abstractmethod + def validate_credentials(self) -> bool: + """ + Validates credentials. + :return: True or False according to whether credentials are valid. + """ + raise NotImplementedError + + @abstractmethod + def validate_event(self) -> bool: + """ + Validates publisher's event. + :return: True or False according to whether event is valid. + """ + raise NotImplementedError + + def log_debug(self, msg, *args, **kwargs): + self.__log(logging.DEBUG, msg, *args, **kwargs) + + def log_info(self, msg, *args, **kwargs): + self.__log(logging.INFO, msg, *args, **kwargs) + + def log_warning(self, msg, *args, **kwargs): + self.__log(logging.WARNING, msg, *args, **kwargs) + + def log_error(self, msg, *args, **kwargs): + self.__log(logging.ERROR, msg, *args, **kwargs) + + def log_critical(self, msg, *args, **kwargs): + self.__log(logging.CRITICAL, msg, *args, **kwargs) + + def __log(self, level, msg, *args, **kwargs): + method = inspect.currentframe().f_back.f_back.f_code.co_name + logger.log(level, f"{self}.{method}(): {msg}", *args, **kwargs) diff --git a/mobilizon_bots/publishers/telegram.py b/mobilizon_bots/publishers/telegram.py new file mode 100644 index 0000000..f6c138a --- /dev/null +++ b/mobilizon_bots/publishers/telegram.py @@ -0,0 +1,66 @@ +import requests + +from .abstract import AbstractPublisher + + +class TelegramPublisher(AbstractPublisher): + + def post(self) -> bool: + chat_id = self.credentials['chat_id'] + text = self.event['text'] + token = self.credentials['token'] + post_params_kwargs = self.event.get('post_params_kwargs') + res = requests.post( + url=f'https://api.telegram.org/bot{token}/sendMessage', + params=dict(chat_id=chat_id, text=text, **post_params_kwargs) + ) + data = self.__validate_response(res) + if data.get('__error'): + self.log_error(data['__error']) + return False + + return True + + def validate_credentials(self) -> bool: + chat_id = self.credentials.get('chat_id') + token = self.credentials.get('token') + username = self.credentials.get('username') + if any(a is None for a in (chat_id, token, username)): + self.log_error("Required info is missing") + return False + + res = requests.get(f'https://api.telegram.org/bot{token}/getMe') + data = self.__validate_response(res) + if data.get('__error'): + self.log_error(data['__error']) + return False + + if not username == data.get('result', {}).get('username'): + self.log_error("Found a different bot than the expected one!") + return False + + return True + + def validate_event(self) -> bool: + text = self.event.get('text') + if not (text and text.strip()): + self.log_error(f"No text was found!") + return False + return True + + @staticmethod + def __validate_response(res): + try: + res.raise_for_status() + except requests.exceptions.HTTPError as e: + return {'__error': str(e)} + + try: + data = res.json() + except ValueError: + return {'__error': "Server returned invalid json data"} + + if not data.get('ok'): + data['__error'] = f"Invalid request (response: {data})" + + return data From a6a14d26899c7075a63836df7a12757c3abe90ee Mon Sep 17 00:00:00 2001 From: Simone Robutti Date: Sun, 2 May 2021 10:24:46 +0200 Subject: [PATCH 2/7] changed logger to be more pythonic --- mobilizon_bots/publishers/abstract.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/mobilizon_bots/publishers/abstract.py b/mobilizon_bots/publishers/abstract.py index 04e1d38..d9083a2 100644 --- a/mobilizon_bots/publishers/abstract.py +++ b/mobilizon_bots/publishers/abstract.py @@ -3,11 +3,7 @@ import logging from abc import ABC, abstractmethod -logging.basicConfig( - format='%(asctime)s - %(levelname)s - %(name)s - %(message)s', - level=logging.INFO -) -logger = logging.getLogger("Publishers") +logger = logging.getLogger(__name__) class AbstractPublisher(ABC): From fb00516d6cf88aec61f617f7e3a4552d2f4e58a8 Mon Sep 17 00:00:00 2001 From: Simone Robutti Date: Sun, 2 May 2021 10:37:46 +0200 Subject: [PATCH 3/7] reworked validation --- mobilizon_bots/publishers/telegram.py | 72 +++++++++++---------------- setup.cfg | 2 + 2 files changed, 32 insertions(+), 42 deletions(-) create mode 100644 setup.cfg diff --git a/mobilizon_bots/publishers/telegram.py b/mobilizon_bots/publishers/telegram.py index f6c138a..fa0ab7d 100644 --- a/mobilizon_bots/publishers/telegram.py +++ b/mobilizon_bots/publishers/telegram.py @@ -4,63 +4,51 @@ from .abstract import AbstractPublisher class TelegramPublisher(AbstractPublisher): - def post(self) -> bool: - chat_id = self.credentials['chat_id'] - text = self.event['text'] - token = self.credentials['token'] - post_params_kwargs = self.event.get('post_params_kwargs') + chat_id = self.credentials["chat_id"] + text = self.event["text"] + token = self.credentials["token"] + post_params_kwargs = self.event.get("post_params_kwargs") res = requests.post( - url=f'https://api.telegram.org/bot{token}/sendMessage', - params=dict(chat_id=chat_id, text=text, **post_params_kwargs) + url=f"https://api.telegram.org/bot{token}/sendMessage", + params=dict(chat_id=chat_id, text=text, **post_params_kwargs), ) - data = self.__validate_response(res) - if data.get('__error'): - self.log_error(data['__error']) - return False + return self._validate_response(res) - return True + def _log_error_and_raise(self, message): + self.log_error(message) + raise ValueError(message) def validate_credentials(self) -> bool: - chat_id = self.credentials.get('chat_id') - token = self.credentials.get('token') - username = self.credentials.get('username') - if any(a is None for a in (chat_id, token, username)): - self.log_error("Required info is missing") - return False + chat_id = self.credentials.get("chat_id") + token = self.credentials.get("token") + username = self.credentials.get("username") + if all([chat_id, token, username]): + # TODO: add composable errors to highlight which credentials are missing + self._log_error_and_raise("Some credentials are missing") - res = requests.get(f'https://api.telegram.org/bot{token}/getMe') - data = self.__validate_response(res) - if data.get('__error'): - self.log_error(data['__error']) - return False + res = requests.get(f"https://api.telegram.org/bot{token}/getMe") + data = self._validate_response(res) - if not username == data.get('result', {}).get('username'): - self.log_error("Found a different bot than the expected one!") - return False - - return True + if not username == data.get("result", {}).get("username"): + self._log_error_and_raise("Found a different bot than the expected one") + return data def validate_event(self) -> bool: - text = self.event.get('text') + text = self.event.get("text") if not (text and text.strip()): - self.log_error(f"No text was found!") - return False - return True + self._log_error_and_raise("No text was found. Invalid event") - @staticmethod - def __validate_response(res): - try: - res.raise_for_status() - except requests.exceptions.HTTPError as e: - return {'__error': str(e)} + def _validate_response(self, res): + res.raise_for_status() try: data = res.json() - except ValueError: - return {'__error': "Server returned invalid json data"} + except ValueError as e: + self.log_error("Server returned invalid json data") + raise ValueError from e - if not data.get('ok'): - data['__error'] = f"Invalid request (response: {data})" + if not data.get("ok"): + raise ValueError(f"Invalid request (response: {data})") return data diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..aa079ec --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[flake8] +max-line-length=120 From fabc1e7eff0fa2f92d31ac65576de75d28421c89 Mon Sep 17 00:00:00 2001 From: Simone Robutti Date: Sun, 2 May 2021 10:46:58 +0200 Subject: [PATCH 4/7] added predicates --- mobilizon_bots/publishers/__init__.py | 31 ++++++++++++--------------- mobilizon_bots/publishers/abstract.py | 15 +++++++++++++ 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/mobilizon_bots/publishers/__init__.py b/mobilizon_bots/publishers/__init__.py index 7832e96..1e407bc 100644 --- a/mobilizon_bots/publishers/__init__.py +++ b/mobilizon_bots/publishers/__init__.py @@ -1,25 +1,22 @@ from typing import Iterable -from . import abstract -from . import telegram - -# WIP -# from . import twitter +from mobilizon_bots.publishers import abstract def run(publishers: Iterable[abstract.AbstractPublisher]) -> dict: invalid_credentials, invalid_event = [], [] for p in publishers: - if not p.validate_credentials(): + if not p.are_credetials_valid(): invalid_credentials.append(p) - if not p.validate_event(): + if not p.is_event_valid(): invalid_event.append(p) if invalid_credentials or invalid_event: + # TODO: consider to use exceptions or data class if necessary return { - 'status': 'fail', - 'description': "Validation failed for at least 1 publisher", - 'invalid_credentials': invalid_credentials, - 'invalid_event': invalid_event, + "status": "fail", + "description": "Validation failed for at least 1 publisher", + "invalid_credentials": invalid_credentials, + "invalid_event": invalid_event, } failed_publishers, successful_publishers = [], [] for p in publishers: @@ -29,12 +26,12 @@ def run(publishers: Iterable[abstract.AbstractPublisher]) -> dict: failed_publishers.append(p) if failed_publishers: return { - 'status': 'fail', - 'description': "Posting failed for at least 1 publisher", - 'failed_publishers': failed_publishers, - 'successful_publishers': successful_publishers, + "status": "fail", + "description": "Posting failed for at least 1 publisher", + "failed_publishers": failed_publishers, + "successful_publishers": successful_publishers, } return { - 'status': 'success', - 'description': "https://www.youtube.com/watch?v=2lHgmC6PBBE", + "status": "success", + "description": "https://www.youtube.com/watch?v=2lHgmC6PBBE", } diff --git a/mobilizon_bots/publishers/abstract.py b/mobilizon_bots/publishers/abstract.py index d9083a2..47fb288 100644 --- a/mobilizon_bots/publishers/abstract.py +++ b/mobilizon_bots/publishers/abstract.py @@ -46,6 +46,21 @@ class AbstractPublisher(ABC): """ raise NotImplementedError + @abstractmethod + def are_credetials_valid(self): + try: + self.validate_credentials() + except Exception: + return False + return True + + def is_event_valid(self): + try: + self.validate_event() + except Exception: + return False + return True + @abstractmethod def validate_event(self) -> bool: """ From a45c2c19ecc6c591600ebc8403ce67324d2fabdf Mon Sep 17 00:00:00 2001 From: Simone Robutti Date: Sun, 2 May 2021 11:23:31 +0200 Subject: [PATCH 5/7] added test predicates --- mobilizon_bots/publishers/abstract.py | 11 ++--- tests/publishers/__init__.py | 0 tests/publishers/test_abstract_predicates.py | 49 ++++++++++++++++++++ tests/test_mobilizon_bots.py | 5 -- 4 files changed, 54 insertions(+), 11 deletions(-) create mode 100644 tests/publishers/__init__.py create mode 100644 tests/publishers/test_abstract_predicates.py delete mode 100644 tests/test_mobilizon_bots.py diff --git a/mobilizon_bots/publishers/abstract.py b/mobilizon_bots/publishers/abstract.py index 47fb288..40538b8 100644 --- a/mobilizon_bots/publishers/abstract.py +++ b/mobilizon_bots/publishers/abstract.py @@ -30,7 +30,7 @@ class AbstractPublisher(ABC): __str__ = __repr__ @abstractmethod - def post(self) -> bool: + def post(self) -> dict: """ Publishes the actual post on social media using ``data`` info. :return: True or False according to whether publisher was able to @@ -39,22 +39,21 @@ class AbstractPublisher(ABC): raise NotImplementedError @abstractmethod - def validate_credentials(self) -> bool: + def validate_credentials(self) -> dict: """ Validates credentials. :return: True or False according to whether credentials are valid. """ raise NotImplementedError - @abstractmethod - def are_credetials_valid(self): + def are_credentials_valid(self) -> bool: try: self.validate_credentials() except Exception: return False return True - def is_event_valid(self): + def is_event_valid(self) -> bool: try: self.validate_event() except Exception: @@ -62,7 +61,7 @@ class AbstractPublisher(ABC): return True @abstractmethod - def validate_event(self) -> bool: + def validate_event(self) -> dict: """ Validates publisher's event. :return: True or False according to whether event is valid. diff --git a/tests/publishers/__init__.py b/tests/publishers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/publishers/test_abstract_predicates.py b/tests/publishers/test_abstract_predicates.py new file mode 100644 index 0000000..f53306b --- /dev/null +++ b/tests/publishers/test_abstract_predicates.py @@ -0,0 +1,49 @@ +import pytest + +from mobilizon_bots.publishers.abstract import AbstractPublisher + + +@pytest.fixture +def mock_publisher_valid(): + class MockPublisher(AbstractPublisher): + def validate_credentials(self) -> dict: + return {} + + def validate_event(self) -> dict: + return {} + + def post(self) -> dict: + pass + + return MockPublisher({}, {}) + + +@pytest.fixture +def mock_publisher_invalid(): + class MockPublisher(AbstractPublisher): + def validate_credentials(self) -> dict: + raise ValueError("Error") + + def validate_event(self) -> dict: + raise ValueError("Error") + + def post(self) -> dict: + raise ValueError() + + return MockPublisher({}, {}) + + +def test_are_credentials_valid(mock_publisher_valid): + assert mock_publisher_valid.are_credentials_valid() + + +def test_are_credentials_valid_false(mock_publisher_invalid): + assert not mock_publisher_invalid.are_credentials_valid() + + +def test_is_event_valid(mock_publisher_valid): + assert mock_publisher_valid.is_event_valid() + + +def test_is_event_valid_false(mock_publisher_invalid): + assert not mock_publisher_invalid.is_event_valid() diff --git a/tests/test_mobilizon_bots.py b/tests/test_mobilizon_bots.py deleted file mode 100644 index 796996b..0000000 --- a/tests/test_mobilizon_bots.py +++ /dev/null @@ -1,5 +0,0 @@ -from mobilizon_bots import __version__ - - -def test_version(): - assert __version__ == '0.1.0' From df837b17b696da76aac78a305719ec8de50a7740 Mon Sep 17 00:00:00 2001 From: Simone Robutti Date: Sun, 2 May 2021 11:27:07 +0200 Subject: [PATCH 6/7] changed visiblity log methods in publisher --- mobilizon_bots/publishers/abstract.py | 14 +++++++++----- mobilizon_bots/publishers/telegram.py | 6 +----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/mobilizon_bots/publishers/abstract.py b/mobilizon_bots/publishers/abstract.py index 40538b8..bd68965 100644 --- a/mobilizon_bots/publishers/abstract.py +++ b/mobilizon_bots/publishers/abstract.py @@ -68,21 +68,25 @@ class AbstractPublisher(ABC): """ raise NotImplementedError - def log_debug(self, msg, *args, **kwargs): + def _log_debug(self, msg, *args, **kwargs): self.__log(logging.DEBUG, msg, *args, **kwargs) - def log_info(self, msg, *args, **kwargs): + def _log_info(self, msg, *args, **kwargs): self.__log(logging.INFO, msg, *args, **kwargs) - def log_warning(self, msg, *args, **kwargs): + def _log_warning(self, msg, *args, **kwargs): self.__log(logging.WARNING, msg, *args, **kwargs) - def log_error(self, msg, *args, **kwargs): + def _log_error(self, msg, *args, **kwargs): self.__log(logging.ERROR, msg, *args, **kwargs) - def log_critical(self, msg, *args, **kwargs): + def _log_critical(self, msg, *args, **kwargs): self.__log(logging.CRITICAL, msg, *args, **kwargs) def __log(self, level, msg, *args, **kwargs): method = inspect.currentframe().f_back.f_back.f_code.co_name logger.log(level, f"{self}.{method}(): {msg}", *args, **kwargs) + + def _log_error_and_raise(self, message): + self._log_error(message) + raise ValueError(message) diff --git a/mobilizon_bots/publishers/telegram.py b/mobilizon_bots/publishers/telegram.py index fa0ab7d..f24c170 100644 --- a/mobilizon_bots/publishers/telegram.py +++ b/mobilizon_bots/publishers/telegram.py @@ -15,10 +15,6 @@ class TelegramPublisher(AbstractPublisher): ) return self._validate_response(res) - def _log_error_and_raise(self, message): - self.log_error(message) - raise ValueError(message) - def validate_credentials(self) -> bool: chat_id = self.credentials.get("chat_id") token = self.credentials.get("token") @@ -45,7 +41,7 @@ class TelegramPublisher(AbstractPublisher): try: data = res.json() except ValueError as e: - self.log_error("Server returned invalid json data") + self._log_error("Server returned invalid json data") raise ValueError from e if not data.get("ok"): From 06f2f3343b9fe053ba9bcbc1c904a9a4b37f2c3c Mon Sep 17 00:00:00 2001 From: Simone Robutti Date: Sun, 2 May 2021 16:43:17 +0200 Subject: [PATCH 7/7] added .idea to .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 081b2f4..b7c6bfa 100644 --- a/.gitignore +++ b/.gitignore @@ -171,4 +171,4 @@ com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties - +.idea