Source code for binp.kv

from json import dumps, loads
from typing import Optional, Union, Type, TypeVar, List

from databases import Database
from pydantic.main import BaseModel

from binp.db import ensure

T = TypeVar('T', bound=BaseModel)


[docs]class KV: """ Basic Key-Value storage with namespace. By default - 'default' namespace used. All values are serialized to JSON by standard JSON module or in case value is subclass of pydantic BaseMode - by pydantic ``.json()``. :Example: .. code-block:: python from binp import BINP binp = BINP() async def save_something(): # save multiple values to storage await binp.kv.set(name="reddec", repo="binp", year=2020) # get single value name = await binp.kv.get('name') assert name == 'reddec' There is some optimization for saving/loading Pydantic classes when class name itself a key. :Example: .. code-block:: python from binp import BINP from pydantic.main import BaseModel binp = BINP() class Author(BaseModel): name: str repo: str year: int async def save_something(): value = Author(name="reddec", repo="binp", year=2020) # save class with key name equal to fully-qualified class name await binp.kv.save(value) # restore saved_value = await binp.kv.load(Author) assert value == saved_value """ def __init__(self, namespace: str = 'default', db: Optional[Database] = None): self.__db = ensure(db) self.__namespace = namespace
[docs] async def save(self, value: BaseModel): """ Save value with key name equal to class name (fqdn) """ values = { value.__class__.__qualname__: value } return await self.set(**values)
[docs] async def load(self, klass: Type[T]) -> Optional[T]: """ Load and parse value by key name equal to class name (fqdn) :param klass: BaseModel inherited class to load and parse """ db = await self.__db() value = await db.fetch_one('SELECT value FROM kv WHERE namespace = :ns AND key = :key', values={ 'ns': self.__namespace, 'key': klass.__qualname__, }) if value is None: return None return klass.parse_raw(value['value'])
[docs] async def set(self, **values: Union[str, int, float, bool, BaseModel]): """ Sav multiple values into storage. All values should be serializable to JSON. """ db = await self.__db() await db.execute_many('INSERT OR REPLACE INTO kv(namespace, key, value) VALUES (:ns, :key, :value)', values=[ { 'ns': self.__namespace, 'key': key, 'value': (value.json() if isinstance(value, BaseModel) else dumps(value, ensure_ascii=False)) } for key, value in values.items() ])
[docs] async def remove(self, *names: str): """ Remove multiple values by names """ db = await self.__db() await db.execute_many('DELETE FROM kv WHERE namespace = :ns AND key = :key', values=[ {'ns': self.__namespace, 'key': name} for name in names ])
[docs] async def get(self, name: str) -> Optional[Union[str, int, float, bool, dict]]: """ Get save value by name. """ db = await self.__db() value = await db.fetch_one('SELECT value FROM kv WHERE namespace = :ns AND key = :key', values={ 'ns': self.__namespace, 'key': name, }) if value is None: return None return loads(value['value'])
[docs] async def namespaces(self) -> List[str]: """ Fetch all namespaces in selected database. """ db = await self.__db() value = await db.fetch_all('SELECT namespace FROM kv GROUP BY namespace') if value is None: return [] return [x['namespace'] for x in value]
[docs] def select(self, namespace: str) -> 'KV': """ Get accessor to another namespace in a same database """ kv = KV(namespace=namespace, db=None) kv.__db = self.__db return kv