metabase-sync 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. metabase_sync-0.1.0/.gitignore +34 -0
  2. metabase_sync-0.1.0/CHANGELOG.md +57 -0
  3. metabase_sync-0.1.0/LICENSE +21 -0
  4. metabase_sync-0.1.0/PKG-INFO +315 -0
  5. metabase_sync-0.1.0/README.md +279 -0
  6. metabase_sync-0.1.0/examples/README.md +34 -0
  7. metabase_sync-0.1.0/pyproject.toml +78 -0
  8. metabase_sync-0.1.0/src/metabase_sync/__init__.py +1 -0
  9. metabase_sync-0.1.0/src/metabase_sync/__main__.py +4 -0
  10. metabase_sync-0.1.0/src/metabase_sync/apply/__init__.py +30 -0
  11. metabase_sync-0.1.0/src/metabase_sync/apply/_cards.py +261 -0
  12. metabase_sync-0.1.0/src/metabase_sync/apply/_collections.py +99 -0
  13. metabase_sync-0.1.0/src/metabase_sync/apply/_dashboards.py +257 -0
  14. metabase_sync-0.1.0/src/metabase_sync/apply/_pulses.py +183 -0
  15. metabase_sync-0.1.0/src/metabase_sync/apply/_runner.py +249 -0
  16. metabase_sync-0.1.0/src/metabase_sync/apply/_shared.py +107 -0
  17. metabase_sync-0.1.0/src/metabase_sync/apply/_snippets.py +81 -0
  18. metabase_sync-0.1.0/src/metabase_sync/cli.py +354 -0
  19. metabase_sync-0.1.0/src/metabase_sync/client.py +222 -0
  20. metabase_sync-0.1.0/src/metabase_sync/diff.py +104 -0
  21. metabase_sync-0.1.0/src/metabase_sync/errors.py +33 -0
  22. metabase_sync-0.1.0/src/metabase_sync/export.py +206 -0
  23. metabase_sync-0.1.0/src/metabase_sync/models.py +154 -0
  24. metabase_sync-0.1.0/src/metabase_sync/plan.py +135 -0
  25. metabase_sync-0.1.0/src/metabase_sync/serialize/__init__.py +0 -0
  26. metabase_sync-0.1.0/src/metabase_sync/serialize/cards.py +184 -0
  27. metabase_sync-0.1.0/src/metabase_sync/serialize/collections.py +93 -0
  28. metabase_sync-0.1.0/src/metabase_sync/serialize/dashboards.py +132 -0
  29. metabase_sync-0.1.0/src/metabase_sync/serialize/databases.py +37 -0
  30. metabase_sync-0.1.0/src/metabase_sync/serialize/paths.py +106 -0
  31. metabase_sync-0.1.0/src/metabase_sync/serialize/pulses.py +123 -0
  32. metabase_sync-0.1.0/src/metabase_sync/serialize/snippets.py +91 -0
  33. metabase_sync-0.1.0/src/metabase_sync/serialize/version.py +56 -0
  34. metabase_sync-0.1.0/src/metabase_sync/serialize/yamlio.py +91 -0
  35. metabase_sync-0.1.0/src/metabase_sync/settings.py +35 -0
  36. metabase_sync-0.1.0/tests/__init__.py +0 -0
  37. metabase_sync-0.1.0/tests/integration/__init__.py +0 -0
  38. metabase_sync-0.1.0/tests/integration/conftest.py +112 -0
  39. metabase_sync-0.1.0/tests/integration/docker-compose.yml +38 -0
  40. metabase_sync-0.1.0/tests/integration/test_round_trip.py +540 -0
  41. metabase_sync-0.1.0/tests/test_client_retries.py +99 -0
  42. metabase_sync-0.1.0/tests/test_concurrency.py +70 -0
  43. metabase_sync-0.1.0/tests/test_legacy_query.py +202 -0
  44. metabase_sync-0.1.0/tests/test_paths.py +53 -0
  45. metabase_sync-0.1.0/tests/test_paths_resolve.py +50 -0
  46. metabase_sync-0.1.0/tests/test_preflight.py +108 -0
  47. metabase_sync-0.1.0/tests/test_recipients.py +59 -0
  48. metabase_sync-0.1.0/tests/test_state_version.py +48 -0
  49. metabase_sync-0.1.0/tests/test_version_check.py +76 -0
  50. metabase_sync-0.1.0/tests/test_yamlio.py +66 -0
