diff --git a/docs-src/conf.py b/docs-src/conf.py index 7abdedc..fd483ae 100644 --- a/docs-src/conf.py +++ b/docs-src/conf.py @@ -32,7 +32,6 @@ "sphinx.ext.napoleon", "sphinx.ext.autodoc", "sphinx_rtd_theme", - #"sphinx.ext.autodoc.typehints", "sphinx_autodoc_typehints", "sphinx.ext.viewcode", "sphinx.ext.autosummary", diff --git a/docs-src/modules.rst b/docs-src/modules.rst index 5fb4573..c78e7b3 100644 --- a/docs-src/modules.rst +++ b/docs-src/modules.rst @@ -4,6 +4,7 @@ glados .. toctree:: :maxdepth: 6 + glados glados.bot glados.configs glados.core diff --git a/setup.cfg b/setup.cfg index c2862ab..a64edc7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = glados -version = 0.0.1-dev15 +version = 0.0.1-dev17 description = A library to help with slackbot development long_description = file: README.md long_description_content_type = text/markdown diff --git a/src/glados/configs.py b/src/glados/configs.py index 024ab11..20c2ea4 100644 --- a/src/glados/configs.py +++ b/src/glados/configs.py @@ -38,7 +38,6 @@ def sections(self) -> List[str]: Returns ------- - List[str]: sorted list of sections in the yaml file """ if not self.config: diff --git a/src/glados/core.py b/src/glados/core.py index 47dface..27ea59c 100644 --- a/src/glados/core.py +++ b/src/glados/core.py @@ -1,4 +1,4 @@ -from typing import List, Dict, TYPE_CHECKING, Optional +from typing import List, Dict, TYPE_CHECKING, Optional, NoReturn import yaml import logging @@ -22,10 +22,10 @@ class Glados: def __init__( self, - config_file=None, - plugins_folder=None, - bots_config_dir=None, - plugins_config_dir=None, + config_file: Optional[str] = None, + plugins_folder: Optional[str] = None, + bots_config_dir: Optional[str] = None, + plugins_config_dir: Optional[str] = None, ): self.router = GladosRouter() self.plugins = list() # type: List[GladosPlugin] @@ -41,7 +41,15 @@ def __init__( self.enable_datastore = False self.datastore = None # type: Optional[DataStore] - def read_config(self, bot_name=None): + def read_config(self, bot_name: Optional[str] = None) -> NoReturn: + """Read the GLaDOS config file. If a bot name is provided it will only install that bot. Else it will install all bots. + + Parameters + ---------- + bot_name + If provided, install only the bot with this name. + + """ # TODO: Fix logging setup if not self.config_file: logging.info("glados config file not set.") @@ -113,7 +121,7 @@ def read_config(self, bot_name=None): logging.warning("datastore section not found in config file") self.enable_datastore = False - def import_bots(self): + def import_bots(self) -> NoReturn: """Import all discovered bots""" logging.info("importing bots...") importer = BotImporter(self.bots_config_dir) @@ -121,8 +129,14 @@ def import_bots(self): self.bots = importer.bots.copy() logging.info(f"successfully imported {len(self.bots)} bots") - def import_plugins(self, bot_name=None): - """Import all discovered plugins and add them to the plugin list.""" + def import_plugins(self, bot_name: Optional[str] = None) -> NoReturn: + """Import all discovered plugins and add them to the plugin list. + + Parameters + ---------- + bot_name + If set GLaDOS will only import the bot name that is provided here. + """ logging.info("Importing plugins...") importer = PluginImporter(self.plugins_folder, self.plugins_config_dir) importer.discover_plugins() @@ -146,37 +160,30 @@ def import_plugins(self, bot_name=None): self.add_plugin(plugin) logging.info(f"successfully imported {len(self.plugins)} plugins") - def add_plugin(self, plugin: GladosPlugin): + def add_plugin(self, plugin: GladosPlugin) -> NoReturn: """Add a plugin to GLaDOS Parameters ---------- - plugin : GladosPlugin + plugin the plugin to be added to GLaDOS - - Returns - ------- - """ logging.debug(f"installing plugin: {plugin.name}") self.plugins.append(plugin) self.router.add_routes(plugin) - def add_bot(self, bot: GladosBot): + def add_bot(self, bot: GladosBot) -> NoReturn: """Add a new bot to GLaDOS. Parameters ---------- - bot : GladosBot + bot the bot to be added to GLaDOS - - Returns - ------- - """ self.bots[bot.name] = bot - def has_datastore(self): + def has_datastore(self) -> bool: + """Returns True if there is a datastore else False""" return ( True if self.enable_datastore is True and self.datastore is not None @@ -184,18 +191,15 @@ def has_datastore(self): ) def request(self, request: GladosRequest): - """Send a request to GLaDOS. + """Send a request to GLaDOS. This returns whatever the plugin returns. 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 + request the request to be sent to GLaDOS - Returns - ------- - """ # DataStore actions if enabled if self.has_datastore(): diff --git a/src/glados/db.py b/src/glados/db.py index fc6ee76..60cf56b 100644 --- a/src/glados/db.py +++ b/src/glados/db.py @@ -1,6 +1,6 @@ import logging from datetime import datetime -from typing import List, Optional +from typing import List, Optional, NoReturn from uuid import uuid4 from sqlalchemy import Column, DateTime, Integer, MetaData, String, Table, create_engine @@ -20,27 +20,27 @@ class DataStoreInteraction(Base): Attributes ---------- - interaction_id: str + interaction_id: :obj: `str` This is the primary key of the datastore. This is the ID of the entry in the datastore. - ts: datetime + ts: :obj: `datetime` This is the time the row was put into the database. - bot: str + bot: :obj: `str` This is the name of the bot it should use when completing followup actions. - data: dict + data: :obj: `dict` Any extra data stored with the interaction. This is a JSON blob. - message_channel: str + message_channel: :obj: `str` The channel that this interaction was sent to. - message_ts: datetime + message_ts: :obj: `datetime` The message timestamp when this interaction was sent. - ttl: int + ttl: :obj: `int` How long this interaction should live for. - followup_ts: datetime + followup_ts: :obj: `datetime` When should the follow up action happen. - followup_action: str + followup_action: :obj: `str` The action name to execute when following up. If None then no action will happen. - cron_followup_action: str + cron_followup_action: :obj: `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 + followed_up: :obj: `datetime` This is the time when the action was followed up at. If it has not happened yet this value will be None. """ @@ -65,6 +65,22 @@ def update(self, **kwargs): class DataStore: + """DataStore is how GLaDOS stores async data. + + Parameters + ---------- + host + postgres host. + username + postgres username. + password + postgres password. + port + postgres port. + database + postgres database to use. + """ + def __init__( self, host: str, @@ -86,37 +102,37 @@ def __init__( self.session_maker = sessionmaker(self.db) def create_session(self) -> Session: - """Generate a new session with the existing connection. - - Returns - ------- - Session: - the session that was generated - """ + """Generate a new session with the existing connection.""" return self.session_maker() - def table_exists(self, table=TABLE_INTERACTIONS) -> bool: + def table_exists(self, table: str = 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. + Parameters + ---------- + table + table name to use. """ return table in self.db.table_names() - def drop_table(self, table=TABLE_INTERACTIONS, force=False): + def drop_table( + self, table: str = TABLE_INTERACTIONS, force: bool = False + ) -> NoReturn: """Drop the GLaDOS table so that it can be re-created. - Returns - ------- - bool: - was the drop table successful. + Parameters + ---------- + table + table name to use. + force + if True will fill force drop the table without checks. """ Table(table, Metadata).drop(self.db, checkfirst=not force) - def create_table(self, tables: Optional[List[str]] = None, force=False): + def create_table( + self, tables: Optional[List[str]] = None, force: bool = False + ) -> NoReturn: """Create the table. If you set force to True then it will drop the existing tables and @@ -124,14 +140,11 @@ def create_table(self, tables: Optional[List[str]] = None, force=False): Parameters ---------- - tables : Optional[List[str]] + tables only take action on these tables. If None, then take action on all tables - force : bool + force drop existing tables and rebuild. (default: False) - Returns - ------- - """ if force: if tables: @@ -151,16 +164,10 @@ def find_by_id(self, interaction_id: str, session: Session) -> DataStoreInteract Parameters ---------- - interaction_id : str + interaction_id interaction ID to find - session : Session + session session to be used - - Returns - ------- - DataStore : - The interaction object - """ result = session.query(DataStoreInteraction).get(interaction_id) return result @@ -172,16 +179,12 @@ def update_interaction( Parameters ---------- - interaction_id : str + interaction_id interaction ID to update - session : Session + session session to be used - kwargs : + kwargs fields and new values to update - - Returns - ------- - """ interaction = session.query(DataStoreInteraction).get( interaction_id @@ -193,19 +196,17 @@ def update_interaction( interaction.update(**kwargs) return interaction - def insert_interaction(self, interaction: DataStoreInteraction, session: Session): + def insert_interaction( + self, interaction: DataStoreInteraction, session: Session + ) -> NoReturn: """Insert an interaction object into the database. Parameters ---------- - row : DataStoreInteraction + interaction The row to be inserted - session : Session + session session to be used - - Returns - ------- - """ session.add(interaction) session.commit() @@ -213,21 +214,17 @@ def insert_interaction(self, interaction: DataStoreInteraction, session: Session def link_to_message_response( self, interaction_id: str, message_response: dict, session: Session - ): + ) -> NoReturn: """Add info from the Slack message into the database for the interaction. Parameters ---------- - interaction_id : str + interaction_id The interaction ID that was returned on adding the message to the database. - message_response : dict + message_response The raw message response from slack. The channel and ts will be pulled from this. - session: Session + session: session to be used - - Returns - ------- - """ ts_str = message_response.get("ts") if not ts_str: @@ -243,23 +240,19 @@ def link_to_message_response( def link_to_message( self, interaction_id: str, channel: str, ts: datetime, session: "Session" - ): + ) -> NoReturn: """Link to message by setting message ts and channel. Parameters ---------- - interaction_id : str + interaction_id interaction ID to link - channel : str + channel channel to link interaction to - ts : datetime + ts ts to link interaction to - session : Session + session session to be used - - Returns - ------- - """ self.update_interaction( interaction_id, session, message_channel=channel, message_ts=ts @@ -272,21 +265,16 @@ def find_interaction_by_channel_ts( Parameters ---------- - channel : str + channel channel of the interaction youre looking for - ts : datetime + ts ts of the interaction you are looking for - session : Session + session session to be used - Returns - ------- - DataStoreInteraction : - the interaction object - Raises ------ - ReferenceError : + ReferenceError There were more than one interaction that matched the channel and message_ts """ query = ( diff --git a/src/glados/request.py b/src/glados/request.py index aac0785..1b9754a 100644 --- a/src/glados/request.py +++ b/src/glados/request.py @@ -1,5 +1,5 @@ from glados import RouteType, BOT_ROUTES, PyJSON, DataStoreInteraction, DataStore -from typing import Union, Optional, TYPE_CHECKING +from typing import Union, Optional, TYPE_CHECKING, NoReturn import json import logging from datetime import datetime @@ -13,11 +13,11 @@ class SlackVerification: Parameters ---------- - data: str + data raw request body. This is used to verify the message is from slack. - timestamp: str + timestamp The X-Slack-Request-Timestamp from the headers of the request. This is used to verify the message is from slack. - signature: str + signature: The X-Slack-Signature from the headers of the request. This is used to verify the message is from slack. """ @@ -28,6 +28,7 @@ def __init__(self, data: str, timestamp: str = None, signature: str = None): @property def json(self) -> dict: + """Returns the dict of the SlackVerification""" return { "data": self.data, "timestamp": self.timestamp, @@ -40,17 +41,17 @@ class GladosRequest: Parameters ---------- - route_type: RouteType + route_type what type of route is this - route: str + route what is the route to be called - slack_verify: SlackVerification + slack_verify slack data used for verifying the request came from Slack - bot_name: str + bot_name The name of the bot to send the request to. This is used for select RouteTypes - json: + json the json paylod of the request - data: dict + data data to send with the request. This should be from a database kwargs @@ -126,49 +127,37 @@ def route(self) -> str: def route(self, value): self._route = value - def set_session(self, session: "Session"): + def set_session(self, session: "Session") -> NoReturn: """Set the session for this request. Parameters ---------- - session : Session + session session to use for this request. - Returns - ------- - None - Raises ------ - ConnectionError + :obj: `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"): + def set_datastore(self, datastore: "DataStore") -> NoReturn: """Set the Datastore and session for the request. Parameters ---------- - datastore : 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 - ------- - - """ + def set_interaction_from_datastore(self) -> NoReturn: + """Get the interaction object from the datastore.""" # Get the interaction object from the datastore # see if there is channel and message ts in the payload. if not self._session: @@ -206,13 +195,8 @@ def add_interaction_to_datastore( Parameters ---------- - interaction : DataStoreInteraction + interaction the interaction to be added - - Returns - ------- - DataStoreInteraction : - The inserted interaction object. """ if not self._datastore: logging.warning("datastore not set for request") @@ -223,19 +207,15 @@ def add_interaction_to_datastore( def link_interaction_to_message_response( self, interaction_id: str, message_response: dict - ): + ) -> NoReturn: """Link interaction to message response Parameters ---------- - interaction_id : str + interaction_id interaction ID to be linked - message_response : dict + message_response JSON payload response from sending message on slack. - - Returns - ------- - """ if not self._session: raise ConnectionError("session not set for request") @@ -246,16 +226,16 @@ def link_interaction_to_message_response( def link_interaction_to_message( self, interaction_id: str, channel: str, message_ts: datetime - ): + ) -> NoReturn: """Link interaction to message Parameters ---------- - interaction_id : str + interaction_id interaction ID to link - channel : str + channel channel to be linked to - message_ts : datetime + message_ts ts to be linked to Returns @@ -268,24 +248,18 @@ def link_interaction_to_message( interaction_id, channel, message_ts, self._session ) - def close_session(self): + def close_session(self) -> NoReturn: """Close session for request""" if not self._session or not self._session.is_active: self._session.close() - def rollback_session(self): + def rollback_session(self) -> NoReturn: """Rollback the session.""" if self._session.is_active: self._session.rollback() def has_interaction(self) -> bool: - """Check if request has interaction. - - Returns - ------- - bool : - True if interaction is set. - """ + """Check if request has interaction. """ return True if self.interaction else False def has_new_interaction(self) -> bool: @@ -301,23 +275,19 @@ def gen_new_interaction( data=None, auto_link: bool = True, auto_set: bool = True, - ): + ) -> DataStoreInteraction: """Generate a new interaction object and set it as new_interaction. Parameters ---------- - followup_action : - followup_ts : - ttl : - data : - auto_link: bool + followup_action + followup_ts + ttl + data + auto_link 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 + auto_set set this new interaction object as the request new_interaction - - Returns - ------- - """ if not data: data = dict() @@ -339,23 +309,27 @@ def gen_new_interaction( return new_interaction @property - def interaction_id(self): + def interaction_id(self) -> Optional[str]: + """Returns the interaction_id of request.interaction""" if not self.has_interaction(): return None return self.interaction.interaction_id @property - def interaction(self): + def interaction(self) -> Optional[DataStoreInteraction]: + """Returns the interaction for the request""" if not self.has_interaction(): return None return self.interaction @property def data(self) -> PyJSON: + """Returns the data object of the request""" return PyJSON(self._data) @property def data_blob(self) -> dict: + """Returns the raw dict of the data object""" return self._data @data.setter diff --git a/src/glados/router.py b/src/glados/router.py index 430fa20..00da0e4 100644 --- a/src/glados/router.py +++ b/src/glados/router.py @@ -20,20 +20,21 @@ def __repr__(self): class GladosRouter(object): + """GladosRouter""" def __init__(self, **kwargs): # routes are stored as: {RouteType.SendMessage: {"ask_user",ask_user, "confirm":confirm}} - self.routes = dict() # type: Dict[RouteType, Dict[str, GladosRoute]] + self.routes = dict() # type: Dict[RouteType, Dict[str, Callable]] 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, Callable] def add_route(self, plugin: "GladosPlugin", route: GladosRoute) -> NoReturn: """Add a route to the router Parameters ---------- - plugin: GladosPlugin + plugin the plugin the route belongs to - route: GladosRoute + route the route to be added Raises @@ -49,36 +50,28 @@ def add_route(self, plugin: "GladosPlugin", route: GladosRoute) -> NoReturn: ) self.routes[route.route_type.value][route.route] = plugin.send_request - def add_routes(self, plugin: "GladosPlugin"): + def add_routes(self, plugin: "GladosPlugin") -> NoReturn: """Add multiple routes to the router. Parameters ---------- - routes : GladosPlugin + routes the plugin to add routes from - Returns - ------- - """ for route in plugin.routes: self.add_route(plugin, route) - def get_route(self, route_type: RouteType, route: str) -> Optional[GladosRoute]: + def get_route(self, route_type: RouteType, route: str) -> Callable: """Get a GladosRoute object for the requested route. Parameters ---------- - route_type: RouteType + route_type the type of route to get - route: str + route the route to get - Returns - ------- - GladosRoute - return the request route - Raises ------ GladosRouteNotFoundError @@ -95,9 +88,9 @@ def route_function(self, route_type: RouteType, route: str) -> Callable: Parameters ---------- - route_type: RouteType + route_type the type of route to get - route: str + route the route to get Returns @@ -108,12 +101,12 @@ def route_function(self, route_type: RouteType, route: str) -> Callable: """ return self.get_route(route_type, route) - def exec_route(self, request): + def exec_route(self, request: GladosRequest): """Execute a route function directly Parameters ---------- - request: GladosRequest + request the GLaDOS request Returns