📦 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)
|
||||
+7
-3
@@ -40,6 +40,7 @@ import os
|
||||
|
||||
from collect_lineage import collect, _LOOKBACK_HOURS
|
||||
from push_lineage import push, _BATCH_SIZE
|
||||
from _safe_paths import safe_output_json_path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@@ -126,6 +127,9 @@ def main() -> None:
|
||||
if missing:
|
||||
parser.error(f"Missing required arguments: {', '.join(missing)}")
|
||||
|
||||
output_path = str(safe_output_json_path(args.output_file))
|
||||
push_result_path = str(safe_output_json_path(args.push_result_file))
|
||||
|
||||
# Step 1: Collect
|
||||
collect(
|
||||
account=args.account,
|
||||
@@ -134,17 +138,17 @@ def main() -> None:
|
||||
warehouse=args.warehouse,
|
||||
lookback_hours=args.lookback_hours,
|
||||
column_lineage=args.column_lineage,
|
||||
output_file=args.output_file,
|
||||
output_file=output_path,
|
||||
)
|
||||
|
||||
# Step 2: Push
|
||||
push(
|
||||
input_file=args.output_file,
|
||||
input_file=output_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,
|
||||
)
|
||||
|
||||
print("Done.")
|
||||
|
||||
+10
-4
@@ -34,8 +34,9 @@ Usage
|
||||
import argparse
|
||||
import os
|
||||
|
||||
from collect_metadata import collect
|
||||
from collect_metadata import _quote_identifier, collect
|
||||
from push_metadata import push, _BATCH_SIZE
|
||||
from _safe_paths import safe_output_json_path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@@ -111,23 +112,28 @@ def main() -> None:
|
||||
if missing:
|
||||
parser.error(f"Missing required arguments: {', '.join(missing)}")
|
||||
|
||||
output_path = str(safe_output_json_path(args.output_file))
|
||||
push_result_path = str(safe_output_json_path(args.push_result_file))
|
||||
|
||||
_quote_identifier(args.warehouse)
|
||||
|
||||
# Step 1: Collect
|
||||
collect(
|
||||
account=args.account,
|
||||
user=args.user,
|
||||
password=args.password,
|
||||
warehouse=args.warehouse,
|
||||
output_file=args.output_file,
|
||||
output_file=output_path,
|
||||
)
|
||||
|
||||
# Step 2: Push
|
||||
push(
|
||||
input_file=args.output_file,
|
||||
input_file=output_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,
|
||||
)
|
||||
|
||||
print("Done.")
|
||||
|
||||
+7
-3
@@ -36,6 +36,7 @@ import os
|
||||
|
||||
from collect_query_logs import collect
|
||||
from push_query_logs import push, _BATCH_SIZE
|
||||
from _safe_paths import safe_output_json_path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@@ -111,23 +112,26 @@ def main() -> None:
|
||||
if missing:
|
||||
parser.error(f"Missing required arguments: {', '.join(missing)}")
|
||||
|
||||
output_path = str(safe_output_json_path(args.output_file))
|
||||
push_result_path = str(safe_output_json_path(args.push_result_file))
|
||||
|
||||
# Step 1: Collect
|
||||
collect(
|
||||
account=args.account,
|
||||
user=args.user,
|
||||
password=args.password,
|
||||
warehouse=args.warehouse,
|
||||
output_file=args.output_file,
|
||||
output_file=output_path,
|
||||
)
|
||||
|
||||
# Step 2: Push
|
||||
push(
|
||||
input_file=args.output_file,
|
||||
input_file=output_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,
|
||||
)
|
||||
|
||||
print("Done.")
|
||||
|
||||
+16
-7
@@ -43,6 +43,7 @@ from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import snowflake.connector
|
||||
from _safe_paths import safe_existing_directory, safe_input_json_path, safe_output_json_path, write_json_file
|
||||
|
||||
# ← SUBSTITUTE: set RESOURCE_TYPE to match your Monte Carlo connection type
|
||||
RESOURCE_TYPE = "snowflake"
|
||||
@@ -70,6 +71,13 @@ def _check_available_memory(min_gb: float = 2.0) -> None:
|
||||
# ← SUBSTITUTE: adjust the lookback window to match your collection cadence
|
||||
_LOOKBACK_HOURS = 24
|
||||
|
||||
|
||||
def _bounded_int(value: int, field: str, *, minimum: int, maximum: int) -> int:
|
||||
value = int(value)
|
||||
if value < minimum or value > maximum:
|
||||
raise ValueError(f"{field} must be between {minimum} and {maximum}")
|
||||
return value
|
||||
|
||||
# Regex for CTAS: CREATE [OR REPLACE] [TRANSIENT] TABLE [IF NOT EXISTS] [db.][schema.]table AS SELECT
|
||||
_CTAS_RE = re.compile(
|
||||
r"CREATE\s+(?:OR\s+REPLACE\s+)?(?:TRANSIENT\s+)?TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?"
|
||||
@@ -181,17 +189,19 @@ def _parse_edges(rows: list[dict]) -> list[_LineageEdge]:
|
||||
|
||||
|
||||
def _fetch_query_history(conn, lookback_hours: int) -> list[dict]:
|
||||
lookback_hours = _bounded_int(lookback_hours, "lookback_hours", minimum=1, maximum=24 * 31)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
f"""
|
||||
"""
|
||||
SELECT QUERY_ID, QUERY_TEXT, START_TIME, END_TIME, USER_NAME, DATABASE_NAME, EXECUTION_STATUS
|
||||
FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY
|
||||
WHERE START_TIME >= DATEADD(hour, -{lookback_hours}, CURRENT_TIMESTAMP())
|
||||
WHERE START_TIME >= DATEADD(hour, -%s, CURRENT_TIMESTAMP())
|
||||
AND EXECUTION_STATUS = 'SUCCESS'
|
||||
AND QUERY_TYPE IN ('CREATE_TABLE_AS_SELECT', 'INSERT', 'MERGE', 'CREATE_VIEW')
|
||||
ORDER BY START_TIME
|
||||
LIMIT 50000
|
||||
"""
|
||||
""",
|
||||
(lookback_hours,),
|
||||
# ← SUBSTITUTE: adjust QUERY_TYPE list, LIMIT, or add a WHERE clause to scope to specific databases
|
||||
)
|
||||
columns = [col[0] for col in cursor.description]
|
||||
@@ -220,6 +230,7 @@ def collect(
|
||||
Returns the manifest dict.
|
||||
"""
|
||||
_check_available_memory()
|
||||
lookback_hours = _bounded_int(lookback_hours, "lookback_hours", minimum=1, maximum=24 * 31)
|
||||
print(f"Connecting to Snowflake account: {account} ...")
|
||||
conn = snowflake.connector.connect(
|
||||
account=account,
|
||||
@@ -241,8 +252,7 @@ def collect(
|
||||
"column_lineage": column_lineage,
|
||||
"edges": [],
|
||||
}
|
||||
with open(output_file, "w") as fh:
|
||||
json.dump(manifest, fh, indent=2)
|
||||
write_json_file(output_file, manifest)
|
||||
return manifest
|
||||
|
||||
edges = _parse_edges(rows)
|
||||
@@ -271,8 +281,7 @@ def collect(
|
||||
for e in edges
|
||||
],
|
||||
}
|
||||
with open(output_file, "w") as fh:
|
||||
json.dump(manifest, fh, indent=2)
|
||||
write_json_file(output_file, manifest)
|
||||
print(f"Lineage manifest written to {output_file}")
|
||||
|
||||
return manifest
|
||||
|
||||
+15
-7
@@ -35,6 +35,7 @@ import os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import snowflake.connector
|
||||
from _safe_paths import safe_existing_directory, safe_input_json_path, safe_output_json_path, write_json_file
|
||||
|
||||
# ← SUBSTITUTE: set RESOURCE_TYPE to match your Monte Carlo connection type
|
||||
RESOURCE_TYPE = "snowflake"
|
||||
@@ -78,6 +79,13 @@ _TABLE_TYPE_MAP = {
|
||||
}
|
||||
|
||||
|
||||
def _quote_identifier(identifier: str) -> str:
|
||||
value = str(identifier).strip()
|
||||
if not value:
|
||||
raise ValueError("Identifier must not be empty")
|
||||
return '"' + value.replace('"', '""') + '"'
|
||||
|
||||
|
||||
def _normalize_table_type(raw_type: str | None) -> str:
|
||||
"""Map Snowflake's TABLE_TYPE value to MC-accepted 'TABLE' or 'VIEW'."""
|
||||
if not raw_type:
|
||||
@@ -115,7 +123,7 @@ def _collect_assets(conn) -> list[dict]:
|
||||
for db in databases:
|
||||
# --- Discover schemas in each database ---
|
||||
try:
|
||||
cursor.execute(f'SHOW SCHEMAS IN DATABASE "{db}"')
|
||||
cursor.execute("SHOW SCHEMAS IN DATABASE IDENTIFIER(%s)", (db,))
|
||||
except Exception as exc:
|
||||
print(f" WARNING: could not list schemas in {db}: {exc}")
|
||||
continue
|
||||
@@ -142,10 +150,11 @@ def _collect_assets(conn) -> list[dict]:
|
||||
BYTES,
|
||||
LAST_ALTERED,
|
||||
COMMENT
|
||||
FROM "{db}".INFORMATION_SCHEMA.TABLES
|
||||
FROM IDENTIFIER(%s)
|
||||
WHERE TABLE_SCHEMA != 'INFORMATION_SCHEMA'
|
||||
ORDER BY TABLE_SCHEMA, TABLE_NAME
|
||||
"""
|
||||
""",
|
||||
(f"{db}.INFORMATION_SCHEMA.TABLES",),
|
||||
)
|
||||
except Exception as exc:
|
||||
print(f" WARNING: could not query INFORMATION_SCHEMA.TABLES in {db}: {exc}")
|
||||
@@ -172,11 +181,11 @@ def _collect_assets(conn) -> list[dict]:
|
||||
cursor.execute(
|
||||
f"""
|
||||
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, COMMENT
|
||||
FROM "{db}".INFORMATION_SCHEMA.COLUMNS
|
||||
FROM IDENTIFIER(%s)
|
||||
WHERE TABLE_SCHEMA = %s
|
||||
ORDER BY TABLE_NAME, ORDINAL_POSITION
|
||||
""",
|
||||
(schema,),
|
||||
(f"{db}.INFORMATION_SCHEMA.COLUMNS", schema),
|
||||
)
|
||||
except Exception as exc:
|
||||
print(f" WARNING: could not fetch columns for {db}.{schema}: {exc}")
|
||||
@@ -264,8 +273,7 @@ def collect(
|
||||
"collected_at": datetime.now(tz=timezone.utc).isoformat(),
|
||||
"assets": assets,
|
||||
}
|
||||
with open(output_file, "w") as fh:
|
||||
json.dump(manifest, fh, indent=2)
|
||||
write_json_file(output_file, manifest)
|
||||
print(f"Asset manifest written to {output_file}")
|
||||
|
||||
return manifest
|
||||
|
||||
+3
-4
@@ -35,6 +35,7 @@ import os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import snowflake.connector
|
||||
from _safe_paths import safe_existing_directory, safe_input_json_path, safe_output_json_path, write_json_file
|
||||
|
||||
# ← SUBSTITUTE: set LOG_TYPE to match your warehouse type (query logs use log_type, not resource_type)
|
||||
LOG_TYPE = "snowflake"
|
||||
@@ -162,8 +163,7 @@ def collect(
|
||||
"window_end": None,
|
||||
"queries": [],
|
||||
}
|
||||
with open(output_file, "w") as fh:
|
||||
json.dump(manifest, fh, indent=2, default=str)
|
||||
write_json_file(output_file, manifest, default=str)
|
||||
return manifest
|
||||
|
||||
start_times = [r["START_TIME"] for r in rows if r.get("START_TIME") is not None]
|
||||
@@ -189,8 +189,7 @@ def collect(
|
||||
for r in rows
|
||||
],
|
||||
}
|
||||
with open(output_file, "w") as fh:
|
||||
json.dump(manifest, fh, indent=2, default=str)
|
||||
write_json_file(output_file, manifest, default=str)
|
||||
print(f"Query log manifest written to {output_file}")
|
||||
|
||||
return manifest
|
||||
|
||||
+4
-6
@@ -43,6 +43,7 @@ from pycarlo.features.ingestion.models import (
|
||||
LineageAssetRef,
|
||||
LineageEvent,
|
||||
)
|
||||
from _safe_paths import safe_existing_directory, safe_input_json_path, safe_output_json_path, read_json_file, write_json_file
|
||||
|
||||
# ← SUBSTITUTE: set RESOURCE_TYPE to match your Monte Carlo connection type
|
||||
RESOURCE_TYPE = "snowflake"
|
||||
@@ -155,8 +156,7 @@ def push(
|
||||
|
||||
Returns a result dict with invocation IDs for each batch.
|
||||
"""
|
||||
with open(input_file) as fh:
|
||||
manifest = json.load(fh)
|
||||
manifest = read_json_file(input_file)
|
||||
|
||||
edges = manifest.get("edges", [])
|
||||
resource_type = manifest.get("resource_type", RESOURCE_TYPE)
|
||||
@@ -182,8 +182,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
|
||||
|
||||
# Split into batches
|
||||
@@ -236,8 +235,7 @@ def push(
|
||||
"batch_size": batch_size,
|
||||
"edges": edges, # preserve for downstream validation
|
||||
}
|
||||
with open(output_file, "w") as fh:
|
||||
json.dump(push_result, fh, indent=2)
|
||||
write_json_file(output_file, push_result)
|
||||
print(f"Push result written to {output_file}")
|
||||
|
||||
return push_result
|
||||
|
||||
+3
-4
@@ -42,6 +42,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
|
||||
|
||||
# ← SUBSTITUTE: set RESOURCE_TYPE to match your Monte Carlo connection type
|
||||
RESOURCE_TYPE = "snowflake"
|
||||
@@ -102,8 +103,7 @@ def push(
|
||||
|
||||
Returns a result dict with invocation IDs for each batch.
|
||||
"""
|
||||
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)
|
||||
@@ -157,8 +157,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)
|
||||
print(f"Push result written to {output_file}")
|
||||
|
||||
return push_result
|
||||
|
||||
+4
-6
@@ -37,6 +37,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
|
||||
|
||||
# ← SUBSTITUTE: set LOG_TYPE to match your warehouse type (query logs use log_type, not resource_type)
|
||||
LOG_TYPE = "snowflake"
|
||||
@@ -107,8 +108,7 @@ def push(
|
||||
|
||||
Returns a result dict with invocation IDs for each batch.
|
||||
"""
|
||||
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)
|
||||
@@ -126,8 +126,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
|
||||
|
||||
# Split into batches
|
||||
@@ -177,8 +176,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)
|
||||
print(f"Push result written to {output_file}")
|
||||
|
||||
return push_result
|
||||
|
||||
Reference in New Issue
Block a user