detectkit 0.2.4__py3-none-any.whl

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 (51) hide show
  1. detectkit/__init__.py +17 -0
  2. detectkit/alerting/__init__.py +13 -0
  3. detectkit/alerting/channels/__init__.py +21 -0
  4. detectkit/alerting/channels/base.py +193 -0
  5. detectkit/alerting/channels/email.py +146 -0
  6. detectkit/alerting/channels/factory.py +193 -0
  7. detectkit/alerting/channels/mattermost.py +53 -0
  8. detectkit/alerting/channels/slack.py +55 -0
  9. detectkit/alerting/channels/telegram.py +110 -0
  10. detectkit/alerting/channels/webhook.py +139 -0
  11. detectkit/alerting/orchestrator.py +369 -0
  12. detectkit/cli/__init__.py +1 -0
  13. detectkit/cli/commands/__init__.py +1 -0
  14. detectkit/cli/commands/init.py +282 -0
  15. detectkit/cli/commands/run.py +486 -0
  16. detectkit/cli/commands/test_alert.py +184 -0
  17. detectkit/cli/main.py +186 -0
  18. detectkit/config/__init__.py +30 -0
  19. detectkit/config/metric_config.py +499 -0
  20. detectkit/config/profile.py +285 -0
  21. detectkit/config/project_config.py +164 -0
  22. detectkit/config/validator.py +124 -0
  23. detectkit/core/__init__.py +6 -0
  24. detectkit/core/interval.py +132 -0
  25. detectkit/core/models.py +106 -0
  26. detectkit/database/__init__.py +27 -0
  27. detectkit/database/clickhouse_manager.py +393 -0
  28. detectkit/database/internal_tables.py +724 -0
  29. detectkit/database/manager.py +324 -0
  30. detectkit/database/tables.py +138 -0
  31. detectkit/detectors/__init__.py +6 -0
  32. detectkit/detectors/base.py +441 -0
  33. detectkit/detectors/factory.py +138 -0
  34. detectkit/detectors/statistical/__init__.py +8 -0
  35. detectkit/detectors/statistical/iqr.py +508 -0
  36. detectkit/detectors/statistical/mad.py +478 -0
  37. detectkit/detectors/statistical/manual_bounds.py +206 -0
  38. detectkit/detectors/statistical/zscore.py +491 -0
  39. detectkit/loaders/__init__.py +6 -0
  40. detectkit/loaders/metric_loader.py +470 -0
  41. detectkit/loaders/query_template.py +164 -0
  42. detectkit/orchestration/__init__.py +9 -0
  43. detectkit/orchestration/task_manager.py +746 -0
  44. detectkit/utils/__init__.py +17 -0
  45. detectkit/utils/stats.py +196 -0
  46. detectkit-0.2.4.dist-info/METADATA +237 -0
  47. detectkit-0.2.4.dist-info/RECORD +51 -0
  48. detectkit-0.2.4.dist-info/WHEEL +5 -0
  49. detectkit-0.2.4.dist-info/entry_points.txt +2 -0
  50. detectkit-0.2.4.dist-info/licenses/LICENSE +21 -0
  51. detectkit-0.2.4.dist-info/top_level.txt +1 -0
