Skip to content

Commit

Permalink
New version
Browse files Browse the repository at this point in the history
This continues to make breaking tweaks in the application! (Buyer
beware!). I got rid of the `ImageSearchResultSet` object because I felt
it was a little too clunky to work with. I also made a better looking
output for when printing search resutls. It uses a table now.
  • Loading branch information
travishathaway committed Apr 9, 2023
1 parent d8d139e commit 0be7c7a
Show file tree
Hide file tree
Showing 10 changed files with 557 additions and 540 deletions.
17 changes: 7 additions & 10 deletions docs/creating-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ class ImgurBackendConfig(BaseModel):

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 an `ImageSearchResultSet`.
Latz will pass an instance of the [httpx.AsyncClient][httpx-async-client], the application
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][httpx-async-client], the application
configuration and the search query to this function for us.

Below is an example of what this could look like:
Expand All @@ -123,15 +123,15 @@ import urllib.parse
import httpx

from latz.exceptions import SearchBackendError
from latz.image import ImageSearchResultSet, ImageSearchResult
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) -> ImageSearchResultSet: # (1)
async def search(client, config, query: str) -> tuple[ImageSearchResult, ...]: # (1)
"""
Search hook that will be invoked by latz while invoking the "search" command
"""
Expand All @@ -140,7 +140,7 @@ async def search(client, config, query: str) -> ImageSearchResultSet: # (1)
})
json_data = await _get(client, SEARCH_ENDPOINT, query)

