├── .gitignore ├── README.md ├── requirements.txt ├── sql.plug └── sql.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /.idea 3 | /atlassian-ide-plugin.xml 4 | /.settings 5 | /config.py 6 | /.venv 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## SQL storage plugin for errbot 2 | 3 | 4 | ### About 5 | [Errbot](http://errbot.io) is a python chatbot, this storage plugin allows you to use it with SQL databases as a persistent storage. 6 | By using [SQLAlchemy](sqlalchemy.org), it has the support for Firebird, Microsoft SQL Server, MySQL, Oracle, PostgreSQL, SQLite, Sybase, IBM DB2, Amazon Redshift, exasol, Sybase SQL Anywhere, MonetDB. 7 | 8 | ### Installation 9 | 10 | 1. Install the support for the database you want to use. See [SQLalchemy doc](http://docs.sqlalchemy.org/en/latest/dialects/) 11 | 2. Then you need to add this section to your config.py, following this example: 12 | ```python 13 | BOT_EXTRA_STORAGE_PLUGINS_DIR='/home/gbin/err-storage' 14 | STORAGE = 'SQL' 15 | STORAGE_CONFIG = { 16 | 'data_url': 'postgresql://scott:tiger@localhost/test', 17 | } 18 | ``` 19 | 20 | 3. Start your bot in text mode: `errbot -T` to give it a shot. 21 | 22 | If you want to migrate from the local storage to SQL, you should be able to backup your data (with STORAGE commented) 23 | then restore it back with STORAGE uncommented. 24 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | jsonpickle 2 | sqlalchemy 3 | -------------------------------------------------------------------------------- /sql.plug: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = SQL 3 | Module = sql 4 | 5 | [Documentation] 6 | Description = This is the errbot storage plugin for SQL. It is compatible with Firebird, Microsoft SQL Server, MySQL, Oracle, PostgreSQL, SQLite, Sybase, IBM DB2, Amazon Redshift, exasol, Sybase SQL Anywhere, MonetDB. 7 | -------------------------------------------------------------------------------- /sql.py: -------------------------------------------------------------------------------- 1 | # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 2 | 3 | import logging 4 | from contextlib import contextmanager 5 | from jsonpickle import encode, decode 6 | from typing import Any 7 | from sqlalchemy import ( 8 | Table, MetaData, Column, Integer, String, 9 | ForeignKey, create_engine, select) 10 | from sqlalchemy.orm import mapper, sessionmaker 11 | from sqlalchemy.orm.exc import NoResultFound 12 | from errbot.storage.base import StorageBase, StoragePluginBase 13 | 14 | log = logging.getLogger('errbot.storage.sql') 15 | 16 | DATA_URL_ENTRY = 'data_url' 17 | 18 | 19 | class KV(object): 20 | """This is a basic key/value. Pickling in JSON.""" 21 | def __init__(self, key: str, value: Any): 22 | self._key = key 23 | self._value = encode(value) 24 | 25 | @property 26 | def key(self) -> str: 27 | return self._key 28 | 29 | @property 30 | def value(self) -> Any: 31 | return decode(self._value) 32 | 33 | 34 | class SQLStorage(StorageBase): 35 | def __init__(self, session, clazz): 36 | self.session = session 37 | self.clazz = clazz 38 | 39 | @contextmanager 40 | def _session_op(self): 41 | try: 42 | yield self.session 43 | self.session.commit() 44 | except: 45 | self.session.rollback() 46 | raise 47 | 48 | def get(self, key: str) -> Any: 49 | try: 50 | with self._session_op() as session: 51 | result = session.query(self.clazz).filter(self.clazz._key == key).one().value 52 | except NoResultFound: 53 | raise KeyError("%s doesn't exists." % key) 54 | return result 55 | 56 | def remove(self, key: str): 57 | try: 58 | with self._session_op() as session: 59 | session.query(self.clazz).filter(self.clazz._key == key).delete() 60 | except NoResultFound: 61 | raise KeyError("%s doesn't exists." % key) 62 | 63 | def set(self, key: str, value: Any) -> None: 64 | with self._session_op() as session: 65 | session.merge(self.clazz(key, value)) 66 | 67 | def len(self): 68 | with self._session_op() as session: 69 | length = session.query(self.clazz).count() 70 | return length 71 | 72 | def keys(self): 73 | return (kv.key for kv in self.session.query(self.clazz).all()) 74 | 75 | def close(self) -> None: 76 | self.session.commit() 77 | 78 | 79 | class SQLPlugin(StoragePluginBase): 80 | def __init__(self, bot_config): 81 | super().__init__(bot_config) 82 | config = self._storage_config 83 | if DATA_URL_ENTRY not in config: 84 | raise Exception( 85 | 'You need to specify a connection URL for the database in your' 86 | 'config.py. For example:\n' 87 | 'STORAGE_CONFIG={\n' 88 | '"data_url": "postgresql://' 89 | 'scott:tiger@localhost/mydatabase/",\n' 90 | '}') 91 | 92 | # Hack around the multithreading issue in memory only sqlite. 93 | # This mode is useful for testing. 94 | if config[DATA_URL_ENTRY].startswith('sqlite://'): 95 | from sqlalchemy.pool import StaticPool 96 | self._engine = create_engine( 97 | config[DATA_URL_ENTRY], 98 | connect_args={'check_same_thread': False}, 99 | poolclass=StaticPool, 100 | echo=bot_config.BOT_LOG_LEVEL == logging.DEBUG) 101 | else: 102 | self._engine = create_engine( 103 | config[DATA_URL_ENTRY], 104 | pool_recycle=config.get('connection_recycle', 1800), 105 | pool_pre_ping=config.get('connection_ping', True), 106 | echo=bot_config.BOT_LOG_LEVEL == logging.DEBUG) 107 | self._metadata = MetaData() 108 | self._sessionmaker = sessionmaker() 109 | self._sessionmaker.configure(bind=self._engine) 110 | 111 | def open(self, namespace: str) -> StorageBase: 112 | 113 | # Create a table with the given namespace 114 | table = Table(namespace, self._metadata, 115 | Column('key', String(767), primary_key=True), 116 | Column('value', String(32768)), 117 | extend_existing=True) 118 | 119 | class NewKV(KV): 120 | pass 121 | 122 | mapper(NewKV, table, properties={ 123 | '_key': table.c.key, 124 | '_value': table.c.value}) 125 | 126 | # ensure that the table for this namespace exists 127 | self._metadata.create_all(self._engine) 128 | 129 | # create an autonomous session for it. 130 | return SQLStorage(self._sessionmaker(), NewKV) 131 | --------------------------------------------------------------------------------