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 Bucket

A 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).

Attributes
idstr
namestr
publicbool

Whether every file is readable at url(key) without a link.

size_bytesint | None
object_countint | None
public_base_urlstr | None

Where a public bucket serves its files (None while it's private).

used_bylist[dict[str, Any]]

Who uses it: apps that bind it (kind project, with the binding name) and agents with it in their tools (kind agent), each with its id and name.

notificationslist[dict[str, Any]]

Where it tells about new files: each with the queue (its name) and prefix (invite-only beta: empty).

regionstr | None

The region it keeps its files in (noclick.regions()), fixed when it was created.

user_idstr | None
created_atAny
console_urlstr | None

This bucket in the developer console (None off the hosted API).

only

bucket.only(*operations) -> BucketOperations

A 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.

Parameters
*operationsstr
Returns
BucketOperations

A ref to just those operations, to list in tools=; bind(...) fixes arguments.

Usage
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
) -> Bucket

Create 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 . _ -.

Parameters
namestrrequired
publicbooldefault False
regionstr | Nonedefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Bucket

The new, empty bucket.

Usage

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
) -> Bucket

A 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.

Parameters
namestrrequired
create_if_missingbooldefault False
publicbooldefault False
regionstr | Nonedefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Bucket

A bucket that loads (or is created) the first time it's used. Nothing is sent yet.

Usage

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=...) -> Bucket

A lazy reference to a bucket by id.

Parameters
bucket_idstrrequired
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
Bucket

A bucket that loads the first time you read a field. Nothing is sent yet.

Usage
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) -> Bucket

Load a bucket by id now; an unknown id raises noclick.NotFound.

Parameters
bucket_idstrrequired
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Bucket

The bucket, loaded: whether it's public, how many files it holds and their total size.

Usage
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).

Parameters
limitint | None

Return at most this many.

(default None)
user_filterAny

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.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
list[Bucket]

Every matching bucket, newest first, fetched page by page.

Usage
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's

list_pageclassmethodasync

noclick.Bucket.list_page(
    *, cursor=None, limit=None, user_filter=None, user=..., client=None
) -> Page

One page of buckets; pass next_cursor back as cursor.

Parameters
cursorstr | None

The next_cursor of the page before; omit it for the first page.

(default None)
limitint | None

Return at most this many.

(default None)
user_filterAny

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.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Page

A page whose data holds the buckets and whose next_cursor is None on the last page.

Usage
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) -> BucketObject

Store 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.

Parameters
keystrrequired
dataDatarequired
content_typestr | Nonedefault None
metadataMapping[str, str] | Nonedefault None
Returns
BucketObject

The file as stored, with its size and etag.

Usage

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) -> bytes

Read the file at key; a missing key raises noclick.NotFound.

Parameters
keystrrequired
Returns
bytes

The file's bytes, exactly as stored; decode text yourself.

Usage
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) -> BucketObject

Look a file up without downloading it; a missing key raises noclick.NotFound.

Parameters
keystrrequired
Returns
BucketObject

The file's size, content type, etag, metadata and when it last changed.

Usage
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.

Parameters
prefixstr | Nonedefault None
delimiterstr | Nonedefault None
limitint | None

Return at most this many.

(default None)
Returns
list[BucketObject]

Every matching file (or the first limit), fetched page by page.

Usage

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) -> Page

One page of objects; pass next_cursor back as cursor.

Parameters
prefixstr | Nonedefault None
delimiterstr | Nonedefault None
cursorstr | None

The next_cursor of the page before; omit it for the first page.

(default None)
limitint | None

Return at most this many.

(default None)
Returns
Page

A page whose data holds BucketObjects and whose next_cursor is None on the last page.

Usage
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) -> None

Delete the file at key. Removing a key that isn't there does nothing.

Parameters
keystrrequired
Usage
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.

Parameters
keystrrequired
Returns
list[ObjectVersion]

Each earlier version.

Usage
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) -> BucketObject

Put key back as version held it; what it holds now is kept as a version in turn.

Parameters
keystrrequired
versionAnyrequired
Returns
BucketObject

The file, as it's stored again.

Usage

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) -> str

A 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.

Parameters
keystrrequired
methodLiteral['GET', 'PUT']default 'GET'
expires_inintdefault 3600
content_typestr | Nonedefault None
Returns
str

The link. Anyone who has it can use it until it expires, so hand it only to whoever needs it.

Usage

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) -> str

The plain URL a public bucket serves key at. A private bucket raises noclick.NotAvailable: use signed_url, or update(public=True).

Parameters
keystrrequired
Returns
str

public_base_url followed by the key; it works for as long as the bucket stays public.

Usage
assets = noclick.Bucket.from_name("public-assets")
print(assets.url("logo.png"))   # https://.../logo.png

updateasync

bucket.update(*, public=None) -> Bucket

Change the bucket's settings. Making it private stops every public URL at once; signed links keep working until they expire.

Parameters
publicbool | Nonedefault None
Returns
Bucket

This bucket, with the server's current fields.

Usage
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) -> Bucket

Put 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.

Parameters
queueAnyrequired
prefixstr | Nonedefault None
enabledbooldefault True
Returns
Bucket

This bucket, whose notifications list where it tells.

Usage

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) -> S3Credentials

Keys 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.

Parameters
read_onlybooldefault False
ttl_sintdefault 3600
Returns
S3Credentials

The endpoint, bucket name, keys and expires_at. The secret is shown once; mint new ones when they expire.

Usage
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() -> Operation

Export 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.

Returns
Operation

An operation whose wait() gives an ExportBundle: the zip at url, valid 7 days.

Usage
bucket = noclick.Bucket.from_name("uploads")
bundle = bucket.export().wait()
print(bundle.url, bundle.size_bytes)

deleteasync

bucket.delete(*, force=False) -> TrashItem

Delete 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.

Parameters
forcebooldefault False
Returns
TrashItem

Its TrashItem: restore() puts it back, files and all.

Usage
bucket = noclick.Bucket.from_name("scratch")
bucket.delete(force=True)

BucketObject

class BucketObject

A 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.

Attributes
keystr
sizeint | None
content_typestr | None
etagstr | None
metadatadict[str, str]
updated_atAny
is_folderbool
rawdict[str, Any]

The response JSON exactly as the API sent it.

ObjectVersion

class ObjectVersion

What 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.

Attributes
idstr
keystr
opstr
sizeint | None
replaced_atAny
rawdict[str, Any]

The response JSON exactly as the API sent it.

BucketOperations

class BucketOperations

Some of a bucket's operations as tools (bucket.only(...)), with the arguments bind(...) fixed.

Attributes
bucketBucket
operationslist[str]
bindingsdict[str, Any]

The arguments NoClick fills on every call, out of the model's reach.

bind

bucket_operations.bind(**arguments) -> BucketOperations

Fix 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.

Parameters
**argumentsAny
Returns
BucketOperations

A new ref with these arguments bound on top of any bound before.

Usage
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 S3Credentials

Short-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.

Attributes
endpointstr
bucketstr
access_key_idstr
secret_access_keystr
session_tokenstr | None
regionstr
read_onlybool
expires_atAny
rawdict[str, Any]

The response JSON exactly as the API sent it.