[neon/backports-focal/mygnuhealth/Neon/release-lts] debian: install local tinydb as the packaged version is out of date
Jonathan Riddell
null at kde.org
Thu Jul 1 14:01:47 BST 2021
Git commit a5695eb1c07393a29e25a833876d46c9fcbe581d by Jonathan Riddell.
Committed on 01/07/2021 at 13:01.
Pushed by jriddell into branch 'Neon/release-lts'.
install local tinydb as the packaged version is out of date
M +0 -1 debian/control
A +1 -0 debian/mygnuhealth.install
A +32 -0 debian/tinydb/__init__.py
A +269 -0 debian/tinydb/database.py
A +124 -0 debian/tinydb/middlewares.py
A +69 -0 debian/tinydb/operations.py
A +0 -0 debian/tinydb/py.typed
A +469 -0 debian/tinydb/queries.py
A +166 -0 debian/tinydb/storages.py
A +704 -0 debian/tinydb/table.py
A +135 -0 debian/tinydb/utils.py
A +1 -0 debian/tinydb/version.py
https://invent.kde.org/neon/backports-focal/mygnuhealth/commit/a5695eb1c07393a29e25a833876d46c9fcbe581d
diff --git a/debian/control b/debian/control
index d259c69..1633cb8 100644
--- a/debian/control
+++ b/debian/control
@@ -10,7 +10,6 @@ Package: mygnuhealth
Architecture: any
Depends: python3-pyside2.qtqml,
python3-pyside2.qtwidgets,
- python3-tinydb,
qml-module-org-kde-kirigami2,
${misc:Depends},
${python3:Depends},
diff --git a/debian/mygnuhealth.install b/debian/mygnuhealth.install
new file mode 100644
index 0000000..61e6818
--- /dev/null
+++ b/debian/mygnuhealth.install
@@ -0,0 +1 @@
+debian/tidybd /usr/lib/python3.8/dist-packages/
diff --git a/debian/tinydb/__init__.py b/debian/tinydb/__init__.py
new file mode 100644
index 0000000..c499334
--- /dev/null
+++ b/debian/tinydb/__init__.py
@@ -0,0 +1,32 @@
+"""
+TinyDB is a tiny, document oriented database optimized for your happiness :)
+
+TinyDB stores differrent types of Python data types using a configurable
+storage mechanism. It comes with a syntax for querying data and storing
+data in multiple tables.
+
+.. codeauthor:: Markus Siemens <markus at m-siemens.de>
+
+Usage example:
+
+>>> from tinydb import TinyDB, where
+>>> from tinydb.storages import MemoryStorage
+>>> db = TinyDB(storage=MemoryStorage)
+>>> db.insert({'data': 5}) # Insert into '_default' table
+>>> db.search(where('data') == 5)
+[{'data': 5, '_id': 1}]
+>>> # Now let's create a new table
+>>> tbl = db.table('our_table')
+>>> for i in range(10):
+... tbl.insert({'data': i})
+...
+>>> len(tbl.search(where('data') < 5))
+5
+"""
+
+from .queries import Query, where
+from .storages import Storage, JSONStorage
+from .database import TinyDB
+from .version import __version__
+
+__all__ = ('TinyDB', 'Storage', 'JSONStorage', 'Query', 'where')
diff --git a/debian/tinydb/database.py b/debian/tinydb/database.py
new file mode 100644
index 0000000..fdf019d
--- /dev/null
+++ b/debian/tinydb/database.py
@@ -0,0 +1,269 @@
+"""
+This module contains the main component of TinyDB: the database.
+"""
+from typing import Dict, Iterator, Set
+
+from . import JSONStorage
+from .storages import Storage
+from .table import Table, Document
+
+
+class TinyDB:
+ """
+ The main class of TinyDB.
+
+ The ``TinyDB`` class is responsible for creating the storage class instance
+ that will store this database's documents, managing the database
+ tables as well as providing access to the default table.
+
+ For table management, a simple ``dict`` is used that stores the table class
+ instances accessible using their table name.
+
+ Default table access is provided by forwarding all unknown method calls
+ and property access operations to the default table by implementing
+ ``__getattr__``.
+
+ When creating a new instance, all arguments and keyword arguments (except
+ for ``storage``) will be passed to the storage class that is provided. If
+ no storage class is specified, :class:`~tinydb.storages.JSONStorage` will be
+ used.
+
+ .. admonition:: Customization
+
+ For customization, the following class variables can be set:
+
+ - ``table_class`` defines the class that is used to create tables,
+ - ``default_table_name`` defines the name of the default table, and
+ - ``default_storage_class`` will define the class that will be used to
+ create storage instances if no other storage is passed.
+
+ .. versionadded:: 4.0
+
+ .. admonition:: Data Storage Model
+
+ Data is stored using a storage class that provides persistence for a
+ ``dict`` instance. This ``dict`` contains all tables and their data.
+ The data is modelled like this::
+
+ {
+ 'table1': {
+ 0: {document...},
+ 1: {document...},
+ },
+ 'table2': {
+ ...
+ }
+ }
+
+ Each entry in this ``dict`` uses the table name as its key and a
+ ``dict`` of documents as its value. The document ``dict`` contains
+ document IDs as keys and the documents themselves as values.
+
+ :param storage: The class of the storage to use. Will be initialized
+ with ``args`` and ``kwargs``.
+ """
+
+ #: The class that will be used to create table instances
+ #:
+ #: .. versionadded:: 4.0
+ table_class = Table
+
+ #: The name of the default table
+ #:
+ #: .. versionadded:: 4.0
+ default_table_name = '_default'
+
+ #: The class that will be used by default to create storage instances
+ #:
+ #: .. versionadded:: 4.0
+ default_storage_class = JSONStorage
+
+ def __init__(self, *args, **kwargs) -> None:
+ """
+ Create a new instance of TinyDB.
+ """
+
+ storage = kwargs.pop('storage', self.default_storage_class)
+
+ # Prepare the storage
+ self._storage = storage(*args, **kwargs) # type: Storage
+
+ self._opened = True
+ self._tables = {} # type: Dict[str, Table]
+
+ def __repr__(self):
+ args = [
+ 'tables={}'.format(list(self.tables())),
+ 'tables_count={}'.format(len(self.tables())),
+ 'default_table_documents_count={}'.format(self.__len__()),
+ 'all_tables_documents_count={}'.format(
+ ['{}={}'.format(table, len(self.table(table)))
+ for table in self.tables()]),
+ ]
+
+ return '<{} {}>'.format(type(self).__name__, ', '.join(args))
+
+ def table(self, name: str, **kwargs) -> Table:
+ """
+ Get access to a specific table.
+
+ If the table hasn't been accessed yet, a new table instance will be
+ created using the :attr:`~tinydb.database.TinyDB.table_class` class.
+ Otherwise, the previously created table instance wil be returned.
+
+ All futher options besides the name are passed to the table class which
+ by default is :class:`~tinydb.table.Table`. Check its documentation
+ for further parameters you can pass.
+
+ :param name: The name of the table.
+ :param kwargs: Keyword arguments to pass to the table class constructor
+ """
+
+ if name in self._tables:
+ return self._tables[name]
+
+ table = self.table_class(self.storage, name, **kwargs)
+ self._tables[name] = table
+
+ return table
+
+ def tables(self) -> Set[str]:
+ """
+ Get the names of all tables in the database.
+
+ :returns: a set of table names
+ """
+
+ # TinyDB stores data as a dict of tables like this:
+ #
+ # {
+ # '_default': {
+ # 0: {document...},
+ # 1: {document...},
+ # },
+ # 'table1': {
+ # ...
+ # }
+ # }
+ #
+ # To get a set of table names, we thus construct a set of this main
+ # dict which returns a set of the dict keys which are the table names.
+ #
+ # Storage.read() may return ``None`` if the database file is empty
+ # so we need to consider this case to and return an empty set in this
+ # case.
+
+ return set(self.storage.read() or {})
+
+ def drop_tables(self) -> None:
+ """
+ Drop all tables from the database. **CANNOT BE REVERSED!**
+ """
+
+ # We drop all tables from this database by writing an empty dict
+ # to the storage thereby returning to the initial state with no tables.
+ self.storage.write({})
+
+ # After that we need to remeber to empty the ``_tables`` dict so we'll
+ # create new table instances when a table is accessed again.
+ self._tables.clear()
+
+ def drop_table(self, name: str) -> None:
+ """
+ Drop a specific table from the database. **CANNOT BE REVERSED!**
+
+ :param name: The name of the table to drop.
+ """
+
+ # If the table is currently opened, we need to forget the table class
+ # instance
+ if name in self._tables:
+ del self._tables[name]
+
+ data = self.storage.read()
+
+ # The database is uninitialized, there's nothing to do
+ if data is None:
+ return
+
+ # The table does not exist, there's nothing to do
+ if name not in data:
+ return
+
+ # Remove the table from the data dict
+ del data[name]
+
+ # Store the updated data back to the storage
+ self.storage.write(data)
+
+ @property
+ def storage(self) -> Storage:
+ """
+ Get the storage instance used for this TinyDB instance.
+
+ :return: This instance's storage
+ :rtype: Storage
+ """
+ return self._storage
+
+ def close(self) -> None:
+ """
+ Close the database.
+
+ This may be needed if the storage instance used for this database
+ needs to perform cleanup operations like closing file handles.
+
+ To ensure this method is called, the TinyDB instance can be used as a
+ context manager::
+
+ with TinyDB('data.json') as db:
+ db.insert({'foo': 'bar'})
+
+ Upon leaving this context, the ``close`` method will be called.
+ """
+ self._opened = False
+ self.storage.close()
+
+ def __enter__(self):
+ """
+ Use the database as a context manager.
+
+ Using the database as a context manager ensures that the
+ :meth:`~tinydb.database.TinyDB.close` method is called upon leaving
+ the context.
+
+ :return: The current instance
+ """
+ return self
+
+ def __exit__(self, *args):
+ """
+ Close the storage instance when leaving a context.
+ """
+ if self._opened:
+ self.close()
+
+ def __getattr__(self, name):
+ """
+ Forward all unknown attribute calls to the default table instance.
+ """
+ return getattr(self.table(self.default_table_name), name)
+
+ # Here we forward magic methods to the default table instance. These are
+ # not handled by __getattr__ so we need to forward them manually here
+
+ def __len__(self):
+ """
+ Get the total number of documents in the default table.
+
+ >>> db = TinyDB('db.json')
+ >>> len(db)
+ 0
+ """
+ return len(self.table(self.default_table_name))
+
+ def __iter__(self) -> Iterator[Document]:
+ """
+ Return an iterater for the default table's documents.
+ """
+ return iter(self.table(self.default_table_name))
diff --git a/debian/tinydb/middlewares.py b/debian/tinydb/middlewares.py
new file mode 100644
index 0000000..05b299b
--- /dev/null
+++ b/debian/tinydb/middlewares.py
@@ -0,0 +1,124 @@
+"""
+Contains the :class:`base class <tinydb.middlewares.Middleware>` for
+middlewares and implementations.
+"""
+
+
+class Middleware:
+ """
+ The base class for all Middlewares.
+
+ Middlewares hook into the read/write process of TinyDB allowing you to
+ extend the behaviour by adding caching, logging, ...
+
+ Your middleware's ``__init__`` method has to call the parent class
+ constructor so the middleware chain can be configured properly.
+ """
+
+ def __init__(self, storage_cls):
+ self._storage_cls = storage_cls
+ self.storage = None
+
+ def __call__(self, *args, **kwargs):
+ """
+ Create the storage instance and store it as self.storage.
+
+ Usually a user creates a new TinyDB instance like this::
+
+ TinyDB(storage=StorageClass)
+
+ The storage keyword argument is used by TinyDB this way::
+
+ self.storage = storage(*args, **kwargs)
+
+ As we can see, ``storage(...)`` runs the constructor and returns the
+ new storage instance.
+
+
+ Using Middlewares, the user will call::
+
+ The 'real' storage class
+ v
+ TinyDB(storage=Middleware(StorageClass))
+ ^
+ Already an instance!
+
+ So, when running ``self.storage = storage(*args, **kwargs)`` Python
+ now will call ``__call__`` and TinyDB will expect the return value to
+ be the storage (or Middleware) instance. Returning the instance is
+ simple, but we also got the underlying (*real*) StorageClass as an
+ __init__ argument that still is not an instance.
+ So, we initialize it in __call__ forwarding any arguments we recieve
+ from TinyDB (``TinyDB(arg1, kwarg1=value, storage=...)``).
+
+ In case of nested Middlewares, calling the instance as if it was an
+ class results in calling ``__call__`` what initializes the next
+ nested Middleware that itself will initialize the next Middleware and
+ so on.
+ """
+
+ self.storage = self._storage_cls(*args, **kwargs)
+
+ return self
+
+ def __getattr__(self, name):
+ """
+ Forward all unknown attribute calls to the underlying storage so we
+ remain as transparent as possible.
+ """
+
+ return getattr(self.__dict__['storage'], name)
+
+
+class CachingMiddleware(Middleware):
+ """
+ Add some caching to TinyDB.
+
+ This Middleware aims to improve the performance of TinyDB by writing only
+ the last DB state every :attr:`WRITE_CACHE_SIZE` time and reading always
+ from cache.
+ """
+
+ #: The number of write operations to cache before writing to disc
+ WRITE_CACHE_SIZE = 1000
+
+ def __init__(self, storage_cls):
+ # Initialize the parent constructor
+ super().__init__(storage_cls)
+
+ # Prepare the cache
+ self.cache = None
+ self._cache_modified_count = 0
+
+ def read(self):
+ if self.cache is None:
+ # Empty cache: read from the storage
+ self.cache = self.storage.read()
+
+ # Return the cached data
+ return self.cache
+
+ def write(self, data):
+ # Store data in cache
+ self.cache = data
+ self._cache_modified_count += 1
+
+ # Check if we need to flush the cache
+ if self._cache_modified_count >= self.WRITE_CACHE_SIZE:
+ self.flush()
+
+ def flush(self):
+ """
+ Flush all unwritten data to disk.
+ """
+ if self._cache_modified_count > 0:
+ # Force-flush the cache by writing the data to the storage
+ self.storage.write(self.cache)
+ self._cache_modified_count = 0
+
+ def close(self):
+ # Flush potentially unwritten data
+ self.flush()
+
+ # Let the storage clean up too
+ self.storage.close()
diff --git a/debian/tinydb/operations.py b/debian/tinydb/operations.py
new file mode 100644
index 0000000..db52f9b
--- /dev/null
+++ b/debian/tinydb/operations.py
@@ -0,0 +1,69 @@
+"""
+A collection of update operations for TinyDB.
+
+They are used for updates like this:
+
+>>> db.update(delete('foo'), where('foo') == 2)
+
+This would delete the ``foo`` field from all documents where ``foo`` equals 2.
+"""
+
+
+def delete(field):
+ """
+ Delete a given field from the document.
+ """
+ def transform(doc):
+ del doc[field]
+
+ return transform
+
+
+def add(field, n):
+ """
+ Add ``n`` to a given field in the document.
+ """
+ def transform(doc):
+ doc[field] += n
+
+ return transform
+
+
+def subtract(field, n):
+ """
+ Substract ``n`` to a given field in the document.
+ """
+ def transform(doc):
+ doc[field] -= n
+
+ return transform
+
+
+def set(field, val):
+ """
+ Set a given field to ``val``.
+ """
+ def transform(doc):
+ doc[field] = val
+
+ return transform
+
+
+def increment(field):
+ """
+ Increment a given field in the document by 1.
+ """
+ def transform(doc):
+ doc[field] += 1
+
+ return transform
+
+
+def decrement(field):
+ """
+ Decrement a given field in the document by 1.
+ """
+ def transform(doc):
+ doc[field] -= 1
+
+ return transform
diff --git a/debian/tinydb/py.typed b/debian/tinydb/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/debian/tinydb/queries.py b/debian/tinydb/queries.py
new file mode 100644
index 0000000..311be27
--- /dev/null
+++ b/debian/tinydb/queries.py
@@ -0,0 +1,469 @@
+"""
+Contains the querying interface.
+
+Starting with :class:`~tinydb.queries.Query` you can construct complex
+queries:
+
+>>> ((where('f1') == 5) & (where('f2') != 2)) | where('s').matches(r'^\\w+$')
+(('f1' == 5) and ('f2' != 2)) or ('s' ~= ^\\w+$ )
+
+Queries are executed by using the ``__call__``:
+
+>>> q = where('val') == 5
+>>> q({'val': 5})
+True
+>>> q({'val': 1})
+False
+"""
+
+import re
+from typing import Mapping, Tuple, Callable, Any, Union, List
+
+from .utils import freeze
+
+__all__ = ('Query', 'where')
+
+
+def is_sequence(obj):
+ return hasattr(obj, '__iter__')
+
+
+class QueryInstance:
+ """
+ A query instance.
+
+ This is the object on which the actual query operations are performed. The
+ :class:`~tinydb.queries.Query` class acts like a query builder and
+ generates :class:`~tinydb.queries.QueryInstance` objects which will
+ evaluate their query against a given document when called.
+
+ Query instances can be combined using logical OR and AND and inverted using
+ logical NOT.
+
+ In order to be usable in a query cache, a query needs to have a stable hash
+ value with the same query always returning the same hash. That way a query
+ instance can be used as a key in a dictionary.
+ """
+
+ def __init__(self, test: Callable[[Mapping], bool], hashval: Tuple):
+ self._test = test
+ self._hash = hashval
+
+ def __call__(self, value: Mapping) -> bool:
+ """
+ Evaluate the query to check if it matches a specified value.
+
+ :param value: The value to check.
+ :return: Wether the value matchs this query.
+ """
+ return self._test(value)
+
+ def __hash__(self):
+ # We calculate the query hash by using the ``hashval`` object which
+ # describes this query uniquely so we can calculate a stable hash value
+ # by simply hashing it
+ return hash(self._hash)
+
+ def __repr__(self):
+ return 'QueryImpl{}'.format(self._hash)
+
+ def __eq__(self, other: object):
+ if isinstance(other, QueryInstance):
+ return self._hash == other._hash
+
+ return False
+
+ # --- Query modifiers -----------------------------------------------------
+
+ def __and__(self, other: 'QueryInstance') -> 'QueryInstance':
+ # We use a frozenset for the hash as the AND operation is commutative
+ # (a & b == b & a) and the frozenset does not consider the order of
+ # elements
+ return QueryInstance(lambda value: self(value) and other(value),
+ ('and', frozenset([self._hash, other._hash])))
+
+ def __or__(self, other: 'QueryInstance') -> 'QueryInstance':
+ # We use a frozenset for the hash as the OR operation is commutative
+ # (a | b == b | a) and the frozenset does not consider the order of
+ # elements
+ return QueryInstance(lambda value: self(value) or other(value),
+ ('or', frozenset([self._hash, other._hash])))
+
+ def __invert__(self) -> 'QueryInstance':
+ return QueryInstance(lambda value: not self(value),
+ ('not', self._hash))
+
+
+class Query(QueryInstance):
+ """
+ TinyDB Queries.
+
+ Allows to build queries for TinyDB databases. There are two main ways of
+ using queries:
+
+ 1) ORM-like usage:
+
+ >>> User = Query()
+ >>> db.search(User.name == 'John Doe')
+ >>> db.search(User['logged-in'] == True)
+
+ 2) Classical usage:
+
+ >>> db.search(where('value') == True)
+
+ Note that ``where(...)`` is a shorthand for ``Query(...)`` allowing for
+ a more fluent syntax.
+
+ Besides the methods documented here you can combine queries using the
+ binary AND and OR operators:
+
+ >>> # Binary AND:
+ >>> db.search((where('field1').exists()) & (where('field2') == 5))
+ >>> # Binary OR:
+ >>> db.search((where('field1').exists()) | (where('field2') == 5))
+
+ Queries are executed by calling the resulting object. They expect to get
+ the document to test as the first argument and return ``True`` or
+ ``False`` depending on whether the documents matches the query or not.
+ """
+
+ def __init__(self) -> None:
+ # The current path of fields to access when evaluating the object
+ self._path = () # type: Tuple[str, ...]
+
+ # Prevent empty queries to be evaluated
+ def notest(_):
+ raise RuntimeError('Empty query was evaluated')
+
+ super().__init__(
+ test=notest,
+ hashval=(None,)
+ )
+
+ def __repr__(self):
+ return '{}()'.format(type(self).__name__)
+
+ def __hash__(self):
+ return super().__hash__()
+
+ def __getattr__(self, item: str):
+ # Generate a new query object with the new query path
+ # We use type(self) to get the class of the current query in case
+ # someone uses a subclass of ``Query``
+ query = type(self)()
+
+ # Now we add the accessed item to the query path ...
+ query._path = self._path + (item,)
+
+ # ... and update the query hash
+ query._hash = ('path', query._path)
+
+ return query
+
+ def __getitem__(self, item: str):
+ # A different syntax for ``__getattr__``
+
+ # We cannot call ``getattr(item)`` here as it would try to resolve
+ # the name as a method name first, only then call our ``__getattr__``
+ # method. By calling ``__getattr__`` directly, we make sure that
+ # calling e.g. ``Query()['test']`` will always generate a query for a
+ # document's ``test`` field instead of returning a reference to the
+ # ``Query.test`` method
+ return self.__getattr__(item)
+
+ def _generate_test(
+ self,
+ test: Callable[[Any], bool],
+ hashval: Tuple,
+ allow_empty_path: bool = False
+ ) -> QueryInstance:
+ """
+ Generate a query based on a test function that first resolves the query
+ path.
+
+ :param test: The test the query executes.
+ :param hashval: The hash of the query.
+ :return: A :class:`~tinydb.queries.QueryInstance` object
+ """
+ if not self._path and not allow_empty_path:
+ raise ValueError('Query has no path')
+
+ def runner(value):
+ try:
+ # Resolve the path
+ for part in self._path:
+ value = value[part]
+ except (KeyError, TypeError):
+ return False
+ else:
+ # Perform the specified test
+ return test(value)
+
+ return QueryInstance(
+ lambda value: runner(value),
+ hashval
+ )
+
+ def __eq__(self, rhs: Any):
+ """
+ Test a dict value for equality.
+
+ >>> Query().f1 == 42
+
+ :param rhs: The value to compare against
+ """
+ return self._generate_test(
+ lambda value: value == rhs,
+ ('==', self._path, freeze(rhs))
+ )
+
+ def __ne__(self, rhs: Any):
+ """
+ Test a dict value for inequality.
+
+ >>> Query().f1 != 42
+
+ :param rhs: The value to compare against
+ """
+ return self._generate_test(
+ lambda value: value != rhs,
+ ('!=', self._path, freeze(rhs))
+ )
+
+ def __lt__(self, rhs: Any) -> QueryInstance:
+ """
+ Test a dict value for being lower than another value.
+
+ >>> Query().f1 < 42
+
+ :param rhs: The value to compare against
+ """
+ return self._generate_test(
+ lambda value: value < rhs,
+ ('<', self._path, rhs)
+ )
+
+ def __le__(self, rhs: Any) -> QueryInstance:
+ """
+ Test a dict value for being lower than or equal to another value.
+
+ >>> where('f1') <= 42
+
+ :param rhs: The value to compare against
+ """
+ return self._generate_test(
+ lambda value: value <= rhs,
+ ('<=', self._path, rhs)
+ )
+
+ def __gt__(self, rhs: Any) -> QueryInstance:
+ """
+ Test a dict value for being greater than another value.
+
+ >>> Query().f1 > 42
+
+ :param rhs: The value to compare against
+ """
+ return self._generate_test(
+ lambda value: value > rhs,
+ ('>', self._path, rhs)
+ )
+
+ def __ge__(self, rhs: Any) -> QueryInstance:
+ """
+ Test a dict value for being greater than or equal to another value.
+
+ >>> Query().f1 >= 42
+
+ :param rhs: The value to compare against
+ """
+ return self._generate_test(
+ lambda value: value >= rhs,
+ ('>=', self._path, rhs)
+ )
+
+ def exists(self) -> QueryInstance:
+ """
+ Test for a dict where a provided key exists.
+
+ >>> Query().f1.exists()
+ """
+ return self._generate_test(
+ lambda _: True,
+ ('exists', self._path)
+ )
+
+ def matches(self, regex: str, flags: int = 0) -> QueryInstance:
+ """
+ Run a regex test against a dict value (whole string has to match).
+
+ >>> Query().f1.matches(r'^\\w+$')
+
+ :param regex: The regular expression to use for matching
+ :param flags: regex flags to pass to ``re.match``
+ """
+ def test(value):
+ if not isinstance(value, str):
+ return False
+
+ return re.match(regex, value, flags) is not None
+
+ return self._generate_test(test, ('matches', self._path, regex))
+
+ def search(self, regex: str, flags: int = 0) -> QueryInstance:
+ """
+ Run a regex test against a dict value (only substring string has to
+ match).
+
+ >>> Query().f1.search(r'^\\w+$')
+
+ :param regex: The regular expression to use for matching
+ :param flags: regex flags to pass to ``re.match``
+ """
+
+ def test(value):
+ if not isinstance(value, str):
+ return False
+
+ return re.search(regex, value, flags) is not None
+
+ return self._generate_test(test, ('search', self._path, regex))
+
+ def test(self, func: Callable[[Mapping], bool], *args) -> QueryInstance:
+ """
+ Run a user-defined test function against a dict value.
+
+ >>> def test_func(val):
+ ... return val == 42
+ ...
+ >>> Query().f1.test(test_func)
+
+ .. warning::
+
+ The test fuction provided needs to be deterministic (returning the
+ same value when provided with the same arguments), otherwise this
+ may mess up the query cache that :class:`~tinydb.table.Table`
+ implements.
+
+ :param func: The function to call, passing the dict as the first
+ argument
+ :param args: Additional arguments to pass to the test function
+ """
+ return self._generate_test(
+ lambda value: func(value, *args),
+ ('test', self._path, func, args)
+ )
+
+ def any(self, cond: Union[QueryInstance, List[Any]]) -> QueryInstance:
+ """
+ Check if a condition is met by any document in a list,
+ where a condition can also be a sequence (e.g. list).
+
+ >>> Query().f1.any(Query().f2 == 1)
+
+ Matches::
+
+ {'f1': [{'f2': 1}, {'f2': 0}]}
+
+ >>> Query().f1.any([1, 2, 3])
+
+ Matches::
+
+ {'f1': [1, 2]}
+ {'f1': [3, 4, 5]}
+
+ :param cond: Either a query that at least one document has to match or
+ a list of which at least one document has to be contained
+ in the tested document.
+ """
+ if callable(cond):
+ def test(value):
+ return is_sequence(value) and any(cond(e) for e in value)
+
+ else:
+ def test(value):
+ return is_sequence(value) and any(e in cond for e in value)
+
+ return self._generate_test(
+ lambda value: test(value),
+ ('any', self._path, freeze(cond))
+ )
+
+ def all(self, cond: Union['QueryInstance', List[Any]]) -> QueryInstance:
+ """
+ Check if a condition is met by all documents in a list,
+ where a condition can also be a sequence (e.g. list).
+
+ >>> Query().f1.all(Query().f2 == 1)
+
+ Matches::
+
+ {'f1': [{'f2': 1}, {'f2': 1}]}
+
+ >>> Query().f1.all([1, 2, 3])
+
+ Matches::
+
+ {'f1': [1, 2, 3, 4, 5]}
+
+ :param cond: Either a query that all documents have to match or a list
+ which has to be contained in the tested document.
+ """
+ if callable(cond):
+ def test(value):
+ return is_sequence(value) and all(cond(e) for e in value)
+
+ else:
+ def test(value):
+ return is_sequence(value) and all(e in value for e in cond)
+
+ return self._generate_test(
+ lambda value: test(value),
+ ('all', self._path, freeze(cond))
+ )
+
+ def one_of(self, items: List[Any]) -> QueryInstance:
+ """
+ Check if the value is contained in a list or generator.
+
+ >>> Query().f1.one_of(['value 1', 'value 2'])
+
+ :param items: The list of items to check with
+ """
+ return self._generate_test(
+ lambda value: value in items,
+ ('one_of', self._path, freeze(items))
+ )
+
+ def fragment(self, document: Mapping) -> QueryInstance:
+ def test(value):
+ for key in document:
+ if key not in value or value[key] != document[key]:
+ return False
+
+ return True
+
+ return self._generate_test(
+ lambda value: test(value),
+ ('fragment', freeze(document)),
+ allow_empty_path=True
+ )
+
+ def noop(self) -> QueryInstance:
+ """
+ Always evaluate to ``True``.
+
+ Useful for having a base value when composing queries dynamically.
+ """
+
+ return QueryInstance(
+ lambda value: True,
+ ()
+ )
+
+
+def where(key: str) -> Query:
+ """
+ A shorthand for ``Query()[key]``
+ """
+ return Query()[key]
diff --git a/debian/tinydb/storages.py b/debian/tinydb/storages.py
new file mode 100644
index 0000000..21ecde2
--- /dev/null
+++ b/debian/tinydb/storages.py
@@ -0,0 +1,166 @@
+"""
+Contains the :class:`base class <tinydb.storages.Storage>` for storages and
+implementations.
+"""
+
+import io
+import json
+import os
+from abc import ABC, abstractmethod
+from typing import Dict, Any, Optional
+
+__all__ = ('Storage', 'JSONStorage', 'MemoryStorage')
+
+
+def touch(path: str, create_dirs: bool):
+ """
+ Create a file if it doesn't exist yet.
+
+ :param path: The file to create.
+ :param create_dirs: Whether to create all missing parent directories.
+ """
+ if create_dirs:
+ base_dir = os.path.dirname(path)
+
+ # Check if we need to create missing parent directories
+ if not os.path.exists(base_dir):
+ os.makedirs(base_dir)
+
+ # Create the file by opening it in 'a' mode which creates the file if it
+ # does not exist yet but does not modify its contents
+ with open(path, 'a'):
+ pass
+
+
+class Storage(ABC):
+ """
+ The abstract base class for all Storages.
+
+ A Storage (de)serializes the current state of the database and stores it in
+ some place (memory, file on disk, ...).
+ """
+
+ # Using ABCMeta as metaclass allows instantiating only storages that have
+ # implemented read and write
+
+ @abstractmethod
+ def read(self) -> Optional[Dict[str, Dict[str, Any]]]:
+ """
+ Read the current state.
+
+ Any kind of deserialization should go here.
+
+ Return ``None`` here to indicate that the storage is empty.
+ """
+
+ raise NotImplementedError('To be overridden!')
+
+ @abstractmethod
+ def write(self, data: Dict[str, Dict[str, Any]]) -> None:
+ """
+ Write the current state of the database to the storage.
+
+ Any kind of serialization should go here.
+
+ :param data: The current state of the database.
+ """
+
+ raise NotImplementedError('To be overridden!')
+
+ def close(self) -> None:
+ """
+ Optional: Close open file handles, etc.
+ """
+
+ pass
+
+
+class JSONStorage(Storage):
+ """
+ Store the data in a JSON file.
+ """
+
+ def __init__(self, path: str, create_dirs=False, encoding=None, access_mode='r+', **kwargs):
+ """
+ Create a new instance.
+
+ Also creates the storage file, if it doesn't exist and the access mode is appropriate for writing.
+
+ :param path: Where to store the JSON data.
+ :param access_mode: mode in which the file is opened (r, r+, w, a, x, b, t, +, U)
+ :type access_mode: str
+ """
+
+ super().__init__()
+
+ self._mode = access_mode
+ self.kwargs = kwargs
+
+ # Create the file if it doesn't exist and creating is allowed by the
+ # access mode
+ if any([character in self._mode for character in ('+', 'w', 'a')]): # any of the writing modes
+ touch(path, create_dirs=create_dirs)
+
+ # Open the file for reading/writing
+ self._handle = open(path, mode=self._mode, encoding=encoding)
+
+ def close(self) -> None:
+ self._handle.close()
+
+ def read(self) -> Optional[Dict[str, Dict[str, Any]]]:
+ # Get the file size by moving the cursor to the file end and reading
+ # its location
+ self._handle.seek(0, os.SEEK_END)
+ size = self._handle.tell()
+
+ if not size:
+ # File is empty so we return ``None`` so TinyDB can properly
+ # initialize the database
+ return None
+ else:
+ # Return the cursor to the beginning of the file
+ self._handle.seek(0)
+
+ # Load the JSON contents of the file
+ return json.load(self._handle)
+
+ def write(self, data: Dict[str, Dict[str, Any]]):
+ # Move the cursor to the beginning of the file just in case
+ self._handle.seek(0)
+
+ # Serialize the database state using the user-provided arguments
+ serialized = json.dumps(data, **self.kwargs)
+
+ # Write the serialized data to the file
+ try:
+ self._handle.write(serialized)
+ except io.UnsupportedOperation:
+ raise IOError('Cannot write to the database. Access mode is "{0}"'.format(self._mode))
+
+ # Ensure the file has been writtens
+ self._handle.flush()
+ os.fsync(self._handle.fileno())
+
+ # Remove data that is behind the new cursor in case the file has
+ # gotten shorter
+ self._handle.truncate()
+
+
+class MemoryStorage(Storage):
+ """
+ Store the data as JSON in memory.
+ """
+
+ def __init__(self):
+ """
+ Create a new instance.
+ """
+
+ super().__init__()
+ self.memory = None
+
+ def read(self) -> Optional[Dict[str, Dict[str, Any]]]:
+ return self.memory
+
+ def write(self, data: Dict[str, Dict[str, Any]]):
+ self.memory = data
diff --git a/debian/tinydb/table.py b/debian/tinydb/table.py
new file mode 100644
index 0000000..0c6454e
--- /dev/null
+++ b/debian/tinydb/table.py
@@ -0,0 +1,704 @@
+"""
+This module implements tables, the central place for accessing and manipulating
+data in TinyDB.
+"""
+
+from typing import (
+ Callable,
+ Dict,
+ Iterable,
+ Iterator,
+ List,
+ Mapping,
+ Optional,
+ Union,
+ cast,
+ Tuple
+)
+
+from .storages import Storage
+from .queries import Query
+from .utils import LRUCache
+
+__all__ = ('Document', 'Table')
+
+
+class Document(dict):
+ """
+ A document stored in the database.
+
+ This class provides a way to access both a document's content as well as
+ its ID using ``doc.doc_id``.
+ """
+
+ def __init__(self, value: Mapping, doc_id: int):
+ super().__init__(value)
+ self.doc_id = doc_id
+
+
+class Table:
+ """
+ Represents a single TinyDB table.
+
+ It provides methods for accessing and manipulating documents.
+
+ .. admonition:: Query Cache
+
+ As an optimization, a query cache is implemented using a
+ :class:`~tinydb.utils.LRUCache`. This class mimics the interface of
+ a normal ``dict``, but starts to remove the least-recently used entries
+ once a threshold is reached.
+
+ The query cache is updated on every search operation. When writing
+ data, the whole cache is discareded as the query results may have
+ changed.
+
+ .. admonition:: Customization
+
+ For customization, the following class variables can be set:
+
+ - ``document_class`` defines the class that is used to represent
+ documents,
+ - ``document_id_class`` defines the class that is used to represent
+ document IDs,
+ - ``query_cache_class`` defines the class that is used for the query
+ cache
+ - ``default_query_cache_capacity`` defines the default capacity of
+ the query cache
+
+ .. versionadded:: 4.0
+
+
+ :param storage: The storage instance to use for this table
+ :param name: The table name
+ :param cache_size: Maximum capacity of query cache
+ """
+
+ #: The class used to represent documents
+ #:
+ #: .. versionadded:: 4.0
+ document_class = Document
+
+ #: The class used to represent a document ID
+ #:
+ #: .. versionadded:: 4.0
+ document_id_class = int
+
+ #: The class used for caching query results
+ #:
+ #: .. versionadded:: 4.0
+ query_cache_class = LRUCache
+
+ #: The default capacity of the query cache
+ #:
+ #: .. versionadded:: 4.0
+ default_query_cache_capacity = 10
+
+ def __init__(
+ self,
+ storage: Storage,
+ name: str,
+ cache_size: int = default_query_cache_capacity
+ ):
+ """
+ Create a table instance.
+ """
+
+ self._storage = storage
+ self._name = name
+ self._query_cache = self.query_cache_class(capacity=cache_size) \
+ # type: LRUCache[Query, List[Document]]
+
+ self._next_id = None
+
+ def __repr__(self):
+ args = [
+ 'name={!r}'.format(self.name),
+ 'total={}'.format(len(self)),
+ 'storage={}'.format(self._storage),
+ ]
+
+ return '<{} {}>'.format(type(self).__name__, ', '.join(args))
+
+ @property
+ def name(self) -> str:
+ """
+ Get the table name.
+ """
+ return self._name
+
+ @property
+ def storage(self) -> Storage:
+ """
+ Get the table storage instance.
+ """
+ return self._storage
+
+ def insert(self, document: Mapping) -> int:
+ """
+ Insert a new document into the table.
+
+ :param document: the document to insert
+ :returns: the inserted document's ID
+ """
+
+ # Make sure the document implements the ``Mapping`` interface
+ if not isinstance(document, Mapping):
+ raise ValueError('Document is not a Mapping')
+
+ # First, we get the document ID for the new document
+ if isinstance(document, Document):
+ # For a `Document` object we use the specified ID
+ doc_id = document.doc_id
+
+ # We also reset the stored next ID so the next insert won't
+ # re-use document IDs by accident when storing an old value
+ self._next_id = None
+ else:
+ # In all other cases we use the next free ID
+ doc_id = self._get_next_id()
+
+ # Now, we update the table and add the document
+ def updater(table: dict):
+ assert doc_id not in table, 'doc_id '+str(doc_id)+' already exists'
+
+ # By calling ``dict(document)`` we convert the data we got to a
+ # ``dict`` instance even if it was a different class that
+ # implemented the ``Mapping`` interface
+ table[doc_id] = dict(document)
+
+ # See below for details on ``Table._update``
+ self._update_table(updater)
+
+ return doc_id
+
+ def insert_multiple(self, documents: Iterable[Mapping]) -> List[int]:
+ """
+ Insert multiple documents into the table.
+
+ :param documents: a Iterable of documents to insert
+ :returns: a list containing the inserted documents' IDs
+ """
+ doc_ids = []
+
+ def updater(table: dict):
+ for document in documents:
+ # Make sure the document implements the ``Mapping`` interface
+ if not isinstance(document, Mapping):
+ raise ValueError('Document is not a Mapping')
+
+ # Get the document ID for this document and store it so we
+ # can return all document IDs later
+ doc_id = self._get_next_id()
+ doc_ids.append(doc_id)
+
+ # Convert the document to a ``dict`` (see Table.insert) and
+ # store it
+ table[doc_id] = dict(document)
+
+ # See below for details on ``Table._update``
+ self._update_table(updater)
+
+ return doc_ids
+
+ def all(self) -> List[Document]:
+ """
+ Get all documents stored in the table.
+
+ :returns: a list with all documents.
+ """
+
+ # iter(self) (implemented in Table.__iter__ provides an iterator
+ # that returns all documents in this table. We use it to get a list
+ # of all documents by using the ``list`` constructor to perform the
+ # conversion.
+
+ return list(iter(self))
+
+ def search(self, cond: Query) -> List[Document]:
+ """
+ Search for all documents matching a 'where' cond.
+
+ :param cond: the condition to check against
+ :returns: list of matching documents
+ """
+
+ # First, we check the query cache to see if it has results for this
+ # query
+ cached_results = self._query_cache.get(cond)
+ if cached_results is not None:
+ return cached_results[:]
+
+ # Perform the search by applying the query to all documents
+ docs = [doc for doc in self if cond(doc)]
+
+ # Update the query cache
+ self._query_cache[cond] = docs[:]
+
+ return docs
+
+ def get(
+ self,
+ cond: Optional[Query] = None,
+ doc_id: Optional[int] = None,
+ ) -> Optional[Document]:
+ """
+ Get exactly one document specified by a query or a document ID.
+
+ Returns ``None`` if the document doesn't exist.
+
+ :param cond: the condition to check against
+ :param doc_id: the document's ID
+
+ :returns: the document or ``None``
+ """
+
+ if doc_id is not None:
+ # Retrieve a document specified by its ID
+ table = self._read_table()
+ raw_doc = table.get(doc_id, None)
+
+ if raw_doc is None:
+ return None
+
+ # Convert the raw data to the document class
+ return self.document_class(raw_doc, doc_id)
+
+ elif cond is not None:
+ # Find a document specified by a query
+ for doc in self:
+ if cond(doc):
+ return doc
+
+ return None
+
+ raise RuntimeError('You have to pass either cond or doc_id')
+
+ def contains(
+ self,
+ cond: Optional[Query] = None,
+ doc_id: Optional[int] = None
+ ) -> bool:
+ """
+ Check whether the database contains a document matching a query or
+ an ID.
+
+ If ``doc_id`` is set, it checks if the db contains the specified ID.
+
+ :param cond: the condition use
+ :param doc_id: the document ID to look for
+ """
+ if doc_id is not None:
+ # Documents specified by ID
+ return self.get(doc_id=doc_id) is not None
+
+ elif cond is not None:
+ # Document specified by condition
+ return self.get(cond) is not None
+
+ raise RuntimeError('You have to pass either cond or doc_id')
+
+ def update(
+ self,
+ fields: Union[Mapping, Callable[[Mapping], None]],
+ cond: Optional[Query] = None,
+ doc_ids: Optional[Iterable[int]] = None,
+ ) -> List[int]:
+ """
+ Update all matching documents to have a given set of fields.
+
+ :param fields: the fields that the matching documents will have
+ or a method that will update the documents
+ :param cond: which documents to update
+ :param doc_ids: a list of document IDs
+ :returns: a list containing the updated document's ID
+ """
+
+ # Define the function that will perform the update
+ if callable(fields):
+ def perform_update(table, doc_id):
+ # Update documents by calling the update function provided by
+ # the user
+ fields(table[doc_id])
+ else:
+ def perform_update(table, doc_id):
+ # Update documents by setting all fields from the provided data
+ table[doc_id].update(fields)
+
+ if doc_ids is not None:
+ # Perform the update operation for documents specified by a list
+ # of document IDs
+
+ updated_ids = list(doc_ids)
+
+ def updater(table: dict):
+ # Call the processing callback with all document IDs
+ for doc_id in updated_ids:
+ perform_update(table, doc_id)
+
+ # Perform the update operation (see _update_table for details)
+ self._update_table(updater)
+
+ return updated_ids
+
+ elif cond is not None:
+ # Perform the update operation for documents specified by a query
+
+ # Collect affected doc_ids
+ updated_ids = []
+
+ def updater(table: dict):
+ _cond = cast('Query', cond)
+
+ # We need to convert the keys iterator to a list because
+ # we may remove entries from the ``table`` dict during
+ # iteration and doing this without the list conversion would
+ # result in an exception (RuntimeError: dictionary changed size
+ # during iteration)
+ for doc_id in list(table.keys()):
+ # Pass through all documents to find documents matching the
+ # query. Call the processing callback with the document ID
+ if _cond(table[doc_id]):
+ # Add ID to list of updated documents
+ updated_ids.append(doc_id)
+
+ # Perform the update (see above)
+ perform_update(table, doc_id)
+
+ # Perform the update operation (see _update_table for details)
+ self._update_table(updater)
+
+ return updated_ids
+
+ else:
+ # Update all documents unconditionally
+
+ updated_ids = []
+
+ def updater(table: dict):
+ # Process all documents
+ for doc_id in list(table.keys()):
+ # Add ID to list of updated documents
+ updated_ids.append(doc_id)
+
+ # Perform the update (see above)
+ perform_update(table, doc_id)
+
+ # Perform the update operation (see _update_table for details)
+ self._update_table(updater)
+
+ return updated_ids
+
+ def update_multiple(
+ self,
+ updates: Iterable[
+ Tuple[Union[Mapping, Callable[[Mapping], None]], Query]
+ ],
+ ) -> List[int]:
+ """
+ Update all matching documents to have a given set of fields.
+
+ :returns: a list containing the updated document's ID
+ """
+
+ # Define the function that will perform the update
+ def perform_update(fields, table, doc_id):
+ if callable(fields):
+ # Update documents by calling the update function provided
+ # by the user
+ fields(table[doc_id])
+ else:
+ # Update documents by setting all fields from the provided
+ # data
+ table[doc_id].update(fields)
+
+ # Perform the update operation for documents specified by a query
+
+ # Collect affected doc_ids
+ updated_ids = []
+
+ def updater(table: dict):
+ # We need to convert the keys iterator to a list because
+ # we may remove entries from the ``table`` dict during
+ # iteration and doing this without the list conversion would
+ # result in an exception (RuntimeError: dictionary changed size
+ # during iteration)
+ for doc_id in list(table.keys()):
+ for fields, cond in updates:
+ _cond = cast('Query', cond)
+
+ # Pass through all documents to find documents matching the
+ # query. Call the processing callback with the document ID
+ if _cond(table[doc_id]):
+ # Add ID to list of updated documents
+ updated_ids.append(doc_id)
+
+ # Perform the update (see above)
+ perform_update(fields, table, doc_id)
+
+ # Perform the update operation (see _update_table for details)
+ self._update_table(updater)
+
+ return updated_ids
+
+ def upsert(self, document: Mapping, cond: Query) -> List[int]:
+ """
+ Update a document, if it exist, insert it otherwise.
+
+ Note: this will update *all* documents matching the query.
+
+ :param document: the document to insert or the fields to update
+ :param cond: which document to look for
+ :returns: a list containing the updated document's ID
+ """
+
+ # Perform the update operation
+ updated_docs = self.update(document, cond)
+
+ # If documents have been updated: return their IDs
+ if updated_docs:
+ return updated_docs
+
+ # There are no documents that match the specified query -> insert the
+ # data as a new document
+ return [self.insert(document)]
+
+ def remove(
+ self,
+ cond: Optional[Query] = None,
+ doc_ids: Optional[Iterable[int]] = None,
+ ) -> List[int]:
+ """
+ Remove all matching documents.
+
+ :param cond: the condition to check against
+ :param doc_ids: a list of document IDs
+ :returns: a list containing the removed documents' ID
+ """
+ if cond is not None:
+ removed_ids = []
+
+ # This updater function will be called with the table data
+ # as its first argument. See ``Table._update`` for details on this
+ # operation
+ def updater(table: dict):
+ # We need to convince MyPy (the static type checker) that
+ # the ``cond is not None`` invariant still holds true when
+ # the updater function is called
+ _cond = cast('Query', cond)
+
+ # We need to convert the keys iterator to a list because
+ # we may remove entries from the ``table`` dict during
+ # iteration and doing this without the list conversion would
+ # result in an exception (RuntimeError: dictionary changed size
+ # during iteration)
+ for doc_id in list(table.keys()):
+ if _cond(table[doc_id]):
+ # Add document ID to list of removed document IDs
+ removed_ids.append(doc_id)
+
+ # Remove document from the table
+ table.pop(doc_id)
+
+ # Perform the remove operation
+ self._update_table(updater)
+
+ return removed_ids
+
+ if doc_ids is not None:
+ # This function returns the list of IDs for the documents that have
+ # been removed. When removing documents identified by a set of
+ # document IDs, it's this list of document IDs we need to return
+ # later.
+ # We convert the document ID iterator into a list so we can both
+ # use the document IDs to remove the specified documents as well as
+ # to return the list of affected document IDs
+ removed_ids = list(doc_ids)
+
+ def updater(table: dict):
+ for doc_id in removed_ids:
+ table.pop(doc_id)
+
+ # Perform the remove operation
+ self._update_table(updater)
+
+ return removed_ids
+
+ raise RuntimeError('Use truncate() to remove all documents')
+
+ def truncate(self) -> None:
+ """
+ Truncate the table by removing all documents.
+ """
+
+ # Update the table by resetting all data
+ self._update_table(lambda table: table.clear())
+
+ # Reset document ID counter
+ self._next_id = None
+
+ def count(self, cond: Query) -> int:
+ """
+ Count the documents matching a query.
+
+ :param cond: the condition use
+ """
+
+ return len(self.search(cond))
+
+ def clear_cache(self) -> None:
+ """
+ Clear the query cache.
+ """
+
+ self._query_cache.clear()
+
+ def __len__(self):
+ """
+ Count the total number of documents in this table.
+ """
+
+ # Using self._read_table() will convert all documents into
+ # the document class. But for counting the number of documents
+ # this conversion is not necessary, thus we read the storage
+ # directly here
+
+ tables = self._storage.read()
+
+ if tables is None:
+ return 0
+
+ try:
+ return len(tables[self.name])
+ except KeyError:
+ return 0
+
+ def __iter__(self) -> Iterator[Document]:
+ """
+ Iterate over all documents stored in the table.
+
+ :returns: an iterator over all documents.
+ """
+
+ # Iterate all documents and their IDs
+ for doc_id, doc in self._read_table().items():
+ # Convert documents to the document class
+ yield self.document_class(doc, doc_id)
+
+ def _get_next_id(self):
+ """
+ Return the ID for a newly inserted document.
+ """
+
+ # If we already know the next ID
+ if self._next_id is not None:
+ next_id = self._next_id
+ self._next_id = next_id + 1
+
+ return next_id
+
+ # Determine the next document ID by finding out the max ID value
+ # of the current table documents
+
+ # Read the table documents
+ table = self._read_table()
+
+ # If the table is empty, set the initial ID
+ if not table:
+ next_id = 1
+ self._next_id = next_id + 1
+
+ return next_id
+
+ # Determine the next ID based on the maximum ID that's currently in use
+ max_id = max(self.document_id_class(i) for i in table.keys())
+ next_id = max_id + 1
+
+ # The next ID we wil return AFTER this call needs to be larger than
+ # the current next ID we calculated
+ self._next_id = next_id + 1
+
+ return next_id
+
+ def _read_table(self) -> Dict[int, Mapping]:
+ """
+ Read the table data from the underlying storage.
+
+ Here we read the data from the underlying storage and convert all
+ IDs to the document ID class. Documents themselves are NOT yet
+ transformed into the document class, we may not want to convert
+ *all* documents when returning only one document for example.
+ """
+
+ # Retrieve the tables from the storage
+ tables = self._storage.read()
+
+ if tables is None:
+ # The database is empty
+ return {}
+
+ # Retrieve the current table's data
+ try:
+ table = tables[self.name]
+ except KeyError:
+ # The table does not exist yet, so it is empty
+ return {}
+
+ # Convert all document IDs to the correct document ID class and return
+ # the table data dict
+ return {
+ self.document_id_class(doc_id): doc
+ for doc_id, doc in table.items()
+ }
+
+ def _update_table(self, updater: Callable[[Dict[int, Mapping]], None]):
+ """
+ Perform an table update operation.
+
+ The storage interface used by TinyDB only allows to read/write the
+ complete database data, but not modifying only portions of it. Thus
+ to only update portions of the table data, we first perform a read
+ operation, perform the update on the table data and then write
+ the updated data back to the storage.
+
+ As a further optimization, we don't convert the documents into the
+ document class, as the table data will *not* be returned to the user.
+ """
+
+ tables = self._storage.read()
+
+ if tables is None:
+ # The database is empty
+ tables = {}
+
+ try:
+ raw_table = tables[self.name]
+ except KeyError:
+ # The table does not exist yet, so it is empty
+ raw_table = {}
+
+ # Convert the document IDs to the document ID class.
+ # This is required as the rest of TinyDB expects the document IDs
+ # to be an instance of ``self.document_id_class`` but the storage
+ # might convert dict keys to strings.
+ table = {
+ self.document_id_class(doc_id): doc
+ for doc_id, doc in raw_table.items()
+ }
+
+ # Perform the table update operation
+ updater(table)
+
+ # Convert the document IDs back to strings.
+ # This is required as some storages (most notably the JSON file format)
+ # don't require IDs other than strings.
+ tables[self.name] = {
+ str(doc_id): doc
+ for doc_id, doc in table.items()
+ }
+
+ # Write the newly updated data back to the storage
+ self._storage.write(tables)
+
+ # Clear the query cache, as the table contents have changed
+ self.clear_cache()
diff --git a/debian/tinydb/utils.py b/debian/tinydb/utils.py
new file mode 100644
index 0000000..61a3262
--- /dev/null
+++ b/debian/tinydb/utils.py
@@ -0,0 +1,135 @@
+"""
+Utility functions.
+"""
+
+from collections import OrderedDict, abc
+from typing import List, Iterator, TypeVar, Generic, Union, Optional
+
+K = TypeVar('K')
+V = TypeVar('V')
+D = TypeVar('D')
+
+__all__ = ('LRUCache', 'freeze')
+
+
+class LRUCache(abc.MutableMapping, Generic[K, V]):
+ """
+ A least-recently used (LRU) cache with a fixed cache size.
+
+ This class acts as a dictionary but has a limited size. If the number of
+ entries in the cache exeeds the cache size, the leat-recently accessed
+ entry will be discareded.
+
+ This is implemented using an ``OrderedDict``. On every access the accessed
+ entry is moved to the front by re-inserting it into the ``OrderedDict``.
+ When adding an entry and the cache size is exceeded, the last entry will
+ be discareded.
+ """
+
+ def __init__(self, capacity=None):
+ self.capacity = capacity
+ self.cache = OrderedDict() # type: OrderedDict[K, V]
+
+ @property
+ def lru(self) -> List[K]:
+ return list(self.cache.keys())
+
+ @property
+ def length(self) -> int:
+ return len(self.cache)
+
+ def clear(self) -> None:
+ self.cache.clear()
+
+ def __len__(self) -> int:
+ return self.length
+
+ def __contains__(self, key: object) -> bool:
+ return key in self.cache
+
+ def __setitem__(self, key: K, value: V) -> None:
+ self.set(key, value)
+
+ def __delitem__(self, key: K) -> None:
+ del self.cache[key]
+
+ def __getitem__(self, key) -> V:
+ value = self.get(key)
+ if value is None:
+ raise KeyError(key)
+
+ return value
+
+ def __iter__(self) -> Iterator[K]:
+ return iter(self.cache)
+
+ def get(self, key: K, default: D = None) -> Optional[Union[V, D]]:
+ value = self.cache.get(key)
+
+ if value is not None:
+ self.cache.move_to_end(key, last=True)
+
+ return value
+
+ return default
+
+ def set(self, key: K, value: V):
+ if self.cache.get(key):
+ self.cache.move_to_end(key, last=True)
+
+ else:
+ self.cache[key] = value
+
+ # Check, if the cache is full and we have to remove old items
+ # If the queue is of unlimited size, self.capacity is NaN and
+ # x > NaN is always False in Python and the cache won't be cleared.
+ if self.capacity is not None and self.length > self.capacity:
+ self.cache.popitem(last=False)
+
+
+class FrozenDict(dict):
+ """
+ An immutable dictoinary.
+
+ This is used to generate stable hashes for queries that contain dicts.
+ Usually, Python dicts are not hashable because they are mutable. This
+ class removes the mutability and implements the ``__hash__`` method.
+ """
+
+ def __hash__(self):
+ # Calculate the has by hashing a tuple of all dict items
+ return hash(tuple(sorted(self.items())))
+
+ def _immutable(self, *args, **kws):
+ raise TypeError('object is immutable')
+
+ # Disable write access to the dict
+ __setitem__ = _immutable
+ __delitem__ = _immutable
+ clear = _immutable
+ setdefault = _immutable
+ popitem = _immutable
+
+ def update(self, e=None, **f):
+ raise TypeError('object is immutable')
+
+ def pop(self, k, d=None):
+ raise TypeError('object is immutable')
+
+
+def freeze(obj):
+ """
+ Freeze an object by making it immutable and thus hashable.
+ """
+ if isinstance(obj, dict):
+ # Transform dicts into ``FrozenDict``s
+ return FrozenDict((k, freeze(v)) for k, v in obj.items())
+ elif isinstance(obj, list):
+ # Transform lists into tuples
+ return tuple(freeze(el) for el in obj)
+ elif isinstance(obj, set):
+ # Transform sets into ``frozenset``s
+ return frozenset(obj)
+ else:
+ # Don't handle all other objects
+ return obj
diff --git a/debian/tinydb/version.py b/debian/tinydb/version.py
new file mode 100644
index 0000000..26a6c39
--- /dev/null
+++ b/debian/tinydb/version.py
@@ -0,0 +1 @@
+__version__ = '4.4.0'
More information about the Neon-commits
mailing list