From 8df8c6b438724efdb88f8624fe60c1e67db9c59b Mon Sep 17 00:00:00 2001 From: Jacob Tomlinson Date: Sun, 7 Aug 2016 22:31:15 +0100 Subject: [PATCH 1/9] Check multiple locations for config files --- opsdroid/__main__.py | 6 +++++- opsdroid/loader.py | 16 ++++++++++++---- tests/test_loader.py | 6 +++--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/opsdroid/__main__.py b/opsdroid/__main__.py index 1831d0893..9517f627e 100644 --- a/opsdroid/__main__.py +++ b/opsdroid/__main__.py @@ -15,7 +15,11 @@ def main(): logging.info("Stated application") with OpsDroid() as opsdroid: loader = Loader(opsdroid) - opsdroid.config = loader.load_config_file("./configuration.yaml") + opsdroid.config = loader.load_config_file([ + "./configuration.yaml", + "~/.opsdroid/configuration.yaml", + "/etc/opsdroid/configuration.yaml" + ]) if "logging" in opsdroid.config: set_logging_level(opsdroid.config['logging']) loader.load_config(opsdroid.config) diff --git a/opsdroid/loader.py b/opsdroid/loader.py index d03f2dc8a..24f1786fd 100644 --- a/opsdroid/loader.py +++ b/opsdroid/loader.py @@ -71,11 +71,19 @@ def __init__(self, opsdroid): self.opsdroid = opsdroid logging.debug("Loaded loader") - def load_config_file(self, config_path): + def load_config_file(self, config_paths): """Load a yaml config file from path.""" - if not os.path.isfile(config_path): - self.opsdroid.critical("Config file " + config_path + - " not found", 1) + config_path = "" + for possible_path in config_paths: + if not os.path.isfile(possible_path): + logging.warning("Config file " + possible_path + + " not found", 1) + else: + config_path = possible_path + break + + if not config_path: + self.opsdroid.critical("No configuration files found", 1) try: with open(config_path, 'r') as stream: diff --git a/tests/test_loader.py b/tests/test_loader.py index 65c9f759b..f91e83d43 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -25,17 +25,17 @@ def reset_subprocess_mocks(self): def test_load_config_file(self): opsdroid, loader = self.setup() - config = loader.load_config_file("tests/configs/minimal.yaml") + config = loader.load_config_file(["tests/configs/minimal.yaml"]) self.assertIsNotNone(config) def test_load_non_existant_config_file(self): opsdroid, loader = self.setup() - loader.load_config_file("file_which_does_not_exist") + loader.load_config_file(["file_which_does_not_exist"]) self.assertEqual(len(opsdroid.mock_calls), 2) def test_load_broken_config_file(self): opsdroid, loader = self.setup() - loader.load_config_file("tests/configs/broken.yaml") + loader.load_config_file(["tests/configs/broken.yaml"]) self.assertRaises(yaml.YAMLError) def test_setup_modules(self): From 840d6434044044de6f611adbd8d6b078678fe020 Mon Sep 17 00:00:00 2001 From: Jacob Tomlinson Date: Tue, 9 Aug 2016 07:47:58 +0100 Subject: [PATCH 2/9] Update documentation (#20) * Started on documentation --- docs/configuration-reference.md | 113 ++++++++++++++++++++++++++++++++ docs/contributing.md | 57 ++++++++++++++++ docs/extending/connectors.md | 3 + docs/extending/databases.md | 3 + docs/extending/skills.md | 103 +++++++++++++++++++++++++++++ docs/getting-started.md | 85 ++++++++++++++++++++++++ docs/index.md | 36 +++++----- mkdocs.yml | 10 +++ 8 files changed, 389 insertions(+), 21 deletions(-) create mode 100644 docs/configuration-reference.md create mode 100644 docs/contributing.md create mode 100644 docs/extending/connectors.md create mode 100644 docs/extending/databases.md create mode 100644 docs/extending/skills.md create mode 100644 docs/getting-started.md diff --git a/docs/configuration-reference.md b/docs/configuration-reference.md new file mode 100644 index 000000000..372785d17 --- /dev/null +++ b/docs/configuration-reference.md @@ -0,0 +1,113 @@ +# Configuration reference + +## Config file + +For configuration you simply need to create a single YAML file named `configuration.yaml`. When you run opsdroid it will look for the file in the following places in order: + + * `./configuration.yaml` + * `~/.opsdroid/configuration.yaml` + * `/etc/opsdroid/configuration.yaml` + +The opsdroid project itself is very simple and requires modules to give it functionality. In your configuration file you must specify the connector, skill and database* modules you wish to use and any options they may require. + +**Connectors** are modules for connecting opsdroid to your specific chat service. **Skills** are modules which define what actions opsdroid should perform based on different chat messages. **Database** modules connect opsdroid to your chosen database and allows skills to store information between messages. + +## Reference + +### `connectors` + +Connector modules which are installed and connect opsdroid to a specific chat service. + +_Config options of the connectors themselves differ between connectors, see the connector documentation for details._ + +```yaml +connectors: + + slack: + token: "mysecretslacktoken" + + # conceptual connector + twitter: + oauth_key: "myoauthkey" + secret_key: "myoauthsecret" +``` + +See [module options](#module-options) for installing custom connectors. + +### `databases` + +Database modules which connect opsdroid to a persistent data storage service. + +Skills can store data in opsdroid's "memory", this is a dictionary which can be persisted in an external database. + +_Config options of the databases themselves differ between databases, see the database documentation for details._ + +```yaml +databases: + mongo: + host: "mymongohost.mycompany.com" + port: "27017" + database: "opsdroid" +``` + +See [module options](#module-options) for installing custom databases. + +### `logging` + +Set the logging level of opsdroid. + +All python logging levels are available in opsdroid. `logging` can be set to `debug`, `info`, `warning`, `error` and `critical`. + +```yaml +logging: debug + +connectors: + shell: + +skills: + hello: + seen: +``` + +### `skills` + +Skill modules which add functionality to opsdroid. + +_Config options of the skills themselves differ between skills, see the skill documentation for details._ + +```yaml +skills: + hello: + seen: +``` + +See [module options](#module-options) for installing custom skills. + +## Module options + +All modules are installed from git repositories. By default if no additional options are specified opsdroid will look for the repository at `https://github.com/opsdroid/-.git`. + +However if you wish to install a module from a different location you can specify the some more options. + +### `repo` + +A git url to install the module from. + +```yaml +connectors: + slack: + token: "mysecretslacktoken" + mynewconnector: + repo: https://github.com/username/myconnector.git +``` + +### `no-cache` + +Set this to do a fresh git clone of the module whenever you start opsdroid. + +```yaml +databases: + mongodb: + repo: https://github.com/username/mymongofork.git + no-cache: true +``` diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 000000000..57ab35fd4 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,57 @@ +# Contributing to the project + +Contributing to the opsdroid ecosystem is strongly encouraged. You can do this by creating modules to be used by opsdroid or by contributing to the project itself. + +## Workflow + +All contributors to the project, including [jacobtomlinson](https://github.com/jacobtomlinson), contribute using the following process: + + * Fork the main project to your own account + * Work on your changes on a feature branch + * Create a pull request back to the main project + * Tests and test coverage will be checked automatically + * A project maintainer will review and merge the pull request + +## Developing + +```shell +# clone the repo +git clone https://github.com/opsdroid/opsdroid.git +cd opsdroid + +# install project dependancies +pip install -r requirements.txt + +# run opsdroid +python -m opsdroid +``` + +Running the tests + +```shell +# install test runner +pip install -U tox + +# run tests +tox +``` + + +## Developing in containers + +Developing in containers can be a great way to ensure that opsdroid will run in a clean python environment and that all dependancies are captured. + +```shell +# build the container +docker build -t opsdroid/opsdroid:myfeature . + +# run opsdroid +docker run --rm -ti -v $(pwd):/usr/src/app opsdroid/opsdroid:myfeature +``` + +Running the tests + +```shell +# run tests +docker run --rm -ti -v $(pwd):/usr/src/app opsdroid/opsdroid:myfeature tox +``` diff --git a/docs/extending/connectors.md b/docs/extending/connectors.md new file mode 100644 index 000000000..09283d145 --- /dev/null +++ b/docs/extending/connectors.md @@ -0,0 +1,3 @@ +# Creating a connector + +To do... diff --git a/docs/extending/databases.md b/docs/extending/databases.md new file mode 100644 index 000000000..caee4130d --- /dev/null +++ b/docs/extending/databases.md @@ -0,0 +1,3 @@ +# Creating a database + +To do diff --git a/docs/extending/skills.md b/docs/extending/skills.md new file mode 100644 index 000000000..8a087f6e9 --- /dev/null +++ b/docs/extending/skills.md @@ -0,0 +1,103 @@ +# Creating skills + +Like all opsdroid modules skills are installed as a git repository. However skills are designed to be simpler than other modules to ensure that it is easy to get started. + +To create a skill you need to create a single python file in your repository with the same name as the skill repository. For example the skill `hello` has a single file called `hello.py`. + +Within this file should be functions which are decorated with an opsdroid skill function to let opsdroid know when to trigger the skill. Let's get started with an example. + +## Hello world + +```python +from opsdroid.skills import match_regex + +@match_regex('hi') +def hello(opsdroid, message): + message.respond('Hey') +``` + +In this example we are importing the `match_regex` decorator from the opsdroid skills library. We are then using it to decorate a simple hello world function. + +The decorator takes a regular expression to match against the message received from the connector. In this case we are checking to see if the message from the user is "hi". + +If the message matches the regular expression then the decorated function is called. As arguments opsdroid will pass a pointer to itself along with a Message object containing information about the message from the user. + +## Message object + +The message object passed to the skill function is an instance of the opsdroid Message class which has the following properties and methods. + +### `text` + +A _string_ containing the message from the user. + +### `user` + +A _string_ containing the username of the user who wrote the message. + +### `room` + +A _string_ containing the name of the room or chat channel the message was sent in. + +### `regex` + +A _[re match object](https://docs.python.org/2/library/re.html#re.MatchObject)_ for the regular expression the message was matched against. + +### `connector` + +A pointer to the opsdroid _connector object_ which receieved the message. + +### `respond(text)` + +A method which responds to the message in the same room using the same connector that it was received. + +## Persisting data + +opsdroid has a memory class which can be used to persist data between different connectors (which run in different process forks) and between restarts of the application. + +The data can be accessed via the `memory` property of the `opsdroid` pointer which is passed to the skill function. The `memory` object has the following methods. + +### `get(key)` + +Returns an object from the memory for the key provided. + +### `put(key, object)` + +Stores the object provided for a specific key. + +### Example + +```python +from opsdroid.skills import match_regex + +@match_regex(r'remember (.*)') +def remember(opsdroid, message): + remember = message.regex.group(1) + opsdroid.memory.put("remember", remember) + message.respond("OK I'll remember that") + +@match_regex(r'remind me') +def remember(opsdroid, message): + message.respond( + opsdroid.memory.get("remember") + ) +``` + +In the above example we have defined two skill functions. The first takes whatever the user says after the work "remember" and stores it in the database. + +The second retrieves and prints out that text when the user says "remind me". + +## Setup + +If your skill requires any setup to be done when opsdroid is started you can create a method simple called `setup` which takes a pointer to opsdroid as it's only argument. + +```python +def setup(opsdroid): + # do some setup stuff here +``` + +## Example modules + +See the following official modules for examples: + + * [hello](https://github.com/opsdroid/skill-hello) - A simple hello world skill. + * [seen](https://github.com/opsdroid/skill-seen) - Makes use of opsdroid memory. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 000000000..89b891fac --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,85 @@ +# Getting started + +## Installation + +You can simply install opsdroid using `pip`. + +``` +pip3 install opsdroid +``` + +Or you can use the official docker image. + +``` +docker pull opsdroid/opsdroid:latest +``` + +## Configuration + +For configuration you simply need to create a single YAML file named `configuration.yaml`. When you run opsdroid it will look for the file in the following places in order: + + * `./configuration.yaml` + * `~/.opsdroid/configuration.yaml` + * `/etc/opsdroid/configuration.yaml` + +The opsdroid project itself is very simple and requires modules to give it functionality. In your configuration file you must specify the connector, skill and database* modules you wish to use and any options they may require. + +**Connectors** are modules for connecting opsdroid to your specific chat service. **Skills** are modules which define what actions opsdroid should perform based on different chat messages. **Database** modules connect opsdroid to your chosen database and allows skills to store information between messages. + +For example a simple barebones configuration would look like: + +```yaml +connectors: + shell: + +skills: + hello: +``` + +This tells opsdroid to use the [shell connector](https://github.com/opsdroid/connector-shell) and [hello skill](https://github.com/opsdroid/skill-hello) from the official module library. + +In opsdroid all modules are git repositories which will be cloned locally the first time they are used. By default if you do not specify a repository opsdroid will look at `https://github.com/opsdroid/-.git` for the repository. Therefore in the above configuration the `connector-shell` and `skill-hello` repositories were pulled from the opsdroid organisation on GitHub. + +You are of course encouraged to write your own modules and make them available on GitHub or any other repository host which is accessible by your opsdroid installation. + +A more advanced config would like similar to the following: + +```yaml +connectors: + slack: + token: "mysecretslacktoken" + +databases: + mongo: + host: "mymongohost.mycompany.com" + port: "27017" + database: "opsdroid" + +skills: + hello: + seen: + myawesomeskill: + repo: "https://github.com/username/myawesomeskill.git" +``` + +In this configuration we are using the [slack connector](https://github.com/opsdroid/connector-slack) with a slack [auth token](https://api.slack.com/tokens) supplied, a [mongo database](https://github.com/opsdroid/database-mongo) connection for persisting data, `hello` and `seen` skills from the official repos and finally a custom skill hosted on GitHub. + +Configuration options such as the `token` in the slack connector or the `host`, `port` and `database` options in the mongo database are specific to those modules. Ensure you check each module's required configuration items before you use them. + +## Running + +If you installed opsdroid using `pip` and have created your `configuration.yaml` file in the correct place you can simple start it by running: + +``` +opsdroid +``` + +If you are using the opsdroid docker image then ensure you add your configuration as a volume and run the container. + +``` +docker run --rm -v /path/to/configuration.yaml:/etc/configuration.yaml:ro opsdroid/opsdroid:latest +``` + +------- + +_\* databases are optional, however bot memory will not persist between different connectors or system reboots without one_ diff --git a/docs/index.md b/docs/index.md index 9025bd780..af85e6d43 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,26 +1,20 @@ -## Building +# opsdroid -`docker build -t opsdroid/opsdroid:dev .` +## Overview +opsdroid is an open source chat-ops bot written in python. It is designed to be extendable, scalable and simple. -`docker run --rm opsdroid/opsdroid:dev` +### ChatOps +_"ChatOps is an operational paradigm where work that is already happening in the background today is brought into a common chatroom. By doing this, you are unifying the communication about what work should get done with actual history of the work being done."_ - [StackStorm](https://docs.stackstorm.com/chatops/chatops.html) -## Configuration +In the new frontier of DevOps it is becoming more and more popular to interact with your automation tools via an instant messenger. opsdroid is a framework to make creating and extending your ChatOps workflows powerful but simple. -Configuration is done in a yaml file called `configuration.yaml`. +### Why use opsdroid? -Example: - -``` -logging: "debug" - -connectors: - shell: - -skills: - hello: -``` - -## Development - -Run tests -`docker run --rm -ti -v $(pwd):/usr/src/app opsdroid/opsdroid:dev tox` + * It's open source + * Simple to modify and extend + * Add you own skills in under 10 lines of python + * Easy to install + * Designed with Docker in mind for simple deployment + * Configurable with a single YAML file + * Can connect to multiple chat services simultaneously + * No coding necessary if using the official modules diff --git a/mkdocs.yml b/mkdocs.yml index c83ba09ca..ca30a7895 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,3 +1,13 @@ site_name: opsdroid repo_url: https://github.com/opsdroid/opsdroid theme: readthedocs + +pages: + - Home: 'index.md' + - Getting Started: 'getting-started.md' + - Configuration Reference: 'configuration-reference.md' + - Extending opsdroid: + - Adding skills: 'extending/skills.md' + - Adding connectors: 'extending/connectors.md' + - Adding databases: 'extending/databases.md' + - Contributing: 'contributing.md' From 9e6ca3e6c4ff1f41316a258ad181d27dfbd290cc Mon Sep 17 00:00:00 2001 From: Jacob Tomlinson Date: Tue, 9 Aug 2016 15:57:53 +0100 Subject: [PATCH 3/9] If only one connector specific do not use multiprocessing (#23) --- opsdroid/core.py | 26 +++++++++++++++++--------- tests/test_core.py | 5 ++++- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/opsdroid/core.py b/opsdroid/core.py index 146844d14..73520e009 100644 --- a/opsdroid/core.py +++ b/opsdroid/core.py @@ -52,17 +52,25 @@ def start_connectors(self, connectors): """Start the connectors.""" if len(connectors) == 0: self.critical("All connectors failed to load", 1) - for connector_module in connectors: - for name, cls in connector_module["module"].__dict__.items(): + elif len(connectors) == 1: + for name, cls in connectors[0]["module"].__dict__.items(): if isinstance(cls, type) and "Connector" in name: - connector_module["config"]["bot-name"] = self.bot_name - connector = cls(connector_module["config"]) + connectors[0]["config"]["bot-name"] = self.bot_name + connector = cls(connectors[0]["config"]) self.connectors.append(connector) - job = Process(target=connector.connect, args=(self,)) - job.start() - self.connector_jobs.append(job) - for job in self.connector_jobs: - job.join() + connector.connect(self) + else: + for connector_module in connectors: + for name, cls in connector_module["module"].__dict__.items(): + if isinstance(cls, type) and "Connector" in name: + connector_module["config"]["bot-name"] = self.bot_name + connector = cls(connector_module["config"]) + self.connectors.append(connector) + job = Process(target=connector.connect, args=(self,)) + job.start() + self.connector_jobs.append(job) + for job in self.connector_jobs: + job.join() def start_databases(self, databases): """Start the databases.""" diff --git a/tests/test_core.py b/tests/test_core.py index ad55e3c96..b2bac73bb 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -69,7 +69,10 @@ def test_start_connectors(self): "tests.mockmodules.connectors.connector") opsdroid.start_connectors([module]) self.assertEqual(len(opsdroid.connectors), 1) - self.assertEqual(len(opsdroid.connector_jobs), 1) + + opsdroid.start_connectors([module, module]) + self.assertEqual(len(opsdroid.connectors), 3) + self.assertEqual(len(opsdroid.connector_jobs), 2) def test_multiple_opsdroids(self): with OpsDroid() as opsdroid: From 8a83182211e98de45a5e6c54b3d996167823a810 Mon Sep 17 00:00:00 2001 From: Jacob Tomlinson Date: Wed, 10 Aug 2016 09:33:00 +0100 Subject: [PATCH 4/9] Skill setup is optional (#25) * Skill setup is optional * Test a module without a setup function --- opsdroid/loader.py | 5 ++++- tests/test_loader.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/opsdroid/loader.py b/opsdroid/loader.py index 24f1786fd..f1dd7a903 100644 --- a/opsdroid/loader.py +++ b/opsdroid/loader.py @@ -157,7 +157,10 @@ def _load_modules(self, modules_type, modules): def _setup_modules(self, modules): """Call the setup function on the passed in modules.""" for module in modules: - module["module"].setup(self.opsdroid) + try: + module["module"].setup(self.opsdroid) + except AttributeError: + pass def _install_module(self, config): # pylint: disable=R0201 diff --git a/tests/test_loader.py b/tests/test_loader.py index f91e83d43..dc5d02461 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -42,6 +42,7 @@ def test_setup_modules(self): opsdroid, loader = self.setup() example_modules = [] example_modules.append({"module": mock.MagicMock()}) + example_modules.append({"module": {"name": "test"}}) loader._setup_modules(example_modules) self.assertEqual(len(example_modules[0]["module"].mock_calls), 1) From 5916a400a6ad8e2229950c45d71aa5aead9d6045 Mon Sep 17 00:00:00 2001 From: Jacob Tomlinson Date: Fri, 12 Aug 2016 14:11:01 +0100 Subject: [PATCH 5/9] Make regex matches case sensitive (#28) * Added test for case sensitive matching * Remove insensitive and multiline as not needed --- opsdroid/helper.py | 2 +- tests/test_helper.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/opsdroid/helper.py b/opsdroid/helper.py index 0b3b108c8..5e52f109c 100644 --- a/opsdroid/helper.py +++ b/opsdroid/helper.py @@ -27,4 +27,4 @@ def set_logging_level(logging_level): def match(regex, message): """Regex match a string.""" - return re.match(regex, message, re.M | re.I) + return re.match(regex, message) diff --git a/tests/test_helper.py b/tests/test_helper.py index 7720cfda5..f6382260e 100644 --- a/tests/test_helper.py +++ b/tests/test_helper.py @@ -33,3 +33,11 @@ def test_match(self): match = helper.match(r"hello (.*)", "hello world") self.assertEqual(match.group(1), "world") + + def test_sensitive_match(self): + """Matches should be case sensitive""" + match = helper.match(r"hello", "hello") + self.assertTrue(match) + + match = helper.match(r"hello", "HELLO") + self.assertFalse(match) From 24cfea88d7fcd2150cd64a35e2cc7fad2b2315b2 Mon Sep 17 00:00:00 2001 From: Jacob Tomlinson Date: Fri, 12 Aug 2016 15:32:26 +0100 Subject: [PATCH 6/9] Messages shouldn't be modified by responding (#30) * Test if messages are affected by responding * Messages are copied when responding --- opsdroid/message.py | 7 +++++-- tests/test_message.py | 9 ++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/opsdroid/message.py b/opsdroid/message.py index f6bf16cdc..7ff949ef7 100644 --- a/opsdroid/message.py +++ b/opsdroid/message.py @@ -1,5 +1,7 @@ """Class to encapsulate a message.""" +from copy import copy + class Message: # pylint: disable=too-few-public-methods @@ -15,5 +17,6 @@ def __init__(self, text, user, room, connector): def respond(self, text): """Respond to this message using the connector it was created by.""" - self.text = text - self.connector.respond(self) + response = copy(self) + response.text = text + self.connector.respond(response) diff --git a/tests/test_message.py b/tests/test_message.py index 83b1be5b5..28b19131c 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -19,4 +19,11 @@ def test_message(self): message.respond("Goodbye world") self.assertEqual(len(mock_connector.mock_calls), 1) - self.assertEqual(message.text, "Goodbye world") + + def test_response_effects(self): + """Responding to a message shouldn't change the message.""" + mock_connector = mock.MagicMock() + message_text = "Hello world" + message = Message(message_text, "user", "default", mock_connector) + message.respond("Goodbye world") + self.assertEqual(message_text, message.text) From a1d97f4e204c007177ad65945b4b7c4909237d51 Mon Sep 17 00:00:00 2001 From: Jacob Tomlinson Date: Thu, 18 Aug 2016 12:01:23 +0100 Subject: [PATCH 7/9] Added microbadger badge (#31) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b0b6cfdd..623100e90 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Ops Droid -[![Build Status](https://travis-ci.org/opsdroid/opsdroid.svg?branch=master)](https://travis-ci.org/opsdroid/opsdroid) [![Coverage Status](https://coveralls.io/repos/github/opsdroid/opsdroid/badge.svg?branch=master)](https://coveralls.io/github/opsdroid/opsdroid?branch=master) [![Docker Image](https://img.shields.io/badge/docker-ready-blue.svg)](https://hub.docker.com/r/opsdroid/opsdroid/) [![Documentation Status](https://readthedocs.org/projects/opsdroid/badge/?version=latest)](http://opsdroid.readthedocs.io/en/latest/?badge=latest) +[![Build Status](https://travis-ci.org/opsdroid/opsdroid.svg?branch=master)](https://travis-ci.org/opsdroid/opsdroid) [![Coverage Status](https://coveralls.io/repos/github/opsdroid/opsdroid/badge.svg?branch=master)](https://coveralls.io/github/opsdroid/opsdroid?branch=master) [![Docker Image](https://img.shields.io/badge/docker-ready-blue.svg)](https://hub.docker.com/r/opsdroid/opsdroid/) [![Docker Layers](https://images.microbadger.com/badges/image/opsdroid/opsdroid.svg)](https://microbadger.com/#/images/opsdroid/opsdroid) [![Documentation Status](https://readthedocs.org/projects/opsdroid/badge/?version=latest)](http://opsdroid.readthedocs.io/en/latest/?badge=latest) An open source python chat-ops bot From 203f0a1d723b3d0ee4822a259a2f1e78063a0fe0 Mon Sep 17 00:00:00 2001 From: Jacob Tomlinson Date: Fri, 19 Aug 2016 11:10:03 +0100 Subject: [PATCH 8/9] Added connector and database base classes (#32) --- opsdroid/connector.py | 59 ++++++++++++++++++++++++++++++++++ opsdroid/database.py | 71 +++++++++++++++++++++++++++++++++++++++++ tests/test_connector.py | 24 ++++++++++++++ tests/test_database.py | 29 +++++++++++++++++ 4 files changed, 183 insertions(+) create mode 100644 opsdroid/connector.py create mode 100644 opsdroid/database.py create mode 100644 tests/test_connector.py create mode 100644 tests/test_database.py diff --git a/opsdroid/connector.py b/opsdroid/connector.py new file mode 100644 index 000000000..2d75a9c07 --- /dev/null +++ b/opsdroid/connector.py @@ -0,0 +1,59 @@ +"""A base class for connectors to inherit from.""" + +from opsdroid.message import Message # NOQA # pylint: disable=unused-import + + +class Connector(): + """A base connector. + + Connectors are used to interact with a given chat service. + + """ + + def __init__(self, config): + """Setup the connector. + + Set some basic properties from the connector config such as the name + of this connector and the name the bot should appear with in chat + service. + + Args: + config (dict): The config for this connector specified in the + `configuration.yaml` file. + + """ + self.name = "" + self.config = config + + def connect(self, opsdroid): + """Connect to chat service and parse all messages. + + This method should block the thread with an infinite loop and create + Message objects for chat messages coming from the service. It should + then call `opsdroid.parse(message)` on those messages. + + Due to this method blocking, if multiple connectors are configured in + opsdroid they will be run in parallel using the multiprocessing + library. + + Args: + opsdroid (OpsDroid): An instance of the opsdroid core. + + """ + raise NotImplementedError + + def respond(self, message): + """Send a message back to the chat service. + + The message object will have a `text` property which should be sent + back to the chat service. It may also have a `room` and `user` property + which gives information on where the message should be directed. + + Args: + message (Message): A message received by the connector. + + Returns: + bool: True for message successfully sent. False otherwise. + + """ + raise NotImplementedError diff --git a/opsdroid/database.py b/opsdroid/database.py new file mode 100644 index 000000000..bf6db5771 --- /dev/null +++ b/opsdroid/database.py @@ -0,0 +1,71 @@ +"""A base class for databases to inherit from.""" + + +class Database(): + """A base database. + + Database classes are used to persist key/value pairs in a database. + + """ + + def __init__(self, config): + """Setup the database. + + Set some basic properties from the database config such as the name + of this database. It could also be a good place to setup properties + to hold things like the database connection object and the database + name. + + Args: + config (dict): The config for this database specified in the + `configuration.yaml` file. + + """ + self.name = "" + self.config = config + self.client = None + self.database = None + + def connect(self, opsdroid): + """Connect to chat service and store the connection object. + + This method should connect to the given database using a native + python library for that database. The library will most likely involve + a connection object which will be used by the put and get methods. + This object should be stored in self. + + Args: + opsdroid (OpsDroid): An instance of the opsdroid core. + + """ + raise NotImplementedError + + def put(self, key, data): + """Store the data object in a database against the key. + + The data object will need to be serialised in a sensible way which + suits the database being used and allows for reconstruction of the + object. + + Args: + key (string): The key to store the data object under. + data (object): The data object to store. + + Returns: + bool: True for data successfully stored, False otherwise. + + """ + raise NotImplementedError + + def get(self, key): + """Return a data object for a given key. + + Args: + key (string): The key to lookup in the database. + + Returns: + object or None: The data object stored for that key, or None if no + object found for that key. + + """ + raise NotImplementedError diff --git a/tests/test_connector.py b/tests/test_connector.py new file mode 100644 index 000000000..a3f072112 --- /dev/null +++ b/tests/test_connector.py @@ -0,0 +1,24 @@ + +import unittest + +from opsdroid.connector import Connector + + +class TestConnectorBaseClass(unittest.TestCase): + """Test the opsdroid connector base class.""" + + def test_init(self): + config = {"example_item": "test"} + connector = Connector(config) + self.assertEqual("", connector.name) + self.assertEqual("test", connector.config["example_item"]) + + def test_connect(self): + connector = Connector({}) + with self.assertRaises(NotImplementedError): + connector.connect({}) + + def test_respond(self): + connector = Connector({}) + with self.assertRaises(NotImplementedError): + connector.respond({}) diff --git a/tests/test_database.py b/tests/test_database.py new file mode 100644 index 000000000..d9c3315f6 --- /dev/null +++ b/tests/test_database.py @@ -0,0 +1,29 @@ + +import unittest + +from opsdroid.database import Database + + +class TestDatabaseBaseClass(unittest.TestCase): + """Test the opsdroid database base class.""" + + def test_init(self): + config = {"example_item": "test"} + database = Database(config) + self.assertEqual("", database.name) + self.assertEqual("test", database.config["example_item"]) + + def test_connect(self): + database = Database({}) + with self.assertRaises(NotImplementedError): + database.connect({}) + + def test_get(self): + database = Database({}) + with self.assertRaises(NotImplementedError): + database.get("test") + + def test_put(self): + database = Database({}) + with self.assertRaises(NotImplementedError): + database.put("test", {}) From 1f650f28edf11339e6b15c3d03892effb0015c59 Mon Sep 17 00:00:00 2001 From: Jacob Tomlinson Date: Fri, 19 Aug 2016 12:27:06 +0100 Subject: [PATCH 9/9] Test for class inheritance instead of class name (#34) --- opsdroid/core.py | 14 ++++++++++---- tests/mockmodules/connectors/connector.py | 4 +++- tests/mockmodules/databases/database.py | 4 +++- tests/test_core.py | 13 +++++-------- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/opsdroid/core.py b/opsdroid/core.py index 73520e009..5b5575312 100644 --- a/opsdroid/core.py +++ b/opsdroid/core.py @@ -6,6 +6,8 @@ from multiprocessing import Process from opsdroid.helper import match from opsdroid.memory import Memory +from opsdroid.connector import Connector +from opsdroid.database import Database class OpsDroid(): @@ -54,7 +56,9 @@ def start_connectors(self, connectors): self.critical("All connectors failed to load", 1) elif len(connectors) == 1: for name, cls in connectors[0]["module"].__dict__.items(): - if isinstance(cls, type) and "Connector" in name: + if isinstance(cls, type) and \ + isinstance(cls({}), Connector): + logging.debug("Adding connector: " + name) connectors[0]["config"]["bot-name"] = self.bot_name connector = cls(connectors[0]["config"]) self.connectors.append(connector) @@ -62,7 +66,8 @@ def start_connectors(self, connectors): else: for connector_module in connectors: for name, cls in connector_module["module"].__dict__.items(): - if isinstance(cls, type) and "Connector" in name: + if isinstance(cls, type) and \ + isinstance(cls({}), Connector): connector_module["config"]["bot-name"] = self.bot_name connector = cls(connector_module["config"]) self.connectors.append(connector) @@ -78,11 +83,12 @@ def start_databases(self, databases): logging.warning("All databases failed to load") for database_module in databases: for name, cls in database_module["module"].__dict__.items(): - if isinstance(cls, type) and "Database" in name: + if isinstance(cls, type) and \ + isinstance(cls({}), Database): logging.debug("Adding database: " + name) database = cls(database_module["config"]) self.memory.databases.append(database) - database.connect() + database.connect(self) def load_regex_skill(self, regex, skill): """Load skills.""" diff --git a/tests/mockmodules/connectors/connector.py b/tests/mockmodules/connectors/connector.py index 5e4455961..0938160c6 100644 --- a/tests/mockmodules/connectors/connector.py +++ b/tests/mockmodules/connectors/connector.py @@ -2,8 +2,10 @@ import unittest.mock as mock +from opsdroid.connector import Connector -class ConnectorTest: + +class ConnectorTest(Connector): """The mocked connector class.""" def __init__(self, config): diff --git a/tests/mockmodules/databases/database.py b/tests/mockmodules/databases/database.py index b1119534b..3a5ae8640 100644 --- a/tests/mockmodules/databases/database.py +++ b/tests/mockmodules/databases/database.py @@ -2,8 +2,10 @@ import unittest.mock as mock +from opsdroid.database import Database -class DatabaseTest: + +class DatabaseTest(Database): """The mocked database class.""" def __init__(self, config): diff --git a/tests/test_core.py b/tests/test_core.py index b2bac73bb..e309f8b5e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -55,10 +55,8 @@ def test_start_databases(self): module["config"] = {} module["module"] = importlib.import_module( "tests.mockmodules.databases.database") - opsdroid.start_databases([module]) - self.assertEqual(len(opsdroid.memory.databases), 1) - self.assertEqual( - len(opsdroid.memory.databases[0].connect.mock_calls), 1) + with self.assertRaises(NotImplementedError): + opsdroid.start_databases([module]) def test_start_connectors(self): with OpsDroid() as opsdroid: @@ -67,12 +65,11 @@ def test_start_connectors(self): module["config"] = {} module["module"] = importlib.import_module( "tests.mockmodules.connectors.connector") - opsdroid.start_connectors([module]) - self.assertEqual(len(opsdroid.connectors), 1) + + with self.assertRaises(NotImplementedError): + opsdroid.start_connectors([module]) opsdroid.start_connectors([module, module]) - self.assertEqual(len(opsdroid.connectors), 3) - self.assertEqual(len(opsdroid.connector_jobs), 2) def test_multiple_opsdroids(self): with OpsDroid() as opsdroid: