dvt-core 0.58.6__cp311-cp311-macosx_10_9_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dbt/__init__.py +7 -0
- dbt/_pydantic_shim.py +26 -0
- dbt/artifacts/__init__.py +0 -0
- dbt/artifacts/exceptions/__init__.py +1 -0
- dbt/artifacts/exceptions/schemas.py +31 -0
- dbt/artifacts/resources/__init__.py +116 -0
- dbt/artifacts/resources/base.py +67 -0
- dbt/artifacts/resources/types.py +93 -0
- dbt/artifacts/resources/v1/analysis.py +10 -0
- dbt/artifacts/resources/v1/catalog.py +23 -0
- dbt/artifacts/resources/v1/components.py +274 -0
- dbt/artifacts/resources/v1/config.py +277 -0
- dbt/artifacts/resources/v1/documentation.py +11 -0
- dbt/artifacts/resources/v1/exposure.py +51 -0
- dbt/artifacts/resources/v1/function.py +52 -0
- dbt/artifacts/resources/v1/generic_test.py +31 -0
- dbt/artifacts/resources/v1/group.py +21 -0
- dbt/artifacts/resources/v1/hook.py +11 -0
- dbt/artifacts/resources/v1/macro.py +29 -0
- dbt/artifacts/resources/v1/metric.py +172 -0
- dbt/artifacts/resources/v1/model.py +145 -0
- dbt/artifacts/resources/v1/owner.py +10 -0
- dbt/artifacts/resources/v1/saved_query.py +111 -0
- dbt/artifacts/resources/v1/seed.py +41 -0
- dbt/artifacts/resources/v1/semantic_layer_components.py +72 -0
- dbt/artifacts/resources/v1/semantic_model.py +314 -0
- dbt/artifacts/resources/v1/singular_test.py +14 -0
- dbt/artifacts/resources/v1/snapshot.py +91 -0
- dbt/artifacts/resources/v1/source_definition.py +84 -0
- dbt/artifacts/resources/v1/sql_operation.py +10 -0
- dbt/artifacts/resources/v1/unit_test_definition.py +77 -0
- dbt/artifacts/schemas/__init__.py +0 -0
- dbt/artifacts/schemas/base.py +191 -0
- dbt/artifacts/schemas/batch_results.py +24 -0
- dbt/artifacts/schemas/catalog/__init__.py +11 -0
- dbt/artifacts/schemas/catalog/v1/__init__.py +0 -0
- dbt/artifacts/schemas/catalog/v1/catalog.py +59 -0
- dbt/artifacts/schemas/freshness/__init__.py +1 -0
- dbt/artifacts/schemas/freshness/v3/__init__.py +0 -0
- dbt/artifacts/schemas/freshness/v3/freshness.py +158 -0
- dbt/artifacts/schemas/manifest/__init__.py +2 -0
- dbt/artifacts/schemas/manifest/v12/__init__.py +0 -0
- dbt/artifacts/schemas/manifest/v12/manifest.py +211 -0
- dbt/artifacts/schemas/results.py +147 -0
- dbt/artifacts/schemas/run/__init__.py +2 -0
- dbt/artifacts/schemas/run/v5/__init__.py +0 -0
- dbt/artifacts/schemas/run/v5/run.py +184 -0
- dbt/artifacts/schemas/upgrades/__init__.py +4 -0
- dbt/artifacts/schemas/upgrades/upgrade_manifest.py +174 -0
- dbt/artifacts/schemas/upgrades/upgrade_manifest_dbt_version.py +2 -0
- dbt/artifacts/utils/validation.py +153 -0
- dbt/cli/__init__.py +1 -0
- dbt/cli/context.py +17 -0
- dbt/cli/exceptions.py +57 -0
- dbt/cli/flags.py +560 -0
- dbt/cli/main.py +2403 -0
- dbt/cli/option_types.py +121 -0
- dbt/cli/options.py +80 -0
- dbt/cli/params.py +844 -0
- dbt/cli/requires.py +490 -0
- dbt/cli/resolvers.py +50 -0
- dbt/cli/types.py +40 -0
- dbt/clients/__init__.py +0 -0
- dbt/clients/checked_load.py +83 -0
- dbt/clients/git.py +164 -0
- dbt/clients/jinja.py +206 -0
- dbt/clients/jinja_static.py +245 -0
- dbt/clients/registry.py +192 -0
- dbt/clients/yaml_helper.py +68 -0
- dbt/compilation.py +876 -0
- dbt/compute/__init__.py +14 -0
- dbt/compute/engines/__init__.py +12 -0
- dbt/compute/engines/spark_engine.cpython-311-darwin.so +0 -0
- dbt/compute/engines/spark_engine.py +642 -0
- dbt/compute/federated_executor.cpython-311-darwin.so +0 -0
- dbt/compute/federated_executor.py +1080 -0
- dbt/compute/filter_pushdown.cpython-311-darwin.so +0 -0
- dbt/compute/filter_pushdown.py +273 -0
- dbt/compute/jar_provisioning.cpython-311-darwin.so +0 -0
- dbt/compute/jar_provisioning.py +255 -0
- dbt/compute/java_compat.cpython-311-darwin.so +0 -0
- dbt/compute/java_compat.py +689 -0
- dbt/compute/jdbc_utils.cpython-311-darwin.so +0 -0
- dbt/compute/jdbc_utils.py +678 -0
- dbt/compute/metadata/__init__.py +40 -0
- dbt/compute/metadata/adapters_registry.cpython-311-darwin.so +0 -0
- dbt/compute/metadata/adapters_registry.py +370 -0
- dbt/compute/metadata/registry.cpython-311-darwin.so +0 -0
- dbt/compute/metadata/registry.py +674 -0
- dbt/compute/metadata/store.cpython-311-darwin.so +0 -0
- dbt/compute/metadata/store.py +1499 -0
- dbt/compute/smart_selector.cpython-311-darwin.so +0 -0
- dbt/compute/smart_selector.py +377 -0
- dbt/compute/strategies/__init__.py +55 -0
- dbt/compute/strategies/base.cpython-311-darwin.so +0 -0
- dbt/compute/strategies/base.py +165 -0
- dbt/compute/strategies/dataproc.cpython-311-darwin.so +0 -0
- dbt/compute/strategies/dataproc.py +207 -0
- dbt/compute/strategies/emr.cpython-311-darwin.so +0 -0
- dbt/compute/strategies/emr.py +203 -0
- dbt/compute/strategies/local.cpython-311-darwin.so +0 -0
- dbt/compute/strategies/local.py +443 -0
- dbt/compute/strategies/standalone.cpython-311-darwin.so +0 -0
- dbt/compute/strategies/standalone.py +262 -0
- dbt/config/__init__.py +4 -0
- dbt/config/catalogs.py +94 -0
- dbt/config/compute.cpython-311-darwin.so +0 -0
- dbt/config/compute.py +513 -0
- dbt/config/dvt_profile.cpython-311-darwin.so +0 -0
- dbt/config/dvt_profile.py +342 -0
- dbt/config/profile.py +422 -0
- dbt/config/project.py +873 -0
- dbt/config/project_utils.py +28 -0
- dbt/config/renderer.py +231 -0
- dbt/config/runtime.py +553 -0
- dbt/config/selectors.py +208 -0
- dbt/config/utils.py +77 -0
- dbt/constants.py +28 -0
- dbt/context/__init__.py +0 -0
- dbt/context/base.py +745 -0
- dbt/context/configured.py +135 -0
- dbt/context/context_config.py +382 -0
- dbt/context/docs.py +82 -0
- dbt/context/exceptions_jinja.py +178 -0
- dbt/context/macro_resolver.py +195 -0
- dbt/context/macros.py +171 -0
- dbt/context/manifest.py +72 -0
- dbt/context/providers.py +2249 -0
- dbt/context/query_header.py +13 -0
- dbt/context/secret.py +58 -0
- dbt/context/target.py +74 -0
- dbt/contracts/__init__.py +0 -0
- dbt/contracts/files.py +413 -0
- dbt/contracts/graph/__init__.py +0 -0
- dbt/contracts/graph/manifest.py +1904 -0
- dbt/contracts/graph/metrics.py +97 -0
- dbt/contracts/graph/model_config.py +70 -0
- dbt/contracts/graph/node_args.py +42 -0
- dbt/contracts/graph/nodes.py +1806 -0
- dbt/contracts/graph/semantic_manifest.py +232 -0
- dbt/contracts/graph/unparsed.py +811 -0
- dbt/contracts/project.py +417 -0
- dbt/contracts/results.py +53 -0
- dbt/contracts/selection.py +23 -0
- dbt/contracts/sql.py +85 -0
- dbt/contracts/state.py +68 -0
- dbt/contracts/util.py +46 -0
- dbt/deprecations.py +348 -0
- dbt/deps/__init__.py +0 -0
- dbt/deps/base.py +152 -0
- dbt/deps/git.py +195 -0
- dbt/deps/local.py +79 -0
- dbt/deps/registry.py +130 -0
- dbt/deps/resolver.py +149 -0
- dbt/deps/tarball.py +120 -0
- dbt/docs/source/_ext/dbt_click.py +119 -0
- dbt/docs/source/conf.py +32 -0
- dbt/env_vars.py +64 -0
- dbt/event_time/event_time.py +40 -0
- dbt/event_time/sample_window.py +60 -0
- dbt/events/__init__.py +15 -0
- dbt/events/base_types.py +36 -0
- dbt/events/core_types_pb2.py +2 -0
- dbt/events/logging.py +108 -0
- dbt/events/types.py +2516 -0
- dbt/exceptions.py +1486 -0
- dbt/flags.py +89 -0
- dbt/graph/__init__.py +11 -0
- dbt/graph/cli.py +249 -0
- dbt/graph/graph.py +172 -0
- dbt/graph/queue.py +214 -0
- dbt/graph/selector.py +374 -0
- dbt/graph/selector_methods.py +975 -0
- dbt/graph/selector_spec.py +222 -0
- dbt/graph/thread_pool.py +18 -0
- dbt/hooks.py +21 -0
- dbt/include/README.md +49 -0
- dbt/include/__init__.py +3 -0
- dbt/include/data/adapters_registry.duckdb +0 -0
- dbt/include/data/build_registry.py +242 -0
- dbt/include/data/csv/adapter_queries.csv +33 -0
- dbt/include/data/csv/syntax_rules.csv +9 -0
- dbt/include/data/csv/type_mappings_bigquery.csv +28 -0
- dbt/include/data/csv/type_mappings_databricks.csv +30 -0
- dbt/include/data/csv/type_mappings_mysql.csv +40 -0
- dbt/include/data/csv/type_mappings_oracle.csv +30 -0
- dbt/include/data/csv/type_mappings_postgres.csv +56 -0
- dbt/include/data/csv/type_mappings_redshift.csv +33 -0
- dbt/include/data/csv/type_mappings_snowflake.csv +38 -0
- dbt/include/data/csv/type_mappings_sqlserver.csv +35 -0
- dbt/include/starter_project/.gitignore +4 -0
- dbt/include/starter_project/README.md +15 -0
- dbt/include/starter_project/__init__.py +3 -0
- dbt/include/starter_project/analyses/.gitkeep +0 -0
- dbt/include/starter_project/dbt_project.yml +36 -0
- dbt/include/starter_project/macros/.gitkeep +0 -0
- dbt/include/starter_project/models/example/my_first_dbt_model.sql +27 -0
- dbt/include/starter_project/models/example/my_second_dbt_model.sql +6 -0
- dbt/include/starter_project/models/example/schema.yml +21 -0
- dbt/include/starter_project/seeds/.gitkeep +0 -0
- dbt/include/starter_project/snapshots/.gitkeep +0 -0
- dbt/include/starter_project/tests/.gitkeep +0 -0
- dbt/internal_deprecations.py +26 -0
- dbt/jsonschemas/__init__.py +3 -0
- dbt/jsonschemas/jsonschemas.py +309 -0
- dbt/jsonschemas/project/0.0.110.json +4717 -0
- dbt/jsonschemas/project/0.0.85.json +2015 -0
- dbt/jsonschemas/resources/0.0.110.json +2636 -0
- dbt/jsonschemas/resources/0.0.85.json +2536 -0
- dbt/jsonschemas/resources/latest.json +6773 -0
- dbt/links.py +4 -0
- dbt/materializations/__init__.py +0 -0
- dbt/materializations/incremental/__init__.py +0 -0
- dbt/materializations/incremental/microbatch.py +236 -0
- dbt/mp_context.py +8 -0
- dbt/node_types.py +37 -0
- dbt/parser/__init__.py +23 -0
- dbt/parser/analysis.py +21 -0
- dbt/parser/base.py +548 -0
- dbt/parser/common.py +266 -0
- dbt/parser/docs.py +52 -0
- dbt/parser/fixtures.py +51 -0
- dbt/parser/functions.py +30 -0
- dbt/parser/generic_test.py +100 -0
- dbt/parser/generic_test_builders.py +333 -0
- dbt/parser/hooks.py +118 -0
- dbt/parser/macros.py +137 -0
- dbt/parser/manifest.py +2204 -0
- dbt/parser/models.py +573 -0
- dbt/parser/partial.py +1178 -0
- dbt/parser/read_files.py +445 -0
- dbt/parser/schema_generic_tests.py +422 -0
- dbt/parser/schema_renderer.py +111 -0
- dbt/parser/schema_yaml_readers.py +935 -0
- dbt/parser/schemas.py +1466 -0
- dbt/parser/search.py +149 -0
- dbt/parser/seeds.py +28 -0
- dbt/parser/singular_test.py +20 -0
- dbt/parser/snapshots.py +44 -0
- dbt/parser/sources.py +558 -0
- dbt/parser/sql.py +62 -0
- dbt/parser/unit_tests.py +621 -0
- dbt/plugins/__init__.py +20 -0
- dbt/plugins/contracts.py +9 -0
- dbt/plugins/exceptions.py +2 -0
- dbt/plugins/manager.py +163 -0
- dbt/plugins/manifest.py +21 -0
- dbt/profiler.py +20 -0
- dbt/py.typed +1 -0
- dbt/query_analyzer.cpython-311-darwin.so +0 -0
- dbt/query_analyzer.py +410 -0
- dbt/runners/__init__.py +2 -0
- dbt/runners/exposure_runner.py +7 -0
- dbt/runners/no_op_runner.py +45 -0
- dbt/runners/saved_query_runner.py +7 -0
- dbt/selected_resources.py +8 -0
- dbt/task/__init__.py +0 -0
- dbt/task/base.py +503 -0
- dbt/task/build.py +197 -0
- dbt/task/clean.py +56 -0
- dbt/task/clone.py +161 -0
- dbt/task/compile.py +150 -0
- dbt/task/compute.cpython-311-darwin.so +0 -0
- dbt/task/compute.py +458 -0
- dbt/task/debug.py +505 -0
- dbt/task/deps.py +280 -0
- dbt/task/docs/__init__.py +3 -0
- dbt/task/docs/api/__init__.py +23 -0
- dbt/task/docs/api/catalog.cpython-311-darwin.so +0 -0
- dbt/task/docs/api/catalog.py +204 -0
- dbt/task/docs/api/lineage.cpython-311-darwin.so +0 -0
- dbt/task/docs/api/lineage.py +234 -0
- dbt/task/docs/api/profile.cpython-311-darwin.so +0 -0
- dbt/task/docs/api/profile.py +204 -0
- dbt/task/docs/api/spark.cpython-311-darwin.so +0 -0
- dbt/task/docs/api/spark.py +186 -0
- dbt/task/docs/generate.py +947 -0
- dbt/task/docs/index.html +250 -0
- dbt/task/docs/serve.cpython-311-darwin.so +0 -0
- dbt/task/docs/serve.py +174 -0
- dbt/task/dvt_output.py +362 -0
- dbt/task/dvt_run.py +204 -0
- dbt/task/freshness.py +322 -0
- dbt/task/function.py +121 -0
- dbt/task/group_lookup.py +46 -0
- dbt/task/init.cpython-311-darwin.so +0 -0
- dbt/task/init.py +604 -0
- dbt/task/java.cpython-311-darwin.so +0 -0
- dbt/task/java.py +316 -0
- dbt/task/list.py +236 -0
- dbt/task/metadata.cpython-311-darwin.so +0 -0
- dbt/task/metadata.py +804 -0
- dbt/task/printer.py +175 -0
- dbt/task/profile.cpython-311-darwin.so +0 -0
- dbt/task/profile.py +1307 -0
- dbt/task/profile_serve.py +615 -0
- dbt/task/retract.py +438 -0
- dbt/task/retry.py +175 -0
- dbt/task/run.py +1387 -0
- dbt/task/run_operation.py +141 -0
- dbt/task/runnable.py +758 -0
- dbt/task/seed.py +103 -0
- dbt/task/show.py +149 -0
- dbt/task/snapshot.py +56 -0
- dbt/task/spark.cpython-311-darwin.so +0 -0
- dbt/task/spark.py +414 -0
- dbt/task/sql.py +110 -0
- dbt/task/target_sync.cpython-311-darwin.so +0 -0
- dbt/task/target_sync.py +766 -0
- dbt/task/test.py +464 -0
- dbt/tests/fixtures/__init__.py +1 -0
- dbt/tests/fixtures/project.py +620 -0
- dbt/tests/util.py +651 -0
- dbt/tracking.py +529 -0
- dbt/utils/__init__.py +3 -0
- dbt/utils/artifact_upload.py +151 -0
- dbt/utils/utils.py +408 -0
- dbt/version.py +270 -0
- dvt_cli/__init__.py +72 -0
- dvt_core-0.58.6.dist-info/METADATA +288 -0
- dvt_core-0.58.6.dist-info/RECORD +324 -0
- dvt_core-0.58.6.dist-info/WHEEL +5 -0
- dvt_core-0.58.6.dist-info/entry_points.txt +2 -0
- dvt_core-0.58.6.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# DVT Profile Serve - Web UI for Profiling Results
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Serves a beautiful web interface to view profiling results stored in
|
|
5
|
+
# metadata_store.duckdb, similar to PipeRider's report viewer.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# dvt profile serve # Start server on http://localhost:8580
|
|
9
|
+
# dvt profile serve --port 9000 # Custom port
|
|
10
|
+
# dvt profile serve --no-browser # Don't auto-open browser
|
|
11
|
+
#
|
|
12
|
+
# Installation:
|
|
13
|
+
# Copy this file to: core/dbt/task/profile_serve.py
|
|
14
|
+
#
|
|
15
|
+
# DVT v0.58.0: New web UI for profiling results
|
|
16
|
+
# =============================================================================
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import threading
|
|
22
|
+
import webbrowser
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Dict, List, Optional
|
|
27
|
+
from urllib.parse import parse_qs, urlparse
|
|
28
|
+
|
|
29
|
+
# Try to import Rich for CLI output
|
|
30
|
+
try:
|
|
31
|
+
from rich.console import Console
|
|
32
|
+
from rich.panel import Panel
|
|
33
|
+
from rich import box
|
|
34
|
+
console = Console()
|
|
35
|
+
HAS_RICH = True
|
|
36
|
+
except ImportError:
|
|
37
|
+
HAS_RICH = False
|
|
38
|
+
console = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ProfileAPIHandler(SimpleHTTPRequestHandler):
|
|
42
|
+
"""HTTP handler for the Profile Viewer API and static files."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, *args, metadata_store_path: Path = None, **kwargs):
|
|
45
|
+
self.metadata_store_path = metadata_store_path
|
|
46
|
+
super().__init__(*args, **kwargs)
|
|
47
|
+
|
|
48
|
+
def do_GET(self):
|
|
49
|
+
"""Handle GET requests."""
|
|
50
|
+
parsed = urlparse(self.path)
|
|
51
|
+
path = parsed.path
|
|
52
|
+
|
|
53
|
+
# API endpoints
|
|
54
|
+
if path == "/api/profiles":
|
|
55
|
+
self._serve_profiles_list()
|
|
56
|
+
elif path == "/api/profile":
|
|
57
|
+
query = parse_qs(parsed.query)
|
|
58
|
+
table_name = query.get("table", [None])[0]
|
|
59
|
+
self._serve_profile_detail(table_name)
|
|
60
|
+
elif path == "/api/summary":
|
|
61
|
+
self._serve_summary()
|
|
62
|
+
elif path == "/" or path == "/index.html":
|
|
63
|
+
self._serve_html()
|
|
64
|
+
else:
|
|
65
|
+
# Serve static files
|
|
66
|
+
super().do_GET()
|
|
67
|
+
|
|
68
|
+
def _serve_json(self, data: Any, status: int = 200):
|
|
69
|
+
"""Send JSON response."""
|
|
70
|
+
self.send_response(status)
|
|
71
|
+
self.send_header("Content-Type", "application/json")
|
|
72
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
73
|
+
self.end_headers()
|
|
74
|
+
self.wfile.write(json.dumps(data, default=str).encode())
|
|
75
|
+
|
|
76
|
+
def _serve_html(self):
|
|
77
|
+
"""Serve the main HTML page."""
|
|
78
|
+
html = self._generate_html()
|
|
79
|
+
self.send_response(200)
|
|
80
|
+
self.send_header("Content-Type", "text/html")
|
|
81
|
+
self.end_headers()
|
|
82
|
+
self.wfile.write(html.encode())
|
|
83
|
+
|
|
84
|
+
def _get_connection(self):
|
|
85
|
+
"""Get DuckDB connection to metadata store."""
|
|
86
|
+
try:
|
|
87
|
+
import duckdb
|
|
88
|
+
return duckdb.connect(str(self.metadata_store_path), read_only=True)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
def _serve_profiles_list(self):
|
|
93
|
+
"""Serve list of all profiled tables."""
|
|
94
|
+
conn = self._get_connection()
|
|
95
|
+
if not conn:
|
|
96
|
+
self._serve_json({"error": "Could not connect to metadata store"}, 500)
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
# Query profile results
|
|
101
|
+
result = conn.execute("""
|
|
102
|
+
SELECT DISTINCT
|
|
103
|
+
source_name,
|
|
104
|
+
table_name,
|
|
105
|
+
adapter_name,
|
|
106
|
+
connection_name,
|
|
107
|
+
COUNT(*) as column_count,
|
|
108
|
+
MAX(last_refreshed) as last_profiled
|
|
109
|
+
FROM column_metadata
|
|
110
|
+
GROUP BY source_name, table_name, adapter_name, connection_name
|
|
111
|
+
ORDER BY source_name, table_name
|
|
112
|
+
""").fetchall()
|
|
113
|
+
|
|
114
|
+
profiles = []
|
|
115
|
+
for row in result:
|
|
116
|
+
profiles.append({
|
|
117
|
+
"source_name": row[0],
|
|
118
|
+
"table_name": row[1],
|
|
119
|
+
"adapter_name": row[2],
|
|
120
|
+
"connection_name": row[3],
|
|
121
|
+
"column_count": row[4],
|
|
122
|
+
"last_profiled": row[5],
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
self._serve_json({"profiles": profiles})
|
|
126
|
+
except Exception as e:
|
|
127
|
+
self._serve_json({"profiles": [], "error": str(e)})
|
|
128
|
+
finally:
|
|
129
|
+
conn.close()
|
|
130
|
+
|
|
131
|
+
def _serve_profile_detail(self, table_name: str):
|
|
132
|
+
"""Serve detailed profile for a specific table."""
|
|
133
|
+
if not table_name:
|
|
134
|
+
self._serve_json({"error": "table parameter required"}, 400)
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
conn = self._get_connection()
|
|
138
|
+
if not conn:
|
|
139
|
+
self._serve_json({"error": "Could not connect to metadata store"}, 500)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
# Query column metadata
|
|
144
|
+
result = conn.execute("""
|
|
145
|
+
SELECT
|
|
146
|
+
column_name,
|
|
147
|
+
adapter_type,
|
|
148
|
+
spark_type,
|
|
149
|
+
is_nullable,
|
|
150
|
+
is_primary_key,
|
|
151
|
+
ordinal_position,
|
|
152
|
+
last_refreshed
|
|
153
|
+
FROM column_metadata
|
|
154
|
+
WHERE table_name = ?
|
|
155
|
+
ORDER BY ordinal_position
|
|
156
|
+
""", [table_name]).fetchall()
|
|
157
|
+
|
|
158
|
+
columns = []
|
|
159
|
+
for row in result:
|
|
160
|
+
columns.append({
|
|
161
|
+
"name": row[0],
|
|
162
|
+
"adapter_type": row[1],
|
|
163
|
+
"spark_type": row[2],
|
|
164
|
+
"is_nullable": row[3],
|
|
165
|
+
"is_primary_key": row[4],
|
|
166
|
+
"ordinal_position": row[5],
|
|
167
|
+
"last_refreshed": row[6],
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
# Try to get row count if available
|
|
171
|
+
row_count = None
|
|
172
|
+
try:
|
|
173
|
+
count_result = conn.execute("""
|
|
174
|
+
SELECT row_count FROM row_counts WHERE table_name = ?
|
|
175
|
+
""", [table_name]).fetchone()
|
|
176
|
+
if count_result:
|
|
177
|
+
row_count = count_result[0]
|
|
178
|
+
except:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
self._serve_json({
|
|
182
|
+
"table_name": table_name,
|
|
183
|
+
"columns": columns,
|
|
184
|
+
"row_count": row_count,
|
|
185
|
+
})
|
|
186
|
+
except Exception as e:
|
|
187
|
+
self._serve_json({"error": str(e)}, 500)
|
|
188
|
+
finally:
|
|
189
|
+
conn.close()
|
|
190
|
+
|
|
191
|
+
def _serve_summary(self):
|
|
192
|
+
"""Serve summary statistics."""
|
|
193
|
+
conn = self._get_connection()
|
|
194
|
+
if not conn:
|
|
195
|
+
self._serve_json({"error": "Could not connect to metadata store"}, 500)
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
# Get summary stats
|
|
200
|
+
tables = conn.execute("""
|
|
201
|
+
SELECT COUNT(DISTINCT table_name) FROM column_metadata
|
|
202
|
+
""").fetchone()[0]
|
|
203
|
+
|
|
204
|
+
columns = conn.execute("""
|
|
205
|
+
SELECT COUNT(*) FROM column_metadata
|
|
206
|
+
""").fetchone()[0]
|
|
207
|
+
|
|
208
|
+
sources = conn.execute("""
|
|
209
|
+
SELECT COUNT(DISTINCT source_name) FROM column_metadata
|
|
210
|
+
""").fetchone()[0]
|
|
211
|
+
|
|
212
|
+
# Get unique connections/adapters
|
|
213
|
+
adapters = conn.execute("""
|
|
214
|
+
SELECT COUNT(DISTINCT adapter_name) FROM column_metadata
|
|
215
|
+
""").fetchone()[0]
|
|
216
|
+
|
|
217
|
+
self._serve_json({
|
|
218
|
+
"total_tables": tables,
|
|
219
|
+
"total_columns": columns,
|
|
220
|
+
"sources": sources,
|
|
221
|
+
"adapters": adapters,
|
|
222
|
+
})
|
|
223
|
+
except Exception as e:
|
|
224
|
+
self._serve_json({"error": str(e)}, 500)
|
|
225
|
+
finally:
|
|
226
|
+
conn.close()
|
|
227
|
+
|
|
228
|
+
def _generate_html(self) -> str:
|
|
229
|
+
"""Generate the HTML page for the profile viewer."""
|
|
230
|
+
return '''<!DOCTYPE html>
|
|
231
|
+
<html lang="en">
|
|
232
|
+
<head>
|
|
233
|
+
<meta charset="UTF-8">
|
|
234
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
235
|
+
<title>DVT Profile Viewer</title>
|
|
236
|
+
<style>
|
|
237
|
+
:root {
|
|
238
|
+
--primary: #6366f1;
|
|
239
|
+
--primary-dark: #4f46e5;
|
|
240
|
+
--success: #10b981;
|
|
241
|
+
--warning: #f59e0b;
|
|
242
|
+
--error: #ef4444;
|
|
243
|
+
--bg: #0f172a;
|
|
244
|
+
--bg-card: #1e293b;
|
|
245
|
+
--text: #f1f5f9;
|
|
246
|
+
--text-dim: #94a3b8;
|
|
247
|
+
--border: #334155;
|
|
248
|
+
}
|
|
249
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
250
|
+
body {
|
|
251
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
252
|
+
background: var(--bg);
|
|
253
|
+
color: var(--text);
|
|
254
|
+
min-height: 100vh;
|
|
255
|
+
}
|
|
256
|
+
.header {
|
|
257
|
+
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
|
258
|
+
padding: 2rem;
|
|
259
|
+
text-align: center;
|
|
260
|
+
}
|
|
261
|
+
.header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
|
262
|
+
.header p { color: rgba(255,255,255,0.8); }
|
|
263
|
+
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
|
264
|
+
.stats-grid {
|
|
265
|
+
display: grid;
|
|
266
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
267
|
+
gap: 1rem;
|
|
268
|
+
margin-bottom: 2rem;
|
|
269
|
+
}
|
|
270
|
+
.stat-card {
|
|
271
|
+
background: var(--bg-card);
|
|
272
|
+
border-radius: 12px;
|
|
273
|
+
padding: 1.5rem;
|
|
274
|
+
border: 1px solid var(--border);
|
|
275
|
+
}
|
|
276
|
+
.stat-card h3 { color: var(--text-dim); font-size: 0.875rem; margin-bottom: 0.5rem; }
|
|
277
|
+
.stat-card .value { font-size: 2rem; font-weight: 700; color: var(--primary); }
|
|
278
|
+
.tables-section { margin-top: 2rem; }
|
|
279
|
+
.tables-section h2 { margin-bottom: 1rem; }
|
|
280
|
+
.table-list {
|
|
281
|
+
display: grid;
|
|
282
|
+
gap: 1rem;
|
|
283
|
+
}
|
|
284
|
+
.table-card {
|
|
285
|
+
background: var(--bg-card);
|
|
286
|
+
border-radius: 12px;
|
|
287
|
+
padding: 1.5rem;
|
|
288
|
+
border: 1px solid var(--border);
|
|
289
|
+
cursor: pointer;
|
|
290
|
+
transition: all 0.2s;
|
|
291
|
+
}
|
|
292
|
+
.table-card:hover {
|
|
293
|
+
border-color: var(--primary);
|
|
294
|
+
transform: translateY(-2px);
|
|
295
|
+
}
|
|
296
|
+
.table-card.selected {
|
|
297
|
+
border-color: var(--primary);
|
|
298
|
+
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3);
|
|
299
|
+
}
|
|
300
|
+
.table-header {
|
|
301
|
+
display: flex;
|
|
302
|
+
justify-content: space-between;
|
|
303
|
+
align-items: center;
|
|
304
|
+
margin-bottom: 0.5rem;
|
|
305
|
+
}
|
|
306
|
+
.table-name { font-weight: 600; font-size: 1.1rem; }
|
|
307
|
+
.table-type {
|
|
308
|
+
font-size: 0.75rem;
|
|
309
|
+
padding: 0.25rem 0.5rem;
|
|
310
|
+
border-radius: 4px;
|
|
311
|
+
background: var(--primary);
|
|
312
|
+
}
|
|
313
|
+
.table-type.source { background: var(--success); }
|
|
314
|
+
.table-type.model { background: var(--warning); }
|
|
315
|
+
.table-meta { color: var(--text-dim); font-size: 0.875rem; }
|
|
316
|
+
.detail-panel {
|
|
317
|
+
position: fixed;
|
|
318
|
+
top: 0;
|
|
319
|
+
right: -500px;
|
|
320
|
+
width: 500px;
|
|
321
|
+
height: 100vh;
|
|
322
|
+
background: var(--bg-card);
|
|
323
|
+
border-left: 1px solid var(--border);
|
|
324
|
+
transition: right 0.3s;
|
|
325
|
+
overflow-y: auto;
|
|
326
|
+
z-index: 100;
|
|
327
|
+
}
|
|
328
|
+
.detail-panel.open { right: 0; }
|
|
329
|
+
.detail-header {
|
|
330
|
+
padding: 1.5rem;
|
|
331
|
+
border-bottom: 1px solid var(--border);
|
|
332
|
+
display: flex;
|
|
333
|
+
justify-content: space-between;
|
|
334
|
+
align-items: center;
|
|
335
|
+
}
|
|
336
|
+
.detail-header h2 { font-size: 1.25rem; }
|
|
337
|
+
.close-btn {
|
|
338
|
+
background: none;
|
|
339
|
+
border: none;
|
|
340
|
+
color: var(--text);
|
|
341
|
+
font-size: 1.5rem;
|
|
342
|
+
cursor: pointer;
|
|
343
|
+
}
|
|
344
|
+
.detail-content { padding: 1.5rem; }
|
|
345
|
+
.column-table {
|
|
346
|
+
width: 100%;
|
|
347
|
+
border-collapse: collapse;
|
|
348
|
+
}
|
|
349
|
+
.column-table th, .column-table td {
|
|
350
|
+
padding: 0.75rem;
|
|
351
|
+
text-align: left;
|
|
352
|
+
border-bottom: 1px solid var(--border);
|
|
353
|
+
}
|
|
354
|
+
.column-table th {
|
|
355
|
+
color: var(--text-dim);
|
|
356
|
+
font-weight: 500;
|
|
357
|
+
font-size: 0.75rem;
|
|
358
|
+
text-transform: uppercase;
|
|
359
|
+
}
|
|
360
|
+
.type-badge {
|
|
361
|
+
font-family: monospace;
|
|
362
|
+
font-size: 0.8rem;
|
|
363
|
+
background: rgba(99, 102, 241, 0.2);
|
|
364
|
+
padding: 0.25rem 0.5rem;
|
|
365
|
+
border-radius: 4px;
|
|
366
|
+
}
|
|
367
|
+
.loading {
|
|
368
|
+
text-align: center;
|
|
369
|
+
padding: 3rem;
|
|
370
|
+
color: var(--text-dim);
|
|
371
|
+
}
|
|
372
|
+
.error {
|
|
373
|
+
background: rgba(239, 68, 68, 0.2);
|
|
374
|
+
border: 1px solid var(--error);
|
|
375
|
+
padding: 1rem;
|
|
376
|
+
border-radius: 8px;
|
|
377
|
+
margin: 1rem 0;
|
|
378
|
+
}
|
|
379
|
+
@media (max-width: 768px) {
|
|
380
|
+
.detail-panel { width: 100%; right: -100%; }
|
|
381
|
+
}
|
|
382
|
+
</style>
|
|
383
|
+
</head>
|
|
384
|
+
<body>
|
|
385
|
+
<div class="header">
|
|
386
|
+
<h1>🔍 DVT Profile Viewer</h1>
|
|
387
|
+
<p>Data profiling results from metadata_store.duckdb</p>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<div class="container">
|
|
391
|
+
<div class="stats-grid" id="stats">
|
|
392
|
+
<div class="stat-card">
|
|
393
|
+
<h3>Total Tables</h3>
|
|
394
|
+
<div class="value" id="stat-tables">-</div>
|
|
395
|
+
</div>
|
|
396
|
+
<div class="stat-card">
|
|
397
|
+
<h3>Total Columns</h3>
|
|
398
|
+
<div class="value" id="stat-columns">-</div>
|
|
399
|
+
</div>
|
|
400
|
+
<div class="stat-card">
|
|
401
|
+
<h3>Sources</h3>
|
|
402
|
+
<div class="value" id="stat-sources">-</div>
|
|
403
|
+
</div>
|
|
404
|
+
<div class="stat-card">
|
|
405
|
+
<h3>Models</h3>
|
|
406
|
+
<div class="value" id="stat-models">-</div>
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
<div class="tables-section">
|
|
411
|
+
<h2>Profiled Tables</h2>
|
|
412
|
+
<div class="table-list" id="table-list">
|
|
413
|
+
<div class="loading">Loading profiles...</div>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
|
|
418
|
+
<div class="detail-panel" id="detail-panel">
|
|
419
|
+
<div class="detail-header">
|
|
420
|
+
<h2 id="detail-title">Table Details</h2>
|
|
421
|
+
<button class="close-btn" onclick="closeDetail()">×</button>
|
|
422
|
+
</div>
|
|
423
|
+
<div class="detail-content" id="detail-content">
|
|
424
|
+
<div class="loading">Select a table to view details</div>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
|
|
428
|
+
<script>
|
|
429
|
+
async function loadSummary() {
|
|
430
|
+
try {
|
|
431
|
+
const resp = await fetch('/api/summary');
|
|
432
|
+
const data = await resp.json();
|
|
433
|
+
document.getElementById('stat-tables').textContent = data.total_tables || 0;
|
|
434
|
+
document.getElementById('stat-columns').textContent = data.total_columns || 0;
|
|
435
|
+
document.getElementById('stat-sources').textContent = data.sources || 0;
|
|
436
|
+
document.getElementById('stat-models').textContent = data.models || 0;
|
|
437
|
+
} catch (e) {
|
|
438
|
+
console.error('Failed to load summary:', e);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function loadProfiles() {
|
|
443
|
+
const container = document.getElementById('table-list');
|
|
444
|
+
try {
|
|
445
|
+
const resp = await fetch('/api/profiles');
|
|
446
|
+
const data = await resp.json();
|
|
447
|
+
|
|
448
|
+
if (data.profiles.length === 0) {
|
|
449
|
+
container.innerHTML = '<div class="loading">No profiles found. Run "dvt profile" first.</div>';
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
container.innerHTML = data.profiles.map(p => `
|
|
454
|
+
<div class="table-card" onclick="showDetail('${p.table_name}')">
|
|
455
|
+
<div class="table-header">
|
|
456
|
+
<span class="table-name">${p.source_name ? p.source_name + '.' : ''}${p.table_name}</span>
|
|
457
|
+
<span class="table-type ${p.type}">${p.type}</span>
|
|
458
|
+
</div>
|
|
459
|
+
<div class="table-meta">
|
|
460
|
+
${p.column_count} columns • Last profiled: ${new Date(p.last_profiled).toLocaleString()}
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
`).join('');
|
|
464
|
+
} catch (e) {
|
|
465
|
+
container.innerHTML = `<div class="error">Failed to load profiles: ${e.message}</div>`;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function showDetail(tableName) {
|
|
470
|
+
const panel = document.getElementById('detail-panel');
|
|
471
|
+
const title = document.getElementById('detail-title');
|
|
472
|
+
const content = document.getElementById('detail-content');
|
|
473
|
+
|
|
474
|
+
panel.classList.add('open');
|
|
475
|
+
title.textContent = tableName;
|
|
476
|
+
content.innerHTML = '<div class="loading">Loading...</div>';
|
|
477
|
+
|
|
478
|
+
// Highlight selected
|
|
479
|
+
document.querySelectorAll('.table-card').forEach(c => c.classList.remove('selected'));
|
|
480
|
+
event.currentTarget.classList.add('selected');
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
const resp = await fetch(`/api/profile?table=${encodeURIComponent(tableName)}`);
|
|
484
|
+
const data = await resp.json();
|
|
485
|
+
|
|
486
|
+
if (data.error) {
|
|
487
|
+
content.innerHTML = `<div class="error">${data.error}</div>`;
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
let html = '';
|
|
492
|
+
if (data.row_count !== null) {
|
|
493
|
+
html += `<p style="margin-bottom: 1rem;">Row count: <strong>${data.row_count.toLocaleString()}</strong></p>`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
html += `
|
|
497
|
+
<table class="column-table">
|
|
498
|
+
<thead>
|
|
499
|
+
<tr>
|
|
500
|
+
<th>#</th>
|
|
501
|
+
<th>Column</th>
|
|
502
|
+
<th>Type</th>
|
|
503
|
+
<th>Spark Type</th>
|
|
504
|
+
</tr>
|
|
505
|
+
</thead>
|
|
506
|
+
<tbody>
|
|
507
|
+
${data.columns.map(c => `
|
|
508
|
+
<tr>
|
|
509
|
+
<td>${c.ordinal_position}</td>
|
|
510
|
+
<td><strong>${c.name}</strong></td>
|
|
511
|
+
<td><span class="type-badge">${c.adapter_type || '-'}</span></td>
|
|
512
|
+
<td><span class="type-badge">${c.spark_type || '-'}</span></td>
|
|
513
|
+
</tr>
|
|
514
|
+
`).join('')}
|
|
515
|
+
</tbody>
|
|
516
|
+
</table>
|
|
517
|
+
`;
|
|
518
|
+
|
|
519
|
+
content.innerHTML = html;
|
|
520
|
+
} catch (e) {
|
|
521
|
+
content.innerHTML = `<div class="error">Failed to load details: ${e.message}</div>`;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function closeDetail() {
|
|
526
|
+
document.getElementById('detail-panel').classList.remove('open');
|
|
527
|
+
document.querySelectorAll('.table-card').forEach(c => c.classList.remove('selected'));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Initialize
|
|
531
|
+
loadSummary();
|
|
532
|
+
loadProfiles();
|
|
533
|
+
</script>
|
|
534
|
+
</body>
|
|
535
|
+
</html>'''
|
|
536
|
+
|
|
537
|
+
def log_message(self, format, *args):
|
|
538
|
+
"""Suppress default logging."""
|
|
539
|
+
pass
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def serve_profile_ui(
|
|
543
|
+
project_dir: Path,
|
|
544
|
+
port: int = 8580,
|
|
545
|
+
host: str = "localhost",
|
|
546
|
+
open_browser: bool = True,
|
|
547
|
+
):
|
|
548
|
+
"""
|
|
549
|
+
Start the profile viewer web server.
|
|
550
|
+
|
|
551
|
+
Args:
|
|
552
|
+
project_dir: Path to the DVT project
|
|
553
|
+
port: Port to serve on (default: 8580)
|
|
554
|
+
host: Host to bind to (default: localhost)
|
|
555
|
+
open_browser: Whether to open browser automatically
|
|
556
|
+
"""
|
|
557
|
+
# Find metadata store
|
|
558
|
+
metadata_store_path = project_dir / ".dvt" / "metadata_store.duckdb"
|
|
559
|
+
|
|
560
|
+
if not metadata_store_path.exists():
|
|
561
|
+
if HAS_RICH:
|
|
562
|
+
console.print(Panel(
|
|
563
|
+
"[yellow]No metadata store found.[/yellow]\n\n"
|
|
564
|
+
"Run [bold cyan]dvt profile[/bold cyan] first to capture profiling data.",
|
|
565
|
+
title="[bold red]Error[/bold red]",
|
|
566
|
+
border_style="red",
|
|
567
|
+
))
|
|
568
|
+
else:
|
|
569
|
+
print("Error: No metadata store found.")
|
|
570
|
+
print("Run 'dvt profile' first to capture profiling data.")
|
|
571
|
+
return False
|
|
572
|
+
|
|
573
|
+
# Create handler with metadata store path
|
|
574
|
+
def handler(*args, **kwargs):
|
|
575
|
+
return ProfileAPIHandler(*args, metadata_store_path=metadata_store_path, **kwargs)
|
|
576
|
+
|
|
577
|
+
# Start server
|
|
578
|
+
server = HTTPServer((host, port), handler)
|
|
579
|
+
url = f"http://{host}:{port}"
|
|
580
|
+
|
|
581
|
+
if HAS_RICH:
|
|
582
|
+
console.print()
|
|
583
|
+
console.print(Panel(
|
|
584
|
+
f"[bold green]Profile Viewer running at:[/bold green]\n\n"
|
|
585
|
+
f" [bold cyan]{url}[/bold cyan]\n\n"
|
|
586
|
+
f"[dim]Press Ctrl+C to stop[/dim]",
|
|
587
|
+
title="[bold magenta]🔍 DVT Profile Viewer[/bold magenta]",
|
|
588
|
+
border_style="magenta",
|
|
589
|
+
box=box.DOUBLE,
|
|
590
|
+
))
|
|
591
|
+
console.print()
|
|
592
|
+
else:
|
|
593
|
+
print(f"\nDVT Profile Viewer running at: {url}")
|
|
594
|
+
print("Press Ctrl+C to stop\n")
|
|
595
|
+
|
|
596
|
+
# Open browser
|
|
597
|
+
if open_browser:
|
|
598
|
+
threading.Timer(1.0, lambda: webbrowser.open(url)).start()
|
|
599
|
+
|
|
600
|
+
try:
|
|
601
|
+
server.serve_forever()
|
|
602
|
+
except KeyboardInterrupt:
|
|
603
|
+
if HAS_RICH:
|
|
604
|
+
console.print("\n[yellow]Server stopped.[/yellow]")
|
|
605
|
+
else:
|
|
606
|
+
print("\nServer stopped.")
|
|
607
|
+
|
|
608
|
+
return True
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
if __name__ == "__main__":
|
|
612
|
+
# For testing
|
|
613
|
+
import sys
|
|
614
|
+
project_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".")
|
|
615
|
+
serve_profile_ui(project_dir)
|