detectkit 0.59.0__tar.gz → 0.60.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 (161) hide show
  1. {detectkit-0.59.0/detectkit.egg-info → detectkit-0.60.0}/PKG-INFO +6 -2
  2. {detectkit-0.59.0 → detectkit-0.60.0}/README.md +1 -1
  3. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/__init__.py +1 -1
  4. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/assets/claude/CLAUDE.section.md +1 -1
  5. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/assets/claude/rules/project.md +24 -3
  6. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +6 -2
  7. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/commands/init.py +13 -1
  8. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/config/profile.py +86 -19
  9. detectkit-0.60.0/detectkit/database/duckdb_manager.py +329 -0
  10. {detectkit-0.59.0 → detectkit-0.60.0/detectkit.egg-info}/PKG-INFO +6 -2
  11. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit.egg-info/SOURCES.txt +1 -0
  12. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit.egg-info/requires.txt +5 -0
  13. {detectkit-0.59.0 → detectkit-0.60.0}/pyproject.toml +8 -1
  14. {detectkit-0.59.0 → detectkit-0.60.0}/LICENSE +0 -0
  15. {detectkit-0.59.0 → detectkit-0.60.0}/MANIFEST.in +0 -0
  16. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/__init__.py +0 -0
  17. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/channels/__init__.py +0 -0
  18. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/channels/base.py +0 -0
  19. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/channels/branding.py +0 -0
  20. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/channels/discord.py +0 -0
  21. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/channels/email.py +0 -0
  22. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/channels/factory.py +0 -0
  23. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/channels/googlechat.py +0 -0
  24. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/channels/mattermost.py +0 -0
  25. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/channels/ntfy.py +0 -0
  26. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/channels/slack.py +0 -0
  27. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/channels/teams.py +0 -0
  28. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/channels/telegram.py +0 -0
  29. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/channels/webhook.py +0 -0
  30. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  31. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/orchestrator/_base.py +0 -0
  32. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  33. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
  34. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  35. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  36. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
  37. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  38. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  39. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/autotune/__init__.py +0 -0
  40. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/autotune/_base.py +0 -0
  41. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/autotune/_types.py +0 -0
  42. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/autotune/autotuner.py +0 -0
  43. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/autotune/axis_spec.py +0 -0
  44. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/autotune/config_emitter.py +0 -0
  45. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/autotune/crossval.py +0 -0
  46. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/autotune/detector_select.py +0 -0
  47. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/autotune/distribution.py +0 -0
  48. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/autotune/grid_search.py +0 -0
  49. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/autotune/labels.py +0 -0
  50. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/autotune/result.py +0 -0
  51. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/autotune/runner.py +0 -0
  52. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/autotune/scoring.py +0 -0
  53. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/autotune/seasonality_search.py +0 -0
  54. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/autotune/settings.py +0 -0
  55. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/autotune/window_select.py +0 -0
  56. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/__init__.py +0 -0
  57. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/_output.py +0 -0
  58. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  59. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
  60. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
  61. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  62. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  63. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  64. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
  65. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  66. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  67. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +0 -0
  68. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/commands/__init__.py +0 -0
  69. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/commands/autotune.py +0 -0
  70. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/commands/clean.py +0 -0
  71. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/commands/init_claude.py +0 -0
  72. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/commands/osi.py +0 -0
  73. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/commands/run.py +0 -0
  74. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/commands/test_alert.py +0 -0
  75. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/commands/tune.py +0 -0
  76. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/commands/ui.py +0 -0
  77. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/commands/unlock.py +0 -0
  78. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/cli/main.py +0 -0
  79. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/config/__init__.py +0 -0
  80. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/config/metric_config.py +0 -0
  81. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/config/metric_io.py +0 -0
  82. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/config/project_config.py +0 -0
  83. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/config/validator.py +0 -0
  84. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/core/__init__.py +0 -0
  85. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/core/interval.py +0 -0
  86. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/core/models.py +0 -0
  87. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/database/__init__.py +0 -0
  88. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/database/_sql_manager.py +0 -0
  89. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/database/clickhouse_manager.py +0 -0
  90. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/database/internal_tables/__init__.py +0 -0
  91. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  92. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  93. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/database/internal_tables/_base.py +0 -0
  94. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  95. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/database/internal_tables/_detections.py +0 -0
  96. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  97. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  98. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/database/internal_tables/_schema.py +0 -0
  99. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  100. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/database/internal_tables/manager.py +0 -0
  101. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/database/manager.py +0 -0
  102. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/database/mysql_manager.py +0 -0
  103. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/database/postgres_manager.py +0 -0
  104. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/database/tables.py +0 -0
  105. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/detectors/__init__.py +0 -0
  106. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/detectors/base.py +0 -0
  107. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/detectors/factory.py +0 -0
  108. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/detectors/seasonality.py +0 -0
  109. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/detectors/statistical/__init__.py +0 -0
  110. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  111. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/detectors/statistical/autoreg.py +0 -0
  112. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/detectors/statistical/iqr.py +0 -0
  113. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/detectors/statistical/mad.py +0 -0
  114. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  115. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/detectors/statistical/zscore.py +0 -0
  116. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/loaders/__init__.py +0 -0
  117. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/loaders/metric_loader.py +0 -0
  118. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/loaders/query_template.py +0 -0
  119. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/orchestration/__init__.py +0 -0
  120. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/orchestration/error_dispatch.py +0 -0
  121. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  122. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  123. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  124. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  125. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  126. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  127. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  128. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/reporting/__init__.py +0 -0
  129. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/reporting/assets/report.js +0 -0
  130. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/reporting/builder.py +0 -0
  131. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/reporting/html_report.py +0 -0
  132. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/semantic/__init__.py +0 -0
  133. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/semantic/errors.py +0 -0
  134. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/semantic/exporter.py +0 -0
  135. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/semantic/importer.py +0 -0
  136. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/semantic/osi_model.py +0 -0
  137. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/semantic/query_gen.py +0 -0
  138. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/tuning/__init__.py +0 -0
  139. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/tuning/assets/tune.js +0 -0
  140. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/tuning/config_writer.py +0 -0
  141. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/tuning/html.py +0 -0
  142. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/tuning/payload.py +0 -0
  143. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/tuning/server.py +0 -0
  144. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/ui/__init__.py +0 -0
  145. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/ui/assets/ui.js +0 -0
  146. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/ui/html.py +0 -0
  147. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/ui/jobs.py +0 -0
  148. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/ui/metric_files.py +0 -0
  149. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/ui/overview.py +0 -0
  150. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/ui/server.py +0 -0
  151. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/utils/__init__.py +0 -0
  152. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/utils/datetime_utils.py +0 -0
  153. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/utils/env_interpolation.py +0 -0
  154. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/utils/json_utils.py +0 -0
  155. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit/utils/stats.py +0 -0
  156. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit.egg-info/dependency_links.txt +0 -0
  157. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit.egg-info/entry_points.txt +0 -0
  158. {detectkit-0.59.0 → detectkit-0.60.0}/detectkit.egg-info/top_level.txt +0 -0
  159. {detectkit-0.59.0 → detectkit-0.60.0}/requirements.txt +0 -0
  160. {detectkit-0.59.0 → detectkit-0.60.0}/setup.cfg +0 -0
  161. {detectkit-0.59.0 → detectkit-0.60.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.59.0
3
+ Version: 0.60.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -38,10 +38,13 @@ Provides-Extra: mysql
38
38
  Requires-Dist: pymysql>=1.0.0; extra == "mysql"
39
39
  Provides-Extra: mariadb
40
40
  Requires-Dist: pymysql>=1.0.0; extra == "mariadb"
41
+ Provides-Extra: duckdb
42
+ Requires-Dist: duckdb>=1.1; extra == "duckdb"
41
43
  Provides-Extra: all-db
42
44
  Requires-Dist: clickhouse-driver>=0.2.0; extra == "all-db"
43
45
  Requires-Dist: psycopg2-binary>=2.9.0; extra == "all-db"
44
46
  Requires-Dist: pymysql>=1.0.0; extra == "all-db"
47
+ Requires-Dist: duckdb>=1.1; extra == "all-db"
45
48
  Provides-Extra: prophet
46
49
  Requires-Dist: prophet>=1.1.0; extra == "prophet"
47
50
  Provides-Extra: timesfm
@@ -55,6 +58,7 @@ Provides-Extra: all
55
58
  Requires-Dist: clickhouse-driver>=0.2.0; extra == "all"
56
59
  Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
57
60
  Requires-Dist: pymysql>=1.0.0; extra == "all"
61
+ Requires-Dist: duckdb>=1.1; extra == "all"
58
62
  Requires-Dist: prophet>=1.1.0; extra == "all"
59
63
  Requires-Dist: timesfm>=0.1.0; extra == "all"
60
64
  Requires-Dist: sqlglot>=20.0; extra == "all"
@@ -99,7 +103,7 @@ Dynamic: license-file
99
103
  - **@mentions** — tag users/groups in alerts, each channel formats natively
100
104
  - **Alert lifecycle** — consecutive anomalies, fraction-of-window rule (`anomaly_window` + `min_anomaly_share`), cooldown, recovery notifications, no-data alerts
101
105
  - **Project-level error alerts** — catch DB outages and pipeline crashes once per run
102
- - **Database agnostic** — ClickHouse, PostgreSQL, MySQL
106
+ - **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed)
103
107
  - **Idempotent** — resume from interruptions, no duplicate processing
104
108
  - **CLI** — `dtk init`, `dtk run --select`, `dtk unlock`, `dtk clean`, tag-based selectors
105
109
  - **AI-native onboarding** — `dtk init-claude` sets up Claude Code context (CLAUDE.md + rules + three skills) so an assistant can scaffold metrics, configure databases, and file feedback upstream
@@ -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
27
+ - **Database agnostic** — ClickHouse, PostgreSQL, MySQL/MariaDB, DuckDB (no server needed)
28
28
  - **Idempotent** — resume from interruptions, no duplicate processing
29
29
  - **CLI** — `dtk init`, `dtk run --select`, `dtk unlock`, `dtk clean`, tag-based selectors
30
30
  - **AI-native onboarding** — `dtk init-claude` sets up Claude Code context (CLAUDE.md + rules + three skills) so an assistant can scaffold metrics, configure databases, and file feedback upstream
@@ -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.59.0"
7
+ __version__ = "0.60.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): you can inspect
17
+ database MCP for the project's ClickHouse / PostgreSQL / MySQL / MariaDB / DuckDB): 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
@@ -116,10 +116,12 @@ alert_channels:
116
116
 
117
117
  ### Database profiles
118
118
 
119
- > ClickHouse, PostgreSQL, MySQL and MariaDB are all fully supported.
119
+ > ClickHouse, PostgreSQL, MySQL, MariaDB and DuckDB are all supported.
120
120
  > ClickHouse/MySQL/MariaDB use two *databases*; PostgreSQL connects to one
