Changelog
This page mirrors the SDK's own CHANGELOG.md. Entries under [Unreleased] are in the SDK source but not yet in a published PyPI release; the guides mark them 0.1.8+.
All notable changes to honeyframeapi. Dates are UTC.
0.1.8 — 2026-10-25
Work accumulates here without a version bump until the next publish. The version is bumped once, at publish time — never per change. On release this heading is renamed to the new
## X.Y.Z — <date>.Current frozen working version: 0.1.7 (PyPI latest published: 0.1.7).
Security (hardening)
- Every value placed in a URL path is percent-encoded (dataset names,
webapp keys, wiki slugs, job names, ids, …), so a name containing
/,?,#,%or..stays inside its own segment instead of re-routing the authenticated request (e.g. a dataset namedx/../../auth/tokens/7can no longer turnDataset.delete()into a token revocation).delete_variablesendsscopeas a query parameter;Webapp.public_urlencodes the key and token. - Exceptions no longer carry credentials. A failed login raises with no
response(its request body was the password). Every otherHoneyframeAPIError.responseis a copy whose.requesthas no Authorization / Cookie / X-API-Key header and no body.detailis capped at 4 KB.TokenAuth/LoginAuthreprs mask the secret. - Redirects are never followed (explicit
follow_redirects=False), so the bearer token cannot be forwarded to another host. - SSE parsing:
\r\n/\rline endings are accepted (also split across chunks), frames are scanned without quadratic re-slicing, and a partial frame over 8 MiB aborts the stream instead of growing memory without bound. - Files the SDK writes are created 0600:
Dataset.to_csv/to_parquet,Bundle.download,Folder.download,Dashboard.export_pdf, experiment artifact downloads andNotebook.export_ipynb(an existing file keeps its permissions). - CLI:
--token-file PATHand--token -(stdin) keep the token out ofpsand shell history; a literal--token VALUEstill works but warns on stderr.honeyframe pat createkeeps the JSON (with the secret) on stdout and adds a "store it now" reminder on stderr. honeyframe plugin runwarns on stderr that plugin code runs in-process as the current user, and hidesHONEYFRAME_*/HUB_*variables from the plugin unless--pass-envis given (run_component(pass_env=False)in the library).pack_pluginskips symlinks, so a link to a file outside the plugin can no longer end up in the uploaded archive.client.sql()falls back to the scratch-dashboard path only when/api/sqlitself is missing (405, or FastAPI's bare route-miss 404). A 404 the endpoint returns ({"error": "connector_not_found"}, a missing project) is raised. The fallback only adopts a scratch dashboard of the same project.- Docstrings and README lead with a PAT read from the environment; a password
is shown only with
getpass. The example SMTP sender verifies the server certificate.
Changed
- Plain-
http://base URLs are refused withHoneyframeConfigError(credentials would travel in clear text), except for loopback hosts (localhost,127.0.0.1,::1). Opt in for a trusted private network withallow_insecure_http=TrueorHONEYFRAME_ALLOW_INSECURE_HTTP=1(a warning is still emitted). A base URL without anhttp(s)://scheme and host is refused at construction instead of failing on the first request. - Public links expire after 30 days by default
(
Dashboard.create_public_link,Webapp.create_public_link; previously they never expired).expires_days=0is the documented way to make a link that never expires; values outside0and1..365raiseValueError(the platform would silently make them never-expiring);expires_days=Nonestill means never but is deprecated. create_patdefaults toexpires_days=90and validates0..730(0= never;Nonesends no lifetime and the platform applies its 90-day default).- Data-API and API-service keys (
PublishedAsset.create_api_key,APIService.create_key) default to a 90-day expiry andsensitivity_max="medium"(previously never and"critical"). With"medium", columns classifiedhigh(e-mail, phone, address, date of birth) orcritical(national ID, record numbers) come back[BLOCKED], while amediumcolumn such as a person's name comes back masked. Passsensitivity_max="high"/"critical"and/orexpires_days=0to keep the old behaviour. An unknown tier or a negative lifetime raisesValueError. Project.run_dbt(wait=True)andRecipe.build(wait=True)now really wait. The trigger response has no run id, so they were silent no-ops; they now go through the newJobsAPI.trigger_and_wait, which finds the run the trigger started inGET /api/pipeline/runsand polls it to a final status (raisesTimeoutErrorwhen no run appears withinstart_timeout).Project.list_dashboards()passesproject_idto the server instead of filtering client-side, so org-level dashboards (no project) that the UI shows for the project are included.Project.create_dataset()returns a handle named after the server-stored (normalised) name, not the name as typed.JobsAPI.list_runsraisesValueErrorfor alimitoutside 1–200 (the server's page cap) or a negativeoffset.experiments.iter_runsdocuments that it returns the newestlimitruns (default 200, not every run) and takeslimitup to 1,000.- ML and plugin recipe
run(timeout=…)now bounds the HTTP call (the run is synchronous; the argument was previously ignored and the 60 s client default applied). httpxis bounded below 1.0 (httpx>=0.24,<1).- Name/title properties load the record on first use instead of raising
KeyErroron a freshly created handle (Dashboard.title,Card.title/card_type,Scenario.name,Connector.name/connector_type,Agent.title,PublishedAsset.asset_key/status,KnowledgeBase.name). - Docstrings and examples use neutral sample data; no other product names or internal ticket references remain in the package.
Added
HoneyframeClient(verify=…)accepts a CA bundle path (file or directory) or anssl.SSLContext, and warns onverify=False.HoneyframeClient(transport=…)plugs in anyhttpx.BaseTransport;allow_insecure_http=(see Changed).client.request(method, path, **kw)— the documented escape hatch for endpoints without a typed method (same auth, headers, retries and typed errors; refuses absolute URLs)._get/_post/_put/_patch/_deletekeep working.- Per-call
timeout=onDashboard.export_pdf,Dashboard.execute_all,Dataset.materializeandclient.request. client.sql()/Project.sql()takeparams=(named binds) and warn + setdf.attrs["truncated"] = Truewhen the server reports the result was cut atrow_limit.JobsAPI.trigger_and_wait,list_pipeline_runs,get_pipeline_run;JobsAPI.wait(raise_on_timeout=True);skippedis a terminal status (JobsAPI.TERMINAL_STATUSES).client.list_dashboards(project_id=…).locale="id" | "en"onbanner()anddate_range_filter()(default stays"id"in 0.1.x so existing apps render unchanged);timezone=is documented as overridable.entity_filter()is a neutral alias ofbranch_filter()(same arguments, same wire type).parquetextra (pip install "honeyframeapi[parquet]"→ pandas + pyarrow) and Trove classifiers (Python 3.9–3.13, OS Independent, proprietary license).HoneyframeClient.__repr__shows the base URL, org and project, never the token.- README "Connection security" and "Versioning and stability" sections; a
test pins
honeyframeapi.__all__.
Deprecated
- The
HUB_API_BASE,HUB_USERNAME,HUB_PASSWORDandHUB_PROJECT_IDenvironment fallbacks emit aDeprecationWarning; use theHONEYFRAME_*names. Dashboard.add_card(config=…): usecard_config=(still honoured, with aDeprecationWarning).expires_days=Noneoncreate_public_link,create_api_keyandAPIService.create_key: passexpires_days=0for "never".
Fixed
- The package type-checks with mypy without
implicit_optional;= Noneparameters are annotatedOptional[...]and public callables have return annotations. - The test for
run_dbtno longer mocks arun_idthe platform never returns. - CAPABILITIES.md:
project.dbtis a property; knowledge banks and LLM Mesh are wrapped; the escape hatch isclient.request.
Changed (PyPI page and docstrings)
- README rewritten for PyPI readers: install from PyPI (
pip install honeyframeapi), Personal Access Token first, placeholder hosts (platform.your-domain.com), neutral example tables, an "Errors and retries" section (429 / budget errors,max_retries,retry_backoff), and a correctedclient.sql()note (it runs throughPOST /api/sql; the scratch card is only a fallback on older platforms).project.dbtis a property, not a call. - No other product or customer names in the README, package metadata or
docstrings;
Homepage/DocumentationURLs point at https://docs.honeyframe.io/. - The CLI docstring no longer claims a
HONEYFRAME_ORG_IDvariable (the org is chosen with-o/--org). - The sdist now ships the package only (
MANIFEST.inprunestests/andexamples/).
Added (statistics worksheets — all 17 EDA cards from code)
honeyframeapi.statistics— the dataset Statistics tab's card surface, which was UI-only: a notebook could not run a single EDA statistical card. NewDatasetStatistics/StatisticsWorksheethandles over/api/statistics, reached asdataset.statisticsand exported from the package root alongsidestat_cardandCARD_TYPES.- Every one of the 17 card types has its own typed method, and all of them
are also reachable generically via
compute(card_type, **params)(for when the card type is itself data):- overview —
univariate(column),correlation(columns),scatter_fit(x, y) - distribution —
quantile_table(columns),cdf_plot(column),fit_distribution(column),shapiro_wilk(column) - tests —
chi2_independence_test(column_a, column_b),ttest_2samp(column, group),mann_whitney_u(column, group),paired_ttest(column_x, column_y),anova_1way(column, group),levene_test(column, group),ks_test_2samp(column, group) - time series —
acf_plot(date_column, value_column, agg=, max_lag=),mann_kendall_test(date_column, value_column, agg=) - multivariate —
pca(columns, n_components=, standardize=, color_by=)
- overview —
- Card types are validated client-side against the same list the platform
keeps, so
compute("univarate", …)raisesValueErrornaming the nearest match and the full catalog instead of costing a 400 round-trip. Required parameters andaggare checked the same way; unknown parameters are forwarded untouched, so a parameter the platform gains later works without an SDK release. - Worksheet CRUD —
list_worksheets(),create_worksheet(name),get_worksheet(id); on the handlecards,add_card(type, **params),update_card(id, **params),remove_card(id),set_cards([...]),rename(name),delete(),compute_card(id),compute_all(). These are the same rows the Statistics tab reads, so a worksheet built from a script opens in the UI. The server's PUT replaces the card list, so every write sends the whole list — an append can't silently drop the rest. - Results are relayed verbatim, including the platform's honest
available: False+reasonpayloads (shapiro_wilkunder n=3,paired_ttestunder 2 complete pairs) and itssampled/gap_filled/constant_columnsflags. Checkavailablebefore reading a p-value. - Two limits are documented rather than papered over: worksheets need the
dataset_worksheetsmigration (an un-migrated tenant reads back as an empty list), and v1 computes on warehouse + uploaded-file datasets only — a lakehouse/external dataset raisesHoneyframeValidationErrorcarrying the platform's own reason.
Added (experiment artifacts — download, not just upload)
ExperimentRun.download_artifact(name, *, dest=None)→GET /api/experiments/runs/{run_id}/artifacts/{name}. The route was served and had no SDK method:log_artifactcould put a file in andartifactscould list it, but nothing could read it back. Returns the raw bytes, and writes them todestas well when given (the bytes still come back, so a checksum needs no re-read).- A run whose row names a file the disk no longer has surfaces the platform's 410 — deliberately distinct from a 404 "no such artifact", which is the distinction you need when diagnosing a restored database against a pruned data dir.
Added (experiment tracking)
honeyframeapi.experiments— MLflow-shaped run ledger over the new/api/experimentssurface (log params, metrics and artifacts per run). NewExperiment/ExperimentRunhandles, exported from the package root, plus threeProjectentry points:project.create_experiment(name, description=""),project.list_experiments(),project.get_experiment(id).Experiment:log_run(run_name, params=, metrics=, tags=, status=),start_run(...)(context manager),list_runs(status=, limit=),get_run(id),compare(run_ids),get_info(),delete().ExperimentRun:log_metrics(...),log_params(...),log_tags(...),log_artifact(path, name=None),finish(status="finished", metrics=None),promote(model_name),delete(), plus lazily-fetchedrun_name,status,metrics,params,tags,artifacts,saved_model_id.- Every
log_*MERGES (right-wins per key), matching the server: logging one metric leaves the others alone, and there is no path that blanks a blob. start_runcloses the run on exit and asfailedwhen the block raises — a crashed training script leaves a failed run rather than one stuckrunningforever. The caller's exception always propagates; a failingfinishnever masks it.compare()returnsmissing_run_idsfor ids that are not in the experiment. Check it: a metric a run never logged isnullin the matrix, never0.promote()works only on a finished Analysis Lab run — the only kind the platform can rebuild a reproducible training recipe from. Anything else raisesHoneyframeValidationErrornaming the reason instead of registering a model with nothing behind it.- Analysis Lab sessions are auto-logged server-side, so
project.list_experiments()already has content on an instance that has trained anything since this release.
Added (dashboard export options)
Dashboard.export_pdf(..., file_type=, width=, height=, scale=)— the export-dialog options (DASHBOARD.EXPORT_MODAL.FORM.{FILE_TYPE,WIDTH,HEIGHT}) on the existing server-side render.file_type∈pdf|png(PNG is a screenshot of the render window, PDF still goes through Chromium's print engine),width/heightare render-window pixels bounded 320–4096 (one axis alone keeps the paper's other axis),scaleis the device-pixel-ratio 0.5–3.0 (raster only). Out-of-range values raise the server's422carrying its own message.- Every new keyword defaults to the previous behavior and is omitted from the
request body when unset, so an unchanged
export_pdf(path)call sends a byte-identical request and still writes an A4 portrait PDF.
Added (code environments — declare, build, poll)
list_code_environments(),get_code_environment(id),create_code_environment(name, …),update_code_environment(id, **f),delete_code_environment(id),validate_code_environment(id)→/api/code-environments. A code environment is an org-scoped PEP 508 manifest a python recipe pins for reproducible dependencies.build_code_environment(id, *, wait=False, …)→POST /api/code-environments/{id}/build. Materializes the manifest as a real venv on the server, built under the recipe sandbox (dropped uid, no platform secrets in the environment, memory-capped) — never as the service user. Returns as soon as the build is queued;wait=Truepollscode_environment_build_log(id)until the state leavesbuildingand raisesTimeoutError(the build keeps running server-side) rather than reporting a result it does not have.- Branch on
build_state(not_built | building | built | stale | failed), not onstatus: it is derived from the row AND the venv on disk, so areadyrow whose tree was wiped reads asnot_built.stalemeans the manifest changed after the build — still runnable, rebuild to match. code_environment_build_availability()says whether this install can build at all and, when it cannot, exactly what is missing. A build that cannot run returnsstarted: Falsewith that reason — it is not an error to retry, and nothing was built.create_code_environment(..., allow_source_builds=True)turns off the default wheels-only install (--only-binary=:all:), letting pip run a package's own build backend inside the sandbox. Off by default; every build under it records the fact in its build log.
Added (asset data catalog)
webapp.catalog(*, page_key=None)→GET /api/webapps/{key}/catalog. The marts behind a published app, what they mean, and which cards use them — at the permission floor of opening the app, so a viewer can read it. Until now the same prose only reachedproject.edit(viaGET /api/dashboards/{id}and/api/data-dictionary/*), which is why published apps hand-maintained "Sumber data" literals in their SPA.- Scoped twice: to the ASSET (only marts behind cards this app embeds) and
to the CALLER (only dashboards this principal can actually execute) — so a
viewer sees only their own slice. A principal who can execute none of them
gets
200withdatasets: [], never a 403. - Never returns SQL or rows. Every key is always present (
None/[]when unknown):{asset_key, asset_id, project_id, locale, truncated, scope: {pages, dashboards_visible, dashboards_denied, cards_visible}, freshness_card, datasets[], sources[]}— a client never has to branch on key presence to tell "no data" from "old backend".truncatedgoes true when the server's caps (50 dashboards / 100 datasets / 200 columns) clipped the response, so partial coverage is reportable rather than implied. page_keynarrows to one page and is URL-encoded through the client's existingparams=path; omitted entirely when not passed (page_key=is a different request from nopage_key).
- Scoped twice: to the ASSET (only marts behind cards this app embeds) and
to the CALLER (only dashboards this principal can actually execute) — so a
viewer sees only their own slice. A principal who can execute none of them
gets
dataset.update_settings(catalog={...})— new kwarg on the existing call, the writer for the authored half of the above. Lands insettings.catalog:{grain, period_column, refresh, status, sumber, sumber_teknis, caveats[{level, teks, bukti}]}.refresh∈harian|bulanan|snapshot-harian|sekali-muat|tidak-dibangun,status∈dipakai|blok-bangunan|tidak-dibangun,caveats[].level∈info|perhatian|kritis— server-validated, so a typo is a 400 rather than a silent blank in the client. Unknown keys inside the block are kept (a seeder can add a field without a backend release). Written whole, not patched; like every other field it is omitted when left atNone.card_descriptiononcard_refblocks (no SDK change — documented onWebapp.catalog): backends new enough to serve the catalog endpoint also attach the card author's "Sumber data" prose to eachcard_refin the config returned byclient.get_webapp()/webapp.reload(). Treat it as an optional key — an older backend, or a card with no description, omits it.
Added (dashboard batch execution)
dashboard.execute_batch(card_ids, *, params=None, asset_id=None)→POST /api/dashboards/{id}/cards/execute-batch, which the platform's own webapp pages have used for a while but the SDK never wrapped. Collapses the per-cardPOST …/executefan-out of a page load into one round-trip; each card runs exactly ascard.execute()would (same gate, same:namebind merge, same masking), so this is the batched form of the single-card path, not ofexecute_all(which speaks{{param}}/filtersinstead).- Returns
{card_id: {card_id, columns, rows, row_count, execution_ms, error, bound_params}}. Those are JSON object keys, i.e. strings — index withres[str(cid)]. A failing card reports in-band via its ownerrorand never sinks the batch. card_idsis the scope, and passing it matters: an unscoped batch runs every card on the dashboard, and one dashboard can back a whole multi-page app (a large app can back ~20 pages from one dashboard).asset_idserves a published asset's pinned snapshot; it is a query param, never part of the body, and a cross-org or unknown id degrades server-side to a live unpinned run rather than erroring.
- Returns
Added (429: rate limit ≠ budget exceeded)
HoneyframeRateLimitErrorandHoneyframeBudgetExceededError, both exported from the package root.error_for_statushad no 429 branch at all, so every 429 fell through to the genericHoneyframeAPIError— collapsing the upstream gateway'srate_limit_exceeded(waitRetry-Afterseconds; retrying IS correct) withbudget_exceeded(the metered budget is spent until the billing month rolls; retrying can never succeed). Perdocs/trafo.md(2026-08-25) the gateway owns budget/rate enforcement outright, so the SDK has to tell the two apart.HoneyframeRateLimitError.retry_afteris parsed from theRetry-Afterheader's delta-seconds form. The HTTP-date form, a negative value and an absent header all degrade toNonerather than to a guessed number — callers fall back to their own backoff.- Both error bodies in the wild are handled: FastAPI's
{"detail": <str|dict>}and the gateway's{"error": {"code": …}}. An unrecognised 429 classifies as a rate limit; onlybudget_exceededescalates to the non-retryable class (erring toward "retryable" for an unknown code, toward "not retryable" for a matched budget code).
- 429 is still never auto-retried.
_RETRYABLE_STATUSstays{502, 503, 504}— the retry loop only knows our own backoff, and neither 429 case is served by it. The typed exception hands the decision to the caller.
Added (license status)
client.license_status()→GET /api/license/status. Returns{"status": …}withstatus∈valid|grace|expired|missing|invalid(pluscustomer/tier/products/users_limit/max_seats/features/expires/days_remaining/version/org_slugwhen a license resolves, and a human-readablemessagein the non-validstates). Onlyvalidandgracepass the platform's lockdown gate, so this is the call that explains a script suddenly 403-ing on every other endpoint.
Added (dataset governance: AI search, tags, settings)
client.ai_search_datasets(intent, *, project_id=None, tags=None, limit=10)and the project-scopedproject.ai_search(intent, *, tags=None, limit=10)→POST /api/datasets/ai-search, the same catalog-discovery ranking the dashboard chat agent calls as a tool.project_id=Noneis the cross-project org-wide search; aproject_idoutside your org is a 404 server-side. Returns{"results": [...], "intent": …, "count": n}.project.list_dataset_tags()→GET /api/datasets/tags(union of the dedicatedtags[]column and the legacysettings.tagspath — both live).project.apply_dataset_tags(names, *, add=None, remove=None)and the single-datasetdataset.apply_tags(add=…, remove=…)→POST /api/datasets/tags/apply; per datasettags = (existing ∪ add) − remove, written to both storage paths. Returns{"updated": n, "missing": [...]}— a name the project doesn't have is reported, not raised. RaisesValueErrorlocally whenaddandremoveare both empty, instead of spending a round trip on a guaranteed 400.dataset.get_settings()/dataset.update_settings(...)→GET/PUT /api/datasets/{name}/settings—/settings, not/config(/config404s on registered and imported datasets).update_settingssends only the fields you pass; every argument left atNoneis omitted and the stored value is untouched. Note this is genuinely a patch, not a clear: the router skips null fields, sopartitioning=Nonecannot empty partitioning (send{"dimensions": []}for that).dataset.tags()— the dataset's tags as the settings surface reports them.
Fixed (test collection on a bare install)
tests/test_datasets.pyhard-importedpandasat module scope (and never used the alias), breaking collection of the whole file in a pandas-free environment. It now uses the housepytest.importorskip("pandas")guard (matchingtests/test_upload.py), and the two DataFrame-asserting tests intests/test_devapi.py(test_sql_prefers_native_endpoint,test_sql_falls_back_to_scratch_card_on_404) skip the same way. Zero behaviour change where pandas is installed.
Docs
README.mdnow points at https://docs.honeyframe.io/ near the top: the README covers the SDK, the portal covers the platform behind it.
Added (webapp attention banner)
banner(branches_sql, *, title, severity, chip_limit, x, y, w, h)— a full-width "Perlu perhatian" attention banner block: one row ofbranches_sql= one chip (label/name+ optionaldetail/value). Executes via the same governed per-block execute pathcode_card()uses (read-only SELECT, run server-side, redacted from a public webapp's config payload) — no bespoke endpoint, so it gets public-webapp PII masking for free. Renders nothing when the query returns zero rows or fails; the chip count shown is always the actual executed row count, never a separately authored number. RaisesValueErroron blankbranches_sql(a banner with no live query has nothing honest to show) and on an invalidseverity(must beinfo/warning/critical) — same discipline aschrome()'s refuse-a-flag-with-nothing-behind-it checks. No per-chip click-through in v1 (deferred — the design's own shipped version never wires one either).
Added (webapp page anchors)
page_anchor(label, block_index)+page(..., anchors=[...])— an in-page "table of contents" pill row rendered between the filter bar and the block grid, jumping to a block by position (the same addressing the grid'sdata-hf-card-indexwrapper and the?hfcard=metric-search deep link already use).page()raisesValueErrorat build time if any anchor'sblock_indexis out of range for theblocksactually passed — the same disciplinechrome()uses to refuse a flag pointing at nothing. No backend/SQL surface: an anchor is a static index over the already-loaded page, so it ships identically to authed and public webapps.
Added (webapp filter slots + chrome builders)
pill_multi_filter(param, options, …)— the inline pill row (REGIONAL [Semua][1][2]…) the renderer has always supported (PillMultiFilter) but no builder emitted, so pages hand-rolled it as a sandboxed HTML block. Publishesparamplus the CSV companion<param>_codesexactly like a branch slot;optionsaccept{"value","label","code"?}dicts or bare strings;all_label,empty_valueandchipsmap toallLabel/emptyValue/chips. Raises on an empty option list (a pill row that filters nothing) and on a badmode.metric_search_filter(label=None, placeholder=None)— the page-level "Cari Metrik" row (MetricSearchFilter). Binds no parameter: it finds the card and jumps to it.branch_filter(..., chips=True)— opt into the removable chip row ("N01 — North ×") instead of the count badge. Multi-select only; asking for it inmode="single"raises rather than silently rendering no chips. Existing calls emit byte-identical config.chrome(...)+Webapp.set_chrome(...)— build the app-level header / rail flags instead of hand-rolling the dict:show_sidebar_search,show_clock(+timezone),show_alert_pill(+alert_pill_sql/alert_pill_text/alert_pill_severity),show_notifications(+notifications_sql),show_share_button,show_print_button,show_theme_toggle,show_user_avatar,show_report_cart(the one default-ON affordance),show_help_fab,show_assistant(+assistant),fullscreen. A flag whose content comes from a query is refused without that query (and content without its flag is refused too), so an app cannot ship an alert pill, a bell, or an assistant launcher with nothing behind it. Only keys the shell actually reads are emitted — there is deliberately noshow_title(no webapp surface reads one).
Added (webapp data API v1)
Webapp.execute_block(page_key, block_idx, params=None)— run one page block's server-side SQL (POST /api/webapps/{key}/pages/{page_key}/blocks/ {block_idx}/execute) and get the{columns, rows, row_count, execution_ms, truncated, error}envelope. Blocks are addressed positionally; the SQL is the config's own (code→sql,html→data_sql) and is never sent from the client, so aread_onlyPAT can call it. Errors return in-band aserror;truncatedflags the 5000-row cap. Completes the read surface next toCard.execute—card_refpages execute per card,html-block pages execute per block.- External contract for read-only PAT holders:
docs/webapp-data-api.md.
Fixed (webapp)
Webapp.update()/.publish()/.unpublish()no longer report a SUCCEEDED write as a failure. All three ended withself.reload(), so a large configPUTthat the server persisted still raised if the follow-upGET /api/webapps/{key}came back truncated by an edge proxy — callers read that as a failed write and re-ran it (scripts 102/103 misreported exactly this way). The read-back failure is now caught: the call returns normally with a stale cache, setsWebapp.last_warning, and emits aUserWarningnaming the write as persisted. No signature change; a clean call leaveslast_warningNone.
Added (native engine — author and build SQL recipes without dbt)
Recipe.set_sql(inline_code, *, expected_version=None)— land runnable SQL on a recipe row via the newPUT /api/flow/recipes/{id}/sql(native-engine orgs; a dbt org gets 409).Recipe.save()can only compileinline_codeforrecipe_type='prepare';sql/join/groupby/window/distinctsaved withinline_code = NULLand then failedbuild_native()with "has empty inline_code". The server resolves{{ ref() }}/{{ source() }}against the project's dataset registry, strips{{ config(...) }}, and REFUSES the write (400) if any{{survives;expected_versionis the same optimistic lock asRecipe.save(). Returns the resolved SQL as stored.Project.build_native(select=None, *, dry_run=False, background=False, wait=False)— dependency-ordered build of the project's native recipes (POST /api/flow/build-native), the nativedbt run.selecttakes the dbt selector shapes (model,model+,+model,+model+, comma- separated);dry_run=Truereturns the resolved order without executing;background=Truereturns{run_id, status:"running"}, pluswait=Trueto block on the run.- Together these close the one non-REST step in the 2026-08-17 multifinance
build, which had to import the backend's
_resolve_native_refsand UPDATEhoneyframe.flow_recipesdirectly for all 59 model recipes.
Added (notebooks — Jupyter .ipynb interchange)
Project.import_ipynb(src, *, name=None, description="", tags=None)— import a Jupyter notebook (path or parsed nbformat dict) as a platform notebook in one call. Markdown/raw cells import as markdown, code as code; Jupyter outputs and execution counts are dropped (the platform renders its own on run).namedefaults to the file stem.Notebook.to_ipynb()/Notebook.export_ipynb(path)— export a platform notebook back to nbformat-4.5 (dict, or written.ipynbfile). Sources round-trip withimport_ipynb; platform outputs are not exported.- First used to publish the PT Mitra Bali Sukses complaint-mining notebook (38 cells) to the test instance, 2026-08-03.
Added (plugin developer kit — honeyframe plugin …)
- New
honeyframe plugin init|check|run|packCLI (and thehoneyframeapi.plugin_devmodule behind it) to scaffold, validate, run, and pack a Honeyframe kernel plugin locally — no monorepo checkout, no platform install, stdlib only.init <dir> --type recipe|transform|connector|scenario_step|monitor_adapterscaffolds a workingplugin.json+ entry template that passescheck.check <dir>mirrors the platform kernel's descriptor validator (id/name regexes, supported vs deferred component types,paramsschema,selectchoices,scenario_stepsecret-param rejection,monitor_adapterdescriptor-only + path rules, entry existence/traversal) for fast local feedback. The platform kernel stays authoritative — it re-validates at install.run <dir> <component> [--op] [--params] [--input] [--source] [--output]executes the entry file with the same op dispatch + return-type checks the platform shim uses (process/read/sources/test/run_step). ⚠️ Localrunis NOT sandboxed — it exec()s your code in-process on your machine; the platform runs plugins in an isolated, credential-scrubbed subprocess.pack <dir> [-o out.zip]validates then zips (plugin.jsonat the archive root; excludes dotfiles,__pycache__,*.pyc, existing zips) — refuses to pack an invalid plugin.
0.1.7 — 2026-07-07
Fixed (typing / correctness — SDK groom)
- The package now type-checks with 0 mypy errors (it ships
py.typed, so downstream type-checkers see these hints).PublishedAsset.reloadis now self-typed so subclass handles (APIService) keep their type through it, andAPIService.publish/unpublishreturn the server dict like the base (Liskov-clean). Also fixedBundle.activate,client.create_meaning, and theSavedModelforward-refs onProject. - New
tests/test_mypy_clean.pygate shellsmypy honeyframeapiand asserts 0 errors (skips cleanly when mypy isn't installed, so the offline suite still passes everywhere). DbtIDEis now exported at the top level (honeyframeapi.DbtIDE).
Added (knowledge banks — RAG corpora)
- New
KnowledgeBasehandle + client factoriesclient.create_knowledge_base(name, *, vector_connector_id=…, embedding_connector_id=…, …)/list_knowledge_bases()/get_knowledge_base(id)/kb_connector_options()— build a RAG corpus from code.KnowledgeBasecovers the whole loop:update/delete,docs()/upload_doc(path)/remove_doc(id),embed()/sync()/clear(),search(query, top_k=…, threshold=…),check_vector_store()(dimension probe),create_tool()(→ a reusablevector_searchagent-tool bound to the KB),link_agent/unlink_agent,usage().kb_connector_options()surfaces the eligible backends — registry-derived, so the new pgvector vector store (and the built-in numpy store, chroma, faiss, pinecone) and embedding providers appear automatically without an SDK change.
Added (bundles — deployment sign-off gate)
Bundle.governance_status()+request_signoff(note=…, feedback_givers=[…], approver_user_id=…)/submit_feedback(verdict=…, note=…)/submit_for_approval()/approve()/reject()/abandon()— the native two-stage approval workflow (FEEDBACK → FINAL APPROVAL) over/api/projects/{id}/bundles/{version}/signoff/*. Every call returns the current{status, blocking, detail, …}. PlusProject.list_signoffs()(per-version status + audit log + the org sign-offmode) andProject.list_bundle_activations(). Makes a CI/CD promote gate scriptable — request a sign-off on a bundle version, pollgovernance_status, thendeployonce approved.
Added (dataset time-travel — as-of read)
Dataset.preview_version(version_id, *, limit=…)— read a snapshot's data as of that version (time-travel), with the same role/column masking live Explore applies. Non-destructive; complements the existingversions()/rollback_version()/update_version().
Added (per-recipe engine selection)
Recipe.engine_status()— what the recipe-engine picker shows ({selected, pinned, default, engines:[{type, isSelectable, statusWarnLevel, …}]}; engines aresqlin-database pushdown +dbt).Project.change_recipe_engines(names, target, *, dry_run=…)— the bulk "Change N recipe engines" mass action (target∈sql|dbt|auto;dry_runreports the per-recipe outcome without writing).
Added (LLM Mesh — governed connections + spend)
client.llm_mesh_overview()(every LLM connection joined with its policy + month-to-date spend/calls + org totals/budget),get_llm_policy(connector_id)/set_llm_policy(connector_id, *, allowed_models=…, monthly_budget_usd=…, budget_blocking=…, cache_enabled=…, max_parallelism=…, usable_by=…)(upsert a connection's governance policy), andllm_mesh_usage(days=…)(windowed per-connection / per-model / per-day roll-up). New LLM provider types merged platform-side (mistral / cohere / azure_openai / huggingface) need no SDK change —connector_typeis a free string andlist_connector_catalog()already surfaces every type the platform advertises.
Fixed (scenario reporter docstring drift)
Scenario.add_reporterdocstring corrected to the real router enums:kind∈mail | slack | teams | webhook(was "email", which the server 400s) andtrigger∈always | success | failure | custom(wason_success/on_failure). Behaviour unchanged —kind/triggerwere already passed through verbatim.
Added (dashboards — parity groom)
Dashboard.public_links/create_public_link/revoke_public_link— token-gated public share links (raw token shown once).Dashboard.export_pdf(path, paper_size=…, orientation=…, current_page_only=…)— server-side headless-Chromium PDF export streamed to disk; surfaces the no-Chromium case (503) as a typedHoneyframeErrorpointing at the in-browser fallback.Dashboard.execute_all(filters=[…])+ buildersdashboard_filter,filter_between,filter_compare,filter_in— global filters mirroring the backendDashboardFiltermodel (between/eq/gte/lte/in) 1:1.Dashboard.shares/add_share/update_share/remove_share;Dashboard.revisions/create_revision/get_revision/diff_revisions/restore_revision/undo/undo_card;Dashboard.move_to_collection+client.list/create/update/delete_collection.- Card Library:
client.create_card_template/list_card_templates→ newCardTemplatehandle (update/delete/insert_into/link_into/preview/duplicate/versions/get_version/restore_version);Card.unlink_template.
Added (published-asset version history)
PublishedAsset.versions/rollback/diff_versions— hoisted onto the base, so every published asset type (data-APIs,APIService) inherits the timelineWebappalready had.
Added (scenarios — multi-triggers + reporters)
Scenario.list_triggers/add_trigger/update_trigger/delete_trigger(many triggers per scenario; the first stored trigger supersedes the legacy singletrigger_typefield, which still works for one-shots).Scenario.list_reporters/add_reporter/update_reporter/delete_reporter/test_reporter.
Added (connectors — per-user creds + secret rotation)
client.list_my_credentials;Connector.set_my_credentials/delete_my_credentials.Connector.rotate/secret_history/rollback_secret— test-before-commit secret rotation with history + rollback.
Added (dataset snapshots)
Dataset.versions/rollback_version/update_version— list snapshot versions, roll the live table back to one, and pin/annotate/retention-tag them.
Added (push notifications)
client.send_push(title, body, *, user_ids=…, role=…, data=…)— send a push notification to users by id and/or role (admin token). WrapsPOST /api/push/send; returns{sent, pruned, total, users}. The primitive behind proactive notifications — e.g. the director-briefing recipe pushingrole="director".
Added (chat — streaming)
client.stream_chat(message, *, persona=…, agent_id=…, …)andAgent.ask_stream(message, …)— consume the streaming endpointPOST /api/chat/stream, yielding Honeyframe SSE events (design §14):token/thinking/tool_start/tool_result/citation/artifact/usage/error/done. Render tokens as they arrive, or fold the whole stream withcollect_stream(events)→{text, thinking, tool_calls, citations, artifact, usage, stop_reason, error}.personaselects a capability persona (e.g.director= governed marts-only); it is intersected with the caller's scopes server-side, so it can only narrow, never escalate. Same auth/org/project headers + 401 re-auth retry as the blocking path.parse_sse_frames(buffer)is also exported for callers that frame SSE themselves. Wire format mirrors the platform's streaming endpoint and the web client's parser — the three are kept in sync.stream_chat(..., images=[…])/Agent.ask_stream(..., images=[…])— attach pictures for a vision-capable model.imagesis a list of content parts in the same shape the web/mobile clients send ({"type": "image_url", "image_url": {"url": "data:image/png;base64,…"}}); ignored server-side when the resolved model isn't vision-capable. Closes the SDK side of the web/mobile vision-attach feature.
Added (webapps — inline data-capture form)
form(form_id, fields, title=…, success_message=…)+field(name, label, type, …)— an INLINE{kind:'form'}block: declare the form's fields right in the webapp config (no separateformasset). Submissions POST to the webapp-scoped endpoint/api/(public/)webapps/{key}/forms/{form_id}/submit, are validated against the declared fields server-side (required / NIK / email / number bounds), rate-limited, and persist toform_submissionskeyed byform_id; the owner reads them viaGET /api/webapps/{key}/forms/{form_id}/submissions. This is the vibe-codable path (the AI editor emits inline fields directly).form_card(form_key, embed_token, …)remains for embedding an existing form.
Added (webapps — data-capture form block)
form_card(form_key, embed_token, title=…)— a no-code{kind:'form'}block that embeds an existing publishedformasset INSIDE a webapp, so an app can capture data (surveys, feedback, "submit a reading", data corrections) alongside its dashboards, charts, and games. No new write path: the field schema, validation (incl. Indonesian NIK), submit, CSV export, and the form→dataset pipeline all reuse the existing forms feature — both the schema fetch and the submit resolve from the form's publicembed_token, so it works identically in the authed host and an anonymous public viewer. List embeddable forms viaGET /api/publish/assets?asset_type=form. Advertised inclient.webapp_capabilities()(block kindform) and taught to the vibe-code AI editor.
Added (webapps — layout density theme tokens)
theme(sidebar_width=…)— expand the sidebar nav to 160–320px (default 256). Collapsed icon-only rail always stays 64px.theme(border_radius=…)— card and panel corner rounding:'none'/'sm'/'md'/'lg'/'xl'. Default'lg'(0.5rem). Set at the CSS-var level so all cards pick it up automatically.theme(content_padding=…)— main content area padding in pixels (0–48). Default 24 (p-6).theme(assistant_width=…)— assistant drawer width in pixels (280–1200). Mobile always full-width.theme(assistant_position=…)— which edge the assistant drawer slides from ('right'/'left').
Added (webapps — no-code leaderboard block)
leaderboard_card(board=…, limit=…, title=…)— a no-code{kind:'leaderboard'}block that renders this webapp's shared leaderboard (top scores) with no SQL and no game JS, auto-scoped to the webapp. Pairs with the in-gamehf.submitScore/Webapp.submit_score: a game writes scores, this block shows them. Advertised inclient.webapp_capabilities()(block kindleaderboard), so the vibe-code AI can add "show a leaderboard" as one block.
Added (webapps — game persistence)
html_block(..., persist_key=…)— a sandboxed block (game) can now persist state across reloads with no server: in-frame,hf.saveState(obj)stores a JSON value andawait hf.loadState()restores it on load. The fenced iframe (opaque origin) still can't touch storage itself — the trusted shell persists it to its ownlocalStorage, namespaced per webapp+block (this browser; no cross-device sync).persist_keypins a stable slot that survives reordering the block; omitted, it falls back to<pageKey>:<blockIndex>. Advertised inclient.webapp_capabilities()(window.hf.saveState/loadState), so an agent building a game just knows it can save a high score.
Added (webapps — server leaderboard + cross-device save)
Webapp.leaderboard(board=…, limit=…, player_id=…)andWebapp.submit_score(player_id, score, player_name=…, board=…, state=…)— read / write a webapp's shared, server-backed leaderboard. In a real game the sandboxed runtime callshf.submitScore(name, score)/await hf.leaderboard()and the shell posts on the fenced frame's behalf (the iframe holds no token and makes no network call); these SDK methods are the authed equivalent for tests/automation. One row per (webapp, board, player) keeps that player's BEST score + an optional cross-device save blob. Advertised inclient.webapp_capabilities()(window.hf.submitScore/leaderboard), so an AI-built game wires it up without being told the endpoint exists.
Added (webapps — vibe-code the whole app + version history)
Webapp.ai_edit(prompt, *, model=…, apply=False)— natural-language editing of the WHOLE webapp config (pages, cards, blocks, layout), not just the theme. The LLM proposes a complete new config; the server coerces it, caps page/block counts, forces every code/html SQL to a read-only SELECT, and runs the same structural validator the manual save path uses. BackendPOST /api/webapps/{key}/ai-editis preview-only — it never writes: it returns{"config", "summary", "prompt"}so you can inspect the change summary / diff before committing.apply=Truepersists the proposal through the normal versioned save, snapshotting it as a new version tagged"AI: <prompt>".Webapp.ai_edit(..., history=[…])— multi-turn chat: pass the prior conversation ([{"role": "user"|"assistant", "content": str}, …]) so the model resolves references to earlier turns ("now make it blue", "add a KPI row to that page"). Server-sanitized + capped. BackendPOST /api/webapps/{key}/ai-editnow accepts an optionalhistoryfield.Webapp.versions()/Webapp.rollback(version)/Webapp.diff_versions(a, b)— scrub the webapp's version history. Every save (manual builder edit or an appliedai_edit) snapshots a version, so this is one timeline across hand-edited and AI-edited states:versions()lists them (newest first),diff_versions()returns a key-by-key{path, kind, left, right}diff, androllback()non-destructively restores an earlier one (re-applied as a new version, so you can go back and forth). Wrap existingGET /api/publish/assets/{id},…/rollback/{version},…/versions/{a}/diff/{b}.- Fullscreen / chromeless webapps — set
chrome={"fullscreen": True}on a webapp config (e.g.client.create_webapp(..., chrome={"fullscreen": True})) to hide the sidebar nav AND the control header so the page content (e.g. an embedded game) fills the viewport. Also forced per-link by a?fullscreen=1or?embedURL param on the viewer — no config edit needed. client.webapp_capabilities()— discover, programmatically, what the webapp engine can build: every block kind + its schema, the sandbox/game contract (a sandboxed html block runs arbitrary canvas/WebGL JS — so games work), card types, the 12-col layout, and the size/SQL limits. Read it instead of hardcoding assumptions. The same self-description feedsWebapp.ai_edit, so the engine and the natural-language builder agree on what's possible. Wraps the newGET /api/webapps/capabilities.html_block(..., allow_cdn=True)— opt a sandboxed html block into a FIXED, vetted library-CDN allowlist (jsDelivr / unpkg / cdnjs / esm.sh) so a game or rich app can load an engine (Phaser, Three.js, Pixi) or assets from those CDNs instead of inlining everything within the 100k-char budget. Makes engine-based games first-class. The iframe stays opaque-origin (no API/JWT access) and only these CDNs are reachable — since inline JS is already permitted, this grants libraries, not new powers.
Added (datasets — register raw source tables)
Project.register_source_table(schema_name, table_name, *, connector_id=…, friendly_name=…, name=…)— register an existing raw connector/warehouse table as a discoverablesource_tabledataset without copying data (wrapsPOST /api/flow/datasets/register). Unlikeimport_dataset(catalog-only), this registers any physicalschema.table. Returns aDatasethandle.Project.register_schema(schema_name, *, connector_id=…, search=…)— bulk-register every table in a connector schema (idempotent). Also exposed asclient.register_source_table(...)/client.register_schema(...)shortcuts on the current project.- Backend:
POST /api/flow/datasets/registernow writes a resolver-correct row — clean barename+schema_namecolumn +connection_config={schema_name, table_name}— so registered tables are immediately queryable, not just visible in the picker. (Previously it storedschema.tableas the name with an emptyschema_name, which only resolved via a name-split fallback and left the resolver without a schema.) Collisions fall back toschema_table; the response gainsschema,table, and acreatedflag for idempotent callers.
Added (chat — embedded-agent auto-grounding)
Client.chat(...)andAgent.ask(...)gain optionalwebapp_key(andpage_key) params. When set, a SQL-mode agent auto-grounds in that webapp's dashboard cards (titles + exact card SQL) server-side, so programmatic chat matches the dashboard with no hand-written prompt — the same grounding the webapp viewer applies to its embedded assistant. Omitted → unchanged plain chat. Backend:POST /api/chatacceptswebapp_key/page_key. No new endpoint; passthrough fields, so this is purely an ergonomic surface on the SDK side.
0.1.6 — 2026-06-15
Single bulk release collapsing the never-published 0.1.6/0.1.7/0.1.8 phantom bumps (version files had raced ahead to 0.1.8 while PyPI sat at 0.1.5). PyPI goes 0.1.5 → 0.1.6 with everything below.
Added (app-builder surface — webapp flexibility hardening)
- Richer app-builder helpers: filter builders, page-level filters / header
signals, and
remove_page()/set_filters()/set_theme()on the app builder. htmlblock gains adata_sqlfield — sandboxed HTML blocks can render real DWH data; README data-bridge docs.
Added (interactive webapp bridge)
- Sandboxed blocks can drive page filters via the interactive bridge /
hfin-iframe runtime (webapp.py builder support).
Added (webapp font + size customization)
- New
theme(...)builder — assemble a webapp theme dict with named params (pass tocreate_webapp(theme=…)orWebapp.set_theme(…)). Beyond the existing colour/branding tokens it addsfont_family,font_scale(renderer clamps 0.7–1.5), andfont_url(loads a custom webfont). The renderer (AppShell) applies font-family to the whole app, scales root font-size, and injects the font stylesheet.
Added (theme editing — structured + natural-language)
Webapp.update_theme(patch)— apply a partial theme update, validated server-side against the theme registry (unknown keys dropped, values coerced/clamped; set a key toNoneto reset). Merges onto the current theme rather than replacing it likeset_theme.Webapp.ai_theme(prompt, model=…)— vibe-code the theme in natural language ("make the app font Poppins and bigger"); the LLM proposes a patch, the server validates it against the registry and applies it. Returns{theme, applied, …}.
Added (collaboration wrappers — Project Wiki G-G1 + Discussions G-G2)
- Project Wiki over
/api/wiki:project.list_wiki_pages(),project.create_wiki_page(title, body=…)andproject.get_wiki_page(slug)→ aWikiPagehandle (.title/.bodylazy-load,.update(title=…, body=…),.delete()). - Discussions — threaded object comments over
/api/discussions:project.list_comments(object_type, object_id)(nested thread, tombstones preserved),project.comment_count(...),project.add_comment(..., parent_id=…),project.edit_comment(comment_id, body)andproject.delete_comment(comment_id).
0.1.5 — 2026-06-14
Added (user-defined Meanings — the semantic-type registry, G-G3)
client.list_meanings()→ the org's semantic-type registry: platform built-ins (read-only,org_id: null) ∪ the org's own meanings, priority-ordered.client.create_meaning(name, label=…, detector=…, name_pattern=…, value_pattern=…, …)→ register a tenant meaning (e.g.mrnwith a conformancevalue_pattern); plusupdate_meaning(id, **fields)/delete_meaning(id). Built-ins are immutable (403).Dataset.meanings()→{column: meaning}the registry detected for the live schema;Dataset.validate_meaning(column)→ a sampled mismatch report{meaning, validatable, total, mismatched, pct_mismatch, examples}.- The trendable + gateable full-table conformance signal is the new
meaning_mismatchmetric type —Dataset.add_metric(name, metric_type= 'meaning_mismatch', column=…)rides the G-Q1 history / check / scenario gate. - Pairs with the platform G-G3 Meanings registry backend.
0.1.4 — 2026-06-14
Added (notebooks — DSSNotebook parity)
project.list_notebooks()/project.create_notebook(name, cells=…)/project.get_notebook(id)→ a newNotebookhandle.Notebook—get/cells/update/set_cells/append_cell(accepts source strings or full cell dicts) /delete. CRUD over a notebook's metadata + cell content (/api/notebooks). Kernel/session execution stays interactive (UI-only) by design.
0.1.3 — 2026-06-14
Added (plugin recipes — custom recipe types, G-E1)
client.plugin_recipe_types()→ the custom recipe types contributed by the plugins your org has enabled (GET /api/plugins/recipe-types). Each entry:{type, plugin, display_name, description, params, standalone}; empty when no plugins are enabled.project.create_plugin_recipe(recipe_type, name, config=…, inputs=…, run=…)→ a newPluginRecipehandle. Creates a flow recipe of a plugin-contributed type via/api/flow/ai-recipes; the server gates creation on the owning plugin being enabled for your org (403 otherwise).PluginRecipe—get/config/update_config(read-modify-writerun_config),run(dispatches the plugin's curated handler →status/rows_affected/output),runs,delete. MirrorsMlRecipe; both are Flow AI recipes under the same endpoints.- Pairs with the platform G-E1 backend (shipped v0.1.8) + the flow-palette UI.
0.1.2 — 2026-06-14
Added (API services — multi-endpoint serving + online model predict, G-L2)
client.create_api_service(...)/project.create_api_service(...)→ a newAPIServicehandle. An API service groups several key-gated endpoints under one published asset (asset_type='api_service').- Endpoint builders —
honeyframeapi.sql_endpoint,lookup_endpoint,model_endpoint,function_endpointcompose the endpoint list without hand-rolling dicts. APIService—add_endpoint/set_endpoints(read-modify-write the config),publish/unpublish,create_key(allowed_endpoints=…)(scope a key to a subset of endpoints),call(endpoint, payload, api_key=…)(hits theX-API-Keyserving route — sql/lookup via GET+params, model online predict via POST), andtest(...)(the JWT builder dry-run).PublishedAsset.create_api_keygainedallowed_endpoints(None = all).- Pairs with the platform G-L2 serving backend;
functionendpoints return 501 until the sandbox lands (G-E1).
0.1.1 — 2026-06-14
Added (ML training — create models, not just read them)
project.train_model / score_model / cluster_model / evaluate_model(...)project.create_ml_recipe(...)→ newMlRecipehandle (run/runs/ update_config/delete). Backed by/api/flow/ai-recipes. Previously the SDK could only read models (SavedModel); now it can create + run training, scoring, clustering and evaluation. The typed helpers build therun_config(table/target/features/algorithm) so you don't hand-assemble it.
Added (webapp building — author + edit + publish a SaaS app from code)
- Page/block builders —
honeyframeapi.page,card_ref,html_block,code_card,nav_itemcompose a webapp config without hand-rolling dicts.code_card(sql, "process_map")covers the process-graph card type shipped in platform v0.1.7 (edge-list or raw event log → directed graph). client.create_webapp(asset_key, title, pages=[…], nav=[…], publish=True)— one call to build and (optionally) publish a standalone app live at/apps/<key>.Webapplifecycle methods —update(),publish(),unpublish(),add_page(),set_nav(). PreviouslyWebappwas read + share-link only, so a webapp could be created but never edited from the SDK.CAPABILITIES.md— full audit of platform capabilities vs SDK coverage, documenting the rawclient._get/_post/_put/_deleteescape hatch for the niche/admin endpoints that have no typed wrapper yet (traces, catalog, governance, …).
Changed
client.publish_webapp(..., publish=False)gained an opt-inpublishflag. It still creates a draft by default (unchanged behavior) — but the webapp viewer only serves published assets, sopublish=True(orWebapp.publish()) is what makes it live. No breaking change.
Tests
- 19 new (
tests/test_webapp_builders.py+tests/test_ml_recipe.py); suite 114 → 133.
0.1.0 — 2026-06-02
Changed
- Version reset
0.4.10→0.1.0for the first public PyPI release. The prior0.4.xline was internal-only (pip install -e sdk/pythonfrom a repo checkout). Public versioning starts fresh at0.1.0and counts upward from here — PyPI version numbers are permanent and monotonic. The codebase is unchanged from0.4.10; this is a numbering reset, not a feature regression.
Entries below
0.1.0document the internal0.4.xhistory (pre-PyPI).
0.4.10 — 2026-06-01
Fixed
- Native-authoring docs pointed at a method signature that doesn't exist.
project.engine()'s docstring and the end-to-end example told native-engine users to callget_recipe(name).save(..., target='sql')— butRecipe.save()has notargetparameter, so copy-pasting it raisedTypeError. Corrected to the real API: author withsave_prepare_steps(...)/save(recipe_type=…); the server materialises through the native engine based on the org's engine profile (no per-call flag).
Changed
- The end-to-end example now finishes at a shareable URL, not a DataFrame.
examples/vibe_code_end_to_end.pygained the last three legs of the loop: build a dashboard on the result →publish_webapp(...)→create_public_link(), printing the token-gated public link. This exercises the 0.4.9 webapp surface and is engine-agnostic (the share leg runs identically on dbt and native). Its pin-test asserts the full call sequence for both engine profiles.
0.4.9 — 2026-06-01
Added (webapps — the last mile of the vibe-code loop)
Webappresource + client factories — publish a dashboard as a shareable webapp and mint token-gated public links, all from code:client.list_webapps()→ org's webapps (GET /api/webapps).client.get_webapp(key)→Webapphandle with fullconfig.client.publish_webapp(asset_key, title, config=…, is_public=…)→ publishes anasset_type='webapp'asset (project-scoped — needs the client'sproject_id) and returns its handle.Webapp.public_links()/.create_public_link(label=, expires_days=)/.revoke_public_link(id)wrap the v0.0.95 share-token endpoints. The raw token + relativeshare_urlcome back once fromcreate_public_link.Webapp.url(authed/apps/{key}) andWebapp.public_url(token, host=)(anonymous/public/apps/{key}?token=…on the SaaS host) helpers.
0.4.8 — 2026-06-01
Fixed (typing)
- The package now type-checks clean (
python -m mypy honeyframeapi→ no issues). Now that 0.4.7 shipspy.typed, the hints are validated:- Resolve forward-ref return types (
-> Project/Dashboard/DbtIDE/…) viaTYPE_CHECKINGimports inclient.py/project.py. - Resource accessors typed
-> int/-> strnow subscript the (API- guaranteed) field instead of.get()— fails loudly on a contract violation rather than returning a mistypedNone. - Annotate mixed-value request-body dicts as
dict[str, Any]. - Guarantee
_send_with_retryalways returns or raises.
- Resolve forward-ref return types (
- Added a
[tool.mypy]config +mypydev dependency so the check is reproducible. No runtime/behaviour change.
0.4.7 — 2026-06-01
Packaging
- Ship the PEP 561
py.typedmarker. The package is fully type-hinted but the marker was missing, so downstream mypy/pyright and IDEs ignored every hint. Now consumers get autocomplete + type-checking against the SDK. A test guards the marker so it can't be dropped.
0.4.6 — 2026-06-01
Added
- CLI parity for the warehouse loop — new
honeyframesubcommands so the whole loop is drivable from a shell, no Python required:honeyframe engine -p <id>— is this install dbt or native?honeyframe lineage <dataset> [--down] -p <id>— upstream/downstreamhoneyframe upload <name> <file.csv|xlsx> [--replace] [--materialize] -p <id>honeyframe export <dataset> <out.csv|parquet> [--max-rows N] -p <id>
0.4.5 — 2026-06-01
Added
Dataset.to_csv(path)/Dataset.to_parquet(path)— export a dataset to a local file, closing the round trip withProject.upload_file(0.4.1). Reads through the same paginated/explorepath asget_dataframe, so server-side data-policy / PII masking is applied — the export never sees more than the caller can browse.to_csvworks without pandas (stdlibcsv);to_parquetneeds pandas + pyarrow. Both takemax_rowsto cap large pulls.
0.4.4 — 2026-06-01
Added
examples/vibe_code_end_to_end.py— a runnable, engine-aware template for the full loop: detect engine → upload data → author (dbt model on a dbt install, or read-through on a native install) → run → read → trace lineage. Driven against a mock backend intests/test_example_end_to_end.pyfor bothdbtandnativeprofiles, so it can't bit-rot.
0.4.3 — 2026-06-01
Added
- Engine detection —
Project.engine()returns the org's catalog engine profile ('dbt'or'native') andProject.uses_dbt()is the boolean shorthand, overGET /api/catalog/engine(cached per handle). Lets a script know whether this Honeyframe has dbt installed (author dbt models viaproject.dbt) or is a standalone native-only install (authortarget='sql'recipes) — so the same automation can drive either deployment.
With 0.4.1 (upload) + 0.4.2 (lineage), the SDK now covers the end-to-end "vibe code the warehouse" loop: detect engine → upload data → author (dbt or native) → run → read → trace lineage, all governed through the API. A backend contract test pins that every endpoint in that loop stays registered.
0.4.2 — 2026-06-01
Added
- Lineage introspection —
Dataset.upstream()/Dataset.downstream()(recursive ancestors / dependents of a model) andProject.dag()(the project's full{nodes, edges}graph), over/api/lineage/*. Works on both dbt and native-engine tenants (reads through the model catalog). Lets a script answer "what feeds this?" / "what breaks if I change it?" without the UI.
0.4.1 — 2026-05-31
Added
Project.upload_dataframe(name, df)andProject.upload_file(name, path)— push local data in as a managed (file-backed) dataset viaPOST /api/datasets/file/upload(DataFrame → CSV → Parquet on the server;.csv/.xlsxfiles supported). Returns aDatasethandle; passmaterialize=Trueto copy it intot<pid>_uploads.<name>immediately so dbt / dashboards / agents can read it. Completes the round trip — the SDK could already read datasets, now it can create them from local data.
0.4.0 — 2026-05-31
Development-through-the-API: the SDK can now author the warehouse, not just read it. All changes are backward-compatible additions.
Added
project.dbt→DbtIDE— dbt authoring over/api/dbt/*:write_file/read_file/list_files/delete_file,create_model,run/test/build/execute,compile,list_models/model_names/runs, andgit_status/commit/push. Warehouse DDL goes through dbt (the dbt-native engine contract), not raw DDL.- Transient-failure retry in the HTTP layer — idempotent requests
(GET/HEAD/OPTIONS) retry on a transport error or a
502/503/504with exponential backoff. Configurable viamax_retries(default 2) /retry_backoff(default 0.5s);max_retries=0disables. Writes are never auto-retried. The 401 re-auth retry is unchanged.
Changed
client.sql()/project.sql()preferPOST /api/sql(governed, SELECT-guarded, row-limited) and fall back to the scratch-card executor only on a 404 from older servers. No API change for callers.
Fixed
- Scratch-card SQL fallback no longer leaks an orphan dashboard when a query
fails — the temp card is always deleted, and a scratch dashboard created by
the failing call is dropped too. A 403 on the fallback now re-raises as
HoneyframeAuthErrorwith a "needs project.edit" hint instead of a bare error.
0.3.0 — 2026-05-30
Added
honeyframeCLI (honeyframeapi.cli) —whoami,version,projects,datasets,sql,pat create|list|revoke,scenario list|run [--wait], with--format json|csv. ReadsHONEYFRAME_*/HUB_*env vars.Recipe.save(recipe_type=…, steps=…)— full-control recipe authoring viaPOST /api/flow/save-recipe-steps(any recipe type), alongside the existingsave_prepare_steps.- Packaging: console-script entry point;
pip install "honeyframeapi[pandas]".
0.2.0 — 2026-05-30
Added
- Personal Access Tokens —
client.create_pat / list_pats / revoke_pat;TokenAuthacceptshf_…tokens. (Backend:/api/auth/tokens+ a settings UI shipped in the same change.) - New handles:
Connector(create/test/update/delete),Scenario(run/run_and_wait/get_last_runs),client.jobs(list/trigger/ logs/wait),Recipe(metadata/runs/build/save_prepare_steps),PublishedAsset(publish + data-API-key minting),Agent.ask,Project.create_dataset/import_dataset,Dataset.materialize, project preferences.
0.1.0 — 2026-05-30
Added
- Core client + auth (
LoginAuthwith 401-refresh,TokenAuth), typed exceptions,X-Org-Id/X-Project-Idinjection. - Read path:
Project/Datasetwithget_dataframe()(paginated/explore),head,profile,ask;client.sql()→ DataFrame (scratch-card executor). - Dashboards:
create_dashboard → add_card/add_cards → execute_all. - pandas as an optional extra; mocked-transport test suite.
Verified against honeyframeapi 0.1.8.