Skip to main content

Unforgettable is a simple cache you can used for tiny repetitive executions.

Project description

Unforgettable - v0.4.0

Unforgettable is a tiny file-backed cache for Python code that repeats small pieces of work. It is useful for local scripts, notebooks, tests, prototypes, and automation where a value is expensive or annoying to compute more than once.

The library has no runtime dependencies and exposes one main class: unforgettable.

Installation

Install with uv:

uv add unforgettable

Or with pip:

pip install unforgettable

Unforgettable requires Python 3.11 or newer.

Quick Start

from unforgettable import unforgettable

cache = unforgettable()

cached_value = cache.get(cache_id="expensive-operation")
if cached_value is None:
    cached_value = "computed result"
    cache.set(content=cached_value, cache_id="expensive-operation")

print(cached_value)

get() returns the cached value when it exists. If there is no value for that cache_id, it returns None.

Use list() to inspect the cache IDs currently available in a cache folder.

from unforgettable import unforgettable

cache = unforgettable()
cache.set(content="computed result", cache_id="expensive-operation")

assert cache.list() == ["expensive-operation"]

Cache Text

from unforgettable import unforgettable

cache = unforgettable()

cache.set(content="hello", cache_id="greeting")

assert cache.get(cache_id="greeting") == "hello"

Cache Bytes

from unforgettable import unforgettable

cache = unforgettable()

content = b"\x80\x81cached bytes"
cache.set(content=content, cache_id="binary-value")

assert cache.get(cache_id="binary-value") == content

Persistent Cache Folder

By default, each cache instance creates its own temporary directory with tempfile. Use cache_folder when you want cached values to persist across Python process runs.

import os

from unforgettable import unforgettable

cache_dir = os.environ.get("UNFORGETTABLE_CACHE_DIR", ".unforgettable-cache")
cache = unforgettable(cache_folder=cache_dir)

cache.set(content="saved between runs", cache_id="stable-key")

Creating a new cache instance with the same folder can read values written by the previous instance.

from unforgettable import unforgettable

first_cache = unforgettable(cache_folder=".unforgettable-cache")
first_cache.set(content="persisted", cache_id="example")

second_cache = unforgettable(cache_folder=".unforgettable-cache")
assert second_cache.get(cache_id="example") == "persisted"

Command Line

Installing the package exposes an unforgettable command.

CLI commands use .unforgettable-memory in the current working directory by default, so you can run commands without passing --cache-folder:

unforgettable set greeting "hello"
unforgettable get greeting

Pass --cache-folder to use a different persistent cache folder:

unforgettable --cache-folder .unforgettable-cache list

When an explicitly selected cache folder does not exist, the CLI asks before creating it. Answer y or yes to create the folder and continue; any other answer exits without creating the folder or changing cache contents.

For unattended automation, choose the missing-folder policy explicitly:

unforgettable --cache-folder .unforgettable-cache --create-cache-folder set greeting "hello"
unforgettable --cache-folder .unforgettable-cache --no-create-cache-folder list

--create-cache-folder creates a missing explicitly selected folder without reading stdin. --no-create-cache-folder exits with status code 1 without creating the folder or changing cache contents. Prompts and errors are written to stderr; command values are written to stdout.

The list command prints one cache ID per line and prints nothing when the selected cache folder has no user-created entries.

Use --output json when an agent or script needs structured output:

unforgettable --cache-folder .unforgettable-cache --output json list

JSON list output has the stable shape {"cache_ids": ["example"]} and uses {"cache_ids": []} for an empty cache folder. Cache IDs with spaces and punctuation are preserved as JSON strings. Plain text remains the default.

Store text under a cache ID:

unforgettable --cache-folder .unforgettable-cache set greeting "hello"

Store text from stdin, including multiline or piped content:

printf 'first line\nsecond line\n' | unforgettable --cache-folder .unforgettable-cache set notes --stdin

Retrieve cached text:

unforgettable --cache-folder .unforgettable-cache get greeting

Retrieved text is written directly to stdout without labels or added trailing newlines, so cached values can be piped into other tools.

With --output json, get emits a structured object containing the cache ID, content, encoding marker, and metadata:

unforgettable --cache-folder .unforgettable-cache --output json get greeting

Text values use encoding: "utf-8" and place the text directly in content. Binary values use encoding: "base64" and place base64-encoded bytes in content, so JSON output remains parseable.

