Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions listeners/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from dataclasses import dataclass
from enum import Enum
from typing import List, Optional


class FilterType(Enum):
MULTI_SELECT = "multi_select"
TOGGLE = "toggle"


@dataclass
class FilterOptions:
name: str
value: str


@dataclass
class Filter:
name: str
display_name: str
type: FilterType
display_name_plural: Optional[str] = None
options: Optional[List[FilterOptions]] = None


LANGUAGES_FILTER = Filter(
name="languages",
display_name="Language",
display_name_plural="Languages",
type=FilterType.MULTI_SELECT.value,
options=[
FilterOptions(name="Python", value="python"),
FilterOptions(name="Java", value="java"),
FilterOptions(name="JavaScript", value="javascript"),
FilterOptions(name="TypeScript", value="typescript"),
],
)

TEMPLATES_FILTER = Filter(name="template", display_name="Templates", type=FilterType.TOGGLE.value)


SAMPLES_FILTER = Filter(name="sample", display_name="Samples", type=FilterType.TOGGLE.value)
62 changes: 19 additions & 43 deletions listeners/functions/filters.py
Original file line number Diff line number Diff line change
@@ -1,62 +1,38 @@
import logging
from enum import Enum
from typing import List, Optional, TypedDict
from dataclasses import asdict
from typing import Dict

from slack_bolt import Ack, Complete, Fail

from listeners.filters import LANGUAGES_FILTER, SAMPLES_FILTER, TEMPLATES_FILTER

FILTER_PROCESSING_ERROR_MSG = (
"We encountered an issue processing filter results. Please try again or contact the app owner if the problem persists."
)


class FilterType(Enum):
MULTI_SELECT = "multi_select"
TOGGLE = "toggle"


class FilterOptions(TypedDict):
name: str
value: str


class SearchFilter(TypedDict):
name: str
display_name: str
filter_type: FilterType
options: Optional[List[FilterOptions]]
def filter_none(items: Dict):
return {k: v for k, v in items if v is not None}


def filters_step_callback(ack: Ack, inputs: dict, fail: Fail, complete: Complete, logger: logging.Logger):
try:
user_context = inputs.get("user_context", {})
logger.debug(f"User {user_context.get('id')} executing filter request")

filters: List[SearchFilter] = [
{
"name": "languages",
"display_name": "Languages",
"type": FilterType.MULTI_SELECT.value,
"options": [
{"name": "Python", "value": "python"},
{"name": "Java", "value": "java"},
{"name": "JavaScript", "value": "javascript"},
{"name": "TypeScript", "value": "typescript"},
],
},
{
"name": "type",
"display_name": "Type",
"type": FilterType.MULTI_SELECT.value,
"options": [
{"name": "Template", "value": "template"},
{"name": "Sample", "value": "sample"},
],
},
]

complete(outputs={"filters": filters})
complete(
outputs={
"filters": [
asdict(LANGUAGES_FILTER, dict_factory=filter_none),
asdict(TEMPLATES_FILTER, dict_factory=filter_none),
asdict(SAMPLES_FILTER, dict_factory=filter_none),
]
}
)
except Exception as e:
logger.error(f"Unexpected error occurred while processing filter request: {type(e).__name__} - {str(e)}", exc_info=e)
logger.error(
f"Unexpected error occurred while processing filter request: {type(e).__name__} - {str(e)}",
exc_info=e,
)
fail(error=FILTER_PROCESSING_ERROR_MSG)
finally:
ack()
30 changes: 13 additions & 17 deletions listeners/functions/search.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import logging
from ast import List
from typing import NotRequired, Optional, TypedDict
from typing import List, NotRequired, Optional, TypedDict

from slack_bolt import Ack, Complete, Fail
from slack_sdk import WebClient
Expand All @@ -12,8 +11,6 @@
"Please try again or contact the app owner if the problem persists."
)

print(SEARCH_PROCESSING_ERROR_MSG)


class EntityReference(TypedDict):
id: str
Expand All @@ -29,21 +26,19 @@ class SearchResult(TypedDict):
content: NotRequired[str]


def search_step_callback(ack: Ack, inputs: dict, fail: Fail, complete: Complete, client: WebClient, logger: logging.Logger):
def search_step_callback(
ack: Ack,
inputs: dict,
fail: Fail,
complete: Complete,
client: WebClient,
logger: logging.Logger,
):
try:
query = inputs.get("query")
filters = inputs.get("filters", {})
languages_filter = filters.get("languages", [])
type_filter = filters.get("type", [])

filters_payload = {}
if languages_filter:
filters_payload["languages"] = languages_filter
if type_filter:
if len(type_filter) == 1:
filters_payload["type"] = type_filter[0]
filters = inputs.get("filters")

response = fetch_sample_data(client=client, query=query, filters=filters_payload, logger=logger)
response = fetch_sample_data(client=client, query=query, filters=filters, logger=logger)

samples = response.get("samples", [])

Expand All @@ -66,7 +61,8 @@ def search_step_callback(ack: Ack, inputs: dict, fail: Fail, complete: Complete,
fail(error=SEARCH_PROCESSING_ERROR_MSG)
else:
logger.error(
f"Unexpected error occurred while processing search request: {type(e).__name__} - {str(e)}", exc_info=e
f"Unexpected error occurred while processing search request: {type(e).__name__} - {str(e)}",
exc_info=e,
)
finally:
ack()
37 changes: 25 additions & 12 deletions listeners/sample_data_service.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import json
import logging

from slack_sdk import WebClient

from listeners.filters import LANGUAGES_FILTER, SAMPLES_FILTER, TEMPLATES_FILTER

API_METHOD = "developer.sampleData.get"


class SlackResponseError(Exception):
def __init__(self, message: str):
Expand All @@ -11,21 +14,31 @@ def __init__(self, message: str):


def fetch_sample_data(client: WebClient, query: str = None, filters: dict = None, logger: logging.Logger = None):
method = "developer.sampleData.get"
params = {}
if query:
params["query"] = query
params = {"query": query}

if filters:
params["filters"] = json.dumps(filters)
selected_filters = {}

languages = filters.get(LANGUAGES_FILTER.name, [])
templates = filters.get(TEMPLATES_FILTER.name, False)
samples = filters.get(SAMPLES_FILTER.name, False)

response = client.api_call(method, params=params)
if languages:
selected_filters[LANGUAGES_FILTER.name] = languages

if templates ^ samples:
if templates:
selected_filters["type"] = TEMPLATES_FILTER.name
elif samples:
selected_filters["type"] = SAMPLES_FILTER.name

if selected_filters:
params["filters"] = selected_filters

response = client.api_call(API_METHOD, params=params)

if not response.get("ok", False):
logger.error(f"Search API request failed with error: {response.get('error', 'no error found')}")
raise SlackResponseError(f"Failed to fetch sample data from Slack API: ok=false for method={method}")

if "samples" not in response:
logger.error(f"Failed to parse API response as sample data. Received: {json.dumps(response)}")
raise SlackResponseError(f"Invalid response format from Slack API from {method}")
raise SlackResponseError(f"Failed to fetch sample data from Slack API: ok=false for method={API_METHOD}")

return response
37 changes: 25 additions & 12 deletions tests/listeners/functions/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ def setup_method(self):
self.expected_filters = [
{
"name": "languages",
"display_name": "Languages",
"display_name": "Language",
"type": "multi_select",
"display_name_plural": "Languages",
"options": [
{"name": "Python", "value": "python"},
{"name": "Java", "value": "java"},
Expand All @@ -25,21 +26,26 @@ def setup_method(self):
],
},
{
"name": "type",
"display_name": "Type",
"type": "multi_select",
"options": [
{"name": "Template", "value": "template"},
{"name": "Sample", "value": "sample"},
],
"name": "template",
"display_name": "Templates",
"type": "toggle",
},
{
"name": "sample",
"display_name": "Samples",
"type": "toggle",
},
]

def test_filters_step_callback_success(self):
inputs = {"user_context": {"id": "U123456"}}

filters_step_callback(
ack=self.mock_ack, inputs=inputs, fail=self.mock_fail, complete=self.mock_complete, logger=self.mock_logger
ack=self.mock_ack,
inputs=inputs,
fail=self.mock_fail,
complete=self.mock_complete,
logger=self.mock_logger,
)

self.mock_complete.assert_called_once()
Expand All @@ -52,7 +58,11 @@ def test_filters_step_callback_success(self):

def test_filters_step_callback_empty_user_context(self):
filters_step_callback(
ack=self.mock_ack, inputs={}, fail=self.mock_fail, complete=self.mock_complete, logger=self.mock_logger
ack=self.mock_ack,
inputs={},
fail=self.mock_fail,
complete=self.mock_complete,
logger=self.mock_logger,
)

self.mock_complete.assert_called_once()
Expand All @@ -66,12 +76,15 @@ def test_filters_step_callback_unexpected_exception(self):
self.mock_complete.side_effect = Exception("Unexpected error")

filters_step_callback(
ack=self.mock_ack, inputs={}, fail=self.mock_fail, complete=self.mock_complete, logger=self.mock_logger
ack=self.mock_ack,
inputs={},
fail=self.mock_fail,
complete=self.mock_complete,
logger=self.mock_logger,
)

self.mock_fail.assert_called_once()
call_args = self.mock_fail.call_args
print(FILTER_PROCESSING_ERROR_MSG)
assert call_args.kwargs["error"] == FILTER_PROCESSING_ERROR_MSG

self.mock_ack.assert_called_once()
Loading