create exceptions.py and move more constants

This commit is contained in:
voussoir 2016-12-12 19:49:36 -08:00
parent 2b34854910
commit 1ecd1f979e
8 changed files with 343 additions and 300 deletions

21
README.md Normal file
View file

@ -0,0 +1,21 @@
Etiquette
=========
This is the readme file.
### Changelog
- **[addition]** A new feature was added.
- **[bugfix]** Incorrect behavior was fixed.
- **[change]** An existing feature was slightly modified or parameters were renamed.
- **[cleanup]** Code was improved, comments were added, or other changes with minor impact on the interface.
- **[removal]** An old feature was removed.
 
- 2016 11 28
- **[addition]** Added `etiquette_upgrader.py`. When an update causes the anatomy of the etiquette database to change, I will increment the `phototagger.DATABASE_VERSION` variable, and add a new function to this script that should automatically make all the necessary changes. Until the database is upgraded, phototagger will not start. Don't forget to make backups just in case.
- 2016 11 05
- **[addition]** Added the ability to download an album as a `.tar` file. No compression is used. I still need to do more experiments to make sure this is working perfectly.

View file

@ -1,5 +1,18 @@
import string import string
ALLOWED_ORDERBY_COLUMNS = [
'extension',
'width',
'height',
'ratio',
'area',
'duration',
'bytes',
'created',
'tagged_at',
'random',
]
# Errors and warnings # Errors and warnings
ERROR_DATABASE_OUTOFDATE = 'Database is out-of-date. {current} should be {new}. Please use etiquette_upgrader.py' ERROR_DATABASE_OUTOFDATE = 'Database is out-of-date. {current} should be {new}. Please use etiquette_upgrader.py'
ERROR_INVALID_ACTION = 'Invalid action' ERROR_INVALID_ACTION = 'Invalid action'
@ -21,7 +34,7 @@ VALID_TAG_CHARS = string.ascii_lowercase + string.digits + '_'
DEFAULT_ID_LENGTH = 12 DEFAULT_ID_LENGTH = 12
DEFAULT_DBNAME = 'phototagger.db' DEFAULT_DBNAME = 'phototagger.db'
DEFAULT_THUMBDIR = '_etiquette\\site_thumbnails' DEFAULT_DATADIR = '.\\_etiquette'
DEFAULT_DIGEST_EXCLUDE_FILES = [ DEFAULT_DIGEST_EXCLUDE_FILES = [
DEFAULT_DBNAME, DEFAULT_DBNAME,
'desktop.ini', 'desktop.ini',

View file

@ -12,20 +12,15 @@ import warnings
import constants import constants
import decorators import decorators
import exceptions
import helpers import helpers
import jsonify import jsonify
import phototagger import phototagger
try: # pip install
sys.path.append('C:\\git\\else\\Bytestring') # https://raw.githubusercontent.com/voussoir/else/master/_voussoirkit/voussoirkit.zip
sys.path.append('C:\\git\\else\\WebstreamZip') from voussoirkit import bytestring
import bytestring from voussoirkit import webstreamzip
import webstreamzip
except ImportError:
# pip install
# https://raw.githubusercontent.com/voussoir/else/master/_voussoirkit/voussoirkit.zip
from vousoirkit import bytestring
from vousoirkit import webstreamzip
site = flask.Flask(__name__) site = flask.Flask(__name__)
site.config.update( site.config.update(
@ -61,7 +56,7 @@ def delete_synonym(synonym):
synonym = phototagger.normalize_tagname(synonym) synonym = phototagger.normalize_tagname(synonym)
try: try:
master_tag = P.get_tag(synonym) master_tag = P.get_tag(synonym)
except phototagger.NoSuchTag: except exceptions.NoSuchTag:
flask.abort(404, 'That synonym doesnt exist') flask.abort(404, 'That synonym doesnt exist')
if synonym not in master_tag.synonyms(): if synonym not in master_tag.synonyms():
@ -79,19 +74,19 @@ def make_json_response(j, *args, **kwargs):
def P_album(albumid): def P_album(albumid):
try: try:
return P.get_album(albumid) return P.get_album(albumid)
except phototagger.NoSuchAlbum: except exceptions.NoSuchAlbum:
flask.abort(404, 'That album doesnt exist') flask.abort(404, 'That album doesnt exist')
def P_photo(photoid): def P_photo(photoid):
try: try:
return P.get_photo(photoid) return P.get_photo(photoid)
except phototagger.NoSuchPhoto: except exceptions.NoSuchPhoto:
flask.abort(404, 'That photo doesnt exist') flask.abort(404, 'That photo doesnt exist')
def P_tag(tagname): def P_tag(tagname):
try: try:
return P.get_tag(tagname) return P.get_tag(tagname)
except phototagger.NoSuchTag as e: except exceptions.NoSuchTag as e:
flask.abort(404, 'That tag doesnt exist: %s' % e) flask.abort(404, 'That tag doesnt exist: %s' % e)
def send_file(filepath): def send_file(filepath):
@ -465,7 +460,7 @@ def get_static(filename):
def get_tags_core(specific_tag=None): def get_tags_core(specific_tag=None):
try: try:
tags = P.export_tags(phototagger.tag_export_easybake, specific_tag=specific_tag) tags = P.export_tags(phototagger.tag_export_easybake, specific_tag=specific_tag)
except phototagger.NoSuchTag: except exceptions.NoSuchTag:
flask.abort(404, 'That tag doesnt exist') flask.abort(404, 'That tag doesnt exist')
tags = tags.split('\n') tags = tags.split('\n')
tags = [t for t in tags if t != ''] tags = [t for t in tags if t != '']
@ -516,7 +511,7 @@ def post_edit_album(albumid):
tag = request.form[action].strip() tag = request.form[action].strip()
try: try:
tag = P_tag(tag) tag = P_tag(tag)
except phototagger.NoSuchTag: except exceptions.NoSuchTag:
response = {'error': 'That tag doesnt exist', 'tagname': tag} response = {'error': 'That tag doesnt exist', 'tagname': tag}
return make_json_response(response, status=404) return make_json_response(response, status=404)
recursive = request.form.get('recursive', False) recursive = request.form.get('recursive', False)
@ -552,7 +547,7 @@ def post_edit_photo(photoid):
try: try:
tag = P.get_tag(tag) tag = P.get_tag(tag)
except phototagger.NoSuchTag: except exceptions.NoSuchTag:
response = {'error': 'That tag doesnt exist', 'tagname': tag} response = {'error': 'That tag doesnt exist', 'tagname': tag}
return make_json_response(response, status=404) return make_json_response(response, status=404)
@ -595,11 +590,11 @@ def post_edit_tags():
status = 400 status = 400
try: try:
response = method(tag) response = method(tag)
except phototagger.TagTooShort: except exceptions.TagTooShort:
response = {'error': constants.ERROR_TAG_TOO_SHORT, 'tagname': tag} response = {'error': constants.ERROR_TAG_TOO_SHORT, 'tagname': tag}
except phototagger.CantSynonymSelf: except exceptions.CantSynonymSelf:
response = {'error': constants.ERROR_SYNONYM_ITSELF, 'tagname': tag} response = {'error': constants.ERROR_SYNONYM_ITSELF, 'tagname': tag}
except phototagger.NoSuchTag as e: except exceptions.NoSuchTag as e:
response = {'error': constants.ERROR_NO_SUCH_TAG, 'tagname': tag} response = {'error': constants.ERROR_NO_SUCH_TAG, 'tagname': tag}
except ValueError as e: except ValueError as e:
response = {'error': e.args[0], 'tagname': tag} response = {'error': e.args[0], 'tagname': tag}

View file

@ -25,5 +25,5 @@ else:
) )
print('Starting server') print('Starting server on port %d' % port)
http.serve_forever() http.serve_forever()

46
exceptions.py Normal file
View file

@ -0,0 +1,46 @@
class CantSynonymSelf(Exception):
pass
class NoSuchAlbum(Exception):
pass
class NoSuchGroup(Exception):
pass
class NoSuchPhoto(Exception):
pass
class NoSuchSynonym(Exception):
pass
class NoSuchTag(Exception):
pass
class PhotoExists(Exception):
pass
class TagExists(Exception):
pass
class GroupExists(Exception):
pass
class TagTooLong(Exception):
pass
class TagTooShort(Exception):
pass
class NotExclusive(Exception):
'''
For when two or more mutually exclusive actions have been requested.
'''
pass
class OutOfOrder(Exception):
'''
For when a requested range (a, b) has b > a
'''
pass

View file

@ -1,6 +1,10 @@
import math import math
import mimetypes
import os
import exceptions
import constants import constants
import warnings
def chunk_sequence(sequence, chunk_length, allow_incomplete=True): def chunk_sequence(sequence, chunk_length, allow_incomplete=True):
''' '''
@ -67,6 +71,42 @@ def fit_into_bounds(image_width, image_height, frame_width, frame_height):
return (new_width, new_height) return (new_width, new_height)
def get_mimetype(filepath):
extension = os.path.splitext(filepath)[1].replace('.', '')
if extension in constants.ADDITIONAL_MIMETYPES:
return constants.ADDITIONAL_MIMETYPES[extension]
mimetype = mimetypes.guess_type(filepath)[0]
if mimetype is not None:
mimetype = mimetype.split('/')[0]
return mimetype
def hyphen_range(s):
'''
Given a string like '1-3', return ints (1, 3) representing lower
and upper bounds.
Supports bytestring.parsebytes and hh:mm:ss format.
'''
s = s.strip()
s = s.replace(' ', '')
if not s:
return (None, None)
parts = s.split('-')
parts = [part.strip() or None for part in parts]
if len(parts) == 1:
low = parts[0]
high = None
elif len(parts) == 2:
(low, high) = parts
else:
raise ValueError('Too many hyphens')
low = _unitconvert(low)
high = _unitconvert(high)
if low is not None and high is not None and low > high:
raise exceptions.OutOfOrder(s, low, high)
return low, high
def hms_to_seconds(hms): def hms_to_seconds(hms):
''' '''
Convert hh:mm:ss string to an integer seconds. Convert hh:mm:ss string to an integer seconds.
@ -133,3 +173,114 @@ def truthystring(s):
if s in {'null', 'none'}: if s in {'null', 'none'}:
return None return None
return False return False
#===============================================================================
def _minmax(key, value, minimums, maximums):
'''
When searching, this function dissects a hyphenated range string
and inserts the correct k:v pair into both minimums and maximums.
('area', '100-200', {}, {}) --> {'area': 100}, {'area': 200} (MODIFIED IN PLACE)
'''
if value is None:
return
if isinstance(value, (int, float)):
minimums[key] = value
return
try:
(low, high) = hyphen_range(value)
except ValueError:
warnings.warn(constants.WARNING_MINMAX_INVALID.format(field=key, value=value))
return
except exceptions.OutOfOrder as e:
warnings.warn(constants.WARNING_MINMAX_OOO.format(field=key, min=e.args[1], max=e.args[2]))
return
if low is not None:
minimums[key] = low
if high is not None:
maximums[key] = high
def _normalize_extensions(extensions):
'''
When searching, this function normalizes the list of inputted extensions.
'''
if isinstance(extensions, str):
extensions = extensions.split()
if extensions is None:
return set()
extensions = [e.lower().strip('.').strip() for e in extensions]
extensions = set(e for e in extensions if e)
return extensions
def _orderby(orderby):
'''
When searching, this function ensures that the user has entered a valid orderby
query, and normalizes the query text.
'random asc' --> ('random', 'asc')
'area' --> ('area', 'desc')
'''
orderby = orderby.lower().strip()
if orderby == '':
return None
orderby = orderby.split(' ')
if len(orderby) == 2:
(column, sorter) = orderby
elif len(orderby) == 1:
column = orderby[0]
sorter = 'desc'
else:
return None
#print(column, sorter)
if column not in constants.ALLOWED_ORDERBY_COLUMNS:
warnings.warn(constants.WARNING_ORDERBY_BADCOL.format(column=column))
return None
if column == 'random':
column = 'RANDOM()'
if sorter not in ['desc', 'asc']:
warnings.warn(constants.WARNING_ORDERBY_BADSORTER.format(column=column, sorter=sorter))
sorter = 'desc'
return (column, sorter)
def _setify_tags(photodb, tags, warn_bad_tags=False):
'''
When searching, this function converts the list of tag strings that the user
requested into Tag objects. If a tag doesn't exist we'll either raise an exception
or just issue a warning.
'''
if tags is None:
return set()
tagset = set()
for tag in tags:
tag = tag.strip()
if tag == '':
continue
try:
tag = photodb.get_tag(tag)
tagset.add(tag)
except NoSuchTag:
if warn_bad_tags:
warnings.warn(constants.WARNING_NO_SUCH_TAG.format(tag=tag))
continue
else:
raise
return tagset
def _unitconvert(value):
'''
When parsing hyphenated ranges, this function is used to convert
strings like "1k" to 1024 and "1:00" to 60.
'''
if value is None:
return None
if ':' in value:
return helpers.hms_to_seconds(value)
elif all(c in '0123456789.' for c in value):
return float(value)
else:
return bytestring.parsebytes(value)

View file

@ -17,21 +17,14 @@ import warnings
import constants import constants
import decorators import decorators
import exceptions
import helpers import helpers
try: # pip install
sys.path.append('C:\\git\\else\\Bytestring') # https://raw.githubusercontent.com/voussoir/else/master/_voussoirkit/voussoirkit.zip
sys.path.append('C:\\git\\else\\Pathclass') from voussoirkit import bytestring
sys.path.append('C:\\git\\else\\SpinalTap') from voussoirkit import pathclass
import bytestring from voussoirkit import spinal
import pathclass
import spinal
except ImportError:
# pip install
# https://raw.githubusercontent.com/voussoir/else/master/_voussoirkit/voussoirkit.zip
from voussoirkit import bytestring
from voussoirkit import pathclass
from voussoirkit import spinal
try: try:
ffmpeg = converter.Converter( ffmpeg = converter.Converter(
@ -185,164 +178,11 @@ CREATE INDEX IF NOT EXISTS index_grouprel_parentid on tag_group_rel(parentid);
CREATE INDEX IF NOT EXISTS index_grouprel_memberid on tag_group_rel(memberid); CREATE INDEX IF NOT EXISTS index_grouprel_memberid on tag_group_rel(memberid);
'''.format(user_version=DATABASE_VERSION) '''.format(user_version=DATABASE_VERSION)
ALLOWED_ORDERBY_COLUMNS = [
'extension',
'width',
'height',
'ratio',
'area',
'duration',
'bytes',
'created',
'tagged_at',
'random',
]
def _helper_extension(ext):
'''
When searching, this function normalizes the list of permissible extensions.
'''
if isinstance(ext, str):
ext = [ext]
if ext is None:
return set()
ext = [e.lower().strip('.') for e in ext]
ext = [e for e in ext if e]
ext = set(ext)
return ext
def _helper_filenamefilter(subject, terms): def _helper_filenamefilter(subject, terms):
basename = subject.lower() basename = subject.lower()
return all(term in basename for term in terms) return all(term in basename for term in terms)
def _helper_minmax(key, value, minimums, maximums):
'''
When searching, this function dissects a hyphenated range string
and inserts the correct k:v pair into both minimums and maximums.
'''
if value is None:
return
if isinstance(value, (int, float)):
minimums[key] = value
return
try:
(low, high) = hyphen_range(value)
except ValueError:
warnings.warn(constants.WARNING_MINMAX_INVALID.format(field=key, value=value))
return
except OutOfOrder as e:
warnings.warn(constants.WARNING_MINMAX_OOO.format(field=key, min=e.args[1], max=e.args[2]))
return
if low is not None:
minimums[key] = low
if high is not None:
maximums[key] = high
def _helper_orderby(orderby):
'''
When searching, this function ensures that the user has entered a valid orderby
query, and normalizes the query text.
'''
orderby = orderby.lower().strip()
if orderby == '':
return None
orderby = orderby.split(' ')
if len(orderby) == 2:
(column, sorter) = orderby
elif len(orderby) == 1:
column = orderby[0]
sorter = 'desc'
else:
return None
#print(column, sorter)
if column not in ALLOWED_ORDERBY_COLUMNS:
warnings.warn(constants.WARNING_ORDERBY_BADCOL.format(column=column))
return None
if column == 'random':
column = 'RANDOM()'
if sorter not in ['desc', 'asc']:
warnings.warn(constants.WARNING_ORDERBY_BADSORTER.format(column=column, sorter=sorter))
sorter = 'desc'
return (column, sorter)
def _helper_setify(photodb, l, warn_bad_tags=False):
'''
When searching, this function converts the list of tag strings that the user
requested into Tag objects. If a tag doesn't exist we'll either raise an exception
or just issue a warning.
'''
if l is None:
return set()
s = set()
for tag in l:
tag = tag.strip()
if tag == '':
continue
try:
tag = photodb.get_tag(tag)
except NoSuchTag:
if not warn_bad_tags:
raise
warnings.warn(constants.WARNING_NO_SUCH_TAG.format(tag=tag))
continue
else:
s.add(tag)
return s
def _helper_unitconvert(value):
'''
When parsing hyphenated ranges, this function is used to convert
strings like "1k" to 1024 and "1:00" to 60.
'''
if value is None:
return None
if ':' in value:
return helpers.hms_to_seconds(value)
elif all(c in '0123456789.' for c in value):
return float(value)
else:
return bytestring.parsebytes(value)
def hyphen_range(s):
'''
Given a string like '1-3', return ints (1, 3) representing lower
and upper bounds.
Supports bytestring.parsebytes and hh:mm:ss format.
'''
s = s.strip()
s = s.replace(' ', '')
if not s:
return (None, None)
parts = s.split('-')
parts = [part.strip() or None for part in parts]
if len(parts) == 1:
low = parts[0]
high = None
elif len(parts) == 2:
(low, high) = parts
else:
raise ValueError('Too many hyphens')
low = _helper_unitconvert(low)
high = _helper_unitconvert(high)
if low is not None and high is not None and low > high:
raise OutOfOrder(s, low, high)
return low, high
def get_mimetype(filepath):
extension = os.path.splitext(filepath)[1].replace('.', '')
if extension in constants.ADDITIONAL_MIMETYPES:
return constants.ADDITIONAL_MIMETYPES[extension]
mimetype = mimetypes.guess_type(filepath)[0]
if mimetype is not None:
mimetype = mimetype.split('/')[0]
return mimetype
def getnow(timestamp=True): def getnow(timestamp=True):
''' '''
Return the current UTC timestamp or datetime object. Return the current UTC timestamp or datetime object.
@ -376,9 +216,9 @@ def normalize_tagname(tagname):
tagname = ''.join(tagname) tagname = ''.join(tagname)
if len(tagname) < constants.MIN_TAG_NAME_LENGTH: if len(tagname) < constants.MIN_TAG_NAME_LENGTH:
raise TagTooShort(tagname) raise exceptions.TagTooShort(tagname)
if len(tagname) > constants.MAX_TAG_NAME_LENGTH: if len(tagname) > constants.MAX_TAG_NAME_LENGTH:
raise TagTooLong(tagname) raise exceptions.TagTooLong(tagname)
return tagname return tagname
@ -437,9 +277,9 @@ def searchfilter_expression(photo_tags, expression, frozen_children, warn_bad_ta
value = any(option in photo_tags for option in frozen_children[token]) value = any(option in photo_tags for option in frozen_children[token])
except KeyError: except KeyError:
if warn_bad_tags: if warn_bad_tags:
warnings.warn(constants.NO_SUCH_TAG.format(tag=token)) warnings.warn(constants.WARNING_NO_SUCH_TAG.format(tag=token))
else: else:
raise NoSuchTag(token) raise exceptions.NoSuchTag(token)
return False return False
operand_stack.append(value) operand_stack.append(value)
if has_operand: if has_operand:
@ -573,50 +413,6 @@ def tag_export_totally_flat(tags):
result[synonym] = children result[synonym] = children
return result return result
####################################################################################################
####################################################################################################
class CantSynonymSelf(Exception):
pass
class NoSuchAlbum(Exception):
pass
class NoSuchGroup(Exception):
pass
class NoSuchPhoto(Exception):
pass
class NoSuchSynonym(Exception):
pass
class NoSuchTag(Exception):
pass
class PhotoExists(Exception):
pass
class TagExists(Exception):
pass
class GroupExists(Exception):
pass
class TagTooLong(Exception):
pass
class TagTooShort(Exception):
pass
class XORException(Exception):
pass
class OutOfOrder(Exception):
pass
#################################################################################################### ####################################################################################################
#################################################################################################### ####################################################################################################
@ -632,15 +428,16 @@ class PDBAlbumMixin:
''' '''
filepath = os.path.abspath(filepath) filepath = os.path.abspath(filepath)
self.cur.execute('SELECT * FROM albums WHERE associated_directory == ?', [filepath]) self.cur.execute('SELECT * FROM albums WHERE associated_directory == ?', [filepath])
f = self.cur.fetchone() fetch = self.cur.fetchone()
if f is None: if fetch is None:
raise NoSuchAlbum(filepath) raise exceptions.NoSuchAlbum(filepath)
return self.get_album(f[SQL_ALBUM['id']]) return self.get_album(fetch[SQL_ALBUM['id']])
def get_albums(self): def get_albums(self):
yield from self.get_things(thing_type='album') yield from self.get_things(thing_type='album')
def new_album(self, def new_album(
self,
associated_directory=None, associated_directory=None,
commit=True, commit=True,
description=None, description=None,
@ -691,7 +488,7 @@ class PDBPhotoMixin:
self.cur.execute('SELECT * FROM photos WHERE filepath == ?', [filepath]) self.cur.execute('SELECT * FROM photos WHERE filepath == ?', [filepath])
fetch = self.cur.fetchone() fetch = self.cur.fetchone()
if fetch is None: if fetch is None:
raise_no_such_thing(NoSuchPhoto, thing_name=filepath) raise_no_such_thing(exceptions.NoSuchPhoto, thing_name=filepath)
photo = Photo(self, fetch) photo = Photo(self, fetch)
return photo return photo
@ -706,10 +503,10 @@ class PDBPhotoMixin:
temp_cur = self.sql.cursor() temp_cur = self.sql.cursor()
temp_cur.execute('SELECT * FROM photos ORDER BY created DESC') temp_cur.execute('SELECT * FROM photos ORDER BY created DESC')
while True: while True:
f = temp_cur.fetchone() fetch = temp_cur.fetchone()
if f is None: if fetch is None:
break break
photo = Photo(self, f) photo = Photo(self, fetch)
yield photo yield photo
@ -750,7 +547,7 @@ class PDBPhotoMixin:
database. Tags may be applied now or later. database. Tags may be applied now or later.
If `allow_duplicates` is False, we will first check the database for any files If `allow_duplicates` is False, we will first check the database for any files
with the same path and raise PhotoExists if found. with the same path and raise exceptions.PhotoExists if found.
Returns the Photo object. Returns the Photo object.
''' '''
@ -759,10 +556,10 @@ class PDBPhotoMixin:
if not allow_duplicates: if not allow_duplicates:
try: try:
existing = self.get_photo_by_path(filename) existing = self.get_photo_by_path(filename)
except NoSuchPhoto: except exceptions.NoSuchPhoto:
pass pass
else: else:
exc = PhotoExists(filename, existing) exc = exceptions.PhotoExists(filename, existing)
exc.photo = existing exc.photo = existing
raise exc raise exc
@ -874,7 +671,7 @@ class PDBPhotoMixin:
QUERY OPTIONS QUERY OPTIONS
warn_bad_tags: warn_bad_tags:
If a tag is not found, issue a warning but continue the search. If a tag is not found, issue a warning but continue the search.
Otherwise, a NoSuchTag exception would be raised. Otherwise, a exceptions.NoSuchTag exception would be raised.
limit: limit:
The maximum number of *successful* results to yield. The maximum number of *successful* results to yield.
@ -890,18 +687,18 @@ class PDBPhotoMixin:
start_time = time.time() start_time = time.time()
maximums = {} maximums = {}
minimums = {} minimums = {}
_helper_minmax('area', area, minimums, maximums) helpers._minmax('area', area, minimums, maximums)
_helper_minmax('created', created, minimums, maximums) helpers._minmax('created', created, minimums, maximums)
_helper_minmax('width', width, minimums, maximums) helpers._minmax('width', width, minimums, maximums)
_helper_minmax('height', height, minimums, maximums) helpers._minmax('height', height, minimums, maximums)
_helper_minmax('ratio', ratio, minimums, maximums) helpers._minmax('ratio', ratio, minimums, maximums)
_helper_minmax('bytes', bytes, minimums, maximums) helpers._minmax('bytes', bytes, minimums, maximums)
_helper_minmax('duration', duration, minimums, maximums) helpers._minmax('duration', duration, minimums, maximums)
orderby = orderby or [] orderby = orderby or []
extension = _helper_extension(extension) extension = helpers._normalize_extensions(extension)
extension_not = _helper_extension(extension_not) extension_not = helpers._normalize_extensions(extension_not)
mimetype = _helper_extension(mimetype) mimetype = helpers._normalize_extensions(mimetype)
if filename is not None: if filename is not None:
if not isinstance(filename, str): if not isinstance(filename, str):
@ -909,14 +706,14 @@ class PDBPhotoMixin:
filename = set(term.lower() for term in filename.strip().split(' ')) filename = set(term.lower() for term in filename.strip().split(' '))
if (tag_musts or tag_mays or tag_forbids) and tag_expression: if (tag_musts or tag_mays or tag_forbids) and tag_expression:
raise XORException('Expression filter cannot be used with musts, mays, forbids') raise exceptions.NotExclusive('Expression filter cannot be used with musts, mays, forbids')
tag_musts = _helper_setify(self, tag_musts, warn_bad_tags=warn_bad_tags) tag_musts = helpers._setify_tags(photodb=self, tags=tag_musts, warn_bad_tags=warn_bad_tags)
tag_mays = _helper_setify(self, tag_mays, warn_bad_tags=warn_bad_tags) tag_mays = helpers._setify_tags(photodb=self, tags=tag_mays, warn_bad_tags=warn_bad_tags)
tag_forbids = _helper_setify(self, tag_forbids, warn_bad_tags=warn_bad_tags) tag_forbids = helpers._setify_tags(photodb=self, tags=tag_forbids, warn_bad_tags=warn_bad_tags)
query = 'SELECT * FROM photos' query = 'SELECT * FROM photos'
orderby = [_helper_orderby(o) for o in orderby] orderby = [helpers._orderby(o) for o in orderby]
orderby = [o for o in orderby if o] orderby = [o for o in orderby if o]
if orderby: if orderby:
whereable_columns = [o[0] for o in orderby if o[0] != 'RANDOM()'] whereable_columns = [o[0] for o in orderby if o[0] != 'RANDOM()']
@ -1025,14 +822,14 @@ class PDBTagMixin:
Redirect to get_tag_by_id or get_tag_by_name after xor-checking the parameters. Redirect to get_tag_by_id or get_tag_by_name after xor-checking the parameters.
''' '''
if not helpers.is_xor(id, name): if not helpers.is_xor(id, name):
raise XORException('One and only one of `id`, `name` can be passed.') raise exceptions.NotExclusive('One and only one of `id`, `name` can be passed.')
if id is not None: if id is not None:
return self.get_tag_by_id(id) return self.get_tag_by_id(id)
elif name is not None: elif name is not None:
return self.get_tag_by_name(name) return self.get_tag_by_name(name)
else: else:
raise_no_such_thing(NoSuchTag, thing_id=id, thing_name=name) raise_no_such_thing(exceptions.NoSuchTag, thing_id=id, thing_name=name)
def get_tag_by_id(self, id): def get_tag_by_id(self, id):
return self.get_thing_by_id('tag', thing_id=id) return self.get_thing_by_id('tag', thing_id=id)
@ -1055,7 +852,7 @@ class PDBTagMixin:
fetch = self.cur.fetchone() fetch = self.cur.fetchone()
if fetch is None: if fetch is None:
# was not a top tag or synonym # was not a top tag or synonym
raise_no_such_thing(NoSuchTag, thing_name=tagname) raise_no_such_thing(exceptions.NoSuchTag, thing_name=tagname)
tagname = fetch[SQL_SYN['master']] tagname = fetch[SQL_SYN['master']]
def get_tags(self): def get_tags(self):
@ -1068,10 +865,10 @@ class PDBTagMixin:
tagname = normalize_tagname(tagname) tagname = normalize_tagname(tagname)
try: try:
self.get_tag_by_name(tagname) self.get_tag_by_name(tagname)
except NoSuchTag: except exceptions.NoSuchTag:
pass pass
else: else:
raise TagExists(tagname) raise exceptions.TagExists(tagname)
tagid = self.generate_id('tags') tagid = self.generate_id('tags')
self._cached_frozen_children = None self._cached_frozen_children = None
@ -1121,10 +918,17 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
''' '''
def __init__( def __init__(
self, self,
databasename=constants.DEFAULT_DBNAME, databasename=None,
thumbnail_folder=constants.DEFAULT_THUMBDIR, data_directory=None,
id_length=constants.DEFAULT_ID_LENGTH, id_length=None,
): ):
if databasename is None:
databasename = constants.DEFAULT_DBNAME
if data_directory is None:
data_directory = constants.DEFAULT_DATADIR
if id_length is None:
id_length = constants.DEFAULT_ID_LENGTH
self.databasename = databasename self.databasename = databasename
self.database_abspath = os.path.abspath(databasename) self.database_abspath = os.path.abspath(databasename)
existing_database = os.path.exists(databasename) existing_database = os.path.exists(databasename)
@ -1143,8 +947,11 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
for statement in statements: for statement in statements:
self.cur.execute(statement) self.cur.execute(statement)
self.thumbnail_folder = os.path.abspath(thumbnail_folder)
os.makedirs(thumbnail_folder, exist_ok=True) self.data_directory = data_directory
self.thumbnail_folder = os.path.join(data_directory, 'site_thumbnails')
self.thumbnail_folder = os.path.abspath(self.thumbnail_folder)
os.makedirs(self.thumbnail_folder, exist_ok=True)
self.id_length = id_length self.id_length = id_length
@ -1189,7 +996,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
) )
try: try:
album = self.get_album_by_path(directory.absolute_path) album = self.get_album_by_path(directory.absolute_path)
except NoSuchAlbum: except exceptions.NoSuchAlbum:
album = self.new_album( album = self.new_album(
associated_directory=directory.absolute_path, associated_directory=directory.absolute_path,
commit=False, commit=False,
@ -1202,7 +1009,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
if current_album is None: if current_album is None:
try: try:
current_album = self.get_album_by_path(current_location.absolute_path) current_album = self.get_album_by_path(current_location.absolute_path)
except NoSuchAlbum: except exceptions.NoSuchAlbum:
current_album = self.new_album( current_album = self.new_album(
associated_directory=current_location.absolute_path, associated_directory=current_location.absolute_path,
commit=False, commit=False,
@ -1213,13 +1020,13 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
parent = albums[current_location.parent.absolute_path] parent = albums[current_location.parent.absolute_path]
try: try:
parent.add(current_album, commit=False) parent.add(current_album, commit=False)
except GroupExists: except exceptions.GroupExists:
pass pass
#print('Added to %s' % parent.title) #print('Added to %s' % parent.title)
for filepath in files: for filepath in files:
try: try:
photo = self.new_photo(filepath.absolute_path, commit=False) photo = self.new_photo(filepath.absolute_path, commit=False)
except PhotoExists as e: except exceptions.PhotoExists as e:
photo = e.photo photo = e.photo
current_album.add_photo(photo, commit=False) current_album.add_photo(photo, commit=False)
@ -1259,7 +1066,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
filepath = filepath.absolute_path filepath = filepath.absolute_path
try: try:
photo = self.get_photo_by_path(filepath) photo = self.get_photo_by_path(filepath)
except NoSuchPhoto: except exceptions.NoSuchPhoto:
pass pass
else: else:
continue continue
@ -1282,7 +1089,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
try: try:
item = self.get_tag(name) item = self.get_tag(name)
note = ('existing_tag', item.qualified_name()) note = ('existing_tag', item.qualified_name())
except NoSuchTag: except exceptions.NoSuchTag:
item = self.new_tag(name) item = self.new_tag(name)
note = ('new_tag', item.qualified_name()) note = ('new_tag', item.qualified_name())
output_notes.append(note) output_notes.append(note)
@ -1330,7 +1137,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
lower.join_group(higher) lower.join_group(higher)
note = ('join_group', '%s.%s' % (higher.name, lower.name)) note = ('join_group', '%s.%s' % (higher.name, lower.name))
output_notes.append(note) output_notes.append(note)
except GroupExists: except exceptions.GroupExists:
pass pass
tag = tags[-1] tag = tags[-1]
@ -1340,7 +1147,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
note = ('new_synonym', '%s+%s' % (tag.name, synonym)) note = ('new_synonym', '%s+%s' % (tag.name, synonym))
output_notes.append(note) output_notes.append(note)
print('New syn %s' % synonym) print('New syn %s' % synonym)
except TagExists: except exceptions.TagExists:
pass pass
return output_notes return output_notes
@ -1405,19 +1212,19 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
'album': 'album':
{ {
'class': Album, 'class': Album,
'exception': NoSuchAlbum, 'exception': exceptions.NoSuchAlbum,
'table': 'albums', 'table': 'albums',
}, },
'tag': 'tag':
{ {
'class': Tag, 'class': Tag,
'exception': NoSuchTag, 'exception': exceptions.NoSuchTag,
'table': 'tags', 'table': 'tags',
}, },
'photo': 'photo':
{ {
'class': Photo, 'class': Photo,
'exception': NoSuchPhoto, 'exception': exceptions.NoSuchPhoto,
'table': 'photos', 'table': 'photos',
}, },
}[thing_type] }[thing_type]
@ -1447,7 +1254,7 @@ class GroupableMixin:
''' '''
Add a Tag object to this group. Add a Tag object to this group.
If that object is already a member of another group, a GroupExists is raised. If that object is already a member of another group, a exceptions.GroupExists is raised.
''' '''
if not isinstance(member, type(self)): if not isinstance(member, type(self)):
raise TypeError('Member must be of type %s' % type(self)) raise TypeError('Member must be of type %s' % type(self))
@ -1459,7 +1266,7 @@ class GroupableMixin:
that_group = self that_group = self
else: else:
that_group = self.group_getter(id=fetch[SQL_TAGGROUP['parentid']]) that_group = self.group_getter(id=fetch[SQL_TAGGROUP['parentid']])
raise GroupExists('%s already in group %s' % (member.name, that_group.name)) raise exceptions.GroupExists('%s already in group %s' % (member.name, that_group.name))
self.photodb._cached_frozen_children = None self.photodb._cached_frozen_children = None
self.photodb.cur.execute('INSERT INTO tag_group_rel VALUES(?, ?)', [self.id, member.id]) self.photodb.cur.execute('INSERT INTO tag_group_rel VALUES(?, ?)', [self.id, member.id])
@ -1740,7 +1547,7 @@ class Photo(ObjectBase):
for tag in other_photo.tags(): for tag in other_photo.tags():
self.add_tag(tag) self.add_tag(tag)
def delete(self, commit=True): def delete(self, delete_file=False, commit=True):
''' '''
Delete the Photo and its relation to any tags and albums. Delete the Photo and its relation to any tags and albums.
''' '''
@ -1748,6 +1555,14 @@ class Photo(ObjectBase):
self.photodb.cur.execute('DELETE FROM photos WHERE id == ?', [self.id]) self.photodb.cur.execute('DELETE FROM photos WHERE id == ?', [self.id])
self.photodb.cur.execute('DELETE FROM photo_tag_rel WHERE photoid == ?', [self.id]) self.photodb.cur.execute('DELETE FROM photo_tag_rel WHERE photoid == ?', [self.id])
self.photodb.cur.execute('DELETE FROM album_photo_rel WHERE photoid == ?', [self.id]) self.photodb.cur.execute('DELETE FROM album_photo_rel WHERE photoid == ?', [self.id])
if delete_file:
path = self.real_path.absolute_path
if commit:
os.remove(path)
else:
queue_action = {'action': os.remove, 'args': [path]}
self.photodb.on_commit_queue.append(queue_action)
if commit: if commit:
log.debug('Committing - delete photo') log.debug('Committing - delete photo')
self.photodb.commit() self.photodb.commit()
@ -1856,7 +1671,7 @@ class Photo(ObjectBase):
return hopeful_filepath return hopeful_filepath
def mimetype(self): def mimetype(self):
return get_mimetype(self.real_filepath) return helpers.get_mimetype(self.real_filepath)
@decorators.time_me @decorators.time_me
def reload_metadata(self, commit=True): def reload_metadata(self, commit=True):
@ -1979,7 +1794,7 @@ class Photo(ObjectBase):
else: else:
queue_action = {'action': os.remove, 'args': [old_path.absolute_path]} queue_action = {'action': os.remove, 'args': [old_path.absolute_path]}
self.photodb.on_commit_queue.append(queue_action) self.photodb.on_commit_queue.append(queue_action)
self.__reinit__() self.__reinit__()
def tags(self): def tags(self):
@ -2036,11 +1851,11 @@ class Tag(ObjectBase, GroupableMixin):
raise ValueError('Cannot assign synonym to itself.') raise ValueError('Cannot assign synonym to itself.')
try: try:
tag = self.photodb.get_tag_by_name(synname) self.photodb.get_tag_by_name(synname)
except NoSuchTag: except exceptions.NoSuchTag:
pass pass
else: else:
raise TagExists(synname) raise exceptions.TagExists(synname)
self.photodb._cached_frozen_children = None self.photodb._cached_frozen_children = None
self.photodb.cur.execute('INSERT INTO tag_synonyms VALUES(?, ?)', [synname, self.name]) self.photodb.cur.execute('INSERT INTO tag_synonyms VALUES(?, ?)', [synname, self.name])
@ -2103,11 +1918,11 @@ class Tag(ObjectBase, GroupableMixin):
''' '''
if self._cached_qualified_name: if self._cached_qualified_name:
return self._cached_qualified_name return self._cached_qualified_name
string = self.name qualname = self.name
for parent in self.walk_parents(): for parent in self.walk_parents():
string = parent.name + '.' + string qualname = parent.name + '.' + qualname
self._cached_qualified_name = string self._cached_qualified_name = qualname
return string return qualname
def remove_synonym(self, synname, commit=True): def remove_synonym(self, synname, commit=True):
''' '''
@ -2137,10 +1952,10 @@ class Tag(ObjectBase, GroupableMixin):
try: try:
self.photodb.get_tag(new_name) self.photodb.get_tag(new_name)
except NoSuchTag: except exceptions.NoSuchTag:
pass pass
else: else:
raise TagExists(new_name) raise exceptions.TagExists(new_name)
self._cached_qualified_name = None self._cached_qualified_name = None
self.photodb._cached_frozen_children = None self.photodb._cached_frozen_children = None

View file

@ -1,3 +1,5 @@
flask flask
gevent gevent
pillow pillow
https://raw.githubusercontent.com/voussoir/else/master/_voussoirkit/voussoirkit.zip
git+https://github.com/senko/python-video-converter.git