detectkit 0.65.0__tar.gz → 0.66.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.65.0/detectkit.egg-info → detectkit-0.66.0}/PKG-INFO +3 -3
  2. {detectkit-0.65.0 → detectkit-0.66.0}/README.md +2 -2
  3. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/__init__.py +1 -1
  4. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/assets/claude/CLAUDE.section.md +1 -1
  5. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/assets/claude/rules/overview.md +5 -4
  6. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/assets/claude/rules/project.md +16 -7
  7. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/commands/init.py +6 -3
  8. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/config/profile.py +23 -10
  9. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/duckdb_manager.py +61 -18
  10. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/mcp/context.py +3 -2
  11. {detectkit-0.65.0 → detectkit-0.66.0/detectkit.egg-info}/PKG-INFO +3 -3
  12. {detectkit-0.65.0 → detectkit-0.66.0}/LICENSE +0 -0
  13. {detectkit-0.65.0 → detectkit-0.66.0}/MANIFEST.in +0 -0
  14. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/__init__.py +0 -0
  15. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/channels/__init__.py +0 -0
  16. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/channels/base.py +0 -0
  17. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/channels/branding.py +0 -0
  18. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/channels/discord.py +0 -0
  19. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/channels/email.py +0 -0
  20. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/channels/factory.py +0 -0
  21. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/channels/googlechat.py +0 -0
  22. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/channels/mattermost.py +0 -0
  23. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/channels/ntfy.py +0 -0
  24. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/channels/slack.py +0 -0
  25. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/channels/teams.py +0 -0
  26. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/channels/telegram.py +0 -0
  27. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/channels/webhook.py +0 -0
  28. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  29. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/orchestrator/_base.py +0 -0
  30. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  31. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
  32. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  33. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  34. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
  35. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  36. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  37. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/autotune/__init__.py +0 -0
  38. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/autotune/_base.py +0 -0
  39. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/autotune/_types.py +0 -0
  40. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/autotune/autotuner.py +0 -0
  41. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/autotune/axis_spec.py +0 -0
  42. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/autotune/config_emitter.py +0 -0
  43. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/autotune/crossval.py +0 -0
  44. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/autotune/detector_select.py +0 -0
  45. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/autotune/distribution.py +0 -0
  46. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/autotune/grid_search.py +0 -0
  47. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/autotune/labels.py +0 -0
  48. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/autotune/result.py +0 -0
  49. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/autotune/runner.py +0 -0
  50. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/autotune/scoring.py +0 -0
  51. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/autotune/seasonality_search.py +0 -0
  52. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/autotune/settings.py +0 -0
  53. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/autotune/window_select.py +0 -0
  54. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/__init__.py +0 -0
  55. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/_output.py +0 -0
  56. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  57. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
  58. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
  59. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  60. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  61. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
  62. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  63. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  64. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  65. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +0 -0
  66. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/commands/__init__.py +0 -0
  67. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/commands/autotune.py +0 -0
  68. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/commands/clean.py +0 -0
  69. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/commands/init_claude.py +0 -0
  70. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/commands/mcp.py +0 -0
  71. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/commands/osi.py +0 -0
  72. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/commands/run.py +0 -0
  73. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/commands/test_alert.py +0 -0
  74. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/commands/tune.py +0 -0
  75. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/commands/ui.py +0 -0
  76. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/commands/unlock.py +0 -0
  77. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/cli/main.py +0 -0
  78. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/config/__init__.py +0 -0
  79. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/config/metric_config.py +0 -0
  80. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/config/metric_io.py +0 -0
  81. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/config/project_config.py +0 -0
  82. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/config/validator.py +0 -0
  83. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/core/__init__.py +0 -0
  84. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/core/interval.py +0 -0
  85. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/core/models.py +0 -0
  86. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/__init__.py +0 -0
  87. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/_sql_manager.py +0 -0
  88. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/bigquery_manager.py +0 -0
  89. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/clickhouse_manager.py +0 -0
  90. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/internal_tables/__init__.py +0 -0
  91. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  92. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  93. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/internal_tables/_base.py +0 -0
  94. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  95. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/internal_tables/_detections.py +0 -0
  96. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  97. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  98. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/internal_tables/_schema.py +0 -0
  99. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  100. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/internal_tables/manager.py +0 -0
  101. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/manager.py +0 -0
  102. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/mysql_manager.py +0 -0
  103. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/postgres_manager.py +0 -0
  104. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/snowflake_manager.py +0 -0
  105. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/source_manager.py +0 -0
  106. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/database/tables.py +0 -0
  107. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/detectors/__init__.py +0 -0
  108. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/detectors/base.py +0 -0
  109. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/detectors/factory.py +0 -0
  110. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/detectors/seasonality.py +0 -0
  111. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/detectors/statistical/__init__.py +0 -0
  112. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  113. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/detectors/statistical/autoreg.py +0 -0
  114. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/detectors/statistical/iqr.py +0 -0
  115. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/detectors/statistical/mad.py +0 -0
  116. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  117. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/detectors/statistical/zscore.py +0 -0
  118. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/loaders/__init__.py +0 -0
  119. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/loaders/errors.py +0 -0
  120. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/loaders/metric_loader.py +0 -0
  121. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/loaders/query_template.py +0 -0
  122. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/mcp/__init__.py +0 -0
  123. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/mcp/errors.py +0 -0
  124. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/mcp/serialize.py +0 -0
  125. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/mcp/server.py +0 -0
  126. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/mcp/tools.py +0 -0
  127. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/orchestration/__init__.py +0 -0
  128. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/orchestration/error_dispatch.py +0 -0
  129. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  130. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  131. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  132. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  133. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  134. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  135. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  136. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/reporting/__init__.py +0 -0
  137. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/reporting/assets/report.js +0 -0
  138. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/reporting/builder.py +0 -0
  139. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/reporting/html_report.py +0 -0
  140. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/semantic/__init__.py +0 -0
  141. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/semantic/errors.py +0 -0
  142. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/semantic/exporter.py +0 -0
  143. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/semantic/importer.py +0 -0
  144. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/semantic/osi_model.py +0 -0
  145. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/semantic/query_gen.py +0 -0
  146. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/tuning/__init__.py +0 -0
  147. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/tuning/assets/tune.js +0 -0
  148. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/tuning/config_writer.py +0 -0
  149. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/tuning/html.py +0 -0
  150. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/tuning/payload.py +0 -0
  151. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/tuning/server.py +0 -0
  152. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/ui/__init__.py +0 -0
  153. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/ui/assets/ui.js +0 -0
  154. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/ui/html.py +0 -0
  155. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/ui/jobs.py +0 -0
  156. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/ui/metric_files.py +0 -0
  157. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/ui/overview.py +0 -0
  158. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/ui/server.py +0 -0
  159. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/utils/__init__.py +0 -0
  160. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/utils/datetime_utils.py +0 -0
  161. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/utils/env_interpolation.py +0 -0
  162. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/utils/json_utils.py +0 -0
  163. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit/utils/stats.py +0 -0
  164. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit.egg-info/SOURCES.txt +0 -0
  165. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit.egg-info/dependency_links.txt +0 -0
  166. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit.egg-info/entry_points.txt +0 -0
  167. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit.egg-info/requires.txt +0 -0
  168. {detectkit-0.65.0 → detectkit-0.66.0}/detectkit.egg-info/top_level.txt +0 -0
  169. {detectkit-0.65.0 → detectkit-0.66.0}/pyproject.toml +0 -0
  170. {detectkit-0.65.0 → detectkit-0.66.0}/requirements.txt +0 -0
  171. {detectkit-0.65.0 → detectkit-0.66.0}/setup.cfg +0 -0
  172. {detectkit-0.65.0 → detectkit-0.66.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.65.0
3
+ Version: 0.66.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -114,7 +114,7 @@ Dynamic: license-file
114
114
  - **@mentions** — tag users/groups in alerts, each channel formats natively
115
115
  - **Alert lifecycle** — consecutive anomalies, fraction-of-window rule (`anomaly_window` + `min_anomaly_share`), cooldown, recovery notifications, no-data alerts
116
116
  - **Project-level error alerts** — catch DB outages and pipeline crashes once per run
117
- - **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed); Snowflake and BigQuery as hybrid-mode sources
117
+ - **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed, incl. MotherDuck cloud paths); Snowflake and BigQuery as hybrid-mode sources
118
118
  - **Hybrid mode** — read metric SQL from a billed-per-query warehouse while all pipeline state lives in a cheap local database
