detectkit 0.63.0__tar.gz → 0.64.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 (171) hide show
  1. {detectkit-0.63.0/detectkit.egg-info → detectkit-0.64.0}/PKG-INFO +7 -2
  2. {detectkit-0.63.0 → detectkit-0.64.0}/README.md +2 -1
  3. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/__init__.py +1 -1
  4. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/assets/claude/CLAUDE.section.md +1 -1
  5. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/assets/claude/rules/metrics.md +3 -0
  6. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/assets/claude/rules/overview.md +3 -1
  7. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/assets/claude/rules/project.md +38 -3
  8. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/commands/run.py +37 -0
  9. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/config/profile.py +173 -17
  10. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/database/manager.py +7 -2
  11. detectkit-0.64.0/detectkit/database/snowflake_manager.py +182 -0
  12. detectkit-0.64.0/detectkit/database/source_manager.py +65 -0
  13. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/loaders/metric_loader.py +8 -6
  14. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/orchestration/task_manager/_base.py +5 -1
  15. {detectkit-0.63.0 → detectkit-0.64.0/detectkit.egg-info}/PKG-INFO +7 -2
  16. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit.egg-info/SOURCES.txt +2 -0
  17. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit.egg-info/requires.txt +5 -0
  18. {detectkit-0.63.0 → detectkit-0.64.0}/pyproject.toml +6 -0
  19. {detectkit-0.63.0 → detectkit-0.64.0}/LICENSE +0 -0
  20. {detectkit-0.63.0 → detectkit-0.64.0}/MANIFEST.in +0 -0
  21. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/__init__.py +0 -0
  22. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/channels/__init__.py +0 -0
  23. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/channels/base.py +0 -0
  24. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/channels/branding.py +0 -0
  25. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/channels/discord.py +0 -0
  26. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/channels/email.py +0 -0
  27. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/channels/factory.py +0 -0
  28. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/channels/googlechat.py +0 -0
  29. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/channels/mattermost.py +0 -0
  30. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/channels/ntfy.py +0 -0
  31. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/channels/slack.py +0 -0
  32. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/channels/teams.py +0 -0
  33. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/channels/telegram.py +0 -0
  34. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/channels/webhook.py +0 -0
  35. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  36. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/orchestrator/_base.py +0 -0
  37. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  38. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
  39. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  40. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  41. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
  42. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  43. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  44. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/autotune/__init__.py +0 -0
  45. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/autotune/_base.py +0 -0
  46. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/autotune/_types.py +0 -0
  47. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/autotune/autotuner.py +0 -0
  48. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/autotune/axis_spec.py +0 -0
  49. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/autotune/config_emitter.py +0 -0
  50. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/autotune/crossval.py +0 -0
  51. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/autotune/detector_select.py +0 -0
  52. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/autotune/distribution.py +0 -0
  53. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/autotune/grid_search.py +0 -0
  54. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/autotune/labels.py +0 -0
  55. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/autotune/result.py +0 -0
  56. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/autotune/runner.py +0 -0
  57. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/autotune/scoring.py +0 -0
  58. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/autotune/seasonality_search.py +0 -0
  59. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/autotune/settings.py +0 -0
  60. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/autotune/window_select.py +0 -0
  61. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/__init__.py +0 -0
  62. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/_output.py +0 -0
  63. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  64. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
  65. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
  66. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  67. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
  68. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  69. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  70. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  71. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +0 -0
  72. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/commands/__init__.py +0 -0
  73. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/commands/autotune.py +0 -0
  74. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/commands/clean.py +0 -0
  75. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/commands/init.py +0 -0
  76. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/commands/init_claude.py +0 -0
  77. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/commands/mcp.py +0 -0
  78. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/commands/osi.py +0 -0
  79. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/commands/test_alert.py +0 -0
  80. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/commands/tune.py +0 -0
  81. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/commands/ui.py +0 -0
  82. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/commands/unlock.py +0 -0
  83. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/cli/main.py +0 -0
  84. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/config/__init__.py +0 -0
  85. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/config/metric_config.py +0 -0
  86. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/config/metric_io.py +0 -0
  87. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/config/project_config.py +0 -0
  88. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/config/validator.py +0 -0
  89. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/core/__init__.py +0 -0
  90. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/core/interval.py +0 -0
  91. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/core/models.py +0 -0
  92. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/database/__init__.py +0 -0
  93. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/database/_sql_manager.py +0 -0
  94. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/database/clickhouse_manager.py +0 -0
  95. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/database/duckdb_manager.py +0 -0
  96. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/database/internal_tables/__init__.py +0 -0
  97. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  98. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  99. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/database/internal_tables/_base.py +0 -0
  100. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  101. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/database/internal_tables/_detections.py +0 -0
  102. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  103. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  104. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/database/internal_tables/_schema.py +0 -0
  105. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  106. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/database/internal_tables/manager.py +0 -0
  107. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/database/mysql_manager.py +0 -0
  108. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/database/postgres_manager.py +0 -0
  109. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/database/tables.py +0 -0
  110. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/detectors/__init__.py +0 -0
  111. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/detectors/base.py +0 -0
  112. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/detectors/factory.py +0 -0
  113. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/detectors/seasonality.py +0 -0
  114. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/detectors/statistical/__init__.py +0 -0
  115. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  116. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/detectors/statistical/autoreg.py +0 -0
  117. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/detectors/statistical/iqr.py +0 -0
  118. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/detectors/statistical/mad.py +0 -0
  119. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  120. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/detectors/statistical/zscore.py +0 -0
  121. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/loaders/__init__.py +0 -0
  122. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/loaders/errors.py +0 -0
  123. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/loaders/query_template.py +0 -0
  124. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/mcp/__init__.py +0 -0
  125. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/mcp/context.py +0 -0
  126. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/mcp/errors.py +0 -0
  127. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/mcp/serialize.py +0 -0
  128. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/mcp/server.py +0 -0
  129. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/mcp/tools.py +0 -0
  130. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/orchestration/__init__.py +0 -0
  131. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/orchestration/error_dispatch.py +0 -0
  132. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  133. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  134. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  135. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  136. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  137. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  138. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/reporting/__init__.py +0 -0
  139. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/reporting/assets/report.js +0 -0
  140. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/reporting/builder.py +0 -0
  141. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/reporting/html_report.py +0 -0
  142. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/semantic/__init__.py +0 -0
  143. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/semantic/errors.py +0 -0
  144. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/semantic/exporter.py +0 -0
  145. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/semantic/importer.py +0 -0
  146. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/semantic/osi_model.py +0 -0
  147. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/semantic/query_gen.py +0 -0
  148. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/tuning/__init__.py +0 -0
  149. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/tuning/assets/tune.js +0 -0
  150. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/tuning/config_writer.py +0 -0
  151. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/tuning/html.py +0 -0
  152. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/tuning/payload.py +0 -0
  153. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/tuning/server.py +0 -0
  154. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/ui/__init__.py +0 -0
  155. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/ui/assets/ui.js +0 -0
  156. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/ui/html.py +0 -0
  157. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/ui/jobs.py +0 -0
  158. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/ui/metric_files.py +0 -0
  159. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/ui/overview.py +0 -0
  160. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/ui/server.py +0 -0
  161. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/utils/__init__.py +0 -0
  162. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/utils/datetime_utils.py +0 -0
  163. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/utils/env_interpolation.py +0 -0
  164. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/utils/json_utils.py +0 -0
  165. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit/utils/stats.py +0 -0
  166. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit.egg-info/dependency_links.txt +0 -0
  167. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit.egg-info/entry_points.txt +0 -0
  168. {detectkit-0.63.0 → detectkit-0.64.0}/detectkit.egg-info/top_level.txt +0 -0
  169. {detectkit-0.63.0 → detectkit-0.64.0}/requirements.txt +0 -0
  170. {detectkit-0.63.0 → detectkit-0.64.0}/setup.cfg +0 -0
  171. {detectkit-0.63.0 → detectkit-0.64.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.63.0
3
+ Version: 0.64.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -40,11 +40,14 @@ Provides-Extra: mariadb
40
40
  Requires-Dist: pymysql>=1.0.0; extra == "mariadb"
41
41
  Provides-Extra: duckdb
42
42
  Requires-Dist: duckdb>=1.1; extra == "duckdb"
43
+ Provides-Extra: snowflake
44
+ Requires-Dist: snowflake-connector-python>=3.12; extra == "snowflake"
43
45
  Provides-Extra: all-db
44
46
  Requires-Dist: clickhouse-driver>=0.2.0; extra == "all-db"
45
47
  Requires-Dist: psycopg2-binary>=2.9.0; extra == "all-db"
46
48
  Requires-Dist: pymysql>=1.0.0; extra == "all-db"
47
49
  Requires-Dist: duckdb>=1.1; extra == "all-db"
50
+ Requires-Dist: snowflake-connector-python>=3.12; extra == "all-db"
48
51
  Provides-Extra: prophet
49
52
  Requires-Dist: prophet>=1.1.0; extra == "prophet"
50
53
  Provides-Extra: timesfm
@@ -61,6 +64,7 @@ Requires-Dist: clickhouse-driver>=0.2.0; extra == "all"
61
64
  Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
62
65
  Requires-Dist: pymysql>=1.0.0; extra == "all"
63
66
  Requires-Dist: duckdb>=1.1; extra == "all"
67
+ Requires-Dist: snowflake-connector-python>=3.12; extra == "all"
64
68
  Requires-Dist: prophet>=1.1.0; extra == "all"
65
69
  Requires-Dist: timesfm>=0.1.0; extra == "all"
66
70
  Requires-Dist: sqlglot>=20.0; extra == "all"
@@ -106,7 +110,7 @@ Dynamic: license-file
106
110
  - **@mentions** — tag users/groups in alerts, each channel formats natively
107
111
  - **Alert lifecycle** — consecutive anomalies, fraction-of-window rule (`anomaly_window` + `min_anomaly_share`), cooldown, recovery notifications, no-data alerts
108
112
  - **Project-level error alerts** — catch DB outages and pipeline crashes once per run
109
- - **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed)
113
+ - **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed); Snowflake as a hybrid-mode source
110
114
  - **Hybrid mode** — read metric SQL from a billed-per-query warehouse while all pipeline state lives in a cheap local database
111
115
  - **Idempotent** — resume from interruptions, no duplicate processing
112
116
  - **CLI** — `dtk init`, `dtk run --select`, `dtk unlock`, `dtk clean`, tag-based selectors
@@ -123,6 +127,7 @@ With database drivers:
123
127
  ```bash
124
128
  pip install detectkit[clickhouse] # ClickHouse
125
129
  pip install detectkit[duckdb] # DuckDB - single file, no server
130
+ pip install detectkit[snowflake] # Snowflake - source-only (hybrid mode)
126
131
  pip install detectkit[all-db] # All databases
127
132
  ```
128
133
 
@@ -24,7 +24,7 @@
24
24
  - **@mentions** — tag users/groups in alerts, each channel formats natively
25
25
  - **Alert lifecycle** — consecutive anomalies, fraction-of-window rule (`anomaly_window` + `min_anomaly_share`), cooldown, recovery notifications, no-data alerts
26
26
  - **Project-level error alerts** — catch DB outages and pipeline crashes once per run
27
- - **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed)
27
+ - **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed); Snowflake as a hybrid-mode source
28
28
  - **Hybrid mode** — read metric SQL from a billed-per-query warehouse while all pipeline state lives in a cheap local database
29
29
  - **Idempotent** — resume from interruptions, no duplicate processing
30
30
  - **CLI** — `dtk init`, `dtk run --select`, `dtk unlock`, `dtk clean`, tag-based selectors
@@ -41,6 +41,7 @@ With database drivers:
41
41
  ```bash
42
42
  pip install detectkit[clickhouse] # ClickHouse
43
43
  pip install detectkit[duckdb] # DuckDB - single file, no server
44
+ pip install detectkit[snowflake] # Snowflake - source-only (hybrid mode)
44
45
  pip install detectkit[all-db] # All databases
45
46
  ```
46
47
 
@@ -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.63.0"
7
+ __version__ = "0.64.0"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -14,7 +14,7 @@ conventions.
14
14
  **Database access for _you_ (recommended, not required).** detectkit itself
15
15
  connects to the database directly via its drivers — it **never** needs an MCP to
16
16
  run. But you assist far better with **read access to the same database** (e.g. a
17
- database MCP for the project's ClickHouse / PostgreSQL / MySQL / MariaDB / DuckDB): you can inspect
17
+ database MCP for the project's ClickHouse / PostgreSQL / MySQL / MariaDB / DuckDB / Snowflake (source-only, hybrid mode)): you can inspect
18
18
  a metric's series, find real incidents to label for `dtk autotune`, sanity-check
19
19
  a metric query before running it, and confirm detections — instead of asking the
20
20
  user to run every query by hand. Without it, fall back to marking incidents
@@ -79,6 +79,9 @@ not applied at runtime by `dtk run`; `source_profile` is live). Only the
79
79
  metric's **load** step ever connects to it — detect/alert and every other
80
80
  command don't — though every `dtk run` fail-fast validates the resolved name
81
81
  up front regardless of `--steps` (a typo exits 1 before any pipeline work).
82
+ The `source_profile` may point at a **source-only** warehouse profile such as
83
+ `snowflake` (usable *only* as a source, never for `_dtk_*` state — see
84
+ `project.md`), which is exactly the case hybrid mode is built for.
82
85
  See the [Hybrid Mode guide](https://dtk.pipelab.dev/guides/hybrid-mode/).
83
86
 
84
87
  `ai_context` is **OSI-compatible grounding** you can add to any metric with **no
@@ -6,7 +6,9 @@ metrics with automatic anomaly detection and multi-channel alerting. It is
6
6
  them with one command. Core logic is pure numpy (no pandas). **ClickHouse,
7
7
  PostgreSQL, MySQL/MariaDB and DuckDB are all fully supported** — only the
8
8
  connection and the SQL dialect of your metric queries differ between them
9
- (DuckDB additionally needs no server at all — just a local file).
9
+ (DuckDB additionally needs no server at all — just a local file); **Snowflake**
10
+ 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
12
 
11
13
  ## The pipeline: load → detect → alert
12
14
 
@@ -57,7 +57,9 @@ profile whose database runs metric SQL, while *every* `_dtk_*` table
57
57
  **state** profile — the one `dtk run` is already connected to for everything
58
58
  else. A per-metric `source_profile` overrides it (`metrics.md`); resolves
59
59
  **metric → project → unset**, same precedence as `loading_delay`. Unset on
60
- both = today's behavior (one profile does everything). Only the LOAD step's
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
63
  metric-SQL query is affected — detect/alert and every other command
62
64
  (`dtk autotune`/`tune`/`ui`/`clean`/`unlock`) only ever touch the state
63
65
  profile. A source-side failure raises `SourceDatabaseError` (message leads
@@ -137,12 +139,15 @@ alert_channels:
137
139
 
138
140
  ### Database profiles
139
141
 
140
- > ClickHouse, PostgreSQL, MySQL, MariaDB and DuckDB are all supported.
142
+ > ClickHouse, PostgreSQL, MySQL, MariaDB and DuckDB are all supported as
143
+ > **state** backends (they hold the `_dtk_*` tables); **Snowflake** is
144
+ > **source-only** — see its block below.
141
145
  > ClickHouse/MySQL/MariaDB use two *databases*; PostgreSQL connects to one
142
146
  > `database` and uses two *schemas*; DuckDB is a single *file* (or `:memory:`
143
147
  > for tests) holding two *schemas*.
144
148
  > `dtk init --db-type {clickhouse,postgres,mysql,mariadb}` scaffolds the right
145
- > shape — DuckDB profiles are written by hand (not yet a `--db-type` choice).
149
+ > shape — DuckDB and Snowflake profiles are written by hand (not yet a
150
+ > `--db-type` choice).
146
151
 
147
152
  **ClickHouse**:
148
153
  ```yaml
@@ -215,6 +220,36 @@ profiles:
215
220
  > process restarts — use it for tests/scratch only, never a real project.
216
221
  > `pip install 'detectkit[duckdb]'`.
217
222
 
223
+ **Snowflake** (**source-only** — hybrid mode; runs a metric's load SQL, never
224
+ holds `_dtk_*` state):
225
+ ```yaml
226
+ profiles:
227
+ warehouse:
228
+ type: snowflake
229
+ account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}" # required — e.g. ab12345.eu-central-1
230
+ user: svc_detectkit # required
231
+ # key-pair auth (recommended — Snowflake retires single-factor passwords for
232
+ # service accounts through 2026); use ONE of key-pair or password:
233
+ private_key_path: /etc/detectkit/snowflake.p8 # PEM private key file
234
+ private_key_passphrase: "{{ env_var('SNOWFLAKE_KEY_PASSPHRASE') }}" # optional
235
+ # password: "{{ env_var('SNOWFLAKE_PASSWORD') }}" # alternative to key-pair
236
+ warehouse: COMPUTE_WH # required — the virtual warehouse to run on
237
+ database: ANALYTICS # required — where the metric SQL reads from
238
+ schema: PUBLIC # optional — default schema (aliases schema_name)
239
+ role: DETECTKIT_RO # optional — Snowflake role
240
+ settings: {} # optional — extra session parameters
241
+ ```
242
+ > **Source-only**: a `snowflake` profile is valid **only** as a metric's or the
243
+ > project's `source_profile` (hybrid mode) — `dtk run` refuses it as a state
244
+ > profile (`--profile`/`default_profile`), so pair it with a full backend
245
+ > (DuckDB/Postgres/ClickHouse) that holds the `_dtk_*` tables. See `metrics.md`
246
+ > and the [Hybrid Mode guide](https://dtk.pipelab.dev/guides/hybrid-mode/).
247
+ > Key-pair auth is first-class and recommended; a plain `password` works too.
248
+ > The session `TIMEZONE` is pinned to UTC (override via `settings`). Billing
249
+ > note: every query resumes the warehouse with a **60-second minimum bill**, so
250
+ > hybrid mode (load from Snowflake, keep cheap local state) is the point.
251
+ > `pip install 'detectkit[snowflake]'`.
252
+
218
253
  ### Alert channels
219
254
 
220
255
  Defined once in `profiles.yml`, referenced by name in each metric's
@@ -341,6 +341,16 @@ def _run_impl(
341
341
  summary["error"] = f"Error: {source_profile_error}"
342
342
  return 1
343
343
 
344
+ # Fail-fast: the STATE profile must be a state-capable type. Like the
345
+ # source_profile check above this is a config error, not an outage —
346
+ # exit 1 without paging error_alerting (create_manager would raise the
347
+ # same refusal, but through the path that fires the error alert).
348
+ state_type_error = _validate_state_profile_type(profiles_config, profile)
349
+ if state_type_error:
350
+ click.echo(click.style(f"Error: {state_type_error}", fg="red", bold=True))
351
+ summary["error"] = f"Error: {state_type_error}"
352
+ return 1
353
+
344
354
  # Create database manager
345
355
  try:
346
356
  db_manager = profiles_config.create_manager(profile)
@@ -519,6 +529,33 @@ def _validate_source_profiles(
519
529
  )
520
530
 
521
531
 
532
+ def _validate_state_profile_type(
533
+ profiles_config: ProfilesConfig, profile_name: str | None
534
+ ) -> str | None:
535
+ """Cheap, connection-free check that the STATE profile is state-capable.
536
+
537
+ A source-only profile type (e.g. ``snowflake``) pointed at by
538
+ ``--profile``/``default_profile`` is a config error, not an outage, so it
539
+ must exit 1 *without* paging ``error_alerting`` — same spirit as the
540
+ source_profile name check above. An unknown/unset profile name returns
541
+ ``None`` here so the existing ``create_manager`` path reports it exactly
542
+ as before. Duck-typed via ``getattr`` like ``_validate_source_profiles``,
543
+ so test doubles need not model the full ProfilesConfig surface.
544
+ """
545
+ known_profiles = getattr(profiles_config, "profiles", {}) or {}
546
+ name = profile_name or getattr(profiles_config, "default_profile", None)
547
+ profile_config = known_profiles.get(name) if name else None
548
+ if profile_config is None or not getattr(profile_config, "is_source_only", False):
549
+ return None
550
+ state_types = ", ".join(sorted(profile_config.STATE_TYPES))
551
+ return (
552
+ f"Profile type '{profile_config.type}' is source-only: it can serve "
553
+ f"as a metric/project 'source_profile' (hybrid mode), but it cannot "
554
+ f"hold detectkit state. Point --profile/default_profile at one of: "
555
+ f"{state_types}."
556
+ )
557
+
558
+
522
559
  def _resolve_report_path(report_path: str, project_root: Path, metric_name: str) -> Path:
523
560
  """Map the ``--report`` value to a concrete output file for a metric.
524
561
 
@@ -5,13 +5,14 @@ Manages database connections and locations (similar to dbt profiles).
5
5
  """
6
6
 
7
7
  from pathlib import Path
8
- from typing import Any
8
+ from typing import Any, ClassVar
9
9
 
10
10
  import yaml
11
- from pydantic import BaseModel, Field, field_validator, model_validator
11
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
12
12
 
13
13
  from detectkit.database.clickhouse_manager import ClickHouseDatabaseManager
14
14
  from detectkit.database.manager import BaseDatabaseManager
15
+ from detectkit.database.source_manager import SourceDatabaseManager
15
16
  from detectkit.utils.env_interpolation import interpolate_env_vars
16
17
 
17
18
 
@@ -22,24 +23,64 @@ class ProfileConfig(BaseModel):
22
23
  Defines connection parameters and database locations for a specific
23
24
  environment (dev, prod, etc.).
24
25
 
26
+ Two classes of profile types exist:
27
+
28
+ - **Full backends** (``STATE_TYPES``: clickhouse/postgres/mysql/mariadb/
29
+ duckdb) can hold detectkit state (the ``_dtk_*`` tables) — and, since
30
+ state managers also expose ``execute_query``, double as hybrid-mode
31
+ sources.
32
+ - **Source-only backends** (``SOURCE_ONLY_TYPES``: snowflake) 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.
36
+
25
37
  Attributes:
26
- type: Database type ("clickhouse", "postgres", "mysql", "mariadb", "duckdb")
38
+ type: Database type ("clickhouse", "postgres", "mysql", "mariadb",
39
+ "duckdb"; source-only: "snowflake")
27
40
  host: Database host (unused for DuckDB — it is an in-process, file-backed
28
- database with no network endpoint)
29
- port: Database port (unused for DuckDB; required for every other backend)
30
- user: Database user (unused for DuckDB)
31
- password: Database password (unused for DuckDB)
32
- database: Connection-target database (PostgreSQL/MySQL/MariaDB; unused
33
- for DuckDB use `path` instead)
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)
48
+ database: Connection-target database (PostgreSQL/MySQL/MariaDB; the
49
+ session default database for Snowflake; unused for DuckDB — use
50
+ `path` instead)
34
51
  path: Path to the DuckDB database file, or ":memory:" for a transient
35
52
  in-process database (DuckDB only)
53
+ account: Snowflake account identifier (e.g. "myorg-myaccount")
54
+ warehouse: Snowflake virtual warehouse to run load queries on
55
+ role: Snowflake role for the session
56
+ schema_name: Session default schema (YAML key: `schema`; Snowflake only)
57
+ private_key_path: Path to a PEM private key for Snowflake key-pair
58
+ auth (recommended for service accounts)
59
+ private_key_passphrase: Passphrase of the private key, if encrypted
36
60
  internal_database: Database/schema for internal tables
37
61
  internal_schema: Schema for internal tables (PostgreSQL/DuckDB only)
38
62
  data_database: Database for user data tables
39
63
  data_schema: Schema for user data (PostgreSQL/DuckDB only)
40
- settings: Additional database-specific settings
64
+ settings: Additional database-specific settings (for Snowflake these
65
+ merge into the session parameters)
41
66
  """
42
67
 
68
+ # `schema_name` carries the YAML key `schema` via its alias (a field
69
+ # literally named `schema` would shadow pydantic's BaseModel attribute).
70
+ model_config = ConfigDict(populate_by_name=True)
71
+
72
+ # Full backends: may hold detectkit state (_dtk_* tables), and therefore
73
+ # also work as hybrid-mode sources.
74
+ STATE_TYPES: ClassVar[frozenset[str]] = frozenset(
75
+ {"clickhouse", "postgres", "mysql", "mariadb", "duckdb"}
76
+ )
77
+ # Source-only backends: connect + execute_query only (no DDL/upserts/
78
+ # 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"})
83
+
43
84
  type: str = Field(..., description="Database type")
44
85
  host: str = Field(default="localhost", description="Database host (unused for DuckDB)")
45
86
  # No default: required for every backend except DuckDB (enforced below by
@@ -70,6 +111,36 @@ class ProfileConfig(BaseModel):
70
111
  ),
71
112
  )
72
113
 
114
+ # Snowflake-only (source-only backend). host/port are meaningless for it:
115
+ # the driver resolves the endpoint from `account`.
116
+ account: str | None = Field(
117
+ default=None,
118
+ description="Snowflake account identifier, e.g. 'myorg-myaccount' (Snowflake only)",
119
+ )
120
+ warehouse: str | None = Field(
121
+ default=None,
122
+ description="Virtual warehouse to run load queries on (Snowflake only; "
123
+ "falls back to the user's default warehouse when unset)",
124
+ )
125
+ role: str | None = Field(
126
+ default=None,
127
+ description="Role for the session (Snowflake only; user's default role when unset)",
128
+ )
129
+ schema_name: str | None = Field(
130
+ default=None,
131
+ alias="schema",
132
+ description="Session default schema (Snowflake only; YAML key: 'schema')",
133
+ )
134
+ private_key_path: str | None = Field(
135
+ default=None,
136
+ description="Path to a PEM private key for key-pair auth (Snowflake only; "
137
+ "recommended for service accounts over 'password')",
138
+ )
139
+ private_key_passphrase: str | None = Field(
140
+ default=None,
141
+ description="Passphrase of the private key, when it is encrypted (Snowflake only)",
142
+ )
143
+
73
144
  # Internal location for _dtk_* tables
74
145
  internal_database: str | None = Field(
75
146
  default=None, description="Database for internal tables (ClickHouse/MySQL/MariaDB)"
@@ -90,14 +161,19 @@ class ProfileConfig(BaseModel):
90
161
  default_factory=dict, description="Additional database settings"
91
162
  )
92
163
 
164
+ @property
165
+ def is_source_only(self) -> bool:
166
+ """True for profile types valid only as a hybrid-mode ``source_profile``."""
167
+ return self.type in self.SOURCE_ONLY_TYPES
168
+
93
169
  @field_validator("type")
94
170
  @classmethod
95
171
  def validate_type(cls, v: str) -> str:
96
172
  """Validate database type."""
97
- allowed_types = {"clickhouse", "postgres", "mysql", "mariadb", "duckdb"}
173
+ allowed_types = cls.STATE_TYPES | cls.SOURCE_ONLY_TYPES
98
174
  if v not in allowed_types:
99
175
  raise ValueError(
100
- f"Invalid database type: {v}. " f"Allowed types: {', '.join(allowed_types)}"
176
+ f"Invalid database type: {v}. " f"Allowed types: {', '.join(sorted(allowed_types))}"
101
177
  )
102
178
  return v
103
179
 
@@ -113,11 +189,32 @@ class ProfileConfig(BaseModel):
113
189
 
114
190
  @model_validator(mode="after")
115
191
  def _validate_port_required(self) -> "ProfileConfig":
116
- """Every backend but DuckDB needs a port; DuckDB has no network endpoint."""
117
- if self.type != "duckdb" and self.port is None:
192
+ """Server backends need a port; DuckDB is in-process, Snowflake connects by account."""
193
+ if self.type not in self._PORTLESS_TYPES and self.port is None:
118
194
  raise ValueError(f"port is required for database type '{self.type}'")
119
195
  return self
120
196
 
197
+ @model_validator(mode="after")
198
+ def _validate_snowflake_fields(self) -> "ProfileConfig":
199
+ """Snowflake profiles need an account, an explicit user, and one auth method."""
200
+ if self.type != "snowflake":
201
+ return self
202
+ if not self.account:
203
+ raise ValueError(
204
+ "snowflake profiles must set 'account' "
205
+ "(the account identifier, e.g. 'myorg-myaccount')"
206
+ )
207
+ if "user" not in self.model_fields_set:
208
+ # `user` carries a generic default ("default") that would silently
209
+ # reach the Snowflake driver; require it spelled out.
210
+ raise ValueError("snowflake profiles must set 'user' explicitly")
211
+ if not self.password and not self.private_key_path:
212
+ raise ValueError(
213
+ "snowflake profiles must set 'password' or 'private_key_path' "
214
+ "(key-pair auth — recommended for service accounts)"
215
+ )
216
+ return self
217
+
121
218
  def get_internal_location(self) -> str:
122
219
  """
123
220
  Get internal location (database or schema).
@@ -193,11 +290,19 @@ class ProfileConfig(BaseModel):
193
290
  Database manager instance
194
291
 
195
292
  Raises:
196
- ValueError: If the database type is unsupported, or required
197
- connection fields (e.g. PostgreSQL ``database``, DuckDB
198
- ``path``) are missing
293
+ ValueError: If the database type is unsupported or source-only,
294
+ or required connection fields (e.g. PostgreSQL ``database``,
295
+ DuckDB ``path``) are missing
199
296
  ImportError: If the backend's driver is not installed
200
297
  """
298
+ if self.type in self.SOURCE_ONLY_TYPES:
299
+ raise ValueError(
300
+ f"Profile type '{self.type}' is source-only: it can serve as a "
301
+ f"metric/project 'source_profile' (hybrid mode — the metric's load "
302
+ f"SQL runs there while _dtk_* state stays in the state profile), "
303
+ f"but it cannot hold detectkit state. Point --profile/"
304
+ f"default_profile at one of: {', '.join(sorted(self.STATE_TYPES))}."
305
+ )
201
306
  if self.type == "clickhouse":
202
307
  # `port` is guaranteed non-None here by `_validate_port_required`
203
308
  # (every type but "duckdb" requires it at construction time).
@@ -273,6 +378,40 @@ class ProfileConfig(BaseModel):
273
378
  else:
274
379
  raise ValueError(f"Unsupported database type: {self.type}")
275
380
 
381
+ def create_source_manager(self) -> SourceDatabaseManager:
382
+ """
383
+ Create a manager for use as a hybrid-mode SOURCE (read-only load queries).
384
+
385
+ Source-only types build their lightweight query-only manager here;
386
+ full backends route through :meth:`create_manager` unchanged, so any
387
+ state-capable profile keeps working as a source exactly as before.
388
+
389
+ Returns:
390
+ A manager satisfying the minimal execute_query + close contract
391
+
392
+ Raises:
393
+ ValueError: If required connection fields are missing
394
+ ImportError: If the backend's driver is not installed
395
+ """
396
+ if self.type == "snowflake":
397
+ from detectkit.database.snowflake_manager import SnowflakeSourceManager
398
+
399
+ # Guaranteed by `_validate_snowflake_fields` at construction time.
400
+ assert self.account is not None
401
+ return SnowflakeSourceManager(
402
+ account=self.account,
403
+ user=self.user,
404
+ password=self.password or None,
405
+ private_key_path=self.private_key_path,
406
+ private_key_passphrase=self.private_key_passphrase,
407
+ warehouse=self.warehouse,
408
+ database=self.database,
409
+ schema=self.schema_name,
410
+ role=self.role,
411
+ settings=self.settings,
412
+ )
413
+ return self.create_manager()
414
+
276
415
 
277
416
  class ProfilesConfig(BaseModel):
278
417
  """
@@ -383,6 +522,23 @@ class ProfilesConfig(BaseModel):
383
522
  profile = self.get_profile(profile_name)
384
523
  return profile.create_manager(ensure_locations=ensure_locations)
385
524
 
525
+ def create_source_manager(self, profile_name: str | None = None) -> SourceDatabaseManager:
526
+ """
527
+ Create a manager for a profile used as a hybrid-mode SOURCE.
528
+
529
+ Unlike :meth:`create_manager`, source-only profile types (e.g.
530
+ Snowflake) are valid here; full backends behave exactly as with
531
+ :meth:`create_manager`.
532
+
533
+ Args:
534
+ profile_name: Profile name (if None, use default)
535
+
536
+ Returns:
537
+ A manager satisfying the minimal execute_query + close contract
538
+ """
539
+ profile = self.get_profile(profile_name)
540
+ return profile.create_source_manager()
541
+
386
542
  def get_alert_channel_config(self, channel_name: str) -> dict[str, Any]:
387
543
  """
388
544
  Get alert channel configuration by name.
@@ -12,16 +12,17 @@ The manager is database-agnostic and provides generic operations:
12
12
  - get_last_timestamp(): Get last timestamp for a metric
13
13
  """
14
14
 
15
- from abc import ABC, abstractmethod
15
+ from abc import abstractmethod
16
16
  from datetime import datetime
17
17
  from typing import Any
18
18
 
19
19
  import numpy as np
20
20
 
21
21
  from detectkit.core.models import TableModel
22
+ from detectkit.database.source_manager import SourceDatabaseManager
22
23
 
23
24
 
24
- class BaseDatabaseManager(ABC):
25
+ class BaseDatabaseManager(SourceDatabaseManager):
25
26
  """
26
27
  Universal database manager interface.
27
28
 
@@ -31,6 +32,10 @@ class BaseDatabaseManager(ABC):
31
32
  Internal table management is handled by higher-level classes that
32
33
  use these generic methods.
33
34
 
35
+ Extends :class:`SourceDatabaseManager` (the minimal read-only
36
+ execute_query + close contract), so every full backend is automatically
37
+ usable as a hybrid-mode source profile.
38
+
34
39
  Key Design Principles:
35
40
  1. Universal methods (not table-specific)
36
41
  2. Works with any table via table_name parameter