Export all cache entries as JSON:

unforgettable --cache-folder .unforgettable-cache export

The export command writes a JSON object to stdout with an entries array. Each entry has the same shape as JSON get: cache_id, content, encoding, and metadata. Empty cache folders export as {"entries": []}. Text and multiline values use encoding: "utf-8"; binary values use encoding: "base64" with base64-encoded content.

Import exported JSON from stdin:

unforgettable --cache-folder .unforgettable-cache import --stdin < memory.json

The import --stdin command upserts entries by cache ID and preserves unrelated entries in the selected cache folder. It exits with status code 1 and writes a stderr diagnostic for empty stdin, malformed JSON, or invalid import data.

Check whether a cache ID exists without retrieving content:

unforgettable --cache-folder .unforgettable-cache exists greeting

The exists command writes true or false in text mode. It exits with status code 0 when the cache ID exists and status code 1 when it is missing.

Delete one cache ID without clearing the whole cache folder:

unforgettable --cache-folder .unforgettable-cache delete greeting

The delete command exits with status code 0 after removing an entry. It exits with status code 1 and writes a diagnostic to stderr when the cache ID is missing.

Inspect metadata for a cache ID before retrieving content:

unforgettable --cache-folder .unforgettable-cache info greeting

The info command prints readable metadata in text mode. With --output json, it emits cache_id, file_name, byte_size, content_type, created_at, and updated_at.

Clean the selected cache folder:

unforgettable --cache-folder .unforgettable-cache clean

get exits with status code 1 when the requested cache ID is missing. info exits with status code 1 when the requested cache ID is missing.

Local AI-Agent Tool Contract

Agents can discover and run Unforgettable without adding it to the current project. Use uvx where that alias is available:

uvx unforgettable --help
uvx unforgettable --version

Use uv tool run as the portable equivalent:

uv tool run unforgettable --help
uv tool run unforgettable --version

Installed environments can use the console script or module execution:

unforgettable --help
python -m unforgettable --help

For compatibility-sensitive workflows, check unforgettable --version before depending on a specific command contract.

The CLI uses stable process behavior suitable for unattended tools:

  • Successful commands exit with status code 0.
  • Missing cache IDs for get exit with status code 1.
  • Missing cache IDs for exists exit with status code 1.
  • Missing cache IDs for delete exit with status code 1.
  • Missing cache IDs for info exit with status code 1.
  • Import failures exit with status code 1.
  • Rejected cache-folder creation exits with status code 1.
  • Invalid usage, such as missing required arguments, exits with status code 2.
  • Command values are written to stdout.
  • Diagnostics, errors, and prompts are written to stderr.

By default, CLI commands use .unforgettable-memory in the current working directory and create that default folder automatically. When --cache-folder selects a missing explicit folder, the CLI prompts on stderr before creating it. Agents should avoid prompts by choosing a policy:

unforgettable --cache-folder .agent-cache --create-cache-folder set run-id "started"
unforgettable --cache-folder .agent-cache --no-create-cache-folder list

Use --create-cache-folder to create the explicit folder without reading from stdin. Use --no-create-cache-folder to fail with status code 1 if the folder is missing.

Store multiline or piped content with set --stdin:

printf 'first line\nsecond line\n' \
  | unforgettable --cache-folder .agent-cache --create-cache-folder set notes --stdin

Retrieve content with get; the cached value is written directly to stdout without labels or added trailing newlines:

unforgettable --cache-folder .agent-cache get notes

Use JSON get when an agent needs content and metadata in one parseable response:

unforgettable --cache-folder .agent-cache --output json get notes

Check for a cache ID without retrieving content:

unforgettable --cache-folder .agent-cache exists notes

Text exists output is true or false. The command exits with status code 0 when the cache ID exists and status code 1 when it is missing.

Delete one obsolete entry without erasing unrelated memory:

unforgettable --cache-folder .agent-cache delete notes

delete exits with status code 0 when it removes an entry. It exits with status code 1 and writes a stderr diagnostic when the cache ID is missing.

Inspect metadata before retrieving content:

unforgettable --cache-folder .agent-cache info notes

info exits with status code 0 when metadata is available. It exits with status code 1 and writes a stderr diagnostic when the cache ID is missing.

Plain text output is the default. Use --output json when a command supports structured output:

unforgettable --cache-folder .agent-cache --output json list
unforgettable --cache-folder .agent-cache --output json get notes
unforgettable --cache-folder .agent-cache --output json exists notes
unforgettable --cache-folder .agent-cache --output json info notes
unforgettable --cache-folder .agent-cache export
unforgettable --cache-folder .agent-cache import --stdin < memory.json

The JSON list shape is stable:

{"cache_ids": ["notes"]}

The JSON exists shape is stable:

{"cache_id": "notes", "exists": true}

The JSON get shape is stable:

{"cache_id": "notes", "content": "first line\nsecond line\n", "encoding": "utf-8", "metadata": {"byte_size": 23, "cache_id": "notes", "content_type": "text/plain", "created_at": "2026-06-06T00:00:00+00:00", "file_name": "1.cache", "updated_at": "2026-06-06T00:00:00+00:00"}}

Binary content in JSON get output uses base64:

{"cache_id": "binary", "content": "gIFjYWNoZWQgYnl0ZXM=", "encoding": "base64", "metadata": {"byte_size": 14, "cache_id": "binary", "content_type": "application/octet-stream", "created_at": "2026-06-06T00:00:00+00:00", "file_name": "1.cache", "updated_at": "2026-06-06T00:00:00+00:00"}}

The JSON export shape is stable:

{"entries": [{"cache_id": "notes", "content": "first line\nsecond line\n", "encoding": "utf-8", "metadata": {"byte_size": 23, "cache_id": "notes", "content_type": "text/plain", "created_at": "2026-06-06T00:00:00+00:00", "file_name": "1.cache", "updated_at": "2026-06-06T00:00:00+00:00"}}]}

Empty cache folders export as:

{"entries": []}

The import --stdin command accepts the same JSON shape produced by export. It upserts matching cache IDs, keeps entries that are not present in the import bundle, and rejects malformed or invalid input before changing existing memory where practical.

The JSON info shape is stable:

{"byte_size": 12, "cache_id": "notes", "content_type": "text/plain", "created_at": "2026-06-06T00:00:00+00:00", "file_name": "1.cache", "updated_at": "2026-06-06T00:00:00+00:00"}

Existing cache folders that only have the legacy index and content files remain readable. When no manifest exists, info derives byte size, content type, and timestamps from the stored content file.

Custom Cache File Extension

Cache content files use the cache extension by default. You can choose a different extension when creating the cache instance.

from unforgettable import unforgettable

cache = unforgettable(
    cache_folder=".unforgettable-cache",
    cache_files_extension="txt",
)

cache.set(content="inspectable text", cache_id="example")

Cleaning A Cache

Call clean() on a cache instance to remove the files in that instance's cache folder.

from unforgettable import unforgettable

cache = unforgettable(cache_folder=".unforgettable-cache")
cache.set(content="temporary", cache_id="example")

cache.clean()

HTTP Request Example

This example caches successful HTTP responses by URL. Failed requests are not cached.

import requests

from unforgettable import unforgettable

cache = unforgettable(cache_folder=".request-cache")


def requests_get(url: str) -> bytes | None:
    cached_response = cache.get(cache_id=url)
    if cached_response is not None:
        return cached_response

    response = requests.get(url=url, timeout=5)
    if response.status_code != 200:
        return None

    cache.set(content=response.content, cache_id=url)
    return response.content


url = "https://github.com/bouli/unforgettable"
first_response = requests_get(url)
second_response = requests_get(url)

API Reference

unforgettable(cache_folder=None, cache_files_extension=None)

Creates a cache instance.

  • cache_folder: optional folder for cache files. When omitted, the instance uses a temporary directory.
  • cache_files_extension: optional extension for stored cache content files. Defaults to cache.

cache.set(content, cache_id)

Stores content under cache_id.

  • content can be str or bytes.
  • Calling set() again with the same cache_id overwrites the existing cached content.

cache.get(cache_id)

Returns the cached value for cache_id.

  • Returns str for text files.
  • Returns bytes for binary files.
  • Returns None when the cache entry does not exist.

cache.get_entry(cache_id)

Returns a structured cached entry for cache_id.

  • Returns a dict with cache_id, content, encoding, and metadata.
  • Uses encoding == "utf-8" for text content.
  • Uses encoding == "base64" for binary content.
  • Returns None when the cache entry does not exist.

cache.export()

