detectkit 0.64.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.64.0/detectkit.egg-info → detectkit-0.65.0}/PKG-INFO +7 -2
- {detectkit-0.64.0 → detectkit-0.65.0}/README.md +2 -1
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/__init__.py +1 -1
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/CLAUDE.section.md +1 -1
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/metrics.md +2 -2
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/overview.md +3 -2
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/project.md +38 -6
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/config/profile.py +84 -16
- detectkit-0.65.0/detectkit/database/bigquery_manager.py +229 -0
- {detectkit-0.64.0 → detectkit-0.65.0/detectkit.egg-info}/PKG-INFO +7 -2
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit.egg-info/SOURCES.txt +1 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit.egg-info/requires.txt +5 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/pyproject.toml +7 -1
- {detectkit-0.64.0 → detectkit-0.65.0}/LICENSE +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/MANIFEST.in +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/discord.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/googlechat.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/ntfy.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/teams.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/axis_spec.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/result.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/runner.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/autotune.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/mcp.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/osi.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/tune.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/ui.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/main.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/config/metric_io.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/core/models.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/duckdb_manager.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/snowflake_manager.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/source_manager.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/statistical/autoreg.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/loaders/errors.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/mcp/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/mcp/context.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/mcp/errors.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/mcp/serialize.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/mcp/server.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/mcp/tools.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/semantic/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/semantic/errors.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/semantic/exporter.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/semantic/importer.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/semantic/osi_model.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/semantic/query_gen.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/tuning/assets/tune.js +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/tuning/config_writer.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/tuning/html.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/tuning/payload.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/tuning/server.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/ui/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/ui/assets/ui.js +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/ui/html.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/ui/jobs.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/ui/metric_files.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/ui/overview.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/ui/server.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/requirements.txt +0 -0
- {detectkit-0.64.0 → detectkit-0.65.0}/setup.cfg +0 -0
- {detectkit-0.64.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
|
|
@@ -42,12 +42,15 @@ Provides-Extra: duckdb
|
|
|
42
42
|
Requires-Dist: duckdb>=1.1; extra == "duckdb"
|
|
43
43
|
Provides-Extra: snowflake
|
|
44
44
|
Requires-Dist: snowflake-connector-python>=3.12; extra == "snowflake"
|
|
45
|
+
Provides-Extra: bigquery
|
|
46
|
+
Requires-Dist: google-cloud-bigquery>=3.15; extra == "bigquery"
|
|
45
47
|
Provides-Extra: all-db
|
|
46
48
|
Requires-Dist: clickhouse-driver>=0.2.0; extra == "all-db"
|
|
47
49
|
Requires-Dist: psycopg2-binary>=2.9.0; extra == "all-db"
|
|
48
50
|
Requires-Dist: pymysql>=1.0.0; extra == "all-db"
|
|
49
51
|
Requires-Dist: duckdb>=1.1; extra == "all-db"
|
|
50
52
|
Requires-Dist: snowflake-connector-python>=3.12; extra == "all-db"
|
|
53
|
+
Requires-Dist: google-cloud-bigquery>=3.15; extra == "all-db"
|
|
51
54
|
Provides-Extra: prophet
|
|
52
55
|
Requires-Dist: prophet>=1.1.0; extra == "prophet"
|
|
53
56
|
Provides-Extra: timesfm
|
|
@@ -65,6 +68,7 @@ Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
|
|
|
65
68
|
Requires-Dist: pymysql>=1.0.0; extra == "all"
|
|
66
69
|
Requires-Dist: duckdb>=1.1; extra == "all"
|
|
67
70
|
Requires-Dist: snowflake-connector-python>=3.12; extra == "all"
|
|
71
|
+
Requires-Dist: google-cloud-bigquery>=3.15; extra == "all"
|
|
68
72
|
Requires-Dist: prophet>=1.1.0; extra == "all"
|
|
69
73
|
Requires-Dist: timesfm>=0.1.0; extra == "all"
|
|
70
74
|
Requires-Dist: sqlglot>=20.0; extra == "all"
|
|
@@ -110,7 +114,7 @@ Dynamic: license-file
|
|
|
110
114
|
- **@mentions** — tag users/groups in alerts, each channel formats natively
|
|
111
115
|
- **Alert lifecycle** — consecutive anomalies, fraction-of-window rule (`anomaly_window` + `min_anomaly_share`), cooldown, recovery notifications, no-data alerts
|
|
112
116
|
- **Project-level error alerts** — catch DB outages and pipeline crashes once per run
|
|
113
|
-
- **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed); Snowflake as
|
|
117
|
+
- **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed); Snowflake and BigQuery as hybrid-mode sources
|
|
114
118
|
- **Hybrid mode** — read metric SQL from a billed-per-query warehouse while all pipeline state lives in a cheap local database
|
|
115
119
|
- **Idempotent** — resume from interruptions, no duplicate processing
|
|
116
120
|
- **CLI** — `dtk init`, `dtk run --select`, `dtk unlock`, `dtk clean`, tag-based selectors
|
|
@@ -128,6 +132,7 @@ With database drivers:
|
|
|
128
132
|
pip install detectkit[clickhouse] # ClickHouse
|
|
129
133
|
pip install detectkit[duckdb] # DuckDB - single file, no server
|
|
130
134
|
pip install detectkit[snowflake] # Snowflake - source-only (hybrid mode)
|
|
135
|
+
pip install detectkit[bigquery] # BigQuery - source-only (hybrid mode)
|
|
131
136
|
pip install detectkit[all-db] # All databases
|
|
132
137
|
```
|
|
133
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); Snowflake as
|
|
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
|
|
@@ -42,6 +42,7 @@ With database drivers:
|
|
|
42
42
|
pip install detectkit[clickhouse] # ClickHouse
|
|
43
43
|
pip install detectkit[duckdb] # DuckDB - single file, no server
|
|
44
44
|
pip install detectkit[snowflake] # Snowflake - source-only (hybrid mode)
|
|
45
|
+
pip install detectkit[bigquery] # BigQuery - source-only (hybrid mode)
|
|
45
46
|
pip install detectkit[all-db] # All databases
|
|
46
47
|
```
|
|
47
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 / Snowflake (source-only, hybrid mode)): 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
|
|
@@ -80,8 +80,8 @@ 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
82
|
The `source_profile` may point at a **source-only** warehouse profile such as
|
|
83
|
-
`snowflake` (usable *only* as a source, never for `_dtk_*` state
|
|
84
|
-
`project.md`), which is exactly the case hybrid mode is built for.
|
|
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.
|
|
85
85
|
See the [Hybrid Mode guide](https://dtk.pipelab.dev/guides/hybrid-mode/).
|
|
86
86
|
|
|
87
87
|
`ai_context` is **OSI-compatible grounding** you can add to any metric with **no
|
|
@@ -7,8 +7,9 @@ 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
9
|
(DuckDB additionally needs no server at all — just a local file); **Snowflake**
|
|
10
|
-
|
|
11
|
-
load SQL, but `_dtk_*` state must live in one of the full
|
|
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).
|
|
12
13
|
|
|
13
14
|
## The pipeline: load → detect → alert
|
|
14
15
|
|
|
@@ -58,8 +58,8 @@ profile whose database runs metric SQL, while *every* `_dtk_*` table
|
|
|
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
60
|
both = today's behavior (one profile does everything). The source profile can
|
|
61
|
-
be a full backend **or** a **source-only** type
|
|
62
|
-
here, never as the state profile
|
|
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
|
|
63
63
|
metric-SQL query is affected — detect/alert and every other command
|
|
64
64
|
(`dtk autotune`/`tune`/`ui`/`clean`/`unlock`) only ever touch the state
|
|
65
65
|
profile. A source-side failure raises `SourceDatabaseError` (message leads
|
|
@@ -140,14 +140,14 @@ alert_channels:
|
|
|
140
140
|
### Database profiles
|
|
141
141
|
|
|
142
142
|
> ClickHouse, PostgreSQL, MySQL, MariaDB and DuckDB are all supported as
|
|
143
|
-
> **state** backends (they hold the `_dtk_*` tables); **Snowflake**
|
|
144
|
-
> **source-only** — see
|
|
143
|
+
> **state** backends (they hold the `_dtk_*` tables); **Snowflake** and
|
|
144
|
+
> **BigQuery** are **source-only** — see their blocks below.
|
|
145
145
|
> ClickHouse/MySQL/MariaDB use two *databases*; PostgreSQL connects to one
|
|
146
146
|
> `database` and uses two *schemas*; DuckDB is a single *file* (or `:memory:`
|
|
147
147
|
> for tests) holding two *schemas*.
|
|
148
148
|
> `dtk init --db-type {clickhouse,postgres,mysql,mariadb}` scaffolds the right
|
|
149
|
-
> shape — DuckDB and
|
|
150
|
-
> `--db-type` choice).
|
|
149
|
+
> shape — DuckDB, Snowflake and BigQuery profiles are written by hand (not yet
|
|
150
|
+
> a `--db-type` choice).
|
|
151
151
|
|
|
152
152
|
**ClickHouse**:
|
|
153
153
|
```yaml
|
|
@@ -250,6 +250,38 @@ profiles:
|
|
|
250
250
|
> hybrid mode (load from Snowflake, keep cheap local state) is the point.
|
|
251
251
|
> `pip install 'detectkit[snowflake]'`.
|
|
252
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
|
+
|
|
253
285
|
### Alert channels
|
|
254
286
|
|
|
255
287
|
Defined once in `profiles.yml`, referenced by name in each metric's
|
|
@@ -29,22 +29,23 @@ class ProfileConfig(BaseModel):
|
|
|
29
29
|
duckdb) can hold detectkit state (the ``_dtk_*`` tables) — and, since
|
|
30
30
|
state managers also expose ``execute_query``, double as hybrid-mode
|
|
31
31
|
sources.
|
|
32
|
-
- **Source-only backends** (``SOURCE_ONLY_TYPES``: snowflake)
|
|
33
|
-
only connect + read query. They are valid **only** as a
|
|
34
|
-
``source_profile``; :meth:`create_manager` refuses them
|
|
35
|
-
profile with a clear error.
|
|
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
36
|
|
|
37
37
|
Attributes:
|
|
38
38
|
type: Database type ("clickhouse", "postgres", "mysql", "mariadb",
|
|
39
|
-
"duckdb"; source-only: "snowflake")
|
|
39
|
+
"duckdb"; source-only: "snowflake", "bigquery")
|
|
40
40
|
host: Database host (unused for DuckDB — it is an in-process, file-backed
|
|
41
|
-
database with no network endpoint — and for Snowflake,
|
|
42
|
-
|
|
43
|
-
port: Database port (unused for DuckDB/Snowflake; required
|
|
44
|
-
other backend)
|
|
45
|
-
user: Database user (unused for DuckDB; required explicitly
|
|
46
|
-
|
|
47
|
-
|
|
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)
|
|
48
49
|
database: Connection-target database (PostgreSQL/MySQL/MariaDB; the
|
|
49
50
|
session default database for Snowflake; unused for DuckDB — use
|
|
50
51
|
`path` instead)
|
|
@@ -57,6 +58,15 @@ class ProfileConfig(BaseModel):
|
|
|
57
58
|
private_key_path: Path to a PEM private key for Snowflake key-pair
|
|
58
59
|
auth (recommended for service accounts)
|
|
59
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)
|
|
60
70
|
internal_database: Database/schema for internal tables
|
|
61
71
|
internal_schema: Schema for internal tables (PostgreSQL/DuckDB only)
|
|
62
72
|
data_database: Database for user data tables
|
|
@@ -76,10 +86,10 @@ class ProfileConfig(BaseModel):
|
|
|
76
86
|
)
|
|
77
87
|
# Source-only backends: connect + execute_query only (no DDL/upserts/
|
|
78
88
|
# locks); valid ONLY as a metric/project `source_profile` (hybrid mode).
|
|
79
|
-
SOURCE_ONLY_TYPES: ClassVar[frozenset[str]] = frozenset({"snowflake"})
|
|
80
|
-
# Backends with no network port to validate (in-process file DB;
|
|
81
|
-
# account-resolved cloud
|
|
82
|
-
_PORTLESS_TYPES: ClassVar[frozenset[str]] = frozenset({"duckdb", "snowflake"})
|
|
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"})
|
|
83
93
|
|
|
84
94
|
type: str = Field(..., description="Database type")
|
|
85
95
|
host: str = Field(default="localhost", description="Database host (unused for DuckDB)")
|
|
@@ -141,6 +151,34 @@ class ProfileConfig(BaseModel):
|
|
|
141
151
|
description="Passphrase of the private key, when it is encrypted (Snowflake only)",
|
|
142
152
|
)
|
|
143
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
|
+
|
|
144
182
|
# Internal location for _dtk_* tables
|
|
145
183
|
internal_database: str | None = Field(
|
|
146
184
|
default=None, description="Database for internal tables (ClickHouse/MySQL/MariaDB)"
|
|
@@ -215,6 +253,23 @@ class ProfileConfig(BaseModel):
|
|
|
215
253
|
)
|
|
216
254
|
return self
|
|
217
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
|
+
|
|
218
273
|
def get_internal_location(self) -> str:
|
|
219
274
|
"""
|
|
220
275
|
Get internal location (database or schema).
|
|
@@ -410,6 +465,19 @@ class ProfileConfig(BaseModel):
|
|
|
410
465
|
role=self.role,
|
|
411
466
|
settings=self.settings,
|
|
412
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
|
+
)
|
|
413
481
|
return self.create_manager()
|
|
414
482
|
|
|
415
483
|
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""BigQuery **source-only** database manager.
|
|
2
|
+
|
|
3
|
+
BigQuery is supported as a hybrid-mode *source* (a metric's ``load`` SQL runs
|
|
4
|
+
against it) but **not** as a state backend: it implements only the minimal
|
|
5
|
+
:class:`~detectkit.database.source_manager.SourceDatabaseManager` contract
|
|
6
|
+
(connect + :meth:`execute_query` + :meth:`close`), with no DDL, upsert, lock or
|
|
7
|
+
internal-table machinery. :meth:`ProfileConfig.create_manager` refuses a
|
|
8
|
+
``bigquery`` profile as state and routes it here via
|
|
9
|
+
:meth:`ProfileConfig.create_source_manager`; the ``_dtk_*`` state lives in a
|
|
10
|
+
cheaper local backend (DuckDB/PostgreSQL/ClickHouse).
|
|
11
|
+
|
|
12
|
+
Behaviors that are deliberate and load-bearing:
|
|
13
|
+
|
|
14
|
+
- **Eager connectivity probe, with bounded retries.** ``bigquery.Client(...)``
|
|
15
|
+
alone performs no network I/O, so construction runs a ``SELECT 1`` probe — a
|
|
16
|
+
query with no table references processes 0 bytes and is free on on-demand
|
|
17
|
+
billing — to fail fast on bad credentials/project at hybrid-pool build
|
|
18
|
+
(parity with the other backends' eager connect). The probe also applies
|
|
19
|
+
:meth:`_job_config`, so a typo in ``settings`` or a bad ``dataset`` surfaces
|
|
20
|
+
immediately. The client library's default retry treats network-level errors
|
|
21
|
+
(connection refused, DNS) as transient and would retry them for 10+ minutes;
|
|
22
|
+
the probe caps that at ``_PROBE_TIMEOUT_S`` and load queries at
|
|
23
|
+
``_RETRY_TIMEOUT_S`` / ``_JOB_RETRY_TIMEOUT_S``, so an unreachable endpoint
|
|
24
|
+
fails the run in seconds-to-minutes, not silently stalls it.
|
|
25
|
+
- **Auth resolution.** ``credentials_json_path`` (a service-account key file)
|
|
26
|
+
when set; otherwise **Application Default Credentials** (gcloud ADC, an
|
|
27
|
+
attached service account, or Workload Identity). A **plain-``http://``**
|
|
28
|
+
``api_endpoint`` — the BigQuery emulator — switches to anonymous credentials
|
|
29
|
+
so no ADC lookup is attempted; an ``https://`` endpoint override (a regional
|
|
30
|
+
``*.rep.googleapis.com`` endpoint, Private Service Connect) is a real,
|
|
31
|
+
authenticated Google endpoint and resolves auth exactly as above.
|
|
32
|
+
- **Timestamps.** BigQuery ``TIMESTAMP`` columns come back as tz-aware UTC
|
|
33
|
+
datetimes — the loader converts tz-aware values to naive UTC since v0.62.0.
|
|
34
|
+
``DATETIME`` comes back naive and is taken verbatim; use ``TIMESTAMP`` (or
|
|
35
|
+
cast) for the metric's ``timestamp`` column unless the naive values already
|
|
36
|
+
are UTC.
|
|
37
|
+
- **No column-name folding.** Unlike Snowflake, BigQuery preserves the case of
|
|
38
|
+
column aliases, so ``SELECT ts AS timestamp`` reaches the loader unchanged.
|
|
39
|
+
|
|
40
|
+
Billing note: on-demand queries bill a 10 MiB minimum of bytes processed per
|
|
41
|
+
referenced table, so frequent small monitoring queries are disproportionately
|
|
42
|
+
expensive — the whole point of hybrid mode is to load from BigQuery and keep
|
|
43
|
+
cheap state elsewhere. A ``settings: {maximum_bytes_billed: ...}`` guardrail
|
|
44
|
+
caps what a single load query may scan.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
from typing import Any
|
|
50
|
+
|
|
51
|
+
from detectkit.database.source_manager import SourceDatabaseManager
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class BigQuerySourceManager(SourceDatabaseManager):
|
|
55
|
+
"""Read-only BigQuery connection for hybrid-mode source profiles.
|
|
56
|
+
|
|
57
|
+
Constructs the client and runs the free ``SELECT 1`` probe eagerly (see
|
|
58
|
+
the module docstring for the probe, auth resolution and timestamp rules).
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
project: GCP project id billed for the queries (e.g. ``"my-project"``)
|
|
62
|
+
credentials_json_path: Path to a service-account JSON key file
|
|
63
|
+
(unset -> Application Default Credentials)
|
|
64
|
+
location: Job location (e.g. ``"EU"``; unset -> BigQuery infers it
|
|
65
|
+
from the referenced datasets)
|
|
66
|
+
dataset: Default dataset for unqualified table names in queries
|
|
67
|
+
api_endpoint: Override the API endpoint — for the BigQuery emulator
|
|
68
|
+
(e.g. ``"http://localhost:9050"``) or private endpoints; a
|
|
69
|
+
plain-``http://`` endpoint without a key file switches auth to
|
|
70
|
+
anonymous credentials (``https://`` endpoints authenticate
|
|
71
|
+
normally via key file or ADC)
|
|
72
|
+
settings: Extra ``QueryJobConfig`` attributes applied to every query
|
|
73
|
+
(e.g. ``maximum_bytes_billed``, ``labels``, ``use_query_cache``);
|
|
74
|
+
unknown attribute names are rejected at the probe
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
# The client library's DEFAULT_RETRY/DEFAULT_JOB_RETRY treat network-level
|
|
78
|
+
# errors as transient and retry them for 600/2400 seconds — an unreachable
|
|
79
|
+
# endpoint would stall a scheduled run for tens of minutes. These caps keep
|
|
80
|
+
# the probe genuinely fail-fast and load queries bounded (long-RUNNING
|
|
81
|
+
# queries are unaffected: waiting on a healthy job is not a retry).
|
|
82
|
+
_PROBE_TIMEOUT_S = 30.0
|
|
83
|
+
_RETRY_TIMEOUT_S = 120.0
|
|
84
|
+
_JOB_RETRY_TIMEOUT_S = 600.0
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
project: str,
|
|
89
|
+
credentials_json_path: str | None = None,
|
|
90
|
+
location: str | None = None,
|
|
91
|
+
dataset: str | None = None,
|
|
92
|
+
api_endpoint: str | None = None,
|
|
93
|
+
settings: dict[str, Any] | None = None,
|
|
94
|
+
) -> None:
|
|
95
|
+
try:
|
|
96
|
+
from google.cloud import bigquery
|
|
97
|
+
except ImportError as exc:
|
|
98
|
+
raise ImportError(
|
|
99
|
+
"google-cloud-bigquery is not installed. "
|
|
100
|
+
"Install with: pip install detectkit[bigquery]"
|
|
101
|
+
) from exc
|
|
102
|
+
|
|
103
|
+
self._project = project
|
|
104
|
+
self._credentials_json_path = credentials_json_path
|
|
105
|
+
self._location = location
|
|
106
|
+
self._dataset = dataset
|
|
107
|
+
self._api_endpoint = api_endpoint
|
|
108
|
+
self._settings = settings or {}
|
|
109
|
+
|
|
110
|
+
self._closed = False
|
|
111
|
+
self._client = bigquery.Client(**self._client_kwargs())
|
|
112
|
+
try:
|
|
113
|
+
self._probe()
|
|
114
|
+
except Exception:
|
|
115
|
+
# Don't leak the just-built client (and the HTTP session the probe
|
|
116
|
+
# request lazily created) — the hybrid pool caches only the error.
|
|
117
|
+
self.close()
|
|
118
|
+
raise
|
|
119
|
+
|
|
120
|
+
def _client_kwargs(self) -> dict[str, Any]:
|
|
121
|
+
"""Build ``bigquery.Client(...)`` kwargs without any network I/O.
|
|
122
|
+
|
|
123
|
+
The testable seam: it resolves auth (service-account key file → ADC →
|
|
124
|
+
anonymous for the emulator path) and includes ``location`` /
|
|
125
|
+
``client_options`` only when set.
|
|
126
|
+
"""
|
|
127
|
+
kwargs: dict[str, Any] = {"project": self._project}
|
|
128
|
+
|
|
129
|
+
if self._credentials_json_path is not None:
|
|
130
|
+
from google.oauth2 import service_account
|
|
131
|
+
|
|
132
|
+
kwargs["credentials"] = service_account.Credentials.from_service_account_file(
|
|
133
|
+
self._credentials_json_path
|
|
134
|
+
)
|
|
135
|
+
elif self._api_endpoint is not None and self._api_endpoint.startswith("http://"):
|
|
136
|
+
# Plain-HTTP endpoint = the emulator: skip the ADC lookup entirely
|
|
137
|
+
# (it would fail on a machine with no gcloud auth). An https
|
|
138
|
+
# endpoint override (regional/private) is a real, authenticated
|
|
139
|
+
# Google endpoint and falls through to ADC below.
|
|
140
|
+
from google.auth.credentials import AnonymousCredentials
|
|
141
|
+
|
|
142
|
+
kwargs["credentials"] = AnonymousCredentials()
|
|
143
|
+
# else: Application Default Credentials, resolved by the client itself.
|
|
144
|
+
|
|
145
|
+
if self._location is not None:
|
|
146
|
+
kwargs["location"] = self._location
|
|
147
|
+
if self._api_endpoint is not None:
|
|
148
|
+
kwargs["client_options"] = {"api_endpoint": self._api_endpoint}
|
|
149
|
+
|
|
150
|
+
return kwargs
|
|
151
|
+
|
|
152
|
+
def _job_config(self) -> Any:
|
|
153
|
+
"""Build the per-query ``QueryJobConfig``, or ``None`` when trivial.
|
|
154
|
+
|
|
155
|
+
``dataset`` becomes ``default_dataset`` (unqualified table names in the
|
|
156
|
+
metric SQL resolve against it); each ``settings`` key must name a real
|
|
157
|
+
``QueryJobConfig`` property (typos would otherwise be silently ignored
|
|
158
|
+
attributes — rejected here instead, surfacing at the connect probe).
|
|
159
|
+
"""
|
|
160
|
+
if self._dataset is None and not self._settings:
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
from google.cloud import bigquery
|
|
164
|
+
|
|
165
|
+
config = bigquery.QueryJobConfig()
|
|
166
|
+
if self._dataset is not None:
|
|
167
|
+
config.default_dataset = bigquery.DatasetReference(self._project, self._dataset)
|
|
168
|
+
for key, value in self._settings.items():
|
|
169
|
+
if not isinstance(getattr(type(config), key, None), property):
|
|
170
|
+
raise ValueError(
|
|
171
|
+
f"Unknown BigQuery query setting '{key}': settings keys must be "
|
|
172
|
+
f"QueryJobConfig attributes (e.g. maximum_bytes_billed, labels)"
|
|
173
|
+
)
|
|
174
|
+
setattr(config, key, value)
|
|
175
|
+
return config
|
|
176
|
+
|
|
177
|
+
def _probe(self) -> None:
|
|
178
|
+
"""Fail fast on bad credentials/project/settings at construction.
|
|
179
|
+
|
|
180
|
+
``SELECT 1`` references no table, processes 0 bytes and is free on
|
|
181
|
+
on-demand billing; it exercises exactly the permission the loader
|
|
182
|
+
needs (``bigquery.jobs.create`` on the project). Retries are capped
|
|
183
|
+
at ``_PROBE_TIMEOUT_S`` (and job-level retry disabled), so an
|
|
184
|
+
unreachable endpoint fails the pool build in seconds instead of the
|
|
185
|
+
library default's 10+ minutes of connection-error retries.
|
|
186
|
+
"""
|
|
187
|
+
from google.cloud.bigquery.retry import DEFAULT_RETRY
|
|
188
|
+
|
|
189
|
+
retry = DEFAULT_RETRY.with_timeout(self._PROBE_TIMEOUT_S)
|
|
190
|
+
self._client.query(
|
|
191
|
+
"SELECT 1", job_config=self._job_config(), retry=retry, job_retry=None
|
|
192
|
+
).result(retry=retry, timeout=self._PROBE_TIMEOUT_S)
|
|
193
|
+
|
|
194
|
+
def execute_query(
|
|
195
|
+
self, query: str, params: dict[str, Any] | None = None
|
|
196
|
+
) -> list[dict[str, Any]]:
|
|
197
|
+
"""Execute a read query and return all rows as column-keyed dicts.
|
|
198
|
+
|
|
199
|
+
BigQuery takes typed ``ScalarQueryParameter`` objects rather than
|
|
200
|
+
DB-API params; the loader renders its SQL with Jinja and always calls
|
|
201
|
+
this param-less, so a non-empty ``params`` is rejected instead of
|
|
202
|
+
being silently dropped. DDL/DML statements (used by tests to seed the
|
|
203
|
+
emulator) return an empty row set.
|
|
204
|
+
"""
|
|
205
|
+
if params:
|
|
206
|
+
raise ValueError(
|
|
207
|
+
"BigQuerySourceManager does not support query params; "
|
|
208
|
+
"render values into the SQL instead (the loader always does)"
|
|
209
|
+
)
|
|
210
|
+
from google.cloud.bigquery.retry import DEFAULT_JOB_RETRY, DEFAULT_RETRY
|
|
211
|
+
|
|
212
|
+
# Bounded retries (see the class constants): network-level errors stop
|
|
213
|
+
# being retried after minutes, not tens of minutes. No `timeout=` on
|
|
214
|
+
# result() — a long-running healthy query may legitimately take longer.
|
|
215
|
+
retry = DEFAULT_RETRY.with_timeout(self._RETRY_TIMEOUT_S)
|
|
216
|
+
rows = self._client.query(
|
|
217
|
+
query,
|
|
218
|
+
job_config=self._job_config(),
|
|
219
|
+
retry=retry,
|
|
220
|
+
job_retry=DEFAULT_JOB_RETRY.with_timeout(self._JOB_RETRY_TIMEOUT_S),
|
|
221
|
+
).result(retry=retry)
|
|
222
|
+
return [dict(row.items()) for row in rows]
|
|
223
|
+
|
|
224
|
+
def close(self) -> None:
|
|
225
|
+
"""Close the client's HTTP transport. Safe to call more than once."""
|
|
226
|
+
if self._closed:
|
|
227
|
+
return
|
|
228
|
+
self._closed = True
|
|
229
|
+
self._client.close()
|
|
@@ -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
|
|
@@ -42,12 +42,15 @@ Provides-Extra: duckdb
|
|
|
42
42
|
Requires-Dist: duckdb>=1.1; extra == "duckdb"
|
|
43
43
|
Provides-Extra: snowflake
|
|
44
44
|
Requires-Dist: snowflake-connector-python>=3.12; extra == "snowflake"
|
|
45
|
+
Provides-Extra: bigquery
|
|
46
|
+
Requires-Dist: google-cloud-bigquery>=3.15; extra == "bigquery"
|
|
45
47
|
Provides-Extra: all-db
|
|
46
48
|
Requires-Dist: clickhouse-driver>=0.2.0; extra == "all-db"
|
|
47
49
|
Requires-Dist: psycopg2-binary>=2.9.0; extra == "all-db"
|
|
48
50
|
Requires-Dist: pymysql>=1.0.0; extra == "all-db"
|
|
49
51
|
Requires-Dist: duckdb>=1.1; extra == "all-db"
|
|
50
52
|
Requires-Dist: snowflake-connector-python>=3.12; extra == "all-db"
|
|
53
|
+
Requires-Dist: google-cloud-bigquery>=3.15; extra == "all-db"
|
|
51
54
|
Provides-Extra: prophet
|
|
52
55
|
Requires-Dist: prophet>=1.1.0; extra == "prophet"
|
|
53
56
|
Provides-Extra: timesfm
|
|
@@ -65,6 +68,7 @@ Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
|
|
|
65
68
|
Requires-Dist: pymysql>=1.0.0; extra == "all"
|
|
66
69
|
Requires-Dist: duckdb>=1.1; extra == "all"
|
|
67
70
|
Requires-Dist: snowflake-connector-python>=3.12; extra == "all"
|
|
71
|
+
Requires-Dist: google-cloud-bigquery>=3.15; extra == "all"
|
|
68
72
|
Requires-Dist: prophet>=1.1.0; extra == "all"
|
|
69
73
|
Requires-Dist: timesfm>=0.1.0; extra == "all"
|
|
70
74
|
Requires-Dist: sqlglot>=20.0; extra == "all"
|
|
@@ -110,7 +114,7 @@ Dynamic: license-file
|
|
|
110
114
|
- **@mentions** — tag users/groups in alerts, each channel formats natively
|
|
111
115
|
- **Alert lifecycle** — consecutive anomalies, fraction-of-window rule (`anomaly_window` + `min_anomaly_share`), cooldown, recovery notifications, no-data alerts
|
|
112
116
|
- **Project-level error alerts** — catch DB outages and pipeline crashes once per run
|
|
113
|
-
- **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed); Snowflake as
|
|
117
|
+
- **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed); Snowflake and BigQuery as hybrid-mode sources
|
|
114
118
|
- **Hybrid mode** — read metric SQL from a billed-per-query warehouse while all pipeline state lives in a cheap local database
|
|
115
119
|
- **Idempotent** — resume from interruptions, no duplicate processing
|
|
116
120
|
- **CLI** — `dtk init`, `dtk run --select`, `dtk unlock`, `dtk clean`, tag-based selectors
|
|
@@ -128,6 +132,7 @@ With database drivers:
|
|
|
128
132
|
pip install detectkit[clickhouse] # ClickHouse
|
|
129
133
|
pip install detectkit[duckdb] # DuckDB - single file, no server
|
|
130
134
|
pip install detectkit[snowflake] # Snowflake - source-only (hybrid mode)
|
|
135
|
+
pip install detectkit[bigquery] # BigQuery - source-only (hybrid mode)
|
|
131
136
|
pip install detectkit[all-db] # All databases
|
|
132
137
|
```
|
|
133
138
|
|
|
@@ -90,6 +90,7 @@ detectkit/core/interval.py
|
|
|
90
90
|
detectkit/core/models.py
|
|
91
91
|
detectkit/database/__init__.py
|
|
92
92
|
detectkit/database/_sql_manager.py
|
|
93
|
+
detectkit/database/bigquery_manager.py
|
|
93
94
|
detectkit/database/clickhouse_manager.py
|
|
94
95
|
detectkit/database/duckdb_manager.py
|
|
95
96
|
detectkit/database/manager.py
|
|
@@ -16,6 +16,7 @@ psycopg2-binary>=2.9.0
|
|
|
16
16
|
pymysql>=1.0.0
|
|
17
17
|
duckdb>=1.1
|
|
18
18
|
snowflake-connector-python>=3.12
|
|
19
|
+
google-cloud-bigquery>=3.15
|
|
19
20
|
prophet>=1.1.0
|
|
20
21
|
timesfm>=0.1.0
|
|
21
22
|
sqlglot>=20.0
|
|
@@ -27,6 +28,10 @@ psycopg2-binary>=2.9.0
|
|
|
27
28
|
pymysql>=1.0.0
|
|
28
29
|
duckdb>=1.1
|
|
29
30
|
snowflake-connector-python>=3.12
|
|
31
|
+
google-cloud-bigquery>=3.15
|
|
32
|
+
|
|
33
|
+
[bigquery]
|
|
34
|
+
google-cloud-bigquery>=3.15
|
|
30
35
|
|
|
31
36
|
[clickhouse]
|
|
32
37
|
clickhouse-driver>=0.2.0
|