diff --git a/docs/cli/installation.mdx b/docs/cli/installation.mdx
index 469c46178..3020dd471 100644
--- a/docs/cli/installation.mdx
+++ b/docs/cli/installation.mdx
@@ -2,6 +2,14 @@
title: "Installation"
---
Missing an installation? submit a new installation request and we will add it as soon as we can.
+
+
+We recommend to install Keep CLI with Python version 3.11 for optimal compatibility and performance.
+This choice ensures seamless integration with all dependencies, including pyarrow, which currently does not support Python 3.12
+
+
+Need Keep CLI on other versions? Feel free to contact us!
+
## Clone and install (Option 1)
### Install
diff --git a/docs/workflows/functions/last.mdx b/docs/workflows/functions/last.mdx
new file mode 100644
index 000000000..a3ec04cee
--- /dev/null
+++ b/docs/workflows/functions/last.mdx
@@ -0,0 +1,27 @@
+---
+title: "last(iterable)"
+sidebarTitle: "last"
+---
+
+### Input
+
+An iterable.
+
+### Output
+
+The last item of the iterable.
+
+### Example
+
+```yaml
+actions:
+ - name: keep-slack
+ foreach: "{{steps.this.results}}"
+ condition:
+ - type: threshold
+ value: "keep.last(keep.split({{ foreach.value }}, ' '))"
+ # each line looks like:
+ # '2023-02-09 20:08:16,773 INFO: uvicorn.access -: 127.0.0.1:53948 - "GET /test2 HTTP/1.1" 503'
+ # where the "503" is the number of the
+ compare_to: 200
+```
diff --git a/docs/workflows/functions/lowercase.mdx b/docs/workflows/functions/lowercase.mdx
new file mode 100644
index 000000000..e945f4b62
--- /dev/null
+++ b/docs/workflows/functions/lowercase.mdx
@@ -0,0 +1,24 @@
+---
+title: "string(string)"
+sidebarTitle: "lowercase"
+---
+
+### Input
+
+A string.
+
+### Output
+
+Returns the string which is lowercased.
+
+### Example
+
+```yaml
+actions:
+ - name: trigger-slack
+ condition:
+ - type: equals
+ value: keep.lowercase('ABC DEF')
+ compare_to: "abc def"
+ compare_type: eq
+```
diff --git a/docs/workflows/functions/uppercase.mdx b/docs/workflows/functions/uppercase.mdx
new file mode 100644
index 000000000..45f3f6672
--- /dev/null
+++ b/docs/workflows/functions/uppercase.mdx
@@ -0,0 +1,24 @@
+---
+title: "string(string)"
+sidebarTitle: "uppercase"
+---
+
+### Input
+
+A string.
+
+### Output
+
+Returns the string which is uppercased.
+
+### Example
+
+```yaml
+actions:
+ - name: trigger-slack
+ condition:
+ - type: equals
+ value: keep.uppercase('abc def')
+ compare_to: "ABC DEF"
+ compare_type: eq
+```
diff --git a/examples/workflows/autosupress.yml b/examples/workflows/autosupress.yml
new file mode 100644
index 000000000..d9952098e
--- /dev/null
+++ b/examples/workflows/autosupress.yml
@@ -0,0 +1,16 @@
+workflow:
+ id: autosupress
+ description: demonstrates how to automatically suppress alerts
+ triggers:
+ - type: alert
+ filters:
+ - key: name
+ value: r"(somename)"
+ actions:
+ - name: dismiss-alert
+ provider:
+ type: mock
+ with:
+ enrich_alert:
+ - key: dismissed
+ value: "true"
diff --git a/keep/api/core/db.py b/keep/api/core/db.py
index b1ba90fc0..ad70afbea 100644
--- a/keep/api/core/db.py
+++ b/keep/api/core/db.py
@@ -777,6 +777,8 @@ def get_alerts_with_filters(tenant_id, provider_id=None, filters=None) -> list[A
if provider_id:
query = query.filter(Alert.provider_id == provider_id)
+ query = query.order_by(Alert.timestamp.desc())
+
# Execute the query
alerts = query.all()
diff --git a/keep/api/routes/workflows.py b/keep/api/routes/workflows.py
index c27c545bd..f4ae01077 100644
--- a/keep/api/routes/workflows.py
+++ b/keep/api/routes/workflows.py
@@ -184,13 +184,20 @@ def run_workflow(
# Finally, run it
try:
# if its event that was triggered by the UI with the Modal
- if "test-workflow" in body.get("fingerprint", ""):
+ if "test-workflow" in body.get("fingerprint", "") or not body:
# some random
- body["id"] = body.get("fingerprint")
+ body["id"] = body.get("fingerprint", "manual-run")
+ body["name"] = body.get("fingerprint", "manual-run")
body["lastReceived"] = datetime.datetime.now(
tz=datetime.timezone.utc
).isoformat()
- alert = AlertDto(**body)
+ try:
+ alert = AlertDto(**body)
+ except TypeError:
+ raise HTTPException(
+ status_code=400,
+ detail="Invalid alert format",
+ )
workflow_execution_id = workflowmanager.scheduler.handle_manual_event_workflow(
workflow_id, tenant_id, created_by, alert
)
diff --git a/keep/functions/__init__.py b/keep/functions/__init__.py
index 2e994a946..6a1760947 100644
--- a/keep/functions/__init__.py
+++ b/keep/functions/__init__.py
@@ -15,37 +15,38 @@ def all(iterable) -> bool:
g = groupby(iterable)
return next(g, True) and not next(g, False)
-
def diff(iterable: iter) -> bool:
# Opposite of all - returns True if any element is different
return not all(iterable)
-
def len(iterable=[]) -> int:
return _len(iterable)
+def uppercase(string) -> str:
+ return string.upper()
+
+def lowercase(string) -> str:
+ return string.lower()
def split(string, delimeter) -> list:
return string.strip().split(delimeter)
-
def strip(string) -> str:
return string.strip()
-
def first(iterable):
return iterable[0]
+def last(iterable):
+ return iterable[-1]
def utcnow() -> datetime.datetime:
dt = datetime.datetime.now(datetime.timezone.utc)
return dt
-
def utcnowiso() -> str:
return utcnow().isoformat()
-
def substract_minutes(dt: datetime.datetime, minutes: int) -> datetime.datetime:
"""
Substract minutes from a datetime object
@@ -59,28 +60,23 @@ def substract_minutes(dt: datetime.datetime, minutes: int) -> datetime.datetime:
"""
return dt - datetime.timedelta(minutes=minutes)
-
def to_utc(dt: datetime.datetime | str) -> datetime.datetime:
if isinstance(dt, str):
dt = parser.parse(dt)
utc_dt = dt.astimezone(pytz.utc)
return utc_dt
-
def datetime_compare(t1, t2) -> float:
diff = (t1 - t2).total_seconds() / 3600
return diff
-
def json_dumps(data: str | dict) -> str:
if isinstance(data, str):
data = json.loads(data)
return json.dumps(data, indent=4, default=str)
-
def encode(string) -> str:
return urllib.parse.quote(string)
-
def dict_to_key_value_list(d: dict) -> list:
return [f"{k}:{v}" for k, v in d.items()]
diff --git a/keep/providers/keep_provider/keep_provider.py b/keep/providers/keep_provider/keep_provider.py
index b9f6ee384..e13fbf6cd 100644
--- a/keep/providers/keep_provider/keep_provider.py
+++ b/keep/providers/keep_provider/keep_provider.py
@@ -1,6 +1,7 @@
"""
Keep Provider is a class that allows to ingest/digest data from Keep.
"""
+
import logging
from typing import Optional
@@ -27,7 +28,7 @@ def dispose(self):
"""
pass
- def _query(self, filters, **kwargs):
+ def _query(self, filters, distinct=True, **kwargs):
"""
Query Keep for alerts.
"""
@@ -35,13 +36,17 @@ def _query(self, filters, **kwargs):
self.context_manager.tenant_id, filters=filters
)
+ fingerprints = {}
alerts = []
if db_alerts:
for alert in db_alerts:
+ if fingerprints.get(alert.fingerprint) and distinct is True:
+ continue
alert_event = alert.event
if alert.alert_enrichment:
alert_event["enrichments"] = alert.alert_enrichment.enrichments
alerts.append(alert_event)
+ fingerprints[alert.fingerprint] = True
return alerts
def validate_config(self):
diff --git a/keep/providers/mock_provider/mock_provider.py b/keep/providers/mock_provider/mock_provider.py
index 12d207d28..a8af0f945 100644
--- a/keep/providers/mock_provider/mock_provider.py
+++ b/keep/providers/mock_provider/mock_provider.py
@@ -23,6 +23,14 @@ def _query(self, **kwargs):
"""
return kwargs.get("command_output")
+ def _notify(self, **kwargs):
+ """This is mock provider that just return the command output.
+
+ Returns:
+ _type_: _description_
+ """
+ return kwargs
+
def dispose(self):
"""
No need to dispose of anything, so just do nothing.
diff --git a/tests/test_functions.py b/tests/test_functions.py
index 958b11335..8af6e8086 100644
--- a/tests/test_functions.py
+++ b/tests/test_functions.py
@@ -32,19 +32,18 @@
("empty list", [], False),
],
)
+
def test_functions_diff(test_description, given, expected):
assert (
functions.diff(given) == expected
), f"{test_description}: Expected {given} to return {expected}"
-
def test_keep_len_function():
"""
Test the len function
"""
assert functions.len([1, 2, 3]) == 3
-
def test_keep_all_function():
"""
Test the all function
@@ -52,7 +51,6 @@ def test_keep_all_function():
assert functions.all([1, 1, 1]) == True
assert functions.all([1, 1, 0]) == False
-
def test_keep_diff_function():
"""
Test the diff function
@@ -60,7 +58,6 @@ def test_keep_diff_function():
assert functions.diff([1, 1, 1]) == False
assert functions.diff([1, 1, 0]) == True
-
def test_keep_split_function():
"""
Test the split function
@@ -68,6 +65,17 @@ def test_keep_split_function():
assert functions.split("a,b,c", ",") == ["a", "b", "c"]
assert functions.split("a|b|c", "|") == ["a", "b", "c"]
+def test_keep_uppercase_function():
+ """
+ Test the uppercase function
+ """
+ assert functions.uppercase("a") == "A"
+
+def test_keep_lowercase_function():
+ """
+ Test the lowercase function
+ """
+ assert functions.lowercase("A") == "a"
def test_keep_strip_function():
"""
@@ -75,13 +83,17 @@ def test_keep_strip_function():
"""
assert functions.strip(" a ") == "a"
-
def test_keep_first_function():
"""
Test the first function
"""
assert functions.first([1, 2, 3]) == 1
+def test_keep_last_function():
+ """
+ Test the last function
+ """
+ assert functions.last([1, 2, 3]) == 3
def test_keep_utcnow_function():
"""
@@ -91,7 +103,6 @@ def test_keep_utcnow_function():
assert isinstance(dt.tzinfo, type(datetime.timezone.utc))
assert isinstance(dt, datetime.datetime)
-
def test_keep_to_utc_function():
"""
Test the to_utc function
@@ -103,7 +114,6 @@ def test_keep_to_utc_function():
now_utc = functions.to_utc(now)
assert now_utc.tzinfo == pytz.utc
-
def test_keep_datetime_compare_function():
"""
Test the datetime_compare function
@@ -114,7 +124,6 @@ def test_keep_datetime_compare_function():
assert int(functions.datetime_compare(dt2, dt1)) == 1
assert int(functions.datetime_compare(dt1, dt1)) == 0
-
def test_keep_encode_function():
"""
Test the encode function