121
- > `database` and uses two *schemas*.
122
- > `dtk init --db-type {clickhouse,postgres,mysql,mariadb}` scaffolds the right shape.
121
+ > `database` and uses two *schemas*; DuckDB is a single *file* (or `:memory:`
122
+ > for tests) holding two *schemas*.
123
+ > `dtk init --db-type {clickhouse,postgres,mysql,mariadb}` scaffolds the right
124
+ > shape — DuckDB profiles are written by hand (not yet a `--db-type` choice).
123
125
 
124
126
  **ClickHouse**:
125
127
  ```yaml
@@ -173,6 +175,25 @@ auto-detected at connect (`SELECT VERSION()`), so MariaDB gets its own
173
175
  `VALUES()`-form upsert instead of MySQL 8.0.19's row-alias form.
174
176
  `pip install 'detectkit[mariadb]'`.
175
177
 
178
+ **DuckDB** (embedded, single file; two schemas like PostgreSQL):
179
+ ```yaml
180
+ profiles:
181
+ prod:
182
+ type: duckdb
183
+ path: /var/lib/detectkit/warehouse.duckdb # required — file path, or ":memory:"
184
+ internal_schema: detectkit # optional — _dtk_* tables (default: "detectkit")
185
+ data_schema: main # optional — data queries (default: "main")
186
+ read_only: false # optional (default: false)
187
+ settings: {} # optional — extra duckdb.connect() config options
188
+ ```
189
+ > Single-writer: DuckDB allows only **one** read-write connection to the file
190
+ > at a time — a `dtk ui`/`dtk tune` session left open will clash with a
191
+ > separately spawned `dtk run` against the same file. Point a read-only
192
+ > consumer at it with `read_only: true`.
193
+ > `path: ":memory:"` has no on-disk state, so resume/idempotency breaks across
194
+ > process restarts — use it for tests/scratch only, never a real project.
195
+ > `pip install 'detectkit[duckdb]'`.
196
+
176
197
  ### Alert channels
177
198
 
178
199
  Defined once in `profiles.yml`, referenced by name in each metric's
@@ -50,13 +50,17 @@ side by side, ask which one to set up.
50
50
 
51
51
  ## Step 1 — Pick the database backend
52
52
 
53
- **ClickHouse, PostgreSQL, MySQL and MariaDB are all fully supported.** Ask
53
+ **ClickHouse, PostgreSQL, MySQL, MariaDB and DuckDB are all supported.** Ask
54
54
  which one the project uses (default to ClickHouse if unsure). `dtk init
