-
Notifications
You must be signed in to change notification settings - Fork 61
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Use standard port numbers - Make it available through docker - Define configuration options - Add command to dump/load questions - Have a dump of a question
- Loading branch information
1 parent
e4f88d1
commit 13214fc
Showing
8 changed files
with
324 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
"""Dump or load data from Metabase.""" | ||
import argparse | ||
import getpass | ||
import json | ||
|
||
import requests | ||
from django.conf import settings | ||
from django.core.management import CommandError | ||
from django.core.management.base import BaseCommand | ||
from django.utils.translation import ugettext_lazy as _ | ||
|
||
|
||
class Command(BaseCommand): | ||
|
||
"""Management command to dump/load data from Metabase.""" | ||
|
||
help = "Dump or load questions from metabase." | ||
|
||
def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False): | ||
"""Override to store the metabase session.""" | ||
super().__init__(stdout, stderr, no_color, force_color) | ||
self.metabase_session = None | ||
|
||
def add_arguments(self, parser): | ||
"""Add command line args for this command.""" | ||
parser.add_argument( | ||
"-d", | ||
"--dump-questions", | ||
type=argparse.FileType("w"), | ||
help=_("Dump questions to file"), | ||
) | ||
parser.add_argument( | ||
"-l", | ||
"--load-questions", | ||
type=argparse.FileType("r"), | ||
help=_("Load questions from a file"), | ||
) | ||
|
||
def handle(self, *args, **kwargs): | ||
"""Entrypoint to the command.""" | ||
self.authenticate_metabase() | ||
|
||
if kwargs["dump_questions"]: | ||
# Dump questions to a file | ||
self.handle_dump_questions(kwargs["dump_questions"]) | ||
elif kwargs["load_questions"]: | ||
# Load questions from a file | ||
self.handle_load_questions(kwargs["load_questions"]) | ||
|
||
def authenticate_metabase(self): | ||
"""Authenticate with metabase and store the session token temporarily.""" | ||
self.stdout.write( | ||
_("Authenticating with Metabase (%s)...") % settings.METABASE_SITE_URL | ||
) | ||
|
||
metabase_user = input("Metabase username: ") | ||
metabase_password = getpass.getpass("Metabase password: ") | ||
|
||
resp = requests.post( | ||
settings.METABASE_SITE_URL + "/api/session", | ||
json={"username": metabase_user, "password": metabase_password}, | ||
) | ||
if not resp.ok: | ||
self.stdout.write(resp.text) | ||
raise CommandError( | ||
_("Could not authenticate with those credentials to Metabase") | ||
) | ||
|
||
self.stdout.write( | ||
self.style.SUCCESS(_("Successfully authenticated to Metabase.")) | ||
) | ||
data = resp.json() | ||
self.metabase_session = data["id"] | ||
|
||
def handle_dump_questions(self, outfile): | ||
"""Dump questions from metabase to a file.""" | ||
self.stdout.write(_("Dumping questions from Metabase...")) | ||
|
||
resp = requests.get( | ||
settings.METABASE_SITE_URL + "/api/card/embeddable", | ||
headers={"X-Metabase-Session": self.metabase_session}, | ||
) | ||
|
||
if not resp.ok: | ||
self.stdout.write(resp.text) | ||
raise CommandError(_("Error getting questions")) | ||
|
||
questions = [] | ||
for question in resp.json(): | ||
# https://www.metabase.com/docs/latest/api-documentation.html#post-apicard | ||
card_resp = requests.get( | ||
settings.METABASE_SITE_URL + "/api/card/" + str(question["id"]), | ||
headers={"X-Metabase-Session": self.metabase_session}, | ||
) | ||
card = card_resp.json() | ||
questions.append( | ||
{ | ||
"id": card["id"], | ||
"name": card["name"], | ||
"description": card["description"], | ||
"result_metadata": card["result_metadata"], | ||
"dataset_query": card["dataset_query"], | ||
"display": card["display"], | ||
"visualization_settings": card["visualization_settings"], | ||
} | ||
) | ||
|
||
questions.sort(key=lambda q: q["id"]) | ||
outfile.write(json.dumps(questions, indent=2)) | ||
outfile.close() | ||
|
||
self.stdout.write( | ||
self.style.SUCCESS(_("Successfully dumped questions from Metabase.")) | ||
) | ||
|
||
def handle_load_questions(self, infile): | ||
"""Load questions from a file into metabase.""" | ||
self.stdout.write(_("Loading questions to Metabase...")) | ||
|
||
# Check if there are existing cards in Metabase (warn if so) | ||
resp = requests.get( | ||
settings.METABASE_SITE_URL + "/api/card", | ||
headers={"X-Metabase-Session": self.metabase_session}, | ||
) | ||
if resp.json(): | ||
self.stdout.write( | ||
self.style.WARNING(_("There are existing questions in this Metabase!!")) | ||
) | ||
proceed = input("Proceed? (y/N): ") | ||
if not proceed.lower().startswith("y"): | ||
return | ||
|
||
errors = 0 | ||
for question in json.load(infile): | ||
# https://www.metabase.com/docs/latest/api-documentation.html#post-apicard | ||
del question["id"] | ||
self.stdout.write(_(" - Loaded %s") % question["name"]) | ||
resp = requests.post( | ||
settings.METABASE_SITE_URL + "/api/card", | ||
headers={"X-Metabase-Session": self.metabase_session}, | ||
json=question, | ||
) | ||
|
||
if not resp.ok: | ||
self.stdout.write(resp.text) | ||
self.stdout.write(self.style.ERROR(_("Error loading question"))) | ||
errors += 1 | ||
|
||
if errors == 0: | ||
self.stdout.write( | ||
self.style.SUCCESS(_("Successfully loaded questions into Metabase.")) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
[ | ||
{ | ||
"id": 1, | ||
"name": "Publisher Performance", | ||
"description": "Show a single publisher's performance on a daily basis", | ||
"result_metadata": [ | ||
{ | ||
"name": "Date", | ||
"display_name": "Date", | ||
"base_type": "type/Date", | ||
"effective_type": "type/Date", | ||
"field_ref": [ | ||
"field", | ||
"Date", | ||
{ | ||
"base-type": "type/Date" | ||
} | ||
], | ||
"semantic_type": null, | ||
"fingerprint": { | ||
"global": { | ||
"distinct-count": 1, | ||
"nil%": 0 | ||
}, | ||
"type": { | ||
"type/DateTime": { | ||
"earliest": "2021-08-13T00:00:00Z", | ||
"latest": "2021-08-13T00:00:00Z" | ||
} | ||
} | ||
} | ||
}, | ||
{ | ||
"name": "sum", | ||
"display_name": "sum", | ||
"base_type": "type/BigInteger", | ||
"effective_type": "type/BigInteger", | ||
"field_ref": [ | ||
"field", | ||
"sum", | ||
{ | ||
"base-type": "type/BigInteger" | ||
} | ||
], | ||
"semantic_type": null, | ||
"fingerprint": { | ||
"global": { | ||
"distinct-count": 1, | ||
"nil%": 0 | ||
}, | ||
"type": { | ||
"type/Number": { | ||
"min": 1, | ||
"q1": 1, | ||
"q3": 1, | ||
"max": 1, | ||
"sd": null, | ||
"avg": 1 | ||
} | ||
} | ||
} | ||
}, | ||
{ | ||
"name": "sum_2", | ||
"display_name": "sum_2", | ||
"base_type": "type/BigInteger", | ||
"effective_type": "type/BigInteger", | ||
"field_ref": [ | ||
"field", | ||
"sum_2", | ||
{ | ||
"base-type": "type/BigInteger" | ||
} | ||
], | ||
"semantic_type": null, | ||
"fingerprint": { | ||
"global": { | ||
"distinct-count": 1, | ||
"nil%": 0 | ||
}, | ||
"type": { | ||
"type/Number": { | ||
"min": 0, | ||
"q1": 0, | ||
"q3": 0, | ||
"max": 0, | ||
"sd": null, | ||
"avg": 0 | ||
} | ||
} | ||
} | ||
} | ||
], | ||
"dataset_query": { | ||
"type": "native", | ||
"native": { | ||
"query": "SELECT \"public\".\"adserver_adimpression\".\"date\" AS \"Date\", sum(\"public\".\"adserver_adimpression\".\"views\") AS \"sum\", sum(\"public\".\"adserver_adimpression\".\"clicks\") AS \"sum_2\"\nFROM \"public\".\"adserver_adimpression\"\nINNER JOIN \"public\".\"adserver_publisher\" ON \"public\".\"adserver_publisher\".\"id\" = \"public\".\"adserver_adimpression\".\"publisher_id\"\nWHERE \"public\".\"adserver_adimpression\".\"date\" >= {{ start_date }}\n AND \"public\".\"adserver_adimpression\".\"date\" < {{ end_date }}\n AND \"public\".\"adserver_publisher\".\"slug\" = {{ publisher_slug }}\nGROUP BY \"public\".\"adserver_adimpression\".\"publisher_id\", \"public\".\"adserver_adimpression\".\"date\" \nORDER BY \"public\".\"adserver_adimpression\".\"publisher_id\", \"public\".\"adserver_adimpression\".\"date\" ASC", | ||
"template-tags": { | ||
"publisher_slug": { | ||
"id": "f118a498-62c3-cf6d-6bc6-c060aaf96fb5", | ||
"name": "publisher_slug", | ||
"display-name": "Publisher slug", | ||
"type": "text", | ||
"required": true, | ||
"default": "readthedocs" | ||
}, | ||
"start_date": { | ||
"id": "7f1144a1-ae40-8d20-a722-de53b03cc81a", | ||
"name": "start_date", | ||
"display-name": "Start date", | ||
"type": "date", | ||
"required": true | ||
}, | ||
"end_date": { | ||
"id": "d85c3237-3e49-b4c2-4e06-9f817d65d282", | ||
"name": "end_date", | ||
"display-name": "End date", | ||
"type": "date", | ||
"required": true | ||
} | ||
} | ||
}, | ||
"database": 2 | ||
}, | ||
"display": "bar", | ||
"visualization_settings": { | ||
"table.pivot_column": "publisher_id", | ||
"table.cell_column": "sum", | ||
"graph.dimensions": [ | ||
"Date" | ||
], | ||
"series_settings": { | ||
"undefined": { | ||
"axis": "left" | ||
}, | ||
"sum": { | ||
"title": "Views" | ||
}, | ||
"sum_2": { | ||
"title": "Clicks" | ||
} | ||
}, | ||
"graph.x_axis.title_text": "", | ||
"graph.metrics": [ | ||
"sum", | ||
"sum_2" | ||
] | ||
} | ||
} | ||
] |