From d2e49a5232f73aa1fb62893d80a91d85859fe961 Mon Sep 17 00:00:00 2001 From: Hamza Kundi Date: Mon, 28 Jul 2025 01:19:53 -0700 Subject: [PATCH] added metabase and metabase utils --- .../docker-compose.metabase_extension.yml | 14 ++++ .../management/commands/export_metabase.py | 66 +++++++++++++++++ .../management/commands/import_metabase.py | 71 +++++++++++++++++++ coldfront/core/utils/metabase_utils.py | 40 +++++++++++ 4 files changed, 191 insertions(+) create mode 100644 bootstrap/development/docker/docker-compose.metabase_extension.yml create mode 100644 coldfront/core/utils/management/commands/export_metabase.py create mode 100644 coldfront/core/utils/management/commands/import_metabase.py create mode 100644 coldfront/core/utils/metabase_utils.py diff --git a/bootstrap/development/docker/docker-compose.metabase_extension.yml b/bootstrap/development/docker/docker-compose.metabase_extension.yml new file mode 100644 index 0000000000..1565534125 --- /dev/null +++ b/bootstrap/development/docker/docker-compose.metabase_extension.yml @@ -0,0 +1,14 @@ +services: + metabase: + image: metabase/metabase:latest + ports: + - "3000:3000" + environment: + MB_DB_FILE: /metabase-data/metabase.db + volumes: + - metabase-data:/metabase-data + restart: unless-stopped + +volumes: + metabase-data: + external: false \ No newline at end of file diff --git a/coldfront/core/utils/management/commands/export_metabase.py b/coldfront/core/utils/management/commands/export_metabase.py new file mode 100644 index 0000000000..5e53d855fc --- /dev/null +++ b/coldfront/core/utils/management/commands/export_metabase.py @@ -0,0 +1,66 @@ +from django.core.management.base import BaseCommand, CommandError +import requests +import json + +from coldfront.core.utils.metabase_utils import get_card_details, get_collection_id, get_collection_items + +class Command(BaseCommand): + help = 'Export Metabase cards from a collection using API key' + + def add_arguments(self, parser): + parser.add_argument( + '--key', + type=str, + required=True, + help='Metabase API key' + ) + parser.add_argument( + '--collection', + type=str, + default='Savio Analytics', + help="Metabase collection name (default: 'Savio Analytics')" + ) + parser.add_argument( + '--url', + type=str, + default='http://metabase:3000/api', + help="Base URL for the Metabase API (default: 'http://metabase:3000/api')" + ) + + def handle(self, *args, **options): + API_KEY = options['key'] + collection_name = options['collection'] + + if not API_KEY: + raise CommandError('Please provide the API key using --key') + + HEADERS = { + 'x-api-key': API_KEY + } + + collection_id = get_collection_id(collection_name, HEADERS, options['url']) + + items = get_collection_items(collection_id, HEADERS, options['url'])['data'] + + cards = [get_card_details(item['id'], HEADERS, options['url']) for item in items] + + # Keep only the info needed to re-generate the cards + cards = [ + { + 'visualization_settings': card['visualization_settings'], + 'dataset_query': card['dataset_query'], + 'parameter_mappings': card['parameter_mappings'], + 'name': card['name'], + 'type': card['type'], + 'display': card['display'], + 'parameters': card['parameters'], + 'description': card['description'] + } + for card in cards + ] + + # Step 5: Output to file + output_file = f'{collection_name}_export.json' + with open(output_file, 'w') as f: + json.dump(cards, f, indent=2) + print(f'Exported {len(cards)} cards to {output_file}') diff --git a/coldfront/core/utils/management/commands/import_metabase.py b/coldfront/core/utils/management/commands/import_metabase.py new file mode 100644 index 0000000000..856dd13ee7 --- /dev/null +++ b/coldfront/core/utils/management/commands/import_metabase.py @@ -0,0 +1,71 @@ +from django.core.management.base import BaseCommand, CommandError +import json +import os + +from coldfront.core.utils.metabase_utils import create_card, get_collection_id + +class Command(BaseCommand): + help = "Import Metabase cards from a JSON file into a specific collection" + + def add_arguments(self, parser): + parser.add_argument( + "--key", + type=str, + required=True, + help="Metabase API key" + ) + parser.add_argument( + "--collection", + type=str, + default="Savio Analytics", + help="Metabase collection name (default: 'Savio Analytics')" + ) + parser.add_argument( + "--file", + type=str, + required=True, + help="Path to JSON file with exported card metadata" + ) + parser.add_argument( + "--url", + type=str, + default="http://metabase:3000/api", + help="Base URL for the Metabase API (default: 'http://metabase:3000/api')" + ) + parser.add_argument( + "--force", + action='store_true', + help="Force creation of cards even if they already exist (default: False)" + ) + + def handle(self, *args, **options): + API_KEY = options["key"] + collection_name = options["collection"] + file_path = options["file"] + + if not os.path.exists(file_path): + raise CommandError(f"File not found: {file_path}") + + HEADERS = { + "x-api-key": API_KEY, + "Content-Type": "application/json" + } + + + # Load exported cards from file + with open(file_path, "r") as f: + cards = json.load(f) + + if not isinstance(cards, list): + raise CommandError("Invalid file format: expected a list of card objects") + + collection_id = get_collection_id(collection_name, HEADERS, options['url']) + self.stdout.write(f"📦 Importing {len(cards)} cards to collection '{collection_name}' (ID {collection_id})") + + for i, card in enumerate(cards, 1): + result = create_card(card, collection_id, HEADERS, options['force'], options['url']) + if result.get('exists'): + self.stdout.write(f"Skipped card [{i}/{len(cards)}], already exists: {card['name']} (ID {result['id']})") + continue + self.stdout.write(f"✅ [{i}/{len(cards)}] Created card: {result['name']} (ID {result['id']})") + self.stdout.write(self.style.SUCCESS("All cards imported successfully!")) diff --git a/coldfront/core/utils/metabase_utils.py b/coldfront/core/utils/metabase_utils.py new file mode 100644 index 0000000000..71bd18b5aa --- /dev/null +++ b/coldfront/core/utils/metabase_utils.py @@ -0,0 +1,40 @@ +import json +import requests + +METABASE_URL = 'http://metabase:3000/api' + +def get_collection_id(collection_name, headers, url=METABASE_URL): + url = f'{url}/collection' + res = requests.get(url, headers=headers) + res.raise_for_status() + collections = res.json() + for col in collections: + if col['name'] == collection_name: + return col['id'] + raise f"Collection '{collection_name}' not found." + +def get_collection_items(collection_id, headers, url=METABASE_URL): + url = f'{url}/collection/{collection_id}/items' + res = requests.get(url, headers=headers) + res.raise_for_status() + return res.json() + +def get_card_details(card_id, headers, url=METABASE_URL): + url = f'{url}/card/{card_id}' + res = requests.get(url, headers=headers) + res.raise_for_status() + return res.json() + +def create_card(card_data, collection_id, headers, force=False, url=METABASE_URL): + if not force: + # Check if card already exists + existing_cards = get_collection_items(collection_id, headers) + for existing_card in existing_cards['data']: + if existing_card['name'] == card_data['name']: + return {"exists": True, "id": existing_card['id']} + payload = card_data.copy() + payload['collection_id'] = collection_id + url = f'{url}/card' + res = requests.post(url, headers=headers, data=json.dumps(payload)) + res.raise_for_status() + return res.json()