119
119
  - **Idempotent** — resume from interruptions, no duplicate processing
120
120
  - **CLI** — `dtk init`, `dtk run --select`, `dtk unlock`, `dtk clean`, tag-based selectors
@@ -130,7 +130,7 @@ With database drivers:
130
130
 
131
131
  ```bash
132
132
  pip install detectkit[clickhouse] # ClickHouse
133
- pip install detectkit[duckdb] # DuckDB - single file, no server
133
+ pip install detectkit[duckdb] # DuckDB - single file, no server (also MotherDuck md: paths)
134
134
  pip install detectkit[snowflake] # Snowflake - source-only (hybrid mode)
135
135
  pip install detectkit[bigquery] # BigQuery - source-only (hybrid mode)
136
136
  pip install detectkit[all-db] # All databases
@@ -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 and BigQuery as hybrid-mode sources
27
+ - **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed, incl. MotherDuck cloud paths); 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
@@ -40,7 +40,7 @@ With database drivers:
40
40
 
41
41
  ```bash
42
42
  pip install detectkit[clickhouse] # ClickHouse
43
- pip install detectkit[duckdb] # DuckDB - single file, no server
43
+ pip install detectkit[duckdb] # DuckDB - single file, no server (also MotherDuck md: paths)
44
44
  pip install detectkit[snowflake] # Snowflake - source-only (hybrid mode)
