falaw.content#
Content addressing for fal-produced media — identify a file by what it is.
fal serves generated media from a CDN URL, and fal’s own documentation is explicit that a URL carries neither identity nor durability:
“Each upload produces a unique URL with no shared namespace.”
“Expired files are permanently deleted and cannot be recovered.”
So a URL is the wrong handle for the two jobs falaw needs done:
Naming an artifact.
lacing.Artifact.asset_idis contractually the SHA-256 of the artifact’s bytes — “two artifacts with the sameasset_idare byte-identical regardless of where they live”. Hashing the URL instead produces a different id for two byte-identical renders and silently breaks every consumer that trusts that contract.Keying a cache. A byte-identical upstream regeneration must produce a downstream cache hit. With a URL in the key it produces a miss, and the miss re-bills a $0.35–$1.50 clip for work that did not change.
This module is the single place falaw turns a URL into a content hash. It
streams the bytes into a
lacing.ArtifactStoreblob store — which is content-addressed,dol-backed and swappable (in-memory / directory / object store) — rather than growing a second blob store of its own;remembers
url -> (content_hash, bytes_size, validators)in a small on-disk index, so re-executing an already-cached plan does not re-download anything. The index is a hint, never an identity, and it is only **sound for immutable URLs** — so falaw decides, per URL, whether it may be trusted (thorwhalen/falaw#23):a fal-served URL is trusted outright, because fal guarantees a URL is minted per upload and therefore never re-points at different bytes;
anything else — an arbitrary caller-supplied
https://…/reference.pngreached throughfalaw.cache.materialize_asset(), or afile://clip that gets re-rendered to the same path — is revalidated before reuse: a conditional request replaying the recordedETag/Last-Modified, or a(mtime, size)check for a local file.304costs a round-trip and no payload; a changed asset yields a changed content hash, as it must.
When falaw cannot check — no validators recorded, or an injected transport with no conditional-request support — it re-fetches rather than trusting. An unverifiable hint is not evidence.
is_immutable_url()is the predicate, andIMMUTABLE_URL_HOSTSis where you add a host you mint yourself;serves the bytes from the store after the URL has expired, so a months-old cache hit still yields a usable artifact instead of a dead link.
The blob store is injected, never constructed inline by callers:
default_content_store() is the falaw-cache-rooted default, and every
public function here takes a store keyword so a caller can point falaw at
an S3-backed store without touching any other code.
So is the transport. Per call it is the fetcher= / asset_fetcher=
argument; for a whole process it is using_url_fetcher(), which covers
content_ref_for_url(), falaw.materialize_asset(),
falaw.execute_plan() and falaw.execute_plan_isolated() in one
place. That matters most to downstream test suites: falaw content-addresses
every media result, so a suite that stubs the fal response but not the
transport resolves its made-up URLs for real — and stays green while doing it,
because a failed fetch degrades to a URL-only artifact with a warning rather
than raising. falaw.testing ships the fake so no consumer has to write
one.
Examples
>>> from lacing import ArtifactStore
>>> store = ArtifactStore.in_memory()
>>> ref = content_ref_for_url(
... "https://fal.media/files/one.png",
... store=store,
... fetcher=lambda url: [b"pretend-", b"png-bytes"],
... )
>>> ref.bytes_size
17
>>> ref.content_hash == __import__("hashlib").sha256(b"pretend-png-bytes").hexdigest()
True
The same bytes served at a different URL yield the same reference — which is the whole point:
>>> other = content_ref_for_url(
... "https://fal.media/files/two.png",
... store=store,
... fetcher=lambda url: [b"pretend-png-bytes"],
... )
>>> other.content_hash == ref.content_hash
True
Module Attributes
A callable that yields the bytes of a URL in chunks. |
Functions
|
Materialize |
The falaw-cache-rooted |
|
The transport used when no |
|
|
Whether |
|
The |
|
Make |
|
Materialize the blob for |
Classes
|
The result of asking an origin whether a URL's bytes changed. |
|
A content-addressed handle on some bytes falaw has materialized. |
|
What an origin gave us to ask "are these bytes still current?" cheaply. |
- class falaw.content.ConditionalOutcome(not_modified, chunks=None, validators=Validators(etag='', last_modified=''))[source]#
Bases:
objectThe result of asking an origin whether a URL’s bytes changed.
Either
not_modified(the remembered content hash still stands, at the cost of one round-trip and no payload), or the new bytes — carried here rather than re-requested, because a caller that discovers staleness has already paid for the response body.
- class falaw.content.ContentRef(content_hash, bytes_size)[source]#
Bases:
objectA content-addressed handle on some bytes falaw has materialized.
content_hashis the SHA-256 hex digest of the bytes — the valuelacing.Artifact.asset_idis contractually required to hold, and the value that goes into a downstream cache key in place of a URL.
- falaw.content.UrlFetcher#
A callable that yields the bytes of a URL in chunks.
The injection seam for tests and for callers that need custom transport (auth headers, retries, a local mirror).
default_url_fetcher()resolves the one in force;using_url_fetcher()installs another.
- class falaw.content.Validators(etag='', last_modified='')[source]#
Bases:
objectWhat an origin gave us to ask “are these bytes still current?” cheaply.
etagandlast_modifiedare the HTTP response headers of the same names, replayed asIf-None-Match/If-Modified-Sinceon the next read. Afile://URL has neither, so falaw synthesises anetagfrom the file’s(mtime_ns, size)— see_file_validators(). Falsy when the origin offered nothing, which is the case falaw must treat as “cannot revalidate”, never as “unchanged”.
- falaw.content.content_ref_for_url(url, *, store=None, fetcher=None, refresh=False, assume_immutable=None)[source]#
Materialize
url’s bytes intostoreand return their content hash.Idempotent and cheap on repeat, but how cheap depends on whether the URL can change behind falaw’s back — and falaw decides that rather than asking the caller to know (thorwhalen/falaw#23):
Immutable URL (fal’s own — see
is_immutable_url()) with a remembered hash whose blob is present: returned immediately, **no network**. This is what makes re-executing an already-cached plan free rather than re-downloading every clip.Mutable URL with a remembered hash: falaw revalidates — a conditional
GETreplaying the recordedETag/Last-Modified(forfile://, a(mtime, size)comparison). A304costs one round-trip and no payload; a200means the bytes really changed and the new ones are stored, so the content hash changes with them.No usable answer — nothing remembered, no validators recorded, or a transport that cannot make conditional requests: a plain fetch.
Step 3 is the important default. A transport that cannot revalidate makes falaw re-fetch, never trust: an unverifiable hint is not evidence.
- Parameters:
url (
str) – The asset URL.file://is supported and treated as mutable.store – Injected
lacing.ArtifactStore; defaults todefault_content_store().fetcher (
Optional[Callable[[str],Iterable[bytes]]]) – Injected byte source; defaults todefault_url_fetcher()(the built-inurllibtransport unlessusing_url_fetcher()has installed another). To support revalidation, a custom transport exposes aconditional_fetch(url, validators) -> ConditionalOutcomeattribute; without one it is simply never asked.refresh (
bool) – Skip every shortcut and re-fetch unconditionally. Rarely needed now that mutable URLs revalidate on their own — keep it for a URL whose origin lies about its validators.assume_immutable (
Optional[bool]) – Override the host-based decision.Truerestores the old unconditional trust (use for a host you mint yourself, and prefer adding it toIMMUTABLE_URL_HOSTS);Falseforces revalidation even for fal.
- Raises:
FalAssetFetchError – the bytes could not be retrieved, or the response was empty. Never returns a reference it could not back with bytes — a silent zero-byte “artifact” is the failure mode this guards.
- Return type:
- falaw.content.default_content_store()[source]#
The falaw-cache-rooted
lacing.ArtifactStore.Rooted at
<falaw cache dir>/content, so it moves with$FALAW_CACHE_DIR/$FALAW_DATA_DIRlike every other piece of falaw state. Constructed per call (the constructor only ensures directories exist) so a test that re-points the cache dir gets a fresh store.Deleting a blob removes it from disk rather than moving it to the OS trash.
dol.Files— the blob backendfrom_directorylays down — trashes by default, including on Linux, which is a kind default for a user’s own files and the wrong one here: every blob is derived data, and the only thing in falaw that deletes one isfalaw.prune.prune_content(), whose whole job is to give the volume its space back. A trashed blob frees nothing (the trash usually lives on the same volume) while the prune report says it did (thorwhalen/falaw#66). The deletion is still an explicit,dry_run-by-default operator act, so the trash was never the safety net.
- falaw.content.default_url_fetcher()[source]#
The transport used when no
fetcher=argument is given.using_url_fetcher()’s installed fetcher if there is one, else the built-inurllib-based one. Resolved at call time, so every falaw entry point that reads asset bytes —content_ref_for_url(),falaw.materialize_asset(),falaw.execute_plan()andfalaw.execute_plan_isolated()— honours an override installed after they were imported.
- falaw.content.is_immutable_url(url)[source]#
Whether
urlis guaranteed never to serve different bytes later.True only for hosts falaw knows mint a fresh URL per upload —
IMMUTABLE_URL_HOSTS, which is fal’s own CDN out of the box, and any subdomain of one. Everything else is mutable, includingfile://: a locally-rendered clip re-rendered to the same path is the textbook case of one URL serving two different things.The default is the safe one on purpose. Guessing “probably immutable” for an unknown host is what thorwhalen/falaw#23 was: a changed input producing an unchanged content address, silently, with the stale hash flowing into
Artifact.asset_idand every downstream cache key.Two rules make this a security boundary rather than a string comparison, and both were added after a review reproduced falaw#23 against the first cut of this function:
The host must be a plain DNS name.
urlsplit().hostnameis not what an HTTP client resolves. Python accepts a backslash (and%2F) inside a netloc, while WHATWG-conformant clients — includingrequests/urllib3— treat them as delimiters. Sohttp://127.0.0.1:8000\@fal.media/xparses here with hostnamefal.mediaand is fetched from127.0.0.1. Anything outside[a-z0-9.-]is therefore refused outright.Matching is label-wise, never by raw suffix.
endswith(".fal.media")is true ofevil.com\.fal.media;host == h or host.endswith("." + h)is not.
>>> is_immutable_url("https://v3b.fal.media/files/x.png") True >>> is_immutable_url("https://fal.media/files/x.png") True >>> is_immutable_url("https://example.com/reference.png") False >>> is_immutable_url("file:///tmp/render.mp4") False
A host that merely contains a trusted name is not that host, however it is spelled:
>>> is_immutable_url("https://fal.media@evil.example/x.png") False >>> is_immutable_url("https://notfal.media/x.png") False >>> is_immutable_url("http://127.0.0.1:8000\\@fal.media/x.png") False
- Return type:
- falaw.content.remembered_ref(url)[source]#
The
ContentRefpreviously recorded forurl, if any.A hint, not a guarantee: it says “the last time falaw fetched this URL, the bytes hashed to this” and says nothing about whether those bytes are still in any store. Callers must verify (
store.has_blob(...), or an already-materialized file on disk) before trusting it.Public because it is the cheap pre-check that lets a caller answer “do I already have this?” without a network round-trip — see
falaw.cache.materialize_asset().It says nothing about whether the URL still serves those bytes. For a mutable URL that question needs
content_ref_for_url(), which revalidates; this is the raw recorded value, and trusting it for a non-is_immutable_url()URL is thorwhalen/falaw#23.- Return type:
- falaw.content.using_url_fetcher(fetcher)[source]#
Make
fetcherfalaw’s default asset transport for the duration.The public seam for replacing falaw’s network transport wholesale — with an authenticated client, a retrying one, a local mirror, or (the common case) an in-memory fake in a test suite.
Prefer this to reaching for the module’s private default: it covers every entry point in one place, it needs no
monkeypatch, and it cannot be invalidated by an internal rename. It is also the only mechanism that reaches a call falaw makes from a thread you did not create — see_DEFAULT_FETCHERfor why that matters.An explicitly passed
fetcher=/asset_fetcher=still wins: this changes the default, never an explicit choice.Nests and restores, so an inner block cannot leak over an outer one:
>>> from lacing import ArtifactStore >>> store = ArtifactStore.in_memory() >>> before = default_url_fetcher() >>> with using_url_fetcher(lambda url: [b"faked"]): ... content_ref_for_url("https://fal.media/x.png", store=store).bytes_size 5 >>> default_url_fetcher() is before True
For a ready-made fake with pinned bytes and 404s, see
falaw.testing.
- falaw.content.write_blob_to_file(content_hash, path, *, store=None)[source]#
Materialize the blob for
content_hashas a copy atpath.Writes via a temporary file and an atomic rename, so a concurrent reader never sees a partial file.
- Return type:
Why a copy and not a hard link#
Hard-linking the store’s own blob file would be free, and an earlier cut of this function did exactly that — but a hard link makes the returned path and the content-store blob the same inode. Any consumer that writes through the returned path (ffmpeg writing in place, an accidental
open(p, "ab")) then leaves a blob whose SHA-256 no longer matches its own name, whilehas_blobkeeps answeringTrue— the store starts serving wrong bytes under a correct content address, which is precisely the failure class content addressing exists to eliminate. Making the link read-only instead is not an option either: the store’s backend writes blobs withopen(path, "wb"), so a read-only blob breaks re-putting identical bytes.A copy costs disk (and, on APFS/btrfs/XFS,
shutil.copyfilegets copy-on-write for free). Immutability of the content store is worth it.Returns
path.- raises KeyError:
the store holds no blob for
content_hash.