diff --git a/python_modules/libraries/dagster-msteams/dagster_msteams/adaptive_card.py b/python_modules/libraries/dagster-msteams/dagster_msteams/adaptive_card.py
index ac1d33c6e3514..1e7bc9db5a5dd 100644
--- a/python_modules/libraries/dagster-msteams/dagster_msteams/adaptive_card.py
+++ b/python_modules/libraries/dagster-msteams/dagster_msteams/adaptive_card.py
@@ -1,13 +1,12 @@
-from typing import TYPE_CHECKING, Any, Optional
+from typing import Any, Optional
-if TYPE_CHECKING:
- from dagster_msteams.client import Link
+from dagster_msteams.utils import Link
class AdaptiveCard:
"""Class to contruct a MS Teams adaptive card for posting Dagster messages."""
- def __init__(self, message: str, link: Optional["Link"] = None):
+ def __init__(self, message: str, link: Optional[Link] = None):
if link:
message += f" [{link.text}]({link.url})"
diff --git a/python_modules/libraries/dagster-msteams/dagster_msteams/card.py b/python_modules/libraries/dagster-msteams/dagster_msteams/card.py
index 54561ef7359ec..e3e4ec4458f2b 100644
--- a/python_modules/libraries/dagster-msteams/dagster_msteams/card.py
+++ b/python_modules/libraries/dagster-msteams/dagster_msteams/card.py
@@ -1,8 +1,7 @@
from collections.abc import Mapping
-from typing import TYPE_CHECKING, Optional
+from typing import Optional
-if TYPE_CHECKING:
- from dagster_msteams.client import Link
+from dagster_msteams.utils import Link
class Card:
@@ -27,7 +26,7 @@ def _create_attachment(self, text_message: str) -> Mapping:
content_type = "application/vnd.microsoft.card.hero"
return {"contentType": content_type, "content": content}
- def add_attachment(self, text_message: str, link: Optional["Link"] = None):
+ def add_attachment(self, text_message: str, link: Optional[Link] = None):
if link:
text_message += f" {link.text}"
hero_card_attachment = self._create_attachment(text_message)
diff --git a/python_modules/libraries/dagster-msteams/dagster_msteams/client.py b/python_modules/libraries/dagster-msteams/dagster_msteams/client.py
index abae5f804034c..4cef4dbba9baf 100644
--- a/python_modules/libraries/dagster-msteams/dagster_msteams/client.py
+++ b/python_modules/libraries/dagster-msteams/dagster_msteams/client.py
@@ -1,5 +1,5 @@
from collections.abc import Mapping
-from typing import NamedTuple, Optional, cast
+from typing import Optional, cast
from urllib.parse import urlparse
import dagster._check as check
@@ -7,11 +7,7 @@
from dagster_msteams.adaptive_card import AdaptiveCard
from dagster_msteams.card import Card
-
-
-class Link(NamedTuple):
- text: str
- url: str
+from dagster_msteams.utils import Link
class TeamsClient:
diff --git a/python_modules/libraries/dagster-msteams/dagster_msteams/hooks.py b/python_modules/libraries/dagster-msteams/dagster_msteams/hooks.py
index 0244ed65dc465..631032daff83f 100644
--- a/python_modules/libraries/dagster-msteams/dagster_msteams/hooks.py
+++ b/python_modules/libraries/dagster-msteams/dagster_msteams/hooks.py
@@ -5,10 +5,8 @@
from dagster._core.execution.context.hook import HookContext
from dagster._utils.warnings import normalize_renamed_param
-from dagster_msteams.card import Card
from dagster_msteams.resources import MSTeamsResource
-
-# TODO test these hooks
+from dagster_msteams.utils import Link
def _default_status_message(context: HookContext, status: str) -> str:
@@ -73,15 +71,16 @@ def my_job():
@failure_hook(required_resource_keys={"msteams"})
def _hook(context: HookContext):
- text = message_fn(context)
- if webserver_base_url:
- text += f"View in Dagster UI"
- card = Card()
- card.add_attachment(text_message=text)
+ message = message_fn(context)
+ link = (
+ Link("View in Dagster UI", f"{webserver_base_url}/runs/{context.run_id}")
+ if webserver_base_url
+ else None
+ )
if isinstance(context.resources.msteams, MSTeamsResource):
- context.resources.msteams.get_client().post_message(payload=card.payload)
+ context.resources.msteams.get_client().post_message(message=message, link=link)
else:
- context.resources.msteams.post_message(payload=card.payload)
+ context.resources.msteams.post_message(message=message, link=link)
return _hook
@@ -133,14 +132,15 @@ def my_job():
@success_hook(required_resource_keys={"msteams"})
def _hook(context: HookContext):
- text = message_fn(context)
- if webserver_base_url:
- text += f"View in webserver"
- card = Card()
- card.add_attachment(text_message=text)
+ message = message_fn(context)
+ link = (
+ Link("View in Dagster UI", f"{webserver_base_url}/runs/{context.run_id}")
+ if webserver_base_url
+ else None
+ )
if isinstance(context.resources.msteams, MSTeamsResource):
- context.resources.msteams.get_client().post_message(payload=card.payload)
+ context.resources.msteams.get_client().post_message(message=message, link=link)
else:
- context.resources.msteams.post_message(payload=card.payload)
+ context.resources.msteams.post_message(message=message, link=link)
return _hook
diff --git a/python_modules/libraries/dagster-msteams/dagster_msteams/utils.py b/python_modules/libraries/dagster-msteams/dagster_msteams/utils.py
new file mode 100644
index 0000000000000..f6a60eb904ec3
--- /dev/null
+++ b/python_modules/libraries/dagster-msteams/dagster_msteams/utils.py
@@ -0,0 +1,6 @@
+from typing import NamedTuple
+
+
+class Link(NamedTuple):
+ text: str
+ url: str
diff --git a/python_modules/libraries/dagster-msteams/dagster_msteams_tests/__init__.py b/python_modules/libraries/dagster-msteams/dagster_msteams_tests/__init__.py
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/python_modules/libraries/dagster-msteams/dagster_msteams_tests/__snapshots__/test_hooks.ambr b/python_modules/libraries/dagster-msteams/dagster_msteams_tests/__snapshots__/test_hooks.ambr
new file mode 100644
index 0000000000000..b66c44b78c704
--- /dev/null
+++ b/python_modules/libraries/dagster-msteams/dagster_msteams_tests/__snapshots__/test_hooks.ambr
@@ -0,0 +1,203 @@
+# serializer version: 1
+# name: test_failure_hook_with_pythonic_resource[https://foo.webhook.office.com/bar/baz]
+ _CallList([
+ _Call(
+ tuple(
+ 'https://foo.webhook.office.com/bar/baz',
+ ),
+ dict({
+ 'headers': dict({
+ 'Content-Type': 'application/json',
+ }),
+ 'json': dict({
+ 'attachments': list([
+ dict({
+ 'content': dict({
+ 'text': "Some custom text View in Dagster UI",
+ 'title': 'Dagster Pipeline Alert',
+ }),
+ 'contentType': 'application/vnd.microsoft.card.hero',
+ }),
+ ]),
+ 'type': 'message',
+ }),
+ 'proxies': None,
+ 'timeout': 60,
+ 'verify': True,
+ }),
+ ),
+ ])
+# ---
+# name: test_failure_hook_with_pythonic_resource[https://foo.westus.logic.azure.com:443/workflows/8be36cde7f394925af220480f6701bd0]
+ _CallList([
+ _Call(
+ tuple(
+ 'https://foo.westus.logic.azure.com:443/workflows/8be36cde7f394925af220480f6701bd0',
+ ),
+ dict({
+ 'headers': dict({
+ 'Content-Type': 'application/json',
+ }),
+ 'json': dict({
+ 'attachments': list([
+ dict({
+ 'content': dict({
+ 'body': list([
+ dict({
+ 'text': 'Some custom text [View in Dagster UI](localhost:3000/runs/d5358b53-37eb-42e2-9c46-97315843c304)',
+ 'type': 'TextBlock',
+ 'wrap': True,
+ }),
+ ]),
+ 'type': 'AdaptiveCard',
+ 'version': '1.0',
+ }),
+ 'contentType': 'application/vnd.microsoft.card.adaptive',
+ 'contentUrl': None,
+ }),
+ ]),
+ 'type': 'message',
+ }),
+ 'proxies': None,
+ 'timeout': 60,
+ 'verify': True,
+ }),
+ ),
+ ])
+# ---
+# name: test_success_hook_with_pythonic_resource[https://foo.webhook.office.com/bar/baz]
+ _CallList([
+ _Call(
+ tuple(
+ 'https://foo.webhook.office.com/bar/baz',
+ ),
+ dict({
+ 'headers': dict({
+ 'Content-Type': 'application/json',
+ }),
+ 'json': dict({
+ 'attachments': list([
+ dict({
+ 'content': dict({
+ 'text': '''
+ Op pass_op on job job_def succeeded!
+ Run ID: 135f8cfd-81ca-4448-8e7f-66b58201391f
+ ''',
+ 'title': 'Dagster Pipeline Alert',
+ }),
+ 'contentType': 'application/vnd.microsoft.card.hero',
+ }),
+ ]),
+ 'type': 'message',
+ }),
+ 'proxies': None,
+ 'timeout': 60,
+ 'verify': True,
+ }),
+ ),
+ _Call(
+ tuple(
+ 'https://foo.webhook.office.com/bar/baz',
+ ),
+ dict({
+ 'headers': dict({
+ 'Content-Type': 'application/json',
+ }),
+ 'json': dict({
+ 'attachments': list([
+ dict({
+ 'content': dict({
+ 'text': '''
+ Op success_solid_with_hook on job job_def succeeded!
+ Run ID: 135f8cfd-81ca-4448-8e7f-66b58201391f
+ ''',
+ 'title': 'Dagster Pipeline Alert',
+ }),
+ 'contentType': 'application/vnd.microsoft.card.hero',
+ }),
+ ]),
+ 'type': 'message',
+ }),
+ 'proxies': None,
+ 'timeout': 60,
+ 'verify': True,
+ }),
+ ),
+ ])
+# ---
+# name: test_success_hook_with_pythonic_resource[https://foo.westus.logic.azure.com:443/workflows/8be36cde7f394925af220480f6701bd0]
+ _CallList([
+ _Call(
+ tuple(
+ 'https://foo.westus.logic.azure.com:443/workflows/8be36cde7f394925af220480f6701bd0',
+ ),
+ dict({
+ 'headers': dict({
+ 'Content-Type': 'application/json',
+ }),
+ 'json': dict({
+ 'attachments': list([
+ dict({
+ 'content': dict({
+ 'body': list([
+ dict({
+ 'text': '''
+ Op pass_op on job job_def succeeded!
+ Run ID: f235ae37-e068-4f90-888d-d59a7cf017db
+ ''',
+ 'type': 'TextBlock',
+ 'wrap': True,
+ }),
+ ]),
+ 'type': 'AdaptiveCard',
+ 'version': '1.0',
+ }),
+ 'contentType': 'application/vnd.microsoft.card.adaptive',
+ 'contentUrl': None,
+ }),
+ ]),
+ 'type': 'message',
+ }),
+ 'proxies': None,
+ 'timeout': 60,
+ 'verify': True,
+ }),
+ ),
+ _Call(
+ tuple(
+ 'https://foo.westus.logic.azure.com:443/workflows/8be36cde7f394925af220480f6701bd0',
+ ),
+ dict({
+ 'headers': dict({
+ 'Content-Type': 'application/json',
+ }),
+ 'json': dict({
+ 'attachments': list([
+ dict({
+ 'content': dict({
+ 'body': list([
+ dict({
+ 'text': '''
+ Op success_solid_with_hook on job job_def succeeded!
+ Run ID: f235ae37-e068-4f90-888d-d59a7cf017db
+ ''',
+ 'type': 'TextBlock',
+ 'wrap': True,
+ }),
+ ]),
+ 'type': 'AdaptiveCard',
+ 'version': '1.0',
+ }),
+ 'contentType': 'application/vnd.microsoft.card.adaptive',
+ 'contentUrl': None,
+ }),
+ ]),
+ 'type': 'message',
+ }),
+ 'proxies': None,
+ 'timeout': 60,
+ 'verify': True,
+ }),
+ ),
+ ])
+# ---
diff --git a/python_modules/libraries/dagster-msteams/dagster_msteams_tests/conftest.py b/python_modules/libraries/dagster-msteams/dagster_msteams_tests/conftest.py
index d8d5cb9113991..1af740cb994e0 100644
--- a/python_modules/libraries/dagster-msteams/dagster_msteams_tests/conftest.py
+++ b/python_modules/libraries/dagster-msteams/dagster_msteams_tests/conftest.py
@@ -1,3 +1,5 @@
+from unittest.mock import patch
+
import pytest
from dagster_msteams.client import TeamsClient
@@ -23,3 +25,17 @@ def json_message():
def teams_client():
client = TeamsClient(hook_url="https://some_url_here/")
return client
+
+
+@pytest.fixture(scope="function", name="mock_post_method")
+def create_mock_post_method():
+ with patch("dagster_msteams.client.post") as mock_post:
+ mock_response = mock_post.return_value
+ mock_response.status_code = 200
+ mock_response.json.return_value = {"message": "Success"}
+ mock_response.text = "1"
+ yield mock_post
+
+
+LEGACY_WEBHOOK_URL = "https://foo.webhook.office.com/bar/baz"
+WEBHOOK_URL = "https://foo.westus.logic.azure.com:443/workflows/8be36cde7f394925af220480f6701bd0"
diff --git a/python_modules/libraries/dagster-msteams/dagster_msteams_tests/test_client.py b/python_modules/libraries/dagster-msteams/dagster_msteams_tests/test_client.py
index c308d6c16603a..3eabbb092b8b8 100644
--- a/python_modules/libraries/dagster-msteams/dagster_msteams_tests/test_client.py
+++ b/python_modules/libraries/dagster-msteams/dagster_msteams_tests/test_client.py
@@ -1,35 +1,9 @@
-import json
from typing import Any
-from unittest.mock import patch
import pytest
from dagster_msteams.client import Link, TeamsClient
-
-@pytest.fixture(scope="function", name="mock_post_method")
-def create_mock_post_method():
- with patch("dagster_msteams.client.post") as mock_post:
- mock_response = mock_post.return_value
- mock_response.status_code = 200
- mock_response.json.return_value = {"message": "Success"}
- mock_response.text = "1"
- yield mock_post
-
-
-@patch("dagster_msteams.client.TeamsClient.post_message")
-def test_post_message(mock_teams_post_message, json_message, teams_client):
- body = {"ok": True}
- mock_teams_post_message.return_value = {
- "status": 200,
- "body": json.dumps(body),
- "headers": "",
- }
- teams_client.post_message(json_message)
- assert mock_teams_post_message.called
-
-
-LEGACY_WEBHOOK_URL = "https://foo.webhook.office.com/bar/baz"
-WEBHOOK_URL = "https://foo.westus.logic.azure.com:443/workflows/8be36cde7f394925af220480f6701bd0"
+from dagster_msteams_tests.conftest import LEGACY_WEBHOOK_URL, WEBHOOK_URL
@pytest.mark.parametrize(
diff --git a/python_modules/libraries/dagster-msteams/dagster_msteams_tests/test_hooks.py b/python_modules/libraries/dagster-msteams/dagster_msteams_tests/test_hooks.py
index 210026cf1e00b..6c730527de3fe 100644
--- a/python_modules/libraries/dagster-msteams/dagster_msteams_tests/test_hooks.py
+++ b/python_modules/libraries/dagster-msteams/dagster_msteams_tests/test_hooks.py
@@ -1,11 +1,15 @@
+from typing import Any
from unittest.mock import patch
+import pytest
from dagster import op
from dagster._core.definitions.decorators.job_decorator import job
from dagster_msteams import MSTeamsResource
from dagster_msteams.hooks import teams_on_failure, teams_on_success
from dagster_msteams.resources import msteams_resource
+from dagster_msteams_tests.conftest import LEGACY_WEBHOOK_URL, WEBHOOK_URL
+
class SomeUserException(Exception):
pass
@@ -25,9 +29,15 @@ def fail_op(_):
raise SomeUserException()
-@patch("dagster_msteams.client.TeamsClient.post_message")
-def test_failure_hook_with_pythonic_resource(mock_teams_post_message):
- @job(resource_defs={"msteams": MSTeamsResource(hook_url="https://some_url_here/")})
+@pytest.mark.parametrize(
+ "webhook_url",
+ [
+ LEGACY_WEBHOOK_URL,
+ WEBHOOK_URL,
+ ],
+)
+def test_failure_hook_with_pythonic_resource(webhook_url: str, snapshot: Any, mock_post_method):
+ @job(resource_defs={"msteams": MSTeamsResource(hook_url=webhook_url)})
def job_def():
pass_op.with_hooks(hook_defs={teams_on_failure()})()
pass_op.alias("fail_op_with_hook").with_hooks(hook_defs={teams_on_failure()})()
@@ -42,12 +52,19 @@ def job_def():
raise_on_error=False,
)
assert not result.success
- assert mock_teams_post_message.call_count == 1
-
-
-@patch("dagster_msteams.client.TeamsClient.post_message")
-def test_success_hook_with_pythonic_resource(mock_teams_post_message):
- @job(resource_defs={"msteams": MSTeamsResource(hook_url="https://some_url_here/")})
+ assert mock_post_method.call_count == 1
+ snapshot.assert_match(mock_post_method.call_args_list)
+
+
+@pytest.mark.parametrize(
+ "webhook_url",
+ [
+ LEGACY_WEBHOOK_URL,
+ WEBHOOK_URL,
+ ],
+)
+def test_success_hook_with_pythonic_resource(webhook_url: str, snapshot: Any, mock_post_method):
+ @job(resource_defs={"msteams": MSTeamsResource(hook_url=webhook_url)})
def job_def():
pass_op.with_hooks(hook_defs={teams_on_success()})()
pass_op.alias("success_solid_with_hook").with_hooks(hook_defs={teams_on_success()})()
@@ -62,7 +79,8 @@ def job_def():
raise_on_error=False,
)
assert not result.success
- assert mock_teams_post_message.call_count == 2
+ assert mock_post_method.call_count == 2
+ snapshot.assert_match(mock_post_method.call_args_list)
@patch("dagster_msteams.client.TeamsClient.post_message")