Bucket
Buckets: files by key, private by default, with signed links and an S3-compatible escape hatch.
bucket = noclick.Bucket.from_name("uploads", create_if_missing=True)
bucket.put("invoices/0042.pdf", pdf_bytes, content_type="application/pdf")
link = bucket.signed_url("invoices/0042.pdf", expires_in=3600)A key is a path-like name (invoices/0042.pdf); / separates folders only
by convention, so listing with delimiter="/" shows them as folders.
class BucketA bucket of files by key. Private by default: files are reached through
this client, a signed_url or S3 credentials. A public bucket also serves
every file at url(key).
- Like every live object, it also has hydrate(), refresh() and raw.
| id | str | |
| name | str | |
| public | bool | Whether every file is readable at |
| size_bytes | int | None | |
| object_count | int | None | |
| public_base_url | str | None | Where a public bucket serves its files (None while it's private). |
| used_by | list[dict[str, Any]] | Who uses it: apps that bind it ( |
| notifications | list[dict[str, Any]] | Where it tells about new files: each with the |
| region | str | None | The region it keeps its files in ( |
| user_id | str | None | |
| created_at | Any | |
| console_url | str | None | This bucket in the developer console (None off the hosted API). |
only
bucket.only(*operations) -> BucketOperationsA tool ref to some of its operations, for tools=: "objects" (list
keys under a prefix), "head" (one file's size, type and metadata),
"put" (store a file from a url or from text, with a content type and
metadata; the model never handles bytes), "signed_url" (a link that reads a
file, to hand a person) and "remove". The bucket itself in an agent's
tools= gives all five, as {name}__objects and so on. Its calls follow
your policy as bucket.objects, bucket.put and the rest (bucket.*). A
file the agent's browser downloads comes back to it as a signed URL, which
put stores from url. An agent may use its own scope's buckets and the
account's. In a toolkit or a hosted MCP server, a bucket is in invite-only
beta.
A ref to just those operations, to list in tools=; bind(...) fixes arguments.
reports = noclick.Bucket.from_name("reports")
analyst = noclick.Agent.from_name(
"analyst",
create_if_missing=True,
instructions="You write the weekly report, save it, and share a link to it.",
tools=[reports.only("put", "signed_url"), "search"],
)createclassmethodasync
noclick.Bucket.create(
name, *, public=False, region=None, user=..., client=None
) -> BucketCreate a bucket called name, unique to its user (a taken name raises
noclick.Conflict with existing_id). public=True serves every
file at a plain URL. region (noclick.regions()) is where its files
are kept, for good; without one, your account's default region, else
us-east. Object storage takes it as a location hint: the bucket is
made there when the store has room there, which is a placement, not a
legal promise of residency. A name is 1-64 letters, digits and . _ -.
FalseNoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)The new, empty bucket.
Private files your app hands out by signed link:
uploads = noclick.Bucket.create("uploads")Public assets, such as images on a site:
assets = noclick.Bucket.create("public-assets", public=True)
print(assets.public_base_url)Files that stay in Europe:
records = noclick.Bucket.create("patient-records", region="eu")from_nameclassmethod
noclick.Bucket.from_name(
name, *, create_if_missing=False, public=False, region=None, user=..., client=None
) -> BucketA lazy reference to the bucket called name in the current (or given)
user. With create_if_missing=True it's created on first use (in region,
as create); one that exists is used as it is (change it with update).
Inside a deployed app (signed in by its deployment) the name is looked up in the app's own
namespace for the environment the code runs in, and one made there is the app's: a name bound to
the app by hand wins, and in a preview of an app whose preview_resources is branch the
branch gets an overlay at its first write: what it writes is its own, and it reads
production's files through it, so a preview never changes production's data.
FalseFalseNoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)A bucket that loads (or is created) the first time it's used. Nothing is sent yet.
Your code is the definition; the first run creates it:
bucket = noclick.Bucket.from_name("uploads", create_if_missing=True)
bucket.put("hello.txt", "hi")A bucket per user, for what their agent saves:
with noclick.as_user("user_42"):
files = noclick.Bucket.from_name("assistant-files", create_if_missing=True)
print(files.objects(prefix="receipts/"))from_idclassmethod
noclick.Bucket.from_id(bucket_id, *, client=None, user=...) -> BucketA lazy reference to a bucket by id.
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
A bucket that loads the first time you read a field. Nothing is sent yet.
bucket = noclick.Bucket.from_id("8d1f3b27-5a6e-4c09-b2d8-9e4a7c1f0b35")
data = bucket.read("invoices/0042.pdf")getclassmethodasync
noclick.Bucket.get(bucket_id, *, user=..., client=None) -> BucketLoad a bucket by id now; an unknown id raises noclick.NotFound.
Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)The bucket, loaded: whether it's public, how many files it holds and their total size.
bucket = noclick.Bucket.get("8d1f3b27-5a6e-4c09-b2d8-9e4a7c1f0b35")
print(bucket.name, bucket.public, bucket.object_count, bucket.size_bytes)listclassmethodasync
noclick.Bucket.list(
*, limit=None, user_filter=None, user=..., client=None
) -> list[Bucket]The current user's buckets (the account's own outside as_user, or those user_filter covers).
Return at most this many.
None)Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)Every matching bucket, newest first, fetched page by page.
for bucket in noclick.Bucket.list():
print(bucket.name, bucket.public, bucket.size_bytes)
everyone = noclick.Bucket.list(user_filter=noclick.ALL_USERS) # every user's, plus your account'slist_pageclassmethodasync
noclick.Bucket.list_page(
*, cursor=None, limit=None, user_filter=None, user=..., client=None
) -> PageOne page of buckets; pass next_cursor back as cursor.
The next_cursor of the page before; omit it for the first page.
None)Return at most this many.
None)Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)A page whose data holds the buckets and whose next_cursor is None on the last page.
page = noclick.Bucket.list_page(limit=50)
while True:
for bucket in page.data:
print(bucket.name, bucket.object_count)
if page.next_cursor is None:
break
page = noclick.Bucket.list_page(limit=50, cursor=page.next_cursor)putasync
bucket.put(key, data, *, content_type=None, metadata=None) -> BucketObjectStore data (bytes, text as UTF-8, or a binary file) at key,
replacing what was there. content_type is guessed from the key's
extension when omitted; metadata is up to 2 KB of your own ASCII
string pairs (names come back lowercased), returned with the file. One
put carries up to 100 MB; send
larger files straight from where they are with a PUT signed link.
NoneNoneThe file as stored, with its size and etag.
Store what your code made:
bucket = noclick.Bucket.from_name("uploads", create_if_missing=True)
stored = bucket.put("invoices/0042.pdf", b"%PDF-1.7 ...", content_type="application/pdf")
print(stored.size, stored.etag)Upload a file from disk, with metadata your app reads back later:
bucket = noclick.Bucket.from_name("uploads")
with open("report.csv", "rb") as f:
bucket.put("reports/2026-09.csv", f, metadata={"user": "acme", "source": "nightly"})readasync
bucket.read(key) -> bytesRead the file at key; a missing key raises noclick.NotFound.
The file's bytes, exactly as stored; decode text yourself.
bucket = noclick.Bucket.from_name("uploads")
pdf = bucket.read("invoices/0042.pdf")
with open("0042.pdf", "wb") as f:
f.write(pdf)headasync
bucket.head(key) -> BucketObjectLook a file up without downloading it; a missing key raises noclick.NotFound.
The file's size, content type, etag, metadata and when it last changed.
bucket = noclick.Bucket.from_name("uploads")
info = bucket.head("invoices/0042.pdf")
print(info.size, info.content_type, info.metadata)objectsasync
bucket.objects(prefix=None, *, delimiter=None, limit=None) -> list[BucketObject]The files whose keys start with prefix, in key order. With
delimiter="/", keys below the next / are rolled up into one folder
entry each (is_folder), so you can walk the bucket a level at a time.
NoneNoneReturn at most this many.
None)Every matching file (or the first limit), fetched page by page.
Everything under a prefix:
bucket = noclick.Bucket.from_name("uploads")
for obj in bucket.objects(prefix="invoices/"):
print(obj.key, obj.size, obj.updated_at)One level at a time, like a file browser:
bucket = noclick.Bucket.from_name("uploads")
for entry in bucket.objects(prefix="invoices/", delimiter="/"):
print(entry.key + (" (folder)" if entry.is_folder else f" {entry.size} bytes"))objects_pageasync
bucket.objects_page(prefix=None, *, delimiter=None, cursor=None, limit=None) -> PageOne page of objects; pass next_cursor back as cursor.
NoneNoneThe next_cursor of the page before; omit it for the first page.
None)Return at most this many.
None)A page whose data holds BucketObjects and whose next_cursor is None on the last page.
bucket = noclick.Bucket.from_name("uploads")
page = bucket.objects_page(prefix="invoices/", limit=100)
while True:
for obj in page.data:
print(obj.key, obj.size)
if page.next_cursor is None:
break
page = bucket.objects_page(prefix="invoices/", limit=100, cursor=page.next_cursor)removeasync
bucket.remove(key) -> NoneDelete the file at key. Removing a key that isn't there does nothing.
bucket = noclick.Bucket.from_name("uploads")
bucket.remove("invoices/0042.pdf")versionsasync
bucket.versions(key) -> list[ObjectVersion]What key held before each write through NoClick of the last 30 days, newest first.
Each earlier version.
bucket = noclick.Bucket.from_name("uploads")
for version in bucket.versions("menu.pdf"):
print(version.replaced_at, version.op, version.size)restore_versionasync
bucket.restore_version(key, version) -> BucketObjectPut key back as version held it; what it holds now is kept as a version in turn.
The file, as it's stored again.
Undo an overwrite:
bucket = noclick.Bucket.from_name("uploads")
bucket.restore_version("menu.pdf", bucket.versions("menu.pdf")[0])signed_urlasync
bucket.signed_url(key, *, method='GET', expires_in=3600, content_type=None) -> strA link that reads (GET) or uploads (PUT) the file at key
without a NoClick key, for expires_in seconds (60 to 604,800, a week).
A PUT link takes up to 5 GB; with content_type the upload must send
that type.
'GET'3600NoneThe link. Anyone who has it can use it until it expires, so hand it only to whoever needs it.
Let a user download a private file:
bucket = noclick.Bucket.from_name("uploads")
link = bucket.signed_url("invoices/0042.pdf", expires_in=3600)Let a browser upload straight to the bucket, so the file never passes through your server:
bucket = noclick.Bucket.from_name("uploads")
upload = bucket.signed_url("avatars/u42.png", method="PUT", expires_in=600, content_type="image/png")
# the browser runs: fetch(upload, {method: "PUT", body: file, headers: {"Content-Type": "image/png"}})urlasync
bucket.url(key) -> strThe plain URL a public bucket serves key at. A private bucket raises
noclick.NotAvailable: use signed_url, or update(public=True).
public_base_url followed by the key; it works for as long as the bucket stays public.
assets = noclick.Bucket.from_name("public-assets")
print(assets.url("logo.png")) # https://.../logo.pngupdateasync
bucket.update(*, public=None) -> BucketChange the bucket's settings. Making it private stops every public URL at once; signed links keep working until they expire.
NoneThis bucket, with the server's current fields.
assets = noclick.Bucket.from_name("public-assets", create_if_missing=True)
assets.update(public=True)
print(assets.public_base_url)notifyinvite-only betaasync
bucket.notify(queue, *, prefix=None, enabled=True) -> BucketPut a message on queue (a Queue or its name, in the bucket's own user)
for every file stored under prefix (the whole bucket when omitted), however it
arrived: put, a PUT signed link, or S3 credentials. The message is
{"event": "put", "bucket", "key", "size", "content_type", "etag", "at"}, so a
worker or an agent with OnQueue picks each upload up. Asking again for the same
queue and prefix changes nothing; enabled=False stops it. A bucket tells up to
10 queues.
NoneTrueThis bucket, whose notifications list where it tells.
Transcribe every call recording as it lands:
recordings = noclick.Bucket.from_name("recordings", create_if_missing=True)
calls = noclick.Queue.from_name("new-recordings", create_if_missing=True)
recordings.notify(calls, prefix="calls/")
for msg in calls.iterate():
link = recordings.signed_url(msg.body["key"])
print(noclick.AI.transcribe(link).text)
msg.ack()Stop:
recordings = noclick.Bucket.from_name("recordings")
recordings.notify("new-recordings", prefix="calls/", enabled=False)s3_credentialsinvite-only betaasync
bucket.s3_credentials(*, read_only=False, ttl_s=3600) -> S3CredentialsKeys for the bucket's S3-compatible endpoint, for rclone, the AWS CLI or
an S3 library, valid ttl_s seconds (900 to 43,200). They reach this
bucket only; read_only=True for keys that can't change anything.
False3600The endpoint, bucket name, keys and expires_at. The secret is shown once; mint new ones when they expire.
import boto3
bucket = noclick.Bucket.from_name("uploads")
creds = bucket.s3_credentials(read_only=True, ttl_s=3600)
s3 = boto3.client(
"s3",
endpoint_url=creds.endpoint,
region_name=creds.region,
aws_access_key_id=creds.access_key_id,
aws_secret_access_key=creds.secret_access_key,
aws_session_token=creds.session_token,
)
print(s3.list_objects_v2(Bucket=creds.bucket, Prefix="invoices/")["KeyCount"])exportasync
bucket.export() -> OperationExport every file, to take anywhere: a zip holding each one at its key
(under buckets/<name>/files/), with index.jsonl listing each key's
content type and metadata. A key that can't be a path in a zip (an empty,
. or .. part) is kept under renamed/, and the index says which.
An operation whose wait() gives an ExportBundle: the zip at url, valid 7 days.
bucket = noclick.Bucket.from_name("uploads")
bundle = bucket.export().wait()
print(bundle.url, bundle.size_bytes)deleteasync
bucket.delete(*, force=False) -> TrashItemDelete the bucket: it goes to the trash, restorable for 30 days. It must be empty unless
force=True, which takes every file in it too (a non-empty bucket otherwise raises
noclick.Conflict); its files are stored, and billed, until it's purged.
A bucket in an agent's tools (used_by) raises noclick.Conflict until
you take it out; so will one an app binds, when apps ship.
Falsebucket = noclick.Bucket.from_name("scratch")
bucket.delete(force=True)BucketObject
class BucketObjectA file in a bucket: its key, size in bytes, content_type, etag
(it changes whenever the content does), the metadata you gave it and
updated_at. In a listing with a delimiter, an entry whose is_folder
is true stands for every key under that prefix, and only its key is set.
| key | str | |
| size | int | None | |
| content_type | str | None | |
| etag | str | None | |
| metadata | dict[str, str] | |
| updated_at | Any | |
| is_folder | bool | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
ObjectVersion
class ObjectVersionWhat a file held before a write through NoClick replaced it (op changed) or removed it
(deleted), kept 30 days. An upload through a signed link or S3 credentials keeps none.
| id | str | |
| key | str | |
| op | str | |
| size | int | None | |
| replaced_at | Any | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
BucketOperations
class BucketOperationsSome of a bucket's operations as tools (bucket.only(...)), with the
arguments bind(...) fixed.
| bucket | Bucket | |
| operations | list[str] | |
| bindings | dict[str, Any] | The arguments NoClick fills on every call, out of the model's reach. |
bind
bucket_operations.bind(**arguments) -> BucketOperationsFix arguments of these operations, as ConnectionOperations.bind does:
a bound parameter leaves the tool's schema and NoClick fills it on every call.
prefix is the one to bind here: every key the tools take and return is
then under it, so one bucket can hold a folder per user
(prefix="users/{{user.id}}/"). Sends nothing.
A new ref with these arguments bound on top of any bound before.
uploads = noclick.Bucket.from_name("uploads")
assistant = noclick.Agent.from_name(
"assistant",
create_if_missing=True,
instructions="You file what the user sends you and share links to it.",
tools=[uploads.only("objects", "put", "signed_url").bind(prefix="users/{{user.id}}/")],
)S3Credentialsinvite-only beta
class S3CredentialsShort-lived keys for the bucket's S3-compatible endpoint, for tools that
speak S3 (rclone, the AWS CLI, boto3). They reach this bucket only (read-only
if you asked) and stop working at expires_at. Give the tool all of
endpoint, access_key_id, secret_access_key and session_token,
with region; bucket is the bucket's name on that endpoint, which isn't
its NoClick name.
| endpoint | str | |
| bucket | str | |
| access_key_id | str | |
| secret_access_key | str | |
| session_token | str | None | |
| region | str | |
| read_only | bool | |
| expires_at | Any | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |