# -*- coding: utf-8 -*-
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import logging
import os
from multiprocessing import Lock
[docs]class Store(object):
"""
Stores data for one space
This is a key-value store, that supports concurrency
across multiple processes.
Configuration of the storage engine is coming from settings of the overall
bot.
Example::
store = Store(context=my_context)
Normally a store is related to one single space. For this, you can use the
function ``bond()`` to set the space unique id.
Example::
store.bond(id=space.id)
Once this is done, the store can be used to remember and to recall values.
Example::
store.remember('gauge', gauge)
...
gauge = store.recall('gauge')
"""
def __init__(self, context=None, **kwargs):
"""
Stores data for one space
:param bot: the overarching bot
:type bot: ShellBot
"""
self.context = context
self.lock = Lock()
self.on_init(**kwargs)
[docs] def on_init(self, **kwargs):
"""
Adds processing to initialization
This function should be expanded in sub-class, where necessary.
This function is the right place to capture additional parameters
provided on instance initialisation.
Example::
def on_init(self, prefix='sqlite', **kwargs):
...
"""
pass
[docs] def check(self):
"""
Checks configuration
This function should be expanded in sub-class, where necessary.
This function is the right place to check parameters that can be used
by this instance.
Example::
def check(self):
self.context.check(self.prefix+'.db', 'store.db')
"""
pass
[docs] def bond(self, id=None):
"""
Creates or uses resource required for the permanent back-end
:param id: the unique identifier of the related space
:type id: str
This function should be expanded in sub-class, where necessary.
This function is the right place to create files, databases, and index
that can be necessary for a store back-end.
Example::
def bond(self, id=None):
db.execute("CREATE TABLE ...
"""
pass
[docs] def to_text(self, value):
"""
Turns a value to a textual representation
:param value: a python object that can be serialized
:type value: object
:return: a textual representation that can be saved in store
:rtype: str
Here we use ``json.dumps()`` to do the job. You can override
this function in your subclass if needed.
"""
return json.dumps(value)
[docs] def from_text(self, textual):
"""
Retrieves a value from a textual representation
:param textual: a textual representation that can be saved in store
:type textual: str
:return: a python object
:rtype: object or None
Here we use ``json.loads()`` to do the job. You can override
this function in your subclass if needed.
"""
try:
return json.loads(textual)
except TypeError:
return None
[docs] def remember(self, key, value):
"""
Remembers a value
:param key: name of the value
:type key: str
:param value: actual value
:type value: any serializable type is accepted
This functions stores or updates a value in the back-end storage
system.
Example::
store.remember('parameter_123', 'George')
This function is safe on multiprocessing and multithreading.
"""
with self.lock:
self._set(key, self.to_text(value))
def _set(self, key, value):
"""
Sets a permanent value
:param key: name of the value
:type key: str
:param value: actual value
:type value: any serializable type is accepted
This functions stores or updates a value in the back-end storage
system.
Example::
store._set('parameter_123', 'George')
This function should be expanded in sub-class.
Example::
def _set(self, key, value):
db.update(self.id, key, value)
...
"""
raise NotImplementedError()
[docs] def recall(self, key, default=None):
"""
Recalls a value
:param key: name of the value
:type key: str
:param default: default value
:type default: any serializable type is accepted
:return: the actual value, or the default value, or None
Example::
value = store.recall('parameter_123')
This function is safe on multiprocessing and multithreading.
"""
with self.lock:
value = self.from_text(self._get(key))
if value is None: # when value remembered was None
value = default
return value
def _get(self, key):
"""
Gets a permanent value
:param key: name of the value
:type key: str
:return: the actual value, or None
Example::
value = store._get('parameter_123')
This function should be expanded in sub-class, where necessary.
Example::
def _get(self, key):
value = db.select(self.id, key)
...
"""
raise NotImplementedError()
[docs] def forget(self, key=None):
"""
Forgets a value or all values
:param key: name of the value to forget, or None
:type key: str
To clear only one value, provides the name of it.
For example::
store.forget('parameter_123')
To clear all values in the store, just call the function
without a value.
For example::
store.forget()
This function is safe on multiprocessing and multithreading.
"""
with self.lock:
self._clear(key)
def _clear(self, key=None):
"""
Forgets a value or all values
:param key: name of the value to forget, or None
:type key: str
To clear only one value, provides the name of it.
For example::
store._clear('parameter_123')
To clear all values in the store, just call the function
without a value.
For example::
store._clear()
This function should be expanded in sub-class, where necessary.
Example::
def forget(self, key):
db.delete(key)
...
"""
raise NotImplementedError()
[docs] def increment(self, key, delta=1):
"""
Increments a value
:param key: name of the value
:type key: str
:param delta: increment to apply
:type delta: int
:return: the new value
Example::
value = store.increment('gauge')
"""
with self.lock:
value = self.from_text(self._get(key))
if not isinstance(value, int):
value = 0
value += delta
self._set(key, self.to_text(value))
return value
[docs] def decrement(self, key, delta=1):
"""
Decrements a value
:param key: name of the value
:type key: str
:param delta: decrement to apply
:type delta: int
:return: the new value
Example::
value = store.decrement('gauge')
"""
with self.lock:
value = self.from_text(self._get(key))
if not isinstance(value, int):
value = 0
value -= delta
self._set(key, self.to_text(value))
return value
[docs] def append(self, key, item):
"""
Appends an item to a list
:param key: name of the list
:type key: str
:param item: a new item to append
:type item: any serializable type is accepted
Example::
>>>store.append('names', 'Alice')
>>>store.append('names', 'Bob')
>>>store.recall('names')
['Alice', 'Bob']
"""
with self.lock:
value = self.from_text(self._get(key))
if not isinstance(value, list):
value = []
value.append(item)
self._set(key, self.to_text(value))
[docs] def update(self, key, label, item):
"""
Updates a dict
:param key: name of the dict
:type key: str
:param label: named entry in the dict
:type label: str
:param item: new value of this entry
:type item: any serializable type is accepted
Example::
>>>store.update('input', 'PO Number', '1234A')
>>>store.recall('input')
{'PO Number': '1234A'}
"""
assert label
with self.lock:
value = self.from_text(self._get(key))
if not isinstance(value, dict):
value = {}
value[label] = item
self._set(key, self.to_text(value))