added query string parser; library.get

--HG--
extra : convert_revision : svn%3A41726ec3-264d-0410-9c23-a9f1637257cc/trunk%4011
This commit is contained in:
adrian.sampson 2008-06-11 07:05:37 +00:00
parent 932f839bca
commit 5daba34d10
2 changed files with 96 additions and 9 deletions

View file

@ -1,4 +1,4 @@
import sqlite3, os, sys, operator
import sqlite3, os, sys, operator, re
from beets.tag import MediaFile, FileTypeError
from string import Template
@ -187,6 +187,7 @@ class Item(object):
class QueryElement(object):
"""A building block for library queries."""
def clause(self):
@ -198,19 +199,36 @@ class QueryElement(object):
class SubstringQueryElement(QueryElement):
"""A query element that matches a substring in a specific item field."""
def __init__(self, field, value):
def __init__(self, field, pattern):
if field not in item_keys:
raise InvalidFieldError(field + ' is not an item key')
self.field = field
self.value = value
self.pattern = pattern
def clause(self):
search = '%' + (self.value.replace('\\','\\\\').replace('%','\\%')
search = '%' + (self.pattern.replace('\\','\\\\').replace('%','\\%')
.replace('_','\\_')) + '%'
clause = self.field + " like ? escape '\\'"
subvals = [search]
return (clause, subvals)
class AnySubstringQueryElement(QueryElement):
"""A query element that matches a substring in any item field."""
def __init__(self, pattern):
self.pattern = pattern
def clause(self):
clause_parts = []
subvals = []
for field in item_keys:
el_clause, el_subvals = (SubstringQueryElement(field, self.pattern)
.clause())
clause_parts.append('(' + el_clause + ')')
subvals += el_subvals
clause = ' or '.join(clause_parts)
return clause, subvals
class AndQueryElement(QueryElement):
"""A conjunction of a list of other query elements. Can be indexed like a
list to access the sub-elements."""
@ -237,7 +255,7 @@ class AndQueryElement(QueryElement):
clause_parts.append('(' + el_clause + ')')
subvals += el_subvals
clause = ' and '.join(clause_parts)
return (clause, subvals)
return clause, subvals
@classmethod
def from_dict(cls, matches):
@ -248,6 +266,56 @@ class AndQueryElement(QueryElement):
elements.append(SubstringQueryElement(key, pattern))
return cls(elements)
# regular expression for _parse_query, below
_pq_regex = re.compile(r'(?:^|(?<=\s))' # zero-width match for whitespace or
# beginning of string
# non-grouping optional segment for the keyword
r'(?:'
r'(\S+?)' # the keyword
r'(?<!\\):' # unescaped :
r')?'
r'(\S+)', # the term itself, greedily consumed
re.I) # case-insensitive
@classmethod
def _parse_query(cls, query_string):
"""Takes a query in the form of a whitespace-separated list of search
terms that may be preceded with a key followed by a colon. Returns a
list of pairs (key, term) where key is None if the search term has no
key.
For instance,
parse_query('stapler color:red') ==
[(None, 'stapler'), ('color', 'red')]
Colons may be 'escaped' with a backslash to disable the keying
behavior.
"""
out = []
for match in cls._pq_regex.finditer(query_string):
out.append((match.group(1), match.group(2).replace(r'\:',':')))
return out
@classmethod
def from_string(cls, query_string):
"""Creates a query from a string in the format used by _parse_query."""
elements = []
for key, pattern in cls._parse_query(query_string):
if key is None: # no key specified; match any field
elements.append(AnySubstringQueryElement(pattern))
elif key.lower() in item_keys: # ignore unrecognized keys
elements.append(SubstringQueryElement(key.lower(), pattern))
if not elements: # no terms in query
elements = [TrueQueryElement()]
return cls(elements)
class TrueQueryElement(QueryElement):
"""A query element that always matches."""
def clause(self):
return '1', ()
class Query(AndQueryElement):
"""A query into the item database."""
@ -263,10 +331,10 @@ class Query(AndQueryElement):
ItemResultIterator."""
cursor = library.conn.cursor()
cursor.execute(*self.statement())
return ItemResultIterator(cursor)
return ResultIterator(cursor)
class ItemResultIterator(object):
"""An iterator into an item result set."""
class ResultIterator(object):
"""An iterator into an item query result set."""
def __init__(self, cursor):
self.cursor = cursor
@ -287,6 +355,7 @@ class ItemResultIterator(object):
class Library(object):
def __init__(self, path='library.blb'):
self.path = path
@ -355,6 +424,17 @@ class Library(object):
else: # something else: special file?
self.__log(path + ' special file, skipping')
def get(self, query):
"""Returns a ResultIterator to the items matching query, which may be
None (match the entire library), a Query object, or a query string."""
if query is None:
query = Query([TrueQueryElement()])
elif isinstance(query, str) or isinstance(query, unicode):
query = Query.from_string(query)
elif not isinstance(query, Query):
raise ValueError('query must be None or have type Query or str')
return query.execute(self)
def save(self):
"""Writes the library to disk (completing a sqlite transaction)."""
self.conn.commit()

9
bts.py
View file

@ -7,6 +7,13 @@ def add(lib, paths):
lib.add(path)
lib.save()
def ls(lib, criteria):
q = ' '.join(criteria)
if not q.strip():
q = None # no criteria => match anything
for item in lib.get(q):
print item.artist + ' - ' + item.album + ' - ' + item.title
if __name__ == "__main__":
# parse options
usage = """usage: %prog [options] command
@ -34,7 +41,7 @@ command is one of: add, remove, update, write, list, help"""
#(remove, ['remove', 'rm']),
#(update, ['update', 'up']),
#(write, ['write', 'wr', 'w']),
#(list, ['list', 'ls']),
(ls, ['list', 'ls']),
(help, ['help', 'h'])
]
for test_command in avail_commands: