153 lines
5.2 KiB
Python
153 lines
5.2 KiB
Python
|
|
"""Hadoop / HDFS / Iceberg catalog and sampling for the Data Hub."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import csv
|
||
|
|
import io
|
||
|
|
import os
|
||
|
|
from typing import Any, Callable
|
||
|
|
|
||
|
|
from webhdfs_util import open_bytes
|
||
|
|
|
||
|
|
HDFS_PATHS = [
|
||
|
|
"/data/historical/sales_orders",
|
||
|
|
"/data/historical/sales_orders/year=2020/part-0.csv",
|
||
|
|
]
|
||
|
|
|
||
|
|
ICEBERG_SCHEMAS = ["hadoop", "curated_masked", "curated"]
|
||
|
|
|
||
|
|
HADOOP_SAMPLES: list[dict[str, str]] = [
|
||
|
|
{"id": "hd1", "label": "Iceberg historical sales", "sql": "SELECT * FROM iceberg.hadoop.historical_sales_hdfs LIMIT 20"},
|
||
|
|
{"id": "hd2", "label": "Row count historical", "sql": "SELECT count(*) FROM iceberg.hadoop.historical_sales_hdfs"},
|
||
|
|
{"id": "hd3", "label": "Curated masked sample", "sql": "SELECT * FROM iceberg.curated_masked.sales_orders_masked LIMIT 15"},
|
||
|
|
{"id": "hd4", "label": "Hive lake schemas", "sql": "SHOW SCHEMAS FROM hive"},
|
||
|
|
{"id": "hd5", "label": "Iceberg hadoop tables", "sql": "SHOW TABLES FROM iceberg.hadoop"},
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def catalog_hadoop(run_trino: Callable[[str, int], dict[str, Any]]) -> dict[str, Any]:
|
||
|
|
objects: list[dict[str, Any]] = []
|
||
|
|
|
||
|
|
for schema in ICEBERG_SCHEMAS:
|
||
|
|
show = run_trino(f"SHOW TABLES FROM iceberg.{schema}", 500)
|
||
|
|
if not show.get("ok"):
|
||
|
|
continue
|
||
|
|
for row in show.get("rows") or []:
|
||
|
|
name = str(row[0])
|
||
|
|
fqn = f"iceberg.{schema}.{name}"
|
||
|
|
cnt = run_trino(f"SELECT count(*) FROM iceberg.{schema}.{name}", 1)
|
||
|
|
count = None
|
||
|
|
if cnt.get("ok") and cnt.get("rows"):
|
||
|
|
try:
|
||
|
|
count = int(cnt["rows"][0][0])
|
||
|
|
except (TypeError, ValueError, IndexError):
|
||
|
|
pass
|
||
|
|
objects.append({
|
||
|
|
"type": "table",
|
||
|
|
"schema": f"iceberg.{schema}",
|
||
|
|
"name": name,
|
||
|
|
"fqn": fqn,
|
||
|
|
"row_count": count,
|
||
|
|
})
|
||
|
|
|
||
|
|
for path in HDFS_PATHS:
|
||
|
|
label = path.rstrip("/").rsplit("/", 1)[-1]
|
||
|
|
objects.append({
|
||
|
|
"type": "file",
|
||
|
|
"schema": "hdfs",
|
||
|
|
"name": label,
|
||
|
|
"fqn": f"hdfs:{path}",
|
||
|
|
"row_count": None,
|
||
|
|
})
|
||
|
|
|
||
|
|
return {"engine": "hadoop", "version": "HDFS + Iceberg", "objects": objects}
|
||
|
|
|
||
|
|
|
||
|
|
def sample_hdfs_csv(path: str, limit: int, offset: int, tabular: Callable[..., dict[str, Any]]) -> dict[str, Any]:
|
||
|
|
try:
|
||
|
|
text = open_bytes(path).decode("utf-8", errors="replace")
|
||
|
|
reader = csv.reader(io.StringIO(text))
|
||
|
|
all_rows = list(reader)
|
||
|
|
if not all_rows:
|
||
|
|
return tabular([], [], 0, row_count=0)
|
||
|
|
columns = [c.strip() for c in all_rows[0]]
|
||
|
|
data = all_rows[1 + offset: 1 + offset + limit]
|
||
|
|
rows = [[cell.strip() for cell in row] for row in data]
|
||
|
|
return tabular(columns, rows, 0, row_count=max(0, len(all_rows) - 1))
|
||
|
|
except Exception as exc:
|
||
|
|
return {"ok": False, "error": str(exc)[:500]}
|
||
|
|
|
||
|
|
|
||
|
|
def sample_hadoop(
|
||
|
|
object_name: str,
|
||
|
|
limit: int,
|
||
|
|
offset: int,
|
||
|
|
run_trino: Callable[[str, int], dict[str, Any]],
|
||
|
|
tabular: Callable[..., dict[str, Any]],
|
||
|
|
) -> dict[str, Any]:
|
||
|
|
if object_name.startswith("hdfs:"):
|
||
|
|
return sample_hdfs_csv(object_name[5:], limit, offset, tabular)
|
||
|
|
sql = f"SELECT * FROM {object_name} OFFSET {offset} LIMIT {limit}"
|
||
|
|
return run_trino(sql, limit)
|
||
|
|
|
||
|
|
|
||
|
|
def table_row_count_hadoop(object_name: str, run_trino: Callable[[str, int], dict[str, Any]]) -> int | None:
|
||
|
|
if object_name.startswith("hdfs:"):
|
||
|
|
try:
|
||
|
|
text = open_bytes(object_name[5:]).decode("utf-8", errors="replace")
|
||
|
|
return max(0, sum(1 for _ in csv.reader(io.StringIO(text))) - 1)
|
||
|
|
except Exception:
|
||
|
|
return None
|
||
|
|
cnt = run_trino(f"SELECT count(*) FROM {object_name}", 1)
|
||
|
|
if cnt.get("ok") and cnt.get("rows"):
|
||
|
|
try:
|
||
|
|
return int(cnt["rows"][0][0])
|
||
|
|
except (TypeError, ValueError, IndexError):
|
||
|
|
pass
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def health_hadoop(run_trino: Callable[[str, int], dict[str, Any]]) -> dict[str, Any]:
|
||
|
|
from webhdfs_util import HDFS_NN_URL
|
||
|
|
import httpx
|
||
|
|
|
||
|
|
nn_ok = False
|
||
|
|
live_dn = None
|
||
|
|
err_parts: list[str] = []
|
||
|
|
try:
|
||
|
|
with httpx.Client(timeout=6.0) as client:
|
||
|
|
r = client.get(f"{HDFS_NN_URL}/jmx?qry=Hadoop:service=NameNode,name=FSNamesystemState")
|
||
|
|
beans = (r.json().get("beans") or [{}])[0]
|
||
|
|
nn_ok = r.status_code < 400
|
||
|
|
live_dn = beans.get("NumLiveDataNodes")
|
||
|
|
except Exception as exc:
|
||
|
|
err_parts.append(f"NN: {exc}")
|
||
|
|
|
||
|
|
trino_ok = False
|
||
|
|
try:
|
||
|
|
tr = run_trino("SELECT 1", 1)
|
||
|
|
trino_ok = bool(tr.get("ok"))
|
||
|
|
if not trino_ok and tr.get("error"):
|
||
|
|
err_parts.append(f"Trino: {tr['error'][:80]}")
|
||
|
|
except Exception as exc:
|
||
|
|
err_parts.append(f"Trino: {exc}")
|
||
|
|
|
||
|
|
ok = nn_ok and trino_ok
|
||
|
|
return {
|
||
|
|
"ok": ok,
|
||
|
|
"namenode": HDFS_NN_URL,
|
||
|
|
"live_datanodes": live_dn,
|
||
|
|
"trino_ok": trino_ok,
|
||
|
|
"error": "; ".join(err_parts) if err_parts else None,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def connection_info() -> dict[str, str]:
|
||
|
|
from webhdfs_util import HDFS_NN_URL, HDFS_USER
|
||
|
|
return {
|
||
|
|
"namenode": HDFS_NN_URL,
|
||
|
|
"user": HDFS_USER,
|
||
|
|
"trino": os.getenv("TRINO_URL", "http://10.0.21.50:8089"),
|
||
|
|
"spark": os.getenv("SPARK_UI_URL", "http://10.0.21.50:8080"),
|
||
|
|
}
|