detectkit/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """
2
+ detectk - Anomaly Detection for Time-Series Metrics
3
+
4
+ A Python library for data analysts and engineers to monitor metrics with automatic anomaly detection.
5
+ """
6
+
7
+ __version__ = "0.1.0"
8
+
9
+ from detectkit.core.interval import Interval
10
+ from detectkit.core.models import ColumnDefinition, TableModel
11
+
12
+ __all__ = [
13
+ "Interval",
14
+ "ColumnDefinition",
15
+ "TableModel",
16
+ "__version__",
17
+ ]
@@ -0,0 +1,13 @@
1
+ """Alerting functionality for detectk."""
2
+
3
+ from detectkit.alerting.orchestrator import (
4
+ AlertConditions,
5
+ AlertOrchestrator,
6
+ DetectionRecord,
7
+ )
8
+
9
+ __all__ = [
10
+ "AlertOrchestrator",
11
+ "AlertConditions",
12
+ "DetectionRecord",
13
+ ]
@@ -0,0 +1,21 @@
1
+ """Alert channels for external notifications."""
2
+
3
+ from detectkit.alerting.channels.base import AlertData, BaseAlertChannel
4
+ from detectkit.alerting.channels.mattermost import MattermostChannel
5
+ from detectkit.alerting.channels.slack import SlackChannel
6
+ from detectkit.alerting.channels.webhook import WebhookChannel
7
+ from detectkit.alerting.channels.telegram import TelegramChannel
8
+ from detectkit.alerting.channels.email import EmailChannel
9
+
10
+ from detectkit.alerting.channels.factory import AlertChannelFactory
11
+
12
+ __all__ = [
13
+ "AlertData",
14
+ "BaseAlertChannel",
15
+ "WebhookChannel",
16
+ "MattermostChannel",
17
+ "SlackChannel",
18
+ "TelegramChannel",
19
+ "EmailChannel",
20
+ "AlertChannelFactory",
21
+ ]
@@ -0,0 +1,193 @@
1
+ """
2
+ Base alert channel interface.
3
+
4
+ All alert channels must inherit from BaseAlertChannel and implement
5
+ the send() method for delivering alerts to specific destinations.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from dataclasses import dataclass
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from detectkit.detectors.base import DetectionResult
13
+
14
+
15
+ @dataclass
16
+ class AlertData:
17
+ """
18
+ Data for alert message.
19
+
20
+ Contains all information needed to format and send an alert.
21
+
22
+ Attributes:
23
+ metric_name: Name of the metric
24
+ timestamp: Timestamp of the anomaly (datetime64)
25
+ timezone: Timezone for display (e.g., "Europe/Moscow")
26
+ value: Actual metric value
27
+ confidence_lower: Lower confidence bound
28
+ confidence_upper: Upper confidence bound
29
+ detector_name: Name/ID of detector that found the anomaly
30
+ detector_params: Detector parameters (JSON string)
31
+ direction: Direction of anomaly ("above" or "below")
32
+ severity: Severity score
33
+ detection_metadata: Additional metadata from detector
34
+ consecutive_count: Number of consecutive anomalies
35
+ """
36
+
37
+ metric_name: str
38
+ timestamp: Any # datetime64 or datetime
39
+ timezone: str
40
+ value: float
41
+ confidence_lower: Optional[float]
42
+ confidence_upper: Optional[float]
43
+ detector_name: str
44
+ detector_params: str
45
+ direction: str
46
+ severity: float
47
+ detection_metadata: Dict[str, Any]
48
+ consecutive_count: int = 1
49
+
50
+
51
+ class BaseAlertChannel(ABC):
52
+ """
53
+ Abstract base class for alert channels.
54
+
55
+ Alert channels deliver notifications to external systems when
56
+ anomalies are detected. Each channel implements a specific
57
+ delivery mechanism (webhook, email, etc.).
58
+
59
+ Example:
60
+ >>> class MyChannel(BaseAlertChannel):
61
+ ... def send(self, alert_data, template=None):
62
+ ... message = self.format_message(alert_data, template)
63
+ ... # Send via specific mechanism
64
+ ... return True
65
+ """
66
+
67
+ @abstractmethod
68
+ def send(
69
+ self,
70
+ alert_data: AlertData,
71
+ template: Optional[str] = None,
72
+ ) -> bool:
73
+ """
74
+ Send alert to this channel.
75
+
76
+ Args:
77
+ alert_data: Alert data to send
78
+ template: Optional custom message template
79
+ Uses default template if None
80
+
81
+ Returns:
82
+ True if sent successfully, False otherwise
83
+
84
+ Raises:
85
+ Exception: If sending fails critically
86
+
87
+ Example:
88
+ >>> alert = AlertData(
89
+ ... metric_name="cpu_usage",
90
+ ... timestamp=datetime.now(),
91
+ ... value=95.0,
92
+ ... ...
93
+ ... )
94
+ >>> success = channel.send(alert)
95
+ """
96
+ pass
97
+
98
+ def format_message(
99
+ self,
100
+ alert_data: AlertData,
101
+ template: Optional[str] = None,
102
+ ) -> str:
103
+ """
104
+ Format alert message from template.
105
+
106
+ Uses default template if none provided. Template variables:
107
+ - {metric_name}
108
+ - {timestamp}
109
+ - {timezone}
110
+ - {value}
111
+ - {confidence_lower}
112
+ - {confidence_upper}
113
+ - {detector_name}
114
+ - {direction}
115
+ - {severity}
116
+ - {consecutive_count}
117
+
118
+ Args:
119
+ alert_data: Alert data to format
120
+ template: Optional custom template string
121
+
122
+ Returns:
123
+ Formatted message string
124
+
125
+ Example:
126
+ >>> template = "Anomaly in {metric_name}: {value}"
127
+ >>> message = channel.format_message(alert_data, template)
128
+ """
129
+ if template is None:
130
+ template = self.get_default_template()
131
+
132
+ # Format timestamp to string
133
+ from datetime import datetime
134
+ import numpy as np
135
+
136
+ ts = alert_data.timestamp
137
+ if isinstance(ts, np.datetime64):
138
+ ts = ts.astype(datetime)
139
+
140
+ # Format timestamp with timezone
141
+ ts_str = ts.strftime("%Y-%m-%d %H:%M:%S")
142
+ if alert_data.timezone:
143
+ ts_str = f"{ts_str} ({alert_data.timezone})"
144
+
145
+ # Format confidence interval
146
+ if alert_data.confidence_lower is not None and alert_data.confidence_upper is not None:
147
+ confidence_str = f"[{alert_data.confidence_lower:.2f}, {alert_data.confidence_upper:.2f}]"
148
+ else:
149
+ confidence_str = "N/A"
150
+
151
+ # Format message
152
+ try:
153
+ message = template.format(
154
+ metric_name=alert_data.metric_name,
155
+ timestamp=ts_str,
156
+ timezone=alert_data.timezone,
157
+ value=alert_data.value,
158
+ confidence_lower=alert_data.confidence_lower,
159
+ confidence_upper=alert_data.confidence_upper,
160
+ confidence_interval=confidence_str,
161
+ detector_name=alert_data.detector_name,
162
+ detector_params=alert_data.detector_params,
163
+ direction=alert_data.direction,
164
+ severity=alert_data.severity,
165
+ consecutive_count=alert_data.consecutive_count,
166
+ )
167
+ except KeyError as e:
168
+ # If template has unknown variables, fall back to default
169
+ message = self.format_message(alert_data, self.get_default_template())
170
+
171
+ return message
172
+
173
+ def get_default_template(self) -> str:
174
+ """
175
+ Get default message template.
176
+
177
+ Returns:
178
+ Default template string
179
+ """
180
+ return (
181
+ "Anomaly detected in metric: {metric_name}\n"
182
+ "Time: {timestamp}\n"
183
+ "Value: {value}\n"
184
+ "Confidence interval: {confidence_interval}\n"
185
+ "Detector: {detector_name}\n"
186
+ "Parameters: {detector_params}\n"
187
+ "Direction: {direction}\n"
188
+ "Severity: {severity:.2f}"
189
+ )
190
+
191
+ def __repr__(self) -> str:
192
+ """String representation of channel."""
193
+ return f"{self.__class__.__name__}()"
@@ -0,0 +1,146 @@
1
+ """
2
+ Email alert channel implementation.
3
+
4
+ Sends anomaly alerts via SMTP email.
5
+ """
6
+
7
+ import smtplib
8
+ from email.mime.multipart import MIMEMultipart
9
+ from email.mime.text import MIMEText
10
+ from typing import List, Optional
11
+
12
+ from detectkit.alerting.channels.base import AlertData, BaseAlertChannel
13
+
14
+
15
+ class EmailChannel(BaseAlertChannel):
16
+ """
17
+ Email alert channel using SMTP.
18
+
19
+ Sends formatted emails via SMTP server.
20
+
21
+ Attributes:
22
+ smtp_host: SMTP server hostname
23
+ smtp_port: SMTP server port
24
+ smtp_username: SMTP authentication username
25
+ smtp_password: SMTP authentication password
26
+ from_email: Sender email address
27
+ to_emails: List of recipient email addresses
28
+ use_tls: Whether to use TLS encryption
29
+ subject_template: Email subject template
30
+
31
+ Example:
32
+ >>> channel = EmailChannel(
33
+ ... smtp_host="smtp.gmail.com",
34
+ ... smtp_port=587,
35
+ ... smtp_username="alerts@example.com",
36
+ ... smtp_password="password",
37
+ ... from_email="alerts@example.com",
38
+ ... to_emails=["team@example.com"]
39
+ ... )
40
+ >>> alert = AlertData(
41
+ ... metric_name="cpu_usage",
42
+ ... timestamp=np.datetime64("2024-01-01T10:00:00"),
43
+ ... value=95.0,
44
+ ... is_anomaly=True
45
+ ... )
46
+ >>> channel.send(alert)
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ smtp_host: str,
52
+ smtp_port: int,
53
+ from_email: str,
54
+ to_emails: List[str],
55
+ smtp_username: Optional[str] = None,
56
+ smtp_password: Optional[str] = None,
57
+ use_tls: bool = True,
58
+ subject_template: str = "Anomaly Alert: {metric_name}",
59
+ template: Optional[str] = None,
60
+ **kwargs,
61
+ ):
62
+ """
63
+ Initialize email channel.
64
+
65
+ Args:
66
+ smtp_host: SMTP server hostname
67
+ smtp_port: SMTP server port (typically 587 for TLS, 465 for SSL)
68
+ from_email: Sender email address
69
+ to_emails: List of recipient email addresses
70
+ smtp_username: SMTP authentication username (optional)
71
+ smtp_password: SMTP authentication password (optional)
72
+ use_tls: Whether to use STARTTLS (default: True)
73
+ subject_template: Email subject template with {metric_name} placeholder
74
+ template: Custom message template (optional)
75
+ **kwargs: Additional parameters (ignored)
76
+
77
+ Raises:
78
+ ValueError: If required parameters are missing
79
+ """
80
+ if not smtp_host:
81
+ raise ValueError("smtp_host is required for EmailChannel")
82
+ if not smtp_port:
83
+ raise ValueError("smtp_port is required for EmailChannel")
84
+ if not from_email:
85
+ raise ValueError("from_email is required for EmailChannel")
86
+ if not to_emails:
87
+ raise ValueError("to_emails is required for EmailChannel")
88
+
89
+ self.smtp_host = smtp_host
90
+ self.smtp_port = smtp_port
91
+ self.smtp_username = smtp_username
92
+ self.smtp_password = smtp_password
93
+ self.from_email = from_email
94
+ self.to_emails = to_emails
95
+ self.use_tls = use_tls
96
+ self.subject_template = subject_template
97
+ self.template = template
98
+
99
+ def send(self, alert_data: AlertData) -> None:
100
+ """
101
+ Send alert via email.
102
+
103
+ Args:
104
+ alert_data: Alert information to send
105
+
106
+ Raises:
107
+ smtplib.SMTPException: If email sending fails
108
+
109
+ Example:
110
+ >>> channel.send(alert_data)
111
+ """
112
+ message_body = self.format_message(alert_data, self.template)
113
+
114
+ # Create email message
115
+ msg = MIMEMultipart("alternative")
116
+ msg["From"] = self.from_email
117
+ msg["To"] = ", ".join(self.to_emails)
118
+ msg["Subject"] = self.subject_template.format(
119
+ metric_name=alert_data.metric_name
120
+ )
121
+
122
+ # Attach plain text body
123
+ msg.attach(MIMEText(message_body, "plain"))
124
+
125
+ try:
126
+ # Connect to SMTP server
127
+ if self.use_tls:
128
+ server = smtplib.SMTP(self.smtp_host, self.smtp_port, timeout=10)
129
+ server.starttls()
130
+ else:
131
+ server = smtplib.SMTP_SSL(self.smtp_host, self.smtp_port, timeout=10)
132
+
133
+ # Login if credentials provided
134
+ if self.smtp_username and self.smtp_password:
135
+ server.login(self.smtp_username, self.smtp_password)
136
+
137
+ # Send email
138
+ server.sendmail(self.from_email, self.to_emails, msg.as_string())
139
+ server.quit()
140
+
141
+ except smtplib.SMTPException as e:
142
+ raise smtplib.SMTPException(f"Failed to send email alert: {e}")
143
+
144
+ def __repr__(self) -> str:
145
+ """String representation."""
146
+ return f"EmailChannel(to={self.to_emails})"
@@ -0,0 +1,193 @@
1
+ """
2
+ Alert channel factory for creating channel instances from configuration.
3
+ """
4
+
5
+ import os
6
+ from typing import Dict, List
7
+
8
+ from detectkit.alerting.channels.base import BaseAlertChannel
9
+ from detectkit.alerting.channels.mattermost import MattermostChannel
10
+ from detectkit.alerting.channels.slack import SlackChannel
11
+ from detectkit.alerting.channels.webhook import WebhookChannel
12
+ from detectkit.alerting.channels.telegram import TelegramChannel
13
+ from detectkit.alerting.channels.email import EmailChannel
14
+
15
+
16
+ class AlertChannelFactory:
17
+ """
18
+ Factory for creating alert channel instances from configuration.
19
+
20
+ Supports environment variable interpolation in config values.
21
+
22
+ Example:
23
+ >>> factory = AlertChannelFactory()
24
+ >>> channel = factory.create("mattermost", {"webhook_url": "https://..."})
25
+ >>> isinstance(channel, MattermostChannel)
26
+ True
27
+ """
28
+
29
+ # Registry of available channel types
30
+ CHANNEL_TYPES = {
31
+ "webhook": WebhookChannel,
32
+ "mattermost": MattermostChannel,
33
+ "slack": SlackChannel,
34
+ "telegram": TelegramChannel,
35
+ "email": EmailChannel,
36
+ }
37
+
38
+ @classmethod
39
+ def create(cls, channel_type: str, params: Dict) -> BaseAlertChannel:
40
+ """
41
+ Create alert channel instance from type and parameters.
42
+
43
+ Supports environment variable interpolation:
44
+ - ${ENV_VAR} or {{ env_var('ENV_VAR') }}
45
+
46
+ Args:
47
+ channel_type: Type of channel (e.g., "mattermost", "slack")
48
+ params: Channel parameters
49
+
50
+ Returns:
51
+ Alert channel instance
52
+
53
+ Raises:
54
+ ValueError: If channel type is unknown
55
+
56
+ Example:
57
+ >>> channel = AlertChannelFactory.create(
58
+ ... "mattermost",
59
+ ... {"webhook_url": "${MATTERMOST_WEBHOOK}"}
60
+ ... )
61
+ """
62
+ channel_type = channel_type.lower()
63
+
64
+ if channel_type not in cls.CHANNEL_TYPES:
65
+ available = ", ".join(sorted(cls.CHANNEL_TYPES.keys()))
66
+ raise ValueError(
67
+ f"Unknown channel type: '{channel_type}'. "
68
+ f"Available types: {available}"
69
+ )
70
+
71
+ # Interpolate environment variables in params
72
+ interpolated_params = cls._interpolate_env_vars(params)
73
+
74
+ channel_class = cls.CHANNEL_TYPES[channel_type]
75
+
76
+ try:
77
+ return channel_class(**interpolated_params)
78
+ except TypeError as e:
79
+ raise ValueError(
80
+ f"Invalid parameters for {channel_type} channel: {e}"
81
+ ) from e
82
+
83
+ @classmethod
84
+ def _interpolate_env_vars(cls, params: Dict) -> Dict:
85
+ """
86
+ Interpolate environment variables in parameter values.
87
+
88
+ Supports formats:
89
+ - ${VAR_NAME}
90
+ - {{ env_var('VAR_NAME') }}
91
+
92
+ Args:
93
+ params: Parameters dictionary
94
+
95
+ Returns:
96
+ Parameters with interpolated values
97
+ """
98
+ import re
99
+
100
+ interpolated = {}
101
+
102
+ for key, value in params.items():
103
+ if isinstance(value, str):
104
+ # Handle ${VAR} format
105
+ value = re.sub(
106
+ r'\$\{([^}]+)\}',
107
+ lambda m: os.environ.get(m.group(1), m.group(0)),
108
+ value,
109
+ )
110
+
111
+ # Handle {{ env_var('VAR') }} format
112
+ value = re.sub(
113
+ r"\{\{\s*env_var\(['\"]([^'\"]+)['\"]\)\s*\}\}",
114
+ lambda m: os.environ.get(m.group(1), m.group(0)),
115
+ value,
116
+ )
117
+
118
+ interpolated[key] = value
119
+
120
+ return interpolated
121
+
122
+ @classmethod
123
+ def create_from_config(cls, channel_config: Dict) -> BaseAlertChannel:
124
+ """
125
+ Create channel from configuration dictionary.
126
+
127
+ Args:
128
+ channel_config: Configuration with 'type' and channel-specific params
129
+ Example: {
130
+ "type": "mattermost",
131
+ "webhook_url": "${MATTERMOST_WEBHOOK}",
132
+ "username": "detectkit"
133
+ }
134
+
135
+ Returns:
136
+ Alert channel instance
137
+
138
+ Example:
139
+ >>> config = {
140
+ ... "type": "mattermost",
141
+ ... "webhook_url": "https://example.com/hooks/xxx"
142
+ ... }
143
+ >>> channel = AlertChannelFactory.create_from_config(config)
144
+ """
145
+ channel_type = channel_config.get("type")
146
+ if not channel_type:
147
+ raise ValueError("Channel config must have 'type' field")
148
+
149
+ # Extract all params except 'type'
150
+ params = {k: v for k, v in channel_config.items() if k != "type"}
151
+
152
+ return cls.create(channel_type, params)
153
+
154
+ @classmethod
155
+ def create_multiple(cls, channel_configs: List[Dict]) -> List[BaseAlertChannel]:
156
+ """
157
+ Create multiple channels from list of configurations.
158
+
159
+ Args:
160
+ channel_configs: List of channel configurations
161
+
162
+ Returns:
163
+ List of channel instances
164
+
165
+ Example:
166
+ >>> configs = [
167
+ ... {"type": "mattermost", "webhook_url": "https://..."},
168
+ ... {"type": "slack", "webhook_url": "https://...", "channel": "#alerts"},
169
+ ... ]
170
+ >>> channels = AlertChannelFactory.create_multiple(configs)
171
+ >>> len(channels)
172
+ 2
173
+ """
174
+ channels = []
175
+ for config in channel_configs:
176
+ channel = cls.create_from_config(config)
177
+ channels.append(channel)
178
+ return channels
179
+
180
+ @classmethod
181
+ def list_available_types(cls) -> List[str]:
182
+ """
183
+ Get list of available channel types.
184
+
185
+ Returns:
186
+ List of channel type names
187
+
188
+ Example:
189
+ >>> types = AlertChannelFactory.list_available_types()
190
+ >>> "mattermost" in types
191
+ True
192
+ """
193
+ return sorted(cls.CHANNEL_TYPES.keys())
@@ -0,0 +1,53 @@
1
+ """
2
+ Mattermost alert channel.
3
+
4
+ Convenience wrapper around WebhookChannel for Mattermost.
5
+ """
6
+
7
+ from typing import Optional
8
+
9
+ from detectkit.alerting.channels.webhook import WebhookChannel
10
+
11
+
12
+ class MattermostChannel(WebhookChannel):
13
+ """
14
+ Mattermost alert channel using incoming webhooks.
15
+
16
+ This is a convenience wrapper around WebhookChannel specifically
17
+ for Mattermost. Mattermost webhooks are compatible with Slack API,
18
+ so WebhookChannel can be used directly.
19
+
20
+ Parameters:
21
+ webhook_url (str): Mattermost incoming webhook URL
22
+ username (str): Bot username to display (default: "detectk")
23
+ icon_emoji (str): Bot emoji icon (default: ":warning:")
24
+ timeout (int): Request timeout in seconds (default: 10)
25
+
26
+ Example:
27
+ >>> channel = MattermostChannel(
28
+ ... webhook_url="https://mattermost.example.com/hooks/xxx"
29
+ ... )
30
+ >>> success = channel.send(alert_data)
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ webhook_url: str,
36
+ username: str = "detectk",
37
+ icon_emoji: str = ":warning:",
38
+ channel: Optional[str] = None,
39
+ timeout: int = 10,
40
+ ):
41
+ """Initialize Mattermost channel with webhook URL."""
42
+ super().__init__(
43
+ webhook_url=webhook_url,
44
+ username=username,
45
+ icon_emoji=icon_emoji,
46
+ channel=channel, # Optional: override webhook's default channel
47
+ timeout=timeout,
48
+ )
49
+
50
+ def __repr__(self) -> str:
51
+ """String representation."""
52
+ url_preview = self.webhook_url[:30] + "..." if len(self.webhook_url) > 30 else self.webhook_url
53
+ return f"MattermostChannel(url='{url_preview}', username='{self.username}')"
@@ -0,0 +1,55 @@
1
+ """
2
+ Slack alert channel.
3
+
4
+ Convenience wrapper around WebhookChannel for Slack.
5
+ """
6
+
7
+ from typing import Optional
8
+
9
+ from detectkit.alerting.channels.webhook import WebhookChannel
10
+
11
+
12
+ class SlackChannel(WebhookChannel):
13
+ """
14
+ Slack alert channel using incoming webhooks.
15
+
16
+ This is a convenience wrapper around WebhookChannel specifically
17
+ for Slack. Slack and Mattermost use compatible webhook formats.
18
+
19
+ Parameters:
20
+ webhook_url (str): Slack incoming webhook URL
21
+ username (str): Bot username to display (default: "detectk")
22
+ icon_emoji (str): Bot emoji icon (default: ":warning:")
23
+ channel (str): Target Slack channel (optional, e.g., "#alerts")
24
+ timeout (int): Request timeout in seconds (default: 10)
25
+
26
+ Example:
27
+ >>> channel = SlackChannel(
28
+ ... webhook_url="https://hooks.slack.com/services/xxx",
29
+ ... channel="#alerts"
30
+ ... )
31
+ >>> success = channel.send(alert_data)
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ webhook_url: str,
37
+ username: str = "detectk",
38
+ icon_emoji: str = ":warning:",
39
+ channel: Optional[str] = None,
40
+ timeout: int = 10,
41
+ ):
42
+ """Initialize Slack channel with webhook URL."""
43
+ super().__init__(
44
+ webhook_url=webhook_url,
45
+ username=username,
46
+ icon_emoji=icon_emoji,
47
+ channel=channel,
48
+ timeout=timeout,
49
+ )
50
+
51
+ def __repr__(self) -> str:
52
+ """String representation."""
53
+ url_preview = self.webhook_url[:30] + "..." if len(self.webhook_url) > 30 else self.webhook_url
54
+ channel_info = f", channel='{self.channel}'" if self.channel else ""
55
+ return f"SlackChannel(url='{url_preview}', username='{self.username}'{channel_info})"