Source code for tagit.model.tags

"""Tags management

Tags are keywords which can be attached to images.
Tags can be retrieved (get) and modified (add, set, remove).
Other actions may be available.

Part of the tagit module.
A copy of the license is provided with the project.
Author: Matthias Baumgartner, 2016

"""
# STANDARD IMPORTS
import itertools

# INNER-MODULE IMPORTS
from ..basics import fst, unique, union

# EXPORTS
__all__ = ('Tags_SQLite', )

## CODE ##

[docs]class Tags(object): """The Tags baseclass and interface. Tags are keywords which can be attached to images. Tags can be retrieved (get) and modified (add, set, remove). Other actions may be available. """
[docs] def get_all(self): """Return all tags in the database.""" abstract()
[docs] def get(self, image): """Return tags of *image*.""" abstract()
[docs] def query(self, tags): """Return all images which satisfy all constraining *tags*.""" abstract()
[docs] def remove(self, images, tags): """Remove all of *tags* from all of *images*.""" abstract()
[docs] def set(self, images, tags): """Set tags of *images* to be exactly *tags*.""" abstract()
[docs] def add(self, images, tags): """Add all *tags* to each of *images*.""" abstract()
[docs] def remove_all(self, images): """Remove all tags from each of *images*.""" abstract()
[docs] def cleanup(self): """Search and remove tags without images.""" abstract()
[docs]class Tags_SQLite(object): """Tag operations based on a SQLite database. The database link is to be passed as *conn*. Database changes are not saved. It's assumed that the basic scheme is present (i.e. image table). """ def __init__(self, conn): self.conn = conn self.query_sort = "image.t_image_create ASC" # FIXME: Not really a feature yet #self.query_sort = "image.id ASC" #self.query_sort = "image.t_db_update ASC" #self.query_sort = "image.path ASC" ########################################################################### #### GETTERS #### ########################################################################### def get_all(self): return map(fst, self.conn.execute("SELECT tag FROM tag").fetchall()) def get(self, image): results = self.conn.execute("SELECT tag.tag FROM tag JOIN img_tag ON img_tag.tag = tag.id JOIN image ON img_tag.image = image.id WHERE image.path = ?", (image, )) return map(fst, results.fetchall())
[docs] def query(self, tags): """Return all images which satisfy all constraining *tags*. .. TODO:: There's a SQLite limit for joins, usually 64. Meaning we're limited to 31 search tags. This seems ok for the moment but one might want to consider ways out of this. """ joins, conds, binds = [], [], [] for i in range(len(tags)): joins.append("img_tag AS img_tag_{0} ON img_tag_{0}.image = image.id".format(i)) joins.append("tag AS tag_{0} ON img_tag_{0}.tag = tag_{0}.id".format(i)) conds.append("tag_{0}.tag LIKE ?".format(i)) # LIKE operator is case insensitive binds.append(tags[i].lower()) return joins, conds, binds ########################################################################### #### TAGS METRICS #### ###########################################################################
def histogram(self, include=None, exclude=None): if include is None: include = [] if exclude is None: exclude = [] query = "SELECT tag.tag, count(img_tag.tag) FROM img_tag" query += " JOIN tag ON tag.id = img_tag.tag" if len(include) + len(exclude) > 0: query += " JOIN image ON image.id = img_tag.image" query += " WHERE " if len(include) > 0: query += ' image.path IN (' + ','.join('?' * len(include)) + ')' if len(exclude) > 0: query += len(include) > 0 and ' AND ' or '' query += ' image.path NOT IN (' + ','.join('?' * len(exclude)) + ')' query += ' GROUP BY img_tag.tag' return dict(self.conn.execute(query, include + exclude).fetchall()) ########################################################################### #### TAGS OPERATIONS #### ########################################################################### def remove(self, images, tags): if len(tags) == 0 or len(images) == 0: return # Remove references args = itertools.product(tags, images) self.conn.executemany("DELETE FROM img_tag WHERE tag = (SELECT id FROM tag WHERE tag = ?) AND image = (SELECT id FROM image WHERE path = ?)", args) # Cleanup for tag in tags: self._remove_orphan_tag(tag) def set(self, images, tags): for img in images: # Create the diff current = self.get(img) added = filter(lambda itm: itm not in current, tags) removed = filter(lambda itm: itm not in tags, current) # Add/remove acc. to diff self.add([img], added) self.remove([img], removed)
[docs] def add(self, images, tags): """Add all *tags* to each of *images*. *images* is a list of images (path). If an image is not in the database, it is ignored. *tags* is a list of strings. If a tag is not in the database, it is added. """ if len(images) == 0 or len(tags) == 0: return # Get ids of *images* img_ids = [self.conn.execute("SELECT id FROM image WHERE path = ?", (img, )).fetchone() for img in images] img_ids = filter(lambda id_: id_ is not None, img_ids) img_ids = map(fst, img_ids) if len(img_ids) == 0: # No valid images given # NOTE: Error management return # Add *tags* to database # FIXME: Find better solution (e.g. use INSERT OR ....) tag_ids = [] for keyword in tags: result = self.conn.execute("SELECT id FROM tag WHERE tag = ?", (keyword, )).fetchone() if result is None: tag_ids.append(self.conn.execute("INSERT INTO tag (tag) VALUES (?)", (keyword, )).lastrowid) else: tag_ids.append(result[0]) # Attach tags to images references = itertools.product(img_ids, tag_ids) self.conn.executemany("INSERT OR IGNORE INTO img_tag (image, tag) VALUES (?, ?)", references)
def remove_all(self, images): tags = [] for img in images: tags += self.conn.execute('SELECT tag.tag FROM tag JOIN img_tag on img_tag.tag = tag.id JOIN image on img_tag.image = image.id WHERE image.path = ?', (img, )).fetchall() self.conn.execute('DELETE FROM img_tag WHERE image = (SELECT id FROM image WHERE path = ?)', (img, )) # Cleanup tags = unique(map(fst, tags)) for tag in tags: self._remove_orphan_tag(tag)
[docs] def cleanup(self): """Search and remove tags without images. This operation may take some time. """ orphans = self.conn.execute("SELECT id FROM tag WHERE id NOT IN (SELECT tag FROM img_tag GROUP BY tag)").fetchall() orphans = map(fst, orphans) for t_id in orphans: self.conn.execute("DELETE FROM tag WHERE id = ?", (t_id, ))
[docs] def rename(self, source, target): """Rename a tag from *source* to *target*. *source* and *target* are the tag names. If *target* exists, the tags will be merged. """ # Check source q_src = self.conn.execute("SELECT id FROM tag WHERE tag = ?", (source, )).fetchall() if len(q_src) == 0: return # Source doesn't exist, no further action required src_id = fst(q_src[0]) # Check target q_trg = self.conn.execute("SELECT id FROM tag WHERE tag = ?", (target, )).fetchall() if len(q_trg) == 0: # Target doesn't exist, rename self.conn.execute("UPDATE tag SET tag = ? WHERE id = ?", (target, src_id)) else: # Target does exist, move trg_id = fst(q_trg[0]) self.conn.execute("UPDATE OR REPLACE img_tag SET tag = ? WHERE tag = ?", (trg_id, src_id)) self.conn.execute("DELETE FROM tag WHERE id = ?", (src_id, )) ########################################################################### #### INTERNALS #### ###########################################################################
def _remove_orphan_tag(self, tag): """Remove a tag if no image refers to it.""" num_references = self.conn.execute("SELECT COUNT(*) FROM img_tag JOIN tag ON img_tag.tag = tag.id WHERE tag.tag = ?", (tag, )).fetchone() if fst(num_references) == 0: self.conn.execute("DELETE FROM tag WHERE id = (SELECT id FROM tag WHERE tag = ?)", (tag, ))
[docs]class Tags_Exif(Tags): """Tags-like interface to EXIF/IPTC metadata. A *meta_adapter* is required to do the IPTC operations. Some operations are not meaningful and return dummy values. """ def __init__(self, adapter): self.meta_adapter = adapter def get_all(self): return [] def get(self, image): return self.meta_adapter.get(image) def query(self, tags): return [] def remove(self, images, tags): for img in images: current = self.meta_adapter.get(img) new = filter(lambda t: t not in tags, current) self.meta_adapter.set(img, new) def set(self, images, tags): for img in images: self.meta_adapter.set(img, tags) def add(self, images, tags): for img in images: self.add(img, tags) def remove_all(self, images): for img in images: self.meta_adapter.set(img, []) def cleanup(self): pass ## EOF ##