55
55
  --db-type {clickhouse,postgres,mysql,mariadb}` scaffolds `profiles.yml` for the
56
- chosen backend. The location fields differ:
56
+ chosen backend (DuckDB isn't a `--db-type` choice yet — write its profile by
57
+ hand; see `project.md`). The location fields differ:
57
58
  - **ClickHouse** / **MySQL** / **MariaDB** — two *databases*: `internal_database` / `data_database`.
58
59
  - **PostgreSQL** — connect to a `database` (must already exist), then two
59
60
  *schemas*: `internal_schema` / `data_schema`.
61
+ - **DuckDB** — a single **file** (`path`, or `":memory:"` for tests only),
62
+ then two *schemas*: `internal_schema` / `data_schema`. No host/port/user/
63
+ password — it's an embedded, single-writer database (see `project.md`).
60
64
 
61
65
  `type: mariadb` is an identical profile alias to `type: mysql` (same location
62
66
  fields; the vendor is auto-detected at connect). The metric query SQL dialect
@@ -123,6 +123,15 @@ _COMMENTED_EXAMPLES = {
123
123
  # password: ""
124
124
  # internal_database: detectkit
125
125
  # data_database: analytics
126
+ """,
127
+ "duckdb": """ # Example DuckDB profile (in-process, single-file; no host/port/user/password)
128
+ # duckdb_dev:
129
+ # type: duckdb
130
+ # path: ./detectkit.duckdb # or ':memory:' for a transient, tests-only DB
131
+ # internal_schema: detectkit
132
+ # 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.
126
135
  """,
127
136
  }
128
137
 
@@ -351,7 +360,10 @@ timeouts:
351
360
  # live under a top-level 'profiles:' mapping). The active dev/prod profiles
352
361
  # are scaffolded for the chosen --db-type; the other backends are included
353
362
  # as commented examples. See the per-database docs for connection details.
354
- other_backends = [t for t in ("clickhouse", "postgres", "mysql") if t != db_type]
363
+ # "duckdb" is not a `--db-type` choice (there is no active/prod scaffold
364
+ # for it — an in-process file DB has no separate dev/prod endpoints), so
365
+ # it always shows up as a commented example, unlike the other backends.
366
+ other_backends = [t for t in ("clickhouse", "postgres", "mysql", "duckdb") if t != db_type]
355
367
  commented = "\n".join(_COMMENTED_EXAMPLES[t] for t in other_backends)
356
368
  profiles_config = (
357
369
  "# Database connection profiles\n\n"
@@ -8,7 +8,7 @@ from pathlib import Path
8
8
  from typing import Any
9
9
 
10
10
  import yaml
11
- from pydantic import BaseModel, Field, field_validator
11
+ from pydantic import BaseModel, Field, field_validator, model_validator
12
12
 
13
13
  from detectkit.database.clickhouse_manager import ClickHouseDatabaseManager
14
14
  from detectkit.database.manager import BaseDatabaseManager
@@ -23,44 +23,68 @@ class ProfileConfig(BaseModel):
23
23
  environment (dev, prod, etc.).
24
24
 
25
25
  Attributes:
26
- type: Database type ("clickhouse", "postgres", "mysql", "mariadb")
27
- host: Database host
28
- port: Database port
29
- user: Database user
30
- password: Database password
26
+ type: Database type ("clickhouse", "postgres", "mysql", "mariadb", "duckdb")
27
+ 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)
34
+ path: Path to the DuckDB database file, or ":memory:" for a transient
35
+ in-process database (DuckDB only)
31
36
  internal_database: Database/schema for internal tables
32
- internal_schema: Schema for internal tables (PostgreSQL only)
37
+ internal_schema: Schema for internal tables (PostgreSQL/DuckDB only)
33
38
  data_database: Database for user data tables
34
- data_schema: Schema for user data (PostgreSQL only)
39
+ data_schema: Schema for user data (PostgreSQL/DuckDB only)
35
40
  settings: Additional database-specific settings
36
41
  """
37
42
 
38
43
  type: str = Field(..., description="Database type")
39
- host: str = Field(default="localhost", description="Database host")
40
- port: int = Field(..., description="Database port")
41
- user: str = Field(default="default", description="Database user")
42
- password: str = Field(default="", description="Database password")
44
+ host: str = Field(default="localhost", description="Database host (unused for DuckDB)")
45
+ # No default: required for every backend except DuckDB (enforced below by
46
+ # `_validate_port_required`, since DuckDB has no network port at all).
47
+ port: int | None = Field(default=None, description="Database port (unused for DuckDB)")
48
+ user: str = Field(default="default", description="Database user (unused for DuckDB)")
49
+ password: str = Field(default="", description="Database password (unused for DuckDB)")
43
50
 
44
51
  # Connection-target database. Required for PostgreSQL (the database to
45
52
  # connect to, inside which internal_schema/data_schema live); optional for
46
- # MySQL/MariaDB; unused for ClickHouse.
53
+ # MySQL/MariaDB; unused for ClickHouse and DuckDB.
47
54
  database: str | None = Field(
48
55
  default=None, description="Database to connect to (PostgreSQL/MySQL/MariaDB)"
49
56
  )
50
57
 
58
+ # DuckDB-only: the database file path (or ":memory:"). host/port/user/
59
+ # password/database above are simply ignored for this backend rather than
60
+ # rejected, since e.g. `host` always carries its "localhost" default.
61
+ path: str | None = Field(
62
+ default=None,
63
+ description="Database file path, or ':memory:' for a transient in-process database (DuckDB only)",
64
+ )
65
+ read_only: bool = Field(
66
+ default=False,
67
+ description=(
68
+ "Open the database read-only (DuckDB only) — lets a reader profile "
69
+ "coexist with the one process holding the file read-write"
70
+ ),
71
+ )
72
+
51
73
  # Internal location for _dtk_* tables
52
74
  internal_database: str | None = Field(
53
75
  default=None, description="Database for internal tables (ClickHouse/MySQL/MariaDB)"
54
76
  )
55
77
  internal_schema: str | None = Field(
56
- default=None, description="Schema for internal tables (PostgreSQL)"
78
+ default=None, description="Schema for internal tables (PostgreSQL/DuckDB)"
57
79
  )
58
80
 
59
81
  # Data location for user tables
60
82
  data_database: str | None = Field(
61
83
  default=None, description="Database for user data tables (ClickHouse/MySQL/MariaDB)"
62
84
  )
63
- data_schema: str | None = Field(default=None, description="Schema for user data (PostgreSQL)")
85
+ data_schema: str | None = Field(
86
+ default=None, description="Schema for user data (PostgreSQL/DuckDB)"
87
+ )
64
88
 
65
89
  settings: dict[str, Any] = Field(
66
90
  default_factory=dict, description="Additional database settings"
@@ -70,7 +94,7 @@ class ProfileConfig(BaseModel):
70
94
  @classmethod
71
95
  def validate_type(cls, v: str) -> str:
72
96
  """Validate database type."""
73
- allowed_types = {"clickhouse", "postgres", "mysql", "mariadb"}
97
+ allowed_types = {"clickhouse", "postgres", "mysql", "mariadb", "duckdb"}
74
98
  if v not in allowed_types:
75
99
  raise ValueError(
76
100
  f"Invalid database type: {v}. " f"Allowed types: {', '.join(allowed_types)}"
@@ -79,12 +103,21 @@ class ProfileConfig(BaseModel):
79
103
 
80
104
  @field_validator("port")
81
105
  @classmethod
82
- def validate_port(cls, v: int) -> int:
83
- """Validate port number."""
106
+ def validate_port(cls, v: int | None) -> int | None:
107
+ """Validate port number (when set — DuckDB profiles may omit it)."""
108
+ if v is None:
109
+ return v
84
110
  if not (1 <= v <= 65535):
85
111
  raise ValueError(f"Port must be between 1 and 65535, got {v}")
86
112
  return v
87
113
 
114
+ @model_validator(mode="after")
115
+ 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:
118
+ raise ValueError(f"port is required for database type '{self.type}'")
119
+ return self
120
+
88
121
  def get_internal_location(self) -> str:
89
122
  """
90
123
  Get internal location (database or schema).
@@ -107,6 +140,10 @@ class ProfileConfig(BaseModel):
107
140
  if not self.internal_database:
108
141
  raise ValueError("internal_database must be set for MySQL/MariaDB")
109
142
  return self.internal_database
143
+ elif self.type == "duckdb":
144
+ # Mirrors DuckDBDatabaseManager's own default (unlike PostgreSQL,
145
+ # an unset internal_schema doesn't need to be an error here).
146
+ return self.internal_schema or "detectkit"
110
147
  else:
111
148
  raise ValueError(f"Unsupported database type: {self.type}")
112
149
 
@@ -132,6 +169,10 @@ class ProfileConfig(BaseModel):
132
169
  if not self.data_database:
133
170
  raise ValueError("data_database must be set for MySQL/MariaDB")
134
171
  return self.data_database
172
+ elif self.type == "duckdb":
173
+ # Mirrors DuckDBDatabaseManager's own default data_schema ("main",
174
+ # DuckDB's always-present default schema).
175
+ return self.data_schema or "main"
135
176
  else:
136
177
  raise ValueError(f"Unsupported database type: {self.type}")
137
178
 
@@ -144,10 +185,14 @@ class ProfileConfig(BaseModel):
144
185
 
145
186
  Raises:
146
187
  ValueError: If the database type is unsupported, or required
147
- connection fields (e.g. PostgreSQL ``database``) are missing
188
+ connection fields (e.g. PostgreSQL ``database``, DuckDB
189
+ ``path``) are missing
148
190
  ImportError: If the backend's driver is not installed
149
191
  """
150
192
  if self.type == "clickhouse":
193
+ # `port` is guaranteed non-None here by `_validate_port_required`
194
+ # (every type but "duckdb" requires it at construction time).
195
+ assert self.port is not None
151
196
  return ClickHouseDatabaseManager(
152
197
  host=self.host,
153
198
  port=self.port,
@@ -165,6 +210,7 @@ class ProfileConfig(BaseModel):
165
210
  "PostgreSQL profiles must set 'database' (the database to "
166
211
  "connect to, inside which internal_schema/data_schema live)"
167
212
  )
213
+ assert self.port is not None
168
214
  return PostgresDatabaseManager(
169
215
  host=self.host,
170
216
  port=self.port,
@@ -180,6 +226,7 @@ class ProfileConfig(BaseModel):
180
226
  # connect time and adjusts the upsert SQL it generates accordingly.
181
227
  from detectkit.database.mysql_manager import MySQLDatabaseManager
182
228
 
229
+ assert self.port is not None
183
230
  return MySQLDatabaseManager(
184
231
  host=self.host,
185
232
  port=self.port,
@@ -190,6 +237,26 @@ class ProfileConfig(BaseModel):
190
237
  data_database=self.get_data_location(),
191
238
  settings=self.settings,
192
239
  )
240
+ elif self.type == "duckdb":
241
+ # In-process, file-backed database: host/port/user/password/database
242
+ # are meaningless for it and are simply not read here (rather than
243
+ # rejected — `host`/`user`/`password` always carry non-empty
244
+ # defaults, so rejecting them would punish every duckdb profile).
245
+ from detectkit.database.duckdb_manager import DuckDBDatabaseManager
246
+
247
+ if not self.path:
248
+ raise ValueError(
249
+ "DuckDB profiles must set 'path' (the database file path, "
250
+ "or ':memory:' for a transient, tests/preview-only "
251
+ "in-process database)"
252
+ )
253
+ return DuckDBDatabaseManager(
254
+ path=self.path,
255
+ internal_schema=self.get_internal_location(),
256
+ data_schema=self.get_data_location(),
257
+ read_only=self.read_only,
258
+ settings=self.settings,
259
+ )
193
260
  else:
194
261
  raise ValueError(f"Unsupported database type: {self.type}")
195
262