afwf Utility Tools ============================================================================== ``afwf`` ships a small set of optional utilities that come up in nearly every workflow. Unlike Alfred's official Python library — which bundles its own HTTP client, cache, etc. from scratch using only the standard library — ``afwf`` delegates to well-maintained third-party packages and keeps each utility in its own installable extra. Disk Cache (``afwf.opt.cache``) ------------------------------------------------------------------------------ **Install:** ``pip install afwf[cache]`` (requires `diskcache `_ ≥ 5.4) Alfred Script Filters run on every keystroke. Any handler that calls an external API, reads a large directory, or does heavy computation should cache its results between invocations. ``TypedCache`` is a thin subclass of ``diskcache.Cache`` that preserves the decorated function's type signature, keeping IDE completion intact. .. code-block:: python from pathlib import Path from afwf.opt.cache.api import TypedCache cache = TypedCache(str(Path.home() / ".cache" / "my-workflow")) @cache.typed_memoize(expire=300) # cache for 5 minutes def fetch_repos() -> list[str]: # expensive network call — only runs once per 5-minute window ... ``typed_memoize`` accepts the same arguments as ``diskcache.Cache.memoize``: - ``expire`` — TTL in seconds (``None`` = never expires) - ``name`` — override the cache key name - ``typed`` — treat argument types as part of the key - ``tag`` — group entries for bulk eviction - ``ignore`` — argument positions / names to exclude from the cache key Typical workflow pattern: .. code-block:: python import afwf.api as afwf from afwf.opt.cache.api import TypedCache from pathlib import Path cache = TypedCache(str(Path.home() / ".cache" / "my-workflow")) @cache.typed_memoize(expire=300) def get_items() -> list[afwf.Item]: ... # build items from a slow source def search(query: str = "") -> afwf.ScriptFilter: items = get_items() # served from disk after the first call sf = afwf.ScriptFilter() sf.items.extend(items) return sf Fuzzy Matching — Generic (``afwf.opt.fuzzy``) ------------------------------------------------------------------------------ **Install:** ``pip install afwf[fuzzy]`` (requires `rapidfuzz `_ ≥ 3.0) ``FuzzyMatcher`` is a generic, type-safe fuzzy matcher for any Python object. Subclass it, implement ``get_name()``, and you get ranked results for any item type. .. code-block:: python import dataclasses from afwf.opt.fuzzy.api import FuzzyMatcher @dataclasses.dataclass class Repo: id: int name: str class RepoMatcher(FuzzyMatcher[Repo]): def get_name(self, item: Repo) -> str | None: return item.name # return None to silently skip an item repos = [ Repo(id=1, name="apple and banana and cherry"), Repo(id=2, name="alice and bob and charlie"), ] matcher = RepoMatcher.from_items(repos) results = matcher.match("alice bob", threshold=0) # → [Repo(id=2, ...)] **``match()`` parameters:** - ``name`` — the search string - ``threshold`` — minimum similarity score 0–100 (default ``70``); set to ``0`` to return all ranked results - ``limit`` — maximum number of results (default ``20``) - ``filter_func`` — optional callable ``(match_tuple) -> bool`` for extra filtering after scoring **Factory methods:** - ``FuzzyMatcher.from_items(items)`` — builds the internal name→item map by calling ``get_name()`` on each item - ``FuzzyMatcher.from_mapper(name_to_item_mapper)`` — supply the map directly when you already have it Fuzzy Item Matching (``afwf.opt.fuzzy_item``) ------------------------------------------------------------------------------ **Install:** ``pip install afwf[fuzzy]`` ``afwf.opt.fuzzy_item`` wires ``FuzzyMatcher`` directly to Alfred :class:`~afwf.item.Item` objects. It provides two classes: - ``Item`` — an :class:`~afwf.item.Item` subclass with a ``set_fuzzy_match_name()`` method that stores the match name in ``variables`` - ``FuzzyItemMatcher`` — a ``FuzzyMatcher[Item]`` whose ``get_name()`` reads that variable This is the pattern you will use in most Script Filters that rank a static list: .. code-block:: python import afwf.api as afwf from afwf.opt.fuzzy_item.api import Item, FuzzyItemMatcher BOOKMARKS = [ ("Alfred App", "https://www.alfredapp.com/"), ("Python Docs", "https://docs.python.org/"), ("GitHub", "https://github.com/"), ("Stack Overflow", "https://stackoverflow.com/"), ] def search_bookmarks(query: str = "") -> afwf.ScriptFilter: items = [ Item(title=title, subtitle=url, arg=url) .open_url(url) .set_fuzzy_match_name(title) for title, url in BOOKMARKS ] if query.strip(): items = FuzzyItemMatcher.from_items(items).match(query, threshold=0) or items sf = afwf.ScriptFilter() sf.items.extend(items) return sf When ``query`` is empty the full list is returned in its original order. When ``query`` is non-empty the list is replaced with the fuzzy-ranked subset (falling back to the full list if nothing scores above the threshold).