search_results = tuple(
return tuple(
ImageSearchResult( # (2)
url=record_image.get("link"),
width=record_image.get("width"),
Expand All @@ -150,9 +150,6 @@ async def search(client, config, query: str) -> ImageSearchResultSet: # (1)
for record_image in record.get("images", tuple())
)

return ImageSearchResultSet(
search_results, len(search_results), search_backend=PLUGIN_NAME
)

async def _get(client: httpx.AsyncClient, url: str, query: str) -> dict:
"""
Expand Down Expand Up @@ -218,8 +215,8 @@ were:

1. Creating our configuration fields, so we can allow users of the plugin to define necessary
access tokens
2. Creating the `search` function which returns an [`ImageSearchResultSet`][latz.image.ImageSearchResultSet]
object.
2. Creating the `search` function which returns a `tuple` of [`ImageSearchResult`][latz.image.ImageSearchResult]
objects.
3. Tying everything together by creating an `search_backend` function decorated by `latz.plugins.hookimpl`.
This function's only responsibility is to return an [`SearchBackendHook`][latz.plugins.hookspec.SearchBackendHook]
object that combines everything we have written in this module so far.
Expand Down
2 changes: 1 addition & 1 deletion latz/__version__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__title__ = "latz"
__description__ = "Tool for finding images. Maybe with location 🤷..."
__version__ = "0.1.5"
__version__ = "0.2.1"
33 changes: 31 additions & 2 deletions latz/commands/search.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
import asyncio
from collections.abc import Iterable, Callable
from functools import partial
from itertools import chain

import click
from rich import print as rprint
from rich.console import Console
from rich.table import Table

from latz import fetch
from latz.image import ImageSearchResult


def display_results(results: Iterable[ImageSearchResult]) -> None:
"""
Displays the `ImageSearchResultSet` objects as a `rich.table.Table`
"""
table = Table(title="Search Results")

table.add_column("#", no_wrap=True)
table.add_column("Link", style="magenta")
table.add_column("Backend", justify="right", style="green")

for idx, result in enumerate(results, start=1):
table.add_row(str(idx), result.url, result.search_backend)

console = Console()
console.print(table)


async def main(search_callables: Iterable[Callable]):
Expand All @@ -14,7 +34,11 @@ async def main(search_callables: Iterable[Callable]):
and prints the output of the query.
"""
results = await fetch.gather_results(search_callables)
rprint(results)

# merge results sets into single tuple
results = tuple(chain(*results))

display_results(results)


@click.command("search")
Expand All @@ -25,13 +49,18 @@ def command(ctx, query: str):
Command that retrieves an image based on a search term
"""
client = fetch.get_async_client()

# We collect all enabled backends here
search_backends = ctx.obj.plugin_manager.get_configured_search_backends(
ctx.obj.config
)

# We use `partial` to create a generator with callables preconfigured with the
# necessary arguments (e.g. `client`, `config` and `query`)
search_callables = (
partial(backend.search, client, ctx.obj.config, query)
for backend in search_backends
)

# This is the function call that kicks everything off
asyncio.run(main(search_callables))
12 changes: 1 addition & 11 deletions latz/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,4 @@ class ImageSearchResult(NamedTuple):
url: str | None
width: int | None
height: int | None


class ImageSearchResultSet(NamedTuple):
"""
Represents a result set of [`ImageSearchResult`][latz.image.ImageSearchResult] objects.
This is the object that must be returned on the `search` method of classes implementing
the [`ImageAPI`][latz.image.ImageAPI] protocol.
"""
results: tuple[ImageSearchResult, ...]
total_number_results: int | None
search_backend: str
search_backend: str | None
9 changes: 5 additions & 4 deletions latz/plugins/hookspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from pydantic import BaseModel

from latz.constants import APP_NAME
from latz.image import ImageSearchResultSet
from latz.image import ImageSearchResult

hookspec = pluggy.HookspecMarker(APP_NAME)
hookimpl = pluggy.HookimplMarker(APP_NAME)
Expand Down Expand Up @@ -50,15 +50,16 @@ def search_backend():
```
"""

search: Callable[[httpx.AsyncClient, Any, str], Awaitable[ImageSearchResultSet]]
search: Callable[
[httpx.AsyncClient, Any, str], Awaitable[tuple[ImageSearchResult, ...]]
]
"""
Callable that implements the search hook.
"""

config_fields: BaseModel
"""
Mapping defining the namespace for the config parameters, the pydantic
model to use and the default values it should contain.
Pydantic model that defines all the settings that this plugin needs.
**Example:**
Expand Down
25 changes: 11 additions & 14 deletions latz/plugins/image/placeholder.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

from ...image import (
ImageSearchResult,
ImageSearchResultSet,
)
from .. import hookimpl, SearchBackendHook

Expand All @@ -15,42 +14,40 @@

class PlaceholderBackendConfig(BaseModel):
"""
Unsplash requires the usage of an ``access_key`` and ``secret_key``
when using their API. We expose these settings here so users of the CLI
tool can use it.
Configuration for the placeholder backend. Currently only supports "bear"
or "kitten".
"""

type: Literal["bear", "kitten"] = Field(
default="kitten", description="The type of placeholder image links to use"
)


async def search(client, config, query: str) -> ImageSearchResultSet:
async def search(client, config, query: str) -> tuple[ImageSearchResult, ...]:
"""
Search function for placeholder backend. It only returns three pre-defined
search results. This is primarily meant for testing and demonstration purposes.
"""
placeholder_type = config.search_backend_settings.placeholder.type
base_url = f"https://place{placeholder_type}.com"
sizes = ((200, 300), (600, 500), (1000, 800))

results = tuple(
return tuple(
ImageSearchResult(
url=urljoin(base_url, f"{width}/{height}"),
width=width,
height=height,
search_backend=PLUGIN_NAME,
)
for width, height in sizes
)

return ImageSearchResultSet(
results=results, total_number_results=len(results), search_backend=PLUGIN_NAME
)


@hookimpl
def search_backend():
"""
Registers our Unsplash image API backend
Registers our placeholder search backend
"""
return SearchBackendHook(
name=PLUGIN_NAME,
search=search,
config_fields=PlaceholderBackendConfig(),
name=PLUGIN_NAME, search=search, config_fields=PlaceholderBackendConfig()
)
21 changes: 8 additions & 13 deletions latz/plugins/image/unsplash.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

from ...image import (
ImageSearchResult,
ImageSearchResultSet,
)
from .. import hookimpl, SearchBackendHook
from ...exceptions import SearchBackendError
Expand Down Expand Up @@ -53,32 +52,28 @@ async def _get(client: httpx.AsyncClient, url: str, query: str) -> dict:
return json_data


async def search(client: httpx.AsyncClient, config, query: str) -> ImageSearchResultSet:
async def search(
client: httpx.AsyncClient, config, query: str
) -> tuple[ImageSearchResult, ...]:
"""
Find images based on a `query` and return an `ImageSearchResultSet`
Find images based on a `query` and return a tuple of `ImageSearchResult` objects.
:raises SearchBackendError: Encountered during problems querying the API
"""
client.headers = httpx.Headers(
{
"Authorization": f"Client-ID {config.search_backend_settings.unsplash.access_key}"
}
)
access_key = config.search_backend_settings.unsplash.access_key
client.headers = httpx.Headers({"Authorization": f"Client-ID {access_key}"})
json_data = await _get(client, SEARCH_ENDPOINT, query)

search_results = tuple(
return tuple(
ImageSearchResult(
url=record.get("links", {}).get("download"),
width=record.get("width"),
height=record.get("height"),
search_backend=PLUGIN_NAME,
)
for record in json_data.get("results", tuple())
)

return ImageSearchResultSet(
search_results, len(search_results), search_backend=PLUGIN_NAME
)


@hookimpl
def search_backend():
Expand Down
Loading

0 comments on commit 0be7c7a

Please sign in to comment.