@@ -0,0 +1,34 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ *.egg
6
+ .venv/
7
+ .python-version
8
+
9
+ # Build
10
+ build/
11
+ dist/
12
+ *.whl
13
+ *.tar.gz
14
+
15
+ # uv
16
+ .uv-cache/
17
+
18
+ # Pytest / coverage
19
+ .pytest_cache/
20
+ .coverage
21
+ .coverage.*
22
+ htmlcov/
23
+
24
+ # Local state / secrets
25
+ .env
26
+ .env.local
27
+ state/.plan.json
28
+ state/.last-apply.json
29
+
30
+ # Editors
31
+ .vscode/
32
+ .idea/
33
+ *.swp
34
+ .DS_Store
@@ -0,0 +1,57 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] — 2026-06-20
11
+
12
+ ### Commands
13
+
14
+ - `metabase-sync export` — pull the live Metabase instance into a YAML + SQL state tree under `state/`.
15
+ - `metabase-sync plan` — itemised per-resource diff between the on-disk state and the live instance. Writes a human-readable report to stdout and a machine-readable `state/.plan.json` for CI consumption.
16
+ - `metabase-sync apply` — re-derives the diff and executes the necessary POST/PUT calls. Idempotent (a second run is a no-op). Writes `state/.last-apply.json` as an audit log.
17
+ - `metabase-sync index` — debug command listing remote resource counts.
18
+ - `metabase-sync --version` prints the package version.
19
+
20
+ ### Resources covered
21
+
22
+ - Collections, native and GUI cards (including dashboard-internal cards), dashboards (tabs + dashcards), snippets (with collection routing), pulses (dashboard subscriptions, recipients matched by email), and database manifests (name + engine only — credentials never serialised).
23
+ - Personal collections are intentionally excluded.
24
+ - Out-of-scope resources (alerts, segments, legacy metrics) produce a warning on export so they aren't silently missed.
25
+
26
+ ### Code-first authoring
27
+
28
+ - Dashboards and pulses reference cards by relative file path (`card_path: ../../cards/foo.sql`) and dashboards by `dashboard_path` — brand-new items can be authored without knowing any Metabase ids ahead of time. Apply allocates ids and writes the new `entity_id` back into the source file.
29
+
30
+ ### Round-trip stability
31
+
32
+ - Serialises Metabase's classic `legacy_query` form for native cards and accepts either classic or MBQL5 form for GUI cards. The diff path strips volatile keys (`lib/uuid`, `info`, `lib.convert/converted?`) so MBQL5 cards don't churn across exports.
33
+ - SQL bodies are stored byte-faithfully (trailing whitespace, template-tag UUIDs all survive).
34
+ - Atomic file writes (tempfile + `os.replace`) — Ctrl+C never leaves a corrupted YAML.
35
+
36
+ ### Production safety
37
+
38
+ - **HTTP retries with exponential backoff** for connection errors, 408, 429, 502, 503, 504. Configurable via `HTTP_MAX_RETRIES` (default 3) and `HTTP_RETRY_BACKOFF_S` (default 1.0). `HTTP_TIMEOUT_S` default is 120s so `result_metadata` recomputation on large cards doesn't trip.
39
+ - **Single PUT for dashboard updates** — metadata + tabs + dashcards in one request, so a dashboard can never be left half-applied.
40
+ - **`result_metadata` preserved** for metadata-only card updates so Metabase doesn't re-run the underlying query on every cosmetic edit.
41
+ - **Reference preflight** — apply walks every dashboard and pulse and resolves every `card_path` / `dashboard_path` against the filesystem before any HTTP call.
42
+ - **Optimistic-concurrency check** — plan captures `updated_at` per item; apply refuses to overwrite anything that changed since plan unless `--force` is set.
43
+ - **Pre-apply confirmation prompt** — `apply` re-prints the plan and asks `[y/N]` unless `--yes`, `CI=true`, or stdout is not a TTY.
44
+ - **Loud error on missing pulse recipients** — apply errors with the list of bad emails unless `--allow-missing-recipients` is set.
45
+ - **`--delete` rejected at CLI parse time** with exit code 2 (planned for a future release).
46
+ - **Terraform exit codes**: `plan` exits 2 when changes are detected, 0 when nothing to do, 1 on error.
47
+
48
+ ### Compatibility
49
+
50
+ - Built and tested against Metabase OSS v0.62.2 (pinned for CI). A non-blocking CI job runs the integration suite against `metabase:latest` to surface upcoming breakage.
51
+ - Supports Python 3.11, 3.12, 3.13.
52
+
53
+ ### Known limitations
54
+
55
+ - `--delete` (opt-in archival of items absent from disk) is not yet implemented.
56
+ - Permissions, users, and groups are out of scope.
57
+ - Alerts, legacy metrics, and segments are not yet supported (export warns when they exist).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 novucs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,315 @@
1
+ Metadata-Version: 2.4
2
+ Name: metabase-sync
3
+ Version: 0.1.0
4
+ Summary: Metabase as code: export, plan, and apply dashboards, cards, collections, snippets and pulses via the Metabase REST API.
5
+ Project-URL: Homepage, https://github.com/novucs/metabase-sync
6
+ Project-URL: Repository, https://github.com/novucs/metabase-sync
7
+ Project-URL: Issues, https://github.com/novucs/metabase-sync/issues
8
+ Project-URL: Changelog, https://github.com/novucs/metabase-sync/blob/main/CHANGELOG.md
9
+ Author: novucs
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: as-code,cli,dashboards,gitops,iac,metabase
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: System Administrators
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Database
23
+ Classifier: Topic :: Software Development :: Version Control
24
+ Classifier: Topic :: System :: Systems Administration
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.11
27
+ Requires-Dist: httpx>=0.27.0
28
+ Requires-Dist: pydantic-settings>=2.6.0
29
+ Requires-Dist: pydantic>=2.8.0
30
+ Requires-Dist: python-slugify>=8.0.4
31
+ Requires-Dist: pyyaml>=6.0.2
32
+ Requires-Dist: rich>=15.0.0
33
+ Requires-Dist: tenacity>=9.1.4
34
+ Requires-Dist: typer>=0.12.5
35
+ Description-Content-Type: text/markdown
36
+
37
+ # metabase-sync
38
+
39
+ [![PyPI](https://img.shields.io/pypi/v/metabase-sync.svg)](https://pypi.org/project/metabase-sync/)
40
+ [![Python](https://img.shields.io/pypi/pyversions/metabase-sync.svg)](https://pypi.org/project/metabase-sync/)
41
+ [![CI](https://github.com/novucs/metabase-sync/actions/workflows/ci.yml/badge.svg)](https://github.com/novucs/metabase-sync/actions/workflows/ci.yml)
42
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
43
+
44
+ Metabase as code. `export`, `plan`, and `apply` your dashboards, cards, collections, snippets and pulses via the regular Metabase REST API. Works with Metabase **OSS** (no Enterprise license required).
45
+
46
+ ```
47
+ $ metabase-sync plan
48
+
49
+ plan ← state/
50
+
51
+ collections: 0 create, 0 update, 12 skip
52
+ cards: 1 create, 1 update, 48 skip
53
+ CREATE collections/finance/cards/q4-revenue.sql Q4 Revenue — SELECT SUM(revenue) FROM finance.q4
54
+ UPDATE collections/finance/cards/profit-margin.sql SQL: 23 → 25 lines, +2 −0
55
+ dashboards: 1 create, 0 update, 8 skip
56
+ CREATE collections/finance/dashboards/q4-review/dashboard.yaml Q4 Review (1 dashcards)
57
+
58
+ 2 create, 1 update, 68 skip
59
+ run `metabase-sync apply` to apply this plan.
60
+ ```
61
+
62
+ ## Why this exists
63
+
64
+ Metabase's [official serialization](https://www.metabase.com/docs/latest/installation-and-operation/serialization) and Remote Sync are gated behind Enterprise / Pro. If you self-host the open-source edition, the `/api/ee/serialization/*` endpoints return 404 and you have no first-party way to put your dashboards under version control.
65
+
66
+ This tool wraps the plain REST API and gives you the same workflow without the licence wall:
67
+
68
+ - One YAML/SQL tree per Metabase instance, suitable for git.
69
+ - A `plan` / `apply` loop that reads exactly like Terraform.
70
+ - Round-trip determinism: `export` → `apply` → `export` produces zero diff.
71
+ - Code-first authoring: drop a new `.sql` and a new `dashboard.yaml`, run `apply`, done. No need to know any Metabase ids ahead of time.
72
+
73
+ ## Install
74
+
75
+ ```bash
76
+ uv tool install metabase-sync # recommended: isolated install
77
+ # or
78
+ pipx install metabase-sync
79
+ # or
80
+ pip install metabase-sync
81
+ ```
82
+
83
+ ## Quickstart
84
+
85
+ ```bash
86
+ mkdir my-metabase && cd my-metabase
87
+ cat > .env <<EOF
88
+ METABASE_URL=https://your-instance.example.com
89
+ METABASE_API_KEY=mb_...
90
+ EOF
91
+ metabase-sync export # creates ./state/
92
+ git init && git add state && git commit -m "import metabase state"
93
+ ```
94
+
95
+ Then your day-to-day:
96
+
97
+ ```bash
98
+ # Edit a .sql or dashboard.yaml in your editor.
99
+ metabase-sync plan # see exactly what will change
100
+ metabase-sync apply # write changes
101
+ ```
102
+
103
+ ## Commands
104
+
105
+ | Command | Purpose |
106
+ | --- | --- |
107
+ | `metabase-sync export` | Pull the live instance into the on-disk state tree. |
108
+ | `metabase-sync plan` | Read-only diff. Prints a per-item report and writes `state/.plan.json`. |
109
+ | `metabase-sync apply` | Re-derives the diff and executes it. Idempotent. |
110
+ | `metabase-sync apply --only cards` | Restrict to one resource (`collections,snippets,cards,dashboards,pulses`). |
111
+ | `metabase-sync index` | Debug: print remote resource counts. |
112
+
113
+ Both `plan` and `apply` re-fetch the live instance, so you can't apply a stale plan against a moved instance.
114
+
115
+ ## Authoring new cards and dashboards in code
116
+
117
+ You don't need to know any Metabase ids ahead of time. Create the files; apply allocates the ids and writes them back.
118
+
119
+ ### A new card
120
+
121
+ `state/collections/finance/cards/q4-revenue.sql`:
122
+
123
+ ```sql
124
+ ---
125
+ entity_id: null
126
+ name: Q4 Revenue
127
+ description: null
128
+ type: question
129
+ display: scalar
130
+ database: bigquery
131
+ parameters: []
132
+ visualization_settings: {}
133
+ enable_embedding: false
134
+ embedding_params: null
135
+ cache_ttl: null
136
+ archived: false
137
+ template_tags: {}
138
+ ---
139
+ ---body---
140
+ SELECT SUM(revenue) FROM finance.q4
141
+ ```
142
+
143
+ Run `plan` to see the CREATE line, then `apply`. The `entity_id: null` becomes the server-assigned nanoid in your local file after apply.
144
+
145
+ ### A new dashboard referencing a new card
146
+
147
+ `state/collections/finance/dashboards/q4-review/dashboard.yaml`:
148
+
149
+ ```yaml
150
+ entity_id: null
151
+ name: Q4 Review
152
+ description: null
153
+ archived: false
154
+ auto_apply_filters: true
155
+ cache_ttl: null
156
+ enable_embedding: false
157
+ embedding_params: null
158
+ position: null
159
+ width: fixed
160
+ parameters: []
161
+ tabs: []
162
+ dashcards:
163
+ - entity_id: null
164
+ card_path: ../../cards/q4-revenue.sql # path relative to this dashboard dir
165
+ tab_position: null
166
+ row: 0
167
+ col: 0
168
+ size_x: 12
169
+ size_y: 6
170
+ parameter_mappings: []
171
+ visualization_settings: {}
172
+ series: []
173
+ ```
174
+
175
+ `card_path` is the file path from this dashboard's directory to the card's `.sql` file. Apply resolves it after creating the card, then writes the new entity_id back to your file.
176
+
177
+ ## On-disk layout
178
+
179
+ ```
180
+ state/
181
+ databases/<name>.yaml # manifest only — name + engine, NO credentials
182
+ snippets/<slug>.sql # YAML frontmatter + raw SQL body
183
+ collections/
184
+ <slug>/_collection.yaml
185
+ /<nested-slug>/_collection.yaml
186
+ /cards/<slug>.sql or .yaml
187
+ /dashboards/<slug>/dashboard.yaml
188
+ /cards/<slug>.sql # dashboard-internal cards
189
+ root/cards/... # cards directly under the root collection
190
+ pulses/<slug>.yaml # dashboard subscriptions
191
+ ```
192
+
193
+ - Native SQL cards: `.sql` file with YAML frontmatter + raw query body.
194
+ - GUI / MBQL cards: `.yaml` file with the full `dataset_query` (classic or MBQL5) inlined.
195
+ - Dashcards: reference cards by `card_path` (relative).
196
+ - Pulses: reference cards by `card_path`, the target dashboard by `dashboard_path`. Recipients are stored by email.
197
+ - Snippets without a collection live in `state/snippets/`; snippets inside a collection live in `state/collections/<...>/snippets/`.
198
+ - Personal collections are filtered out.
199
+
200
+ ## Configuration
201
+
202
+ | Env var | Required | Default | Notes |
203
+ | --- | --- | --- | --- |
204
+ | `METABASE_URL` | yes | — | e.g. `https://metabase.example.com` |
205
+ | `METABASE_API_KEY` | yes | — | Admin API key (see below) |
206
+ | `STATE_DIR` | no | `state/` | Relative to CWD |
207
+ | `HTTP_TIMEOUT_S` | no | `120` | Per HTTP request (large cards' `result_metadata` recompute can take time) |
208
+ | `HTTP_MAX_RETRIES` | no | `3` | Retries on 408 / 429 / 502 / 503 / 504 / connection errors |
209
+ | `HTTP_RETRY_BACKOFF_S` | no | `1.0` | Base delay; doubles per retry (1s, 2s, 4s) |
210
+
211
+ A `.env` file in the current working directory is read automatically.
212
+
213
+ API keys are minted from the Metabase admin UI (Settings → Admin settings → Authentication → API keys). The key needs admin permissions to fetch + write everything `export` and `apply` touch.
214
+
215
+ ## Exit codes
216
+
217
+ `plan` and `apply` follow the terraform convention so CI pipelines can fan out:
218
+
219
+ | Code | Meaning |
220
+ | --- | --- |
221
+ | 0 | Success and no changes (or apply finished cleanly) |
222
+ | 1 | Error (HTTP failure, preflight failure, missing recipient, concurrency drift) |
223
+ | 2 | `plan` detected pending changes (informational; not an error) |
224
+
225
+ ## CI/CD recipes
226
+
227
+ See [`examples/`](examples/) for two GitHub Actions templates:
228
+
229
+ - [`github-actions-plan-on-pr.yml`](examples/github-actions-plan-on-pr.yml) — comment the plan output on every PR that touches `state/`.
230
+ - [`github-actions-apply-on-merge.yml`](examples/github-actions-apply-on-merge.yml) — apply automatically on merge to `main`, gated by an environment approval.
231
+
232
+ ## Round-trip guarantees
233
+
234
+ - `export` is deterministic: byte-identical output across runs.
235
+ - `plan` against a freshly-exported tree reports `nothing to do.`.
236
+ - SQL bodies are byte-faithful — trailing whitespace and template-tag UUIDs survive the round-trip.
237
+
238
+ ## Caveats
239
+
240
+ Before running this on a production Metabase, you should know:
241
+
242
+ - **Apply overwrites concurrent UI edits unless you re-plan first.** `apply` runs a fresh `plan` and checks the captured `updated_at` for every item it touched. If a UI user has edited an item since you ran `plan`, apply aborts with the list and tells you to re-plan. Pass `--force` to overwrite anyway.
243
+ - **Dashboard contents are a full replacement.** Tabs and dashcards are PUT in one go with client-assigned negative temp ids; the server replaces existing rows and allocates new `dashcard.entity_id`s. Two simultaneous applies race; wrap CI in a concurrency group to serialise them.
244
+ - **Out-of-scope resources are silently NOT synced.** Alerts, segments, legacy metrics, permissions, users and groups are not part of the state tree. `export` prints a warning if it finds any so you don't discover this in production.
245
+ - **Personal collections are excluded.** Cards/dashboards inside a user's personal collection don't appear in the export. Dashboards in shared collections that reference a personal-collection card will fail the reference preflight at plan time, not at apply time.
246
+ - **`--delete` is not yet implemented.** Items removed from `state/` are not auto-archived on apply. Archive them through the UI or directly via the API. Passing `--delete` is rejected with exit code 2.
247
+
248
+ ### Pre-apply backup
249
+
250
+ `apply --backup-dir <path>` re-exports the live instance to `<path>` before mutating. If apply messes something up, `metabase-sync apply --state-dir <path>` rolls forward against the backup.
251
+
252
+ ```bash
253
+ metabase-sync apply --backup-dir /tmp/pre-apply-$(date +%Y%m%d-%H%M%S)
254
+ ```
255
+
256
+ ## Troubleshooting / FAQ
257
+
258
+ **`401 Unauthorized` on the first call.** Your API key was revoked or you're pointed at the wrong instance. Run `metabase-sync diagnose` — it prints the URL and key length.
259
+
260
+ **`plan` reports updates I didn't make.** Most often a Metabase version mismatch — server-generated `lib/uuid` values on GUI cards regenerate on every UI save. The tool strips them at diff time; if you still see noise, `metabase-sync export` once to refresh and try again.
261
+
262
+ **How do I rename a card?** Edit the `name:` in the frontmatter. The `entity_id` stays the same so the rename round-trips cleanly.
263
+
264
+ **How do I move a card to another collection?** Move the file. The collection that contains the card is determined by its on-disk path; on apply the collection_id is rebound and Metabase moves it.
265
+
266
+ **How do I delete a card?** Archive it via the Metabase UI for now. The `--delete` flag is not implemented in this release. After archival, re-export and commit.
267
+
268
+ **`metabase-sync diagnose` to file a bug.** It captures everything we'd ask for in a triage thread. Paste the output into the GitHub issue template.
269
+
270
+ **My Metabase is on a really old version.** Check the [compatibility band](#compatibility). Versions older than v0.45 are explicitly refused; v0.45–v0.55 will warn but try; v0.55–v0.62 are in our tested band; newer versions warn but proceed.
271
+
272
+ ## Compatibility
273
+
274
+ | Metabase | Status |
275
+ | --- | --- |
276
+ | <v0.45 | **Refused** — collection API shape too different |
277
+ | v0.45–v0.55 | Warns; proceed at your own risk |
278
+ | v0.55–v0.62 | **Tested** in CI integration matrix |
279
+ | >v0.62 | Warns; we want to hear about issues |
280
+ | `latest` | Non-blocking CI job runs against it to surface upcoming breakage |
281
+
282
+ We pin `v0.62.2` for the blocking CI job; the matrix in `.github/workflows/ci.yml` runs the same integration suite against `v0.55`, `v0.58`, `v0.60`, and `latest` non-blockingly.
283
+
284
+ Python: 3.11, 3.12, 3.13.
285
+
286
+ If something goes wrong, you can `cp -r` the backup over `state/` and re-apply to roll back.
287
+
288
+ ## Security
289
+
290
+ - The API key is read from the environment (or an `.env` file). Anything that captures the environment — CI logs, shell history (`printenv`), error stack traces from third-party deps that `repr()` settings — can leak it. Tools like [sops](https://github.com/getsops/sops) + `sops exec-env encrypted.env 'metabase-sync apply'` work well.
291
+ - HTTPS verification follows httpx's defaults (CA bundle from `certifi`). The tool does not disable cert verification.
292
+ - `state/.plan.json` and `state/.last-apply.json` include full SQL bodies — they're written under `state/` and should be `.gitignore`d. The default `.gitignore` snippet:
293
+ ```
294
+ state/.plan.json
295
+ state/.last-apply.json
296
+ ```
297
+ - The tool never serialises database connection credentials (`details` is stripped from `databases/<name>.yaml`).
298
+
299
+ ## Limitations
300
+
301
+ - `--delete` (opt-in archival of items absent from disk) is not yet implemented.
302
+ - Permissions, users, and groups are out of scope.
303
+ - Alerts, legacy metrics, and segments are not yet supported.
304
+
305
+ ## Contributing
306
+
307
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for dev setup, running tests, and the release process.
308
+
309
+ ## License
310
+
311
+ MIT — see [LICENSE](LICENSE).
312
+
313
+ ## Disclaimer
314
+
315
+ This is an unofficial, community-built tool. Not affiliated with or endorsed by Metabase, Inc. "Metabase" is a trademark of Metabase, Inc.