detectkit 0.63.0__tar.gz → 0.65.0__tar.gz
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.
- {detectkit-0.63.0/detectkit.egg-info → detectkit-0.65.0}/PKG-INFO +12 -2
- {detectkit-0.63.0 → detectkit-0.65.0}/README.md +3 -1
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/__init__.py +1 -1
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/CLAUDE.section.md +1 -1
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/metrics.md +3 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/overview.md +4 -1
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/project.md +70 -3
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/commands/run.py +37 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/config/profile.py +241 -17
- detectkit-0.65.0/detectkit/database/bigquery_manager.py +229 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/database/manager.py +7 -2
- detectkit-0.65.0/detectkit/database/snowflake_manager.py +182 -0
- detectkit-0.65.0/detectkit/database/source_manager.py +65 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/loaders/metric_loader.py +8 -6
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/_base.py +5 -1
- {detectkit-0.63.0 → detectkit-0.65.0/detectkit.egg-info}/PKG-INFO +12 -2
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit.egg-info/SOURCES.txt +3 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit.egg-info/requires.txt +10 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/pyproject.toml +12 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/LICENSE +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/MANIFEST.in +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/channels/discord.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/channels/googlechat.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/channels/ntfy.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/channels/teams.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/autotune/axis_spec.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/autotune/result.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/autotune/runner.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/commands/autotune.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/commands/mcp.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/commands/osi.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/commands/tune.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/commands/ui.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/cli/main.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/config/metric_io.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/core/models.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/database/duckdb_manager.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/detectors/statistical/autoreg.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/loaders/errors.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/mcp/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/mcp/context.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/mcp/errors.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/mcp/serialize.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/mcp/server.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/mcp/tools.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/semantic/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/semantic/errors.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/semantic/exporter.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/semantic/importer.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/semantic/osi_model.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/semantic/query_gen.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/tuning/assets/tune.js +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/tuning/config_writer.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/tuning/html.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/tuning/payload.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/tuning/server.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/ui/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/ui/assets/ui.js +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/ui/html.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/ui/jobs.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/ui/metric_files.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/ui/overview.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/ui/server.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/requirements.txt +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/setup.cfg +0 -0
- {detectkit-0.63.0 → detectkit-0.65.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: detectkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.65.0
|
|
4
4
|
Summary: Metric monitoring with automatic anomaly detection
|
|
5
5
|
Author: detectkit team
|
|
6
6
|
License: MIT
|
|
@@ -40,11 +40,17 @@ Provides-Extra: mariadb
|
|
|
40
40
|
Requires-Dist: pymysql>=1.0.0; extra == "mariadb"
|
|
41
41
|
Provides-Extra: duckdb
|
|
42
42
|
Requires-Dist: duckdb>=1.1; extra == "duckdb"
|
|
43
|
+
Provides-Extra: snowflake
|
|
44
|
+
Requires-Dist: snowflake-connector-python>=3.12; extra == "snowflake"
|
|
45
|
+
Provides-Extra: bigquery
|
|
46
|
+
Requires-Dist: google-cloud-bigquery>=3.15; extra == "bigquery"
|
|
43
47
|
Provides-Extra: all-db
|
|
44
48
|
Requires-Dist: clickhouse-driver>=0.2.0; extra == "all-db"
|
|
45
49
|
Requires-Dist: psycopg2-binary>=2.9.0; extra == "all-db"
|
|
46
50
|
Requires-Dist: pymysql>=1.0.0; extra == "all-db"
|
|
47
51
|
Requires-Dist: duckdb>=1.1; extra == "all-db"
|
|
52
|
+
Requires-Dist: snowflake-connector-python>=3.12; extra == "all-db"
|
|
53
|
+
Requires-Dist: google-cloud-bigquery>=3.15; extra == "all-db"
|
|
48
54
|
Provides-Extra: prophet
|
|
49
55
|
Requires-Dist: prophet>=1.1.0; extra == "prophet"
|
|
50
56
|
Provides-Extra: timesfm
|
|
@@ -61,6 +67,8 @@ Requires-Dist: clickhouse-driver>=0.2.0; extra == "all"
|
|
|
61
67
|
Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
|
|
62
68
|
Requires-Dist: pymysql>=1.0.0; extra == "all"
|
|
63
69
|
Requires-Dist: duckdb>=1.1; extra == "all"
|
|
70
|
+
Requires-Dist: snowflake-connector-python>=3.12; extra == "all"
|
|
71
|
+
Requires-Dist: google-cloud-bigquery>=3.15; extra == "all"
|
|
64
72
|
Requires-Dist: prophet>=1.1.0; extra == "all"
|
|
65
73
|
Requires-Dist: timesfm>=0.1.0; extra == "all"
|
|
66
74
|
Requires-Dist: sqlglot>=20.0; extra == "all"
|
|
@@ -106,7 +114,7 @@ Dynamic: license-file
|
|
|
106
114
|
- **@mentions** — tag users/groups in alerts, each channel formats natively
|
|
107
115
|
- **Alert lifecycle** — consecutive anomalies, fraction-of-window rule (`anomaly_window` + `min_anomaly_share`), cooldown, recovery notifications, no-data alerts
|
|
108
116
|
- **Project-level error alerts** — catch DB outages and pipeline crashes once per run
|
|
109
|
-
- **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed)
|
|
117
|
+
- **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed); Snowflake and BigQuery as hybrid-mode sources
|
|
110
118
|
- **Hybrid mode** — read metric SQL from a billed-per-query warehouse while all pipeline state lives in a cheap local database
|
|
111
119
|
- **Idempotent** — resume from interruptions, no duplicate processing
|
|
112
120
|
- **CLI** — `dtk init`, `dtk run --select`, `dtk unlock`, `dtk clean`, tag-based selectors
|
|
@@ -123,6 +131,8 @@ With database drivers:
|
|
|
123
131
|
```bash
|
|
124
132
|
pip install detectkit[clickhouse] # ClickHouse
|
|
125
133
|
pip install detectkit[duckdb] # DuckDB - single file, no server
|
|
134
|
+
pip install detectkit[snowflake] # Snowflake - source-only (hybrid mode)
|
|
135
|
+
pip install detectkit[bigquery] # BigQuery - source-only (hybrid mode)
|
|
126
136
|
pip install detectkit[all-db] # All databases
|
|
127
137
|
```
|
|
128
138
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
- **@mentions** — tag users/groups in alerts, each channel formats natively
|
|
25
25
|
- **Alert lifecycle** — consecutive anomalies, fraction-of-window rule (`anomaly_window` + `min_anomaly_share`), cooldown, recovery notifications, no-data alerts
|
|
26
26
|
- **Project-level error alerts** — catch DB outages and pipeline crashes once per run
|
|
27
|
-
- **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed)
|
|
27
|
+
- **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed); Snowflake and BigQuery as hybrid-mode sources
|
|
28
28
|
- **Hybrid mode** — read metric SQL from a billed-per-query warehouse while all pipeline state lives in a cheap local database
|
|
29
29
|
- **Idempotent** — resume from interruptions, no duplicate processing
|
|
30
30
|
- **CLI** — `dtk init`, `dtk run --select`, `dtk unlock`, `dtk clean`, tag-based selectors
|
|
@@ -41,6 +41,8 @@ With database drivers:
|
|
|
41
41
|
```bash
|
|
42
42
|
pip install detectkit[clickhouse] # ClickHouse
|
|
43
43
|
pip install detectkit[duckdb] # DuckDB - single file, no server
|
|
44
|
+
pip install detectkit[snowflake] # Snowflake - source-only (hybrid mode)
|
|
45
|
+
pip install detectkit[bigquery] # BigQuery - source-only (hybrid mode)
|
|
44
46
|
pip install detectkit[all-db] # All databases
|
|
45
47
|
```
|
|
46
48
|
|
|
@@ -4,7 +4,7 @@ detectk - Anomaly Detection for Time-Series Metrics
|
|
|
4
4
|
A Python library for data analysts and engineers to monitor metrics with automatic anomaly detection.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "0.
|
|
7
|
+
__version__ = "0.65.0"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -14,7 +14,7 @@ conventions.
|
|
|
14
14
|
**Database access for _you_ (recommended, not required).** detectkit itself
|
|
15
15
|
connects to the database directly via its drivers — it **never** needs an MCP to
|
|
16
16
|
run. But you assist far better with **read access to the same database** (e.g. a
|
|
17
|
-
database MCP for the project's ClickHouse / PostgreSQL / MySQL / MariaDB / DuckDB): you can inspect
|
|
17
|
+
database MCP for the project's ClickHouse / PostgreSQL / MySQL / MariaDB / DuckDB / Snowflake / BigQuery (the last two source-only, hybrid mode)): you can inspect
|
|
18
18
|
a metric's series, find real incidents to label for `dtk autotune`, sanity-check
|
|
19
19
|
a metric query before running it, and confirm detections — instead of asking the
|
|
20
20
|
user to run every query by hand. Without it, fall back to marking incidents
|
|
@@ -79,6 +79,9 @@ not applied at runtime by `dtk run`; `source_profile` is live). Only the
|
|
|
79
79
|
metric's **load** step ever connects to it — detect/alert and every other
|
|
80
80
|
command don't — though every `dtk run` fail-fast validates the resolved name
|
|
81
81
|
up front regardless of `--steps` (a typo exits 1 before any pipeline work).
|
|
82
|
+
The `source_profile` may point at a **source-only** warehouse profile such as
|
|
83
|
+
`snowflake` or `bigquery` (usable *only* as a source, never for `_dtk_*` state
|
|
84
|
+
— see `project.md`), which is exactly the case hybrid mode is built for.
|
|
82
85
|
See the [Hybrid Mode guide](https://dtk.pipelab.dev/guides/hybrid-mode/).
|
|
83
86
|
|
|
84
87
|
`ai_context` is **OSI-compatible grounding** you can add to any metric with **no
|
|
@@ -6,7 +6,10 @@ metrics with automatic anomaly detection and multi-channel alerting. It is
|
|
|
6
6
|
them with one command. Core logic is pure numpy (no pandas). **ClickHouse,
|
|
7
7
|
PostgreSQL, MySQL/MariaDB and DuckDB are all fully supported** — only the
|
|
8
8
|
connection and the SQL dialect of your metric queries differ between them
|
|
9
|
-
(DuckDB additionally needs no server at all — just a local file)
|
|
9
|
+
(DuckDB additionally needs no server at all — just a local file); **Snowflake**
|
|
10
|
+
and **BigQuery** are supported as **source-only** backends (hybrid mode — they
|
|
11
|
+
run a metric's load SQL, but `_dtk_*` state must live in one of the full
|
|
12
|
+
backends).
|
|
10
13
|
|
|
11
14
|
## The pipeline: load → detect → alert
|
|
12
15
|
|
|
@@ -57,7 +57,9 @@ profile whose database runs metric SQL, while *every* `_dtk_*` table
|
|
|
57
57
|
**state** profile — the one `dtk run` is already connected to for everything
|
|
58
58
|
else. A per-metric `source_profile` overrides it (`metrics.md`); resolves
|
|
59
59
|
**metric → project → unset**, same precedence as `loading_delay`. Unset on
|
|
60
|
-
both = today's behavior (one profile does everything).
|
|
60
|
+
both = today's behavior (one profile does everything). The source profile can
|
|
61
|
+
be a full backend **or** a **source-only** type (`snowflake`, `bigquery`) —
|
|
62
|
+
valid only here, never as the state profile. Only the LOAD step's
|
|
61
63
|
metric-SQL query is affected — detect/alert and every other command
|
|
62
64
|
(`dtk autotune`/`tune`/`ui`/`clean`/`unlock`) only ever touch the state
|
|
63
65
|
profile. A source-side failure raises `SourceDatabaseError` (message leads
|
|
@@ -137,12 +139,15 @@ alert_channels:
|
|
|
137
139
|
|
|
138
140
|
### Database profiles
|
|
139
141
|
|
|
140
|
-
> ClickHouse, PostgreSQL, MySQL, MariaDB and DuckDB are all supported
|
|
142
|
+
> ClickHouse, PostgreSQL, MySQL, MariaDB and DuckDB are all supported as
|
|
143
|
+
> **state** backends (they hold the `_dtk_*` tables); **Snowflake** and
|
|
144
|
+
> **BigQuery** are **source-only** — see their blocks below.
|
|
141
145
|
> ClickHouse/MySQL/MariaDB use two *databases*; PostgreSQL connects to one
|
|
142
146
|
> `database` and uses two *schemas*; DuckDB is a single *file* (or `:memory:`
|
|
143
147
|
> for tests) holding two *schemas*.
|
|
144
148
|
> `dtk init --db-type {clickhouse,postgres,mysql,mariadb}` scaffolds the right
|
|
145
|
-
> shape — DuckDB profiles are written by hand (not yet
|
|
149
|
+
> shape — DuckDB, Snowflake and BigQuery profiles are written by hand (not yet
|
|
150
|
+
> a `--db-type` choice).
|
|
146
151
|
|
|
147
152
|
**ClickHouse**:
|
|
148
153
|
```yaml
|
|
@@ -215,6 +220,68 @@ profiles:
|
|
|
215
220
|
> process restarts — use it for tests/scratch only, never a real project.
|
|
216
221
|
> `pip install 'detectkit[duckdb]'`.
|
|
217
222
|
|
|
223
|
+
**Snowflake** (**source-only** — hybrid mode; runs a metric's load SQL, never
|
|
224
|
+
holds `_dtk_*` state):
|
|
225
|
+
```yaml
|
|
226
|
+
profiles:
|
|
227
|
+
warehouse:
|
|
228
|
+
type: snowflake
|
|
229
|
+
account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}" # required — e.g. ab12345.eu-central-1
|
|
230
|
+
user: svc_detectkit # required
|
|
231
|
+
# key-pair auth (recommended — Snowflake retires single-factor passwords for
|
|
232
|
+
# service accounts through 2026); use ONE of key-pair or password:
|
|
233
|
+
private_key_path: /etc/detectkit/snowflake.p8 # PEM private key file
|
|
234
|
+
private_key_passphrase: "{{ env_var('SNOWFLAKE_KEY_PASSPHRASE') }}" # optional
|
|
235
|
+
# password: "{{ env_var('SNOWFLAKE_PASSWORD') }}" # alternative to key-pair
|
|
236
|
+
warehouse: COMPUTE_WH # required — the virtual warehouse to run on
|
|
237
|
+
database: ANALYTICS # required — where the metric SQL reads from
|
|
238
|
+
schema: PUBLIC # optional — default schema (aliases schema_name)
|
|
239
|
+
role: DETECTKIT_RO # optional — Snowflake role
|
|
240
|
+
settings: {} # optional — extra session parameters
|
|
241
|
+
```
|
|
242
|
+
> **Source-only**: a `snowflake` profile is valid **only** as a metric's or the
|
|
243
|
+
> project's `source_profile` (hybrid mode) — `dtk run` refuses it as a state
|
|
244
|
+
> profile (`--profile`/`default_profile`), so pair it with a full backend
|
|
245
|
+
> (DuckDB/Postgres/ClickHouse) that holds the `_dtk_*` tables. See `metrics.md`
|
|
246
|
+
> and the [Hybrid Mode guide](https://dtk.pipelab.dev/guides/hybrid-mode/).
|
|
247
|
+
> Key-pair auth is first-class and recommended; a plain `password` works too.
|
|
248
|
+
> The session `TIMEZONE` is pinned to UTC (override via `settings`). Billing
|
|
249
|
+
> note: every query resumes the warehouse with a **60-second minimum bill**, so
|
|
250
|
+
> hybrid mode (load from Snowflake, keep cheap local state) is the point.
|
|
251
|
+
> `pip install 'detectkit[snowflake]'`.
|
|
252
|
+
|
|
253
|
+
**BigQuery** (**source-only** — hybrid mode; runs a metric's load SQL, never
|
|
254
|
+
holds `_dtk_*` state):
|
|
255
|
+
```yaml
|
|
256
|
+
profiles:
|
|
257
|
+
warehouse:
|
|
258
|
+
type: bigquery
|
|
259
|
+
project: my-analytics-project # required — GCP project id billed for the queries
|
|
260
|
+
credentials_json_path: /etc/detectkit/bq-sa.json # optional — service-account JSON key file
|
|
261
|
+
# # (unset -> Application Default Credentials)
|
|
262
|
+
location: EU # optional — job location (e.g. EU / US)
|
|
263
|
+
dataset: analytics # optional — default dataset so unqualified table names resolve
|
|
264
|
+
api_endpoint: null # optional — endpoint override; plain-http (the emulator) -> anonymous
|
|
265
|
+
# # auth when no key file; https endpoints authenticate normally
|
|
266
|
+
settings: # optional — extra QueryJobConfig attributes applied to every query
|
|
267
|
+
maximum_bytes_billed: 1000000000
|
|
268
|
+
```
|
|
269
|
+
> **Source-only**: a `bigquery` profile is valid **only** as a metric's or the
|
|
270
|
+
> project's `source_profile` (hybrid mode) — `dtk run` refuses it as a state
|
|
271
|
+
> profile (`--profile`/`default_profile`), so pair it with a full backend
|
|
272
|
+
> (DuckDB/Postgres/ClickHouse) that holds the `_dtk_*` tables. See `metrics.md`
|
|
273
|
+
> and the [Hybrid Mode guide](https://dtk.pipelab.dev/guides/hybrid-mode/).
|
|
274
|
+
> Auth: `credentials_json_path` (a service-account key file) when set, else
|
|
275
|
+
> **Application Default Credentials** (gcloud ADC / an attached service account
|
|
276
|
+
> / Workload Identity); host/port/user/password are unused. Timestamps: a
|
|
277
|
+
> BigQuery `TIMESTAMP` column comes back tz-aware UTC (handled by the loader),
|
|
278
|
+
> `DATETIME` comes back naive — prefer `TIMESTAMP` (or a cast) for the metric's
|
|
279
|
+
> timestamp column. Billing note: on-demand queries bill a **10 MiB minimum**
|
|
280
|
+
> of bytes processed per query per referenced table, so frequent small
|
|
281
|
+
> monitoring queries are disproportionately expensive — load from BigQuery,
|
|
282
|
+
> keep cheap local state; `settings: {maximum_bytes_billed: ...}` caps what a
|
|
283
|
+
> single query may scan. `pip install 'detectkit[bigquery]'`.
|
|
284
|
+
|
|
218
285
|
### Alert channels
|
|
219
286
|
|
|
220
287
|
Defined once in `profiles.yml`, referenced by name in each metric's
|
|
@@ -341,6 +341,16 @@ def _run_impl(
|
|
|
341
341
|
summary["error"] = f"Error: {source_profile_error}"
|
|
342
342
|
return 1
|
|
343
343
|
|
|
344
|
+
# Fail-fast: the STATE profile must be a state-capable type. Like the
|
|
345
|
+
# source_profile check above this is a config error, not an outage —
|
|
346
|
+
# exit 1 without paging error_alerting (create_manager would raise the
|
|
347
|
+
# same refusal, but through the path that fires the error alert).
|
|
348
|
+
state_type_error = _validate_state_profile_type(profiles_config, profile)
|
|
349
|
+
if state_type_error:
|
|
350
|
+
click.echo(click.style(f"Error: {state_type_error}", fg="red", bold=True))
|
|
351
|
+
summary["error"] = f"Error: {state_type_error}"
|
|
352
|
+
return 1
|
|
353
|
+
|
|
344
354
|
# Create database manager
|
|
345
355
|
try:
|
|
346
356
|
db_manager = profiles_config.create_manager(profile)
|
|
@@ -519,6 +529,33 @@ def _validate_source_profiles(
|
|
|
519
529
|
)
|
|
520
530
|
|
|
521
531
|
|
|
532
|
+
def _validate_state_profile_type(
|
|
533
|
+
profiles_config: ProfilesConfig, profile_name: str | None
|
|
534
|
+
) -> str | None:
|
|
535
|
+
"""Cheap, connection-free check that the STATE profile is state-capable.
|
|
536
|
+
|
|
537
|
+
A source-only profile type (e.g. ``snowflake``) pointed at by
|
|
538
|
+
``--profile``/``default_profile`` is a config error, not an outage, so it
|
|
539
|
+
must exit 1 *without* paging ``error_alerting`` — same spirit as the
|
|
540
|
+
source_profile name check above. An unknown/unset profile name returns
|
|
541
|
+
``None`` here so the existing ``create_manager`` path reports it exactly
|
|
542
|
+
as before. Duck-typed via ``getattr`` like ``_validate_source_profiles``,
|
|
543
|
+
so test doubles need not model the full ProfilesConfig surface.
|
|
544
|
+
"""
|
|
545
|
+
known_profiles = getattr(profiles_config, "profiles", {}) or {}
|
|
546
|
+
name = profile_name or getattr(profiles_config, "default_profile", None)
|
|
547
|
+
profile_config = known_profiles.get(name) if name else None
|
|
548
|
+
if profile_config is None or not getattr(profile_config, "is_source_only", False):
|
|
549
|
+
return None
|
|
550
|
+
state_types = ", ".join(sorted(profile_config.STATE_TYPES))
|
|
551
|
+
return (
|
|
552
|
+
f"Profile type '{profile_config.type}' is source-only: it can serve "
|
|
553
|
+
f"as a metric/project 'source_profile' (hybrid mode), but it cannot "
|
|
554
|
+
f"hold detectkit state. Point --profile/default_profile at one of: "
|
|
555
|
+
f"{state_types}."
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
|
|
522
559
|
def _resolve_report_path(report_path: str, project_root: Path, metric_name: str) -> Path:
|
|
523
560
|
"""Map the ``--report`` value to a concrete output file for a metric.
|
|
524
561
|
|
|
@@ -5,13 +5,14 @@ Manages database connections and locations (similar to dbt profiles).
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any, ClassVar
|
|
9
9
|
|
|
10
10
|
import yaml
|
|
11
|
-
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
12
12
|
|
|
13
13
|
from detectkit.database.clickhouse_manager import ClickHouseDatabaseManager
|
|
14
14
|
from detectkit.database.manager import BaseDatabaseManager
|
|
15
|
+
from detectkit.database.source_manager import SourceDatabaseManager
|
|
15
16
|
from detectkit.utils.env_interpolation import interpolate_env_vars
|
|
16
17
|
|
|
17
18
|
|
|
@@ -22,24 +23,74 @@ class ProfileConfig(BaseModel):
|
|
|
22
23
|
Defines connection parameters and database locations for a specific
|
|
23
24
|
environment (dev, prod, etc.).
|
|
24
25
|
|
|
26
|
+
Two classes of profile types exist:
|
|
27
|
+
|
|
28
|
+
- **Full backends** (``STATE_TYPES``: clickhouse/postgres/mysql/mariadb/
|
|
29
|
+
duckdb) can hold detectkit state (the ``_dtk_*`` tables) — and, since
|
|
30
|
+
state managers also expose ``execute_query``, double as hybrid-mode
|
|
31
|
+
sources.
|
|
32
|
+
- **Source-only backends** (``SOURCE_ONLY_TYPES``: snowflake, bigquery)
|
|
33
|
+
implement only connect + read query. They are valid **only** as a
|
|
34
|
+
metric/project ``source_profile``; :meth:`create_manager` refuses them
|
|
35
|
+
as a state profile with a clear error.
|
|
36
|
+
|
|
25
37
|
Attributes:
|
|
26
|
-
type: Database type ("clickhouse", "postgres", "mysql", "mariadb",
|
|
38
|
+
type: Database type ("clickhouse", "postgres", "mysql", "mariadb",
|
|
39
|
+
"duckdb"; source-only: "snowflake", "bigquery")
|
|
27
40
|
host: Database host (unused for DuckDB — it is an in-process, file-backed
|
|
28
|
-
database with no network endpoint
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
for
|
|
41
|
+
database with no network endpoint — and for Snowflake/BigQuery,
|
|
42
|
+
which connect by `account` / `project`)
|
|
43
|
+
port: Database port (unused for DuckDB/Snowflake/BigQuery; required
|
|
44
|
+
for every other backend)
|
|
45
|
+
user: Database user (unused for DuckDB/BigQuery; required explicitly
|
|
46
|
+
for Snowflake)
|
|
47
|
+
password: Database password (unused for DuckDB/BigQuery; for Snowflake
|
|
48
|
+
either `password` or `private_key_path` must be set)
|
|
49
|
+
database: Connection-target database (PostgreSQL/MySQL/MariaDB; the
|
|
50
|
+
session default database for Snowflake; unused for DuckDB — use
|
|
51
|
+
`path` instead)
|
|
34
52
|
path: Path to the DuckDB database file, or ":memory:" for a transient
|
|
35
53
|
in-process database (DuckDB only)
|
|
54
|
+
account: Snowflake account identifier (e.g. "myorg-myaccount")
|
|
55
|
+
warehouse: Snowflake virtual warehouse to run load queries on
|
|
56
|
+
role: Snowflake role for the session
|
|
57
|
+
schema_name: Session default schema (YAML key: `schema`; Snowflake only)
|
|
58
|
+
private_key_path: Path to a PEM private key for Snowflake key-pair
|
|
59
|
+
auth (recommended for service accounts)
|
|
60
|
+
private_key_passphrase: Passphrase of the private key, if encrypted
|
|
61
|
+
project: GCP project id billed for the queries (BigQuery only)
|
|
62
|
+
credentials_json_path: Path to a service-account JSON key file
|
|
63
|
+
(BigQuery only; unset -> Application Default Credentials)
|
|
64
|
+
location: Job location, e.g. "EU" (BigQuery only; unset -> inferred
|
|
65
|
+
from the referenced datasets)
|
|
66
|
+
dataset: Default dataset for unqualified table names in load queries
|
|
67
|
+
(BigQuery only)
|
|
68
|
+
api_endpoint: API endpoint override — the BigQuery emulator or a
|
|
69
|
+
private endpoint (BigQuery only)
|
|
36
70
|
internal_database: Database/schema for internal tables
|
|
37
71
|
internal_schema: Schema for internal tables (PostgreSQL/DuckDB only)
|
|
38
72
|
data_database: Database for user data tables
|
|
39
73
|
data_schema: Schema for user data (PostgreSQL/DuckDB only)
|
|
40
|
-
settings: Additional database-specific settings
|
|
74
|
+
settings: Additional database-specific settings (for Snowflake these
|
|
75
|
+
merge into the session parameters)
|
|
41
76
|
"""
|
|
42
77
|
|
|
78
|
+
# `schema_name` carries the YAML key `schema` via its alias (a field
|
|
79
|
+
# literally named `schema` would shadow pydantic's BaseModel attribute).
|
|
80
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
81
|
+
|
|
82
|
+
# Full backends: may hold detectkit state (_dtk_* tables), and therefore
|
|
83
|
+
# also work as hybrid-mode sources.
|
|
84
|
+
STATE_TYPES: ClassVar[frozenset[str]] = frozenset(
|
|
85
|
+
{"clickhouse", "postgres", "mysql", "mariadb", "duckdb"}
|
|
86
|
+
)
|
|
87
|
+
# Source-only backends: connect + execute_query only (no DDL/upserts/
|
|
88
|
+
# locks); valid ONLY as a metric/project `source_profile` (hybrid mode).
|
|
89
|
+
SOURCE_ONLY_TYPES: ClassVar[frozenset[str]] = frozenset({"snowflake", "bigquery"})
|
|
90
|
+
# Backends with no network port to validate (in-process file DB;
|
|
91
|
+
# account/project-resolved cloud drivers).
|
|
92
|
+
_PORTLESS_TYPES: ClassVar[frozenset[str]] = frozenset({"duckdb", "snowflake", "bigquery"})
|
|
93
|
+
|
|
43
94
|
type: str = Field(..., description="Database type")
|
|
44
95
|
host: str = Field(default="localhost", description="Database host (unused for DuckDB)")
|
|
45
96
|
# No default: required for every backend except DuckDB (enforced below by
|
|
@@ -70,6 +121,64 @@ class ProfileConfig(BaseModel):
|
|
|
70
121
|
),
|
|
71
122
|
)
|
|
72
123
|
|
|
124
|
+
# Snowflake-only (source-only backend). host/port are meaningless for it:
|
|
125
|
+
# the driver resolves the endpoint from `account`.
|
|
126
|
+
account: str | None = Field(
|
|
127
|
+
default=None,
|
|
128
|
+
description="Snowflake account identifier, e.g. 'myorg-myaccount' (Snowflake only)",
|
|
129
|
+
)
|
|
130
|
+
warehouse: str | None = Field(
|
|
131
|
+
default=None,
|
|
132
|
+
description="Virtual warehouse to run load queries on (Snowflake only; "
|
|
133
|
+
"falls back to the user's default warehouse when unset)",
|
|
134
|
+
)
|
|
135
|
+
role: str | None = Field(
|
|
136
|
+
default=None,
|
|
137
|
+
description="Role for the session (Snowflake only; user's default role when unset)",
|
|
138
|
+
)
|
|
139
|
+
schema_name: str | None = Field(
|
|
140
|
+
default=None,
|
|
141
|
+
alias="schema",
|
|
142
|
+
description="Session default schema (Snowflake only; YAML key: 'schema')",
|
|
143
|
+
)
|
|
144
|
+
private_key_path: str | None = Field(
|
|
145
|
+
default=None,
|
|
146
|
+
description="Path to a PEM private key for key-pair auth (Snowflake only; "
|
|
147
|
+
"recommended for service accounts over 'password')",
|
|
148
|
+
)
|
|
149
|
+
private_key_passphrase: str | None = Field(
|
|
150
|
+
default=None,
|
|
151
|
+
description="Passphrase of the private key, when it is encrypted (Snowflake only)",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# BigQuery-only (source-only backend). host/port/user/password are
|
|
155
|
+
# meaningless for it: the client resolves the endpoint from `project` and
|
|
156
|
+
# authenticates via a service-account key file or ADC.
|
|
157
|
+
project: str | None = Field(
|
|
158
|
+
default=None,
|
|
159
|
+
description="GCP project id billed for the queries, e.g. 'my-project' (BigQuery only)",
|
|
160
|
+
)
|
|
161
|
+
credentials_json_path: str | None = Field(
|
|
162
|
+
default=None,
|
|
163
|
+
description="Path to a service-account JSON key file (BigQuery only; "
|
|
164
|
+
"unset -> Application Default Credentials)",
|
|
165
|
+
)
|
|
166
|
+
location: str | None = Field(
|
|
167
|
+
default=None,
|
|
168
|
+
description="Job location, e.g. 'EU' (BigQuery only; unset -> inferred "
|
|
169
|
+
"from the referenced datasets)",
|
|
170
|
+
)
|
|
171
|
+
dataset: str | None = Field(
|
|
172
|
+
default=None,
|
|
173
|
+
description="Default dataset for unqualified table names in load queries (BigQuery only)",
|
|
174
|
+
)
|
|
175
|
+
api_endpoint: str | None = Field(
|
|
176
|
+
default=None,
|
|
177
|
+
description="API endpoint override (BigQuery only) — a plain-http endpoint "
|
|
178
|
+
"(the BigQuery emulator) uses anonymous credentials when no key file is "
|
|
179
|
+
"set; https endpoints (regional/private) authenticate normally",
|
|
180
|
+
)
|
|
181
|
+
|
|
73
182
|
# Internal location for _dtk_* tables
|
|
74
183
|
internal_database: str | None = Field(
|
|
75
184
|
default=None, description="Database for internal tables (ClickHouse/MySQL/MariaDB)"
|
|
@@ -90,14 +199,19 @@ class ProfileConfig(BaseModel):
|
|
|
90
199
|
default_factory=dict, description="Additional database settings"
|
|
91
200
|
)
|
|
92
201
|
|
|
202
|
+
@property
|
|
203
|
+
def is_source_only(self) -> bool:
|
|
204
|
+
"""True for profile types valid only as a hybrid-mode ``source_profile``."""
|
|
205
|
+
return self.type in self.SOURCE_ONLY_TYPES
|
|
206
|
+
|
|
93
207
|
@field_validator("type")
|
|
94
208
|
@classmethod
|
|
95
209
|
def validate_type(cls, v: str) -> str:
|
|
96
210
|
"""Validate database type."""
|
|
97
|
-
allowed_types =
|
|
211
|
+
allowed_types = cls.STATE_TYPES | cls.SOURCE_ONLY_TYPES
|
|
98
212
|
if v not in allowed_types:
|
|
99
213
|
raise ValueError(
|
|
100
|
-
f"Invalid database type: {v}. " f"Allowed types: {', '.join(allowed_types)}"
|
|
214
|
+
f"Invalid database type: {v}. " f"Allowed types: {', '.join(sorted(allowed_types))}"
|
|
101
215
|
)
|
|
102
216
|
return v
|
|
103
217
|
|
|
@@ -113,11 +227,49 @@ class ProfileConfig(BaseModel):
|
|
|
113
227
|
|
|
114
228
|
@model_validator(mode="after")
|
|
115
229
|
def _validate_port_required(self) -> "ProfileConfig":
|
|
116
|
-
"""
|
|
117
|
-
if self.type
|
|
230
|
+
"""Server backends need a port; DuckDB is in-process, Snowflake connects by account."""
|
|
231
|
+
if self.type not in self._PORTLESS_TYPES and self.port is None:
|
|
118
232
|
raise ValueError(f"port is required for database type '{self.type}'")
|
|
119
233
|
return self
|
|
120
234
|
|
|
235
|
+
@model_validator(mode="after")
|
|
236
|
+
def _validate_snowflake_fields(self) -> "ProfileConfig":
|
|
237
|
+
"""Snowflake profiles need an account, an explicit user, and one auth method."""
|
|
238
|
+
if self.type != "snowflake":
|
|
239
|
+
return self
|
|
240
|
+
if not self.account:
|
|
241
|
+
raise ValueError(
|
|
242
|
+
"snowflake profiles must set 'account' "
|
|
243
|
+
"(the account identifier, e.g. 'myorg-myaccount')"
|
|
244
|
+
)
|
|
245
|
+
if "user" not in self.model_fields_set:
|
|
246
|
+
# `user` carries a generic default ("default") that would silently
|
|
247
|
+
# reach the Snowflake driver; require it spelled out.
|
|
248
|
+
raise ValueError("snowflake profiles must set 'user' explicitly")
|
|
249
|
+
if not self.password and not self.private_key_path:
|
|
250
|
+
raise ValueError(
|
|
251
|
+
"snowflake profiles must set 'password' or 'private_key_path' "
|
|
252
|
+
"(key-pair auth — recommended for service accounts)"
|
|
253
|
+
)
|
|
254
|
+
return self
|
|
255
|
+
|
|
256
|
+
@model_validator(mode="after")
|
|
257
|
+
def _validate_bigquery_fields(self) -> "ProfileConfig":
|
|
258
|
+
"""BigQuery profiles need an explicit GCP project id.
|
|
259
|
+
|
|
260
|
+
Auth needs nothing further: `credentials_json_path` when set, else
|
|
261
|
+
Application Default Credentials (gcloud ADC / attached service
|
|
262
|
+
account / Workload Identity).
|
|
263
|
+
"""
|
|
264
|
+
if self.type != "bigquery":
|
|
265
|
+
return self
|
|
266
|
+
if not self.project:
|
|
267
|
+
raise ValueError(
|
|
268
|
+
"bigquery profiles must set 'project' (the GCP project id billed "
|
|
269
|
+
"for the queries, e.g. 'my-project')"
|
|
270
|
+
)
|
|
271
|
+
return self
|
|
272
|
+
|
|
121
273
|
def get_internal_location(self) -> str:
|
|
122
274
|
"""
|
|
123
275
|
Get internal location (database or schema).
|
|
@@ -193,11 +345,19 @@ class ProfileConfig(BaseModel):
|
|
|
193
345
|
Database manager instance
|
|
194
346
|
|
|
195
347
|
Raises:
|
|
196
|
-
ValueError: If the database type is unsupported
|
|
197
|
-
connection fields (e.g. PostgreSQL ``database``,
|
|
198
|
-
``path``) are missing
|
|
348
|
+
ValueError: If the database type is unsupported or source-only,
|
|
349
|
+
or required connection fields (e.g. PostgreSQL ``database``,
|
|
350
|
+
DuckDB ``path``) are missing
|
|
199
351
|
ImportError: If the backend's driver is not installed
|
|
200
352
|
"""
|
|
353
|
+
if self.type in self.SOURCE_ONLY_TYPES:
|
|
354
|
+
raise ValueError(
|
|
355
|
+
f"Profile type '{self.type}' is source-only: it can serve as a "
|
|
356
|
+
f"metric/project 'source_profile' (hybrid mode — the metric's load "
|
|
357
|
+
f"SQL runs there while _dtk_* state stays in the state profile), "
|
|
358
|
+
f"but it cannot hold detectkit state. Point --profile/"
|
|
359
|
+
f"default_profile at one of: {', '.join(sorted(self.STATE_TYPES))}."
|
|
360
|
+
)
|
|
201
361
|
if self.type == "clickhouse":
|
|
202
362
|
# `port` is guaranteed non-None here by `_validate_port_required`
|
|
203
363
|
# (every type but "duckdb" requires it at construction time).
|
|
@@ -273,6 +433,53 @@ class ProfileConfig(BaseModel):
|
|
|
273
433
|
else:
|
|
274
434
|
raise ValueError(f"Unsupported database type: {self.type}")
|
|
275
435
|
|
|
436
|
+
def create_source_manager(self) -> SourceDatabaseManager:
|
|
437
|
+
"""
|
|
438
|
+
Create a manager for use as a hybrid-mode SOURCE (read-only load queries).
|
|
439
|
+
|
|
440
|
+
Source-only types build their lightweight query-only manager here;
|
|
441
|
+
full backends route through :meth:`create_manager` unchanged, so any
|
|
442
|
+
state-capable profile keeps working as a source exactly as before.
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
A manager satisfying the minimal execute_query + close contract
|
|
446
|
+
|
|
447
|
+
Raises:
|
|
448
|
+
ValueError: If required connection fields are missing
|
|
449
|
+
ImportError: If the backend's driver is not installed
|
|
450
|
+
"""
|
|
451
|
+
if self.type == "snowflake":
|
|
452
|
+
from detectkit.database.snowflake_manager import SnowflakeSourceManager
|
|
453
|
+
|
|
454
|
+
# Guaranteed by `_validate_snowflake_fields` at construction time.
|
|
455
|
+
assert self.account is not None
|
|
456
|
+
return SnowflakeSourceManager(
|
|
457
|
+
account=self.account,
|
|
458
|
+
user=self.user,
|
|
459
|
+
password=self.password or None,
|
|
460
|
+
private_key_path=self.private_key_path,
|
|
461
|
+
private_key_passphrase=self.private_key_passphrase,
|
|
462
|
+
warehouse=self.warehouse,
|
|
463
|
+
database=self.database,
|
|
464
|
+
schema=self.schema_name,
|
|
465
|
+
role=self.role,
|
|
466
|
+
settings=self.settings,
|
|
467
|
+
)
|
|
468
|
+
if self.type == "bigquery":
|
|
469
|
+
from detectkit.database.bigquery_manager import BigQuerySourceManager
|
|
470
|
+
|
|
471
|
+
# Guaranteed by `_validate_bigquery_fields` at construction time.
|
|
472
|
+
assert self.project is not None
|
|
473
|
+
return BigQuerySourceManager(
|
|
474
|
+
project=self.project,
|
|
475
|
+
credentials_json_path=self.credentials_json_path,
|
|
476
|
+
location=self.location,
|
|
477
|
+
dataset=self.dataset,
|
|
478
|
+
api_endpoint=self.api_endpoint,
|
|
479
|
+
settings=self.settings,
|
|
480
|
+
)
|
|
481
|
+
return self.create_manager()
|
|
482
|
+
|
|
276
483
|
|
|
277
484
|
class ProfilesConfig(BaseModel):
|
|
278
485
|
"""
|
|
@@ -383,6 +590,23 @@ class ProfilesConfig(BaseModel):
|
|
|
383
590
|
profile = self.get_profile(profile_name)
|
|
384
591
|
return profile.create_manager(ensure_locations=ensure_locations)
|
|
385
592
|
|
|
593
|
+
def create_source_manager(self, profile_name: str | None = None) -> SourceDatabaseManager:
|
|
594
|
+
"""
|
|
595
|
+
Create a manager for a profile used as a hybrid-mode SOURCE.
|
|
596
|
+
|
|
597
|
+
Unlike :meth:`create_manager`, source-only profile types (e.g.
|
|
598
|
+
Snowflake) are valid here; full backends behave exactly as with
|
|
599
|
+
:meth:`create_manager`.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
profile_name: Profile name (if None, use default)
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
A manager satisfying the minimal execute_query + close contract
|
|
606
|
+
"""
|
|
607
|
+
profile = self.get_profile(profile_name)
|
|
608
|
+
return profile.create_source_manager()
|
|
609
|
+
|
|
386
610
|
def get_alert_channel_config(self, channel_name: str) -> dict[str, Any]:
|
|
387
611
|
"""
|
|
388
612
|
Get alert channel configuration by name.
|