-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
322 additions
and
5 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
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,92 @@ | ||
import copy | ||
import json | ||
from urllib.parse import urljoin | ||
|
||
import httpx | ||
from django.conf import settings | ||
|
||
|
||
# Metabase API client | ||
# See: https://www.metabase.com/docs/latest/api/ | ||
class Client: | ||
def __init__(self, base_url): | ||
self._client = httpx.Client( | ||
base_url=urljoin(base_url, "/api"), | ||
headers={ | ||
"X-API-KEY": settings.METABASE_API_KEY, | ||
}, | ||
timeout=httpx.Timeout(5, read=60), # Use a not-so-long but not not-so-short read timeout | ||
) | ||
|
||
@staticmethod | ||
def _build_metabase_field(field, base_type="type/Text"): | ||
return ["field", field, {"base-type": base_type}] | ||
|
||
@staticmethod | ||
def _build_metabase_filter(field, values, base_type="type/Text"): | ||
return [ | ||
"=", | ||
Client._build_metabase_field(field, base_type), | ||
*values, | ||
] | ||
|
||
@staticmethod | ||
def _join_metabase_filters(*filters): | ||
return [ | ||
"and", | ||
*filters, | ||
] | ||
|
||
def build_query(self, *, select=None, table=None, where=None, group_by=None, limit=None): | ||
query = {} | ||
if select: | ||
query["fields"] = [self._build_metabase_field(field) for field in select] | ||
if table: | ||
query["source-table"] = table | ||
if where: | ||
query["filter"] = [self._build_metabase_filter(field, values) for field, values in where.items()] | ||
if group_by: | ||
query["breakout"] = [self._build_metabase_field(field) for field in group_by] | ||
if limit: | ||
query["limit"] = limit | ||
|
||
return query | ||
|
||
def merge_query(self, into, query): | ||
into = copy.deepcopy(into) | ||
|
||
if "fields" in query: | ||
into.setdefault("fields", []) | ||
into["fields"].extend(query["fields"]) | ||
if "filter" in query: | ||
into.setdefault("filter", []) | ||
into["filter"] = self._join_metabase_filters(into["filter"], query["filter"]) | ||
if "breakout" in query: | ||
into.setdefault("breakout", []) | ||
into["breakout"].extend(query["breakout"]) | ||
if "limit" in query: | ||
into["limit"] = query["limit"] | ||
|
||
return into | ||
|
||
def build_dataset_query(self, *, database, query): | ||
return {"database": database, "type": "query", "query": query, "parameters": []} | ||
|
||
def fetch_dataset_results(self, query): | ||
# /!\ MB (hardcoded) limit to the first 2000 rows when "viewing", "download" has a 1_000_000 rows limits | ||
return ( | ||
self._client.post("/dataset/json", data={"query": json.dumps(query)}) | ||
.raise_for_status() | ||
.json() | ||
) | ||
|
||
def fetch_card_results(self, card, filters=None, group_by=None): | ||
if not any([filters, group_by]): | ||
return self._client.post(f"/card/{card}/query/json").raise_for_status().json() | ||
|
||
dataset_query = self._client.get(f"/card/{card}").raise_for_status().json()["dataset_query"] | ||
dataset_query["query"] = self.merge_query( | ||
dataset_query["query"], | ||
self.build_query(where=filters, group_by=group_by), | ||
) | ||
return self.fetch_dataset_results(dataset_query) |
Empty file.
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,8 @@ | ||
from django.urls import path | ||
|
||
from . import views | ||
|
||
app_name = "api" | ||
urlpatterns = [ | ||
path("dataset/<slug:name>", views.dataset, name="dataset"), | ||
] |
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,57 @@ | ||
from django.conf import settings | ||
from django.db.models.enums import TextChoices | ||
from django.http import ( | ||
HttpResponseNotFound, | ||
JsonResponse, | ||
) | ||
|
||
from itoutils import metabase | ||
|
||
|
||
class DataSetName(TextChoices): | ||
DI_SERVICES = "di_services", "data·inclusion - Services" | ||
DI_STRUCTURES = "di_structures", "data·inclusion - Structures" | ||
|
||
|
||
QUERIES = { | ||
DataSetName.DI_SERVICES: { | ||
"database": 2, | ||
"type": "query", | ||
"query": { | ||
"source-table": 2052, | ||
"filter": [ | ||
"starts-with", | ||
["field", 59654, {"base-type": "type/Text"}], | ||
"000", | ||
{"case-sensitive": False}, | ||
], | ||
}, | ||
}, | ||
DataSetName.DI_STRUCTURES: { | ||
"database": 2, | ||
"type": "query", | ||
"query": { | ||
"source-table": 2051, | ||
"filter": [ | ||
"starts-with", | ||
["field", 59588, {"base-type": "type/Text"}], | ||
"000", | ||
{"case-sensitive": False}, | ||
], | ||
}, | ||
}, | ||
} | ||
|
||
|
||
def dataset(request, name): | ||
if name not in DataSetName: | ||
return HttpResponseNotFound() | ||
|
||
query = QUERIES[DataSetName(name)] | ||
if department := request.GET.get("department"): | ||
# TODO: Check is_digit() + 2A/2B if applicable, and length | ||
query["query"]["filter"][2] = str(department)[:3] | ||
|
||
# TODO: handle errors and whatnot | ||
data = metabase.Client(settings.METABASE_URL).fetch_dataset_results(query) | ||
return JsonResponse(data, safe=False) # Metabase return a list |
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
Oops, something went wrong.