Creating Plugins
This guide will show you how to create your own latz search backend hook.
These search backend hooks allow you to add additional image search APIs to latz.
Once complete, you will be able to use these new search backends with the latz search
command.
Check out latz-imgur on GitHub if you would like to skip ahead and browse the final working example.
Requirements
To follow along, you will need to create an Imgur account and register an application
via their web interface. Once complete, save the client_id
you receive as we will be using
that for this application. Head over to their documentation for more information.
For managing dependencies and to make it easier to publish to PyPI later, we use the tool poetry. Please install and configure this if you do not currently have it on your computer.
Setting up our environment
The first steps necessary are creating a directory for our project and adding a pyproject.toml
file. To do so, we run the following commands:
mkdir latz-imgur && cd latz-imgur
poetry init \
--name=latz-imgur \
--description="Imgur plugin for latz" \
--author="Your name <your-email@example.com" \
--python="^3.10" \
--dependency="latz" \
--dependency="pydantic" \
--dependency="httpx" \
--no-interaction
Note
The extra dependencies we install are latz
, pydantic
and httpx
. You will see exactly how we
use all three of these dependencies below.
Now that we have a directory and a pyproject.toml
file, we can use the following commands to initialize
our development environment:
# Install project dependencies including "latz"
poetry install
# Start a shell where we have access to all of our project dependencies
poetry shell
Create the plugin
At this point, you can open the folder (latz-imgur
in our example) with your favorite
IDE or text editor. The first thing we need to do is create a Python module called latz_imgur
at the top of the directory structure. Be sure this folder contains an __init__.py
file
so that it is recognized as a Python module 😉.
In that new Python module, we create a file called main.py
. This file will contain all the
code necessary for our Imgur plugin. Below, we go through each section of this file. Feel free
to incrementally add to this file as your go along or download the full version
here: latz_imgur/main.py.
Plugin configuration
Each new image API plugin may require a different set of settings. Latz recognizes this and
therefore allows you to dynamically add any new settings you wish. For our application, we add
the required access_key
setting that the Imgur API requires.
We define these extra settings as pydantic models.
from pydantic import BaseModel, Field
# Module level constant declaring the name of our plugin
PLUGIN_NAME = "imgur"
class ImgurBackendConfig(BaseModel):
"""
Imgur requires the usage of an ``access_key`` when using their API.
We expose these settings here so users of the CLI tool can configure it
themselves.
"""
access_key: str = Field(description="Access key for the Imgur API")
Note
Latz uses this ImgurBackendConfig
model to dynamically generate its own AppConfig
model at runtime. Check out Dynamic model creation in the
pydantic docs to learn more.
Search backend hook function
Now that our plugin is able to gather the configuration settings necessary to run (i.e. the
"access_key" we get from Imgur), we are ready to write the actual search API code. To make this
work, we need to define an async search function that returns a tuple
of ImageSearchResult
objects. Latz will pass an instance of the httpx.AsyncClient, the application
configuration and the search query to this function for us.
Below is an example of what this could look like:
Note
Click on the tool tips in the code to learn more
import urllib.parse
import httpx
from latz.exceptions import SearchBackendError
from latz.image import ImageSearchResult
#: Base URL for the Imgur API
BASE_URL = "https://api.imgur.com/3/"
#: Endpoint used for searching images
SEARCH_ENDPOINT = urllib.parse.urljoin(BASE_URL, "gallery/search")
async def search(client, config, query: str) -> tuple[ImageSearchResult, ...]: # (1)
"""
Search hook that will be invoked by latz while invoking the "search" command
"""
client.headers = httpx.Headers({
"Authorization": f"Client-ID {config.search_backend_settings.imgur.access_key}"
})
json_data = await _get(client, SEARCH_ENDPOINT, query)
return tuple(
ImageSearchResult( # (2)
url=record_image.get("link"),
width=record_image.get("width"),
height=record_image.get("height")
)
for record in json_data.get("data", tuple())
for record_image in record.get("images", tuple())
)
async def _get(client: httpx.AsyncClient, url: str, query: str) -> dict:
"""
Wraps `client.get` call in a try, except so that we raise
an application specific exception instead.
:raises SearchBackendError: Encountered during problems querying the API
"""
try:
resp = await client.get(url, params={"query": query})
resp.raise_for_status()
except httpx.HTTPError as exc:
raise SearchBackendError(str(exc), original=exc)
json_data = resp.json()
if not isinstance(json_data, dict):
raise SearchBackendError("Received malformed response from search backend")
return json_data
- The arguments passed to this function give you everything you need to make a search
request. The
client
is a httpx.AsyncClient, theconfig
object is the application configuration and thequery
string is the search string passed in from the command line. ImageSearchResult
is a special type defined by latz. Using this type helps ensure the result you return will be properly rendered.
Registering everything with latz
We are now at the final step: registering everything we have written with latz. To do this,
we need to use the latz.plugins.hookimpl
decorator to register our plugins. We do this
by decorating a function called search_backend
that returns a SearchBackendHook
object.
The SearchBackendHook
object is an object which has three fields:
name
: name of the plugin that users will use to specify it their configurationsearch
: async function that will be called to search for imagesconfig_fields
: Pydantic model representing the config fields we want to expose in the application
Here is what this function looks like:
from latz.plugins import hookimpl, SearchBackendHook
@hookimpl
def search_backend():
"""
Registers our Imgur image API backend
"""
return SearchBackendHook(
name=PLUGIN_NAME,
search=search,
config_fields=ImgurBackendConfig(access_key=""),
)
Wrapping up
In this guide, we showed how to create a latz search backend hook. The most important steps were:
- Creating our configuration fields, so we can allow users of the plugin to define necessary access tokens
- Creating the
search
function which returns atuple
ofImageSearchResult
objects. - Tying everything together by creating an
search_backend
function decorated bylatz.plugins.hookimpl
. This function's only responsibility is to return anSearchBackendHook
object that combines everything we have written in this module so far.
Thanks for following along and happy plugin writing ✌️