django-slack-tools 0.2.2__py3-none-any.whl → 0.3.0__py3-none-any.whl
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.
- django_slack_tools/__init__.py +0 -1
- django_slack_tools/app_settings.py +54 -90
- django_slack_tools/locale/ko_KR/LC_MESSAGES/django.po +150 -186
- django_slack_tools/py.typed +0 -0
- django_slack_tools/slack_messages/admin/message.py +1 -52
- django_slack_tools/slack_messages/admin/message_recipient.py +2 -2
- django_slack_tools/slack_messages/admin/messaging_policy.py +5 -5
- django_slack_tools/slack_messages/backends/__init__.py +3 -3
- django_slack_tools/slack_messages/backends/base.py +39 -203
- django_slack_tools/slack_messages/backends/dummy.py +0 -12
- django_slack_tools/slack_messages/backends/{logging.py → logging_.py} +0 -6
- django_slack_tools/slack_messages/backends/slack.py +21 -75
- django_slack_tools/slack_messages/message_templates/__init__.py +5 -0
- django_slack_tools/slack_messages/message_templates/base.py +17 -0
- django_slack_tools/{utils/template → slack_messages/message_templates}/django.py +30 -25
- django_slack_tools/slack_messages/message_templates/python.py +38 -0
- django_slack_tools/slack_messages/messenger.py +158 -0
- django_slack_tools/slack_messages/middlewares/__init__.py +4 -0
- django_slack_tools/slack_messages/middlewares/base.py +34 -0
- django_slack_tools/slack_messages/middlewares/django.py +218 -0
- django_slack_tools/slack_messages/migrations/0001_initial.py +4 -4
- django_slack_tools/slack_messages/migrations/0005_alter_slackmessagingpolicy_template_type.py +46 -0
- django_slack_tools/slack_messages/migrations/0006_alter_slackmessage_id.py +26 -0
- django_slack_tools/slack_messages/models/mention.py +1 -1
- django_slack_tools/slack_messages/models/message.py +5 -2
- django_slack_tools/slack_messages/models/message_recipient.py +1 -1
- django_slack_tools/slack_messages/models/messaging_policy.py +4 -4
- django_slack_tools/slack_messages/request.py +91 -0
- django_slack_tools/slack_messages/response.py +21 -0
- django_slack_tools/slack_messages/shortcuts.py +78 -0
- django_slack_tools/slack_messages/tasks.py +16 -49
- django_slack_tools/slack_messages/template_loaders/__init__.py +11 -0
- django_slack_tools/slack_messages/template_loaders/base.py +16 -0
- django_slack_tools/slack_messages/template_loaders/django.py +74 -0
- django_slack_tools/slack_messages/template_loaders/errors.py +9 -0
- django_slack_tools/{utils/slack/django.py → slack_messages/validators.py} +1 -1
- django_slack_tools/utils/django/__init__.py +0 -0
- django_slack_tools/utils/import_helper.py +37 -0
- django_slack_tools/utils/repr.py +10 -0
- django_slack_tools/utils/slack/__init__.py +1 -9
- {django_slack_tools-0.2.2.dist-info → django_slack_tools-0.3.0.dist-info}/METADATA +12 -60
- django_slack_tools-0.3.0.dist-info/RECORD +58 -0
- {django_slack_tools-0.2.2.dist-info → django_slack_tools-0.3.0.dist-info}/WHEEL +1 -1
- django_slack_tools/slack_messages/message.py +0 -110
- django_slack_tools/utils/slack/message.py +0 -93
- django_slack_tools/utils/template/__init__.py +0 -5
- django_slack_tools/utils/template/base.py +0 -17
- django_slack_tools/utils/template/dict.py +0 -52
- django_slack_tools-0.2.2.dist-info/RECORD +0 -43
- /django_slack_tools/utils/{model_mixins.py → django/model_mixins.py} +0 -0
- /django_slack_tools/utils/{widgets.py → django/widgets.py} +0 -0
- {django_slack_tools-0.2.2.dist-info → django_slack_tools-0.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,13 +6,13 @@ from typing import TYPE_CHECKING, cast
|
|
|
6
6
|
from django.contrib import admin, messages
|
|
7
7
|
from django.contrib.admin.filters import DateFieldListFilter
|
|
8
8
|
from django.db.models import Count
|
|
9
|
-
from django.db.models.query import QuerySet
|
|
10
9
|
from django.utils.translation import gettext_lazy as _
|
|
11
10
|
|
|
12
11
|
from django_slack_tools.app_settings import app_settings
|
|
13
12
|
from django_slack_tools.slack_messages.models import SlackMessageRecipient
|
|
14
13
|
|
|
15
14
|
if TYPE_CHECKING:
|
|
15
|
+
from django.db.models.query import QuerySet
|
|
16
16
|
from django.http import HttpRequest
|
|
17
17
|
|
|
18
18
|
class SlackMessageRecipientWithAnnotates(SlackMessageRecipient): # noqa: D101
|
|
@@ -25,7 +25,7 @@ class SlackMessageRecipientAdmin(admin.ModelAdmin):
|
|
|
25
25
|
|
|
26
26
|
def get_queryset(self, request: HttpRequest) -> QuerySet[SlackMessageRecipientWithAnnotates]: # noqa: D102
|
|
27
27
|
return cast(
|
|
28
|
-
QuerySet[
|
|
28
|
+
"QuerySet[SlackMessageRecipientWithAnnotates]", # Unsafe force type casting
|
|
29
29
|
super()
|
|
30
30
|
.get_queryset(request)
|
|
31
31
|
.annotate(
|
|
@@ -7,15 +7,15 @@ from django.contrib import admin
|
|
|
7
7
|
from django.contrib.admin.filters import DateFieldListFilter
|
|
8
8
|
from django.db import models
|
|
9
9
|
from django.db.models import Count
|
|
10
|
-
from django.db.models.query import QuerySet
|
|
11
10
|
from django.utils.html import format_html
|
|
12
11
|
from django.utils.translation import gettext_lazy as _
|
|
13
12
|
|
|
14
13
|
from django_slack_tools.slack_messages.models import SlackMessagingPolicy
|
|
14
|
+
from django_slack_tools.utils.django.widgets import JSONWidget
|
|
15
15
|
from django_slack_tools.utils.slack import get_block_kit_builder_url
|
|
16
|
-
from django_slack_tools.utils.widgets import JSONWidget
|
|
17
16
|
|
|
18
17
|
if TYPE_CHECKING:
|
|
18
|
+
from django.db.models.query import QuerySet
|
|
19
19
|
from django.http import HttpRequest
|
|
20
20
|
from django_stubs_ext import StrOrPromise
|
|
21
21
|
|
|
@@ -35,7 +35,7 @@ class SlackMessagingPolicyAdmin(admin.ModelAdmin):
|
|
|
35
35
|
|
|
36
36
|
def get_queryset(self, request: HttpRequest) -> QuerySet[SlackMessagingPolicyWithAnnotates]: # noqa: D102
|
|
37
37
|
return cast(
|
|
38
|
-
QuerySet[
|
|
38
|
+
"QuerySet[SlackMessagingPolicyWithAnnotates]", # Unsafe force type casting
|
|
39
39
|
super()
|
|
40
40
|
.get_queryset(request)
|
|
41
41
|
.annotate(
|
|
@@ -90,7 +90,7 @@ class SlackMessagingPolicyAdmin(admin.ModelAdmin):
|
|
|
90
90
|
# ------------------------------------------------------------------------
|
|
91
91
|
date_hierarchy = "last_modified"
|
|
92
92
|
search_fields = ("code",)
|
|
93
|
-
list_display = ("id", "code", "enabled", "_count_recipients", "created", "last_modified")
|
|
93
|
+
list_display = ("id", "code", "enabled", "_count_recipients", "template_type", "created", "last_modified")
|
|
94
94
|
list_display_links = ("id", "code")
|
|
95
95
|
list_filter = (
|
|
96
96
|
"enabled",
|
|
@@ -107,7 +107,7 @@ class SlackMessagingPolicyAdmin(admin.ModelAdmin):
|
|
|
107
107
|
(
|
|
108
108
|
None,
|
|
109
109
|
{
|
|
110
|
-
"fields": ("code", "enabled", "recipients", "header_defaults", "template"),
|
|
110
|
+
"fields": ("code", "enabled", "recipients", "header_defaults", "template_type", "template"),
|
|
111
111
|
},
|
|
112
112
|
),
|
|
113
113
|
(
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from .base import BaseBackend
|
|
2
2
|
from .dummy import DummyBackend
|
|
3
|
-
from .
|
|
3
|
+
from .logging_ import LoggingBackend
|
|
4
4
|
from .slack import SlackBackend, SlackRedirectBackend
|
|
5
5
|
|
|
6
6
|
__all__ = (
|
|
7
7
|
"BaseBackend",
|
|
8
|
-
"SlackBackend",
|
|
9
|
-
"SlackRedirectBackend",
|
|
10
8
|
"DummyBackend",
|
|
11
9
|
"LoggingBackend",
|
|
10
|
+
"SlackBackend",
|
|
11
|
+
"SlackRedirectBackend",
|
|
12
12
|
)
|
|
@@ -2,228 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import dataclasses
|
|
6
5
|
import traceback
|
|
7
6
|
from abc import ABC, abstractmethod
|
|
8
7
|
from logging import getLogger
|
|
9
|
-
from typing import TYPE_CHECKING, Any, cast
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Optional, cast
|
|
10
9
|
|
|
11
10
|
from slack_sdk.errors import SlackApiError
|
|
12
11
|
|
|
13
|
-
from django_slack_tools.slack_messages.
|
|
14
|
-
from django_slack_tools.utils.slack import MessageBody
|
|
15
|
-
from django_slack_tools.utils.template import DictTemplate, DjangoTemplate
|
|
12
|
+
from django_slack_tools.slack_messages.response import MessageResponse
|
|
16
13
|
|
|
17
14
|
if TYPE_CHECKING:
|
|
18
15
|
from slack_sdk.web import SlackResponse
|
|
19
16
|
|
|
20
|
-
from django_slack_tools.slack_messages.
|
|
21
|
-
from django_slack_tools.slack_messages.models.message_recipient import SlackMessageRecipient
|
|
22
|
-
from django_slack_tools.utils.slack import MessageHeader
|
|
23
|
-
from django_slack_tools.utils.template import BaseTemplate
|
|
17
|
+
from django_slack_tools.slack_messages.request import MessageBody, MessageHeader, MessageRequest
|
|
24
18
|
|
|
25
|
-
logger = getLogger(__name__)
|
|
26
19
|
|
|
27
|
-
|
|
28
|
-
"""Set of reserved context keys automatically created."""
|
|
20
|
+
logger = getLogger(__name__)
|
|
29
21
|
|
|
30
22
|
|
|
31
23
|
class BaseBackend(ABC):
|
|
32
24
|
"""Abstract base class for messaging backends."""
|
|
33
25
|
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
header: MessageHeader,
|
|
40
|
-
body: MessageBody,
|
|
41
|
-
) -> SlackMessage:
|
|
42
|
-
"""Prepare message.
|
|
43
|
-
|
|
44
|
-
Args:
|
|
45
|
-
policy: Related policy instance.
|
|
46
|
-
channel: Channel to send message.
|
|
47
|
-
header: Slack message control header.
|
|
48
|
-
body: Slack message body.
|
|
49
|
-
|
|
50
|
-
Returns:
|
|
51
|
-
Prepared message.
|
|
52
|
-
"""
|
|
53
|
-
_header: dict = policy.header_defaults if policy else {}
|
|
54
|
-
_header.update(dataclasses.asdict(header))
|
|
55
|
-
|
|
56
|
-
_body = dataclasses.asdict(body)
|
|
57
|
-
|
|
58
|
-
return SlackMessage(policy=policy, channel=channel, header=_header, body=_body)
|
|
59
|
-
|
|
60
|
-
def prepare_messages_from_policy(
|
|
61
|
-
self,
|
|
62
|
-
policy: SlackMessagingPolicy,
|
|
63
|
-
*,
|
|
64
|
-
header: MessageHeader,
|
|
65
|
-
context: dict[str, Any],
|
|
66
|
-
) -> list[SlackMessage]:
|
|
67
|
-
"""Prepare messages from policy.
|
|
68
|
-
|
|
69
|
-
Args:
|
|
70
|
-
policy: Policy to create messages from.
|
|
71
|
-
header: Common message header.
|
|
72
|
-
context: Message context.
|
|
73
|
-
|
|
74
|
-
Returns:
|
|
75
|
-
Prepared messages.
|
|
76
|
-
"""
|
|
77
|
-
overridden_reserved = RESERVED_CONTEXT_KWARGS & set(context.keys())
|
|
78
|
-
if overridden_reserved:
|
|
79
|
-
logger.warning(
|
|
80
|
-
"Template keyword argument(s) %s reserved for passing mentions, but already exists."
|
|
81
|
-
" User provided value will override it.",
|
|
82
|
-
", ".join(f"`{s}`" for s in overridden_reserved),
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
messages: list[SlackMessage] = []
|
|
86
|
-
for recipient in policy.recipients.all():
|
|
87
|
-
logger.debug("Sending message to recipient %s", recipient)
|
|
88
|
-
|
|
89
|
-
# Initialize template instance
|
|
90
|
-
template = self._get_template_instance_from_policy(policy)
|
|
91
|
-
|
|
92
|
-
# Prepare rendering arguments
|
|
93
|
-
render_context = self._get_default_context(policy=policy, recipient=recipient)
|
|
94
|
-
render_context.update(context)
|
|
95
|
-
logger.debug("Context prepared as: %r", render_context)
|
|
96
|
-
|
|
97
|
-
# Render template and parse as body
|
|
98
|
-
rendered = template.render(context=render_context)
|
|
99
|
-
body = MessageBody.from_any(rendered)
|
|
100
|
-
|
|
101
|
-
# Create message instance
|
|
102
|
-
message = self.prepare_message(policy=policy, channel=recipient.channel, header=header, body=body)
|
|
103
|
-
messages.append(message)
|
|
104
|
-
|
|
105
|
-
return SlackMessage.objects.bulk_create(messages)
|
|
106
|
-
|
|
107
|
-
def _get_template_instance_from_policy(self, policy: SlackMessagingPolicy) -> BaseTemplate:
|
|
108
|
-
"""Get template instance."""
|
|
109
|
-
if policy.template_type == SlackMessagingPolicy.TemplateType.DICT:
|
|
110
|
-
return DictTemplate(policy.template)
|
|
111
|
-
|
|
112
|
-
if policy.template_type == SlackMessagingPolicy.TemplateType.DJANGO:
|
|
113
|
-
return DjangoTemplate(file=policy.template)
|
|
114
|
-
|
|
115
|
-
if policy.template_type == SlackMessagingPolicy.TemplateType.DJANGO_INLINE:
|
|
116
|
-
return DjangoTemplate(inline=policy.template)
|
|
117
|
-
|
|
118
|
-
msg = f"Unsupported template type: {policy.template_type!r}"
|
|
119
|
-
raise ValueError(msg)
|
|
120
|
-
|
|
121
|
-
def _get_default_context(self, *, policy: SlackMessagingPolicy, recipient: SlackMessageRecipient) -> dict[str, Any]:
|
|
122
|
-
"""Get default context for rendering.
|
|
123
|
-
|
|
124
|
-
Following default context keys are created:
|
|
26
|
+
def deliver(self, request: MessageRequest) -> MessageResponse:
|
|
27
|
+
"""Deliver message request."""
|
|
28
|
+
if request.body is None:
|
|
29
|
+
msg = "Message body is required."
|
|
30
|
+
raise ValueError(msg)
|
|
125
31
|
|
|
126
|
-
- `policy`: Policy code.
|
|
127
|
-
- `mentions`: List of mentions.
|
|
128
|
-
- `mentions_as_str`: Comma-separated joined string of mentions.
|
|
129
|
-
"""
|
|
130
|
-
mentions: list[SlackMention] = list(recipient.mentions.all())
|
|
131
|
-
mentions_as_str = ", ".join(mention.mention for mention in mentions)
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
"policy": policy.code,
|
|
135
|
-
"mentions": mentions,
|
|
136
|
-
"mentions_as_str": mentions_as_str,
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
def send_messages(self, *messages: SlackMessage, raise_exception: bool, get_permalink: bool) -> int:
|
|
140
|
-
"""Shortcut to send multiple messages.
|
|
141
|
-
|
|
142
|
-
Args:
|
|
143
|
-
messages: Messages to send.
|
|
144
|
-
raise_exception: Whether to propagate exceptions.
|
|
145
|
-
get_permalink: Try to get the message permalink via additional Slack API call.
|
|
146
|
-
|
|
147
|
-
Returns:
|
|
148
|
-
Count of messages sent successfully.
|
|
149
|
-
"""
|
|
150
|
-
num_sent = 0
|
|
151
|
-
for message in messages:
|
|
152
|
-
sent = self.send_message(message=message, raise_exception=raise_exception, get_permalink=get_permalink)
|
|
153
|
-
num_sent += 1 if sent.ok else 0
|
|
154
|
-
|
|
155
|
-
return num_sent
|
|
156
|
-
|
|
157
|
-
def send_message(
|
|
158
|
-
self,
|
|
159
|
-
message: SlackMessage,
|
|
160
|
-
*,
|
|
161
|
-
raise_exception: bool,
|
|
162
|
-
get_permalink: bool,
|
|
163
|
-
) -> SlackMessage:
|
|
164
|
-
"""Send message.
|
|
165
|
-
|
|
166
|
-
Args:
|
|
167
|
-
message: Prepared message.
|
|
168
|
-
raise_exception: Whether to propagate exceptions.
|
|
169
|
-
get_permalink: Try to get the message permalink via additional Slack API call.
|
|
170
|
-
|
|
171
|
-
Returns:
|
|
172
|
-
Message sent to Slack.
|
|
173
|
-
"""
|
|
174
32
|
try:
|
|
175
|
-
response
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if raise_exception:
|
|
205
|
-
raise
|
|
206
|
-
|
|
207
|
-
logger.exception("Error occurred while sending message but suppressed because `raise_exception` set.")
|
|
208
|
-
message.exception = traceback.format_exc()
|
|
209
|
-
finally:
|
|
210
|
-
message.save()
|
|
211
|
-
|
|
212
|
-
message.refresh_from_db()
|
|
213
|
-
return message
|
|
33
|
+
response = self._send_message(
|
|
34
|
+
channel=request.channel,
|
|
35
|
+
header=request.header,
|
|
36
|
+
body=request.body,
|
|
37
|
+
)
|
|
38
|
+
error = None
|
|
39
|
+
except SlackApiError as err:
|
|
40
|
+
response = err.response
|
|
41
|
+
error = traceback.format_exc()
|
|
42
|
+
|
|
43
|
+
ok = cast("bool", response.get("ok"))
|
|
44
|
+
data: Any
|
|
45
|
+
if ok:
|
|
46
|
+
ts = cast("Optional[str]", response.get("ts", None))
|
|
47
|
+
data = response.get("message", {})
|
|
48
|
+
parent_ts = data.get("thread_ts", None)
|
|
49
|
+
else:
|
|
50
|
+
ts = None
|
|
51
|
+
data = response.data
|
|
52
|
+
parent_ts = None
|
|
53
|
+
|
|
54
|
+
return MessageResponse(
|
|
55
|
+
request=request,
|
|
56
|
+
ok=ok,
|
|
57
|
+
error=error,
|
|
58
|
+
data=data,
|
|
59
|
+
ts=ts,
|
|
60
|
+
parent_ts=parent_ts,
|
|
61
|
+
)
|
|
214
62
|
|
|
215
63
|
@abstractmethod
|
|
216
|
-
def _send_message(self,
|
|
64
|
+
def _send_message(self, *, channel: str, header: MessageHeader, body: MessageBody) -> SlackResponse:
|
|
217
65
|
"""Internal implementation of actual 'send message' behavior."""
|
|
218
|
-
|
|
219
|
-
@abstractmethod
|
|
220
|
-
def _get_permalink(self, *, message: SlackMessage, raise_exception: bool) -> str:
|
|
221
|
-
"""Get a permalink for given message identifier."""
|
|
222
|
-
|
|
223
|
-
@abstractmethod
|
|
224
|
-
def _record_request(self, response: SlackResponse) -> Any:
|
|
225
|
-
"""Extract request data to be recorded. Should return JSON-serializable object."""
|
|
226
|
-
|
|
227
|
-
@abstractmethod
|
|
228
|
-
def _record_response(self, response: SlackResponse) -> Any:
|
|
229
|
-
"""Extract response data to be recorded. Should return JSON-serializable object."""
|
|
@@ -7,8 +7,6 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
from slack_sdk.web import SlackResponse
|
|
9
9
|
|
|
10
|
-
from django_slack_tools.slack_messages.models import SlackMessage
|
|
11
|
-
|
|
12
10
|
from .base import BaseBackend
|
|
13
11
|
|
|
14
12
|
logger = getLogger(__name__)
|
|
@@ -17,9 +15,6 @@ logger = getLogger(__name__)
|
|
|
17
15
|
class DummyBackend(BaseBackend):
|
|
18
16
|
"""An dummy backend that does nothing with message."""
|
|
19
17
|
|
|
20
|
-
def prepare_message(self, *args: Any, **kwargs: Any) -> SlackMessage: # noqa: D102, ARG002
|
|
21
|
-
return SlackMessage(header={}, body={})
|
|
22
|
-
|
|
23
18
|
def _send_message(self, *args: Any, **kwargs: Any) -> SlackResponse: # noqa: ARG002
|
|
24
19
|
return SlackResponse(
|
|
25
20
|
client=None,
|
|
@@ -30,10 +25,3 @@ class DummyBackend(BaseBackend):
|
|
|
30
25
|
headers={},
|
|
31
26
|
status_code=200,
|
|
32
27
|
)
|
|
33
|
-
|
|
34
|
-
def _get_permalink(self, *, message: SlackMessage, raise_exception: bool) -> str: # noqa: ARG002
|
|
35
|
-
return ""
|
|
36
|
-
|
|
37
|
-
def _record_request(self, *args: Any, **kwargs: Any) -> Any: ...
|
|
38
|
-
|
|
39
|
-
def _record_response(self, *args: Any, **kwargs: Any) -> Any: ...
|
|
@@ -20,9 +20,3 @@ class LoggingBackend(DummyBackend):
|
|
|
20
20
|
def _send_message(self, *args: Any, **kwargs: Any) -> SlackResponse:
|
|
21
21
|
logger.debug("Sending an message with following args=%r, kwargs=%r", args, kwargs)
|
|
22
22
|
return super()._send_message(*args, **kwargs)
|
|
23
|
-
|
|
24
|
-
def _record_request(self, *args: Any, **kwargs: Any) -> Any:
|
|
25
|
-
logger.debug("Recording request with args=%r, kwargs=%r", args, kwargs)
|
|
26
|
-
|
|
27
|
-
def _record_response(self, *args: Any, **kwargs: Any) -> Any:
|
|
28
|
-
logger.debug("Recording response with args=%r, kwargs=%r", args, kwargs)
|
|
@@ -3,21 +3,18 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from logging import getLogger
|
|
6
|
-
from typing import TYPE_CHECKING, Any
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
7
|
|
|
8
|
-
from django.core.exceptions import ImproperlyConfigured
|
|
9
8
|
from django.utils.module_loading import import_string
|
|
10
9
|
from django.utils.translation import gettext_lazy as _
|
|
11
10
|
from slack_bolt import App
|
|
12
|
-
from slack_sdk.errors import SlackApiError
|
|
13
11
|
|
|
14
12
|
from .base import BaseBackend
|
|
15
13
|
|
|
16
14
|
if TYPE_CHECKING:
|
|
17
15
|
from slack_sdk.web import SlackResponse
|
|
18
16
|
|
|
19
|
-
from django_slack_tools.slack_messages.
|
|
20
|
-
from django_slack_tools.utils.slack import MessageBody
|
|
17
|
+
from django_slack_tools.slack_messages.request import MessageBody, MessageHeader
|
|
21
18
|
|
|
22
19
|
|
|
23
20
|
logger = getLogger(__name__)
|
|
@@ -26,11 +23,7 @@ logger = getLogger(__name__)
|
|
|
26
23
|
class SlackBackend(BaseBackend):
|
|
27
24
|
"""Backend actually sending the messages."""
|
|
28
25
|
|
|
29
|
-
def __init__(
|
|
30
|
-
self,
|
|
31
|
-
*,
|
|
32
|
-
slack_app: App | Callable[[], App] | str,
|
|
33
|
-
) -> None:
|
|
26
|
+
def __init__(self, *, slack_app: App | str) -> None:
|
|
34
27
|
"""Initialize backend.
|
|
35
28
|
|
|
36
29
|
Args:
|
|
@@ -39,57 +32,18 @@ class SlackBackend(BaseBackend):
|
|
|
39
32
|
if isinstance(slack_app, str):
|
|
40
33
|
slack_app = import_string(slack_app)
|
|
41
34
|
|
|
42
|
-
if callable(slack_app):
|
|
43
|
-
slack_app = slack_app()
|
|
44
|
-
|
|
45
35
|
if not isinstance(slack_app, App):
|
|
46
|
-
msg = "
|
|
47
|
-
raise
|
|
36
|
+
msg = f"Expected {App!s} instance, got {type(slack_app)}"
|
|
37
|
+
raise TypeError(msg)
|
|
48
38
|
|
|
49
39
|
self._slack_app = slack_app
|
|
50
40
|
|
|
51
|
-
def _send_message(self,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"""Get a permalink for given message identifier."""
|
|
58
|
-
if not message.ts:
|
|
59
|
-
msg = "Message timestamp is not set, can't retrieve permalink."
|
|
60
|
-
raise ValueError(msg)
|
|
61
|
-
|
|
62
|
-
try:
|
|
63
|
-
_permalink_resp = self._slack_app.client.chat_getPermalink(
|
|
64
|
-
channel=message.channel,
|
|
65
|
-
message_ts=message.ts,
|
|
66
|
-
)
|
|
67
|
-
except SlackApiError:
|
|
68
|
-
if raise_exception:
|
|
69
|
-
raise
|
|
70
|
-
|
|
71
|
-
logger.exception(
|
|
72
|
-
"Error occurred while sending retrieving message's permalink,"
|
|
73
|
-
" but ignored as `raise_exception` not set.",
|
|
74
|
-
)
|
|
75
|
-
return ""
|
|
76
|
-
|
|
77
|
-
return _permalink_resp.get("permalink", default="")
|
|
78
|
-
|
|
79
|
-
def _record_request(self, response: SlackResponse) -> dict[str, Any]:
|
|
80
|
-
# Remove auth header (token) from request before recording
|
|
81
|
-
response.req_args.get("headers", {}).pop("Authorization", None)
|
|
82
|
-
|
|
83
|
-
return response.req_args
|
|
84
|
-
|
|
85
|
-
def _record_response(self, response: SlackResponse) -> dict[str, Any]:
|
|
86
|
-
return {
|
|
87
|
-
"http_verb": response.http_verb,
|
|
88
|
-
"api_url": response.api_url,
|
|
89
|
-
"status_code": response.status_code,
|
|
90
|
-
"headers": response.headers,
|
|
91
|
-
"data": response.data,
|
|
92
|
-
}
|
|
41
|
+
def _send_message(self, *, channel: str, header: MessageHeader, body: MessageBody) -> SlackResponse:
|
|
42
|
+
return self._slack_app.client.chat_postMessage(
|
|
43
|
+
channel=channel,
|
|
44
|
+
**header.model_dump(),
|
|
45
|
+
**body.model_dump(),
|
|
46
|
+
)
|
|
93
47
|
|
|
94
48
|
|
|
95
49
|
class SlackRedirectBackend(SlackBackend):
|
|
@@ -109,27 +63,19 @@ class SlackRedirectBackend(SlackBackend):
|
|
|
109
63
|
|
|
110
64
|
super().__init__(slack_app=slack_app)
|
|
111
65
|
|
|
112
|
-
def
|
|
113
|
-
"""Prepare message to send, with modified for redirection.
|
|
114
|
-
|
|
115
|
-
Args:
|
|
116
|
-
args: Positional arguments to pass to super method.
|
|
117
|
-
channel: Original channel to send message.
|
|
118
|
-
body: Message content.
|
|
119
|
-
kwargs: Keyword arguments to pass to super method.
|
|
120
|
-
|
|
121
|
-
Returns:
|
|
122
|
-
Prepared message instance.
|
|
123
|
-
"""
|
|
124
|
-
# Modify channel to force messages always sent to specific channel
|
|
125
|
-
# Add an attachment that informing message has been redirected
|
|
66
|
+
def _send_message(self, *, channel: str, header: MessageHeader, body: MessageBody) -> SlackResponse:
|
|
126
67
|
if self.inform_redirect:
|
|
127
|
-
body.attachments
|
|
68
|
+
attachments = body.attachments or []
|
|
69
|
+
attachments.append(
|
|
128
70
|
self._make_inform_attachment(original_channel=channel),
|
|
129
|
-
|
|
130
|
-
|
|
71
|
+
)
|
|
72
|
+
body.attachments = attachments
|
|
131
73
|
|
|
132
|
-
return
|
|
74
|
+
return self._slack_app.client.chat_postMessage(
|
|
75
|
+
channel=self.redirect_channel,
|
|
76
|
+
**header.model_dump(),
|
|
77
|
+
**body.model_dump(),
|
|
78
|
+
)
|
|
133
79
|
|
|
134
80
|
def _make_inform_attachment(self, *, original_channel: str) -> dict[str, Any]:
|
|
135
81
|
msg_redirect_inform = _(
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# noqa: D100
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Any, Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
_T = TypeVar("_T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseTemplate(ABC, Generic[_T]):
|
|
11
|
+
"""Base class for templates."""
|
|
12
|
+
|
|
13
|
+
template: _T
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def render(self, context: dict[str, Any]) -> Any:
|
|
17
|
+
"""Render the template with the given context."""
|
|
@@ -16,6 +16,7 @@ if TYPE_CHECKING:
|
|
|
16
16
|
from typing import Any
|
|
17
17
|
|
|
18
18
|
from django.template.backends.base import BaseEngine
|
|
19
|
+
from django.template.base import Template
|
|
19
20
|
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
21
22
|
|
|
@@ -23,6 +24,8 @@ logger = logging.getLogger(__name__)
|
|
|
23
24
|
class DjangoTemplate(BaseTemplate):
|
|
24
25
|
"""Template utilizing Django built-in template engine."""
|
|
25
26
|
|
|
27
|
+
template: Template
|
|
28
|
+
|
|
26
29
|
@overload
|
|
27
30
|
def __init__(self, *, file: str, engine: BaseEngine | None = None) -> None: ... # pragma: no cover
|
|
28
31
|
|
|
@@ -45,7 +48,6 @@ class DjangoTemplate(BaseTemplate):
|
|
|
45
48
|
|
|
46
49
|
Raises:
|
|
47
50
|
TypeError: Some of the arguments are missing or multiple are provided.
|
|
48
|
-
ValueError: Unsupported value provided.
|
|
49
51
|
"""
|
|
50
52
|
engine = engines["django"] if engine is None else engine
|
|
51
53
|
|
|
@@ -61,13 +63,11 @@ class DjangoTemplate(BaseTemplate):
|
|
|
61
63
|
msg = "Unreachable code"
|
|
62
64
|
raise NotImplementedError(msg)
|
|
63
65
|
|
|
64
|
-
self.template = template
|
|
65
|
-
|
|
66
|
-
def render(self, *, context: dict[str, Any] | None = None) -> dict: # noqa: D102
|
|
67
|
-
context = {} if context is None else context
|
|
66
|
+
self.template = template # type: ignore[assignment] # False-positive error
|
|
68
67
|
|
|
68
|
+
def render(self, context: dict[str, Any]) -> Any: # noqa: D102
|
|
69
69
|
logger.debug("Rendering template with context: %r", context)
|
|
70
|
-
rendered = self.template.render(context=context)
|
|
70
|
+
rendered = self.template.render(context=context) # type: ignore[arg-type] # False-positive error
|
|
71
71
|
return _xml_to_dict(rendered)
|
|
72
72
|
|
|
73
73
|
|
|
@@ -93,7 +93,7 @@ def _xml_to_dict(xml: str) -> dict:
|
|
|
93
93
|
xml,
|
|
94
94
|
attr_prefix="",
|
|
95
95
|
cdata_key="text",
|
|
96
|
-
force_list=("blocks", "elements"),
|
|
96
|
+
force_list=("blocks", "elements", "options"),
|
|
97
97
|
postprocessor=_xml_postprocessor,
|
|
98
98
|
)
|
|
99
99
|
return dict(next(iter(obj.values())))
|
|
@@ -101,11 +101,11 @@ def _xml_to_dict(xml: str) -> dict:
|
|
|
101
101
|
|
|
102
102
|
def _preprocess_xml(xml: str) -> str:
|
|
103
103
|
"""Normalize XML text nodes."""
|
|
104
|
-
root = ET.fromstring(xml) # noqa: S314; TODO(lasuillard): Naive belief that XML is safe
|
|
104
|
+
root = ET.fromstring(xml) # noqa: S314 ; TODO(lasuillard): Naive belief that XML is safe
|
|
105
105
|
for node in root.iter():
|
|
106
106
|
node.tag = _rename_tag(node.tag)
|
|
107
107
|
|
|
108
|
-
if node.tag
|
|
108
|
+
if node.tag in ("text", "elements") and node.text:
|
|
109
109
|
text = dedent(node.text)
|
|
110
110
|
text = _remove_single_newline(text)
|
|
111
111
|
logger.debug("Normalized text node: %r -> %r", node.text, text)
|
|
@@ -114,22 +114,6 @@ def _preprocess_xml(xml: str) -> str:
|
|
|
114
114
|
return ET.tostring(root, encoding="unicode")
|
|
115
115
|
|
|
116
116
|
|
|
117
|
-
def _rename_tag(tag: str) -> str:
|
|
118
|
-
"""Rename tags."""
|
|
119
|
-
if tag == "block":
|
|
120
|
-
return "blocks"
|
|
121
|
-
|
|
122
|
-
if tag == "element":
|
|
123
|
-
return "elements"
|
|
124
|
-
|
|
125
|
-
return tag
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def _remove_single_newline(text: str) -> str:
|
|
129
|
-
"""Remove a single newline from repeated newlines. If the are just one newline, replace it with space."""
|
|
130
|
-
return re.sub(r"([\n]+)", lambda m: "\n" * (m.group(1).count("\n") - 1) or " ", text)
|
|
131
|
-
|
|
132
|
-
|
|
133
117
|
def _xml_postprocessor(path: Any, key: str, value: Any) -> tuple[str, Any]: # noqa: ARG001
|
|
134
118
|
if value == "true":
|
|
135
119
|
return key, True
|
|
@@ -137,4 +121,25 @@ def _xml_postprocessor(path: Any, key: str, value: Any) -> tuple[str, Any]: # n
|
|
|
137
121
|
if value == "false":
|
|
138
122
|
return key, False
|
|
139
123
|
|
|
124
|
+
# TODO(lasuillard): Should coerce all numeric-like strings to numbers?
|
|
125
|
+
if key == "indent":
|
|
126
|
+
return key, int(value)
|
|
127
|
+
|
|
140
128
|
return key, value
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
_TAG_MAPPING = {
|
|
132
|
+
"block": "blocks",
|
|
133
|
+
"element": "elements",
|
|
134
|
+
"option": "options",
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _rename_tag(tag: str) -> str:
|
|
139
|
+
"""Rename tags."""
|
|
140
|
+
return _TAG_MAPPING.get(tag, tag)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _remove_single_newline(text: str) -> str:
|
|
144
|
+
"""Remove a single newline from repeated newlines. If the are just one newline, replace it with space."""
|
|
145
|
+
return re.sub(r"([\n]+)", lambda m: "\n" * (m.group(1).count("\n") - 1) or " ", text)
|