Add some type annotations, document some exceptions.
This commit is contained in:
parent
2e0b4cfa14
commit
e883409daf
3 changed files with 355 additions and 208 deletions
|
@ -7,6 +7,7 @@ import hashlib
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
import PIL.Image
|
import PIL.Image
|
||||||
|
import typing
|
||||||
import zipstream
|
import zipstream
|
||||||
|
|
||||||
from voussoirkit import bytestring
|
from voussoirkit import bytestring
|
||||||
|
@ -123,7 +124,7 @@ def album_photos_as_filename_map(
|
||||||
|
|
||||||
return arcnames
|
return arcnames
|
||||||
|
|
||||||
def checkerboard_image(color_1, color_2, image_size, checker_size):
|
def checkerboard_image(color_1, color_2, image_size, checker_size) -> PIL.Image:
|
||||||
'''
|
'''
|
||||||
Generate a PIL Image with a checkerboard pattern.
|
Generate a PIL Image with a checkerboard pattern.
|
||||||
|
|
||||||
|
@ -200,10 +201,10 @@ def decollide_names(things, namer):
|
||||||
final[thing] = myname
|
final[thing] = myname
|
||||||
return final
|
return final
|
||||||
|
|
||||||
def dict_to_tuple(d):
|
def dict_to_tuple(d) -> tuple:
|
||||||
return tuple(sorted(d.items()))
|
return tuple(sorted(d.items()))
|
||||||
|
|
||||||
def generate_image_thumbnail(filepath, width, height):
|
def generate_image_thumbnail(filepath, width, height) -> PIL.Image:
|
||||||
if not os.path.isfile(filepath):
|
if not os.path.isfile(filepath):
|
||||||
raise FileNotFoundError(filepath)
|
raise FileNotFoundError(filepath)
|
||||||
image = PIL.Image.open(filepath)
|
image = PIL.Image.open(filepath)
|
||||||
|
@ -234,7 +235,7 @@ def generate_image_thumbnail(filepath, width, height):
|
||||||
image = image.convert('RGB')
|
image = image.convert('RGB')
|
||||||
return image
|
return image
|
||||||
|
|
||||||
def generate_video_thumbnail(filepath, outfile, width, height, **special):
|
def generate_video_thumbnail(filepath, outfile, width, height, **special) -> PIL.Image:
|
||||||
if not os.path.isfile(filepath):
|
if not os.path.isfile(filepath):
|
||||||
raise FileNotFoundError(filepath)
|
raise FileNotFoundError(filepath)
|
||||||
probe = constants.ffmpeg.probe(filepath)
|
probe = constants.ffmpeg.probe(filepath)
|
||||||
|
@ -267,7 +268,7 @@ def generate_video_thumbnail(filepath, outfile, width, height, **special):
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_mimetype(filepath):
|
def get_mimetype(filepath) -> typing.Optional[str]:
|
||||||
'''
|
'''
|
||||||
Extension to mimetypes.guess_type which uses my
|
Extension to mimetypes.guess_type which uses my
|
||||||
constants.ADDITIONAL_MIMETYPES.
|
constants.ADDITIONAL_MIMETYPES.
|
||||||
|
@ -278,7 +279,7 @@ def get_mimetype(filepath):
|
||||||
mimetype = mimetypes.guess_type(filepath)[0]
|
mimetype = mimetypes.guess_type(filepath)[0]
|
||||||
return mimetype
|
return mimetype
|
||||||
|
|
||||||
def hash_photoset(photos):
|
def hash_photoset(photos) -> str:
|
||||||
'''
|
'''
|
||||||
Given some photos, return a fingerprint string for that particular set.
|
Given some photos, return a fingerprint string for that particular set.
|
||||||
'''
|
'''
|
||||||
|
@ -290,7 +291,7 @@ def hash_photoset(photos):
|
||||||
|
|
||||||
return hasher.hexdigest()
|
return hasher.hexdigest()
|
||||||
|
|
||||||
def hyphen_range(s):
|
def hyphen_range(s) -> tuple:
|
||||||
'''
|
'''
|
||||||
Given a string like '1-3', return numbers (1, 3) representing lower
|
Given a string like '1-3', return numbers (1, 3) representing lower
|
||||||
and upper bounds.
|
and upper bounds.
|
||||||
|
@ -319,9 +320,9 @@ def hyphen_range(s):
|
||||||
if low is not None and high is not None and low > high:
|
if low is not None and high is not None and low > high:
|
||||||
raise exceptions.OutOfOrder(range=s, min=low, max=high)
|
raise exceptions.OutOfOrder(range=s, min=low, max=high)
|
||||||
|
|
||||||
return low, high
|
return (low, high)
|
||||||
|
|
||||||
def is_xor(*args):
|
def is_xor(*args) -> bool:
|
||||||
'''
|
'''
|
||||||
Return True if and only if one arg is truthy.
|
Return True if and only if one arg is truthy.
|
||||||
'''
|
'''
|
||||||
|
@ -336,7 +337,7 @@ def now(timestamp=True):
|
||||||
return n.timestamp()
|
return n.timestamp()
|
||||||
return n
|
return n
|
||||||
|
|
||||||
def parse_unit_string(s):
|
def parse_unit_string(s) -> typing.Union[int, float, None]:
|
||||||
'''
|
'''
|
||||||
Try to parse the string as an int, float, or bytestring, or hms.
|
Try to parse the string as an int, float, or bytestring, or hms.
|
||||||
'''
|
'''
|
||||||
|
@ -357,7 +358,12 @@ def parse_unit_string(s):
|
||||||
else:
|
else:
|
||||||
return bytestring.parsebytes(s)
|
return bytestring.parsebytes(s)
|
||||||
|
|
||||||
def read_filebytes(filepath, range_min=0, range_max=None, chunk_size=bytestring.MIBIBYTE):
|
def read_filebytes(
|
||||||
|
filepath,
|
||||||
|
range_min=0,
|
||||||
|
range_max=None,
|
||||||
|
chunk_size=bytestring.MIBIBYTE,
|
||||||
|
) -> typing.Iterable[bytes]:
|
||||||
'''
|
'''
|
||||||
Yield chunks of bytes from the file between the endpoints.
|
Yield chunks of bytes from the file between the endpoints.
|
||||||
'''
|
'''
|
||||||
|
@ -373,19 +379,15 @@ def read_filebytes(filepath, range_min=0, range_max=None, chunk_size=bytestring.
|
||||||
with f:
|
with f:
|
||||||
f.seek(range_min)
|
f.seek(range_min)
|
||||||
while sent_amount < range_span:
|
while sent_amount < range_span:
|
||||||
chunk = f.read(chunk_size)
|
|
||||||
if len(chunk) == 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
needed = range_span - sent_amount
|
needed = range_span - sent_amount
|
||||||
if len(chunk) >= needed:
|
chunk = f.read(min(needed, chunk_size))
|
||||||
yield chunk[:needed]
|
if len(chunk) == 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
yield chunk
|
yield chunk
|
||||||
sent_amount += len(chunk)
|
sent_amount += len(chunk)
|
||||||
|
|
||||||
def remove_path_badchars(filepath, allowed=''):
|
def remove_path_badchars(filepath, allowed='') -> str:
|
||||||
'''
|
'''
|
||||||
Remove the bad characters seen in constants.FILENAME_BADCHARS, except
|
Remove the bad characters seen in constants.FILENAME_BADCHARS, except
|
||||||
those which you explicitly permit.
|
those which you explicitly permit.
|
||||||
|
@ -409,15 +411,18 @@ def slice_before(li, item):
|
||||||
index = li.index(item)
|
index = li.index(item)
|
||||||
return li[:index]
|
return li[:index]
|
||||||
|
|
||||||
def split_easybake_string(ebstring):
|
def split_easybake_string(ebstring) -> tuple[str, str, str]:
|
||||||
'''
|
'''
|
||||||
Given an easybake string, return (tagname, synonym, rename_to), where
|
Given an easybake string, return (tagname, synonym, rename_to), where
|
||||||
tagname may be a full qualified name, and at least one of
|
tagname may be a full qualified name, and at least one of
|
||||||
synonym or rename_to will be None since both are not posible at once.
|
synonym or rename_to will be None since both are not posible at once.
|
||||||
|
|
||||||
'languages.python' -> ('languages.python', None, None)
|
>>> split_easybake_string('languages.python')
|
||||||
'languages.python+py' -> ('languages.python', 'py', None)
|
('languages.python', None, None)
|
||||||
'languages.python=bestlang' -> ('languages.python', None, 'bestlang')
|
>>> split_easybake_string('languages.python+py')
|
||||||
|
('languages.python', 'py', None)
|
||||||
|
>>> split_easybake_string('languages.python=bestlang')
|
||||||
|
('languages.python', None, 'bestlang')
|
||||||
'''
|
'''
|
||||||
ebstring = ebstring.strip()
|
ebstring = ebstring.strip()
|
||||||
ebstring = ebstring.strip('.+=')
|
ebstring = ebstring.strip('.+=')
|
||||||
|
@ -454,7 +459,7 @@ def split_easybake_string(ebstring):
|
||||||
tagname = tagname.strip('.')
|
tagname = tagname.strip('.')
|
||||||
return (tagname, synonym, rename_to)
|
return (tagname, synonym, rename_to)
|
||||||
|
|
||||||
def truthystring(s, fallback=False):
|
def truthystring(s, fallback=False) -> typing.Union[bool, None]:
|
||||||
'''
|
'''
|
||||||
If s is already a boolean, int, or None, return a boolean or None.
|
If s is already a boolean, int, or None, return a boolean or None.
|
||||||
If s is a string, return True, False, or None based on the options presented
|
If s is a string, return True, False, or None based on the options presented
|
||||||
|
@ -480,7 +485,7 @@ def truthystring(s, fallback=False):
|
||||||
return None
|
return None
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def zip_album(album, recursive=True):
|
def zip_album(album, recursive=True) -> zipstream.ZipFile:
|
||||||
'''
|
'''
|
||||||
Given an album, return a zipstream zipfile that contains the album's
|
Given an album, return a zipstream zipfile that contains the album's
|
||||||
photos (recursive = include children's photos) organized into folders
|
photos (recursive = include children's photos) organized into folders
|
||||||
|
@ -521,7 +526,7 @@ def zip_album(album, recursive=True):
|
||||||
|
|
||||||
return zipfile
|
return zipfile
|
||||||
|
|
||||||
def zip_photos(photos):
|
def zip_photos(photos) -> zipstream.ZipFile:
|
||||||
'''
|
'''
|
||||||
Given some photos, return a zipstream zipfile that contains the files.
|
Given some photos, return a zipstream zipfile that contains the files.
|
||||||
'''
|
'''
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -8,6 +8,7 @@ import sqlite3
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
import types
|
import types
|
||||||
|
import typing
|
||||||
|
|
||||||
from voussoirkit import cacheclass
|
from voussoirkit import cacheclass
|
||||||
from voussoirkit import configlayers
|
from voussoirkit import configlayers
|
||||||
|
@ -34,19 +35,19 @@ class PDBAlbumMixin:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def get_album(self, id):
|
def get_album(self, id) -> objects.Album:
|
||||||
return self.get_thing_by_id('album', id)
|
return self.get_thing_by_id('album', id)
|
||||||
|
|
||||||
def get_album_count(self):
|
def get_album_count(self) -> int:
|
||||||
return self.sql_select_one('SELECT COUNT(id) FROM albums')[0]
|
return self.sql_select_one('SELECT COUNT(id) FROM albums')[0]
|
||||||
|
|
||||||
def get_albums(self):
|
def get_albums(self) -> typing.Iterable[objects.Album]:
|
||||||
return self.get_things(thing_type='album')
|
return self.get_things(thing_type='album')
|
||||||
|
|
||||||
def get_albums_by_id(self, ids):
|
def get_albums_by_id(self, ids) -> typing.Iterable[objects.Album]:
|
||||||
return self.get_things_by_id('album', ids)
|
return self.get_things_by_id('album', ids)
|
||||||
|
|
||||||
def get_albums_by_path(self, directory):
|
def get_albums_by_path(self, directory) -> typing.Iterable[objects.Album]:
|
||||||
'''
|
'''
|
||||||
Yield Albums with the `associated_directory` of this value,
|
Yield Albums with the `associated_directory` of this value,
|
||||||
NOT case-sensitive.
|
NOT case-sensitive.
|
||||||
|
@ -58,10 +59,10 @@ class PDBAlbumMixin:
|
||||||
album_ids = (album_id for (album_id,) in album_rows)
|
album_ids = (album_id for (album_id,) in album_rows)
|
||||||
return self.get_albums_by_id(album_ids)
|
return self.get_albums_by_id(album_ids)
|
||||||
|
|
||||||
def get_albums_by_sql(self, query, bindings=None):
|
def get_albums_by_sql(self, query, bindings=None) -> typing.Iterable[objects.Album]:
|
||||||
return self.get_things_by_sql('album', query, bindings)
|
return self.get_things_by_sql('album', query, bindings)
|
||||||
|
|
||||||
def get_albums_within_directory(self, directory):
|
def get_albums_within_directory(self, directory) -> typing.Iterable[objects.Album]:
|
||||||
# This function is something of a stopgap measure since `search` only
|
# This function is something of a stopgap measure since `search` only
|
||||||
# searches for photos and then yields their containing albums. Thus it
|
# searches for photos and then yields their containing albums. Thus it
|
||||||
# is not possible for search to find albums that contain no photos.
|
# is not possible for search to find albums that contain no photos.
|
||||||
|
@ -78,7 +79,7 @@ class PDBAlbumMixin:
|
||||||
albums = self.get_albums_by_id(album_ids)
|
albums = self.get_albums_by_id(album_ids)
|
||||||
return albums
|
return albums
|
||||||
|
|
||||||
def get_root_albums(self):
|
def get_root_albums(self) -> typing.Iterable[objects.Album]:
|
||||||
'''
|
'''
|
||||||
Yield Albums that have no parent.
|
Yield Albums that have no parent.
|
||||||
'''
|
'''
|
||||||
|
@ -94,7 +95,7 @@ class PDBAlbumMixin:
|
||||||
associated_directories=None,
|
associated_directories=None,
|
||||||
author=None,
|
author=None,
|
||||||
photos=None,
|
photos=None,
|
||||||
):
|
) -> objects.Album:
|
||||||
'''
|
'''
|
||||||
Create a new album. Photos can be added now or later.
|
Create a new album. Photos can be added now or later.
|
||||||
'''
|
'''
|
||||||
|
@ -131,7 +132,7 @@ class PDBAlbumMixin:
|
||||||
return album
|
return album
|
||||||
|
|
||||||
@decorators.transaction
|
@decorators.transaction
|
||||||
def purge_deleted_associated_directories(self, albums=None):
|
def purge_deleted_associated_directories(self, albums=None) -> typing.Iterable[pathclass.Path]:
|
||||||
directories = self.sql_select('SELECT DISTINCT directory FROM album_associated_directories')
|
directories = self.sql_select('SELECT DISTINCT directory FROM album_associated_directories')
|
||||||
directories = (pathclass.Path(directory) for (directory,) in directories)
|
directories = (pathclass.Path(directory) for (directory,) in directories)
|
||||||
directories = [directory for directory in directories if not directory.is_dir]
|
directories = [directory for directory in directories if not directory.is_dir]
|
||||||
|
@ -148,7 +149,7 @@ class PDBAlbumMixin:
|
||||||
yield from directories
|
yield from directories
|
||||||
|
|
||||||
@decorators.transaction
|
@decorators.transaction
|
||||||
def purge_empty_albums(self, albums=None):
|
def purge_empty_albums(self, albums=None) -> typing.Iterable[objects.Album]:
|
||||||
if albums is None:
|
if albums is None:
|
||||||
to_check = set(self.get_albums())
|
to_check = set(self.get_albums())
|
||||||
else:
|
else:
|
||||||
|
@ -171,24 +172,24 @@ class PDBBookmarkMixin:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def get_bookmark(self, id):
|
def get_bookmark(self, id) -> objects.Bookmark:
|
||||||
return self.get_thing_by_id('bookmark', id)
|
return self.get_thing_by_id('bookmark', id)
|
||||||
|
|
||||||
def get_bookmark_count(self):
|
def get_bookmark_count(self) -> int:
|
||||||
return self.sql_select_one('SELECT COUNT(id) FROM bookmarks')[0]
|
return self.sql_select_one('SELECT COUNT(id) FROM bookmarks')[0]
|
||||||
|
|
||||||
def get_bookmarks(self):
|
def get_bookmarks(self) -> typing.Iterable[objects.Bookmark]:
|
||||||
return self.get_things(thing_type='bookmark')
|
return self.get_things(thing_type='bookmark')
|
||||||
|
|
||||||
def get_bookmarks_by_id(self, ids):
|
def get_bookmarks_by_id(self, ids) -> typing.Iterable[objects.Bookmark]:
|
||||||
return self.get_things_by_id('bookmark', ids)
|
return self.get_things_by_id('bookmark', ids)
|
||||||
|
|
||||||
def get_bookmarks_by_sql(self, query, bindings=None):
|
def get_bookmarks_by_sql(self, query, bindings=None) -> typing.Iterable[objects.Bookmark]:
|
||||||
return self.get_things_by_sql('bookmark', query, bindings)
|
return self.get_things_by_sql('bookmark', query, bindings)
|
||||||
|
|
||||||
@decorators.required_feature('bookmark.new')
|
@decorators.required_feature('bookmark.new')
|
||||||
@decorators.transaction
|
@decorators.transaction
|
||||||
def new_bookmark(self, url, title=None, *, author=None):
|
def new_bookmark(self, url, title=None, *, author=None) -> objects.Bookmark:
|
||||||
# These might raise exceptions.
|
# These might raise exceptions.
|
||||||
title = objects.Bookmark.normalize_title(title)
|
title = objects.Bookmark.normalize_title(title)
|
||||||
url = objects.Bookmark.normalize_url(url)
|
url = objects.Bookmark.normalize_url(url)
|
||||||
|
@ -245,7 +246,7 @@ class PDBCacheManagerMixin:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def clear_all_caches(self):
|
def clear_all_caches(self) -> None:
|
||||||
self.caches['album'].clear()
|
self.caches['album'].clear()
|
||||||
self.caches['bookmark'].clear()
|
self.caches['bookmark'].clear()
|
||||||
self.caches['photo'].clear()
|
self.caches['photo'].clear()
|
||||||
|
@ -430,7 +431,7 @@ class PDBPhotoMixin:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def assert_no_such_photo_by_path(self, filepath):
|
def assert_no_such_photo_by_path(self, filepath) -> None:
|
||||||
try:
|
try:
|
||||||
existing = self.get_photo_by_path(filepath)
|
existing = self.get_photo_by_path(filepath)
|
||||||
except exceptions.NoSuchPhoto:
|
except exceptions.NoSuchPhoto:
|
||||||
|
@ -438,10 +439,10 @@ class PDBPhotoMixin:
|
||||||
else:
|
else:
|
||||||
raise exceptions.PhotoExists(existing)
|
raise exceptions.PhotoExists(existing)
|
||||||
|
|
||||||
def get_photo(self, id):
|
def get_photo(self, id) -> objects.Photo:
|
||||||
return self.get_thing_by_id('photo', id)
|
return self.get_thing_by_id('photo', id)
|
||||||
|
|
||||||
def get_photo_by_path(self, filepath):
|
def get_photo_by_path(self, filepath) -> objects.Photo:
|
||||||
filepath = pathclass.Path(filepath)
|
filepath = pathclass.Path(filepath)
|
||||||
query = 'SELECT * FROM photos WHERE filepath == ?'
|
query = 'SELECT * FROM photos WHERE filepath == ?'
|
||||||
bindings = [filepath.absolute_path]
|
bindings = [filepath.absolute_path]
|
||||||
|
@ -451,13 +452,13 @@ class PDBPhotoMixin:
|
||||||
photo = self.get_cached_instance('photo', photo_row)
|
photo = self.get_cached_instance('photo', photo_row)
|
||||||
return photo
|
return photo
|
||||||
|
|
||||||
def get_photo_count(self):
|
def get_photo_count(self) -> int:
|
||||||
return self.sql_select_one('SELECT COUNT(id) FROM photos')[0]
|
return self.sql_select_one('SELECT COUNT(id) FROM photos')[0]
|
||||||
|
|
||||||
def get_photos_by_id(self, ids):
|
def get_photos_by_id(self, ids) -> typing.Iterable[objects.Photo]:
|
||||||
return self.get_things_by_id('photo', ids)
|
return self.get_things_by_id('photo', ids)
|
||||||
|
|
||||||
def get_photos_by_recent(self, count=None):
|
def get_photos_by_recent(self, count=None) -> typing.Iterable[objects.Photo]:
|
||||||
'''
|
'''
|
||||||
Yield photo objects in order of creation time.
|
Yield photo objects in order of creation time.
|
||||||
'''
|
'''
|
||||||
|
@ -476,7 +477,7 @@ class PDBPhotoMixin:
|
||||||
if count <= 0:
|
if count <= 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
def get_photos_by_hash(self, sha256):
|
def get_photos_by_hash(self, sha256) -> typing.Iterable[objects.Photo]:
|
||||||
if not isinstance(sha256, str) or len(sha256) != 64:
|
if not isinstance(sha256, str) or len(sha256) != 64:
|
||||||
raise TypeError(f'sha256 shoulbe the 64-character hexdigest string.')
|
raise TypeError(f'sha256 shoulbe the 64-character hexdigest string.')
|
||||||
|
|
||||||
|
@ -484,7 +485,7 @@ class PDBPhotoMixin:
|
||||||
bindings = [sha256]
|
bindings = [sha256]
|
||||||
yield from self.get_photos_by_sql(query, bindings)
|
yield from self.get_photos_by_sql(query, bindings)
|
||||||
|
|
||||||
def get_photos_by_sql(self, query, bindings=None):
|
def get_photos_by_sql(self, query, bindings=None) -> typing.Iterable[objects.Photo]:
|
||||||
return self.get_things_by_sql('photo', query, bindings)
|
return self.get_things_by_sql('photo', query, bindings)
|
||||||
|
|
||||||
@decorators.required_feature('photo.new')
|
@decorators.required_feature('photo.new')
|
||||||
|
@ -500,7 +501,7 @@ class PDBPhotoMixin:
|
||||||
known_hash=None,
|
known_hash=None,
|
||||||
searchhidden=False,
|
searchhidden=False,
|
||||||
tags=None,
|
tags=None,
|
||||||
):
|
) -> objects.Photo:
|
||||||
'''
|
'''
|
||||||
Given a filepath, determine its attributes and create a new Photo object
|
Given a filepath, determine its attributes and create a new Photo object
|
||||||
in the database. Tags may be applied now or later.
|
in the database. Tags may be applied now or later.
|
||||||
|
@ -574,7 +575,7 @@ class PDBPhotoMixin:
|
||||||
return photo
|
return photo
|
||||||
|
|
||||||
@decorators.transaction
|
@decorators.transaction
|
||||||
def purge_deleted_files(self, photos=None):
|
def purge_deleted_files(self, photos=None) -> typing.Iterable[objects.Photo]:
|
||||||
'''
|
'''
|
||||||
Delete Photos whose corresponding file on disk is missing.
|
Delete Photos whose corresponding file on disk is missing.
|
||||||
|
|
||||||
|
@ -1020,13 +1021,13 @@ class PDBSQLMixin:
|
||||||
self.savepoints = []
|
self.savepoints = []
|
||||||
self._cached_sql_tables = None
|
self._cached_sql_tables = None
|
||||||
|
|
||||||
def assert_table_exists(self, table):
|
def assert_table_exists(self, table) -> None:
|
||||||
if not self._cached_sql_tables:
|
if not self._cached_sql_tables:
|
||||||
self._cached_sql_tables = self.get_sql_tables()
|
self._cached_sql_tables = self.get_sql_tables()
|
||||||
if table not in self._cached_sql_tables:
|
if table not in self._cached_sql_tables:
|
||||||
raise exceptions.BadTable(table)
|
raise exceptions.BadTable(table)
|
||||||
|
|
||||||
def commit(self, message=None):
|
def commit(self, message=None) -> None:
|
||||||
if message is not None:
|
if message is not None:
|
||||||
self.log.debug('Committing - %s.', message)
|
self.log.debug('Committing - %s.', message)
|
||||||
|
|
||||||
|
@ -1048,13 +1049,13 @@ class PDBSQLMixin:
|
||||||
self.savepoints.clear()
|
self.savepoints.clear()
|
||||||
self.sql.commit()
|
self.sql.commit()
|
||||||
|
|
||||||
def get_sql_tables(self):
|
def get_sql_tables(self) -> set[str]:
|
||||||
query = 'SELECT name FROM sqlite_master WHERE type = "table"'
|
query = 'SELECT name FROM sqlite_master WHERE type = "table"'
|
||||||
table_rows = self.sql_select(query)
|
table_rows = self.sql_select(query)
|
||||||
tables = set(name for (name,) in table_rows)
|
tables = set(name for (name,) in table_rows)
|
||||||
return tables
|
return tables
|
||||||
|
|
||||||
def release_savepoint(self, savepoint, allow_commit=False):
|
def release_savepoint(self, savepoint, allow_commit=False) -> None:
|
||||||
'''
|
'''
|
||||||
Releasing a savepoint removes that savepoint from the timeline, so that
|
Releasing a savepoint removes that savepoint from the timeline, so that
|
||||||
you can no longer roll back to it. Then your choices are to commit
|
you can no longer roll back to it. Then your choices are to commit
|
||||||
|
@ -1078,7 +1079,7 @@ class PDBSQLMixin:
|
||||||
self.sql_execute(f'RELEASE "{savepoint}"')
|
self.sql_execute(f'RELEASE "{savepoint}"')
|
||||||
self.savepoints = helpers.slice_before(self.savepoints, savepoint)
|
self.savepoints = helpers.slice_before(self.savepoints, savepoint)
|
||||||
|
|
||||||
def rollback(self, savepoint=None):
|
def rollback(self, savepoint=None) -> None:
|
||||||
'''
|
'''
|
||||||
Given a savepoint, roll the database back to the moment before that
|
Given a savepoint, roll the database back to the moment before that
|
||||||
savepoint was created. Keep in mind that a @transaction savepoint is
|
savepoint was created. Keep in mind that a @transaction savepoint is
|
||||||
|
@ -1117,7 +1118,7 @@ class PDBSQLMixin:
|
||||||
self.savepoints.clear()
|
self.savepoints.clear()
|
||||||
self.on_commit_queue.clear()
|
self.on_commit_queue.clear()
|
||||||
|
|
||||||
def savepoint(self, message=None):
|
def savepoint(self, message=None) -> str:
|
||||||
savepoint_id = passwordy.random_hex(length=16)
|
savepoint_id = passwordy.random_hex(length=16)
|
||||||
if message:
|
if message:
|
||||||
self.log.log(5, 'Savepoint %s for %s.', savepoint_id, message)
|
self.log.log(5, 'Savepoint %s for %s.', savepoint_id, message)
|
||||||
|
@ -1130,13 +1131,13 @@ class PDBSQLMixin:
|
||||||
self.on_rollback_queue.append(savepoint_id)
|
self.on_rollback_queue.append(savepoint_id)
|
||||||
return savepoint_id
|
return savepoint_id
|
||||||
|
|
||||||
def sql_delete(self, table, pairs):
|
def sql_delete(self, table, pairs) -> None:
|
||||||
self.assert_table_exists(table)
|
self.assert_table_exists(table)
|
||||||
(qmarks, bindings) = sqlhelpers.delete_filler(pairs)
|
(qmarks, bindings) = sqlhelpers.delete_filler(pairs)
|
||||||
query = f'DELETE FROM {table} {qmarks}'
|
query = f'DELETE FROM {table} {qmarks}'
|
||||||
self.sql_execute(query, bindings)
|
self.sql_execute(query, bindings)
|
||||||
|
|
||||||
def sql_execute(self, query, bindings=[]):
|
def sql_execute(self, query, bindings=[]) -> sqlite3.Cursor:
|
||||||
if bindings is None:
|
if bindings is None:
|
||||||
bindings = []
|
bindings = []
|
||||||
cur = self.sql.cursor()
|
cur = self.sql.cursor()
|
||||||
|
@ -1144,7 +1145,7 @@ class PDBSQLMixin:
|
||||||
cur.execute(query, bindings)
|
cur.execute(query, bindings)
|
||||||
return cur
|
return cur
|
||||||
|
|
||||||
def sql_executescript(self, script):
|
def sql_executescript(self, script) -> None:
|
||||||
'''
|
'''
|
||||||
The problem with Python's default executescript is that it executes a
|
The problem with Python's default executescript is that it executes a
|
||||||
COMMIT before running your script. If I wanted a commit I'd write one!
|
COMMIT before running your script. If I wanted a commit I'd write one!
|
||||||
|
@ -1157,7 +1158,7 @@ class PDBSQLMixin:
|
||||||
self.log.loud(line)
|
self.log.loud(line)
|
||||||
cur.execute(line)
|
cur.execute(line)
|
||||||
|
|
||||||
def sql_insert(self, table, data):
|
def sql_insert(self, table, data) -> None:
|
||||||
self.assert_table_exists(table)
|
self.assert_table_exists(table)
|
||||||
column_names = constants.SQL_COLUMNS[table]
|
column_names = constants.SQL_COLUMNS[table]
|
||||||
(qmarks, bindings) = sqlhelpers.insert_filler(column_names, data)
|
(qmarks, bindings) = sqlhelpers.insert_filler(column_names, data)
|
||||||
|
@ -1165,7 +1166,7 @@ class PDBSQLMixin:
|
||||||
query = f'INSERT INTO {table} VALUES({qmarks})'
|
query = f'INSERT INTO {table} VALUES({qmarks})'
|
||||||
self.sql_execute(query, bindings)
|
self.sql_execute(query, bindings)
|
||||||
|
|
||||||
def sql_select(self, query, bindings=None):
|
def sql_select(self, query, bindings=None) -> typing.Iterable:
|
||||||
cur = self.sql_execute(query, bindings)
|
cur = self.sql_execute(query, bindings)
|
||||||
while True:
|
while True:
|
||||||
fetch = cur.fetchone()
|
fetch = cur.fetchone()
|
||||||
|
@ -1177,7 +1178,7 @@ class PDBSQLMixin:
|
||||||
cur = self.sql_execute(query, bindings)
|
cur = self.sql_execute(query, bindings)
|
||||||
return cur.fetchone()
|
return cur.fetchone()
|
||||||
|
|
||||||
def sql_update(self, table, pairs, where_key):
|
def sql_update(self, table, pairs, where_key) -> None:
|
||||||
self.assert_table_exists(table)
|
self.assert_table_exists(table)
|
||||||
(qmarks, bindings) = sqlhelpers.update_filler(pairs, where_key=where_key)
|
(qmarks, bindings) = sqlhelpers.update_filler(pairs, where_key=where_key)
|
||||||
query = f'UPDATE {table} {qmarks}'
|
query = f'UPDATE {table} {qmarks}'
|
||||||
|
@ -1189,7 +1190,7 @@ class PDBTagMixin:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def assert_no_such_tag(self, name):
|
def assert_no_such_tag(self, name) -> None:
|
||||||
try:
|
try:
|
||||||
existing_tag = self.get_tag_by_name(name)
|
existing_tag = self.get_tag_by_name(name)
|
||||||
except exceptions.NoSuchTag:
|
except exceptions.NoSuchTag:
|
||||||
|
@ -1203,7 +1204,7 @@ class PDBTagMixin:
|
||||||
names = set(name for (name,) in tag_rows)
|
names = set(name for (name,) in tag_rows)
|
||||||
return names
|
return names
|
||||||
|
|
||||||
def get_all_tag_names(self):
|
def get_all_tag_names(self) -> set[str]:
|
||||||
'''
|
'''
|
||||||
Return a set containing the names of all tags as strings.
|
Return a set containing the names of all tags as strings.
|
||||||
Useful for when you don't want the overhead of actual Tag objects.
|
Useful for when you don't want the overhead of actual Tag objects.
|
||||||
|
@ -1216,19 +1217,19 @@ class PDBTagMixin:
|
||||||
synonyms = {syn: tag for (syn, tag) in syn_rows}
|
synonyms = {syn: tag for (syn, tag) in syn_rows}
|
||||||
return synonyms
|
return synonyms
|
||||||
|
|
||||||
def get_all_synonyms(self):
|
def get_all_synonyms(self) -> dict:
|
||||||
'''
|
'''
|
||||||
Return a dict mapping {synonym: mastertag} as strings.
|
Return a dict mapping {synonym: mastertag} as strings.
|
||||||
'''
|
'''
|
||||||
return self.get_cached_tag_export(self._get_all_synonyms)
|
return self.get_cached_tag_export(self._get_all_synonyms)
|
||||||
|
|
||||||
def get_root_tags(self):
|
def get_root_tags(self) -> typing.Iterable[objects.Tag]:
|
||||||
'''
|
'''
|
||||||
Yield Tags that have no parent.
|
Yield Tags that have no parent.
|
||||||
'''
|
'''
|
||||||
return self.get_root_things('tag')
|
return self.get_root_things('tag')
|
||||||
|
|
||||||
def get_tag(self, name=None, id=None):
|
def get_tag(self, name=None, id=None) -> objects.Tag:
|
||||||
'''
|
'''
|
||||||
Redirect to get_tag_by_id or get_tag_by_name.
|
Redirect to get_tag_by_id or get_tag_by_name.
|
||||||
'''
|
'''
|
||||||
|
@ -1240,10 +1241,10 @@ class PDBTagMixin:
|
||||||
else:
|
else:
|
||||||
return self.get_tag_by_name(name)
|
return self.get_tag_by_name(name)
|
||||||
|
|
||||||
def get_tag_by_id(self, id):
|
def get_tag_by_id(self, id) -> objects.Tag:
|
||||||
return self.get_thing_by_id('tag', thing_id=id)
|
return self.get_thing_by_id('tag', thing_id=id)
|
||||||
|
|
||||||
def get_tag_by_name(self, tagname):
|
def get_tag_by_name(self, tagname) -> objects.Tag:
|
||||||
if isinstance(tagname, objects.Tag):
|
if isinstance(tagname, objects.Tag):
|
||||||
if tagname.photodb == self:
|
if tagname.photodb == self:
|
||||||
return tagname
|
return tagname
|
||||||
|
@ -1277,24 +1278,24 @@ class PDBTagMixin:
|
||||||
tag = self.get_cached_instance('tag', tag_row)
|
tag = self.get_cached_instance('tag', tag_row)
|
||||||
return tag
|
return tag
|
||||||
|
|
||||||
def get_tag_count(self):
|
def get_tag_count(self) -> int:
|
||||||
return self.sql_select_one('SELECT COUNT(id) FROM tags')[0]
|
return self.sql_select_one('SELECT COUNT(id) FROM tags')[0]
|
||||||
|
|
||||||
def get_tags(self):
|
def get_tags(self) -> typing.Iterable[objects.Tag]:
|
||||||
'''
|
'''
|
||||||
Yield all Tags in the database.
|
Yield all Tags in the database.
|
||||||
'''
|
'''
|
||||||
return self.get_things(thing_type='tag')
|
return self.get_things(thing_type='tag')
|
||||||
|
|
||||||
def get_tags_by_id(self, ids):
|
def get_tags_by_id(self, ids) -> typing.Iterable[objects.Tag]:
|
||||||
return self.get_things_by_id('tag', ids)
|
return self.get_things_by_id('tag', ids)
|
||||||
|
|
||||||
def get_tags_by_sql(self, query, bindings=None):
|
def get_tags_by_sql(self, query, bindings=None) -> typing.Iterable[objects.Tag]:
|
||||||
return self.get_things_by_sql('tag', query, bindings)
|
return self.get_things_by_sql('tag', query, bindings)
|
||||||
|
|
||||||
@decorators.required_feature('tag.new')
|
@decorators.required_feature('tag.new')
|
||||||
@decorators.transaction
|
@decorators.transaction
|
||||||
def new_tag(self, tagname, description=None, *, author=None):
|
def new_tag(self, tagname, description=None, *, author=None) -> objects.Tag:
|
||||||
'''
|
'''
|
||||||
Register a new tag and return the Tag object.
|
Register a new tag and return the Tag object.
|
||||||
'''
|
'''
|
||||||
|
@ -1324,7 +1325,7 @@ class PDBTagMixin:
|
||||||
|
|
||||||
return tag
|
return tag
|
||||||
|
|
||||||
def normalize_tagname(self, tagname):
|
def normalize_tagname(self, tagname) -> str:
|
||||||
tagname = objects.Tag.normalize_name(
|
tagname = objects.Tag.normalize_name(
|
||||||
tagname,
|
tagname,
|
||||||
# valid_chars=self.config['tag']['valid_chars'],
|
# valid_chars=self.config['tag']['valid_chars'],
|
||||||
|
@ -1339,7 +1340,7 @@ class PDBUserMixin:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def assert_no_such_user(self, username):
|
def assert_no_such_user(self, username) -> None:
|
||||||
try:
|
try:
|
||||||
existing_user = self.get_user(username=username)
|
existing_user = self.get_user(username=username)
|
||||||
except exceptions.NoSuchUser:
|
except exceptions.NoSuchUser:
|
||||||
|
@ -1347,14 +1348,14 @@ class PDBUserMixin:
|
||||||
else:
|
else:
|
||||||
raise exceptions.UserExists(existing_user)
|
raise exceptions.UserExists(existing_user)
|
||||||
|
|
||||||
def assert_valid_password(self, password):
|
def assert_valid_password(self, password) -> None:
|
||||||
if not isinstance(password, bytes):
|
if not isinstance(password, bytes):
|
||||||
raise TypeError(f'Password must be {bytes}, not {type(password)}.')
|
raise TypeError(f'Password must be {bytes}, not {type(password)}.')
|
||||||
|
|
||||||
if len(password) < self.config['user']['min_password_length']:
|
if len(password) < self.config['user']['min_password_length']:
|
||||||
raise exceptions.PasswordTooShort(min_length=self.config['user']['min_password_length'])
|
raise exceptions.PasswordTooShort(min_length=self.config['user']['min_password_length'])
|
||||||
|
|
||||||
def assert_valid_username(self, username):
|
def assert_valid_username(self, username) -> None:
|
||||||
if not isinstance(username, str):
|
if not isinstance(username, str):
|
||||||
raise TypeError(f'Username must be {str}, not {type(username)}.')
|
raise TypeError(f'Username must be {str}, not {type(username)}.')
|
||||||
|
|
||||||
|
@ -1374,7 +1375,7 @@ class PDBUserMixin:
|
||||||
if badchars:
|
if badchars:
|
||||||
raise exceptions.InvalidUsernameChars(username=username, badchars=badchars)
|
raise exceptions.InvalidUsernameChars(username=username, badchars=badchars)
|
||||||
|
|
||||||
def generate_user_id(self):
|
def generate_user_id(self) -> str:
|
||||||
'''
|
'''
|
||||||
User IDs are randomized instead of integers like the other objects,
|
User IDs are randomized instead of integers like the other objects,
|
||||||
so they get their own method.
|
so they get their own method.
|
||||||
|
@ -1391,7 +1392,7 @@ class PDBUserMixin:
|
||||||
|
|
||||||
return user_id
|
return user_id
|
||||||
|
|
||||||
def get_user(self, username=None, id=None):
|
def get_user(self, username=None, id=None) -> objects.User:
|
||||||
'''
|
'''
|
||||||
Redirect to get_user_by_id or get_user_by_username.
|
Redirect to get_user_by_id or get_user_by_username.
|
||||||
'''
|
'''
|
||||||
|
@ -1403,10 +1404,10 @@ class PDBUserMixin:
|
||||||
else:
|
else:
|
||||||
return self.get_user_by_username(username)
|
return self.get_user_by_username(username)
|
||||||
|
|
||||||
def get_user_by_id(self, id):
|
def get_user_by_id(self, id) -> objects.User:
|
||||||
return self.get_thing_by_id('user', id)
|
return self.get_thing_by_id('user', id)
|
||||||
|
|
||||||
def get_user_by_username(self, username):
|
def get_user_by_username(self, username) -> objects.User:
|
||||||
user_row = self.sql_select_one('SELECT * FROM users WHERE username == ?', [username])
|
user_row = self.sql_select_one('SELECT * FROM users WHERE username == ?', [username])
|
||||||
|
|
||||||
if user_row is None:
|
if user_row is None:
|
||||||
|
@ -1414,10 +1415,10 @@ class PDBUserMixin:
|
||||||
|
|
||||||
return self.get_cached_instance('user', user_row)
|
return self.get_cached_instance('user', user_row)
|
||||||
|
|
||||||
def get_user_count(self):
|
def get_user_count(self) -> int:
|
||||||
return self.sql_select_one('SELECT COUNT(id) FROM users')[0]
|
return self.sql_select_one('SELECT COUNT(id) FROM users')[0]
|
||||||
|
|
||||||
def get_user_id_or_none(self, user_obj_or_id):
|
def get_user_id_or_none(self, user_obj_or_id) -> typing.Optional[str]:
|
||||||
'''
|
'''
|
||||||
For methods that create photos, albums, etc., we sometimes associate
|
For methods that create photos, albums, etc., we sometimes associate
|
||||||
them with an author but sometimes not. The callers of those methods
|
them with an author but sometimes not. The callers of those methods
|
||||||
|
@ -1446,17 +1447,17 @@ class PDBUserMixin:
|
||||||
|
|
||||||
return author_id
|
return author_id
|
||||||
|
|
||||||
def get_users(self):
|
def get_users(self) -> typing.Iterable[objects.User]:
|
||||||
return self.get_things('user')
|
return self.get_things('user')
|
||||||
|
|
||||||
def get_users_by_id(self, ids):
|
def get_users_by_id(self, ids) -> typing.Iterable[objects.User]:
|
||||||
return self.get_things_by_id('user', ids)
|
return self.get_things_by_id('user', ids)
|
||||||
|
|
||||||
def get_users_by_sql(self, query, bindings=None):
|
def get_users_by_sql(self, query, bindings=None) -> typing.Iterable[objects.User]:
|
||||||
return self.get_things_by_sql('user', query, bindings)
|
return self.get_things_by_sql('user', query, bindings)
|
||||||
|
|
||||||
@decorators.required_feature('user.login')
|
@decorators.required_feature('user.login')
|
||||||
def login(self, username=None, id=None, *, password):
|
def login(self, username=None, id=None, *, password) -> objects.User:
|
||||||
'''
|
'''
|
||||||
Return the User object for the user if the credentials are correct.
|
Return the User object for the user if the credentials are correct.
|
||||||
'''
|
'''
|
||||||
|
@ -1476,7 +1477,7 @@ class PDBUserMixin:
|
||||||
|
|
||||||
@decorators.required_feature('user.new')
|
@decorators.required_feature('user.new')
|
||||||
@decorators.transaction
|
@decorators.transaction
|
||||||
def new_user(self, username, password, *, display_name=None):
|
def new_user(self, username, password, *, display_name=None) -> objects.User:
|
||||||
# These might raise exceptions.
|
# These might raise exceptions.
|
||||||
self.assert_valid_username(username)
|
self.assert_valid_username(username)
|
||||||
self.assert_no_such_user(username=username)
|
self.assert_no_such_user(username=username)
|
||||||
|
@ -1953,6 +1954,7 @@ class PhotoDB(
|
||||||
self.sql_executescript(constants.DB_PRAGMAS)
|
self.sql_executescript(constants.DB_PRAGMAS)
|
||||||
self.sql.commit()
|
self.sql.commit()
|
||||||
|
|
||||||
|
# Will add -> PhotoDB when forward references are supported
|
||||||
@classmethod
|
@classmethod
|
||||||
def closest_photodb(cls, path, *args, **kwargs):
|
def closest_photodb(cls, path, *args, **kwargs):
|
||||||
'''
|
'''
|
||||||
|
@ -1989,7 +1991,7 @@ class PhotoDB(
|
||||||
else:
|
else:
|
||||||
return f'PhotoDB(data_directory={self.data_directory})'
|
return f'PhotoDB(data_directory={self.data_directory})'
|
||||||
|
|
||||||
def close(self):
|
def close(self) -> None:
|
||||||
# Wrapped in hasattr because if the object fails __init__, Python will
|
# Wrapped in hasattr because if the object fails __init__, Python will
|
||||||
# still call __del__ and thus close(), even though the attributes
|
# still call __del__ and thus close(), even though the attributes
|
||||||
# we're trying to clean up never got set.
|
# we're trying to clean up never got set.
|
||||||
|
@ -1999,7 +2001,7 @@ class PhotoDB(
|
||||||
if getattr(self, 'ephemeral', False):
|
if getattr(self, 'ephemeral', False):
|
||||||
self.ephemeral_directory.cleanup()
|
self.ephemeral_directory.cleanup()
|
||||||
|
|
||||||
def generate_id(self, table):
|
def generate_id(self, table) -> str:
|
||||||
'''
|
'''
|
||||||
Create a new ID number that is unique to the given table.
|
Create a new ID number that is unique to the given table.
|
||||||
Note that while this method may INSERT / UPDATE, it does not commit.
|
Note that while this method may INSERT / UPDATE, it does not commit.
|
||||||
|
@ -2032,7 +2034,7 @@ class PhotoDB(
|
||||||
self.sql_update(table='id_numbers', pairs=pairs, where_key='tab')
|
self.sql_update(table='id_numbers', pairs=pairs, where_key='tab')
|
||||||
return new_id
|
return new_id
|
||||||
|
|
||||||
def load_config(self):
|
def load_config(self) -> None:
|
||||||
(config, needs_rewrite) = configlayers.load_file(
|
(config, needs_rewrite) = configlayers.load_file(
|
||||||
filepath=self.config_filepath,
|
filepath=self.config_filepath,
|
||||||
defaults=constants.DEFAULT_CONFIGURATION,
|
defaults=constants.DEFAULT_CONFIGURATION,
|
||||||
|
@ -2042,6 +2044,6 @@ class PhotoDB(
|
||||||
if needs_rewrite:
|
if needs_rewrite:
|
||||||
self.save_config()
|
self.save_config()
|
||||||
|
|
||||||
def save_config(self):
|
def save_config(self) -> None:
|
||||||
with self.config_filepath.open('w', encoding='utf-8') as handle:
|
with self.config_filepath.open('w', encoding='utf-8') as handle:
|
||||||
handle.write(json.dumps(self.config, indent=4, sort_keys=True))
|
handle.write(json.dumps(self.config, indent=4, sort_keys=True))
|
||||||
|
|
Loading…
Reference in a new issue