Skip to main content

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 named x/../../auth/tokens/7 can no longer turn Dataset.delete() into a token revocation). delete_variable sends scope as a query parameter; Webapp.public_url encodes the key and token.
  • Exceptions no longer carry credentials. A failed login raises with no response (its request body was the password). Every other HoneyframeAPIError.response is a copy whose .request has no Authorization / Cookie / X-API-Key header and no body. detail is capped at 4 KB. TokenAuth / LoginAuth reprs 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 / \r line 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 and Notebook.export_ipynb (an existing file keeps its permissions).
  • CLI: --token-file PATH and --token - (stdin) keep the token out of ps and shell history; a literal --token VALUE still works but warns on stderr. honeyframe pat create keeps the JSON (with the secret) on stdout and adds a "store it now" reminder on stderr.
  • honeyframe plugin run warns on stderr that plugin code runs in-process as the current user, and hides HONEYFRAME_* / HUB_* variables from the plugin unless --pass-env is given (run_component(pass_env=False) in the library). pack_plugin skips 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/sql itself 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 with HoneyframeConfigError (credentials would travel in clear text), except for loopback hosts (localhost, 127.0.0.1, ::1). Opt in for a trusted private network with allow_insecure_http=True or HONEYFRAME_ALLOW_INSECURE_HTTP=1 (a warning is still emitted). A base URL without an http(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=0 is the documented way to make a link that never expires; values outside 0 and 1..365 raise ValueError (the platform would silently make them never-expiring); expires_days=None still means never but is deprecated.
  • create_pat defaults to expires_days=90 and validates 0..730 (0 = never; None sends 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 and sensitivity_max="medium" (previously never and "critical"). With "medium", columns classified high (e-mail, phone, address, date of birth) or critical (national ID, record numbers) come back [BLOCKED], while a medium column such as a person's name comes back masked. Pass sensitivity_max="high"/"critical" and/or expires_days=0 to keep the old behaviour. An unknown tier or a negative lifetime raises ValueError.
  • Project.run_dbt(wait=True) and Recipe.build(wait=True) now really wait. The trigger response has no run id, so they were silent no-ops; they now go through the new JobsAPI.trigger_and_wait, which finds the run the trigger started in GET /api/pipeline/runs and polls it to a final status (raises TimeoutError when no run appears within start_timeout).
  • Project.list_dashboards() passes project_id to 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_runs raises ValueError for a limit outside 1–200 (the server's page cap) or a negative offset.
  • experiments.iter_runs documents that it returns the newest limit runs (default 200, not every run) and takes limit up 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).
  • httpx is bounded below 1.0 (httpx>=0.24,<1).
  • Name/title properties load the record on first use instead of raising KeyError on 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 an ssl.SSLContext, and warns on verify=False.
  • HoneyframeClient(transport=…) plugs in any httpx.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/_delete keep working.
  • Per-call timeout= on Dashboard.export_pdf, Dashboard.execute_all, Dataset.materialize and client.request.
  • client.sql() / Project.sql() take params= (named binds) and warn + set df.attrs["truncated"] = True when the server reports the result was cut at row_limit.
  • JobsAPI.trigger_and_wait, list_pipeline_runs, get_pipeline_run; JobsAPI.wait(raise_on_timeout=True); skipped is a terminal status (JobsAPI.TERMINAL_STATUSES).
  • client.list_dashboards(project_id=…).
  • locale="id" | "en" on banner() and date_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 of branch_filter() (same arguments, same wire type).
  • parquet extra (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_PASSWORD and HUB_PROJECT_ID environment fallbacks emit a DeprecationWarning; use the HONEYFRAME_* names.
  • Dashboard.add_card(config=…): use card_config= (still honoured, with a DeprecationWarning).
  • expires_days=None on create_public_link, create_api_key and APIService.create_key: pass expires_days=0 for "never".

Fixed​

  • The package type-checks with mypy without implicit_optional; = None parameters are annotated Optional[...] and public callables have return annotations.
  • The test for run_dbt no longer mocks a run_id the platform never returns.
  • CAPABILITIES.md: project.dbt is a property; knowledge banks and LLM Mesh are wrapped; the escape hatch is client.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 corrected client.sql() note (it runs through POST /api/sql; the scratch card is only a fallback on older platforms). project.dbt is a property, not a call.
  • No other product or customer names in the README, package metadata or docstrings; Homepage / Documentation URLs point at https://docs.honeyframe.io/.
  • The CLI docstring no longer claims a HONEYFRAME_ORG_ID variable (the org is chosen with -o/--org).
  • The sdist now ships the package only (MANIFEST.in prunes tests/ and examples/).

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. New DatasetStatistics / StatisticsWorksheet handles over /api/statistics, reached as dataset.statistics and exported from the package root alongside stat_card and CARD_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=)
  • Card types are validated client-side against the same list the platform keeps, so compute("univarate", …) raises ValueError naming the nearest match and the full catalog instead of costing a 400 round-trip. Required parameters and agg are 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 handle cards, 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 + reason payloads (shapiro_wilk under n=3, paired_ttest under 2 complete pairs) and its sampled / gap_filled / constant_columns flags. Check available before reading a p-value.
  • Two limits are documented rather than papered over: worksheets need the dataset_worksheets migration (an un-migrated tenant reads back as an empty list), and v1 computes on warehouse + uploaded-file datasets only — a lakehouse/external dataset raises HoneyframeValidationError carrying 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_artifact could put a file in and artifacts could list it, but nothing could read it back. Returns the raw bytes, and writes them to dest as 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/experiments surface (log params, metrics and artifacts per run). New Experiment / ExperimentRun handles, exported from the package root, plus three Project entry 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-fetched run_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_run closes the run on exit and as failed when the block raises — a crashed training script leaves a failed run rather than one stuck running forever. The caller's exception always propagates; a failing finish never masks it.
    • compare() returns missing_run_ids for ids that are not in the experiment. Check it: a metric a run never logged is null in the matrix, never 0.
    • promote() works only on a finished Analysis Lab run — the only kind the platform can rebuild a reproducible training recipe from. Anything else raises HoneyframeValidationError naming 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/height are render-window pixels bounded 320–4096 (one axis alone keeps the paper's other axis), scale is the device-pixel-ratio 0.5–3.0 (raster only). Out-of-range values raise the server's 422 carrying 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=True polls code_environment_build_log(id) until the state leaves building and raises TimeoutError (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 on status: it is derived from the row AND the venv on disk, so a ready row whose tree was wiped reads as not_built. stale means 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 returns started: False with 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 reached project.edit (via GET /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 200 with datasets: [], 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". truncated goes true when the server's caps (50 dashboards / 100 datasets / 200 columns) clipped the response, so partial coverage is reportable rather than implied.
    • page_key narrows to one page and is URL-encoded through the client's existing params= path; omitted entirely when not passed (page_key= is a different request from no page_key).
  • dataset.update_settings(catalog={...}) — new kwarg on the existing call, the writer for the authored half of the above. Lands in settings.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 at None.
  • card_description on card_ref blocks (no SDK change — documented on Webapp.catalog): backends new enough to serve the catalog endpoint also attach the card author's "Sumber data" prose to each card_ref in the config returned by client.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-card POST …/execute fan-out of a page load into one round-trip; each card runs exactly as card.execute() would (same gate, same :name bind merge, same masking), so this is the batched form of the single-card path, not of execute_all (which speaks {{param}}/filters instead).
    • Returns {card_id: {card_id, columns, rows, row_count, execution_ms, error, bound_params}}. Those are JSON object keys, i.e. strings — index with res[str(cid)]. A failing card reports in-band via its own error and never sinks the batch.
    • card_ids is 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_id serves 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.

Added (429: rate limit ≠ budget exceeded)​

  • HoneyframeRateLimitError and HoneyframeBudgetExceededError, both exported from the package root. error_for_status had no 429 branch at all, so every 429 fell through to the generic HoneyframeAPIError — collapsing the upstream gateway's rate_limit_exceeded (wait Retry-After seconds; retrying IS correct) with budget_exceeded (the metered budget is spent until the billing month rolls; retrying can never succeed). Per docs/trafo.md (2026-08-25) the gateway owns budget/rate enforcement outright, so the SDK has to tell the two apart.
    • HoneyframeRateLimitError.retry_after is parsed from the Retry-After header's delta-seconds form. The HTTP-date form, a negative value and an absent header all degrade to None rather 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; only budget_exceeded escalates 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_STATUS stays {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": …} with status ∈ valid | grace | expired | missing | invalid (plus customer/tier/products/users_limit/max_seats/ features/expires/days_remaining/version/org_slug when a license resolves, and a human-readable message in the non-valid states). Only valid and grace pass 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-scoped project.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=None is the cross-project org-wide search; a project_id outside your org is a 404 server-side. Returns {"results": [...], "intent": …, "count": n}.
  • project.list_dataset_tags() → GET /api/datasets/tags (union of the dedicated tags[] column and the legacy settings.tags path — both live).
  • project.apply_dataset_tags(names, *, add=None, remove=None) and the single-dataset dataset.apply_tags(add=…, remove=…) → POST /api/datasets/tags/apply; per dataset tags = (existing ∪ add) − remove, written to both storage paths. Returns {"updated": n, "missing": [...]} — a name the project doesn't have is reported, not raised. Raises ValueError locally when add and remove are 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 (/config 404s on registered and imported datasets). update_settings sends only the fields you pass; every argument left at None is omitted and the stored value is untouched. Note this is genuinely a patch, not a clear: the router skips null fields, so partitioning=None cannot 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.py hard-imported pandas at module scope (and never used the alias), breaking collection of the whole file in a pandas-free environment. It now uses the house pytest.importorskip("pandas") guard (matching tests/test_upload.py), and the two DataFrame-asserting tests in tests/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.md now 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 of branches_sql = one chip (label/name + optional detail/value). Executes via the same governed per-block execute path code_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. Raises ValueError on blank branches_sql (a banner with no live query has nothing honest to show) and on an invalid severity (must be info / warning / critical) — same discipline as chrome()'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's data-hf-card-index wrapper and the ?hfcard= metric-search deep link already use). page() raises ValueError at build time if any anchor's block_index is out of range for the blocks actually passed — the same discipline chrome() 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. Publishes param plus the CSV companion <param>_codes exactly like a branch slot; options accept {"value","label","code"?} dicts or bare strings; all_label, empty_value and chips map to allLabel / emptyValue / chips. Raises on an empty option list (a pill row that filters nothing) and on a bad mode.
  • 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 in mode="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 no show_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 a read_only PAT can call it. Errors return in-band as error; truncated flags the 5000-row cap. Completes the read surface next to Card.execute — card_ref pages 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 with self.reload(), so a large config PUT that the server persisted still raised if the follow-up GET /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, sets Webapp.last_warning, and emits a UserWarning naming the write as persisted. No signature change; a clean call leaves last_warning None.

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 new PUT /api/flow/recipes/{id}/sql (native-engine orgs; a dbt org gets 409). Recipe.save() can only compile inline_code for recipe_type='prepare'; sql / join / groupby / window / distinct saved with inline_code = NULL and then failed build_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_version is the same optimistic lock as Recipe.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 native dbt run. select takes the dbt selector shapes (model, model+, +model, +model+, comma- separated); dry_run=True returns the resolved order without executing; background=True returns {run_id, status:"running"}, plus wait=True to 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_refs and UPDATE honeyframe.flow_recipes directly 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). name defaults to the file stem.
  • Notebook.to_ipynb() / Notebook.export_ipynb(path) — export a platform notebook back to nbformat-4.5 (dict, or written .ipynb file). Sources round-trip with import_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|pack CLI (and the honeyframeapi.plugin_dev module 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_adapter scaffolds a working plugin.json + entry template that passes check.
    • check <dir> mirrors the platform kernel's descriptor validator (id/name regexes, supported vs deferred component types, params schema, select choices, scenario_step secret-param rejection, monitor_adapter descriptor-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). ⚠️ Local run is 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.json at 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.reload is now self-typed so subclass handles (APIService) keep their type through it, and APIService.publish/unpublish return the server dict like the base (Liskov-clean). Also fixed Bundle.activate, client.create_meaning, and the SavedModel forward-refs on Project.
  • New tests/test_mypy_clean.py gate shells mypy honeyframeapi and asserts 0 errors (skips cleanly when mypy isn't installed, so the offline suite still passes everywhere).
  • DbtIDE is now exported at the top level (honeyframeapi.DbtIDE).

Added (knowledge banks — RAG corpora)​

  • New KnowledgeBase handle + client factories client.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. KnowledgeBase covers 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 reusable vector_search agent-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, …}. Plus Project.list_signoffs() (per-version status + audit log + the org sign-off mode) and Project.list_bundle_activations(). Makes a CI/CD promote gate scriptable — request a sign-off on a bundle version, poll governance_status, then deploy once 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 existing versions() / 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 are sql in-database pushdown + dbt). Project.change_recipe_engines(names, target, *, dry_run=…) — the bulk "Change N recipe engines" mass action (target ∈ sql | dbt | auto; dry_run reports 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), and llm_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_type is a free string and list_connector_catalog() already surfaces every type the platform advertises.

Fixed (scenario reporter docstring drift)​

  • Scenario.add_reporter docstring corrected to the real router enums: kind ∈ mail | slack | teams | webhook (was "email", which the server 400s) and trigger ∈ always | success | failure | custom (was on_success / on_failure). Behaviour unchanged — kind/trigger were 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 typed HoneyframeError pointing at the in-browser fallback.
  • Dashboard.execute_all(filters=[…]) + builders dashboard_filter, filter_between, filter_compare, filter_in — global filters mirroring the backend DashboardFilter model (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 → new CardTemplate handle (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 timeline Webapp already 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 single trigger_type field, 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). Wraps POST /api/push/send; returns {sent, pruned, total, users}. The primitive behind proactive notifications — e.g. the director-briefing recipe pushing role="director".

Added (chat — streaming)​

  • client.stream_chat(message, *, persona=…, agent_id=…, …) and Agent.ask_stream(message, …) — consume the streaming endpoint POST /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 with collect_stream(events) → {text, thinking, tool_calls, citations, artifact, usage, stop_reason, error}. persona selects 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. images is 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 separate form asset). 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 to form_submissions keyed by form_id; the owner reads them via GET /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 published form asset 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 public embed_token, so it works identically in the authed host and an anonymous public viewer. List embeddable forms via GET /api/publish/assets?asset_type=form. Advertised in client.webapp_capabilities() (block kind form) 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-game hf.submitScore / Webapp.submit_score: a game writes scores, this block shows them. Advertised in client.webapp_capabilities() (block kind leaderboard), 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 and await hf.loadState() restores it on load. The fenced iframe (opaque origin) still can't touch storage itself — the trusted shell persists it to its own localStorage, namespaced per webapp+block (this browser; no cross-device sync). persist_key pins a stable slot that survives reordering the block; omitted, it falls back to <pageKey>:<blockIndex>. Advertised in client.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=…) and Webapp.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 calls hf.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 in client.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. Backend POST /api/webapps/{key}/ai-edit is preview-only — it never writes: it returns {"config", "summary", "prompt"} so you can inspect the change summary / diff before committing. apply=True persists 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. Backend POST /api/webapps/{key}/ai-edit now accepts an optional history field.
  • Webapp.versions() / Webapp.rollback(version) / Webapp.diff_versions(a, b) — scrub the webapp's version history. Every save (manual builder edit or an applied ai_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, and rollback() non-destructively restores an earlier one (re-applied as a new version, so you can go back and forth). Wrap existing GET /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=1 or ?embed URL 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 feeds Webapp.ai_edit, so the engine and the natural-language builder agree on what's possible. Wraps the new GET /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 discoverable source_table dataset without copying data (wraps POST /api/flow/datasets/register). Unlike import_dataset (catalog-only), this registers any physical schema.table. Returns a Dataset handle.
  • Project.register_schema(schema_name, *, connector_id=…, search=…) — bulk-register every table in a connector schema (idempotent). Also exposed as client.register_source_table(...) / client.register_schema(...) shortcuts on the current project.
  • Backend: POST /api/flow/datasets/register now writes a resolver-correct row — clean bare name + schema_name column + connection_config={schema_name, table_name} — so registered tables are immediately queryable, not just visible in the picker. (Previously it stored schema.table as the name with an empty schema_name, which only resolved via a name-split fallback and left the resolver without a schema.) Collisions fall back to schema_table; the response gains schema, table, and a created flag for idempotent callers.

Added (chat — embedded-agent auto-grounding)​

  • Client.chat(...) and Agent.ask(...) gain optional webapp_key (and page_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/chat accepts webapp_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.
  • html block gains a data_sql field — 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 / hf in-iframe runtime (webapp.py builder support).

Added (webapp font + size customization)​

  • New theme(...) builder — assemble a webapp theme dict with named params (pass to create_webapp(theme=…) or Webapp.set_theme(…)). Beyond the existing colour/branding tokens it adds font_family, font_scale (renderer clamps 0.7–1.5), and font_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 to None to reset). Merges onto the current theme rather than replacing it like set_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=…) and project.get_wiki_page(slug) → a WikiPage handle (.title / .body lazy-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) and project.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. mrn with a conformance value_pattern); plus update_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_mismatch metric 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 new Notebook handle.
  • 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 new PluginRecipe handle. 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-write run_config), run (dispatches the plugin's curated handler → status / rows_affected / output), runs, delete. Mirrors MlRecipe; 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 new APIService handle. 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_endpoint compose 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 the X-API-Key serving route — sql/lookup via GET+params, model online predict via POST), and test(...) (the JWT builder dry-run).
  • PublishedAsset.create_api_key gained allowed_endpoints (None = all).
  • Pairs with the platform G-L2 serving backend; function endpoints 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(...) → new MlRecipe handle (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 the run_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_item compose 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>.
  • Webapp lifecycle methods — update(), publish(), unpublish(), add_page(), set_nav(). Previously Webapp was 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 raw client._get/_post/_put/_delete escape hatch for the niche/admin endpoints that have no typed wrapper yet (traces, catalog, governance, …).

Changed​

  • client.publish_webapp(..., publish=False) gained an opt-in publish flag. It still creates a draft by default (unchanged behavior) — but the webapp viewer only serves published assets, so publish=True (or Webapp.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.0 for the first public PyPI release. The prior 0.4.x line was internal-only (pip install -e sdk/python from a repo checkout). Public versioning starts fresh at 0.1.0 and counts upward from here — PyPI version numbers are permanent and monotonic. The codebase is unchanged from 0.4.10; this is a numbering reset, not a feature regression.

Entries below 0.1.0 document the internal 0.4.x history (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 call get_recipe(name).save(..., target='sql') — but Recipe.save() has no target parameter, so copy-pasting it raised TypeError. Corrected to the real API: author with save_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.py gained 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)​

  • Webapp resource + 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) → Webapp handle with full config.
    • client.publish_webapp(asset_key, title, config=…, is_public=…) → publishes an asset_type='webapp' asset (project-scoped — needs the client's project_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 + relative share_url come back once from create_public_link.
    • Webapp.url (authed /apps/{key}) and Webapp.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 ships py.typed, the hints are validated:
    • Resolve forward-ref return types (-> Project/Dashboard/DbtIDE/…) via TYPE_CHECKING imports in client.py / project.py.
    • Resource accessors typed -> int/-> str now subscript the (API- guaranteed) field instead of .get() — fails loudly on a contract violation rather than returning a mistyped None.
    • Annotate mixed-value request-body dicts as dict[str, Any].
    • Guarantee _send_with_retry always returns or raises.
  • Added a [tool.mypy] config + mypy dev dependency so the check is reproducible. No runtime/behaviour change.

0.4.7 — 2026-06-01​

Packaging​

  • Ship the PEP 561 py.typed marker. 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 honeyframe subcommands 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/downstream
    • honeyframe 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 with Project.upload_file (0.4.1). Reads through the same paginated /explore path as get_dataframe, so server-side data-policy / PII masking is applied — the export never sees more than the caller can browse. to_csv works without pandas (stdlib csv); to_parquet needs pandas + pyarrow. Both take max_rows to 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 in tests/test_example_end_to_end.py for both dbt and native profiles, 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') and Project.uses_dbt() is the boolean shorthand, over GET /api/catalog/engine (cached per handle). Lets a script know whether this Honeyframe has dbt installed (author dbt models via project.dbt) or is a standalone native-only install (author target='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) and Project.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) and Project.upload_file(name, path) — push local data in as a managed (file-backed) dataset via POST /api/datasets/file/upload (DataFrame → CSV → Parquet on the server; .csv/.xlsx files supported). Returns a Dataset handle; pass materialize=True to copy it into t<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, and git_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/504 with exponential backoff. Configurable via max_retries (default 2) / retry_backoff (default 0.5s); max_retries=0 disables. Writes are never auto-retried. The 401 re-auth retry is unchanged.

Changed​

  • client.sql() / project.sql() prefer POST /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 HoneyframeAuthError with a "needs project.edit" hint instead of a bare error.

0.3.0 — 2026-05-30​

Added​

  • honeyframe CLI (honeyframeapi.cli) — whoami, version, projects, datasets, sql, pat create|list|revoke, scenario list|run [--wait], with --format json|csv. Reads HONEYFRAME_*/HUB_* env vars.
  • Recipe.save(recipe_type=…, steps=…) — full-control recipe authoring via POST /api/flow/save-recipe-steps (any recipe type), alongside the existing save_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; TokenAuth accepts hf_… 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 (LoginAuth with 401-refresh, TokenAuth), typed exceptions, X-Org-Id/X-Project-Id injection.
  • Read path: Project/Dataset with get_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.