Returns all cache entries in a JSON-compatible structure.

  • Returns a dict with an entries list.
  • Each entry has the same cache_id, content, encoding, and metadata shape as cache.get_entry(cache_id).
  • Returns {"entries": []} when no user cache entries exist.
  • Derives metadata for legacy cache folders that do not have a manifest.

cache.import_entries(exported_entries)

Imports entries from the JSON-compatible structure returned by cache.export().

  • Upserts entries by cache_id.
  • Preserves unrelated cache entries.
  • Supports encoding == "utf-8" text and encoding == "base64" binary content.
  • Raises ValueError for invalid import data before changing memory where practical.

cache.exists(cache_id)

Returns True when cache_id has a cached value and False when it does not.

cache.delete(cache_id)

Removes the cached value for cache_id.

  • Returns True when an entry was removed.
  • Returns False when the cache entry does not exist.
  • Preserves unrelated cache entries.

cache.info(cache_id)

Returns metadata for cache_id.

  • Returns a dict with cache_id, file_name, byte_size, content_type, created_at, and updated_at.
  • Returns None when the cache entry does not exist.
  • Derives metadata for legacy cache folders that do not have a manifest.

cache.list()

Returns a list[str] containing user-created cache IDs available in the cache folder.

  • Returns an empty list when no user cache entries exist.
  • Omits internal cache index bookkeeping.
  • Preserves cache IDs with spaces and punctuation.

cache.clean()

Removes files from the cache instance's folder.

Development

Install dependencies:

uv sync --dev

Run tests:

make tests

Run the coverage report:

make report

Build the package:

make build

Release Verification

Before publishing a release, run the local tests, build the wheel, and verify the agent-facing execution paths:

  • uv run --dev pytest -q
  • uv run --dev pytest -q tests/test_packaging.py
  • uv build --wheel
  • uv tool run unforgettable --help
  • uv tool run unforgettable --version
  • unforgettable --cache-folder .release-cache --create-cache-folder set notes "release check"
  • unforgettable --cache-folder .release-cache --output json get notes
  • unforgettable --cache-folder .release-cache --output json exists notes
  • unforgettable --cache-folder .release-cache --output json info notes
  • unforgettable --cache-folder .release-cache export
  • unforgettable --cache-folder .release-cache import --stdin < memory.json
  • unforgettable --cache-folder .release-cache delete notes
  • uvx unforgettable --help where the uvx alias is installed
  • uvx unforgettable --version where the uvx alias is installed
  • unforgettable --help after installing the built wheel
  • python -m unforgettable --help after installing the built wheel
  • unforgettable --version after installing the built wheel

If uvx is not installed, record that it was unavailable and use uv tool run unforgettable --help and uv tool run unforgettable --version as the equivalent ephemeral execution checks. The packaging test builds the wheel and verifies installed console script, module execution, and version output contracts. The agent memory smoke checks above exercise set, structured JSON get, exists, info, export, import --stdin, and delete; keep those commands passing before release.

See Also

License

This package is distributed under the MIT license.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

unforgettable-0.4.0.tar.gz (22.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

unforgettable-0.4.0-py3-none-any.whl (12.0 kB view details)

Uploaded Python 3

File details

Details for the file unforgettable-0.4.0.tar.gz.

File metadata

  • Download URL: unforgettable-0.4.0.tar.gz
  • Upload date:
  • Size: 22.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for unforgettable-0.4.0.tar.gz
Algorithm Hash digest
SHA256 807a4e7bcc7eb54d8577adb1063c93dcef44fa144c3113f03ff65d8104e7f4bc
MD5 df8b81403aa933e7164dd5eab0ac91e8
BLAKE2b-256 7f45137f2a2cba38710b873aecc7dcb6c72f7d8d5116bf84f322dba57b31fb9a

See more details on using hashes here.

Provenance

The following attestation bundles were made for unforgettable-0.4.0.tar.gz:

Publisher: publish-pypi.yml on bouli/unforgettable

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file unforgettable-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: unforgettable-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 12.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for unforgettable-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0c3e018553fe706140556fac722986afb7177c5af0f77f61fd2dccaf5ae7f4de
MD5 4494f06b74a8aaefcdd84e96fa8e51cc
BLAKE2b-256 c070197e8e75e31a2950e5ed51708272a62be941f962de3fa8b345d3fb8ed326

See more details on using hashes here.

Provenance

The following attestation bundles were made for unforgettable-0.4.0-py3-none-any.whl:

Publisher: publish-pypi.yml on bouli/unforgettable

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page