detectkit 0.56.1__tar.gz → 0.56.2__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 (156) hide show
  1. {detectkit-0.56.1/detectkit.egg-info → detectkit-0.56.2}/PKG-INFO +1 -1
  2. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/__init__.py +1 -1
  3. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/alerting/orchestrator/_base.py +11 -0
  4. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/alerting/orchestrator/_decision.py +13 -2
  5. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/config/metric_config.py +29 -0
  6. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/orchestration/task_manager/_alert_step.py +7 -0
  7. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/orchestration/task_manager/_base.py +18 -1
  8. {detectkit-0.56.1 → detectkit-0.56.2/detectkit.egg-info}/PKG-INFO +1 -1
  9. {detectkit-0.56.1 → detectkit-0.56.2}/LICENSE +0 -0
  10. {detectkit-0.56.1 → detectkit-0.56.2}/MANIFEST.in +0 -0
  11. {detectkit-0.56.1 → detectkit-0.56.2}/README.md +0 -0
  12. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/alerting/__init__.py +0 -0
  13. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/alerting/channels/__init__.py +0 -0
  14. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/alerting/channels/base.py +0 -0
  15. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/alerting/channels/branding.py +0 -0
  16. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/alerting/channels/email.py +0 -0
  17. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/alerting/channels/factory.py +0 -0
  18. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/alerting/channels/mattermost.py +0 -0
  19. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/alerting/channels/slack.py +0 -0
  20. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/alerting/channels/telegram.py +0 -0
  21. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/alerting/channels/webhook.py +0 -0
  22. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/alerting/orchestrator/__init__.py +0 -0
  23. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  24. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  25. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  26. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/alerting/orchestrator/_replay.py +0 -0
  27. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/alerting/orchestrator/_types.py +0 -0
  28. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  29. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/autotune/__init__.py +0 -0
  30. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/autotune/_base.py +0 -0
  31. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/autotune/_types.py +0 -0
  32. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/autotune/autotuner.py +0 -0
  33. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/autotune/axis_spec.py +0 -0
  34. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/autotune/config_emitter.py +0 -0
  35. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/autotune/crossval.py +0 -0
  36. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/autotune/detector_select.py +0 -0
  37. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/autotune/distribution.py +0 -0
  38. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/autotune/grid_search.py +0 -0
  39. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/autotune/labels.py +0 -0
  40. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/autotune/result.py +0 -0
  41. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/autotune/runner.py +0 -0
  42. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/autotune/scoring.py +0 -0
  43. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/autotune/seasonality_search.py +0 -0
  44. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/autotune/settings.py +0 -0
  45. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/autotune/window_select.py +0 -0
  46. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/__init__.py +0 -0
  47. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/_output.py +0 -0
  48. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
  49. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  50. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
  51. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/assets/claude/rules/cli.md +0 -0
  52. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  53. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  54. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  55. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/assets/claude/rules/project.md +0 -0
  56. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
  57. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  58. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  59. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  60. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +0 -0
  61. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/commands/__init__.py +0 -0
  62. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/commands/autotune.py +0 -0
  63. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/commands/clean.py +0 -0
  64. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/commands/init.py +0 -0
  65. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/commands/init_claude.py +0 -0
  66. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/commands/osi.py +0 -0
  67. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/commands/run.py +0 -0
  68. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/commands/test_alert.py +0 -0
  69. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/commands/tune.py +0 -0
  70. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/commands/ui.py +0 -0
  71. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/commands/unlock.py +0 -0
  72. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/cli/main.py +0 -0
  73. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/config/__init__.py +0 -0
  74. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/config/metric_io.py +0 -0
  75. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/config/profile.py +0 -0
  76. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/config/project_config.py +0 -0
  77. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/config/validator.py +0 -0
  78. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/core/__init__.py +0 -0
  79. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/core/interval.py +0 -0
  80. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/core/models.py +0 -0
  81. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/database/__init__.py +0 -0
  82. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/database/_sql_manager.py +0 -0
  83. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/database/clickhouse_manager.py +0 -0
  84. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/database/internal_tables/__init__.py +0 -0
  85. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/database/internal_tables/_alert_states.py +0 -0
  86. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  87. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/database/internal_tables/_base.py +0 -0
  88. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/database/internal_tables/_datapoints.py +0 -0
  89. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/database/internal_tables/_detections.py +0 -0
  90. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/database/internal_tables/_maintenance.py +0 -0
  91. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/database/internal_tables/_metrics.py +0 -0
  92. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/database/internal_tables/_schema.py +0 -0
  93. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/database/internal_tables/_tasks.py +0 -0
  94. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/database/internal_tables/manager.py +0 -0
  95. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/database/manager.py +0 -0
  96. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/database/mysql_manager.py +0 -0
  97. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/database/postgres_manager.py +0 -0
  98. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/database/tables.py +0 -0
  99. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/detectors/__init__.py +0 -0
  100. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/detectors/base.py +0 -0
  101. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/detectors/factory.py +0 -0
  102. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/detectors/seasonality.py +0 -0
  103. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/detectors/statistical/__init__.py +0 -0
  104. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/detectors/statistical/_windowed.py +0 -0
  105. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/detectors/statistical/autoreg.py +0 -0
  106. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/detectors/statistical/iqr.py +0 -0
  107. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/detectors/statistical/mad.py +0 -0
  108. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  109. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/detectors/statistical/zscore.py +0 -0
  110. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/loaders/__init__.py +0 -0
  111. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/loaders/metric_loader.py +0 -0
  112. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/loaders/query_template.py +0 -0
  113. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/orchestration/__init__.py +0 -0
  114. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/orchestration/error_dispatch.py +0 -0
  115. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/orchestration/task_manager/__init__.py +0 -0
  116. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  117. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  118. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/orchestration/task_manager/_types.py +0 -0
  119. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/orchestration/task_manager/manager.py +0 -0
  120. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/reporting/__init__.py +0 -0
  121. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/reporting/assets/report.js +0 -0
  122. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/reporting/builder.py +0 -0
  123. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/reporting/html_report.py +0 -0
  124. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/semantic/__init__.py +0 -0
  125. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/semantic/errors.py +0 -0
  126. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/semantic/exporter.py +0 -0
  127. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/semantic/importer.py +0 -0
  128. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/semantic/osi_model.py +0 -0
  129. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/semantic/query_gen.py +0 -0
  130. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/tuning/__init__.py +0 -0
  131. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/tuning/assets/tune.js +0 -0
  132. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/tuning/config_writer.py +0 -0
  133. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/tuning/html.py +0 -0
  134. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/tuning/payload.py +0 -0
  135. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/tuning/server.py +0 -0
  136. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/ui/__init__.py +0 -0
  137. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/ui/assets/ui.js +0 -0
  138. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/ui/html.py +0 -0
  139. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/ui/jobs.py +0 -0
  140. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/ui/metric_files.py +0 -0
  141. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/ui/overview.py +0 -0
  142. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/ui/server.py +0 -0
  143. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/utils/__init__.py +0 -0
  144. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/utils/datetime_utils.py +0 -0
  145. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/utils/env_interpolation.py +0 -0
  146. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/utils/json_utils.py +0 -0
  147. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit/utils/stats.py +0 -0
  148. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit.egg-info/SOURCES.txt +0 -0
  149. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit.egg-info/dependency_links.txt +0 -0
  150. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit.egg-info/entry_points.txt +0 -0
  151. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit.egg-info/requires.txt +0 -0
  152. {detectkit-0.56.1 → detectkit-0.56.2}/detectkit.egg-info/top_level.txt +0 -0
  153. {detectkit-0.56.1 → detectkit-0.56.2}/pyproject.toml +0 -0
  154. {detectkit-0.56.1 → detectkit-0.56.2}/requirements.txt +0 -0
  155. {detectkit-0.56.1 → detectkit-0.56.2}/setup.cfg +0 -0
  156. {detectkit-0.56.1 → detectkit-0.56.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.56.1
3
+ Version: 0.56.2
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -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.56.1"
7
+ __version__ = "0.56.2"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -38,6 +38,7 @@ class _OrchestratorBase:
38
38
  help_url: str | None = None,
39
39
  ai_synonyms: list[str] | None = None,
40
40
  loading_delay_seconds: int = 0,
41
+ grid_phase_seconds: int = 0,
41
42
  ):
