kpi-engine 1.0.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 (34) hide show
  1. kpi_engine-1.0.0/LICENSE +21 -0
  2. kpi_engine-1.0.0/PKG-INFO +28 -0
  3. kpi_engine-1.0.0/README.md +401 -0
  4. kpi_engine-1.0.0/kpi_engine/__init__.py +16 -0
  5. kpi_engine-1.0.0/kpi_engine/alerts/__init__.py +7 -0
  6. kpi_engine-1.0.0/kpi_engine/alerts/dispatcher.py +16 -0
  7. kpi_engine-1.0.0/kpi_engine/alerts/email.py +46 -0
  8. kpi_engine-1.0.0/kpi_engine/alerts/evaluator.py +30 -0
  9. kpi_engine-1.0.0/kpi_engine/alerts/pagerduty.py +30 -0
  10. kpi_engine-1.0.0/kpi_engine/alerts/slack.py +20 -0
  11. kpi_engine-1.0.0/kpi_engine/audit.py +102 -0
  12. kpi_engine-1.0.0/kpi_engine/backends/__init__.py +6 -0
  13. kpi_engine-1.0.0/kpi_engine/backends/base.py +11 -0
  14. kpi_engine-1.0.0/kpi_engine/backends/dataframe.py +73 -0
  15. kpi_engine-1.0.0/kpi_engine/backends/derived.py +16 -0
  16. kpi_engine-1.0.0/kpi_engine/backends/sql.py +33 -0
  17. kpi_engine-1.0.0/kpi_engine/comparator.py +29 -0
  18. kpi_engine-1.0.0/kpi_engine/engine.py +151 -0
  19. kpi_engine-1.0.0/kpi_engine/models.py +79 -0
  20. kpi_engine-1.0.0/kpi_engine/period.py +68 -0
  21. kpi_engine-1.0.0/kpi_engine/registry.py +34 -0
  22. kpi_engine-1.0.0/kpi_engine/scheduler.py +56 -0
  23. kpi_engine-1.0.0/kpi_engine/server.py +79 -0
  24. kpi_engine-1.0.0/kpi_engine.egg-info/PKG-INFO +28 -0
  25. kpi_engine-1.0.0/kpi_engine.egg-info/SOURCES.txt +32 -0
  26. kpi_engine-1.0.0/kpi_engine.egg-info/dependency_links.txt +1 -0
  27. kpi_engine-1.0.0/kpi_engine.egg-info/requires.txt +27 -0
  28. kpi_engine-1.0.0/kpi_engine.egg-info/top_level.txt +1 -0
  29. kpi_engine-1.0.0/pyproject.toml +33 -0
  30. kpi_engine-1.0.0/setup.cfg +4 -0
  31. kpi_engine-1.0.0/tests/test_alerts.py +109 -0
  32. kpi_engine-1.0.0/tests/test_comparator.py +76 -0
  33. kpi_engine-1.0.0/tests/test_engine.py +120 -0
  34. kpi_engine-1.0.0/tests/test_period.py +83 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 muhammadsufiyanbaig
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: kpi-engine
3
+ Version: 1.0.0
4
+ Summary: Declarative KPI computation and alerting framework
5
+ Requires-Python: >=3.8
6
+ License-File: LICENSE
7
+ Requires-Dist: pandas>=1.3
8
+ Requires-Dist: sqlalchemy>=1.4
9
+ Requires-Dist: jinja2>=3.0
10
+ Requires-Dist: pydantic>=2.0
11
+ Requires-Dist: pyyaml>=6.0
12
+ Requires-Dist: numpy>=1.21
13
+ Requires-Dist: python-dateutil>=2.8
14
+ Provides-Extra: alerts
15
+ Requires-Dist: requests>=2.28; extra == "alerts"
16
+ Provides-Extra: server
17
+ Requires-Dist: fastapi>=0.100; extra == "server"
18
+ Requires-Dist: uvicorn>=0.22; extra == "server"
19
+ Provides-Extra: scheduler
20
+ Requires-Dist: croniter>=1.3; extra == "scheduler"
21
+ Provides-Extra: bigquery
22
+ Requires-Dist: google-cloud-bigquery>=3.0; extra == "bigquery"
23
+ Provides-Extra: snowflake
24
+ Requires-Dist: snowflake-connector-python>=3.0; extra == "snowflake"
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0; extra == "dev"
27
+ Requires-Dist: pytest-cov; extra == "dev"
28
+ Dynamic: license-file
@@ -0,0 +1,401 @@
1
+ # kpi-engine
2
+
3
+ > **A declarative framework for defining, computing, and alerting on KPIs from SQL or DataFrames — with built-in period-over-period comparisons.**
4
+
5
+ [![PyPI version](https://img.shields.io/pypi/v/kpi-engine.svg)](https://pypi.org/project/kpi-engine/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/kpi-engine.svg)](https://pypi.org/project/kpi-engine/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
+
9
+ ---
10
+
11
+ ## Overview
12
+
13
+ `kpi-engine` brings structure and repeatability to business metrics. Instead of writing ad-hoc SQL queries and notebook cells to compute KPIs, you define them once in a declarative YAML or Python DSL — then `kpi-engine` handles computation, historical comparisons, trend analysis, and alerting automatically.
14
+
15
+ **Supported backends:** PostgreSQL, MySQL, SQLite, BigQuery, Snowflake (via SQLAlchemy) and pandas DataFrames.
16
+
17
+ ---
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install kpi-engine
23
+ ```
24
+
25
+ With optional extras:
26
+
27
+ ```bash
28
+ pip install "kpi-engine[alerts]" # Slack, email, PagerDuty
29
+ pip install "kpi-engine[server]" # FastAPI REST server
30
+ pip install "kpi-engine[scheduler]" # Cron scheduling
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Quick Start
36
+
37
+ ### From a YAML file
38
+
39
+ ```yaml
40
+ # kpis.yaml
41
+ kpis:
42
+ - name: monthly_revenue
43
+ label: Monthly Revenue
44
+ source: sql
45
+ query: >
46
+ SELECT SUM(amount) FROM orders
47
+ WHERE order_date >= '{{ period_start }}'::date
48
+ AND order_date < '{{ period_end }}'::date
49
+ aggregation: sum
50
+ unit: USD
51
+ compare: [MoM, YoY]
52
+ alerts:
53
+ - condition: "< 100000"
54
+ severity: critical
55
+ message: Revenue dropped below $100K
56
+ ```
57
+
58
+ ```python
59
+ from sqlalchemy import create_engine
60
+ from kpi_engine import KPIEngine
61
+
62
+ engine = KPIEngine.from_yaml(
63
+ "kpis.yaml",
64
+ connection=create_engine("postgresql://user:pass@host/db")
65
+ )
66
+
67
+ results = engine.run(period="last_month")
68
+
69
+ for kpi in results:
70
+ print(f"{kpi.label}: {kpi.value:,.2f} {kpi.unit}")
71
+ if "MoM" in kpi.comparisons:
72
+ print(f" MoM: {kpi.mom_change_pct:+.1f}%")
73
+ print(f" Status: {kpi.alert_status}")
74
+ ```
75
+
76
+ ### From Python directly
77
+
78
+ ```python
79
+ from kpi_engine import KPIEngine
80
+ from kpi_engine.models import KPIDefinition, Alert
81
+ import pandas as pd
82
+
83
+ df = pd.DataFrame({
84
+ "revenue": [1000, 2000, 3000],
85
+ "order_date": pd.to_datetime(["2024-11-01", "2024-11-15", "2024-11-28"]),
86
+ })
87
+
88
+ kpis = [
89
+ KPIDefinition(
90
+ name="revenue",
91
+ label="Monthly Revenue",
92
+ source="dataframe",
93
+ aggregation="sum",
94
+ unit="USD",
95
+ query="orders.revenue", # "table.column" format
96
+ compare=["MoM"],
97
+ alerts=[Alert(condition="< 1000", severity="warning")],
98
+ )
99
+ ]
100
+
101
+ engine = KPIEngine(kpis=kpis, dataframes={"orders": df})
102
+ results = engine.run(period="2024-11")
103
+ ```
104
+
105
+ ---
106
+
107
+ ## How It Works
108
+
109
+ ```
110
+ KPI Definitions (YAML or Python DSL)
111
+
112
+
113
+ ┌─────────────────────────────┐
114
+ │ KPI Registry │ ← Parses and validates all KPI definitions
115
+ └──────────────┬──────────────┘
116
+
117
+ ┌─────────────────────────────┐
118
+ │ Period Resolver │ ← Converts "last_month", "2024-Q3", "yesterday"
119
+ │ │ into concrete start/end datetime pairs
120
+ └──────────────┬──────────────┘
121
+
122
+ ┌─────────────────────────────────────────────────────┐
123
+ │ Computation Engine │
124
+ │ ┌──────────────┐ ┌──────────────┐ Derived KPI │
125
+ │ │ SQL Backend │ │ DataFrame │ (expression) │
126
+ │ │ (SQLAlchemy) │ │ Backend │ │
127
+ │ └──────────────┘ └──────────────┘ │
128
+ └──────────────┬──────────────────────────────────────┘
129
+
130
+ ┌─────────────────────────────┐
131
+ │ Period-over-Period Comparator│ ← Computes Δ and Δ%
132
+ └──────────────┬──────────────┘
133
+
134
+ ┌─────────────────────────────┐
135
+ │ Alert Evaluator │ ← Threshold, change %, anomaly rules
136
+ └──────────────┬──────────────┘
137
+
138
+ ┌─────────────────────────────┐
139
+ │ Alert Dispatcher │ ← Slack, email, PagerDuty, webhooks
140
+ └──────────────┬──────────────┘
141
+
142
+ ┌─────────────────────────────┐
143
+ │ KPIResult + Audit Log │ ← Structured result + CSV/SQLite history
144
+ └─────────────────────────────┘
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Period Strings
150
+
151
+ | Input | Resolves To |
152
+ |-------|------------|
153
+ | `"yesterday"` | Previous calendar day |
154
+ | `"last_week"` | Mon–Sun of the previous week |
155
+ | `"last_month"` | Full previous calendar month |
156
+ | `"last_quarter"` | Previous Q1/Q2/Q3/Q4 |
157
+ | `"2024-Q3"` | July 1 – September 30, 2024 |
158
+ | `"2024-11"` | All of November 2024 |
159
+
160
+ ---
161
+
162
+ ## KPI Sources
163
+
164
+ ### SQL Backend
165
+
166
+ Queries run via SQLAlchemy. Use Jinja2 template variables `{{ period_start }}` and `{{ period_end }}` in your query:
167
+
168
+ ```python
169
+ KPIDefinition(
170
+ name="signups",
171
+ label="New Signups",
172
+ source="sql",
173
+ aggregation="count",
174
+ query="SELECT COUNT(*) FROM users WHERE created_at >= '{{ period_start }}'",
175
+ )
176
+ ```
177
+
178
+ ### DataFrame Backend
179
+
180
+ Pass a dict of DataFrames. Use `"table.column"` in the `query` field:
181
+
182
+ ```python
183
+ KPIDefinition(
184
+ name="revenue",
185
+ source="dataframe",
186
+ aggregation="sum",
187
+ query="sales.amount", # sales DataFrame, amount column
188
+ )
189
+ ```
190
+
191
+ Aggregations: `sum`, `avg`, `count`, `last`, `rate`
192
+
193
+ ### Derived KPIs
194
+
195
+ Computed from already-resolved KPI values using a Python expression:
196
+
197
+ ```python
198
+ KPIDefinition(
199
+ name="arpu",
200
+ label="ARPU",
201
+ source="derived",
202
+ expression="revenue / active_users",
203
+ unit="USD",
204
+ )
205
+ ```
206
+
207
+ Derived KPIs always run after their dependencies. The engine builds a DAG automatically.
208
+
209
+ ---
210
+
211
+ ## Alerts
212
+
213
+ ### Condition syntax
214
+
215
+ | Condition | Triggers when |
216
+ |-----------|--------------|
217
+ | `"< 1000"` | value is below 1000 |
218
+ | `"> 0.15"` | value is above 0.15 |
219
+ | `"<= 100"` | value is at most 100 |
220
+ | `">= 500"` | value is at least 500 |
221
+ | `"== 0"` | value equals 0 |
222
+
223
+ ### Alert channels
224
+
225
+ **Slack:**
226
+ ```python
227
+ from kpi_engine.alerts import SlackChannel
228
+
229
+ engine = KPIEngine(
230
+ kpis=kpis,
231
+ alert_channels=[SlackChannel(webhook_url="https://hooks.slack.com/...")]
232
+ )
233
+ ```
234
+
235
+ **Email:**
236
+ ```python
237
+ from kpi_engine.alerts import EmailChannel
238
+
239
+ EmailChannel(
240
+ smtp_host="smtp.gmail.com", smtp_port=587,
241
+ from_email="alerts@company.com",
242
+ to_emails=["team@company.com"],
243
+ username="alerts@company.com", password="..."
244
+ )
245
+ ```
246
+
247
+ **PagerDuty:**
248
+ ```python
249
+ from kpi_engine.alerts import PagerDutyChannel
250
+
251
+ PagerDutyChannel(integration_key="your-integration-key")
252
+ ```
253
+
254
+ ---
255
+
256
+ ## REST API
257
+
258
+ ```python
259
+ pip install "kpi-engine[server]"
260
+ ```
261
+
262
+ ```python
263
+ engine.serve(port=8000)
264
+ ```
265
+
266
+ | Endpoint | Description |
267
+ |----------|-------------|
268
+ | `GET /kpis?period=last_month` | Compute all KPIs |
269
+ | `GET /kpis/{name}?period=2024-11` | Compute a single KPI |
270
+ | `GET /kpis/{name}/history?n=10` | Last n results |
271
+
272
+ ---
273
+
274
+ ## Scheduling
275
+
276
+ ```python
277
+ pip install "kpi-engine[scheduler]"
278
+ ```
279
+
280
+ ```python
281
+ scheduler = engine.schedule(
282
+ cron="0 9 1 * *", # 1st of every month at 9am UTC
283
+ period_fn=lambda: "last_month",
284
+ callback=lambda results: print(f"Done: {len(results)} KPIs")
285
+ )
286
+ # runs in a background daemon thread
287
+ # scheduler.stop() to cancel
288
+ ```
289
+
290
+ ---
291
+
292
+ ## Audit Log
293
+
294
+ ```python
295
+ engine = KPIEngine(kpis=kpis, connection=conn, audit_log="audit.csv")
296
+ # or
297
+ engine = KPIEngine(kpis=kpis, connection=conn, audit_log="audit.db") # SQLite
298
+ ```
299
+
300
+ Every `engine.run()` call appends results to the audit log automatically.
301
+
302
+ ---
303
+
304
+ ## API Reference
305
+
306
+ ### `KPIEngine`
307
+
308
+ ```python
309
+ KPIEngine(
310
+ kpis: list[KPIDefinition],
311
+ connection=None, # SQLAlchemy engine
312
+ dataframes: dict = None, # {"table_name": pd.DataFrame}
313
+ alert_channels: list = None,
314
+ audit_log: str = None # path to .csv or .db file
315
+ )
316
+ ```
317
+
318
+ | Method | Returns | Description |
319
+ |--------|---------|-------------|
320
+ | `engine.run(period)` | `list[KPIResult]` | Compute all KPIs |
321
+ | `engine.run_kpi(name, period)` | `KPIResult` | Compute one KPI |
322
+ | `engine.history(name, n)` | `list[KPIResult]` | Last n results |
323
+ | `engine.schedule(cron, period_fn)` | `KPIScheduler` | Schedule recurring runs |
324
+ | `engine.serve(port)` | — | Start REST API (blocking) |
325
+ | `KPIEngine.from_yaml(path, ...)` | `KPIEngine` | Load from YAML config |
326
+
327
+ ### `KPIResult`
328
+
329
+ ```python
330
+ result.value # float
331
+ result.unit # str
332
+ result.alert_status # "ok" | "warning" | "critical"
333
+ result.comparisons # dict[str, ComparisonResult]
334
+ result.alerts_triggered # list[AlertResult]
335
+ result.mom_change_pct # float | None
336
+ result.yoy_change_pct # float | None
337
+ result.period_start # datetime
338
+ result.period_end # datetime
339
+ result.query_duration_ms # float
340
+ ```
341
+
342
+ ### `KPIDefinition` fields
343
+
344
+ | Field | Type | Description |
345
+ |-------|------|-------------|
346
+ | `name` | `str` | Unique identifier |
347
+ | `label` | `str` | Human-readable name |
348
+ | `source` | `str` | `"sql"` \| `"dataframe"` \| `"derived"` |
349
+ | `aggregation` | `str` | `"sum"` \| `"avg"` \| `"count"` \| `"rate"` \| `"last"` |
350
+ | `query` | `str` | SQL template or `"table.column"` |
351
+ | `expression` | `str` | Python expression for derived KPIs |
352
+ | `compare` | `list[str]` | `["MoM", "YoY", "QoQ", "WoW", "DoD"]` |
353
+ | `polarity` | `str` | `"higher_is_better"` \| `"lower_is_better"` |
354
+ | `alerts` | `list[Alert]` | Alert definitions |
355
+ | `unit` | `str` | Display unit (e.g. `"USD"`, `"%"`) |
356
+
357
+ ---
358
+
359
+ ## Project Structure
360
+
361
+ ```
362
+ kpi-engine/
363
+ ├── kpi_engine/
364
+ │ ├── engine.py # KPIEngine orchestrator
365
+ │ ├── registry.py # KPI registry and validation
366
+ │ ├── models.py # KPIDefinition, KPIResult, Alert dataclasses
367
+ │ ├── period.py # Period resolution logic
368
+ │ ├── backends/
369
+ │ │ ├── base.py # BaseBackend abstract class
370
+ │ │ ├── sql.py # SQLAlchemy backend
371
+ │ │ ├── dataframe.py # Pandas backend
372
+ │ │ └── derived.py # Derived KPI expression evaluator
373
+ │ ├── comparator.py # Period-over-period comparison
374
+ │ ├── alerts/
375
+ │ │ ├── evaluator.py # Alert threshold evaluation
376
+ │ │ ├── dispatcher.py # Routes alerts to channels
377
+ │ │ ├── slack.py # Slack webhook channel
378
+ │ │ ├── email.py # SMTP email channel
379
+ │ │ └── pagerduty.py # PagerDuty Events API channel
380
+ │ ├── scheduler.py # Cron-based scheduling
381
+ │ ├── audit.py # Audit log (CSV or SQLite)
382
+ │ └── server.py # FastAPI REST server
383
+ └── tests/
384
+ ```
385
+
386
+ ---
387
+
388
+ ## License
389
+
390
+ MIT — see [LICENSE](LICENSE)
391
+
392
+ ---
393
+
394
+ ## Contributing
395
+
396
+ PRs welcome. Add tests for new KPI types and alert conditions.
397
+
398
+ ```bash
399
+ pip install -e ".[dev]"
400
+ pytest tests/ -v
401
+ ```
@@ -0,0 +1,16 @@
1
+ from .engine import KPIEngine
2
+ from .models import Alert, AlertResult, ComparisonResult, KPIDefinition, KPIResult
3
+ from .period import Period, resolve_period
4
+ from .registry import KPIRegistry
5
+
6
+ __all__ = [
7
+ "KPIEngine",
8
+ "KPIDefinition",
9
+ "KPIResult",
10
+ "Alert",
11
+ "AlertResult",
12
+ "ComparisonResult",
13
+ "Period",
14
+ "resolve_period",
15
+ "KPIRegistry",
16
+ ]
@@ -0,0 +1,7 @@
1
+ from .evaluator import AlertEvaluator
2
+ from .dispatcher import AlertDispatcher
3
+ from .slack import SlackChannel
4
+ from .email import EmailChannel
5
+ from .pagerduty import PagerDutyChannel
6
+
7
+ __all__ = ["AlertEvaluator", "AlertDispatcher", "SlackChannel", "EmailChannel", "PagerDutyChannel"]
@@ -0,0 +1,16 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
5
+
6
+ class AlertDispatcher:
7
+ def __init__(self, channels: list):
8
+ self.channels = channels
9
+
10
+ def dispatch(self, kpi, value, triggered_alerts):
11
+ for alert_result in triggered_alerts:
12
+ for channel in self.channels:
13
+ try:
14
+ channel.send(kpi, value, alert_result)
15
+ except Exception as e:
16
+ logger.warning("Failed to dispatch alert via %s: %s", channel.__class__.__name__, e)
@@ -0,0 +1,46 @@
1
+ import smtplib
2
+ from email.mime.multipart import MIMEMultipart
3
+ from email.mime.text import MIMEText
4
+ from typing import List
5
+
6
+
7
+ class EmailChannel:
8
+ def __init__(
9
+ self,
10
+ smtp_host: str,
11
+ smtp_port: int,
12
+ from_email: str,
13
+ to_emails: List[str],
14
+ username: str = None,
15
+ password: str = None,
16
+ use_tls: bool = True,
17
+ ):
18
+ self.smtp_host = smtp_host
19
+ self.smtp_port = smtp_port
20
+ self.from_email = from_email
21
+ self.to_emails = to_emails
22
+ self.username = username
23
+ self.password = password
24
+ self.use_tls = use_tls
25
+
26
+ def send(self, kpi, value, alert_result):
27
+ msg = MIMEMultipart()
28
+ msg["From"] = self.from_email
29
+ msg["To"] = ", ".join(self.to_emails)
30
+ msg["Subject"] = f"KPI Alert [{alert_result.severity.upper()}]: {kpi.label}"
31
+
32
+ body = (
33
+ f"KPI Alert triggered.\n\n"
34
+ f"KPI: {kpi.label}\n"
35
+ f"Value: {value} {kpi.unit}\n"
36
+ f"Severity: {alert_result.severity}\n"
37
+ f"Message: {alert_result.message}\n"
38
+ )
39
+ msg.attach(MIMEText(body, "plain"))
40
+
41
+ with smtplib.SMTP(self.smtp_host, self.smtp_port) as server:
42
+ if self.use_tls:
43
+ server.starttls()
44
+ if self.username and self.password:
45
+ server.login(self.username, self.password)
46
+ server.sendmail(self.from_email, self.to_emails, msg.as_string())
@@ -0,0 +1,30 @@
1
+ from ..models import Alert, AlertResult
2
+
3
+
4
+ class AlertEvaluator:
5
+ def evaluate(self, value: float, alerts: list) -> list:
6
+ results = []
7
+ for alert in alerts:
8
+ triggered = self._check_condition(value, alert.condition)
9
+ results.append(AlertResult(
10
+ alert=alert,
11
+ triggered=triggered,
12
+ message=alert.message or f"KPI value {value} {alert.condition}",
13
+ severity=alert.severity
14
+ ))
15
+ return results
16
+
17
+ def _check_condition(self, value: float, condition: str) -> bool:
18
+ """Evaluate a condition string like '< 50000' or '> 0.15'."""
19
+ condition = condition.strip()
20
+ if condition.startswith("<= "):
21
+ return value <= float(condition[3:])
22
+ if condition.startswith(">= "):
23
+ return value >= float(condition[3:])
24
+ if condition.startswith("== "):
25
+ return value == float(condition[3:])
26
+ if condition.startswith("< "):
27
+ return value < float(condition[2:])
28
+ if condition.startswith("> "):
29
+ return value > float(condition[2:])
30
+ return False
@@ -0,0 +1,30 @@
1
+ import requests
2
+
3
+ _SEVERITY_MAP = {"info": "info", "warning": "warning", "critical": "critical"}
4
+
5
+
6
+ class PagerDutyChannel:
7
+ API_URL = "https://events.pagerduty.com/v2/enqueue"
8
+
9
+ def __init__(self, integration_key: str):
10
+ self.integration_key = integration_key
11
+
12
+ def send(self, kpi, value, alert_result):
13
+ payload = {
14
+ "routing_key": self.integration_key,
15
+ "event_action": "trigger",
16
+ "payload": {
17
+ "summary": f"KPI Alert: {kpi.label} — {alert_result.message}",
18
+ "severity": _SEVERITY_MAP.get(alert_result.severity, "warning"),
19
+ "source": "kpi-engine",
20
+ "custom_details": {
21
+ "kpi_name": kpi.name,
22
+ "kpi_label": kpi.label,
23
+ "value": value,
24
+ "unit": kpi.unit,
25
+ "condition": alert_result.alert.condition,
26
+ },
27
+ },
28
+ }
29
+ response = requests.post(self.API_URL, json=payload, timeout=10)
30
+ response.raise_for_status()
@@ -0,0 +1,20 @@
1
+ import requests
2
+
3
+
4
+ class SlackChannel:
5
+ def __init__(self, webhook_url: str, channel: str = None):
6
+ self.webhook_url = webhook_url
7
+ self.channel = channel
8
+
9
+ def send(self, kpi, value, alert_result):
10
+ message = {
11
+ "text": (
12
+ f"*KPI Alert* [{alert_result.severity.upper()}]\n"
13
+ f"*{kpi.label}*: {value} {kpi.unit}\n"
14
+ f"{alert_result.message}"
15
+ )
16
+ }
17
+ if self.channel:
18
+ message["channel"] = self.channel
19
+ response = requests.post(self.webhook_url, json=message, timeout=10)
20
+ response.raise_for_status()