From 726bcceefcc9b4deafb4948e14222d57ce8e6162 Mon Sep 17 00:00:00 2001 From: Zach Priddy Date: Wed, 15 Apr 2020 14:51:03 -0700 Subject: [PATCH 01/10] Add DataStore and Interactions DataStoreInteractions are data that is related to a message or interaction that needs to be retrieved when a user responds to a message. Notes: DataStoreInteractions will auto be retrieved using the original channel and ts of the message the user is responding to. In most cases the response from the plugin is the json response from slack of sending a message. This data will be autolinked to the new_interaction object in the request. If there is no new_interaction set by the plugin, it will be skipped. It can also be skipped by setting request.auto_link to False. You can create more than one interaction entry per request, it will just have to be done manually using the functions added to the request module. TODO: Need to add in a function to look for all interactions that have not been followed up and that the followup_ts has passed. In these cases it should take the bot and followup_action and execute them. Also we may need to purge old interactions in the datastore every once in a while --- setup.cfg | 1 + src/glados/__init__.py | 10 +- src/glados/core.py | 68 ++++++++-- src/glados/db.py | 271 ++++++++++++++++++++++++++++++++++++++ src/glados/request.py | 207 ++++++++++++++++++++++++++++- tests/__init__.py | 2 +- tests/glados.yaml | 3 + tests/glados_limited.yaml | 3 + 8 files changed, 542 insertions(+), 23 deletions(-) create mode 100644 src/glados/db.py diff --git a/setup.cfg b/setup.cfg index f2855d6..b720dab 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,6 +33,7 @@ install_requires = slackclient==2.5.0 # fixes bug with LinkButtonElement pyyaml requests + sqlalchemy [options.packages.find] where=src diff --git a/src/glados/__init__.py b/src/glados/__init__.py index dcf468f..bca2d7d 100644 --- a/src/glados/__init__.py +++ b/src/glados/__init__.py @@ -17,15 +17,13 @@ from .configs import GladosConfig from .utils import read_config +from .db import DataStore, DataStoreInteraction + from .core import Glados # LOGGING_FORMAT = "%(asctime)s :: %(levelname)-8s :: [%(filename)s:%(lineno)s :: %(funcName)s() ] %(message)s" -LOGGING_FORMAT = ( - "%(levelname)-8s :: [%(filename)s:%(lineno)s :: %(funcName)s() ] %(message)s" -) -logging.basicConfig( - level=logging.DEBUG, format=LOGGING_FORMAT, datefmt="%Y-%m-%d %H:%M:%S" -) +LOGGING_FORMAT = "%(levelname)-8s :: [%(filename)s:%(lineno)s :: %(funcName)s() ] %(message)s" +logging.basicConfig(level=logging.DEBUG, format=LOGGING_FORMAT, datefmt="%Y-%m-%d %H:%M:%S") __all__ = [ diff --git a/src/glados/core.py b/src/glados/core.py index 63817cd..1b4cf09 100644 --- a/src/glados/core.py +++ b/src/glados/core.py @@ -1,4 +1,4 @@ -from typing import List, Dict, TYPE_CHECKING +from typing import List, Dict, TYPE_CHECKING, Optional import yaml import logging @@ -10,6 +10,7 @@ BotImporter, PluginImporter, read_config, + DataStore, ) if TYPE_CHECKING: @@ -20,11 +21,7 @@ class Glados: """Glados is the core of the GLaDOS package.""" def __init__( - self, - config_file=None, - plugins_folder=None, - bots_config_dir=None, - plugins_config_dir=None, + self, config_file=None, plugins_folder=None, bots_config_dir=None, plugins_config_dir=None, ): self.router = GladosRouter() self.plugins = list() # type: List[GladosPlugin] @@ -37,6 +34,8 @@ def __init__( self.logging_level = logging.getLevelName("DEBUG") self.logging_format = "%(asctime)s :: %(levelname)-8s :: [%(filename)s:%(lineno)s :: %(funcName)s() ] %(message)s" self.global_config = None + self.enable_datastore = False + self.datastore = None # type: Optional[DataStore] def read_config(self, bot_name=None): # TODO: Fix logging setup @@ -53,9 +52,7 @@ def read_config(self, bot_name=None): self.logging_level = config.get("logging_level", self.logging_level) self.logging_format = config.get("logging_format", self.logging_format) logging.basicConfig( - level=self.logging_level, - format=self.logging_format, - datefmt="%Y-%m-%d %H:%M:%S", + level=self.logging_level, format=self.logging_format, datefmt="%Y-%m-%d %H:%M:%S", ) self.plugins_folder = config.get("plugins_folder") @@ -73,6 +70,36 @@ def read_config(self, bot_name=None): if import_plugins == "limited": self.import_plugins(bot_name=bot_name) + # Config datastore + if "datastore" in self.global_config.sections: + ds_config = self.global_config.config.datastore + ds_enabled = ds_config.get("enabled", False) + ds_host = ds_config.get("host") + ds_port = ds_config.get("port", 5432) + ds_username = ds_config.get("username") + ds_password = ds_config.get("password") + ds_database = ds_config.get("database", "glados") + ds_recreate = ds_config.get("recreate", False) + if None in [ds_enabled, ds_host, ds_port, ds_username, ds_password, ds_database]: + logging.warning( + "missing datastore config item(s) or datastore disabled. disabling datastore." + ) + self.enable_datastore = False + else: + self.enable_datastore = ds_enabled + if ds_enabled: + self.datastore = DataStore( + host=ds_host, + port=ds_port, + username=ds_username, + password=ds_password, + database=ds_database, + ) + self.datastore.create_table(force=ds_recreate) + else: + logging.warning("datastore section not found in config file") + self.enable_datastore = False + def import_bots(self): """Import all discovered bots""" logging.info("importing bots...") @@ -136,9 +163,14 @@ def add_bot(self, bot: GladosBot): """ self.bots[bot.name] = bot + def has_datastore(self): + return True if self.enable_datastore is True and self.datastore is not None else False + def request(self, request: GladosRequest): """Send a request to GLaDOS. + This function will also set the datastore session for the request, try to find the interaction in the datastore and fetch it. This info is available in the request. + Parameters ---------- request : GladosRequest @@ -148,4 +180,20 @@ def request(self, request: GladosRequest): ------- """ - return self.router.exec_route(request) + # DataStore actions if enabled + if self.has_datastore(): + request.set_datastore(self.datastore) + request.set_interaction_from_datastore() + + response = self.router.exec_route(request) + + if self.has_datastore() and request.auto_link and request.new_interaction: + try: + request.link_interaction_to_message_response(request.new_interaction, response) + except Exception as e: + logging.error(f"error linking response to interaction: {e} response: {response}") + + if self.has_datastore(): + request.close_session() + + return response diff --git a/src/glados/db.py b/src/glados/db.py new file mode 100644 index 0000000..47759ed --- /dev/null +++ b/src/glados/db.py @@ -0,0 +1,271 @@ +import logging +from datetime import datetime +from typing import List, Optional +from uuid import uuid4 + +from sqlalchemy import Column, DateTime, Integer, MetaData, String, Table, create_engine +from sqlalchemy.dialects.postgresql import JSONB, UUID +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import Query, Session, sessionmaker + +Metadata = MetaData() +Base = declarative_base(metadata=Metadata) + +TABLE_INTERACTIONS = "interactions" + + +class DataStoreInteraction(Base): + __tablename__ = TABLE_INTERACTIONS + interaction_id = Column(UUID, primary_key=True, default=str(uuid4())) + ts = Column(DateTime, default=datetime.now()) + bot = Column(String, nullable=False) + data = Column(JSONB, default=dict()) + message_channel = Column(String, default=None) + message_ts = Column(DateTime, default=None) + ttl = Column(Integer, default=None) + followup_ts = Column(DateTime, default=None) + followup_action = Column(String, default=None) + cron_followup_action = Column(String, default=None) + followed_up = Column(DateTime, default=None) + followed_up_ts = Column(DateTime, default=None) + + @property + def __sql_values__(self) -> dict: + values = self.__dict__.copy() + values.pop("_sa_instance_state") + values.pop("interaction_id") + return values + + @property + def object(self): + return self + + def update_row(self, session: Session): + session.query(DataStoreInteraction).filter( + DataStoreInteraction.interaction_id == self.interaction_id + ).update(self.__sql_values__) + + +class DataStore: + def __init__( + self, host: str, username: str, password: str, port: int = 5432, database: str = "glados" + ): + self.host = host + self.port = port + self.username = username + self.password = password + self.database = database + + self.db = create_engine( + f"postgres://{self.username}:{self.password}@{self.host}:{self.port}/" + f"{self.database}" + ) + + def create_session(self) -> Session: + """Generate a new session with the existing connection. + + Returns + ------- + Session: + the session that was generated + """ + session = sessionmaker(self.db) # type: sessionmaker + return session() + + def table_exists(self, table=TABLE_INTERACTIONS) -> bool: + """Check to see if the GLaDOS table is found in postgres. + + Returns + ------- + bool: + True if the table is found. False if it is not found. + + """ + return table in self.db.table_names() + + def drop_table(self, table=TABLE_INTERACTIONS, force=False): + """Drop the GLaDOS table so that it can be re-created. + + Returns + ------- + bool: + was the drop table successful. + """ + Table(table, Metadata).drop(self.db, checkfirst=not force) + + def create_table(self, tables: Optional[List[str]] = None, force=False): + """Create the table. + + If you set force to True then it will drop the existing tables and + then recreate them. ALL DATA WILL BE LOST + + Parameters + ---------- + tables : Optional[List[str]] + only take action on these tables. If None, then take action on all tables + force : bool + drop existing tables and rebuild. (default: False) + + Returns + ------- + + """ + if force: + if tables: + for table in tables: + self.drop_table(table) + else: + Base.metadata.drop_all(self.db, checkfirst=False) + + if tables: + for table in tables: + Table(table, Metadata).create(self.db) + else: + Metadata.create_all(self.db) + + def find_by_id(self, interaction_id: str, session: Session) -> DataStoreInteraction: + """Find an interaction by interaction_id. + + Parameters + ---------- + interaction_id : str + interaction ID to find + session : Session + session to be used + + Returns + ------- + DataStore : + The interaction object + + """ + result = session.query(DataStoreInteraction).get(interaction_id) + + logging.debug(result.interaction_id) + return result.object + + def update_interaction(self, interaction_id, session: Session, **kwargs) -> DataStoreInteraction: + """Find and update an interaction with the provided values. + + Parameters + ---------- + interaction_id : str + interaction ID to update + session : Session + session to be used + kwargs : dict + fields and new values to update + + Returns + ------- + + """ + s = ( + session.query(DataStoreInteraction) + .filter(DataStoreInteraction.interaction_id == interaction_id) + .update(kwargs) + ) + return s + + def insert_interaction(self, interaction: DataStoreInteraction, session: Session): + """Insert an interaction object into the database. + + Parameters + ---------- + row : DataStoreInteraction + The row to be inserted + session : Session + session to be used + + Returns + ------- + + """ + session.add(interaction) + session.commit() + return interaction + + def link_to_message_response( + self, interaction_id: str, message_response: dict, session: Session + ): + """Add info from the Slack message into the database for the interaction. + + Parameters + ---------- + interaction_id : str + The interaction ID that was returned on adding the message to the database. + message_response : dict + The raw message response from slack. The channel and ts will be pulled from this. + session: Session + session to be used + + Returns + ------- + + """ + ts_str = message_response.get("ts") + if not ts_str: + raise KeyError(f"ts missing from message body: {message_response}") + channel = message_response.get("channel", {}).get("id") + if not channel: + raise KeyError(f"channel missing from message body: {message_response}") + ts = datetime.fromtimestamp(ts_str) + + self.update_interaction(interaction_id, session, message_channel=channel, message_ts=ts) + + def link_to_message(self, interaction_id: str, channel: str, ts: datetime, session: "Session"): + """Link to message by setting message ts and channel. + + Parameters + ---------- + interaction_id : str + interaction ID to link + channel : str + channel to link interaction to + ts : datetime + ts to link interaction to + session : Session + session to be used + + Returns + ------- + + """ + self.update_interaction(interaction_id, session, message_channel=channel, message_ts=ts) + + def find_interaction_by_channel_ts( + self, channel: str, ts: datetime, session: Session + ) -> Optional[DataStoreInteraction]: + """Find the interaction in the datastore by channel and message ts. + + Parameters + ---------- + channel : str + channel of the interaction youre looking for + ts : datetime + ts of the interaction you are looking for + session : Session + session to be used + + Returns + ------- + DataStoreInteraction : + the interaction object + + Raises + ------ + ReferenceError : + There were more than one interaction that matched the channel and message_ts + """ + query = session.query(DataStoreInteraction).filter( + DataStoreInteraction.message_ts == ts and DataStoreInteraction.message_channel == channel + ) # type: Query + if query.count() == 1: + return query.all()[0] + elif query.count() == 0: + logging.error(f"no matching interaction for channel: {channel} and ts: {ts}") + return None + else: + raise ReferenceError( + f"more than one matching interaction for channel: {channel} and ts: " f"{ts}" + ) diff --git a/src/glados/request.py b/src/glados/request.py index 13dfd3a..9c92230 100644 --- a/src/glados/request.py +++ b/src/glados/request.py @@ -1,5 +1,12 @@ from glados import RouteType, BOT_ROUTES, PyJSON -from typing import Union +from typing import Union, Optional, TYPE_CHECKING +import json +import logging +from datetime import datetime + +if TYPE_CHECKING: + from sqlalchemy.orm import Session + from glados import DataStoreInteraction, DataStore class SlackVerification: @@ -44,6 +51,8 @@ class GladosRequest: The name of the bot to send the request to. This is used for select RouteTypes json: the json paylod of the request + data: dict + data to send with the request. This should be from a database kwargs Examples @@ -65,6 +74,7 @@ def __init__( slack_verify: SlackVerification = None, bot_name: str = None, json: Union[str, dict] = None, + data: dict = None, **kwargs, ): @@ -79,6 +89,15 @@ def __init__( self.response_url = None self.trigger_id = None + self._data = data + + self._datastore = None # type: Optional[DataStore] + self._session = ( + None + ) # type: Optional[Session] # This is the datastore session for this request. + self._interaction = None # type: Optional[DataStoreInteraction] # This is the interaction database object for this request. + self.auto_link = True # if this is true, then it will expect the response from the plugin to want to try to autolink to the interaction datastore. + if route_type is RouteType.Interaction: self.response_url = self.json.get("response_url") self.trigger_id = self.json.get("trigger_id") @@ -90,18 +109,194 @@ def __init__( if route_type is RouteType.Events: self._route = self.json.event.type + self.new_interaction = None + @property def route(self) -> str: """the actual route If the route automatically prefixed the route with the bot name, it will return the route with the prefix """ - return ( - f"{self.bot_name}_{self._route}" - if self.route_type in BOT_ROUTES - else self._route - ) + return f"{self.bot_name}_{self._route}" if self.route_type in BOT_ROUTES else self._route @route.setter def route(self, value): self._route = value + + def set_session(self, session: "Session"): + """Set the session for this request. + + Parameters + ---------- + session : Session + session to use for this request. + + Returns + ------- + None + + Raises + ------ + ConnectionError + If the session is not active raise a ConnectionError + """ + self._session = session + if not self._session.is_active: + raise ConnectionError("request session is not active") + + def set_datastore(self, datastore: "DataStore"): + """Set the Datastore and session for the request. + + Parameters + ---------- + datastore : DataStore + Datastore to use. This datastore will be used to create the session. + + Returns + ------- + + """ + self._datastore = datastore + self.set_session(self._datastore.create_session()) + + def set_interaction_from_datastore(self) -> None: + """Get the interaction object from the datastore. + + Returns + ------- + + """ + # Get the interaction object from the datastore + # see if there is channel and message ts in the payload. + if not self._session: + raise ConnectionError("session not set for request") + + container_payload = self.json.get("container") + if not container_payload: + logging.warning(f"no container block in body: {self.json.to_dict()}") + self._interaction = None + return self.interaction + + channel = container_payload.get("channel_id") + message_ts = container_payload.get("message_ts") + + if None in [channel, message_ts]: + logging.warning(f"missing channel_id or message_ts in container: {container_payload}") + self._interaction = None + return self.interaction + + interaction = self._datastore.find_interaction_by_channel_ts( + channel, message_ts, self._session + ) + self._interaction = interaction + + def add_interaction_to_datastore(self, interaction: DataStoreInteraction) -> Optional[DataStoreInteraction]: + """Add an interaction to the datastore and return the updated interaction. + + Notes + ----- + The interaction_id can be retrieved by doing interaction.interaction_id + + Parameters + ---------- + interaction : DataStoreInteraction + the interaction to be added + + Returns + ------- + DataStoreInteraction : + The inserted interaction object. + """ + if not self._datastore: + logging.warning("datastore not set for request") + return + if not self._session: + raise ConnectionError("session not set for request") + return self._datastore.insert_interaction(interaction, self._session) + + + def link_interaction_to_message_response(self, interaction_id: str, message_response: dict): + """Link interaction to message response + + Parameters + ---------- + interaction_id : str + interaction ID to be linked + message_response : dict + JSON payload response from sending message on slack. + + Returns + ------- + + """ + if not self._session: + raise ConnectionError("session not set for request") + + self._datastore.link_to_message_response(interaction_id, message_response, self._session) + + def link_interaction_to_message(self, interaction_id: str, channel: str, message_ts: datetime): + """Link interaction to message + + Parameters + ---------- + interaction_id : str + interaction ID to link + channel : str + channel to be linked to + message_ts : datetime + ts to be linked to + + Returns + ------- + + """ + if not self._session: + raise ConnectionError("session not set for request") + self._datastore.link_to_message(interaction_id, channel, message_ts, self._session) + + def close_session(self): + """Close session for request""" + if not self._session or not self._session.is_active: + self._session.close() + + def has_interaction(self) -> bool: + """Check if request has interaction. + + Returns + ------- + bool : + True if interaction is set. + """ + return True if self.interaction else False + + @property + def interaction_id(self): + if not self.has_interaction(): + return None + return self.interaction.interaction_id + + @property + def interaction(self): + if not self.has_interaction(): + return None + return self.interaction + + @property + def data(self) -> PyJSON: + return PyJSON(self._data) + + @property + def data_blob(self) -> dict: + return self._data + + @data.setter + def data(self, value): + if type(value) is str: + try: + self._data = json.loads(value) + except json.JSONDecodeError: + logging.error(f"JSONDecodeError on string {value}") + except Exception as e: + logging.error(f"{e} on parsing JSON from: {value}") + if type(value) is dict: + self._data = value diff --git a/tests/__init__.py b/tests/__init__.py index 625a768..570bcec 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -2,7 +2,7 @@ GLADOS_CONFIG_FILE = "tests/glados.yaml" GLADOS_CONFIG_FILE_LIMITED = "tests/glados_limited.yaml" -GLADOS_CONFIG_SECTIONS = sorted(["glados", "my_other_config"]) +GLADOS_CONFIG_SECTIONS = sorted(["datastore", "glados", "my_other_config"]) LOGGING_FORMAT = ( "%(levelname)-8s :: [%(filename)s:%(lineno)s :: %(funcName)s() ] %(message)s" diff --git a/tests/glados.yaml b/tests/glados.yaml index 7571e49..276b827 100644 --- a/tests/glados.yaml +++ b/tests/glados.yaml @@ -6,6 +6,9 @@ glados: logging_format: "%(levelname)-8s :: [%(filename)s:%(lineno)s :: %(funcName).5s() ] %(message)s" logging_level: DEBUG +datastore: + enabled: false + my_other_config: log: yes force_quit: no diff --git a/tests/glados_limited.yaml b/tests/glados_limited.yaml index d49a324..60b2644 100644 --- a/tests/glados_limited.yaml +++ b/tests/glados_limited.yaml @@ -7,6 +7,9 @@ glados: logging_format: "%(levelname)-8s :: [%(filename)s:%(lineno)s :: %(funcName).5s() ] %(message)s" logging_level: DEBUG +datastore: + enabled: false + my_other_config: log: yes force_quit: no From b95c32c244a7c09fde8844dcd7612e3609ad939f Mon Sep 17 00:00:00 2001 From: Zachary Priddy Date: Wed, 15 Apr 2020 14:56:58 -0700 Subject: [PATCH 02/10] Small Changes --- src/glados/db.py | 2 +- src/glados/request.py | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/glados/db.py b/src/glados/db.py index 47759ed..771056a 100644 --- a/src/glados/db.py +++ b/src/glados/db.py @@ -25,7 +25,7 @@ class DataStoreInteraction(Base): ttl = Column(Integer, default=None) followup_ts = Column(DateTime, default=None) followup_action = Column(String, default=None) - cron_followup_action = Column(String, default=None) + cron_followup_action = Column(String, default=None) # TODO(zpriddy): Do I need? Is this the same as followup_action? followed_up = Column(DateTime, default=None) followed_up_ts = Column(DateTime, default=None) diff --git a/src/glados/request.py b/src/glados/request.py index 9c92230..5df89aa 100644 --- a/src/glados/request.py +++ b/src/glados/request.py @@ -190,7 +190,9 @@ def set_interaction_from_datastore(self) -> None: ) self._interaction = interaction - def add_interaction_to_datastore(self, interaction: DataStoreInteraction) -> Optional[DataStoreInteraction]: + def add_interaction_to_datastore( + self, interaction: DataStoreInteraction + ) -> Optional[DataStoreInteraction]: """Add an interaction to the datastore and return the updated interaction. Notes @@ -214,7 +216,6 @@ def add_interaction_to_datastore(self, interaction: DataStoreInteraction) -> Opt raise ConnectionError("session not set for request") return self._datastore.insert_interaction(interaction, self._session) - def link_interaction_to_message_response(self, interaction_id: str, message_response: dict): """Link interaction to message response @@ -269,6 +270,18 @@ def has_interaction(self) -> bool: """ return True if self.interaction else False + def gen_new_interaction(self, *, followup_action=None, followup_ts=None, ttl=None, data=None): + if not data: + data = dict() + self.new_interaction = DataStoreInteraction( + bot=self.bot_name, + followup_action=followup_action, + followup_ts=followup_ts, + ttl=ttl, + data=data, + ) + return self.new_interaction + @property def interaction_id(self): if not self.has_interaction(): From 014240609180e0d96337a8b14fed98680cd2a579 Mon Sep 17 00:00:00 2001 From: Zachary Priddy Date: Wed, 15 Apr 2020 15:03:02 -0700 Subject: [PATCH 03/10] Fix import order --- src/glados/__init__.py | 3 ++- src/glados/request.py | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/glados/__init__.py b/src/glados/__init__.py index bca2d7d..b0c617a 100644 --- a/src/glados/__init__.py +++ b/src/glados/__init__.py @@ -1,6 +1,7 @@ import logging from .utils import PyJSON, get_var, get_enc_var +from .db import DataStore, DataStoreInteraction from .route_type import RouteType, EventRoutes, BOT_ROUTES, VERIFY_ROUTES from .request import GladosRequest, SlackVerification from .errors import ( @@ -17,7 +18,7 @@ from .configs import GladosConfig from .utils import read_config -from .db import DataStore, DataStoreInteraction + from .core import Glados diff --git a/src/glados/request.py b/src/glados/request.py index 5df89aa..a3cd4a1 100644 --- a/src/glados/request.py +++ b/src/glados/request.py @@ -1,4 +1,4 @@ -from glados import RouteType, BOT_ROUTES, PyJSON +from glados import RouteType, BOT_ROUTES, PyJSON, DataStoreInteraction, DataStore from typing import Union, Optional, TYPE_CHECKING import json import logging @@ -6,7 +6,6 @@ if TYPE_CHECKING: from sqlalchemy.orm import Session - from glados import DataStoreInteraction, DataStore class SlackVerification: @@ -271,6 +270,19 @@ def has_interaction(self) -> bool: return True if self.interaction else False def gen_new_interaction(self, *, followup_action=None, followup_ts=None, ttl=None, data=None): + """Generate a new interaction object and set it as new_interaction. + + Parameters + ---------- + followup_action : + followup_ts : + ttl : + data : + + Returns + ------- + + """ if not data: data = dict() self.new_interaction = DataStoreInteraction( From bb3ad51974cd753906e532404646cc84ef8c059c Mon Sep 17 00:00:00 2001 From: Zachary Priddy Date: Wed, 15 Apr 2020 15:04:41 -0700 Subject: [PATCH 04/10] Fix linting --- src/glados/__init__.py | 1 - src/glados/configs.py | 8 ++------ src/glados/db.py | 13 +++++++++---- src/glados/message_blocks.py | 20 +++++--------------- src/glados/plugin.py | 28 +++++++--------------------- 5 files changed, 23 insertions(+), 47 deletions(-) diff --git a/src/glados/__init__.py b/src/glados/__init__.py index b0c617a..eb1ae33 100644 --- a/src/glados/__init__.py +++ b/src/glados/__init__.py @@ -19,7 +19,6 @@ from .utils import read_config - from .core import Glados # LOGGING_FORMAT = "%(asctime)s :: %(levelname)-8s :: [%(filename)s:%(lineno)s :: %(funcName)s() ] %(message)s" diff --git a/src/glados/configs.py b/src/glados/configs.py index 024ab11..57bd780 100644 --- a/src/glados/configs.py +++ b/src/glados/configs.py @@ -22,14 +22,10 @@ def read_config(self): logging.critical(f"glados config file not found: {self.config_file} - {e}") raise FileNotFoundError(e) except OSError as e: - logging.critical( - f"error reading glados config file: {self.config_file} - {e}" - ) + logging.critical(f"error reading glados config file: {self.config_file} - {e}") raise OSError(e) except yaml.YAMLError as e: - logging.critical( - f"error reading yaml in glados config file: {self.config_file} - {e}" - ) + logging.critical(f"error reading yaml in glados config file: {self.config_file} - {e}") raise yaml.YAMLError(e) @property diff --git a/src/glados/db.py b/src/glados/db.py index 771056a..d451566 100644 --- a/src/glados/db.py +++ b/src/glados/db.py @@ -25,7 +25,9 @@ class DataStoreInteraction(Base): ttl = Column(Integer, default=None) followup_ts = Column(DateTime, default=None) followup_action = Column(String, default=None) - cron_followup_action = Column(String, default=None) # TODO(zpriddy): Do I need? Is this the same as followup_action? + cron_followup_action = Column( + String, default=None + ) # TODO(zpriddy): Do I need? Is this the same as followup_action? followed_up = Column(DateTime, default=None) followed_up_ts = Column(DateTime, default=None) @@ -42,7 +44,7 @@ def object(self): def update_row(self, session: Session): session.query(DataStoreInteraction).filter( - DataStoreInteraction.interaction_id == self.interaction_id + DataStoreInteraction.interaction_id == self.interaction_id ).update(self.__sql_values__) @@ -144,7 +146,9 @@ def find_by_id(self, interaction_id: str, session: Session) -> DataStoreInteract logging.debug(result.interaction_id) return result.object - def update_interaction(self, interaction_id, session: Session, **kwargs) -> DataStoreInteraction: + def update_interaction( + self, interaction_id, session: Session, **kwargs + ) -> DataStoreInteraction: """Find and update an interaction with the provided values. Parameters @@ -258,7 +262,8 @@ def find_interaction_by_channel_ts( There were more than one interaction that matched the channel and message_ts """ query = session.query(DataStoreInteraction).filter( - DataStoreInteraction.message_ts == ts and DataStoreInteraction.message_channel == channel + DataStoreInteraction.message_ts == ts + and DataStoreInteraction.message_channel == channel ) # type: Query if query.count() == 1: return query.all()[0] diff --git a/src/glados/message_blocks.py b/src/glados/message_blocks.py index b2dea89..e71d29c 100644 --- a/src/glados/message_blocks.py +++ b/src/glados/message_blocks.py @@ -192,9 +192,7 @@ def section( secondary content """ self._blocks.append( - SectionBlock( - text=text, block_id=block_id, fields=fields, accessory=accessory - ) + SectionBlock(text=text, block_id=block_id, fields=fields, accessory=accessory) ) return self @@ -237,15 +235,11 @@ def image( Cannot exceed 255 characters. """ self._blocks.append( - ImageBlock( - image_url=image_url, alt_text=alt_text, title=title, block_id=block_id - ) + ImageBlock(image_url=image_url, alt_text=alt_text, title=title, block_id=block_id) ) return self - def actions( - self, *, elements: List[InteractiveElement], block_id: Optional[str] = None - ): + def actions(self, *, elements: List[InteractiveElement], block_id: Optional[str] = None): """A block that is used to hold interactive elements. https://api.slack.com/reference/block-kit/blocks#actions @@ -258,9 +252,7 @@ def actions( self._blocks.append(ActionsBlock(elements=elements, block_id=block_id)) return self - def context( - self, *, elements: List[InteractiveElement], block_id: Optional[str] = None - ): + def context(self, *, elements: List[InteractiveElement], block_id: Optional[str] = None): """Displays message context, which can include both images and text. https://api.slack.com/reference/block-kit/blocks#context @@ -355,9 +347,7 @@ def submit_length(self): # # return True - @JsonValidator( - f"private_metadata cannot exceed {private_metadata_max_length} characters" - ) + @JsonValidator(f"private_metadata cannot exceed {private_metadata_max_length} characters") def private_metadata_max_length(self): if self._private_metadata is None: return True diff --git a/src/glados/plugin.py b/src/glados/plugin.py index aca6187..6e75ee4 100644 --- a/src/glados/plugin.py +++ b/src/glados/plugin.py @@ -41,9 +41,7 @@ def to_dict(self): # read the user config # running_config = plugin_config.update(user_config class PluginConfig: - def __init__( - self, name, config_file, module=None, enabled=False, bot=None, **kwargs - ): + def __init__(self, name, config_file, module=None, enabled=False, bot=None, **kwargs): if not bot: bot = dict() self.name = name @@ -121,18 +119,14 @@ def load_discovered_plugins_config(self, write_to_user_config=True): with open(config_file) as file: c = yaml.load(file, yaml.FullLoader) if len(c.keys()) != 1: - logging.critical( - f"zero or more than one object in config file: {config_file}" - ) + logging.critical(f"zero or more than one object in config file: {config_file}") continue plugin_name = list(c.keys())[0] c[plugin_name]["config_file"] = config_file plugin_package_config = PluginConfig(plugin_name, **c[plugin_name]) if plugin_name is None: - logging.critical( - f"invalid or missing plugin name. config file: {config_file}" - ) + logging.critical(f"invalid or missing plugin name. config file: {config_file}") continue user_config_path = Path(self.plugins_config_folder, f"{plugin_name}.yaml") @@ -151,9 +145,7 @@ def load_discovered_plugins_config(self, write_to_user_config=True): c = yaml.load(file, yaml.FullLoader) if len(c.keys()) != 1: - logging.critical( - f"zero or more than one object in config file: {config_file}" - ) + logging.critical(f"zero or more than one object in config file: {config_file}") continue c[plugin_name]["config_file"] = str(user_config_path) @@ -193,9 +185,7 @@ def get_required_bot( raise GladosError(f"no bot name set for plugin: {plugin_name}") bot = bots.get(bot_name) if not bot: - logging.error( - f"bot: {bot_name} is not found. disabling plugin: {plugin_name}" - ) + logging.error(f"bot: {bot_name} is not found. disabling plugin: {plugin_name}") raise GladosBotNotFoundError( f"bot: {bot_name} is not found as required for {plugin_name}" ) @@ -232,13 +222,9 @@ def __init__(self, config: PluginConfig, bot: GladosBot, **kwargs): self._routes = dict() # type: Dict[int, Dict[str, GladosRoute]] for route in RouteType._member_names_: - self._routes[ - RouteType[route].value - ] = dict() # type: Dict[str, GladosRoute] + self._routes[RouteType[route].value] = dict() # type: Dict[str, GladosRoute] - def add_route( - self, route_type: RouteType, route: Union[EventRoutes, str], function: Callable - ): + def add_route(self, route_type: RouteType, route: Union[EventRoutes, str], function: Callable): """Add a new route to the plugin Parameters From 794b43204d0bc4743e30aec2047e79274683ac8d Mon Sep 17 00:00:00 2001 From: Zachary Priddy Date: Wed, 15 Apr 2020 15:15:04 -0700 Subject: [PATCH 05/10] More linting - this time 80 wide lol --- src/glados/__init__.py | 8 ++++++-- src/glados/configs.py | 8 ++++++-- src/glados/core.py | 33 +++++++++++++++++++++++++++------ src/glados/db.py | 26 ++++++++++++++++++++------ src/glados/message_blocks.py | 20 +++++++++++++++----- src/glados/plugin.py | 28 +++++++++++++++++++++------- src/glados/request.py | 30 +++++++++++++++++++++++------- 7 files changed, 118 insertions(+), 35 deletions(-) diff --git a/src/glados/__init__.py b/src/glados/__init__.py index eb1ae33..f6f5fe4 100644 --- a/src/glados/__init__.py +++ b/src/glados/__init__.py @@ -22,8 +22,12 @@ from .core import Glados # LOGGING_FORMAT = "%(asctime)s :: %(levelname)-8s :: [%(filename)s:%(lineno)s :: %(funcName)s() ] %(message)s" -LOGGING_FORMAT = "%(levelname)-8s :: [%(filename)s:%(lineno)s :: %(funcName)s() ] %(message)s" -logging.basicConfig(level=logging.DEBUG, format=LOGGING_FORMAT, datefmt="%Y-%m-%d %H:%M:%S") +LOGGING_FORMAT = ( + "%(levelname)-8s :: [%(filename)s:%(lineno)s :: %(funcName)s() ] %(message)s" +) +logging.basicConfig( + level=logging.DEBUG, format=LOGGING_FORMAT, datefmt="%Y-%m-%d %H:%M:%S" +) __all__ = [ diff --git a/src/glados/configs.py b/src/glados/configs.py index 57bd780..024ab11 100644 --- a/src/glados/configs.py +++ b/src/glados/configs.py @@ -22,10 +22,14 @@ def read_config(self): logging.critical(f"glados config file not found: {self.config_file} - {e}") raise FileNotFoundError(e) except OSError as e: - logging.critical(f"error reading glados config file: {self.config_file} - {e}") + logging.critical( + f"error reading glados config file: {self.config_file} - {e}" + ) raise OSError(e) except yaml.YAMLError as e: - logging.critical(f"error reading yaml in glados config file: {self.config_file} - {e}") + logging.critical( + f"error reading yaml in glados config file: {self.config_file} - {e}" + ) raise yaml.YAMLError(e) @property diff --git a/src/glados/core.py b/src/glados/core.py index 1b4cf09..f5afe74 100644 --- a/src/glados/core.py +++ b/src/glados/core.py @@ -21,7 +21,11 @@ class Glados: """Glados is the core of the GLaDOS package.""" def __init__( - self, config_file=None, plugins_folder=None, bots_config_dir=None, plugins_config_dir=None, + self, + config_file=None, + plugins_folder=None, + bots_config_dir=None, + plugins_config_dir=None, ): self.router = GladosRouter() self.plugins = list() # type: List[GladosPlugin] @@ -52,7 +56,9 @@ def read_config(self, bot_name=None): self.logging_level = config.get("logging_level", self.logging_level) self.logging_format = config.get("logging_format", self.logging_format) logging.basicConfig( - level=self.logging_level, format=self.logging_format, datefmt="%Y-%m-%d %H:%M:%S", + level=self.logging_level, + format=self.logging_format, + datefmt="%Y-%m-%d %H:%M:%S", ) self.plugins_folder = config.get("plugins_folder") @@ -80,7 +86,14 @@ def read_config(self, bot_name=None): ds_password = ds_config.get("password") ds_database = ds_config.get("database", "glados") ds_recreate = ds_config.get("recreate", False) - if None in [ds_enabled, ds_host, ds_port, ds_username, ds_password, ds_database]: + if None in [ + ds_enabled, + ds_host, + ds_port, + ds_username, + ds_password, + ds_database, + ]: logging.warning( "missing datastore config item(s) or datastore disabled. disabling datastore." ) @@ -164,7 +177,11 @@ def add_bot(self, bot: GladosBot): self.bots[bot.name] = bot def has_datastore(self): - return True if self.enable_datastore is True and self.datastore is not None else False + return ( + True + if self.enable_datastore is True and self.datastore is not None + else False + ) def request(self, request: GladosRequest): """Send a request to GLaDOS. @@ -189,9 +206,13 @@ def request(self, request: GladosRequest): if self.has_datastore() and request.auto_link and request.new_interaction: try: - request.link_interaction_to_message_response(request.new_interaction, response) + request.link_interaction_to_message_response( + request.new_interaction, response + ) except Exception as e: - logging.error(f"error linking response to interaction: {e} response: {response}") + logging.error( + f"error linking response to interaction: {e} response: {response}" + ) if self.has_datastore(): request.close_session() diff --git a/src/glados/db.py b/src/glados/db.py index d451566..ef8b76e 100644 --- a/src/glados/db.py +++ b/src/glados/db.py @@ -50,7 +50,12 @@ def update_row(self, session: Session): class DataStore: def __init__( - self, host: str, username: str, password: str, port: int = 5432, database: str = "glados" + self, + host: str, + username: str, + password: str, + port: int = 5432, + database: str = "glados", ): self.host = host self.port = port @@ -215,9 +220,13 @@ def link_to_message_response( raise KeyError(f"channel missing from message body: {message_response}") ts = datetime.fromtimestamp(ts_str) - self.update_interaction(interaction_id, session, message_channel=channel, message_ts=ts) + self.update_interaction( + interaction_id, session, message_channel=channel, message_ts=ts + ) - def link_to_message(self, interaction_id: str, channel: str, ts: datetime, session: "Session"): + def link_to_message( + self, interaction_id: str, channel: str, ts: datetime, session: "Session" + ): """Link to message by setting message ts and channel. Parameters @@ -235,7 +244,9 @@ def link_to_message(self, interaction_id: str, channel: str, ts: datetime, sessi ------- """ - self.update_interaction(interaction_id, session, message_channel=channel, message_ts=ts) + self.update_interaction( + interaction_id, session, message_channel=channel, message_ts=ts + ) def find_interaction_by_channel_ts( self, channel: str, ts: datetime, session: Session @@ -268,9 +279,12 @@ def find_interaction_by_channel_ts( if query.count() == 1: return query.all()[0] elif query.count() == 0: - logging.error(f"no matching interaction for channel: {channel} and ts: {ts}") + logging.error( + f"no matching interaction for channel: {channel} and ts: {ts}" + ) return None else: raise ReferenceError( - f"more than one matching interaction for channel: {channel} and ts: " f"{ts}" + f"more than one matching interaction for channel: {channel} and ts: " + f"{ts}" ) diff --git a/src/glados/message_blocks.py b/src/glados/message_blocks.py index e71d29c..b2dea89 100644 --- a/src/glados/message_blocks.py +++ b/src/glados/message_blocks.py @@ -192,7 +192,9 @@ def section( secondary content """ self._blocks.append( - SectionBlock(text=text, block_id=block_id, fields=fields, accessory=accessory) + SectionBlock( + text=text, block_id=block_id, fields=fields, accessory=accessory + ) ) return self @@ -235,11 +237,15 @@ def image( Cannot exceed 255 characters. """ self._blocks.append( - ImageBlock(image_url=image_url, alt_text=alt_text, title=title, block_id=block_id) + ImageBlock( + image_url=image_url, alt_text=alt_text, title=title, block_id=block_id + ) ) return self - def actions(self, *, elements: List[InteractiveElement], block_id: Optional[str] = None): + def actions( + self, *, elements: List[InteractiveElement], block_id: Optional[str] = None + ): """A block that is used to hold interactive elements. https://api.slack.com/reference/block-kit/blocks#actions @@ -252,7 +258,9 @@ def actions(self, *, elements: List[InteractiveElement], block_id: Optional[str] self._blocks.append(ActionsBlock(elements=elements, block_id=block_id)) return self - def context(self, *, elements: List[InteractiveElement], block_id: Optional[str] = None): + def context( + self, *, elements: List[InteractiveElement], block_id: Optional[str] = None + ): """Displays message context, which can include both images and text. https://api.slack.com/reference/block-kit/blocks#context @@ -347,7 +355,9 @@ def submit_length(self): # # return True - @JsonValidator(f"private_metadata cannot exceed {private_metadata_max_length} characters") + @JsonValidator( + f"private_metadata cannot exceed {private_metadata_max_length} characters" + ) def private_metadata_max_length(self): if self._private_metadata is None: return True diff --git a/src/glados/plugin.py b/src/glados/plugin.py index 6e75ee4..aca6187 100644 --- a/src/glados/plugin.py +++ b/src/glados/plugin.py @@ -41,7 +41,9 @@ def to_dict(self): # read the user config # running_config = plugin_config.update(user_config class PluginConfig: - def __init__(self, name, config_file, module=None, enabled=False, bot=None, **kwargs): + def __init__( + self, name, config_file, module=None, enabled=False, bot=None, **kwargs + ): if not bot: bot = dict() self.name = name @@ -119,14 +121,18 @@ def load_discovered_plugins_config(self, write_to_user_config=True): with open(config_file) as file: c = yaml.load(file, yaml.FullLoader) if len(c.keys()) != 1: - logging.critical(f"zero or more than one object in config file: {config_file}") + logging.critical( + f"zero or more than one object in config file: {config_file}" + ) continue plugin_name = list(c.keys())[0] c[plugin_name]["config_file"] = config_file plugin_package_config = PluginConfig(plugin_name, **c[plugin_name]) if plugin_name is None: - logging.critical(f"invalid or missing plugin name. config file: {config_file}") + logging.critical( + f"invalid or missing plugin name. config file: {config_file}" + ) continue user_config_path = Path(self.plugins_config_folder, f"{plugin_name}.yaml") @@ -145,7 +151,9 @@ def load_discovered_plugins_config(self, write_to_user_config=True): c = yaml.load(file, yaml.FullLoader) if len(c.keys()) != 1: - logging.critical(f"zero or more than one object in config file: {config_file}") + logging.critical( + f"zero or more than one object in config file: {config_file}" + ) continue c[plugin_name]["config_file"] = str(user_config_path) @@ -185,7 +193,9 @@ def get_required_bot( raise GladosError(f"no bot name set for plugin: {plugin_name}") bot = bots.get(bot_name) if not bot: - logging.error(f"bot: {bot_name} is not found. disabling plugin: {plugin_name}") + logging.error( + f"bot: {bot_name} is not found. disabling plugin: {plugin_name}" + ) raise GladosBotNotFoundError( f"bot: {bot_name} is not found as required for {plugin_name}" ) @@ -222,9 +232,13 @@ def __init__(self, config: PluginConfig, bot: GladosBot, **kwargs): self._routes = dict() # type: Dict[int, Dict[str, GladosRoute]] for route in RouteType._member_names_: - self._routes[RouteType[route].value] = dict() # type: Dict[str, GladosRoute] + self._routes[ + RouteType[route].value + ] = dict() # type: Dict[str, GladosRoute] - def add_route(self, route_type: RouteType, route: Union[EventRoutes, str], function: Callable): + def add_route( + self, route_type: RouteType, route: Union[EventRoutes, str], function: Callable + ): """Add a new route to the plugin Parameters diff --git a/src/glados/request.py b/src/glados/request.py index a3cd4a1..bbdd47f 100644 --- a/src/glados/request.py +++ b/src/glados/request.py @@ -116,7 +116,11 @@ def route(self) -> str: If the route automatically prefixed the route with the bot name, it will return the route with the prefix """ - return f"{self.bot_name}_{self._route}" if self.route_type in BOT_ROUTES else self._route + return ( + f"{self.bot_name}_{self._route}" + if self.route_type in BOT_ROUTES + else self._route + ) @route.setter def route(self, value): @@ -180,7 +184,9 @@ def set_interaction_from_datastore(self) -> None: message_ts = container_payload.get("message_ts") if None in [channel, message_ts]: - logging.warning(f"missing channel_id or message_ts in container: {container_payload}") + logging.warning( + f"missing channel_id or message_ts in container: {container_payload}" + ) self._interaction = None return self.interaction @@ -215,7 +221,9 @@ def add_interaction_to_datastore( raise ConnectionError("session not set for request") return self._datastore.insert_interaction(interaction, self._session) - def link_interaction_to_message_response(self, interaction_id: str, message_response: dict): + def link_interaction_to_message_response( + self, interaction_id: str, message_response: dict + ): """Link interaction to message response Parameters @@ -232,9 +240,13 @@ def link_interaction_to_message_response(self, interaction_id: str, message_resp if not self._session: raise ConnectionError("session not set for request") - self._datastore.link_to_message_response(interaction_id, message_response, self._session) + self._datastore.link_to_message_response( + interaction_id, message_response, self._session + ) - def link_interaction_to_message(self, interaction_id: str, channel: str, message_ts: datetime): + def link_interaction_to_message( + self, interaction_id: str, channel: str, message_ts: datetime + ): """Link interaction to message Parameters @@ -252,7 +264,9 @@ def link_interaction_to_message(self, interaction_id: str, channel: str, message """ if not self._session: raise ConnectionError("session not set for request") - self._datastore.link_to_message(interaction_id, channel, message_ts, self._session) + self._datastore.link_to_message( + interaction_id, channel, message_ts, self._session + ) def close_session(self): """Close session for request""" @@ -269,7 +283,9 @@ def has_interaction(self) -> bool: """ return True if self.interaction else False - def gen_new_interaction(self, *, followup_action=None, followup_ts=None, ttl=None, data=None): + def gen_new_interaction( + self, *, followup_action=None, followup_ts=None, ttl=None, data=None + ): """Generate a new interaction object and set it as new_interaction. Parameters From 2768d9dbce8446f1c3eb3c5cee8e4100f906ed56 Mon Sep 17 00:00:00 2001 From: Zachary Priddy Date: Wed, 15 Apr 2020 16:52:18 -0700 Subject: [PATCH 06/10] Make requested changes --- setup.cfg | 2 +- src/glados/core.py | 15 ++++++++-- src/glados/db.py | 69 +++++++++++++++++-------------------------- src/glados/request.py | 36 +++++++++++++++++++--- 4 files changed, 72 insertions(+), 50 deletions(-) diff --git a/setup.cfg b/setup.cfg index b720dab..c2862ab 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,7 +33,7 @@ install_requires = slackclient==2.5.0 # fixes bug with LinkButtonElement pyyaml requests - sqlalchemy + sqlalchemy[postgresql] [options.packages.find] where=src diff --git a/src/glados/core.py b/src/glados/core.py index f5afe74..47dface 100644 --- a/src/glados/core.py +++ b/src/glados/core.py @@ -199,8 +199,13 @@ def request(self, request: GladosRequest): """ # DataStore actions if enabled if self.has_datastore(): - request.set_datastore(self.datastore) - request.set_interaction_from_datastore() + try: + request.set_datastore(self.datastore) + request.set_interaction_from_datastore() + except Exception as e: + logging.error( + f"error setting up datastore or retrieving interaction : {e} for request: {request}" + ) response = self.router.exec_route(request) @@ -213,8 +218,12 @@ def request(self, request: GladosRequest): logging.error( f"error linking response to interaction: {e} response: {response}" ) + request._session.rollback() + finally: + request.close_session() + return response - if self.has_datastore(): + elif self.has_datastore(): request.close_session() return response diff --git a/src/glados/db.py b/src/glados/db.py index ef8b76e..443adc3 100644 --- a/src/glados/db.py +++ b/src/glados/db.py @@ -7,6 +7,7 @@ from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import Query, Session, sessionmaker +from sqlalchemy.orm.exc import MultipleResultsFound Metadata = MetaData() Base = declarative_base(metadata=Metadata) @@ -31,21 +32,10 @@ class DataStoreInteraction(Base): followed_up = Column(DateTime, default=None) followed_up_ts = Column(DateTime, default=None) - @property - def __sql_values__(self) -> dict: - values = self.__dict__.copy() - values.pop("_sa_instance_state") - values.pop("interaction_id") - return values - - @property - def object(self): - return self - - def update_row(self, session: Session): - session.query(DataStoreInteraction).filter( - DataStoreInteraction.interaction_id == self.interaction_id - ).update(self.__sql_values__) + def update(self, **kwargs): + for k, v in kwargs: + if hasattr(self, k): + setattr(self, k, v) class DataStore: @@ -67,6 +57,7 @@ def __init__( f"postgres://{self.username}:{self.password}@{self.host}:{self.port}/" f"{self.database}" ) + self.session_maker = sessionmaker(self.db) def create_session(self) -> Session: """Generate a new session with the existing connection. @@ -76,8 +67,7 @@ def create_session(self) -> Session: Session: the session that was generated """ - session = sessionmaker(self.db) # type: sessionmaker - return session() + return self.session_maker() def table_exists(self, table=TABLE_INTERACTIONS) -> bool: """Check to see if the GLaDOS table is found in postgres. @@ -147,9 +137,7 @@ def find_by_id(self, interaction_id: str, session: Session) -> DataStoreInteract """ result = session.query(DataStoreInteraction).get(interaction_id) - - logging.debug(result.interaction_id) - return result.object + return result def update_interaction( self, interaction_id, session: Session, **kwargs @@ -162,19 +150,22 @@ def update_interaction( interaction ID to update session : Session session to be used - kwargs : dict + kwargs : fields and new values to update Returns ------- """ - s = ( - session.query(DataStoreInteraction) - .filter(DataStoreInteraction.interaction_id == interaction_id) - .update(kwargs) - ) - return s + interaction = session.query(DataStoreInteraction).get( + interaction_id + ) # type: DataStoreInteraction + for k, v in kwargs.items(): + if hasattr(interaction, k): + continue + kwargs.pop(k) + interaction.update(**kwargs) + return interaction def insert_interaction(self, interaction: DataStoreInteraction, session: Session): """Insert an interaction object into the database. @@ -272,19 +263,13 @@ def find_interaction_by_channel_ts( ReferenceError : There were more than one interaction that matched the channel and message_ts """ - query = session.query(DataStoreInteraction).filter( - DataStoreInteraction.message_ts == ts - and DataStoreInteraction.message_channel == channel + query = ( + session.query(DataStoreInteraction) + .filter(DataStoreInteraction.message_ts == ts) + .filter(DataStoreInteraction.message_channel == channel) ) # type: Query - if query.count() == 1: - return query.all()[0] - elif query.count() == 0: - logging.error( - f"no matching interaction for channel: {channel} and ts: {ts}" - ) - return None - else: - raise ReferenceError( - f"more than one matching interaction for channel: {channel} and ts: " - f"{ts}" - ) + try: + result = query.one_or_none() + return result + except MultipleResultsFound as e: + raise ReferenceError(e) diff --git a/src/glados/request.py b/src/glados/request.py index bbdd47f..aac0785 100644 --- a/src/glados/request.py +++ b/src/glados/request.py @@ -95,7 +95,7 @@ def __init__( None ) # type: Optional[Session] # This is the datastore session for this request. self._interaction = None # type: Optional[DataStoreInteraction] # This is the interaction database object for this request. - self.auto_link = True # if this is true, then it will expect the response from the plugin to want to try to autolink to the interaction datastore. + self.auto_link = False # if this is true, then it will expect the response from the plugin to want to try to autolink to the interaction datastore. if route_type is RouteType.Interaction: self.response_url = self.json.get("response_url") @@ -273,6 +273,11 @@ def close_session(self): if not self._session or not self._session.is_active: self._session.close() + def rollback_session(self): + """Rollback the session.""" + if self._session.is_active: + self._session.rollback() + def has_interaction(self) -> bool: """Check if request has interaction. @@ -283,8 +288,19 @@ def has_interaction(self) -> bool: """ return True if self.interaction else False + def has_new_interaction(self) -> bool: + """check if request has a new interaction object.""" + return True if self.new_interaction else False + def gen_new_interaction( - self, *, followup_action=None, followup_ts=None, ttl=None, data=None + self, + *, + followup_action=None, + followup_ts=None, + ttl=None, + data=None, + auto_link: bool = True, + auto_set: bool = True, ): """Generate a new interaction object and set it as new_interaction. @@ -294,6 +310,10 @@ def gen_new_interaction( followup_ts : ttl : data : + auto_link: bool + set this request to auto-link using the return payload. The return payload must be the response from sending a slack message. + auto_set: bool + set this new interaction object as the request new_interaction Returns ------- @@ -301,14 +321,22 @@ def gen_new_interaction( """ if not data: data = dict() - self.new_interaction = DataStoreInteraction( + + self.auto_link = auto_link + + new_interaction = DataStoreInteraction( bot=self.bot_name, followup_action=followup_action, followup_ts=followup_ts, ttl=ttl, data=data, ) - return self.new_interaction + + if auto_set: + self.new_interaction = new_interaction + return self.new_interaction + + return new_interaction @property def interaction_id(self): From e764cac1db3eeeb8333b9ad2ea9982240c7cf344 Mon Sep 17 00:00:00 2001 From: Zachary Priddy Date: Wed, 15 Apr 2020 17:22:41 -0700 Subject: [PATCH 07/10] Add documentation --- docs-src/glados.db.rst | 7 +++++++ docs-src/modules.rst | 1 + src/glados/db.py | 33 +++++++++++++++++++++++++++++---- 3 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 docs-src/glados.db.rst diff --git a/docs-src/glados.db.rst b/docs-src/glados.db.rst new file mode 100644 index 0000000..ea55d40 --- /dev/null +++ b/docs-src/glados.db.rst @@ -0,0 +1,7 @@ +glados.db module +================= + +.. automodule:: glados.db + :members: + :undoc-members: + :show-inheritance: diff --git a/docs-src/modules.rst b/docs-src/modules.rst index d22d926..5fb4573 100644 --- a/docs-src/modules.rst +++ b/docs-src/modules.rst @@ -7,6 +7,7 @@ glados glados.bot glados.configs glados.core + glados.db glados.errors glados.message_blocks glados.plugin diff --git a/src/glados/db.py b/src/glados/db.py index 443adc3..3852072 100644 --- a/src/glados/db.py +++ b/src/glados/db.py @@ -16,6 +16,33 @@ class DataStoreInteraction(Base): + """DataStoreInteraction represents a row in the datastore. This is used to update data in the datastore. + + Attributes + ---------- + interaction_id: str + This is the primary key of the datastore. This is the ID of the entry in the datastore. + ts: datetime + This is the time the row was put into the database. + bot: str + This is the name of the bot it should use when completing followup actions. + data: dict + Any extra data stored with the interaction. This is a JSON blob. + message_channel: str + The channel that this interaction was sent to. + message_ts: datetime + The message timestamp when this interaction was sent. + ttl: int + How long this interaction should live for. + followup_ts: datetime + When should the follow up action happen. + followup_action: str + The action name to execute when following up. If None then no action will happen. + cron_followup_action: str + The action name to execute on a normal cron schedule like every 5 min. If None then no action will happen. + followed_up: datetime + This is the time when the action was followed up at. If it has not happened yet this value will be None. + """ __tablename__ = TABLE_INTERACTIONS interaction_id = Column(UUID, primary_key=True, default=str(uuid4())) ts = Column(DateTime, default=datetime.now()) @@ -26,13 +53,11 @@ class DataStoreInteraction(Base): ttl = Column(Integer, default=None) followup_ts = Column(DateTime, default=None) followup_action = Column(String, default=None) - cron_followup_action = Column( - String, default=None - ) # TODO(zpriddy): Do I need? Is this the same as followup_action? + cron_followup_action = Column(String, default=None) followed_up = Column(DateTime, default=None) - followed_up_ts = Column(DateTime, default=None) def update(self, **kwargs): + """Update the object dropping any arguments that are not valid""" for k, v in kwargs: if hasattr(self, k): setattr(self, k, v) From e33a3d4dfdc0a05b0b0f29882ecfb06ef42b822c Mon Sep 17 00:00:00 2001 From: Zachary Priddy Date: Wed, 15 Apr 2020 17:35:31 -0700 Subject: [PATCH 08/10] Setup docs --- docs-src/glados.db.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-src/glados.db.rst b/docs-src/glados.db.rst index ea55d40..08d0931 100644 --- a/docs-src/glados.db.rst +++ b/docs-src/glados.db.rst @@ -3,5 +3,5 @@ glados.db module .. automodule:: glados.db :members: - :undoc-members: + :no-undoc-members: :show-inheritance: From 056cea1fe6b5dbf9b373ac17799d5c924bfd8d32 Mon Sep 17 00:00:00 2001 From: Zachary Priddy Date: Wed, 15 Apr 2020 17:36:44 -0700 Subject: [PATCH 09/10] Relinted --- src/glados/db.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/glados/db.py b/src/glados/db.py index 3852072..fc6ee76 100644 --- a/src/glados/db.py +++ b/src/glados/db.py @@ -43,6 +43,7 @@ class DataStoreInteraction(Base): followed_up: datetime This is the time when the action was followed up at. If it has not happened yet this value will be None. """ + __tablename__ = TABLE_INTERACTIONS interaction_id = Column(UUID, primary_key=True, default=str(uuid4())) ts = Column(DateTime, default=datetime.now()) From 0c6431e585c9c50ee27ae9d62037e04e8c6290f7 Mon Sep 17 00:00:00 2001 From: Zachary Priddy Date: Wed, 15 Apr 2020 17:40:17 -0700 Subject: [PATCH 10/10] one more time for docs --- docs-src/glados.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs-src/glados.rst b/docs-src/glados.rst index 99030fb..7042bcd 100644 --- a/docs-src/glados.rst +++ b/docs-src/glados.rst @@ -21,6 +21,7 @@ Submodules glados.bot glados.configs glados.core + glados.db glados.errors glados.message_blocks glados.plugin