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.
Files changed (65) hide show
  1. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/CHANGELOG.md +22 -0
  2. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/PKG-INFO +3 -2
  3. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/README.md +1 -0
  4. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/pyproject.toml +1 -1
  5. metabase_sync-0.3.1/src/metabase_sync/__init__.py +1 -0
  6. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/cli.py +1 -0
  7. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/client.py +6 -1
  8. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/cards.py +4 -1
  9. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/settings.py +1 -0
  10. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_legacy_query.py +36 -0
  11. metabase_sync-0.2.2/src/metabase_sync/__init__.py +0 -1
  12. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/.gitignore +0 -0
  13. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/LICENSE +0 -0
  14. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/examples/README.md +0 -0
  15. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/__main__.py +0 -0
  16. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/apply/__init__.py +0 -0
  17. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/apply/_cards.py +0 -0
  18. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/apply/_collections.py +0 -0
  19. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/apply/_dashboards.py +0 -0
  20. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/apply/_pulses.py +0 -0
  21. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/apply/_runner.py +0 -0
  22. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/apply/_shared.py +0 -0
  23. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/apply/_snippets.py +0 -0
  24. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/diff.py +0 -0
  25. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/errors.py +0 -0
  26. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/export.py +0 -0
  27. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/models.py +0 -0
  28. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/plan.py +0 -0
  29. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/__init__.py +0 -0
  30. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/canon.py +0 -0
  31. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/collections.py +0 -0
  32. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/dashboards.py +0 -0
  33. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/databases.py +0 -0
  34. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/paths.py +0 -0
  35. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/pulses.py +0 -0
  36. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/snippets.py +0 -0
  37. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/version.py +0 -0
  38. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/src/metabase_sync/serialize/yamlio.py +0 -0
  39. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/__init__.py +0 -0
  40. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/__init__.py +0 -0
  41. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/_builders.py +0 -0
  42. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/_mb.py +0 -0
  43. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/conftest.py +0 -0
  44. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/docker-compose.yml +0 -0
  45. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/test_apply_semantics.py +0 -0
  46. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/test_cards.py +0 -0
  47. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/test_cli.py +0 -0
  48. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/test_dashboards.py +0 -0
  49. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/test_export.py +0 -0
  50. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/test_pulses.py +0 -0
  51. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/integration/test_snippets.py +0 -0
  52. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_canon.py +0 -0
  53. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_card_diff.py +0 -0
  54. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_client_retries.py +0 -0
  55. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_concurrency.py +0 -0
  56. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_dashboard_diff.py +0 -0
  57. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_paths.py +0 -0
  58. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_paths_resolve.py +0 -0
  59. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_preflight.py +0 -0
  60. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_pulse_diff.py +0 -0
  61. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_recipients.py +0 -0
  62. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_state_version.py +0 -0
  63. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_template_tags.py +0 -0
  64. {metabase_sync-0.2.2 → metabase_sync-0.3.1}/tests/test_version_check.py +0 -0
  65. {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.4
1
+ Metadata-Version: 2.5
2
2
  Name: metabase-sync
3
- Version: 0.2.2
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.2.2"
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"
@@ -86,6 +86,7 @@ def _client_from_settings(s):
86
86
  timeout_s=s.http_timeout_s,
87
87
  max_retries=s.http_max_retries,
88
88
  retry_backoff_s=s.http_retry_backoff_s,
89
+ headers=s.metabase_headers,
89
90
  )
90
91
 
91
92
 
@@ -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={"X-API-Key": api_key, "Accept": "application/json"},
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
- return sql, stage.get("template-tags") or {}
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