Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for API keys and session ID deprecation #249

Merged
merged 1 commit into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 [email protected] \
--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 [email protected] \
--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: [email protected]
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="[email protected]",
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
Loading