detectkit 0.10.0__tar.gz → 0.12.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 (104) hide show
  1. {detectkit-0.10.0/detectkit.egg-info → detectkit-0.12.0}/PKG-INFO +4 -2
  2. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/__init__.py +1 -1
  3. detectkit-0.12.0/detectkit/alerting/channels/branding.py +20 -0
  4. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/channels/email.py +46 -2
  5. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/channels/mattermost.py +12 -4
  6. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/channels/slack.py +12 -4
  7. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/channels/webhook.py +28 -7
  8. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/overview.md +3 -3
  9. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/project.md +34 -22
  10. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +22 -12
  11. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/commands/init.py +189 -97
  12. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/main.py +11 -3
  13. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/config/profile.py +39 -3
  14. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/core/models.py +11 -0
  15. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/__init__.py +6 -0
  16. detectkit-0.12.0/detectkit/database/_sql_manager.py +398 -0
  17. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/clickhouse_manager.py +38 -16
  18. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_alert_states.py +14 -29
  19. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_datapoints.py +6 -5
  20. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_detections.py +9 -11
  21. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_maintenance.py +5 -7
  22. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_schema.py +5 -1
  23. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/manager.py +73 -0
  24. detectkit-0.12.0/detectkit/database/mysql_manager.py +132 -0
  25. detectkit-0.12.0/detectkit/database/postgres_manager.py +118 -0
  26. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/tables.py +3 -0
  27. {detectkit-0.10.0 → detectkit-0.12.0/detectkit.egg-info}/PKG-INFO +4 -2
  28. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit.egg-info/SOURCES.txt +4 -0
  29. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit.egg-info/requires.txt +3 -1
  30. {detectkit-0.10.0 → detectkit-0.12.0}/pyproject.toml +5 -1
  31. {detectkit-0.10.0 → detectkit-0.12.0}/LICENSE +0 -0
  32. {detectkit-0.10.0 → detectkit-0.12.0}/MANIFEST.in +0 -0
  33. {detectkit-0.10.0 → detectkit-0.12.0}/README.md +0 -0
  34. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/__init__.py +0 -0
  35. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/channels/__init__.py +0 -0
  36. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/channels/base.py +0 -0
  37. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/channels/factory.py +0 -0
  38. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/channels/telegram.py +0 -0
  39. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  40. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_base.py +0 -0
  41. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  42. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
  43. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  44. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  45. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  46. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  47. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/__init__.py +0 -0
  48. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/_output.py +0 -0
  49. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
  50. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  51. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
  52. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  53. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  54. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  55. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/commands/__init__.py +0 -0
  56. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/commands/clean.py +0 -0
  57. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/commands/init_claude.py +0 -0
  58. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/commands/run.py +0 -0
  59. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/commands/test_alert.py +0 -0
  60. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/cli/commands/unlock.py +0 -0
  61. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/config/__init__.py +0 -0
  62. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/config/metric_config.py +0 -0
  63. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/config/project_config.py +0 -0
  64. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/config/validator.py +0 -0
  65. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/core/__init__.py +0 -0
  66. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/core/interval.py +0 -0
  67. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/internal_tables/__init__.py +0 -0
  68. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_base.py +0 -0
  69. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  70. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  71. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/database/internal_tables/manager.py +0 -0
  72. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/detectors/__init__.py +0 -0
  73. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/detectors/base.py +0 -0
  74. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/detectors/factory.py +0 -0
  75. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/detectors/seasonality.py +0 -0
  76. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/detectors/statistical/__init__.py +0 -0
  77. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  78. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/detectors/statistical/iqr.py +0 -0
  79. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/detectors/statistical/mad.py +0 -0
  80. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  81. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/detectors/statistical/zscore.py +0 -0
  82. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/loaders/__init__.py +0 -0
  83. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/loaders/metric_loader.py +0 -0
  84. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/loaders/query_template.py +0 -0
  85. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/orchestration/__init__.py +0 -0
  86. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/orchestration/error_dispatch.py +0 -0
  87. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  88. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  89. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  90. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  91. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  92. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  93. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  94. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/utils/__init__.py +0 -0
  95. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/utils/datetime_utils.py +0 -0
  96. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/utils/env_interpolation.py +0 -0
  97. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/utils/json_utils.py +0 -0
  98. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit/utils/stats.py +0 -0
  99. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit.egg-info/dependency_links.txt +0 -0
  100. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit.egg-info/entry_points.txt +0 -0
  101. {detectkit-0.10.0 → detectkit-0.12.0}/detectkit.egg-info/top_level.txt +0 -0
  102. {detectkit-0.10.0 → detectkit-0.12.0}/requirements.txt +0 -0
  103. {detectkit-0.10.0 → detectkit-0.12.0}/setup.cfg +0 -0
  104. {detectkit-0.10.0 → detectkit-0.12.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.10.0
3
+ Version: 0.12.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -61,7 +61,9 @@ Requires-Dist: black>=23.0; extra == "dev"
61
61
  Requires-Dist: mypy>=1.0; extra == "dev"
62
62
  Requires-Dist: ruff>=0.1.0; extra == "dev"
63
63
  Provides-Extra: integration
64
- Requires-Dist: testcontainers[clickhouse]>=4.0; extra == "integration"
64
+ Requires-Dist: testcontainers[clickhouse,mysql,postgres]>=4.0; extra == "integration"
65
+ Requires-Dist: psycopg2-binary>=2.9.0; extra == "integration"
66
+ Requires-Dist: pymysql>=1.0.0; extra == "integration"
65
67
  Dynamic: license-file
66
68
 
67
69
  # detectkit
@@ -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.10.0"
7
+ __version__ = "0.12.0"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -0,0 +1,20 @@
1
+ """
2
+ Default detectkit branding for alert channels.
3
+
4
+ Centralizes the bot identity — display name and avatar — so every channel
5
+ defaults to the **detectkit brand**. These are only the fallbacks: each channel
6
+ still accepts per-channel overrides (``username`` / ``from_name`` for the name,
7
+ ``icon_url`` / ``icon_emoji`` for the avatar). Keeping them in one place means
8
+ the brand avatar URL and name have a single source of truth.
9
+ """
10
+
11
+ # Bot display name shown as the message sender across channels (webhook-family
12
+ # ``username`` and the email ``From`` display name).
13
+ BRAND_USERNAME = "detectkit"
14
+
15
+ # Brand avatar served from the docs site. Slack/Mattermost render the webhook
16
+ # bot icon from a **raster** URL (an SVG is not rendered as a bot avatar), so
17
+ # this points at a PNG. Override per channel with ``icon_url`` (a custom image)
18
+ # or opt out of the avatar entirely with ``icon_emoji``. The PNG is generated by
19
+ # ``website/scripts/make-bot-icon.mjs`` and served from ``website/public/``.
20
+ BRAND_ICON_URL = "https://dtk.pipelab.dev/bot-icon.png"
@@ -4,11 +4,14 @@ Email alert channel implementation.
4
4
  Sends anomaly alerts via SMTP email.
5
5
  """
6
6
 
7
+ import html
7
8
  import smtplib
8
9
  from email.mime.multipart import MIMEMultipart
9
10
  from email.mime.text import MIMEText
11
+ from email.utils import formataddr
10
12
 
11
13
  from detectkit.alerting.channels.base import AlertData, BaseAlertChannel
14
+ from detectkit.alerting.channels.branding import BRAND_ICON_URL, BRAND_USERNAME
12
15
 
13
16
 
14
17
  class EmailChannel(BaseAlertChannel):
@@ -55,6 +58,7 @@ class EmailChannel(BaseAlertChannel):
55
58
  smtp_password: str | None = None,
56
59
  use_tls: bool = True,
57
60
  subject_template: str = "⚠ Alert: {metric_name}",
61
+ from_name: str = BRAND_USERNAME,
58
62
  template: str | None = None,
59
63
  **kwargs,
60
64
  ):
@@ -70,6 +74,9 @@ class EmailChannel(BaseAlertChannel):
70
74
  smtp_password: SMTP authentication password (optional)
71
75
  use_tls: Whether to use STARTTLS (default: True)
72
76
  subject_template: Email subject template with {metric_name} placeholder
77
+ from_name: Sender display name shown in the ``From`` header — the
78
+ email equivalent of the bot name (default: "detectkit"). The
79
+ brand logo is also rendered in the HTML body.
73
80
  template: Custom message template (optional)
74
81
  **kwargs: Additional parameters (ignored)
75
82
 
@@ -93,6 +100,7 @@ class EmailChannel(BaseAlertChannel):
93
100
  self.to_emails = to_emails
94
101
  self.use_tls = use_tls
95
102
  self.subject_template = subject_template
103
+ self.from_name = from_name
96
104
  self.template = template
97
105
 
98
106
  def send(self, alert_data: AlertData, template: str | None = None) -> bool:
@@ -117,12 +125,17 @@ class EmailChannel(BaseAlertChannel):
117
125
 
118
126
  # Create email message
119
127
  msg = MIMEMultipart("alternative")
120
- msg["From"] = self.from_email
128
+ # Branded From: "detectkit <alerts@example.com>" — the email equivalent
129
+ # of a bot display name. formataddr quotes the name when required.
130
+ msg["From"] = formataddr((self.from_name, self.from_email))
121
131
  msg["To"] = ", ".join(self.to_emails)
122
132
  msg["Subject"] = self.subject_template.format(metric_name=alert_data.metric_name)
123
133
 
124
- # Attach plain text body
134
+ # Attach both parts. In multipart/alternative the LAST part is the
135
+ # preferred one, so HTML (with the brand logo) is shown when supported
136
+ # and the plain-text body remains the fallback.
125
137
  msg.attach(MIMEText(message_body, "plain"))
138
+ msg.attach(MIMEText(self._build_html_body(message_body), "html"))
126
139
 
127
140
  try:
128
141
  # Connect to SMTP server
@@ -145,6 +158,37 @@ class EmailChannel(BaseAlertChannel):
145
158
 
146
159
  return True
147
160
 
161
+ def _build_html_body(self, message_body: str) -> str:
162
+ """Wrap the plain-text body in a branded HTML layout.
163
+
164
+ Renders a small header with the detectkit brand logo and name above the
165
+ message (kept in a ``<pre>`` so the alert-centric text layout carries
166
+ over verbatim). The logo is referenced by URL; clients that block remote
167
+ images simply fall back to the alt text and the plain-text part.
168
+
169
+ Args:
170
+ message_body: The formatted plain-text alert body.
171
+
172
+ Returns:
173
+ An HTML document string.
174
+ """
175
+ safe_body = html.escape(message_body)
176
+ return (
177
+ '<div style="font-family:-apple-system,Segoe UI,Roboto,Helvetica,'
178
+ 'Arial,sans-serif;color:#1b1916;max-width:640px">'
179
+ '<div style="margin-bottom:12px">'
180
+ f'<img src="{BRAND_ICON_URL}" width="28" height="28" '
181
+ f'alt="{html.escape(BRAND_USERNAME)}" '
182
+ 'style="border-radius:6px;vertical-align:middle">'
183
+ '<span style="font-weight:600;font-size:16px;color:#d15b36;'
184
+ f'vertical-align:middle;margin-left:8px">{html.escape(BRAND_USERNAME)}</span>'
185
+ "</div>"
186
+ '<pre style="white-space:pre-wrap;font-family:JetBrains Mono,'
187
+ "ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;"
188
+ f'line-height:1.5;margin:0">{safe_body}</pre>'
189
+ "</div>"
190
+ )
191
+
148
192
  def format_mentions(self, mentions: list[str]) -> str:
149
193
  """
150
194
  Format mentions for email.
@@ -4,6 +4,7 @@ Mattermost alert channel.
4
4
  Convenience wrapper around WebhookChannel for Mattermost.
5
5
  """
6
6
 
7
+ from detectkit.alerting.channels.branding import BRAND_USERNAME
7
8
  from detectkit.alerting.channels.webhook import WebhookChannel
8
9
 
9
10
 
@@ -15,10 +16,15 @@ class MattermostChannel(WebhookChannel):
15
16
  for Mattermost. Mattermost webhooks are compatible with Slack API,
16
17
  so WebhookChannel can be used directly.
17
18
 
19
+ The bot defaults to the **detectkit brand avatar** and name; override the
20
+ avatar with ``icon_url`` (a custom image) or opt out of the avatar with
21
+ ``icon_emoji``. See :class:`WebhookChannel` for the icon precedence rules.
22
+
18
23
  Parameters:
19
24
  webhook_url (str): Mattermost incoming webhook URL
20
- username (str): Bot username to display (default: "detectk")
21
- icon_emoji (str): Bot emoji icon (default: ":warning:")
25
+ username (str): Bot username to display (default: "detectkit")
26
+ icon_url (str): Bot avatar image URL (default: detectkit brand avatar)
27
+ icon_emoji (str): Bot emoji icon — use instead of an avatar image
22
28
  timeout (int): Request timeout in seconds (default: 10)
23
29
 
24
30
  Example:
@@ -31,8 +37,9 @@ class MattermostChannel(WebhookChannel):
31
37
  def __init__(
32
38
  self,
33
39
  webhook_url: str,
34
- username: str = "detectk",
35
- icon_emoji: str = ":warning:",
40
+ username: str = BRAND_USERNAME,
41
+ icon_url: str | None = None,
42
+ icon_emoji: str | None = None,
36
43
  channel: str | None = None,
37
44
  timeout: int = 10,
38
45
  ):
@@ -40,6 +47,7 @@ class MattermostChannel(WebhookChannel):
40
47
  super().__init__(
41
48
  webhook_url=webhook_url,
42
49
  username=username,
50
+ icon_url=icon_url,
43
51
  icon_emoji=icon_emoji,
44
52
  channel=channel, # Optional: override webhook's default channel
45
53
  timeout=timeout,
@@ -4,6 +4,7 @@ Slack alert channel.
4
4
  Convenience wrapper around WebhookChannel for Slack.
5
5
  """
6
6
 
7
+ from detectkit.alerting.channels.branding import BRAND_USERNAME
7
8
  from detectkit.alerting.channels.webhook import WebhookChannel
8
9
 
9
10
 
@@ -14,10 +15,15 @@ class SlackChannel(WebhookChannel):
14
15
  This is a convenience wrapper around WebhookChannel specifically
15
16
  for Slack. Slack and Mattermost use compatible webhook formats.
16
17
 
18
+ The bot defaults to the **detectkit brand avatar** and name; override the
19
+ avatar with ``icon_url`` (a custom image) or opt out of the avatar with
20
+ ``icon_emoji``. See :class:`WebhookChannel` for the icon precedence rules.
21
+
17
22
  Parameters:
18
23
  webhook_url (str): Slack incoming webhook URL
19
- username (str): Bot username to display (default: "detectk")
20
- icon_emoji (str): Bot emoji icon (default: ":warning:")
24
+ username (str): Bot username to display (default: "detectkit")
25
+ icon_url (str): Bot avatar image URL (default: detectkit brand avatar)
26
+ icon_emoji (str): Bot emoji icon — use instead of an avatar image
21
27
  channel (str): Target Slack channel (optional, e.g., "#alerts")
22
28
  timeout (int): Request timeout in seconds (default: 10)
23
29
 
@@ -32,8 +38,9 @@ class SlackChannel(WebhookChannel):
32
38
  def __init__(
33
39
  self,
34
40
  webhook_url: str,
35
- username: str = "detectk",
36
- icon_emoji: str = ":warning:",
41
+ username: str = BRAND_USERNAME,
42
+ icon_url: str | None = None,
43
+ icon_emoji: str | None = None,
37
44
  channel: str | None = None,
38
45
  timeout: int = 10,
39
46
  ):
@@ -41,6 +48,7 @@ class SlackChannel(WebhookChannel):
41
48
  super().__init__(
42
49
  webhook_url=webhook_url,
43
50
  username=username,
51
+ icon_url=icon_url,
44
52
  icon_emoji=icon_emoji,
45
53
  channel=channel,
46
54
  timeout=timeout,
@@ -8,6 +8,7 @@ Compatible with Mattermost, Slack, and other webhook-based systems.
8
8
  import requests
9
9
 
10
10
  from detectkit.alerting.channels.base import AlertData, BaseAlertChannel
11
+ from detectkit.alerting.channels.branding import BRAND_ICON_URL, BRAND_USERNAME
11
12
 
12
13
 
13
14
  class WebhookChannel(BaseAlertChannel):
@@ -24,14 +25,21 @@ class WebhookChannel(BaseAlertChannel):
24
25
  {
25
26
  "text": "message",
26
27
  "username": "bot_name",
27
- "icon_emoji": ":emoji:",
28
+ "icon_url": "https://.../bot-icon.png", # or "icon_emoji": ":emoji:"
28
29
  "channel": "#channel" (optional)
29
30
  }
30
31
 
32
+ Branding: the bot defaults to the **detectkit brand avatar** (``icon_url``)
33
+ and name (``username``). An explicit ``icon_url`` overrides it with a custom
34
+ image; an explicit ``icon_emoji`` opts out of the avatar in favor of an
35
+ emoji. Only one icon field is sent (``icon_url`` wins when both are set).
36
+
31
37
  Parameters:
32
38
  webhook_url (str): Webhook URL to send alerts to
33
- username (str): Bot username to display (default: "detectk")
34
- icon_emoji (str): Bot emoji icon (default: ":warning:")
39
+ username (str): Bot username to display (default: "detectkit")
40
+ icon_url (str): Bot avatar image URL (default: detectkit brand avatar)
41
+ icon_emoji (str): Bot emoji icon — use instead of an avatar image
42
+ (default: None; falls back to the brand avatar)
35
43
  channel (str): Target channel (optional, for Slack/Mattermost)
36
44
  timeout (int): Request timeout in seconds (default: 10)
37
45
  extra_headers (dict): Additional HTTP headers (optional)
@@ -58,8 +66,9 @@ class WebhookChannel(BaseAlertChannel):
58
66
  def __init__(
59
67
  self,
60
68
  webhook_url: str,
61
- username: str = "detectk",
62
- icon_emoji: str = ":warning:",
69
+ username: str = BRAND_USERNAME,
70
+ icon_url: str | None = None,
71
+ icon_emoji: str | None = None,
63
72
  channel: str | None = None,
64
73
  timeout: int = 10,
65
74
  extra_headers: dict[str, str] | None = None,
@@ -70,6 +79,12 @@ class WebhookChannel(BaseAlertChannel):
70
79
 
71
80
  self.webhook_url = webhook_url
72
81
  self.username = username
82
+ # Default to the detectkit brand avatar. An explicit icon_url or
83
+ # icon_emoji opts out of the default; we only fill in the brand avatar
84
+ # when the user configured neither.
85
+ if icon_url is None and icon_emoji is None:
86
+ icon_url = BRAND_ICON_URL
87
+ self.icon_url = icon_url
73
88
  self.icon_emoji = icon_emoji
74
89
  self.channel = channel
75
90
  self.timeout = timeout
@@ -118,12 +133,18 @@ class WebhookChannel(BaseAlertChannel):
118
133
  "text": body,
119
134
  }
120
135
 
121
- payload = {
136
+ payload: dict[str, object] = {
122
137
  "username": self.username,
123
- "icon_emoji": self.icon_emoji,
124
138
  "attachments": [attachment],
125
139
  }
126
140
 
141
+ # Send exactly one icon field: the avatar image (brand default or a
142
+ # custom override) takes precedence over an emoji.
143
+ if self.icon_url:
144
+ payload["icon_url"] = self.icon_url
145
+ elif self.icon_emoji:
146
+ payload["icon_emoji"] = self.icon_emoji
147
+
127
148
  # Add channel if specified (for Slack)
128
149
  if self.channel:
129
150
  payload["channel"] = self.channel
@@ -3,9 +3,9 @@
3
3
  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
- them with one command. Core logic is pure numpy (no pandas). **ClickHouse is
7
- the only implemented backend today**; PostgreSQL and MySQL are planned (their
8
- profiles validate but creating a manager raises `NotImplementedError`).
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.
9
9
 
10
10
  ## The pipeline: load → detect → alert
11
11
 
@@ -78,11 +78,11 @@ alert_channels:
78
78
 
79
79
  ### Database profiles
80
80
 
81
- > ClickHouse is the only implemented backend today. PostgreSQL and MySQL are
82
- > planned their profiles validate, but creating a manager raises
83
- > `NotImplementedError("... coming soon")`.
81
+ > ClickHouse, PostgreSQL and MySQL are all fully supported. ClickHouse/MySQL use
82
+ > two *databases*; PostgreSQL connects to one `database` and uses two *schemas*.
83
+ > `dtk init --db-type {clickhouse,postgres,mysql}` scaffolds the right shape.
84
84
 
85
- **ClickHouse** (priority backend):
85
+ **ClickHouse**:
86
86
  ```yaml
87
87
  profiles:
88
88
  prod:
@@ -98,36 +98,34 @@ profiles:
98
98
  max_memory_usage: 10000000000
99
99
  ```
100
100
 
101
- **PostgreSQL** (planned, not yet implemented):
101
+ **PostgreSQL** (connect to `database`, tables in schemas):
102
102
  ```yaml
103
103
  profiles:
104
104
  prod:
105
105
  type: postgres
106
106
  host: localhost
107
107
  port: 5432
108
- database: analytics # required
108
+ database: detectkit # required — must already exist
109
109
  user: postgres
110
110
  password: "..."
111
- internal_schema: detectkit # required — _dtk_* tables
111
+ internal_schema: detectkit # required — _dtk_* tables (auto-created)
112
112
  data_schema: public # required — data queries
113
- pool_size: 5 # optional
114
- max_overflow: 10 # optional
113
+ settings: {} # optional — extra psycopg2.connect kwargs
115
114
  ```
116
115
 
117
- **MySQL** (planned, not yet implemented):
116
+ **MySQL** (8.0+; two databases):
118
117
  ```yaml
119
118
  profiles:
120
119
  prod:
121
120
  type: mysql
122
121
  host: localhost
123
122
  port: 3306
124
- database: analytics # required
125
123
  user: root
126
124
  password: "..."
127
- internal_database: detectkit # required
125
+ internal_database: detectkit # required — _dtk_* tables (auto-created)
128
126
  data_database: analytics # required
129
- charset: utf8mb4 # optional
130
- autocommit: true # optional
127
+ database: analytics # optional — default db for the connection
128
+ settings: {} # optional — extra pymysql.connect kwargs
131
129
  ```
132
130
 
133
131
  ### Alert channels
@@ -135,17 +133,24 @@ profiles:
135
133
  Defined once in `profiles.yml`, referenced by name in each metric's
136
134
  `alerting.channels` (and in `error_alerting.channels`).
137
135
 
136
+ The bot defaults to the **detectkit brand** name + avatar on every channel.
137
+ Override per channel; Telegram and email brand differently (see their notes).
138
+
138
139
  **Mattermost** / **Slack** (Slack-compatible webhook API, same fields):
139
140
  ```yaml
140
141
  alert_channels:
141
142
  mattermost_ops:
142
143
  type: mattermost # or: slack
143
144
  webhook_url: "{{ env_var('MATTERMOST_WEBHOOK_URL') }}" # required
144
- username: "detectkit" # optional (default: "detectkit")
145
- icon_emoji: ":warning:" # optional
146
145
  channel: "alerts" # optional — override webhook default ("#alerts" for Slack)
147
146
  timeout: 10 # optional HTTP timeout (s)
147
+ # Bot identity defaults to the detectkit brand; override any of:
148
+ # username: "detectkit" # display name
149
+ # icon_url: "https://.../bot.png" # avatar image (default: brand avatar)
150
+ # icon_emoji: ":warning:" # emoji instead of an avatar image
148
151
  ```
152
+ > Icon precedence: `icon_url` (default: brand avatar) wins over `icon_emoji`;
153
+ > set either to opt out of the brand avatar.
149
154
  > Slack note: `@username` in a Slack webhook is **display-only** (no real ping).
150
155
  > Use Slack user IDs (`U…`) for real pings.
151
156
 
@@ -157,6 +162,9 @@ alert_channels:
157
162
  bot_token: "{{ env_var('TG_BOT_TOKEN') }}" # required
158
163
  chat_id: "-1001234567890" # required
159
164
  ```
165
+ > Telegram shows the bot account's own avatar (set in @BotFather, not
166
+ > per-message), so detectkit can't override it. Brand it via `/setuserpic` —
167
+ > the detectkit avatar is at `https://dtk.pipelab.dev/bot-icon.png`.
160
168
 
161
169
  **Email** (SMTP):
162
170
  ```yaml
@@ -167,10 +175,14 @@ alert_channels:
167
175
  smtp_port: 587 # required (587 TLS, 465 SSL)
168
176
  from_email: "alerts@example.com" # required
169
177
  to_emails: ["ops@example.com"] # required (list)
178
+ from_name: "detectkit" # optional — From display name (default: "detectkit")
170
179
  smtp_username: "..." # optional
171
180
  smtp_password: "..." # optional (use env_var)
172
181
  use_tls: true # optional (default: true)
173
182
  ```
183
+ > Sends as `detectkit <from_email>` with the brand logo in an HTML body (plain
184
+ > text stays the fallback). The avatar mail clients show is set by the sending
185
+ > domain (BIMI), not the message — brand it via `from_name` + your domain.
174
186
 
175
187
  **Webhook** (generic):
176
188
  ```yaml
@@ -185,12 +197,12 @@ alert_channels:
185
197
  ## Notes
186
198
 
187
199
  - **First-run setup:** the `profiles.yml` that `dtk init` writes is a
188
- placeholder its `dev` profile points `internal_database` / `data_database`
189
- at example values (`detectkit` / `default`) on `localhost`. Edit the host,
190
- credentials and both database names to match your environment before running
191
- (the **`dtk-setup-project`** skill walks this). There is no `database:` field —
192
- ClickHouse needs both `internal_database` and `data_database`, or the run
193
- raises `internal_database must be set for ClickHouse`.
200
+ placeholder scaffolded for `--db-type` (default ClickHouse) its `dev`
201
+ profile points the location fields at example values on `localhost`. Edit the
202
+ host, credentials and location names to match your environment before running
203
+ (the **`dtk-setup-project`** skill walks this). ClickHouse/MySQL use
204
+ `internal_database` / `data_database` (no `database:` field on ClickHouse);
205
+ PostgreSQL connects to a `database` and uses `internal_schema` / `data_schema`.
194
206
  - `dtk run` (without `--profile`) uses the `default_profile` declared in
195
207
  **`profiles.yml`**; the `default_profile` in `detectkit_project.yml` is not
196
208
  read at runtime — keep them in sync to avoid confusion.
@@ -40,12 +40,17 @@ side by side, ask which one to set up.
40
40
 
41
41
  ## Step 1 — Pick the database backend
42
42
 
43
- **ClickHouse is the only implemented backend today.** PostgreSQL and MySQL
44
- profiles validate, but `dtk run` raises `NotImplementedError("PostgreSQL
45
- support coming soon")` / `"MySQL support coming soon"` when it builds the
46
- manager. If the user wants Postgres/MySQL, say plainly that runs won't work
47
- yet; you can still write the profile shape for later, but set expectations.
48
- Assume ClickHouse unless told otherwise.
43
+ **ClickHouse, PostgreSQL and MySQL are all fully supported.** Ask which one the
44
+ project uses (default to ClickHouse if unsure). `dtk init --db-type
45
+ {clickhouse,postgres,mysql}` scaffolds `profiles.yml` for the chosen backend.
46
+ The location fields differ:
47
+ - **ClickHouse** / **MySQL** two *databases*: `internal_database` / `data_database`.
48
+ - **PostgreSQL** connect to a `database` (must already exist), then two
49
+ *schemas*: `internal_schema` / `data_schema`.
50
+
51
+ The metric query SQL dialect also differs (e.g. `toStartOfInterval` on
52
+ ClickHouse vs `date_trunc`/`to_timestamp` on Postgres vs `FROM_UNIXTIME` on
53
+ MySQL). Everything else — detectors, alerting, the CLI — is identical.
49
54
 
50
55
  ## Step 2 — Connection details (gather, don't guess)
51
56
 
@@ -74,8 +79,8 @@ values):
74
79
  data, e.g. `detectkit` or `monitoring`.
75
80
  - `data_database` — where the source tables your metric queries read from live.
76
81
 
77
- For Postgres these are `internal_schema` / `data_schema`; for MySQL they are
78
- `internal_database` / `data_database` (relevant only once those backends land).
82
+ For Postgres these are `internal_schema` / `data_schema` (inside the connected
83
+ `database`); for MySQL they are `internal_database` / `data_database`.
79
84
 
80
85
  ## Step 4 — Profile name & `default_profile`
81
86
 
@@ -111,14 +116,19 @@ If the user wants alerts now, add one channel under `alert_channels:` (it is
111
116
  referenced by name from each metric's `alerting.channels`). Pick the type and
112
117
  gather its required fields (see `project.md` for the full set per type):
113
118
 
119
+ The bot defaults to the **detectkit brand** name + avatar everywhere; the
120
+ identity fields below are optional overrides.
121
+
114
122
  - **mattermost** / **slack** — `webhook_url` (use `env_var`); optional
115
- `channel`, `username`, `icon_emoji`.
116
- - **telegram** — `bot_token` (env_var) + `chat_id`.
123
+ `channel`, `username`, `icon_url` (avatar; default brand), `icon_emoji`.
124
+ - **telegram** — `bot_token` (env_var) + `chat_id`. (Bot avatar is set in
125
+ @BotFather, not by detectkit.)
117
126
  - **email** — `smtp_host`, `smtp_port`, `from_email`, `to_emails` (list);
127
+ optional `from_name` (From display name, default `detectkit`);
118
128
  `smtp_username` / `smtp_password` via env_var.
119
129
  - **webhook** — generic `webhook_url` (required) + optional `extra_headers`
120
- (also accepts `username` / `icon_emoji` / `channel`). There is no `url`,
121
- `method` or `headers` field.
130
+ (also accepts `username` / `icon_url` / `icon_emoji` / `channel`). There is
131
+ no `url`, `method` or `headers` field.
122
132
 
123
133
  ```yaml
124
134
  # at the end of profiles.yml