Skip to content
Open
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
14 changes: 14 additions & 0 deletions bootstrap/development/docker/docker-compose.metabase_extension.yml
Original file line number Diff line number Diff line change
@@ -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
66 changes: 66 additions & 0 deletions coldfront/core/utils/management/commands/export_metabase.py
Original file line number Diff line number Diff line change
@@ -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}')
71 changes: 71 additions & 0 deletions coldfront/core/utils/management/commands/import_metabase.py
Original file line number Diff line number Diff line change
@@ -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!"))
40 changes: 40 additions & 0 deletions coldfront/core/utils/metabase_utils.py
Original file line number Diff line number Diff line change
@@ -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()