45
45
  pip install detectkit[bigquery] # BigQuery - source-only (hybrid mode)
46
46
  pip install detectkit[all-db] # All databases
@@ -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.65.0"
7
+ __version__ = "0.66.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 / BigQuery (the last two source-only, hybrid mode)): you can inspect
17
+ database MCP for the project's ClickHouse / PostgreSQL / MySQL / MariaDB / DuckDB (incl. MotherDuck `md:` paths) / 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
@@ -6,10 +6,11 @@ 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); **Snowflake**
10
- and **BigQuery** are supported as **source-only** backends (hybrid mode — they
11
- run a metric's load SQL, but `_dtk_*` state must live in one of the full
12
- backends).
9
+ (DuckDB additionally needs no server at all — just a local file, or a
10
+ `md:<database>` path to run fully on **MotherDuck**, DuckDB's cloud);
11
+ **Snowflake** and **BigQuery** are supported as **source-only** backends
12
+ (hybrid mode — they run a metric's load SQL, but `_dtk_*` state must live in one
13
+ of the full backends).
13
14
 
14
15
  ## The pipeline: load → detect → alert
15
16
 
@@ -201,23 +201,32 @@ auto-detected at connect (`SELECT VERSION()`), so MariaDB gets its own
201
201
  `VALUES()`-form upsert instead of MySQL 8.0.19's row-alias form.
202
202
  `pip install 'detectkit[mariadb]'`.
203
203
 
204
- **DuckDB** (embedded, single file; two schemas like PostgreSQL):
204
+ **DuckDB** (embedded, single file; two schemas like PostgreSQL — or
205
+ **MotherDuck**, DuckDB's cloud, via a `md:` path):
205
206
  ```yaml
206
207
  profiles:
207
208
  prod:
208
209
  type: duckdb
209
- path: /var/lib/detectkit/warehouse.duckdb # required — file path, or ":memory:"
210
+ path: /var/lib/detectkit/warehouse.duckdb # required — file path, ":memory:",
211
+ # # or "md:<database>" for MotherDuck
210
212
  internal_schema: detectkit # optional — _dtk_* tables (default: "detectkit")
211
213
  data_schema: main # optional — data queries (default: "main")
212
- read_only: false # optional (default: false)
214
+ read_only: false # optional (default: false; local files only)
215
+ motherduck_token: "{{ env_var('MOTHERDUCK_TOKEN') }}" # optional — only for "md:" paths
213
216
  settings: {} # optional — extra duckdb.connect() config options
214
217
  ```
215
- > Single-writer: DuckDB allows only **one** read-write connection to the file
216
- > at a time — a `dtk ui`/`dtk tune` session left open will clash with a
217
- > separately spawned `dtk run` against the same file. Point a read-only
218
- > consumer at it with `read_only: true`.
218
+ > Single-writer (**local files only**): a local DuckDB file allows only **one**
219
+ > read-write connection at a time — a `dtk ui`/`dtk tune` session left open will
220
+ > clash with a separately spawned `dtk run` against the same file. Point a
221
+ > read-only consumer at it with `read_only: true`. A `md:<database>` MotherDuck
222
+ > path is a **served** database with no single-writer rule — `dtk ui` and a
223
+ > spawned `dtk run` coexist, and `read_only` doesn't apply.
219
224
  > `path: ":memory:"` has no on-disk state, so resume/idempotency breaks across
220
225
  > process restarts — use it for tests/scratch only, never a real project.
226
+ > `motherduck_token` (env-interpolated) authenticates `md:` paths; unset, the
227
+ > `motherduck` extension falls back to a `motherduck_token` environment
228
+ > variable (lowercase — the extension's own lookup name). MotherDuck is a full
229
+ > state backend — `_dtk_*` tables can live there — and needs no extra beyond
221
230
  > `pip install 'detectkit[duckdb]'`.
222
231
 
223
232
  **Snowflake** (**source-only** — hybrid mode; runs a metric's load SQL, never
@@ -127,11 +127,14 @@ _COMMENTED_EXAMPLES = {
127
127
  "duckdb": """ # Example DuckDB profile (in-process, single-file; no host/port/user/password)
128
128
  # duckdb_dev:
129
129
  # type: duckdb
130
- # path: ./detectkit.duckdb # or ':memory:' for a transient, tests-only DB
130
+ # path: ./detectkit.duckdb # or ':memory:' (transient, tests-only), or
131
+ # # # 'md:<database>' for MotherDuck (cloud; add
132
+ # # # motherduck_token: "{{ env_var('MOTHERDUCK_TOKEN') }}")
131
133
  # internal_schema: detectkit
132
134
  # data_schema: main
133
- # # Only ONE read-write connection at a time don't point a scheduled
134
- # # `dtk run` and a long-lived `dtk ui` at the same file simultaneously.
135
+ # # Only ONE read-write connection at a time (local files; md: paths are
136
+ # # served and don't have this limit) don't point a scheduled `dtk run`
137
+ # # and a long-lived `dtk ui` at the same local file simultaneously.
135
138
  """,
136
139
  }
137
140
 
@@ -49,8 +49,11 @@ class ProfileConfig(BaseModel):
49
49
  database: Connection-target database (PostgreSQL/MySQL/MariaDB; the
50
50
  session default database for Snowflake; unused for DuckDB — use
51
51
  `path` instead)
52
- path: Path to the DuckDB database file, or ":memory:" for a transient
53
- in-process database (DuckDB only)
52
+ path: Path to the DuckDB database file, ":memory:" for a transient
53
+ in-process database, or "md:<database>" for a MotherDuck cloud
54
+ database (DuckDB only)
55
+ motherduck_token: MotherDuck service token for "md:" paths (DuckDB
56
+ only; unset -> the extension reads the motherduck_token env var)
54
57
  account: Snowflake account identifier (e.g. "myorg-myaccount")
55
58
  warehouse: Snowflake virtual warehouse to run load queries on
56
59
  role: Snowflake role for the session
@@ -106,20 +109,29 @@ class ProfileConfig(BaseModel):
106
109
  default=None, description="Database to connect to (PostgreSQL/MySQL/MariaDB)"
107
110
  )
108
111
 
109
- # DuckDB-only: the database file path (or ":memory:"). host/port/user/
110
- # password/database above are simply ignored for this backend rather than
111
- # rejected, since e.g. `host` always carries its "localhost" default.
112
+ # DuckDB-only: the database file path (or ":memory:", or "md:<database>"
113
+ # for MotherDuck). host/port/user/password/database above are simply
114
+ # ignored for this backend rather than rejected, since e.g. `host` always
115
+ # carries its "localhost" default.
112
116
  path: str | None = Field(
113
117
  default=None,
114
- description="Database file path, or ':memory:' for a transient in-process database (DuckDB only)",
118
+ description="Database file path, ':memory:' for a transient in-process "
119
+ "database, or 'md:<database>' for a MotherDuck cloud database (DuckDB only)",
115
120
  )
116
121
  read_only: bool = Field(
117
122
  default=False,
118
123
  description=(
119
- "Open the database read-only (DuckDB only)lets a reader profile "
120
- "coexist with the one process holding the file read-write"
124
+ "Open the database read-only (DuckDB local files only — MotherDuck "
125
+ "has no read-only attach) lets a reader profile coexist with the "
126
+ "one process holding the file read-write"
121
127
  ),
122
128
  )
129
+ motherduck_token: str | None = Field(
130
+ default=None,
131
+ description="MotherDuck service token for 'md:' paths (DuckDB only; "
132
+ "env-interpolated — unset falls back to the motherduck_token "
133
+ "environment variable read by the extension itself)",
134
+ )
123
135
 
124
136
  # Snowflake-only (source-only backend). host/port are meaningless for it:
125
137
  # the driver resolves the endpoint from `account`.
@@ -419,14 +431,15 @@ class ProfileConfig(BaseModel):
419
431
  if not self.path:
420
432
  raise ValueError(
421
433
  "DuckDB profiles must set 'path' (the database file path, "
422
- "or ':memory:' for a transient, tests/preview-only "
423
- "in-process database)"
434
+ "':memory:' for a transient, tests/preview-only in-process "
435
+ "database, or 'md:<database>' for MotherDuck)"
424
436
  )
425
437
  return DuckDBDatabaseManager(
426
438
  path=self.path,
427
439
  internal_schema=self.get_internal_location(),
428
440
  data_schema=self.get_data_location(),
429
441
  read_only=self.read_only,
442
+ motherduck_token=self.motherduck_token,
430
443
  settings=self.settings,
431
444
  ensure_locations=ensure_locations,
432
445
  )
@@ -9,10 +9,30 @@ internal/data *locations* are DuckDB **schemas** inside that one file,
9
9
  created with ``CREATE SCHEMA IF NOT EXISTS`` (``main`` always exists and is
10
10
  never explicitly created).
11
11
 
12
+ **MotherDuck.** The same manager also speaks `MotherDuck
13
+ <https://motherduck.com/>`_ — DuckDB's serverless cloud service — through the
14
+ same ``duckdb`` client: a ``path`` of the form ``md:<database>`` attaches the
15
+ named MotherDuck database (the ``motherduck`` core extension autoloads on
16
+ first use; the first connect downloads it, so it needs network access). Auth
17
+ is a service token, passed as the ``motherduck_token`` connect config (the
18
+ profile's ``motherduck_token`` field) — when unset, the extension itself
19
+ falls back to a ``motherduck_token`` environment variable. Everything below
20
+ the connect is identical: same SQL surface, same ``ON CONFLICT`` upsert, same
21
+ internal-tables flow. The **local-file operational caveats do not apply** to
22
+ ``md:`` paths — MotherDuck is a served database, so multiple processes
23
+ (`dtk ui` + a concurrently spawned `dtk run`) can hold connections at once;
24
+ the single-writer rule below is a property of local files only. One
25
+ asymmetry: MotherDuck does not support DuckDB's ``read_only=True`` attach
26
+ flag, so the strict read-only probe (``ensure_locations=False``) skips the
27
+ forced read-only for ``md:`` paths — its purpose there (preventing a missing
28
+ local *file* from being created as a connect side effect) doesn't apply to a
29
+ served database; the probe still runs no DDL.
30
+
12
31
  **Operational model — read this before pointing a scheduled `dtk run` and a
13
- long-lived `dtk ui` at the same file.** A DuckDB file is held read-write by
14
- **one process at a time**: a second *process* attempting a read-write attach
15
- fails. (Within the single writing process DuckDB itself allows further
32
+ long-lived `dtk ui` at the same file.** (Local files only ``md:`` paths
33
+ are served and have no single-writer rule.) A DuckDB file is held read-write
34
+ by **one process at a time**: a second *process* attempting a read-write
35
+ attach fails. (Within the single writing process DuckDB itself allows further
16
36
  connections — they share the cached database instance and may write
17
37
  concurrently under MVCC with optimistic-conflict errors, and a same-process
18
38
  ``read_only=True`` attach fails on the config mismatch rather than the lock —
@@ -188,21 +208,29 @@ class DuckDBDatabaseManager(SQLDatabaseManager):
188
208
 
189
209
  Args:
190
210
  path: Path to the DuckDB database file (created if it doesn't exist),
191
- or the literal string ``":memory:"`` for a transient in-process
192
- database. ``:memory:`` is **tests/preview-only** its state is
193
- not persisted to disk and is lost when the process exits, which
194
- breaks detectkit's resume-from-last-timestamp idempotency across
195
- runs; use a real file path for anything but a one-off test.
211
+ the literal string ``":memory:"`` for a transient in-process
212
+ database, or ``"md:<database>"`` for a MotherDuck cloud database
213
+ (see the module docstring). ``:memory:`` is **tests/preview-only**
214
+ its state is not persisted to disk and is lost when the process
215
+ exits, which breaks detectkit's resume-from-last-timestamp
216
+ idempotency across runs; use a real file path (or ``md:``) for
217
+ anything but a one-off test.
196
218
  internal_schema: Schema for internal ``_dtk_*`` tables.
197
219
  data_schema: Schema for user data tables. Defaults to ``"main"``,
198
220
  DuckDB's always-present default schema.
199
- read_only: Open the file read-only. Required when another process
200
- already holds the file read-write (DuckDB allows many concurrent
201
- *readers*, never a reader alongside a writer). A read-only
202
- connection cannot create schemas/tables, so it assumes the
203
- internal/data schemas already exist.
221
+ read_only: Open the file read-only (local files only — MotherDuck
222
+ does not support a read-only attach). Required when another
223
+ process already holds the file read-write (DuckDB allows many
224
+ concurrent *readers*, never a reader alongside a writer). A
225
+ read-only connection cannot create schemas/tables, so it assumes
226
+ the internal/data schemas already exist.
227
+ motherduck_token: MotherDuck service token, sent as the
228
+ ``motherduck_token`` connect config for ``md:`` paths (ignored
229
+ for local paths). Unset -> the ``motherduck`` extension falls
230
+ back to the ``motherduck_token`` environment variable.
204
231
  settings: Extra ``duckdb.connect`` ``config`` options (e.g.
205
- ``{"memory_limit": "512MB"}``).
232
+ ``{"memory_limit": "512MB"}``); merged over the token, so an
233
+ explicit ``settings["motherduck_token"]`` wins.
206
234
  ensure_locations: When False, skip creating the internal/data
207
235
  schemas as a side effect of connecting (a strict read-only
208
236
  probe — see
@@ -235,6 +263,7 @@ class DuckDBDatabaseManager(SQLDatabaseManager):
235
263
  internal_schema: str = "detectkit",
236
264
  data_schema: str = "main",
237
265
  read_only: bool = False,
266
+ motherduck_token: str | None = None,
238
267
  settings: dict[str, Any] | None = None,
239
268
  ensure_locations: bool = True,
240
269
  ) -> None:
@@ -245,9 +274,12 @@ class DuckDBDatabaseManager(SQLDatabaseManager):
245
274
  if not path:
246
275
  raise ValueError(
247
276
  "DuckDBDatabaseManager requires a non-empty `path` (a database file "
248
- "path, or ':memory:' for a transient, tests/preview-only in-process "
249
- "database whose state is lost between runs)."
277
+ "path, ':memory:' for a transient, tests/preview-only in-process "
278
+ "database whose state is lost between runs, or 'md:<database>' for "
279
+ "a MotherDuck cloud database)."
250
280
  )
281
+ self._is_motherduck = path.startswith("md:")
282
+ self._motherduck_token = motherduck_token
251
283
  # `ensure_locations=False` is a strict read-only PROBE (see
252
284
  # `SQLDatabaseManager.__init__`): skipping `_ensure_locations()` is
253
285
  # not enough on its own for DuckDB, because a plain read-write
@@ -258,7 +290,12 @@ class DuckDBDatabaseManager(SQLDatabaseManager):
258
290
  # place, and DuckDB rejects a read-only in-memory connection outright
259
291
  # (`CatalogException: Cannot launch in-memory database in read-only
260
292
  # mode!`), so forcing it there would break rather than protect.
261
- self._read_only = True if (not ensure_locations and path != ":memory:") else read_only
293
+ # "md:" paths are exempted for the same shape of reason: there is no
294
+ # local file to create as a connect side effect (MotherDuck is a
295
+ # served database), and MotherDuck does not support the read-only
296
+ # attach flag — the probe still runs no DDL.
297
+ force_read_only = not ensure_locations and path != ":memory:" and not self._is_motherduck
298
+ self._read_only = True if force_read_only else read_only
262
299
  # Kept separately (and typed `str`, not `str | None`) from the base
263
300
  # class's `self._database` so `_connect()` doesn't need to narrow an
264
301
  # Optional it knows — by the ValueError check above — can't be None.
@@ -275,7 +312,13 @@ class DuckDBDatabaseManager(SQLDatabaseManager):
275
312
  )
276
313
 
277
314
  def _connect(self) -> Any:
278
- raw = duckdb.connect(self._path, read_only=self._read_only, config=self._settings or {})
315
+ config = dict(self._settings or {})
316
+ if self._is_motherduck and self._motherduck_token:
317
+ # `settings` merges over the token (an explicit
318
+ # settings["motherduck_token"] wins), mirroring the Snowflake
319
+ # manager's settings-over-pin precedence.
320
+ config = {"motherduck_token": self._motherduck_token, **config}
321
+ raw = duckdb.connect(self._path, read_only=self._read_only, config=config)
279
322
  return _DuckDBConnectionAdapter(raw)
280
323
 
281
324
  def _ensure_locations(self) -> None:
@@ -131,14 +131,15 @@ def _resolve_duckdb_path(profile_config: ProfileConfig, project_root: Path) -> P
131
131
  would land wherever the launcher happened to start the process, and could
132
132
  even *create* a stray state file there. Every other project-relative path
133
133
  in detectkit (metrics/, incidents/, …) is resolved against the project
134
- root, so a relative DuckDB ``path`` should be too. ``":memory:"`` and an
134
+ root, so a relative DuckDB ``path`` should be too. ``":memory:"``,
135
+ ``"md:..."`` (a MotherDuck database name, not a filesystem path) and an
135
136
  already-absolute path pass through unchanged. Returns a **copy**
136
137
  (``model_copy``) — the original ``ProfileConfig``/``ProfilesConfig`` is
137
138
  shared state (e.g. reused by ``get_server_info``) and must not be mutated.
138
139
  """
139
140
  if profile_config.type != "duckdb" or not profile_config.path:
140
141
  return profile_config
141
- if profile_config.path == ":memory:":
142
+ if profile_config.path == ":memory:" or profile_config.path.startswith("md:"):
142
143
  return profile_config
143
144
  raw_path = Path(profile_config.path)
144
145
  if raw_path.is_absolute():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.65.0
3
+ Version: 0.66.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -114,7 +114,7 @@ Dynamic: license-file
114
114
  - **@mentions** — tag users/groups in alerts, each channel formats natively
115
115
  - **Alert lifecycle** — consecutive anomalies, fraction-of-window rule (`anomaly_window` + `min_anomaly_share`), cooldown, recovery notifications, no-data alerts
116
116
  - **Project-level error alerts** — catch DB outages and pipeline crashes once per run
117
- - **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed); Snowflake and BigQuery as hybrid-mode sources
117
+ - **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed, incl. MotherDuck cloud paths); Snowflake and BigQuery as hybrid-mode sources
118
118
  - **Hybrid mode** — read metric SQL from a billed-per-query warehouse while all pipeline state lives in a cheap local database
119
119
  - **Idempotent** — resume from interruptions, no duplicate processing
120
120
  - **CLI** — `dtk init`, `dtk run --select`, `dtk unlock`, `dtk clean`, tag-based selectors
@@ -130,7 +130,7 @@ With database drivers:
130
130
 
131
131
  ```bash
132
132
  pip install detectkit[clickhouse] # ClickHouse
133
- pip install detectkit[duckdb] # DuckDB - single file, no server
133
+ pip install detectkit[duckdb] # DuckDB - single file, no server (also MotherDuck md: paths)
134
134
  pip install detectkit[snowflake] # Snowflake - source-only (hybrid mode)
135
135
  pip install detectkit[bigquery] # BigQuery - source-only (hybrid mode)
136
136
  pip install detectkit[all-db] # All databases
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes