metabase-sync 0.2.2__tar.gz → 0.3.1__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.
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/CHANGELOG.md +22 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/PKG-INFO +3 -2
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/README.md +1 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/pyproject.toml +1 -1
- metabase_sync-0.3.1/src/metabase_sync/__init__.py +1 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/cli.py +1 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/client.py +6 -1
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/cards.py +4 -1
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/settings.py +1 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_legacy_query.py +36 -0
- metabase_sync-0.2.2/src/metabase_sync/__init__.py +0 -1
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/.gitignore +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/LICENSE +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/examples/README.md +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/__main__.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/apply/__init__.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/apply/_cards.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/apply/_collections.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/apply/_dashboards.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/apply/_pulses.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/apply/_runner.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/apply/_shared.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/apply/_snippets.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/diff.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/errors.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/export.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/models.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/plan.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/__init__.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/canon.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/collections.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/dashboards.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/databases.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/paths.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/pulses.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/snippets.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/version.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/yamlio.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/__init__.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/__init__.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/_builders.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/_mb.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/conftest.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/docker-compose.yml +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/test_apply_semantics.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/test_cards.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/test_cli.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/test_dashboards.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/test_export.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/test_pulses.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/test_snippets.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_canon.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_card_diff.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_client_retries.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_concurrency.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_dashboard_diff.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_paths.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_paths_resolve.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_preflight.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_pulse_diff.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_recipients.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_state_version.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_template_tags.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_version_check.py +0 -0
- {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_yamlio.py +0 -0
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.1] - 2026-10-27
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Native cards export and apply on Metabase v0.64.3+.** Those versions
|
|
15
|
+
return a native stage's `template-tags` as a list instead of a map keyed by
|
|
16
|
+
tag name, which crashed apply. Export now converts the list back to the map,
|
|
17
|
+
so state files keep the same shape across versions.
|
|
18
|
+
|
|
19
|
+
### Testing
|
|
20
|
+
|
|
21
|
+
- Unit coverage for list-form template tags (`test_legacy_query.py`); the
|
|
22
|
+
integration suite verified against Metabase v0.64.3.5 and the pinned v0.62.2.
|
|
23
|
+
|
|
24
|
+
## [0.3.0] - 2026-10-27
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **`METABASE_HEADERS` sends extra headers on every request**, as a JSON
|
|
29
|
+
object. Use it to reach a Metabase behind an auth proxy such as Google
|
|
30
|
+
Cloud IAP (`{"Authorization": "Bearer <token>"}`).
|
|
31
|
+
|
|
10
32
|
## [0.2.2] — 2026-07-16
|
|
11
33
|
|
|
12
34
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: metabase-sync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Metabase as code: export, plan, and apply dashboards, cards, collections, snippets and pulses via the Metabase REST API.
|
|
5
5
|
Project-URL: Homepage, https://github.com/novucs/metabase-sync
|
|
6
6
|
Project-URL: Repository, https://github.com/novucs/metabase-sync
|
|
@@ -203,6 +203,7 @@ state/
|
|
|
203
203
|
| --- | --- | --- | --- |
|
|
204
204
|
| `METABASE_URL` | yes | — | e.g. `https://metabase.example.com` |
|
|
205
205
|
| `METABASE_API_KEY` | yes | — | Admin API key (see below) |
|
|
206
|
+
| `METABASE_HEADERS` | no | `{}` | JSON object of extra request headers, e.g. `{"Authorization": "Bearer ..."}` for an auth proxy in front of Metabase |
|
|
206
207
|
| `STATE_DIR` | no | `state/` | Relative to CWD |
|
|
207
208
|
| `HTTP_TIMEOUT_S` | no | `120` | Per HTTP request (large cards' `result_metadata` recompute can take time) |
|
|
208
209
|
| `HTTP_MAX_RETRIES` | no | `3` | Retries on 408 / 429 / 502 / 503 / 504 / connection errors |
|
|
@@ -167,6 +167,7 @@ state/
|
|
|
167
167
|
| --- | --- | --- | --- |
|
|
168
168
|
| `METABASE_URL` | yes | — | e.g. `https://metabase.example.com` |
|
|
169
169
|
| `METABASE_API_KEY` | yes | — | Admin API key (see below) |
|
|
170
|
+
| `METABASE_HEADERS` | no | `{}` | JSON object of extra request headers, e.g. `{"Authorization": "Bearer ..."}` for an auth proxy in front of Metabase |
|
|
170
171
|
| `STATE_DIR` | no | `state/` | Relative to CWD |
|
|
171
172
|
| `HTTP_TIMEOUT_S` | no | `120` | Per HTTP request (large cards' `result_metadata` recompute can take time) |
|
|
172
173
|
| `HTTP_MAX_RETRIES` | no | `3` | Retries on 408 / 429 / 502 / 503 / 504 / connection errors |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "metabase-sync"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.1"
|
|
4
4
|
description = "Metabase as code: export, plan, and apply dashboards, cards, collections, snippets and pulses via the Metabase REST API."
|
|
5
5
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
6
6
|
authors = [{ name = "novucs" }]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.1"
|
|
@@ -68,10 +68,15 @@ class MetabaseClient:
|
|
|
68
68
|
timeout_s: float = 120.0,
|
|
69
69
|
max_retries: int = 3,
|
|
70
70
|
retry_backoff_s: float = 1.0,
|
|
71
|
+
headers: dict[str, str] | None = None,
|
|
71
72
|
) -> None:
|
|
72
73
|
self._http = httpx.Client(
|
|
73
74
|
base_url=base_url.rstrip("/"),
|
|
74
|
-
headers={
|
|
75
|
+
headers={
|
|
76
|
+
**(headers or {}),
|
|
77
|
+
"X-API-Key": api_key,
|
|
78
|
+
"Accept": "application/json",
|
|
79
|
+
},
|
|
75
80
|
timeout=timeout_s,
|
|
76
81
|
transport=httpx.HTTPTransport(retries=max_retries),
|
|
77
82
|
)
|
|
@@ -168,7 +168,10 @@ def _native_query(query: dict[str, Any]) -> tuple[str, dict[str, Any]] | None:
|
|
|
168
168
|
if isinstance(stage, dict) and stage.get("lib/type") == "mbql.stage/native":
|
|
169
169
|
sql = stage.get("native")
|
|
170
170
|
if isinstance(sql, str):
|
|
171
|
-
|
|
171
|
+
tags = stage.get("template-tags") or {}
|
|
172
|
+
if isinstance(tags, list):
|
|
173
|
+
tags = {tag["name"]: tag for tag in tags}
|
|
174
|
+
return sql, tags
|
|
172
175
|
|
|
173
176
|
return None
|
|
174
177
|
|
|
@@ -22,6 +22,7 @@ class Settings(BaseSettings):
|
|
|
22
22
|
|
|
23
23
|
metabase_url: str
|
|
24
24
|
metabase_api_key: str
|
|
25
|
+
metabase_headers: dict[str, str] = Field(default_factory=dict)
|
|
25
26
|
state_dir: Path = Field(default=Path("state"))
|
|
26
27
|
http_timeout_s: float = Field(default=120.0)
|
|
27
28
|
http_max_retries: int = Field(default=3)
|
|
@@ -13,6 +13,7 @@ from pathlib import Path
|
|
|
13
13
|
|
|
14
14
|
import yaml
|
|
15
15
|
|
|
16
|
+
from metabase_sync.apply._cards import _build_dataset_query
|
|
16
17
|
from metabase_sync.models import Card
|
|
17
18
|
from metabase_sync.serialize.cards import read_card_file, write_card
|
|
18
19
|
|
|
@@ -125,6 +126,41 @@ def test_native_card_from_mbql5_stage_round_trip(tmp_path: Path):
|
|
|
125
126
|
assert fm["database"] == "sample"
|
|
126
127
|
|
|
127
128
|
|
|
129
|
+
def test_mbql5_stage_template_tag_list_round_trips_to_apply(tmp_path: Path):
|
|
130
|
+
tag = {
|
|
131
|
+
"id": "fixed-uuid",
|
|
132
|
+
"name": "date_range",
|
|
133
|
+
"display-name": "Date Range",
|
|
134
|
+
"type": "dimension",
|
|
135
|
+
"dimension": ["field", {"base-type": "type/DateTime"}, 31],
|
|
136
|
+
}
|
|
137
|
+
card = _make_native_card(
|
|
138
|
+
legacy_query=None,
|
|
139
|
+
dataset_query={
|
|
140
|
+
"lib/type": "mbql/query",
|
|
141
|
+
"database": 2,
|
|
142
|
+
"stages": [
|
|
143
|
+
{
|
|
144
|
+
"lib/type": "mbql.stage/native",
|
|
145
|
+
"native": "SELECT 1 WHERE {{date_range}}",
|
|
146
|
+
"template-tags": [tag],
|
|
147
|
+
}
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
path = tmp_path / "card.sql"
|
|
152
|
+
write_card(path, card, db_name_by_id={2: "sample"})
|
|
153
|
+
|
|
154
|
+
fm, body, _ = read_card_file(path)
|
|
155
|
+
dq = _build_dataset_query(fm, body, None, 2)
|
|
156
|
+
assert dq["native"]["template-tags"] == {
|
|
157
|
+
"date_range": {
|
|
158
|
+
**tag,
|
|
159
|
+
"dimension": ["field", 31, {"base-type": "type/DateTime"}],
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
128
164
|
def test_export_prefers_live_dataset_query_over_stale_legacy_query(tmp_path: Path):
|
|
129
165
|
"""The `legacy_query` projection can lag behind the live `dataset_query`
|
|
130
166
|
after an API write: Metabase answers a PUT by updating dataset_query while
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|