Features
Publishing
Publishing notebooks, data, and files to S3-compatible storage or Framework Cloud
Overview
Framework publishes rendered notebooks, datasets, and other files so that colleagues can open them by URL. It publishes to three kinds of destination.
- Storage buckets. AWS S3 and S3-compatible services, such as MinIO and DigitalOcean Spaces, configured in the project's settings.
- Framework Cloud. The framework.pub service or a self-hosted server, for projects created from or registered with it.
- GitHub Pages. The project's own GitHub repository, with
to="github".
Every publishing function warns about files with private in their path and asks before it uploads them. The Private Paths section explains the rule.
Choosing a Destination
publish() and publish_notebook() choose the destination in this order.
- When you pass
connection, or the settings define any storage bucket, they publish to a bucket. - Otherwise, when the project has a Framework Cloud project token, they publish to Framework Cloud.
- Otherwise, they raise an error that shows how to add a bucket.
to="cloud" explicitly selects Framework Cloud, including when a bucket is configured.
to="bucket" explicitly selects storage, and to="github" selects GitHub Pages.
publish_data(), publish_dir(), publish_list(), and storage_test() work with buckets only.
Configuring a Bucket
Buckets live under connections.storage_buckets, which standard and privacy-sensitive projects keep in settings/connections.yml.
connections:
default_storage_bucket: reports
storage_buckets:
reports:
bucket: my-reports-bucket
region: us-east-1
prefix: projects/vaccine-study
static_hosting: true
minio:
bucket: shared
endpoint: https://minio.example.org:9000
| Option | Meaning |
|---|---|
bucket |
The bucket name |
region |
The AWS region. Default: us-east-1 |
prefix |
A path prefix for every upload |
endpoint |
The address of an S3-compatible service. http:// turns off TLS. |
static_hosting |
When true, notebooks upload as <dest>/index.html |
default |
When true, marks the bucket as the default |
The default bucket is the one named by default_storage_bucket, else the one marked default: true, else the first one defined. Publishing to S3 needs the s3 extra.
uv add "framework-py[s3]"
Credentials
Credentials come from the environment, usually the project's .env file. New projects have empty S3_* entries there to fill in.
# .env (ignored by git)
S3_ACCESS_KEY=AKIA...
S3_SECRET_KEY=...
Environment variables take precedence over values in the settings.
| Setting | Environment variables, in order |
|---|---|
| access key | S3_ACCESS_KEY, AWS_ACCESS_KEY_ID |
| secret key | S3_SECRET_KEY, AWS_SECRET_ACCESS_KEY |
| session token | S3_SESSION_TOKEN, AWS_SESSION_TOKEN |
| bucket | S3_BUCKET |
| endpoint | S3_ENDPOINT |
| region | S3_REGION, AWS_REGION, AWS_DEFAULT_REGION |
Because environment variables take precedence, a value in S3_BUCKET applies to every bucket in the project. Leave it empty when the settings name more than one bucket.
storage_test() checks that the bucket exists and the credentials reach it.
import framework as fw
fw.storage_test()
fw.storage_test("minio")
Publishing to a Bucket
Uploaded files are public. Framework uploads with a public-read ACL, and for buckets that have ACLs turned off, the bucket's policy decides access. Each function returns the URL of what it uploaded.
Notebooks
publish_notebook() renders a notebook with Quarto and uploads the HTML.
fw.publish_notebook("notebooks/exploration.qmd")
fw.publish_notebook("notebooks/exploration.qmd", dest="reports/2026-q4")
fw.publish_notebook("notebooks/exploration.qmd", connection="minio")
Without static hosting, the notebook is uploaded as one self-contained file, <dest>.html. With static_hosting: true, it is uploaded as <dest>/index.html, and the returned URL ends in /.
static_hosting: true https://my-reports-bucket.s3.us-east-1.amazonaws.com/projects/vaccine-study/reports/2026-q4/
static_hosting: false https://my-reports-bucket.s3.us-east-1.amazonaws.com/projects/vaccine-study/reports/2026-q4.html
On a static-hosting bucket, self_contained=False uploads the HTML with its supporting files instead of embedding them.
Data
publish_data() uploads a DataFrame as CSV, Parquet, or JSON, or uploads an existing file.
fw.publish_data(results, "datasets/results.csv")
fw.publish_data(results, "datasets/results.parquet", format="parquet")
fw.publish_data(results, "datasets/results.csv", compress=True)
fw.publish_data("outputs/public/tables/summary.csv", "datasets/summary.csv")
With compress=True, the file is gzipped and .gz is added to the destination.
Folders and Files
publish_dir() uploads every file in a folder. pattern is a regular expression on the file paths, and recursive=False skips subfolders.
fw.publish_dir("outputs/figures", dest="figures/2026-q4")
fw.publish_dir("outputs/figures", pattern=r"\.png$")
publish() uploads one file or a folder. A folder returns a list of URLs.
fw.publish("outputs/reports/quarterly_report.html")
fw.publish("outputs/reports/quarterly_report.html", dest="reports/final.html")
Listing
publish_list() returns the objects under the bucket's prefix with their key, size, modification time, and URL.
fw.publish_list()
fw.publish_list(prefix="reports")
Publishing to Framework Cloud
A project created with fw.new(key=...), or registered with the cloud when it was created, has FW_PROJECT_TOKEN and FW_CLOUD_URL in its .env. When the settings define no storage bucket, publish() and publish_notebook() send documents to that server.
fw.publish_notebook("notebooks/exploration.qmd")
fw.publish("outputs/notebooks/exploration.html")
Framework Cloud accepts the following sources.
- An image, PDF, CSV, JSON, text, binary, or other file.
- A self-contained
.htmlfile. - An
.htmlfile with its sibling<name>_files/folder, uploaded together. - A folder of rendered output, whose
index.htmlor first HTML file is the entry page. - A
.qmdfile, which Framework renders to HTML first.
Each publish adds a new version of the document. The slug argument names the document and turns on its readable URL. slug=True uses the name derived from the file, and slug=False turns the readable URL off. publish_notebook() publishes HTML only when the destination is Framework Cloud.
You can also publish an object from your Python session. On Framework Cloud, each one becomes a file that Cloud can show well.
- pandas and polars DataFrames and Series, and numpy arrays with one or two
dimensions, become tables. Every row is kept, along with a summary of each
column (missing values, distinct values, numeric statistics with a histogram,
and the most common values), computed over all rows. An index other than the
default becomes the first column.
pandas.crosstab()output andstatsmodelsanova_lm()tables publish the same way. - A pandas
Stylerpublishes as a formatted HTML table. - scipy.stats test results (
ttest_ind(),pearsonr(),chi2_contingency(),mannwhitneyu(),shapiro(), and others) become a test summary with the method, statistic, degrees of freedom, p-value, and confidence interval when the result has one. - statsmodels results (OLS, WLS, GLM, Logit, Probit, Poisson, NegativeBinomial, MixedLM, QuantReg, MNLogit, and others) become model summaries: coefficients with standard errors, p-values, and 95% Wald intervals, fit statistics, the formula, and the model family and link. Mixed models add their variance components.
- Fitted scikit-learn linear models publish their coefficients. Tree models, xgboost, and lightgbm models publish feature importance and their main settings.
- matplotlib Figures and Axes and seaborn plots and grids publish as SVG, or as PNG when the SVG would be larger than 5 MB. plotly Figures publish as HTML, and altair charts as Vega-Lite with their data included.
Model exports never include the observations a model was fit on, residuals, fitted values, or the serialized model. Integer columns with values outside the browser-safe range (±9,007,199,254,740,991) are exported as text to keep identifiers exact. When the destination is a bucket, tables upload as CSV, figures as PNG, and statsmodels results as an HTML summary.
fw.publish(figure, to="cloud", slug="treatment-effect", title="Treatment effect")
fw.publish(results_table, to="cloud", slug="estimates")
fw.publish(fitted_model, to="cloud", slug="adjusted-model", password="reviewer-password")
title, description, kind, password, and visibility set artifact metadata.
Passwords require at least eight characters. Visibility can be "public" or "unlisted".
Anyone with an unlisted link can open it unless it is password protected.
Publishing sends only the document you name. The Framework Cloud guide describes what else the cloud receives.
Publishing to GitHub Pages
to="github" publishes a page to the project's GitHub Pages site. The documents go from your computer to GitHub and never pass through Framework Cloud.
fw.publish("outputs/report.html", to="github")
fw.publish_notebook("notebooks/analysis.qmd", to="github", slug="main-results")
GitHub Pages sites are public, even when the repository is private. Every run prints that notice, and an interactive session asks Publish it? [y/N] before it changes anything. A script goes ahead without asking, because the call itself is the explicit choice. The private-path check still applies.
Before the first publish, turn on "Publish with GitHub Pages" for the project under Projects on Framework Cloud. That turns on Pages for the repository and commits a workflow that deploys the repository's published/ folder from the default branch.
Each publish takes these steps.
- Find the project's git repository and its GitHub remote: the branch's upstream remote, then
origin, then any other GitHub remote. Without one, it raises an error that says to create the project on GitHub first. - Copy the page to
published/<slug>/index.htmlat the top of the repository, replacing that folder completely. An.htmlfile's<name>_files/folder is copied beside it under the same name, so relative links keep working. A folder must have anindex.htmlat its top. A.qmdor.ipynbfile is rendered to HTML first. - Commit only
published/<slug>/, with the messagePublish <slug>. Anything else you have staged stays staged and out of the commit. When nothing changed, it reports that and neither commits nor pushes. - Push. A branch with no upstream is pushed with
git push -u <remote> HEAD. On a branch other than the default branch, it warns that the page won't appear until the branch is merged. - Return and report the URL.
The slug defaults to the file name in lowercase, with each run of other characters replaced by -. Final Report (v2).html becomes final-report-v2. An explicit slug must start with a lowercase letter or digit and use only lowercase letters, digits, ., _, and -, because GitHub Pages URLs are case-sensitive. dest, connection, and slug=True or slug=False apply only to other destinations and raise an error with to="github".
A project with a Framework Cloud project token asks the cloud for the site's URL. When the cloud reports that Pages is off, publishing still commits and pushes, and it warns you to turn Pages on. Otherwise the URL comes from the remote: https://<owner>.github.io/<repo>/<slug>/, or https://<owner>.github.io/<slug>/ for a repository named <owner>.github.io.
Private Paths
Publishing happens only when you call a publishing function, and framework-py never uploads anything on its own. Every publishing function checks the full path of each file it is about to upload. A path that contains private in any letter case, such as outputs/private/tables/detail.csv or ~/Private/notes.html, gets a warning that lists the files.
In an interactive session, the function then asks before it uploads anything. Answer y to upload, and anything else cancels. A session with no terminal, such as a script, a scheduled job, CI, or a notebook run by an assistant, cannot answer, so the function raises an error instead.
Warning: 1 file(s) to upload have 'private' in their path:
/home/me/study/outputs/private/tables/detail.csv
FrameworkError: Not uploading files with 'private' in their path without confirmation.
Review them, then pass include_private=True to upload them.
include_private=True uploads without asking. Only a person who has reviewed the files should pass it. The older name allow_private still works but warns that it is deprecated. A DataFrame passed to publish_data() has no path, so the check does not apply to it. Review the data before you publish it.
On macOS, the system resolves /tmp and /var to paths under /private/. That leading /private/ does not count. A folder you named private always counts.
In privacy-sensitive projects, save shareable results with public=True and publish them from the public folders.
fw.save_table(aggregated, "summary", public=True)
fw.publish_data("outputs/public/tables/summary.csv", "datasets/summary.csv")
Organizing Published Files
A bucket prefix per project keeps a shared bucket organized.
team/vaccine-study/reports/...
team/vaccine-study/datasets/...
Dates or versions in destination names keep earlier releases available.
fw.publish_notebook("notebooks/exploration.qmd", dest="reports/exploration-2026-10-31")
Adding an artifact to a collection
Select an existing collection in the current project by its ID or slug. The client resolves the collection before uploading and does not create collections. The collection follows the artifact's latest version.
fw.publish("outputs/effect.png", to="cloud", collection="main-findings", share_unlisted=True)
Public collections require public artifacts. Sharing an unlisted artifact with
readers of an unlisted collection requires explicit consent through
share_unlisted.
If attachment fails after the artifact uploads, publishing returns its URL and
warns that collection attachment failed. Attach the artifact from the project
Outputs page; publishing again creates another version.