detectkit 0.62.0__tar.gz → 0.63.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 (169) hide show
  1. {detectkit-0.62.0/detectkit.egg-info → detectkit-0.63.0}/PKG-INFO +3 -2
  2. {detectkit-0.62.0 → detectkit-0.63.0}/README.md +2 -1
  3. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/__init__.py +1 -1
  4. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/assets/claude/rules/cli.md +7 -0
  5. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/assets/claude/rules/metrics.md +8 -0
  6. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/assets/claude/rules/overview.md +3 -2
  7. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/loaders/metric_loader.py +83 -0
  8. {detectkit-0.62.0 → detectkit-0.63.0/detectkit.egg-info}/PKG-INFO +3 -2
  9. {detectkit-0.62.0 → detectkit-0.63.0}/LICENSE +0 -0
  10. {detectkit-0.62.0 → detectkit-0.63.0}/MANIFEST.in +0 -0
  11. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/__init__.py +0 -0
  12. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/channels/__init__.py +0 -0
  13. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/channels/base.py +0 -0
  14. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/channels/branding.py +0 -0
  15. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/channels/discord.py +0 -0
  16. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/channels/email.py +0 -0
  17. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/channels/factory.py +0 -0
  18. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/channels/googlechat.py +0 -0
  19. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/channels/mattermost.py +0 -0
  20. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/channels/ntfy.py +0 -0
  21. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/channels/slack.py +0 -0
  22. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/channels/teams.py +0 -0
  23. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/channels/telegram.py +0 -0
  24. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/channels/webhook.py +0 -0
  25. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  26. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/orchestrator/_base.py +0 -0
  27. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  28. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
  29. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  30. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  31. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
  32. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  33. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  34. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/autotune/__init__.py +0 -0
  35. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/autotune/_base.py +0 -0
  36. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/autotune/_types.py +0 -0
  37. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/autotune/autotuner.py +0 -0
  38. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/autotune/axis_spec.py +0 -0
  39. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/autotune/config_emitter.py +0 -0
  40. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/autotune/crossval.py +0 -0
  41. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/autotune/detector_select.py +0 -0
  42. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/autotune/distribution.py +0 -0
  43. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/autotune/grid_search.py +0 -0
  44. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/autotune/labels.py +0 -0
  45. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/autotune/result.py +0 -0
  46. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/autotune/runner.py +0 -0
  47. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/autotune/scoring.py +0 -0
  48. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/autotune/seasonality_search.py +0 -0
  49. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/autotune/settings.py +0 -0
  50. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/autotune/window_select.py +0 -0
  51. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/__init__.py +0 -0
  52. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/_output.py +0 -0
  53. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
  54. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  55. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
  56. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  57. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
  58. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
  59. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  60. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  61. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  62. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +0 -0
  63. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/commands/__init__.py +0 -0
  64. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/commands/autotune.py +0 -0
  65. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/commands/clean.py +0 -0
  66. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/commands/init.py +0 -0
  67. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/commands/init_claude.py +0 -0
  68. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/commands/mcp.py +0 -0
  69. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/commands/osi.py +0 -0
  70. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/commands/run.py +0 -0
  71. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/commands/test_alert.py +0 -0
  72. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/commands/tune.py +0 -0
  73. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/commands/ui.py +0 -0
  74. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/commands/unlock.py +0 -0
  75. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/cli/main.py +0 -0
  76. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/config/__init__.py +0 -0
  77. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/config/metric_config.py +0 -0
  78. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/config/metric_io.py +0 -0
  79. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/config/profile.py +0 -0
  80. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/config/project_config.py +0 -0
  81. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/config/validator.py +0 -0
  82. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/core/__init__.py +0 -0
  83. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/core/interval.py +0 -0
  84. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/core/models.py +0 -0
  85. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/database/__init__.py +0 -0
  86. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/database/_sql_manager.py +0 -0
  87. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/database/clickhouse_manager.py +0 -0
  88. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/database/duckdb_manager.py +0 -0
  89. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/database/internal_tables/__init__.py +0 -0
  90. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  91. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  92. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/database/internal_tables/_base.py +0 -0
  93. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  94. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/database/internal_tables/_detections.py +0 -0
  95. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  96. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  97. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/database/internal_tables/_schema.py +0 -0
  98. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  99. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/database/internal_tables/manager.py +0 -0
  100. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/database/manager.py +0 -0
  101. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/database/mysql_manager.py +0 -0
  102. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/database/postgres_manager.py +0 -0
  103. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/database/tables.py +0 -0
  104. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/detectors/__init__.py +0 -0
  105. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/detectors/base.py +0 -0
  106. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/detectors/factory.py +0 -0
  107. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/detectors/seasonality.py +0 -0
  108. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/detectors/statistical/__init__.py +0 -0
  109. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  110. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/detectors/statistical/autoreg.py +0 -0
  111. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/detectors/statistical/iqr.py +0 -0
  112. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/detectors/statistical/mad.py +0 -0
  113. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  114. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/detectors/statistical/zscore.py +0 -0
  115. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/loaders/__init__.py +0 -0
  116. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/loaders/errors.py +0 -0
  117. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/loaders/query_template.py +0 -0
  118. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/mcp/__init__.py +0 -0
  119. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/mcp/context.py +0 -0
  120. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/mcp/errors.py +0 -0
  121. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/mcp/serialize.py +0 -0
  122. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/mcp/server.py +0 -0
  123. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/mcp/tools.py +0 -0
  124. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/orchestration/__init__.py +0 -0
  125. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/orchestration/error_dispatch.py +0 -0
  126. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  127. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  128. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  129. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  130. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  131. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  132. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  133. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/reporting/__init__.py +0 -0
  134. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/reporting/assets/report.js +0 -0
  135. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/reporting/builder.py +0 -0
  136. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/reporting/html_report.py +0 -0
  137. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/semantic/__init__.py +0 -0
  138. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/semantic/errors.py +0 -0
  139. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/semantic/exporter.py +0 -0
  140. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/semantic/importer.py +0 -0
  141. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/semantic/osi_model.py +0 -0
  142. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/semantic/query_gen.py +0 -0
  143. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/tuning/__init__.py +0 -0
  144. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/tuning/assets/tune.js +0 -0
  145. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/tuning/config_writer.py +0 -0
  146. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/tuning/html.py +0 -0
  147. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/tuning/payload.py +0 -0
  148. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/tuning/server.py +0 -0
  149. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/ui/__init__.py +0 -0
  150. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/ui/assets/ui.js +0 -0
  151. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/ui/html.py +0 -0
  152. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/ui/jobs.py +0 -0
  153. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/ui/metric_files.py +0 -0
  154. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/ui/overview.py +0 -0
  155. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/ui/server.py +0 -0
  156. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/utils/__init__.py +0 -0
  157. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/utils/datetime_utils.py +0 -0
  158. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/utils/env_interpolation.py +0 -0
  159. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/utils/json_utils.py +0 -0
  160. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit/utils/stats.py +0 -0
  161. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit.egg-info/SOURCES.txt +0 -0
  162. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit.egg-info/dependency_links.txt +0 -0
  163. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit.egg-info/entry_points.txt +0 -0
  164. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit.egg-info/requires.txt +0 -0
  165. {detectkit-0.62.0 → detectkit-0.63.0}/detectkit.egg-info/top_level.txt +0 -0
  166. {detectkit-0.62.0 → detectkit-0.63.0}/pyproject.toml +0 -0
  167. {detectkit-0.62.0 → detectkit-0.63.0}/requirements.txt +0 -0
  168. {detectkit-0.62.0 → detectkit-0.63.0}/setup.cfg +0 -0
  169. {detectkit-0.62.0 → detectkit-0.63.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.62.0
3
+ Version: 0.63.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -110,7 +110,7 @@ Dynamic: license-file
110
110
  - **Hybrid mode** — read metric SQL from a billed-per-query warehouse while all pipeline state lives in a cheap local database
111
111
  - **Idempotent** — resume from interruptions, no duplicate processing
112
112
  - **CLI** — `dtk init`, `dtk run --select`, `dtk unlock`, `dtk clean`, tag-based selectors
113
- - **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
113
+ - **AI-native onboarding** — `dtk init-claude` sets up Claude Code context (CLAUDE.md + rules + five skills) so an assistant can scaffold metrics, configure databases, and file feedback upstream
114
114
 
115
115
  ## Installation
116
116
 
@@ -122,6 +122,7 @@ With database drivers:
122
122
 
123
123
  ```bash
124
124
  pip install detectkit[clickhouse] # ClickHouse
125
+ pip install detectkit[duckdb] # DuckDB - single file, no server
125
126
  pip install detectkit[all-db] # All databases
126
127
  ```
127
128
 
@@ -28,7 +28,7 @@
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
31
- - **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
31
+ - **AI-native onboarding** — `dtk init-claude` sets up Claude Code context (CLAUDE.md + rules + five skills) so an assistant can scaffold metrics, configure databases, and file feedback upstream
32
32
 
33
33
  ## Installation
34
34
 
@@ -40,6 +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
44
  pip install detectkit[all-db] # All databases
44
45
  ```
45
46
 
@@ -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.62.0"
7
+ __version__ = "0.63.0"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -458,6 +458,13 @@ fail their step when `dtk run` / `dtk autotune` / `dtk clean` exits non-zero
458
458
  (see "Exit codes" above), and can additionally parse `dtk run --json`'s stdout
459
459
  summary for per-metric detail.
460
460
 
461
+ On GitHub Actions specifically, detectkit ships a ready-made composite action
462
+ instead of hand-rolling those steps: `uses: alexeiveselov92/detectkit@vX.Y.Z`
463
+ installs detectkit from PyPI and runs `dtk run` / `dtk autotune` / `dtk clean`
464
+ in a given project directory. It surfaces the 0/1/2 exit-code contract (see
465
+ "Exit codes" above) as the job's own outcome, and exposes the
466
+ `dtk run --json` summary as an action output for downstream steps to consume.
467
+
461
468
  ## Troubleshooting
462
469
 
463
470
  - **"Metric not found"** — selector doesn't match. Use the bare name
@@ -119,6 +119,14 @@ The query must return a **timestamp** column and a numeric **value** column
119
119
  (default names `timestamp` / `value`; remap via `query_columns`). It may also
120
120
  return seasonality columns (declare them in `query_columns.seasonality`).
121
121
 
122
+ Returned timestamps must already be **bucketed to the interval grid**
123
+ (`toStartOfInterval` / `time_bucket` / `date_trunc`) — gap filling maps rows
124
+ onto the grid by exact match, and the grid's phase comes from
125
+ `loading_start_time`. A batch that returns rows on a different phase (e.g.
126
+ `:28` past the hour vs a grid phased at `:00`) can land 100% off-grid and
127
+ load as silently all-`NULL`; detectkit logs a one-time warning naming both
128
+ phases when that happens.
129
+
122
130
  ## Seasonality features
123
131
 
124
132
  Two ways to provide seasonality keys that detectors group by:
@@ -4,8 +4,9 @@ detectkit is a Python library and CLI (`dtk`) for monitoring time-series
4
4
  metrics with automatic anomaly detection and multi-channel alerting. It is
5
5
  **dbt-like**: metrics live as YAML + SQL in a project directory, and you run
6
6
  them with one command. Core logic is pure numpy (no pandas). **ClickHouse,
7
- PostgreSQL and MySQL are all fully supported** — only the connection and the SQL
8
- dialect of your metric queries differ between them.
7
+ PostgreSQL, MySQL/MariaDB and DuckDB are all fully supported** — only the
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
10
 
10
11
  ## The pipeline: load → detect → alert
11
12
 
@@ -9,6 +9,7 @@ Loads time-series data from databases with:
9
9
  - Integration with InternalTablesManager
10
10
  """
11
11
 
12
+ import logging
12
13
  from datetime import datetime, timedelta
13
14
 
14
15
  import numpy as np
@@ -21,6 +22,30 @@ from detectkit.loaders.query_template import QueryTemplate
21
22
  from detectkit.utils.datetime_utils import now_utc_naive, to_naive_utc
22
23
  from detectkit.utils.json_utils import json_dumps_sorted
23
24
 
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def _format_offset_seconds(seconds: int) -> str:
29
+ """Render an offset-within-interval (seconds) as a human clock offset.
30
+
31
+ Used to describe a grid/source "phase" without assuming a particular
32
+ interval unit: ``0`` -> ``":00"``, ``1680`` -> ``":28"`` (28 minutes),
33
+ ``90`` -> ``":01:30"``, ``3661`` -> ``"1:01:01"``.
34
+ """
35
+ hours, remainder = divmod(int(seconds), 3600)
36
+ minutes, secs = divmod(remainder, 60)
37
+ if hours:
38
+ return f"{hours}:{minutes:02d}:{secs:02d}"
39
+ if secs:
40
+ return f":{minutes:02d}:{secs:02d}"
41
+ return f":{minutes:02d}"
42
+
43
+
44
+ def _format_datetime64(ts: np.datetime64) -> str:
45
+ """Render a datetime64 as ``YYYY-MM-DD HH:MM:SS`` (naive UTC convention)."""
46
+ dt: datetime = ts.astype("datetime64[s]").astype(datetime)
47
+ return dt.strftime("%Y-%m-%d %H:%M:%S")
48
+
24
49
 
25
50
  class MetricLoader:
26
51
  """
@@ -76,6 +101,9 @@ class MetricLoader:
76
101
  self.internal_manager = internal_manager
77
102
  self.source_profile_name = source_profile_name
78
103
  self.query_template = QueryTemplate()
104
+ # Emitted at most once per loader instance (one instance per metric
105
+ # per run) - see _check_grid_alignment.
106
+ self._warned_grid_misalignment = False
79
107
 
80
108
  def load(
81
109
  self,
@@ -246,6 +274,7 @@ class MetricLoader:
246
274
  timestamp_array, value_array = self._fill_gaps(
247
275
  timestamp_array, value_array, from_date, to_date, interval_seconds
248
276
  )
277
+ self._check_grid_alignment(original_timestamps, timestamp_array, interval_seconds)
249
278
 
250
279
  # Realign query-provided seasonality to the gap-filled grid.
251
280
  # Gap rows can appear ANYWHERE in the range and _fill_gaps also
@@ -423,6 +452,60 @@ class MetricLoader:
423
452
 
424
453
  return full_timestamps, filled_values
425
454
 
455
+ def _check_grid_alignment(
456
+ self,
457
+ original_timestamps: np.ndarray,
458
+ grid_timestamps: np.ndarray,
459
+ interval_seconds: int,
460
+ ) -> None:
461
+ """Warn once if a non-empty query result landed entirely off the grid.
462
+
463
+ ``_fill_gaps`` maps source rows onto the gap-filled grid by exact
464
+ timestamp match. If the query returns rows bucketed to a different
465
+ grid "phase" than ``loading_start_time`` establishes (e.g. rows at
466
+ :28 past the hour against a 10min grid phased at :00), NONE of them
467
+ match and the batch loads as silently 100% NULL - no error, no
468
+ partial data, just an empty-looking series. Partial alignment is not
469
+ the trap this guards against (some rows landing is normal, e.g. a
470
+ source with occasional off-grid duplicates), so this only fires when
471
+ the overlap is exactly zero. Observability only - does not change
472
+ what gets loaded.
473
+ """
474
+ if self._warned_grid_misalignment:
475
+ return
476
+ if len(original_timestamps) == 0 or len(grid_timestamps) == 0:
477
+ return
478
+ if np.any(np.isin(original_timestamps, grid_timestamps)):
479
+ return
480
+
481
+ self._warned_grid_misalignment = True
482
+
483
+ grid_phase_seconds = int(
484
+ grid_timestamps[0].astype("datetime64[s]").astype(np.int64) % interval_seconds
485
+ )
486
+ source_offsets = (
487
+ original_timestamps.astype("datetime64[s]").astype(np.int64) % interval_seconds
488
+ )
489
+ offsets, counts = np.unique(source_offsets, return_counts=True)
490
+ source_phase_seconds = int(offsets[int(np.argmax(counts))])
491
+
492
+ sample_ts = original_timestamps[0]
493
+ nearest_ts = grid_timestamps[int(np.argmin(np.abs(grid_timestamps - sample_ts)))]
494
+
495
+ logger.warning(
496
+ "%s: query returned %d row(s) but none align with the metric's %s grid "
497
+ "(grid phase %s, source phase %s). e.g. source row %s vs nearest grid "
498
+ "slot %s. Bucket the query's timestamps (toStartOfInterval / time_bucket "
499
+ "/ date_trunc) or set loading_start_time to match the source phase.",
500
+ self.config.name,
501
+ len(original_timestamps),
502
+ self.config.interval,
503
+ _format_offset_seconds(grid_phase_seconds),
504
+ _format_offset_seconds(source_phase_seconds),
505
+ _format_datetime64(sample_ts),
506
+ _format_datetime64(nearest_ts),
507
+ )
508
+
426
509
  def _extract_seasonality(
427
510
  self,
428
511
  timestamps: np.ndarray,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.62.0
3
+ Version: 0.63.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -110,7 +110,7 @@ Dynamic: license-file
110
110
  - **Hybrid mode** — read metric SQL from a billed-per-query warehouse while all pipeline state lives in a cheap local database
111
111
  - **Idempotent** — resume from interruptions, no duplicate processing
112
112
  - **CLI** — `dtk init`, `dtk run --select`, `dtk unlock`, `dtk clean`, tag-based selectors
113
- - **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
113
+ - **AI-native onboarding** — `dtk init-claude` sets up Claude Code context (CLAUDE.md + rules + five skills) so an assistant can scaffold metrics, configure databases, and file feedback upstream
114
114
 
115
115
  ## Installation
116
116
 
@@ -122,6 +122,7 @@ With database drivers:
122
122
 
123
123
  ```bash
124
124
  pip install detectkit[clickhouse] # ClickHouse
125
+ pip install detectkit[duckdb] # DuckDB - single file, no server
125
126
  pip install detectkit[all-db] # All databases
126
127
  ```
127
128
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes