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
getexit with status code1. - Missing cache IDs for
existsexit with status code1. - Missing cache IDs for
deleteexit with status code1. - Missing cache IDs for
infoexit with status code1. - 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 tocache.
cache.set(content, cache_id)
Stores content under cache_id.
contentcan bestrorbytes.- Calling
set()again with the samecache_idoverwrites the existing cached content.
cache.get(cache_id)
Returns the cached value for cache_id.
- Returns
strfor text files. - Returns
bytesfor binary files. - Returns
Nonewhen the cache entry does not exist.
cache.get_entry(cache_id)
Returns a structured cached entry for cache_id.
- Returns a
dictwithcache_id,content,encoding, andmetadata. - Uses
encoding == "utf-8"for text content. - Uses
encoding == "base64"for binary content. - Returns
Nonewhen the cache entry does not exist.
cache.export()
Returns all cache entries in a JSON-compatible structure.
- Returns a
dictwith anentrieslist. - Each entry has the same
cache_id,content,encoding, andmetadatashape ascache.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 andencoding == "base64"binary content. - Raises
ValueErrorfor 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
Truewhen an entry was removed. - Returns
Falsewhen the cache entry does not exist. - Preserves unrelated cache entries.
cache.info(cache_id)
Returns metadata for cache_id.
- Returns a
dictwithcache_id,file_name,byte_size,content_type,created_at, andupdated_at. - Returns
Nonewhen 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 -quv run --dev pytest -q tests/test_packaging.pyuv build --wheeluv tool run unforgettable --helpuv tool run unforgettable --versionunforgettable --cache-folder .release-cache --create-cache-folder set notes "release check"unforgettable --cache-folder .release-cache --output json get notesunforgettable --cache-folder .release-cache --output json exists notesunforgettable --cache-folder .release-cache --output json info notesunforgettable --cache-folder .release-cache exportunforgettable --cache-folder .release-cache import --stdin < memory.jsonunforgettable --cache-folder .release-cache delete notesuvx unforgettable --helpwhere theuvxalias is installeduvx unforgettable --versionwhere theuvxalias is installedunforgettable --helpafter installing the built wheelpython -m unforgettable --helpafter installing the built wheelunforgettable --versionafter 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
807a4e7bcc7eb54d8577adb1063c93dcef44fa144c3113f03ff65d8104e7f4bc
|
|
| MD5 |
df8b81403aa933e7164dd5eab0ac91e8
|
|
| BLAKE2b-256 |
7f45137f2a2cba38710b873aecc7dcb6c72f7d8d5116bf84f322dba57b31fb9a
|
Provenance
The following attestation bundles were made for unforgettable-0.4.0.tar.gz:
Publisher:
publish-pypi.yml on bouli/unforgettable
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
unforgettable-0.4.0.tar.gz -
Subject digest:
807a4e7bcc7eb54d8577adb1063c93dcef44fa144c3113f03ff65d8104e7f4bc - Sigstore transparency entry: 1740676959
- Sigstore integration time:
-
Permalink:
bouli/unforgettable@30c84976ca944fb8659681e3a555c1bbae7e1bd1 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/bouli
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@30c84976ca944fb8659681e3a555c1bbae7e1bd1 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0c3e018553fe706140556fac722986afb7177c5af0f77f61fd2dccaf5ae7f4de
|
|
| MD5 |
4494f06b74a8aaefcdd84e96fa8e51cc
|
|
| BLAKE2b-256 |
c070197e8e75e31a2950e5ed51708272a62be941f962de3fa8b345d3fb8ed326
|
Provenance
The following attestation bundles were made for unforgettable-0.4.0-py3-none-any.whl:
Publisher:
publish-pypi.yml on bouli/unforgettable
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
unforgettable-0.4.0-py3-none-any.whl -
Subject digest:
0c3e018553fe706140556fac722986afb7177c5af0f77f61fd2dccaf5ae7f4de - Sigstore transparency entry: 1740676969
- Sigstore integration time:
-
Permalink:
bouli/unforgettable@30c84976ca944fb8659681e3a555c1bbae7e1bd1 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/bouli
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@30c84976ca944fb8659681e3a555c1bbae7e1bd1 -
Trigger Event:
push
-
Statement type: