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.
Files changed (172) hide show
  1. {detectkit-0.64.0/detectkit.egg-info → detectkit-0.65.0}/PKG-INFO +7 -2
  2. {detectkit-0.64.0 → detectkit-0.65.0}/README.md +2 -1
  3. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/__init__.py +1 -1
  4. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/CLAUDE.section.md +1 -1
  5. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/metrics.md +2 -2
  6. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/overview.md +3 -2
  7. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/project.md +38 -6
  8. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/config/profile.py +84 -16
  9. detectkit-0.65.0/detectkit/database/bigquery_manager.py +229 -0
  10. {detectkit-0.64.0 → detectkit-0.65.0/detectkit.egg-info}/PKG-INFO +7 -2
  11. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit.egg-info/SOURCES.txt +1 -0
  12. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit.egg-info/requires.txt +5 -0
  13. {detectkit-0.64.0 → detectkit-0.65.0}/pyproject.toml +7 -1
  14. {detectkit-0.64.0 → detectkit-0.65.0}/LICENSE +0 -0
  15. {detectkit-0.64.0 → detectkit-0.65.0}/MANIFEST.in +0 -0
  16. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/__init__.py +0 -0
  17. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/__init__.py +0 -0
  18. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/base.py +0 -0
  19. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/branding.py +0 -0
  20. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/discord.py +0 -0
  21. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/email.py +0 -0
  22. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/factory.py +0 -0
  23. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/googlechat.py +0 -0
  24. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/mattermost.py +0 -0
  25. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/ntfy.py +0 -0
  26. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/slack.py +0 -0
  27. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/teams.py +0 -0
  28. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/telegram.py +0 -0
  29. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/channels/webhook.py +0 -0
  30. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  31. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_base.py +0 -0
  32. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  33. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
  34. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  35. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  36. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
  37. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  38. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  39. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/__init__.py +0 -0
  40. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/_base.py +0 -0
  41. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/_types.py +0 -0
  42. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/autotuner.py +0 -0
  43. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/axis_spec.py +0 -0
  44. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/config_emitter.py +0 -0
  45. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/crossval.py +0 -0
  46. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/detector_select.py +0 -0
  47. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/distribution.py +0 -0
  48. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/grid_search.py +0 -0
  49. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/labels.py +0 -0
  50. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/result.py +0 -0
  51. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/runner.py +0 -0
  52. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/scoring.py +0 -0
  53. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/seasonality_search.py +0 -0
  54. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/settings.py +0 -0
  55. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/autotune/window_select.py +0 -0
  56. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/__init__.py +0 -0
  57. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/_output.py +0 -0
  58. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  59. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
  60. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
  61. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  62. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
  63. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  64. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  65. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  66. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +0 -0
  67. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/__init__.py +0 -0
  68. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/autotune.py +0 -0
  69. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/clean.py +0 -0
  70. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/init.py +0 -0
  71. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/init_claude.py +0 -0
  72. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/mcp.py +0 -0
  73. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/osi.py +0 -0
  74. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/run.py +0 -0
  75. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/test_alert.py +0 -0
  76. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/tune.py +0 -0
  77. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/ui.py +0 -0
  78. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/commands/unlock.py +0 -0
  79. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/cli/main.py +0 -0
  80. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/config/__init__.py +0 -0
  81. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/config/metric_config.py +0 -0
  82. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/config/metric_io.py +0 -0
  83. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/config/project_config.py +0 -0
  84. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/config/validator.py +0 -0
  85. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/core/__init__.py +0 -0
  86. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/core/interval.py +0 -0
  87. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/core/models.py +0 -0
  88. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/__init__.py +0 -0
  89. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/_sql_manager.py +0 -0
  90. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/clickhouse_manager.py +0 -0
  91. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/duckdb_manager.py +0 -0
  92. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/__init__.py +0 -0
  93. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  94. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  95. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_base.py +0 -0
  96. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  97. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_detections.py +0 -0
  98. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  99. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  100. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_schema.py +0 -0
  101. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  102. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/internal_tables/manager.py +0 -0
  103. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/manager.py +0 -0
  104. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/mysql_manager.py +0 -0
  105. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/postgres_manager.py +0 -0
  106. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/snowflake_manager.py +0 -0
  107. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/source_manager.py +0 -0
  108. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/database/tables.py +0 -0
  109. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/__init__.py +0 -0
  110. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/base.py +0 -0
  111. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/factory.py +0 -0
  112. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/seasonality.py +0 -0
  113. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/statistical/__init__.py +0 -0
  114. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  115. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/statistical/autoreg.py +0 -0
  116. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/statistical/iqr.py +0 -0
  117. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/statistical/mad.py +0 -0
  118. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  119. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/detectors/statistical/zscore.py +0 -0
  120. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/loaders/__init__.py +0 -0
  121. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/loaders/errors.py +0 -0
  122. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/loaders/metric_loader.py +0 -0
  123. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/loaders/query_template.py +0 -0
  124. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/mcp/__init__.py +0 -0
  125. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/mcp/context.py +0 -0
  126. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/mcp/errors.py +0 -0
  127. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/mcp/serialize.py +0 -0
  128. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/mcp/server.py +0 -0
  129. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/mcp/tools.py +0 -0
  130. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/orchestration/__init__.py +0 -0
  131. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/orchestration/error_dispatch.py +0 -0
  132. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  133. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  134. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  135. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  136. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  137. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  138. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  139. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/reporting/__init__.py +0 -0
  140. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/reporting/assets/report.js +0 -0
  141. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/reporting/builder.py +0 -0
  142. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/reporting/html_report.py +0 -0
  143. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/semantic/__init__.py +0 -0
  144. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/semantic/errors.py +0 -0
  145. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/semantic/exporter.py +0 -0
  146. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/semantic/importer.py +0 -0
  147. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/semantic/osi_model.py +0 -0
  148. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/semantic/query_gen.py +0 -0
  149. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/tuning/__init__.py +0 -0
  150. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/tuning/assets/tune.js +0 -0
  151. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/tuning/config_writer.py +0 -0
  152. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/tuning/html.py +0 -0
  153. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/tuning/payload.py +0 -0
  154. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/tuning/server.py +0 -0
  155. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/ui/__init__.py +0 -0
  156. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/ui/assets/ui.js +0 -0
  157. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/ui/html.py +0 -0
  158. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/ui/jobs.py +0 -0
  159. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/ui/metric_files.py +0 -0
  160. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/ui/overview.py +0 -0
  161. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/ui/server.py +0 -0
  162. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/utils/__init__.py +0 -0
  163. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/utils/datetime_utils.py +0 -0
  164. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/utils/env_interpolation.py +0 -0
  165. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/utils/json_utils.py +0 -0
  166. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit/utils/stats.py +0 -0
  167. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit.egg-info/dependency_links.txt +0 -0
  168. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit.egg-info/entry_points.txt +0 -0
  169. {detectkit-0.64.0 → detectkit-0.65.0}/detectkit.egg-info/top_level.txt +0 -0
  170. {detectkit-0.64.0 → detectkit-0.65.0}/requirements.txt +0 -0
  171. {detectkit-0.64.0 → detectkit-0.65.0}/setup.cfg +0 -0
  172. {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.64.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 a hybrid-mode source
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 a hybrid-mode source
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.64.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 — see
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
- is supported as a **source-only** backend (hybrid mode — it runs a metric's
11
- load SQL, but `_dtk_*` state must live in one of the full backends).
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 like `snowflake` (valid only
62
- here, never as the state profile). Only the LOAD step's
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** is
144
- > **source-only** — see its block below.
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 Snowflake profiles are written by hand (not yet a
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) implement
33
- only connect + read query. They are valid **only** as a metric/project
34
- ``source_profile``; :meth:`create_manager` refuses them as a state
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, which
42
- connects by `account`)
43
- port: Database port (unused for DuckDB/Snowflake; required for every
44
- other backend)
45
- user: Database user (unused for DuckDB; required explicitly for Snowflake)
46
- password: Database password (unused for DuckDB; for Snowflake either
47
- `password` or `private_key_path` must be set)
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; an
81
- # account-resolved cloud driver).
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.64.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 a hybrid-mode source
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