Implementing Your Own Backend¶
Support for this is still incredibly provisional.
In theory, all you need to do is implement the PicPocket
protocol and everything should work.
Unfortunately, getting initialize
or load
requires modifying the APIS dict in the base module, so it can’t yet be done in a way that allows use of the cli <api/cli>.
See the definition of testenv:py{311}-tests-postgres in tox.ini for information on how to run all tests against your backend.
Adding Support for a New DBAPI 2.0-Compatible Database¶
If you want to support a database that has an asynchronous DBAPI-compatible package, it’s fairly straight-forward. PicPocket’s PostgreSQL and SQLite backends use the same code for almost all operations, and should require very little changes to the non-shared code as features are added to PicPocket.
You will need to subclass picpocket.database.dbapi.DbApi
and picpocket.database.logic.SQL
.
In practice, this might be slightly more difficult if your database’s functionality differs greatly from both PostgreSQL and SQLite.
DbApi¶
- class picpocket.database.dbapi.DbApi(*args, **kwargs)¶
A DBAPI 2.0 Implementation of PicPocket
This class implements all required to support the PicPocket protocol other than the configuration and mounts properties and the parse_connection_info, initialize, get_api_version, get_version, and matching_version methods
To add support for a new database you need to implement those methods yourself as well as some new methods related to getting connection and cursor objects, and a class for generating sql statements (which is largely modelled on psycopg’s sql module).
It will be assumed that an implementation is using the tables listed as class attributes (presently, LOCATIONS_TABLE, TASKS_TABLE, TASK_INVOCATIONS_TABLE, IMAGES_TABLE), that each of those tables will be named that attribute name in lowercase and without the _table. For all tables, it will be assumed that all columns listed exist. If your table uses different types, you will need to override that class attribute
Todo
DbApi additionaly assumed a tags table (with columns id, name, escaped_name, depth, description) and an image_tags table (with columns image, tag).
Todo
DbApi only really does type formatting on columns it knows differ between the postgres and sqlite. Once PicPocket adds proper JSON support for filtering on exif data, the
Types
system will probably be addapted so that it accounts for input and output formats (e.g., postgres’s JSON types expects text input but provides a deserialized output vs. sqlite’s where input and output will both be strings but json operations are supported).- abstract property sql: SQL¶
The sql object to use when dynamically generating sql.
See
picpocket.types
- abstract async connect()¶
Establish a connection with the underlying database.
- Returns:
A dbapi-compatible Connection object
- abstract cursor(connection, commit: bool = False) AsyncContextManager ¶
Yield a cursor that rolls back on error and optionally commits
- Parameters:
connection – A connection object returned by connect
commit – Whether to commit on (successful) exit
- Returns:
A dbapi-compatible Cursor object
- async ensure_foreign_keys(cursor)¶
Ensure a cursor will support cascading changes.
This will be a no-op for most DBs (or an exception for DBs that cannot support it I guess?) and only exists because an SQLite cursor needs to run a pragma to enable this and we don’t want to do that on every single cursor creation.
- async import_data(path: Path, locations: list[str] | dict[str, Optional[pathlib.Path]] | None = None)¶
Import a PicPocket backup
Load data (locations, tasks, images, tags) from a PicPocket backup created using
export_data()
. This is the recommended way to migrate between backends.This method is safe to rerun as PicPocket will skip duplicates instead of overwriting.
Note
This file will not contain the images themselves, just the metadata you’ve created for the image (tags, captions, alt text, etc.).
- Parameters:
path – The JSON file to load data from.
locations – Only import data related to this set of locations. This can be supplied either as a list of locations or as a dict of location/path pairs. If a path is provided, it will be mounted prior to loading images and then unmounted afterward. When importing a location without a set path, this must be provided as a dict. For locations with paths definied within the backup, you can pass None as the path to use the default location.
- async export_data(path: Path, locations: list[str | int] | None = None)¶
Create a PicPocket backup
Export data (locations, tasks, iamges, tags) from PicPocket. This is the recommended way to migrate between backends.
Warning
This file will not contain the images themselves, just the metadata you’ve created for the image (tags, captions, alt text, etc.).
- Parameters:
path – Where to save teh JSON file to store data to.
locations – Only export images and task related to this location.
- async add_location(name: str, path: Path | None = None, *, description: str | None = None, source: bool = False, destination: bool = False, removable: bool = True) int ¶
Add a new location to PicPocket.
Locations represent sources to fetch images from (e.g., cameras) and storage locations to store data in. A location must be specified either as a source to import images from, a destination to import images to, or both.
- Parameters:
name – The (unique) name of the location being added. E.g. photos, “EOS 6D”, phone
path – The path where the location is/gets mounted. If this path is not consistent (or not unique), you may want to leave this blank and instead mount/unmount the drive as necessary.
description – A text description of what the location is/how it is used. E.g., “main photo storage”, “my digital camera”
source – Whether the location is somewhere images may be copied from (to a new location)
destination – Whether the location is somewhere images may be compied to.
removable – Whether the location is a permanent or removable storage device.
- Returns:
The integer id representing the location
- async edit_location(name_or_id: str | int, new_name: str | None = None, /, *, path: ~pathlib.Path | ~picpocket.internal_use.NotSupplied | None = <picpocket.internal_use.NotSupplied object>, description: str | ~picpocket.internal_use.NotSupplied | None = <picpocket.internal_use.NotSupplied object>, source: bool | None = None, destination: bool | None = None, removable: bool | None = None)¶
Edit a PicPocket location.
Edit information about about a PicPocket location.
- Parameters:
name/id – the name or id of the location.
new_name – What to rename the location to.
path – Where the location gets mounted.
description – A description of the location.
source – Whether the location is a place images are imported from
destination – Whether the location is a place images are imported to.
removable – Whether the location is a permanent or removable storage device.
- async remove_location(name_or_id: int | str, /, *, force: bool = False)¶
Remove a location from PicPocket.
The location will not be removed if it has images associated with it unless explicitly asked. Even in that case, it will not touch the image files on disk.
- Parameters:
name/id – the name or of the location.
force – Remove the location even if it currently has images associated with it and remove those images from the database.
- Returns:
Whether the location was deleted.
- async get_location(name_or_id: str | int, /, *, expect: bool = False) Location | None ¶
Get information about a location
- Parameters:
name/id – The location to find
expect – Error if the location doesn’t exist
- Returns:
The matching location if it exists.
- async list_locations() list[picpocket.database.types.Location] ¶
Fetch all known locations
- async mount(name_or_id: str | int, /, path: Path)¶
Supply a path where a location is currently available
Mount a location to a specific path. For removable locations without unique or consistent mount points, this is how you alert PicPocket to its present location. This will also work for locations that have set attach points but are temporarily available at a different location. Mounting an already-mounted location will replace the previous mount point.
- Parameters:
name/id – The location to mount
path – Where to mount the location
- async unmount(name_or_id: str | int)¶
Remove a temporary mount point
Unmount a location that was previously mounted. Unmounting a drive that isn’t mounted won’t cause an error.
- Parameters:
name/id – The location to mount
- async import_location(name_or_id: str | int, /, *, file_formats: set[str] | None = None, batch_size: int = 1000, creator: str | None = None, tags: list[str] | None = None) list[int] ¶
Import all images from a location
Import all images that are stored within a location. If an image already exists within PicPocket, its user-supplied won’t be updated but properties of the file itself (such as dimensions or exif data) will be updated in PicPocket if its changed on disk.
- Parameters:
name/id – The location to import
file_formats – Only import images that have these suffixes. Suffixes can be supplied with or without leading dot. If not supplied, the PicPocket default file formats will be imported.
batch_size – How often to commit while importing images.
creator – Mark all images as having been created by this person.
tags – Apply all these tags to all imported images.
- Returns:
The ids of all images that were added (or edited)
- async add_task(name: str, source: str | int, destination: str | int, *, description: str | None = None, creator: str | None = None, tags: list[str] | None = None, source_path: str | None = None, destination_format: str | None = None, file_formats: set[str] | None = None, force: bool = False)¶
Add a repeatable task to PicPocket.
Todo
Perform operations on images, clean up path stuff, scheduled and automatic tasks
Warning
This method will be changing substantially as support for other types of task are added.
Add a task to PicPocket. Currently, all supported tasks are imports from one known location to another. Support for things like converting images on import are forthcoming. Images will be copied from the source location without modifying the source image. Tasks will refuse to overwrite existing files on import.
see
picpocket.tasks
- Parameters:
name – What to call the task (must be unique)
source – The name/id of the location to import images from
destination – The name/id of the location to copy images to
creator – Who to list as the creator of all imported images
tags – Apply these tags to all imported images
source_path –
Where, (relative to the source location’s root) to start looking for images. If not supplied, root will be used. The following dyamic directory names are allowed:
{year}: A year
{month}: A month
{day}: A day
{date:FORMAT}: A date in the supplied format
{regex:PATTERN}: A name that matches a regex
{str:RAW}: A folder name that contains {
destination_format –
Where (relative to the destination root) images should be copied to. This is supplied as a format string with the following allowed formatters:
directory: The source filepath (relative to the source) root.
file: The file’s name
name: The file’s name (without extension)
extension: The file’s extension (without the leading dot).
uuid: A UUID4 hex
date: A date object representing the source image’s last modified time. You can supply the format to save the date using strftime formatting (e.g. date:%Y-%m-%d-%H-%M-%S)
hash: A hash of the image contents
index: A 1-indexed number representing the order images are encountered by the running task. PicPocket makes no promises about the order images are added
file_formats – Only import images that have these suffixes. Suffixes can be supplied with or without leading dot. If not supplied, the PicPocket default file formats will be imported.
force – Overwrite any previously-existing task with this name
- async run_task(name: str, *, since: datetime | None = None, full: bool = False, tags: list[str] | None = None) list[int] ¶
Run a task
Todo
Allow offsets for last ran.
Running a task will never overwrite an image file, but it will re-add images that were deleted if the source image still matches all criteria. When running a task, the time the task previously ran will be supplied and any specified date-based path segments from before that last ran date will be skipped, as will any files with last-modified dates that are earlier.
- Parameters:
name – The name of the task to run
since – Ignore files with last-modified dates older than this date and in folders representing earlier dates. Ignored if full is supplied.
full – Don’t filter directories based on the last-run date of the task.
tags – Any tags to include to this specific run
- Returns:
The ids of all imported images.
- async remove_task(name: str)¶
Remove a task
Running this on a non-existent task will not error.
- Parameters:
remove (name The name of the task to) –
- async list_tasks() list[picpocket.database.types.Task] ¶
Get all tasks in PicPocket
- async add_image_copy(source: Path, name_or_id: str | int, destination: Path, /, *, creator: str | None = None, title: str | None = None, caption: str | None = None, alt: str | None = None, rating: int | None = None, tags: list[str] | None = None) int ¶
Copy an image into PicPocket
Create a copy of a supplied image and then add that image to PicPocket.
- Parameters:
source – The image to create a copy of
name/id – The location to create a copy of the image in
destination – The path (relative to the location root) to copy the image to
creator – Who made the image
title – The title of the image
caption – A caption of the image
alt – Descriptive text of the image to be used as alt text
rating – A numeric rating of the image
tags – Any number of tags to apply to the image
- Returns:
The id of the copied image
- async edit_image(id: int, *, creator: str | ~picpocket.internal_use.NotSupplied | None = <picpocket.internal_use.NotSupplied object>, title: str | ~picpocket.internal_use.NotSupplied | None = <picpocket.internal_use.NotSupplied object>, caption: str | ~picpocket.internal_use.NotSupplied | None = <picpocket.internal_use.NotSupplied object>, alt: str | ~picpocket.internal_use.NotSupplied | None = <picpocket.internal_use.NotSupplied object>, rating: int | ~picpocket.internal_use.NotSupplied | None = <picpocket.internal_use.NotSupplied object>)¶
Edit information about an image.
Edit user-supplied metadata about an image. For all properties, supplying None will erase the existing value of that property
- Parameters:
id – The image to edit
creator – Who made the image
title – The title of the image
caption – A caption of the image
alt – Descriptive text of the image to be used as alt text
rating – A numeric rating of the image
- async tag_image(id: int, tag: str)¶
Apply a tag to an image
Tags in PicPocket are nestable, and are supplied as lists of strings (e.g., [“bird”, “goose”, “Canada Goose”]). When adding a tag, it’s recommended that you don’t add its parents.
- Parameters:
id – The image to tag
tag – The tag to apply
- async untag_image(id: int, tag: str)¶
Remove a tag from an image
Todo
allow cascading?
Removing a non-existent tag will not error.
- Parameters:
id – The image to untag
tag – The tag to remove
- async move_image(id: int, path: Path, location: int | None = None)¶
Move an image.
PicPocket will attempt to move both the on-disk location as well as its internal record. If the current file location is empty and a file exists at the destination, PicPocket will assume the file was already moved on-disk and will update its records. In all other cases, if a file exists at the destination (either on-disk or in PicPocket), PicPocket will error.
- Parameters:
id – the image to move
path – The new location (relative to the root of its destination location).
location – The id of the location to move the image to. If unsupplied, the file will be moved within its source location.
- async remove_image(id: int, *, delete: bool = False)¶
Remove an image from PicPocket
PicPocket will only delete the file on-disk if specifically requested.
- Parameters:
id – The image to remove from PicPocket
delete – Delete the image on-disk as well. If True, the image will only be removed from PicPocket if the delete is successful
- async find_image(path: Path, tags: bool = False) Image | None ¶
Attempt to find an image given its path
- Parameters:
path – The full path to the image
tags – Whether to fetch the image’s tags
- Returns:
The image, if it exists in PicPocket
- async get_image(id: int, tags: bool = False) Image | None ¶
Get an image in PicPocket
- Parameters:
id – The image to fetch
tags – Whether to fetch the image’s tags
- Returns:
The image matching the id (if it exists)
- async count_images(filter: Comparison | None = None, tagged: bool | None = None, any_tags: list[str] | None = None, all_tags: list[str] | None = None, no_tags: list[str] | None = None, reachable: bool | None = None) int ¶
Count the number of images in PicPocket
Get the number of images in PicPocket that match a criteria.
- You can filter the result on any of the following properties:
name: The name of the image (without extension)
extension: The file suffix (without leading dot)
creator: Who created the image
location: Only include images at this location
path: The text path, relative to its location’s root
title: The title of the image
caption: The image caption
alt: The image alt text
rating: The rating of the image.
- Parameters:
filter – Only include images that match this filter
tagged – Only include images that are(n’t) tagged
any_tags – Only include images that are tagged with at least one of the supplied tags
all_tags – Only include images that are tagged with all of the supplied tags
no_tags – Don’t include images that have any of the supplied tags
reachable – Only include images that are currently reachable
- Returns:
The number of images that match the criteria
- async get_image_ids(filter: Comparison | None = None, *, tagged: bool | None = None, any_tags: list[str] | None = None, all_tags: list[str] | None = None, no_tags: list[str] | None = None, order: tuple[str, ...] | None = None, limit: int | None = None, offset: int | None = None, reachable: bool | None = None) list[int] ¶
Get the ids of images in PicPocket
Get the ids of images in PicPocket that match a criteria.
You can filter and order the result on any of the following properties:
name: The name of the image (without extension)
extension: The file suffix (without leading dot)
creator: Who created the image
location: Only include images at this location
path: The text path, relative to its location’s root
title: The title of the image
caption: The image caption
alt: The image alt text
rating: The rating of the image.
When ordering images, properties will be sorted in ascending order by default (e.g., for ratings: Lower rated images will be supplied first). Putting a minus sign (-) in front of that property will sort in descending order instead. You can also use the exclamation mark (!) to specify whether images lacking a property (e.g., unrated images) should be sorted at the beginning or the end (e.g., !rating will order unrated images and then worst-to-best; rating! will order unrated images after the best images). When supplying unspecified properties in descending order, the exclamation mark should be supplied after the minus sign (e.g., -!rating will return untagged images and then best-to-worst). As well as above properties, ‘random’ can be used to order images randomly. Random ordering can be done after properties (e.g., -!rating,random will return images unrated and then best to worst, but with unrated images and images with a given rating will be returned in a random order).
Note
Orderings are not preserved across invocations, so running repeatedly with increasing offsets is not guaranteed to include all images or supply each images exactly once. That said, if you don’t add/edit/remove images between invocations (and don’t use random ordering), it should be fine.
- Parameters:
filter – Only include images that match this filter
tagged – Only include images that are(n’t) tagged
any_tags – Only include images that are tagged with at least one of the supplied tags
all_tags – Only include images that are tagged with all of the supplied tags
no_tags – Don’t include images that have any of the supplied tags
reachable – Only include images that are currently reachable
order – how to sort the returned ids as a list of properties
limit – Only return this many image ids
offset – If limit is supplied, start returing images from this offset.
reachable – Only include images that are(n’t) currently reachable
- Returns:
The ids of all images that match this criteria
- async search_images(filter: Comparison | None = None, *, tagged: bool | None = None, any_tags: list[str] | None = None, all_tags: list[str] | None = None, no_tags: list[str] | None = None, order: tuple[str, ...] | None = None, limit: int | None = None, offset: int | None = None, reachable: bool | None = None) list[picpocket.database.types.Image] ¶
Search images in PicPocket
Get images in PicPocket that match a criteria.
You can filter and order the result on any of the following properties:
name: The name of the image (without extension)
extension: The file suffix (without leading dot)
creator: Who created the image
location: Only include images at this location
path: The text path, relative to its location’s root
title: The title of the image
caption: The image caption
alt: The image alt text
rating: The rating of the image.
When ordering images, properties will be sorted in ascending order by default (e.g., for ratings: Lower rated images will be supplied first). Putting a minus sign (-) in front of that property will sort in descending order instead. You can also use the exclamation mark (!) to specify whether images lacking a property (e.g., unrated images) should be sorted at the beginning or the end (e.g., !rating will order unrated images and then worst-to-best; rating! will order unrated images after the best images). When supplying unspecified properties in descending order, the exclamation mark should be supplied after the minus sign (e.g., -!rating will return untagged images and then best-to-worst). As well as above properties, ‘random’ can be used to order images randomly. Random ordering can be done after properties (e.g., -!rating,random will return images unrated and then best to worst, but with unrated images and images with a given rating will be returned in a random order).
Note
Orderings are not preserved across invocations, so running repeatedly with increasing offsets is not guaranteed to include all images or supply each images exactly once. That said, if you don’t add/edit/remove images between invocations (and don’t use random ordering), it should be fine.
- Parameters:
filter – Only include images that match this filter
tagged – Only include images that are(n’t) tagged
any_tags – Only include images that are tagged with at least one of the supplied tags
all_tags – Only include images that are tagged with all of the supplied tags
no_tags – Don’t include images that have any of the supplied tags
reachable – Only include images that are currently reachable
order – how to sort the returned ids as a list of properties
limit – Only return this many image ids
offset – If limit is supplied, start returing images from this offset.
reachable – Only include images that are(n’t) currently reachable
- Returns:
The images that match this criteria
- async verify_image_files(*, location: int | None = None, path: Path | None = None, reparse_exif: bool = False) list[picpocket.database.types.Image] ¶
Check that images in PicPocket exist on-disk
Check that images exist on-disk and that stored file information is accurate. If file metadata (e.g., dimensiosns, exif data) has changed, it will be updated as necessary. PicPocket will not remove missing images from its database.
This method will skip any removable locations that aren’t currently attached.
- Parameters:
location – Only check images in this location
path – Only scan images within this directory
reparse_exif – Reread EXIF data even if the image hasn’t been touched
- Returns:
All images that exist in PicPocket that can’t be found on-disk.
- async add_tag(tag: str, description: str | ~picpocket.internal_use.NotSupplied | None = <picpocket.internal_use.NotSupplied object>)¶
Add a tag to PicPocket
Tags in PicPocket are nestable, and are supplied as lists of strings (e.g., [“bird”, “goose”, “Canada Goose”]). You don’t need to explicitly add tags (tagging an image will create that tag). You only need to add a tag if you want to have a description associated with a tag.
Adding an existing tag will not error, and if supplied, the description will be updated.
- Parameters:
tag – The tag to add
description – A description of the tag
- async move_tag(current: str, new: str, /, cascade: bool = True) int ¶
Move a tag to a new name
If a tag already exists with the same name, the two tags will be merged (keeping that tag’s description).
Note
Cascade is always based on the supplied tag, not where the change was made in the tag. Even if you edit a parent section of the tag’s name, it will only cascade to this tag’s children.
- Parameters:
current – The name of the tag
new – The new name of the tag
cascade – Whether to move child tags as well.
- Returns:
The number of moved tags
- async remove_tag(tag: str, cascade: bool = False)¶
Remove a tag from PicPocket
- Parameters:
tag – The tag to remove
cascade – If True, also remove any tags that are descendents of this tag
- async get_tag(tag: str, children: bool = False) Tag ¶
Get the description of a tag
- Parameters:
name – The tag to get the description of
children – Fetch children as well
- Returns:
The tag
- async all_tag_names() set[str] ¶
Get the names of all used tags
- Returns:
The list of tags
- async all_tags() dict[str, Any] ¶
Return the tags PicPocket is aware of
- Returns:
A recursive dict representing the known tag structure within PicPocket. The outer level will be the names of root level tags and for each tag, their value will be a dict containing their description and the dict of their child tags.
- async get_tag_set(*image_ids: int, minimum: int = 1) dict[str, int] ¶
Get the set of tags applied to any of a set of images
- Parameters:
image_ids – The images to find tags for
minimum – Only return tags that have been applied to this many images.
- Returns:
A dict with the tags as keys and their counts
- async create_session(data: dict[str, Any], expires: datetime | None = None) int ¶
Store temporary data about a user query
- Parameters:
data – The JSON-serializable data
expires – The expiration date for the data (if not supplied, the default will be used).
- Returns:
the session id
- async get_session(session_id: int) dict[str, Any] | None ¶
Fetch session data
- Parameters:
session_id – The id of the session
- Returns:
the session data, if it exists
- async prune_sessions()¶
remove any expired sessions
SQL¶
- class picpocket.database.logic.Types(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)¶
Types of PicPocket property
- BOOLEAN = 'boolean'¶
- DATETIME = 'datetime'¶
- ID = 'id'¶
- JSON = 'json'¶
- NUMBER = 'number'¶
- TEXT = 'text'¶
- class picpocket.database.logic.SQL¶
A class for configuring how to manage SQL for a given database.
This includes some checks for what features the database supports as well as methods for crafting safe query strings the database will accept. The latter are modelled on psycopg’s sql <https://www.psycopg.org/psycopg3/docs/api/sql.html> module with the default implementations providing the support SQLite would require.
- abstract property types: set[picpocket.database.logic.Types]¶
The supported data types
- property arrays: bool¶
Whether the database supports arrays
- abstract property param: str¶
The symbol the database uses for positional paramaters.
We can’t just use ‘paramstyle’ because APIs are inconsistent on whether they use that to refer to their positional or named param style.
Warning
This method should not be used in conjunction with any of the below formatting methods!
- Returns:
a symbol to use when generating SQL that uses positional placeholders.
- format(statement: str, *parts)¶
add sections (e.g., identifiers, placeholders) to a statement
The base statement should be using {} for positional formatting and each part being formatted in should represent already-formatted sql (using either format or any of the other sql methods).
- Parameters:
statment – The statement to format
parts – the sections to add to the statement
- Returns:
The formatted statement.
- join(joiner: str, parts: Iterable)¶
join together several sections of a statement
Parts should already be formatted by another sql format method before being passed in to join.
- Parameters:
joiner – The string to use to join the sections
parts – The sections to join together
- Returns:
The formatted section
- identifier(identifier: str)¶
Format an identifier (i.e., table, column names)
Warning
The default implementation is written with the assumption that identifiers will only be passed in from a reliable source (i.e., PicPocket’s own API) and not from user input. This method is not thorough enough to be trusted with user-supplied identifiers.
- Parameters:
identifier – the table/column name
- Returns:
A formatted version of the identifier
- literal(literal: Any)¶
Format a literal (e.g., a number or string)
- Parameters:
literal – the literal to format
- Returns:
A formatted version of that literal
- abstract placeholder(placeholder: str)¶
Format a named placeholder
- Parameters:
placeholder – the placeholder to format
- Returns:
A formatted version of that placeholder
- property escape: str | None¶
The symbol used to escape search terms for LIKE queries