|
1 | | -"""Analytics data — queries the 6 looker_* BigQuery views.""" |
| 1 | +"""Analytics data — 6 BigQuery views queried in parallel with TTL cache.""" |
2 | 2 | from __future__ import annotations |
| 3 | +from concurrent.futures import ThreadPoolExecutor, as_completed |
3 | 4 | from google.cloud import bigquery |
4 | 5 | from config import PROJECT, DATASET |
| 6 | +from services.cache import ttl_cache |
5 | 7 |
|
| 8 | +_ds = f"{PROJECT}.{DATASET}" |
6 | 9 |
|
7 | | -def get_analytics(bq: bigquery.Client) -> dict: |
8 | | - ds = f"{PROJECT}.{DATASET}" |
9 | | - |
10 | | - cluster_dist = list(bq.query(f""" |
| 10 | +_QUERIES = { |
| 11 | + "cluster_distribution": f""" |
11 | 12 | SELECT cluster_label, product_count, avg_price |
12 | | - FROM `{ds}.looker_cluster_distribution` |
13 | | - ORDER BY product_count DESC |
14 | | - LIMIT 40 |
15 | | - """).result()) |
16 | | - |
17 | | - pricing = list(bq.query(f""" |
| 13 | + FROM `{_ds}.looker_cluster_distribution` |
| 14 | + ORDER BY product_count DESC LIMIT 40""", |
| 15 | + "pricing": f""" |
18 | 16 | SELECT cluster_label, avg_price, price_min, price_max, product_count |
19 | | - FROM `{ds}.looker_pricing_per_cluster` |
20 | | - ORDER BY avg_price DESC |
21 | | - LIMIT 20 |
22 | | - """).result()) |
23 | | - |
24 | | - heatmap = list(bq.query(f""" |
| 17 | + FROM `{_ds}.looker_pricing_per_cluster` |
| 18 | + ORDER BY avg_price DESC LIMIT 20""", |
| 19 | + "heatmap": f""" |
25 | 20 | SELECT category, department, product_count, avg_price |
26 | | - FROM `{ds}.looker_heatmap_cat_dept` |
27 | | - ORDER BY product_count DESC |
28 | | - """).result()) |
29 | | - |
30 | | - quality = list(bq.query(f""" |
| 21 | + FROM `{_ds}.looker_heatmap_cat_dept` |
| 22 | + ORDER BY product_count DESC""", |
| 23 | + "quality": f""" |
31 | 24 | SELECT completeness_pct, total_records, valid_records, |
32 | 25 | field_name_completeness, field_brand_completeness, |
33 | 26 | field_cat_completeness, field_price_completeness, |
34 | 27 | price_mean, price_min, price_max |
35 | | - FROM `{ds}.looker_data_quality` |
36 | | - LIMIT 1 |
37 | | - """).result()) |
38 | | - |
39 | | - timeline = list(bq.query(f""" |
| 28 | + FROM `{_ds}.looker_data_quality` LIMIT 1""", |
| 29 | + "timeline": f""" |
40 | 30 | SELECT sale_date, cluster_label, sales_count, sales_revenue |
41 | | - FROM `{ds}.looker_sales_timeline` |
42 | | - ORDER BY sale_date ASC |
43 | | - """).result()) |
44 | | - |
45 | | - brands = list(bq.query(f""" |
| 31 | + FROM `{_ds}.looker_sales_timeline` |
| 32 | + ORDER BY sale_date ASC""", |
| 33 | + "brands": f""" |
46 | 34 | SELECT cluster_label, brand, product_count |
47 | | - FROM `{ds}.looker_brands_per_cluster` |
48 | | - ORDER BY cluster_label, product_count DESC |
49 | | - LIMIT 100 |
50 | | - """).result()) |
| 35 | + FROM `{_ds}.looker_brands_per_cluster` |
| 36 | + ORDER BY cluster_label, product_count DESC LIMIT 100""", |
| 37 | +} |
| 38 | + |
| 39 | + |
| 40 | +def _row(r) -> dict: |
| 41 | + return { |
| 42 | + k: (float(v) if hasattr(v, "__float__") and not isinstance(v, (int, str, bool)) else v) |
| 43 | + for k, v in dict(r).items() |
| 44 | + } |
| 45 | + |
| 46 | + |
| 47 | +def _run(bq: bigquery.Client, key: str) -> tuple[str, list]: |
| 48 | + rows = list(bq.query(_QUERIES[key]).result()) |
| 49 | + return key, rows |
| 50 | + |
| 51 | + |
| 52 | +@ttl_cache(seconds=600) # 10-minute cache — data only changes after pipeline run |
| 53 | +def get_analytics(bq: bigquery.Client) -> dict: |
| 54 | + results: dict[str, list] = {} |
| 55 | + |
| 56 | + # Fire all 6 queries in parallel |
| 57 | + with ThreadPoolExecutor(max_workers=6) as pool: |
| 58 | + futures = {pool.submit(_run, bq, key): key for key in _QUERIES} |
| 59 | + for future in as_completed(futures): |
| 60 | + key, rows = future.result() |
| 61 | + results[key] = rows |
51 | 62 |
|
52 | | - def row(r): |
53 | | - return {k: (float(v) if hasattr(v, '__float__') and not isinstance(v, (int, str, bool)) else v) |
54 | | - for k, v in dict(r).items()} |
| 63 | + timeline_rows = results["timeline"] |
| 64 | + quality_rows = results["quality"] |
55 | 65 |
|
56 | 66 | return { |
57 | | - "cluster_distribution": [row(r) for r in cluster_dist], |
58 | | - "pricing": [row(r) for r in pricing], |
59 | | - "heatmap": [row(r) for r in heatmap], |
60 | | - "quality": row(quality[0]) if quality else {}, |
| 67 | + "cluster_distribution": [_row(r) for r in results["cluster_distribution"]], |
| 68 | + "pricing": [_row(r) for r in results["pricing"]], |
| 69 | + "heatmap": [_row(r) for r in results["heatmap"]], |
| 70 | + "quality": _row(quality_rows[0]) if quality_rows else {}, |
61 | 71 | "timeline": [ |
62 | 72 | {**{k: v for k, v in dict(r).items() if k != "sale_date"}, |
63 | 73 | "sale_date": str(r.sale_date)} |
64 | | - for r in timeline |
| 74 | + for r in timeline_rows |
65 | 75 | ], |
66 | | - "brands": [row(r) for r in brands], |
| 76 | + "brands": [_row(r) for r in results["brands"]], |
67 | 77 | } |
0 commit comments