From 9cc804dbb808b243cbfd1ff5e68feeae1ea65c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Jir=C3=A1t?= Date: Thu, 24 Sep 2026 11:23:28 +0200 Subject: [PATCH 1/2] feat(data-app): make python-js the default everywhere, incl. type-less sync creates `data-app create` already defaulted to `--type python-js`, but `sync push` / `sync clone` created a data app with no recorded `_keboola.data_app_type` through the plain Storage path, leaving the platform to pick its own default (streamlit). Such a create now goes through the Data Science `create_app` as python-js, records the type in the local `_keboola` block, and adds a `data_app_type_default` warning to the push result naming how to keep a Streamlit app a Streamlit app. Docs, agent context, the kbagent skill/agent, the serve OpenAPI tag and the web UI now lead with Python/JS as the recommended runtime; Streamlit stays available via `--type streamlit`. The Streamlit quick recipe (which ran a Streamlit repo under the python-js default) is replaced by a python-js one. --- README.md | 2 +- docs/web-server-endpoints.md | 2 +- plugins/kbagent/agents/keboola-expert.md | 2 +- plugins/kbagent/skills/kbagent/SKILL.md | 2 +- .../kbagent/references/commands-reference.md | 4 +- .../kbagent/references/data-app-workflow.md | 24 ++++++++--- .../skills/kbagent/references/gotchas.md | 2 +- src/keboola_agent_cli/commands/context.py | 5 ++- src/keboola_agent_cli/server/app.py | 2 +- .../services/_sync_push_ops.py | 41 +++++++++++++++--- tests/test_sync_data_app_type.py | 43 +++++++++++++------ web/frontend/src/pages/DataApps.tsx | 2 +- 12 files changed, 96 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 99acc299..1c7370ef 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ kbagent workspace query --project prod --workspace-id WS_ID \ | **Agent Tasks** | Schedule AI agents inside `kbagent serve` (CRON / manual / chained). Two action flavours per task: `claude` / `codex` / `gemini` with a prompt, or a raw kbagent CLI command. Per-run cost & token timeline with authoritative Claude 4.x pricing built-in; persisted JSONL history (`0600`); live SSE replay; **Artifacts tab** auto-renders long-form markdown reports (GFM tables, Copy / Download `.md`). Subprocesses get `KBAGENT_SERVE_URL` + `KBAGENT_SERVE_TOKEN` auto-injected for self-calls via `kbagent http`. (since 0.40.0) | | **Workspaces** | Create Snowflake/BQ workspace, load tables, run SQL. Create from transformation config for instant debugging. Orphan detection + garbage collection. | | **Sharing** | Cross-project bucket sharing with org/project/user access control. Share, link, unlink. | -| **Data apps** | First-class lifecycle for Streamlit / Flask / Node deployments (`keboola.data-apps`). `create / deploy / start / stop / password / delete` (since 0.27.0); `secrets-set / -list / -get / -remove` for `#`-prefixed runtime secrets with per-project KMS encryption (since 0.29.0); `validate-repo` pre-flight Golden Rule check that catches misconfigured git repos before a deploy (since 0.29.0); `logs` tails the container log buffer for triaging stuck deploys / runtime crashes (since 0.43.8). Hides the redeploy contract and per-project KMS encryption of git PATs. | +| **Data apps** | First-class lifecycle for Python/JS apps (`keboola.data-apps`; `python-js` is the default type, Streamlit via `--type streamlit`). `create / deploy / start / stop / password / delete` (since 0.27.0); `secrets-set / -list / -get / -remove` for `#`-prefixed runtime secrets with per-project KMS encryption (since 0.29.0); `validate-repo` pre-flight Golden Rule check that catches misconfigured git repos before a deploy (since 0.29.0); `logs` tails the container log buffer for triaging stuck deploys / runtime crashes (since 0.43.8). Hides the redeploy contract and per-project KMS encryption of git PATs. | | **Project members & invitations** | `project invite` (single or `--from-csv` bulk with parallel workers), `project member-list / member-remove / member-set-role`, `project invitation-list / invitation-cancel`. Role whitelist enforced at the CLI layer; Manage API "already invited" treated as `noop` not error (since 0.29.0). | | **Lineage** | Column-level dependency analysis across projects. SQL/Python parsing, AI-enhanced detection, interactive web browser, Mermaid/HTML/ER export. | | **Semantic layer** | Define and manage a metastore semantic model per project — datasets, metrics, relationships, constraints, glossary. Validate (incl. `--deep`), export, diff two models/files, import/promote across projects, AI-assisted `build` from tables. `kbagent semantic-layer ...` (alias `sl`). | diff --git a/docs/web-server-endpoints.md b/docs/web-server-endpoints.md index ea052642..466ac1c6 100644 --- a/docs/web-server-endpoints.md +++ b/docs/web-server-endpoints.md @@ -294,7 +294,7 @@ Flow Notifications-tab recipients (Notification Service subscriptions) -- audit ### `data-apps` (18 operations) -Streamlit / R / Python data apps -- create, deploy, start/stop, manage secrets. Mirrors `kbagent data-app *`. +Python/JS (default), Streamlit and R data apps -- create, deploy, start/stop, manage secrets. Mirrors `kbagent data-app *`. | Method | Path | Summary | |---|---|---| diff --git a/plugins/kbagent/agents/keboola-expert.md b/plugins/kbagent/agents/keboola-expert.md index e88f8aad..b2ab3c50 100644 --- a/plugins/kbagent/agents/keboola-expert.md +++ b/plugins/kbagent/agents/keboola-expert.md @@ -357,7 +357,7 @@ its absence is NOT a promise the entry is version-independent (see §1 Rule 6). `config detail` -> `configuration.runtime` FIRST (an empty `data-app logs` grep rules nothing out). `create` defaults it ON at **0.87.0+**; <= 0.86.0 patch + redeploy. -- **Data-app type in `sync`**: a `keboola.data-apps` config's runtime type (`python-js` / `streamlit`) lives only on the Data Science `/apps` record. `sync pull` records it as `_keboola.data_app_type`, and `sync push` / `sync clone` send it through `create_app`. A tree pulled before this carries no type, so re-pull the source before you clone, or the app deploys under the platform default, `streamlit` (since 0.94.0). +- **Data-app type in `sync`**: a `keboola.data-apps` config's runtime type (`python-js` / `streamlit`) lives only on the Data Science `/apps` record. `sync pull` records it as `_keboola.data_app_type`, and `sync push` / `sync clone` send it through `create_app`. A tree pulled before this carries no type (since 0.94.0). A type-less data app is created as `python-js`, the default, with a `data_app_type_default` push warning *(since vNEXT)*. So re-pull the source before you clone a Streamlit app, or set `_keboola.data_app_type: streamlit`. - **`ENCRYPTION_FAILED` on an Azure stack is a VERSION GATE, not a bad token**: <= 0.85.0 rejected the Azure `KBC::ProjectSecureKV::` cipher, so private-repo `create` and `secrets-set` could not work there at all. Upgrade to 0.86.0+; do diff --git a/plugins/kbagent/skills/kbagent/SKILL.md b/plugins/kbagent/skills/kbagent/SKILL.md index 637d4b02..f0b263ad 100644 --- a/plugins/kbagent/skills/kbagent/SKILL.md +++ b/plugins/kbagent/skills/kbagent/SKILL.md @@ -13,7 +13,7 @@ description: > Triggers: kbagent, Keboola, keboola config, keboola job, keboola lineage, keboola sync, gitops, dev branch, merge request, - data app, streamlit deploy, semantic layer, sl, dev-portal, + data app, python-js app, semantic layer, sl, dev-portal, data stream, OTLP, scoped token, encrypt secrets, feature flag, flow schedule, invite member, SQL transformation edit, sync action, keboola docs, table snapshot, auth, login, sign in, diff --git a/plugins/kbagent/skills/kbagent/references/commands-reference.md b/plugins/kbagent/skills/kbagent/references/commands-reference.md index 73e50b91..65e6f27b 100644 --- a/plugins/kbagent/skills/kbagent/references/commands-reference.md +++ b/plugins/kbagent/skills/kbagent/references/commands-reference.md @@ -280,7 +280,7 @@ Non-SOX Branches 2.0: merge a dev branch into production with review. Alias `mr` - `workspace gc [--project NAME ...] [--dry-run] [--yes]` -- garbage-collect orphaned workspaces (and any lingering `keboola.sandboxes` configs). `--dry-run` previews without deleting; `--project` repeatable, omit to GC across all connected projects - `workspace from-transformation --project ALIAS --component-id ID --config-id ID [--row-id ID]` -- workspace from existing transform -## Data Apps (Streamlit / Flask / Node deployments) +## Data Apps (Python/JS by default; Streamlit via --type) Lifecycle for `keboola.data-apps`. Combines Storage API (config body, git block, encrypted secrets, runtime size) with Data Science API (`/apps` -- deployment record, state, URL, configVersion). The CLI encapsulates the §9 redeploy contract so callers cannot pin to the empty-shell v2; see `data-app-workflow.md` for the gotcha inventory and recipes. Since v0.33.0 the JSON output envelope's data-app id key is `app_id` (renamed from bare `id` for symmetry with the `--app-id` input flag); `config_id` is unchanged. - `data-app list [--project NAME ...] [--branch ID]` -- list data apps across projects (Data Science index merged with Storage names). Since v0.43.9 filters out workspace/sandbox deployments (`componentId=keboola.sandboxes`, `type=snowflake`/`bigquery`) that the Data Science `/apps` collection also returns, so the listing matches the Apps UI. Envelope carries `component_id` per app. - `data-app detail --project NAME --app-id ID [--branch ID]` -- merged view (state, desired, url, configVersion, slug, git block with PAT redacted) @@ -361,7 +361,7 @@ Requires the project to be added with its **master ('owner') Storage API token** - `sync init --project ALIAS [--directory DIR] [--git-branching] [--adopt-existing]` -- initialize sync working directory; `--adopt-existing` adopts a `.keboola/manifest.json` already written by the kbc Go CLI without overwriting (idempotent; validates `project_id` against the alias token) - `sync pull --project ALIAS [--all-projects] [--force] [--theirs] [--dry-run] [--with-samples] [--no-storage] [--no-jobs] [--job-limit N] [--branch ID]` -- download configs to local files. **Auto-inits:** if the target directory has no `.keboola/manifest.json`, pull runs `init` first, so a separate `sync init` is not needed for a first checkout of a project. For large projects (>100 configs), automatically fetches jobs per-config when the grouped API limit is insufficient. `--force` is conflict-aware (since 0.53.0): a locally-modified config whose remote is unchanged is **preserved** (pending delta stays pushable, never silently re-stamped); a true merge conflict (local AND remote both changed since last pull) **aborts** the pull (exit 1, `SYNC_CONFLICT`; `--json` lists `details.conflicts`); local-untouched + remote-changed takes remote. `--theirs` (since v0.72.0) is the supported "discard local, take production" reconcile path: overwrites locally-modified configs/rows, restores deleted/missing files, resolves conflicts by taking remote (no abort, no manifest surgery). Since v0.72.0 plain pull also re-materializes a tracked config whose local dir was deleted (manifest<->disk invariant), so delete-dir-then-pull refetches. Config-level `isDisabled` round-trips (since v0.72.0) as sparse `is_disabled: true` in `_config.yml` -- absent key = enabled. `--branch` (0.47.0+) per-invocation dev-branch override, beats every other branch source. Ignored components (since 0.91.0): `keboola.sandboxes` + `keboola.mcp-server-tool` are always excluded, unioned with the manifest's `ignoredComponents` list; a component newly ignored has its manifest entry dropped and local directory removed, reported with pull action `"ignored"` (distinct from `"removed"` = genuinely deleted on remote). Config-folder round-trip *(since 0.94.0)*: pull captures each config's UI folder (`KBC.configuration.folderName`, from the branch-only `search/component-configurations` endpoint) into the manifest, and reports `folder_lookup_failed` when that lookup fails, keeping the previously captured folder. - `sync push --project ALIAS [--all-projects] [--dry-run] [--force] [--allow-plaintext-on-encrypt-failure] [--branch ID] [--no-name-drift-warnings]` -- push local changes (auto-encrypts secrets, fails if encryption fails). Fresh-CREATE writeback updates placeholder manifest entries in place (since 0.47.0) and propagates any `KBC.configuration.*` metadata via `set_config_metadata`. Fresh-CREATE variable binding (since 0.47.2): when a `keboola.variables` config + its values row are created alongside a transformation in the same push, the transformation's `variables_id` / `variables_values_id` placeholders are rebound to the assigned ULIDs and the row's `values` are hoisted even without a `_keboola` block, so `job run` succeeds with no post-push `config variables-set` step (unresolvable/ambiguous links surface a `variable_link` entry in `errors[]`, never a broken link). Never-fetched guard (since v0.72.0): a manifest entry with an empty `pull_hash` and no local files (pre-0.72 name-collision phantom) is **never** planned as a remote DELETE -- diff/push exclude it and report it under `never_fetched` with a warning (run `sync pull` to materialize); local deletion of a properly-pulled config still deletes on push. Adopted-by-id writeback (since v0.72.0): pushing an untracked file whose `_keboola.config_id` resolves on the branch also writes the manifest entry, so follow-up diffs are stable. `--branch` (0.47.0+) per-invocation override; when no `/` subtree exists on disk (since 0.47.2) the local default tree (`main/`) is promoted to the target branch (API writes still target the branch id); `--no-name-drift-warnings` (0.47.0+) drops the cosmetic warnings array. Branch-scoped since v0.89.0 (issue #649): push consumes the diff's changeset, so configs tracked on another branch's tree are never planned as creates -- they ride along on the result envelope under `orphaned` instead (see `sync diff`). **Since 0.91.0 (#686)** the manifest baseline `pull_config_hash` is stamped from the API response (or a read-back), not from the files on disk, so a pushed multi-statement SQL transformation -- or anything disabled in the UI whose local YAML lacks `is_disabled` -- no longer shows permanent phantom `REMOTE MODIFIED` drift; if the config cannot be read back after the write the baseline is left UNTOUCHED and a `warnings[]` entry says to run `sync pull` (never a disk-derived fallback). One legacy change is refused per-change with `SYNC_LEGACY_BOUNDARY`: a tree pulled before statement-boundary markers existed whose only difference from the remote is the lost boundaries (pushing it would collapse separate SQL statements into one) -- run `sync pull` for that project first. Ignored components (since 0.91.0) are filtered out on both sides of the diff push builds on, so a stale local directory for an ignored component (e.g. `keboola.mcp-server-tool`) is never classified as `DELETED` and can never be pushed as a remote deletion. -- `sync clone --source DIR --target ALIAS --target-dir DIR [--bucket-map FILE] [--variable-values FILE] [--instance-rename FILE] [--dry-run] [--branch ID]` -- clone a reference synced project into a **fresh** target project and parameterize it. Copies the reference tree at `--source` into `--target-dir`, applies declarative overrides from JSON/YAML files (`--bucket-map` `{old_bucket_id: new_bucket_id}` rewrites storage input/output table refs; `--variable-values` `{var_name: value}` overrides `keboola.variables` rows; `--instance-rename` `{old_path_prefix: new_path_prefix}` renames config dirs + manifest paths), re-points the manifest at the target project, and pushes. Because the reference's config ids do not exist in the fresh target, every config is CREATEd fresh and **keboola.flow task `configId`s + transformation variable links are remapped reference->ULID** by push Phase C/D (the push result carries `flow_task_remaps`). **Idempotent**: re-running with an existing `--target-dir` skips copy/overrides and just pushes, reporting `no_changes` / `created: 0`. Fails fast (`CONFIG_ERROR`) if the target already contains the reference's configs -- clone requires a fresh/empty target. `SyncService.clone_project(...)` returns a typed `CloneResult` for in-process SDK callers. Override files must be flat `{id: scalar}` mappings *(since v0.89.0)* -- a nested mapping, list, or null value is rejected with `CONFIG_ERROR` (exit 5) naming the key and its actual type. `--branch` is optional on a fresh clone *(since v0.93.1)*. It defaults to the target's production branch, resolved from the API the same way `sync init` does. Pass `--branch ` only to target a dev branch. The config folder (`KBC.configuration.folderName`) is recreated in the target *(since 0.94.0)* -- clone re-points its production configs onto the branch push resolves, so the create-path writeback carries the folder for a plain, `--branch`, and git-branching production clone. **Data-app runtime type (since 0.94.0)**: a `keboola.data-apps` config's type (`python-js` / `streamlit`) lives only on the Data Science `/apps` record, so `sync pull` records it in `_keboola.data_app_type` and clone sends it through `create_app`. Re-pull the source before cloning a tree pulled by an older version, or the app deploys under the platform default. +- `sync clone --source DIR --target ALIAS --target-dir DIR [--bucket-map FILE] [--variable-values FILE] [--instance-rename FILE] [--dry-run] [--branch ID]` -- clone a reference synced project into a **fresh** target project and parameterize it. Copies the reference tree at `--source` into `--target-dir`, applies declarative overrides from JSON/YAML files (`--bucket-map` `{old_bucket_id: new_bucket_id}` rewrites storage input/output table refs; `--variable-values` `{var_name: value}` overrides `keboola.variables` rows; `--instance-rename` `{old_path_prefix: new_path_prefix}` renames config dirs + manifest paths), re-points the manifest at the target project, and pushes. Because the reference's config ids do not exist in the fresh target, every config is CREATEd fresh and **keboola.flow task `configId`s + transformation variable links are remapped reference->ULID** by push Phase C/D (the push result carries `flow_task_remaps`). **Idempotent**: re-running with an existing `--target-dir` skips copy/overrides and just pushes, reporting `no_changes` / `created: 0`. Fails fast (`CONFIG_ERROR`) if the target already contains the reference's configs -- clone requires a fresh/empty target. `SyncService.clone_project(...)` returns a typed `CloneResult` for in-process SDK callers. Override files must be flat `{id: scalar}` mappings *(since v0.89.0)* -- a nested mapping, list, or null value is rejected with `CONFIG_ERROR` (exit 5) naming the key and its actual type. `--branch` is optional on a fresh clone *(since v0.93.1)*. It defaults to the target's production branch, resolved from the API the same way `sync init` does. Pass `--branch ` only to target a dev branch. The config folder (`KBC.configuration.folderName`) is recreated in the target *(since 0.94.0)* -- clone re-points its production configs onto the branch push resolves, so the create-path writeback carries the folder for a plain, `--branch`, and git-branching production clone. **Data-app runtime type (since 0.94.0)**: a `keboola.data-apps` config's type (`python-js` / `streamlit`) lives only on the Data Science `/apps` record, so `sync pull` records it in `_keboola.data_app_type` and clone sends it through `create_app`. A config with no recorded type is created as `python-js`, the default, with a `data_app_type_default` warning *(since vNEXT)*. Re-pull a tree pulled by an older version before cloning a Streamlit app, or it is created as `python-js`. - `sync diff --project ALIAS [--all-projects] [--branch ID]` -- 3-way diff (local vs base vs remote), detects conflicts. `--branch` (0.47.0+) per-invocation dev-branch override. Branch-scoped since v0.89.0 (issue #649): the local side is read from exactly ONE tree (the target branch's subtree, or `main/` when the target has none). Manifest entries belonging to another branch's tree -- what `sync pull --branch ` leaves behind when it re-targets the manifest -- are excluded from the changeset and reported under `orphaned` (`summary.orphaned` + details with `component_id`, `config_id`, `path`, `branch_id`, `branch_path`, `exists_on_target`, `reason`, `hint`); human mode previews the first 10. An orphaned FILE whose `_keboola.config_id` still resolves on the target is adopted (diffed as `unchanged`/`modified`), never re-created; same-tree id claims keep the #482/#497 fork-by-copy CREATE. Fix a non-zero `summary.orphaned` with `sync pull`. **Since 0.91.0 (#686)** a manifest entry without `metadata.config_hash_version` (written by a pre-0.91.0 kbagent) is compared leniently: a stored hash equal to the pre-0.91.0 hash of the SAME remote config counts as in sync, so the phantom `codes changed` entries disappear immediately; every other field is still pinned by that hash, so real remote drift is unaffected. One `sync pull` per project stamps the version and ends the leniency. Ignored components (since 0.91.0) -- `keboola.sandboxes`, `keboola.mcp-server-tool`, and anything listed in the manifest's `ignoredComponents` -- are excluded from BOTH sides of the comparison, so a stale local directory for one of them never shows up as `DELETED`. - `sync status [--directory DIR]` -- show locally modified/added/deleted configs. Also surfaces `plaintext_secret_warnings` (since 0.55.0): in-sync configs/rows whose `#`-secrets are still plaintext on the remote (a leftover from pre-0.54.0 writes; #378). Pending (un-pushed) edits are not flagged. Fix = re-push on >=0.54.0 + rotate (version history keeps the plaintext). - `sync branch-link --project ALIAS [--branch-id ID] [--branch-name NAME]` -- link git branch to Keboola dev branch diff --git a/plugins/kbagent/skills/kbagent/references/data-app-workflow.md b/plugins/kbagent/skills/kbagent/references/data-app-workflow.md index 5bbe1d8f..bdedf687 100644 --- a/plugins/kbagent/skills/kbagent/references/data-app-workflow.md +++ b/plugins/kbagent/skills/kbagent/references/data-app-workflow.md @@ -1,7 +1,13 @@ -# Data App Workflow -- Streamlit / Flask / Node Lifecycle +# Data App Workflow -- Python/JS App Lifecycle Data apps in Keboola are deployed from a git repo into a managed container -that auto-suspends after idle. Two API surfaces own them: +that auto-suspends after idle. **`python-js` is the default and recommended +runtime type** -- one contract for Python, Node, and mixed Python + Node apps +(). Build new apps on it. +`streamlit`, `r`, and the other types still work, but only when you pass +`--type` explicitly. + +Two API surfaces own them: | Layer | What it owns | |---|---| @@ -113,19 +119,25 @@ container. ## Quick recipes -### Public-repo Streamlit app from scratch (no auth gate) +### Public-repo Python/JS app from scratch (no auth gate) ```bash +# A repo built from a dataapp-developer template (see above). +# --type defaults to python-js, so it is omitted here. kbagent --json data-app create \ --project prod \ - --name "Hello Streamlit" \ - --slug hello-streamlit \ - --git-repo https://github.com/streamlit/streamlit-example \ + --name "Hello App" \ + --slug hello-app \ + --git-repo https://github.com/myorg/hello-app \ --git-public \ --auth public \ --wait ``` +An existing Streamlit repo still deploys, but you must pass `--type streamlit`. +Without it the app is created as `python-js` and the Streamlit repo fails that +contract. + Three calls under the hood: `POST /apps` (mint id + configId) → `PUT Storage config` (full body with git block + parameters.id back-pointer) → `PATCH /apps {desiredState=running, configVersion, restartIfRunning=true}`. diff --git a/plugins/kbagent/skills/kbagent/references/gotchas.md b/plugins/kbagent/skills/kbagent/references/gotchas.md index b39d8d9f..6293b3d8 100644 --- a/plugins/kbagent/skills/kbagent/references/gotchas.md +++ b/plugins/kbagent/skills/kbagent/references/gotchas.md @@ -920,7 +920,7 @@ without losing data. A `keboola.data-apps` config's runtime type (`python-js` / `streamlit` / ...) lives only on the Data Science `/apps` record, never in the Storage config body. So `sync pull` used to drop it, and `sync push` / `sync clone` recreated the config through the Storage API alone. A cloned `python-js` app then deployed under the platform default, `streamlit` (since 0.94.0). -`sync pull` now reads the type from the DS `/apps` list and records it in the config's `_keboola` block as `data_app_type`. The config hash already ignores that key, so it adds no `sync diff` noise. `sync push` and `sync clone` route a `keboola.data-apps` CREATE through the Data Science `create_app` when the local config carries a `data_app_type`. That call sends the type and writes the new app's `parameters.id`. A config with no recorded type still uses the plain `create_config` path. +`sync pull` now reads the type from the DS `/apps` list and records it in the config's `_keboola` block as `data_app_type`. The config hash already ignores that key, so it adds no `sync diff` noise. `sync push` and `sync clone` route a `keboola.data-apps` CREATE through the Data Science `create_app` when the local config carries a `data_app_type`. That call sends the type and writes the new app's `parameters.id`. A config with no recorded type (hand-authored, or pulled before 0.94.0) is created as `python-js`, the default, through the same `create_app` call *(since vNEXT)*. Before that it went through the plain `create_config` path and the platform picked `streamlit`. The push records the type in the local `_keboola` block and adds a `data_app_type_default` warning to the result. To keep a Streamlit app a Streamlit app, re-pull the source first or set `_keboola.data_app_type: streamlit`. The DS `/apps` list also returns sandbox and workspace records. Each carries a parent component's id and a backend `type` such as `snowflake`. So kbagent builds the type map from `componentId == keboola.data-apps` records only. diff --git a/src/keboola_agent_cli/commands/context.py b/src/keboola_agent_cli/commands/context.py index f9fe59b1..3eb5f108 100644 --- a/src/keboola_agent_cli/commands/context.py +++ b/src/keboola_agent_cli/commands/context.py @@ -1369,9 +1369,10 @@ kbagent workspace gc [--project NAME] [--dry-run] [--yes] Garbage-collect orphaned workspaces (keboola.sandboxes config missing). Use --dry-run to preview. -### Data Apps (Streamlit / Flask / Node deployments) +### Data Apps (Python/JS by default; Streamlit via --type) -Lifecycle for `keboola.data-apps`. Combines the Storage API (config body -- +Lifecycle for `keboola.data-apps`. New apps default to `--type python-js` +(Python, Node, or both), the recommended runtime. Combines the Storage API (config body -- git block, slug, runtime size, encrypted secrets) with the Data Science API (/apps -- deployment record, state, URL, configVersion). Encapsulates the §9 redeploy contract so callers cannot pin to the empty-shell v2. diff --git a/src/keboola_agent_cli/server/app.py b/src/keboola_agent_cli/server/app.py index af284010..0e0b3981 100644 --- a/src/keboola_agent_cli/server/app.py +++ b/src/keboola_agent_cli/server/app.py @@ -270,7 +270,7 @@ "name": "data-apps", "description": ( "**Execution.** " - "Streamlit / R / Python data apps -- create, deploy, " + "Python/JS (default), Streamlit and R data apps -- create, deploy, " "start/stop, manage secrets. " "Mirrors `kbagent data-app *`." ), diff --git a/src/keboola_agent_cli/services/_sync_push_ops.py b/src/keboola_agent_cli/services/_sync_push_ops.py index 6f1dac20..3f9d93ac 100644 --- a/src/keboola_agent_cli/services/_sync_push_ops.py +++ b/src/keboola_agent_cli/services/_sync_push_ops.py @@ -25,7 +25,7 @@ from ._sync_baseline import apply_stamp, row_baseline from ._sync_data_app import create_synced_data_app from ._sync_writeback import writeback_after_push, writeback_create_row_in_manifest -from .data_app_service import DATA_APP_COMPONENT_ID +from .data_app_service import DATA_APP_COMPONENT_ID, DEFAULT_TYPE if TYPE_CHECKING: from .sync_service import SyncService @@ -108,6 +108,24 @@ def _script_normalization_warning( } +def _default_data_app_type_warning(*, config_path: str, type_: str) -> dict[str, Any]: + """Push-envelope warning for a data app created with no recorded type.""" + message = ( + f"Created data app {config_path or '(new config)'} as '{type_}' (the default): its " + f"_config.yml records no _keboola.data_app_type. If the source is a Streamlit app, " + f"re-pull the source tree before cloning, or set '_keboola.data_app_type: streamlit'." + ) + logger.warning("%s", message) + return { + "change_type": "data_app_type_default", + "component_id": DATA_APP_COMPONENT_ID, + "config_id": "", + "config_path": config_path, + "message": message, + "data_app_type": type_, + } + + def push_row_change( service: SyncService, client: Any, @@ -399,9 +417,9 @@ def push_create( ``keboola.data-apps`` configs. A data app carries its runtime type on the DS ``/apps`` record, not in the Storage config, so a plain ``create_config`` would drop it and the cloned app would deploy under the - platform default (CLI-8). When a data-apps config records its type in the - ``_keboola`` footer, creation is routed through the DS client so the type - travels; otherwise the plain Storage path is unchanged. + platform default (CLI-8). A data-apps create is therefore always routed + through the DS client: with the type recorded in the ``_keboola`` footer, + or ``python-js`` (plus a push warning) when the footer records none. """ branch_path = service._resolve_source_branch_path(manifest, project_root, branch_id) config_dir = project_root / branch_path / config_path_str @@ -433,12 +451,23 @@ def push_create( allow_plaintext_fallback=allow_plaintext_fallback, ) - data_app_type = (local_data.get("_keboola") or {}).get("data_app_type") - if component_id == DATA_APP_COMPONENT_ID and data_app_type and ds_client is not None: + if component_id == DATA_APP_COMPONENT_ID and ds_client is not None: # Carry the DS runtime type into the target (CLI-8): create the DS # /apps record with the type, then fill the Storage config body. # ds_branch_id is None for a production push (POST /apps wants # branchId=null there), the dev branch id otherwise. + data_app_type = (local_data.get("_keboola") or {}).get("data_app_type") + if not data_app_type: + # No recorded type (a hand-authored config, or a tree pulled before + # 0.94.0): create it as python-js, the supported default, never + # the platform's own default (streamlit). Record the type locally + # so the tree states what was created. + data_app_type = DEFAULT_TYPE + pristine_data.setdefault("_keboola", {})["data_app_type"] = data_app_type + if warnings is not None: + warnings.append( + _default_data_app_type_warning(config_path=config_path_str, type_=data_app_type) + ) result = create_synced_data_app( client, ds_client, diff --git a/tests/test_sync_data_app_type.py b/tests/test_sync_data_app_type.py index 6e25c079..42c9b74d 100644 --- a/tests/test_sync_data_app_type.py +++ b/tests/test_sync_data_app_type.py @@ -353,9 +353,13 @@ def test_push_create_data_app_sends_type(tmp_config_dir: Path, tmp_path: Path) - assert _find_config(project_root, DATA_APP_COMPONENT)["parameters"]["id"] == "77777" -def test_push_create_data_app_without_type_falls_back(tmp_config_dir: Path, tmp_path: Path) -> None: - """A tree pulled before the fix (no recorded type) keeps the old behavior: - a plain create_config, no DS record -- no regression, no wrong type sent.""" +def test_push_create_data_app_without_type_defaults_to_python_js( + tmp_config_dir: Path, tmp_path: Path +) -> None: + """No recorded type (a hand-authored config, or a tree pulled before 0.94.0) + creates the app as python-js through the DS client -- never a plain + create_config, which leaves the platform to pick its own default + (streamlit). The default is recorded locally and surfaced as a warning.""" project_root = tmp_path / "project" api = FakeApi(_sql_components(["SELECT 1;"])) store = _init_and_pull(tmp_config_dir, project_root, api) @@ -366,16 +370,31 @@ def test_push_create_data_app_without_type_falls_back(tmp_config_dir: Path, tmp_ assert result["errors"] == [] assert result["created"] == 1 - # No type recorded => DS is never asked to create the app. - assert ds.create_app_calls == [] - # It went through the plain Storage create instead (FakeApi mints "cfg-new"). - created = next( - c - for comp in api.components - if comp["id"] == DATA_APP_COMPONENT - for c in comp["configurations"] + assert len(ds.create_app_calls) == 1 + assert ds.create_app_calls[0]["type_"] == "python-js" + # The default is written to the local file, so the tree states the type. + assert ( + _find_config(project_root, DATA_APP_COMPONENT)["_keboola"]["data_app_type"] == "python-js" ) - assert created["id"] == "cfg-new" + type_warnings = [w for w in result["warnings"] if w["change_type"] == "data_app_type_default"] + assert len(type_warnings) == 1 + assert type_warnings[0]["data_app_type"] == "python-js" + assert "streamlit" in type_warnings[0]["message"] + + +def test_push_create_data_app_with_type_emits_no_default_warning( + tmp_config_dir: Path, tmp_path: Path +) -> None: + """A recorded type is used as-is and raises no default-type warning.""" + project_root = tmp_path / "project" + api = FakeApi(_sql_components(["SELECT 1;"])) + store = _init_and_pull(tmp_config_dir, project_root, api) + _author_data_app(project_root, with_type=True) + + result = _service(store, api, FakeDs(api)).push(alias="prod", project_root=project_root) + + warnings = result.get("warnings", []) + assert not [w for w in warnings if w["change_type"] == "data_app_type_default"] # =================================================================== diff --git a/web/frontend/src/pages/DataApps.tsx b/web/frontend/src/pages/DataApps.tsx index 580dc92d..dca2741c 100644 --- a/web/frontend/src/pages/DataApps.tsx +++ b/web/frontend/src/pages/DataApps.tsx @@ -141,7 +141,7 @@ export function DataAppsPage() { return (
- + {!project ? ( ) : q.isLoading ? ( From 0d093cec408d5afe99fd08d0333ed61760f0e530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Jir=C3=A1t?= Date: Thu, 24 Sep 2026 13:03:52 +0200 Subject: [PATCH 2/2] fix(data-app): address review -- runnable python-js recipe, context-neutral default-type warning Point the public-repo quick recipe at keboola/data-app-python-js-hello-world instead of a placeholder repo, and reword the data_app_type_default warning so it reads correctly for a hand-authored push as well as a clone (NB-2, NIT-1 on #783). --- .../skills/kbagent/references/data-app-workflow.md | 8 ++++---- src/keboola_agent_cli/services/_sync_push_ops.py | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/plugins/kbagent/skills/kbagent/references/data-app-workflow.md b/plugins/kbagent/skills/kbagent/references/data-app-workflow.md index bdedf687..9d06040b 100644 --- a/plugins/kbagent/skills/kbagent/references/data-app-workflow.md +++ b/plugins/kbagent/skills/kbagent/references/data-app-workflow.md @@ -122,13 +122,13 @@ container. ### Public-repo Python/JS app from scratch (no auth gate) ```bash -# A repo built from a dataapp-developer template (see above). +# Keboola's public Python + JS example app. # --type defaults to python-js, so it is omitted here. kbagent --json data-app create \ --project prod \ - --name "Hello App" \ - --slug hello-app \ - --git-repo https://github.com/myorg/hello-app \ + --name "Hello World" \ + --slug hello-world \ + --git-repo https://github.com/keboola/data-app-python-js-hello-world \ --git-public \ --auth public \ --wait diff --git a/src/keboola_agent_cli/services/_sync_push_ops.py b/src/keboola_agent_cli/services/_sync_push_ops.py index 3f9d93ac..03c7fc70 100644 --- a/src/keboola_agent_cli/services/_sync_push_ops.py +++ b/src/keboola_agent_cli/services/_sync_push_ops.py @@ -112,8 +112,9 @@ def _default_data_app_type_warning(*, config_path: str, type_: str) -> dict[str, """Push-envelope warning for a data app created with no recorded type.""" message = ( f"Created data app {config_path or '(new config)'} as '{type_}' (the default): its " - f"_config.yml records no _keboola.data_app_type. If the source is a Streamlit app, " - f"re-pull the source tree before cloning, or set '_keboola.data_app_type: streamlit'." + f"_config.yml records no _keboola.data_app_type. To create a Streamlit app, set " + f"'_keboola.data_app_type: streamlit' before pushing; a tree pulled with kbagent " + f"0.94.0 or later records the type itself." ) logger.warning("%s", message) return {