42
43
  self.metric_name = metric_name
43
44
  self.interval = interval
@@ -69,6 +70,16 @@ class _OrchestratorBase:
69
70
  # step AND the recovery mixin's own fetch bound) computes the same
70
71
  # delay-aware boundary as the load step's maturity cut-off.
71
72
  self.loading_delay_seconds = int(loading_delay_seconds or 0)
73
+ # Phase of the metric's interval grid on the epoch clock, in
74
+ # [0, interval). The loader anchors datapoints on loading_start_time, so
75
+ # a non-epoch-aligned start puts the stored grid at an arbitrary phase;
76
+ # ``get_last_complete_point`` floors to THIS phase (not plain epoch time)
77
+ # so its exact-timestamp no-data lookup asks for a boundary the loader
78
+ # actually writes. Constructor state, like ``loading_delay_seconds`` —
79
+ # the two together keep the no-data expectation in lockstep with the load
80
+ # step's grid. Defaults to 0 (the epoch grid), so direct-API callers and
81
+ # epoch-aligned metrics are unchanged (issue #114).
82
+ self.grid_phase_seconds = int(grid_phase_seconds or 0) % interval.seconds
72
83
 
73
84
  @staticmethod
74
85
  def _group_by_timestamp(
@@ -474,6 +474,15 @@ class _DecisionMixin(_OrchestratorBase):
474
474
  def get_last_complete_point(self, now: datetime | None = None) -> datetime:
475
475
  """Floor ``now`` to the previous fully completed *mature* interval boundary.
476
476
 
477
+ Flooring happens on the metric's **own** interval grid, whose phase on
478
+ the epoch clock is ``grid_phase_seconds`` (0 = epoch-aligned). The loader
479
+ anchors datapoints on ``loading_start_time``, so a non-epoch-aligned
480
+ start (e.g. ``:07`` on a 10-minute grid) puts the stored series at an
481
+ arbitrary phase; flooring plain epoch time would return a boundary the
482
+ loader never writes and make the exact-timestamp no-data lookup fire a
483
+ permanent false alert (issue #114). Phasing here keeps the no-data
484
+ expectation on the same grid the load step persists.
485
+
477
486
  With a ``loading_delay`` configured, the loader only trusts
478
487
  ``[t, t+interval)`` once ``now >= t + interval + delay`` — so the same
479
488
  delay shifts the effective now back here BEFORE flooring, keeping the
@@ -487,10 +496,12 @@ class _DecisionMixin(_OrchestratorBase):
487
496
  now = to_aware_utc(now)
488
497
 
489
498
  interval_seconds = self.interval.seconds
499
+ phase = self.grid_phase_seconds # already normalised into [0, interval)
490
500
  # Shift the effective now back by the maturity delay BEFORE flooring
491
501
  # (subtract-then-floor keeps the boundary correct for delays that
492
- # aren't a multiple of the interval).
502
+ # aren't a multiple of the interval), then floor onto the metric's grid
503
+ # phase rather than the epoch grid.
493
504
  effective_epoch = int(now.timestamp()) - self.loading_delay_seconds
494
- floored = (effective_epoch // interval_seconds) * interval_seconds
505
+ floored = ((effective_epoch - phase) // interval_seconds) * interval_seconds + phase
495
506
  last_complete = floored - interval_seconds
496
507
  return datetime.fromtimestamp(last_complete, tz=timezone.utc)
@@ -5,6 +5,7 @@ Defines configuration structure for individual metrics loaded from YAML files.
5
5
  """
6
6
 
7
7
  import re
8
+ from datetime import datetime, timezone
8
9
  from pathlib import Path
9
10
  from typing import Any
10
11
 
@@ -1051,3 +1052,31 @@ def resolve_loading_delay_seconds(
1051
1052
  continue
1052
1053
  return loading_delay_to_seconds(value)
1053
1054
  return 0
1055
+
1056
+
1057
+ def resolve_grid_phase_seconds(loading_start_time: str | None, interval_seconds: int) -> int:
1058
+ """Phase of the metric's interval grid on the epoch clock, in ``[0, interval)``.
1059
+
1060
+ The loader anchors every datapoint on ``loading_start_time`` (first run) or
1061
+ the resume cursor descended from it, so the stored grid sits at
1062
+ ``loading_start_time_epoch % interval`` — an arbitrary phase whenever the
1063
+ start isn't a multiple of the interval (e.g. ``00:07:00`` on a 10-minute
1064
+ grid). The alert step's no-data check does an **exact-timestamp** lookup, so
1065
+ it must floor to that same phase; flooring plain epoch time (phase 0) instead
1066
+ asks for a boundary the loader never writes and fires a permanent false
1067
+ no-data alert. This is the single seam that keeps the two grid phases in
1068
+ lockstep — the mirror of :func:`resolve_loading_delay_seconds` for the
1069
+ maturity delay (see issue #114).
1070
+
1071
+ Returns ``0`` (the epoch grid — unchanged behaviour) when no
1072
+ ``loading_start_time`` is set or it can't be parsed in the canonical
1073
+ ``"%Y-%m-%d %H:%M:%S"`` UTC form the loader uses.
1074
+ """
1075
+ if not loading_start_time or interval_seconds <= 0:
1076
+ return 0
1077
+ try:
1078
+ start = datetime.strptime(loading_start_time, "%Y-%m-%d %H:%M:%S")
1079
+ except (ValueError, TypeError):
1080
+ return 0
1081
+ epoch = int(start.replace(tzinfo=timezone.utc).timestamp())
1082
+ return epoch % interval_seconds
@@ -33,6 +33,12 @@ class _AlertStepMixin(_TaskManagerBase):
33
33
  # every other last-complete-point computation) must not run ahead of
34
34
  # what load has had a chance to persist under a loading_delay.
35
35
  loading_delay_seconds = self._loading_delay_seconds(config)
36
+ # Same grid phase as the load step: the no-data check floors to the
37
+ # metric's own interval grid (anchored on loading_start_time), not plain
38
+ # epoch time, so a non-epoch-aligned start (e.g. :07 on a 10min grid)
39
+ # doesn't make the exact-timestamp lookup ask for a boundary the loader
40
+ # never writes — a permanent false no-data alert (issue #114).
41
+ grid_phase_seconds = self._grid_phase_seconds(config)
36
42
 
37
43
  for i, alerting_config in enumerate(active_configs):
38
44
  if multi:
@@ -75,6 +81,7 @@ class _AlertStepMixin(_TaskManagerBase):
75
81
  project_name=getattr(self.project_config, "name", None),
76
82
  help_url=help_url,
77
83
  loading_delay_seconds=loading_delay_seconds,
84
+ grid_phase_seconds=grid_phase_seconds,
78
85
  )
79
86
 
80
87
  last_point = orchestrator.get_last_complete_point()
@@ -7,7 +7,11 @@ from datetime import datetime
7
7
  from detectkit.alerting.channels.base import BaseAlertChannel
8
8
  from detectkit.alerting.channels.factory import AlertChannelFactory
9
9
  from detectkit.alerting.orchestrator import DetectionRecord, hydrate_detection_records
10
- from detectkit.config.metric_config import MetricConfig, resolve_loading_delay_seconds
10
+ from detectkit.config.metric_config import (
11
+ MetricConfig,
12
+ resolve_grid_phase_seconds,
13
+ resolve_loading_delay_seconds,
14
+ )
11
15
  from detectkit.database.internal_tables import InternalTablesManager
12
16
 
13
17
 
@@ -40,6 +44,19 @@ class _TaskManagerBase:
40
44
  getattr(self.project_config, "loading_delay", None),
41
45
  )
42
46
 
47
+ def _grid_phase_seconds(self, config: MetricConfig) -> int:
48
+ """Phase of *config*'s interval grid on the epoch clock (metric-derived).
49
+
50
+ The loader anchors the stored grid on ``loading_start_time``; the alert
51
+ step's exact-timestamp no-data lookup must floor to the same phase, so
52
+ this one seam feeds the orchestrator's ``get_last_complete_point`` — the
53
+ mirror of ``_loading_delay_seconds`` for the grid phase (issue #114).
54
+ """
55
+ return resolve_grid_phase_seconds(
56
+ config.loading_start_time,
57
+ config.get_interval().seconds,
58
+ )
59
+
43
60
  def _load_recent_detections(
44
61
  self,
45
62
  metric_name: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.56.1
3
+ Version: 0.56.2
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes