📦 deps(thirdparty): update snapshots
This commit is contained in:
+66
@@ -0,0 +1,66 @@
|
||||
"""Path guards for local Monte Carlo template manifests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _allow_external_paths() -> bool:
|
||||
return os.getenv("MCD_ALLOW_EXTERNAL_PATHS", "").lower() in {"1", "true", "yes"}
|
||||
|
||||
|
||||
def _is_relative_to(path: Path, root: Path) -> bool:
|
||||
try:
|
||||
path.relative_to(root)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def _resolve_local_path(raw_path: str, *, expect_file: bool = False, create_parent: bool = False) -> Path:
|
||||
value = str(raw_path).strip()
|
||||
if not value or "\0" in value:
|
||||
raise ValueError("Path must be a non-empty filesystem path")
|
||||
base = Path.cwd().resolve()
|
||||
candidate = Path(value).expanduser()
|
||||
resolved = (candidate if candidate.is_absolute() else base / candidate).resolve()
|
||||
if not _allow_external_paths() and not _is_relative_to(resolved, base):
|
||||
raise ValueError(f"Path must stay under the current working directory: {raw_path!r}")
|
||||
if expect_file and not resolved.is_file():
|
||||
raise FileNotFoundError(f"Input file not found: {resolved}")
|
||||
if create_parent:
|
||||
resolved.parent.mkdir(parents=True, exist_ok=True)
|
||||
return resolved
|
||||
|
||||
|
||||
def safe_input_json_path(raw_path: str) -> Path:
|
||||
path = _resolve_local_path(raw_path, expect_file=True)
|
||||
if path.suffix.lower() != ".json":
|
||||
raise ValueError(f"Input manifest must be a .json file: {path}")
|
||||
return path
|
||||
|
||||
|
||||
def safe_output_json_path(raw_path: str) -> Path:
|
||||
path = _resolve_local_path(raw_path, create_parent=True)
|
||||
if path.suffix.lower() != ".json":
|
||||
raise ValueError(f"Output manifest must be a .json file: {path}")
|
||||
return path
|
||||
|
||||
|
||||
def safe_existing_directory(raw_path: str) -> Path:
|
||||
path = _resolve_local_path(raw_path)
|
||||
if not path.is_dir():
|
||||
raise NotADirectoryError(f"Directory not found: {path}")
|
||||
return path
|
||||
|
||||
|
||||
def read_json_file(raw_path: str):
|
||||
with safe_input_json_path(raw_path).open() as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
def write_json_file(raw_path: str, payload, *, indent: int = 2, default=None) -> None:
|
||||
with safe_output_json_path(raw_path).open("w") as fh:
|
||||
json.dump(payload, fh, indent=indent, default=default)
|
||||
+12
-4
@@ -14,8 +14,9 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import os
|
||||
|
||||
from collect_metadata import collect
|
||||
from collect_metadata import _require_bq_identifier, collect
|
||||
from push_metadata import push
|
||||
from _safe_paths import safe_output_json_path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@@ -49,21 +50,28 @@ def main() -> None:
|
||||
if missing:
|
||||
parser.error(f"Missing required push arguments/env vars: {missing}")
|
||||
|
||||
manifest_path = str(safe_output_json_path(args.manifest_file))
|
||||
push_result_path = str(safe_output_json_path(args.push_result_file))
|
||||
|
||||
args.project_id = _require_bq_identifier(args.project_id, "project_id")
|
||||
args.datasets = [_require_bq_identifier(d, "dataset") for d in args.datasets or []] or None
|
||||
args.tables = [_require_bq_identifier(t, "table") for t in args.tables or []] or None
|
||||
|
||||
collect(
|
||||
project_id=args.project_id,
|
||||
datasets=args.datasets,
|
||||
tables=args.tables,
|
||||
only_freshness_and_volume=args.only_freshness_and_volume,
|
||||
output_file=args.manifest_file,
|
||||
output_file=manifest_path,
|
||||
)
|
||||
|
||||
push(
|
||||
input_file=args.manifest_file,
|
||||
input_file=manifest_path,
|
||||
resource_uuid=args.resource_uuid,
|
||||
key_id=args.key_id,
|
||||
key_token=args.key_token,
|
||||
batch_size=args.batch_size,
|
||||
output_file=args.push_result_file,
|
||||
output_file=push_result_path,
|
||||
)
|
||||
|
||||
|
||||
|
||||
+7
-3
@@ -15,6 +15,7 @@ import os
|
||||
|
||||
from collect_query_logs import LOOKBACK_HOURS, LOOKBACK_LAG_HOURS, collect
|
||||
from push_query_logs import push
|
||||
from _safe_paths import safe_output_json_path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@@ -43,20 +44,23 @@ def main() -> None:
|
||||
if missing:
|
||||
parser.error(f"Missing required push arguments/env vars: {missing}")
|
||||
|
||||
manifest_path = str(safe_output_json_path(args.manifest_file))
|
||||
push_result_path = str(safe_output_json_path(args.push_result_file))
|
||||
|
||||
collect(
|
||||
project_id=args.project_id,
|
||||
lookback_hours=args.lookback_hours,
|
||||
lookback_lag_hours=args.lookback_lag_hours,
|
||||
output_file=args.manifest_file,
|
||||
output_file=manifest_path,
|
||||
)
|
||||
|
||||
push(
|
||||
input_file=args.manifest_file,
|
||||
input_file=manifest_path,
|
||||
resource_uuid=args.resource_uuid,
|
||||
key_id=args.key_id,
|
||||
key_token=args.key_token,
|
||||
batch_size=args.batch_size,
|
||||
output_file=args.push_result_file,
|
||||
output_file=push_result_path,
|
||||
)
|
||||
|
||||
|
||||
|
||||
+32
-9
@@ -26,14 +26,24 @@ import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from google.cloud import bigquery
|
||||
from _safe_paths import safe_existing_directory, safe_input_json_path, safe_output_json_path, write_json_file
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
RESOURCE_TYPE = "bigquery"
|
||||
_BQ_IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9_-]+$")
|
||||
|
||||
|
||||
def _require_bq_identifier(value: str, field: str) -> str:
|
||||
value = str(value).strip()
|
||||
if not value or not _BQ_IDENTIFIER_RE.fullmatch(value):
|
||||
raise ValueError(f"Invalid BigQuery {field}: {value!r}")
|
||||
return value
|
||||
|
||||
# BigQuery type → Monte Carlo canonical type
|
||||
BQ_TYPE_MAP: dict[str, str] = {
|
||||
@@ -71,16 +81,20 @@ def _fetch_iceberg_tables(
|
||||
tables: list[str] | None = None,
|
||||
) -> list[dict]:
|
||||
"""Query TABLE_STORAGE for BigLake (Iceberg) tables."""
|
||||
project_id = _require_bq_identifier(project_id, "project_id")
|
||||
datasets = [_require_bq_identifier(d, "dataset") for d in datasets or []] or None
|
||||
tables = [_require_bq_identifier(t, "table") for t in tables or []] or None
|
||||
conditions = [
|
||||
"managed_table_type = 'BIGLAKE'",
|
||||
"deleted = FALSE",
|
||||
]
|
||||
query_parameters = []
|
||||
if datasets:
|
||||
ds_list = ", ".join(f"'{d}'" for d in datasets)
|
||||
conditions.append(f"table_schema IN ({ds_list})")
|
||||
conditions.append("table_schema IN UNNEST(@datasets)")
|
||||
query_parameters.append(bigquery.ArrayQueryParameter("datasets", "STRING", datasets))
|
||||
if tables:
|
||||
tbl_list = ", ".join(f"'{t}'" for t in tables)
|
||||
conditions.append(f"table_name IN ({tbl_list})")
|
||||
conditions.append("table_name IN UNNEST(@tables)")
|
||||
query_parameters.append(bigquery.ArrayQueryParameter("tables", "STRING", tables))
|
||||
|
||||
where = " AND ".join(conditions)
|
||||
query = f"""
|
||||
@@ -96,7 +110,8 @@ def _fetch_iceberg_tables(
|
||||
ORDER BY table_schema, table_name
|
||||
"""
|
||||
log.info("Querying TABLE_STORAGE for Iceberg tables ...")
|
||||
rows = list(client.query(query).result())
|
||||
job_config = bigquery.QueryJobConfig(query_parameters=query_parameters)
|
||||
rows = list(client.query(query, job_config=job_config).result())
|
||||
log.info("Found %d Iceberg table(s).", len(rows))
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
@@ -108,18 +123,24 @@ def _fetch_columns(
|
||||
table_name: str,
|
||||
) -> list[dict]:
|
||||
"""Fetch column metadata for a specific table."""
|
||||
project_id = _require_bq_identifier(project_id, "project_id")
|
||||
dataset = _require_bq_identifier(dataset, "dataset")
|
||||
table_name = _require_bq_identifier(table_name, "table")
|
||||
query = f"""
|
||||
SELECT column_name, data_type, ordinal_position, is_nullable, column_default
|
||||
FROM `{project_id}.{dataset}.INFORMATION_SCHEMA.COLUMNS`
|
||||
WHERE table_name = '{table_name}'
|
||||
WHERE table_name = @table_name
|
||||
ORDER BY ordinal_position
|
||||
"""
|
||||
job_config = bigquery.QueryJobConfig(
|
||||
query_parameters=[bigquery.ScalarQueryParameter("table_name", "STRING", table_name)]
|
||||
)
|
||||
return [
|
||||
{
|
||||
"name": row["column_name"],
|
||||
"type": map_bq_type(row["data_type"]),
|
||||
}
|
||||
for row in client.query(query).result()
|
||||
for row in client.query(query, job_config=job_config).result()
|
||||
]
|
||||
|
||||
|
||||
@@ -155,6 +176,9 @@ def collect(
|
||||
omits fields from the manifest. Use this for periodic hourly pushes
|
||||
after the initial full metadata push.
|
||||
"""
|
||||
project_id = _require_bq_identifier(project_id, "project_id")
|
||||
datasets = [_require_bq_identifier(d, "dataset") for d in datasets or []] or None
|
||||
tables = [_require_bq_identifier(t, "table") for t in tables or []] or None
|
||||
client = bigquery.Client(project=project_id) # ← SUBSTITUTE: adjust auth if needed
|
||||
|
||||
if only_freshness_and_volume:
|
||||
@@ -200,8 +224,7 @@ def collect(
|
||||
"collected_at": datetime.now(timezone.utc).isoformat(),
|
||||
"assets": assets,
|
||||
}
|
||||
with open(output_file, "w") as fh:
|
||||
json.dump(manifest, fh, indent=2)
|
||||
write_json_file(output_file, manifest)
|
||||
log.info("Manifest written to %s (%d assets)", output_file, len(assets))
|
||||
|
||||
return manifest
|
||||
|
||||
+2
-2
@@ -23,6 +23,7 @@ import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from google.cloud import bigquery
|
||||
from _safe_paths import safe_existing_directory, safe_input_json_path, safe_output_json_path, write_json_file
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -113,8 +114,7 @@ def collect(
|
||||
"query_log_count": len(entries),
|
||||
"queries": entries,
|
||||
}
|
||||
with open(output_file, "w") as fh:
|
||||
json.dump(manifest, fh, indent=2)
|
||||
write_json_file(output_file, manifest)
|
||||
log.info("Query log manifest written to %s", output_file)
|
||||
|
||||
return manifest
|
||||
|
||||
+3
-4
@@ -33,6 +33,7 @@ from pycarlo.features.ingestion.models import (
|
||||
AssetVolume,
|
||||
RelationalAsset,
|
||||
)
|
||||
from _safe_paths import safe_existing_directory, safe_input_json_path, safe_output_json_path, read_json_file, write_json_file
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -92,8 +93,7 @@ def push(
|
||||
"""Read a metadata manifest and push assets to Monte Carlo in batches."""
|
||||
endpoint = _ENDPOINT
|
||||
log.info("Using endpoint: %s", endpoint)
|
||||
with open(input_file) as fh:
|
||||
manifest = json.load(fh)
|
||||
manifest = read_json_file(input_file)
|
||||
|
||||
asset_dicts = manifest.get("assets", [])
|
||||
resource_type = manifest.get("resource_type", RESOURCE_TYPE)
|
||||
@@ -147,8 +147,7 @@ def push(
|
||||
"batch_count": total_batches,
|
||||
"batch_size": batch_size,
|
||||
}
|
||||
with open(output_file, "w") as fh:
|
||||
json.dump(push_result, fh, indent=2)
|
||||
write_json_file(output_file, push_result)
|
||||
log.info("Push result written to %s", output_file)
|
||||
|
||||
return push_result
|
||||
|
||||
+4
-6
@@ -32,6 +32,7 @@ from dateutil.parser import isoparse
|
||||
from pycarlo.core import Client, Session
|
||||
from pycarlo.features.ingestion import IngestionService
|
||||
from pycarlo.features.ingestion.models import QueryLogEntry
|
||||
from _safe_paths import safe_existing_directory, safe_input_json_path, safe_output_json_path, read_json_file, write_json_file
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -95,8 +96,7 @@ def push(
|
||||
endpoint = _ENDPOINT
|
||||
log.info("Using endpoint: %s", endpoint)
|
||||
|
||||
with open(input_file) as fh:
|
||||
manifest = json.load(fh)
|
||||
manifest = read_json_file(input_file)
|
||||
|
||||
queries = manifest.get("queries", [])
|
||||
log_type = manifest.get("log_type", LOG_TYPE)
|
||||
@@ -114,8 +114,7 @@ def push(
|
||||
"batch_count": 0,
|
||||
"batch_size": batch_size,
|
||||
}
|
||||
with open(output_file, "w") as fh:
|
||||
json.dump(push_result, fh, indent=2)
|
||||
write_json_file(output_file, push_result)
|
||||
return push_result
|
||||
|
||||
batches = [entries[i : i + batch_size] for i in range(0, len(entries), batch_size)]
|
||||
@@ -165,8 +164,7 @@ def push(
|
||||
"batch_count": total_batches,
|
||||
"batch_size": batch_size,
|
||||
}
|
||||
with open(output_file, "w") as fh:
|
||||
json.dump(push_result, fh, indent=2)
|
||||
write_json_file(output_file, push_result)
|
||||
log.info("Push result written to %s", output_file)
|
||||
|
||||
return push_result
|
||||
|
||||
Reference in New Issue
Block a user