Skip to content

Commit

Permalink
Support for API keys and session ID deprecation (#249)
Browse files Browse the repository at this point in the history
  • Loading branch information
gouline authored Apr 2, 2024
1 parent 7a98a24 commit 9dad3b9
Show file tree
Hide file tree
Showing 5 changed files with 52 additions and 26 deletions.
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ Once `dbt compile` finishes, `manifest.json` can be found in the `target/` direc

See [dbt documentation](https://docs.getdbt.com/docs/running-a-dbt-project/run-your-dbt-projects) for more information.

## Metabase API

All commands require authentication against the [Metabase API](https://www.metabase.com/docs/latest/api-documentation) using one of these methods:

* API key (`--metabase-api-key`)
- Strongly **recommended** for automation, see [documentation](https://www.metabase.com/docs/latest/people-and-groups/api-keys) (Metabase 49 or later).
* Username and password (`--metabase-username` / `--metabase-password`)
- Fallback for older versions of Metabase and smaller instances.

## Exporting Models

Let's start by defining a short sample `schema.yml` as below.
Expand Down Expand Up @@ -81,8 +90,7 @@ This is already enough to propagate the primary keys, foreign keys and descripti
dbt-metabase models \
--manifest-path target/manifest.json \
--metabase-url https://metabase.example.com \
--metabase-username user@example.com \
--metabase-password Password123 \
--metabase-api-key mb_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX= \
--metabase-database business \
--include-schemas public
```
Expand Down Expand Up @@ -208,8 +216,7 @@ dbt-metabase allows you to extract questions and dashboards from Metabase as [db
dbt-metabase exposures \
--manifest-path ./target/manifest.json \
--metabase-url https://metabase.example.com \
--metabase-username user@example.com \
--metabase-password Password123 \
--metabase-api-key mb_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX= \
--output-path models/ \
--exclude-collections "temp*"
```
Expand Down Expand Up @@ -259,8 +266,7 @@ A configuration file can be created in `~/.dbt-metabase/config.yml` for dbt-meta
config:
manifest_path: target/manifest.json
metabase_url: https://metabase.example.com
metabase_username: user@example.com
metabase_password: Password123
metabase_api_key: mb_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=
# Configuration specific to models command
models:
metabase_database: business
Expand All @@ -282,8 +288,7 @@ from dbtmetabase import DbtMetabase, Filter
c = DbtMetabase(
manifest_path="target/manifest.json",
metabase_url="https://metabase.example.com",
metabase_username="user@example.com",
metabase_password="Password123",
metabase_api_key="mb_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=",
)
# Exporting models
Expand Down
17 changes: 14 additions & 3 deletions dbtmetabase/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,29 +95,38 @@ def _add_setup(func: Callable) -> Callable:
type=click.STRING,
help="Metabase URL, e.g. 'https://metabase.example.com'.",
)
@click.option(
"--metabase-api-key",
metavar="API_KEY",
envvar="METABASE_API_KEY",
show_envvar=True,
type=click.STRING,
help="Metabase API key (required unless providing username/password).",
)
@click.option(
"--metabase-username",
metavar="USERNAME",
envvar="METABASE_USERNAME",
show_envvar=True,
type=click.STRING,
help="Metabase username (required unless providing session ID).",
help="Metabase username (required unless providing API key).",
)
@click.option(
"--metabase-password",
metavar="PASSWORD",
envvar="METABASE_PASSWORD",
show_envvar=True,
type=click.STRING,
help="Metabase password (required unless providing session ID).",
help="Metabase password (required unless providing API key).",
)
@click.option(
"--metabase-session-id",
metavar="TOKEN",
envvar="METABASE_SESSION_ID",
show_envvar=True,
type=click.STRING,
help="Metabase session ID (alternative to username/password).",
help="Metabase session ID (deprecated and will be removed in future).",
hidden=True,
)
@click.option(
"--skip-verify",
Expand Down Expand Up @@ -160,6 +169,7 @@ def _add_setup(func: Callable) -> Callable:
def wrapper(
manifest_path: str,
metabase_url: str,
metabase_api_key: str,
metabase_username: str,
metabase_password: str,
metabase_session_id: Optional[str],
Expand All @@ -179,6 +189,7 @@ def wrapper(
core=DbtMetabase(
manifest_path=manifest_path,
metabase_url=metabase_url,
metabase_api_key=metabase_api_key,
metabase_username=metabase_username,
metabase_password=metabase_password,
metabase_session_id=metabase_session_id,
Expand Down
9 changes: 6 additions & 3 deletions dbtmetabase/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def __init__(
self,
manifest_path: Union[str, Path],
metabase_url: str,
metabase_api_key: Optional[str] = None,
metabase_username: Optional[str] = None,
metabase_password: Optional[str] = None,
metabase_session_id: Optional[str] = None,
Expand All @@ -37,9 +38,10 @@ def __init__(
Args:
manifest_path (Union[str,Path]): Path to dbt manifest.json, usually in target/ directory after compilation.
metabase_url (str): Metabase URL, e.g. "https://metabase.example.com".
metabase_username (Optional[str], optional): Metabase username (required unless providing session ID). Defaults to None.
metabase_password (Optional[str], optional): Metabase password (required unless providing session ID). Defaults to None.
metabase_session_id (Optional[str], optional): Metabase session ID. Defaults to None.
metabase_api_key (Optional[str], optional): Metabase API key (required unless providing username/password or session ID). Defaults to None.
metabase_username (Optional[str], optional): Metabase username (required unless providing API key or session ID). Defaults to None.
metabase_password (Optional[str], optional): Metabase password (required unless providing API key or session ID). Defaults to None.
metabase_session_id (Optional[str], optional): Metabase session ID (deprecated and will be removed in future). Defaults to None.
skip_verify (bool, optional): Skip TLS certificate verification (not recommended). Defaults to False.
cert (Optional[Union[str, Tuple[str, str]]], optional): Path to a custom certificate. Defaults to None.
http_timeout (int, optional): HTTP request timeout in secs. Defaults to 15.
Expand All @@ -52,6 +54,7 @@ def __init__(
)
self._metabase = Metabase(
url=metabase_url,
api_key=metabase_api_key,
username=metabase_username,
password=metabase_password,
session_id=metabase_session_id,
Expand Down
30 changes: 18 additions & 12 deletions dbtmetabase/metabase.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class Metabase:
def __init__(
self,
url: str,
api_key: Optional[str],
username: Optional[str],
password: Optional[str],
session_id: Optional[str],
Expand All @@ -38,19 +39,24 @@ def __init__(
http_adapter or HTTPAdapter(max_retries=Retry(total=3, backoff_factor=1)),
)

if not session_id:
if username and password:
session = dict(
self._api(
method="post",
path="/api/session",
json={"username": username, "password": password},
)
if api_key:
self.session.headers["X-API-KEY"] = api_key
elif username and password:
session = dict(
self._api(
method="post",
path="/api/session",
json={"username": username, "password": password},
)
session_id = str(session["id"])
else:
raise ArgumentError("Metabase credentials or session ID required")
self.session.headers["X-Metabase-Session"] = session_id
)
self.session.headers["X-Metabase-Session"] = str(session["id"])
elif session_id:
_logger.warning(
"Metabase session ID is deprecated and will be removed in future, use API key or username/password instead"
)
self.session.headers["X-Metabase-Session"] = session_id
else:
raise ArgumentError("Metabase API key or username/password required")

_logger.info("Metabase session established")

Expand Down
1 change: 1 addition & 0 deletions tests/_mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class MockMetabase(Metabase):
def __init__(self, url: str):
super().__init__(
url=url,
api_key=None,
username=None,
password=None,
session_id="dummy",
Expand Down

0 comments on commit 9dad3b9

Please sign in to comment.