Skip to content

Commit

Permalink
updated to use spiffworkflow-connector-command w/ burnettk
Browse files Browse the repository at this point in the history
  • Loading branch information
jasquat committed Oct 13, 2023
1 parent 8390fac commit ed3231b
Show file tree
Hide file tree
Showing 7 changed files with 468 additions and 263 deletions.
54 changes: 54 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Tests

on: [push]

jobs:
linting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip
restore-keys: ${{ runner.os }}-pip
- run: python -m pip install ruff
- run: |
ruff .
test:
needs: linting
strategy:
fail-fast: true
matrix:
python-version: [ "3.10", "3.11" ]
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v3
- name: Set up python ${{ matrix.python-version }}
id: setup-python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: 1.6.1
virtualenvs-create: true
virtualenvs-in-project: true
- name: Load cached venv
id: cached-poetry-dependencies
uses: actions/cache@v3
with:
path: .venv
key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction --no-root
- name: Install library
run: poetry install --no-interaction
- name: Run tests
run: |
source .venv/bin/activate
pytest tests/
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python 3.11.0
435 changes: 260 additions & 175 deletions poetry.lock

Large diffs are not rendered by default.

51 changes: 50 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "connector_slack"
version = "0.1.0"
version = "1.0.0"
description = "Send messages to Slack through a SpiffWorkflow Service Task"
authors = ["Dan Funk <[email protected]>"]
readme = "README.md"
Expand All @@ -9,10 +9,59 @@ packages = [{include = "connector_slack", from = "src" }]
[tool.poetry.dependencies]
python = "^3.11"
requests = "^2.28.1"
spiffworkflow-connector-command = {git = "https://github.com/sartography/spiffworkflow-connector-command.git", rev = "main"}

[tool.poetry.group.dev.dependencies]
pytest = "^7.2.0"
mypy = "^1.6.0"
ruff = "^0.0.292"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.ruff]
select = [
"B", # flake8-bugbear
"C", # mccabe
"E", # pycodestyle error
# "ERA", # eradicate
"F", # pyflakes
"N", # pep8-naming
"PL", # pylint
"S", # flake8-bandit
"UP", # pyupgrade
"W", # pycodestyle warning
"I001" # isort
]

ignore = [
"C901", # "complexity" category
"PLR", # "refactoring" category has "too many lines in method" type stuff
"PLE1205" # saw this Too many arguments for `logging` format string give a false positive once
]

line-length = 130

# target python 3.10
target-version = "py310"

exclude = [
"migrations"
]

[tool.ruff.per-file-ignores]
"migrations/versions/*.py" = ["E501"]
"tests/**/*.py" = ["PLR2004", "S101"] # PLR2004 is about magic vars, S101 allows assert

[tool.ruff.isort]
force-single-line = true

[tool.mypy]
strict = true
disallow_any_generics = false
warn_unreachable = true
pretty = true
show_column_numbers = true
show_error_codes = true
show_error_context = true
63 changes: 36 additions & 27 deletions src/connector_slack/commands/post_message.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
"""Send message to a slack channel."""
import json
from typing import Any

import requests
import requests # type: ignore
from spiffworkflow_connector_command.command_interface import CommandErrorDict
from spiffworkflow_connector_command.command_interface import CommandResultDictV2
from spiffworkflow_connector_command.command_interface import ConnectorCommand
from spiffworkflow_connector_command.command_interface import ConnectorProxyResponseDict

class PostMessage:

class PostMessage(ConnectorCommand):
"""Send a message to slack."""

SLACK_URL = "https://slack.com/api/chat.postMessage"
Expand All @@ -20,45 +25,49 @@ def __init__(self, token: str, channel: str, message: str):
self.channel = channel
self.message = message

def execute(self, config, task_data):
def execute(self, _config: Any, _task_data: Any) -> CommandResultDictV2:

headers = {"Authorization": f"Bearer {self.token}",
"Content-type": "application/json"}
body = {"channel": self.channel,
"text": self.message
}

command_response = {}
status = 0
error: CommandErrorDict | None = None

try:
response = requests.post(self.SLACK_URL, headers=headers, json=body)
response = requests.post(self.SLACK_URL, headers=headers, json=body, timeout=3000)
if 'application/json' in response.headers.get('Content-Type', ''):
response_json = response.json()
if response_json['ok'] == True:
return {
"response": response.json(),
"status": response.status_code,
"mimetype": "application/json",
}
if response_json['ok'] is True:
command_response = response.json()
status = response.status_code
else:
message = ". ".join(response_json.get('response_metadata',{}).get("messages", []))
if not message:
message = response_json['error']
status_code = response.status_code
if status_code == 200:
status_code = 400 # Don't return a 200 on a failure.
return {
"response": {"error": message},
"status": status_code,
"mimetype": "application/json",
}
status = status_code
error = {"error_code": "SlackMessageFailed","message": message}
else:
return {
"response": {"error": "Unreadable (non JSON) response from Slack"},
"status": response.status_code,
"mimetype": "application/json",
}
except Exception as e:
return {
"response": f'{"error": {e}}',
"status": 500,
"mimetype": "application/json",
}
error = {"error_code": "SlackMessageFailed", "message": "Unreadable (non JSON) response from Slack"}
status = response.status_code
except Exception as exception:
error = {"error_code": exception.__class__.__name__, "message": str(exception)}
status = 500

return_response: ConnectorProxyResponseDict = {
"command_response": command_response,
"error": error,
}
result: CommandResultDictV2 = {
"response": return_response,
"status": status,
"mimetype": "application/json",
}

return result
67 changes: 67 additions & 0 deletions tests/connector_slack/unit/test_post_message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from unittest.mock import MagicMock
from unittest.mock import patch

from connector_slack.commands.post_message import PostMessage


class TestPostMessage:
def test_successful_post(self) -> None:
success_response = {
"ok": True,
"channel": "C123456",
"ts": "1503435956.000247",
"message": {
"text": "Here's a message for you",
"username": "ecto1",
"bot_id": "B123456",
"attachments": [
{
"text": "This is an attachment",
"id": 1,
"fallback": "This is an attachment's fallback"
}
],
"type": "message",
"subtype": "bot_message",
"ts": "1503435956.000247"
}
}
with patch("requests.post") as mock_post:
mock_post.return_value.status_code = 200
mock_post.return_value.headers = {"Content-Type": 'application/json; charset=utf-8'}
mock_post.return_value.json = MagicMock(return_value=success_response)
poster = PostMessage('xxx', 'my_channel', 'hello world!')
response = poster.execute({}, {})
assert response['status'] == 200
assert response['mimetype'] == "application/json"
assert response['response']['command_response'] == success_response


def test_connection_error(self) -> None:
with patch("requests.post") as mock_post:
mock_post.return_value.status_code = 404
poster = PostMessage('xxx', 'my_channel', 'hello world!')
response = poster.execute({}, {})
assert response['status'] == 404
assert response['mimetype'] == "application/json"
assert response["response"]["error"] is not None
assert "error_code" in response["response"]["error"]
assert response["response"]["error"]["error_code"] == "SlackMessageFailed"
assert response["response"]["error"]["message"] == "Unreadable (non JSON) response from Slack"

def test_error_from_slack(self) -> None:
example_error = {'ok': False, 'error': 'invalid_arguments', 'warning': 'missing_charset',
'response_metadata': {'messages': ['[ERROR] missing required field: channel'],
'warnings': ['missing_charset']}}
with patch("requests.post") as mock_post:
mock_post.return_value.status_code = 200
mock_post.return_value.headers = {"Content-Type": 'application/json; charset=utf-8'}
mock_post.return_value.json = MagicMock(return_value=example_error)
poster = PostMessage('xxx', 'my_channel', 'hello world!')
response = poster.execute({}, {})
assert response['status'] == 400
assert response['mimetype'] == "application/json"
assert response["response"]["error"] is not None
assert "error_code" in response["response"]["error"]
assert response["response"]["error"]["error_code"] == "SlackMessageFailed"
assert response["response"]["error"]["message"] == "[ERROR] missing required field: channel"
60 changes: 0 additions & 60 deletions tests/connector_slack/unit/test_slack_connection.py

This file was deleted.

0 comments on commit ed3231b

Please sign in to comment.