Source code for digikamdb.table
"""
Basic Digikam Table Class
"""
import logging
import re
from typing import Any, Iterable, Mapping, Optional, Union
from sqlalchemy import delete, inspect, text
from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
from .exceptions import (
DigikamError,
DigikamObjectNotFoundError,
DigikamMultipleObjectsFoundError,
DigikamDataIntegrityError
)
log = logging.getLogger(__name__)
[docs]
class DigikamTable:
"""
An abstract base class for table classes
Provides some low-level methods for accessing data:
* Members can be accessed by id via ``object[id]``.
* Class is iterable, returning all rows of the table.
* Some internal functionality
Parameters:
digikam: The "parent" ``Digikam`` object
log_create: Used internally to control logging
"""
#: Function returning the corresponding mapped class
_class_function = None
#: ID column
_id_column = '_id'
#: Raise an Exception when ``[]`` does not find a suitable row.
#: Otherwise, ``None`` is returned.
_raise_on_not_found = True
def __init__(
self,
digikam: 'Digikam', # noqa: F821
log_create: bool = True
):
if log_create:
log.debug('Creating %s object', self.__class__.__name__)
self._digikam = digikam
self._session = self.digikam.session
self._is_mysql = self.digikam.is_mysql
self.Class = self.__class__._class_function(self.digikam)
setattr(self, self.Class.__name__, self.Class)
@property
def digikam(self) -> 'Digikam': # noqa: F821
"""
The ``Digikam`` object.
"""
return self._digikam
def __iter__(self) -> Iterable:
yield from self._select()
def __contains__(self, key: str) -> bool:
kwargs = { self._id_column: key }
return self._select(**kwargs).one_or_none() is not None
def __getitem__(self, key: Any) -> 'DigikamObject': # noqa: F821
kwargs = { self._id_column: key }
try:
if self._raise_on_not_found:
return self._select(**kwargs).one()
else: # pragma: no cover
# This will not happen now, but we keep it for safety
return self._select(**kwargs).one_or_none()
except NoResultFound:
raise DigikamObjectNotFoundError('No %s object for %s=%s' % (
self.Class.__name__, self._id_column, key
))
except MultipleResultsFound: # pragma: no cover
raise DigikamMultipleObjectsFoundError('Multiple %s objects for %s=%s' % (
self.Class.__name__, self._id_column, key
))
[docs]
def select(
self,
*args,
**kwargs
) -> '~sqlalchemy.orm.Query': # noqa: F821
"""
Returns the result of a ``SELECT`` on the table.
Each positional argument must be a string containing a valid ``WHERE``
clause (without ``WHERE``). These clauses are combined with ``AND``.
Keyword arguments are used as additional ``WHERE`` clauses checking for
equality. For example, ``select('a > 2', b = 1)`` will result in a
``SELECT ... WHERE a>2 AND b=1`` SQL statement.
The result is a :class:`~sqlalchemy.orm.Query` object that can be
refined further. When adding additional conditions, the column names
must be prefixed with ``_``.
The results can be accessed by iterating over the query or through
methods like :meth:`~sqlalchemy.orm.Query.all` or
:meth:`~sqlalchemy.orm.Query.one_or_none`.
Args:
args: ``WHERE`` clauses as text
kwargs: Columns to check for equality
Returns:
The resulting ``Query`` object.
See also:
* SQLAlchemy Query :meth:`~sqlalchemy.orm.Query.filter` method
* SQLAlchemy Query :meth:`~sqlalchemy.orm.Query.filter_by` method
"""
query = self._select(**kwargs)
for arg in args:
txt = arg.strip()
log.debug(' adding WHERE %s', txt)
query = query.filter(text(txt))
return query
@staticmethod
def _underscore_kwargs(kwargs: Mapping) -> Mapping:
ret = {}
for k, v in kwargs.items():
if not k.startswith('_'):
k = '_' + k
ret[k] = v
return ret
def _select(
self,
**kwargs
) -> '~sqlalchemy.orm.Query': # noqa: F821
"""
Returns a select result for the table.
Args:
kwargs: Keyword arguments are used as arguments for
:meth:`~sqlalchemy.orm.Query.filter_by`.
Returns:
Iterable query.
"""
kwargs = self._underscore_kwargs(kwargs)
log.debug(
'%s: Selecting %s objects with %s',
self.__class__.__name__,
self.Class.__name__,
kwargs
)
query = self._session.query(self.Class)
if kwargs:
query = query.filter_by(**kwargs)
return query
def _insert(self, **kwargs) -> 'DigikamObject': # noqa: F821
"""
Inserts a new record.
Args:
kwargs: The keyword arguments are used as properties for the new record.
Returns:
The generated object.
"""
kwargs = self._underscore_kwargs(kwargs)
log.debug(
'%s: Creating %s object with %s',
self.__class__.__name__,
self.Class.__name__,
kwargs
)
new = self.Class(**kwargs)
self._session.add(new)
self._session.flush()
return new
def _delete(self, **kwargs) -> None:
"""
Deletes rows from the table.
Args:
kwargs: Keyword arguments are used as arguments for
:meth:`~sqlalchemy.orm.Query.filter_by`.
"""
kwargs = self._underscore_kwargs(kwargs)
log.debug(
'%s: Deleting %s objects with %s',
self.__class__.__name__,
self.Class.__name__,
kwargs
)
if not kwargs: # pragma: no cover
raise ValueError('Objects to delete must be specified')
self._session.execute(
delete(self.Class)
.filter_by(**kwargs))